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

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

@ -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(&)

@ -9,7 +9,9 @@ class EmailSmtpSettingsController < ApplicationController
def create
if @encrypted_config.update(email_configs)
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

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

@ -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']
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..)
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 }
.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

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

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

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

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

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

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

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

@ -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 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 () {

@ -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.addEventListener('click', (e) => {
e.stopPropagation()
const text = this.dataset.text || this.innerText.trim()
if (navigator.clipboard) {

@ -23,7 +23,7 @@
</div>
<div
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">
{{ optionValue(option) }}
@ -55,12 +55,12 @@
>
<div
v-else-if="field.type === 'signature' && signature"
class="flex justify-between h-full gap-1 overflow-hidden"
:class="isNarrow ? 'flex-row' : 'flex-col'"
class="flex justify-between h-full gap-1 overflow-hidden w-full"
:class="isNarrow && (withSignatureId || field.preferences?.reason_field_uuid) ? 'flex-row' : 'flex-col'"
>
<div
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%"
>
<img
@ -69,7 +69,7 @@
>
</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="isNarrow ? 'w-1/2' : 'w-full'"
>

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

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

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

@ -79,13 +79,52 @@
name="buttons"
/>
<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
v-if="withSignYourselfButton"
v-else-if="withSignYourselfButton"
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"
:target="template.submitters.length > 1 ? '' : '_blank'"
:data-turbo-frame="template.submitters.length > 1 ? 'modal' : ''"
data-turbo-frame="modal"
@click="maybeShowErrorTemplateAlert"
>
<IconWritingSign

@ -18,7 +18,7 @@
#
# 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_emailable (emailable_type,emailable_id)
# index_email_events_on_message_id (message_id)

@ -65,12 +65,18 @@ class Submission < ApplicationRecord
scope :active, -> { where(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 {
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)
}
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) }
enum :source, {

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

@ -73,6 +73,8 @@ class User < ApplicationRecord
scope :archived, -> { where.not(archived_at: nil) }
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
super || build_access_token.tap(&:save!)
end

@ -2,7 +2,11 @@
<% if @pagy.pages > 1 %>
<div class="flex my-6 justify-center md:justify-between">
<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] %>
</div>
<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>
<% if Docuseal.multitenant? || Accounts.can_send_emails?(@submitter.account) %>
<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>
<% 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">
<%= 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>
<% end %>
</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>
<%= 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') %>">
<% if field['type'] == 'signature' %>
<% 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 overflow-hidden <%= is_narrow ? 'w-1/2' : 'flex-grow' %>" style="min-height: 50%">
<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 && (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 %>">
</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="truncate uppercase">
ID: <%= attachment.uuid %>

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

@ -1,5 +1,5 @@
<% 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 %>
<div class="flex justify-between items-center w-full mb-4">
<div class="flex items-center flex-grow min-w-0">
@ -61,12 +61,12 @@
</div>
</div>
<% end %>
<% if @pagy.count > 0 %>
<% if @pagy.count.nil? || @pagy.count > 0 %>
<div class="space-y-4">
<%= render partial: 'templates/submission', collection: @submissions, locals: { with_template: true } %>
</div>
<% 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' %>
<% end %>
<% if @submissions.present? || (params[:q].blank? && filter_params.blank?) %>

@ -43,10 +43,10 @@
<% end %>
</div>
<% 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>
<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>
<% end %>
</div>

@ -39,13 +39,13 @@
<% end %>
</div>
</div>
<% if @pagy.count > 0 %>
<% if @pagy.count.nil? || @pagy.count > 0 %>
<div class="grid gap-4 md:grid-cols-3">
<%= render partial: 'templates/template', collection: @templates %>
</div>
<% 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 %>

@ -49,7 +49,12 @@
<% end %>
</div>
<% 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>
<% end %>
</div>

@ -1,7 +1,7 @@
<%= render 'title', template: @template %>
<% 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? %>
<% 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="flex justify-between items-center md:items-end">
<div>
@ -35,27 +35,33 @@
<%= svg_icon('list', class: 'w-5 h-5') %>
<span class="font-normal"><%= t('all') %></span>
</div>
<% unless can?(:manage, :countless) %>
<div class="badge badge-neutral badge-outline font-medium">
<%= params[:status].blank? && filter_params.blank? ? @pagy.count : @base_submissions.unscope(:group, :order).select(:id).distinct.count %>
</div>
<% end %>
</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">
<div class="flex items-center space-x-1">
<%= svg_icon('clock', class: 'w-5 h-5') %>
<span class="font-normal"><%= t('pending') %></span>
</div>
<% unless can?(:manage, :countless) %>
<div class="badge badge-neutral badge-outline font-medium">
<%= params[:status] == 'pending' && filter_params.blank? ? @pagy.count : @base_submissions.pending.unscope(:group, :order).select(:id).distinct.count %>
</div>
<% end %>
</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">
<div class="flex items-center space-x-1">
<%= svg_icon('circle_check', class: 'w-5 h-5') %>
<span class="font-normal"><%= t('completed') %></span>
</div>
<% unless can?(:manage, :countless) %>
<div class="badge badge-neutral badge-outline font-medium">
<%= params[:status] == 'completed' && filter_params.blank? ? @pagy.count : @base_submissions.completed.unscope(:group, :order).select(:id).distinct.count %>
</div>
<% end %>
</a>
</div>
<div class="flex items-end flex-col md:flex-row gap-2 w-full md:w-fit">
@ -89,7 +95,7 @@
<% end %>
<% end %>
<% 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') %>
<span class="mr-1"><%= t('sign_it_yourself') %></span>
<% end %>

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

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

@ -12,7 +12,7 @@
<%= render 'templates/dashboard_dropzone', style: 'height: 114px' %>
<% end %>
<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">
<%= render 'dashboard/toggle_view', selected: 'templates' %>
</div>
@ -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 %>
<div class="mt-2">
<%= view_archived_html %>

@ -380,8 +380,18 @@
<%= t('embedding_url') %>
</label>
<div class="flex gap-2 mb-4 mt-2">
<%= 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| %>
<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>
<%= 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
require 'pagy/extras/countless'
Pagy::DEFAULT[:limit] = 10
Pagy::DEFAULT.freeze

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

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

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

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

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

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

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

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

Binary file not shown.

@ -83,13 +83,15 @@ describe 'Templates API' do
end
describe 'PUT /api/templates' do
it 'update a template' do
template = create(:template, account:,
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,

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

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

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

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

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

Loading…
Cancel
Save