Merge from docusealco/wip

pull/493/head 2.0.2
Alex Turchyn 5 months ago committed by GitHub
commit c88715daec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -50,7 +50,7 @@ ENV OPENSSL_CONF=/app/openssl_legacy.cnf
WORKDIR /app 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\ RUN echo $'.include = /etc/ssl/openssl.cnf\n\
\n\ \n\

@ -100,6 +100,7 @@ module Api
permitted_params = [ permitted_params = [
:name, :name,
:external_id, :external_id,
:shared_link,
{ {
submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email]], submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email]],
fields: [[:uuid, :submitter_uuid, :name, :type, fields: [[:uuid, :submitter_uuid, :name, :type,

@ -55,6 +55,14 @@ class ApplicationController < ActionController::Base
request.session[:impersonated_user_id] = user.uuid request.session[:impersonated_user_id] = user.uuid
end 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 private
def with_locale(&) def with_locale(&)

@ -9,7 +9,9 @@ class EmailSmtpSettingsController < ApplicationController
def create def create
if @encrypted_config.update(email_configs) 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') redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved')
else else

@ -11,15 +11,15 @@ class SendSubmissionEmailController < ApplicationController
def create def create
if params[:template_slug] if params[:template_slug]
@submitter = Submitter.joins(submission: :template).find_by!(email: params[:email].to_s.downcase, @submitter = Submitter.completed.joins(submission: :template).find_by!(email: params[:email].to_s.downcase,
template: { slug: params[:template_slug] }) template: { slug: params[:template_slug] })
elsif params[:submission_slug] elsif params[:submission_slug]
@submitter = Submitter.joins(:submission).find_by(email: params[:email].to_s.downcase, @submitter = Submitter.completed.joins(:submission).find_by(email: params[:email].to_s.downcase,
submission: { slug: params[:submission_slug] }) submission: { slug: params[:submission_slug] })
return redirect_to submissions_preview_completed_path(params[:submission_slug], status: :error) unless @submitter return redirect_to submissions_preview_completed_path(params[:submission_slug], status: :error) unless @submitter
else else
@submitter = Submitter.find_by!(slug: params[:submitter_slug]) @submitter = Submitter.completed.find_by!(slug: params[:submitter_slug])
end end
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes) RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)

@ -8,20 +8,28 @@ class StartFormController < ApplicationController
around_action :with_browser_locale, only: %i[show completed] around_action :with_browser_locale, only: %i[show completed]
before_action :maybe_redirect_com, 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 :load_template
before_action :authorize_start!, only: :update
def show 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) if @template.shared_link?
.submitters.new(account_id: @template.account_id, @submitter = @template.submissions.new(account_id: @template.account_id)
uuid: (filter_undefined_submitters(@template).first || .submitters.new(account_id: @template.account_id,
@template.submitters.first)['uuid']) 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 end
def update def update
return redirect_to start_form_path(@template.slug) if @template.archived_at?
@submitter = find_or_initialize_submitter(@template, submitter_params) @submitter = find_or_initialize_submitter(@template, submitter_params)
if @submitter.completed_at? if @submitter.completed_at?
@ -59,6 +67,8 @@ class StartFormController < ApplicationController
end end
def completed def completed
return redirect_to start_form_path(@template.slug) if !@template.shared_link? || @template.archived_at?
@submitter = Submitter.where(submission: @template.submissions) @submitter = Submitter.where(submission: @template.submissions)
.where.not(completed_at: nil) .where.not(completed_at: nil)
.find_by!(email: params[:email]) .find_by!(email: params[:email])
@ -66,6 +76,24 @@ class StartFormController < ApplicationController
private 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) def enqueue_submission_create_webhooks(submitter)
WebhookUrls.for_account_id(submitter.account_id, 'submission.created').each do |webhook_url| WebhookUrls.for_account_id(submitter.account_id, 'submission.created').each do |webhook_url|
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submitter.submission_id, SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submitter.submission_id,
@ -74,31 +102,29 @@ class StartFormController < ApplicationController
end end
def find_or_initialize_submitter(template, submitter_params) def find_or_initialize_submitter(template, submitter_params)
Submitter.where(submission: template.submissions.where(expire_at: Time.current..) Submitter
.or(template.submissions.where(expire_at: nil)).where(archived_at: nil)) .where(submission: template.submissions.where(expire_at: Time.current..)
.order(id: :desc) .or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
.where(declined_at: nil) .order(id: :desc)
.where(external_id: nil) .where(declined_at: nil)
.where(ip: [nil, request.remote_ip]) .where(external_id: nil)
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel } .where(ip: [nil, request.remote_ip])
.find_or_initialize_by(email: submitter_params[:email], **submitter_params.compact_blank) .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 end
def assign_submission_attributes(submitter, template) 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( submitter.assign_attributes(
uuid: (filter_undefined_submitters(template).first || @template.submitters.first)['uuid'], uuid: (filter_undefined_submitters(template).first || @template.submitters.first)['uuid'],
ip: request.remote_ip, ip: request.remote_ip,
ua: request.user_agent, ua: request.user_agent,
values: resubmit_submitter&.preferences&.fetch('default_values', nil) || {}, values: @resubmit_submitter&.preferences&.fetch('default_values', nil) || {},
preferences: resubmit_submitter&.preferences.presence || { 'send_email' => true }, preferences: @resubmit_submitter&.preferences.presence || { 'send_email' => true },
metadata: resubmit_submitter&.metadata.presence || {} metadata: @resubmit_submitter&.metadata.presence || {}
) )
if submitter.values.present? 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) submitter.attachments << attachment.dup if submitter.values.value?(attachment.uuid)
end end
end end
@ -120,15 +146,21 @@ class StartFormController < ApplicationController
end end
def submitter_params 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| params.require(:submitter).permit(:email, :phone, :name).tap do |attrs|
attrs[:email] = Submissions.normalize_email(attrs[:email]) attrs[:email] = Submissions.normalize_email(attrs[:email])
end end
end end
def load_template def load_template
slug = params[:slug] || params[:start_form_slug] @template =
if @resubmit_submitter
@template = Template.find_by!(slug:) @resubmit_submitter.template
else
Template.find_by!(slug: params[:slug] || params[:start_form_slug])
end
end end
def multiple_submitters_error_message def multiple_submitters_error_message

@ -18,6 +18,6 @@ class SubmissionsArchivedController < ApplicationController
@submissions.order(id: :desc) @submissions.order(id: :desc)
end end
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events)) @pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events))
end end
end end

@ -19,6 +19,6 @@ class SubmissionsDashboardController < ApplicationController
@submissions.order(id: :desc) @submissions.order(id: :desc)
end end
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events)) @pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events))
end end
end end

@ -9,7 +9,7 @@ class TemplateFoldersController < ApplicationController
@templates = Templates.search(@templates, params[:q]) @templates = Templates.search(@templates, params[:q])
@templates = Templates::Order.call(@templates, current_user, cookies.permanent[:dashboard_templates_order]) @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 end
def edit; end def edit; end

@ -7,6 +7,6 @@ class TemplatesArchivedController < ApplicationController
@templates = @templates.where.not(archived_at: nil).preload(:author, :folder, :template_accesses).order(id: :desc) @templates = @templates.where.not(archived_at: nil).preload(:author, :folder, :template_accesses).order(id: :desc)
@templates = Templates.search(@templates, params[:q]) @templates = Templates.search(@templates, params[:q])
@pagy, @templates = pagy(@templates, limit: 12) @pagy, @templates = pagy_auto(@templates, limit: 12)
end end
end end

@ -15,7 +15,7 @@ class TemplatesArchivedSubmissionsController < ApplicationController
@submissions.order(id: :desc) @submissions.order(id: :desc)
end 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 rescue ActiveRecord::RecordNotFound
redirect_to root_path redirect_to root_path
end end

@ -21,7 +21,7 @@ class TemplatesController < ApplicationController
submissions.order(id: :desc) submissions.order(id: :desc)
end 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 rescue ActiveRecord::RecordNotFound
redirect_to root_path redirect_to root_path
end end

@ -17,7 +17,7 @@ class TemplatesDashboardController < ApplicationController
@pagy, @template_folders = pagy( @pagy, @template_folders = pagy(
@template_folders, @template_folders,
items: FOLDERS_PER_PAGE, limit: FOLDERS_PER_PAGE,
page: @template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1 page: @template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1
) )
@ -35,7 +35,7 @@ class TemplatesDashboardController < ApplicationController
(@template_folders.size < 7 ? 9 : 6) (@template_folders.size < 7 ? 9 : 6)
end end
@pagy, @templates = pagy(@templates, limit:) @pagy, @templates = pagy_auto(@templates, limit:)
end end
end end

@ -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

@ -24,6 +24,7 @@ import SubmitForm from './elements/submit_form'
import PromptPassword from './elements/prompt_password' import PromptPassword from './elements/prompt_password'
import EmailsTextarea from './elements/emails_textarea' import EmailsTextarea from './elements/emails_textarea'
import ToggleOnSubmit from './elements/toggle_on_submit' import ToggleOnSubmit from './elements/toggle_on_submit'
import CheckOnClick from './elements/check_on_click'
import PasswordInput from './elements/password_input' import PasswordInput from './elements/password_input'
import SearchInput from './elements/search_input' import SearchInput from './elements/search_input'
import ToggleAttribute from './elements/toggle_attribute' import ToggleAttribute from './elements/toggle_attribute'
@ -103,6 +104,7 @@ safeRegisterElement('set-date-button', SetDateButton)
safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox) safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox)
safeRegisterElement('app-tour', AppTour) safeRegisterElement('app-tour', AppTour)
safeRegisterElement('dashboard-dropzone', DashboardDropzone) safeRegisterElement('dashboard-dropzone', DashboardDropzone)
safeRegisterElement('check-on-click', CheckOnClick)
safeRegisterElement('template-builder', class extends HTMLElement { safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () { connectedCallback () {

@ -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)
}
}

@ -3,8 +3,6 @@ export default class extends HTMLElement {
this.clearChecked() this.clearChecked()
this.addEventListener('click', (e) => { this.addEventListener('click', (e) => {
e.stopPropagation()
const text = this.dataset.text || this.innerText.trim() const text = this.dataset.text || this.innerText.trim()
if (navigator.clipboard) { if (navigator.clipboard) {

@ -23,7 +23,7 @@
</div> </div>
<div <div
v-if="isActive && withLabel && (!area.option_uuid || !option.value)" v-if="isActive && withLabel && (!area.option_uuid || !option.value)"
class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap pointer-events-none" class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap pointer-events-none field-area-active-label"
> >
<template v-if="area.option_uuid && !option.value"> <template v-if="area.option_uuid && !option.value">
{{ optionValue(option) }} {{ optionValue(option) }}
@ -55,12 +55,12 @@
> >
<div <div
v-else-if="field.type === 'signature' && signature" v-else-if="field.type === 'signature' && signature"
class="flex justify-between h-full gap-1 overflow-hidden" class="flex justify-between h-full gap-1 overflow-hidden w-full"
:class="isNarrow ? 'flex-row' : 'flex-col'" :class="isNarrow && (withSignatureId || field.preferences?.reason_field_uuid) ? 'flex-row' : 'flex-col'"
> >
<div <div
class="flex overflow-hidden" class="flex overflow-hidden"
:class="isNarrow ? 'w-1/2' : 'flex-grow'" :class="isNarrow && (withSignatureId || field.preferences?.reason_field_uuid) ? 'w-1/2' : 'flex-grow'"
style="min-height: 50%" style="min-height: 50%"
> >
<img <img
@ -69,7 +69,7 @@
> >
</div> </div>
<div <div
v-if="withSignatureId" v-if="withSignatureId || field.preferences?.reason_field_uuid"
class="text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem]" class="text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem]"
:class="isNarrow ? 'w-1/2' : 'w-full'" :class="isNarrow ? 'w-1/2' : 'w-full'"
> >

@ -211,7 +211,7 @@ export default {
}, },
computed: { computed: {
computedPreviousValue () { computedPreviousValue () {
if (this.isUsePreviousValue) { if (this.isUsePreviousValue && this.field.required === true) {
return this.previousValue return this.previousValue
} else { } else {
return null return null

@ -414,7 +414,7 @@ export default {
} }
}, },
computedPreviousValue () { computedPreviousValue () {
if (this.isUsePreviousValue) { if (this.isUsePreviousValue && this.field.required === true) {
return this.previousValue return this.previousValue
} else { } else {
return null return null

@ -151,8 +151,10 @@ export default {
resizeTextarea () { resizeTextarea () {
const textarea = this.$refs.textarea const textarea = this.$refs.textarea
textarea.style.height = 'auto' if (textarea) {
textarea.style.height = Math.min(250, textarea.scrollHeight) + 'px' textarea.style.height = 'auto'
textarea.style.height = Math.min(250, textarea.scrollHeight) + 'px'
}
}, },
toggleTextArea () { toggleTextArea () {
this.isTextArea = true this.isTextArea = true

@ -79,13 +79,52 @@
name="buttons" name="buttons"
/> />
<template v-else> <template v-else>
<form
v-if="withSignYourselfButton && template.submitters.length < 2"
target="_blank"
data-turbo="false"
class="inline"
method="post"
:action="`/d/${template.slug}`"
@submit="maybeShowErrorTemplateAlert"
>
<input
type="hidden"
name="_method"
value="put"
autocomplete="off"
>
<input
type="hidden"
name="authenticity_token"
:value="authenticityToken"
autocomplete="off"
>
<input
type="hidden"
name="selfsign"
value="true"
autocomplete="off"
>
<button
class="btn btn-primary btn-ghost text-base hidden md:flex"
type="submit"
>
<IconWritingSign
width="22"
class="inline"
/>
<span class="hidden md:inline">
{{ t('sign_yourself') }}
</span>
</button>
</form>
<a <a
v-if="withSignYourselfButton" v-else-if="withSignYourselfButton"
id="sign_yourself_button" id="sign_yourself_button"
:href="template.submitters.length > 1 ? `/templates/${template.id}/submissions/new?selfsign=true` : `/d/${template.slug}`" :href="`/templates/${template.id}/submissions/new?selfsign=true`"
class="btn btn-primary btn-ghost text-base hidden md:flex" class="btn btn-primary btn-ghost text-base hidden md:flex"
:target="template.submitters.length > 1 ? '' : '_blank'" data-turbo-frame="modal"
:data-turbo-frame="template.submitters.length > 1 ? 'modal' : ''"
@click="maybeShowErrorTemplateAlert" @click="maybeShowErrorTemplateAlert"
> >
<IconWritingSign <IconWritingSign

@ -18,10 +18,10 @@
# #
# Indexes # Indexes
# #
# index_email_events_on_account_id (account_id) # index_email_events_on_account_id_and_event_datetime (account_id,event_datetime)
# index_email_events_on_email (email) # index_email_events_on_email (email)
# index_email_events_on_emailable (emailable_type,emailable_id) # index_email_events_on_emailable (emailable_type,emailable_id)
# index_email_events_on_message_id (message_id) # index_email_events_on_message_id (message_id)
# #
# Foreign Keys # Foreign Keys
# #

@ -65,12 +65,18 @@ class Submission < ApplicationRecord
scope :active, -> { where(archived_at: nil) } scope :active, -> { where(archived_at: nil) }
scope :archived, -> { where.not(archived_at: nil) } scope :archived, -> { where.not(archived_at: nil) }
scope :pending, -> { joins(:submitters).where(submitters: { completed_at: nil }).group(:id) } scope :pending, lambda {
where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
.and(Submitter.arel_table[:completed_at].eq(nil))).select(1).arel.exists)
}
scope :completed, lambda { scope :completed, lambda {
where.not(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id]) where.not(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
.and(Submitter.arel_table[:completed_at].eq(nil))).select(1).arel.exists) .and(Submitter.arel_table[:completed_at].eq(nil))).select(1).arel.exists)
} }
scope :declined, -> { joins(:submitters).where.not(submitters: { declined_at: nil }).group(:id) } scope :declined, lambda {
where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
.and(Submitter.arel_table[:declined_at].not_eq(nil))).select(1).arel.exists)
}
scope :expired, -> { pending.where(expire_at: ..Time.current) } scope :expired, -> { pending.where(expire_at: ..Time.current) }
enum :source, { enum :source, {

@ -10,6 +10,7 @@
# name :string not null # name :string not null
# preferences :text not null # preferences :text not null
# schema :text not null # schema :text not null
# shared_link :boolean default(FALSE), not null
# slug :string not null # slug :string not null
# source :text not null # source :text not null
# submitters :text not null # submitters :text not null

@ -73,6 +73,8 @@ class User < ApplicationRecord
scope :archived, -> { where.not(archived_at: nil) } scope :archived, -> { where.not(archived_at: nil) }
scope :admins, -> { where(role: ADMIN_ROLE) } scope :admins, -> { where(role: ADMIN_ROLE) }
validates :email, format: { with: /\A[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\z/ }
def access_token def access_token
super || build_access_token.tap(&:save!) super || build_access_token.tap(&:save!)
end end

@ -2,7 +2,11 @@
<% if @pagy.pages > 1 %> <% if @pagy.pages > 1 %>
<div class="flex my-6 justify-center md:justify-between"> <div class="flex my-6 justify-center md:justify-between">
<div class="hidden md:block text-sm"> <div class="hidden md:block text-sm">
<%= @pagy.from %>-<%= local_assigns.fetch(:to, @pagy.to) %> of <%= local_assigns.fetch(:count, @pagy.count) %> <%= local_assigns[:items_name] || 'items' %> <% if @pagy.count.nil? %>
<%= t("pagination.#{local_assigns.fetch(:items_name, 'items')}.range_without_total", from: local_assigns.fetch(:from, @pagy.from), to: local_assigns.fetch(:to, @pagy.to)) %>
<% else %>
<%= t("pagination.#{local_assigns.fetch(:items_name, 'items')}.range_with_total", from: local_assigns.fetch(:from, @pagy.from), to: local_assigns.fetch(:to, @pagy.to), count: local_assigns.fetch(:count, @pagy.count)) %>
<% end %>
<%= local_assigns[:left_additional_html] %> <%= local_assigns[:left_additional_html] %>
</div> </div>
<div class="flex items-center space-x-1.5"> <div class="flex items-center space-x-1.5">

@ -0,0 +1,5 @@
<% if configs = account.account_configs.find_by(key: AccountConfig::POLICY_LINKS_KEY) %>
<div class="max-w-md mx-auto flex flex-wrap gap-1 justify-center text-sm text-base-content/60 mt-2">
<%= auto_link(MarkdownToHtml.call(configs.value)) %>
</div>
<% end %>

@ -23,12 +23,12 @@
</div> </div>
<% if Docuseal.multitenant? || Accounts.can_send_emails?(@submitter.account) %> <% if Docuseal.multitenant? || Accounts.can_send_emails?(@submitter.account) %>
<toggle-submit class="block"> <toggle-submit class="block">
<%= button_to button_title(title: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, class: 'base-button w-full' %> <%= button_to button_title(title: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { template_slug: @template.slug, email: params[:email] }, class: 'base-button w-full' %>
</toggle-submit> </toggle-submit>
<% end %> <% end %>
<% if Templates.filter_undefined_submitters(@template).size == 1 && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false %> <% if Templates.filter_undefined_submitters(@template).size == 1 && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false && @template.shared_link? %>
<toggle-submit class="block"> <toggle-submit class="block">
<%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@template.slug), params: { submitter: { email: params[:email] }, resubmit: @submitter.slug }, method: :put, class: 'white-button w-full' %> <%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@template.slug), params: { submitter: { email: params[:email] }, resubmit: true }, method: :put, class: 'white-button w-full' %>
</toggle-submit> </toggle-submit>
<% end %> <% end %>
</div> </div>

@ -0,0 +1,28 @@
<% content_for(:html_title, "#{@template.name} | DocuSeal") %>
<% content_for(:html_description, t('share_link_is_currently_disabled')) %>
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<div class="text-center w-full space-y-6">
<%= render 'banner' %>
<p class="text-xl font-semibold text-center">
<%= t('share_link_is_currently_disabled') %>
</p>
</div>
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
<div class="flex items-center">
<div class="mr-3">
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
</div>
<div>
<p class="text-lg font-bold mb-1"><%= @template.name %></p>
<% if @template.archived_at? %>
<p dir="auto" class="text-sm"><%= t('form_has_been_deleted_by_html', name: @template.account.name) %></p>
<% end %>
</div>
</div>
</div>
</div>
</div>
</div>
<%= render 'shared/attribution', link_path: '/start', account: @template.account %>

@ -42,3 +42,4 @@
</div> </div>
</div> </div>
<%= render 'shared/attribution', link_path: '/start', account: @template.account %> <%= render 'shared/attribution', link_path: '/start', account: @template.account %>
<%= render 'start_form/policy', account: @template.account %>

@ -6,11 +6,11 @@
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-mono' if font == 'Courier' %> <%= 'font-serif' if font == 'Times' %> <%= 'font-bold' if font_type == 'bold' || font_type == 'bold_italic' %> <%= 'italic' if font_type == 'italic' || font_type == 'bold_italic' %> <%= align == 'right' ? 'text-right' : (align == 'center' ? 'text-center' : '') %>" style="<%= "color: #{color}; " if color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size'].to_i * 1.23}pt); line-height: `clamp(6pt, 2.0vw, #{(field['preferences']['font_size'].to_i * 1.23) + 3}pt)`" if field.dig('preferences', 'font_size') %>"> <field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-mono' if font == 'Courier' %> <%= 'font-serif' if font == 'Times' %> <%= 'font-bold' if font_type == 'bold' || font_type == 'bold_italic' %> <%= 'italic' if font_type == 'italic' || font_type == 'bold_italic' %> <%= align == 'right' ? 'text-right' : (align == 'center' ? 'text-center' : '') %>" style="<%= "color: #{color}; " if color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size'].to_i * 1.23}pt); line-height: `clamp(6pt, 2.0vw, #{(field['preferences']['font_size'].to_i * 1.23) + 3}pt)`" if field.dig('preferences', 'font_size') %>">
<% if field['type'] == 'signature' %> <% if field['type'] == 'signature' %>
<% is_narrow = area['h']&.positive? && (area['w'].to_f / area['h']) > 6 %> <% is_narrow = area['h']&.positive? && (area['w'].to_f / area['h']) > 6 %>
<div class="flex justify-between w-full h-full gap-1 <%= is_narrow ? 'flex-row' : 'flex-col' %>"> <div class="flex justify-between w-full h-full gap-1 <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'flex-row' : 'flex-col' %>">
<div class="flex overflow-hidden <%= is_narrow ? 'w-1/2' : 'flex-grow' %>" style="min-height: 50%"> <div class="flex overflow-hidden <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'w-1/2' : 'flex-grow' %>" style="min-height: 50%">
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>"> <img class="object-contain mx-auto" src="<%= attachments_index[value].url %>">
</div> </div>
<% if local_assigns[:with_signature_id] && attachment = attachments_index[value] %> <% if (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) && attachment = attachments_index[value] %>
<div class="text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem] <%= is_narrow ? 'w-1/2' : 'w-full' %>"> <div class="text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem] <%= is_narrow ? 'w-1/2' : 'w-full' %>">
<div class="truncate uppercase"> <div class="truncate uppercase">
ID: <%= attachment.uuid %> ID: <%= attachment.uuid %>

@ -21,7 +21,7 @@
<%= render 'submissions_filters/filter_button', filter_params: %> <%= render 'submissions_filters/filter_button', filter_params: %>
</div> </div>
</div> </div>
<% if @pagy.count > 0 %> <% if @pagy.count.nil? || @pagy.count > 0 %>
<div class="space-y-4"> <div class="space-y-4">
<%= render partial: 'templates/submission', collection: @submissions, locals: { with_template: true, archived: true } %> <%= render partial: 'templates/submission', collection: @submissions, locals: { with_template: true, archived: true } %>
</div> </div>

@ -1,5 +1,5 @@
<% filter_params = params.permit(Submissions::Filter::ALLOWED_PARAMS).compact_blank %> <% filter_params = params.permit(Submissions::Filter::ALLOWED_PARAMS).compact_blank %>
<% is_show_tabs = @pagy.count >= 5 || params[:status].present? || filter_params.present? %> <% is_show_tabs = (@pagy.count.nil? || @pagy.count >= 5) || params[:status].present? || filter_params.present? %>
<% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %> <% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %>
<div class="flex justify-between items-center w-full mb-4"> <div class="flex justify-between items-center w-full mb-4">
<div class="flex items-center flex-grow min-w-0"> <div class="flex items-center flex-grow min-w-0">
@ -61,12 +61,12 @@
</div> </div>
</div> </div>
<% end %> <% end %>
<% if @pagy.count > 0 %> <% if @pagy.count.nil? || @pagy.count > 0 %>
<div class="space-y-4"> <div class="space-y-4">
<%= render partial: 'templates/submission', collection: @submissions, locals: { with_template: true } %> <%= render partial: 'templates/submission', collection: @submissions, locals: { with_template: true } %>
</div> </div>
<% end %> <% end %>
<% if params[:q].blank? && params[:status].blank? && filter_params.blank? && @pagy.count < 5 %> <% if params[:q].blank? && params[:status].blank? && filter_params.blank? && @pagy.count.present? && @pagy.count < 5 %>
<%= render 'templates/dropzone' %> <%= render 'templates/dropzone' %>
<% end %> <% end %>
<% if @submissions.present? || (params[:q].blank? && filter_params.blank?) %> <% if @submissions.present? || (params[:q].blank? && filter_params.blank?) %>

@ -43,10 +43,10 @@
<% end %> <% end %>
</div> </div>
<% undefined_submitters = Templates.filter_undefined_submitters(@submitter.submission.template) %> <% undefined_submitters = Templates.filter_undefined_submitters(@submitter.submission.template) %>
<% if undefined_submitters.size == 1 && undefined_submitters.first['uuid'] == @submitter.uuid && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false && !@submitter.template.archived_at? %> <% if undefined_submitters.size == 1 && undefined_submitters.first['uuid'] == @submitter.uuid && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false %>
<div class="divider uppercase"><%= t('or') %></div> <div class="divider uppercase"><%= t('or') %></div>
<toggle-submit class="block"> <toggle-submit class="block">
<%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@submitter.submission.template.slug), params: { submitter: { email: @submitter.email, phone: @submitter.phone, name: @submitter.name }, resubmit: @submitter.slug }, method: :put, class: 'white-button w-full' %> <%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), resubmit_form_path, params: { resubmit: @submitter.slug }, method: :put, class: 'white-button w-full' %>
</toggle-submit> </toggle-submit>
<% end %> <% end %>
</div> </div>

@ -39,13 +39,13 @@
<% end %> <% end %>
</div> </div>
</div> </div>
<% if @pagy.count > 0 %> <% if @pagy.count.nil? || @pagy.count > 0 %>
<div class="grid gap-4 md:grid-cols-3"> <div class="grid gap-4 md:grid-cols-3">
<%= render partial: 'templates/template', collection: @templates %> <%= render partial: 'templates/template', collection: @templates %>
</div> </div>
<% templates_order_select_html = capture do %> <% templates_order_select_html = capture do %>
<% if params[:q].blank? && @pagy.pages > 1 %> <% 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 %>
<% end %> <% end %>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', right_additional_html: templates_order_select_html %> <%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', right_additional_html: templates_order_select_html %>

@ -49,7 +49,12 @@
<% end %> <% end %>
</div> </div>
<% 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 %>
<span class="flex items-center justify-center space-x-2">
<%= svg_icon('link', class: 'w-4 h-4 md:w-6 md:h-6 text-white') %>
<span><%= t('link') %></span>
</span>
<% end %>
</div> </div>
<% end %> <% end %>
</div> </div>

@ -1,7 +1,7 @@
<%= render 'title', template: @template %> <%= render 'title', template: @template %>
<% filter_params = params.permit(Submissions::Filter::ALLOWED_PARAMS).compact_blank %> <% filter_params = params.permit(Submissions::Filter::ALLOWED_PARAMS).compact_blank %>
<% is_show_tabs = @pagy.pages > 1 || params[:q].present? || params[:status].present? || filter_params.present? %> <% is_show_tabs = @pagy.pages > 1 || params[:q].present? || params[:status].present? || filter_params.present? %>
<% if !@pagy.count.zero? || params[:q].present? || params[:status].present? || filter_params.present? %> <% if @pagy.count.nil? || !@pagy.count.zero? || params[:q].present? || params[:status].present? || filter_params.present? %>
<div class="<%= is_show_tabs ? 'mb-4' : 'mb-6' %>"> <div class="<%= is_show_tabs ? 'mb-4' : 'mb-6' %>">
<div class="flex justify-between items-center md:items-end"> <div class="flex justify-between items-center md:items-end">
<div> <div>
@ -35,27 +35,33 @@
<%= svg_icon('list', class: 'w-5 h-5') %> <%= svg_icon('list', class: 'w-5 h-5') %>
<span class="font-normal"><%= t('all') %></span> <span class="font-normal"><%= t('all') %></span>
</div> </div>
<div class="badge badge-neutral badge-outline font-medium"> <% unless can?(:manage, :countless) %>
<%= params[:status].blank? && filter_params.blank? ? @pagy.count : @base_submissions.unscope(:group, :order).select(:id).distinct.count %> <div class="badge badge-neutral badge-outline font-medium">
</div> <%= params[:status].blank? && filter_params.blank? ? @pagy.count : @base_submissions.unscope(:group, :order).select(:id).distinct.count %>
</div>
<% end %>
</a> </a>
<a href="<%= url_for(params.to_unsafe_h.merge(status: :pending)) %>" class="<%= params[:status] == 'pending' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-700"> <a href="<%= url_for(params.to_unsafe_h.merge(status: :pending)) %>" class="<%= params[:status] == 'pending' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-700">
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<%= svg_icon('clock', class: 'w-5 h-5') %> <%= svg_icon('clock', class: 'w-5 h-5') %>
<span class="font-normal"><%= t('pending') %></span> <span class="font-normal"><%= t('pending') %></span>
</div> </div>
<div class="badge badge-neutral badge-outline font-medium"> <% unless can?(:manage, :countless) %>
<%= params[:status] == 'pending' && filter_params.blank? ? @pagy.count : @base_submissions.pending.unscope(:group, :order).select(:id).distinct.count %> <div class="badge badge-neutral badge-outline font-medium">
</div> <%= params[:status] == 'pending' && filter_params.blank? ? @pagy.count : @base_submissions.pending.unscope(:group, :order).select(:id).distinct.count %>
</div>
<% end %>
</a> </a>
<a href="<%= url_for(params.to_unsafe_h.merge(status: :completed)) %>" class="<%= params[:status] == 'completed' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-700"> <a href="<%= url_for(params.to_unsafe_h.merge(status: :completed)) %>" class="<%= params[:status] == 'completed' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-700">
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<%= svg_icon('circle_check', class: 'w-5 h-5') %> <%= svg_icon('circle_check', class: 'w-5 h-5') %>
<span class="font-normal"><%= t('completed') %></span> <span class="font-normal"><%= t('completed') %></span>
</div> </div>
<div class="badge badge-neutral badge-outline font-medium"> <% unless can?(:manage, :countless) %>
<%= params[:status] == 'completed' && filter_params.blank? ? @pagy.count : @base_submissions.completed.unscope(:group, :order).select(:id).distinct.count %> <div class="badge badge-neutral badge-outline font-medium">
</div> <%= params[:status] == 'completed' && filter_params.blank? ? @pagy.count : @base_submissions.completed.unscope(:group, :order).select(:id).distinct.count %>
</div>
<% end %>
</a> </a>
</div> </div>
<div class="flex items-end flex-col md:flex-row gap-2 w-full md:w-fit"> <div class="flex items-end flex-col md:flex-row gap-2 w-full md:w-fit">
@ -89,7 +95,7 @@
<% end %> <% end %>
<% end %> <% end %>
<% if @template.submitters.size == 1 %> <% if @template.submitters.size == 1 %>
<%= link_to start_form_url(slug: @template.slug), id: 'sign_yourself_button', class: 'white-button mt-6', target: '_blank', rel: 'noopener' do %> <%= button_to start_form_path(@template.slug), params: { selfsign: true }, method: :put, class: 'white-button w-full', form: { style: 'display: inline', target: '_blank', data: { turbo: false } } do %>
<%= svg_icon('writing', class: 'w-6 h-6') %> <%= svg_icon('writing', class: 'w-6 h-6') %>
<span class="mr-1"><%= t('sign_it_yourself') %></span> <span class="mr-1"><%= t('sign_it_yourself') %></span>
<% end %> <% end %>

@ -12,7 +12,7 @@
<%= render 'shared/search_input', placeholder: "#{t('search')}..." %> <%= render 'shared/search_input', placeholder: "#{t('search')}..." %>
<% end %> <% end %>
</div> </div>
<% if @pagy.count > 0 %> <% if @pagy.count.nil? || @pagy.count > 0 %>
<div class="grid gap-4 md:grid-cols-3"> <div class="grid gap-4 md:grid-cols-3">
<%= render partial: 'templates/template', collection: @templates %> <%= render partial: 'templates/template', collection: @templates %>
</div> </div>

@ -29,7 +29,7 @@
</div> </div>
<% end %> <% end %>
</div> </div>
<% if @pagy.count > 0 %> <% if @pagy.count.nil? || @pagy.count > 0 %>
<div class="space-y-4"> <div class="space-y-4">
<%= render partial: 'templates/submission', collection: @submissions, locals: { template: @template, archived: true } %> <%= render partial: 'templates/submission', collection: @submissions, locals: { template: @template, archived: true } %>
</div> </div>

@ -12,7 +12,7 @@
<%= render 'templates/dashboard_dropzone', style: 'height: 114px' %> <%= render 'templates/dashboard_dropzone', style: 'height: 114px' %>
<% end %> <% end %>
<div class="flex items-center flex-grow min-w-0"> <div class="flex items-center flex-grow min-w-0">
<% if has_archived || @pagy.count > 0 || @template_folders.present? %> <% if has_archived || @pagy.count.nil? || @pagy.count > 0 || @template_folders.present? %>
<div class="mr-2"> <div class="mr-2">
<%= render 'dashboard/toggle_view', selected: 'templates' %> <%= render 'dashboard/toggle_view', selected: 'templates' %>
</div> </div>
@ -45,7 +45,7 @@
<% end %> <% end %>
<% templates_order_select_html = capture do %> <% templates_order_select_html = capture do %>
<% if params[:q].blank? && @pagy.pages > 1 %> <% 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 %>
<% end %> <% end %>
<% if @template_folders.present? %> <% if @template_folders.present? %>
@ -84,9 +84,9 @@
<% if show_dropzone %> <% if show_dropzone %>
<%= render 'templates/dropzone' %> <%= render 'templates/dropzone' %>
<% end %> <% end %>
<% if @templates.present? || params[:q].blank? %> <% if @templates.present? || @template_folders.present? || params[:q].blank? %>
<% if @pagy.pages > 1 %> <% 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 %> <% else %>
<div class="mt-2"> <div class="mt-2">
<%= view_archived_html %> <%= view_archived_html %>

@ -380,8 +380,18 @@
<%= t('embedding_url') %> <%= t('embedding_url') %>
</label> </label>
<div class="flex gap-2 mb-4 mt-2"> <div class="flex gap-2 mb-4 mt-2">
<input id="embedding_url" type="text" value="<%= start_form_url(slug: @template.slug) %>" class="base-input w-full" autocomplete="off" readonly> <%= 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') %> <div class="flex gap-2">
<input id="embedding_url" type="text" value="<%= start_form_url(slug: @template.slug) %>" class="base-input w-full" autocomplete="off" readonly>
<check-on-click data-element-id="template_shared_link">
<%= 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') %>
</check-on-click>
</div>
<div class="flex items-center justify-between gap-1 pt-3">
<span><%= t('enable_shared_link') %></span>
<%= f.check_box :shared_link, { class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %>
</div>
<% end %>
</div> </div>
</div> </div>
<%= render 'templates_code_modal/placeholder' %> <%= render 'templates_code_modal/placeholder' %>

@ -0,0 +1,16 @@
<%= render 'shared/turbo_modal_large', title: t('share_link') do %>
<div class="mt-2 mb-4 px-5">
<%= 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| %>
<div class="flex items-center justify-between gap-1 px-1">
<span><%= t('enable_shared_link') %></span>
<%= f.check_box :shared_link, { disabled: !can?(:update, @template), class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %>
</div>
<div class="flex gap-2 mt-3">
<input id="embedding_url" type="text" value="<%= start_form_url(slug: @template.slug) %>" class="base-input w-full" autocomplete="off" readonly>
<check-on-click data-element-id="template_shared_link">
<%= 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') %>
</check-on-click>
</div>
<% end %>
</div>
<% end %>

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'pagy/extras/countless'
Pagy::DEFAULT[:limit] = 10 Pagy::DEFAULT[:limit] = 10
Pagy::DEFAULT.freeze Pagy::DEFAULT.freeze

@ -745,6 +745,10 @@ en: &en
three_months: 3 months three_months: 3 months
eu_data_residency: EU data residency 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. 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: submission_sources:
api: API api: API
bulk: Bulk Send bulk: Bulk Send
@ -829,6 +833,22 @@ en: &en
scopes: scopes:
write: Update your data write: Update your data
read: Read 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 es: &es
stripe_integration: Integración con Stripe stripe_integration: Integración con Stripe
@ -1557,6 +1577,10 @@ es: &es
three_months: 3 meses three_months: 3 meses
eu_data_residency: Datos alojados UE 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. 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: submission_sources:
api: API api: API
bulk: Envío masivo bulk: Envío masivo
@ -1641,6 +1665,22 @@ es: &es
scopes: scopes:
write: Actualizar tus datos write: Actualizar tus datos
read: Leer 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 it: &it
stripe_integration: Integrazione Stripe stripe_integration: Integrazione Stripe
@ -2367,6 +2407,10 @@ it: &it
three_months: 3 mesi three_months: 3 mesi
eu_data_residency: "Dati nell'UE" 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." 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: submission_sources:
api: API api: API
bulk: Invio massivo bulk: Invio massivo
@ -2451,6 +2495,22 @@ it: &it
scopes: scopes:
write: Aggiorna i tuoi dati write: Aggiorna i tuoi dati
read: Leggi 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 fr: &fr
stripe_integration: Intégration Stripe stripe_integration: Intégration Stripe
@ -3180,6 +3240,10 @@ fr: &fr
three_months: 3 mois three_months: 3 mois
eu_data_residency: "Données dans l'UE" 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é." 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: submission_sources:
api: API api: API
bulk: Envoi en masse bulk: Envoi en masse
@ -3264,6 +3328,22 @@ fr: &fr
scopes: scopes:
write: Mettre à jour vos données write: Mettre à jour vos données
read: Lire 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 pt: &pt
stripe_integration: Integração com Stripe stripe_integration: Integração com Stripe
@ -3992,6 +4072,10 @@ pt: &pt
three_months: 3 meses three_months: 3 meses
eu_data_residency: Dados na UE 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. 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: submission_sources:
api: API api: API
bulk: Envio em massa bulk: Envio em massa
@ -4036,8 +4120,8 @@ pt: &pt
start: Iniciar start: Iniciar
previous: Anterior previous: Anterior
next: Próximo next: Próximo
template_and_submissions: 'Modelos e Envios' 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 'Envios' para assinar documentos individuais ou verificar o status de cada solicitação de assinatura." 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: 'Enviar um arquivo PDF'
upload_a_pdf_file_description: 'Envie um documento PDF para criar um modelo de formulário de assinatura.' 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' select_a_signer_party: 'Selecionar parte assinante'
@ -4077,6 +4161,22 @@ pt: &pt
scopes: scopes:
write: Atualizar seus dados write: Atualizar seus dados
read: Ler 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 de: &de
stripe_integration: Stripe-Integration stripe_integration: Stripe-Integration
@ -4805,6 +4905,10 @@ de: &de
three_months: 3 Monate three_months: 3 Monate
eu_data_residency: EU-Datenspeicher 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. 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: submission_sources:
api: API api: API
bulk: Massenversand bulk: Massenversand
@ -4889,6 +4993,22 @@ de: &de
scopes: scopes:
write: Aktualisiere deine Daten write: Aktualisiere deine Daten
read: Lese 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: pl:
require_phone_2fa_to_open: Wymagaj uwierzytelniania telefonicznego 2FA do otwarcia 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' open_source_documents_software: 'oprogramowanie do dokumentów open source'
eu_data_residency: Dane w UE 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. 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: uk:
require_phone_2fa_to_open: Вимагати двофакторну автентифікацію через телефон для відкриття require_phone_2fa_to_open: Вимагати двофакторну автентифікацію через телефон для відкриття
@ -5027,6 +5149,8 @@ uk:
open_source_documents_software: 'відкрите програмне забезпечення для документів' open_source_documents_software: 'відкрите програмне забезпечення для документів'
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: "Введіть адресу електронної пошти, пов'язану із завершеним поданням."
privacy_policy: Політика конфіденційності
esignature_disclosure: Використання e-підпису
cs: cs:
require_phone_2fa_to_open: Vyžadovat otevření pomocí telefonního 2FA 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' open_source_documents_software: 'open source software pro dokumenty'
eu_data_residency: 'Uložení dat v EU' 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. 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: he:
require_phone_2fa_to_open: דרוש אימות דו-שלבי באמצעות טלפון לפתיחה require_phone_2fa_to_open: דרוש אימות דו-שלבי באמצעות טלפון לפתיחה
@ -5165,6 +5291,8 @@ he:
open_source_documents_software: 'תוכנה בקוד פתוח למסמכים' open_source_documents_software: 'תוכנה בקוד פתוח למסמכים'
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: 'אנא הזן את כתובת הדוא"ל המשויכת למשלוח שהושלם.'
privacy_policy: 'מדיניות פרטיות'
esignature_disclosure: 'גילוי חתימה אלקטרונית'
nl: nl:
require_phone_2fa_to_open: Vereis telefoon 2FA om te openen require_phone_2fa_to_open: Vereis telefoon 2FA om te openen
@ -5234,6 +5362,8 @@ nl:
open_source_documents_software: 'open source documenten software' open_source_documents_software: 'open source documenten software'
eu_data_residency: Gegevens EU 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. 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: ar:
require_phone_2fa_to_open: "تطلب فتح عبر تحقق الهاتف ذو العاملين" require_phone_2fa_to_open: "تطلب فتح عبر تحقق الهاتف ذو العاملين"
@ -5303,6 +5433,8 @@ ar:
open_source_documents_software: 'برنامج مستندات مفتوح المصدر' open_source_documents_software: 'برنامج مستندات مفتوح المصدر'
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: 'يرجى إدخال عنوان البريد الإلكتروني المرتبط بالإرسال المكتمل.'
privacy_policy: 'سياسة الخصوصية'
esignature_disclosure: 'إفصاح التوقيع الإلكتروني'
ko: ko:
require_phone_2fa_to_open: 휴대폰 2FA를 열 때 요구함 require_phone_2fa_to_open: 휴대폰 2FA를 열 때 요구함
@ -5372,6 +5504,8 @@ ko:
open_source_documents_software: '오픈소스 문서 소프트웨어' open_source_documents_software: '오픈소스 문서 소프트웨어'
eu_data_residency: 'EU 데이터 보관' eu_data_residency: 'EU 데이터 보관'
please_enter_your_email_address_associated_with_the_completed_submission: '완료된 제출과 연결된 이메일 주소를 입력하세요.' please_enter_your_email_address_associated_with_the_completed_submission: '완료된 제출과 연결된 이메일 주소를 입력하세요.'
privacy_policy: 개인정보 처리방침
esignature_disclosure: 전자서명 공개
ja: ja:
require_phone_2fa_to_open: 電話による2段階認証が必要です require_phone_2fa_to_open: 電話による2段階認証が必要です
@ -5441,6 +5575,8 @@ ja:
open_source_documents_software: 'オープンソースのドキュメントソフトウェア' open_source_documents_software: 'オープンソースのドキュメントソフトウェア'
eu_data_residency: 'EU データ居住' eu_data_residency: 'EU データ居住'
please_enter_your_email_address_associated_with_the_completed_submission: '完了した提出に関連付けられたメールアドレスを入力してください。' please_enter_your_email_address_associated_with_the_completed_submission: '完了した提出に関連付けられたメールアドレスを入力してください。'
privacy_policy: プライバシーポリシー
esignature_disclosure: 電子署名に関する開示
en-US: en-US:
<<: *en <<: *en

@ -106,6 +106,7 @@ Rails.application.routes.draw do
resource :form, only: %i[show], controller: 'templates_form_preview' resource :form, only: %i[show], controller: 'templates_form_preview'
resource :code_modal, only: %i[show], controller: 'templates_code_modal' resource :code_modal, only: %i[show], controller: 'templates_code_modal'
resource :preferences, only: %i[show create], controller: 'templates_preferences' 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 :recipients, only: %i[create], controller: 'templates_recipients'
resources :submissions_export, only: %i[index new] resources :submissions_export, only: %i[index new]
end end
@ -131,6 +132,8 @@ Rails.application.routes.draw do
get :completed get :completed
end end
resource :resubmit_form, controller: 'start_form', only: :update
resources :submit_form, only: %i[], path: '' do resources :submit_form, only: %i[], path: '' do
get :success, on: :collection get :success, on: :collection
end end

@ -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

@ -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

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -174,7 +174,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_05_18_070555) do
t.text "data", null: false t.text "data", null: false
t.datetime "event_datetime", null: false t.datetime "event_datetime", null: false
t.datetime "created_at", 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 ["email"], name: "index_email_events_on_email"
t.index ["emailable_type", "emailable_id"], name: "index_email_events_on_emailable" t.index ["emailable_type", "emailable_id"], name: "index_email_events_on_emailable"
t.index ["message_id"], name: "index_email_events_on_message_id" 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.bigint "folder_id", null: false
t.string "external_id" t.string "external_id"
t.text "preferences", null: false 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 ["account_id"], name: "index_templates_on_account_id"
t.index ["author_id"], name: "index_templates_on_author_id" t.index ["author_id"], name: "index_templates_on_author_id"
t.index ["external_id"], name: "index_templates_on_external_id" t.index ["external_id"], name: "index_templates_on_external_id"

@ -31,7 +31,9 @@ module Submissions
def call(submission) def call(submission)
account = submission.account 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) document = build_audit_trail(submission)
pkcs = Accounts.load_signing_pkcs(account) pkcs = Accounts.load_signing_pkcs(account)
@ -41,8 +43,6 @@ module Submissions
document.trailer.info[:Creator] = "#{Docuseal.product_name} (#{Docuseal::PRODUCT_URL})" document.trailer.info[:Creator] = "#{Docuseal.product_name} (#{Docuseal::PRODUCT_URL})"
last_submitter = submission.submitters.select(&:completed_at).max_by(&:completed_at)
if pkcs if pkcs
sign_params = { sign_params = {
reason: sign_reason, reason: sign_reason,
@ -322,7 +322,7 @@ module Submissions
resized_image = image.resize([scale, 1].min) resized_image = image.resize([scale, 1].min)
io = StringIO.new(resized_image.write_to_buffer('.png')) 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) height = resized_image.height * (width.to_f / resized_image.width)
if height > MAX_IMAGE_HEIGHT if height > MAX_IMAGE_HEIGHT

@ -152,7 +152,8 @@ module Submissions
TESTING_FOOTER TESTING_FOOTER
end end
else 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 end
text = HexaPDF::Layout::TextFragment.create( text = HexaPDF::Layout::TextFragment.create(
@ -182,6 +183,8 @@ module Submissions
with_headings = find_last_submitter(submitter.submission, submitter:).blank? if with_headings.nil? 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| submitter.submission.template_fields.each do |field|
next if field['type'] == 'heading' && !with_headings next if field['type'] == 'heading' && !with_headings
next if field['submitter_uuid'] != submitter.uuid && field['type'] != 'heading' 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_value = submitter.values[field.dig('preferences', 'reason_field_uuid')].presence
reason_string = 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')} " \ "#{reason_value ? "#{I18n.t('reason')}: " : ''}#{reason_value || I18n.t('digitally_signed_by')} " \
"#{submitter.name}#{submitter.email.present? ? " <#{submitter.email}>" : ''}\n" \ "#{submitter.name}#{submitter.email.present? ? " <#{submitter.email}>" : ''}\n" \
"#{I18n.l(attachment.created_at.in_time_zone(submitter.account.timezone), format: :long)} " \ "#{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 = field['options']&.find { |o| o['uuid'] == area['option_uuid'] }
option_name = option['value'].presence 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) value = Array.wrap(value).include?(option_name)
end end
@ -509,7 +512,7 @@ module Submissions
end end
else else
if field['type'] == 'date' 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 end
value = NumberUtils.format_number(value, field.dig('preferences', 'format')) if field['type'] == 'number' value = NumberUtils.format_number(value, field.dig('preferences', 'format')) if field['type'] == 'number'

@ -9,6 +9,7 @@ module Templates
template = original_template.account.templates.new template = original_template.account.templates.new
template.external_id = external_id template.external_id = external_id
template.shared_link = original_template.shared_link
template.author = author template.author = author
template.name = name.presence || "#{original_template.name} (#{I18n.t('clone')})" template.name = name.presence || "#{original_template.name} (#{I18n.t('clone')})"

@ -118,10 +118,10 @@ module Templates
page = page.copy(interpretation: :srgb) page = page.copy(interpretation: :srgb)
bitdepth = 2**page.stats.to_a[1..3].pluck(2).uniq.size
data = data =
if format == FORMAT 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:, page.write_to_buffer(format, compression: 7, filter: 0, bitdepth:,
palette: true, Q: bitdepth == 8 ? Q : 5, dither: 0) palette: true, Q: bitdepth == 8 ? Q : 5, dither: 0)
else else
@ -138,6 +138,8 @@ module Templates
blob blob
rescue Vips::Error, Pdfium::PdfiumError => e rescue Vips::Error, Pdfium::PdfiumError => e
Rollbar.warning(e) if defined?(Rollbar) Rollbar.warning(e) if defined?(Rollbar)
nil
ensure ensure
doc_page&.close doc_page&.close
end end

@ -6,7 +6,7 @@ module Templates
only: %w[ only: %w[
id archived_at fields name preferences schema id archived_at fields name preferences schema
slug source submitters created_at updated_at 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], methods: %i[application_key folder_name],
include: { author: { only: %i[id email first_name last_name] } } include: { author: { only: %i[id email first_name last_name] } }

Binary file not shown.

@ -83,13 +83,15 @@ describe 'Templates API' do
end end
describe 'PUT /api/templates' do describe 'PUT /api/templates' do
it 'update a template' do let(:template) do
template = create(:template, account:, create(:template, account:,
author:, author:,
folder:, folder:,
external_id: SecureRandom.base58(10), external_id: SecureRandom.base58(10),
preferences: template_preferences) preferences: template_preferences)
end
it 'updates a template' do
put "/api/templates/#{template.id}", headers: { 'x-auth-token': author.access_token.token }, params: { put "/api/templates/#{template.id}", headers: { 'x-auth-token': author.access_token.token }, params: {
name: 'Updated Template Name', name: 'Updated Template Name',
external_id: '123456' external_id: '123456'
@ -106,6 +108,24 @@ describe 'Templates API' do
updated_at: template.updated_at updated_at: template.updated_at
}.to_json)) }.to_json))
end 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 end
describe 'DELETE /api/templates/:id' do describe 'DELETE /api/templates/:id' do
@ -206,6 +226,7 @@ describe 'Templates API' do
name: 'sample-document' name: 'sample-document'
} }
], ],
shared_link: template.shared_link,
author_id: author.id, author_id: author.id,
archived_at: nil, archived_at: nil,
created_at: template.created_at, created_at: template.created_at,

@ -33,6 +33,14 @@ RSpec.describe 'Profile Settings' do
expect(user.last_name).to eq('Beckham') expect(user.last_name).to eq('Beckham')
expect(user.email).to eq('david.beckham@example.com') expect(user.email).to eq('david.beckham@example.com')
end 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 end
context 'when changes password' do context 'when changes password' do

@ -51,6 +51,16 @@ RSpec.describe 'App Setup' do
end end
context 'when invalid information' do 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 it 'does not setup the app if the password is too short' do
fill_setup_form(form_data.merge(password: 'pass')) fill_setup_form(form_data.merge(password: 'pass'))

@ -5,7 +5,9 @@ RSpec.describe 'Signing Form' do
let(:author) { create(:user, account:) } let(:author) { create(:user, account:) }
context 'when the template form link is opened' do 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 before do
visit start_form_path(slug: template.slug) visit start_form_path(slug: template.slug)
@ -811,7 +813,9 @@ RSpec.describe 'Signing Form' do
end end
context 'when the template requires multiple submitters' do 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 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 it 'shows an explanation error message if a logged-in user associated with the template account opens the link' do

@ -92,6 +92,23 @@ RSpec.describe 'Team Settings' do
end end
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 it 'updates a user' do
first(:link, 'Edit').click first(:link, 'Edit').click

@ -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

@ -16,7 +16,7 @@ RSpec.describe 'Template' do
expect(page).to have_content(template.name) expect(page).to have_content(template.name)
expect(page).to have_content('There are no Submissions') 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_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
end end

Loading…
Cancel
Save