diff --git a/.rubocop.yml b/.rubocop.yml index 4265d3ab1..499a63e66 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -60,10 +60,10 @@ RSpec/NestedGroups: Max: 6 RSpec/MultipleExpectations: - Max: 20 + Max: 25 RSpec/ExampleLength: - Max: 40 + Max: 50 RSpec/MultipleMemoizedHelpers: Max: 9 diff --git a/app/controllers/account_configs_controller.rb b/app/controllers/account_configs_controller.rb index 1130feefc..02aee5217 100644 --- a/app/controllers/account_configs_controller.rb +++ b/app/controllers/account_configs_controller.rb @@ -17,7 +17,8 @@ class AccountConfigsController < ApplicationController AccountConfig::FLATTEN_RESULT_PDF_KEY, AccountConfig::WITH_SIGNATURE_ID, AccountConfig::COMBINE_PDF_RESULT_KEY, - AccountConfig::REQUIRE_SIGNING_REASON_KEY + AccountConfig::REQUIRE_SIGNING_REASON_KEY, + AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY ].freeze InvalidKey = Class.new(StandardError) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index bf2df2d30..a48146ee6 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -15,6 +15,7 @@ class ApplicationController < ActionController::Base helper_method :button_title, :current_account, + :form_link_host, :svg_icon impersonates :user, with: ->(uuid) { User.find_by(uuid:) } @@ -105,6 +106,10 @@ def svg_icon(icon_name, class: '') render_to_string(partial: "icons/#{icon_name}", locals: { class: }) end + def form_link_host + Docuseal.default_url_options[:host] + end + def maybe_redirect_com return if request.domain != 'docuseal.co' diff --git a/app/controllers/esign_settings_controller.rb b/app/controllers/esign_settings_controller.rb index 00c27a050..34feb84e9 100644 --- a/app/controllers/esign_settings_controller.rb +++ b/app/controllers/esign_settings_controller.rb @@ -11,6 +11,8 @@ def to_key end end + prepend_before_action :maybe_redirect_com, only: %i[show] + before_action :load_encrypted_config authorize_resource :encrypted_config, parent: false, only: %i[new create] authorize_resource :encrypted_config, only: %i[update destroy show] diff --git a/app/controllers/submissions_archived_controller.rb b/app/controllers/submissions_archived_controller.rb index d0e6d764c..c13745ffb 100644 --- a/app/controllers/submissions_archived_controller.rb +++ b/app/controllers/submissions_archived_controller.rb @@ -9,7 +9,14 @@ def index .or(@submissions.where.not(templates: { archived_at: nil })) .preload(:created_by_user, template: :author) @submissions = Submissions.search(@submissions, params[:q], search_template: true) + @submissions = Submissions::Filter.call(@submissions, current_user, params) - @pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc)) + @submissions = if params[:completed_at_from].present? || params[:completed_at_to].present? + @submissions.order(Submitter.arel_table[:completed_at].maximum.desc) + else + @submissions.order(id: :desc) + end + + @pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events)) end end diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index a7a7f216a..d2c206535 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -6,6 +6,8 @@ class SubmissionsController < ApplicationController load_and_authorize_resource :submission, only: %i[show destroy] + prepend_before_action :maybe_redirect_com, only: %i[show] + def show @submission = Submissions.preload_with_pages(@submission) diff --git a/app/controllers/submissions_dashboard_controller.rb b/app/controllers/submissions_dashboard_controller.rb index aa17b5c42..164e893a7 100644 --- a/app/controllers/submissions_dashboard_controller.rb +++ b/app/controllers/submissions_dashboard_controller.rb @@ -11,10 +11,17 @@ def index .preload(:created_by_user, template: :author) @submissions = Submissions.search(@submissions, params[:q], search_template: true) + @submissions = Submissions::Filter.call(@submissions, current_user, params) @submissions = @submissions.pending if params[:status] == 'pending' @submissions = @submissions.completed if params[:status] == 'completed' - @pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events).order(id: :desc)) + @submissions = if params[:completed_at_from].present? || params[:completed_at_to].present? + @submissions.order(Submitter.arel_table[:completed_at].maximum.desc) + else + @submissions.order(id: :desc) + end + + @pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events)) end end diff --git a/app/controllers/submissions_download_controller.rb b/app/controllers/submissions_download_controller.rb index 2a6517a91..9ba81b91d 100644 --- a/app/controllers/submissions_download_controller.rb +++ b/app/controllers/submissions_download_controller.rb @@ -53,8 +53,15 @@ def current_user_submitter?(submitter) end def build_urls(submitter) + filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id, + key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value + Submitters.select_attachments_for_download(submitter).map do |attachment| - ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i) + ActiveStorage::Blob.proxy_url( + attachment.blob, + expires_at: FILES_TTL.from_now.to_i, + filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format) + ) end end @@ -65,6 +72,13 @@ def build_combined_url(submitter) attachment = submitter.submission.combined_document_attachment attachment ||= Submissions::GenerateCombinedAttachment.call(submitter) - ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i) + filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id, + key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value + + ActiveStorage::Blob.proxy_url( + attachment.blob, + expires_at: FILES_TTL.from_now.to_i, + filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format) + ) end end diff --git a/app/controllers/submissions_filters_controller.rb b/app/controllers/submissions_filters_controller.rb new file mode 100644 index 000000000..bd5eae1ee --- /dev/null +++ b/app/controllers/submissions_filters_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class SubmissionsFiltersController < ApplicationController + ALLOWED_NAMES = %w[ + author + completed_at + created_at + ].freeze + + skip_authorization_check + + def show + return head :not_found unless ALLOWED_NAMES.include?(params[:name]) + + render params[:name] + end +end diff --git a/app/controllers/submissions_preview_controller.rb b/app/controllers/submissions_preview_controller.rb index 65b3fff3b..0d61cb003 100644 --- a/app/controllers/submissions_preview_controller.rb +++ b/app/controllers/submissions_preview_controller.rb @@ -4,7 +4,7 @@ class SubmissionsPreviewController < ApplicationController skip_before_action :authenticate_user! skip_authorization_check - before_action :maybe_redirect_com, only: %i[show completed] + prepend_before_action :maybe_redirect_com, only: %i[show completed] TTL = 40.minutes @@ -20,7 +20,7 @@ def show @submission ||= Submission.find_by!(slug: params[:slug]) - if !@submission.submitters.all?(&:completed_at?) && current_user.blank? + if @submission.account.archived_at? || (!@submission.submitters.all?(&:completed_at?) && current_user.blank?) raise ActionController::RoutingError, I18n.t('not_found') end diff --git a/app/controllers/submit_form_controller.rb b/app/controllers/submit_form_controller.rb index 642c31198..903433314 100644 --- a/app/controllers/submit_form_controller.rb +++ b/app/controllers/submit_form_controller.rb @@ -7,8 +7,6 @@ class SubmitFormController < ApplicationController skip_before_action :authenticate_user! skip_authorization_check - before_action :maybe_redirect_com, only: %i[show completed] - CONFIG_KEYS = [].freeze def show @@ -17,7 +15,9 @@ def show submission = @submitter.submission return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at? - return render :archived if submission.template.archived_at? || submission.archived_at? + return render :archived if submission.template.archived_at? || + submission.archived_at? || + @submitter.account.archived_at? return render :expired if submission.expired? return render :declined if @submitter.declined_at? return render :awaiting if submission.template.preferences['submitters_order'] == 'preserved' && @@ -27,8 +27,7 @@ def show Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user) - @attachments_index = ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments) - .preload(:blob).index_by(&:uuid) + @attachments_index = build_attachments_index(submission) @form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS) @@ -80,4 +79,11 @@ def completed end def success; end + + private + + def build_attachments_index(submission) + ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments) + .preload(:blob).index_by(&:uuid) + end end diff --git a/app/controllers/submitters_send_email_controller.rb b/app/controllers/submitters_send_email_controller.rb index 910a4d69a..f3eb31871 100644 --- a/app/controllers/submitters_send_email_controller.rb +++ b/app/controllers/submitters_send_email_controller.rb @@ -6,7 +6,7 @@ class SubmittersSendEmailController < ApplicationController def create if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter, event_type: 'send_email', - created_at: 24.hours.ago..Time.current) + created_at: 10.hours.ago..Time.current) Rollbar.warning("Already sent: #{@submitter.id}") if defined?(Rollbar) return redirect_back(fallback_location: submission_path(@submitter.submission), diff --git a/app/controllers/templates_archived_submissions_controller.rb b/app/controllers/templates_archived_submissions_controller.rb index e0bdca255..bf023677c 100644 --- a/app/controllers/templates_archived_submissions_controller.rb +++ b/app/controllers/templates_archived_submissions_controller.rb @@ -7,8 +7,15 @@ class TemplatesArchivedSubmissionsController < ApplicationController def index @submissions = @submissions.where.not(archived_at: nil) @submissions = Submissions.search(@submissions, params[:q], search_values: true) + @submissions = Submissions::Filter.call(@submissions, current_user, params) - @pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc)) + @submissions = if params[:completed_at_from].present? || params[:completed_at_to].present? + @submissions.order(Submitter.arel_table[:completed_at].maximum.desc) + else + @submissions.order(id: :desc) + end + + @pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events)) rescue ActiveRecord::RecordNotFound redirect_to root_path end diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb index 5a578685b..04d63e1cc 100644 --- a/app/controllers/templates_controller.rb +++ b/app/controllers/templates_controller.rb @@ -9,13 +9,20 @@ def show submissions = @template.submissions.accessible_by(current_ability) submissions = submissions.active if @template.archived_at.blank? submissions = Submissions.search(submissions, params[:q], search_values: true) + submissions = Submissions::Filter.call(submissions, current_user, params) @base_submissions = submissions submissions = submissions.pending if params[:status] == 'pending' submissions = submissions.completed if params[:status] == 'completed' - @pagy, @submissions = pagy(submissions.preload(submitters: :start_form_submission_events).order(id: :desc)) + submissions = if params[:completed_at_from].present? || params[:completed_at_to].present? + submissions.order(Submitter.arel_table[:completed_at].maximum.desc) + else + submissions.order(id: :desc) + end + + @pagy, @submissions = pagy(submissions.preload(submitters: :start_form_submission_events)) rescue ActiveRecord::RecordNotFound redirect_to root_path end diff --git a/app/javascript/application.js b/app/javascript/application.js index 03339e0c1..15f31a722 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -30,6 +30,7 @@ import ToggleAttribute from './elements/toggle_attribute' import LinkedInput from './elements/linked_input' import CheckboxGroup from './elements/checkbox_group' import MaskedInput from './elements/masked_input' +import SetDateButton from './elements/set_date_button' import * as TurboInstantClick from './lib/turbo_instant_click' @@ -97,6 +98,7 @@ safeRegisterElement('toggle-attribute', ToggleAttribute) safeRegisterElement('linked-input', LinkedInput) safeRegisterElement('checkbox-group', CheckboxGroup) safeRegisterElement('masked-input', MaskedInput) +safeRegisterElement('set-date-button', SetDateButton) safeRegisterElement('template-builder', class extends HTMLElement { connectedCallback () { diff --git a/app/javascript/elements/set_date_button.js b/app/javascript/elements/set_date_button.js new file mode 100644 index 000000000..409619dec --- /dev/null +++ b/app/javascript/elements/set_date_button.js @@ -0,0 +1,20 @@ +export default class extends HTMLElement { + connectedCallback () { + this.button.addEventListener('click', () => { + this.fromInput.value = this.dataset.fromValue || '' + this.toInput.value = this.dataset.toValue || '' + }) + } + + get button () { + return this.querySelector('button') + } + + get fromInput () { + return document.getElementById(this.dataset.fromId) + } + + get toInput () { + return document.getElementById(this.dataset.toId) + } +} diff --git a/app/javascript/submission_form/date_step.vue b/app/javascript/submission_form/date_step.vue index fcd9cc0c8..7984ac093 100644 --- a/app/javascript/submission_form/date_step.vue +++ b/app/javascript/submission_form/date_step.vue @@ -37,6 +37,7 @@
o.uuid === c.value) - const values = [this.values[c.field_uuid]].flat() + if (field.options) { + const option = field.options.find((o) => o.uuid === c.value) + const values = [this.values[c.field_uuid]].flat() - return acc && values.includes(this.optionValue(option, field.options.indexOf(option))) + return acc && values.includes(this.optionValue(option, field.options.indexOf(option))) + } else { + return acc && [this.values[c.field_uuid]].flat().includes(c.value) + } } else if (['not_equal', 'does_not_contain'].includes(c.action) && field) { const option = field.options.find((o) => o.uuid === c.value) const values = [this.values[c.field_uuid]].flat() @@ -1243,9 +1247,13 @@ export default { if (response.status === 422 || response.status === 500) { const data = await response.json() - const i18nError = data.error ? this.t(data.error.replace(/\s+/g, '_').toLowerCase()) : '' + if (data.error) { + const i18nKey = data.error.replace(/\s+/g, '_').toLowerCase() - alert(i18nError !== data.error ? i18nError : (data.error || this.t('value_is_invalid'))) + alert(this.t(i18nKey) !== i18nKey ? this.t(i18nKey) : data.error) + } else { + alert(this.t('value_is_invalid')) + } return Promise.reject(new Error(data.error)) } diff --git a/app/javascript/submission_form/i18n.js b/app/javascript/submission_form/i18n.js index 42fed56b9..22f2c3550 100644 --- a/app/javascript/submission_form/i18n.js +++ b/app/javascript/submission_form/i18n.js @@ -67,7 +67,7 @@ const en = { documents_have_been_signed: 'Documents have been signed!', create_a_free_account: 'Create a Free Account', powered_by: 'Powered by', - please_check_the_box_to_continue: 'Please check the box to continue', + 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', use_international_format: 'Use international format: +1xxx', @@ -89,7 +89,8 @@ const en = { pay_with_strip: 'Pay with Stripe', reupload: 'Reupload', upload: 'Upload', - files: 'Files' + files: 'Files', + signature_is_too_small_please_redraw: 'Signature is too small. Please redraw.' } const es = { @@ -160,7 +161,7 @@ const es = { documents_have_been_signed: '¡Los documentos han sido firmados!', create_a_free_account: 'Crear una Cuenta Gratuita', powered_by: 'Desarrollado por', - please_check_the_box_to_continue: 'Por favor marque la casilla para continuar', + 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', use_international_format: 'Usar formato internacional: +1xxx', @@ -182,7 +183,8 @@ const es = { pay_with_strip: 'Pagar con Stripe', reupload: 'Volver a subir', upload: 'Subir', - files: 'Archivos' + files: 'Archivos', + signature_is_too_small_please_redraw: 'La firma es demasiado pequeña. Por favor, dibújala de nuevo.' } const it = { @@ -253,7 +255,7 @@ const it = { documents_have_been_signed: 'I documenti sono stati firmati!', create_a_free_account: 'Crea un Account Gratuito', powered_by: 'Desarrollado por', - please_check_the_box_to_continue: 'Si prega di spuntare la casella per continuare', + 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', use_international_format: 'Usa formato internazionale: +1xxx', @@ -275,7 +277,8 @@ const it = { pay_with_strip: 'Paga con Stripe', reupload: 'Ricarica', upload: 'Carica', - files: 'File' + files: 'File', + signature_is_too_small_please_redraw: 'La firma è troppo piccola. Ridisegnala per favore.' } const de = { @@ -346,7 +349,7 @@ const de = { documents_have_been_signed: 'Dokumente wurden unterschrieben!', create_a_free_account: 'Kostenloses Konto erstellen', powered_by: 'Bereitgestellt von', - please_check_the_box_to_continue: 'Bitte setzen Sie das Häkchen, um fortzufahren', + please_check_the_box_to_continue: 'Bitte setzen Sie das Häkchen, um fortzufahren.', open_source_documents_software: 'Open-Source-Dokumentensoftware', verified_phone_number: 'Telefonnummer überprüfen', use_international_format: 'Internationales Format verwenden: +1xxx', @@ -368,7 +371,8 @@ const de = { pay_with_strip: 'Mit Stripe bezahlen', reupload: 'Erneut hochladen', upload: 'Hochladen', - files: 'Dateien' + files: 'Dateien', + signature_is_too_small_please_redraw: 'Die Unterschrift ist zu klein. Bitte erneut zeichnen.' } const fr = { @@ -439,7 +443,7 @@ const fr = { documents_have_been_signed: 'Les documents ont été signés!', create_a_free_account: 'Créer un Compte Gratuit', powered_by: 'Propulsé par', - please_check_the_box_to_continue: 'Veuillez cocher la case pour continuer', + 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', use_international_format: 'Utiliser le format international : +1xxx', @@ -461,7 +465,8 @@ const fr = { pay_with_strip: 'Paiement avec Stripe', reupload: 'Recharger', upload: 'Télécharger', - files: 'Fichiers' + files: 'Fichiers', + signature_is_too_small_please_redraw: 'La signature est trop petite. Veuillez la redessiner.' } const pl = { @@ -532,7 +537,7 @@ const pl = { documents_have_been_signed: 'Dokumenty zostały podpisane!', create_a_free_account: 'Utwórz darmowe konto', powered_by: 'Napędzany przez', - please_check_the_box_to_continue: 'Proszę zaznaczyć pole, aby kontynuować', + 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', use_international_format: 'Użyj międzynarodowego formatu: +1xxx', @@ -554,7 +559,8 @@ const pl = { pay_with_strip: 'Płatność za pomocą Stripe', reupload: 'Ponowne przesłanie', upload: 'Przesyłanie', - files: 'Pliki' + files: 'Pliki', + signature_is_too_small_please_redraw: 'Podpis jest zbyt mały. Proszę narysować go ponownie.' } const uk = { @@ -625,7 +631,7 @@ const uk = { documents_have_been_signed: 'Документи були підписані!', create_a_free_account: 'Створити безкоштовний обліковий запис', powered_by: 'Працює на базі', - please_check_the_box_to_continue: 'Будь ласка, позначте прапорець, щоб продовжити', + please_check_the_box_to_continue: 'Будь ласка, позначте прапорець, щоб продовжити.', open_source_documents_software: 'відкритий програмний засіб для документів', verified_phone_number: 'Підтвердіть номер телефону', use_international_format: 'Використовуйте міжнародний формат: +1xxx', @@ -647,7 +653,8 @@ const uk = { pay_with_strip: 'Сплатити за допомогою Stripe', reupload: 'Перезавантажити', upload: 'Завантажити', - files: 'Файли' + files: 'Файли', + signature_is_too_small_please_redraw: 'Підпис занадто малий. Будь ласка, перемалюйте його.' } const cs = { @@ -718,7 +725,7 @@ const cs = { documents_have_been_signed: 'Dokumenty byly podepsány!', create_a_free_account: 'Vytvořit bezplatný účet', powered_by: 'Poháněno', - please_check_the_box_to_continue: 'Prosím, zaškrtněte políčko pro pokračování', + 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', use_international_format: 'Použijte mezinárodní formát: +1xxx', @@ -740,7 +747,8 @@ const cs = { pay_with_strip: 'Zaplacení přes Stripe', reupload: 'Znovu nahrát', upload: 'Nahrát', - files: 'Soubory' + files: 'Soubory', + signature_is_too_small_please_redraw: 'Podpis je příliš malý. Prosím, překreslete ho.' } const pt = { @@ -811,7 +819,7 @@ const pt = { documents_have_been_signed: 'Os documentos foram assinados!', create_a_free_account: 'Criar uma Conta Gratuita', powered_by: 'Desenvolvido por', - please_check_the_box_to_continue: 'Por favor, marque a caixa para continuar', + 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', use_international_format: 'Use formato internacional: +1xxx', @@ -833,7 +841,8 @@ const pt = { pay_with_strip: 'Pagar com Stripe', reupload: 'Reenviar', upload: 'Carregar', - files: 'Arquivos' + files: 'Arquivos', + signature_is_too_small_please_redraw: 'A assinatura é muito pequena. Por favor, redesenhe-a.' } const he = { @@ -905,7 +914,7 @@ const he = { documents_have_been_signed: 'המסמכים נחתמו!', create_a_free_account: 'צור חשבון חינם', powered_by: 'מופעל על ידי', - please_check_the_box_to_continue: 'אנא סמן את התיבה כדי להמשיך', + please_check_the_box_to_continue: 'אנא סמן את התיבה כדי להמשיך.', open_source_documents_software: 'תוכנה פתוחה למסמכים', verified_phone_number: 'אימות מספר טלפון', use_international_format: 'השתמש בפורמט בינלאומי: +1xxx', @@ -927,7 +936,8 @@ const he = { pay_with_strip: 'שלם עם סטרייפ', reupload: 'העלה שוב', upload: 'העלאה', - files: 'קבצים' + files: 'קבצים', + signature_is_too_small_please_redraw: 'החתימה קטנה מדי. אנא צייר מחדש.' } const nl = { @@ -999,7 +1009,7 @@ const nl = { documents_have_been_signed: 'De documenten zijn ondertekend!', create_a_free_account: 'Maak een gratis account aan', powered_by: 'Aangedreven door', - please_check_the_box_to_continue: 'Vink het vakje aan om door te gaan', + 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: 'Geverifieerd telefoonnummer', use_international_format: 'Gebruik internationaal formaat: +1xxx', @@ -1021,7 +1031,8 @@ const nl = { pay_with_strip: 'Betalen met Stripe', reupload: 'Opnieuw uploaden', upload: 'Uploaden', - files: 'Bestanden' + files: 'Bestanden', + signature_is_too_small_please_redraw: 'De handtekening is te klein. Teken deze opnieuw, alstublieft.' } const ar = { @@ -1092,7 +1103,7 @@ const ar = { documents_have_been_signed: 'تم توقيع الوثائق!', create_a_free_account: 'إنشاء حساب مجاني', powered_by: 'مدعوم من', - please_check_the_box_to_continue: 'الرجاء التحقق من الخانة للمتابعة', + please_check_the_box_to_continue: 'الرجاء التحقق من الخانة للمتابعة.', open_source_documents_software: 'برنامج وثائق مفتوح المصدر', verified_phone_number: 'تحقق من رقم الهاتف', use_international_format: 'استخدم الشكل الدولي: +1xxx', @@ -1114,7 +1125,8 @@ const ar = { pay_with_strip: 'الدفع بواسطة Stripe', reupload: 'إعادة التحميل', upload: 'تحميل', - files: 'الملفات' + files: 'الملفات', + signature_is_too_small_please_redraw: 'التوقيع صغير جدًا. يرجى إعادة الرسم.' } const ko = { @@ -1184,7 +1196,7 @@ const ko = { documents_have_been_signed: '문서가 서명되었습니다!', create_a_free_account: '무료 계정 생성', powered_by: '구동', - please_check_the_box_to_continue: '계속하려면 확인란을 선택하십시오', + please_check_the_box_to_continue: '계속하려면 확인란을 선택하십시오.', open_source_documents_software: '오픈 소스 문서 소프트웨어', verified_phone_number: '전화번호 확인됨', use_international_format: '국제 포맷 사용: +1xxx', @@ -1206,7 +1218,8 @@ const ko = { pay_with_strip: '스트라이프로 결제', reupload: '다시 업로드', upload: '업로드', - files: '파일' + files: '파일', + signature_is_too_small_please_redraw: '서명이 너무 작습니다. 다시 그려주세요.' } const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar, ko } diff --git a/app/javascript/submission_form/phone_step.vue b/app/javascript/submission_form/phone_step.vue index 3394df743..9c4678947 100644 --- a/app/javascript/submission_form/phone_step.vue +++ b/app/javascript/submission_form/phone_step.vue @@ -65,11 +65,15 @@ v-show="!isCodeSent" class="flex w-full rounded-full outline-neutral-content outline-2 outline-offset-2 focus-within:outline" > -
+
{{ selectedCountry.flag }} +{{ selectedCountry.dial }}
<% end %> + <% Submissions::Filter::ALLOWED_PARAMS.each do |key| %> + <% if params[key].present? %> + + <% end %> + <% end %> <% if params[:q].present? %>
- <% unless @template.archived_at? %> + <% if !@template.archived_at? && !@template.account.archived_at? %> <%= form_for @submitter, url: start_form_path(@template.slug), data: { turbo_frame: :_top }, method: :put, html: { class: 'space-y-4' } do |f| %>
<%= f.label :email, t('email'), class: 'label' %> diff --git a/app/views/submissions/_send_email.html.erb b/app/views/submissions/_send_email.html.erb index b9638b492..9031c8e4c 100644 --- a/app/views/submissions/_send_email.html.erb +++ b/app/views/submissions/_send_email.html.erb @@ -2,7 +2,7 @@ <% can_send_emails = Accounts.can_send_emails?(current_account) %> <% end %> <% elsif @submission.submitters.to_a.size == 1 && !@submission.expired? && !@submission.submitters.to_a.first.declined_at? && !@submission.archived_at? %> - <%= render 'shared/clipboard_copy', text: submit_form_url(slug: @submission.submitters.to_a.first.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy_share_link'), copied_title: t('copied_to_clipboard') %> + <%= render 'shared/clipboard_copy', text: submit_form_url(slug: @submission.submitters.to_a.first.slug, host: form_link_host), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy_share_link'), copied_title: t('copied_to_clipboard') %> <% end %>
diff --git a/app/views/submissions_archived/index.html.erb b/app/views/submissions_archived/index.html.erb index 846a044df..8a5216c17 100644 --- a/app/views/submissions_archived/index.html.erb +++ b/app/views/submissions_archived/index.html.erb @@ -1,22 +1,31 @@ +<% filter_params = params.permit(Submissions::Filter::ALLOWED_PARAMS).compact_blank %>
<%= link_to root_path do %> ← <%= t('back_to_active') %> <% end %>
-
-
-

<%= t('submissions') %> <%= t('archived') %>

+
+
+
+

<%= t('submissions') %> <%= t('archived') %>

+
+
+ <% if params[:q].present? || @pagy.pages > 1 || filter_params.present? %> + <%= render 'shared/search_input', placeholder: "#{t('search')}..." %> + <% end %> +
+
+
+ <%= render 'submissions_filters/applied_filters', filter_params: %> + <%= render 'submissions_filters/filter_button', filter_params: %>
- <% if params[:q].present? || @pagy.pages > 1 %> - <%= render 'shared/search_input', placeholder: "#{t('search')}..." %> - <% end %>
<% if @pagy.count > 0 %>
<%= render partial: 'templates/submission', collection: @submissions, locals: { with_template: true, archived: true } %>
-<% elsif params[:q].present? %> +<% elsif params[:q].present? || filter_params.present? %>
<%= t('submissions_not_found') %> diff --git a/app/views/submissions_dashboard/index.html.erb b/app/views/submissions_dashboard/index.html.erb index 040e2301d..5c9a733f0 100644 --- a/app/views/submissions_dashboard/index.html.erb +++ b/app/views/submissions_dashboard/index.html.erb @@ -1,4 +1,5 @@ -<% is_show_tabs = @pagy.count >= 5 || params[:status].present? %> +<% filter_params = params.permit(Submissions::Filter::ALLOWED_PARAMS).compact_blank %> +<% is_show_tabs = @pagy.count >= 5 || params[:status].present? || filter_params.present? %> <% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %>
@@ -10,7 +11,7 @@
- <% if params[:q].present? || @pagy.pages > 1 %> + <% if params[:q].present? || @pagy.pages > 1 || filter_params.present? %> <%= render 'shared/search_input' %> <% end %> <% if can?(:create, ::Template) %> @@ -32,25 +33,31 @@ <% end %> <% end %> <% if is_show_tabs %> -
- -
- <%= svg_icon('list', class: 'w-5 h-5') %> - <%= t('all') %> -
-
- -
- <%= svg_icon('clock', class: 'w-5 h-5') %> - <%= t('pending') %> -
-
- -
- <%= svg_icon('circle_check', class: 'w-5 h-5') %> - <%= t('completed') %> -
-
+ <% end %> <% if @pagy.count > 0 %> @@ -58,10 +65,10 @@ <%= render partial: 'templates/submission', collection: @submissions, locals: { with_template: true } %>
<% end %> -<% if params[:q].blank? && params[:status].blank? && @pagy.count < 5 %> +<% if params[:q].blank? && params[:status].blank? && filter_params.blank? && @pagy.count < 5 %> <%= render 'templates/dropzone' %> <% end %> -<% if @submissions.present? || params[:q].blank? %> +<% if @submissions.present? || (params[:q].blank? && filter_params.blank?) %> <% if @pagy.pages > 1 %> <%= render 'shared/pagination', pagy: @pagy, items_name: 'submissions', left_additional_html: view_archived_html %> <% else %> @@ -69,7 +76,7 @@ <%= view_archived_html %>
<% end %> -<% elsif params[:q].present? %> +<% elsif params[:q].present? || filter_params.present? %>
<%= t('submissions_not_found') %> diff --git a/app/views/submissions_filters/_applied_filters.html.erb b/app/views/submissions_filters/_applied_filters.html.erb new file mode 100644 index 000000000..ad988d717 --- /dev/null +++ b/app/views/submissions_filters/_applied_filters.html.erb @@ -0,0 +1,50 @@ +<% query_params = params.permit(:q, :status).merge(filter_params) %> +<% if query_params[:completed_at_from].present? || query_params[:completed_at_to].present? %> +
+ <%= link_to submissions_filter_path('completed_at', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 w-full pr-1 md:max-w-[140px]' do %> + <%= svg_icon('calendar_check', class: 'w-5 h-5 shrink-0') %> + + <% if query_params[:completed_at_from] == query_params[:completed_at_to] %> + <%= l(Date.parse(query_params[:completed_at_from]), locale: current_account.locale) %> + <% else %> + <%= query_params[:completed_at_from].present? ? l(Date.parse(query_params[:completed_at_from]), locale: current_account.locale) : '∞' %> + - + <%= query_params[:completed_at_to].present? ? l(Date.parse(query_params[:completed_at_to]), locale: current_account.locale) : t('today') %> + <% end %> + + <% end %> + <%= link_to url_for(params.to_unsafe_h.except(:completed_at_from, :completed_at_to)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %> + <%= svg_icon('x', class: 'w-5 h-5') %> + <% end %> +
+<% end %> +<% if query_params[:created_at_from].present? || query_params[:created_at_to].present? %> +
+ <%= link_to submissions_filter_path('created_at', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 w-full pr-1 md:max-w-[140px]' do %> + <%= svg_icon('calendar', class: 'w-5 h-5 shrink-0') %> + + <% if query_params[:created_at_from] == query_params[:created_at_to] %> + <%= l(Date.parse(query_params[:created_at_from]), locale: current_account.locale) %> + <% else %> + <%= query_params[:created_at_from].present? ? l(Date.parse(query_params[:created_at_from]), locale: current_account.locale) : '∞' %> + - + <%= query_params[:created_at_to].present? ? l(Date.parse(query_params[:created_at_to]), locale: current_account.locale) : t('today') %> + <% end %> + + <% end %> + <%= link_to url_for(params.to_unsafe_h.except(:created_at_to, :created_at_from)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %> + <%= svg_icon('x', class: 'w-5 h-5') %> + <% end %> +
+<% end %> +<% if params[:author].present? %> +
+ <%= link_to submissions_filter_path('author', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 w-full pr-1 md:max-w-[140px]' do %> + <%= svg_icon('user', class: 'w-5 h-5 shrink-0') %> + <%= current_account.users.accessible_by(current_ability).where(account: current_account).find_by(email: params[:author])&.full_name || 'NA' %> + <% end %> + <%= link_to url_for(params.to_unsafe_h.except(:author)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %> + <%= svg_icon('x', class: 'w-5 h-5') %> + <% end %> +
+<% end %> diff --git a/app/views/submissions_filters/_date_buttons.html.erb b/app/views/submissions_filters/_date_buttons.html.erb new file mode 100644 index 000000000..3eb15e9ca --- /dev/null +++ b/app/views/submissions_filters/_date_buttons.html.erb @@ -0,0 +1,39 @@ +<% current_time = Time.current.in_time_zone(current_account.timezone) %> +<% week_start = TimeUtils.timezone_abbr(current_account.timezone, current_time).in?(TimeUtils::US_TIMEZONES) ? :sunday : :monday %> +
+ + + + + + + + + + + + + + + + + + + + + +
diff --git a/app/views/submissions_filters/_filter_button.html.erb b/app/views/submissions_filters/_filter_button.html.erb new file mode 100644 index 000000000..b840729d4 --- /dev/null +++ b/app/views/submissions_filters/_filter_button.html.erb @@ -0,0 +1,27 @@ +<% query_params = params.permit(:q, :status).merge(filter_params) %> + diff --git a/app/views/submissions_filters/_filter_modal.html.erb b/app/views/submissions_filters/_filter_modal.html.erb new file mode 100644 index 000000000..afb3916f6 --- /dev/null +++ b/app/views/submissions_filters/_filter_modal.html.erb @@ -0,0 +1,18 @@ +<%= render 'shared/turbo_modal', title: local_assigns[:title] do %> + <%= form_for '', url: params[:path], method: :get, data: { turbo_frame: :_top }, html: { autocomplete: :off } do |f| %> + <%= hidden_field_tag :status, params[:status] if params[:status].present? %> + <%= hidden_field_tag :q, params[:q] if params[:q].present? %> + <% local_assigns[:default_params].each do |key, value| %> + <%= hidden_field_tag(key, value) if value.present? %> + <% end %> + <%= yield %> +
+ <%= f.button button_title(title: t('apply'), disabled_with: t('applying')), name: nil, class: 'base-button' %> +
+ <% if params[:with_remove] %> +
+ <%= link_to t('remove_filter'), "#{params[:path]}?#{params.to_unsafe_h.slice(:q, :status).merge(local_assigns[:default_params]).to_query}", class: 'link', data: { turbo_frame: :_top } %> +
+ <% end %> + <% end %> +<% end %> diff --git a/app/views/submissions_filters/author.html.erb b/app/views/submissions_filters/author.html.erb new file mode 100644 index 000000000..b9f3b27f7 --- /dev/null +++ b/app/views/submissions_filters/author.html.erb @@ -0,0 +1,7 @@ +<%= render 'filter_modal', title: t('author'), default_params: params.permit(*(Submissions::Filter::ALLOWED_PARAMS - ['author'])) do %> +
+
+ <%= select_tag :author, options_for_select(current_account.users.accessible_by(current_ability).where.not(role: :integration).where(account: current_account).map { |u| [u.full_name, u.email] }, params[:author].presence || current_user.email), required: true, class: 'base-select' %> +
+
+<% end %> diff --git a/app/views/submissions_filters/completed_at.html.erb b/app/views/submissions_filters/completed_at.html.erb new file mode 100644 index 000000000..0a0b6b6fe --- /dev/null +++ b/app/views/submissions_filters/completed_at.html.erb @@ -0,0 +1,15 @@ +<%= render 'filter_modal', title: t('completed_at'), default_params: params.permit(*(Submissions::Filter::ALLOWED_PARAMS - %w[completed_at_to completed_at_from])) do %> +
+
+
+ <%= label_tag 'completed_at_from', t('from'), for: 'date_from', class: 'label text-sm' %> + <%= date_field_tag 'completed_at_from', params[:completed_at_from], id: 'date_from', class: 'base-input !h-10', autocomplete: 'off' %> +
+
+ <%= label_tag 'completed_at_to', t('to'), for: 'date_to', class: 'label text-sm' %> + <%= date_field_tag 'completed_at_to', params[:completed_at_to], id: 'date_to', class: 'base-input !h-10', autocomplete: 'off' %> +
+
+ <%= render 'date_buttons' %> +
+<% end %> diff --git a/app/views/submissions_filters/created_at.html.erb b/app/views/submissions_filters/created_at.html.erb new file mode 100644 index 000000000..2f0894645 --- /dev/null +++ b/app/views/submissions_filters/created_at.html.erb @@ -0,0 +1,15 @@ +<%= render 'filter_modal', title: t('created_at'), default_params: params.permit(*(Submissions::Filter::ALLOWED_PARAMS - %w[created_at_to created_at_from])) do %> +
+
+
+ <%= label_tag 'created_at_from', t('from'), for: 'date_from', class: 'label text-sm' %> + <%= date_field_tag 'created_at_from', params[:created_at_from], id: 'date_from', class: 'base-input !h-10', autocomplete: 'off' %> +
+
+ <%= label_tag 'created_at_to', t('to'), for: 'date_to', class: 'label text-sm' %> + <%= date_field_tag 'created_at_to', params[:created_at_to], id: 'date_to', class: 'base-input !h-10', autocomplete: 'off' %> +
+
+ <%= render 'date_buttons' %> +
+<% end %> diff --git a/app/views/submitter_mailer/invitation_email.html.erb b/app/views/submitter_mailer/invitation_email.html.erb index d5987306b..968ec02b9 100644 --- a/app/views/submitter_mailer/invitation_email.html.erb +++ b/app/views/submitter_mailer/invitation_email.html.erb @@ -7,7 +7,7 @@ <% else %>

<%= t('hi_there') %>,

<%= 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.template.name) %>

-

<%= 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')) %>

+

<%= 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: ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host])) %>

<%= t('please_contact_us_by_replying_to_this_email_if_you_didn_t_request_this') %>

<%= t('thanks') %>,
<%= @current_account.name %> diff --git a/app/views/templates/_submission.html.erb b/app/views/templates/_submission.html.erb index 8ae256d5d..ad0d490b0 100644 --- a/app/views/templates/_submission.html.erb +++ b/app/views/templates/_submission.html.erb @@ -92,7 +92,7 @@

<% else %>
- <%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-sm btn-neutral text-white md:w-36 flex z-[1]', icon_class: 'w-6 h-6 text-white', copy_title: t('copy_link').length < 10 ? t('copy_link') : t('copy'), copy_title_md: t('copy'), copied_title_md: t('copied') %> + <%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug, host: form_link_host), class: 'btn btn-sm btn-neutral text-white md:w-36 flex z-[1]', icon_class: 'w-6 h-6 text-white', copy_title: t('copy_link').length < 10 ? t('copy_link') : t('copy'), copy_title_md: t('copy'), copied_title_md: t('copied') %>
<% end %> <% end %> @@ -176,7 +176,7 @@ <% else %> - <%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'absolute md:relative top-0 right-0 btn btn-xs text-xs btn-neutral text-white w-28 md:w-36 flex z-[1]', icon_class: 'w-4 h-4 text-white', copy_title: t('copy_link'), copy_title_md: t('copy_link').length < 10 ? t('copy_link') : t('copy'), copied_title_md: t('copied') %> + <%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug, host: form_link_host), class: 'absolute md:relative top-0 right-0 btn btn-xs text-xs btn-neutral text-white w-28 md:w-36 flex z-[1]', icon_class: 'w-4 h-4 text-white', copy_title: t('copy_link'), copy_title_md: t('copy_link').length < 10 ? t('copy_link') : t('copy'), copied_title_md: t('copied') %> <% end %>
<% end %> diff --git a/app/views/templates/_title.html.erb b/app/views/templates/_title.html.erb index d765fd00f..83726b216 100644 --- a/app/views/templates/_title.html.erb +++ b/app/views/templates/_title.html.erb @@ -1,7 +1,7 @@
-

+

<% template.name.split(/(_)/).each do |item| %><%= item %><% end %> <% if template.archived_at? %> <%= t('archived') %> diff --git a/app/views/templates/show.html.erb b/app/views/templates/show.html.erb index b3d1faa0b..637292876 100644 --- a/app/views/templates/show.html.erb +++ b/app/views/templates/show.html.erb @@ -1,6 +1,7 @@ <%= render 'title', template: @template %> -<% is_show_tabs = @pagy.pages > 1 || params[:status].present? %> -<% if !@pagy.count.zero? || params[:q].present? || params[:status].present? %> +<% 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? %>
@@ -9,7 +10,7 @@

- <% if params[:q].present? || params[:status].present? || @pagy.pages > 1 %> + <% if params[:q].present? || params[:status].present? || filter_params.present? || @pagy.pages > 1 %> <%= render 'shared/search_input', title_selector: 'h2' %> <% end %> <%= link_to new_template_submissions_export_path(@template), class: 'hidden md:flex btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %> @@ -27,34 +28,40 @@
<% end %> <% if is_show_tabs %> -
- -
- <%= svg_icon('list', class: 'w-5 h-5') %> - <%= t('all') %> -
-
- <%= params[:status].blank? ? @pagy.count : @base_submissions.count %> -
-
- -
- <%= svg_icon('clock', class: 'w-5 h-5') %> - <%= t('pending') %> -
-
- <%= params[:status] == 'pending' ? @pagy.count : @base_submissions.pending.count %> -
-
- -
- <%= svg_icon('circle_check', class: 'w-5 h-5') %> - <%= t('completed') %> -
-
- <%= params[:status] == 'completed' ? @pagy.count : @base_submissions.completed.count %> -
-
+ <% end %> <% if @submissions.present? %> @@ -65,31 +72,33 @@
-

+

<%= t('there_are_no_submissions') %>

- <% if @template.archived_at.blank? && params[:q].blank? %> -

<%= t('send_an_invitation_to_fill_and_complete_the_form') %>

-
- <% if can?(:create, Submission) %> - <%= link_to new_template_submission_path(@template, with_link: true), class: 'base-button mt-6', data: { turbo_frame: 'modal' } do %> - <%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %> - <%= t('send_to_recipients') %> + <% if @template.archived_at.blank? && params[:q].blank? && filter_params.blank? %> +
+

<%= t('send_an_invitation_to_fill_and_complete_the_form') %>

+
+ <% if can?(:create, Submission) %> + <%= link_to new_template_submission_path(@template, with_link: true), class: 'base-button mt-6', data: { turbo_frame: 'modal' } do %> + <%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %> + <%= t('send_to_recipients') %> + <% end %> <% end %> - <% end %> - <% if @template.submitters.size == 1 %> - <%= link_to start_form_url(slug: @template.slug), class: 'white-button mt-6', target: '_blank', rel: 'noopener' do %> - <%= svg_icon('writing', class: 'w-6 h-6') %> - <%= t('sign_it_yourself') %> + <% if @template.submitters.size == 1 %> + <%= link_to start_form_url(slug: @template.slug), class: 'white-button mt-6', target: '_blank', rel: 'noopener' do %> + <%= svg_icon('writing', class: 'w-6 h-6') %> + <%= t('sign_it_yourself') %> + <% end %> + <% else %> + <%= link_to new_template_submission_path(@template, selfsign: true), class: 'white-button mt-6', data: { turbo_frame: 'modal' } do %> + <%= svg_icon('writing', class: 'w-6 h-6') %> + <%= t('sign_it_yourself') %> + <% end %> <% end %> - <% else %> - <%= link_to new_template_submission_path(@template, selfsign: true), class: 'white-button mt-6', data: { turbo_frame: 'modal' } do %> - <%= svg_icon('writing', class: 'w-6 h-6') %> - <%= t('sign_it_yourself') %> - <% end %> - <% end %> - <% end %> -
+
+
+ <% end %>
diff --git a/app/views/templates_archived_submissions/index.html.erb b/app/views/templates_archived_submissions/index.html.erb index 779b9e8e2..eac39e1eb 100644 --- a/app/views/templates_archived_submissions/index.html.erb +++ b/app/views/templates_archived_submissions/index.html.erb @@ -1,3 +1,5 @@ +<% filter_params = params.permit(Submissions::Filter::ALLOWED_PARAMS).compact_blank %> +<% with_filters = @pagy.pages > 1 || params[:q].present? || filter_params.present? %> <%= render 'templates/title', template: @template %>
<%= link_to template_path(@template) do %> @@ -5,23 +7,33 @@ <%= t('back_to_active') %> <% end %>
-
-
-

<%= t('submissions') %> <%= t('archived') %>

-
-
- <%= render 'shared/search_input' %> - <%= link_to new_template_submissions_export_path(@template), class: 'order-3 md:order-1 btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %> - <%= svg_icon('download', class: 'w-6 h-6 stroke-2') %> - <%= t('export') %> - <% end %> +
+
+
+

<%= t('submissions') %> <%= t('archived') %>

+
+
+ <% if with_filters %> + <%= render 'shared/search_input' %> + <% end %> + <%= link_to new_template_submissions_export_path(@template), class: 'btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %> + <%= svg_icon('download', class: 'w-6 h-6 stroke-2') %> + <%= t('export') %> + <% end %> +
+ <% if with_filters %> +
+ <%= render 'submissions_filters/applied_filters', filter_params: %> + <%= render 'submissions_filters/filter_button', filter_params: %> +
+ <% end %>
<% if @pagy.count > 0 %>
<%= render partial: 'templates/submission', collection: @submissions, locals: { template: @template, archived: true } %>
-<% elsif params[:q].present? %> +<% elsif params[:q].present? || filter_params.present? %>
<%= t('submissions_not_found') %> diff --git a/config/initializers/active_storage.rb b/config/initializers/active_storage.rb index 81c7240c8..451ef815c 100644 --- a/config/initializers/active_storage.rb +++ b/config/initializers/active_storage.rb @@ -13,10 +13,11 @@ def signed_uuid ActiveSupport.on_load(:active_storage_blob) do attribute :uuid, :string, default: -> { SecureRandom.uuid } - def self.proxy_url(blob, expires_at: nil) + def self.proxy_url(blob, expires_at: nil, filename: nil, host: nil) Rails.application.routes.url_helpers.blobs_proxy_url( - signed_uuid: blob.signed_uuid(expires_at:), filename: blob.filename, - **Docuseal.default_url_options + signed_uuid: blob.signed_uuid(expires_at:), filename: filename || blob.filename, + **Docuseal.default_url_options, + **{ host: }.compact ) end diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index 12b896aaf..588699256 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -22,6 +22,9 @@ en: &en thanks: Thanks unarchive: Unarchive first_party: 'First Party' + remove_filter: Remove filter + document_download_filename_format: Document download filename format + document_name: Document Name docuseal_trusted_signature: DocuSeal Trusted Signature hello_name: Hello %{name} you_are_invited_to_product_name: You are invited to %{product_name} @@ -344,7 +347,7 @@ en: &en edit_message: Edit message smtp_not_configured: SMTP not Configured configure_smtp_settings_in_order_to_send_emails_: 'Configure SMTP settings in order to send emails:' - go_to_smtp_setting: Go to SMTP setting + go_to_smtp_settings: Go to SMTP settings save_as_default_template_message: Save as default template message re_send_sms: Re-send SMS send_sms: Send SMS @@ -619,7 +622,7 @@ en: &en verified: Verified unverified: Unverified document: Document - completed_at: Completed At + completed_at: Completed at edit_recipient: Edit Recipient update_recipient: Update Recipient use_international_format_1xxx_: 'Use internatioanl format: +1xxx...' @@ -631,6 +634,18 @@ en: &en key: Key value: Value webhook_secret: Webhook Secret + author: Author + to: To + created_at: Created at + apply: Apply + applying: Applying + today: Today + yesterday: Yesterday + this_week: This week + last_week: Last week + this_month: This month + last_month: Last month + this_year: This year submission_event_names: send_email_to_html: 'Email sent to %{submitter_name}' send_reminder_email_to_html: 'Reminder email sent to %{submitter_name}' @@ -667,6 +682,9 @@ en: &en read: Read your data es: &es + remove_filter: Eliminar filtro + document_download_filename_format: Formato del nombre del archivo de descarga del documento + document_name: Nombre del documento unarchive: Desarchivar awaiting_completion_by_the_other_party: "Esperando la finalización por la otra parte" enforce_recipients_order: 'Hacer cumplir el orden de los destinatarios' @@ -993,7 +1011,7 @@ es: &es edit_message: Editar mensaje smtp_not_configured: SMTP no configurado configure_smtp_settings_in_order_to_send_emails_: 'Configura los ajustes de SMTP para enviar correos electrónicos:' - go_to_smtp_setting: Ir a la configuración de SMTP + go_to_smtp_settings: Ir a la configuración de SMTP save_as_default_template_message: Guardar como mensaje de plantilla predeterminado re_send_sms: Reenviar SMS send_sms: Enviar SMS @@ -1280,6 +1298,18 @@ es: &es key: Clave value: Valor webhook_secret: Secreto del Webhook + author: Autor + to: A + created_at: Creado el + apply: Aplicar + applying: Aplicando + today: Hoy + yesterday: Ayer + this_week: Esta Semana + last_week: La Semana Pasada + this_month: Este Mes + last_month: El Mes Pasado + this_year: Este Año submission_event_names: send_email_to_html: 'Correo electrónico enviado a %{submitter_name}' send_reminder_email_to_html: 'Correo de recordatorio enviado a %{submitter_name}' @@ -1316,6 +1346,9 @@ es: &es read: Leer tus datos it: &it + remove_filter: Rimuovi filtro + document_download_filename_format: Formato del nome file scaricato + document_name: Nome del Documento unarchive: Ripristina awaiting_completion_by_the_other_party: "In attesa di completamento da parte dell'altra parte" enforce_recipients_order: Aplicar el orden de los destinatarios @@ -1642,7 +1675,7 @@ it: &it edit_message: Modifica messaggio smtp_not_configured: SMTP non configurato configure_smtp_settings_in_order_to_send_emails_: 'Configura le impostazioni SMTP per inviare email:' - go_to_smtp_setting: Vai alle impostazioni SMTP + go_to_smtp_settings: Vai alle impostazioni SMTP save_as_default_template_message: Salva come messaggio modello predefinito re_send_sms: Reinvio SMS send_sms: Invia SMS @@ -1929,6 +1962,18 @@ it: &it key: Chiave value: Valore webhook_secret: Segreto del Webhook + author: Autore + to: A + created_at: Creato il + apply: Applica + applying: Applicazione + today: Oggi + yesterday: Ieri + this_week: Questa Settimana + last_week: Settimana Scorsa + this_month: Questo Mese + last_month: Mese Scorso + this_year: "Quest'Anno" submission_event_names: send_email_to_html: 'E-mail inviato a %{submitter_name}' send_reminder_email_to_html: 'E-mail di promemoria inviato a %{submitter_name}' @@ -1965,6 +2010,9 @@ it: &it read: Leggi i tuoi dati fr: &fr + remove_filter: Supprimer le filtre + document_download_filename_format: Format du nom de fichier du téléchargement de document + document_name: Nom du document unarchive: Désarchiver awaiting_completion_by_the_other_party: "En attente de la complétion par l'autre partie" enforce_recipients_order: "Respecter l'ordre des destinataires" @@ -2292,7 +2340,7 @@ fr: &fr edit_message: Modifier le message smtp_not_configured: SMTP non configuré configure_smtp_settings_in_order_to_send_emails_: 'Configurez les paramètres SMTP pour envoyer des e-mails:' - go_to_smtp_setting: Aller aux paramètres SMTP + go_to_smtp_settings: Aller aux paramètres SMTP save_as_default_template_message: Enregistrer comme modèle de message par défaut re_send_sms: Renvoyer le SMS send_sms: Envoyer SMS @@ -2579,6 +2627,18 @@ fr: &fr key: Clé value: Valeur webhook_secret: Secret du Webhook + author: Auteur + to: À + created_at: Créé le + apply: Appliquer + applying: Application en cours + today: "Aujourd'hui" + yesterday: Hier + this_week: Cette Semaine + last_week: La Semaine Dernière + this_month: Ce Mois-ci + last_month: Le Mois Dernier + this_year: Cette Année submission_event_names: send_email_to_html: 'E-mail envoyé à %{submitter_name}' send_reminder_email_to_html: 'E-mail de rappel envoyé à %{submitter_name}' @@ -2615,6 +2675,9 @@ fr: &fr read: Lire vos données pt: &pt + remove_filter: Remover filtro + document_download_filename_format: Formato do nome do arquivo de download do documento + document_name: Nome do documento unarchive: Desarquivar awaiting_completion_by_the_other_party: "Aguardando a conclusão pela outra parte" enforce_recipients_order: 'Forçar a ordem dos recipientes' @@ -2941,7 +3004,7 @@ pt: &pt edit_message: Editar mensagem smtp_not_configured: SMTP não configurado configure_smtp_settings_in_order_to_send_emails_: 'Configure as configurações de SMTP para enviar e-mails:' - go_to_smtp_setting: Ir para a configuração de SMTP + go_to_smtp_settings: Ir para a configuração de SMTP save_as_default_template_message: Salvar como mensagem de modelo padrão re_send_sms: Reenviar SMS send_sms: Enviar SMS @@ -3228,6 +3291,18 @@ pt: &pt key: Chave value: Valor webhook_secret: Segredo do Webhook + author: Autor + to: Para + created_at: Criado em + apply: Aplicar + applying: Aplicando + today: Hoje + yesterday: Ontem + this_week: Esta Semana + last_week: Semana Passada + this_month: Este Mês + last_month: Mês Passado + this_year: Este Ano submission_event_names: send_email_to_html: 'E-mail enviado para %{submitter_name}' send_reminder_email_to_html: 'E-mail de lembrete enviado para %{submitter_name}' @@ -3264,6 +3339,9 @@ pt: &pt read: Ler seus dados de: &de + remove_filter: Filter entfernen + document_download_filename_format: Format des Dateinamens beim Herunterladen von Dokumenten + document_name: Dokumentname unarchive: Wiederherstellen awaiting_completion_by_the_other_party: "Warten auf die Fertigstellung durch die andere Partei" enforce_recipients_order: 'Empfängerreihenfolge durchsetzen' @@ -3590,7 +3668,7 @@ de: &de edit_message: Nachricht bearbeiten smtp_not_configured: SMTP nicht konfiguriert configure_smtp_settings_in_order_to_send_emails_: 'Konfiguriere die SMTP-Einstellungen, um E-Mails zu senden:' - go_to_smtp_setting: Zu den SMTP-Einstellungen gehen + go_to_smtp_settings: Zu den SMTP-Einstellungen gehen save_as_default_template_message: Als Standardvorlage speichern re_send_sms: SMS erneut senden send_sms: SMS senden @@ -3877,6 +3955,18 @@ de: &de key: Schlüssel value: Wert webhook_secret: Webhook-Geheimnis + author: Autor + to: An + created_at: Erstellt am + apply: Anwenden + applying: Anwenden + today: Heute + yesterday: Gestern + this_week: Diese Woche + last_week: Letzte Woche + this_month: Dieser Monat + last_month: Letzter Monat + this_year: Dieses Jahr submission_event_names: send_email_to_html: 'E-Mail gesendet an %{submitter_name}' send_reminder_email_to_html: 'Erinnerungs-E-Mail gesendet an %{submitter_name}' diff --git a/config/routes.rb b/config/routes.rb index 140503ff7..dd5683164 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -90,6 +90,7 @@ 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' + resources :submissions_filters, only: %i[show], param: 'name' resources :templates, only: %i[new create edit update show destroy] do resource :debug, only: %i[show], controller: 'templates_debug' if Rails.env.development? resources :documents, only: %i[create], controller: 'template_documents' diff --git a/lib/docuseal.rb b/lib/docuseal.rb index 34a440468..e63ef58e4 100644 --- a/lib/docuseal.rb +++ b/lib/docuseal.rb @@ -3,6 +3,7 @@ module Docuseal URL_CACHE = ActiveSupport::Cache::MemoryStore.new PRODUCT_URL = 'https://www.docuseal.com' + PRODUCT_EMAIL_URL = ENV.fetch('PRODUCT_EMAIL_URL', PRODUCT_URL) NEWSLETTER_URL = "#{PRODUCT_URL}/newsletters".freeze ENQUIRIES_URL = "#{PRODUCT_URL}/enquiries".freeze PRODUCT_NAME = 'DocuSeal' diff --git a/lib/replace_email_variables.rb b/lib/replace_email_variables.rb index f403b82c5..572fd0a46 100644 --- a/lib/replace_email_variables.rb +++ b/lib/replace_email_variables.rb @@ -19,6 +19,8 @@ module ReplaceEmailVariables DOCUMENTS_LINKS = /\{+documents\.links\}+/i DOCUMENTS_LINK = /\{+documents\.link\}+/i + EMAIL_HOST = ENV.fetch('EMAIL_HOST', nil) + module_function # rubocop:disable Metrics @@ -64,10 +66,17 @@ def replace(text, var, html_escape: false) def build_submitter_link(submitter, tracking_event_type) if tracking_event_type == 'click_email' + url_options = + if EMAIL_HOST.present? + { host: EMAIL_HOST, protocol: ENV['FORCE_SSL'].present? ? 'https' : 'http' } + else + Docuseal.default_url_options + end + Rails.application.routes.url_helpers.submit_form_url( slug: submitter.slug, t: SubmissionEvents.build_tracking_param(submitter, 'click_email'), - **Docuseal.default_url_options + **url_options ) else Rails.application.routes.url_helpers.submit_form_url( diff --git a/lib/submissions.rb b/lib/submissions.rb index 1c35b0c2b..ab9d98876 100644 --- a/lib/submissions.rb +++ b/lib/submissions.rb @@ -26,7 +26,7 @@ def search(submissions, keyword, search_values: false, search_template: false) arel = arel.or(Template.arel_table[:name].lower.matches("%#{keyword.downcase}%")) end - submissions.joins(:submitters).where(arel).distinct + submissions.joins(:submitters).where(arel).group(:id) end def update_template_fields!(submission) @@ -88,17 +88,19 @@ def create_from_submitters(template:, user:, submissions_attrs:, source:, ) end - def send_signature_requests(submissions) - submissions.each do |submission| + def send_signature_requests(submissions, delay: nil) + submissions.each_with_index do |submission, index| + delay_seconds = (delay + index).seconds if delay + submitters = submission.submitters.reject(&:completed_at?) if submission.submitters_order_preserved? first_submitter = submission.template_submitters.filter_map { |s| submitters.find { |e| e.uuid == s['uuid'] } }.first - Submitters.send_signature_requests([first_submitter]) if first_submitter + Submitters.send_signature_requests([first_submitter], delay_seconds:) if first_submitter else - Submitters.send_signature_requests(submitters) + Submitters.send_signature_requests(submitters, delay_seconds:) end end end diff --git a/lib/submissions/filter.rb b/lib/submissions/filter.rb new file mode 100644 index 000000000..f4a2271a2 --- /dev/null +++ b/lib/submissions/filter.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +module Submissions + module Filter + ALLOWED_PARAMS = %w[ + author + completed_at_from + completed_at_to + created_at_from + created_at_to + ].freeze + + DATE_PARAMS = %w[ + completed_at_from + completed_at_to + created_at_from + created_at_to + ].freeze + + module_function + + def call(submissions, current_user, params) + filters = normalize_filter_params(params, current_user) + + if filters[:author].present? + user = current_user.account.users.find_by(email: filters[:author]) + submissions = submissions.where(created_by_user_id: user&.id || -1) + end + + submissions = submissions.where(created_at: filters[:created_at_from]..) if filters[:created_at_from].present? + + if filters[:created_at_to].present? + submissions = submissions.where(created_at: ..filters[:created_at_to].end_of_day) + end + + if filters[:completed_at_from].present? || filters[:completed_at_to].present? + completed_arel = Submitter.arel_table[:completed_at].maximum + submissions = submissions.completed.joins(:submitters).group(:id) + + if filters[:completed_at_from].present? + submissions = submissions.having(completed_arel.gteq(filters[:completed_at_from])) + end + + if filters[:completed_at_to].present? + submissions = submissions.having(completed_arel.lteq(filters[:completed_at_to].end_of_day)) + end + end + + submissions + end + + def normalize_filter_params(params, current_user) + tz = ActiveSupport::TimeZone[current_user.account.timezone] || Time.zone + + ALLOWED_PARAMS.each_with_object({}) do |key, acc| + next if params[key].blank? + + value = DATE_PARAMS.include?(key) ? tz.parse(params[key]) : params[key] + + acc[key.to_sym] = value + end + end + end +end diff --git a/lib/submissions/generate_audit_trail.rb b/lib/submissions/generate_audit_trail.rb index 1e3e2dd1f..ab932be8d 100644 --- a/lib/submissions/generate_audit_trail.rb +++ b/lib/submissions/generate_audit_trail.rb @@ -23,7 +23,7 @@ module GenerateAuditTrail RTL_REGEXP = TextUtils::RTL_REGEXP MAX_IMAGE_HEIGHT = 100 - US_TIMEZONES = %w[EST CST MST PST HST AKDT].freeze + US_TIMEZONES = TimeUtils::US_TIMEZONES module_function @@ -64,7 +64,10 @@ def call(submission) def build_audit_trail(submission) account = submission.account - verify_url = Rails.application.routes.url_helpers.settings_esign_url(**Docuseal.default_url_options) + verify_url = Rails.application.routes.url_helpers.settings_esign_url( + **Docuseal.default_url_options, host: ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host]) + ) + page_size = if TimeUtils.timezone_abbr(account.timezone, Time.current.beginning_of_year).in?(US_TIMEZONES) :Letter @@ -411,7 +414,7 @@ def add_logo(column, _submission = nil) column.image(PdfIcons.logo_io, width: 40, height: 40, position: :float) column.formatted_text([{ text: 'DocuSeal', - link: Docuseal::PRODUCT_URL }], + link: Docuseal::PRODUCT_EMAIL_URL }], font_size: 20, font: [FONT_NAME, { variant: :bold }], width: 100, diff --git a/lib/submissions/generate_export_files.rb b/lib/submissions/generate_export_files.rb index ac7cca5bf..506ee1ba5 100644 --- a/lib/submissions/generate_export_files.rb +++ b/lib/submissions/generate_export_files.rb @@ -110,6 +110,10 @@ def build_submission_data(submitter, submitter_name, submitters_count) { name: column_name(I18n.t('completed_at'), submitter_name, submitters_count), value: submitter.completed_at.to_s + }, + { + name: column_name(I18n.t('link'), submitter_name, submitters_count), + value: submitter.completed_at? ? nil : r.submit_form_url(slug: submitter.slug, **Docuseal.default_url_options) } ].reject { |e| e[:value].blank? } end @@ -150,5 +154,9 @@ def submitter_formatted_fields(submitter) { name: template_field_name, uuid: template_field['uuid'], value: } end end + + def r + Rails.application.routes.url_helpers + end end end diff --git a/lib/submitters.rb b/lib/submitters.rb index 18b2d1517..bfd168ad2 100644 --- a/lib/submitters.rb +++ b/lib/submitters.rb @@ -94,12 +94,16 @@ def normalize_preferences(account, user, params) preferences end - def send_signature_requests(submitters) - submitters.each do |submitter| + def send_signature_requests(submitters, delay_seconds: nil) + submitters.each_with_index do |submitter, index| next if submitter.email.blank? next if submitter.preferences['send_email'] == false - SendSubmitterInvitationEmailJob.perform_async('submitter_id' => submitter.id) + if delay_seconds + SendSubmitterInvitationEmailJob.perform_in((delay_seconds + index).seconds, 'submitter_id' => submitter.id) + else + SendSubmitterInvitationEmailJob.perform_async('submitter_id' => submitter.id) + end end end @@ -112,4 +116,19 @@ def current_submitter_order?(submitter) acc && submitter.submission.submitters.find { |e| e.uuid == item['uuid'] }&.completed_at? end end + + def build_document_filename(submitter, blob, filename_format) + return blob.filename.to_s if filename_format.blank? + + filename = ReplaceEmailVariables.call(filename_format, submitter:) + + filename = filename.gsub('{document.name}', blob.filename.base) + + filename = filename.gsub( + '{submission.completed_at}', + I18n.l(submitter.completed_at.beginning_of_year.in_time_zone(submitter.account.timezone), format: :short) + ) + + "#{filename}.#{blob.filename.extension}" + end end diff --git a/lib/submitters/submit_values.rb b/lib/submitters/submit_values.rb index 40e96618c..65ecad757 100644 --- a/lib/submitters/submit_values.rb +++ b/lib/submitters/submit_values.rb @@ -93,7 +93,11 @@ def normalized_values(params) if params[:cast_boolean] == 'true' v == 'true' elsif params[:cast_number] == 'true' - (v.to_f % 1).zero? ? v.to_i : v.to_f + if v == '' + nil + else + (v.to_f % 1).zero? ? v.to_i : v.to_f + end elsif params[:normalize_phone] == 'true' v.to_s.gsub(/[^0-9+]/, '') else @@ -207,7 +211,7 @@ def check_field_condition(submitter, field, fields_uuid_index) end def replace_default_variables(value, attrs, submission, with_time: false) - return value if value.in?([true, false]) + return value if value.in?([true, false]) || value.is_a?(Numeric) return if value.blank? value.to_s.gsub(VARIABLE_REGEXP) do |e| diff --git a/lib/time_utils.rb b/lib/time_utils.rb index 1f4991958..9150b1b97 100644 --- a/lib/time_utils.rb +++ b/lib/time_utils.rb @@ -22,6 +22,8 @@ module TimeUtils DEFAULT_DATE_FORMAT_US = 'MM/DD/YYYY' DEFAULT_DATE_FORMAT = 'DD/MM/YYYY' + US_TIMEZONES = %w[EST CST MST PST HST AKDT].freeze + module_function def timezone_abbr(timezone, time = Time.current) diff --git a/spec/factories/submissions.rb b/spec/factories/submissions.rb index 7a055dbed..ea59021ee 100644 --- a/spec/factories/submissions.rb +++ b/spec/factories/submissions.rb @@ -17,7 +17,8 @@ submission.template_submitters.each do |template_submitter| create(:submitter, submission:, account_id: submission.account_id, - uuid: template_submitter['uuid']) + uuid: template_submitter['uuid'], + created_at: submission.created_at) end end end diff --git a/spec/factories/submitters.rb b/spec/factories/submitters.rb index 7bd771b0c..d478186ef 100644 --- a/spec/factories/submitters.rb +++ b/spec/factories/submitters.rb @@ -5,7 +5,6 @@ submission email { Faker::Internet.email } name { Faker::Name.name } - phone { Faker::PhoneNumber.phone_number } before(:create) do |submitter, _| submitter.account_id = submitter.submission.account_id diff --git a/spec/factories/templates.rb b/spec/factories/templates.rb index f4f2ff624..0f7375121 100644 --- a/spec/factories/templates.rb +++ b/spec/factories/templates.rb @@ -9,9 +9,13 @@ transient do submitter_count { 1 } + only_field_types do + %w[text date checkbox radio signature number multiple select initials image file stamp cells phone payment] + end + except_field_types { [] } end - after(:create) do |template, evaluator| + after(:create) do |template, ev| blob = ActiveStorage::Blob.create_and_upload!( io: Rails.root.join('spec/fixtures/sample-document.pdf').open, filename: 'sample-document.pdf', @@ -28,7 +32,7 @@ template.schema = [{ attachment_uuid: attachment.uuid, name: 'sample-document' }] number_words = %w[first second third fourth fifth sixth seventh eighth ninth tenth] - template.submitters = Array.new(evaluator.submitter_count) do |i| + template.submitters = Array.new(ev.submitter_count) do |i| { 'name' => "#{number_words[i]&.capitalize} Party", 'uuid' => SecureRandom.uuid @@ -43,12 +47,71 @@ 'name' => 'First Name', 'type' => 'text', 'required' => true, + 'preferences' => {}, + 'areas' => [ + { + 'x' => 0.09273546006944444, + 'y' => 0.1099851117387033, + 'w' => 0.2701497395833333, + 'h' => 0.0372705365913556, + 'attachment_uuid' => attachment.uuid, + 'page' => 0 + } + ] + }, + { + 'uuid' => SecureRandom.uuid, + 'submitter_uuid' => submitter['uuid'], + 'name' => 'Birthday', + 'type' => 'date', + 'required' => true, + 'preferences' => { 'format' => 'DD/MM/YYYY' }, + 'areas' => [ + { + 'x' => 0.09166666666666666, + 'y' => 0.1762778204144282, + 'w' => 0.2763888888888889, + 'h' => 0.0359029261474578, + 'attachment_uuid' => attachment.uuid, + 'page' => 0 + } + ] + }, + { + 'uuid' => SecureRandom.uuid, + 'submitter_uuid' => submitter['uuid'], + 'name' => 'Do you agree?', + 'type' => 'checkbox', + 'required' => true, + 'preferences' => {}, + 'areas' => [ + { + 'x' => 0.09051106770833334, + 'y' => 0.227587027259332, + 'w' => 0.2784450954861111, + 'h' => 0.04113074042239687, + 'attachment_uuid' => attachment.uuid, + 'page' => 0 + } + ] + }, + { + 'uuid' => SecureRandom.uuid, + 'submitter_uuid' => submitter['uuid'], + 'name' => 'First child', + 'type' => 'radio', + 'required' => true, + 'preferences' => {}, + 'options' => [ + { 'value' => 'Girl', 'uuid' => SecureRandom.uuid }, + { 'value' => 'Boy', 'uuid' => SecureRandom.uuid } + ], 'areas' => [ { 'x' => 0.09027777777777778, - 'y' => 0.1197252208047105, - 'w' => 0.3069444444444444, - 'h' => 0.03336604514229637, + 'y' => 0.3020184190330008, + 'w' => 0.2, + 'h' => 0.02857142857142857, 'attachment_uuid' => attachment.uuid, 'page' => 0 } @@ -57,12 +120,213 @@ { 'uuid' => SecureRandom.uuid, 'submitter_uuid' => submitter['uuid'], - 'name' => '', + 'name' => 'Signature', 'type' => 'signature', 'required' => true, - 'areas' => [] + 'preferences' => {}, + 'areas' => [ + { + 'x' => 0.08611111111111111, + 'y' => 0.3487183422870299, + 'w' => 0.2, + 'h' => 0.0707269155206287, + 'attachment_uuid' => attachment.uuid, + 'page' => 0 + } + ] + }, + { + 'uuid' => SecureRandom.uuid, + 'submitter_uuid' => submitter['uuid'], + 'name' => 'House number', + 'type' => 'number', + 'required' => true, + 'preferences' => {}, + 'areas' => [ + { + 'x' => 0.08333333333333333, + 'y' => 0.4582041442824252, + 'w' => 0.2, + 'h' => 0.02857142857142857, + 'attachment_uuid' => attachment.uuid, + 'page' => 0 + } + ] + }, + { + 'uuid' => SecureRandom.uuid, + 'submitter_uuid' => submitter['uuid'], + 'name' => 'Colors', + 'type' => 'multiple', + 'required' => true, + 'preferences' => {}, + 'options' => [ + { 'value' => 'Red', 'uuid' => SecureRandom.uuid }, + { 'value' => 'Green', 'uuid' => SecureRandom.uuid }, + { 'value' => 'Blue', 'uuid' => SecureRandom.uuid } + ], + 'areas' => [ + { + 'x' => 0.45, + 'y' => 0.1133998465080583, + 'w' => 0.2, + 'h' => 0.02857142857142857, + 'attachment_uuid' => attachment.uuid, + 'page' => 0 + } + ] + }, + { + 'uuid' => SecureRandom.uuid, + 'submitter_uuid' => submitter['uuid'], + 'name' => 'Gender', + 'type' => 'select', + 'required' => true, + 'preferences' => {}, + 'options' => [ + { 'value' => 'Male', 'uuid' => SecureRandom.uuid }, + { 'value' => 'Female', 'uuid' => SecureRandom.uuid } + ], + 'areas' => [ + { + 'x' => 0.4513888888888889, + 'y' => 0.1752954719877206, + 'w' => 0.2, + 'h' => 0.02857142857142857, + 'attachment_uuid' => attachment.uuid, + 'page' => 0 + } + ] + }, + { + 'uuid' => SecureRandom.uuid, + 'submitter_uuid' => submitter['uuid'], + 'name' => 'Initials', + 'type' => 'initials', + 'required' => true, + 'preferences' => {}, + 'areas' => [ + { + 'x' => 0.4486111111111111, + 'y' => 0.2273599386032233, + 'w' => 0.1, + 'h' => 0.02857142857142857, + 'attachment_uuid' => attachment.uuid, + 'page' => 0 + } + ] + }, + { + 'uuid' => SecureRandom.uuid, + 'submitter_uuid' => submitter['uuid'], + 'name' => 'Avatar', + 'type' => 'image', + 'required' => true, + 'preferences' => {}, + 'areas' => [ + { + 'x' => 0.7180555555555556, + 'y' => 0.1129547198772064, + 'w' => 0.2, + 'h' => 0.1414538310412574, + 'attachment_uuid' => attachment.uuid, + 'page' => 0 + } + ] + }, + { + 'uuid' => SecureRandom.uuid, + 'submitter_uuid' => submitter['uuid'], + 'name' => 'Attachment', + 'type' => 'file', + 'required' => true, + 'preferences' => {}, + 'areas' => [ + { + 'x' => 0.7166666666666667, + 'y' => 0.3020107444359171, + 'w' => 0.2, + 'h' => 0.02857142857142857, + 'attachment_uuid' => attachment.uuid, + 'page' => 0 + } + ] + }, + { + 'uuid' => SecureRandom.uuid, + 'submitter_uuid' => submitter['uuid'], + 'name' => 'Stamp', + 'type' => 'stamp', + 'required' => true, + 'readonly' => true, + 'preferences' => {}, + 'areas' => [ + { + 'x' => 0.7166666666666667, + 'y' => 0.3771910974673829, + 'w' => 0.2, + 'h' => 0.0707269155206287, + 'attachment_uuid' => attachment.uuid, + 'page' => 0 + } + ] + }, + { + 'uuid' => SecureRandom.uuid, + 'submitter_uuid' => submitter['uuid'], + 'name' => 'Cell code', + 'type' => 'cells', + 'required' => true, + 'preferences' => {}, + 'areas' => [ + { + 'x' => 0.4472222222222222, + 'y' => 0.3530851880276286, + 'w' => 0.2, + 'h' => 0.02857142857142857, + 'cell_w' => 0.04, + 'attachment_uuid' => attachment.uuid, + 'page' => 0 + } + ] + }, + { + 'uuid' => SecureRandom.uuid, + 'submitter_uuid' => submitter['uuid'], + 'name' => 'Payment', + 'type' => 'payment', + 'required' => true, + 'preferences' => { 'currency' => 'EUR', 'price' => 1000 }, + 'areas' => [ + { + 'x' => 0.4486111111111111, + 'y' => 0.43168073676132, + 'w' => 0.2, + 'h' => 0.02857142857142857, + 'attachment_uuid' => attachment.uuid, + 'page' => 0 + } + ] + }, + { + 'uuid' => SecureRandom.uuid, + 'submitter_uuid' => submitter['uuid'], + 'name' => 'Mobile Phone', + 'type' => 'phone', + 'required' => true, + 'preferences' => {}, + 'areas' => [ + { + 'x' => 0.44443359375, + 'y' => 0.3010283960092095, + 'w' => 0.2, + 'h' => 0.02857142857142857, + 'attachment_uuid' => attachment.uuid, + 'page' => 0 + } + ] } - ] + ].select { |f| ev.only_field_types.include?(f['type']) && ev.except_field_types.exclude?(f['type']) } fields end diff --git a/spec/fixtures/sample-image.png b/spec/fixtures/sample-image.png new file mode 100644 index 000000000..f86af5d88 Binary files /dev/null and b/spec/fixtures/sample-image.png differ diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 86a15a9f8..1157da614 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -10,6 +10,7 @@ require 'capybara/rspec' require 'webmock/rspec' require 'sidekiq/testing' +require 'signing_form_helper' Sidekiq::Testing.fake! @@ -50,6 +51,7 @@ config.include FactoryBot::Syntax::Methods config.include Devise::Test::IntegrationHelpers + config.include SigningFormHelper config.before(:each, type: :system) do if ENV['HEADLESS'] == 'false' @@ -67,3 +69,5 @@ Sidekiq::Testing.inline! if example.metadata[:sidekiq] == :inline end end + +ActiveSupport.run_load_hooks(:rails_specs, self) diff --git a/spec/requests/templates_spec.rb b/spec/requests/templates_spec.rb index 26bdb5462..3d6e1284b 100644 --- a/spec/requests/templates_spec.rb +++ b/spec/requests/templates_spec.rb @@ -176,33 +176,7 @@ def template_body(template) id: template.id, slug: template.slug, name: template.name, - fields: [ - { - 'uuid' => template.fields[0]['uuid'], - 'submitter_uuid' => template.submitters[0]['uuid'], - 'name' => 'First Name', - 'type' => 'text', - 'required' => true, - 'areas' => [ - { - 'x' => 0.09027777777777778, - 'y' => 0.1197252208047105, - 'w' => 0.3069444444444444, - 'h' => 0.03336604514229637, - 'attachment_uuid' => template_attachment_uuid, - 'page' => 0 - } - ] - }, - { - 'uuid' => template.fields[1]['uuid'], - 'submitter_uuid' => template.submitters[0]['uuid'], - 'name' => '', - 'type' => 'signature', - 'required' => true, - 'areas' => [] - } - ], + fields: template.fields, submitters: [ { name: 'First Party', diff --git a/spec/signing_form_helper.rb b/spec/signing_form_helper.rb new file mode 100644 index 000000000..13aadfc26 --- /dev/null +++ b/spec/signing_form_helper.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module SigningFormHelper + module_function + + def draw_canvas + page.find('canvas').click([], { x: 150, y: 100 }) + page.execute_script <<~JS + const canvas = document.getElementsByTagName('canvas')[0]; + const ctx = canvas.getContext('2d'); + + ctx.beginPath(); + ctx.moveTo(150, 100); + ctx.lineTo(450, 100); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(150, 100); + ctx.lineTo(150, 150); + ctx.stroke(); + JS + sleep 1 + end + + def field_value(submitter, field_name) + field = template_field(submitter.template, field_name) + + submitter.values[field['uuid']] + end + + def template_field(template, field_name) + template.fields.find { |f| f['name'] == field_name || f['title'] == field_name } || {} + end +end diff --git a/spec/system/personalization_spec.rb b/spec/system/personalization_spec.rb index d5e4f1131..f82173a1c 100644 --- a/spec/system/personalization_spec.rb +++ b/spec/system/personalization_spec.rb @@ -17,7 +17,5 @@ expect(page).to have_content('Completed Notification Email') expect(page).to have_content('Documents Copy Email') expect(page).to have_content('Company Logo') - expect(page).to have_content('Unlock with DocuSeal Pro') - expect(page).to have_content('Display your company name and logo when signing documents') end end diff --git a/spec/system/signing_form_spec.rb b/spec/system/signing_form_spec.rb new file mode 100644 index 000000000..349e029bc --- /dev/null +++ b/spec/system/signing_form_spec.rb @@ -0,0 +1,598 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Signing Form', type: :system do + let(:account) { create(:account) } + 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]) } + + before do + visit start_form_path(slug: template.slug) + end + + it 'shows the email step', type: :system do + expect(page).to have_content('You have been invited to submit a form') + expect(page).to have_content("Invited by #{account.name}") + expect(page).to have_field('Email', type: 'email') + expect(page).to have_button('Start') + end + + it 'completes the form' do + # Submit's email step + fill_in 'Email', with: 'john.dou@example.com' + click_button 'Start' + + # Text step + fill_in 'First Name', with: 'John' + click_button 'next' + + # Date step + fill_in 'Birthday', with: I18n.l(20.years.ago, format: '%Y-%m-%d') + click_button 'next' + + # Checkbox step + check 'Do you agree?' + click_button 'next' + + # Radio step + choose 'Boy' + click_button 'next' + + # Signature step + draw_canvas + click_button 'next' + + # Number step + fill_in 'House number', with: '123' + click_button 'next' + + # Multiple choice step + %w[Red Blue].each { |color| check color } + click_button 'next' + + # Select step + select 'Male', from: 'Gender' + click_button 'next' + + # Initials step + draw_canvas + click_button 'next' + + # Image step + find('#dropzone').click + find('input[type="file"]', visible: false).attach_file(Rails.root.join('spec/fixtures/sample-image.png')) + click_button 'next' + + # File step + find('#dropzone').click + find('input[type="file"]', visible: false).attach_file(Rails.root.join('spec/fixtures/sample-document.pdf')) + click_button 'next' + + # Cell step + fill_in 'Cell code', with: '123' + click_on 'Complete' + + expect(page).to have_button('Download') + expect(page).to have_content('Document has been signed!') + + submitter = template.submissions.last.submitters.last + + expect(submitter.email).to eq('john.dou@example.com') + expect(submitter.ip).to eq('127.0.0.1') + expect(submitter.ua).to be_present + expect(submitter.opened_at).to be_present + expect(submitter.completed_at).to be_present + expect(submitter.declined_at).to be_nil + + expect(field_value(submitter, 'First Name')).to eq 'John' + expect(field_value(submitter, 'Birthday')).to eq 20.years.ago.strftime('%Y-%m-%d') + expect(field_value(submitter, 'Do you agree?')).to be_truthy + expect(field_value(submitter, 'First child')).to eq 'Boy' + expect(field_value(submitter, 'Signature')).to be_present + expect(field_value(submitter, 'House number')).to eq 123 + expect(field_value(submitter, 'Colors')).to contain_exactly('Red', 'Blue') + expect(field_value(submitter, 'Gender')).to eq 'Male' + expect(field_value(submitter, 'Initials')).to be_present + expect(field_value(submitter, 'Avatar')).to be_present + expect(field_value(submitter, 'Attachment')).to be_present + expect(field_value(submitter, 'Cell code')).to eq '123' + end + end + + context 'when the submitter form link is opened' do + let(:template) { create(:template, account:, author:, except_field_types: %w[phone payment stamp]) } + let(:submission) { create(:submission, template:) } + let(:submitter) do + create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:, email: 'robin@example.com') + end + + before do + visit submit_form_path(slug: submitter.slug) + end + + it 'complete the form' do + # Text step + fill_in 'First Name', with: 'John' + click_button 'next' + + # Date step + fill_in 'Birthday', with: I18n.l(20.years.ago, format: '%Y-%m-%d') + click_button 'next' + + # Checkbox step + check 'Do you agree?' + click_button 'next' + + # Radio step + choose 'Boy' + click_button 'next' + + # Signature step + draw_canvas + click_button 'next' + + # Number step + fill_in 'House number', with: '123' + click_button 'next' + + # Multiple choice step + %w[Red Blue].each { |color| check color } + click_button 'next' + + # Select step + select 'Male', from: 'Gender' + click_button 'next' + + # Initials step + draw_canvas + click_button 'next' + + # Image step + find('#dropzone').click + find('input[type="file"]', visible: false).attach_file(Rails.root.join('spec/fixtures/sample-image.png')) + click_button 'next' + + # File step + find('#dropzone').click + find('input[type="file"]', visible: false).attach_file(Rails.root.join('spec/fixtures/sample-document.pdf')) + click_button 'next' + + # Cell step + fill_in 'Cell code', with: '123' + click_on 'Complete' + + expect(page).to have_button('Download') + expect(page).to have_content('Document has been signed!') + + submitter.reload + + expect(submitter.email).to eq 'robin@example.com' + expect(submitter.ip).to eq('127.0.0.1') + expect(submitter.ua).to be_present + expect(submitter.opened_at).to be_present + expect(submitter.completed_at).to be_present + expect(submitter.declined_at).to be_nil + + expect(field_value(submitter, 'First Name')).to eq 'John' + expect(field_value(submitter, 'Birthday')).to eq 20.years.ago.strftime('%Y-%m-%d') + expect(field_value(submitter, 'Do you agree?')).to be_truthy + expect(field_value(submitter, 'First child')).to eq 'Boy' + expect(field_value(submitter, 'Signature')).to be_present + expect(field_value(submitter, 'House number')).to eq 123 + expect(field_value(submitter, 'Colors')).to contain_exactly('Red', 'Blue') + expect(field_value(submitter, 'Gender')).to eq 'Male' + expect(field_value(submitter, 'Initials')).to be_present + expect(field_value(submitter, 'Avatar')).to be_present + expect(field_value(submitter, 'Attachment')).to be_present + expect(field_value(submitter, 'Cell code')).to eq '123' + end + end + + context 'when the text step' do + let(:template) { create(:template, account:, author:, only_field_types: %w[text]) } + let(:submission) { create(:submission, template:) } + let(:submitter) do + create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:) + end + + it 'completes the form if the field is filled' do + visit submit_form_path(slug: submitter.slug) + + input = find_field('First Name') + + expect(input[:required]).to be_truthy + expect(input[:placeholder]).to eq 'Type here...' + + fill_in 'First Name', with: 'Mary' + click_button 'Complete' + + expect(page).to have_content('Form has been completed!') + + submitter.reload + + expect(submitter.completed_at).to be_present + expect(field_value(submitter, 'First Name')).to eq 'Mary' + end + + it 'toggle multiple text button' do + visit submit_form_path(slug: submitter.slug) + + input = find_field('First Name') + + expect(input.tag_name).to eq('input') + + find(:css, 'div[data-tip="Toggle Multiline Text"]').click + + input = find_field('First Name') + + expect(input.tag_name).to eq('textarea') + expect(page).not_to have_selector(:css, 'div[data-tip="Toggle Multiline Text"]') + + fill_in 'First Name', with: 'Very long text' + click_button 'Complete' + + expect(page).to have_content('Form has been completed!') + + submitter.reload + + expect(field_value(submitter, 'First Name')).to eq 'Very long text' + expect(submitter.completed_at).to be_present + end + end + + context 'when the date step' do + let(:template) { create(:template, account:, author:, only_field_types: %w[date]) } + let(:submission) { create(:submission, template:) } + let(:submitter) do + create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:) + end + + it 'completes the form if the field is filled' do + visit submit_form_path(slug: submitter.slug) + + input = find_field('Birthday') + + expect(input[:required]).to be_truthy + + fill_in 'Birthday', with: I18n.l(25.years.ago, format: '%Y-%m-%d') + click_button 'Complete' + + expect(page).to have_content('Form has been completed!') + + submitter.reload + + expect(submitter.completed_at).to be_present + expect(field_value(submitter, 'Birthday')).to eq 25.years.ago.strftime('%Y-%m-%d') + end + + it 'pre-fills the current date into the form field' do + visit submit_form_path(slug: submitter.slug) + + input = find_field('Birthday') + + expect(input[:value]).to eq '' + + click_button 'Set Today' + + input = find_field('Birthday') + + expect(input[:value]).to eq Time.zone.now.strftime('%Y-%m-%d') + + click_button 'Complete' + + expect(page).to have_content('Form has been completed!') + + submitter.reload + + expect(submitter.completed_at).to be_present + expect(field_value(submitter, 'Birthday')).to eq Time.zone.now.strftime('%Y-%m-%d') + end + end + + context 'when the checkbox step' do + let(:template) { create(:template, account:, author:, only_field_types: %w[checkbox]) } + let(:submission) { create(:submission, template:) } + let(:submitter) do + create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:) + end + + it 'completes the form if the checkbox is checked' do + visit submit_form_path(slug: submitter.slug) + + check 'Do you agree?' + click_button 'Complete' + + expect(page).to have_content('Form has been completed!') + + submitter.reload + + expect(submitter.completed_at).to be_present + expect(field_value(submitter, 'Do you agree?')).to be true + end + end + + context 'when the radio step' do + let(:template) { create(:template, account:, author:, only_field_types: %w[radio]) } + let(:submission) { create(:submission, template:) } + let(:submitter) do + create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:) + end + + it 'completes the form if the checkbox is checked' do + visit submit_form_path(slug: submitter.slug) + + %w[Girl Boy].map { |v| find_field(v) }.each { |input| expect(input[:required]).to be_truthy } + + choose 'Boy' + click_button 'Complete' + + expect(page).to have_content('Form has been completed!') + + submitter.reload + + expect(submitter.completed_at).to be_present + expect(field_value(submitter, 'First child')).to eq 'Boy' + end + end + + context 'when the signature step' do + let(:template) { create(:template, account:, author:, only_field_types: %w[signature]) } + let(:submission) { create(:submission, template:) } + let(:submitter) do + create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:) + end + + it 'completes the form if the canvas is drawn' do + visit submit_form_path(slug: submitter.slug) + + find('#expand_form_button').click + draw_canvas + click_button 'Sign and Complete' + + expect(page).to have_content('Document has been signed!') + + submitter.reload + + expect(submitter.completed_at).to be_present + expect(field_value(submitter, 'Signature')).to be_present + end + + it 'completes the form if the canvas is typed' do + visit submit_form_path(slug: submitter.slug) + + find('#expand_form_button').click + click_link 'Type' + fill_in 'signature_text_input', with: 'John Doe' + click_button 'Sign and Complete' + + expect(page).to have_content('Document has been signed!') + + submitter.reload + + expect(submitter.completed_at).to be_present + expect(field_value(submitter, 'Signature')).to be_present + end + end + + context 'when the number step' do + let(:template) { create(:template, account:, author:, only_field_types: %w[number]) } + let(:submission) { create(:submission, template:) } + let(:submitter) do + create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:) + end + + it 'completes the form if the field is filled' do + visit submit_form_path(slug: submitter.slug) + + input = find_field('House number') + + expect(input[:required]).to be_truthy + expect(input[:placeholder]).to eq 'Type here...' + + fill_in 'House number', with: '4' + click_button 'Complete' + + expect(page).to have_content('Form has been completed!') + + submitter.reload + + expect(submitter.completed_at).to be_present + expect(field_value(submitter, 'House number')).to eq 4 + end + end + + context 'when the multiple choice step' do + let(:template) { create(:template, account:, author:, only_field_types: %w[multiple]) } + let(:submission) { create(:submission, template:) } + let(:submitter) do + create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:) + end + + it 'completes the form if the multiple choice is checked' do + visit submit_form_path(slug: submitter.slug) + + %w[Red Green].each { |color| check color } + click_button 'Complete' + + expect(page).to have_content('Form has been completed!') + + submitter.reload + + expect(submitter.completed_at).to be_present + expect(field_value(submitter, 'Colors')).to contain_exactly('Red', 'Green') + end + end + + context 'when the select step' do + let(:template) { create(:template, account:, author:, only_field_types: %w[select]) } + let(:submission) { create(:submission, template:) } + let(:submitter) do + create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:) + end + + it 'completes the form if the multiple choice is checked' do + visit submit_form_path(slug: submitter.slug) + + select 'Female', from: 'Gender' + + click_button 'Complete' + + expect(page).to have_content('Form has been completed!') + + submitter.reload + + expect(submitter.completed_at).to be_present + expect(field_value(submitter, 'Gender')).to eq 'Female' + end + end + + context 'when the initials step' do + let(:template) { create(:template, account:, author:, only_field_types: %w[initials]) } + let(:submission) { create(:submission, template:) } + let(:submitter) do + create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:) + end + + it 'completes the form if the canvas is typed' do + visit submit_form_path(slug: submitter.slug) + + find('#expand_form_button').click + fill_in 'initials_text_input', with: 'John Doe' + click_button 'Complete' + + expect(page).to have_content('Document has been signed!') + + submitter.reload + + expect(submitter.completed_at).to be_present + expect(field_value(submitter, 'Initials')).to be_present + end + + it 'completes the form if the canvas is drawn' do + visit submit_form_path(slug: submitter.slug) + + find('#expand_form_button').click + click_link 'Draw' + draw_canvas + click_button 'Complete' + + expect(page).to have_content('Document has been signed!') + + submitter.reload + + expect(submitter.completed_at).to be_present + expect(field_value(submitter, 'Initials')).to be_present + end + + it 'completes the form if the initials is uploaded' do + visit submit_form_path(slug: submitter.slug) + + find('#expand_form_button').click + find('span[data-tip="Click to upload"]').click + find('input[type="file"]', visible: false).attach_file(Rails.root.join('spec/fixtures/sample-image.png')) + click_button 'Complete' + + expect(page).to have_content('Document has been signed!') + + submitter.reload + + expect(submitter.completed_at).to be_present + expect(field_value(submitter, 'Initials')).to be_present + end + end + + context 'when the image step' do + let(:template) { create(:template, account:, author:, only_field_types: %w[image]) } + let(:submission) { create(:submission, template:) } + let(:submitter) do + create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:) + end + + it 'completes the form if the image is uploaded' do + visit submit_form_path(slug: submitter.slug) + + find('#expand_form_button').click + find('#dropzone').click + find('input[type="file"]', visible: false).attach_file(Rails.root.join('spec/fixtures/sample-image.png')) + click_button 'Complete' + + expect(page).to have_content('Form has been completed!') + + submitter.reload + + expect(submitter.completed_at).to be_present + expect(field_value(submitter, 'Avatar')).to be_present + end + end + + context 'when the file step' do + let(:template) { create(:template, account:, author:, only_field_types: %w[file]) } + let(:submission) { create(:submission, template:) } + let(:submitter) do + create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:) + end + + it 'completes the form if the file is uploaded' do + visit submit_form_path(slug: submitter.slug) + + find('#expand_form_button').click + find('#dropzone').click + find('input[type="file"]', visible: false).attach_file(Rails.root.join('spec/fixtures/sample-document.pdf')) + click_button 'Complete' + + expect(page).to have_content('Form has been completed!') + + submitter.reload + + expect(submitter.completed_at).to be_present + expect(field_value(submitter, 'Attachment')).to be_present + end + end + + context 'when the cells step' do + let(:template) { create(:template, account:, author:, only_field_types: %w[cells]) } + let(:submission) { create(:submission, template:) } + let(:submitter) do + create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:) + end + + it 'completes the form if the field is filled' do + visit submit_form_path(slug: submitter.slug) + + input = find_field('Cell code') + + expect(input[:required]).to be_truthy + expect(input[:placeholder]).to eq 'Type here...' + + fill_in 'Cell code', with: '456' + click_button 'Complete' + + expect(page).to have_content('Form has been completed!') + + submitter.reload + + expect(submitter.completed_at).to be_present + expect(field_value(submitter, 'Cell code')).to eq '456' + end + end + + it 'sends completed email' do + template = create(:template, account:, author:, only_field_types: %w[text signature]) + submission = create(:submission, template:) + submitter = create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:) + + visit submit_form_path(slug: submitter.slug) + + fill_in 'First Name', with: 'Adam' + click_on 'next' + click_link 'Type' + fill_in 'signature_text_input', with: 'Adam' + + expect do + click_on 'Sign and Complete' + end.to change(ProcessSubmitterCompletionJob.jobs, :size).by(1) + end +end diff --git a/spec/system/submit_form_spec.rb b/spec/system/submit_form_spec.rb deleted file mode 100644 index 7c2f32230..000000000 --- a/spec/system/submit_form_spec.rb +++ /dev/null @@ -1,87 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Submit Form' do - let(:account) { create(:account) } - let(:user) { create(:user, account:) } - let(:template) { create(:template, account:, author: user) } - - before do - sign_in(user) - end - - context 'when initialized by shared link' do - before do - visit start_form_path(slug: template.slug) - end - - it 'shows start form page' do - expect(page).to have_content('You have been invited to submit a form') - expect(page).to have_content(template.name) - expect(page).to have_content("Invited by #{template.account.name}") - end - - it 'complete the form' do - fill_in 'Email', with: 'john.dou@example.com' - click_button 'Start' - - fill_in 'First Name', with: 'Adam' - click_on 'next' - click_on 'type_text_button' - fill_in 'signature_text_input', with: 'Adam' - - expect do - click_on 'Sign and Complete' - end.not_to(change(Submitter, :count)) - - submitter = Submitter.find_by(email: 'john.dou@example.com') - - expect(page).to have_button('Download') - expect(submitter.email).to eq('john.dou@example.com') - expect(submitter.ip).to eq('127.0.0.1') - expect(submitter.ua).to be_present - expect(submitter.opened_at).to be_present - expect(submitter.completed_at).to be_present - expect(submitter.values.values).to include('Adam') - end - end - - context 'when initialized by shared email address' do - let(:submission) { create(:submission, template:, created_by_user: user) } - let(:submitters) { template.submitters.map { |s| create(:submitter, submission:, uuid: s['uuid']) } } - let(:submitter) { submitters.first } - - before do - visit submit_form_path(slug: submitter.slug) - end - - it 'completes the form' do - fill_in 'First Name', with: 'Sally' - click_on 'next' - click_on 'type_text_button' - fill_in 'signature_text_input', with: 'Sally' - click_on 'Sign and Complete' - - submitter.reload - - expect(page).to have_button('Download') - expect(submitter.ip).to eq('127.0.0.1') - expect(submitter.ua).to be_present - expect(submitter.opened_at).to be_present - expect(submitter.completed_at).to be_present - expect(submitter.values.values).to include('Sally') - end - - it 'sends completed email' do - fill_in 'First Name', with: 'Adam' - click_on 'next' - click_on 'type_text_button' - fill_in 'signature_text_input', with: 'Adam' - - expect do - click_on 'Sign and Complete' - end.to change(ProcessSubmitterCompletionJob.jobs, :size).by(1) - end - end -end diff --git a/spec/system/template_spec.rb b/spec/system/template_spec.rb index 0e36ef94d..0b512041f 100644 --- a/spec/system/template_spec.rb +++ b/spec/system/template_spec.rb @@ -5,7 +5,7 @@ RSpec.describe 'Template' do let!(:account) { create(:account) } let!(:user) { create(:user, account:) } - let!(:template) { create(:template, account:, author: user) } + let!(:template) { create(:template, account:, author: user, except_field_types: %w[phone payment]) } before do sign_in(user) @@ -140,4 +140,98 @@ end end end + + context 'when filtering submissions' do + let(:second_user) { create(:user, account:) } + + it 'displays only submissions by the selected author' do + first_user_submissions = create_list(:submission, 5, :with_submitters, template:, created_by_user: user) + second_user_submissions = create_list(:submission, 6, :with_submitters, template:, created_by_user: second_user) + + visit template_path(template) + + (first_user_submissions + second_user_submissions).map(&:submitters).flatten.last(10).uniq.each do |submitter| + expect(page).to have_content(submitter.name) + end + + page.find('.dropdown', text: 'Filter').click + click_link 'Author' + within '#modal' do + select second_user.full_name, from: 'author' + click_button 'Apply' + end + + second_user_submissions.map(&:submitters).flatten.uniq.each do |submitter| + expect(page).to have_content(submitter.name) + end + + first_user_submissions.map(&:submitters).flatten.uniq.each do |submitter| + expect(page).not_to have_content(submitter.name) + end + end + + it 'displays submissions created within the selected date range' do + last_week_submissions = create_list(:submission, 5, :with_submitters, template:, created_by_user: user, + created_at: 9.days.ago) + this_week_submissions = create_list(:submission, 6, :with_submitters, template:, created_by_user: user, + created_at: 5.days.ago) + + visit template_path(template) + + (last_week_submissions + this_week_submissions).map(&:submitters).flatten.last(10).uniq.each do |submitter| + expect(page).to have_content(submitter.name) + end + + page.find('.dropdown', text: 'Filter').click + click_link 'Created at' + within '#modal' do + fill_in 'From', with: I18n.l(10.days.ago, format: '%Y-%m-%d') + fill_in 'To', with: I18n.l(6.days.ago, format: '%Y-%m-%d') + click_button 'Apply' + end + + last_week_submissions.map(&:submitters).flatten.uniq.each do |submitter| + expect(page).to have_content(submitter.name) + end + + this_week_submissions.map(&:submitters).flatten.uniq.each do |submitter| + expect(page).not_to have_content(submitter.name) + end + end + + it 'displays submissions completed within the selected date range' do + last_week_submissions = create_list(:submission, 5, :with_submitters, template:, created_by_user: user) + this_week_submissions = create_list(:submission, 6, :with_submitters, template:, created_by_user: user) + + last_week_submissions.map(&:submitters).flatten.each do |submitter| + submitter.update!(completed_at: rand(6..10).days.ago) + end + + this_week_submissions.map(&:submitters).flatten.each do |submitter| + submitter.update!(completed_at: rand(2..5).days.ago) + end + + visit template_path(template) + + (last_week_submissions + this_week_submissions).map(&:submitters).flatten.last(10).uniq.each do |submitter| + expect(page).to have_content(submitter.name) + end + + page.find('.dropdown', text: 'Filter').click + click_link 'Completed at' + within '#modal' do + fill_in 'From', with: I18n.l(5.days.ago, format: '%Y-%m-%d') + fill_in 'To', with: I18n.l(1.day.ago, format: '%Y-%m-%d') + click_button 'Apply' + end + + this_week_submissions.map(&:submitters).flatten.uniq.each do |submitter| + expect(page).to have_content(submitter.name) + end + + last_week_submissions.map(&:submitters).flatten.uniq.each do |submitter| + expect(page).not_to have_content(submitter.name) + end + end + end end