Compare commits

..

No commits in common. '673cc1e0dfd50936a8cd07e5c57da9329bd2b4e0' and 'cabfe456086ad9fe4b70b15d04d01a54a8a15649' have entirely different histories.

@ -144,7 +144,7 @@ GEM
cldr-plurals-runtime-rb (1.1.0)
cmdparse (3.0.7)
coderay (1.1.3)
concurrent-ruby (1.3.7)
concurrent-ruby (1.3.6)
connection_pool (3.0.2)
crack (1.0.1)
bigdecimal
@ -195,13 +195,13 @@ GEM
railties (>= 6.1.0)
faker (3.6.1)
i18n (>= 1.8.11, < 2)
faraday (2.14.3)
faraday (2.14.2)
faraday-net_http (>= 2.0, < 3.5)
json
logger
faraday-follow_redirects (0.5.0)
faraday (>= 1, < 3)
faraday-net_http (3.4.4)
faraday-net_http (3.4.2)
net-http (~> 0.5)
ferrum (0.17.2)
addressable (~> 2.5)
@ -323,15 +323,15 @@ GEM
net-smtp (0.5.1)
net-protocol
nio4r (2.7.5)
nokogiri (1.19.4-aarch64-linux-gnu)
nokogiri (1.19.3-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.19.4-aarch64-linux-musl)
nokogiri (1.19.3-aarch64-linux-musl)
racc (~> 1.4)
nokogiri (1.19.4-arm64-darwin)
nokogiri (1.19.3-arm64-darwin)
racc (~> 1.4)
nokogiri (1.19.4-x86_64-linux-gnu)
nokogiri (1.19.3-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.19.4-x86_64-linux-musl)
nokogiri (1.19.3-x86_64-linux-musl)
racc (~> 1.4)
numo-narray-alt (0.10.3)
onnxruntime (0.10.1-aarch64-linux)

@ -2,10 +2,6 @@
module Api
class SubmissionsController < ApiBaseController
SUBMISSION_COLUMNS = %i[id name slug source submitters_order expire_at created_at updated_at
archived_at variables template_id template_submitters created_by_user_id].freeze
TEMPLATE_COLUMNS = %i[id name external_id created_at updated_at folder_id submitters].freeze
load_and_authorize_resource :template, only: :create
load_and_authorize_resource :submission, only: %i[show index destroy]
@ -17,22 +13,10 @@ module Api
submissions = Submissions.search(current_user, @submissions, params[:q])
submissions = filter_submissions(submissions, params)
with_fields = params[:include].to_s.include?('fields') || params[:include].to_s.include?('combined_document_url')
submissions = paginate(
submissions.select(with_fields ? nil : SUBMISSION_COLUMNS)
.preload(:created_by_user, :submitters, combined_document_attachment: :blob,
audit_trail_attachment: :blob)
)
ActiveRecord::Associations::Preloader.new(
records: submissions,
associations: :template,
scope: with_fields ? nil : Template.select(TEMPLATE_COLUMNS)
).call
ActiveRecord::Associations::Preloader.new(records: submissions.filter_map(&:template),
associations: { folder: :parent_folder }).call
submissions = paginate(submissions.preload(:created_by_user, :submitters,
template: { folder: :parent_folder },
combined_document_attachment: :blob,
audit_trail_attachment: :blob))
expires_at = Accounts.link_expires_at(current_account)

@ -104,7 +104,7 @@ module Api
private
def maybe_filter_by_completed_at(submitters, params)
def maybe_filder_by_completed_at(submitters, params)
if params[:completed_after].present?
submitters = submitters.where(completed_at: Time.zone.parse(params[:completed_after])..)
end
@ -177,7 +177,7 @@ module Api
submitters = submitters.joins(:submission).where(submissions: { template_id: params[:template_id] })
end
maybe_filter_by_completed_at(submitters, params)
maybe_filder_by_completed_at(submitters, params)
end
def assign_external_id(submitter, attrs)

@ -48,7 +48,7 @@ module Api
archived = params.key?(:archived) ? params[:archived] : params.dig(:template, :archived)
if archived.in?([true, false]) && current_ability.can?(:destroy, @template)
if archived.in?([true, false])
@template.archived_at = archived == true ? Time.current : nil
end
@ -57,10 +57,7 @@ module Api
SearchEntries.enqueue_reindex(@template)
WebhookUrls.enqueue_events(@template, 'template.updated')
if @template.saved_change_to_archived_at? && @template.archived_at?
WebhookUrls.enqueue_events(@template, 'template.archived')
end
WebhookUrls.enqueue_events(@template, 'template.archived') if archived == true
render json: @template.as_json(only: %i[id updated_at])
end

@ -7,7 +7,7 @@ class SubmissionsArchivedController < ApplicationController
@submissions = @submissions.left_joins(:template)
@submissions = @submissions.where.not(archived_at: nil)
.or(@submissions.where.not(templates: { archived_at: nil }))
.preload(:template_accesses, :created_by_user)
.preload(:template_accesses, :created_by_user, template: :author)
@submissions = Submissions.search(current_user, @submissions, params[:q], search_template: true)
@submissions = Submissions::Filter.call(@submissions, current_user, params)
@ -18,15 +18,6 @@ class SubmissionsArchivedController < ApplicationController
@submissions.order(id: :desc)
end
@pagy, @submissions = pagy_auto(@submissions.select_for_list.preload(submitters: :start_form_submission_events))
template_scope = @submissions.all?(&:template_submitters) ? Template.select_for_list : nil
ActiveRecord::Associations::Preloader.new(records: @submissions,
associations: :template,
scope: template_scope).call
ActiveRecord::Associations::Preloader.new(records: @submissions.filter_map(&:template),
associations: :author).call
@pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events))
end
end

@ -1,7 +1,9 @@
# frozen_string_literal: true
class SubmissionsController < ApplicationController
load_and_authorize_resource :template, only: %i[new create]
before_action :load_template, only: %i[new create]
authorize_resource :template, only: %i[new create]
load_and_authorize_resource :submission, only: %i[show destroy]
prepend_before_action :maybe_redirect_com, only: %i[show]
@ -111,4 +113,8 @@ class SubmissionsController < ApplicationController
def submissions_params
params.permit(submission: { submitters: [:uuid, :email, :phone, :name, { values: {} }] })
end
def load_template
@template = Template.accessible_by(current_ability).find(params[:template_id])
end
end

@ -8,7 +8,7 @@ class SubmissionsDashboardController < ApplicationController
@submissions = @submissions.where(archived_at: nil)
.where(templates: { archived_at: nil })
.preload(:template_accesses, :created_by_user)
.preload(:template_accesses, :created_by_user, template: :author)
@submissions = Submissions.search(current_user, @submissions, params[:q], search_template: true)
@submissions = Submissions::Filter.call(@submissions, current_user, params)
@ -19,15 +19,6 @@ class SubmissionsDashboardController < ApplicationController
@submissions.order(id: :desc)
end
@pagy, @submissions = pagy_auto(@submissions.select_for_list.preload(submitters: :start_form_submission_events))
template_scope = @submissions.all?(&:template_submitters) ? Template.select_for_list : nil
ActiveRecord::Associations::Preloader.new(records: @submissions,
associations: :template,
scope: template_scope).call
ActiveRecord::Associations::Preloader.new(records: @submissions.filter_map(&:template),
associations: :author).call
@pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events))
end
end

@ -30,11 +30,9 @@ class TemplateFoldersController < ApplicationController
(@template_folders.size < 7 ? 9 : 6)
end
@pagy, @templates = pagy_auto(@templates.select_for_list, limit:)
@pagy, @templates = pagy_auto(@templates, limit:)
if params[:q].present? && @templates.blank?
@related_submissions_pagy, @related_submissions = load_related_submissions(@template_folder)
end
load_related_submissions if params[:q].present? && @templates.blank?
else
@pagy, @template_folders = pagy(@template_folders, limit: FOLDERS_PER_PAGE)
@ -57,10 +55,11 @@ class TemplateFoldersController < ApplicationController
def selected_order
@selected_order ||=
if can?(:manage, :countless)
if cookies.permanent[:dashboard_templates_order].blank? ||
(cookies.permanent[:dashboard_templates_order] == 'used_at' && can?(:manage, :countless))
'created_at'
else
cookies.permanent[:dashboard_templates_order].presence || 'created_at'
cookies.permanent[:dashboard_templates_order]
end
end
@ -68,20 +67,20 @@ class TemplateFoldersController < ApplicationController
params.require(:template_folder).permit(:name)
end
def load_related_submissions(template_folder)
related_submissions =
def load_related_submissions
@related_submissions =
Submission.accessible_by(current_ability)
.where(archived_at: nil)
.where(template_id: current_account.templates.active
.where(folder: [template_folder, *template_folder.subfolders])
.where(folder: [@template_folder, *@template_folder.subfolders])
.select(:id))
.preload(:template_accesses, :created_by_user,
template: :author,
submitters: :start_form_submission_events)
related_submissions = Submissions.search(current_user, related_submissions, params[:q])
.order(id: :desc)
@related_submissions = Submissions.search(current_user, @related_submissions, params[:q])
.order(id: :desc)
pagy_auto(related_submissions.select_for_list, limit: 5)
@related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5)
end
end

@ -10,17 +10,11 @@ class TemplatesArchivedController < ApplicationController
@templates = Templates.search(current_user, @templates, params[:q])
@pagy, @templates = pagy_auto(@templates.select_for_list, limit: 12)
@pagy, @templates = pagy_auto(@templates, limit: 12)
return unless params[:q].present? && @templates.blank?
@related_submissions_pagy, @related_submissions = load_related_submissions
end
private
def load_related_submissions
related_submissions =
@related_submissions =
Submission.accessible_by(current_ability)
.joins(:template)
.where.not(templates: { archived_at: nil })
@ -28,9 +22,9 @@ class TemplatesArchivedController < ApplicationController
template: :author,
submitters: :start_form_submission_events)
related_submissions = Submissions.search(current_user, related_submissions, params[:q])
.order(id: :desc)
@related_submissions = Submissions.search(current_user, @related_submissions, params[:q])
.order(id: :desc)
pagy_auto(related_submissions.select_for_list, limit: 5)
@related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5)
end
end

@ -15,7 +15,7 @@ class TemplatesArchivedSubmissionsController < ApplicationController
@submissions.order(id: :desc)
end
@pagy, @submissions = pagy_auto(@submissions.select_for_list.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

@ -19,8 +19,7 @@ class TemplatesController < ApplicationController
submissions.order(id: :desc)
end
@pagy, @submissions =
pagy_auto(submissions.select_for_list.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

@ -11,126 +11,86 @@ class TemplatesDashboardController < ApplicationController
helper_method :selected_order
def index
@default_folder = current_account.default_template_folder
@template_folders =
TemplateFolders.filter_active_folders(@template_folders.where(parent_folder_id: nil), @templates)
@template_folders = @template_folders.where.not(id: @default_folder.id) if params[:q].blank?
@template_folders = TemplateFolders.search(@template_folders, params[:q])
@template_folders = TemplateFolders.sort(@template_folders, current_user, selected_order)
@shared_templates = Templates.shared(current_user).active
@pagy, @template_folders, @show_default_folder, @show_shared_folder, @show_shared_inline =
load_folders(@template_folders, @templates, @shared_templates)
@pagy, @template_folders = pagy(
@template_folders,
limit: FOLDERS_PER_PAGE,
page: @template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1
)
if @pagy.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD
@templates = @templates.none
else
if @show_shared_inline
@templates = @shared_templates.preload(:template_sharings)
else
@templates = @templates.active
@templates = @templates.where(folder_id: @default_folder.id) if params[:q].blank?
end
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
@templates = filter_templates(@templates).preload(:author, :template_accesses)
@templates = Templates::Order.call(@templates, current_user, selected_order)
@pagy, @templates = load_templates(@templates.select_for_list, @pagy.count,
show_shared_inline: @show_shared_inline)
limit =
if @template_folders.size < 4
TEMPLATES_PER_PAGE
else
(@template_folders.size < 7 ? 9 : 6)
end
if params[:q].present? && @templates.blank?
@related_submissions_pagy, @related_submissions = load_related_submissions
end
@pagy, @templates = pagy_auto(@templates, limit:)
load_related_submissions if params[:q].present? && @templates.blank?
end
end
private
def load_templates(templates, folders_count, show_shared_inline: false)
templates = templates.preload(:author, :template_accesses)
def filter_templates(templates)
rel = templates.active
templates =
if show_shared_inline
Templates.search_shared(current_user, templates, params[:q])
else
Templates.search(current_user, templates, params[:q])
end
if params[:q].blank?
if Docuseal.multitenant? ? current_account.testing? : current_account.linked_account_account
shared_account_ids = [current_user.account_id]
shared_account_ids << TemplateSharing::ALL_ID if !Docuseal.multitenant? && !current_account.testing?
templates = Templates::Order.call(templates, current_user, selected_order)
shared_template_ids = TemplateSharing.where(account_id: shared_account_ids).select(:template_id)
limit =
if folders_count < 4
TEMPLATES_PER_PAGE
rel = Template.where(
Template.arel_table[:id].in(
rel.where(folder_id: current_account.default_template_folder.id).select(:id).arel
.union(:all, shared_template_ids.arel)
)
)
else
(folders_count < 7 ? 9 : 6)
rel = rel.where(folder_id: current_account.default_template_folder.id)
end
pagy_auto(templates, limit:)
end
def load_folders(template_folders, templates, shared_templates)
if params[:q].present?
pagy(template_folders, limit: FOLDERS_PER_PAGE,
page: template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1)
else
load_folders_with_pinned(template_folders, templates, shared_templates)
end
end
def load_folders_with_pinned(template_folders, templates, shared_templates)
folders_count = template_folders.count
shared_exists = shared_templates.exists?
default_has_templates = templates.active.exists?(folder_id: current_account.default_template_folder.id)
show_inline_folders =
folders_count + (shared_exists && default_has_templates ? 1 : 0) <= SHOW_TEMPLATES_FOLDERS_THRESHOLD
show_shared_inline = shared_exists && !default_has_templates && show_inline_folders
show_shared_in_grid = shared_exists && !show_shared_inline
show_default_in_grid = !show_inline_folders && default_has_templates
pinned_count = (show_default_in_grid ? 1 : 0) + (show_shared_in_grid ? 1 : 0)
pagy = Pagy::Offset.new(count: folders_count + pinned_count,
page: show_inline_folders ? 1 : [params[:page].to_s.to_i, 1].max,
limit: FOLDERS_PER_PAGE,
raise_range_error: true)
show_default_folder = show_default_in_grid && pagy.page == 1
show_shared_folder = show_shared_in_grid && pagy.page == 1
folder_offset = pagy.page == 1 ? 0 : pagy.offset - pinned_count
folder_limit = pagy.page == 1 ? FOLDERS_PER_PAGE - pinned_count : FOLDERS_PER_PAGE
template_folders = template_folders.offset(folder_offset).limit(folder_limit)
[pagy, template_folders, show_default_folder, show_shared_folder, show_shared_inline]
Templates.search(current_user, rel, params[:q])
end
def selected_order
@selected_order ||=
if can?(:manage, :countless)
if cookies.permanent[:dashboard_templates_order].blank? ||
(cookies.permanent[:dashboard_templates_order] == 'used_at' && can?(:manage, :countless))
'created_at'
else
cookies.permanent[:dashboard_templates_order].presence || 'created_at'
cookies.permanent[:dashboard_templates_order]
end
end
def load_related_submissions
related_submissions = Submission.accessible_by(current_ability)
.left_joins(:template)
.where(archived_at: nil)
.where(templates: { archived_at: nil })
.preload(:template_accesses, :created_by_user,
template: :author,
submitters: :start_form_submission_events)
related_submissions = Submissions.search(current_user, related_submissions, params[:q])
.order(id: :desc)
pagy_auto(related_submissions.select_for_list, limit: 5)
@related_submissions = Submission.accessible_by(current_ability)
.left_joins(:template)
.where(archived_at: nil)
.where(templates: { archived_at: nil })
.preload(:template_accesses, :created_by_user,
template: :author,
submitters: :start_form_submission_events)
@related_submissions = Submissions.search(current_user, @related_submissions, params[:q])
.order(id: :desc)
@related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5)
end
end

@ -4,7 +4,7 @@ class TemplatesRestoreController < ApplicationController
load_and_authorize_resource :template
def create
authorize!(:destroy, @template)
authorize!(:update, @template)
@template.update!(archived_at: nil)

@ -1,47 +0,0 @@
# frozen_string_literal: true
class TemplatesSharedController < ApplicationController
def index
authorize!(:read, Template)
@is_archived = params[:archived] == 'true'
@templates = Templates.shared(current_user)
@has_archived = !@is_archived && @templates.archived.exists?
@templates = @is_archived ? @templates.archived : @templates.active
@templates = @templates.preload(:author, :template_accesses, :template_sharings)
.order(id: :desc)
@templates = Templates.search_shared(current_user, @templates, params[:q])
@pagy, @templates = pagy_auto(@templates.select_for_list, limit: 12)
return unless params[:q].present? && @templates.blank?
@related_submissions_pagy, @related_submissions = load_related_submissions(is_archived: @is_archived)
end
private
def load_related_submissions(is_archived:)
shared_templates = Templates.shared(current_user)
shared_templates = is_archived ? shared_templates.archived : shared_templates.active
related_submissions =
Submission.accessible_by(current_ability)
.where(template_id: shared_templates.select(:id))
.preload(:template_accesses, :created_by_user,
template: :author,
submitters: :start_form_submission_events)
related_submissions = related_submissions.where(archived_at: nil) unless is_archived
related_submissions = Submissions.search(current_user, related_submissions, params[:q])
.order(id: :desc)
pagy_auto(related_submissions.select_for_list, limit: 5)
end
end

@ -289,7 +289,6 @@
:id="currentField.uuid"
dir="auto"
:required="currentField.required"
:aria-label="showFieldNames && (currentField.name || currentField.title) ? undefined : (currentField.name || currentField.title || t('select_your_option'))"
:aria-describedby="currentField.description ? currentField.uuid + '-desc' : undefined"
class="select base-input !text-2xl w-full text-center font-normal"
:class="{ 'text-gray-300': !values[currentField.uuid] }"
@ -318,7 +317,7 @@
<div v-else-if="currentField.type === 'radio'">
<label
v-if="showFieldNames && (currentField.name || currentField.title)"
:id="currentField.uuid + '-radio-label'"
:for="currentField.uuid"
dir="auto"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
:class="{ 'mb-2': !currentField.description }"
@ -357,8 +356,6 @@
</div>
<div
class="space-y-3.5 mx-auto"
role="radiogroup"
:aria-labelledby="(currentField.name || currentField.title) ? currentField.uuid + '-radio-label' : null"
:class="{ hidden: !showFieldNames || (currentField.options.every((e) => !e.value) && currentField.options.length > 4) }"
>
<div

@ -4,7 +4,6 @@ const en = {
close: 'Close',
uploaded_files: 'Uploaded files',
signature_drawing_area: 'Signature drawing area. Use mouse or touch to draw your signature.',
initials_drawing_area: 'Initials drawing area. Use mouse or touch to draw your initials.',
kba: 'KBA',
please_upload_an_image_file: 'Please upload an image file',
must_be_characters_length: 'Must be {number} characters long',
@ -89,7 +88,6 @@ const en = {
please_check_the_box_to_continue: 'Please check the box to continue.',
open_source_documents_software: 'open source documents software',
verified_phone_number: 'Verify Phone Number',
country_code: 'Country code',
use_international_format: 'Use international format: +1xxx',
six_digits_code: '6-digit code',
change_phone_number: 'Change phone number',
@ -122,7 +120,6 @@ const es = {
close: 'Cerrar',
uploaded_files: 'Archivos subidos',
signature_drawing_area: 'Área de dibujo de firma. Use el ratón o toque para dibujar su firma.',
initials_drawing_area: 'Área de dibujo de iniciales. Use el ratón o toque para dibujar sus iniciales.',
kba: 'KBA',
please_upload_an_image_file: 'Por favor, sube un archivo de imagen',
must_be_characters_length: 'Debe tener {number} caracteres de longitud',
@ -207,7 +204,6 @@ const es = {
please_check_the_box_to_continue: 'Por favor marque la casilla para continuar.',
open_source_documents_software: 'software de documentos de código abierto',
verified_phone_number: 'Verificar número de teléfono',
country_code: 'Código de país',
use_international_format: 'Usar formato internacional: +1xxx',
six_digits_code: 'Código de 6 dígitos',
change_phone_number: 'Cambiar número de teléfono',
@ -240,7 +236,6 @@ const it = {
close: 'Chiudi',
uploaded_files: 'File caricati',
signature_drawing_area: 'Area di disegno della firma. Usa il mouse o il tocco per disegnare la tua firma.',
initials_drawing_area: 'Area di disegno delle iniziali. Usa il mouse o il tocco per disegnare le tue iniziali.',
kba: 'KBA',
please_upload_an_image_file: 'Per favore carica un file immagine',
must_be_characters_length: 'Deve essere lungo {number} caratteri',
@ -325,7 +320,6 @@ const it = {
please_check_the_box_to_continue: 'Si prega di spuntare la casella per continuare.',
open_source_documents_software: 'software di documenti open source',
verified_phone_number: 'Verifica numero di telefono',
country_code: 'Prefisso internazionale',
use_international_format: 'Usa formato internazionale: +1xxx',
six_digits_code: 'Codice a 6 cifre',
change_phone_number: 'Cambia numero di telefono',
@ -358,7 +352,6 @@ const de = {
close: 'Schließen',
uploaded_files: 'Hochgeladene Dateien',
signature_drawing_area: 'Unterschriftszeichenbereich. Verwenden Sie die Maus oder Berührung, um Ihre Unterschrift zu zeichnen.',
initials_drawing_area: 'Zeichenbereich für Initialen. Verwenden Sie die Maus oder Berührung, um Ihre Initialen zu zeichnen.',
kba: 'KBA',
please_upload_an_image_file: 'Bitte laden Sie eine Bilddatei hoch',
must_be_characters_length: 'Muss {number} Zeichen lang sein',
@ -443,7 +436,6 @@ const de = {
please_check_the_box_to_continue: 'Bitte aktivieren Sie das Kontrollkästchen, um fortzufahren.',
open_source_documents_software: 'Open-Source-Dokumentensoftware',
verified_phone_number: 'Telefonnummer verifizieren',
country_code: 'Ländervorwahl',
use_international_format: 'Internationales Format verwenden: +1xxx',
six_digits_code: '6-stelliger Code',
change_phone_number: 'Telefonnummer ändern',
@ -476,7 +468,6 @@ const fr = {
close: 'Fermer',
uploaded_files: 'Fichiers téléchargés',
signature_drawing_area: 'Zone de dessin de signature. Utilisez la souris ou le toucher pour dessiner votre signature.',
initials_drawing_area: 'Zone de dessin des initiales. Utilisez la souris ou le toucher pour dessiner vos initiales.',
kba: 'KBA',
please_upload_an_image_file: 'Veuillez téléverser un fichier image',
must_be_characters_length: 'Doit comporter {number} caractères',
@ -561,7 +552,6 @@ const fr = {
please_check_the_box_to_continue: 'Veuillez cocher la case pour continuer.',
open_source_documents_software: 'logiciel de documents open source',
verified_phone_number: 'Vérifier le numéro de téléphone',
country_code: 'Indicatif du pays',
use_international_format: 'Utilisez le format international : +1xxx',
six_digits_code: 'Code à 6 chiffres',
change_phone_number: 'Changer de numéro de téléphone',
@ -594,7 +584,6 @@ const pl = {
close: 'Zamknij',
uploaded_files: 'Przesłane pliki',
signature_drawing_area: 'Obszar rysowania podpisu. Użyj myszy lub dotyku, aby narysować swój podpis.',
initials_drawing_area: 'Obszar rysowania inicjałów. Użyj myszy lub dotyku, aby narysować swoje inicjały.',
kba: 'KBA',
please_upload_an_image_file: 'Proszę przesłać plik obrazu',
must_be_characters_length: 'Musi mieć długość {number} znaków',
@ -679,7 +668,6 @@ const pl = {
please_check_the_box_to_continue: 'Proszę zaznaczyć pole, aby kontynuować.',
open_source_documents_software: 'oprogramowanie do dokumentów open source',
verified_phone_number: 'Zweryfikuj numer telefonu',
country_code: 'Kod kraju',
use_international_format: 'Użyj międzynarodowego formatu: +1xxx',
six_digits_code: '6-cyfrowy kod',
change_phone_number: 'Zmień numer telefonu',
@ -712,7 +700,6 @@ const uk = {
close: 'Закрити',
uploaded_files: 'Завантажені файли',
signature_drawing_area: 'Область малювання підпису. Використовуйте мишу або дотик, щоб намалювати свій підпис.',
initials_drawing_area: 'Область малювання ініціалів. Використовуйте мишу або дотик, щоб намалювати свої ініціали.',
kba: 'KBA',
please_upload_an_image_file: 'Будь ласка, завантажте файл зображення',
must_be_characters_length: 'Має містити {number} символів',
@ -797,7 +784,6 @@ const uk = {
please_check_the_box_to_continue: 'Будь ласка, позначте прапорець, щоб продовжити.',
open_source_documents_software: 'відкрите програмне забезпечення для документів',
verified_phone_number: 'Підтвердіть номер телефону',
country_code: 'Код країни',
use_international_format: 'Використовуйте міжнародний формат: +1xxx',
six_digits_code: '6-значний код',
change_phone_number: 'Змінити номер телефону',
@ -830,7 +816,6 @@ const cs = {
close: 'Zavřít',
uploaded_files: 'Nahrané soubory',
signature_drawing_area: 'Oblast pro kreslení podpisu. Použijte myš nebo dotyk k nakreslení podpisu.',
initials_drawing_area: 'Oblast pro kreslení iniciál. Použijte myš nebo dotyk k nakreslení iniciál.',
kba: 'KBA',
please_upload_an_image_file: 'Nahrajte prosím obrázkový soubor',
must_be_characters_length: 'Musí mít délku {number} znaků',
@ -915,7 +900,6 @@ const cs = {
please_check_the_box_to_continue: 'Prosím, zaškrtněte políčko pro pokračování.',
open_source_documents_software: 'open source software pro dokumenty',
verified_phone_number: 'Ověřte telefonní číslo',
country_code: 'Předvolba země',
use_international_format: 'Použijte mezinárodní formát: +1xxx',
six_digits_code: '6-místný kód',
change_phone_number: 'Změnit telefonní číslo',
@ -948,7 +932,6 @@ const pt = {
close: 'Fechar',
uploaded_files: 'Arquivos enviados',
signature_drawing_area: 'Área de desenho da assinatura. Use o mouse ou toque para desenhar sua assinatura.',
initials_drawing_area: 'Área de desenho das iniciais. Use o mouse ou toque para desenhar suas iniciais.',
kba: 'KBA',
please_upload_an_image_file: 'Por favor, envie um arquivo de imagem',
must_be_characters_length: 'Deve ter {number} caracteres',
@ -1033,7 +1016,6 @@ const pt = {
please_check_the_box_to_continue: 'Por favor, marque a caixa para continuar.',
open_source_documents_software: 'software de documentos de código aberto',
verified_phone_number: 'Verificar Número de Telefone',
country_code: 'Código do país',
use_international_format: 'Use formato internacional: +1xxx',
six_digits_code: 'Código de 6 dígitos',
change_phone_number: 'Alterar número de telefone',
@ -1066,7 +1048,6 @@ const he = {
close: 'סגור',
uploaded_files: 'קבצים שהועלו',
signature_drawing_area: 'אזור ציור חתימה. השתמש בעכבר או במגע כדי לצייר את החתימה שלך.',
initials_drawing_area: 'אזור ציור ראשי תיבות. השתמש בעכבר או במגע כדי לצייר את ראשי התיבות שלך.',
kba: 'KBA',
please_upload_an_image_file: 'אנא העלה קובץ תמונה',
must_be_characters_length: 'חייב להיות באורך של {number} תווים',
@ -1151,7 +1132,6 @@ const he = {
please_check_the_box_to_continue: 'אנא סמן את התיבה כדי להמשיך.',
open_source_documents_software: 'תוכנה פתוחה למסמכים',
verified_phone_number: 'אימות מספר טלפון',
country_code: 'קידומת מדינה',
use_international_format: 'השתמש בפורמט בינלאומי: +1xxx',
six_digits_code: 'קוד שש ספרות',
change_phone_number: 'שינוי מספר טלפון',
@ -1184,7 +1164,6 @@ const nl = {
close: 'Sluiten',
uploaded_files: 'Geüploade bestanden',
signature_drawing_area: 'Handtekening tekengebied. Gebruik de muis of aanraking om uw handtekening te tekenen.',
initials_drawing_area: 'Tekengebied voor initialen. Gebruik de muis of aanraking om uw initialen te tekenen.',
kba: 'KBA',
please_upload_an_image_file: 'Upload alstublieft een afbeeldingsbestand',
must_be_characters_length: 'Moet {number} tekens lang zijn',
@ -1269,7 +1248,6 @@ const nl = {
please_check_the_box_to_continue: 'Vink het vakje aan om door te gaan.',
open_source_documents_software: 'Open source documenten software',
verified_phone_number: 'Telefoonnummer verifiëren',
country_code: 'Landcode',
use_international_format: 'Gebruik internationaal formaat: +1xxx',
six_digits_code: '6-cijferige code',
change_phone_number: 'Wijzig telefoonnummer',
@ -1302,7 +1280,6 @@ const ar = {
close: 'إغلاق',
uploaded_files: 'الملفات المرفوعة',
signature_drawing_area: 'منطقة رسم التوقيع. استخدم الماوس أو اللمس لرسم توقيعك.',
initials_drawing_area: 'منطقة رسم الحروف الأولى. استخدم الماوس أو اللمس لرسم الحروف الأولى.',
kba: 'KBA',
please_upload_an_image_file: 'يرجى تحميل ملف صورة',
must_be_characters_length: 'يجب أن يكون الطول {number} حرفًا',
@ -1387,7 +1364,6 @@ const ar = {
please_check_the_box_to_continue: 'الرجاء التحقق من الخانة للمتابعة.',
open_source_documents_software: 'برنامج وثائق مفتوح المصدر',
verified_phone_number: 'تحقق من رقم الهاتف',
country_code: 'رمز الدولة',
use_international_format: 'استخدم الشكل الدولي: +1xxx',
six_digits_code: 'رمز مكون من 6 أرقام',
change_phone_number: 'تغيير رقم الهاتف',
@ -1420,7 +1396,6 @@ const ko = {
close: '닫기',
uploaded_files: '업로드된 파일',
signature_drawing_area: '서명 그리기 영역. 마우스 또는 터치를 사용하여 서명을 그리세요.',
initials_drawing_area: '이니셜 그리기 영역. 마우스 또는 터치를 사용하여 이니셜을 그리세요.',
kba: 'KBA',
please_upload_an_image_file: '이미지 파일을 업로드해 주세요',
must_be_characters_length: '{number}자여야 합니다',
@ -1505,7 +1480,6 @@ const ko = {
please_check_the_box_to_continue: '계속하려면 확인란을 선택하십시오.',
open_source_documents_software: '오픈 소스 문서 소프트웨어',
verified_phone_number: '전화번호 확인됨',
country_code: '국가 번호',
use_international_format: '국제 포맷 사용: +1xxx',
six_digits_code: '6자리 코드',
change_phone_number: '전화번호 변경',
@ -1538,7 +1512,6 @@ const ja = {
close: '閉じる',
uploaded_files: 'アップロードされたファイル',
signature_drawing_area: '署名描画エリア。マウスまたはタッチを使用して署名を描いてください。',
initials_drawing_area: 'イニシャル描画エリア。マウスまたはタッチを使用してイニシャルを描いてください。',
kba: 'KBA',
please_upload_an_image_file: '画像ファイルをアップロードしてください',
must_be_characters_length: '{number}文字でなければなりません',
@ -1623,7 +1596,6 @@ const ja = {
please_check_the_box_to_continue: '続行するにはボックスにチェックを入れてください。',
open_source_documents_software: 'オープンソースのドキュメントソフトウェア',
verified_phone_number: '電話番号を認証',
country_code: '国番号',
use_international_format: '国際形式を使用してください:+1xxx',
six_digits_code: '6桁のコード',
change_phone_number: '電話番号を変更',

@ -141,7 +141,6 @@
<img
v-if="modelValue || computedPreviousValue"
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
:alt="field.name || t('initials')"
class="mx-auto bg-white border border-base-300 rounded max-h-44"
>
<div class="relative">
@ -157,8 +156,6 @@
<canvas
v-show="!modelValue && !computedPreviousValue"
ref="canvas"
role="img"
:aria-label="t('initials_drawing_area')"
class="bg-white border border-base-300 rounded-2xl w-full draw-canvas"
/>
</div>
@ -168,7 +165,6 @@
ref="textInput"
class="base-input !text-2xl w-full mt-6 text-center"
:required="field.required && !isInitialsStarted"
:aria-label="field.name || t('initials')"
:placeholder="`${t('type_initial_here')}...`"
type="text"
@focus="$emit('focus')"

@ -35,7 +35,7 @@
:placeholder="t('email')"
type="email"
:required="submitters.includes(submitter)"
:autofocus="index === 0"
autofocus="true"
name="submission[submitters][][email]"
>
</div>

@ -1,7 +1,7 @@
<template>
<label
v-if="showFieldNames && (field.name || field.title)"
:id="field.uuid + '-group-label'"
:for="field.uuid"
dir="auto"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
:class="{ 'mb-2': !field.description }"
@ -44,8 +44,6 @@
<div
class="space-y-3.5 mx-auto"
:class="{ hidden: !showOptions }"
role="group"
:aria-labelledby="showFieldNames && (field.name || field.title) ? field.uuid + '-group-label' : undefined"
>
<div
v-for="(option, index) in field.options"

@ -81,7 +81,6 @@
<select
id="country_code_select"
class="absolute top-0 bottom-0 right-0 left-0 opacity-0 w-full h-full cursor-pointer"
:aria-label="t('country_code')"
:disabled="!!defaultValue"
@change="onCountrySelect(countries.find((country) => country.flag === $event.target.value))"
>

@ -27,7 +27,6 @@
>
<button
id="type_text_button"
ref="typeTextButton"
type="button"
:aria-label="t('draw_signature')"
class="btn btn-outline btn-sm font-medium type-text-button"
@ -50,7 +49,6 @@
>
<button
id="type_text_button"
ref="typeTextButton"
type="button"
:aria-label="t('type_text')"
class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap type-text-button"
@ -73,7 +71,7 @@
>
<button
type="button"
:aria-label="t('upload')"
:aria-label="t('take_photo')"
class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap upload-image-button"
@click="$refs.takePhotoInput.click()"
>
@ -201,7 +199,7 @@
<canvas
v-show="!modelValue && !computedPreviousValue"
ref="canvas"
role="img"
role="application"
:aria-label="t('signature_drawing_area')"
style="padding: 1px; 0"
class="bg-white border border-base-300 rounded-2xl w-full draw-canvas"
@ -230,8 +228,6 @@
>
<canvas
ref="qrCanvas"
role="img"
:aria-label="t('scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature')"
class="h-full"
width="132"
height="132"
@ -246,7 +242,6 @@
ref="textInput"
class="base-input !text-2xl w-full mt-6"
:required="field.required && !isSignatureStarted"
:aria-label="field.name || t('signature')"
:placeholder="`${t('type_signature_here')}...`"
type="text"
@input="updateWrittenSignature"
@ -256,7 +251,6 @@
class="select base-input !text-2xl w-full mt-6 text-center"
:class="{ 'text-gray-300': !reason }"
required
:aria-label="t('select_a_reason')"
:name="`values[${field.preferences.reason_field_uuid}]`"
@change="$event.target.value === 'other' ? [reason = '', isOtherReason = true] : $emit('update:reason', $event.target.value)"
>
@ -302,7 +296,6 @@
class="base-input !text-2xl w-full mt-6"
required
:name="`values[${field.preferences.reason_field_uuid}]`"
:aria-label="t('select_a_reason')"
:placeholder="t('type_here_')"
:value="reason"
type="text"
@ -723,12 +716,10 @@ export default {
this.clear()
this.isTextSignature = !this.isTextSignature
this.$nextTick(() => {
if (this.isTextSignature) {
if (this.isTextSignature) {
this.$nextTick(() => {
if (this.$refs.textInput) {
if (this.submitter.name) {
this.$refs.typeTextButton?.focus()
} else {
if (!this.submitter.name) {
this.$refs.textInput.focus()
}
@ -736,10 +727,8 @@ export default {
this.$emit('start')
}
} else {
this.$refs.typeTextButton?.focus()
}
})
})
}
},
async initTypedSignature () {
if (this.signatureText) {

@ -726,10 +726,6 @@ export default {
return
}
if (this.inputMode && (this.isValueInput || this.isCheckboxInput || this.isSelectInput)) {
return
}
document.activeElement?.blur()
e.preventDefault()

@ -42,7 +42,6 @@
class="absolute w-5 h-5 -ml-2.5 -mt-2.5 rounded-full bg-white border-2 border-neutral-600 cursor-move shadow"
:style="{ left: `${corner.x * 100}%`, top: `${corner.y * 100}%` }"
@mousedown.prevent="onCornerMousedown(cornerIndex)"
@touchstart.prevent="onCornerTouchstart(cornerIndex)"
/>
</div>
</div>
@ -219,9 +218,6 @@ export default {
beforeUnmount () {
window.removeEventListener('mousemove', this.onMousemove)
window.removeEventListener('mouseup', this.onMouseup)
window.removeEventListener('touchmove', this.onTouchmove)
window.removeEventListener('touchend', this.onTouchend)
window.removeEventListener('touchcancel', this.onTouchend)
},
methods: {
transformPoint (point, rotate, flipH, flipV) {
@ -280,48 +276,27 @@ export default {
y: Math.min(Math.max((event.clientY - rect.top) / rect.height, 0), 1)
}
},
startCornerDrag (index) {
onCornerMousedown (index) {
this.draggingIndex = index
this.cornersTouched = true
window.addEventListener('mousemove', this.onMousemove)
window.addEventListener('mouseup', this.onMouseup, { once: true })
},
dragCorner (point) {
onMousemove (event) {
if (this.draggingIndex === null) {
return
}
this.corners[this.draggingIndex] = this.inverseTransformPoint(this.pagePoint(point), this.rotate, this.flipH, this.flipV)
},
onCornerMousedown (index) {
this.startCornerDrag(index)
const point = this.inverseTransformPoint(this.pagePoint(event), this.rotate, this.flipH, this.flipV)
window.addEventListener('mousemove', this.onMousemove)
window.addEventListener('mouseup', this.onMouseup, { once: true })
},
onMousemove (event) {
this.dragCorner(event)
this.corners[this.draggingIndex] = point
},
onMouseup () {
window.removeEventListener('mousemove', this.onMousemove)
this.draggingIndex = null
},
onCornerTouchstart (index) {
this.startCornerDrag(index)
window.addEventListener('touchmove', this.onTouchmove, { passive: false })
window.addEventListener('touchend', this.onTouchend)
window.addEventListener('touchcancel', this.onTouchend)
},
onTouchmove (event) {
this.dragCorner(event.touches[0])
},
onTouchend () {
window.removeEventListener('touchmove', this.onTouchmove)
window.removeEventListener('touchend', this.onTouchend)
window.removeEventListener('touchcancel', this.onTouchend)
this.draggingIndex = null
},
submit (scan) {
this.isProcessing = scan ? 'scan' : 'crop'

@ -6,7 +6,6 @@
class="relative mx-auto select-none cursor-crosshair"
:style="pageStyle"
@mousedown.prevent="onMousedown"
@touchstart.prevent="onTouchstart"
>
<img
:src="imageUrl"
@ -252,9 +251,6 @@ export default {
beforeUnmount () {
window.removeEventListener('mousemove', this.onMousemove)
window.removeEventListener('mouseup', this.onMouseup)
window.removeEventListener('touchmove', this.onTouchmove)
window.removeEventListener('touchend', this.onTouchend)
window.removeEventListener('touchcancel', this.onTouchend)
},
methods: {
inverseRotatePoint (point, rotate) {
@ -361,61 +357,27 @@ export default {
y: Math.min(Math.max((event.clientY - rect.top) / rect.height, 0), 1)
}
},
startMarquee (point) {
const start = this.pagePoint(point)
this.marquee = { x1: start.x, y1: start.y, x2: start.x, y2: start.y }
},
updateMarquee (point) {
if (!this.marquee) {
return
}
const next = this.pagePoint(point)
this.marquee.x2 = next.x
this.marquee.y2 = next.y
},
onMousedown (event) {
if (event.button !== 0 || (!this.imagePage && !this.textNodes)) {
return
}
this.startMarquee(event)
const point = this.pagePoint(event)
this.marquee = { x1: point.x, y1: point.y, x2: point.x, y2: point.y }
window.addEventListener('mousemove', this.onMousemove)
window.addEventListener('mouseup', this.onMouseup, { once: true })
},
onMousemove (event) {
this.updateMarquee(event)
const point = this.pagePoint(event)
this.marquee.x2 = point.x
this.marquee.y2 = point.y
},
onMouseup () {
window.removeEventListener('mousemove', this.onMousemove)
this.finishMarquee()
},
onTouchstart (event) {
if (!this.imagePage && !this.textNodes) {
return
}
this.startMarquee(event.touches[0])
window.addEventListener('touchmove', this.onTouchmove, { passive: false })
window.addEventListener('touchend', this.onTouchend)
window.addEventListener('touchcancel', this.onTouchend)
},
onTouchmove (event) {
this.updateMarquee(event.touches[0])
},
onTouchend () {
window.removeEventListener('touchmove', this.onTouchmove)
window.removeEventListener('touchend', this.onTouchend)
window.removeEventListener('touchcancel', this.onTouchend)
this.finishMarquee()
},
finishMarquee () {
if (!this.marquee) {
return
}

@ -97,7 +97,7 @@ class SubmitterMailer < ApplicationMailer
to: user.role == 'integration' ? user.friendly_name.sub(/\+\w+@/, '@') : user.friendly_name,
reply_to: @submitter.friendly_name,
subject: I18n.t(:name_declined_by_submitter,
name: (@submission.name || @submission.template&.name).to_s.truncate(20),
name: (@submission.name || @submission.template.name).truncate(20),
submitter: @submitter.name || @submitter.email || @submitter.phone))
end
end
@ -144,7 +144,6 @@ class SubmitterMailer < ApplicationMailer
end
def otp_verification_email(submitter, locale: nil)
@current_account = submitter.account
@submitter = submitter
@otp_code = EmailVerificationCodes.generate([submitter.email.downcase.strip, submitter.slug].join(':'))

@ -107,11 +107,6 @@ class Submission < ApplicationRecord
.and(Submitter.arel_table[:completed_at].eq(nil))).select(1).arel.exists)
}
scope :select_for_list, lambda {
select(:id, :name, :created_by_user_id, :account_id,
:created_at, :archived_at, :expire_at, :template_id, :template_submitters)
}
enum :source, {
invite: 'invite',
bulk: 'bulk',

@ -28,8 +28,8 @@
#
# Indexes
#
# index_submitters_on_account_id_and_completed_at (account_id,completed_at) WHERE (completed_at IS NOT NULL)
# index_submitters_on_account_id_and_id (account_id,id)
# index_submitters_on_completed_at_and_account_id (completed_at,account_id)
# index_submitters_on_email (email)
# index_submitters_on_external_id (external_id)
# index_submitters_on_slug (slug) UNIQUE

@ -81,8 +81,6 @@ class Template < ApplicationRecord
scope :active, -> { where(archived_at: nil) }
scope :archived, -> { where.not(archived_at: nil) }
scope :select_for_list, -> { select(:id, :name, :author_id, :account_id, :created_at, :archived_at, :folder_id) }
def application_key
external_id
end

@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M10 19h-5a2 2 0 0 1 -2 -2v-11a2 2 0 0 1 2 -2h4l3 3h7a2 2 0 0 1 2 2v2.5"></path>
<path d="M17.8 20.817l-2.172 1.138a.392 .392 0 0 1 -.568 -.41l.415 -2.411l-1.757 -1.707a.389 .389 0 0 1 .217 -.665l2.428 -.352l1.086 -2.193a.392 .392 0 0 1 .702 0l1.086 2.193l2.428 .352a.39 .39 0 0 1 .217 .665l-1.757 1.707l.414 2.41a.39 .39 0 0 1 -.567 .411l-2.172 -1.138"></path>
</svg>

Before

Width:  |  Height:  |  Size: 662 B

@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 19h-7a2 2 0 0 1 -2 -2v-11a2 2 0 0 1 2 -2h4l3 3h7a2 2 0 0 1 2 2v3.5"></path>
<path d="M19 22v-6"></path>
<path d="M22 19l-3 -3l-3 3"></path>
</svg>

Before

Width:  |  Height:  |  Size: 447 B

@ -70,7 +70,7 @@
<div class="form-control">
<%= f.label :current_password, t('current_password'), class: 'label' %>
<%= f.password_field :current_password, autocomplete: 'current-password', class: 'base-input' %>
<% if Docuseal.multitenant? || Accounts.can_send_emails?(current_account) %>
<% if Accounts.can_send_emails?(current_account) %>
<span class="label-text-alt mt-1">
<%= t('dont_remember_your_current_password_click_here_to_reset_it_html') %>
</span>

@ -4,9 +4,6 @@
<input name="<%= key %>" value="<%= params[key] %>" class="hidden">
<% end %>
<% end %>
<% if params[:archived].present? %>
<input name="archived" value="<%= params[:archived] %>" class="hidden">
<% end %>
<% if params[:q].present? %>
<div class="relative">
<a href="<%= url_for(params: request.query_parameters.except('q')) %>" title="<%= t('clear') %>" class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-auto text-neutral text-2xl font-extralight">

@ -10,7 +10,7 @@
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
</div>
<div>
<p dir="auto" class="text-lg font-bold mb-1"><%= @submitter.submission.template&.name %></p>
<p dir="auto" class="text-lg font-bold mb-1"><%= @submitter.submission.template.name %></p>
<p dir="auto" class="text-sm">
<%= t(@submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(@submitter.completed_at.to_date, format: :long)) %>
</p>

@ -91,8 +91,8 @@
</dynamic-list>
<%= local_assigns[:variables_form] %>
<div>
<%= render('submitters_order', f:, template:) if can_send_emails %>
<%= render 'send_email', f:, template:, can_send_emails: %>
<%= render('submitters_order', f:, template:) if Accounts.can_send_emails?(current_account) %>
<%= render 'send_email', f:, template: %>
<% if has_phone_field %>
<%= render 'send_sms', f: %>
<% end %>

@ -69,8 +69,8 @@
<% end %>
<%= local_assigns[:variables_form] %>
<div>
<%= render('submitters_order', f:, template:) if can_send_emails %>
<%= render 'send_email', f:, template:, can_send_emails: %>
<%= render('submitters_order', f:, template:) if Accounts.can_send_emails?(current_account) %>
<%= render 'send_email', f:, template: %>
<%= render 'extra_fields', f: %>
</div>
<div class="form-control">

@ -56,7 +56,7 @@
</dynamic-list>
<%= local_assigns[:variables_form] %>
<div>
<%= render 'submitters_order', f:, template: %>
<%= render('submitters_order', f:, template:) if Accounts.can_send_emails?(current_account) %>
<%= render 'send_sms', f:, checked: true %>
<%= render 'extra_phone_fields', f: %>
</div>

@ -2,14 +2,15 @@
<% template_submitters = local_assigns[:submitter]&.submission&.template_submitters || template.submitters %>
<% message_field_id = "message_field_#{SecureRandom.hex(3)}" %>
<div class="form-control">
<% can_send_emails = Accounts.can_send_emails?(current_account) %>
<div class="flex justify-between items-center">
<%= f.label :send_email, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %>
<%= f.check_box :send_email, id: uuid, class: 'base-checkbox', disabled: !can_send_emails || local_assigns[:disable_email], checked: can_send_emails && !local_assigns.key?(:resend_email) && !local_assigns[:disable_email] && template&.preferences&.dig('request_email_enabled') != false %>
<span class="label"><%= local_assigns[:resend_email] ? t('re_send_email') : t('send_email') %></span>
<% end %>
<div>
<%= render 'submissions/email_stats' %>
<% if can_send_emails %>
<%= render 'submissions/email_stats' %>
<%= content_for(:edit_button) || capture do %>
<toggle-visible data-element-ids="<%= [message_field_id].to_json %>" class="flex">
<label>

@ -1,11 +1,9 @@
<% if template.preferences['submitters_order'] == 'preserved' || template.submitters.any? { |s| s['order'] } || (template.submitters.size > 1 && template.fields.any? { |f| f['type'] == 'verification' }) %>
<%= f.hidden_field :preserve_order, value: '1' %>
<% elsif template.submitters.size > 1 %>
<% account_submissions = template.submissions.where(account_id: current_account.id) %>
<% last_submission = account_submissions.active.last || account_submissions.archived.last %>
<div class="form-control">
<%= f.label :preserve_order, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %>
<%= f.check_box :preserve_order, id: uuid, class: 'base-checkbox', checked: last_submission&.submitters_order.in?(['preserved', nil]) %>
<%= f.check_box :preserve_order, id: uuid, class: 'base-checkbox', checked: template.submissions.last&.submitters_order.in?(['preserved', nil]) %>
<span class="label"><%= t('preserve_order') %></span>
<span class="tooltip" data-tip="<%= t('when_checked_notifications_will_be_sent_to_the_second_party_once_the_form_is_completed_by_the_previous_party_uncheck_this_option_to_send_notifications_to_all_parties_simultaneously_right_away') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>

@ -3,7 +3,6 @@
<% prefillable_fields = @template.fields.select { |f| f['prefillable'] } %>
<% default_tab = cookies.permanent[:add_recipients_tab].presence || 'email' %>
<% recipient_form_fields = Accounts.load_recipient_form_fields(current_account) if prefillable_fields.blank? %>
<% can_send_emails = Accounts.can_send_emails?(current_account) %>
<% only_detailed = require_phone_2fa || require_email_2fa || prefillable_fields.present? || recipient_form_fields.present? %>
<% with_list = @template.variables_schema.blank? %>
<% variables_form = render 'variables_form', schema: @template.variables_schema if @template.variables_schema.present? && @template.variables_schema.any? { |_, v| !v['disabled'] } %>
@ -26,18 +25,18 @@
<div class="px-5 mb-5 mt-4">
<% unless only_detailed %>
<div id="email" class="<%= 'hidden' if default_tab != 'email' %>">
<%= render 'email_form', template: @template, variables_form:, can_send_emails: %>
<%= render 'email_form', template: @template, variables_form: %>
</div>
<div id="phone" class="<%= 'hidden' if default_tab != 'phone' %>">
<%= render 'phone_form', template: @template, variables_form: %>
</div>
<% end %>
<div id="detailed" class="<%= 'hidden' if !only_detailed && default_tab != 'detailed' %>">
<%= render 'detailed_form', template: @template, require_phone_2fa:, require_email_2fa:, prefillable_fields:, recipient_form_fields:, variables_form:, can_send_emails: %>
<%= render 'detailed_form', template: @template, require_phone_2fa:, require_email_2fa:, prefillable_fields:, recipient_form_fields:, variables_form: %>
</div>
<% if with_list %>
<div id="list" class="hidden">
<%= render 'list_form', template: @template, can_send_emails: %>
<%= render 'list_form', template: @template %>
</div>
<% end %>
<%= render 'submissions/error' %>

@ -12,7 +12,7 @@
<div class="flex justify-between py-1.5 items-center pr-4 sticky top-0 md:relative z-10 bg-base-100">
<a href="<%= signed_in? && @submission.account_id == current_account&.id && @submission.template ? template_path(@submission.template) : '/' %>" class="flex items-center space-x-3 py-1">
<span><%= render 'submissions/logo' %></span>
<h1 class="text-xl md:text-3xl font-semibold focus:text-clip" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;"><% (@submission.name || @submission.template&.name).to_s.split(/(_)/).each do |item| %><%= item %><wbr><% end %></h1>
<h1 class="text-xl md:text-3xl font-semibold focus:text-clip" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;"><% (@submission.name || @submission.template.name).split(/(_)/).each do |item| %><%= item %><wbr><% end %></h1>
</a>
<div class="space-x-3 flex items-center">
<% is_all_completed = @submission.submitters.to_a.all?(&:completed_at?) %>
@ -232,7 +232,7 @@
</span>
</div>
<% end %>
<% if signed_in? && submitter && submitter.email && !submitter.completed_at && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:update, @submission) && (Docuseal.multitenant? || Accounts.can_send_emails?(current_account)) && !@submission.expired? && !submitter.declined_at? %>
<% if signed_in? && submitter && submitter.email && !submitter.completed_at && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:update, @submission) && Accounts.can_send_emails?(current_account) && !@submission.expired? && !submitter.declined_at? %>
<div class="mt-2 mb-1">
<%= button_to button_title(title: submitter.sent_at? ? t('re_send_email') : t('send_email'), disabled_with: t('sending')), submitter_send_email_index_path(submitter), class: 'btn btn-sm btn-primary w-full' %>
</div>

@ -11,7 +11,7 @@
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
</div>
<div>
<p dir="auto" class="text-lg font-bold mb-1"><%= @submission.name || @submission.template&.name %></p>
<p dir="auto" class="text-lg font-bold mb-1"><%= @submission.name || @submission.template.name %></p>
<% last_submitter = @submission.submitters.completed.order(:completed_at).last %>
<% if last_submitter %>
<p dir="auto" class="text-sm">

@ -2,4 +2,4 @@
<% data_fields = Submissions.filtered_conditions_fields(submitter).to_json %>
<% invite_submitters = (submitter.submission.template_submitters || submitter.submission.template.submitters).select { |s| s['invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } }.to_json %>
<% optional_invite_submitters = (submitter.submission.template_submitters || submitter.submission.template.submitters).select { |s| s['optional_invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } }.to_json %>
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-schema="<%= schema.to_json %>" data-reuse-signature="<%= configs[:reuse_signature] %>" data-require-signing-reason="<%= configs[:require_signing_reason] %>" data-with-signature-id="<%= configs[:with_signature_id] %>" data-with-field-labels="<%= configs[:with_field_labels] %>" data-with-confetti="<%= configs[:with_confetti] %>" data-completed-redirect-url="<%= submitter.preferences['completed_redirect_url'].presence || submitter.submission.template&.preferences&.dig('completed_redirect_url') %>" data-completed-message="<%= (configs[:completed_message]&.compact_blank.presence || submitter.submission.template&.preferences&.dig('completed_message') || {}).to_json %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.preferences.key?('go_to_last') ? submitter.preferences['go_to_last'] : submitter.opened_at? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Docuseal.multitenant? || Accounts.can_send_emails?(submitter.submission.account) %>" data-optional-invite-submitters="<%= optional_invite_submitters %>" data-invite-submitters="<%= invite_submitters %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>" data-signature-text="<%= params[:signature] %>" data-previous-signature-value="<%= local_assigns[:signature_attachment]&.uuid %>" data-remember-signature="<%= configs[:prefill_signature] %>" data-dry-run="<%= local_assigns[:dry_run] %>" data-expand="<%= local_assigns[:expand] %>" data-scroll-padding="<%= local_assigns[:scroll_padding] %>" data-language="<%= I18n.locale.to_s.split('-').first %>"></submission-form>
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-schema="<%= schema.to_json %>" data-reuse-signature="<%= configs[:reuse_signature] %>" data-require-signing-reason="<%= configs[:require_signing_reason] %>" data-with-signature-id="<%= configs[:with_signature_id] %>" data-with-field-labels="<%= configs[:with_field_labels] %>" data-with-confetti="<%= configs[:with_confetti] %>" data-completed-redirect-url="<%= submitter.preferences['completed_redirect_url'].presence || submitter.submission.template&.preferences&.dig('completed_redirect_url') %>" data-completed-message="<%= (configs[:completed_message]&.compact_blank.presence || submitter.submission.template&.preferences&.dig('completed_message') || {}).to_json %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.preferences.key?('go_to_last') ? submitter.preferences['go_to_last'] : submitter.opened_at? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(submitter.submission.account) %>" data-optional-invite-submitters="<%= optional_invite_submitters %>" data-invite-submitters="<%= invite_submitters %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>" data-signature-text="<%= params[:signature] %>" data-previous-signature-value="<%= local_assigns[:signature_attachment]&.uuid %>" data-remember-signature="<%= configs[:prefill_signature] %>" data-dry-run="<%= local_assigns[:dry_run] %>" data-expand="<%= local_assigns[:expand] %>" data-scroll-padding="<%= local_assigns[:scroll_padding] %>" data-language="<%= I18n.locale.to_s.split('-').first %>"></submission-form>

@ -10,7 +10,7 @@
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
</div>
<div dir="auto">
<p class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template&.name %></p>
<p class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template.name %></p>
<p class="text-sm"><%= t('form_has_been_deleted_by_html', name: @submitter.account.name) %></p>
</div>
</div>

@ -10,7 +10,7 @@
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
</div>
<div dir="auto">
<p class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template&.name %></p>
<p class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template.name %></p>
<p class="text-sm"><%= t('awaiting_completion_by_the_other_party') %></p>
</div>
</div>

@ -10,7 +10,7 @@
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
</div>
<div>
<p dir="auto" class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template&.name %></p>
<p dir="auto" class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template.name %></p>
<p dir="auto" class="text-sm">
<%= t(@submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(@submitter.completed_at.to_date, format: :long)) %>
</p>

@ -10,7 +10,7 @@
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
</div>
<div dir="auto">
<p class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template&.name %></p>
<p class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template.name %></p>
<p class="text-sm"><%= t('form_has_been_declined_on_html', time: l(@submitter.declined_at, format: :long)) %></p>
</div>
</div>

@ -10,7 +10,7 @@
<%= svg_icon('user_share', class: 'w-10 h-10') %>
</div>
<div dir="auto">
<p class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template&.name %></p>
<p class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template.name %></p>
<p class="text-sm"><%= t('document_has_been_delegated_on_html', time: l(@submitter.submission_events.order(event_timestamp: :desc).find_by!(event_type: :delegate_form).event_timestamp, format: :long)) %></p>
</div>
</div>

@ -1,4 +1,4 @@
<% content_for(:html_title, "#{@submitter.submission.name || @submitter.submission.template&.name} | DocuSeal") %>
<% content_for(:html_title, "#{@submitter.submission.name || @submitter.submission.template.name} | DocuSeal") %>
<% I18n.with_locale(@submitter.account.locale) do %>
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @submitter.account.name)) %>
<% end %>
@ -14,7 +14,7 @@
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
</div>
<div>
<p dir="auto" class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template&.name %></p>
<p dir="auto" class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template.name %></p>
<% last_submitter = @submitter.submission.submitters.completed.order(:completed_at).last %>
<% if last_submitter %>
<p dir="auto" class="text-sm">

@ -10,7 +10,7 @@
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
</div>
<div dir="auto">
<p class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template&.name %></p>
<p class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template.name %></p>
<p class="text-sm"><%= t('form_expired_at_html', time: l(@submitter.submission.expire_at, format: :long)) %></p>
</div>
</div>

@ -1,4 +1,4 @@
<% content_for(:html_title, "#{@submitter.submission.name || @submitter.submission.template&.name} | DocuSeal") %>
<% content_for(:html_title, "#{@submitter.submission.name || @submitter.submission.template.name} | DocuSeal") %>
<% I18n.with_locale(@submitter.account.locale) do %>
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @submitter.account.name)) %>
<% end %>
@ -20,7 +20,7 @@
<%= render('submit_form/banner') %>
<header id="signing_form_header" class="sticky min-[1230px]:static top-0 z-50 bg-base-100 py-2 px-2 flex items-center md:-mx-[8px]" style="margin-bottom: -16px">
<h1 class="text-xl md:text-2xl font-medium focus:text-clip" style="width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
<%= @submitter.submission.name || @submitter.submission.template&.name %>
<%= @submitter.submission.name || @submitter.submission.template.name %>
</h1>
<div class="flex items-center gap-2 group" style="margin-left: 20px; flex-shrink: 0">
<% if @form_configs[:with_decline] %>

@ -2,6 +2,6 @@
<%= render 'custom_content', content: @body, submitter: @submitter %>
<% else %>
<p><%= t('hi_there') %>,</p>
<p><%= I18n.t(:name_has_been_completed_by_submitters, name: @submitter.submission.name || @submitter.submission.template&.name, submitters: @submitter.submission.submitters.order(:completed_at).map { |e| e.name || e.email || e.phone }.uniq.join(', ')) %></p>
<p><%= I18n.t(:name_has_been_completed_by_submitters, name: @submitter.submission.name || @submitter.submission.template.name, submitters: @submitter.submission.submitters.order(:completed_at).map { |e| e.name || e.email || e.phone }.uniq.join(', ')) %></p>
<p><%= link_to submission_url(@submitter.submission), submission_url(@submitter.submission) %></p>
<% end %>

@ -1,4 +1,4 @@
<p><%= t('hi_there') %>,</p>
<p><%= t('name_declined_by_submitter_with_the_following_reason', name: @submitter.submission.name || @submitter.submission.template&.name, submitter: @submitter.name || @submitter.email || @submitter.phone) %></p>
<p><%= t('name_declined_by_submitter_with_the_following_reason', name: @submitter.submission.name || @submitter.submission.template.name, submitter: @submitter.name || @submitter.email || @submitter.phone) %></p>
<%= simple_format(h(@submitter.submission_events.find_by(event_type: :decline_form).data['reason']), {}, sanitize: false) %>
<p><%= link_to submission_url(@submitter.submission), submission_url(@submitter.submission) %></p>

@ -2,10 +2,10 @@
<%= render 'custom_content', content: @body, submitter: @submitter, sig: @sig %>
<% else %>
<p><%= t('hi_there') %>,</p>
<p><%= t('please_check_the_copy_of_your_name_in_the_email_attachments', name: @submitter.submission.name || @submitter.submission.template&.name) %>
<p><%= t('please_check_the_copy_of_your_name_in_the_email_attachments', name: @submitter.submission.name || @submitter.submission.template.name) %>
<p><%= t('alternatively_you_can_review_and_download_your_copy_using_the_link_below') %></p>
<p>
<%= link_to @submitter.submission.name || @submitter.submission.template&.name, submissions_preview_url(@submitter.submission.slug, { sig: @sig, host: @custom_domain || ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host]) }.compact) %>
<%= link_to @submitter.submission.name || @submitter.submission.template.name, submissions_preview_url(@submitter.submission.slug, { sig: @sig, host: @custom_domain || ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host]) }.compact) %>
</p>
<p>
<%= t('thanks') %>,<br><%= @current_account.name %>

@ -5,7 +5,7 @@
<% end %>
<% else %>
<p><%= t('hi_there') %>,</p>
<p><%= I18n.t(@submitter.with_signature_fields? ? :you_have_been_invited_to_sign_the_name : :you_have_been_invited_to_submit_the_name_form, name: @submitter.submission.name || @submitter.submission.template&.name) %></p>
<p><%= I18n.t(@submitter.with_signature_fields? ? :you_have_been_invited_to_sign_the_name : :you_have_been_invited_to_submit_the_name_form, name: @submitter.submission.name || @submitter.submission.template.name) %></p>
<p><%= link_to I18n.t(@submitter.with_signature_fields? ? :review_and_sign : :review_and_submit), submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email'), host: @custom_domain || ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host])) %></p>
<p><%= t('please_contact_us_by_replying_to_this_email_if_you_have_any_questions') %></p>
<p>

@ -1,3 +1,3 @@
<p><%= t('your_verification_code_to_access_the_name', name: @submitter.submission.name || @submitter.submission.template&.name) %></p>
<p><%= t('your_verification_code_to_access_the_name', name: @submitter.submission.name || @submitter.submission.template.name) %></p>
<p><b><%= @otp_code %></b></p>
<p><%= t('please_reply_to_this_email_if_you_didnt_request_this') %></p>

@ -17,7 +17,7 @@
</submitter-item>
</div>
<div>
<%= render 'submissions/send_email', f:, template: @submitter.template, submitter: @submitter, resend_email: @submitter.sent_at?, submitter_email_message: @submitter_email_message, disable_save_as_default_template_option: true, message_per_submitter: false, can_send_emails: Accounts.can_send_emails?(current_account) %>
<%= render 'submissions/send_email', f:, template: @submitter.template, submitter: @submitter, resend_email: @submitter.sent_at?, submitter_email_message: @submitter_email_message, disable_save_as_default_template_option: true, message_per_submitter: false %>
<%= render 'submissions/send_sms', f:, resend_sms: @submitter.sent_at? %>
</div>
<div class="form-control mt-4">

@ -1,12 +1,11 @@
<% is_long = folder.name.size > 32 %>
<% icon = folder.default? ? 'folder_star' : 'folder' %>
<a href="<%= folder_path(folder) %>" class="flex h-full flex-col justify-between rounded-2xl py-5 px-6 w-full bg-base-200 before:border-2 before:border-base-300 before:border-dashed before:absolute before:left-0 before:right-0 before:top-0 before:bottom-0 before:hidden before:rounded-2xl relative" data-targets="dashboard-dropzone.folderCards" data-full-name="<%= folder.full_name %>">
<% if !is_long %>
<%= svg_icon(icon, class: 'w-6 h-6') %>
<%= svg_icon('folder', class: 'w-6 h-6') %>
<% end %>
<div class="text-lg font-semibold mt-1" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: <%= is_long ? 2 : 1 %>;">
<% if is_long %>
<%= svg_icon(icon, class: 'w-6 h-6 inline') %>
<%= svg_icon('folder', class: 'w-6 h-6 inline') %>
<% end %>
<%= folder.name %>
</div>

@ -1,6 +0,0 @@
<a href="<%= templates_shared_index_path %>" class="flex h-full flex-col justify-between rounded-2xl py-5 px-6 w-full bg-base-200 before:border-2 before:border-base-300 before:border-dashed before:absolute before:left-0 before:right-0 before:top-0 before:bottom-0 before:hidden before:rounded-2xl relative">
<%= svg_icon('folder_up', class: 'w-6 h-6') %>
<div class="text-lg font-semibold mt-1 capitalize" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 1;">
<%= t('shared') %>
</div>
</a>

@ -30,7 +30,7 @@
<% end %>
</h1>
<div class="flex space-x-2">
<% if params[:q].present? || @pagy.count.nil? || @pagy.count > 0 || @template_folders.present? %>
<% if params[:q].present? || @pagy.pages > 1 || @template_folders.present? %>
<%= render 'shared/search_input' %>
<% end %>
<% if can?(:create, ::Template) %>
@ -54,8 +54,8 @@
<%= render partial: 'templates/template', collection: @templates %>
</div>
<% templates_order_select_html = capture do %>
<% if params[:q].blank? && @pagy.pages > 1 && !can?(:manage, :countless) %>
<%= render 'shared/templates_order_select', with_recently_used: @pagy.count.present? && @pagy.count < 10_000, selected_order: %>
<% if params[:q].blank? && @pagy.pages > 1 %>
<%= render 'shared/templates_order_select', with_recently_used: @pagy.count.present? && @pagy.count < 10_000 && !can?(:manage, :countless), selected_order: %>
<% end %>
<% end %>
<%= render 'shared/pagination', pagy: @pagy, items_name: @templates.present? ? 'templates' : 'template_folders', right_additional_html: templates_order_select_html %>

@ -7,7 +7,7 @@
<div class="font-medium items-start w-full group-hover:link text-sm flex space-x-1">
<%= svg_icon('file_text', class: 'w-4 h-4 mt-0.5 flex-shrink-0') %>
<span style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;">
<% (submission.name || template&.name).to_s.split(/(_)/).each do |item| %><%= item %><wbr><% end %>
<% (submission.name || template.name).split(/(_)/).each do |item| %><%= item %><wbr><% end %>
<%= svg_icon('arrow_right', class: 'w-4 h-4 sm:inline group-hover:visible invisible hidden') %>
</span>
</div>
@ -51,7 +51,7 @@
<a href="<%= submission_path(submission) %>" class="text-lg break-all peer">
<%= submitter.name || submitter.email || submitter.phone %>
</a>
<% if !submitter.completed_at? && can?(:update, submission) && !submitter.start_form_submission_events.any? && !submission.archived_at? && !submission.expired? && !submitter.declined_at? %>
<% if can?(:update, submission) && !submitter.start_form_submission_events.any? && !submission.archived_at? && !submission.expired? && !submitter.declined_at? %>
<span class="pl-0.5 tooltip tooltip-top md:opacity-0 md:hover:opacity-100 md:peer-hover:opacity-100" data-tip="<%= t('edit') %>">
<%= link_to edit_submitter_path(submitter), class: 'shrink-0', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('pencil', class: 'w-5 h-5') %>
@ -144,7 +144,7 @@
<a href="<%= submission_path(submission) %>" class="text-lg break-all peer">
<%= submitter.name || submitter.email || submitter.phone %>
</a>
<% if !submitter.completed_at? && can?(:update, submission) && !submitter.start_form_submission_events.any? && !submission.archived_at? && !submission.expired? && !submitter.declined_at? %>
<% if can?(:update, submission) && !submitter.start_form_submission_events.any? && !submission.archived_at? && !submission.expired? && !submitter.declined_at? %>
<span class="pl-0.5 tooltip tooltip-top md:opacity-0 md:hover:opacity-100 md:peer-hover:opacity-100" data-tip="<%= t('edit') %>">
<%= link_to edit_submitter_path(submitter), class: 'shrink-0', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('pencil', class: 'w-5 h-5') %>

@ -11,7 +11,7 @@
<%= svg_icon('user', class: 'w-4 h-4') %>
<span><%= template.author.full_name.presence || template.author.email.to_s.sub(/\+\w+@/, '@') %></span>
<% if template.account_id != current_account.id %>
<span class="badge badge-neutral badge-outline badge-sm text-[10px] uppercase !border-base-content/60"><%= t('shared') %></span>
<span class="badge badge-neutral badge-outline badge-sm text-[10px] uppercase"><%= t('shared') %></span>
<% end %>
</p>
<p class="flex text-xs text-base-content/60">
@ -19,7 +19,7 @@
<%= svg_icon('calendar', class: 'w-4 h-4') %>
<span><%= l(template.created_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %></span>
</span>
<% if local_assigns[:with_folder] %>
<% if template.archived_at? %>
<span class="flex items-center space-x-1 w-1/2">
<%= svg_icon('folder', class: 'w-4 h-4 flex-shrink-0') %>
<span class="truncate"><%= template.folder.full_name %></span>
@ -37,14 +37,14 @@
</a>
</span>
<% end %>
<% if template.archived_at? && can?(:destroy, template) %>
<% if template.archived_at? && can?(:update, template) %>
<span class="tooltip tooltip-left" data-tip="<%= t('restore') %>">
<%= button_to template_restore_index_path(template), class: 'btn btn-xs hover:btn-outline bg-base-200 btn-circle' do %>
<%= svg_icon('rotate', class: 'w-4 h-4 enabled') %>
<%= svg_icon('loader', class: 'w-4 h-4 animate-spin disabled') %>
<% end %>
</span>
<% elsif !template.archived_at? && can?(:update, template) %>
<% elsif can?(:update, template) %>
<span class="tooltip tooltip-left" data-tip="<%= t('edit') %>">
<a href="<%= edit_template_path(template) %>" class="btn btn-xs hover:btn-outline bg-base-200 btn-circle">
<%= svg_icon('pencil', class: 'w-4 h-4') %>

@ -31,12 +31,9 @@
<% else %>
<div class="flex items-center justify-between">
<div class="flex items-center">
<a href="<%= templates_shared_index_path %>" class="flex items-center space-x-1 mt-1">
<%= svg_icon('folder', class: 'w-5 h-5 flex-shrink-0') %>
<span class="text-sm capitalize">
<%= t('shared') %>
</span>
</a>
<div class="flex items-center space-x-1 mt-1 peer">
<span class="badge badge-neutral badge-outline badge-md text-xs text-white uppercase"><%= t('shared') %></span>
</div>
</div>
</div>
<% end %>
@ -91,7 +88,7 @@
<% end %>
<% end %>
<% if template.archived_at? %>
<% if can?(:destroy, template) %>
<% if can?(:create, template) %>
<%= button_to button_title(title: t('restore'), disabled_with: t('restoring')[..-4], icon: svg_icon('rotate', class: 'w-6 h-6')), template_restore_index_path(template), class: 'btn btn-outline btn-sm flex-1' %>
<% end %>
<%= link_to template_preview_path(template), class: 'btn btn-outline btn-sm flex-1' do %>

@ -14,7 +14,7 @@
</div>
<% if @pagy.count.nil? || @pagy.count > 0 %>
<div class="grid gap-4 md:grid-cols-3">
<%= render partial: 'templates/template', collection: @templates, locals: { with_folder: true } %>
<%= render partial: 'templates/template', collection: @templates %>
</div>
<% elsif params[:q].present? %>
<div class="text-center">

@ -23,7 +23,7 @@
</h1>
</div>
<div class="flex space-x-2">
<% if params[:q].present? || @pagy.count.nil? || @pagy.count > 1 || @template_folders.present? %>
<% if params[:q].present? || @pagy.pages > 1 || @template_folders.present? %>
<%= render 'shared/search_input' %>
<% end %>
<% if can?(:create, ::Template) %>
@ -45,18 +45,12 @@
<% end %>
<% end %>
<% templates_order_select_html = capture do %>
<% if params[:q].blank? && @pagy.pages > 1 && !can?(:manage, :countless) %>
<%= render 'shared/templates_order_select', with_recently_used: @pagy.count.present? && @pagy.count < 10_000, selected_order: %>
<% if params[:q].blank? && @pagy.pages > 1 %>
<%= render 'shared/templates_order_select', with_recently_used: @pagy.count.present? && @pagy.count < 10_000 && !can?(:manage, :countless), selected_order: %>
<% end %>
<% end %>
<% if @template_folders.present? || @show_default_folder || @show_shared_folder %>
<% if @template_folders.present? %>
<div class="grid gap-4 md:grid-cols-3 <%= 'mb-6' if @templates.present? %>">
<% if @show_default_folder %>
<%= render 'template_folders/folder', folder: @default_folder %>
<% end %>
<% if @show_shared_folder %>
<%= render 'template_folders/shared_folder' %>
<% end %>
<%= render partial: 'template_folders/folder', collection: @template_folders, as: :folder %>
</div>
<% end %>

@ -1,54 +0,0 @@
<div>
<%= link_to(@is_archived ? templates_shared_index_path : root_path, class: 'flex items-center') do %>
<%= svg_icon('chevron_left', class: 'w-5 h-5') %>
<span style="margin-left: 3px"><%= @is_archived ? t('back_to_active') : t('home') %></span>
<% end %>
</div>
<div class="relative flex justify-between items-center w-full mb-4">
<h1 class="text-2xl truncate md:text-3xl font-bold flex items-center flex-grow min-w-0 space-x-2 md:flex <%= 'hidden' if params[:q].present? %>">
<%= svg_icon('folder', class: 'w-9 h-9 flex-shrink-0') %>
<span class="truncate capitalize"><%= t('shared') %></span>
<% if @is_archived %>
<span class="badge badge-outline badge-lg align-middle"><%= t('archived') %></span>
<% end %>
</h1>
<div class="flex space-x-2">
<% if params[:q].present? || @pagy.count.nil? || @pagy.count > 1 %>
<%= render 'shared/search_input' %>
<% end %>
</div>
</div>
<% view_archived_html = capture do %>
<% if @has_archived && params[:q].blank? %>
<div>
<a href="<%= templates_shared_index_path(archived: true) %>" class="link text-sm"><%= t('view_archived') %></a>
</div>
<% end %>
<% end %>
<% if @pagy.count.nil? || @pagy.count > 0 %>
<div class="grid gap-4 md:grid-cols-3">
<%= render partial: 'templates/template', collection: @templates %>
</div>
<% elsif params[:q].present? %>
<div class="text-center">
<div class="mt-16 text-3xl font-semibold">
<%= t('templates_not_found') %>
</div>
</div>
<% if @related_submissions.present? %>
<h1 class="text-2xl md:text-3xl sm:text-4xl font-bold mt-8 md:mt-4">
<%= t('submissions') %>
</h1>
<div class="space-y-4 mt-4">
<%= render partial: 'templates/submission', collection: @related_submissions, locals: { with_template: true } %>
</div>
<%= render 'shared/pagination', pagy: @related_submissions_pagy, items_name: 'submissions', next_page_path: @is_archived ? submissions_archived_index_path(q: params[:q]) : submissions_path(q: params[:q]) %>
<% end %>
<% end %>
<% if @pagy.pages > 1 %>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', left_additional_html: view_archived_html %>
<% else %>
<div class="mt-2">
<%= view_archived_html %>
</div>
<% end %>

@ -18,7 +18,7 @@
<span class="label-text-alt"><%= t('email_address_is_awaiting_confirmation_follow_the_link_in_the_email_to_confirm', email: f.object.unconfirmed_email) %></span>
</label>
<% end %>
<% if user.persisted? && (Docuseal.multitenant? || Accounts.can_send_emails?(current_account)) %>
<% if user.persisted? && Accounts.can_send_emails?(current_account) %>
<span class="label-text-alt mt-2 mx-1">
<%= t('click_here_to_send_a_reset_password_email_html') %>
</span>

@ -91,7 +91,6 @@ Rails.application.routes.draw do
resource :templates_upload, only: %i[show], path: 'new'
end
resources :templates_archived, only: %i[index], path: 'templates/archived'
resources :templates_shared, only: %i[index], path: 'templates/shared'
resources :folders, only: %i[show edit update destroy], controller: 'template_folders'
resources :template_sharings_testing, only: %i[create]
resources :templates, only: %i[index], controller: 'templates_dashboard'

@ -1,11 +0,0 @@
# frozen_string_literal: true
class RemoveSubmittersCompletedAtIndex < ActiveRecord::Migration[8.1]
def up
remove_index :submitters, %i[completed_at account_id], if_exists: true
end
def down
add_index :submitters, %i[completed_at account_id], if_not_exists: true
end
end

@ -1,11 +0,0 @@
# frozen_string_literal: true
class AddSubmittersCompletedAtPartialIndex < ActiveRecord::Migration[8.1]
def up
add_index :submitters, %i[account_id completed_at], where: 'completed_at IS NOT NULL', if_not_exists: true
end
def down
remove_index :submitters, %i[account_id completed_at], if_exists: true
end
end

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.1].define(version: 2026_06_27_083558) do
ActiveRecord::Schema[8.1].define(version: 2026_05_06_121640) do
# These are extensions that must be enabled in order to support this database
enable_extension "btree_gin"
enable_extension "pg_catalog.plpgsql"
@ -405,8 +405,8 @@ ActiveRecord::Schema[8.1].define(version: 2026_06_27_083558) do
t.datetime "updated_at", null: false
t.string "uuid", null: false
t.text "values", null: false
t.index ["account_id", "completed_at"], name: "index_submitters_on_account_id_and_completed_at", where: "(completed_at IS NOT NULL)"
t.index ["account_id", "id"], name: "index_submitters_on_account_id_and_id"
t.index ["completed_at", "account_id"], name: "index_submitters_on_completed_at_and_account_id"
t.index ["email"], name: "index_submitters_on_email"
t.index ["external_id"], name: "index_submitters_on_external_id"
t.index ["slug"], name: "index_submitters_on_slug", unique: true

@ -4,8 +4,16 @@ module Abilities
module TemplateConditions
module_function
def collection(user)
Template.where(account_id: user.account_id)
def collection(user, ability: nil)
templates = Template.where(account_id: user.account_id)
return templates unless user.account.testing?
shared_ids =
TemplateSharing.where({ ability:, account_id: [user.account_id, TemplateSharing::ALL_ID] }.compact)
.select(:template_id)
Template.where(Template.arel_table[:id].in(templates.select(:id).arel.union(:all, shared_ids.arel)))
end
def entity(template, user:, ability: nil)

@ -6,8 +6,12 @@ module EmailMessages
def find_or_create_for_account_user(account, user, subject, body)
subject = I18n.t(:you_are_invited_to_sign_a_document) if subject.blank?
message = account.email_messages.new(author: user, subject:, body:).tap(&:validate)
sha1 = Digest::SHA1.hexdigest({ subject:, body: }.to_json)
account.email_messages.find_by(sha1: message.sha1) || message.tap { |m| m.save!(validate: false) }
message = account.email_messages.find_by(sha1:)
message ||= account.email_messages.create!(author: user, subject:, body:)
message
end
end

@ -30,9 +30,9 @@ module Mcp
def call(arguments, _current_user, current_ability)
template = Template.accessible_by(current_ability).find_by(id: arguments['template_id'])
if !template || !current_ability.can?(:read, template)
return { content: [{ type: 'text', text: 'Template not found' }], isError: true }
end
return { content: [{ type: 'text', text: 'Template not found' }], isError: true } unless template
current_ability.authorize!(:read, template)
submitters_index = template.submitters.index_by { |s| s['uuid'] }

@ -69,13 +69,11 @@ module Mcp
module_function
# rubocop:disable Metrics
# rubocop:disable Metrics/MethodLength
def call(arguments, current_user, current_ability)
template = Template.accessible_by(current_ability).find_by(id: arguments['template_id'])
if !template || !current_ability.can?(:read, template)
return { content: [{ type: 'text', text: 'Template not found' }], isError: true }
end
return { content: [{ type: 'text', text: 'Template not found' }], isError: true } unless template
if template.archived_at?
return { content: [{ type: 'text', text: 'Template has been archived' }], isError: true }
@ -103,8 +101,8 @@ module Mcp
template:,
user: current_user,
source: :mcp,
submitters_order: template.preferences['submitters_order'].presence || 'random',
submissions_attrs: { submitters: },
submitters_order: 'random',
submissions_attrs: { submitters: submitters },
params: { 'send_email' => true, 'submitters' => submitters }
)
@ -143,7 +141,7 @@ module Mcp
rescue Submissions::CreateFromSubmitters::BaseError => e
{ content: [{ type: 'text', text: e.message }], isError: true }
end
# rubocop:enable Metrics
# rubocop:enable Metrics/MethodLength
end
end
end

@ -61,7 +61,7 @@ module Submissions
ActiveStorage::Attachment.create!(
blob: ActiveStorage::Blob.create_and_upload!(
io: io.tap(&:rewind), filename: "#{I18n.t('audit_log')} - " \
"#{submission.name || submission.template&.name}.pdf"
"#{submission.name || submission.template.name}.pdf"
),
name: 'audit_trail',
record: submission

@ -37,7 +37,7 @@ module Submissions
ActiveStorage::Attachment.create!(
blob: ActiveStorage::Blob.create_and_upload!(
io: io.tap(&:rewind), filename: "#{submission.name || submission.template&.name}.pdf"
io: io.tap(&:rewind), filename: "#{submission.name || submission.template.name}.pdf"
),
name: with_audit ? 'combined_document' : 'merged_document',
record: submission

@ -68,7 +68,7 @@ module Submissions
submission:,
values_hash:,
name: 'preview_merged_document',
filename: "#{submission.name || template&.name}.pdf"
filename: "#{submission.name || template.name}.pdf"
)
ApplicationRecord.no_touching { attachment.save! }
@ -112,7 +112,7 @@ module Submissions
submitter:,
uuid: GenerateResultAttachments.images_pdf_uuid(original_documents.select(&:image?)),
values_hash:,
filename: "#{submission.name || template&.name}.pdf"
filename: "#{submission.name || template.name}.pdf"
)
ApplicationRecord.no_touching do

@ -125,7 +125,7 @@ module Submissions
tsa_url:,
pkcs:,
uuid: images_pdf_uuid(original_documents.select(&:image?)),
name: submission.name || submission.template&.name
name: submission.name || submission.template.name
)
ApplicationRecord.no_touching do

@ -305,7 +305,9 @@ module Submitters
return unless blob
return blob if blob.attachments.take&.record&.account_id == account.id
return blob unless blob.attachments.exists?
return blob if account.submitters.exists?(id: blob.attachments.where(record_type: 'Submitter').select(:record_id))
nil
end

@ -47,21 +47,6 @@ module Templates
nil
end
def shared(current_user)
account = current_user.account
return Template.none if Docuseal.multitenant? ? !account.testing? : !account.linked_account_account
shared_account_ids = [current_user.account_id]
shared_account_ids << TemplateSharing::ALL_ID if !Docuseal.multitenant? && !account.testing?
exists_access = TemplateAccess.where(TemplateAccess.arel_table[:template_id].eq(Template.arel_table[:id]))
.select(1).arel.exists
Template.where(id: TemplateSharing.where(account_id: shared_account_ids).select(:template_id))
.where.not(exists_access)
end
def search(current_user, templates, keyword)
if Docuseal.fulltext_search?
fulltext_search(current_user, templates, keyword)
@ -70,21 +55,6 @@ module Templates
end
end
def search_shared(current_user, templates, keyword)
return templates if keyword.blank?
if Docuseal.fulltext_search?
templates.where(
id: SearchEntry.where(record_type: 'Template')
.where(account_id: current_user.account.linked_account_account&.account_id)
.where(*SearchEntries.build_tsquery(keyword))
.select(:record_id)
)
else
plain_search(templates, keyword)
end
end
def plain_search(templates, keyword)
return templates if keyword.blank?
@ -98,7 +68,8 @@ module Templates
templates.where(
id: SearchEntry.where(record_type: 'Template')
.where(account_id: current_user.account_id)
.where(account_id: [current_user.account_id,
current_user.account.linked_account_account&.account_id].compact)
.where(*SearchEntries.build_tsquery(keyword))
.select(:record_id)
)

Loading…
Cancel
Save