Compare commits

...
48 Commits
Author SHA1 Message Date
Alex Turchyn fa2d86b20a Merge from docusealco/wip 2024-11-25 12:49:04 +02:00
Pete Matsyburka 8e212e4586 adjust title size 2024-11-24 13:18:37 +02:00
Pete Matsyburka a59c18f04b adjust filters ui 2024-11-24 13:03:58 +02:00
Pete Matsyburka 1ab0f1c90c fix form error message 2024-11-23 22:21:25 +02:00
Pete Matsyburka 9beaaa0851 adjust filters 2024-11-23 17:38:11 +02:00
Alex Turchyn b86aac86d1 add template submissions filters 2024-11-22 22:23:20 +02:00
Pete Matsyburka c2eb94b571 update url 2024-11-22 12:10:29 +02:00
Pete Matsyburka a377e45a63 use product email url 2024-11-22 10:30:42 +02:00
Pete Matsyburka 58574abc6b fix filename 2024-11-21 23:18:27 +02:00
Pete Matsyburka cbae888971 add file host param 2024-11-21 23:03:30 +02:00
Pete Matsyburka 944e1f31db adjust condition 2024-11-21 18:23:34 +02:00
Pete Matsyburka 711b3b3190 use filename format in email attachments 2024-11-20 21:49:37 +02:00
Pete Matsyburka e7abadcefb add filename format option 2024-11-20 01:12:26 +02:00
Alex Turchyn 18d60f5522 add signing form specs 2024-11-19 21:28:58 +02:00
Pete Matsyburka 6d5c6e086d use email host 2024-11-19 00:05:41 +02:00
Pete Matsyburka 5a48ab0360 configure form link 2024-11-18 21:08:32 +02:00
Pete Matsyburka 599785b6d1 fix number cast empty string 2024-11-17 21:29:20 +02:00
Pete Matsyburka f2fd5fb82b fix default number 2024-11-17 21:15:10 +02:00
Pete Matsyburka f2782bd84e fix form show 2024-11-16 23:06:00 +02:00
Pete Matsyburka e52a721fc9 fix typo 2024-11-16 18:19:25 +02:00
Pete Matsyburka b035123ff9 disable email var 2024-11-16 16:13:30 +02:00
Pete Matsyburka ca8de23082 use account archived at 2024-11-15 22:17:55 +02:00
Pete Matsyburka f514654c40 rails spec hook 2024-11-15 00:30:33 +02:00
Pete Matsyburka 99bc528a6c fix spec 2024-11-14 23:36:03 +02:00
Pete Matsyburka f3c77d0177 fix bulk order 2024-11-14 22:19:22 +02:00
Pete Matsyburka db2b5f35bd fix cells length 2024-11-14 21:15:40 +02:00
Pete Matsyburka 5728381725 fix role 2024-11-14 20:09:29 +02:00
Pete Matsyburka a7affb1a49 add link to export 2024-11-14 20:04:51 +02:00
Pete Matsyburka 21bf5ed9f7 fix fields message 2024-11-14 14:38:28 +02:00
Pete Matsyburka 8ddd4ee916 adjust duration 2024-11-14 09:35:20 +02:00
Pete Matsyburka 0e2fd8ee4f use email host 2024-11-13 17:53:55 +02:00
Pete Matsyburka 27901063ed fix preview 2024-11-12 17:11:09 +02:00
Pete Matsyburka ce31ebde1e add delay 2024-11-11 19:32:27 +02:00
Alex Turchyn 0c2abf211e Merge from docusealco/wip 2024-11-11 17:44:28 +02:00
Pete Matsyburka 8ecaa4fd7a adjust custom font size 2024-11-11 14:25:07 +02:00
Pete Matsyburka 6759c26d86 fix rubocop 2024-11-10 13:11:25 +02:00
Pete Matsyburka 76f1f140ac add field color 2024-11-10 12:48:53 +02:00
Pete Matsyburka ca7ab5aab7 adjust webhooks controller 2024-11-09 11:15:19 +02:00
Alex Turchyn fd3530ac62 update Webhooks UI 2024-11-09 00:41:46 +02:00
Pete Matsyburka 4ea863592b fix form 2024-11-08 22:12:07 +02:00
Pete Matsyburka 3f87d0336d do not use same reply to as to 2024-11-08 13:02:32 +02:00
Pete Matsyburka e251482481 unique party names 2024-11-08 12:15:13 +02:00
Pete Matsyburka c79fca75f3 add complete button 2024-11-07 23:41:49 +02:00
Alex Turchyn 0b0f0b497e fix acroform select field 2024-11-06 20:30:01 +02:00
Pete Matsyburka e9034d8da7 webhook secret priority 2024-11-06 14:06:33 +02:00
Pete Matsyburka 8fbb8b8f6e do not touch updated_at 2024-11-05 22:55:03 +02:00
Alex Turchyn 4635fdd32b remove replace_timestamps helper 2024-11-05 22:34:14 +02:00
Pete Matsyburka 6dd4382446 optimize webhooks query 2024-11-04 16:19:43 +02:00
105 changed files with 2320 additions and 499 deletions

No files matched your search

+4 -4
View File
@@ -51,19 +51,19 @@ Metrics/AbcSize:
- spec/**/* - spec/**/*
Metrics/ModuleLength: Metrics/ModuleLength:
Max: 500 Max: 1000
Metrics/ClassLength: Metrics/ClassLength:
Max: 500 Max: 1000
RSpec/NestedGroups: RSpec/NestedGroups:
Max: 6 Max: 6
RSpec/MultipleExpectations: RSpec/MultipleExpectations:
Max: 20 Max: 25
RSpec/ExampleLength: RSpec/ExampleLength:
Max: 40 Max: 50
RSpec/MultipleMemoizedHelpers: RSpec/MultipleMemoizedHelpers:
Max: 9 Max: 9
@@ -17,7 +17,8 @@ class AccountConfigsController < ApplicationController
AccountConfig::FLATTEN_RESULT_PDF_KEY, AccountConfig::FLATTEN_RESULT_PDF_KEY,
AccountConfig::WITH_SIGNATURE_ID, AccountConfig::WITH_SIGNATURE_ID,
AccountConfig::COMBINE_PDF_RESULT_KEY, AccountConfig::COMBINE_PDF_RESULT_KEY,
AccountConfig::REQUIRE_SIGNING_REASON_KEY AccountConfig::REQUIRE_SIGNING_REASON_KEY,
AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY
].freeze ].freeze
InvalidKey = Class.new(StandardError) InvalidKey = Class.new(StandardError)
@@ -15,6 +15,7 @@ class ApplicationController < ActionController::Base
helper_method :button_title, helper_method :button_title,
:current_account, :current_account,
:form_link_host,
:svg_icon :svg_icon
impersonates :user, with: ->(uuid) { User.find_by(uuid:) } impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
@@ -105,6 +106,10 @@ class ApplicationController < ActionController::Base
render_to_string(partial: "icons/#{icon_name}", locals: { class: }) render_to_string(partial: "icons/#{icon_name}", locals: { class: })
end end
def form_link_host
Docuseal.default_url_options[:host]
end
def maybe_redirect_com def maybe_redirect_com
return if request.domain != 'docuseal.co' return if request.domain != 'docuseal.co'
@@ -11,6 +11,8 @@ class EsignSettingsController < ApplicationController
end end
end end
prepend_before_action :maybe_redirect_com, only: %i[show]
before_action :load_encrypted_config before_action :load_encrypted_config
authorize_resource :encrypted_config, parent: false, only: %i[new create] authorize_resource :encrypted_config, parent: false, only: %i[new create]
authorize_resource :encrypted_config, only: %i[update destroy show] authorize_resource :encrypted_config, only: %i[update destroy show]
@@ -9,7 +9,14 @@ class SubmissionsArchivedController < ApplicationController
.or(@submissions.where.not(templates: { archived_at: nil })) .or(@submissions.where.not(templates: { archived_at: nil }))
.preload(:created_by_user, template: :author) .preload(:created_by_user, template: :author)
@submissions = Submissions.search(@submissions, params[:q], search_template: true) @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
end end
@@ -6,6 +6,8 @@ class SubmissionsController < ApplicationController
load_and_authorize_resource :submission, only: %i[show destroy] load_and_authorize_resource :submission, only: %i[show destroy]
prepend_before_action :maybe_redirect_com, only: %i[show]
def show def show
@submission = Submissions.preload_with_pages(@submission) @submission = Submissions.preload_with_pages(@submission)
@@ -11,10 +11,17 @@ class SubmissionsDashboardController < ApplicationController
.preload(:created_by_user, template: :author) .preload(:created_by_user, template: :author)
@submissions = Submissions.search(@submissions, params[:q], search_template: true) @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.pending if params[:status] == 'pending'
@submissions = @submissions.completed if params[:status] == 'completed' @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
end end
@@ -53,8 +53,15 @@ class SubmissionsDownloadController < ApplicationController
end end
def build_urls(submitter) 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| 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
end end
@@ -65,6 +72,13 @@ class SubmissionsDownloadController < ApplicationController
attachment = submitter.submission.combined_document_attachment attachment = submitter.submission.combined_document_attachment
attachment ||= Submissions::GenerateCombinedAttachment.call(submitter) 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
end end
@@ -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
@@ -4,7 +4,7 @@ class SubmissionsPreviewController < ApplicationController
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
skip_authorization_check 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 TTL = 40.minutes
@@ -20,7 +20,7 @@ class SubmissionsPreviewController < ApplicationController
@submission ||= Submission.find_by!(slug: params[:slug]) @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') raise ActionController::RoutingError, I18n.t('not_found')
end end
+11 -5
View File
@@ -7,8 +7,6 @@ class SubmitFormController < ApplicationController
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
skip_authorization_check skip_authorization_check
before_action :maybe_redirect_com, only: %i[show completed]
CONFIG_KEYS = [].freeze CONFIG_KEYS = [].freeze
def show def show
@@ -17,7 +15,9 @@ class SubmitFormController < ApplicationController
submission = @submitter.submission submission = @submitter.submission
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at? 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 :expired if submission.expired?
return render :declined if @submitter.declined_at? return render :declined if @submitter.declined_at?
return render :awaiting if submission.template.preferences['submitters_order'] == 'preserved' && return render :awaiting if submission.template.preferences['submitters_order'] == 'preserved' &&
@@ -27,8 +27,7 @@ class SubmitFormController < ApplicationController
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user) Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
@attachments_index = ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments) @attachments_index = build_attachments_index(submission)
.preload(:blob).index_by(&:uuid)
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS) @form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
@@ -80,4 +79,11 @@ class SubmitFormController < ApplicationController
end end
def success; 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 end
@@ -6,7 +6,7 @@ class SubmittersSendEmailController < ApplicationController
def create def create
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter, if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
event_type: 'send_email', 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) Rollbar.warning("Already sent: #{@submitter.id}") if defined?(Rollbar)
return redirect_back(fallback_location: submission_path(@submitter.submission), return redirect_back(fallback_location: submission_path(@submitter.submission),
@@ -7,8 +7,15 @@ class TemplatesArchivedSubmissionsController < ApplicationController
def index def index
@submissions = @submissions.where.not(archived_at: nil) @submissions = @submissions.where.not(archived_at: nil)
@submissions = Submissions.search(@submissions, params[:q], search_values: true) @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 rescue ActiveRecord::RecordNotFound
redirect_to root_path redirect_to root_path
end end
+8 -1
View File
@@ -9,13 +9,20 @@ class TemplatesController < ApplicationController
submissions = @template.submissions.accessible_by(current_ability) submissions = @template.submissions.accessible_by(current_ability)
submissions = submissions.active if @template.archived_at.blank? submissions = submissions.active if @template.archived_at.blank?
submissions = Submissions.search(submissions, params[:q], search_values: true) submissions = Submissions.search(submissions, params[:q], search_values: true)
submissions = Submissions::Filter.call(submissions, current_user, params)
@base_submissions = submissions @base_submissions = submissions
submissions = submissions.pending if params[:status] == 'pending' submissions = submissions.pending if params[:status] == 'pending'
submissions = submissions.completed if params[:status] == 'completed' 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 rescue ActiveRecord::RecordNotFound
redirect_to root_path redirect_to root_path
end end
+2 -1
View File
@@ -10,7 +10,8 @@ class WebhookSecretController < ApplicationController
webhook_secret_params[:key] => webhook_secret_params[:value] webhook_secret_params[:key] => webhook_secret_params[:value]
}.compact_blank) }.compact_blank)
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_secret_has_been_saved')) redirect_back(fallback_location: settings_webhook_path(@webhook_url),
notice: I18n.t('webhook_secret_has_been_saved'))
end end
private private
+34 -9
View File
@@ -1,22 +1,47 @@
# frozen_string_literal: true # frozen_string_literal: true
class WebhookSettingsController < ApplicationController class WebhookSettingsController < ApplicationController
before_action :load_webhook_url load_and_authorize_resource :webhook_url, parent: false, only: %i[index show new create update destroy]
authorize_resource :webhook_url, parent: false load_and_authorize_resource :webhook_url, only: %i[resend], id_param: :webhook_id
def index
@webhook_urls = @webhook_urls.order(id: :desc)
@webhook_url = @webhook_urls.first_or_initialize
render @webhook_urls.size > 1 ? 'index' : 'show'
end
def show; end def show; end
def new; end
def create def create
@webhook_url.assign_attributes(webhook_params) @webhook_url.save!
@webhook_url.url.present? ? @webhook_url.save! : @webhook_url.delete redirect_to settings_webhooks_path, notice: I18n.t('webhook_url_has_been_saved')
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_url_has_been_saved'))
end end
def update def update
@webhook_url.update!(update_params)
redirect_back(fallback_location: settings_webhook_path(@webhook_url),
notice: I18n.t('webhook_url_has_been_updated'))
end
def destroy
@webhook_url.destroy!
redirect_to settings_webhooks_path, notice: I18n.t('webhook_url_has_been_deleted')
end
def resend
submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last
if submitter.blank? || @webhook_url.blank?
return redirect_back(fallback_location: settings_webhooks_path,
alert: I18n.t('unable_to_resend_webhook_request'))
end
SendFormCompletedWebhookRequestJob.perform_async('submitter_id' => submitter.id, SendFormCompletedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
'webhook_url_id' => @webhook_url.id) 'webhook_url_id' => @webhook_url.id)
@@ -25,11 +50,11 @@ class WebhookSettingsController < ApplicationController
private private
def load_webhook_url def create_params
@webhook_url = current_account.webhook_urls.first_or_initialize params.require(:webhook_url).permit(:url, events: []).reverse_merge(events: [])
end end
def webhook_params def update_params
params.require(:webhook_url).permit(:url) params.require(:webhook_url).permit(:url)
end end
end end
+2
View File
@@ -30,6 +30,7 @@ import ToggleAttribute from './elements/toggle_attribute'
import LinkedInput from './elements/linked_input' import LinkedInput from './elements/linked_input'
import CheckboxGroup from './elements/checkbox_group' import CheckboxGroup from './elements/checkbox_group'
import MaskedInput from './elements/masked_input' import MaskedInput from './elements/masked_input'
import SetDateButton from './elements/set_date_button'
import * as TurboInstantClick from './lib/turbo_instant_click' import * as TurboInstantClick from './lib/turbo_instant_click'
@@ -97,6 +98,7 @@ safeRegisterElement('toggle-attribute', ToggleAttribute)
safeRegisterElement('linked-input', LinkedInput) safeRegisterElement('linked-input', LinkedInput)
safeRegisterElement('checkbox-group', CheckboxGroup) safeRegisterElement('checkbox-group', CheckboxGroup)
safeRegisterElement('masked-input', MaskedInput) safeRegisterElement('masked-input', MaskedInput)
safeRegisterElement('set-date-button', SetDateButton)
safeRegisterElement('template-builder', class extends HTMLElement { safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () { connectedCallback () {
+4
View File
@@ -12,6 +12,10 @@ window.customElements.define('draw-signature', class extends HTMLElement {
this.pad = new SignaturePad(this.canvas) this.pad = new SignaturePad(this.canvas)
if (this.dataset.color) {
this.pad.penColor = this.dataset.color
}
this.pad.addEventListener('endStroke', () => { this.pad.addEventListener('endStroke', () => {
this.updateSubmitButtonVisibility() this.updateSubmitButtonVisibility()
}) })
@@ -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)
}
}
+6 -3
View File
@@ -39,7 +39,6 @@
</template> </template>
</div> </div>
<div <div
v-if="isActive"
ref="scrollToElem" ref="scrollToElem"
class="absolute" class="absolute"
:style="{ top: scrollPadding }" :style="{ top: scrollPadding }"
@@ -405,8 +404,12 @@ export default {
} }
if (this.field.preferences?.font_size) { if (this.field.preferences?.font_size) {
style.fontSize = `clamp(4pt, 1.6vw, ${this.field.preferences.font_size}pt)` style.fontSize = `clamp(4pt, 1.6vw, ${parseInt(this.field.preferences.font_size) * 1.23}pt)`
style.lineHeight = `clamp(6pt, 2.0vw, ${parseInt(this.field.preferences.font_size) + 3}pt)` style.lineHeight = `clamp(6pt, 2.0vw, ${parseInt(this.field.preferences.font_size) * 1.23 + 3}pt)`
}
if (this.field.preferences?.color) {
style.color = this.field.preferences.color
} }
return style return style
@@ -37,6 +37,7 @@
<AppearsOn :field="field" /> <AppearsOn :field="field" />
<div class="text-center"> <div class="text-center">
<input <input
:id="field.uuid"
ref="input" ref="input"
v-model="value" v-model="value"
class="base-input !text-2xl text-center w-full" class="base-input !text-2xl text-center w-full"
@@ -1,5 +1,6 @@
<template> <template>
<div <div
id="dropzone"
class="flex h-32 w-full" class="flex h-32 w-full"
@dragover.prevent @dragover.prevent
@drop.prevent="onDropFiles" @drop.prevent="onDropFiles"
+78 -10
View File
@@ -11,7 +11,7 @@
:with-label="!isAnonymousChecboxes && showFieldNames" :with-label="!isAnonymousChecboxes && showFieldNames"
:current-step="currentStepFields" :current-step="currentStepFields"
:scroll-padding="scrollPadding" :scroll-padding="scrollPadding"
@focus-step="[saveStep(), currentField.type !== 'checkbox' ? isFormVisible = true : '', goToStep($event, false, true)]" @focus-step="[saveStep(), goToStep($event, false, true), currentField.type !== 'checkbox' ? isFormVisible = true : '']"
/> />
<FieldAreas <FieldAreas
:steps="readonlyConditionalFields.map((e) => [e])" :steps="readonlyConditionalFields.map((e) => [e])"
@@ -24,6 +24,44 @@
:fields="formulaFields" :fields="formulaFields"
:values="values" :values="values"
/> />
<Teleport
v-if="completeButtonToRef"
:to="completeButtonToRef"
>
<span
v-if="(emptyValueRequiredStep && ((stepFields.length - 1) !== currentStep || currentStepFields !== emptyValueRequiredStep)) || isCompleted"
class="tooltip-left"
:class="{ tooltip: !isCompleted }"
:data-tip="t('fill_all_required_fields_to_complete')"
>
<button
class="btn btn-sm btn-neutral text-white px-4 w-full flex justify-center btn-disabled pointer-events-auto"
@click="[isFormVisible = true, !isCompleted && goToStep(stepFields.indexOf(emptyValueRequiredStep), true, false)]"
>
{{ t('complete') }}
</button>
</span>
<button
v-else
id="complete_form_button"
class="btn btn-sm btn-neutral text-white px-4 w-full flex justify-center"
form="steps_form"
type="submit"
name="completed"
value="true"
:disabled="isSubmittingComplete"
>
<span class="flex items-center">
<IconInnerShadowTop
v-if="isSubmittingComplete"
class="mr-1 animate-spin w-5 h-5"
/>
<span>
{{ t('complete') }}
</span>
</span>
</button>
</Teleport>
<button <button
v-if="!isFormVisible" v-if="!isFormVisible"
id="expand_form_button" id="expand_form_button"
@@ -74,6 +112,7 @@
> >
<form <form
v-if="!isCompleted && !isInvite" v-if="!isCompleted && !isInvite"
id="steps_form"
ref="form" ref="form"
:action="submitPath" :action="submitPath"
method="post" method="post"
@@ -592,6 +631,11 @@ export default {
required: false, required: false,
default: false default: false
}, },
completeButtonToRef: {
type: Object,
required: false,
default: null
},
attachments: { attachments: {
type: Array, type: Array,
required: false, required: false,
@@ -772,6 +816,7 @@ export default {
phoneVerifiedValues: {}, phoneVerifiedValues: {},
orientation: screen?.orientation?.type, orientation: screen?.orientation?.type,
isSubmitting: false, isSubmitting: false,
isSubmittingComplete: false,
submittedValues: {}, submittedValues: {},
recalculateButtonDisabledKey: '' recalculateButtonDisabledKey: ''
} }
@@ -780,6 +825,13 @@ export default {
isMobile () { isMobile () {
return /android|iphone|ipad/i.test(navigator.userAgent) return /android|iphone|ipad/i.test(navigator.userAgent)
}, },
emptyValueRequiredStep () {
return this.stepFields.find((fields, index) => {
return fields.some((f) => {
return f.required && isEmpty(this.values[f.uuid])
})
})
},
submitButtonText () { submitButtonText () {
if (this.alwaysMinimize) { if (this.alwaysMinimize) {
return this.t('submit') return this.t('submit')
@@ -987,10 +1039,14 @@ export default {
} else if (['not_empty', 'checked'].includes(c.action)) { } else if (['not_empty', 'checked'].includes(c.action)) {
return acc && !isEmpty(this.values[c.field_uuid]) return acc && !isEmpty(this.values[c.field_uuid])
} else if (['equal', 'contains'].includes(c.action) && field) { } else if (['equal', 'contains'].includes(c.action) && field) {
const option = field.options.find((o) => o.uuid === c.value) if (field.options) {
const values = [this.values[c.field_uuid]].flat() 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) { } else if (['not_equal', 'does_not_contain'].includes(c.action) && field) {
const option = field.options.find((o) => o.uuid === c.value) const option = field.options.find((o) => o.uuid === c.value)
const values = [this.values[c.field_uuid]].flat() const values = [this.values[c.field_uuid]].flat()
@@ -1146,9 +1202,15 @@ export default {
scrollIntoArea (area) { scrollIntoArea (area) {
return this.$refs.areas.scrollIntoArea(area) return this.$refs.areas.scrollIntoArea(area)
}, },
async submitStep () { async submitStep (e) {
this.isSubmitting = true this.isSubmitting = true
const forceComplete = e?.submitter?.getAttribute('name') === 'completed'
if (forceComplete) {
this.isSubmittingComplete = true
}
const submitStep = this.currentStep const submitStep = this.currentStep
const stepPromise = ['signature', 'phone', 'initials', 'payment'].includes(this.currentField.type) const stepPromise = ['signature', 'phone', 'initials', 'payment'].includes(this.currentField.type)
@@ -1157,7 +1219,7 @@ export default {
stepPromise().then(async () => { stepPromise().then(async () => {
const emptyRequiredField = this.stepFields.find((fields, index) => { const emptyRequiredField = this.stepFields.find((fields, index) => {
if (index >= submitStep) { if (forceComplete ? index === submitStep : index >= submitStep) {
return false return false
} }
@@ -1167,7 +1229,7 @@ export default {
}) })
const formData = new FormData(this.$refs.form) const formData = new FormData(this.$refs.form)
const isLastStep = submitStep === this.stepFields.length - 1 const isLastStep = (submitStep === this.stepFields.length - 1) || forceComplete
if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length) { if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length) {
formData.append('completed', 'true') formData.append('completed', 'true')
@@ -1185,14 +1247,18 @@ export default {
if (response.status === 422 || response.status === 500) { if (response.status === 422 || response.status === 500) {
const data = await response.json() 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)) return Promise.reject(new Error(data.error))
} }
const nextStep = (isLastStep && emptyRequiredField) || this.stepFields[submitStep + 1] const nextStep = (isLastStep && emptyRequiredField) || (forceComplete ? null : this.stepFields[submitStep + 1])
if (nextStep) { if (nextStep) {
if (this.alwaysMinimize) { if (this.alwaysMinimize) {
@@ -1213,6 +1279,7 @@ export default {
console.error(error) console.error(error)
}).finally(() => { }).finally(() => {
this.isSubmitting = false this.isSubmitting = false
this.isSubmittingComplete = false
}) })
}).catch(error => { }).catch(error => {
if (error?.message === 'Image too small') { if (error?.message === 'Image too small') {
@@ -1222,6 +1289,7 @@ export default {
} }
}).finally(() => { }).finally(() => {
this.isSubmitting = false this.isSubmitting = false
this.isSubmittingComplete = false
}) })
}, },
minimizeForm () { minimizeForm () {
+52 -26
View File
@@ -1,5 +1,6 @@
const en = { const en = {
complete: 'Complete', complete: 'Complete',
fill_all_required_fields_to_complete: 'Fill all required fields to complete',
sign_and_complete: 'Sign and Complete', sign_and_complete: 'Sign and Complete',
text: 'Text', text: 'Text',
by_clicking_you_agree_to_the: 'By clicking "{button}", you agree to the', by_clicking_you_agree_to_the: 'By clicking "{button}", you agree to the',
@@ -66,7 +67,7 @@ const en = {
documents_have_been_signed: 'Documents have been signed!', documents_have_been_signed: 'Documents have been signed!',
create_a_free_account: 'Create a Free Account', create_a_free_account: 'Create a Free Account',
powered_by: 'Powered by', 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', open_source_documents_software: 'open source documents software',
verified_phone_number: 'Verify Phone Number', verified_phone_number: 'Verify Phone Number',
use_international_format: 'Use international format: +1xxx', use_international_format: 'Use international format: +1xxx',
@@ -88,11 +89,13 @@ const en = {
pay_with_strip: 'Pay with Stripe', pay_with_strip: 'Pay with Stripe',
reupload: 'Reupload', reupload: 'Reupload',
upload: 'Upload', upload: 'Upload',
files: 'Files' files: 'Files',
signature_is_too_small_please_redraw: 'Signature is too small. Please redraw.'
} }
const es = { const es = {
complete: 'Completar', complete: 'Completar',
fill_all_required_fields_to_complete: 'Complete todos los campos requeridos para finalizar',
sign_and_complete: 'Firmar y Completar', sign_and_complete: 'Firmar y Completar',
invite: 'Invitar', invite: 'Invitar',
email: 'Correo electrónico', email: 'Correo electrónico',
@@ -158,7 +161,7 @@ const es = {
documents_have_been_signed: '¡Los documentos han sido firmados!', documents_have_been_signed: '¡Los documentos han sido firmados!',
create_a_free_account: 'Crear una Cuenta Gratuita', create_a_free_account: 'Crear una Cuenta Gratuita',
powered_by: 'Desarrollado por', 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', open_source_documents_software: 'software de documentos de código abierto',
verified_phone_number: 'Verificar número de teléfono', verified_phone_number: 'Verificar número de teléfono',
use_international_format: 'Usar formato internacional: +1xxx', use_international_format: 'Usar formato internacional: +1xxx',
@@ -180,11 +183,13 @@ const es = {
pay_with_strip: 'Pagar con Stripe', pay_with_strip: 'Pagar con Stripe',
reupload: 'Volver a subir', reupload: 'Volver a subir',
upload: '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 = { const it = {
complete: 'Completa', complete: 'Completa',
fill_all_required_fields_to_complete: 'Compila tutti i campi obbligatori per completare',
sign_and_complete: 'Firma e Completa', sign_and_complete: 'Firma e Completa',
invite: 'Invita', invite: 'Invita',
email: 'Email', email: 'Email',
@@ -250,7 +255,7 @@ const it = {
documents_have_been_signed: 'I documenti sono stati firmati!', documents_have_been_signed: 'I documenti sono stati firmati!',
create_a_free_account: 'Crea un Account Gratuito', create_a_free_account: 'Crea un Account Gratuito',
powered_by: 'Desarrollado por', 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', open_source_documents_software: 'software di documenti open source',
verified_phone_number: 'Verifica numero di telefono', verified_phone_number: 'Verifica numero di telefono',
use_international_format: 'Usa formato internazionale: +1xxx', use_international_format: 'Usa formato internazionale: +1xxx',
@@ -272,11 +277,13 @@ const it = {
pay_with_strip: 'Paga con Stripe', pay_with_strip: 'Paga con Stripe',
reupload: 'Ricarica', reupload: 'Ricarica',
upload: 'Carica', upload: 'Carica',
files: 'File' files: 'File',
signature_is_too_small_please_redraw: 'La firma è troppo piccola. Ridisegnala per favore.'
} }
const de = { const de = {
complete: 'Abschließen', complete: 'Abschließen',
fill_all_required_fields_to_complete: 'Alle erforderlichen Felder ausfüllen, um abzuschließen',
sign_and_complete: 'Signieren und Abschließen', sign_and_complete: 'Signieren und Abschließen',
invite: 'Einladen', invite: 'Einladen',
email: 'E-Mail', email: 'E-Mail',
@@ -342,7 +349,7 @@ const de = {
documents_have_been_signed: 'Dokumente wurden unterschrieben!', documents_have_been_signed: 'Dokumente wurden unterschrieben!',
create_a_free_account: 'Kostenloses Konto erstellen', create_a_free_account: 'Kostenloses Konto erstellen',
powered_by: 'Bereitgestellt von', 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', open_source_documents_software: 'Open-Source-Dokumentensoftware',
verified_phone_number: 'Telefonnummer überprüfen', verified_phone_number: 'Telefonnummer überprüfen',
use_international_format: 'Internationales Format verwenden: +1xxx', use_international_format: 'Internationales Format verwenden: +1xxx',
@@ -364,11 +371,13 @@ const de = {
pay_with_strip: 'Mit Stripe bezahlen', pay_with_strip: 'Mit Stripe bezahlen',
reupload: 'Erneut hochladen', reupload: 'Erneut hochladen',
upload: 'Hochladen', upload: 'Hochladen',
files: 'Dateien' files: 'Dateien',
signature_is_too_small_please_redraw: 'Die Unterschrift ist zu klein. Bitte erneut zeichnen.'
} }
const fr = { const fr = {
complete: 'Terminer', complete: 'Terminer',
fill_all_required_fields_to_complete: 'Veuillez remplir tous les champs obligatoires pour compléter',
sign_and_complete: 'Signer et Terminer', sign_and_complete: 'Signer et Terminer',
invite: 'Inviter', invite: 'Inviter',
email: 'Courriel', email: 'Courriel',
@@ -434,7 +443,7 @@ const fr = {
documents_have_been_signed: 'Les documents ont été signés!', documents_have_been_signed: 'Les documents ont été signés!',
create_a_free_account: 'Créer un Compte Gratuit', create_a_free_account: 'Créer un Compte Gratuit',
powered_by: 'Propulsé par', 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', open_source_documents_software: 'logiciel de documents open source',
verified_phone_number: 'Vérifier le numéro de téléphone', verified_phone_number: 'Vérifier le numéro de téléphone',
use_international_format: 'Utiliser le format international : +1xxx', use_international_format: 'Utiliser le format international : +1xxx',
@@ -456,11 +465,13 @@ const fr = {
pay_with_strip: 'Paiement avec Stripe', pay_with_strip: 'Paiement avec Stripe',
reupload: 'Recharger', reupload: 'Recharger',
upload: 'Télécharger', upload: 'Télécharger',
files: 'Fichiers' files: 'Fichiers',
signature_is_too_small_please_redraw: 'La signature est trop petite. Veuillez la redessiner.'
} }
const pl = { const pl = {
complete: 'Zakończ', complete: 'Zakończ',
fill_all_required_fields_to_complete: 'Uzupełnij wszystkie wymagane pola, aby zakończyć',
sign_and_complete: 'Podpisz i zakończ', sign_and_complete: 'Podpisz i zakończ',
invite: 'Zaproś', invite: 'Zaproś',
email: 'E-mail', email: 'E-mail',
@@ -526,7 +537,7 @@ const pl = {
documents_have_been_signed: 'Dokumenty zostały podpisane!', documents_have_been_signed: 'Dokumenty zostały podpisane!',
create_a_free_account: 'Utwórz darmowe konto', create_a_free_account: 'Utwórz darmowe konto',
powered_by: 'Napędzany przez', 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', open_source_documents_software: 'oprogramowanie do dokumentów open source',
verified_phone_number: 'Zweryfikuj numer telefonu', verified_phone_number: 'Zweryfikuj numer telefonu',
use_international_format: 'Użyj międzynarodowego formatu: +1xxx', use_international_format: 'Użyj międzynarodowego formatu: +1xxx',
@@ -548,11 +559,13 @@ const pl = {
pay_with_strip: 'Płatność za pomocą Stripe', pay_with_strip: 'Płatność za pomocą Stripe',
reupload: 'Ponowne przesłanie', reupload: 'Ponowne przesłanie',
upload: 'Przesył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 = { const uk = {
complete: 'Завершити', complete: 'Завершити',
fill_all_required_fields_to_complete: "Заповніть всі обов'язкові поля для завершення",
sign_and_complete: 'Підписати і завершити', sign_and_complete: 'Підписати і завершити',
invite: 'Запросити', invite: 'Запросити',
email: 'Електронна пошта', email: 'Електронна пошта',
@@ -618,7 +631,7 @@ const uk = {
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: 'Будь ласка, позначте прапорець, щоб продовжити.',
open_source_documents_software: 'відкритий програмний засіб для документів', open_source_documents_software: 'відкритий програмний засіб для документів',
verified_phone_number: 'Підтвердіть номер телефону', verified_phone_number: 'Підтвердіть номер телефону',
use_international_format: 'Використовуйте міжнародний формат: +1xxx', use_international_format: 'Використовуйте міжнародний формат: +1xxx',
@@ -640,11 +653,13 @@ const uk = {
pay_with_strip: 'Сплатити за допомогою Stripe', pay_with_strip: 'Сплатити за допомогою Stripe',
reupload: 'Перезавантажити', reupload: 'Перезавантажити',
upload: 'Завантажити', upload: 'Завантажити',
files: 'Файли' files: 'Файли',
signature_is_too_small_please_redraw: 'Підпис занадто малий. Будь ласка, перемалюйте його.'
} }
const cs = { const cs = {
complete: 'Dokončit', complete: 'Dokončit',
fill_all_required_fields_to_complete: 'Please complete all mandatory fields',
sign_and_complete: 'Podepsat a dokončit', sign_and_complete: 'Podepsat a dokončit',
invite: 'Pozvat', invite: 'Pozvat',
email: 'E-mail', email: 'E-mail',
@@ -710,7 +725,7 @@ const cs = {
documents_have_been_signed: 'Dokumenty byly podepsány!', documents_have_been_signed: 'Dokumenty byly podepsány!',
create_a_free_account: 'Vytvořit bezplatný účet', create_a_free_account: 'Vytvořit bezplatný účet',
powered_by: 'Poháněno', 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', open_source_documents_software: 'open source software pro dokumenty',
verified_phone_number: 'Ověřte telefonní číslo', verified_phone_number: 'Ověřte telefonní číslo',
use_international_format: 'Použijte mezinárodní formát: +1xxx', use_international_format: 'Použijte mezinárodní formát: +1xxx',
@@ -732,11 +747,13 @@ const cs = {
pay_with_strip: 'Zaplacení přes Stripe', pay_with_strip: 'Zaplacení přes Stripe',
reupload: 'Znovu nahrát', reupload: 'Znovu nahrát',
upload: '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 = { const pt = {
complete: 'Completar', complete: 'Completar',
preencher_todos_os_campos_obrigatórios_para_concluir: 'Preencher todos os campos obrigatórios para concluir',
sign_and_complete: 'Assinar e Completar', sign_and_complete: 'Assinar e Completar',
invite: 'Convidar', invite: 'Convidar',
email: 'E-mail', email: 'E-mail',
@@ -802,7 +819,7 @@ const pt = {
documents_have_been_signed: 'Os documentos foram assinados!', documents_have_been_signed: 'Os documentos foram assinados!',
create_a_free_account: 'Criar uma Conta Gratuita', create_a_free_account: 'Criar uma Conta Gratuita',
powered_by: 'Desenvolvido por', 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', open_source_documents_software: 'software de documentos de código aberto',
verified_phone_number: 'Verificar Número de Telefone', verified_phone_number: 'Verificar Número de Telefone',
use_international_format: 'Use formato internacional: +1xxx', use_international_format: 'Use formato internacional: +1xxx',
@@ -824,11 +841,13 @@ const pt = {
pay_with_strip: 'Pagar com Stripe', pay_with_strip: 'Pagar com Stripe',
reupload: 'Reenviar', reupload: 'Reenviar',
upload: 'Carregar', upload: 'Carregar',
files: 'Arquivos' files: 'Arquivos',
signature_is_too_small_please_redraw: 'A assinatura é muito pequena. Por favor, redesenhe-a.'
} }
const he = { const he = {
complete: 'השלם', complete: 'השלם',
fill_all_required_fields_to_complete: 'נא למלא את כל השדות הנדרשים להשלמה',
sign_and_complete: 'חתום והשלם', sign_and_complete: 'חתום והשלם',
invite: 'הזמן', invite: 'הזמן',
email: 'דוא"ל', email: 'דוא"ל',
@@ -895,7 +914,7 @@ const he = {
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: 'אנא סמן את התיבה כדי להמשיך.',
open_source_documents_software: 'תוכנה פתוחה למסמכים', open_source_documents_software: 'תוכנה פתוחה למסמכים',
verified_phone_number: 'אימות מספר טלפון', verified_phone_number: 'אימות מספר טלפון',
use_international_format: 'השתמש בפורמט בינלאומי: +1xxx', use_international_format: 'השתמש בפורמט בינלאומי: +1xxx',
@@ -917,11 +936,13 @@ const he = {
pay_with_strip: 'שלם עם סטרייפ', pay_with_strip: 'שלם עם סטרייפ',
reupload: 'העלה שוב', reupload: 'העלה שוב',
upload: 'העלאה', upload: 'העלאה',
files: 'קבצים' files: 'קבצים',
signature_is_too_small_please_redraw: 'החתימה קטנה מדי. אנא צייר מחדש.'
} }
const nl = { const nl = {
complete: 'Voltooien', complete: 'Voltooien',
vul_alle_verplichte_velden_in_om_te_voltooien: 'Vul alle verplichte velden in om te voltooien',
sign_and_complete: 'Ondertekenen en voltooien', sign_and_complete: 'Ondertekenen en voltooien',
invite: 'Uitnodigen', invite: 'Uitnodigen',
email: 'E-mail', email: 'E-mail',
@@ -988,7 +1009,7 @@ const nl = {
documents_have_been_signed: 'De documenten zijn ondertekend!', documents_have_been_signed: 'De documenten zijn ondertekend!',
create_a_free_account: 'Maak een gratis account aan', create_a_free_account: 'Maak een gratis account aan',
powered_by: 'Aangedreven door', 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', open_source_documents_software: 'Open source documenten software',
verified_phone_number: 'Geverifieerd telefoonnummer', verified_phone_number: 'Geverifieerd telefoonnummer',
use_international_format: 'Gebruik internationaal formaat: +1xxx', use_international_format: 'Gebruik internationaal formaat: +1xxx',
@@ -1010,11 +1031,13 @@ const nl = {
pay_with_strip: 'Betalen met Stripe', pay_with_strip: 'Betalen met Stripe',
reupload: 'Opnieuw uploaden', reupload: 'Opnieuw uploaden',
upload: 'Uploaden', upload: 'Uploaden',
files: 'Bestanden' files: 'Bestanden',
signature_is_too_small_please_redraw: 'De handtekening is te klein. Teken deze opnieuw, alstublieft.'
} }
const ar = { const ar = {
complete: 'اكتمال', complete: 'اكتمال',
fill_all_required_fields_to_complete: 'يرجى ملء جميع الحقول المطلوبة لإكمال',
sign_and_complete: 'التوقيع والاكتمال', sign_and_complete: 'التوقيع والاكتمال',
invite: 'دعوة', invite: 'دعوة',
email: 'البريد الإلكتروني', email: 'البريد الإلكتروني',
@@ -1080,7 +1103,7 @@ const ar = {
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: 'الرجاء التحقق من الخانة للمتابعة.',
open_source_documents_software: 'برنامج وثائق مفتوح المصدر', open_source_documents_software: 'برنامج وثائق مفتوح المصدر',
verified_phone_number: 'تحقق من رقم الهاتف', verified_phone_number: 'تحقق من رقم الهاتف',
use_international_format: 'استخدم الشكل الدولي: +1xxx', use_international_format: 'استخدم الشكل الدولي: +1xxx',
@@ -1102,11 +1125,13 @@ const ar = {
pay_with_strip: 'الدفع بواسطة Stripe', pay_with_strip: 'الدفع بواسطة Stripe',
reupload: 'إعادة التحميل', reupload: 'إعادة التحميل',
upload: 'تحميل', upload: 'تحميل',
files: 'الملفات' files: 'الملفات',
signature_is_too_small_please_redraw: 'التوقيع صغير جدًا. يرجى إعادة الرسم.'
} }
const ko = { const ko = {
complete: '완료', complete: '완료',
fill_all_required_fields_to_complete: '모든 필수 필드를 작성하여 완료하세요',
sign_and_complete: '서명하고 완료하기', sign_and_complete: '서명하고 완료하기',
invite: '초대하기', invite: '초대하기',
email: '이메일', email: '이메일',
@@ -1171,7 +1196,7 @@ const ko = {
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: '계속하려면 확인란을 선택하십시오.',
open_source_documents_software: '오픈 소스 문서 소프트웨어', open_source_documents_software: '오픈 소스 문서 소프트웨어',
verified_phone_number: '전화번호 확인됨', verified_phone_number: '전화번호 확인됨',
use_international_format: '국제 포맷 사용: +1xxx', use_international_format: '국제 포맷 사용: +1xxx',
@@ -1193,7 +1218,8 @@ const ko = {
pay_with_strip: '스트라이프로 결제', pay_with_strip: '스트라이프로 결제',
reupload: '다시 업로드', reupload: '다시 업로드',
upload: '업로드', upload: '업로드',
files: '파일' files: '파일',
signature_is_too_small_please_redraw: '서명이 너무 작습니다. 다시 그려주세요.'
} }
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar, ko } const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar, ko }
@@ -226,6 +226,10 @@ export default {
if (this.$refs.canvas) { if (this.$refs.canvas) {
this.pad = new SignaturePad(this.$refs.canvas) this.pad = new SignaturePad(this.$refs.canvas)
if (this.field.preferences?.color) {
this.pad.penColor = this.field.preferences.color
}
this.pad.addEventListener('beginStroke', () => { this.pad.addEventListener('beginStroke', () => {
this.isInitialsStarted = true this.isInitialsStarted = true
@@ -65,11 +65,15 @@
v-show="!isCodeSent" v-show="!isCodeSent"
class="flex w-full rounded-full outline-neutral-content outline-2 outline-offset-2 focus-within:outline" class="flex w-full rounded-full outline-neutral-content outline-2 outline-offset-2 focus-within:outline"
> >
<div class="relative inline-block"> <div
id="country_code"
class="relative inline-block"
>
<div class="btn bg-base-200 border border-neutral-300 text-2xl whitespace-nowrap font-normal rounded-l-full"> <div class="btn bg-base-200 border border-neutral-300 text-2xl whitespace-nowrap font-normal rounded-l-full">
{{ selectedCountry.flag }} +{{ selectedCountry.dial }} {{ selectedCountry.flag }} +{{ selectedCountry.dial }}
</div> </div>
<select <select
id="country_code_select"
class="absolute top-0 bottom-0 right-0 left-0 opacity-0 w-full h-full cursor-pointer" class="absolute top-0 bottom-0 right-0 left-0 opacity-0 w-full h-full cursor-pointer"
:disabled="!!defaultValue" :disabled="!!defaultValue"
@change="onCountrySelect(countries.find((country) => country.flag === $event.target.value))" @change="onCountrySelect(countries.find((country) => country.flag === $event.target.value))"
@@ -259,7 +259,7 @@
class="text-base-content/60 text-xs text-center w-full mt-1" class="text-base-content/60 text-xs text-center w-full mt-1"
> >
{{ t('by_clicking_you_agree_to_the').replace('{button}', buttonText.charAt(0).toUpperCase() + buttonText.slice(1)) }} <a {{ t('by_clicking_you_agree_to_the').replace('{button}', buttonText.charAt(0).toUpperCase() + buttonText.slice(1)) }} <a
href="https://www.docuseal.com/esign-disclosure" href="https://www.docuseal.co/esign-disclosure"
target="_blank" target="_blank"
> >
<span class="inline md:hidden"> <span class="inline md:hidden">
@@ -423,6 +423,10 @@ export default {
if (this.$refs.canvas) { if (this.$refs.canvas) {
this.pad = new SignaturePad(this.$refs.canvas) this.pad = new SignaturePad(this.$refs.canvas)
if (this.field.preferences?.color) {
this.pad.penColor = this.field.preferences.color
}
this.pad.addEventListener('endStroke', () => { this.pad.addEventListener('endStroke', () => {
this.isSignatureStarted = true this.isSignatureStarted = true
+3 -1
View File
@@ -114,7 +114,9 @@ export default {
const area = this.field.areas?.[0] const area = this.field.areas?.[0]
if (area) { if (area) {
return parseInt(area.w / area.cell_w) + 1 const num = area.w / area.cell_w
return (num % 1) > 0.2 ? parseInt(num) + 1 : parseInt(num)
} else { } else {
return null return null
} }
+1
View File
@@ -175,6 +175,7 @@
<div <div
ref="textContainer" ref="textContainer"
class="flex items-center px-0.5" class="flex items-center px-0.5"
:style="{ color: field.preferences?.color }"
:class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type) }" :class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type) }"
> >
<IconCheck <IconCheck
+4 -2
View File
@@ -4,7 +4,7 @@
class="mx-auto pl-3 md:pl-4 h-full" class="mx-auto pl-3 md:pl-4 h-full"
> >
<div <div
v-if="pendingFieldAttachmentUuids.length" v-if="pendingFieldAttachmentUuids.length && editable"
class="top-1.5 sticky h-0 z-20 max-w-2xl mx-auto" class="top-1.5 sticky h-0 z-20 max-w-2xl mx-auto"
> >
<div class="alert border-base-content/30 py-2 px-2.5"> <div class="alert border-base-content/30 py-2 px-2.5">
@@ -1342,7 +1342,9 @@ export default {
} }
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.previews.scrollTop = this.$refs.previews.scrollHeight if (this.$refs.previews) {
this.$refs.previews.scrollTop = this.$refs.previews.scrollHeight
}
this.scrollIntoDocument(data.schema[0]) this.scrollIntoDocument(data.schema[0])
}) })
@@ -79,7 +79,7 @@
:stroke-width="1.6" :stroke-width="1.6"
/> />
<span class="py-1"> <span class="py-1">
{{ t('add') }} {{ names[submitters.length] }} {{ t('add') }} {{ names[lastPartyIndex] }}
</span> </span>
</a> </a>
</li> </li>
@@ -202,7 +202,7 @@
:stroke-width="1.6" :stroke-width="1.6"
/> />
<span class="py-1"> <span class="py-1">
{{ t('add') }} {{ names[submitters.length] }} {{ t('add') }} {{ names[lastPartyIndex] }}
</span> </span>
</a> </a>
</li> </li>
@@ -321,6 +321,15 @@ export default {
this.t('twentieth_party') this.t('twentieth_party')
] ]
}, },
lastPartyIndex () {
const index = Math.max(...this.submitters.map((s) => this.names.indexOf(s.name)))
if (index !== -1) {
return index + 1
} else {
return this.submitters.length
}
},
selectedSubmitter () { selectedSubmitter () {
return this.submitters.find((e) => e.uuid === this.modelValue) return this.submitters.find((e) => e.uuid === this.modelValue)
} }
@@ -353,7 +362,7 @@ export default {
}, },
addSubmitter () { addSubmitter () {
const newSubmitter = { const newSubmitter = {
name: this.names[this.submitters.length], name: this.names[this.lastPartyIndex],
uuid: v4() uuid: v4()
} }
-6
View File
@@ -126,7 +126,6 @@ const en = {
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Unlock SMS-verified phone number field with paid plan. Use text field for phone numbers without verification.', unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Unlock SMS-verified phone number field with paid plan. Use text field for phone numbers without verification.',
available_only_in_pro: 'Available only in Pro', available_only_in_pro: 'Available only in Pro',
failed_to_download_files: 'Failed to download files', failed_to_download_files: 'Failed to download files',
signature_is_too_small_please_redraw: 'Signature is too small - please redraw.',
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Please add fields for the {submitter_name}. Or, remove the {submitter_name} if not needed.', please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Please add fields for the {submitter_name}. Or, remove the {submitter_name} if not needed.',
draw_field: 'Draw {field} Field', draw_field: 'Draw {field} Field',
replace: 'Replace', replace: 'Replace',
@@ -277,7 +276,6 @@ const es = {
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Desbloquea el campo de número de teléfono verificado por SMS con un plan pago. Usa el campo de texto para números de teléfono sin verificación.', unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Desbloquea el campo de número de teléfono verificado por SMS con un plan pago. Usa el campo de texto para números de teléfono sin verificación.',
available_only_in_pro: 'Disponible solo en Pro', available_only_in_pro: 'Disponible solo en Pro',
failed_to_download_files: 'Error al descargar los archivos', failed_to_download_files: 'Error al descargar los archivos',
signature_is_too_small_please_redraw: 'La firma es demasiado pequeña. Por favor, dibújala de nuevo.',
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Por favor, añade campos para {submitter_name} o elimina {submitter_name} si no es necesario.', please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Por favor, añade campos para {submitter_name} o elimina {submitter_name} si no es necesario.',
draw_field: 'Dibujar campo {field}', draw_field: 'Dibujar campo {field}',
replace: 'Reemplazar', replace: 'Reemplazar',
@@ -434,7 +432,6 @@ const it = {
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Sblocca il campo numero di telefono verificato tramite SMS con un piano a pagamento. Usa il campo di testo per numeri di telefono senza verifica.', unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Sblocca il campo numero di telefono verificato tramite SMS con un piano a pagamento. Usa il campo di testo per numeri di telefono senza verifica.',
available_only_in_pro: 'Disponibile solo in Pro', available_only_in_pro: 'Disponibile solo in Pro',
failed_to_download_files: 'Impossibile scaricare i file', failed_to_download_files: 'Impossibile scaricare i file',
signature_is_too_small_please_redraw: 'La firma è troppo piccola. Ridisegnala per favore.',
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Aggiungi campi per {submitter_name} o rimuovi {submitter_name} se non necessario.', please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Aggiungi campi per {submitter_name} o rimuovi {submitter_name} se non necessario.',
draw_field: 'Disegna il campo {field}', draw_field: 'Disegna il campo {field}',
replace: 'Sostituisci', replace: 'Sostituisci',
@@ -585,7 +582,6 @@ const pt = {
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Desbloqueie o campo de número de telefone verificado por SMS com um plano pago. Use o campo de texto para números de telefone sem verificação.', unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Desbloqueie o campo de número de telefone verificado por SMS com um plano pago. Use o campo de texto para números de telefone sem verificação.',
available_only_in_pro: 'Disponível apenas no Pro', available_only_in_pro: 'Disponível apenas no Pro',
failed_to_download_files: 'Falha ao baixar arquivos', failed_to_download_files: 'Falha ao baixar arquivos',
signature_is_too_small_please_redraw: 'A assinatura é muito pequena. Por favor, redesenhe.',
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Adicione campos para {submitter_name} ou remova {submitter_name} se não for necessário.', please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Adicione campos para {submitter_name} ou remova {submitter_name} se não for necessário.',
draw_field: 'Desenhar campo {field}', draw_field: 'Desenhar campo {field}',
replace: 'Substituir', replace: 'Substituir',
@@ -739,7 +735,6 @@ const fr = {
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Débloquez le champ de numéro de téléphone vérifié par SMS avec un plan payant. Utilisez un champ texte pour les numéros de téléphone sans vérification.', unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Débloquez le champ de numéro de téléphone vérifié par SMS avec un plan payant. Utilisez un champ texte pour les numéros de téléphone sans vérification.',
available_only_in_pro: 'Disponible uniquement en Pro', available_only_in_pro: 'Disponible uniquement en Pro',
failed_to_download_files: 'Échec du téléchargement des fichiers', failed_to_download_files: 'Échec du téléchargement des fichiers',
signature_is_too_small_please_redraw: 'La signature est trop petite. Veuillez la redessiner.',
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Veuillez ajouter des champs pour {submitter_name} ou retirer {submitter_name} si ce n\'est pas nécessaire.', please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Veuillez ajouter des champs pour {submitter_name} ou retirer {submitter_name} si ce n\'est pas nécessaire.',
draw_field: 'Dessiner le champ {field}', draw_field: 'Dessiner le champ {field}',
replace: 'Remplacer', replace: 'Remplacer',
@@ -893,7 +888,6 @@ const de = {
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Schalte das SMS-verifizierte Telefonnummernfeld mit einem kostenpflichtigen Plan frei. Verwende das Textfeld für Telefonnummern ohne Verifizierung.', unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Schalte das SMS-verifizierte Telefonnummernfeld mit einem kostenpflichtigen Plan frei. Verwende das Textfeld für Telefonnummern ohne Verifizierung.',
available_only_in_pro: 'Nur in Pro verfügbar', available_only_in_pro: 'Nur in Pro verfügbar',
failed_to_download_files: 'Fehler beim Herunterladen der Dateien', failed_to_download_files: 'Fehler beim Herunterladen der Dateien',
signature_is_too_small_please_redraw: 'Die Unterschrift ist zu klein. Bitte erneut zeichnen.',
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Bitte füge Felder für {submitter_name} hinzu oder entferne {submitter_name}, falls nicht erforderlich.', please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Bitte füge Felder für {submitter_name} hinzu oder entferne {submitter_name}, falls nicht erforderlich.',
draw_field: 'Feld {field} zeichnen', draw_field: 'Feld {field} zeichnen',
replace: 'Ersetzen', replace: 'Ersetzen',
@@ -279,7 +279,11 @@ export default {
this.mappings.forEach((mapping) => { this.mappings.forEach((mapping) => {
if (mapping.field_name && mapping.column_index != null) { if (mapping.field_name && mapping.column_index != null) {
submittersIndex[mapping.submitter_uuid] ||= { uuid: mapping.submitter_uuid, fields: [] } submittersIndex[mapping.submitter_uuid] ||= {
uuid: mapping.submitter_uuid,
role: this.submitters.find((s) => s.uuid === mapping.submitter_uuid).name,
fields: []
}
if (['name', 'email', 'phone', 'external_id'].includes(mapping.field_name.toLowerCase())) { if (['name', 'email', 'phone', 'external_id'].includes(mapping.field_name.toLowerCase())) {
submittersIndex[mapping.submitter_uuid][mapping.field_name.toLowerCase()] = row[mapping.column_index] submittersIndex[mapping.submitter_uuid][mapping.field_name.toLowerCase()] = row[mapping.column_index]
@@ -296,7 +300,7 @@ export default {
}) })
if (Object.keys(submittersIndex).length !== 0) { if (Object.keys(submittersIndex).length !== 0) {
submissions.push({ submitters: Object.values(submittersIndex) }) submissions.push({ submitters: this.submitters.map((s) => submittersIndex[s.uuid]).filter(Boolean) })
} }
}) })
@@ -28,9 +28,9 @@ class SendFormCompletedWebhookRequestJob
timestamp: Time.current, timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter) data: Submitters::SerializeForWebhook.call(submitter)
}.to_json, }.to_json,
**webhook_url.secret.to_h,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT) 'User-Agent' => USER_AGENT,
**webhook_url.secret.to_h)
rescue Faraday::Error rescue Faraday::Error
nil nil
end end
@@ -24,9 +24,9 @@ class SendSubmissionCompletedWebhookRequestJob
timestamp: Time.current, timestamp: Time.current,
data: Submissions::SerializeForApi.call(submission) data: Submissions::SerializeForApi.call(submission)
}.to_json, }.to_json,
**webhook_url.secret.to_h,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT) 'User-Agent' => USER_AGENT,
**webhook_url.secret.to_h)
rescue Faraday::Error rescue Faraday::Error
nil nil
end end
+1 -1
View File
@@ -16,7 +16,7 @@ class ApplicationMailer < ActionMailer::Base
after_action :set_message_uuid after_action :set_message_uuid
def default_url_options def default_url_options
Docuseal.default_url_options Docuseal.default_url_options.merge(host: ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host]))
end end
def set_message_metadata def set_message_metadata
+19 -8
View File
@@ -138,9 +138,11 @@ class SubmitterMailer < ApplicationMailer
private private
def build_submitter_reply_to(submitter) def build_submitter_reply_to(submitter)
reply_to = reply_to = submitter.preferences['reply_to'].presence
submitter.preferences['reply_to'].presence ||
(submitter.submission.created_by_user || submitter.template.author)&.friendly_name&.sub(/\+\w+@/, '@') if reply_to.blank? && (submitter.submission.created_by_user || submitter.template.author)&.email != submitter.email
reply_to = (submitter.submission.created_by_user || submitter.template.author)&.friendly_name&.sub(/\+\w+@/, '@')
end
return nil if reply_to.to_s.match?(NO_REPLY_REGEXP) return nil if reply_to.to_s.match?(NO_REPLY_REGEXP)
@@ -150,6 +152,9 @@ class SubmitterMailer < ApplicationMailer
def add_completed_email_attachments!(submitter, with_audit_log: true, with_documents: true) def add_completed_email_attachments!(submitter, with_audit_log: true, with_documents: true)
documents = with_documents ? Submitters.select_attachments_for_download(submitter) : [] documents = with_documents ? Submitters.select_attachments_for_download(submitter) : []
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
total_size = 0 total_size = 0
audit_trail_data = nil audit_trail_data = nil
@@ -159,9 +164,14 @@ class SubmitterMailer < ApplicationMailer
total_size = audit_trail_data.size total_size = audit_trail_data.size
end end
total_size = add_attachments_with_size_limit(documents, total_size) total_size = add_attachments_with_size_limit(submitter, documents, total_size, filename_format)
attachments[submitter.submission.audit_trail.filename.to_s.tr('"', "'")] = audit_trail_data if audit_trail_data if audit_trail_data
audit_trail_filename =
Submitters.build_document_filename(submitter, submitter.submission.audit_trail.blob, filename_format)
attachments[audit_trail_filename.tr('"', "'")] = audit_trail_data
end
if with_documents if with_documents
file_fields = submitter.submission.template_fields.select { |e| e['type'].in?(%w[file payment]) } file_fields = submitter.submission.template_fields.select { |e| e['type'].in?(%w[file payment]) }
@@ -170,7 +180,7 @@ class SubmitterMailer < ApplicationMailer
storage_attachments = storage_attachments =
submitter.attachments.where(uuid: submitter.values.values_at(*file_fields.pluck('uuid')).flatten) submitter.attachments.where(uuid: submitter.values.values_at(*file_fields.pluck('uuid')).flatten)
add_attachments_with_size_limit(storage_attachments, total_size) add_attachments_with_size_limit(submitter, storage_attachments, total_size)
end end
end end
@@ -181,7 +191,7 @@ class SubmitterMailer < ApplicationMailer
user.role == 'integration' ? user.friendly_name.sub(/\+\w+@/, '@') : user.friendly_name user.role == 'integration' ? user.friendly_name.sub(/\+\w+@/, '@') : user.friendly_name
end end
def add_attachments_with_size_limit(storage_attachments, current_size) def add_attachments_with_size_limit(submitter, storage_attachments, current_size, filename_format = nil)
total_size = current_size total_size = current_size
storage_attachments.each do |attachment| storage_attachments.each do |attachment|
@@ -189,7 +199,8 @@ class SubmitterMailer < ApplicationMailer
break if total_size >= MAX_ATTACHMENTS_SIZE break if total_size >= MAX_ATTACHMENTS_SIZE
attachments[attachment.filename.to_s.tr('"', "'")] = attachment.download filename = Submitters.build_document_filename(submitter, attachment.blob, filename_format)
attachments[filename.to_s.tr('"', "'")] = attachment.download
end end
total_size total_size
+1
View File
@@ -43,6 +43,7 @@ class AccountConfig < ApplicationRecord
REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason' REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason'
REUSE_SIGNATURE_KEY = 'reuse_signature' REUSE_SIGNATURE_KEY = 'reuse_signature'
COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key' COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key'
DOCUMENT_FILENAME_FORMAT_KEY = 'document_filename_format'
DEFAULT_VALUES = { DEFAULT_VALUES = {
SUBMITTER_INVITATION_EMAIL_KEY => lambda { SUBMITTER_INVITATION_EMAIL_KEY => lambda {
+5 -2
View File
@@ -62,8 +62,11 @@ class Submission < ApplicationRecord
through: :template, source: :documents_attachments through: :template, source: :documents_attachments
scope :active, -> { where(archived_at: nil) } scope :active, -> { where(archived_at: nil) }
scope :pending, -> { joins(:submitters).where(submitters: { completed_at: nil }).distinct } scope :pending, -> { joins(:submitters).where(submitters: { completed_at: nil }).group(:id) }
scope :completed, -> { where.not(id: pending.select(:submission_id)) } scope :completed, lambda {
where.not(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
.and(Submitter.arel_table[:completed_at].eq(nil))).select(1).arel.exists)
}
enum :source, { enum :source, {
invite: 'invite', invite: 'invite',
+14
View File
@@ -164,6 +164,20 @@
</div> </div>
<% end %> <% end %>
<% end %> <% end %>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY) %>
<% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %>
<div class="py-2.5">
<span>
<%= t('document_download_filename_format') %>
</span>
<div class="mt-3">
<%= f.select :value, [["#{I18n.t('document_name')}.pdf", '{document.name}'], ["#{I18n.t('document_name')} - name@domain.com.pdf", '{document.name} - {submission.submitters}'], ["#{I18n.t('document_name')} - name@domain.com - #{I18n.l(Time.current.beginning_of_year.in_time_zone(current_account.timezone), format: :short)}.pdf", '{document.name} - {submission.submitters} - {submission.completed_at}']], {}, class: 'base-select', onchange: 'this.form.requestSubmit()' %>
</div>
</div>
<% end %>
<% end %>
</div> </div>
<% end %> <% end %>
</div> </div>
+8
View File
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M11.5 21h-5.5a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v6" />
<path d="M16 3v4" />
<path d="M8 3v4" />
<path d="M4 11h16" />
<path d="M15 19l2 2l4 -4" />
</svg>

After

Width:  |  Height:  |  Size: 464 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M4 4h16v2.172a2 2 0 0 1 -.586 1.414l-4.414 4.414v7l-6 2v-8.5l-4.48 -4.928a2 2 0 0 1 -.52 -1.345v-2.227z" />
</svg>

After

Width:  |  Height:  |  Size: 400 B

+5
View File
@@ -0,0 +1,5 @@
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" x-bind:width="size" x-bind:height="size" viewBox="0 0 24 24" fill="none" stroke="currentColor" x-bind:stroke-width="stroke" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
<path d="M5 13a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v6a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-6z"></path>
<path d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0 -2 0"></path>
<path d="M8 11v-4a4 4 0 1 1 8 0v4"></path>
</svg>

After

Width:  |  Height:  |  Size: 496 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
<path d="M18 6l-12 12"></path>
<path d="M6 6l12 12"></path>
</svg>

After

Width:  |  Height:  |  Size: 290 B

+1 -1
View File
@@ -2,5 +2,5 @@
--- ---
</p> </p>
<p> <p>
<%= t('sent_using_product_name_free_document_signing_html', product_url: "#{Docuseal::PRODUCT_URL}/start", product_name: Docuseal.product_name) %> <%= t('sent_using_product_name_free_document_signing_html', product_url: "#{Docuseal::PRODUCT_EMAIL_URL}/start", product_name: Docuseal.product_name) %>
</p> </p>
+5
View File
@@ -2,6 +2,11 @@
<% if params[:status].present? %> <% if params[:status].present? %>
<input name="status" value="<%= params[:status] %>" class="hidden"> <input name="status" value="<%= params[:status] %>" class="hidden">
<% end %> <% end %>
<% Submissions::Filter::ALLOWED_PARAMS.each do |key| %>
<% if params[key].present? %>
<input name="<%= key %>" value="<%= params[key] %>" class="hidden">
<% end %>
<% end %>
<% if params[:q].present? %> <% if params[:q].present? %>
<div class="relative"> <div class="relative">
<a href="<%= url_for(params.to_unsafe_h.except(:q)) %>" title="<%= t('clear') %>" class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-auto text-neutral text-2xl font-extralight"> <a href="<%= url_for(params.to_unsafe_h.except(:q)) %>" title="<%= t('clear') %>" class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-auto text-neutral text-2xl font-extralight">
+1 -1
View File
@@ -25,7 +25,7 @@
</div> </div>
</div> </div>
</div> </div>
<% 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| %> <%= form_for @submitter, url: start_form_path(@template.slug), data: { turbo_frame: :_top }, method: :put, html: { class: 'space-y-4' } do |f| %>
<div dir="auto" class="form-control !mt-0"> <div dir="auto" class="form-control !mt-0">
<%= f.label :email, t('email'), class: 'label' %> <%= f.label :email, t('email'), class: 'label' %>
+2 -2
View File
@@ -2,7 +2,7 @@
<% can_send_emails = Accounts.can_send_emails?(current_account) %> <% can_send_emails = Accounts.can_send_emails?(current_account) %>
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<%= f.label :send_email, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %> <%= f.label :send_email, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %>
<%= f.check_box :send_email, id: uuid, class: 'base-checkbox', disabled: !can_send_emails, checked: can_send_emails && !local_assigns.key?(:resend_email) %> <%= f.check_box :send_email, id: uuid, class: 'base-checkbox', disabled: !can_send_emails || local_assigns[:disable_email], checked: can_send_emails && !local_assigns.key?(:resend_email) && !local_assigns[:disable_email] %>
<span class="label"><%= local_assigns[:resend_email] ? t('re_send_email') : t('send_email') %></span> <span class="label"><%= local_assigns[:resend_email] ? t('re_send_email') : t('send_email') %></span>
<% end %> <% end %>
<div> <div>
@@ -26,7 +26,7 @@
<%= t('configure_smtp_settings_in_order_to_send_emails_') %> <%= t('configure_smtp_settings_in_order_to_send_emails_') %>
<br> <br>
<a class="link font-medium" data-turbo-frame="_top" href="<%= settings_email_index_path %>"> <a class="link font-medium" data-turbo-frame="_top" href="<%= settings_email_index_path %>">
<%= t('go_to_smtp_setting') %> <%= t('go_to_smtp_settings') %>
</a> </a>
</p> </p>
</div> </div>
+2 -1
View File
@@ -1,5 +1,6 @@
<% align = field.dig('preferences', 'align') %> <% align = field.dig('preferences', 'align') %>
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-serif' if field.dig('preferences', 'font') == 'Times' %> <%= align == 'right' ? 'justify-end' : (align == 'center' ? 'justify-center' : '') %>" style="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']}pt); line-height: `clamp(6pt, 2.0vw, #{field['preferences']['font_size'].to_i + 3}pt)`" if field.dig('preferences', 'font_size') %>"> <% color = field.dig('preferences', 'color') %>
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-serif' if field.dig('preferences', 'font') == 'Times' %> <%= align == 'right' ? 'justify-end' : (align == 'center' ? 'justify-center' : '') %>" style="<%= "color: #{color}; " if color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size'].to_i * 1.23}pt); line-height: `clamp(6pt, 2.0vw, #{(field['preferences']['font_size'].to_i * 1.23) + 3}pt)`" if field.dig('preferences', 'font_size') %>">
<% if field['type'] == 'signature' %> <% if field['type'] == 'signature' %>
<div class="flex flex-col justify-between h-full overflow-hidden"> <div class="flex flex-col justify-between h-full overflow-hidden">
<div class="flex-grow flex overflow-hidden" style="min-height: 50%"> <div class="flex-grow flex overflow-hidden" style="min-height: 50%">
+1 -1
View File
@@ -59,7 +59,7 @@
</div> </div>
<% end %> <% end %>
<% elsif @submission.submitters.to_a.size == 1 && !@submission.expired? && !@submission.submitters.to_a.first.declined_at? && !@submission.archived_at? %> <% 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 %> <% end %>
</div> </div>
</div> </div>
+16 -7
View File
@@ -1,22 +1,31 @@
<% filter_params = params.permit(Submissions::Filter::ALLOWED_PARAMS).compact_blank %>
<div> <div>
<%= link_to root_path do %> <%= link_to root_path do %>
&larr; &larr;
<span><%= t('back_to_active') %></span> <span><%= t('back_to_active') %></span>
<% end %> <% end %>
</div> </div>
<div class="flex justify-between mb-4 items-center"> <div class="flex flex-col md:flex-row md:items-center mb-4 gap-3">
<div> <div class="flex w-full justify-between">
<h1 class="text-4xl font-bold md:block <%= 'hidden' if params[:q].present? %>"><%= t('submissions') %> <span class="badge badge-outline badge-lg align-middle"><%= t('archived') %></span></h1> <div>
<h1 class="text-4xl font-bold md:block <%= 'hidden' if params[:q].present? %>"><%= t('submissions') %> <span class="badge badge-outline badge-lg align-middle"><%= t('archived') %></span></h1>
</div>
<div>
<% if params[:q].present? || @pagy.pages > 1 || filter_params.present? %>
<%= render 'shared/search_input', placeholder: "#{t('search')}..." %>
<% end %>
</div>
</div>
<div class="flex flex-col items-end md:flex-row gap-2">
<%= render 'submissions_filters/applied_filters', filter_params: %>
<%= render 'submissions_filters/filter_button', filter_params: %>
</div> </div>
<% if params[:q].present? || @pagy.pages > 1 %>
<%= render 'shared/search_input', placeholder: "#{t('search')}..." %>
<% end %>
</div> </div>
<% if @pagy.count > 0 %> <% if @pagy.count > 0 %>
<div class="space-y-4"> <div class="space-y-4">
<%= render partial: 'templates/submission', collection: @submissions, locals: { with_template: true, archived: true } %> <%= render partial: 'templates/submission', collection: @submissions, locals: { with_template: true, archived: true } %>
</div> </div>
<% elsif params[:q].present? %> <% elsif params[:q].present? || filter_params.present? %>
<div class="text-center"> <div class="text-center">
<div class="mt-16 text-3xl font-semibold"> <div class="mt-16 text-3xl font-semibold">
<%= t('submissions_not_found') %> <%= t('submissions_not_found') %>
+31 -24
View File
@@ -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 %> <% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %>
<div class="flex justify-between items-center w-full mb-4"> <div class="flex justify-between items-center w-full mb-4">
<div class="flex items-center flex-grow min-w-0"> <div class="flex items-center flex-grow min-w-0">
@@ -10,7 +11,7 @@
</h1> </h1>
</div> </div>
<div class="flex space-x-2"> <div class="flex space-x-2">
<% if params[:q].present? || @pagy.pages > 1 %> <% if params[:q].present? || @pagy.pages > 1 || filter_params.present? %>
<%= render 'shared/search_input' %> <%= render 'shared/search_input' %>
<% end %> <% end %>
<% if can?(:create, ::Template) %> <% if can?(:create, ::Template) %>
@@ -32,25 +33,31 @@
<% end %> <% end %>
<% end %> <% end %>
<% if is_show_tabs %> <% if is_show_tabs %>
<div class="flex items-center md:items-end flex-col space-y-2 md:space-y-0 md:flex-row md:space-x-2 mb-4"> <div class="flex items-center flex-col md:flex-row md:flex-wrap gap-2 mb-4">
<a href="<%= url_for(params.to_unsafe_h.except(:status)) %>" class="<%= params[:status].blank? ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600"> <div class="flex items-center md:items-end flex-col md:flex-row gap-2 w-full md:w-fit">
<div class="flex items-center space-x-1"> <a href="<%= url_for(params.to_unsafe_h.except(:status)) %>" class="<%= params[:status].blank? ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
<%= svg_icon('list', class: 'w-5 h-5') %> <div class="flex items-center space-x-1">
<span class="font-normal"><%= t('all') %></span> <%= svg_icon('list', class: 'w-5 h-5') %>
</div> <span class="font-normal"><%= t('all') %></span>
</a> </div>
<a href="<%= url_for(params.to_unsafe_h.merge(status: :pending)) %>" class="<%= params[:status] == 'pending' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600"> </a>
<div class="flex items-center space-x-1"> <a href="<%= url_for(params.to_unsafe_h.merge(status: :pending)) %>" class="<%= params[:status] == 'pending' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
<%= svg_icon('clock', class: 'w-5 h-5') %> <div class="flex items-center space-x-1">
<span class="font-normal"><%= t('pending') %></span> <%= svg_icon('clock', class: 'w-5 h-5') %>
</div> <span class="font-normal"><%= t('pending') %></span>
</a> </div>
<a href="<%= url_for(params.to_unsafe_h.merge(status: :completed)) %>" class="<%= params[:status] == 'completed' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600"> </a>
<div class="flex items-center space-x-1"> <a href="<%= url_for(params.to_unsafe_h.merge(status: :completed)) %>" class="<%= params[:status] == 'completed' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
<%= svg_icon('circle_check', class: 'w-5 h-5') %> <div class="flex items-center space-x-1">
<span class="font-normal"><%= t('completed') %></span> <%= svg_icon('circle_check', class: 'w-5 h-5') %>
</div> <span class="font-normal"><%= t('completed') %></span>
</a> </div>
</a>
</div>
<div class="flex items-end flex-col md:flex-row gap-2 w-full md:w-fit">
<%= render 'submissions_filters/applied_filters', filter_params: %>
<%= render 'submissions_filters/filter_button', filter_params: %>
</div>
</div> </div>
<% end %> <% end %>
<% if @pagy.count > 0 %> <% if @pagy.count > 0 %>
@@ -58,10 +65,10 @@
<%= render partial: 'templates/submission', collection: @submissions, locals: { with_template: true } %> <%= render partial: 'templates/submission', collection: @submissions, locals: { with_template: true } %>
</div> </div>
<% end %> <% end %>
<% if params[:q].blank? && params[:status].blank? && @pagy.count < 5 %> <% if params[:q].blank? && params[:status].blank? && filter_params.blank? && @pagy.count < 5 %>
<%= render 'templates/dropzone' %> <%= render 'templates/dropzone' %>
<% end %> <% end %>
<% if @submissions.present? || params[:q].blank? %> <% if @submissions.present? || (params[:q].blank? && filter_params.blank?) %>
<% if @pagy.pages > 1 %> <% if @pagy.pages > 1 %>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'submissions', left_additional_html: view_archived_html %> <%= render 'shared/pagination', pagy: @pagy, items_name: 'submissions', left_additional_html: view_archived_html %>
<% else %> <% else %>
@@ -69,7 +76,7 @@
<%= view_archived_html %> <%= view_archived_html %>
</div> </div>
<% end %> <% end %>
<% elsif params[:q].present? %> <% elsif params[:q].present? || filter_params.present? %>
<div class="text-center"> <div class="text-center">
<div class="mt-16 text-3xl font-semibold"> <div class="mt-16 text-3xl font-semibold">
<%= t('submissions_not_found') %> <%= t('submissions_not_found') %>
@@ -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? %>
<div class="tooltip tooltip-bottom flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-34 border-neutral-700" data-tip="<%= t('completed_at') %>">
<%= 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') %>
<span class="flex flex-row md:flex-col font-normal text-left md:text-center md:text-xs">
<% if query_params[:completed_at_from] == query_params[:completed_at_to] %>
<span><%= l(Date.parse(query_params[:completed_at_from]), locale: current_account.locale) %></span>
<% else %>
<span><%= query_params[:completed_at_from].present? ? l(Date.parse(query_params[:completed_at_from]), locale: current_account.locale) : '∞' %></span>
<span class="px-1 md:px-0 md:hidden">-</span>
<span><%= query_params[:completed_at_to].present? ? l(Date.parse(query_params[:completed_at_to]), locale: current_account.locale) : t('today') %></span>
<% end %>
</span>
<% 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 %>
</div>
<% end %>
<% if query_params[:created_at_from].present? || query_params[:created_at_to].present? %>
<div class="tooltip tooltip-bottom flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-34 border-neutral-700" data-tip="<%= t('created_at') %>">
<%= 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') %>
<span class="flex flex-row md:flex-col font-normal text-left md:text-center md:text-xs">
<% if query_params[:created_at_from] == query_params[:created_at_to] %>
<span><%= l(Date.parse(query_params[:created_at_from]), locale: current_account.locale) %></span>
<% else %>
<span><%= query_params[:created_at_from].present? ? l(Date.parse(query_params[:created_at_from]), locale: current_account.locale) : '∞' %></span>
<span class="px-1 md:px-0 md:hidden">-</span>
<span><%= query_params[:created_at_to].present? ? l(Date.parse(query_params[:created_at_to]), locale: current_account.locale) : t('today') %></span>
<% end %>
</span>
<% 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 %>
</div>
<% end %>
<% if params[:author].present? %>
<div class="tooltip tooltip-bottom flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-34 border-neutral-700" data-tip="<%= t('author') %>">
<%= 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') %>
<span class="font-normal truncate"><%= current_account.users.accessible_by(current_ability).where(account: current_account).find_by(email: params[:author])&.full_name || 'NA' %></span>
<% 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 %>
</div>
<% end %>
@@ -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 %>
<div class="flex flex-wrap gap-2 mt-4 md:mt-2">
<set-date-button data-from-value="<%= current_time.to_date %>" data-to-value="<%= current_time.to_date %>" data-from-id="date_from" data-to-id="date_to">
<button class="btn btn-xs btn-primary font-medium normal-case" data-turbo="false" type="button">
<%= t('today') %>
</button>
</set-date-button>
<set-date-button data-from-value="<%= (current_time - 1.day).to_date %>" data-to-value="<%= (current_time - 1.day).to_date %>" data-from-id="date_from" data-to-id="date_to">
<button class="btn btn-xs btn-primary font-medium normal-case" data-turbo="false" type="button">
<%= t('yesterday') %>
</button>
</set-date-button>
<set-date-button data-from-value="<%= current_time.beginning_of_week(week_start).to_date %>" data-to-value="<%= current_time.end_of_week(week_start).to_date %>" data-from-id="date_from" data-to-id="date_to">
<button class="btn btn-xs btn-primary font-medium normal-case" data-turbo="false" type="button">
<%= t('this_week') %>
</button>
</set-date-button>
<set-date-button data-from-value="<%= (current_time - 1.week).beginning_of_week(week_start).to_date %>" data-to-value="<%= (current_time - 1.week).end_of_week(week_start).to_date %>" data-from-id="date_from" data-to-id="date_to">
<button class="btn btn-xs btn-primary font-medium normal-case" data-turbo="false" type="button">
<%= t('last_week') %>
</button>
</set-date-button>
<set-date-button data-from-value="<%= current_time.beginning_of_month.to_date %>" data-to-value="<%= current_time.end_of_month.to_date %>" data-from-id="date_from" data-to-id="date_to">
<button class="btn btn-xs btn-primary font-medium normal-case" data-turbo="false" type="button">
<%= t('this_month') %>
</button>
</set-date-button>
<set-date-button data-from-value="<%= (current_time - 1.month).beginning_of_month.to_date %>" data-to-value="<%= (current_time - 1.month).end_of_month.to_date %>" data-from-id="date_from" data-to-id="date_to">
<button class="btn btn-xs btn-primary font-medium normal-case" data-turbo="false" type="button">
<%= t('last_month') %>
</button>
</set-date-button>
<set-date-button data-from-value="<%= current_time.beginning_of_year.to_date %>" data-to-value="<%= current_time.end_of_year.to_date %>" data-from-id="date_from" data-to-id="date_to">
<button class="btn btn-xs btn-primary font-medium normal-case" data-turbo="false" type="button">
<%= t('this_year') %>
</button>
</set-date-button>
</div>
@@ -0,0 +1,27 @@
<% query_params = params.permit(:q, :status).merge(filter_params) %>
<div class="dropdown dropdown-end">
<label tabindex="0" class="cursor-pointer flex h-10 px-3 py-1 space-x-1 text-lg items-center justify-between border text-center text-neutral rounded-xl border-neutral-300 hover:border-neutral-700">
<%= svg_icon('filter', class: 'w-5 h-5 flex-shrink-0 stroke-2') %>
<span class="<%= filter_params.present? ? 'md:hidden' : '' %>">Filter</span>
</label>
<ul tabindex="0" class="z-10 dropdown-content p-2 mt-2 shadow menu text-base bg-base-100 rounded-box min-w-[180px] text-right">
<li class="flex">
<%= link_to submissions_filter_path('completed_at', query_params.merge(path: url_for)), data: { turbo_frame: 'modal' } do %>
<%= svg_icon('calendar_check', class: 'w-5 h-5 flex-shrink-0 stroke-2') %>
<span><%= t('completed_at') %></span>
<% end %>
</li>
<li class="flex">
<%= link_to submissions_filter_path('created_at', query_params.merge(path: url_for)), data: { turbo_frame: 'modal' } do %>
<%= svg_icon('calendar', class: 'w-5 h-5 flex-shrink-0 stroke-2') %>
<span><%= t('created_at') %></span>
<% end %>
</li>
<li class="flex">
<%= link_to submissions_filter_path('author', query_params.merge(path: url_for)), data: { turbo_frame: 'modal' } do %>
<%= svg_icon('user', class: 'w-5 h-5 flex-shrink-0 stroke-2') %>
<span><%= t('author') %></span>
<% end %>
</li>
</ul>
</div>
@@ -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 %>
<div class="form-control mt-4">
<%= f.button button_title(title: t('apply'), disabled_with: t('applying')), name: nil, class: 'base-button' %>
</div>
<% if params[:with_remove] %>
<div class="text-center w-full mt-4">
<%= 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 } %>
</div>
<% end %>
<% end %>
<% end %>
@@ -0,0 +1,7 @@
<%= render 'filter_modal', title: t('author'), default_params: params.permit(*(Submissions::Filter::ALLOWED_PARAMS - ['author'])) do %>
<div class="space-y-2">
<div class="form-control mt-6">
<%= 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' %>
</div>
</div>
<% end %>
@@ -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 %>
<div class="space-y-3">
<div class="flex flex-col md:flex-row gap-2">
<div class="form-control w-full">
<%= 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' %>
</div>
<div class="form-control w-full">
<%= 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' %>
</div>
</div>
<%= render 'date_buttons' %>
</div>
<% end %>
@@ -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 %>
<div class="space-y-3">
<div class="flex flex-col md:flex-row gap-2">
<div class="form-control w-full">
<%= 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' %>
</div>
<div class="form-control w-full">
<%= 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' %>
</div>
</div>
<%= render 'date_buttons' %>
</div>
<% end %>
@@ -15,9 +15,10 @@
<%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %> <%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %>
</head> </head>
<body> <body>
<draw-signature data-slug="<%= params[:slug] %>" class="flex items-center h-screen p-2 justify-center"> <% field = (@submitter.submission.template_fields || @submitter.template.fields).find { |f| f['type'] == 'signature' && f['uuid'].starts_with?(params[:f]) } %>
<draw-signature data-slug="<%= params[:slug] %>" data-color="<%= field.dig('preferences', 'color') %>" class="flex items-center h-screen p-2 justify-center">
<%= form_for '', url: submit_form_path(params[:slug]), html: { style: 'max-width: 900px; width: 100%; margin-bottom: 120px' }, method: :put do |f| %> <%= form_for '', url: submit_form_path(params[:slug]), html: { style: 'max-width: 900px; width: 100%; margin-bottom: 120px' }, method: :put do |f| %>
<input value="" type="hidden" name="values[<%= (@submitter.submission.template_fields || @submitter.template.fields).find { |f| f['type'] == 'signature' && f['uuid'].starts_with?(params[:f]) }['uuid'] %>]"> <input value="" type="hidden" name="values[<%= field['uuid'] %>]">
<div class="font-semibold text-4xl text-center w-full mb-2"> <div class="font-semibold text-4xl text-center w-full mb-2">
<%= t('draw_signature') %> <%= t('draw_signature') %>
</div> </div>
@@ -7,7 +7,7 @@
<% else %> <% else %>
<p><%= t('hi_there') %>,</p> <p><%= t('hi_there') %>,</p>
<p><%= I18n.t(@submitter.with_signature_fields? ? :you_have_been_invited_to_sign_the_name : :you_have_been_invited_to_submit_the_name_form, name: @submitter.submission.template.name) %></p> <p><%= I18n.t(@submitter.with_signature_fields? ? :you_have_been_invited_to_sign_the_name : :you_have_been_invited_to_submit_the_name_form, name: @submitter.submission.template.name) %></p>
<p><%= link_to I18n.t(@submitter.with_signature_fields? ? :review_and_sign : :review_and_submit), submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email')) %></p> <p><%= link_to I18n.t(@submitter.with_signature_fields? ? :review_and_sign : :review_and_submit), submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email'), host: ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host])) %></p>
<p><%= t('please_contact_us_by_replying_to_this_email_if_you_didn_t_request_this') %></p> <p><%= t('please_contact_us_by_replying_to_this_email_if_you_didn_t_request_this') %></p>
<p> <p>
<%= t('thanks') %>,<br><%= @current_account.name %> <%= t('thanks') %>,<br><%= @current_account.name %>
+2 -2
View File
@@ -92,7 +92,7 @@
</div> </div>
<% else %> <% else %>
<div class="flex-1 md:flex-none"> <div class="flex-1 md:flex-none">
<%= 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') %>
</div> </div>
<% end %> <% end %>
<% end %> <% end %>
@@ -176,7 +176,7 @@
</span> </span>
</a> </a>
<% else %> <% 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 %>
</div> </div>
<% end %> <% end %>
+1 -1
View File
@@ -1,7 +1,7 @@
<div class="flex flex-col items-start md:flex-row space-y-2 md:space-y-0 md:space-x-2 md:justify-between md:items-start mb-6 md:mb-3"> <div class="flex flex-col items-start md:flex-row space-y-2 md:space-y-0 md:space-x-2 md:justify-between md:items-start mb-6 md:mb-3">
<div class="relative flex items-start justify-between w-full space-x-0"> <div class="relative flex items-start justify-between w-full space-x-0">
<div> <div>
<h1 class="text-3xl md:text-4xl font-semibold" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;"> <h1 class="text-3xl md:text-[2em] font-semibold" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;">
<% template.name.split(/(_)/).each do |item| %><%= item %><wbr><% end %> <% template.name.split(/(_)/).each do |item| %><%= item %><wbr><% end %>
<% if template.archived_at? %> <% if template.archived_at? %>
<span class="ml-1 badge badge-outline badge-lg align-middle"><%= t('archived') %></span> <span class="ml-1 badge badge-outline badge-lg align-middle"><%= t('archived') %></span>
+61 -52
View File
@@ -1,6 +1,7 @@
<%= render 'title', template: @template %> <%= render 'title', template: @template %>
<% is_show_tabs = @pagy.pages > 1 || params[:status].present? %> <% filter_params = params.permit(Submissions::Filter::ALLOWED_PARAMS).compact_blank %>
<% if !@pagy.count.zero? || params[:q].present? || params[:status].present? %> <% is_show_tabs = @pagy.pages > 1 || params[:q].present? || params[:status].present? || filter_params.present? %>
<% if !@pagy.count.zero? || params[:q].present? || params[:status].present? || filter_params.present? %>
<div class="<%= is_show_tabs ? 'mb-4' : 'mb-6' %>"> <div class="<%= is_show_tabs ? 'mb-4' : 'mb-6' %>">
<div class="flex justify-between items-center md:items-end"> <div class="flex justify-between items-center md:items-end">
<div> <div>
@@ -9,7 +10,7 @@
</h2> </h2>
</div> </div>
<div class="flex justify-end space-x-2"> <div class="flex justify-end space-x-2">
<% 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' %> <%= render 'shared/search_input', title_selector: 'h2' %>
<% end %> <% end %>
<%= link_to new_template_submissions_export_path(@template), class: 'hidden md:flex btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %> <%= 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 @@
</div> </div>
<% end %> <% end %>
<% if is_show_tabs %> <% if is_show_tabs %>
<div class="flex items-center md:items-end flex-col space-y-2 md:space-y-0 md:flex-row md:space-x-2 mb-4"> <div class="flex items-center flex-col md:flex-row md:flex-wrap gap-2 mb-4">
<a href="<%= url_for(params.to_unsafe_h.except(:status)) %>" class="<%= params[:status].blank? ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600"> <div class="flex items-center md:items-end flex-col md:flex-row gap-2 w-full md:w-fit">
<div class="flex items-center space-x-1"> <a href="<%= url_for(params.to_unsafe_h.except(:status)) %>" class="<%= params[:status].blank? ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-700">
<%= svg_icon('list', class: 'w-5 h-5') %> <div class="flex items-center space-x-1">
<span class="font-normal"><%= t('all') %></span> <%= svg_icon('list', class: 'w-5 h-5') %>
</div> <span class="font-normal"><%= t('all') %></span>
<div class="badge badge-neutral badge-outline font-medium"> </div>
<%= params[:status].blank? ? @pagy.count : @base_submissions.count %> <div class="badge badge-neutral badge-outline font-medium">
</div> <%= params[:status].blank? && filter_params.blank? ? @pagy.count : @base_submissions.unscope(:group, :order).count %>
</a> </div>
<a href="<%= url_for(params.to_unsafe_h.merge(status: :pending)) %>" class="<%= params[:status] == 'pending' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600"> </a>
<div class="flex items-center space-x-1"> <a href="<%= url_for(params.to_unsafe_h.merge(status: :pending)) %>" class="<%= params[:status] == 'pending' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-700">
<%= svg_icon('clock', class: 'w-5 h-5') %> <div class="flex items-center space-x-1">
<span class="font-normal"><%= t('pending') %></span> <%= svg_icon('clock', class: 'w-5 h-5') %>
</div> <span class="font-normal"><%= t('pending') %></span>
<div class="badge badge-neutral badge-outline font-medium"> </div>
<%= params[:status] == 'pending' ? @pagy.count : @base_submissions.pending.count %> <div class="badge badge-neutral badge-outline font-medium">
</div> <%= params[:status] == 'pending' && filter_params.blank? ? @pagy.count : @base_submissions.pending.unscope(:group, :order).count %>
</a> </div>
<a href="<%= url_for(params.to_unsafe_h.merge(status: :completed)) %>" class="<%= params[:status] == 'completed' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600"> </a>
<div class="flex items-center space-x-1"> <a href="<%= url_for(params.to_unsafe_h.merge(status: :completed)) %>" class="<%= params[:status] == 'completed' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-700">
<%= svg_icon('circle_check', class: 'w-5 h-5') %> <div class="flex items-center space-x-1">
<span class="font-normal"><%= t('completed') %></span> <%= svg_icon('circle_check', class: 'w-5 h-5') %>
</div> <span class="font-normal"><%= t('completed') %></span>
<div class="badge badge-neutral badge-outline font-medium"> </div>
<%= params[:status] == 'completed' ? @pagy.count : @base_submissions.completed.count %> <div class="badge badge-neutral badge-outline font-medium">
</div> <%= params[:status] == 'completed' && filter_params.blank? ? @pagy.count : @base_submissions.completed.unscope(:group, :order).count %>
</a> </div>
</a>
</div>
<div class="flex items-end flex-col md:flex-row gap-2 w-full md:w-fit">
<%= render 'submissions_filters/applied_filters', filter_params: %>
<%= render 'submissions_filters/filter_button', filter_params: %>
</div>
</div> </div>
<% end %> <% end %>
<% if @submissions.present? %> <% if @submissions.present? %>
@@ -65,31 +72,33 @@
<div class="card bg-base-200"> <div class="card bg-base-200">
<div class="card-body text-center px-4 py-16"> <div class="card-body text-center px-4 py-16">
<div class="max-w-lg mx-auto"> <div class="max-w-lg mx-auto">
<p class="text-3xl font-bold text-base-content mb-4"> <p class="text-3xl font-bold text-base-content">
<%= t('there_are_no_submissions') %> <%= t('there_are_no_submissions') %>
</p> </p>
<% if @template.archived_at.blank? && params[:q].blank? %> <% if @template.archived_at.blank? && params[:q].blank? && filter_params.blank? %>
<p><%= t('send_an_invitation_to_fill_and_complete_the_form') %></p> <div class="mt-4">
<div class="space-y-2 flex flex-col"> <p><%= t('send_an_invitation_to_fill_and_complete_the_form') %></p>
<% if can?(:create, Submission) %> <div class="space-y-2 flex flex-col">
<%= link_to new_template_submission_path(@template, with_link: true), class: 'base-button mt-6', data: { turbo_frame: 'modal' } do %> <% if can?(:create, Submission) %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %> <%= link_to new_template_submission_path(@template, with_link: true), class: 'base-button mt-6', data: { turbo_frame: 'modal' } do %>
<span class="mr-1"><%= t('send_to_recipients') %></span> <%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span class="mr-1"><%= t('send_to_recipients') %></span>
<% end %>
<% end %> <% end %>
<% end %> <% if @template.submitters.size == 1 %>
<% if @template.submitters.size == 1 %> <%= link_to start_form_url(slug: @template.slug), class: 'white-button mt-6', target: '_blank', rel: 'noopener' do %>
<%= 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') %>
<%= svg_icon('writing', class: 'w-6 h-6') %> <span class="mr-1"><%= t('sign_it_yourself') %></span>
<span class="mr-1"><%= t('sign_it_yourself') %></span> <% end %>
<% 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') %>
<span class="mr-1"><%= t('sign_it_yourself') %></span>
<% end %>
<% end %> <% end %>
<% else %> </div>
<%= link_to new_template_submission_path(@template, selfsign: true), class: 'white-button mt-6', data: { turbo_frame: 'modal' } do %> </div>
<%= svg_icon('writing', class: 'w-6 h-6') %> <% end %>
<span class="mr-1"><%= t('sign_it_yourself') %></span>
<% end %>
<% end %>
<% end %>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -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 %> <%= render 'templates/title', template: @template %>
<div> <div>
<%= link_to template_path(@template) do %> <%= link_to template_path(@template) do %>
@@ -5,23 +7,33 @@
<span><%= t('back_to_active') %></span> <span><%= t('back_to_active') %></span>
<% end %> <% end %>
</div> </div>
<div class="flex justify-between mb-6 md:items-end flex-col md:flex-row"> <div class="flex flex-col md:flex-row md:items-center mb-6 gap-3">
<div> <div class="flex w-full justify-between md:items-end items-center">
<h1 class="text-3xl font-bold md:block"><%= t('submissions') %> <span class="badge badge-outline badge-lg align-middle"><%= t('archived') %></span></h1> <div>
</div> <h1 class="text-3xl font-bold md:block"><%= t('submissions') %> <span class="badge badge-outline badge-lg align-middle"><%= t('archived') %></span></h1>
<div class="flex space-x-2 mt-3 md:mt-0 justify-end"> </div>
<%= render 'shared/search_input' %> <div class="flex space-x-2 justify-end">
<%= link_to new_template_submissions_export_path(@template), class: 'order-3 md:order-1 btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %> <% if with_filters %>
<%= svg_icon('download', class: 'w-6 h-6 stroke-2') %> <%= render 'shared/search_input' %>
<span><%= t('export') %></span> <% end %>
<% 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') %>
<span><%= t('export') %></span>
<% end %>
</div>
</div> </div>
<% if with_filters %>
<div class="flex flex-col items-end md:flex-row gap-2">
<%= render 'submissions_filters/applied_filters', filter_params: %>
<%= render 'submissions_filters/filter_button', filter_params: %>
</div>
<% end %>
</div> </div>
<% if @pagy.count > 0 %> <% if @pagy.count > 0 %>
<div class="space-y-4"> <div class="space-y-4">
<%= render partial: 'templates/submission', collection: @submissions, locals: { template: @template, archived: true } %> <%= render partial: 'templates/submission', collection: @submissions, locals: { template: @template, archived: true } %>
</div> </div>
<% elsif params[:q].present? %> <% elsif params[:q].present? || filter_params.present? %>
<div class="text-center"> <div class="text-center">
<div class="mt-16 text-3xl font-semibold"> <div class="mt-16 text-3xl font-semibold">
<%= t('submissions_not_found') %> <%= t('submissions_not_found') %>
+34
View File
@@ -0,0 +1,34 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
<%= render 'shared/settings_nav' %>
<div class="flex-grow min-w-0">
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center mb-4">
<h1 class="text-4xl font-bold">Webhooks</h1>
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center">
<%= render 'shared/test_mode_toggle' %>
<% if @webhook_url.persisted? %>
<%= link_to new_settings_webhook_path, class: 'md:ml-3 btn bg-white btn-outline btn-md gap-2 w-full md:w-fit', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('plus', class: 'w-6 h-6') %>
<span><%= t('new_webhook') %></span>
<% end %>
<% end %>
</div>
</div>
<div class="space-y-4">
<% @webhook_urls.each do |webhook_url| %>
<%= link_to settings_webhook_path(webhook_url), class: 'card bg-base-200' do %>
<div class="card-body p-6 min-w-0">
<p class="flex items-center space-x-1">
<%= svg_icon('world', class: 'w-6 h-6 shrink-0') %>
<span class="text-xl font-semibold truncate"><%= webhook_url.url %></span>
</p>
<div class="flex flex-wrap gap-2 mt-2">
<% webhook_url.events.each do |event| %>
<span class="badge badge-outline"><%= event %></span>
<% end %>
</div>
</div>
<% end %>
<% end %>
</div>
</div>
</div>
+29
View File
@@ -0,0 +1,29 @@
<%= render 'shared/turbo_modal', title: t('new_webhook') do %>
<%= form_for @webhook_url, url: settings_webhooks_path, html: { class: 'space-y-4' }, data: { turbo_frame: :_top } do |f| %>
<div class="space-y-4">
<div class="form-control">
<%= f.label :url, 'Webhook URL', class: 'label' %>
<%= f.url_field :url, class: 'base-input', placeholder: 'https://example.com/hook', required: true %>
</div>
<div class="space-y-4">
<% WebhookUrl::EVENTS.group_by { |e| e.include?('form') }.each do |_, events| %>
<div class="grid grid-cols-1 md:grid-cols-2 gap-y-2">
<%= f.collection_check_boxes(:events, events, :to_s, :to_s, include_hidden: false) do |b| %>
<div class="flex">
<label class="flex items-center space-x-2">
<%= b.check_box class: 'base-checkbox', checked: @webhook_url.events.include?(b.value) %>
<span>
<%= b.label %>
</span>
</label>
</div>
<% end %>
</div>
<% end %>
</div>
</div>
<div class="form-control pt-2">
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
</div>
<% end %>
<% end %>
+70 -30
View File
@@ -1,38 +1,78 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10"> <div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
<%= render 'shared/settings_nav' %> <%= render 'shared/settings_nav' %>
<div class="flex-grow"> <div class="flex-grow">
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-end mb-4"> <div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center mb-4">
<h1 class="text-4xl font-bold">Webhooks</h1> <h1 class="text-4xl font-bold">Webhook</h1>
<%= render 'shared/test_mode_toggle' %> <div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center">
<% if params[:action] == 'index' %>
<%= render 'shared/test_mode_toggle' %>
<% end %>
<% if @webhook_url.persisted? && params[:action] == 'index' %>
<%= link_to new_settings_webhook_path, class: 'md:ml-3 btn bg-white btn-outline btn-md gap-2 w-full md:w-fit', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('plus', class: 'w-6 h-6') %>
<span><%= t('new_webhook') %></span>
<% end %>
<% end %>
</div>
</div> </div>
<div class="card bg-base-200"> <div class="card bg-base-200">
<div class="card-body p-6"> <div class="card-body p-6">
<%= form_for @webhook_url, url: settings_webhooks_path, method: :post, html: { autocomplete: 'off' } do |f| %> <div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-end md:relative">
<%= f.label :url, 'Webhook URL', class: 'text-sm font-semibold' %> <%= label_tag :url, 'Webhook URL', class: 'text-sm font-semibold' %>
<div class="flex flex-row flex-wrap space-y-2 md:space-y-0 md:flex-nowrap md:space-x-2 mt-2"> <% if @webhook_url.persisted? %>
<%= f.url_field :url, class: 'input font-mono input-bordered w-full', placeholder: 'https://example.com/hook' %> <div class="flex items-center space-x-2 md:absolute md:right-0">
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button w-full md:w-32' %> <%= link_to webhook_secret_path(@webhook_url), class: 'btn btn-outline btn-sm bg-white', data: { turbo_frame: 'modal' } do %>
<% if @webhook_url.persisted? %> <%= svg_icon('lock', class: 'w-4 h-4') %>
<a href="<%= webhook_secret_path(@webhook_url) %>" data-turbo-frame="modal" class="white-button w-full md:w-auto"> <span><%= @webhook_url.secret.present? ? t('edit_secret') : t('add_secret') %></span>
<%= @webhook_url.secret.present? ? t('edit_secret') : t('add_secret') %> <% end %>
</a> <div class="tooltip tooltip-left md:tooltip-top" data-tip="<%= t('delete_webhook') %>">
<% end %> <%= button_to settings_webhook_path(@webhook_url), class: 'btn btn-warning btn-sm', method: :delete, data: { turbo_confirm: t('are_you_sure_') } do %>
</div> <span><%= t('delete') %></span>
<% end %>
<%= form_for @webhook_url, url: @webhook_url.url.present? ? webhook_preference_path(@webhook_url) : '', method: :put, html: { autocomplete: 'off' } do |f| %>
<% WebhookUrl::EVENTS.group_by { |e| e.include?('form') }.each do |_, events| %>
<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 mt-4 gap-y-2">
<% events.each do |event| %>
<%= f.fields_for :events do |ff| %>
<div class="flex">
<label class="flex items-center space-x-2 cursor-pointer">
<%= ff.check_box event, class: 'base-checkbox', checked: @webhook_url.events.include?(event), onchange: 'this.form.requestSubmit()', disabled: @webhook_url.url.blank? %>
<span>
<%= event %>
</span>
</label>
</div>
<% end %> <% end %>
</div>
</div>
<% end %>
</div>
<%= form_for @webhook_url, url: @webhook_url.persisted? ? settings_webhook_path(@webhook_url) : settings_webhooks_path, html: { autocomplete: 'off' } do |f| %>
<div class="flex flex-row flex-wrap space-y-2 md:space-y-0 md:flex-nowrap md:space-x-2">
<%= f.url_field :url, class: 'input font-mono input-bordered w-full', placeholder: 'https://example.com/hook', required: true %>
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button w-full md:w-32' %>
</div>
<% unless @webhook_url.persisted? %>
<div class="space-y-4 mt-4">
<% WebhookUrl::EVENTS.group_by { |e| e.include?('form') }.each do |_, events| %>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-y-2">
<%= f.collection_check_boxes(:events, events, :to_s, :to_s, include_hidden: false) do |b| %>
<div class="flex">
<label class="flex items-center space-x-2">
<%= b.check_box class: 'base-checkbox', checked: @webhook_url.events.include?(b.value) %>
<span>
<%= b.label %>
</span>
</label>
</div>
<% end %>
</div>
<% end %>
</div>
<% end %>
<% end %>
<% if @webhook_url.persisted? %>
<%= form_for @webhook_url, url: webhook_preference_path(@webhook_url), method: :put, html: { autocomplete: 'off', class: 'mt-2' } do |f| %>
<div class="space-y-4">
<% WebhookUrl::EVENTS.group_by { |e| e.include?('form') }.each do |_, events| %>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-y-4">
<% events.each do |event| %>
<%= f.fields_for :events do |ff| %>
<div class="flex">
<label class="flex items-center cursor-pointer">
<%= ff.check_box event, class: 'base-checkbox', checked: @webhook_url.events.include?(event), onchange: 'this.form.requestSubmit()' %>
<span class="ml-2"><%= event %></span>
</label>
</div>
<% end %>
<% end %>
</div>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
@@ -44,12 +84,12 @@
<div class="space-y-4 mt-4"> <div class="space-y-4 mt-4">
<div class="collapse collapse-open bg-base-200 px-1"> <div class="collapse collapse-open bg-base-200 px-1">
<div class="p-4 text-xl font-medium"> <div class="p-4 text-xl font-medium">
<div class="flex items-center justify-between gap-2"> <div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center md:h-8">
<span> <span>
<%= t('submission_example_payload') %> <%= t('submission_example_payload') %>
</span> </span>
<% if @webhook_url.url.present? && @webhook_url.events.include?('form.completed') %> <% if @webhook_url.url.present? && @webhook_url.events.include?('form.completed') %>
<%= button_to button_title(title: 'Test Webhook', disabled_with: t('sending'), icon_disabled: svg_icon('loader', class: 'w-4 h-4 animate-spin')), settings_webhooks_path, class: 'btn btn-neutral btn-outline btn-sm', method: :put %> <%= button_to button_title(title: 'Test Webhook', disabled_with: t('sending'), icon_disabled: svg_icon('loader', class: 'w-4 h-4 animate-spin')), settings_webhook_resend_path(@webhook_url), class: 'btn btn-neutral btn-outline btn-sm', method: :post %>
<% end %> <% end %>
</div> </div>
</div> </div>
+4 -3
View File
@@ -13,10 +13,11 @@ end
ActiveSupport.on_load(:active_storage_blob) do ActiveSupport.on_load(:active_storage_blob) do
attribute :uuid, :string, default: -> { SecureRandom.uuid } 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( Rails.application.routes.url_helpers.blobs_proxy_url(
signed_uuid: blob.signed_uuid(expires_at:), filename: blob.filename, signed_uuid: blob.signed_uuid(expires_at:), filename: filename || blob.filename,
**Docuseal.default_url_options **Docuseal.default_url_options,
**{ host: }.compact
) )
end end
+129 -15
View File
@@ -22,6 +22,9 @@ en: &en
thanks: Thanks thanks: Thanks
unarchive: Unarchive unarchive: Unarchive
first_party: 'First Party' 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 docuseal_trusted_signature: DocuSeal Trusted Signature
hello_name: Hello %{name} hello_name: Hello %{name}
you_are_invited_to_product_name: You are invited to %{product_name} you_are_invited_to_product_name: You are invited to %{product_name}
@@ -344,7 +347,7 @@ en: &en
edit_message: Edit message edit_message: Edit message
smtp_not_configured: SMTP not Configured smtp_not_configured: SMTP not Configured
configure_smtp_settings_in_order_to_send_emails_: 'Configure SMTP settings in order to send emails:' 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 save_as_default_template_message: Save as default template message
re_send_sms: Re-send SMS re_send_sms: Re-send SMS
send_sms: Send SMS send_sms: Send SMS
@@ -511,13 +514,17 @@ en: &en
webhook_secret_has_been_saved: Webhook Secret has been saved. webhook_secret_has_been_saved: Webhook Secret has been saved.
webhook_url_has_been_saved: Webhook URL has been saved. webhook_url_has_been_saved: Webhook URL has been saved.
webhook_request_has_been_sent: Webhook request has been sent. webhook_request_has_been_sent: Webhook request has been sent.
webhook_url_has_been_updated: Webhook URL has been updated.
webhook_url_has_been_deleted: Webhook URL has been deleted.
unable_to_resend_webhook_request: Unable to resend webhook request.
new_webhook: New Webhook
delete_webhook: Delete webhook
count_submissions_have_been_created: '%{count} submissions have been created.' count_submissions_have_been_created: '%{count} submissions have been created.'
gmail_has_been_connected: Gmail has been connected gmail_has_been_connected: Gmail has been connected
microsoft_account_has_been_connected: Microsoft Account has been connected microsoft_account_has_been_connected: Microsoft Account has been connected
sms_length_cant_be_longer_than_120_bytes: SMS length can't be longer than 120 bytes sms_length_cant_be_longer_than_120_bytes: SMS length can't be longer than 120 bytes
connected_successfully: Connected successfully. connected_successfully: Connected successfully.
user_nameid_not_found: 'User %{nameid} not found.' user_nameid_not_found: 'User %{nameid} not found.'
webhook_request_has_been_sent: Webhook request has been sent.
sso_settings_have_been_updated: SSO settings have been updated. sso_settings_have_been_updated: SSO settings have been updated.
sms_has_been_sent: SMS has been sent. sms_has_been_sent: SMS has been sent.
account_has_been_created: Account has been created. account_has_been_created: Account has been created.
@@ -615,7 +622,7 @@ en: &en
verified: Verified verified: Verified
unverified: Unverified unverified: Unverified
document: Document document: Document
completed_at: Completed At completed_at: Completed at
edit_recipient: Edit Recipient edit_recipient: Edit Recipient
update_recipient: Update Recipient update_recipient: Update Recipient
use_international_format_1xxx_: 'Use internatioanl format: +1xxx...' use_international_format_1xxx_: 'Use internatioanl format: +1xxx...'
@@ -627,6 +634,18 @@ en: &en
key: Key key: Key
value: Value value: Value
webhook_secret: Webhook Secret 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: submission_event_names:
send_email_to_html: '<b>Email sent</b> to %{submitter_name}' send_email_to_html: '<b>Email sent</b> to %{submitter_name}'
send_reminder_email_to_html: '<b>Reminder email sent</b> to %{submitter_name}' send_reminder_email_to_html: '<b>Reminder email sent</b> to %{submitter_name}'
@@ -663,6 +682,9 @@ en: &en
read: Read your data read: Read your data
es: &es 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 unarchive: Desarchivar
awaiting_completion_by_the_other_party: "Esperando la finalización por la otra parte" awaiting_completion_by_the_other_party: "Esperando la finalización por la otra parte"
enforce_recipients_order: 'Hacer cumplir el orden de los destinatarios' enforce_recipients_order: 'Hacer cumplir el orden de los destinatarios'
@@ -989,7 +1011,7 @@ es: &es
edit_message: Editar mensaje edit_message: Editar mensaje
smtp_not_configured: SMTP no configurado smtp_not_configured: SMTP no configurado
configure_smtp_settings_in_order_to_send_emails_: 'Configura los ajustes de SMTP para enviar correos electrónicos:' 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 save_as_default_template_message: Guardar como mensaje de plantilla predeterminado
re_send_sms: Reenviar SMS re_send_sms: Reenviar SMS
send_sms: Enviar SMS send_sms: Enviar SMS
@@ -1156,13 +1178,17 @@ es: &es
webhook_secret_has_been_saved: El secreto del Webhook ha sido guardado. webhook_secret_has_been_saved: El secreto del Webhook ha sido guardado.
webhook_url_has_been_saved: La URL del Webhook ha sido guardada. webhook_url_has_been_saved: La URL del Webhook ha sido guardada.
webhook_request_has_been_sent: La solicitud del Webhook ha sido enviada. webhook_request_has_been_sent: La solicitud del Webhook ha sido enviada.
webhook_url_has_been_updated: La URL del Webhook ha sido actualizada.
webhook_url_has_been_deleted: La URL del Webhook ha sido eliminada.
unable_to_resend_webhook_request: No se pudo reenviar la solicitud del webhook.
new_webhook: Nuevo Webhook
delete_webhook: Eliminar webhook
count_submissions_have_been_created: '%{count} envíos han sido creados.' count_submissions_have_been_created: '%{count} envíos han sido creados.'
gmail_has_been_connected: Gmail ha sido conectado. gmail_has_been_connected: Gmail ha sido conectado.
microsoft_account_has_been_connected: La cuenta de Microsoft ha sido conectada. microsoft_account_has_been_connected: La cuenta de Microsoft ha sido conectada.
sms_length_cant_be_longer_than_120_bytes: La longitud del SMS no puede ser mayor a 120 bytes. sms_length_cant_be_longer_than_120_bytes: La longitud del SMS no puede ser mayor a 120 bytes.
connected_successfully: Conectado con éxito. connected_successfully: Conectado con éxito.
user_nameid_not_found: 'Usuario %{nameid} no encontrado.' user_nameid_not_found: 'Usuario %{nameid} no encontrado.'
webhook_request_has_been_sent: La solicitud del Webhook ha sido enviada.
sso_settings_have_been_updated: La configuración de SSO ha sido actualizada. sso_settings_have_been_updated: La configuración de SSO ha sido actualizada.
sms_has_been_sent: El SMS ha sido enviado. sms_has_been_sent: El SMS ha sido enviado.
account_has_been_created: La cuenta ha sido creada. account_has_been_created: La cuenta ha sido creada.
@@ -1272,6 +1298,18 @@ es: &es
key: Clave key: Clave
value: Valor value: Valor
webhook_secret: Secreto del Webhook 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: submission_event_names:
send_email_to_html: '<b>Correo electrónico enviado</b> a %{submitter_name}' send_email_to_html: '<b>Correo electrónico enviado</b> a %{submitter_name}'
send_reminder_email_to_html: '<b>Correo de recordatorio enviado</b> a %{submitter_name}' send_reminder_email_to_html: '<b>Correo de recordatorio enviado</b> a %{submitter_name}'
@@ -1308,6 +1346,9 @@ es: &es
read: Leer tus datos read: Leer tus datos
it: &it it: &it
remove_filter: Rimuovi filtro
document_download_filename_format: Formato del nome file scaricato
document_name: Nome del Documento
unarchive: Ripristina unarchive: Ripristina
awaiting_completion_by_the_other_party: "In attesa di completamento da parte dell'altra parte" awaiting_completion_by_the_other_party: "In attesa di completamento da parte dell'altra parte"
enforce_recipients_order: Aplicar el orden de los destinatarios enforce_recipients_order: Aplicar el orden de los destinatarios
@@ -1634,7 +1675,7 @@ it: &it
edit_message: Modifica messaggio edit_message: Modifica messaggio
smtp_not_configured: SMTP non configurato smtp_not_configured: SMTP non configurato
configure_smtp_settings_in_order_to_send_emails_: 'Configura le impostazioni SMTP per inviare email:' 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 save_as_default_template_message: Salva come messaggio modello predefinito
re_send_sms: Reinvio SMS re_send_sms: Reinvio SMS
send_sms: Invia SMS send_sms: Invia SMS
@@ -1801,13 +1842,17 @@ it: &it
webhook_secret_has_been_saved: Il segreto del Webhook è stato salvato. webhook_secret_has_been_saved: Il segreto del Webhook è stato salvato.
webhook_url_has_been_saved: "L'URL del Webhook è stato salvato." webhook_url_has_been_saved: "L'URL del Webhook è stato salvato."
webhook_request_has_been_sent: La richiesta del Webhook è stata inviata. webhook_request_has_been_sent: La richiesta del Webhook è stata inviata.
webhook_url_has_been_updated: "L'URL del Webhook è stata aggiornata."
webhook_url_has_been_deleted: "L'URL del Webhook è stata eliminata."
unable_to_resend_webhook_request: Impossibile reinviare la richiesta del webhook.
new_webhook: Nuovo Webhook
delete_webhook: Elimina webhook
count_submissions_have_been_created: '%{count} invii sono stati creati.' count_submissions_have_been_created: '%{count} invii sono stati creati.'
gmail_has_been_connected: Gmail è stato connesso. gmail_has_been_connected: Gmail è stato connesso.
microsoft_account_has_been_connected: "L'account Microsoft è stato connesso." microsoft_account_has_been_connected: "L'account Microsoft è stato connesso."
sms_length_cant_be_longer_than_120_bytes: "La lunghezza dell'SMS non può superare i 120 byte." sms_length_cant_be_longer_than_120_bytes: "La lunghezza dell'SMS non può superare i 120 byte."
connected_successfully: Collegamento avvenuto con successo. connected_successfully: Collegamento avvenuto con successo.
user_nameid_not_found: 'Utente %{nameid} non trovato.' user_nameid_not_found: 'Utente %{nameid} non trovato.'
webhook_request_has_been_sent: La richiesta del Webhook è stata inviata.
sso_settings_have_been_updated: Le impostazioni SSO sono state aggiornate. sso_settings_have_been_updated: Le impostazioni SSO sono state aggiornate.
sms_has_been_sent: "L'SMS è stato inviato." sms_has_been_sent: "L'SMS è stato inviato."
account_has_been_created: "L'account è stato creato." account_has_been_created: "L'account è stato creato."
@@ -1917,6 +1962,18 @@ it: &it
key: Chiave key: Chiave
value: Valore value: Valore
webhook_secret: Segreto del Webhook 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: submission_event_names:
send_email_to_html: '<b>E-mail inviato</b> a %{submitter_name}' send_email_to_html: '<b>E-mail inviato</b> a %{submitter_name}'
send_reminder_email_to_html: '<b>E-mail di promemoria inviato</b> a %{submitter_name}' send_reminder_email_to_html: '<b>E-mail di promemoria inviato</b> a %{submitter_name}'
@@ -1953,6 +2010,9 @@ it: &it
read: Leggi i tuoi dati read: Leggi i tuoi dati
fr: &fr 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 unarchive: Désarchiver
awaiting_completion_by_the_other_party: "En attente de la complétion par l'autre partie" awaiting_completion_by_the_other_party: "En attente de la complétion par l'autre partie"
enforce_recipients_order: "Respecter l'ordre des destinataires" enforce_recipients_order: "Respecter l'ordre des destinataires"
@@ -2280,7 +2340,7 @@ fr: &fr
edit_message: Modifier le message edit_message: Modifier le message
smtp_not_configured: SMTP non configuré smtp_not_configured: SMTP non configuré
configure_smtp_settings_in_order_to_send_emails_: 'Configurez les paramètres SMTP pour envoyer des e-mails:' 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 save_as_default_template_message: Enregistrer comme modèle de message par défaut
re_send_sms: Renvoyer le SMS re_send_sms: Renvoyer le SMS
send_sms: Envoyer SMS send_sms: Envoyer SMS
@@ -2447,13 +2507,17 @@ fr: &fr
webhook_secret_has_been_saved: Le secret du Webhook a été enregistré. webhook_secret_has_been_saved: Le secret du Webhook a été enregistré.
webhook_url_has_been_saved: "L'URL du Webhook a été enregistrée." webhook_url_has_been_saved: "L'URL du Webhook a été enregistrée."
webhook_request_has_been_sent: La demande du Webhook a été envoyée. webhook_request_has_been_sent: La demande du Webhook a été envoyée.
webhook_url_has_been_updated: "L'URL du Webhook a été mise à jour."
webhook_url_has_been_deleted: "L'URL du Webhook a été supprimée."
unable_to_resend_webhook_request: Impossible de renvoyer la requête du webhook.
new_webhook: Nouveau Webhook
delete_webhook: Supprimer le webhook
count_submissions_have_been_created: '%{count} soumissions ont été créées.' count_submissions_have_been_created: '%{count} soumissions ont été créées.'
gmail_has_been_connected: Gmail a été connecté. gmail_has_been_connected: Gmail a été connecté.
microsoft_account_has_been_connected: Le compte Microsoft a été connecté. microsoft_account_has_been_connected: Le compte Microsoft a été connecté.
sms_length_cant_be_longer_than_120_bytes: La longueur du SMS ne peut pas dépasser 120 octets. sms_length_cant_be_longer_than_120_bytes: La longueur du SMS ne peut pas dépasser 120 octets.
connected_successfully: Connecté avec succès. connected_successfully: Connecté avec succès.
user_nameid_not_found: 'Utilisateur %{nameid} introuvable.' user_nameid_not_found: 'Utilisateur %{nameid} introuvable.'
webhook_request_has_been_sent: La demande du Webhook a été envoyée.
sso_settings_have_been_updated: Les paramètres de SSO ont été mis à jour. sso_settings_have_been_updated: Les paramètres de SSO ont été mis à jour.
sms_has_been_sent: Le SMS a été envoyé. sms_has_been_sent: Le SMS a été envoyé.
account_has_been_created: Le compte a été créé. account_has_been_created: Le compte a été créé.
@@ -2563,6 +2627,18 @@ fr: &fr
key: Clé key: Clé
value: Valeur value: Valeur
webhook_secret: Secret du Webhook 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: submission_event_names:
send_email_to_html: '<b>E-mail envoyé</b> à %{submitter_name}' send_email_to_html: '<b>E-mail envoyé</b> à %{submitter_name}'
send_reminder_email_to_html: '<b>E-mail de rappel envoyé</b> à %{submitter_name}' send_reminder_email_to_html: '<b>E-mail de rappel envoyé</b> à %{submitter_name}'
@@ -2599,6 +2675,9 @@ fr: &fr
read: Lire vos données read: Lire vos données
pt: &pt 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 unarchive: Desarquivar
awaiting_completion_by_the_other_party: "Aguardando a conclusão pela outra parte" awaiting_completion_by_the_other_party: "Aguardando a conclusão pela outra parte"
enforce_recipients_order: 'Forçar a ordem dos recipientes' enforce_recipients_order: 'Forçar a ordem dos recipientes'
@@ -2925,7 +3004,7 @@ pt: &pt
edit_message: Editar mensagem edit_message: Editar mensagem
smtp_not_configured: SMTP não configurado 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:' 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 save_as_default_template_message: Salvar como mensagem de modelo padrão
re_send_sms: Reenviar SMS re_send_sms: Reenviar SMS
send_sms: Enviar SMS send_sms: Enviar SMS
@@ -3092,13 +3171,17 @@ pt: &pt
webhook_secret_has_been_saved: O segredo do Webhook foi salvo. webhook_secret_has_been_saved: O segredo do Webhook foi salvo.
webhook_url_has_been_saved: A URL do Webhook foi salva. webhook_url_has_been_saved: A URL do Webhook foi salva.
webhook_request_has_been_sent: A solicitação do Webhook foi enviada. webhook_request_has_been_sent: A solicitação do Webhook foi enviada.
webhook_url_has_been_updated: URL do Webhook foi atualizada.
webhook_url_has_been_deleted: URL do Webhook foi excluída.
unable_to_resend_webhook_request: Não foi possível reenviar a solicitação do webhook.
new_webhook: Novo Webhook
delete_webhook: Excluir webhook
count_submissions_have_been_created: '%{count} submissões foram criadas.' count_submissions_have_been_created: '%{count} submissões foram criadas.'
gmail_has_been_connected: O Gmail foi conectado gmail_has_been_connected: O Gmail foi conectado
microsoft_account_has_been_connected: A conta da Microsoft foi conectada microsoft_account_has_been_connected: A conta da Microsoft foi conectada
sms_length_cant_be_longer_than_120_bytes: O comprimento do SMS não pode ultrapassar 120 bytes sms_length_cant_be_longer_than_120_bytes: O comprimento do SMS não pode ultrapassar 120 bytes
connected_successfully: Conectado com sucesso. connected_successfully: Conectado com sucesso.
user_nameid_not_found: 'Usuário %{nameid} não encontrado.' user_nameid_not_found: 'Usuário %{nameid} não encontrado.'
webhook_request_has_been_sent: A solicitação do Webhook foi enviada.
sso_settings_have_been_updated: As configurações de SSO foram atualizadas. sso_settings_have_been_updated: As configurações de SSO foram atualizadas.
sms_has_been_sent: O SMS foi enviado. sms_has_been_sent: O SMS foi enviado.
account_has_been_created: A conta foi criada. account_has_been_created: A conta foi criada.
@@ -3208,6 +3291,18 @@ pt: &pt
key: Chave key: Chave
value: Valor value: Valor
webhook_secret: Segredo do Webhook 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: submission_event_names:
send_email_to_html: '<b>E-mail enviado</b> para %{submitter_name}' send_email_to_html: '<b>E-mail enviado</b> para %{submitter_name}'
send_reminder_email_to_html: '<b>E-mail de lembrete enviado</b> para %{submitter_name}' send_reminder_email_to_html: '<b>E-mail de lembrete enviado</b> para %{submitter_name}'
@@ -3244,6 +3339,9 @@ pt: &pt
read: Ler seus dados read: Ler seus dados
de: &de de: &de
remove_filter: Filter entfernen
document_download_filename_format: Format des Dateinamens beim Herunterladen von Dokumenten
document_name: Dokumentname
unarchive: Wiederherstellen unarchive: Wiederherstellen
awaiting_completion_by_the_other_party: "Warten auf die Fertigstellung durch die andere Partei" awaiting_completion_by_the_other_party: "Warten auf die Fertigstellung durch die andere Partei"
enforce_recipients_order: 'Empfängerreihenfolge durchsetzen' enforce_recipients_order: 'Empfängerreihenfolge durchsetzen'
@@ -3570,7 +3668,7 @@ de: &de
edit_message: Nachricht bearbeiten edit_message: Nachricht bearbeiten
smtp_not_configured: SMTP nicht konfiguriert smtp_not_configured: SMTP nicht konfiguriert
configure_smtp_settings_in_order_to_send_emails_: 'Konfiguriere die SMTP-Einstellungen, um E-Mails zu senden:' 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 save_as_default_template_message: Als Standardvorlage speichern
re_send_sms: SMS erneut senden re_send_sms: SMS erneut senden
send_sms: SMS senden send_sms: SMS senden
@@ -3699,8 +3797,8 @@ de: &de
api_key: API-Schlüssel api_key: API-Schlüssel
logo: Logo logo: Logo
back: Zurück back: Zurück
add_secret: Geheimnis hinzufügen add_secret: Geheimnis hinzuf
edit_secret: Geheimnis bearbeiten edit_secret: Geheimnis bearb
submission_example_payload: Beispiel-Payload für Einreichung submission_example_payload: Beispiel-Payload für Einreichung
there_are_no_signatures: Es gibt keine Unterschriften there_are_no_signatures: Es gibt keine Unterschriften
signed_with_trusted_certificate: Signiert mit vertrauenswürdigem Zertifikat signed_with_trusted_certificate: Signiert mit vertrauenswürdigem Zertifikat
@@ -3737,13 +3835,17 @@ de: &de
webhook_secret_has_been_saved: Das Webhook-Geheimnis wurde gespeichert. webhook_secret_has_been_saved: Das Webhook-Geheimnis wurde gespeichert.
webhook_url_has_been_saved: Die Webhook-URL wurde gespeichert. webhook_url_has_been_saved: Die Webhook-URL wurde gespeichert.
webhook_request_has_been_sent: Die Webhook-Anfrage wurde gesendet. webhook_request_has_been_sent: Die Webhook-Anfrage wurde gesendet.
webhook_url_has_been_updated: Webhook-URL wurde aktualisiert.
webhook_url_has_been_deleted: Webhook-URL wurde gelöscht.
unable_to_resend_webhook_request: Webhook-Anfrage konnte nicht erneut gesendet werden.
new_webhook: Neuer Webhook
delete_webhook: Webhook löschen
count_submissions_have_been_created: '%{count} Einreichungen wurden erstellt.' count_submissions_have_been_created: '%{count} Einreichungen wurden erstellt.'
gmail_has_been_connected: Gmail wurde verbunden. gmail_has_been_connected: Gmail wurde verbunden.
microsoft_account_has_been_connected: Microsoft-Konto wurde verbunden. microsoft_account_has_been_connected: Microsoft-Konto wurde verbunden.
sms_length_cant_be_longer_than_120_bytes: Die SMS-Länge darf 120 Bytes nicht überschreiten. sms_length_cant_be_longer_than_120_bytes: Die SMS-Länge darf 120 Bytes nicht überschreiten.
connected_successfully: Erfolgreich verbunden. connected_successfully: Erfolgreich verbunden.
user_nameid_not_found: 'Benutzer %{nameid} nicht gefunden.' user_nameid_not_found: 'Benutzer %{nameid} nicht gefunden.'
webhook_request_has_been_sent: Die Webhook-Anfrage wurde gesendet.
sso_settings_have_been_updated: Die SSO-Einstellungen wurden aktualisiert. sso_settings_have_been_updated: Die SSO-Einstellungen wurden aktualisiert.
sms_has_been_sent: Die SMS wurde gesendet. sms_has_been_sent: Die SMS wurde gesendet.
account_has_been_created: Das Konto wurde erstellt. account_has_been_created: Das Konto wurde erstellt.
@@ -3853,6 +3955,18 @@ de: &de
key: Schlüssel key: Schlüssel
value: Wert value: Wert
webhook_secret: Webhook-Geheimnis 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: submission_event_names:
send_email_to_html: '<b>E-Mail gesendet</b> an %{submitter_name}' send_email_to_html: '<b>E-Mail gesendet</b> an %{submitter_name}'
send_reminder_email_to_html: '<b>Erinnerungs-E-Mail gesendet</b> an %{submitter_name}' send_reminder_email_to_html: '<b>Erinnerungs-E-Mail gesendet</b> an %{submitter_name}'
+4 -1
View File
@@ -90,6 +90,7 @@ Rails.application.routes.draw do
resources :folders, only: %i[show edit update destroy], controller: 'template_folders' resources :folders, only: %i[show edit update destroy], controller: 'template_folders'
resources :template_sharings_testing, only: %i[create] resources :template_sharings_testing, only: %i[create]
resources :templates, only: %i[index], controller: 'templates_dashboard' 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 resources :templates, only: %i[new create edit update show destroy] do
resource :debug, only: %i[show], controller: 'templates_debug' if Rails.env.development? resource :debug, only: %i[show], controller: 'templates_debug' if Rails.env.development?
resources :documents, only: %i[create], controller: 'template_documents' resources :documents, only: %i[create], controller: 'template_documents'
@@ -168,7 +169,9 @@ Rails.application.routes.draw do
defaults: { status: :integration } defaults: { status: :integration }
resource :personalization, only: %i[show create], controller: 'personalization_settings' resource :personalization, only: %i[show create], controller: 'personalization_settings'
resources :api, only: %i[index create], controller: 'api_settings' resources :api, only: %i[index create], controller: 'api_settings'
resource :webhooks, only: %i[show create update], controller: 'webhook_settings' resources :webhooks, only: %i[index show new create update destroy], controller: 'webhook_settings' do
post :resend
end
resource :account, only: %i[show update destroy] resource :account, only: %i[show update destroy]
resources :profile, only: %i[index] do resources :profile, only: %i[index] do
collection do collection do
+1
View File
@@ -3,6 +3,7 @@
module Docuseal module Docuseal
URL_CACHE = ActiveSupport::Cache::MemoryStore.new URL_CACHE = ActiveSupport::Cache::MemoryStore.new
PRODUCT_URL = 'https://www.docuseal.com' PRODUCT_URL = 'https://www.docuseal.com'
PRODUCT_EMAIL_URL = ENV.fetch('PRODUCT_EMAIL_URL', PRODUCT_URL)
NEWSLETTER_URL = "#{PRODUCT_URL}/newsletters".freeze NEWSLETTER_URL = "#{PRODUCT_URL}/newsletters".freeze
ENQUIRIES_URL = "#{PRODUCT_URL}/enquiries".freeze ENQUIRIES_URL = "#{PRODUCT_URL}/enquiries".freeze
PRODUCT_NAME = 'DocuSeal' PRODUCT_NAME = 'DocuSeal'
+10 -1
View File
@@ -19,6 +19,8 @@ module ReplaceEmailVariables
DOCUMENTS_LINKS = /\{+documents\.links\}+/i DOCUMENTS_LINKS = /\{+documents\.links\}+/i
DOCUMENTS_LINK = /\{+documents\.link\}+/i DOCUMENTS_LINK = /\{+documents\.link\}+/i
EMAIL_HOST = ENV.fetch('EMAIL_HOST', nil)
module_function module_function
# rubocop:disable Metrics # rubocop:disable Metrics
@@ -64,10 +66,17 @@ module ReplaceEmailVariables
def build_submitter_link(submitter, tracking_event_type) def build_submitter_link(submitter, tracking_event_type)
if tracking_event_type == 'click_email' 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( Rails.application.routes.url_helpers.submit_form_url(
slug: submitter.slug, slug: submitter.slug,
t: SubmissionEvents.build_tracking_param(submitter, 'click_email'), t: SubmissionEvents.build_tracking_param(submitter, 'click_email'),
**Docuseal.default_url_options **url_options
) )
else else
Rails.application.routes.url_helpers.submit_form_url( Rails.application.routes.url_helpers.submit_form_url(
+7 -5
View File
@@ -26,7 +26,7 @@ module Submissions
arel = arel.or(Template.arel_table[:name].lower.matches("%#{keyword.downcase}%")) arel = arel.or(Template.arel_table[:name].lower.matches("%#{keyword.downcase}%"))
end end
submissions.joins(:submitters).where(arel).distinct submissions.joins(:submitters).where(arel).group(:id)
end end
def update_template_fields!(submission) def update_template_fields!(submission)
@@ -88,17 +88,19 @@ module Submissions
) )
end end
def send_signature_requests(submissions) def send_signature_requests(submissions, delay: nil)
submissions.each do |submission| submissions.each_with_index do |submission, index|
delay_seconds = (delay + index).seconds if delay
submitters = submission.submitters.reject(&:completed_at?) submitters = submission.submitters.reject(&:completed_at?)
if submission.submitters_order_preserved? if submission.submitters_order_preserved?
first_submitter = first_submitter =
submission.template_submitters.filter_map { |s| submitters.find { |e| e.uuid == s['uuid'] } }.first 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 else
Submitters.send_signature_requests(submitters) Submitters.send_signature_requests(submitters, delay_seconds:)
end end
end end
end end
+64
View File
@@ -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
+6 -3
View File
@@ -23,7 +23,7 @@ module Submissions
RTL_REGEXP = TextUtils::RTL_REGEXP RTL_REGEXP = TextUtils::RTL_REGEXP
MAX_IMAGE_HEIGHT = 100 MAX_IMAGE_HEIGHT = 100
US_TIMEZONES = %w[EST CST MST PST HST AKDT].freeze US_TIMEZONES = TimeUtils::US_TIMEZONES
module_function module_function
@@ -64,7 +64,10 @@ module Submissions
def build_audit_trail(submission) def build_audit_trail(submission)
account = submission.account 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 = page_size =
if TimeUtils.timezone_abbr(account.timezone, Time.current.beginning_of_year).in?(US_TIMEZONES) if TimeUtils.timezone_abbr(account.timezone, Time.current.beginning_of_year).in?(US_TIMEZONES)
:Letter :Letter
@@ -411,7 +414,7 @@ module Submissions
column.image(PdfIcons.logo_io, width: 40, height: 40, position: :float) column.image(PdfIcons.logo_io, width: 40, height: 40, position: :float)
column.formatted_text([{ text: 'DocuSeal', column.formatted_text([{ text: 'DocuSeal',
link: Docuseal::PRODUCT_URL }], link: Docuseal::PRODUCT_EMAIL_URL }],
font_size: 20, font_size: 20,
font: [FONT_NAME, { variant: :bold }], font: [FONT_NAME, { variant: :bold }],
width: 100, width: 100,
+8
View File
@@ -110,6 +110,10 @@ module Submissions
{ {
name: column_name(I18n.t('completed_at'), submitter_name, submitters_count), name: column_name(I18n.t('completed_at'), submitter_name, submitters_count),
value: submitter.completed_at.to_s 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? } ].reject { |e| e[:value].blank? }
end end
@@ -150,5 +154,9 @@ module Submissions
{ name: template_field_name, uuid: template_field['uuid'], value: } { name: template_field_name, uuid: template_field['uuid'], value: }
end end
end end
def r
Rails.application.routes.url_helpers
end
end end
end end
+12 -2
View File
@@ -68,7 +68,7 @@ module Submissions
name: item['name']) name: item['name'])
end end
return result_attachments.map { |e| e.tap(&:save!) } if image_pdfs.size < 2 return ApplicationRecord.no_touching { result_attachments.map { |e| e.tap(&:save!) } } if image_pdfs.size < 2
images_pdf = images_pdf =
image_pdfs.each_with_object(HexaPDF::Document.new) do |pdf, doc| image_pdfs.each_with_object(HexaPDF::Document.new) do |pdf, doc|
@@ -87,7 +87,9 @@ module Submissions
name: template.name name: template.name
) )
(result_attachments + [images_pdf_attachment]).map { |e| e.tap(&:save!) } ApplicationRecord.no_touching do
(result_attachments + [images_pdf_attachment]).map { |e| e.tap(&:save!) }
end
end end
def generate_pdfs(submitter) def generate_pdfs(submitter)
@@ -177,6 +179,8 @@ module Submissions
font_size = preferences_font_size font_size = preferences_font_size
font_size ||= (([page.box.width, page.box.height].min / A4_SIZE[0].to_f) * FONT_SIZE).to_i font_size ||= (([page.box.width, page.box.height].min / A4_SIZE[0].to_f) * FONT_SIZE).to_i
fill_color = field.dig('preferences', 'color').presence
font = pdf.fonts.add(field.dig('preferences', 'font').presence || FONT_NAME) font = pdf.fonts.add(field.dig('preferences', 'font').presence || FONT_NAME)
value = submitter.values[field['uuid']] value = submitter.values[field['uuid']]
@@ -378,6 +382,7 @@ module Submissions
next if char.blank? next if char.blank?
text = HexaPDF::Layout::TextFragment.create(char, font:, text = HexaPDF::Layout::TextFragment.create(char, font:,
fill_color:,
font_size:) font_size:)
line_height = layouter.fit([text], cell_width, height).lines.first.height line_height = layouter.fit([text], cell_width, height).lines.first.height
@@ -385,6 +390,7 @@ module Submissions
if preferences_font_size.blank? && line_height > (area['h'] * height) if preferences_font_size.blank? && line_height > (area['h'] * height)
text = HexaPDF::Layout::TextFragment.create(char, text = HexaPDF::Layout::TextFragment.create(char,
font:, font:,
fill_color:,
font_size: (font_size / 1.4).to_i) font_size: (font_size / 1.4).to_i)
line_height = layouter.fit([text], cell_width, height).lines.first.height line_height = layouter.fit([text], cell_width, height).lines.first.height
@@ -393,6 +399,7 @@ module Submissions
if preferences_font_size.blank? && line_height > (area['h'] * height) if preferences_font_size.blank? && line_height > (area['h'] * height)
text = HexaPDF::Layout::TextFragment.create(char, text = HexaPDF::Layout::TextFragment.create(char,
font:, font:,
fill_color:,
font_size: (font_size / 1.9).to_i) font_size: (font_size / 1.9).to_i)
line_height = layouter.fit([text], cell_width, height).lines.first.height line_height = layouter.fit([text], cell_width, height).lines.first.height
@@ -412,6 +419,7 @@ module Submissions
value = TextUtils.maybe_rtl_reverse(Array.wrap(value).join(', ')) value = TextUtils.maybe_rtl_reverse(Array.wrap(value).join(', '))
text = HexaPDF::Layout::TextFragment.create(value, font:, text = HexaPDF::Layout::TextFragment.create(value, font:,
fill_color:,
font_size:) font_size:)
lines = layouter.fit([text], area['w'] * width, height).lines lines = layouter.fit([text], area['w'] * width, height).lines
@@ -420,6 +428,7 @@ module Submissions
if preferences_font_size.blank? && box_height > (area['h'] * height) + 1 if preferences_font_size.blank? && box_height > (area['h'] * height) + 1
text = HexaPDF::Layout::TextFragment.create(value, text = HexaPDF::Layout::TextFragment.create(value,
font:, font:,
fill_color:,
font_size: (font_size / 1.4).to_i) font_size: (font_size / 1.4).to_i)
lines = layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width, height).lines lines = layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width, height).lines
@@ -430,6 +439,7 @@ module Submissions
if preferences_font_size.blank? && box_height > (area['h'] * height) + 1 if preferences_font_size.blank? && box_height > (area['h'] * height) + 1
text = HexaPDF::Layout::TextFragment.create(value, text = HexaPDF::Layout::TextFragment.create(value,
font:, font:,
fill_color:,
font_size: (font_size / 1.9).to_i) font_size: (font_size / 1.9).to_i)
lines = layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width, height).lines lines = layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width, height).lines
+22 -3
View File
@@ -94,12 +94,16 @@ module Submitters
preferences preferences
end end
def send_signature_requests(submitters) def send_signature_requests(submitters, delay_seconds: nil)
submitters.each do |submitter| submitters.each_with_index do |submitter, index|
next if submitter.email.blank? next if submitter.email.blank?
next if submitter.preferences['send_email'] == false 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
end end
@@ -112,4 +116,19 @@ module Submitters
acc && submitter.submission.submitters.find { |e| e.uuid == item['uuid'] }&.completed_at? acc && submitter.submission.submitters.find { |e| e.uuid == item['uuid'] }&.completed_at?
end end
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 end
+6 -2
View File
@@ -93,7 +93,11 @@ module Submitters
if params[:cast_boolean] == 'true' if params[:cast_boolean] == 'true'
v == 'true' v == 'true'
elsif params[:cast_number] == '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' elsif params[:normalize_phone] == 'true'
v.to_s.gsub(/[^0-9+]/, '') v.to_s.gsub(/[^0-9+]/, '')
else else
@@ -207,7 +211,7 @@ module Submitters
end end
def replace_default_variables(value, attrs, submission, with_time: false) 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? return if value.blank?
value.to_s.gsub(VARIABLE_REGEXP) do |e| value.to_s.gsub(VARIABLE_REGEXP) do |e|
+20 -3
View File
@@ -6,6 +6,20 @@ module Templates
FIELD_NAME_REGEXP = /\A(?=.*\p{L})[\p{L}\d\s-]+\z/ FIELD_NAME_REGEXP = /\A(?=.*\p{L})[\p{L}\d\s-]+\z/
SKIP_FIELD_DESCRIPTION = %w[undefined].freeze SKIP_FIELD_DESCRIPTION = %w[undefined].freeze
SELECT_PLACEHOLDER_REGEXP = /\b(
Select |
Choose |
Wählen |
Auswählen |
Sélectionner|
Choisir |
Seleccionar |
Elegir |
Seleziona |
Scegliere |
Selecionar |
Escolher
)\b/ix
module_function module_function
@@ -143,8 +157,8 @@ module Templates
{ {
**attrs, **attrs,
type: 'select', type: 'select',
options: build_options(field[:Opt]), options: build_options(field[:Opt], 'select'),
default_value: field.field_value default_value: field.field_value.to_s.match?(SELECT_PLACEHOLDER_REGEXP) ? nil : field.field_value
} }
elsif field.field_type == :Ch && field.concrete_field_type == :multi_select && field[:Opt].present? elsif field.field_type == :Ch && field.concrete_field_type == :multi_select && field[:Opt].present?
{ {
@@ -178,11 +192,14 @@ module Templates
def build_options(values, type = nil) def build_options(values, type = nil)
is_skip_single_value = type.in?(%w[radio multiple]) && values.uniq.size == 1 is_skip_single_value = type.in?(%w[radio multiple]) && values.uniq.size == 1
values.map do |option| values.filter_map do |option|
is_option_number = option.is_a?(Symbol) && option.to_s.match?(/\A\d+\z/) is_option_number = option.is_a?(Symbol) && option.to_s.match?(/\A\d+\z/)
option = option[1] if option.is_a?(Array) && option.size == 2
option = option.encode('utf-8', invalid: :replace, undef: :replace, replace: '') if option.is_a?(String) option = option.encode('utf-8', invalid: :replace, undef: :replace, replace: '') if option.is_a?(String)
next if type == 'select' && option.to_s.match?(SELECT_PLACEHOLDER_REGEXP)
{ {
uuid: SecureRandom.uuid, uuid: SecureRandom.uuid,
value: is_option_number || is_skip_single_value ? '' : option value: is_option_number || is_skip_single_value ? '' : option
+9
View File
@@ -122,6 +122,15 @@ module Templates
io = StringIO.new io = StringIO.new
pdf.acro_form.each_field do |field|
next if field.field_type != :Ch ||
field[:Opt].blank? ||
%i[combo_box editable_combo_box].exclude?(field.concrete_field_type) ||
!field.field_value.to_s.match?(FindAcroFields::SELECT_PLACEHOLDER_REGEXP)
field[:V] = ''
end
pdf.acro_form.create_appearances(force: true) if pdf.acro_form[:NeedAppearances] pdf.acro_form.create_appearances(force: true) if pdf.acro_form[:NeedAppearances]
pdf.acro_form.flatten pdf.acro_form.flatten
+2
View File
@@ -22,6 +22,8 @@ module TimeUtils
DEFAULT_DATE_FORMAT_US = 'MM/DD/YYYY' DEFAULT_DATE_FORMAT_US = 'MM/DD/YYYY'
DEFAULT_DATE_FORMAT = 'DD/MM/YYYY' DEFAULT_DATE_FORMAT = 'DD/MM/YYYY'
US_TIMEZONES = %w[EST CST MST PST HST AKDT].freeze
module_function module_function
def timezone_abbr(timezone, time = Time.current) def timezone_abbr(timezone, time = Time.current)
+1 -1
View File
@@ -10,7 +10,7 @@ module WebhookUrls
event_arel = events.map { |event| Arel::Table.new(:webhook_urls)[:events].matches("%\"#{event}\"%") }.reduce(:or) event_arel = events.map { |event| Arel::Table.new(:webhook_urls)[:events].matches("%\"#{event}\"%") }.reduce(:or)
if Docuseal.multitenant? if Docuseal.multitenant? || account_id == 1
rel.where(event_arel) rel.where(event_arel)
else else
linked_account_rel = linked_account_rel =
+2 -1
View File
@@ -17,7 +17,8 @@ FactoryBot.define do
submission.template_submitters.each do |template_submitter| submission.template_submitters.each do |template_submitter|
create(:submitter, submission:, create(:submitter, submission:,
account_id: submission.account_id, account_id: submission.account_id,
uuid: template_submitter['uuid']) uuid: template_submitter['uuid'],
created_at: submission.created_at)
end end
end end
end end
-1
View File
@@ -5,7 +5,6 @@ FactoryBot.define do
submission submission
email { Faker::Internet.email } email { Faker::Internet.email }
name { Faker::Name.name } name { Faker::Name.name }
phone { Faker::PhoneNumber.phone_number }
before(:create) do |submitter, _| before(:create) do |submitter, _|
submitter.account_id = submitter.submission.account_id submitter.account_id = submitter.submission.account_id
+273 -9
View File
@@ -9,9 +9,13 @@ FactoryBot.define do
transient do transient do
submitter_count { 1 } 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 end
after(:create) do |template, evaluator| after(:create) do |template, ev|
blob = ActiveStorage::Blob.create_and_upload!( blob = ActiveStorage::Blob.create_and_upload!(
io: Rails.root.join('spec/fixtures/sample-document.pdf').open, io: Rails.root.join('spec/fixtures/sample-document.pdf').open,
filename: 'sample-document.pdf', filename: 'sample-document.pdf',
@@ -28,7 +32,7 @@ FactoryBot.define do
template.schema = [{ attachment_uuid: attachment.uuid, name: 'sample-document' }] template.schema = [{ attachment_uuid: attachment.uuid, name: 'sample-document' }]
number_words = %w[first second third fourth fifth sixth seventh eighth ninth tenth] 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", 'name' => "#{number_words[i]&.capitalize} Party",
'uuid' => SecureRandom.uuid 'uuid' => SecureRandom.uuid
@@ -43,12 +47,13 @@ FactoryBot.define do
'name' => 'First Name', 'name' => 'First Name',
'type' => 'text', 'type' => 'text',
'required' => true, 'required' => true,
'preferences' => {},
'areas' => [ 'areas' => [
{ {
'x' => 0.09027777777777778, 'x' => 0.09273546006944444,
'y' => 0.1197252208047105, 'y' => 0.1099851117387033,
'w' => 0.3069444444444444, 'w' => 0.2701497395833333,
'h' => 0.03336604514229637, 'h' => 0.0372705365913556,
'attachment_uuid' => attachment.uuid, 'attachment_uuid' => attachment.uuid,
'page' => 0 'page' => 0
} }
@@ -57,12 +62,271 @@ FactoryBot.define do
{ {
'uuid' => SecureRandom.uuid, 'uuid' => SecureRandom.uuid,
'submitter_uuid' => submitter['uuid'], 'submitter_uuid' => submitter['uuid'],
'name' => '', '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.3020184190330008,
'w' => 0.2,
'h' => 0.02857142857142857,
'attachment_uuid' => attachment.uuid,
'page' => 0
}
]
},
{
'uuid' => SecureRandom.uuid,
'submitter_uuid' => submitter['uuid'],
'name' => 'Signature',
'type' => 'signature', 'type' => 'signature',
'required' => true, '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 fields
end end
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

@@ -26,11 +26,11 @@ RSpec.describe SendFormCompletedWebhookRequestJob do
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id) described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
expect(WebMock).to have_requested(:post, webhook_url.url).with( expect(WebMock).to have_requested(:post, webhook_url.url).with(
body: replace_timestamps({ body: {
'event_type' => 'form.completed', 'event_type' => 'form.completed',
'timestamp' => Time.current, 'timestamp' => /.*/,
'data' => Submitters::SerializeForWebhook.call(submitter.reload) 'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
}.deep_stringify_keys), },
headers: { headers: {
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => 'DocuSeal.com Webhook' 'User-Agent' => 'DocuSeal.com Webhook'
@@ -43,11 +43,11 @@ RSpec.describe SendFormCompletedWebhookRequestJob do
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id) described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
expect(WebMock).to have_requested(:post, webhook_url.url).with( expect(WebMock).to have_requested(:post, webhook_url.url).with(
body: replace_timestamps({ body: {
'event_type' => 'form.completed', 'event_type' => 'form.completed',
'timestamp' => Time.current, 'timestamp' => /.*/,
'data' => Submitters::SerializeForWebhook.call(submitter.reload) 'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
}.deep_stringify_keys), },
headers: { headers: {
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => 'DocuSeal.com Webhook', 'User-Agent' => 'DocuSeal.com Webhook',
@@ -26,11 +26,11 @@ RSpec.describe SendFormDeclinedWebhookRequestJob do
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id) described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
expect(WebMock).to have_requested(:post, webhook_url.url).with( expect(WebMock).to have_requested(:post, webhook_url.url).with(
body: replace_timestamps({ body: {
'event_type' => 'form.declined', 'event_type' => 'form.declined',
'timestamp' => Time.current, 'timestamp' => /.*/,
'data' => Submitters::SerializeForWebhook.call(submitter.reload) 'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
}.deep_stringify_keys), },
headers: { headers: {
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => 'DocuSeal.com Webhook' 'User-Agent' => 'DocuSeal.com Webhook'
@@ -43,11 +43,11 @@ RSpec.describe SendFormDeclinedWebhookRequestJob do
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id) described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
expect(WebMock).to have_requested(:post, webhook_url.url).with( expect(WebMock).to have_requested(:post, webhook_url.url).with(
body: replace_timestamps({ body: {
'event_type' => 'form.declined', 'event_type' => 'form.declined',
'timestamp' => Time.current, 'timestamp' => /.*/,
'data' => Submitters::SerializeForWebhook.call(submitter.reload) 'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
}.deep_stringify_keys), },
headers: { headers: {
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => 'DocuSeal.com Webhook', 'User-Agent' => 'DocuSeal.com Webhook',
@@ -26,11 +26,11 @@ RSpec.describe SendFormStartedWebhookRequestJob do
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id) described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
expect(WebMock).to have_requested(:post, webhook_url.url).with( expect(WebMock).to have_requested(:post, webhook_url.url).with(
body: replace_timestamps({ body: {
'event_type' => 'form.started', 'event_type' => 'form.started',
'timestamp' => Time.current, 'timestamp' => /.*/,
'data' => Submitters::SerializeForWebhook.call(submitter.reload) 'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
}.deep_stringify_keys), },
headers: { headers: {
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => 'DocuSeal.com Webhook' 'User-Agent' => 'DocuSeal.com Webhook'
@@ -43,11 +43,11 @@ RSpec.describe SendFormStartedWebhookRequestJob do
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id) described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
expect(WebMock).to have_requested(:post, webhook_url.url).with( expect(WebMock).to have_requested(:post, webhook_url.url).with(
body: replace_timestamps({ body: {
'event_type' => 'form.started', 'event_type' => 'form.started',
'timestamp' => Time.current, 'timestamp' => /.*/,
'data' => Submitters::SerializeForWebhook.call(submitter.reload) 'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
}.deep_stringify_keys), },
headers: { headers: {
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => 'DocuSeal.com Webhook', 'User-Agent' => 'DocuSeal.com Webhook',
@@ -26,11 +26,11 @@ RSpec.describe SendFormViewedWebhookRequestJob do
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id) described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
expect(WebMock).to have_requested(:post, webhook_url.url).with( expect(WebMock).to have_requested(:post, webhook_url.url).with(
body: replace_timestamps({ body: {
'event_type' => 'form.viewed', 'event_type' => 'form.viewed',
'timestamp' => Time.current, 'timestamp' => /.*/,
'data' => Submitters::SerializeForWebhook.call(submitter.reload) 'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
}.deep_stringify_keys), },
headers: { headers: {
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => 'DocuSeal.com Webhook' 'User-Agent' => 'DocuSeal.com Webhook'
@@ -43,11 +43,11 @@ RSpec.describe SendFormViewedWebhookRequestJob do
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id) described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
expect(WebMock).to have_requested(:post, webhook_url.url).with( expect(WebMock).to have_requested(:post, webhook_url.url).with(
body: replace_timestamps({ body: {
'event_type' => 'form.viewed', 'event_type' => 'form.viewed',
'timestamp' => Time.current, 'timestamp' => /.*/,
'data' => Submitters::SerializeForWebhook.call(submitter.reload) 'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
}.deep_stringify_keys), },
headers: { headers: {
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => 'DocuSeal.com Webhook', 'User-Agent' => 'DocuSeal.com Webhook',
@@ -23,11 +23,11 @@ RSpec.describe SendSubmissionArchivedWebhookRequestJob do
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id) described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
expect(WebMock).to have_requested(:post, webhook_url.url).with( expect(WebMock).to have_requested(:post, webhook_url.url).with(
body: replace_timestamps({ body: {
'event_type' => 'submission.archived', 'event_type' => 'submission.archived',
'timestamp' => Time.current, 'timestamp' => /.*/,
'data' => submission.reload.as_json(only: %i[id archived_at]) 'data' => JSON.parse(submission.reload.as_json(only: %i[id archived_at]).to_json)
}.deep_stringify_keys), },
headers: { headers: {
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => 'DocuSeal.com Webhook' 'User-Agent' => 'DocuSeal.com Webhook'
@@ -40,11 +40,11 @@ RSpec.describe SendSubmissionArchivedWebhookRequestJob do
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id) described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
expect(WebMock).to have_requested(:post, webhook_url.url).with( expect(WebMock).to have_requested(:post, webhook_url.url).with(
body: replace_timestamps({ body: {
'event_type' => 'submission.archived', 'event_type' => 'submission.archived',
'timestamp' => Time.current, 'timestamp' => /.*/,
'data' => submission.reload.as_json(only: %i[id archived_at]) 'data' => JSON.parse(submission.reload.as_json(only: %i[id archived_at]).to_json)
}.deep_stringify_keys), },
headers: { headers: {
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => 'DocuSeal.com Webhook', 'User-Agent' => 'DocuSeal.com Webhook',
@@ -23,11 +23,11 @@ RSpec.describe SendSubmissionCompletedWebhookRequestJob do
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id) described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
expect(WebMock).to have_requested(:post, webhook_url.url).with( expect(WebMock).to have_requested(:post, webhook_url.url).with(
body: replace_timestamps({ body: {
'event_type' => 'submission.completed', 'event_type' => 'submission.completed',
'timestamp' => Time.current, 'timestamp' => /.*/,
'data' => Submissions::SerializeForApi.call(submission.reload) 'data' => JSON.parse(Submissions::SerializeForApi.call(submission.reload).to_json)
}.deep_stringify_keys), },
headers: { headers: {
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => 'DocuSeal.com Webhook' 'User-Agent' => 'DocuSeal.com Webhook'
@@ -40,11 +40,11 @@ RSpec.describe SendSubmissionCompletedWebhookRequestJob do
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id) described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
expect(WebMock).to have_requested(:post, webhook_url.url).with( expect(WebMock).to have_requested(:post, webhook_url.url).with(
body: replace_timestamps({ body: {
'event_type' => 'submission.completed', 'event_type' => 'submission.completed',
'timestamp' => Time.current, 'timestamp' => /.*/,
'data' => Submissions::SerializeForApi.call(submission.reload) 'data' => JSON.parse(Submissions::SerializeForApi.call(submission.reload).to_json)
}.deep_stringify_keys), },
headers: { headers: {
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => 'DocuSeal.com Webhook', 'User-Agent' => 'DocuSeal.com Webhook',
@@ -23,11 +23,11 @@ RSpec.describe SendSubmissionCreatedWebhookRequestJob do
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id) described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
expect(WebMock).to have_requested(:post, webhook_url.url).with( expect(WebMock).to have_requested(:post, webhook_url.url).with(
body: replace_timestamps({ body: {
'event_type' => 'submission.created', 'event_type' => 'submission.created',
'timestamp' => Time.current, 'timestamp' => /.*/,
'data' => Submissions::SerializeForApi.call(submission.reload) 'data' => JSON.parse(Submissions::SerializeForApi.call(submission.reload).to_json)
}.deep_stringify_keys), },
headers: { headers: {
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => 'DocuSeal.com Webhook' 'User-Agent' => 'DocuSeal.com Webhook'
@@ -40,11 +40,11 @@ RSpec.describe SendSubmissionCreatedWebhookRequestJob do
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id) described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
expect(WebMock).to have_requested(:post, webhook_url.url).with( expect(WebMock).to have_requested(:post, webhook_url.url).with(
body: replace_timestamps({ body: {
'event_type' => 'submission.created', 'event_type' => 'submission.created',
'timestamp' => Time.current, 'timestamp' => /.*/,
'data' => Submissions::SerializeForApi.call(submission.reload) 'data' => JSON.parse(Submissions::SerializeForApi.call(submission.reload).to_json)
}.deep_stringify_keys), },
headers: { headers: {
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => 'DocuSeal.com Webhook', 'User-Agent' => 'DocuSeal.com Webhook',
@@ -22,11 +22,11 @@ RSpec.describe SendTemplateCreatedWebhookRequestJob do
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id) described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id)
expect(WebMock).to have_requested(:post, webhook_url.url).with( expect(WebMock).to have_requested(:post, webhook_url.url).with(
body: replace_timestamps({ body: {
'event_type' => 'template.created', 'event_type' => 'template.created',
'timestamp' => Time.current, 'timestamp' => /.*/,
'data' => Templates::SerializeForApi.call(template.reload) 'data' => JSON.parse(Templates::SerializeForApi.call(template.reload).to_json)
}.deep_stringify_keys), },
headers: { headers: {
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => 'DocuSeal.com Webhook' 'User-Agent' => 'DocuSeal.com Webhook'
@@ -39,11 +39,11 @@ RSpec.describe SendTemplateCreatedWebhookRequestJob do
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id) described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id)
expect(WebMock).to have_requested(:post, webhook_url.url).with( expect(WebMock).to have_requested(:post, webhook_url.url).with(
body: replace_timestamps({ body: {
'event_type' => 'template.created', 'event_type' => 'template.created',
'timestamp' => Time.current, 'timestamp' => /.*/,
'data' => Templates::SerializeForApi.call(template.reload) 'data' => JSON.parse(Templates::SerializeForApi.call(template.reload).to_json)
}.deep_stringify_keys), },
headers: { headers: {
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => 'DocuSeal.com Webhook', 'User-Agent' => 'DocuSeal.com Webhook',
@@ -22,11 +22,11 @@ RSpec.describe SendTemplateUpdatedWebhookRequestJob do
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id) described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id)
expect(WebMock).to have_requested(:post, webhook_url.url).with( expect(WebMock).to have_requested(:post, webhook_url.url).with(
body: replace_timestamps({ body: {
'event_type' => 'template.updated', 'event_type' => 'template.updated',
'timestamp' => Time.current, 'timestamp' => /.*/,
'data' => Templates::SerializeForApi.call(template.reload) 'data' => JSON.parse(Templates::SerializeForApi.call(template.reload).to_json)
}.deep_stringify_keys), },
headers: { headers: {
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => 'DocuSeal.com Webhook' 'User-Agent' => 'DocuSeal.com Webhook'
@@ -39,11 +39,11 @@ RSpec.describe SendTemplateUpdatedWebhookRequestJob do
described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id) described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id)
expect(WebMock).to have_requested(:post, webhook_url.url).with( expect(WebMock).to have_requested(:post, webhook_url.url).with(
body: replace_timestamps({ body: {
'event_type' => 'template.updated', 'event_type' => 'template.updated',
'timestamp' => Time.current, 'timestamp' => /.*/,
'data' => Templates::SerializeForApi.call(template.reload) 'data' => JSON.parse(Templates::SerializeForApi.call(template.reload).to_json)
}.deep_stringify_keys), },
headers: { headers: {
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => 'DocuSeal.com Webhook', 'User-Agent' => 'DocuSeal.com Webhook',
+3 -15
View File
@@ -10,6 +10,7 @@ require 'capybara/cuprite'
require 'capybara/rspec' require 'capybara/rspec'
require 'webmock/rspec' require 'webmock/rspec'
require 'sidekiq/testing' require 'sidekiq/testing'
require 'signing_form_helper'
Sidekiq::Testing.fake! Sidekiq::Testing.fake!
@@ -50,6 +51,7 @@ RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods config.include FactoryBot::Syntax::Methods
config.include Devise::Test::IntegrationHelpers config.include Devise::Test::IntegrationHelpers
config.include SigningFormHelper
config.before(:each, type: :system) do config.before(:each, type: :system) do
if ENV['HEADLESS'] == 'false' if ENV['HEADLESS'] == 'false'
@@ -68,18 +70,4 @@ RSpec.configure do |config|
end end
end end
def replace_timestamps(data, replace = /.*/) ActiveSupport.run_load_hooks(:rails_specs, self)
timestamp_fields = %w[created_at updated_at completed_at sent_at opened_at timestamp]
data.each do |key, value|
if timestamp_fields.include?(key) && (value.is_a?(String) || value.is_a?(Time))
data[key] = replace
elsif value.is_a?(Hash)
replace_timestamps(value)
elsif value.is_a?(Array)
value.each { |item| replace_timestamps(item) if item.is_a?(Hash) }
end
end
data
end
+1 -27
View File
@@ -176,33 +176,7 @@ describe 'Templates API', type: :request do
id: template.id, id: template.id,
slug: template.slug, slug: template.slug,
name: template.name, name: template.name,
fields: [ fields: template.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' => []
}
],
submitters: [ submitters: [
{ {
name: 'First Party', name: 'First Party',
+34
View File
@@ -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
Loaded 100 of 105 files, more files were not shown because too many files have changed in this diff. Show more