diff --git a/Gemfile.lock b/Gemfile.lock index 88d95b09..b2518108 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -338,18 +338,18 @@ GEM net-smtp (0.5.0) net-protocol nio4r (2.7.4) - nokogiri (1.18.5) + nokogiri (1.18.8) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.18.5-aarch64-linux-gnu) + nokogiri (1.18.8-aarch64-linux-gnu) racc (~> 1.4) - nokogiri (1.18.5-aarch64-linux-musl) + nokogiri (1.18.8-aarch64-linux-musl) racc (~> 1.4) - nokogiri (1.18.5-arm64-darwin) + nokogiri (1.18.8-arm64-darwin) racc (~> 1.4) - nokogiri (1.18.5-x86_64-linux-gnu) + nokogiri (1.18.8-x86_64-linux-gnu) racc (~> 1.4) - nokogiri (1.18.5-x86_64-linux-musl) + nokogiri (1.18.8-x86_64-linux-musl) racc (~> 1.4) oj (3.16.8) bigdecimal (>= 3.0) diff --git a/README.md b/README.md index 05d2acdf..50b20b12 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d - PDF signature verification - Users management - Mobile-optimized -- 6 UI languages with signing available in 13 languages +- 6 UI languages with signing available in 14 languages - API and Webhooks for integrations - Easy to deploy in minutes diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index daeb44cc..5a2797ff 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -45,7 +45,7 @@ class AccountsController < ApplicationController def destroy authorize!(:manage, current_account) - true_user.update!(locked_at: Time.current) + true_user.update!(locked_at: Time.current, email: true_user.email.sub('@', '+removed@')) # rubocop:disable Layout/LineLength render turbo_stream: turbo_stream.replace( diff --git a/app/controllers/api/submissions_controller.rb b/app/controllers/api/submissions_controller.rb index 89d361ec..17d961dd 100644 --- a/app/controllers/api/submissions_controller.rb +++ b/app/controllers/api/submissions_controller.rb @@ -11,17 +11,7 @@ module Api def index submissions = Submissions.search(@submissions, params[:q]) - submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present? - - if params[:template_folder].present? - submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] }) - end - - if params.key?(:archived) - submissions = params[:archived].in?(['true', true]) ? submissions.archived : submissions.active - end - - submissions = Submissions::Filter.call(submissions, current_user, params) + submissions = filter_submissions(submissions, params) submissions = paginate(submissions.preload(:created_by_user, :submitters, template: :folder, @@ -115,6 +105,21 @@ module Api private + def filter_submissions(submissions, params) + submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present? + submissions = submissions.where(slug: params[:slug]) if params[:slug].present? + + if params[:template_folder].present? + submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] }) + end + + if params.key?(:archived) + submissions = params[:archived].in?(['true', true]) ? submissions.archived : submissions.active + end + + Submissions::Filter.call(submissions, current_user, params) + end + def build_create_json(submissions) json = submissions.flat_map do |submission| submission.submitters.map do |s| diff --git a/app/controllers/api/submitters_controller.rb b/app/controllers/api/submitters_controller.rb index 1fd06d74..c2e6a07f 100644 --- a/app/controllers/api/submitters_controller.rb +++ b/app/controllers/api/submitters_controller.rb @@ -7,15 +7,7 @@ module Api def index submitters = Submitters.search(@submitters, params[:q]) - submitters = submitters.where(external_id: params[:application_key]) if params[:application_key].present? - submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present? - submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present? - - if params[:template_id].present? - submitters = submitters.joins(:submission).where(submission: { template_id: params[:template_id] }) - end - - submitters = maybe_filder_by_completed_at(submitters, params) + submitters = filter_submitters(submitters, params) submitters = paginate( submitters.preload(:template, :submission, :submission_events, @@ -163,6 +155,19 @@ module Api submitter end + def filter_submitters(submitters, params) + submitters = submitters.where(external_id: params[:application_key]) if params[:application_key].present? + submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present? + submitters = submitters.where(slug: params[:slug]) if params[:slug].present? + submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present? + + if params[:template_id].present? + submitters = submitters.joins(:submission).where(submission: { template_id: params[:template_id] }) + end + + maybe_filder_by_completed_at(submitters, params) + end + def assign_external_id(submitter, attrs) submitter.external_id = attrs[:application_key] if attrs.key?(:application_key) submitter.external_id = attrs[:external_id] if attrs.key?(:external_id) diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb index a695e728..5e8da098 100644 --- a/app/controllers/api/templates_controller.rb +++ b/app/controllers/api/templates_controller.rb @@ -90,6 +90,7 @@ module Api templates = params[:archived].in?(['true', true]) ? templates.archived : templates.active templates = templates.where(external_id: params[:application_key]) if params[:application_key].present? templates = templates.where(external_id: params[:external_id]) if params[:external_id].present? + templates = templates.where(slug: params[:slug]) if params[:slug].present? templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present? templates diff --git a/app/controllers/start_form_controller.rb b/app/controllers/start_form_controller.rb index a0a72044..90c32bde 100644 --- a/app/controllers/start_form_controller.rb +++ b/app/controllers/start_form_controller.rb @@ -11,6 +11,8 @@ class StartFormController < ApplicationController before_action :load_template def show + raise ActionController::RoutingError, I18n.t('not_found') if @template.preferences['require_phone_2fa'] == true + @submitter = @template.submissions.new(account_id: @template.account_id) .submitters.new(uuid: (filter_undefined_submitters(@template).first || @template.submitters.first)['uuid']) diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index 1dac63bd..0240b5be 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -57,6 +57,11 @@ class SubmissionsController < ApplicationController Submissions.send_signature_requests(submissions) redirect_to template_path(@template), notice: I18n.t('new_recipients_have_been_added') + rescue Submissions::CreateFromSubmitters::BaseError => e + render turbo_stream: turbo_stream.replace(:submitters_error, + partial: 'submissions/error', + locals: { error: e.message }), + status: :unprocessable_entity end def destroy diff --git a/app/controllers/submit_form_controller.rb b/app/controllers/submit_form_controller.rb index fcd0fdc8..a9a489c8 100644 --- a/app/controllers/submit_form_controller.rb +++ b/app/controllers/submit_form_controller.rb @@ -7,11 +7,11 @@ class SubmitFormController < ApplicationController skip_before_action :authenticate_user! skip_authorization_check + before_action :load_submitter, only: %i[show update completed] + CONFIG_KEYS = [].freeze def show - @submitter = Submitter.find_by!(slug: params[:slug]) - submission = @submitter.submission return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at? @@ -50,44 +50,44 @@ class SubmitFormController < ApplicationController end def update - submitter = Submitter.find_by!(slug: params[:slug]) - - if submitter.completed_at? + if @submitter.completed_at? return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_entity end - if submitter.template.archived_at? || submitter.submission.archived_at? + if @submitter.template.archived_at? || @submitter.submission.archived_at? return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_entity end - if submitter.submission.expired? + if @submitter.submission.expired? return render json: { error: I18n.t('form_has_been_expired') }, status: :unprocessable_entity end - if submitter.declined_at? + if @submitter.declined_at? return render json: { error: I18n.t('form_has_been_declined') }, status: :unprocessable_entity end - Submitters::SubmitValues.call(submitter, params, request) + Submitters::SubmitValues.call(@submitter, params, request) head :ok rescue Submitters::SubmitValues::RequiredFieldError => e - Rollbar.warning("Required field #{submitter.id}: #{e.message}") if defined?(Rollbar) + Rollbar.warning("Required field #{@submitter.id}: #{e.message}") if defined?(Rollbar) render json: { field_uuid: e.message }, status: :unprocessable_entity rescue Submitters::SubmitValues::ValidationError => e render json: { error: e.message }, status: :unprocessable_entity end - def completed - @submitter = Submitter.completed.find_by!(slug: params[:submit_form_slug]) - end + def completed; end def success; end private + def load_submitter + @submitter = Submitter.find_by!(slug: params[:slug] || params[:submit_form_slug]) + end + def build_attachments_index(submission) ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments) .preload(:blob).index_by(&:uuid) diff --git a/app/controllers/template_folders_controller.rb b/app/controllers/template_folders_controller.rb index a3418015..e5cb2bbf 100644 --- a/app/controllers/template_folders_controller.rb +++ b/app/controllers/template_folders_controller.rb @@ -5,8 +5,9 @@ class TemplateFoldersController < ApplicationController def show @templates = @template_folder.templates.active.accessible_by(current_ability) - .preload(:author, :template_accesses).order(id: :desc) + .preload(:author, :template_accesses) @templates = Templates.search(@templates, params[:q]) + @templates = Templates::Order.call(@templates, current_user, cookies.permanent[:dashboard_templates_order]) @pagy, @templates = pagy(@templates, limit: 12) end diff --git a/app/controllers/templates_dashboard_controller.rb b/app/controllers/templates_dashboard_controller.rb index 7c806fa9..0bd9a2be 100644 --- a/app/controllers/templates_dashboard_controller.rb +++ b/app/controllers/templates_dashboard_controller.rb @@ -9,9 +9,11 @@ class TemplatesDashboardController < ApplicationController FOLDERS_PER_PAGE = 18 def index - @template_folders = @template_folders.where(id: @templates.active.select(:folder_id)).order(id: :desc) + @template_folders = @template_folders.where(id: @templates.active.select(:folder_id)) @template_folders = TemplateFolders.search(@template_folders, params[:q]) + @template_folders = sort_template_folders(@template_folders, current_user, + cookies.permanent[:dashboard_templates_order]) @pagy, @template_folders = pagy( @template_folders, @@ -24,6 +26,7 @@ class TemplatesDashboardController < ApplicationController else @template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME } @templates = filter_templates(@templates) + @templates = Templates::Order.call(@templates, current_user, cookies.permanent[:dashboard_templates_order]) limit = if @template_folders.size < 4 @@ -39,7 +42,7 @@ class TemplatesDashboardController < ApplicationController private def filter_templates(templates) - rel = templates.active.preload(:author, :template_accesses).order(id: :desc) + rel = templates.active.preload(:author, :template_accesses) if params[:q].blank? if Docuseal.multitenant? && !current_account.testing? @@ -54,4 +57,38 @@ class TemplatesDashboardController < ApplicationController Templates.search(rel, params[:q]) end + + def sort_template_folders(template_folders, current_user, order) + case order + when 'used_at' + subquery = + Template.left_joins(:submissions) + .group(:folder_id) + .where(account_id: current_user.account_id) + .select( + :folder_id, + Template.arel_table[:updated_at].maximum.as('updated_at_max'), + Submission.arel_table[:created_at].maximum.as('submission_created_at_max') + ) + + template_folders = template_folders.joins( + Template.arel_table + .join(subquery.arel.as('templates'), Arel::Nodes::OuterJoin) + .on(TemplateFolder.arel_table[:id].eq(Template.arel_table[:folder_id])) + .join_sources + ) + + template_folders.order( + Arel::Nodes::Case.new + .when(Template.arel_table[:submission_created_at_max].gt(Template.arel_table[:updated_at_max])) + .then(Template.arel_table[:submission_created_at_max]) + .else(Template.arel_table[:updated_at_max]) + .desc + ) + when 'name' + template_folders.order(name: :asc) + else + template_folders.order(id: :desc) + end + end end diff --git a/app/controllers/templates_preferences_controller.rb b/app/controllers/templates_preferences_controller.rb index a9221f7b..07111a16 100644 --- a/app/controllers/templates_preferences_controller.rb +++ b/app/controllers/templates_preferences_controller.rb @@ -24,8 +24,8 @@ class TemplatesPreferencesController < ApplicationController documents_copy_email_enabled documents_copy_email_attach_audit documents_copy_email_attach_documents documents_copy_email_reply_to completed_notification_email_attach_documents - completed_redirect_url - submitters_order + completed_redirect_url validate_unique_submitters + submitters_order require_phone_2fa completed_notification_email_subject completed_notification_email_body completed_notification_email_enabled completed_notification_email_attach_audit] + [completed_message: %i[title body], diff --git a/app/javascript/elements/fetch_form.js b/app/javascript/elements/fetch_form.js new file mode 100644 index 00000000..b3378a63 --- /dev/null +++ b/app/javascript/elements/fetch_form.js @@ -0,0 +1,36 @@ +export default class extends HTMLElement { + connectedCallback () { + this.form.addEventListener('submit', (e) => { + e.preventDefault() + + this.submit() + }) + + if (this.dataset.onload === 'true') { + this.form.querySelector('button').click() + } + } + + submit () { + fetch(this.form.action, { + method: this.form.method, + body: new FormData(this.form) + }).then(async (resp) => { + if (!resp.ok) { + try { + const data = JSON.parse(await resp.text()) + + if (data.error) { + alert(data.error) + } + } catch (err) { + console.error(err) + } + } + }) + } + + get form () { + return this.querySelector('form') + } +} diff --git a/app/javascript/elements/scroll_buttons.js b/app/javascript/elements/scroll_buttons.js new file mode 100644 index 00000000..6525adf9 --- /dev/null +++ b/app/javascript/elements/scroll_buttons.js @@ -0,0 +1,62 @@ +export default class extends HTMLElement { + connectedCallback () { + this.header = document.querySelector('#signing_form_header') + + window.addEventListener('scroll', this.onScroll) + window.addEventListener('resize', this.onResize) + + if (!this.isNarrow() && this.isHeaderNotVisible()) { + this.showButtons({ animate: false }) + } + } + + disconnectedCallback () { + window.removeEventListener('scroll', this.onScroll) + window.removeEventListener('resize', this.onResize) + } + + onResize = () => { + if (this.isNarrow()) { + this.hideButtons(true) + } else if (this.isHeaderNotVisible()) { + this.showButtons() + } + } + + isNarrow () { + return window.innerWidth < 1230 + } + + onScroll = () => { + if (this.isHeaderNotVisible() && !this.isNarrow()) { + this.showButtons() + } else { + this.hideButtons() + } + } + + isHeaderNotVisible () { + const rect = this.header.getBoundingClientRect() + return rect.bottom <= 0 || rect.top >= window.innerHeight + } + + showButtons ({ animate } = { animate: true }) { + if (animate) { + this.classList.add('transition-transform', 'duration-300') + } + + this.classList.remove('hidden', '-translate-y-10', 'opacity-0') + this.classList.add('translate-y-0', 'opacity-100') + } + + hideButtons () { + this.classList.remove('translate-y-0', 'opacity-100') + this.classList.add('-translate-y-10', 'opacity-0') + + setTimeout(() => { + if (this.classList.contains('-translate-y-10')) { + this.classList.add('hidden') + } + }, 300) + } +} diff --git a/app/javascript/form.js b/app/javascript/form.js index 2e07f033..45b3b3d2 100644 --- a/app/javascript/form.js +++ b/app/javascript/form.js @@ -3,11 +3,15 @@ import { createApp, reactive } from 'vue' import Form from './submission_form/form' import DownloadButton from './elements/download_button' import ToggleSubmit from './elements/toggle_submit' +import FetchForm from './elements/fetch_form' +import ScrollButtons from './elements/scroll_buttons' const safeRegisterElement = (name, element, options = {}) => !window.customElements.get(name) && window.customElements.define(name, element, options) safeRegisterElement('download-button', DownloadButton) safeRegisterElement('toggle-submit', ToggleSubmit) +safeRegisterElement('fetch-form', FetchForm) +safeRegisterElement('scroll-buttons', ScrollButtons) safeRegisterElement('submission-form', class extends HTMLElement { connectedCallback () { this.appElem = document.createElement('div') diff --git a/app/javascript/submission_form/area.vue b/app/javascript/submission_form/area.vue index ce212a4f..d18aae5f 100644 --- a/app/javascript/submission_form/area.vue +++ b/app/javascript/submission_form/area.vue @@ -183,7 +183,7 @@ v-else ref="textContainer" dir="auto" - class="flex items-center px-0.5 w-full" + class="flex px-0.5 w-full" :class="{ ...alignClasses, ...fontClasses }" > { + button.setAttribute('disabled', 'true') + }) }, methods: { sendCopyToEmail () { diff --git a/app/javascript/submission_form/crop_canvas.js b/app/javascript/submission_form/crop_canvas.js index 977c5ade..472d2764 100644 --- a/app/javascript/submission_form/crop_canvas.js +++ b/app/javascript/submission_form/crop_canvas.js @@ -1,4 +1,4 @@ -function cropCanvasAndExportToPNG (canvas) { +function cropCanvasAndExportToPNG (canvas, { errorOnTooSmall } = { errorOnTooSmall: false }) { const ctx = canvas.getContext('2d') const width = canvas.width @@ -33,6 +33,10 @@ function cropCanvasAndExportToPNG (canvas) { croppedCanvas.height = croppedHeight const croppedCtx = croppedCanvas.getContext('2d') + if (errorOnTooSmall && (croppedWidth < 20 || croppedHeight < 20)) { + return Promise.reject(new Error('Image too small')) + } + croppedCtx.drawImage(canvas, leftmost, topmost, croppedWidth, croppedHeight, 0, 0, croppedWidth, croppedHeight) return new Promise((resolve, reject) => { diff --git a/app/javascript/submission_form/form.vue b/app/javascript/submission_form/form.vue index 01b7f4b7..2a9d8e97 100644 --- a/app/javascript/submission_form/form.vue +++ b/app/javascript/submission_form/form.vue @@ -11,7 +11,7 @@ :with-label="!isAnonymousChecboxes && showFieldNames" :current-step="currentStepFields" :scroll-padding="scrollPadding" - @focus-step="[saveStep(), goToStep($event, false, true), currentField.type !== 'checkbox' ? isFormVisible = true : '']" + @focus-step="[saveStep(), currentField.type !== 'checkbox' ? isFormVisible = true : '', goToStep($event, false, true)]" /> { - cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => { + return new Promise((resolve, reject) => { + cropCanvasAndExportToPNG(this.$refs.canvas, { errorOnTooSmall: true }).then(async (blob) => { const file = new File([blob], 'initials.png', { type: 'image/png' }) if (this.dryRun) { @@ -373,6 +373,14 @@ export default { return resolve(attachment) }) } + }).catch((error) => { + if (this.field.required === true) { + alert(this.t('signature_is_too_small_or_simple_please_redraw')) + + return reject(error) + } else { + return resolve({}) + } }) }) } diff --git a/app/javascript/submission_form/signature_step.vue b/app/javascript/submission_form/signature_step.vue index 2f3f8856..e12784fa 100644 --- a/app/javascript/submission_form/signature_step.vue +++ b/app/javascript/submission_form/signature_step.vue @@ -683,13 +683,17 @@ export default { } if (this.isSignatureStarted && this.pad.toData().length > 0 && !isValidSignatureCanvas(this.pad.toData())) { - alert(this.t('signature_is_too_small_or_simple_please_redraw')) + if (this.field.required === true || this.pad.toData().length > 0) { + alert(this.t('signature_is_too_small_or_simple_please_redraw')) - return Promise.reject(new Error('Image too small or simple')) + return Promise.reject(new Error('Image too small or simple')) + } else { + Promise.resolve({}) + } } - return new Promise((resolve) => { - cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => { + return new Promise((resolve, reject) => { + cropCanvasAndExportToPNG(this.$refs.canvas, { errorOnTooSmall: true }).then(async (blob) => { const file = new File([blob], 'signature.png', { type: 'image/png' }) if (this.dryRun) { @@ -725,6 +729,14 @@ export default { return resolve(attachment) }) } + }).catch((error) => { + if (this.field.required === true) { + alert(this.t('signature_is_too_small_or_simple_please_redraw')) + + return reject(error) + } else { + return resolve({}) + } }) }) } diff --git a/app/javascript/template_builder/area.vue b/app/javascript/template_builder/area.vue index a5c65613..e0a5937e 100644 --- a/app/javascript/template_builder/area.vue +++ b/app/javascript/template_builder/area.vue @@ -160,15 +160,15 @@
{ - if (f !== this.item) { + if (f !== this.item && (!f.conditions?.length || !f.conditions.find((c) => c.field_uuid === this.item.uuid))) { acc.push(f) } diff --git a/app/javascript/template_builder/font_modal.vue b/app/javascript/template_builder/font_modal.vue index cbe38915..dffe4f4b 100644 --- a/app/javascript/template_builder/font_modal.vue +++ b/app/javascript/template_builder/font_modal.vue @@ -57,7 +57,7 @@
<%= f.label :last_name, t('last_name'), class: 'label' %> - <%= f.text_field :last_name, required: true, class: 'base-input', dir: 'auto' %> + <%= f.text_field :last_name, required: false, class: 'base-input', dir: 'auto' %>
diff --git a/app/views/shared/_app_tour.html.erb b/app/views/shared/_app_tour.html.erb index 46b5d15d..09af71fa 100644 --- a/app/views/shared/_app_tour.html.erb +++ b/app/views/shared/_app_tour.html.erb @@ -1,5 +1,5 @@ <% if current_user.created_at > 2.weeks.ago || params[:tour] == 'true' %> - <% user_config = current_user.user_configs.find_or_initialize_by(key: UserConfig::SHOW_APP_TOUR) %> + <% user_config = current_user.user_configs.find_by(key: UserConfig::SHOW_APP_TOUR) || UserConfig.new(key: UserConfig::SHOW_APP_TOUR, user: current_user) %> <% if user_config.new_record? || user_config.value || params[:tour] == 'true' %> <% end %> diff --git a/app/views/shared/_html_modal.html.erb b/app/views/shared/_html_modal.html.erb index 00d269c9..c6baa601 100644 --- a/app/views/shared/_html_modal.html.erb +++ b/app/views/shared/_html_modal.html.erb @@ -1,4 +1,4 @@ -<% uuid = SecureRandom.uuid %> +<% uuid = local_assigns[:uuid] || SecureRandom.uuid %> -
- <% if @pagy.prev %> - <%== link.call(@pagy.prev, '«', classes: 'join-item btn min-h-full h-10') %> - <% else %> - « - <% end %> - - <%= t('page_number', number: @pagy.page) %> - - <% if @pagy.next %> - <%== link.call(@pagy.next, '»', classes: 'join-item btn min-h-full h-10') %> - <% else %> - » - <% end %> +
+ <%= local_assigns[:right_additional_html] %> +
+ <% if @pagy.prev %> + <%== link.call(@pagy.prev, '«', classes: 'join-item btn min-h-full h-10') %> + <% else %> + « + <% end %> + + <%= t('page_number', number: @pagy.page) %> + + <% if @pagy.next %> + <%== link.call(@pagy.next, '»', classes: 'join-item btn min-h-full h-10') %> + <% else %> + » + <% end %> +
<% end %> diff --git a/app/views/shared/_templates_order_select.html.erb b/app/views/shared/_templates_order_select.html.erb new file mode 100644 index 00000000..b4a9a930 --- /dev/null +++ b/app/views/shared/_templates_order_select.html.erb @@ -0,0 +1,34 @@ +<% dashboard_templates_order = cookies.permanent[:dashboard_templates_order] || 'created_at' %> + diff --git a/app/views/submissions/_detailed_form.html.erb b/app/views/submissions/_detailed_form.html.erb index 37a2a88a..847ac5a8 100644 --- a/app/views/submissions/_detailed_form.html.erb +++ b/app/views/submissions/_detailed_form.html.erb @@ -31,7 +31,7 @@ "> - " id="detailed_phone_<%= item['uuid'] %>"> + <%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('#{t('use_international_format_1xxx_')}') : ''", oninput: "this.setCustomValidity('')", name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: local_assigns[:require_phone_2fa] == true ? t(:phone) : "#{t('phone')} (#{t('optional')})", id: "detailed_phone_#{item['uuid']}", required: local_assigns[:require_phone_2fa] == true %>
diff --git a/app/views/submissions/_error.html.erb b/app/views/submissions/_error.html.erb new file mode 100644 index 00000000..1f95212b --- /dev/null +++ b/app/views/submissions/_error.html.erb @@ -0,0 +1,7 @@ +
+ <% if local_assigns[:error] %> +
+ <%= local_assigns[:error] %> +
+ <% end %> +
diff --git a/app/views/submissions/_value.html.erb b/app/views/submissions/_value.html.erb index 248b410d..c9ab473d 100644 --- a/app/views/submissions/_value.html.erb +++ b/app/views/submissions/_value.html.erb @@ -1,8 +1,9 @@ <% align = field.dig('preferences', 'align') %> +<% valign = field.dig('preferences', 'valign') %> <% color = field.dig('preferences', 'color') %> <% font = field.dig('preferences', 'font') %> <% font_type = field.dig('preferences', 'font_type') %> -width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size'].to_i * 1.23}pt); line-height: `clamp(6pt, 2.0vw, #{(field['preferences']['font_size'].to_i * 1.23) + 3}pt)`" if field.dig('preferences', 'font_size') %>"> +width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size'].to_i * 1.23}pt); line-height: `clamp(6pt, 2.0vw, #{(field['preferences']['font_size'].to_i * 1.23) + 3}pt)`" if field.dig('preferences', 'font_size') %>"> <% if field['type'] == 'signature' %>
@@ -61,17 +62,19 @@
<% elsif field['type'] == 'date' %> -
+
<% value = Time.current.in_time_zone(local_assigns[:timezone]).to_date.to_s if value == '{{date}}' %> - <%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), local_assigns[:locale]) %> +
<%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), local_assigns[:locale]) %>
<% elsif field['type'] == 'number' %> -
- <%= NumberUtils.format_number(value, field.dig('preferences', 'format')) %> +
+
<%= NumberUtils.format_number(value, field.dig('preferences', 'format')) %>
<% else %> -
<%= Array.wrap(value).join(', ') %>
+
+
<%= Array.wrap(value).join(', ') %>
+
<% end %> diff --git a/app/views/submissions/new.html.erb b/app/views/submissions/new.html.erb index 5de57e76..b9874d75 100644 --- a/app/views/submissions/new.html.erb +++ b/app/views/submissions/new.html.erb @@ -1,10 +1,11 @@ +<% require_phone_2fa = @template.preferences['require_phone_2fa'] == true %> <%= render 'shared/turbo_modal_large', title: params[:selfsign] ? t('add_recipients') : t('add_new_recipients') do %> - <% options = [[t('via_email'), 'email'], [t('via_phone'), 'phone'], [t('detailed'), 'detailed'], [t('upload_list'), 'list']].compact %> + <% options = [require_phone_2fa ? nil : [t('via_email'), 'email'], require_phone_2fa ? nil : [t('via_phone'), 'phone'], [t('detailed'), 'detailed'], [t('upload_list'), 'list']].compact %>
<% options.each_with_index do |(label, value), index| %>
- <%= radio_button_tag 'option', value, value == 'email', class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %> + <%= radio_button_tag 'option', value, value == (require_phone_2fa ? 'detailed' : 'email'), class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %> @@ -13,18 +14,21 @@
-
- <%= render 'email_form', template: @template %> -
- - <%= content_for(:modal_extra) %> <% end %> diff --git a/app/views/submissions_dashboard/index.html.erb b/app/views/submissions_dashboard/index.html.erb index 5c9a733f..e09732ff 100644 --- a/app/views/submissions_dashboard/index.html.erb +++ b/app/views/submissions_dashboard/index.html.erb @@ -57,6 +57,7 @@
<%= render 'submissions_filters/applied_filters', filter_params: %> <%= render 'submissions_filters/filter_button', filter_params: %> + <%= render 'submissions_filters/actions', filter_params: %>
<% end %> diff --git a/app/views/submissions_filters/_actions.html.erb b/app/views/submissions_filters/_actions.html.erb new file mode 100644 index 00000000..e69de29b diff --git a/app/views/submit_form/show.html.erb b/app/views/submit_form/show.html.erb index b4774423..491256a6 100644 --- a/app/views/submit_form/show.html.erb +++ b/app/views/submit_form/show.html.erb @@ -8,17 +8,18 @@
- <%# flex block w-full sticky top-0 z-50 space-x-2 items-center bg-yellow-100 p-2 border-y border-yellow-200 %> + <%# flex block w-full sticky top-0 z-50 space-x-2 items-center bg-yellow-100 p-2 border-y border-yellow-200 transition-transform duration-300 %> <%= local_assigns[:banner_html] || capture do %> <%= render('submit_form/banner') %> -
+
<%= @submitter.submission.template.name %>
<% if @form_configs[:with_decline] %> + <% decline_modal_checkbox_uuid = SecureRandom.uuid %>
- <%= render 'shared/html_modal', title: t(:decline), btn_text: t(:decline), btn_class: 'btn btn-sm !px-5', button_id: 'decline_button' do %> + <%= render 'shared/html_modal', title: t(:decline), btn_text: t(:decline), btn_class: 'btn btn-sm !px-5', button_id: 'decline_button', uuid: decline_modal_checkbox_uuid do %> <%= render 'submit_form/decline_form', submitter: @submitter %> <% end %>
@@ -35,6 +36,26 @@
+ <% end %> <% schema.each do |item| %> <% document = @submitter.submission.template_schema_documents.find { |a| a.uuid == item['attachment_uuid'] } %> diff --git a/app/views/template_folders/show.html.erb b/app/views/template_folders/show.html.erb index 65acdcd5..ba2dc868 100644 --- a/app/views/template_folders/show.html.erb +++ b/app/views/template_folders/show.html.erb @@ -35,7 +35,12 @@
<%= render partial: 'templates/template', collection: @templates %>
- <%= render 'shared/pagination', pagy: @pagy, items_name: 'templates' %> + <% templates_order_select_html = capture do %> + <% if params[:q].blank? && @pagy.pages > 1 %> + <%= render('shared/templates_order_select', with_recently_used: @pagy.count < 10_000) %> + <% end %> + <% end %> + <%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', right_additional_html: templates_order_select_html %> <% elsif params[:q].present? %>
diff --git a/app/views/templates/show.html.erb b/app/views/templates/show.html.erb index c3085e4f..f6696969 100644 --- a/app/views/templates/show.html.erb +++ b/app/views/templates/show.html.erb @@ -61,6 +61,7 @@
<%= render 'submissions_filters/applied_filters', filter_params: %> <%= render 'submissions_filters/filter_button', filter_params: %> + <%= render 'submissions_filters/actions', filter_params: filter_params.merge(template_id: @template.id) %>
<% end %> diff --git a/app/views/templates_dashboard/index.html.erb b/app/views/templates_dashboard/index.html.erb index c6f42c94..25ca67ad 100644 --- a/app/views/templates_dashboard/index.html.erb +++ b/app/views/templates_dashboard/index.html.erb @@ -34,6 +34,11 @@
<% end %> <% end %> +<% templates_order_select_html = capture do %> + <% if params[:q].blank? && @pagy.pages > 1 %> + <%= render('shared/templates_order_select', with_recently_used: @pagy.count < 10_000) %> + <% end %> +<% end %> <% if @template_folders.present? %>
<%= render partial: 'template_folders/folder', collection: @template_folders, as: :folder %> @@ -71,7 +76,7 @@ <% end %> <% if @templates.present? || params[:q].blank? %> <% if @pagy.pages > 1 %> - <%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', left_additional_html: view_archived_html %> + <%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', left_additional_html: view_archived_html, right_additional_html: templates_order_select_html %> <% else %>
<%= view_archived_html %> diff --git a/app/views/templates_preferences/_form_fields.html.erb b/app/views/templates_preferences/_form_fields.html.erb new file mode 100644 index 00000000..e69de29b diff --git a/app/views/templates_preferences/show.html.erb b/app/views/templates_preferences/show.html.erb index 013e3918..d5010f34 100644 --- a/app/views/templates_preferences/show.html.erb +++ b/app/views/templates_preferences/show.html.erb @@ -51,7 +51,7 @@ <%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %> <% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY).value %> - <%= f.fields_for :preferences, Struct.new(:completed_redirect_url, :completed_message).new(@template.preferences['completed_redirect_url'].presence, Struct.new(:title, :body).new(*(@template.preferences['completed_message'] || {}).values_at('title', 'body'))) do |ff| %> + <%= f.fields_for :preferences, Struct.new(:completed_redirect_url, :completed_message, :require_phone_2fa).new(@template.preferences['completed_redirect_url'].presence, Struct.new(:title, :body).new(*(@template.preferences['completed_message'] || {}).values_at('title', 'body')), @template.preferences['require_phone_2fa'] == true) do |ff| %>
<%= ff.label :completed_redirect_url, t('redirect_on_completion_url'), class: 'label' %> <%= ff.url_field :completed_redirect_url, required: false, class: 'base-input', dir: 'auto' %> @@ -64,6 +64,7 @@
<% end %> + <%= render 'templates_preferences/form_fields', ff: %> <% end %>
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %> @@ -334,6 +335,18 @@
<% end %> <% end %> + <% if can?(:manage, :personalization_advanced) %> + <%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-2' }, data: { close_on_submit: false } do |f| %> +
+ + <%= t('ensure_unique_recipients') %> + + <%= f.fields_for :preferences, Struct.new(:validate_unique_submitters).new(@template.preferences['validate_unique_submitters']) do |ff| %> + <%= ff.check_box :validate_unique_submitters, { class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', '' %> + <% end %> +
+ <% end %> + <% end %>
<%= button_tag button_title(title: t('save'), disabled_with: t('updating')), class: 'base-button', form: :submitters_form %>
diff --git a/config/application.rb b/config/application.rb index 758d2f81..8021f58a 100644 --- a/config/application.rb +++ b/config/application.rb @@ -25,7 +25,8 @@ module DocuSeal config.active_storage.draw_routes = ENV['MULTITENANT'] != 'true' - config.i18n.available_locales = %i[en en-US en-GB es-ES fr-FR pt-PT de-DE it-IT es it de fr pl uk cs pt he nl ar ko] + config.i18n.available_locales = %i[en en-US en-GB es-ES fr-FR pt-PT de-DE it-IT + es it de fr pl uk cs pt he nl ar ko ja] config.i18n.fallbacks = [:en] config.exceptions_app = ->(env) { ErrorsController.action(:show).call(env) } diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index 538480f5..d5845178 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -18,14 +18,22 @@ en: &en language_nl: Nederlands language_ar: العربية language_ko: 한국어 + language_ja: 日本語 hi_there: Hi there thanks: Thanks bcc_recipients: BCC recipients + resend_pending: Re-send pending always_enforce_signing_order: Always enforce the signing order + ensure_unique_recipients: Ensure unique recipients edit_per_party: Edit per party reply_to: Reply to pending_by_me: Pending by me partially_completed: Partially completed + require_phone_2fa_to_open: Require phone 2FA to open + the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: The sender has requested a two factor authentication via one time password sent to your %{phone} phone number. + send_verification_code: Send verification code + code_has_been_resent: Code has been re-sent + invalid_code: Invalid code unarchive: Unarchive signed: Signed first_party: 'First Party' @@ -47,6 +55,8 @@ en: &en you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'You have been invited to %{account_name} %{product_name}. Please sign up using the link below:' sent_using_product_name_in_testing_mode_html: 'Sent using %{product_name} in testing mode' sent_using_product_name_free_document_signing_html: 'Sent using %{product_name} free document signing.' + sent_with_docuseal_pro_html: 'Sent with DocuSeal Pro' + show_send_with_docuseal_pro_attribution_in_emails_html: Show "Sent with DocuSeal Pro" attribution in emails sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Sign documents with trusted certificate provided by DocuSeal. Your documents and data are never shared with DocuSeal. PDF checksum is provided to generate a trusted signature. you_have_been_invited_to_submit_the_name_form: 'You have been invited to submit the "%{name}" form.' you_have_been_invited_to_sign_the_name: 'You have been invited to sign the "%{name}".' @@ -704,6 +714,9 @@ en: &en welcome_to_docuseal: Welcome to DocuSeal start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: Start a quick tour to learn how to create an send your first document start_tour: Start Tour + name_a_z: Name A-Z + recently_used: Recently used + newest_first: Newest first submission_sources: api: API bulk: Bulk Send @@ -788,6 +801,13 @@ en: &en read: Read your data es: &es + resend_pending: Reenviar pendiente + ensure_unique_recipients: Asegurar destinatarios únicos + require_phone_2fa_to_open: Requiere 2FA por teléfono para abrir + the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: El remitente ha solicitado una autenticación de dos factores mediante una contraseña de un solo uso enviada a su número de teléfono %{phone}. + send_verification_code: Enviar código de verificación + code_has_been_resent: El código ha sido reenviado + invalid_code: Código inválido always_enforce_signing_order: Siempre imponer el orden de firma bcc_recipients: Destinatarios CCO edit_per_party: Editar por parte @@ -817,6 +837,8 @@ es: &es you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Has sido invitado a %{account_name} %{product_name}. Por favor, regístrate usando el enlace a continuación:' sent_using_product_name_in_testing_mode_html: 'Enviado usando %{product_name} en Modo de Prueba' sent_using_product_name_free_document_signing_html: 'Enviado usando la firma de documentos gratuita de %{product_name}.' + sent_with_docuseal_pro_html: 'Enviado con DocuSeal Pro' + show_send_with_docuseal_pro_attribution_in_emails_html: Mostrar el mensaje "Enviado con DocuSeal Pro" en los correos electrónicos sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Firme documentos con un certificado de confianza proporcionado por DocuSeal. Sus documentos y datos nunca se comparten con DocuSeal. Se proporciona un checksum de PDF para generar una firma de confianza. hi_there: Hola thanks: Gracias @@ -1474,6 +1496,9 @@ es: &es welcome_to_docuseal: Bienvenido a DocuSeal start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: Inicia una guía rápida para aprender a crear y enviar tu primer documento. start_tour: Iniciar guía + name_a_z: Nombre A-Z + recently_used: Usado recientemente + newest_first: Más reciente primero submission_sources: api: API bulk: Envío masivo @@ -1558,6 +1583,13 @@ es: &es read: Leer tus datos it: &it + resend_pending: Reinvia in sospeso + ensure_unique_recipients: Assicurarsi destinatari unici + require_phone_2fa_to_open: Richiedi l'autenticazione a due fattori tramite telefono per aprire + the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: Il mittente ha richiesto un'autenticazione a due fattori tramite una password monouso inviata al tuo numero di telefono %{phone}. + send_verification_code: Invia codice di verifica + code_has_been_resent: Il codice è stato inviato di nuovo + invalid_code: Codice non valido always_enforce_signing_order: Applicare sempre l'ordine di firma bcc_recipients: Destinatari BCC edit_per_party: Modifica per partito @@ -1586,6 +1618,8 @@ it: &it you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Sei stato invitato a %{account_name} %{product_name}. Registrati utilizzando il link qui sotto:' sent_using_product_name_in_testing_mode_html: 'Inviato utilizzando %{product_name} in Modalità di Test' sent_using_product_name_free_document_signing_html: 'Inviato utilizzando la firma di documenti gratuita di %{product_name}.' + sent_with_docuseal_pro_html: 'Inviato con DocuSeal Pro' + show_send_with_docuseal_pro_attribution_in_emails_html: Mostra la dicitura "Inviato con DocuSeal Pro" nelle email sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: "Firma documenti con un certificato di fiducia fornito da DocuSeal. I tuoi documenti e i tuoi dati non vengono mai condivisi con DocuSeal. Il checksum PDF è fornito per generare una firma di fiducia." hi_there: Ciao thanks: Grazie @@ -2243,6 +2277,9 @@ it: &it welcome_to_docuseal: Benvenuto in DocuSeal start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: Inizia un tour rapido per imparare a creare e inviare il tuo primo documento. start_tour: Inizia il tour + name_a_z: Nome A-Z + recently_used: Recentemente usato + newest_first: Più recenti prima submission_sources: api: API bulk: Invio massivo @@ -2327,6 +2364,13 @@ it: &it read: Leggi i tuoi dati fr: &fr + resend_pending: Renvoyer en attente + ensure_unique_recipients: Assurer l'unicité des destinataires + require_phone_2fa_to_open: Requiert une 2FA par téléphone pour ouvrir + the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: L'expéditeur a demandé une authentification à deux facteurs via un mot de passe à usage unique envoyé à votre numéro de téléphone %{phone}. + send_verification_code: Envoyer le code de vérification + code_has_been_resent: Le code a été renvoyé + invalid_code: Code invalide always_enforce_signing_order: Toujours appliquer l'ordre de signature bcc_recipients: Destinataires en CCI edit_per_party: Éditer par partie @@ -2356,6 +2400,8 @@ fr: &fr you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Vous avez été invité à %{account_name} %{product_name}. Veuillez vous inscrire en utilisant le lien ci-dessous:' sent_using_product_name_in_testing_mode_html: 'Envoyé en utilisant %{product_name} en Mode Test' sent_using_product_name_free_document_signing_html: 'Envoyé en utilisant la signature de documents gratuite de %{product_name}.' + sent_with_docuseal_pro_html: 'Envoyé avec DocuSeal Pro' + show_send_with_docuseal_pro_attribution_in_emails_html: Afficher "Envoyé avec DocuSeal Pro" dans les e-mails sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Signez des documents avec un certificat de confiance fourni par DocuSeal. Vos documents et données ne sont jamais partagés avec DocuSeal. Un checksum PDF est fourni pour générer une signature de confiance. hi_there: Bonjour thanks: Merci @@ -3014,6 +3060,9 @@ fr: &fr welcome_to_docuseal: Bienvenue sur DocuSeal start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: Lancez une visite rapide pour apprendre à créer et envoyer votre premier document. start_tour: Démarrer + name_a_z: Nom A-Z + recently_used: Récemment utilisé + newest_first: Le plus récent d'abord submission_sources: api: API bulk: Envoi en masse @@ -3098,6 +3147,13 @@ fr: &fr read: Lire vos données pt: &pt + resend_pending: Re-enviar pendente + ensure_unique_recipients: Garantir destinatários únicos + require_phone_2fa_to_open: Necessário autenticação de dois fatores via telefone para abrir + the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: O remetente solicitou uma autenticação de dois fatores via senha de uso único enviada para seu número %{phone}. + send_verification_code: Enviar código de verificação + code_has_been_resent: Código foi reenviado + invalid_code: Código inválido always_enforce_signing_order: Sempre impor a ordem de assinatura bcc_recipients: Destinatários BCC edit_per_party: Edita por festa @@ -3127,6 +3183,8 @@ pt: &pt you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Você foi convidado para %{account_name} %{product_name}. Inscreva-se usando o link abaixo:' sent_using_product_name_in_testing_mode_html: 'Enviado usando %{product_name} no Modo de Teste' sent_using_product_name_free_document_signing_html: 'Enviado usando a assinatura gratuita de documentos de %{product_name}.' + sent_with_docuseal_pro_html: 'Enviado com DocuSeal Pro' + show_send_with_docuseal_pro_attribution_in_emails_html: Mostrar "Enviado com DocuSeal Pro" nos e-mails sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Assine documentos com certificado confiável fornecido pela DocuSeal. Seus documentos e dados nunca são compartilhados com a DocuSeal. O checksum do PDF é fornecido para gerar uma assinatura confiável. hi_there: Olá thanks: Obrigado @@ -3395,7 +3453,7 @@ pt: &pt verify_pdf: Verificar PDF sign_out: Sair page_number: 'Página %{number}' - powered_by: Oferecido por + powered_by: Desenvolvido por count_documents_signed_with_html: '%{count} documentos assinados com' storage: Armazenamento notifications: Notificações @@ -3784,6 +3842,9 @@ pt: &pt welcome_to_docuseal: Bem-vindo ao DocuSeal start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: Comece um tour rápido para aprender a criar e enviar seu primeiro documento. start_tour: Iniciar tour + name_a_z: Nome A-Z + recently_used: Recentemente usado + newest_first: Mais recente primeiro submission_sources: api: API bulk: Envio em massa @@ -3869,6 +3930,13 @@ pt: &pt read: Ler seus dados de: &de + resend_pending: Ausstehende erneut senden + ensure_unique_recipients: Stellen Sie einzigartige Empfänger sicher + require_phone_2fa_to_open: Telefon-2FA zum Öffnen erforderlich + the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: Der Absender hat eine Zwei-Faktor-Authentifizierung per Einmalpasswort angefordert, das an Ihre %{phone}-Telefonnummer gesendet wurde. + send_verification_code: Bestätigungscode senden + code_has_been_resent: Code wurde erneut gesendet + invalid_code: Ungültiger Code always_enforce_signing_order: Immer die Reihenfolge der Unterschriften erzwingen bcc_recipients: BCC-Empfänger edit_per_party: Bearbeiten pro Partei @@ -3898,6 +3966,8 @@ de: &de you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Sie wurden zu %{account_name} %{product_name} eingeladen. Bitte registrieren Sie sich über den folgenden Link:' sent_using_product_name_in_testing_mode_html: 'Gesendet über %{product_name} im Testmodus' sent_using_product_name_free_document_signing_html: 'Gesendet mit der kostenlosen Dokumentensignierung von %{product_name}.' + sent_with_docuseal_pro_html: Gesendet mit DocuSeal Pro + show_send_with_docuseal_pro_attribution_in_emails_html: '"Gesendet mit DocuSeal Pro" in E-Mails anzeigen' sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Unterzeichnen Sie Dokumente mit einem vertrauenswürdigen Zertifikat von DocuSeal. Ihre Dokumente und Daten werden niemals mit DocuSeal geteilt. Eine PDF-Prüfziffer wird bereitgestellt, um eine vertrauenswürdige Signatur zu generieren. hi_there: Hallo thanks: Danke @@ -4555,6 +4625,9 @@ de: &de welcome_to_docuseal: Willkommen bei DocuSeal start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: Starte eine kurze Tour, um zu lernen, wie du dein erstes Dokument erstellst und versendest. start_tour: Starten + name_a_z: Name A-Z + recently_used: Kürzlich verwendet + newest_first: Neueste zuerst submission_sources: api: API bulk: Massenversand @@ -4639,6 +4712,11 @@ de: &de read: Lese deine Daten pl: + require_phone_2fa_to_open: Wymagaj uwierzytelniania telefonicznego 2FA do otwarcia + the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: Nadawca zażądał uwierzytelnienia dwuetapowego poprzez jednorazowe hasło wysłane na Twój %{phone} numer telefonu. + send_verification_code: Wyślij kod weryfikacyjny + code_has_been_resent: Kod został wysłany ponownie + invalid_code: Niepoprawny kod awaiting_completion_by_the_other_party: "Oczekuje na dokończenie przez drugą stronę" view: Widok hi_there: Cześć, @@ -4696,8 +4774,16 @@ pl: too_many_attempts: Zbyt wiele prób. verification_code: Kod Weryfikacyjny resend_code: Wyślij Kod Ponownie + powered_by: 'Napędzany prze' + count_documents_signed_with_html: '%{count} dokumentów podpisanych za pomocą' + open_source_documents_software: 'oprogramowanie do dokumentów open source' uk: + require_phone_2fa_to_open: Вимагати двофакторну автентифікацію через телефон для відкриття + the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: Відправник запросив двофакторну автентифікацію за допомогою одноразового пароля, відправленого на ваш номер телефону %{phone}. + send_verification_code: Надіслати код підтвердження + code_has_been_resent: Код повторно надіслано + invalid_code: Невірний код awaiting_completion_by_the_other_party: "Очікує завершення з боку іншої сторони" view: Переглянути hi_there: Привіт, @@ -4755,8 +4841,16 @@ uk: too_many_attempts: Забагато спроб. verification_code: Код перевірки resend_code: Відправити код знову + powered_by: 'Працює на базі' + count_documents_signed_with_html: '%{count} документів підписано за допомогою' + open_source_documents_software: 'відкрите програмне забезпечення для документів' cs: + require_phone_2fa_to_open: Vyžadovat otevření pomocí telefonního 2FA + the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: Odesílatel požádal o dvoufaktorové ověření pomocí jednorázového hesla zaslaného na vaše telefonní číslo %{phone}. + send_verification_code: Odeslat ověřovací kód + code_has_been_resent: Kód byl znovu odeslán + invalid_code: Neplatný kód awaiting_completion_by_the_other_party: "Čeká se na dokončení druhou stranou" view: Zobrazit hi_there: Ahoj, @@ -4814,8 +4908,16 @@ cs: too_many_attempts: Příliš mnoho pokusů. verification_code: Ověřovací Kód resend_code: Znovu Odeslat Kód + powered_by: 'Poháněno' + count_documents_signed_with_html: '%{count} dokumentů podepsáno pomocí' + open_source_documents_software: 'open source software pro dokumenty' he: + require_phone_2fa_to_open: דרוש אימות דו-שלבי באמצעות טלפון לפתיחה + the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: השולח ביקש אימות דו-שלבי באמצעות סיסמה חד פעמית שנשלחה למספר הטלפון שלך %{phone}. + send_verification_code: שלח קוד אימות + code_has_been_resent: הקוד נשלח מחדש + invalid_code: קוד שגוי awaiting_completion_by_the_other_party: "המתנה להשלמה מצד הצד השני" view: תצוגה hi_there: שלום, @@ -4873,8 +4975,16 @@ he: too_many_attempts: יותר מדי ניסיונות. verification_code: קוד אימות resend_code: שלח קוד מחדש + powered_by: 'מופעל על ידי' + count_documents_signed_with_html: '%{count} מסמכים נחתמו באמצעות' + open_source_documents_software: 'תוכנה בקוד פתוח למסמכים' nl: + require_phone_2fa_to_open: Vereis telefoon 2FA om te openen + the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: De afzender heeft gevraagd om tweefactorauthenticatie via een eenmalig wachtwoord, verzonden naar uw %{phone} telefoonnummer. + send_verification_code: Verificatiecode verzenden + code_has_been_resent: Code is opnieuw verzonden + invalid_code: Invalid code awaiting_completion_by_the_other_party: "In afwachting van voltooiing door de andere partij" view: Bekijken hi_there: Hallo, @@ -4932,8 +5042,16 @@ nl: too_many_attempts: Te veel pogingen. verification_code: Verificatiecode resend_code: Code Opnieuw Verzenden + powered_by: 'Aangedreven door' + count_documents_signed_with_html: '%{count} documenten ondertekend met' + open_source_documents_software: 'open source documenten software' ar: + require_phone_2fa_to_open: "تطلب فتح عبر تحقق الهاتف ذو العاملين" + the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: "المرسل طلب تحقق ذو عاملين عبر كلمة مرور لمرة واحدة مرسل إلى رقم هاتفك %{phone}." + send_verification_code: "إرسال رمز التحقق" + code_has_been_resent: "تم إعادة إرسال الرمز" + invalid_code: "رمز غير صالح" awaiting_completion_by_the_other_party: "في انتظار إكتمال الطرف الآخر" view: عرض hi_there: مرحبا, @@ -4991,8 +5109,16 @@ ar: too_many_attempts: عدد المحاولات كبير جدًا. verification_code: رمز التحقق resend_code: إعادة إرسال الرمز + powered_by: 'مشغل بواسطة' + count_documents_signed_with_html: '%{count} مستندات تم توقيعها باستخدام' + open_source_documents_software: 'برنامج مستندات مفتوح المصدر' ko: + require_phone_2fa_to_open: 휴대폰 2FA를 열 때 요구함 + the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: 발신자가 %{phone} 전화번호로 보내진 일회용 비밀번호를 통해 이중 인증을 요청했습니다. + send_verification_code: 인증 코드 보내기 + code_has_been_resent: 코드가 재전송되었습니다. + invalid_code: 잘못된 코드 awaiting_completion_by_the_other_party: "다른 당사자의 완료를 기다리고 있습니다" view: 보기 hi_there: 안녕하세요, @@ -5050,6 +5176,76 @@ ko: too_many_attempts: 시도 횟수가 너무 많습니다. verification_code: 인증 코드 resend_code: 코드 재전송 + powered_by: '제공:' + count_documents_signed_with_html: '%{count}개의 문서가 다음을 통해 서명됨' + open_source_documents_software: '오픈소스 문서 소프트웨어' + +ja: + require_phone_2fa_to_open: 電話による2段階認証が必要です + the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: 送信者は、%{phone} に送信されたワンタイムパスワードによる2段階認証を要求しました。 + send_verification_code: 認証コードを送信 + code_has_been_resent: コードが再送信されました + invalid_code: 無効なコードです + awaiting_completion_by_the_other_party: 他の当事者による完了を待機中 + view: 表示 + hi_there: こんにちは + download: ダウンロード + decline: 辞退 + declined: 辞退済み + decline_reason: 辞退の理由 + provide_a_reason: 理由を入力してください + notify_the_sender_with_the_reason_you_declined: 辞退理由を送信者に通知してください + form_has_been_declined_on_html: '%{time} にフォームが辞退されました' + name_declined_by_submitter: '"%{name}" は %{submitter} により辞退されました' + name_declined_by_submitter_with_the_following_reason: '"%{name}" は %{submitter} により次の理由で辞退されました:' + email: メール + digitally_signed_by: 電子署名者 + role: 役割 + provide_your_email_to_start: 開始するにはメールアドレスを入力してください + start: 開始 + reason: 理由 + starting: 開始中 + form_expired_at_html: '%{time} にフォームの有効期限が切れました' + invited_by_html: '%{name} により招待されました' + you_have_been_invited_to_submit_a_form: フォームの提出に招待されました + verification_code_code: '認証コード: %{code}' + signed_on_time: '%{time} に署名済み' + completed_on_time: '%{time} に完了' + document_has_been_signed_already: ドキュメントはすでに署名されています + form_has_been_submitted_already: フォームはすでに送信されています + send_copy_to_email: メールにコピーを送信 + sending: 送信中 + resubmit: 再送信 + form_has_been_deleted_by_html: '%{name} によりフォームが削除されました。' + or: または + download_documents: ドキュメントをダウンロード + downloading: ダウンロード中 + completed_successfully: 正常に完了しました + password: パスワード + sign_in: サインイン + signing_in: サインイン中 + sign_in_with_microsoft: Microsoftでサインイン + sign_in_with_google: Googleでサインイン + forgot_your_password_: パスワードをお忘れですか? + create_free_account: 無料アカウントを作成 + already_have_an_account: すでにアカウントをお持ちですか? + first_name: 名 + last_name: 姓 + sign_up: 登録 + signing_up: 登録中 + profile_details: プロフィールの詳細 + sign_up_with_google: Googleで登録 + sign_up_with_microsoft: Microsoftで登録 + by_creating_an_account_you_agree_to_our_html: 'プライバシーポリシーおよび利用規約に同意の上、アカウントを作成します。' + enter_email_to_continue: 続行するにはメールを入力してください + the_code_has_been_sent_to_your_email: コードがあなたのメールに送信されました + enter_the_verification_code_from_your_email: メールに記載された認証コードを入力してください + too_many_attempts: 試行回数が多すぎます + verification_code: 認証コード + resend_code: コードを再送信 + powered_by: '提供元:' + count_documents_signed_with_html: '%{count} 件のドキュメントが以下で署名されました' + open_source_documents_software: 'オープンソースのドキュメントソフトウェア' en-US: <<: *en diff --git a/lib/api_path_consider_json_middleware.rb b/lib/api_path_consider_json_middleware.rb index 48bb1b0a..013c2ee4 100644 --- a/lib/api_path_consider_json_middleware.rb +++ b/lib/api_path_consider_json_middleware.rb @@ -8,7 +8,9 @@ class ApiPathConsiderJsonMiddleware def call(env) if env['PATH_INFO'].starts_with?('/api') && (!env['PATH_INFO'].ends_with?('/documents') || env['REQUEST_METHOD'] != 'POST') && - !env['PATH_INFO'].ends_with?('/attachments') + !env['PATH_INFO'].ends_with?('/attachments') && + !env['PATH_INFO'].ends_with?('/submitter_sms_clicks') && + !env['PATH_INFO'].ends_with?('/submitter_email_clicks') env['CONTENT_TYPE'] = 'application/json' end diff --git a/lib/docuseal.rb b/lib/docuseal.rb index f74b99d1..15db4be0 100644 --- a/lib/docuseal.rb +++ b/lib/docuseal.rb @@ -12,7 +12,7 @@ module Docuseal DISCORD_URL = 'https://discord.gg/qygYCDGck9' TWITTER_URL = 'https://twitter.com/docusealco' TWITTER_HANDLE = '@docusealco' - CHATGPT_URL = 'https://chatgpt.com/g/g-9hg8AAw0r-docuseal' + CHATGPT_URL = "#{PRODUCT_URL}/chat".freeze SUPPORT_EMAIL = 'support@docuseal.com' HOST = ENV.fetch('HOST', 'localhost') AATL_CERT_NAME = 'docuseal_aatl' diff --git a/lib/replace_email_variables.rb b/lib/replace_email_variables.rb index a78cedf9..ac7732ae 100644 --- a/lib/replace_email_variables.rb +++ b/lib/replace_email_variables.rb @@ -19,6 +19,7 @@ module ReplaceEmailVariables SUBMITTERS_N_EMAIL = /\{+submitters\[(?\d+)\]\.email\}+/i SUBMITTERS_N_NAME = /\{+submitters\[(?\d+)\]\.name\}+/i SUBMITTERS_N_FIRST_NAME = /\{+submitters\[(?\d+)\]\.first_name\}+/i + SUBMITTERS_N_FIELD_VALUE = /\{+submitters\[(?\d+)\]\.(?[^}]+)\}+/i DOCUMENTS_LINKS = /\{+documents\.links\}+/i DOCUMENTS_LINK = /\{+documents\.link\}+/i @@ -59,6 +60,10 @@ module ReplaceEmailVariables build_submitters_n_field(submitter.submission, match[:index].to_i - 1, :first_name) end + text = replace(text, SUBMITTERS_N_FIELD_VALUE, html_escape:) do |match| + build_submitters_n_field(submitter.submission, match[:index].to_i - 1, :values, match[:field_name].to_s.strip) + end + replace(text, SENDER_EMAIL, html_escape:) { submitter.submission.created_by_user&.email.to_s.sub(/\+\w+@/, '@') } end # rubocop:enable Metrics @@ -69,10 +74,33 @@ module ReplaceEmailVariables ) end - def build_submitters_n_field(submission, index, field_name) + def build_submitters_n_field(submission, index, field_name, value_name = nil) uuid = (submission.template_submitters || submission.template.submitters).dig(index, 'uuid') - submission.submitters.find { |s| s.uuid == uuid }.try(field_name) + submitter = submission.submitters.find { |s| s.uuid == uuid } + + return unless submitter + + value = submitter.try(field_name) + + if value_name + field = (submission.template_fields || submission.template.fields).find { |e| e['name'] == value_name } + + return unless field + + value = + if field['type'].in?(%w[image signature initials stamp payment file]) + attachment_uuid = Array.wrap(value[field['uuid']]).first + + attachment = submitter.attachments.find { |e| e.uuid == attachment_uuid } + + ActiveStorage::Blob.proxy_url(attachment.blob) if attachment + else + value[field&.dig('uuid')] + end + end + + value end def replace(text, var, html_escape: false) diff --git a/lib/submissions.rb b/lib/submissions.rb index ab9d65ea..ddb4c3f8 100644 --- a/lib/submissions.rb +++ b/lib/submissions.rb @@ -134,16 +134,11 @@ module Submissions end def filtered_conditions_schema(submission, values: nil, include_submitter_uuid: nil) - fields_uuid_index = nil - (submission.template_schema || submission.template.schema).filter_map do |item| if item['conditions'].present? - fields_uuid_index ||= - (submission.template_fields || submission.template.fields).index_by { |f| f['uuid'] } - values ||= submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } - next unless check_item_conditions(item, values, fields_uuid_index, include_submitter_uuid:) + next unless check_item_conditions(item, values, submission.fields_uuid_index, include_submitter_uuid:) end item @@ -151,21 +146,21 @@ module Submissions end def filtered_conditions_fields(submitter, only_submitter_fields: true) - fields = submitter.submission.template_fields || submitter.submission.template.fields + submission = submitter.submission + + fields = submission.template_fields || submission.template.fields - fields_uuid_index = nil values = nil fields.filter_map do |field| next if field['submitter_uuid'] != submitter.uuid && only_submitter_fields if field['conditions'].present? - fields_uuid_index ||= fields.index_by { |f| f['uuid'] } - values ||= submitter.submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } + values ||= submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } submitter_conditions = [] - next unless check_item_conditions(field, values, fields_uuid_index, + next unless check_item_conditions(field, values, submission.fields_uuid_index, include_submitter_uuid: submitter.uuid, submitter_conditions_acc: submitter_conditions) diff --git a/lib/submissions/create_from_submitters.rb b/lib/submissions/create_from_submitters.rb index 6801aa13..a0c274fa 100644 --- a/lib/submissions/create_from_submitters.rb +++ b/lib/submissions/create_from_submitters.rb @@ -65,6 +65,12 @@ module Submissions raise BaseError, 'Defined more signing parties than in template' end + if template.preferences['validate_unique_submitters'] == true + submission_emails = submission.submitters.filter_map(&:email) + + raise BaseError, 'Recipient emails should differ' if submission_emails.uniq.size != submission_emails.size + end + next if submission.submitters.blank? maybe_add_invite_submitters(submission, template) diff --git a/lib/submissions/generate_audit_trail.rb b/lib/submissions/generate_audit_trail.rb index 3bc461a6..e1febb51 100644 --- a/lib/submissions/generate_audit_trail.rb +++ b/lib/submissions/generate_audit_trail.rb @@ -50,12 +50,12 @@ module Submissions } document.sign(io, **sign_params) + + Submissions::GenerateResultAttachments.maybe_enable_ltv(io, sign_params) else document.write(io) end - Submissions::GenerateResultAttachments.maybe_enable_ltv(io, sign_params) - ActiveStorage::Attachment.create!( blob: ActiveStorage::Blob.create_and_upload!( io: io.tap(&:rewind), filename: "#{I18n.t('audit_log')} - #{submission.template.name}.pdf" diff --git a/lib/submissions/generate_combined_attachment.rb b/lib/submissions/generate_combined_attachment.rb index 8e565a2b..7cda0b46 100644 --- a/lib/submissions/generate_combined_attachment.rb +++ b/lib/submissions/generate_combined_attachment.rb @@ -24,12 +24,12 @@ module Submissions } sign_pdf(io, pdf, sign_params) + + Submissions::GenerateResultAttachments.maybe_enable_ltv(io, sign_params) else pdf.write(io, incremental: true, validate: true) end - Submissions::GenerateResultAttachments.maybe_enable_ltv(io, sign_params) - ActiveStorage::Attachment.create!( blob: ActiveStorage::Blob.create_and_upload!( io: io.tap(&:rewind), filename: "#{submission.template.name}.pdf" diff --git a/lib/submissions/generate_result_attachments.rb b/lib/submissions/generate_result_attachments.rb index 2fe766c0..87c04402 100644 --- a/lib/submissions/generate_result_attachments.rb +++ b/lib/submissions/generate_result_attachments.rb @@ -221,7 +221,7 @@ module Submissions font_variant = nil unless font_name.in?(DEFAULT_FONTS) end - font = pdf.fonts.add(font_name, variant: font_variant) + font = pdf.fonts.add(font_name, variant: font_variant, custom_encoding: font_name.in?(DEFAULT_FONTS)) value = submitter.values[field['uuid']] value = field['default_value'] if field['type'] == 'heading' @@ -229,8 +229,9 @@ module Submissions text_align = field.dig('preferences', 'align').to_s.to_sym.presence || (value.to_s.match?(RTL_REGEXP) ? :right : :left) - layouter = HexaPDF::Layout::TextLayouter.new(text_valign: :center, text_align:, - font:, font_size:) + text_valign = (field.dig('preferences', 'valign').to_s.presence || 'center').to_sym + + layouter = HexaPDF::Layout::TextLayouter.new(text_valign:, text_align:, font:, font_size:) next if Array.wrap(value).compact_blank.blank? @@ -515,10 +516,19 @@ module Submissions 0 end + align_y_diff = + if text_valign == :top + 0 + elsif text_valign == :bottom + height_diff + TEXT_TOP_MARGIN + else + height_diff / 2 + end + layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width, height_diff.positive? ? box_height : area['h'] * height) .draw(canvas, (area['x'] * width) - right_align_x_adjustment + TEXT_LEFT_MARGIN, - height - (area['y'] * height) + height_diff - TEXT_TOP_MARGIN) + height - (area['y'] * height) + align_y_diff - TEXT_TOP_MARGIN) end end end diff --git a/lib/submitters.rb b/lib/submitters.rb index bcead9f2..32e97720 100644 --- a/lib/submitters.rb +++ b/lib/submitters.rb @@ -97,6 +97,7 @@ module Submitters def send_signature_requests(submitters, delay_seconds: nil) submitters.each_with_index do |submitter, index| next if submitter.email.blank? + next if submitter.declined_at? next if submitter.preferences['send_email'] == false if delay_seconds diff --git a/lib/submitters/serialize_for_api.rb b/lib/submitters/serialize_for_api.rb index ef17189e..01f1246d 100644 --- a/lib/submitters/serialize_for_api.rb +++ b/lib/submitters/serialize_for_api.rb @@ -49,7 +49,8 @@ module Submitters def serialize_events(events) events.map do |event| - event.as_json(only: %i[id submitter_id event_type event_timestamp]).merge('data' => event.data.slice('reason')) + event.as_json(only: %i[id submitter_id event_type event_timestamp]) + .merge('data' => event.data.slice('reason', 'firstname', 'lastname', 'method', 'country')) end end end diff --git a/lib/submitters/submit_values.rb b/lib/submitters/submit_values.rb index 0fd499f0..efff5b46 100644 --- a/lib/submitters/submit_values.rb +++ b/lib/submitters/submit_values.rb @@ -171,6 +171,8 @@ module Submitters end def build_formula_values(submitter) + submission_values = nil + computed_values = submitter.submission.template_fields.each_with_object({}) do |field, acc| next if field['submitter_uuid'] != submitter.uuid next if field['type'] == 'payment' @@ -179,7 +181,14 @@ module Submitters next if formula.blank? - acc[field['uuid']] = calculate_formula_value(formula, submitter.values.merge(acc.compact_blank)) + submission_values ||= + if submitter.submission.template_submitters.size > 1 + merge_submitters_values(submitter) + else + submitter.values + end + + acc[field['uuid']] = calculate_formula_value(formula, submission_values.merge(acc.compact_blank)) end computed_values.compact_blank @@ -204,8 +213,6 @@ module Submitters def maybe_remove_condition_values(submitter, required_field_uuids_acc: nil) submission = submitter.submission - fields_uuid_index = submission.template_fields.index_by { |e| e['uuid'] } - submitters_values = nil has_other_submitters = submission.template_submitters.size > 1 @@ -228,11 +235,11 @@ module Submitters end if has_other_submitters && !submitters_values && - field_conditions_other_submitter?(submitter, field, fields_uuid_index) + field_conditions_other_submitter?(submitter, field, submission.fields_uuid_index) submitters_values = merge_submitters_values(submitter) end - unless check_field_conditions(submitters_values || submitter.values, field, fields_uuid_index) + unless check_field_conditions(submitters_values || submitter.values, field, submission.fields_uuid_index) submitter.values.delete(field['uuid']) required_field_uuids_acc.delete(field['uuid']) end diff --git a/lib/templates/order.rb b/lib/templates/order.rb new file mode 100644 index 00000000..e1c7400d --- /dev/null +++ b/lib/templates/order.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Templates + module Order + module_function + + def call(templates, current_user, order) + case order + when 'used_at' + subquery = Submission.select(:template_id, Submission.arel_table[:created_at].maximum.as('created_at')) + .where(account_id: current_user.account_id) + .group(:template_id) + + templates = templates.joins( + Template.arel_table + .join(subquery.arel.as('submissions'), Arel::Nodes::OuterJoin) + .on(Template.arel_table[:id].eq(Submission.arel_table[:template_id])) + .join_sources + ) + + templates.order( + Arel::Nodes::Case.new + .when(Submission.arel_table[:created_at].gt(Template.arel_table[:updated_at])) + .then(Submission.arel_table[:created_at]) + .else(Template.arel_table[:updated_at]) + .desc + ) + when 'name' + templates.order(name: :asc) + else + templates.order(id: :desc) + end + end + end +end diff --git a/package.json b/package.json index 29d4e173..b0c4ece8 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ ] }, "browserslist": [ - "last 3 years" + "last 5 years" ], "devDependencies": { "@babel/eslint-parser": "^7.21.8", diff --git a/spec/system/signing_form_spec.rb b/spec/system/signing_form_spec.rb index bbf1d797..300edc2e 100644 --- a/spec/system/signing_form_spec.rb +++ b/spec/system/signing_form_spec.rb @@ -506,6 +506,9 @@ RSpec.describe 'Signing Form', type: :system do 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')) + + sleep 0.1 + click_button 'Complete' expect(page).to have_content('Document has been signed!')