Compare commits

...
44 Commits
Author SHA1 Message Date
Alex Turchyn c5178b96d3 Merge from docusealco/wip 2024-12-02 14:30:33 +02:00
Pete Matsyburka 39a2159196 fix values hash 2024-12-01 11:41:19 +02:00
Pete Matsyburka 5724159b7b verification 2024-12-01 11:23:51 +02:00
Pete Matsyburka 05b7cc9fa6 fix link 2024-11-29 22:03:09 +02:00
Pete Matsyburka 18b13b5b1e adjust policy links 2024-11-29 19:49:44 +02:00
Alex Turchyn d25555b9b0 add policy links to personalization settings 2024-11-29 13:52:47 +02:00
Pete Matsyburka eec266ee83 fix archived false api 2024-11-29 13:30:33 +02:00
Alex Turchyn 75683631f0 add logical 'or' to field conditions 2024-11-29 13:28:11 +02:00
Pete Matsyburka 36c64d1de4 fix counters 2024-11-28 14:57:20 +02:00
Pete Matsyburka 2552b24f21 add documents copy attachment toggle 2024-11-27 21:59:45 +02:00
Alex Turchyn e6ca6dfd85 fix missing file format 2024-11-25 19:40:21 +02:00
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
99 changed files with 2792 additions and 425 deletions

No files matched your search

+2 -2
View File
@@ -60,10 +60,10 @@ RSpec/NestedGroups:
Max: 6
RSpec/MultipleExpectations:
Max: 20
Max: 25
RSpec/ExampleLength:
Max: 40
Max: 50
RSpec/MultipleMemoizedHelpers:
Max: 9
@@ -17,7 +17,8 @@ class AccountConfigsController < ApplicationController
AccountConfig::FLATTEN_RESULT_PDF_KEY,
AccountConfig::WITH_SIGNATURE_ID,
AccountConfig::COMBINE_PDF_RESULT_KEY,
AccountConfig::REQUIRE_SIGNING_REASON_KEY
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY
].freeze
InvalidKey = Class.new(StandardError)
+2 -2
View File
@@ -87,7 +87,7 @@ module Api
def filter_templates(templates, params)
templates = Templates.search(templates, params[:q])
templates = params[:archived] ? templates.archived : templates.active
templates = params[:archived].in?(['true', true]) ? templates.archived : templates.active
templates = templates.where(external_id: params[:application_key]) if params[:application_key].present?
templates = templates.where(external_id: params[:external_id]) if params[:external_id].present?
templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present?
@@ -105,7 +105,7 @@ module Api
:required, :readonly, :default_value,
:title, :description,
{ preferences: {},
conditions: [%i[field_uuid value action]],
conditions: [%i[field_uuid value action operation]],
options: [%i[value uuid]],
validation: %i[message pattern],
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
@@ -15,6 +15,7 @@ class ApplicationController < ActionController::Base
helper_method :button_title,
:current_account,
:form_link_host,
:svg_icon
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
@@ -105,6 +106,10 @@ class ApplicationController < ActionController::Base
render_to_string(partial: "icons/#{icon_name}", locals: { class: })
end
def form_link_host
Docuseal.default_url_options[:host]
end
def maybe_redirect_com
return if request.domain != 'docuseal.co'
@@ -11,6 +11,8 @@ class EsignSettingsController < ApplicationController
end
end
prepend_before_action :maybe_redirect_com, only: %i[show]
before_action :load_encrypted_config
authorize_resource :encrypted_config, parent: false, only: %i[new create]
authorize_resource :encrypted_config, only: %i[update destroy show]
@@ -6,7 +6,8 @@ class PersonalizationSettingsController < ApplicationController
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY,
AccountConfig::FORM_COMPLETED_MESSAGE_KEY
AccountConfig::FORM_COMPLETED_MESSAGE_KEY,
*(Docuseal.multitenant? ? [] : [AccountConfig::POLICY_LINKS_KEY])
].freeze
InvalidKey = Class.new(StandardError)
@@ -9,7 +9,14 @@ class SubmissionsArchivedController < ApplicationController
.or(@submissions.where.not(templates: { archived_at: nil }))
.preload(:created_by_user, template: :author)
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
@submissions = Submissions::Filter.call(@submissions, current_user, params)
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
else
@submissions.order(id: :desc)
end
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events))
end
end
@@ -6,6 +6,8 @@ class SubmissionsController < ApplicationController
load_and_authorize_resource :submission, only: %i[show destroy]
prepend_before_action :maybe_redirect_com, only: %i[show]
def show
@submission = Submissions.preload_with_pages(@submission)
@@ -11,10 +11,17 @@ class SubmissionsDashboardController < ApplicationController
.preload(:created_by_user, template: :author)
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
@submissions = Submissions::Filter.call(@submissions, current_user, params)
@submissions = @submissions.pending if params[:status] == 'pending'
@submissions = @submissions.completed if params[:status] == 'completed'
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events).order(id: :desc))
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
else
@submissions.order(id: :desc)
end
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events))
end
end
@@ -53,8 +53,15 @@ class SubmissionsDownloadController < ApplicationController
end
def build_urls(submitter)
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
Submitters.select_attachments_for_download(submitter).map do |attachment|
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
ActiveStorage::Blob.proxy_url(
attachment.blob,
expires_at: FILES_TTL.from_now.to_i,
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
)
end
end
@@ -65,6 +72,13 @@ class SubmissionsDownloadController < ApplicationController
attachment = submitter.submission.combined_document_attachment
attachment ||= Submissions::GenerateCombinedAttachment.call(submitter)
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
ActiveStorage::Blob.proxy_url(
attachment.blob,
expires_at: FILES_TTL.from_now.to_i,
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
)
end
end
@@ -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_authorization_check
before_action :maybe_redirect_com, only: %i[show completed]
prepend_before_action :maybe_redirect_com, only: %i[show completed]
TTL = 40.minutes
@@ -20,7 +20,7 @@ class SubmissionsPreviewController < ApplicationController
@submission ||= Submission.find_by!(slug: params[:slug])
if !@submission.submitters.all?(&:completed_at?) && current_user.blank?
if @submission.account.archived_at? || (!@submission.submitters.all?(&:completed_at?) && current_user.blank?)
raise ActionController::RoutingError, I18n.t('not_found')
end
+11 -5
View File
@@ -7,8 +7,6 @@ class SubmitFormController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
before_action :maybe_redirect_com, only: %i[show completed]
CONFIG_KEYS = [].freeze
def show
@@ -17,7 +15,9 @@ class SubmitFormController < ApplicationController
submission = @submitter.submission
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
return render :archived if submission.template.archived_at? || submission.archived_at?
return render :archived if submission.template.archived_at? ||
submission.archived_at? ||
@submitter.account.archived_at?
return render :expired if submission.expired?
return render :declined if @submitter.declined_at?
return render :awaiting if submission.template.preferences['submitters_order'] == 'preserved' &&
@@ -27,8 +27,7 @@ class SubmitFormController < ApplicationController
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
@attachments_index = ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments)
.preload(:blob).index_by(&:uuid)
@attachments_index = build_attachments_index(submission)
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
@@ -80,4 +79,11 @@ class SubmitFormController < ApplicationController
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
@@ -6,7 +6,7 @@ class SubmittersSendEmailController < ApplicationController
def create
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
event_type: 'send_email',
created_at: 24.hours.ago..Time.current)
created_at: 10.hours.ago..Time.current)
Rollbar.warning("Already sent: #{@submitter.id}") if defined?(Rollbar)
return redirect_back(fallback_location: submission_path(@submitter.submission),
@@ -7,8 +7,15 @@ class TemplatesArchivedSubmissionsController < ApplicationController
def index
@submissions = @submissions.where.not(archived_at: nil)
@submissions = Submissions.search(@submissions, params[:q], search_values: true)
@submissions = Submissions::Filter.call(@submissions, current_user, params)
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
else
@submissions.order(id: :desc)
end
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events))
rescue ActiveRecord::RecordNotFound
redirect_to root_path
end
+9 -2
View File
@@ -9,13 +9,20 @@ class TemplatesController < ApplicationController
submissions = @template.submissions.accessible_by(current_ability)
submissions = submissions.active if @template.archived_at.blank?
submissions = Submissions.search(submissions, params[:q], search_values: true)
submissions = Submissions::Filter.call(submissions, current_user, params)
@base_submissions = submissions
submissions = submissions.pending if params[:status] == 'pending'
submissions = submissions.completed if params[:status] == 'completed'
@pagy, @submissions = pagy(submissions.preload(submitters: :start_form_submission_events).order(id: :desc))
submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
else
submissions.order(id: :desc)
end
@pagy, @submissions = pagy(submissions.preload(submitters: :start_form_submission_events))
rescue ActiveRecord::RecordNotFound
redirect_to root_path
end
@@ -108,7 +115,7 @@ class TemplatesController < ApplicationController
:required, :readonly, :default_value,
:title, :description,
{ preferences: {},
conditions: [%i[field_uuid value action]],
conditions: [%i[field_uuid value action operation]],
options: [%i[value uuid]],
validation: %i[message pattern],
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
@@ -22,6 +22,7 @@ class TemplatesPreferencesController < ApplicationController
preferences: %i[bcc_completed request_email_subject request_email_body
documents_copy_email_subject documents_copy_email_body
documents_copy_email_enabled documents_copy_email_attach_audit
documents_copy_email_attach_documents
completed_notification_email_attach_documents
completed_redirect_url
submitters_order
@@ -55,11 +55,12 @@ class TemplatesUploadsController < ApplicationController
tempfile.write(DownloadUtils.call(params[:url]).body)
tempfile.rewind
filename = URI.decode_www_form_component(params[:filename]) if params[:filename].present?
filename ||= File.basename(URI.decode_www_form_component(params[:url]))
file = ActionDispatch::Http::UploadedFile.new(
tempfile:,
filename: File.basename(
URI.decode_www_form_component(params[:filename].presence || params[:url]), '.*'
),
filename:,
type: Marcel::MimeType.for(tempfile)
)
+3
View File
@@ -30,6 +30,7 @@ import ToggleAttribute from './elements/toggle_attribute'
import LinkedInput from './elements/linked_input'
import CheckboxGroup from './elements/checkbox_group'
import MaskedInput from './elements/masked_input'
import SetDateButton from './elements/set_date_button'
import * as TurboInstantClick from './lib/turbo_instant_click'
@@ -97,6 +98,7 @@ safeRegisterElement('toggle-attribute', ToggleAttribute)
safeRegisterElement('linked-input', LinkedInput)
safeRegisterElement('checkbox-group', CheckboxGroup)
safeRegisterElement('masked-input', MaskedInput)
safeRegisterElement('set-date-button', SetDateButton)
safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () {
@@ -111,6 +113,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
backgroundColor: '#faf7f5',
locale: this.dataset.locale,
withPhone: this.dataset.withPhone === 'true',
withVerification: ['true', 'false'].includes(this.dataset.withVerification) ? this.dataset.withVerification === 'true' : null,
withLogo: this.dataset.withLogo !== 'false',
editable: this.dataset.editable !== 'false',
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
+17
View File
@@ -97,6 +97,23 @@ button[disabled] .enabled {
bottom: auto;
}
.tooltip-bottom-start:before {
transform: translateX(-31%);
top: var(--tooltip-offset);
left: 100%;
right: auto;
bottom: auto;
}
.tooltip-bottom-start:after {
transform: translateX(-25%);
border-color: transparent transparent var(--tooltip-color) transparent;
top: var(--tooltip-tail-offset);
left: 50%;
right: auto;
bottom: auto;
}
.autocomplete {
background: white;
z-index: 1000;
@@ -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)
}
}
+5 -3
View File
@@ -209,7 +209,7 @@
</template>
<script>
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1 } from '@tabler/icons-vue'
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconId } from '@tabler/icons-vue'
export default {
name: 'FieldArea',
@@ -311,7 +311,8 @@ export default {
cells: this.t('cells'),
stamp: this.t('stamp'),
payment: this.t('payment'),
phone: this.t('phone')
phone: this.t('phone'),
verification: this.t('verify_id')
}
},
alignClasses () {
@@ -340,7 +341,8 @@ export default {
cells: IconColumns3,
multiple: IconChecks,
phone: IconPhoneCheck,
payment: IconCreditCard
payment: IconCreditCard,
verification: IconId
}
},
image () {
@@ -37,6 +37,7 @@
<AppearsOn :field="field" />
<div class="text-center">
<input
:id="field.uuid"
ref="input"
v-model="value"
class="base-input !text-2xl text-center w-full"
@@ -1,5 +1,6 @@
<template>
<div
id="dropzone"
class="flex h-32 w-full"
@dragover.prevent
@drop.prevent="onDropFiles"
+79 -29
View File
@@ -11,7 +11,7 @@
:with-label="!isAnonymousChecboxes && showFieldNames"
:current-step="currentStepFields"
: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
:steps="readonlyConditionalFields.map((e) => [e])"
@@ -448,9 +448,22 @@
@focus="scrollIntoField(currentField)"
@submit="!isSubmitting && submitStep()"
/>
<VerificationStep
v-else-if="currentField.type === 'verification'"
ref="currentStep"
:key="currentField.uuid"
:locale="language?.toLowerCase() || browserLanguage"
:submitter="submitter"
:empty-value-required-step="emptyValueRequiredStep"
:field="currentField"
:submitter-slug="submitterSlug"
:values="values"
@focus="scrollIntoField(currentField)"
@submit="!isSubmitting && submitStep()"
/>
</div>
<div
v-if="currentField.type !== 'payment' || submittedValues[currentField.uuid]"
v-if="(currentField.type !== 'payment' && currentField.type !== 'verification') || submittedValues[currentField.uuid]"
:class="currentField.type === 'signature' ? 'mt-2' : 'mt-6 md:mt-8'"
>
<button
@@ -537,6 +550,7 @@ import AttachmentStep from './attachment_step'
import MultiSelectStep from './multi_select_step'
import PhoneStep from './phone_step'
import PaymentStep from './payment_step'
import VerificationStep from './verification_step'
import TextStep from './text_step'
import NumberStep from './number_step'
import DateStep from './date_step'
@@ -579,6 +593,7 @@ export default {
IconWritingSign,
AttachmentStep,
InitialsStep,
VerificationStep,
InviteForm,
MultiSelectStep,
IconInnerShadowTop,
@@ -908,7 +923,23 @@ export default {
return this.fields.filter((f) => f.readonly && f.conditions?.length && this.checkFieldConditions(f))
},
stepFields () {
return this.fields.filter((f) => !f.readonly).reduce((acc, f) => {
const verificationFields = []
const sortedFields = this.fields.reduce((acc, f) => {
if (f.type === 'verification') {
verificationFields.push(f)
} else if (!f.readonly) {
acc.push(f)
}
return acc
}, [])
if (verificationFields.length) {
sortedFields.push(verificationFields.pop())
}
return sortedFields.reduce((acc, f) => {
const prevStep = acc[acc.length - 1]
if (this.checkFieldConditions(f)) {
@@ -1027,31 +1058,46 @@ export default {
},
checkFieldConditions (field) {
if (field.conditions?.length) {
return field.conditions.reduce((acc, c) => {
const field = this.fieldsUuidIndex[c.field_uuid]
if (['not_empty', 'checked', 'equal', 'contains'].includes(c.action) && field && !this.checkFieldConditions(field)) {
return false
}
if (['empty', 'unchecked'].includes(c.action)) {
return acc && isEmpty(this.values[c.field_uuid])
} else if (['not_empty', 'checked'].includes(c.action)) {
return acc && !isEmpty(this.values[c.field_uuid])
} else if (['equal', 'contains'].includes(c.action) && field) {
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)))
} else if (['not_equal', 'does_not_contain'].includes(c.action) && field) {
const option = field.options.find((o) => o.uuid === c.value)
const values = [this.values[c.field_uuid]].flat()
return acc && !values.includes(this.optionValue(option, field.options.indexOf(option)))
const result = field.conditions.reduce((acc, cond) => {
if (cond.operation === 'or') {
acc.push(acc.pop() || this.checkFieldCondition(cond))
} else {
return acc
acc.push(this.checkFieldCondition(cond))
}
}, true)
return acc
}, [])
return !result.includes(false)
} else {
return true
}
},
checkFieldCondition (condition) {
const field = this.fieldsUuidIndex[condition.field_uuid]
if (['not_empty', 'checked', 'equal', 'contains'].includes(condition.action) && field && !this.checkFieldConditions(field)) {
return false
}
if (['empty', 'unchecked'].includes(condition.action)) {
return isEmpty(this.values[condition.field_uuid])
} else if (['not_empty', 'checked'].includes(condition.action)) {
return !isEmpty(this.values[condition.field_uuid])
} else if (['equal', 'contains'].includes(condition.action) && field) {
if (field.options) {
const option = field.options.find((o) => o.uuid === condition.value)
const values = [this.values[condition.field_uuid]].flat()
return values.includes(this.optionValue(option, field.options.indexOf(option)))
} else {
return [this.values[condition.field_uuid]].flat().includes(condition.value)
}
} else if (['not_equal', 'does_not_contain'].includes(condition.action) && field) {
const option = field.options.find((o) => o.uuid === condition.value)
const values = [this.values[condition.field_uuid]].flat()
return !values.includes(this.optionValue(option, field.options.indexOf(option)))
} else {
return true
}
@@ -1209,7 +1255,7 @@ export default {
const submitStep = this.currentStep
const stepPromise = ['signature', 'phone', 'initials', 'payment'].includes(this.currentField.type)
const stepPromise = ['signature', 'phone', 'initials', 'payment', 'verification'].includes(this.currentField.type)
? this.$refs.currentStep.submit
: () => Promise.resolve({})
@@ -1243,9 +1289,13 @@ export default {
if (response.status === 422 || response.status === 500) {
const data = await response.json()
const i18nError = data.error ? this.t(data.error.replace(/\s+/g, '_').toLowerCase()) : ''
if (data.error) {
const i18nKey = data.error.replace(/\s+/g, '_').toLowerCase()
alert(i18nError !== data.error ? i18nError : (data.error || this.t('value_is_invalid')))
alert(this.t(i18nKey) !== i18nKey ? this.t(i18nKey) : data.error)
} else {
alert(this.t('value_is_invalid'))
}
return Promise.reject(new Error(data.error))
}
+78 -26
View File
@@ -1,4 +1,7 @@
const en = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.',
verify_id: 'Verify ID',
identity_verification: 'Identity verification',
complete: 'Complete',
fill_all_required_fields_to_complete: 'Fill all required fields to complete',
sign_and_complete: 'Sign and Complete',
@@ -67,7 +70,7 @@ const en = {
documents_have_been_signed: 'Documents have been signed!',
create_a_free_account: 'Create a Free Account',
powered_by: 'Powered by',
please_check_the_box_to_continue: 'Please check the box to continue',
please_check_the_box_to_continue: 'Please check the box to continue.',
open_source_documents_software: 'open source documents software',
verified_phone_number: 'Verify Phone Number',
use_international_format: 'Use international format: +1xxx',
@@ -89,10 +92,14 @@ const en = {
pay_with_strip: 'Pay with Stripe',
reupload: 'Reupload',
upload: 'Upload',
files: 'Files'
files: 'Files',
signature_is_too_small_please_redraw: 'Signature is too small. Please redraw.'
}
const es = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete todos los campos requeridos para continuar con la verificación de identidad.',
verify_id: 'Verificar ID',
identity_verification: 'Verificación de identidad',
complete: 'Completar',
fill_all_required_fields_to_complete: 'Complete todos los campos requeridos para finalizar',
sign_and_complete: 'Firmar y Completar',
@@ -160,7 +167,7 @@ const es = {
documents_have_been_signed: '¡Los documentos han sido firmados!',
create_a_free_account: 'Crear una Cuenta Gratuita',
powered_by: 'Desarrollado por',
please_check_the_box_to_continue: 'Por favor marque la casilla para continuar',
please_check_the_box_to_continue: 'Por favor marque la casilla para continuar.',
open_source_documents_software: 'software de documentos de código abierto',
verified_phone_number: 'Verificar número de teléfono',
use_international_format: 'Usar formato internacional: +1xxx',
@@ -182,10 +189,14 @@ const es = {
pay_with_strip: 'Pagar con Stripe',
reupload: 'Volver a subir',
upload: 'Subir',
files: 'Archivos'
files: 'Archivos',
signature_is_too_small_please_redraw: 'La firma es demasiado pequeña. Por favor, dibújala de nuevo.'
}
const it = {
complete_all_required_fields_to_proceed_with_identity_verification: "Compila tutti i campi obbligatori per procedere con la verifica dell'identità.",
verify_id: 'Verifica ID',
identity_verification: "Verifica dell'identità",
complete: 'Completa',
fill_all_required_fields_to_complete: 'Compila tutti i campi obbligatori per completare',
sign_and_complete: 'Firma e Completa',
@@ -253,7 +264,7 @@ const it = {
documents_have_been_signed: 'I documenti sono stati firmati!',
create_a_free_account: 'Crea un Account Gratuito',
powered_by: 'Desarrollado por',
please_check_the_box_to_continue: 'Si prega di spuntare la casella per continuare',
please_check_the_box_to_continue: 'Si prega di spuntare la casella per continuare.',
open_source_documents_software: 'software di documenti open source',
verified_phone_number: 'Verifica numero di telefono',
use_international_format: 'Usa formato internazionale: +1xxx',
@@ -275,10 +286,14 @@ const it = {
pay_with_strip: 'Paga con Stripe',
reupload: 'Ricarica',
upload: 'Carica',
files: 'File'
files: 'File',
signature_is_too_small_please_redraw: 'La firma è troppo piccola. Ridisegnala per favore.'
}
const de = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Vervollständigen Sie alle erforderlichen Felder, um mit der Identitätsverifizierung fortzufahren.',
verify_id: 'ID überprüfen',
identity_verification: 'Identitätsüberprüfung',
complete: 'Abschließen',
fill_all_required_fields_to_complete: 'Alle erforderlichen Felder ausfüllen, um abzuschließen',
sign_and_complete: 'Signieren und Abschließen',
@@ -346,7 +361,7 @@ const de = {
documents_have_been_signed: 'Dokumente wurden unterschrieben!',
create_a_free_account: 'Kostenloses Konto erstellen',
powered_by: 'Bereitgestellt von',
please_check_the_box_to_continue: 'Bitte setzen Sie das Häkchen, um fortzufahren',
please_check_the_box_to_continue: 'Bitte setzen Sie das Häkchen, um fortzufahren.',
open_source_documents_software: 'Open-Source-Dokumentensoftware',
verified_phone_number: 'Telefonnummer überprüfen',
use_international_format: 'Internationales Format verwenden: +1xxx',
@@ -368,10 +383,14 @@ const de = {
pay_with_strip: 'Mit Stripe bezahlen',
reupload: 'Erneut hochladen',
upload: 'Hochladen',
files: 'Dateien'
files: 'Dateien',
signature_is_too_small_please_redraw: 'Die Unterschrift ist zu klein. Bitte erneut zeichnen.'
}
const fr = {
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour continuer la vérification de l'identité.",
verif_id: "Vérification de l'ID",
verif_identite: "Vérification de l'identité",
complete: 'Terminer',
fill_all_required_fields_to_complete: 'Veuillez remplir tous les champs obligatoires pour compléter',
sign_and_complete: 'Signer et Terminer',
@@ -439,7 +458,7 @@ const fr = {
documents_have_been_signed: 'Les documents ont été signés!',
create_a_free_account: 'Créer un Compte Gratuit',
powered_by: 'Propulsé par',
please_check_the_box_to_continue: 'Veuillez cocher la case pour continuer',
please_check_the_box_to_continue: 'Veuillez cocher la case pour continuer.',
open_source_documents_software: 'logiciel de documents open source',
verified_phone_number: 'Vérifier le numéro de téléphone',
use_international_format: 'Utiliser le format international : +1xxx',
@@ -461,10 +480,14 @@ const fr = {
pay_with_strip: 'Paiement avec Stripe',
reupload: 'Recharger',
upload: 'Télécharger',
files: 'Fichiers'
files: 'Fichiers',
signature_is_too_small_please_redraw: 'La signature est trop petite. Veuillez la redessiner.'
}
const pl = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Uzupełnij wszystkie wymagane pola, aby kontynuować weryfikację tożsamości.',
verify_id: 'Zweryfikuj ID',
identity_verification: 'Weryfikacja tożsamości',
complete: 'Zakończ',
fill_all_required_fields_to_complete: 'Uzupełnij wszystkie wymagane pola, aby zakończyć',
sign_and_complete: 'Podpisz i zakończ',
@@ -532,7 +555,7 @@ const pl = {
documents_have_been_signed: 'Dokumenty zostały podpisane!',
create_a_free_account: 'Utwórz darmowe konto',
powered_by: 'Napędzany przez',
please_check_the_box_to_continue: 'Proszę zaznaczyć pole, aby kontynuować',
please_check_the_box_to_continue: 'Proszę zaznaczyć pole, aby kontynuować.',
open_source_documents_software: 'oprogramowanie do dokumentów open source',
verified_phone_number: 'Zweryfikuj numer telefonu',
use_international_format: 'Użyj międzynarodowego formatu: +1xxx',
@@ -554,10 +577,14 @@ const pl = {
pay_with_strip: 'Płatność za pomocą Stripe',
reupload: 'Ponowne przesłanie',
upload: 'Przesyłanie',
files: 'Pliki'
files: 'Pliki',
signature_is_too_small_please_redraw: 'Podpis jest zbyt mały. Proszę narysować go ponownie.'
}
const uk = {
complete_all_required_fields_to_proceed_with_identity_verification: "Заповніть всі обов'язкові поля, щоб продовжити перевірку особи.",
verify_id: 'Підтвердження ідентичності',
identity_verification: 'Ідентифікація особи',
complete: 'Завершити',
fill_all_required_fields_to_complete: "Заповніть всі обов'язкові поля для завершення",
sign_and_complete: 'Підписати і завершити',
@@ -625,7 +652,7 @@ const uk = {
documents_have_been_signed: 'Документи були підписані!',
create_a_free_account: 'Створити безкоштовний обліковий запис',
powered_by: 'Працює на базі',
please_check_the_box_to_continue: 'Будь ласка, позначте прапорець, щоб продовжити',
please_check_the_box_to_continue: 'Будь ласка, позначте прапорець, щоб продовжити.',
open_source_documents_software: 'відкритий програмний засіб для документів',
verified_phone_number: 'Підтвердіть номер телефону',
use_international_format: 'Використовуйте міжнародний формат: +1xxx',
@@ -647,10 +674,14 @@ const uk = {
pay_with_strip: 'Сплатити за допомогою Stripe',
reupload: 'Перезавантажити',
upload: 'Завантажити',
files: 'Файли'
files: 'Файли',
signature_is_too_small_please_redraw: 'Підпис занадто малий. Будь ласка, перемалюйте його.'
}
const cs = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Vyplňte všechna povinná pole, abyste mohli pokračovat v ověření identity.',
verify_id: 'Ověřit ID',
identity_verification: 'Ověření identity',
complete: 'Dokončit',
fill_all_required_fields_to_complete: 'Please complete all mandatory fields',
sign_and_complete: 'Podepsat a dokončit',
@@ -718,7 +749,7 @@ const cs = {
documents_have_been_signed: 'Dokumenty byly podepsány!',
create_a_free_account: 'Vytvořit bezplatný účet',
powered_by: 'Poháněno',
please_check_the_box_to_continue: 'Prosím, zaškrtněte políčko pro pokračování',
please_check_the_box_to_continue: 'Prosím, zaškrtněte políčko pro pokračování.',
open_source_documents_software: 'open source software pro dokumenty',
verified_phone_number: 'Ověřte telefonní číslo',
use_international_format: 'Použijte mezinárodní formát: +1xxx',
@@ -740,10 +771,14 @@ const cs = {
pay_with_strip: 'Zaplacení přes Stripe',
reupload: 'Znovu nahrát',
upload: 'Nahrát',
files: 'Soubory'
files: 'Soubory',
signature_is_too_small_please_redraw: 'Podpis je příliš malý. Prosím, překreslete ho.'
}
const pt = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Preencha todos os campos obrigatórios para prosseguir com a verificação de identidade.',
verify_id: 'Verificar ID',
identity_verification: 'Verificação de identidade',
complete: 'Completar',
preencher_todos_os_campos_obrigatórios_para_concluir: 'Preencher todos os campos obrigatórios para concluir',
sign_and_complete: 'Assinar e Completar',
@@ -811,7 +846,7 @@ const pt = {
documents_have_been_signed: 'Os documentos foram assinados!',
create_a_free_account: 'Criar uma Conta Gratuita',
powered_by: 'Desenvolvido por',
please_check_the_box_to_continue: 'Por favor, marque a caixa para continuar',
please_check_the_box_to_continue: 'Por favor, marque a caixa para continuar.',
open_source_documents_software: 'software de documentos de código aberto',
verified_phone_number: 'Verificar Número de Telefone',
use_international_format: 'Use formato internacional: +1xxx',
@@ -833,10 +868,14 @@ const pt = {
pay_with_strip: 'Pagar com Stripe',
reupload: 'Reenviar',
upload: 'Carregar',
files: 'Arquivos'
files: 'Arquivos',
signature_is_too_small_please_redraw: 'A assinatura é muito pequena. Por favor, redesenhe-a.'
}
const he = {
complete_all_required_fields_to_proceed_with_identity_verification: 'מלא את כל השדות הנדרשים כדי להמשיך עם אימות זהות.',
verify_id: 'אמת מזהה',
identity_verification: 'אימות זהות',
complete: 'השלם',
fill_all_required_fields_to_complete: 'נא למלא את כל השדות הנדרשים להשלמה',
sign_and_complete: 'חתום והשלם',
@@ -905,7 +944,7 @@ const he = {
documents_have_been_signed: 'המסמכים נחתמו!',
create_a_free_account: 'צור חשבון חינם',
powered_by: 'מופעל על ידי',
please_check_the_box_to_continue: 'אנא סמן את התיבה כדי להמשיך',
please_check_the_box_to_continue: 'אנא סמן את התיבה כדי להמשיך.',
open_source_documents_software: 'תוכנה פתוחה למסמכים',
verified_phone_number: 'אימות מספר טלפון',
use_international_format: 'השתמש בפורמט בינלאומי: +1xxx',
@@ -927,10 +966,14 @@ const he = {
pay_with_strip: 'שלם עם סטרייפ',
reupload: 'העלה שוב',
upload: 'העלאה',
files: 'קבצים'
files: 'קבצים',
signature_is_too_small_please_redraw: 'החתימה קטנה מדי. אנא צייר מחדש.'
}
const nl = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Vul alle verplichte velden in om door te gaan met de identiteitsverificatie.',
verify_id: 'Verifiëren ID',
identity_verification: 'Identiteitsverificatie',
complete: 'Voltooien',
vul_alle_verplichte_velden_in_om_te_voltooien: 'Vul alle verplichte velden in om te voltooien',
sign_and_complete: 'Ondertekenen en voltooien',
@@ -999,7 +1042,7 @@ const nl = {
documents_have_been_signed: 'De documenten zijn ondertekend!',
create_a_free_account: 'Maak een gratis account aan',
powered_by: 'Aangedreven door',
please_check_the_box_to_continue: 'Vink het vakje aan om door te gaan',
please_check_the_box_to_continue: 'Vink het vakje aan om door te gaan.',
open_source_documents_software: 'Open source documenten software',
verified_phone_number: 'Geverifieerd telefoonnummer',
use_international_format: 'Gebruik internationaal formaat: +1xxx',
@@ -1021,10 +1064,14 @@ const nl = {
pay_with_strip: 'Betalen met Stripe',
reupload: 'Opnieuw uploaden',
upload: 'Uploaden',
files: 'Bestanden'
files: 'Bestanden',
signature_is_too_small_please_redraw: 'De handtekening is te klein. Teken deze opnieuw, alstublieft.'
}
const ar = {
complete_all_required_fields_to_proceed_with_identity_verification: 'أكمل جميع الحقول المطلوبة للمتابعة في التحقق من الهوية.',
verify_id: 'تحقق من الهوية',
identity_verification: 'التحقق من الهوية',
complete: 'اكتمال',
fill_all_required_fields_to_complete: 'يرجى ملء جميع الحقول المطلوبة لإكمال',
sign_and_complete: 'التوقيع والاكتمال',
@@ -1092,7 +1139,7 @@ const ar = {
documents_have_been_signed: 'تم توقيع الوثائق!',
create_a_free_account: 'إنشاء حساب مجاني',
powered_by: 'مدعوم من',
please_check_the_box_to_continue: 'الرجاء التحقق من الخانة للمتابعة',
please_check_the_box_to_continue: 'الرجاء التحقق من الخانة للمتابعة.',
open_source_documents_software: 'برنامج وثائق مفتوح المصدر',
verified_phone_number: 'تحقق من رقم الهاتف',
use_international_format: 'استخدم الشكل الدولي: +1xxx',
@@ -1114,10 +1161,14 @@ const ar = {
pay_with_strip: 'الدفع بواسطة Stripe',
reupload: 'إعادة التحميل',
upload: 'تحميل',
files: 'الملفات'
files: 'الملفات',
signature_is_too_small_please_redraw: 'التوقيع صغير جدًا. يرجى إعادة الرسم.'
}
const ko = {
complete_all_required_fields_to_proceed_with_identity_verification: '신원 확인을 진행하려면 모든 필수 필드를 작성하십시오.',
verify_id: '아이디 확인',
identity_verification: '신원 확인',
complete: '완료',
fill_all_required_fields_to_complete: '모든 필수 필드를 작성하여 완료하세요',
sign_and_complete: '서명하고 완료하기',
@@ -1184,7 +1235,7 @@ const ko = {
documents_have_been_signed: '문서가 서명되었습니다!',
create_a_free_account: '무료 계정 생성',
powered_by: '구동',
please_check_the_box_to_continue: '계속하려면 확인란을 선택하십시오',
please_check_the_box_to_continue: '계속하려면 확인란을 선택하십시오.',
open_source_documents_software: '오픈 소스 문서 소프트웨어',
verified_phone_number: '전화번호 확인됨',
use_international_format: '국제 포맷 사용: +1xxx',
@@ -1206,7 +1257,8 @@ const ko = {
pay_with_strip: '스트라이프로 결제',
reupload: '다시 업로드',
upload: '업로드',
files: '파일'
files: '파일',
signature_is_too_small_please_redraw: '서명이 너무 작습니다. 다시 그려주세요.'
}
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar, ko }
@@ -1,7 +1,6 @@
<template>
<label
v-if="!modelValue && !sessionId"
:for="field.uuid"
class="label text-2xl mb-2"
>
<MarkdownContent
@@ -65,11 +65,15 @@
v-show="!isCodeSent"
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">
{{ selectedCountry.flag }} +{{ selectedCountry.dial }}
</div>
<select
id="country_code_select"
class="absolute top-0 bottom-0 right-0 left-0 opacity-0 w-full h-full cursor-pointer"
:disabled="!!defaultValue"
@change="onCountrySelect(countries.find((country) => country.flag === $event.target.value))"
+3 -1
View File
@@ -114,7 +114,9 @@ export default {
const area = this.field.areas?.[0]
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 {
return null
}
@@ -0,0 +1,169 @@
<template>
<label
class="label text-2xl mb-2"
>
<MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>{{ field.name || t('identity_verification') }}</template>
</label>
<div
v-if="field.description"
dir="auto"
class="mb-4 px-1"
>
<MarkdownContent :string="field.description" />
</div>
<div
v-if="emptyValueRequiredStep && emptyValueRequiredStep[0] !== field"
class="px-1"
>
{{ t('complete_all_required_fields_to_proceed_with_identity_verification') }}
</div>
<div v-else>
<div
v-if="isLoading"
class="w-full flex space-x-2 justify-center mb-2"
>
<IconInnerShadowTop
width="40"
class="animate-spin h-10"
/>
</div>
<div
ref="widgetContainer"
/>
</div>
</template>
<script>
import MarkdownContent from './markdown_content'
import { IconInnerShadowTop } from '@tabler/icons-vue'
import phoneData from './phone_data'
export default {
name: 'VerificationStep',
components: {
MarkdownContent,
IconInnerShadowTop
},
inject: ['baseUrl', 't'],
props: {
modelValue: {
type: String,
required: false,
default: ''
},
submitter: {
type: Object,
required: true
},
field: {
type: Object,
required: true
},
emptyValueRequiredStep: {
type: Object,
required: false,
default: null
},
values: {
type: Object,
required: true
},
locale: {
type: String,
required: false,
default: 'en'
},
submitterSlug: {
type: String,
required: true
}
},
emits: ['focus', 'submit', 'update:model-value', 'attached'],
data () {
return {
isCreatingCheckout: false,
isMathLoaded: false,
isLoading: false,
eidEasyData: {}
}
},
computed: {
countryCode () {
const browserTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
const browserTz = browserTimeZone.split('/')[1]
const country = phoneData.find(([a, b, c, tz]) => tz.includes(browserTz))
return country[0]
},
browserCountry () {
return (navigator.language || navigator.userLanguage || 'en').split('-')[1]
},
widgetSettings () {
return {
clientId: this.eidEasyData.client_id,
docId: this.eidEasyData.doc_id,
language: this.locale,
countryCode: this.browserCountry,
enabledMethods: {
signature: this.eidEasyData.available_methods
},
selectedMethod: null,
enabledCountries: 'all',
onSuccess: (data) => {
this.$emit('submit')
},
onFail: (error) => {
console.error(error)
}
}
}
},
async mounted () {
this.isLoading = true
Promise.all([
import('@eid-easy/eideasy-widget'),
this.start()
]).finally(() => {
this.isLoading = false
})
},
methods: {
start () {
return fetch(this.baseUrl + `/api/identity_verification/${this.field.uuid}`, {
method: 'PUT',
body: JSON.stringify({
submitter_slug: this.submitterSlug
}),
headers: { 'Content-Type': 'application/json' }
}).then(async (resp) => {
this.eidEasyData = await resp.json()
const eidEasyWidget = document.createElement('eideasy-widget')
for (const key in this.widgetSettings) {
eidEasyWidget[key] = this.widgetSettings[key]
}
this.$refs.widgetContainer.innerHTML = ''
this.$refs.widgetContainer.appendChild(eidEasyWidget)
})
},
async submit () {
return fetch(this.baseUrl + '/api/identity_verification', {
method: 'POST',
body: JSON.stringify({
submitter_slug: this.submitterSlug
}),
headers: { 'Content-Type': 'application/json' }
}).then(async (resp) => {
return resp
})
}
}
}
</script>
+12 -4
View File
@@ -4,7 +4,7 @@
class="mx-auto pl-3 md:pl-4 h-full"
>
<div
v-if="pendingFieldAttachmentUuids.length"
v-if="pendingFieldAttachmentUuids.length && editable"
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">
@@ -455,6 +455,7 @@ export default {
fieldTypes: this.fieldTypes,
backgroundColor: this.backgroundColor,
withPhone: this.withPhone,
withVerification: this.withVerification,
withPayment: this.withPayment,
isPaymentConnected: this.isPaymentConnected,
withFormula: this.withFormula,
@@ -635,6 +636,11 @@ export default {
required: false,
default: false
},
withVerification: {
type: Boolean,
required: false,
default: null
},
withPayment: {
type: Boolean,
required: false,
@@ -1075,7 +1081,7 @@ export default {
} else if (type === 'image') {
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight)
} else if (type === 'signature' || type === 'stamp') {
} else if (type === 'signature' || type === 'stamp' || type === 'verification') {
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight) / 2
} else if (type === 'initials') {
@@ -1239,7 +1245,7 @@ export default {
w: area.maskW / 5 / area.maskW,
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH)
}
} else if (field.type === 'signature' || field.type === 'stamp') {
} else if (field.type === 'signature' || field.type === 'stamp' || field.type === 'verification') {
baseArea = {
w: area.maskW / 5 / area.maskW,
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2
@@ -1342,7 +1348,9 @@ export default {
}
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])
})
@@ -29,15 +29,26 @@
>{{ t('available_in_pro') }}</a>
</div>
<form @submit.prevent="validateSaveAndClose">
<div class="my-4 space-y-5">
<div class="my-4">
<div
v-for="(condition, cindex) in conditions"
:key="cindex"
class="space-y-4 relative"
>
<div
v-if="cindex > 0"
class="divider -mb-2 mx-1"
>
<button
class="btn btn-xs btn-primary w-24"
@click.prevent="condition.operation === 'or' ? delete condition.operation : condition.operation = 'or'"
>
{{ condition.operation === 'or' ? t('or') : t('and') }}
</button>
</div>
<div
v-if="conditions.length > 1"
class="flex justify-between border-b mx-1 -mb-1 pb-1"
class="flex justify-between mx-1"
>
<span class="text-sm">
{{ t('condition') }} {{ cindex + 1 }}
@@ -30,7 +30,7 @@
v-for="(icon, type) in fieldIconsSorted"
:key="type"
>
<li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')">
<li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification')">
<a
href="#"
class="text-sm py-1 px-2"
@@ -51,11 +51,11 @@
</template>
<script>
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading } from '@tabler/icons-vue'
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading, IconId } from '@tabler/icons-vue'
export default {
name: 'FiledTypeDropdown',
inject: ['withPhone', 'withPayment', 't', 'fieldTypes'],
inject: ['withPhone', 'withPayment', 'withVerification', 't', 'fieldTypes'],
props: {
modelValue: {
type: String,
@@ -111,7 +111,8 @@ export default {
cells: this.t('cells'),
stamp: this.t('stamp'),
payment: this.t('payment'),
phone: this.t('phone')
phone: this.t('phone'),
verification: this.t('verify_id')
}
},
fieldLabels () {
@@ -130,7 +131,8 @@ export default {
cells: this.t('cells_field'),
stamp: this.t('stamp_field'),
payment: this.t('payment_field'),
phone: this.t('phone_field')
phone: this.t('phone_field'),
verification: this.t('verify_id')
}
},
fieldIcons () {
@@ -150,7 +152,8 @@ export default {
cells: IconColumns3,
stamp: IconRubberStamp,
payment: IconCreditCard,
phone: IconPhoneCheck
phone: IconPhoneCheck,
verification: IconId
}
},
fieldIconsSorted () {
+29 -3
View File
@@ -111,14 +111,14 @@
:key="type"
>
<button
v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')"
v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification')"
draggable="true"
class="field-type-button group flex items-center justify-center border border-dashed w-full rounded relative"
:style="{ backgroundColor }"
:class="drawFieldType === type ? 'border-base-content/40' : 'border-base-300 hover:border-base-content/20'"
@dragstart="onDragstart({ type: type })"
@dragend="$emit('drag-end')"
@click="['file', 'payment'].includes(type) ? $emit('add-field', type) : $emit('set-draw-type', type)"
@click="['file', 'payment', 'verification'].includes(type) ? $emit('add-field', type) : $emit('set-draw-type', type)"
>
<div
class="flex items-console transition-all cursor-grab h-full absolute left-0"
@@ -160,6 +160,32 @@
</div>
</a>
</div>
<div
v-else-if="withVerification === false && type == 'verification' && (fieldTypes.length === 0 || fieldTypes.includes(type))"
class="tooltip tooltip-bottom flex tooltip-bottom-start"
:data-tip="t('obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more')"
>
<a
href="https://www.docuseal.com/contact"
target="_blank"
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
:style="{ backgroundColor }"
>
<div class="w-0 absolute left-0">
<IconLock
width="18"
height="18"
stroke-width="1.5"
/>
</div>
<div class="flex items-center flex-col px-2 py-2">
<component :is="icon" />
<span class="text-xs mt-1">
{{ fieldNames[type] }}
</span>
</div>
</a>
</div>
</template>
</div>
<div
@@ -197,7 +223,7 @@ export default {
IconDrag,
IconLock
},
inject: ['save', 'backgroundColor', 'withPhone', 'withPayment', 't', 'fieldsDragFieldRef'],
inject: ['save', 'backgroundColor', 'withPhone', 'withVerification', 'withPayment', 't', 'fieldsDragFieldRef'],
props: {
fields: {
type: Array,
+30 -12
View File
@@ -1,4 +1,6 @@
const en = {
verify_id: 'Verify ID',
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Obtain qualified electronic signature (QeS) with the trusted provider. Click to learn more.',
editable: 'Editable',
recurrent: 'Recurrent',
one_off: 'One-off',
@@ -126,7 +128,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.',
available_only_in_pro: 'Available only in Pro',
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.',
draw_field: 'Draw {field} Field',
replace: 'Replace',
@@ -149,10 +150,14 @@ const en = {
preferences: 'Preferences',
available_in_pro: 'Available in Pro',
some_fields_are_missing_in_the_formula: 'Some fields are missing in the formula.',
learn_more: 'Learn more'
learn_more: 'Learn more',
and: 'and',
or: 'or'
}
const es = {
verify_id: 'Verificar ID',
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Obtenga una firma electrónica cualificada (QeS) con el proveedor de confianza. Haga clic para obtener más información.',
recurrent: 'Recurrente',
one_off: 'Único',
editable: 'Editable',
@@ -277,7 +282,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.',
available_only_in_pro: 'Disponible solo en Pro',
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.',
draw_field: 'Dibujar campo {field}',
replace: 'Reemplazar',
@@ -303,10 +307,14 @@ const es = {
preferences: 'Preferencias',
available_in_pro: 'Disponible en Pro',
some_fields_are_missing_in_the_formula: 'Faltan algunos campos en la fórmula.',
learn_more: 'Aprende más'
learn_more: 'Aprende más',
and: 'y',
or: 'o'
}
const it = {
verify_id: 'Verifica ID',
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Ottieni una firma elettronica qualificata (QeS) con il fornitore di fiducia. Clicca per saperne di più.',
ricorrente: 'Ricorrente',
una_volta: 'Una volta',
editable: 'Modificabile',
@@ -434,7 +442,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.',
available_only_in_pro: 'Disponibile solo in Pro',
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.',
draw_field: 'Disegna il campo {field}',
replace: 'Sostituisci',
@@ -457,10 +464,14 @@ const it = {
preferences: 'Preferenze',
available_in_pro: 'Disponibile in Pro',
some_fields_are_missing_in_the_formula: 'Alcuni campi mancano nella formula.',
learn_more: 'Scopri di più'
learn_more: 'Scopri di più',
and: 'e',
or: 'o'
}
const pt = {
verify_id: 'Verificar ID',
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Obtenha a assinatura eletrônica qualificada (QeS) com o provedor confiável. Clique para saber mais.',
recurrent: 'Recurrente',
one_off: 'Único',
editable: 'Editável',
@@ -585,7 +596,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.',
available_only_in_pro: 'Disponível apenas no Pro',
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.',
draw_field: 'Desenhar campo {field}',
replace: 'Substituir',
@@ -611,10 +621,14 @@ const pt = {
preferences: 'Preferências',
available_in_pro: 'Disponível no Pro',
some_fields_are_missing_in_the_formula: 'Faltam alguns campos na fórmula.',
learn_more: 'Saiba mais'
learn_more: 'Saiba mais',
and: 'e',
or: 'ou'
}
const fr = {
verify_id: "Vérifier l'ID",
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Obtenez une signature électronique qualifiée (QeS) avec le fournisseur de confiance. Cliquez pour en savoir plus.',
recurrent: 'Récurrent',
one_off: 'Ponctuel',
editable: 'Éditable',
@@ -739,7 +753,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.',
available_only_in_pro: 'Disponible uniquement en Pro',
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.',
draw_field: 'Dessiner le champ {field}',
replace: 'Remplacer',
@@ -765,10 +778,14 @@ const fr = {
preferences: 'Préférences',
available_in_pro: 'Disponible en version Pro',
some_fields_are_missing_in_the_formula: 'Certains champs manquent dans la formule.',
learn_more: 'En savoir plus'
learn_more: 'En savoir plus',
and: 'et',
or: 'ou'
}
const de = {
verify_id: 'ID überprüfen',
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Erhalten Sie eine qualifizierte elektronische Signatur (QeS) beim vertrauenswürdigen Anbieter. Klicken Sie hier, um mehr zu erfahren.',
wiederkehrend: 'Wiederkehrend',
einmalig: 'Einmalig',
editable: 'Bearbeitbar',
@@ -893,7 +910,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.',
available_only_in_pro: 'Nur in Pro verfügbar',
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.',
draw_field: 'Feld {field} zeichnen',
replace: 'Ersetzen',
@@ -919,7 +935,9 @@ const de = {
preferences: 'Einstellungen',
available_in_pro: 'In Pro verfügbar',
some_fields_are_missing_in_the_formula: 'Einige Felder fehlen in der Formel.',
learn_more: 'Erfahren Sie mehr'
learn_more: 'Erfahren Sie mehr',
and: 'und',
or: 'oder'
}
export { en, es, it, pt, fr, de }
@@ -279,7 +279,11 @@ export default {
this.mappings.forEach((mapping) => {
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())) {
submittersIndex[mapping.submitter_uuid][mapping.field_name.toLowerCase()] = row[mapping.column_index]
@@ -296,7 +300,7 @@ export default {
})
if (Object.keys(submittersIndex).length !== 0) {
submissions.push({ submitters: Object.values(submittersIndex) })
submissions.push({ submitters: this.submitters.map((s) => submittersIndex[s.uuid]).filter(Boolean) })
}
})
@@ -55,7 +55,7 @@
v-for="(icon, type) in fieldIconsSorted"
:key="type"
>
<li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')">
<li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification')">
<a
href="#"
class="text-sm py-1 px-2"
@@ -85,7 +85,7 @@ export default {
IconPlus,
IconX
},
inject: ['withPhone', 'withPayment', 'backgroundColor', 't'],
inject: ['withPhone', 'withPayment', 'withVerification', 'backgroundColor', 't'],
props: {
modelValue: {
type: String,
+1 -1
View File
@@ -16,7 +16,7 @@ class ApplicationMailer < ActionMailer::Base
after_action :set_message_uuid
def default_url_options
Docuseal.default_url_options
Docuseal.default_url_options.merge(host: ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host]))
end
def set_message_metadata
+19 -7
View File
@@ -106,8 +106,11 @@ class SubmitterMailer < ApplicationMailer
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
@documents = add_completed_email_attachments!(
submitter, with_audit_log: @submitter.template.preferences['documents_copy_email_attach_audit'] != false &&
add_completed_email_attachments!(
submitter,
with_documents: @submitter.template.preferences['documents_copy_email_attach_documents'] != false &&
(@email_config.nil? || @email_config.value['attach_documents'] != false),
with_audit_log: @submitter.template.preferences['documents_copy_email_attach_audit'] != false &&
(@email_config.nil? || @email_config.value['attach_audit_log'] != false)
)
@@ -152,6 +155,9 @@ class SubmitterMailer < ApplicationMailer
def add_completed_email_attachments!(submitter, with_audit_log: true, with_documents: true)
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
audit_trail_data = nil
@@ -161,9 +167,14 @@ class SubmitterMailer < ApplicationMailer
total_size = audit_trail_data.size
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
file_fields = submitter.submission.template_fields.select { |e| e['type'].in?(%w[file payment]) }
@@ -172,7 +183,7 @@ class SubmitterMailer < ApplicationMailer
storage_attachments =
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
@@ -183,7 +194,7 @@ class SubmitterMailer < ApplicationMailer
user.role == 'integration' ? user.friendly_name.sub(/\+\w+@/, '@') : user.friendly_name
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
storage_attachments.each do |attachment|
@@ -191,7 +202,8 @@ class SubmitterMailer < ApplicationMailer
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
total_size
+2
View File
@@ -43,6 +43,8 @@ class AccountConfig < ApplicationRecord
REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason'
REUSE_SIGNATURE_KEY = 'reuse_signature'
COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key'
DOCUMENT_FILENAME_FORMAT_KEY = 'document_filename_format'
POLICY_LINKS_KEY = 'policy_links'
DEFAULT_VALUES = {
SUBMITTER_INVITATION_EMAIL_KEY => lambda {
+5 -2
View File
@@ -62,8 +62,11 @@ class Submission < ApplicationRecord
through: :template, source: :documents_attachments
scope :active, -> { where(archived_at: nil) }
scope :pending, -> { joins(:submitters).where(submitters: { completed_at: nil }).distinct }
scope :completed, -> { where.not(id: pending.select(:submission_id)) }
scope :pending, -> { joins(:submitters).where(submitters: { completed_at: nil }).group(: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, {
invite: 'invite',
+2
View File
@@ -46,6 +46,8 @@ class SubmissionEvent < ApplicationRecord
click_sms: 'click_sms',
phone_verified: 'phone_verified',
start_form: 'start_form',
start_verification: 'start_verification',
complete_verification: 'complete_verification',
view_form: 'view_form',
invite_party: 'invite_party',
complete_form: 'complete_form',
+1
View File
@@ -53,6 +53,7 @@ class Submitter < ApplicationRecord
has_many_attached :documents
has_many_attached :attachments
has_many_attached :preview_documents
has_many :document_generation_events, dependent: :destroy
has_many :submission_events, dependent: :destroy
+14
View File
@@ -164,6 +164,20 @@
</div>
<% 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>
<% end %>
</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

+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

@@ -8,7 +8,7 @@
<div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<%= f.hidden_field :key %>
<%= f.fields_for :value, Struct.new(:subject, :body, :attach_audit_log).new(*f.object.value.values_at('subject', 'body', 'attach_audit_log')) do |ff| %>
<%= f.fields_for :value, Struct.new(:subject, :body, :attach_audit_log, :attach_documents).new(*f.object.value.values_at('subject', 'body', 'attach_audit_log', 'attach_documents')) do |ff| %>
<div class="form-control">
<%= ff.label :subject, t('subject'), class: 'label' %>
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
@@ -24,7 +24,13 @@
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<div class="flex items-center justify-between py-2.5 mx-1">
<div class="flex items-center justify-between pt-2.5 mx-1">
<span>
<%= t('attach_documents') %>
</span>
<%= ff.check_box :attach_documents, { checked: ff.object.attach_documents != false, class: 'toggle' }, 'true', 'false' %>
</div>
<div class="flex items-center justify-between pb-2.5 mx-1">
<span>
<%= t('attach_audit_log_pdf') %>
</span>
@@ -0,0 +1,23 @@
<% unless Docuseal.multitenant? %>
<div class="collapse collapse-plus bg-base-200 mt-4">
<input type="checkbox">
<div class="collapse-title text-xl font-medium">
<div>
<%= t('policy_links') %>
</div>
</div>
<div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::POLICY_LINKS_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<%= f.hidden_field :key %>
<div class="form-control">
<autoresize-textarea>
<%= f.text_area :value, class: 'base-input w-full py-2 font-mono', placeholder: "#{t('markdown_content_e_g')}\n[#{t('privacy_policy')}](https://example.com/privacy)" %>
</autoresize-textarea>
</div>
<div class="form-control pt-2">
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
</div>
<% end %>
</div>
</div>
<% end %>
@@ -19,6 +19,7 @@
<div class="space-y-4">
<%= render 'form_completed_message_form' %>
<%= render 'form_completed_button_form' %>
<%= render 'form_policy_links_form' %>
</div>
<%= render 'form_customization_settings' %>
</div>
+1 -1
View File
@@ -2,5 +2,5 @@
---
</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>
+5
View File
@@ -2,6 +2,11 @@
<% if params[:status].present? %>
<input name="status" value="<%= params[:status] %>" class="hidden">
<% 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? %>
<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">
+1 -1
View File
@@ -25,7 +25,7 @@
</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| %>
<div dir="auto" class="form-control !mt-0">
<%= f.label :email, t('email'), class: 'label' %>
+2 -2
View File
@@ -2,7 +2,7 @@
<% can_send_emails = Accounts.can_send_emails?(current_account) %>
<div class="flex justify-between items-center">
<%= f.label :send_email, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %>
<%= f.check_box :send_email, id: uuid, class: 'base-checkbox', disabled: !can_send_emails, 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>
<% end %>
<div>
@@ -26,7 +26,7 @@
<%= t('configure_smtp_settings_in_order_to_send_emails_') %>
<br>
<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>
</p>
</div>
+1 -1
View File
@@ -59,7 +59,7 @@
</div>
<% end %>
<% elsif @submission.submitters.to_a.size == 1 && !@submission.expired? && !@submission.submitters.to_a.first.declined_at? && !@submission.archived_at? %>
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: @submission.submitters.to_a.first.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy_share_link'), copied_title: t('copied_to_clipboard') %>
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: @submission.submitters.to_a.first.slug, host: form_link_host), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy_share_link'), copied_title: t('copied_to_clipboard') %>
<% end %>
</div>
</div>
+16 -7
View File
@@ -1,22 +1,31 @@
<% filter_params = params.permit(Submissions::Filter::ALLOWED_PARAMS).compact_blank %>
<div>
<%= link_to root_path do %>
&larr;
<span><%= t('back_to_active') %></span>
<% end %>
</div>
<div class="flex justify-between mb-4 items-center">
<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 class="flex flex-col md:flex-row md:items-center mb-4 gap-3">
<div class="flex w-full justify-between">
<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>
<% if params[:q].present? || @pagy.pages > 1 %>
<%= render 'shared/search_input', placeholder: "#{t('search')}..." %>
<% end %>
</div>
<% if @pagy.count > 0 %>
<div class="space-y-4">
<%= render partial: 'templates/submission', collection: @submissions, locals: { with_template: true, archived: true } %>
</div>
<% elsif params[:q].present? %>
<% elsif params[:q].present? || filter_params.present? %>
<div class="text-center">
<div class="mt-16 text-3xl font-semibold">
<%= 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 %>
<div class="flex justify-between items-center w-full mb-4">
<div class="flex items-center flex-grow min-w-0">
@@ -10,7 +11,7 @@
</h1>
</div>
<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' %>
<% end %>
<% if can?(:create, ::Template) %>
@@ -32,25 +33,31 @@
<% end %>
<% end %>
<% 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">
<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 space-x-1">
<%= svg_icon('list', class: 'w-5 h-5') %>
<span class="font-normal"><%= t('all') %></span>
</div>
</a>
<a href="<%= url_for(params.to_unsafe_h.merge(status: :pending)) %>" class="<%= params[:status] == 'pending' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
<div class="flex items-center space-x-1">
<%= svg_icon('clock', class: 'w-5 h-5') %>
<span class="font-normal"><%= t('pending') %></span>
</div>
</a>
<a href="<%= url_for(params.to_unsafe_h.merge(status: :completed)) %>" class="<%= params[:status] == 'completed' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
<div class="flex items-center space-x-1">
<%= svg_icon('circle_check', class: 'w-5 h-5') %>
<span class="font-normal"><%= t('completed') %></span>
</div>
</a>
<div class="flex items-center flex-col md:flex-row md:flex-wrap gap-2 mb-4">
<div class="flex items-center md:items-end flex-col md:flex-row gap-2 w-full md:w-fit">
<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 space-x-1">
<%= svg_icon('list', class: 'w-5 h-5') %>
<span class="font-normal"><%= t('all') %></span>
</div>
</a>
<a href="<%= url_for(params.to_unsafe_h.merge(status: :pending)) %>" class="<%= params[:status] == 'pending' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
<div class="flex items-center space-x-1">
<%= svg_icon('clock', class: 'w-5 h-5') %>
<span class="font-normal"><%= t('pending') %></span>
</div>
</a>
<a href="<%= url_for(params.to_unsafe_h.merge(status: :completed)) %>" class="<%= params[:status] == 'completed' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
<div class="flex items-center space-x-1">
<%= svg_icon('circle_check', class: 'w-5 h-5') %>
<span class="font-normal"><%= t('completed') %></span>
</div>
</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>
<% end %>
<% if @pagy.count > 0 %>
@@ -58,10 +65,10 @@
<%= render partial: 'templates/submission', collection: @submissions, locals: { with_template: true } %>
</div>
<% end %>
<% if params[:q].blank? && params[:status].blank? && @pagy.count < 5 %>
<% if params[:q].blank? && params[:status].blank? && filter_params.blank? && @pagy.count < 5 %>
<%= render 'templates/dropzone' %>
<% end %>
<% if @submissions.present? || params[:q].blank? %>
<% if @submissions.present? || (params[:q].blank? && filter_params.blank?) %>
<% if @pagy.pages > 1 %>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'submissions', left_additional_html: view_archived_html %>
<% else %>
@@ -69,7 +76,7 @@
<%= view_archived_html %>
</div>
<% end %>
<% elsif params[:q].present? %>
<% elsif params[:q].present? || filter_params.present? %>
<div class="text-center">
<div class="mt-16 text-3xl font-semibold">
<%= 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 %>
+7
View File
@@ -63,6 +63,13 @@
<% end %>
<% end %>
<%= render 'shared/attribution', link_path: '/start', account: @submitter.account, with_style: false %>
<% if @form_configs[:policy_links].present? %>
<div class="text-center md:text-neutral-500 md:pr-3 md:pb-3 md:text-sm md:text-left mt-2 md:mt-0 md:fixed md:bottom-0 md:right-0">
<div class="md:max-w-[8rem] flex flex-wrap md:flex-col justify-center md:justify-start md:items-start space-x-2 md:space-x-0">
<%= auto_link(MarkdownToHtml.call(@form_configs[:policy_links])) %>
</div>
</div>
<% end %>
</div>
</div>
</div>
@@ -7,7 +7,7 @@
<% else %>
<p><%= t('hi_there') %>,</p>
<p><%= I18n.t(@submitter.with_signature_fields? ? :you_have_been_invited_to_sign_the_name : :you_have_been_invited_to_submit_the_name_form, name: @submitter.submission.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('thanks') %>,<br><%= @current_account.name %>
+2 -2
View File
@@ -92,7 +92,7 @@
</div>
<% else %>
<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>
<% end %>
<% end %>
@@ -176,7 +176,7 @@
</span>
</a>
<% 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 %>
</div>
<% 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="relative flex items-start justify-between w-full space-x-0">
<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 %>
<% if template.archived_at? %>
<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 %>
<% is_show_tabs = @pagy.pages > 1 || params[:status].present? %>
<% if !@pagy.count.zero? || params[:q].present? || params[:status].present? %>
<% filter_params = params.permit(Submissions::Filter::ALLOWED_PARAMS).compact_blank %>
<% is_show_tabs = @pagy.pages > 1 || params[:q].present? || params[:status].present? || filter_params.present? %>
<% if !@pagy.count.zero? || params[:q].present? || params[:status].present? || filter_params.present? %>
<div class="<%= is_show_tabs ? 'mb-4' : 'mb-6' %>">
<div class="flex justify-between items-center md:items-end">
<div>
@@ -9,7 +10,7 @@
</h2>
</div>
<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' %>
<% end %>
<%= link_to new_template_submissions_export_path(@template), class: 'hidden md:flex btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %>
@@ -27,34 +28,40 @@
</div>
<% end %>
<% 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">
<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 space-x-1">
<%= svg_icon('list', class: 'w-5 h-5') %>
<span class="font-normal"><%= t('all') %></span>
</div>
<div class="badge badge-neutral badge-outline font-medium">
<%= params[:status].blank? ? @pagy.count : @base_submissions.count %>
</div>
</a>
<a href="<%= url_for(params.to_unsafe_h.merge(status: :pending)) %>" class="<%= params[:status] == 'pending' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
<div class="flex items-center space-x-1">
<%= svg_icon('clock', class: 'w-5 h-5') %>
<span class="font-normal"><%= t('pending') %></span>
</div>
<div class="badge badge-neutral badge-outline font-medium">
<%= params[:status] == 'pending' ? @pagy.count : @base_submissions.pending.count %>
</div>
</a>
<a href="<%= url_for(params.to_unsafe_h.merge(status: :completed)) %>" class="<%= params[:status] == 'completed' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
<div class="flex items-center space-x-1">
<%= svg_icon('circle_check', class: 'w-5 h-5') %>
<span class="font-normal"><%= t('completed') %></span>
</div>
<div class="badge badge-neutral badge-outline font-medium">
<%= params[:status] == 'completed' ? @pagy.count : @base_submissions.completed.count %>
</div>
</a>
<div class="flex items-center flex-col md:flex-row md:flex-wrap gap-2 mb-4">
<div class="flex items-center md:items-end flex-col md:flex-row gap-2 w-full md:w-fit">
<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">
<div class="flex items-center space-x-1">
<%= svg_icon('list', class: 'w-5 h-5') %>
<span class="font-normal"><%= t('all') %></span>
</div>
<div class="badge badge-neutral badge-outline font-medium">
<%= params[:status].blank? && filter_params.blank? ? @pagy.count : @base_submissions.unscope(:group, :order).select(:id).distinct.count %>
</div>
</a>
<a href="<%= url_for(params.to_unsafe_h.merge(status: :pending)) %>" class="<%= params[:status] == 'pending' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-700">
<div class="flex items-center space-x-1">
<%= svg_icon('clock', class: 'w-5 h-5') %>
<span class="font-normal"><%= t('pending') %></span>
</div>
<div class="badge badge-neutral badge-outline font-medium">
<%= params[:status] == 'pending' && filter_params.blank? ? @pagy.count : @base_submissions.pending.unscope(:group, :order).select(:id).distinct.count %>
</div>
</a>
<a href="<%= url_for(params.to_unsafe_h.merge(status: :completed)) %>" class="<%= params[:status] == 'completed' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-700">
<div class="flex items-center space-x-1">
<%= svg_icon('circle_check', class: 'w-5 h-5') %>
<span class="font-normal"><%= t('completed') %></span>
</div>
<div class="badge badge-neutral badge-outline font-medium">
<%= params[:status] == 'completed' && filter_params.blank? ? @pagy.count : @base_submissions.completed.unscope(:group, :order).select(:id).distinct.count %>
</div>
</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>
<% end %>
<% if @submissions.present? %>
@@ -65,31 +72,33 @@
<div class="card bg-base-200">
<div class="card-body text-center px-4 py-16">
<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') %>
</p>
<% if @template.archived_at.blank? && params[:q].blank? %>
<p><%= t('send_an_invitation_to_fill_and_complete_the_form') %></p>
<div class="space-y-2 flex flex-col">
<% if can?(:create, Submission) %>
<%= link_to new_template_submission_path(@template, with_link: true), class: 'base-button mt-6', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span class="mr-1"><%= t('send_to_recipients') %></span>
<% if @template.archived_at.blank? && params[:q].blank? && filter_params.blank? %>
<div class="mt-4">
<p><%= t('send_an_invitation_to_fill_and_complete_the_form') %></p>
<div class="space-y-2 flex flex-col">
<% if can?(:create, Submission) %>
<%= link_to new_template_submission_path(@template, with_link: true), class: 'base-button mt-6', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span class="mr-1"><%= t('send_to_recipients') %></span>
<% end %>
<% end %>
<% end %>
<% if @template.submitters.size == 1 %>
<%= link_to start_form_url(slug: @template.slug), class: 'white-button mt-6', target: '_blank', rel: 'noopener' do %>
<%= svg_icon('writing', class: 'w-6 h-6') %>
<span class="mr-1"><%= t('sign_it_yourself') %></span>
<% if @template.submitters.size == 1 %>
<%= link_to start_form_url(slug: @template.slug), class: 'white-button mt-6', target: '_blank', rel: 'noopener' do %>
<%= svg_icon('writing', class: 'w-6 h-6') %>
<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 %>
<% 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 %>
</div>
</div>
</div>
<% end %>
</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 %>
<div>
<%= link_to template_path(@template) do %>
@@ -5,23 +7,33 @@
<span><%= t('back_to_active') %></span>
<% end %>
</div>
<div class="flex justify-between mb-6 md:items-end flex-col md:flex-row">
<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>
<div class="flex space-x-2 mt-3 md:mt-0 justify-end">
<%= render 'shared/search_input' %>
<%= link_to new_template_submissions_export_path(@template), class: 'order-3 md:order-1 btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('download', class: 'w-6 h-6 stroke-2') %>
<span><%= t('export') %></span>
<% end %>
<div class="flex flex-col md:flex-row md:items-center mb-6 gap-3">
<div class="flex w-full justify-between md:items-end items-center">
<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>
<div class="flex space-x-2 justify-end">
<% if with_filters %>
<%= render 'shared/search_input' %>
<% end %>
<%= link_to new_template_submissions_export_path(@template), class: 'btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('download', class: 'w-6 h-6 stroke-2') %>
<span><%= t('export') %></span>
<% end %>
</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>
<% if @pagy.count > 0 %>
<div class="space-y-4">
<%= render partial: 'templates/submission', collection: @submissions, locals: { template: @template, archived: true } %>
</div>
<% elsif params[:q].present? %>
<% elsif params[:q].present? || filter_params.present? %>
<div class="text-center">
<div class="mt-16 text-3xl font-semibold">
<%= t('submissions_not_found') %>
+14 -8
View File
@@ -117,7 +117,7 @@
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %>
<toggle-on-submit data-element-id="email_saved_alert2"></toggle-on-submit>
<% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY).value %>
<%= f.fields_for :preferences, Struct.new(:documents_copy_email_subject, :documents_copy_email_body, :documents_copy_email_enabled, :documents_copy_email_attach_audit).new(@template.preferences['documents_copy_email_subject'].presence || configs['subject'], @template.preferences['documents_copy_email_body'].presence || configs['body'], @template.preferences['documents_copy_email_enabled'], configs['attach_audit_log'] != false && @template.preferences['documents_copy_email_attach_audit'] != false) do |ff| %>
<%= f.fields_for :preferences, Struct.new(:documents_copy_email_subject, :documents_copy_email_body, :documents_copy_email_enabled, :documents_copy_email_attach_audit, :documents_copy_email_attach_documents).new(@template.preferences['documents_copy_email_subject'].presence || configs['subject'], @template.preferences['documents_copy_email_body'].presence || configs['body'], @template.preferences['documents_copy_email_enabled'], configs['attach_audit_log'] != false && @template.preferences['documents_copy_email_attach_audit'] != false, configs['attach_documents'] != false && @template.preferences['documents_copy_email_attach_documents'] != false) do |ff| %>
<div class="form-control">
<%= ff.label :documents_copy_email_subject, t('email_subject'), class: 'label' %>
<%= ff.text_field :documents_copy_email_subject, required: true, class: 'base-input', dir: 'auto' %>
@@ -133,6 +133,12 @@
<%= ff.text_area :documents_copy_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<div class="flex items-center justify-between pt-2.5 px-1 mb-2">
<span>
<%= t('attach_documents_to_the_email') %>
</span>
<%= ff.check_box :documents_copy_email_attach_documents, { checked: ff.object.documents_copy_email_attach_documents != false, class: 'toggle', onchange: 'this.form.requestSubmit()', disabled: configs['attach_documents'] == false }, 'true', 'false' %>
</div>
<div class="flex items-center justify-between pt-2.5 px-1 mb-2">
<span>
<%= t('attach_audit_log_pdf_to_the_email') %>
@@ -180,24 +186,24 @@
<%= ff.text_area :completed_notification_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<div class="flex items-center justify-between pt-2.5 px-1 mb-2">
<span>
<%= t('send_emails_automatically_on_completion') %>
</span>
<%= ff.check_box :completed_notification_email_enabled, { checked: ff.object.completed_notification_email_enabled != false, class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %>
</div>
<div class="flex items-center justify-between pt-2.5 px-1 mb-2">
<span>
<%= t('attach_documents_to_the_email') %>
</span>
<%= ff.check_box :completed_notification_email_attach_documents, { checked: ff.object.completed_notification_email_attach_documents != false, class: 'toggle', onchange: 'this.form.requestSubmit()', disabled: configs['attach_documents'] == false }, 'true', 'false' %>
</div>
<div class="flex items-center justify-between py-2.5 px-1 mb-2">
<div class="flex items-center justify-between pt-2.5 px-1 mb-2">
<span>
<%= t('attach_audit_log_pdf_to_the_email') %>
</span>
<%= ff.check_box :completed_notification_email_attach_audit, { checked: ff.object.completed_notification_email_attach_audit != false, class: 'toggle', onchange: 'this.form.requestSubmit()', disabled: configs['attach_audit_log'] == false }, 'true', 'false' %>
</div>
<div class="flex items-center justify-between py-2.5 px-1 mb-2">
<span>
<%= t('send_emails_automatically_on_completion') %>
</span>
<%= ff.check_box :completed_notification_email_enabled, { checked: ff.object.completed_notification_email_enabled != false, class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %>
</div>
<% end %>
<div class="form-control pt-2">
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
+5 -3
View File
@@ -12,11 +12,13 @@ end
ActiveSupport.on_load(:active_storage_blob) do
attribute :uuid, :string, default: -> { SecureRandom.uuid }
attribute :io_data, :string, default: ''
def self.proxy_url(blob, expires_at: nil)
def self.proxy_url(blob, expires_at: nil, filename: nil, host: nil)
Rails.application.routes.url_helpers.blobs_proxy_url(
signed_uuid: blob.signed_uuid(expires_at:), filename: blob.filename,
**Docuseal.default_url_options
signed_uuid: blob.signed_uuid(expires_at:), filename: filename || blob.filename,
**Docuseal.default_url_options,
**{ host: }.compact
)
end
+139 -7
View File
@@ -22,6 +22,9 @@ en: &en
thanks: Thanks
unarchive: Unarchive
first_party: 'First Party'
remove_filter: Remove filter
document_download_filename_format: Document download filename format
document_name: Document Name
docuseal_trusted_signature: DocuSeal Trusted Signature
hello_name: Hello %{name}
you_are_invited_to_product_name: You are invited to %{product_name}
@@ -344,7 +347,7 @@ en: &en
edit_message: Edit message
smtp_not_configured: SMTP not Configured
configure_smtp_settings_in_order_to_send_emails_: 'Configure SMTP settings in order to send emails:'
go_to_smtp_setting: Go to SMTP setting
go_to_smtp_settings: Go to SMTP settings
save_as_default_template_message: Save as default template message
re_send_sms: Re-send SMS
send_sms: Send SMS
@@ -615,11 +618,13 @@ en: &en
stamp_field: Stamp Field
payment_field: Payment Field
phone_field: Phone Field
verification_field: Identity Verification
identity_verification: Identity verification
paid_price: 'Paid %{price}'
verified: Verified
unverified: Unverified
document: Document
completed_at: Completed At
completed_at: Completed at
edit_recipient: Edit Recipient
update_recipient: Update Recipient
use_international_format_1xxx_: 'Use internatioanl format: +1xxx...'
@@ -631,6 +636,21 @@ en: &en
key: Key
value: Value
webhook_secret: Webhook Secret
author: Author
to: To
created_at: Created at
apply: Apply
applying: Applying
today: Today
yesterday: Yesterday
this_week: This week
last_week: Last week
this_month: This month
last_month: Last month
this_year: This year
policy_links: Policy Links
markdown_content_e_g: Markdown content, e.g.
privacy_policy: Privacy Policy
submission_event_names:
send_email_to_html: '<b>Email sent</b> to %{submitter_name}'
send_reminder_email_to_html: '<b>Reminder email sent</b> to %{submitter_name}'
@@ -644,6 +664,8 @@ en: &en
view_form_by_html: '<b>Form viewed</b> by %{submitter_name}'
invite_party_by_html: '<b>Invited</b> %{invited_submitter_name} by %{submitter_name}'
complete_form_by_html: '<b>Submission completed</b> by %{submitter_name}'
start_verification_by_html: '<b>Identity verification started</b> by %{submitter_name}'
complete_verification_by_html: '<b>Identity verification completed</b> by %{submitter_name} with %{provider}'
api_complete_form_by_html: '<b>Submission completed via API</b> by %{submitter_name}'
import_list:
select_worksheet: Select Worksheet
@@ -667,6 +689,9 @@ en: &en
read: Read your data
es: &es
remove_filter: Eliminar filtro
document_download_filename_format: Formato del nombre del archivo de descarga del documento
document_name: Nombre del documento
unarchive: Desarchivar
awaiting_completion_by_the_other_party: "Esperando la finalización por la otra parte"
enforce_recipients_order: 'Hacer cumplir el orden de los destinatarios'
@@ -993,7 +1018,7 @@ es: &es
edit_message: Editar mensaje
smtp_not_configured: SMTP no configurado
configure_smtp_settings_in_order_to_send_emails_: 'Configura los ajustes de SMTP para enviar correos electrónicos:'
go_to_smtp_setting: Ir a la configuración de SMTP
go_to_smtp_settings: Ir a la configuración de SMTP
save_as_default_template_message: Guardar como mensaje de plantilla predeterminado
re_send_sms: Reenviar SMS
send_sms: Enviar SMS
@@ -1264,6 +1289,8 @@ es: &es
stamp_field: Campo de Sello
payment_field: Campo de Pago
phone_field: Campo de Teléfono
verification_field: Verificación de Identidad
identity_verification: Verificación de Identidad
paid_price: 'Pagado %{price}'
verified: Verificado
unverified: No verificado
@@ -1280,6 +1307,21 @@ es: &es
key: Clave
value: Valor
webhook_secret: Secreto del Webhook
author: Autor
to: A
created_at: Creado el
apply: Aplicar
applying: Aplicando
today: Hoy
yesterday: Ayer
this_week: Esta Semana
last_week: La Semana Pasada
this_month: Este Mes
last_month: El Mes Pasado
this_year: Este Año
policy_links: Enlaces de Políticas
markdown_content_e_g: Contenido Markdown, por ej.
privacy_policy: Política de Privacidad
submission_event_names:
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}'
@@ -1294,6 +1336,8 @@ es: &es
invite_party_by_html: '<b>Invitado</b> %{invited_submitter_name} por %{submitter_name}'
complete_form_by_html: '<b>Envío completado</b> por %{submitter_name}'
api_complete_form_by_html: '<b>Envío completado vía API</b> por %{submitter_name}'
start_verification_by_html: '<b>Verificación de identidad iniciada</b> por %{submitter_name}'
complete_verification_by_html: '<b>Verificación de identidad completada</b> por %{submitter_name} con %{provider}'
import_list:
select_worksheet: Seleccionar hoja de cálculo
open: Abrir
@@ -1316,6 +1360,9 @@ es: &es
read: Leer tus datos
it: &it
remove_filter: Rimuovi filtro
document_download_filename_format: Formato del nome file scaricato
document_name: Nome del Documento
unarchive: Ripristina
awaiting_completion_by_the_other_party: "In attesa di completamento da parte dell'altra parte"
enforce_recipients_order: Aplicar el orden de los destinatarios
@@ -1642,7 +1689,7 @@ it: &it
edit_message: Modifica messaggio
smtp_not_configured: SMTP non configurato
configure_smtp_settings_in_order_to_send_emails_: 'Configura le impostazioni SMTP per inviare email:'
go_to_smtp_setting: Vai alle impostazioni SMTP
go_to_smtp_settings: Vai alle impostazioni SMTP
save_as_default_template_message: Salva come messaggio modello predefinito
re_send_sms: Reinvio SMS
send_sms: Invia SMS
@@ -1913,6 +1960,8 @@ it: &it
stamp_field: Campo Timbro
payment_field: Campo Pagamento
phone_field: Campo Telefono
verification_field: "Verifica dell'identità"
identity_verification: "Verifica dell'identità"
paid_price: 'Pagato %{price}'
verified: Verificato
unverified: Non verificato
@@ -1929,6 +1978,21 @@ it: &it
key: Chiave
value: Valore
webhook_secret: Segreto del Webhook
author: Autore
to: A
created_at: Creato il
apply: Applica
applying: Applicazione
today: Oggi
yesterday: Ieri
this_week: Questa Settimana
last_week: Settimana Scorsa
this_month: Questo Mese
last_month: Mese Scorso
this_year: "Quest'Anno"
policy_links: Collegamenti alle Politiche
markdown_content_e_g: Contenuto Markdown, ad es.
privacy_policy: Politica sulla Privacy
submission_event_names:
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}'
@@ -1943,6 +2007,8 @@ it: &it
invite_party_by_html: '<b>Invitato</b> %{invited_submitter_name} da %{submitter_name}'
complete_form_by_html: '<b>Invio completato</b> da %{submitter_name}'
api_complete_form_by_html: '<b>Invio completato tramite API</b> da %{submitter_name}'
start_verification_by_html: "<b>Verifica dell'identità iniziata</b> da %{submitter_name}"
complete_verification_by_html: "<b>Verifica dell'identità completata</b> da %{submitter_name} con %{provider}"
import_list:
select_worksheet: Seleziona il foglio di lavoro
open: Apri
@@ -1965,6 +2031,9 @@ it: &it
read: Leggi i tuoi dati
fr: &fr
remove_filter: Supprimer le filtre
document_download_filename_format: Format du nom de fichier du téléchargement de document
document_name: Nom du document
unarchive: Désarchiver
awaiting_completion_by_the_other_party: "En attente de la complétion par l'autre partie"
enforce_recipients_order: "Respecter l'ordre des destinataires"
@@ -2292,7 +2361,7 @@ fr: &fr
edit_message: Modifier le message
smtp_not_configured: SMTP non configuré
configure_smtp_settings_in_order_to_send_emails_: 'Configurez les paramètres SMTP pour envoyer des e-mails:'
go_to_smtp_setting: Aller aux paramètres SMTP
go_to_smtp_settings: Aller aux paramètres SMTP
save_as_default_template_message: Enregistrer comme modèle de message par défaut
re_send_sms: Renvoyer le SMS
send_sms: Envoyer SMS
@@ -2563,6 +2632,8 @@ fr: &fr
stamp_field: Champ de Tampon
payment_field: Champ de Paiement
phone_field: Champ de Téléphone
verification_field: "Vérification d'Identité"
identity_verification: "Vérification d'identité"
paid_price: 'Payé %{price}'
verified: Vérifié
unverified: Non vérifié
@@ -2579,6 +2650,21 @@ fr: &fr
key: Clé
value: Valeur
webhook_secret: Secret du Webhook
author: Auteur
to: À
created_at: Créé le
apply: Appliquer
applying: Application en cours
today: "Aujourd'hui"
yesterday: Hier
this_week: Cette Semaine
last_week: La Semaine Dernière
this_month: Ce Mois-ci
last_month: Le Mois Dernier
this_year: Cette Année
policy_links: Liens des Politiques
markdown_content_e_g: Contenu Markdown, par ex.
privacy_policy: Politique de Confidentialité
submission_event_names:
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}'
@@ -2592,6 +2678,8 @@ fr: &fr
view_form_by_html: '<b>Formulaire consulté</b> par %{submitter_name}'
invite_party_by_html: '<b>Invité</b> %{invited_submitter_name} par %{submitter_name}'
complete_form_by_html: '<b>Soumission terminée</b> par %{submitter_name}'
start_verification_by_html: "<b>Vérification d'identité commencée</b> par %{submitter_name}"
complete_verification_by_html: "<b>Vérification d'identité terminée</b> par %{submitter_name} avec %{provider}"
api_complete_form_by_html: "<b>Soumission terminée via l'API</b> par %{submitter_name}"
import_list:
select_worksheet: Sélectionner la feuille de calcul
@@ -2615,6 +2703,9 @@ fr: &fr
read: Lire vos données
pt: &pt
remove_filter: Remover filtro
document_download_filename_format: Formato do nome do arquivo de download do documento
document_name: Nome do documento
unarchive: Desarquivar
awaiting_completion_by_the_other_party: "Aguardando a conclusão pela outra parte"
enforce_recipients_order: 'Forçar a ordem dos recipientes'
@@ -2941,7 +3032,7 @@ pt: &pt
edit_message: Editar mensagem
smtp_not_configured: SMTP não configurado
configure_smtp_settings_in_order_to_send_emails_: 'Configure as configurações de SMTP para enviar e-mails:'
go_to_smtp_setting: Ir para a configuração de SMTP
go_to_smtp_settings: Ir para a configuração de SMTP
save_as_default_template_message: Salvar como mensagem de modelo padrão
re_send_sms: Reenviar SMS
send_sms: Enviar SMS
@@ -3212,6 +3303,8 @@ pt: &pt
stamp_field: Campo de Carimbo
payment_field: Campo de Pagamento
phone_field: Campo de Telefone
verification_field: "Verificação de Identidade"
identity_verification: "Verificação de identidade"
paid_price: 'Pago %{price}'
verified: Verificado
unverified: Não verificado
@@ -3228,6 +3321,21 @@ pt: &pt
key: Chave
value: Valor
webhook_secret: Segredo do Webhook
author: Autor
to: Para
created_at: Criado em
apply: Aplicar
applying: Aplicando
today: Hoje
yesterday: Ontem
this_week: Esta Semana
last_week: Semana Passada
this_month: Este Mês
last_month: Mês Passado
this_year: Este Ano
policy_links: Links de Políticas
markdown_content_e_g: Conteúdo Markdown, ex.
privacy_policy: Política de Privacidade
submission_event_names:
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}'
@@ -3241,6 +3349,8 @@ pt: &pt
view_form_by_html: '<b>Formulário visualizado</b> por %{submitter_name}'
invite_party_by_html: '<b>Convidado</b> %{invited_submitter_name} por %{submitter_name}'
complete_form_by_html: '<b>Submissão concluída</b> por %{submitter_name}'
start_verification_by_html: '<b>Verificação de identidade iniciada</b> por %{submitter_name}'
complete_verification_by_html: '<b>Verificação de identidade concluída</b> por %{submitter_name} com %{provider}'
api_complete_form_by_html: '<b>Submissão concluída via API</b> por %{submitter_name}'
import_list:
select_worksheet: Selecionar planilha
@@ -3264,6 +3374,9 @@ pt: &pt
read: Ler seus dados
de: &de
remove_filter: Filter entfernen
document_download_filename_format: Format des Dateinamens beim Herunterladen von Dokumenten
document_name: Dokumentname
unarchive: Wiederherstellen
awaiting_completion_by_the_other_party: "Warten auf die Fertigstellung durch die andere Partei"
enforce_recipients_order: 'Empfängerreihenfolge durchsetzen'
@@ -3590,7 +3703,7 @@ de: &de
edit_message: Nachricht bearbeiten
smtp_not_configured: SMTP nicht konfiguriert
configure_smtp_settings_in_order_to_send_emails_: 'Konfiguriere die SMTP-Einstellungen, um E-Mails zu senden:'
go_to_smtp_setting: Zu den SMTP-Einstellungen gehen
go_to_smtp_settings: Zu den SMTP-Einstellungen gehen
save_as_default_template_message: Als Standardvorlage speichern
re_send_sms: SMS erneut senden
send_sms: SMS senden
@@ -3861,6 +3974,8 @@ de: &de
stamp_field: Stempelfeld
payment_field: Zahlungsfeld
phone_field: Telefonfeld
verification_field: Identitätsüberprüfung
identity_verification: Identitätsüberprüfung
paid_price: 'Bezahlt %{price}'
verified: Verifiziert
unverified: Nicht verifiziert
@@ -3877,6 +3992,21 @@ de: &de
key: Schlüssel
value: Wert
webhook_secret: Webhook-Geheimnis
author: Autor
to: An
created_at: Erstellt am
apply: Anwenden
applying: Anwenden
today: Heute
yesterday: Gestern
this_week: Diese Woche
last_week: Letzte Woche
this_month: Dieser Monat
last_month: Letzter Monat
this_year: Dieses Jahr
policy_links: Richtlinien-Links
markdown_content_e_g: Markdown-Inhalt, z. B.
privacy_policy: Datenschutzrichtlinie
submission_event_names:
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}'
@@ -3890,6 +4020,8 @@ de: &de
view_form_by_html: '<b>Formular angesehen</b> von %{submitter_name}'
invite_party_by_html: '<b>Eingeladen</b> %{invited_submitter_name} von %{submitter_name}'
complete_form_by_html: '<b>Einreichung abgeschlossen</b> von %{submitter_name}'
start_verification_by_html: '<b>Identitätsüberprüfung gestartet</b> von %{submitter_name}'
complete_verification_by_html: '<b>Identitätsüberprüfung abgeschlossen</b> von %{submitter_name} mit %{provider}'
api_complete_form_by_html: '<b>Einreichung über API abgeschlossen</b> von %{submitter_name}'
import_list:
select_worksheet: Arbeitsblatt auswählen
+1
View File
@@ -90,6 +90,7 @@ Rails.application.routes.draw do
resources :folders, only: %i[show edit update destroy], controller: 'template_folders'
resources :template_sharings_testing, only: %i[create]
resources :templates, only: %i[index], controller: 'templates_dashboard'
resources :submissions_filters, only: %i[show], param: 'name'
resources :templates, only: %i[new create edit update show destroy] do
resource :debug, only: %i[show], controller: 'templates_debug' if Rails.env.development?
resources :documents, only: %i[create], controller: 'template_documents'
+1
View File
@@ -3,6 +3,7 @@
module Docuseal
URL_CACHE = ActiveSupport::Cache::MemoryStore.new
PRODUCT_URL = 'https://www.docuseal.com'
PRODUCT_EMAIL_URL = ENV.fetch('PRODUCT_EMAIL_URL', PRODUCT_URL)
NEWSLETTER_URL = "#{PRODUCT_URL}/newsletters".freeze
ENQUIRIES_URL = "#{PRODUCT_URL}/enquiries".freeze
PRODUCT_NAME = 'DocuSeal'
+10 -1
View File
@@ -19,6 +19,8 @@ module ReplaceEmailVariables
DOCUMENTS_LINKS = /\{+documents\.links\}+/i
DOCUMENTS_LINK = /\{+documents\.link\}+/i
EMAIL_HOST = ENV.fetch('EMAIL_HOST', nil)
module_function
# rubocop:disable Metrics
@@ -64,10 +66,17 @@ module ReplaceEmailVariables
def build_submitter_link(submitter, tracking_event_type)
if tracking_event_type == 'click_email'
url_options =
if EMAIL_HOST.present?
{ host: EMAIL_HOST, protocol: ENV['FORCE_SSL'].present? ? 'https' : 'http' }
else
Docuseal.default_url_options
end
Rails.application.routes.url_helpers.submit_form_url(
slug: submitter.slug,
t: SubmissionEvents.build_tracking_param(submitter, 'click_email'),
**Docuseal.default_url_options
**url_options
)
else
Rails.application.routes.url_helpers.submit_form_url(
+7 -5
View File
@@ -26,7 +26,7 @@ module Submissions
arel = arel.or(Template.arel_table[:name].lower.matches("%#{keyword.downcase}%"))
end
submissions.joins(:submitters).where(arel).distinct
submissions.joins(:submitters).where(arel).group(:id)
end
def update_template_fields!(submission)
@@ -88,17 +88,19 @@ module Submissions
)
end
def send_signature_requests(submissions)
submissions.each do |submission|
def send_signature_requests(submissions, delay: nil)
submissions.each_with_index do |submission, index|
delay_seconds = (delay + index).seconds if delay
submitters = submission.submitters.reject(&:completed_at?)
if submission.submitters_order_preserved?
first_submitter =
submission.template_submitters.filter_map { |s| submitters.find { |e| e.uuid == s['uuid'] } }.first
Submitters.send_signature_requests([first_submitter]) if first_submitter
Submitters.send_signature_requests([first_submitter], delay_seconds:) if first_submitter
else
Submitters.send_signature_requests(submitters)
Submitters.send_signature_requests(submitters, delay_seconds:)
end
end
end
+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
+20 -6
View File
@@ -23,7 +23,7 @@ module Submissions
RTL_REGEXP = TextUtils::RTL_REGEXP
MAX_IMAGE_HEIGHT = 100
US_TIMEZONES = %w[EST CST MST PST HST AKDT].freeze
US_TIMEZONES = TimeUtils::US_TIMEZONES
module_function
@@ -64,7 +64,10 @@ module Submissions
def build_audit_trail(submission)
account = submission.account
verify_url = Rails.application.routes.url_helpers.settings_esign_url(**Docuseal.default_url_options)
verify_url = Rails.application.routes.url_helpers.settings_esign_url(
**Docuseal.default_url_options, host: ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host])
)
page_size =
if TimeUtils.timezone_abbr(account.timezone, Time.current.beginning_of_year).in?(US_TIMEZONES)
:Letter
@@ -231,6 +234,11 @@ module Submissions
e['type'] == 'phone' && e['submitter_uuid'] == submitter.uuid && submitter.values[e['uuid']].present?
end
is_id_verified =
submission.template_fields.any? do |e|
e['type'] == 'verification' && e['submitter_uuid'] == submitter.uuid && submitter.values[e['uuid']].present?
end
submitter_field_counters = Hash.new { 0 }
info_rows = [
@@ -253,6 +261,9 @@ module Submissions
submitter.phone && is_phone_verified && {
text: "#{I18n.t('phone_verification')}: #{I18n.t('verified')}\n"
},
is_id_verified && {
text: "#{I18n.t('identity_verification')}: #{I18n.t('verified')}\n"
},
completed_event.data['ip'] && { text: "IP: #{completed_event.data['ip']}\n" },
completed_event.data['sid'] && { text: "#{I18n.t('session_id')}: #{completed_event.data['sid']}\n" },
completed_event.data['ua'] && { text: "User agent: #{completed_event.data['ua']}\n" },
@@ -373,9 +384,12 @@ module Submissions
end
text =
if event.event_type == 'invite_party' &&
(invited_submitter = submission.submitters.find { |e| e.uuid == event.data['uuid'] }) &&
(name = submission.template_submitters.find { |e| e['uuid'] == event.data['uuid'] }&.dig('name'))
if event.event_type == 'complete_verification'
I18n.t('submission_event_names.complete_verification_by_html', provider: event.data['method'],
submitter_name:)
elsif event.event_type == 'invite_party' &&
(invited_submitter = submission.submitters.find { |e| e.uuid == event.data['uuid'] }) &&
(name = submission.template_submitters.find { |e| e['uuid'] == event.data['uuid'] }&.dig('name'))
invited_submitter_name = [invited_submitter.name || invited_submitter.email || invited_submitter.phone,
name].join(' ')
I18n.t('submission_event_names.invite_party_by_html', invited_submitter_name:,
@@ -411,7 +425,7 @@ module Submissions
column.image(PdfIcons.logo_io, width: 40, height: 40, position: :float)
column.formatted_text([{ text: 'DocuSeal',
link: Docuseal::PRODUCT_URL }],
link: Docuseal::PRODUCT_EMAIL_URL }],
font_size: 20,
font: [FONT_NAME, { variant: :bold }],
width: 100,
+8
View File
@@ -110,6 +110,10 @@ module Submissions
{
name: column_name(I18n.t('completed_at'), submitter_name, submitters_count),
value: submitter.completed_at.to_s
},
{
name: column_name(I18n.t('link'), submitter_name, submitters_count),
value: submitter.completed_at? ? nil : r.submit_form_url(slug: submitter.slug, **Docuseal.default_url_options)
}
].reject { |e| e[:value].blank? }
end
@@ -150,5 +154,9 @@ module Submissions
{ name: template_field_name, uuid: template_field['uuid'], value: }
end
end
def r
Rails.application.routes.url_helpers
end
end
end
@@ -5,8 +5,12 @@ module Submissions
module_function
# rubocop:disable Metrics
def call(submission, values_hash: nil)
values_hash ||= build_values_hash(submission)
def call(submission, values_hash: nil, submitter: nil)
values_hash ||= if submitter
build_submitter_values_hash(submitter)
else
build_values_hash(submission)
end
configs = submission.account.account_configs.where(key: [AccountConfig::FLATTEN_RESULT_PDF_KEY,
AccountConfig::WITH_SIGNATURE_ID])
@@ -16,8 +20,14 @@ module Submissions
pdfs_index = GenerateResultAttachments.build_pdfs_index(submission, flatten: is_flatten)
submission.submitters.where(completed_at: nil).preload(attachments_attachments: :blob).each do |submitter|
GenerateResultAttachments.fill_submitter_fields(submitter, submission.account, pdfs_index,
submitters = if submitter
submission.submitters.where(id: submitter.id)
else
submission.submitters.where(completed_at: nil)
end
submitters.preload(attachments_attachments: :blob).each do |s|
GenerateResultAttachments.fill_submitter_fields(s, submission.account, pdfs_index,
with_signature_id:, is_flatten:)
end
@@ -36,7 +46,7 @@ module Submissions
image_pdfs << pdf
end
build_pdf_attachment(pdf:, submission:,
build_pdf_attachment(pdf:, submission:, submitter:,
uuid: item['attachment_uuid'],
values_hash:,
name: item['name'])
@@ -55,6 +65,7 @@ module Submissions
build_pdf_attachment(
pdf: images_pdf,
submission:,
submitter:,
uuid: GenerateResultAttachments.images_pdf_uuid(original_documents.select(&:image?)),
values_hash:,
name: template.name
@@ -66,10 +77,21 @@ module Submissions
end
def build_values_hash(submission)
submission.submitters.reduce({}) { |acc, s| acc.merge(s.values) }.hash
Digest::MD5.hexdigest(
submission.submitters.reduce({}) { |acc, s| acc.merge(s.values) }.to_json
)
end
def build_pdf_attachment(pdf:, submission:, uuid:, name:, values_hash:)
def build_submitter_values_hash(submitter)
submission = submitter.submission
Digest::MD5.hexdigest(
submission.submitters.where.not(completed_at: nil).or(submission.submitters.where(id: submitter.id))
.reduce({}) { |acc, s| acc.merge(s.values) }.to_json
)
end
def build_pdf_attachment(pdf:, submission:, submitter:, uuid:, name:, values_hash:)
io = StringIO.new
begin
@@ -82,12 +104,13 @@ module Submissions
ActiveStorage::Attachment.new(
blob: ActiveStorage::Blob.create_and_upload!(io: io.tap(&:rewind), filename: "#{name}.pdf"),
io_data: io.string,
metadata: { original_uuid: uuid,
values_hash:,
analyzed: true,
sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(io.string)) },
name: 'preview_documents',
record: submission
record: submitter || submission
)
end
# rubocop:enable Metrics
@@ -42,6 +42,8 @@ module Submissions
# rubocop:disable Metrics
def call(submitter)
return generate_detached_signature_attachments(submitter) if detached_signature?(submitter)
pdfs_index = generate_pdfs(submitter)
template = submitter.submission.template
@@ -675,6 +677,14 @@ module Submissions
"#{Docuseal.product_name} (#{Docuseal::PRODUCT_URL})"
end
def detached_signature?(_submitter)
false
end
def generate_detached_signature_attachments(_submitter)
[]
end
def h
Rails.application.routes.url_helpers
end
+22 -3
View File
@@ -94,12 +94,16 @@ module Submitters
preferences
end
def send_signature_requests(submitters)
submitters.each do |submitter|
def send_signature_requests(submitters, delay_seconds: nil)
submitters.each_with_index do |submitter, index|
next if submitter.email.blank?
next if submitter.preferences['send_email'] == false
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => submitter.id)
if delay_seconds
SendSubmitterInvitationEmailJob.perform_in((delay_seconds + index).seconds, 'submitter_id' => submitter.id)
else
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => submitter.id)
end
end
end
@@ -112,4 +116,19 @@ module Submitters
acc && submitter.submission.submitters.find { |e| e.uuid == item['uuid'] }&.completed_at?
end
end
def build_document_filename(submitter, blob, filename_format)
return blob.filename.to_s if filename_format.blank?
filename = ReplaceEmailVariables.call(filename_format, submitter:)
filename = filename.gsub('{document.name}', blob.filename.base)
filename = filename.gsub(
'{submission.completed_at}',
I18n.l(submitter.completed_at.beginning_of_year.in_time_zone(submitter.account.timezone), format: :short)
)
"#{filename}.#{blob.filename.extension}"
end
end
+4 -1
View File
@@ -10,7 +10,8 @@ module Submitters
AccountConfig::ALLOW_TO_DECLINE_KEY,
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
AccountConfig::REUSE_SIGNATURE_KEY,
AccountConfig::ALLOW_TYPED_SIGNATURE].freeze
AccountConfig::ALLOW_TYPED_SIGNATURE,
*(Docuseal.multitenant? ? [] : [AccountConfig::POLICY_LINKS_KEY])].freeze
module_function
@@ -26,6 +27,7 @@ module Submitters
with_decline = find_safe_value(configs, AccountConfig::ALLOW_TO_DECLINE_KEY) != false
with_signature_id = find_safe_value(configs, AccountConfig::WITH_SIGNATURE_ID) == true
require_signing_reason = find_safe_value(configs, AccountConfig::REQUIRE_SIGNING_REASON_KEY) == true
policy_links = find_safe_value(configs, AccountConfig::POLICY_LINKS_KEY)
attrs = {
completed_button:,
@@ -33,6 +35,7 @@ module Submitters
with_confetti:,
reuse_signature:,
with_decline:,
policy_links:,
completed_message:,
require_signing_reason:,
prefill_signature:,
+46 -23
View File
@@ -93,7 +93,11 @@ module Submitters
if params[:cast_boolean] == 'true'
v == 'true'
elsif params[:cast_number] == 'true'
(v.to_f % 1).zero? ? v.to_i : v.to_f
if v == ''
nil
else
(v.to_f % 1).zero? ? v.to_i : v.to_f
end
elsif params[:normalize_phone] == 'true'
v.to_s.gsub(/[^0-9+]/, '')
else
@@ -124,6 +128,17 @@ module Submitters
next
end
if field['type'] == 'verification'
acc[field['uuid']] =
if submitter.submission_events.exists?(event_type: :complete_verification)
I18n.t(:verified, locale: :en)
elsif field['required']
raise ValidationError, 'ID Not Verified'
end
next
end
value = field['default_value']
next if value.blank?
@@ -171,43 +186,51 @@ module Submitters
submitter.submission.template_fields.each do |field|
next if field['submitter_uuid'] != submitter.uuid
submitter.values.delete(field['uuid']) unless check_field_condition(submitter, field, fields_uuid_index)
submitter.values.delete(field['uuid']) unless check_field_conditions(submitter, field, fields_uuid_index)
end
submitter.values
end
def check_field_condition(submitter, field, fields_uuid_index)
def check_field_conditions(submitter, field, fields_uuid_index)
return true if field['conditions'].blank?
submitter_values = submitter.values
field['conditions'].reduce(true) do |acc, c|
case c['action']
when 'empty', 'unchecked'
acc && submitter_values[c['field_uuid']].blank?
when 'not_empty', 'checked'
acc && submitter_values[c['field_uuid']].present?
when 'equal', 'contains'
field = fields_uuid_index[c['field_uuid']]
option = field['options'].find { |o| o['uuid'] == c['value'] }
values = Array.wrap(submitter_values[c['field_uuid']])
acc && values.include?(option['value'].presence || "Option #{field['options'].index(option)}")
when 'not_equal', 'does_not_contain'
field = fields_uuid_index[c['field_uuid']]
option = field['options'].find { |o| o['uuid'] == c['value'] }
values = Array.wrap(submitter_values[c['field_uuid']])
acc && values.exclude?(option['value'].presence || "Option #{field['options'].index(option)}")
field['conditions'].each_with_object([]) do |c, acc|
if c['operation'] == 'or'
acc.push(acc.pop || check_field_condition(c, submitter_values, fields_uuid_index))
else
acc
acc.push(check_field_condition(c, submitter_values, fields_uuid_index))
end
end.exclude?(false)
end
def check_field_condition(condition, submitter_values, fields_uuid_index)
case condition['action']
when 'empty', 'unchecked'
submitter_values[condition['field_uuid']].blank?
when 'not_empty', 'checked'
submitter_values[condition['field_uuid']].present?
when 'equal', 'contains'
field = fields_uuid_index[condition['field_uuid']]
option = field['options'].find { |o| o['uuid'] == condition['value'] }
values = Array.wrap(submitter_values[condition['field_uuid']])
values.include?(option['value'].presence || "#{I18n.t('option')} #{field['options'].index(option)}")
when 'not_equal', 'does_not_contain'
field = fields_uuid_index[condition['field_uuid']]
option = field['options'].find { |o| o['uuid'] == condition['value'] }
values = Array.wrap(submitter_values[condition['field_uuid']])
values.exclude?(option['value'].presence || "#{I18n.t('option')} #{field['options'].index(option)}")
else
true
end
end
def replace_default_variables(value, attrs, submission, with_time: false)
return value if value.in?([true, false])
return value if value.in?([true, false]) || value.is_a?(Numeric)
return if value.blank?
value.to_s.gsub(VARIABLE_REGEXP) do |e|
+2
View File
@@ -22,6 +22,8 @@ module TimeUtils
DEFAULT_DATE_FORMAT_US = 'MM/DD/YYYY'
DEFAULT_DATE_FORMAT = 'DD/MM/YYYY'
US_TIMEZONES = %w[EST CST MST PST HST AKDT].freeze
module_function
def timezone_abbr(timezone, time = Time.current)
+1
View File
@@ -6,6 +6,7 @@
"@babel/plugin-transform-runtime": "7.21.4",
"@babel/preset-env": "7.21.5",
"@babel/runtime": "7.21.5",
"@eid-easy/eideasy-widget": "^2.132.1",
"@github/catalyst": "^2.0.0-beta",
"@hotwired/turbo": "https://github.com/docusealco/turbo#main",
"@hotwired/turbo-rails": "^7.3.0",
+2 -1
View File
@@ -17,7 +17,8 @@ FactoryBot.define do
submission.template_submitters.each do |template_submitter|
create(:submitter, submission:,
account_id: submission.account_id,
uuid: template_submitter['uuid'])
uuid: template_submitter['uuid'],
created_at: submission.created_at)
end
end
end
-1
View File
@@ -5,7 +5,6 @@ FactoryBot.define do
submission
email { Faker::Internet.email }
name { Faker::Name.name }
phone { Faker::PhoneNumber.phone_number }
before(:create) do |submitter, _|
submitter.account_id = submitter.submission.account_id
+273 -9
View File
@@ -9,9 +9,13 @@ FactoryBot.define do
transient do
submitter_count { 1 }
only_field_types do
%w[text date checkbox radio signature number multiple select initials image file stamp cells phone payment]
end
except_field_types { [] }
end
after(:create) do |template, evaluator|
after(:create) do |template, ev|
blob = ActiveStorage::Blob.create_and_upload!(
io: Rails.root.join('spec/fixtures/sample-document.pdf').open,
filename: 'sample-document.pdf',
@@ -28,7 +32,7 @@ FactoryBot.define do
template.schema = [{ attachment_uuid: attachment.uuid, name: 'sample-document' }]
number_words = %w[first second third fourth fifth sixth seventh eighth ninth tenth]
template.submitters = Array.new(evaluator.submitter_count) do |i|
template.submitters = Array.new(ev.submitter_count) do |i|
{
'name' => "#{number_words[i]&.capitalize} Party",
'uuid' => SecureRandom.uuid
@@ -43,12 +47,13 @@ FactoryBot.define do
'name' => 'First Name',
'type' => 'text',
'required' => true,
'preferences' => {},
'areas' => [
{
'x' => 0.09027777777777778,
'y' => 0.1197252208047105,
'w' => 0.3069444444444444,
'h' => 0.03336604514229637,
'x' => 0.09273546006944444,
'y' => 0.1099851117387033,
'w' => 0.2701497395833333,
'h' => 0.0372705365913556,
'attachment_uuid' => attachment.uuid,
'page' => 0
}
@@ -57,12 +62,271 @@ FactoryBot.define do
{
'uuid' => SecureRandom.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',
'required' => true,
'areas' => []
'preferences' => {},
'areas' => [
{
'x' => 0.08611111111111111,
'y' => 0.3487183422870299,
'w' => 0.2,
'h' => 0.0707269155206287,
'attachment_uuid' => attachment.uuid,
'page' => 0
}
]
},
{
'uuid' => SecureRandom.uuid,
'submitter_uuid' => submitter['uuid'],
'name' => 'House number',
'type' => 'number',
'required' => true,
'preferences' => {},
'areas' => [
{
'x' => 0.08333333333333333,
'y' => 0.4582041442824252,
'w' => 0.2,
'h' => 0.02857142857142857,
'attachment_uuid' => attachment.uuid,
'page' => 0
}
]
},
{
'uuid' => SecureRandom.uuid,
'submitter_uuid' => submitter['uuid'],
'name' => 'Colors',
'type' => 'multiple',
'required' => true,
'preferences' => {},
'options' => [
{ 'value' => 'Red', 'uuid' => SecureRandom.uuid },
{ 'value' => 'Green', 'uuid' => SecureRandom.uuid },
{ 'value' => 'Blue', 'uuid' => SecureRandom.uuid }
],
'areas' => [
{
'x' => 0.45,
'y' => 0.1133998465080583,
'w' => 0.2,
'h' => 0.02857142857142857,
'attachment_uuid' => attachment.uuid,
'page' => 0
}
]
},
{
'uuid' => SecureRandom.uuid,
'submitter_uuid' => submitter['uuid'],
'name' => 'Gender',
'type' => 'select',
'required' => true,
'preferences' => {},
'options' => [
{ 'value' => 'Male', 'uuid' => SecureRandom.uuid },
{ 'value' => 'Female', 'uuid' => SecureRandom.uuid }
],
'areas' => [
{
'x' => 0.4513888888888889,
'y' => 0.1752954719877206,
'w' => 0.2,
'h' => 0.02857142857142857,
'attachment_uuid' => attachment.uuid,
'page' => 0
}
]
},
{
'uuid' => SecureRandom.uuid,
'submitter_uuid' => submitter['uuid'],
'name' => 'Initials',
'type' => 'initials',
'required' => true,
'preferences' => {},
'areas' => [
{
'x' => 0.4486111111111111,
'y' => 0.2273599386032233,
'w' => 0.1,
'h' => 0.02857142857142857,
'attachment_uuid' => attachment.uuid,
'page' => 0
}
]
},
{
'uuid' => SecureRandom.uuid,
'submitter_uuid' => submitter['uuid'],
'name' => 'Avatar',
'type' => 'image',
'required' => true,
'preferences' => {},
'areas' => [
{
'x' => 0.7180555555555556,
'y' => 0.1129547198772064,
'w' => 0.2,
'h' => 0.1414538310412574,
'attachment_uuid' => attachment.uuid,
'page' => 0
}
]
},
{
'uuid' => SecureRandom.uuid,
'submitter_uuid' => submitter['uuid'],
'name' => 'Attachment',
'type' => 'file',
'required' => true,
'preferences' => {},
'areas' => [
{
'x' => 0.7166666666666667,
'y' => 0.3020107444359171,
'w' => 0.2,
'h' => 0.02857142857142857,
'attachment_uuid' => attachment.uuid,
'page' => 0
}
]
},
{
'uuid' => SecureRandom.uuid,
'submitter_uuid' => submitter['uuid'],
'name' => 'Stamp',
'type' => 'stamp',
'required' => true,
'readonly' => true,
'preferences' => {},
'areas' => [
{
'x' => 0.7166666666666667,
'y' => 0.3771910974673829,
'w' => 0.2,
'h' => 0.0707269155206287,
'attachment_uuid' => attachment.uuid,
'page' => 0
}
]
},
{
'uuid' => SecureRandom.uuid,
'submitter_uuid' => submitter['uuid'],
'name' => 'Cell code',
'type' => 'cells',
'required' => true,
'preferences' => {},
'areas' => [
{
'x' => 0.4472222222222222,
'y' => 0.3530851880276286,
'w' => 0.2,
'h' => 0.02857142857142857,
'cell_w' => 0.04,
'attachment_uuid' => attachment.uuid,
'page' => 0
}
]
},
{
'uuid' => SecureRandom.uuid,
'submitter_uuid' => submitter['uuid'],
'name' => 'Payment',
'type' => 'payment',
'required' => true,
'preferences' => { 'currency' => 'EUR', 'price' => 1000 },
'areas' => [
{
'x' => 0.4486111111111111,
'y' => 0.43168073676132,
'w' => 0.2,
'h' => 0.02857142857142857,
'attachment_uuid' => attachment.uuid,
'page' => 0
}
]
},
{
'uuid' => SecureRandom.uuid,
'submitter_uuid' => submitter['uuid'],
'name' => 'Mobile Phone',
'type' => 'phone',
'required' => true,
'preferences' => {},
'areas' => [
{
'x' => 0.44443359375,
'y' => 0.3010283960092095,
'w' => 0.2,
'h' => 0.02857142857142857,
'attachment_uuid' => attachment.uuid,
'page' => 0
}
]
}
]
].select { |f| ev.only_field_types.include?(f['type']) && ev.except_field_types.exclude?(f['type']) }
fields
end
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

+4
View File
@@ -10,6 +10,7 @@ require 'capybara/cuprite'
require 'capybara/rspec'
require 'webmock/rspec'
require 'sidekiq/testing'
require 'signing_form_helper'
Sidekiq::Testing.fake!
@@ -50,6 +51,7 @@ RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
config.include Devise::Test::IntegrationHelpers
config.include SigningFormHelper
config.before(:each, type: :system) do
if ENV['HEADLESS'] == 'false'
@@ -67,3 +69,5 @@ RSpec.configure do |config|
Sidekiq::Testing.inline! if example.metadata[:sidekiq] == :inline
end
end
ActiveSupport.run_load_hooks(:rails_specs, self)
+1 -27
View File
@@ -176,33 +176,7 @@ describe 'Templates API', type: :request do
id: template.id,
slug: template.slug,
name: template.name,
fields: [
{
'uuid' => template.fields[0]['uuid'],
'submitter_uuid' => template.submitters[0]['uuid'],
'name' => 'First Name',
'type' => 'text',
'required' => true,
'areas' => [
{
'x' => 0.09027777777777778,
'y' => 0.1197252208047105,
'w' => 0.3069444444444444,
'h' => 0.03336604514229637,
'attachment_uuid' => template_attachment_uuid,
'page' => 0
}
]
},
{
'uuid' => template.fields[1]['uuid'],
'submitter_uuid' => template.submitters[0]['uuid'],
'name' => '',
'type' => 'signature',
'required' => true,
'areas' => []
}
],
fields: template.fields,
submitters: [
{
name: 'First Party',
+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
-2
View File
@@ -17,7 +17,5 @@ RSpec.describe 'Personalization' do
expect(page).to have_content('Completed Notification Email')
expect(page).to have_content('Documents Copy Email')
expect(page).to have_content('Company Logo')
expect(page).to have_content('Unlock with DocuSeal Pro')
expect(page).to have_content('Display your company name and logo when signing documents')
end
end
+791
View File
@@ -0,0 +1,791 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Signing Form', type: :system do
let(:account) { create(:account) }
let(:author) { create(:user, account:) }
context 'when the template form link is opened' do
let(:template) { create(:template, account:, author:, except_field_types: %w[phone payment stamp]) }
before do
visit start_form_path(slug: template.slug)
end
it 'shows the email step', type: :system do
expect(page).to have_content('You have been invited to submit a form')
expect(page).to have_content("Invited by #{account.name}")
expect(page).to have_field('Email', type: 'email')
expect(page).to have_button('Start')
end
it 'completes the form' do
# Submit's email step
fill_in 'Email', with: 'john.dou@example.com'
click_button 'Start'
# Text step
fill_in 'First Name', with: 'John'
click_button 'next'
# Date step
fill_in 'Birthday', with: I18n.l(20.years.ago, format: '%Y-%m-%d')
click_button 'next'
# Checkbox step
check 'Do you agree?'
click_button 'next'
# Radio step
choose 'Boy'
click_button 'next'
# Signature step
draw_canvas
click_button 'next'
# Number step
fill_in 'House number', with: '123'
click_button 'next'
# Multiple choice step
%w[Red Blue].each { |color| check color }
click_button 'next'
# Select step
select 'Male', from: 'Gender'
click_button 'next'
# Initials step
draw_canvas
click_button 'next'
# Image step
find('#dropzone').click
find('input[type="file"]', visible: false).attach_file(Rails.root.join('spec/fixtures/sample-image.png'))
click_button 'next'
# File step
find('#dropzone').click
find('input[type="file"]', visible: false).attach_file(Rails.root.join('spec/fixtures/sample-document.pdf'))
click_button 'next'
# Cell step
fill_in 'Cell code', with: '123'
click_on 'Complete'
expect(page).to have_button('Download')
expect(page).to have_content('Document has been signed!')
submitter = template.submissions.last.submitters.last
expect(submitter.email).to eq('john.dou@example.com')
expect(submitter.ip).to eq('127.0.0.1')
expect(submitter.ua).to be_present
expect(submitter.opened_at).to be_present
expect(submitter.completed_at).to be_present
expect(submitter.declined_at).to be_nil
expect(field_value(submitter, 'First Name')).to eq 'John'
expect(field_value(submitter, 'Birthday')).to eq 20.years.ago.strftime('%Y-%m-%d')
expect(field_value(submitter, 'Do you agree?')).to be_truthy
expect(field_value(submitter, 'First child')).to eq 'Boy'
expect(field_value(submitter, 'Signature')).to be_present
expect(field_value(submitter, 'House number')).to eq 123
expect(field_value(submitter, 'Colors')).to contain_exactly('Red', 'Blue')
expect(field_value(submitter, 'Gender')).to eq 'Male'
expect(field_value(submitter, 'Initials')).to be_present
expect(field_value(submitter, 'Avatar')).to be_present
expect(field_value(submitter, 'Attachment')).to be_present
expect(field_value(submitter, 'Cell code')).to eq '123'
end
end
context 'when the submitter form link is opened' do
let(:template) { create(:template, account:, author:, except_field_types: %w[phone payment stamp]) }
let(:submission) { create(:submission, template:) }
let(:submitter) do
create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:, email: 'robin@example.com')
end
before do
visit submit_form_path(slug: submitter.slug)
end
it 'complete the form' do
# Text step
fill_in 'First Name', with: 'John'
click_button 'next'
# Date step
fill_in 'Birthday', with: I18n.l(20.years.ago, format: '%Y-%m-%d')
click_button 'next'
# Checkbox step
check 'Do you agree?'
click_button 'next'
# Radio step
choose 'Boy'
click_button 'next'
# Signature step
draw_canvas
click_button 'next'
# Number step
fill_in 'House number', with: '123'
click_button 'next'
# Multiple choice step
%w[Red Blue].each { |color| check color }
click_button 'next'
# Select step
select 'Male', from: 'Gender'
click_button 'next'
# Initials step
draw_canvas
click_button 'next'
# Image step
find('#dropzone').click
find('input[type="file"]', visible: false).attach_file(Rails.root.join('spec/fixtures/sample-image.png'))
click_button 'next'
# File step
find('#dropzone').click
find('input[type="file"]', visible: false).attach_file(Rails.root.join('spec/fixtures/sample-document.pdf'))
click_button 'next'
# Cell step
fill_in 'Cell code', with: '123'
click_on 'Complete'
expect(page).to have_button('Download')
expect(page).to have_content('Document has been signed!')
submitter.reload
expect(submitter.email).to eq 'robin@example.com'
expect(submitter.ip).to eq('127.0.0.1')
expect(submitter.ua).to be_present
expect(submitter.opened_at).to be_present
expect(submitter.completed_at).to be_present
expect(submitter.declined_at).to be_nil
expect(field_value(submitter, 'First Name')).to eq 'John'
expect(field_value(submitter, 'Birthday')).to eq 20.years.ago.strftime('%Y-%m-%d')
expect(field_value(submitter, 'Do you agree?')).to be_truthy
expect(field_value(submitter, 'First child')).to eq 'Boy'
expect(field_value(submitter, 'Signature')).to be_present
expect(field_value(submitter, 'House number')).to eq 123
expect(field_value(submitter, 'Colors')).to contain_exactly('Red', 'Blue')
expect(field_value(submitter, 'Gender')).to eq 'Male'
expect(field_value(submitter, 'Initials')).to be_present
expect(field_value(submitter, 'Avatar')).to be_present
expect(field_value(submitter, 'Attachment')).to be_present
expect(field_value(submitter, 'Cell code')).to eq '123'
end
end
context 'when the text step' do
let(:template) { create(:template, account:, author:, only_field_types: %w[text]) }
let(:submission) { create(:submission, template:) }
let(:submitter) do
create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:)
end
it 'completes the form if the field is filled' do
visit submit_form_path(slug: submitter.slug)
input = find_field('First Name')
expect(input[:required]).to be_truthy
expect(input[:placeholder]).to eq 'Type here...'
fill_in 'First Name', with: 'Mary'
click_button 'Complete'
expect(page).to have_content('Form has been completed!')
submitter.reload
expect(submitter.completed_at).to be_present
expect(field_value(submitter, 'First Name')).to eq 'Mary'
end
it 'toggle multiple text button' do
visit submit_form_path(slug: submitter.slug)
input = find_field('First Name')
expect(input.tag_name).to eq('input')
find(:css, 'div[data-tip="Toggle Multiline Text"]').click
input = find_field('First Name')
expect(input.tag_name).to eq('textarea')
expect(page).not_to have_selector(:css, 'div[data-tip="Toggle Multiline Text"]')
fill_in 'First Name', with: 'Very long text'
click_button 'Complete'
expect(page).to have_content('Form has been completed!')
submitter.reload
expect(field_value(submitter, 'First Name')).to eq 'Very long text'
expect(submitter.completed_at).to be_present
end
end
context 'when the date step' do
let(:template) { create(:template, account:, author:, only_field_types: %w[date]) }
let(:submission) { create(:submission, template:) }
let(:submitter) do
create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:)
end
it 'completes the form if the field is filled' do
visit submit_form_path(slug: submitter.slug)
input = find_field('Birthday')
expect(input[:required]).to be_truthy
fill_in 'Birthday', with: I18n.l(25.years.ago, format: '%Y-%m-%d')
click_button 'Complete'
expect(page).to have_content('Form has been completed!')
submitter.reload
expect(submitter.completed_at).to be_present
expect(field_value(submitter, 'Birthday')).to eq 25.years.ago.strftime('%Y-%m-%d')
end
it 'pre-fills the current date into the form field' do
visit submit_form_path(slug: submitter.slug)
input = find_field('Birthday')
expect(input[:value]).to eq ''
click_button 'Set Today'
input = find_field('Birthday')
expect(input[:value]).to eq Time.zone.now.strftime('%Y-%m-%d')
click_button 'Complete'
expect(page).to have_content('Form has been completed!')
submitter.reload
expect(submitter.completed_at).to be_present
expect(field_value(submitter, 'Birthday')).to eq Time.zone.now.strftime('%Y-%m-%d')
end
end
context 'when the checkbox step' do
let(:template) { create(:template, account:, author:, only_field_types: %w[checkbox]) }
let(:submission) { create(:submission, template:) }
let(:submitter) do
create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:)
end
it 'completes the form if the checkbox is checked' do
visit submit_form_path(slug: submitter.slug)
check 'Do you agree?'
click_button 'Complete'
expect(page).to have_content('Form has been completed!')
submitter.reload
expect(submitter.completed_at).to be_present
expect(field_value(submitter, 'Do you agree?')).to be true
end
end
context 'when the radio step' do
let(:template) { create(:template, account:, author:, only_field_types: %w[radio]) }
let(:submission) { create(:submission, template:) }
let(:submitter) do
create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:)
end
it 'completes the form if the checkbox is checked' do
visit submit_form_path(slug: submitter.slug)
%w[Girl Boy].map { |v| find_field(v) }.each { |input| expect(input[:required]).to be_truthy }
choose 'Boy'
click_button 'Complete'
expect(page).to have_content('Form has been completed!')
submitter.reload
expect(submitter.completed_at).to be_present
expect(field_value(submitter, 'First child')).to eq 'Boy'
end
end
context 'when the signature step' do
let(:template) { create(:template, account:, author:, only_field_types: %w[signature]) }
let(:submission) { create(:submission, template:) }
let(:submitter) do
create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:)
end
it 'completes the form if the canvas is drawn' do
visit submit_form_path(slug: submitter.slug)
find('#expand_form_button').click
draw_canvas
click_button 'Sign and Complete'
expect(page).to have_content('Document has been signed!')
submitter.reload
expect(submitter.completed_at).to be_present
expect(field_value(submitter, 'Signature')).to be_present
end
it 'completes the form if the canvas is typed' do
visit submit_form_path(slug: submitter.slug)
find('#expand_form_button').click
click_link 'Type'
fill_in 'signature_text_input', with: 'John Doe'
click_button 'Sign and Complete'
expect(page).to have_content('Document has been signed!')
submitter.reload
expect(submitter.completed_at).to be_present
expect(field_value(submitter, 'Signature')).to be_present
end
end
context 'when the number step' do
let(:template) { create(:template, account:, author:, only_field_types: %w[number]) }
let(:submission) { create(:submission, template:) }
let(:submitter) do
create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:)
end
it 'completes the form if the field is filled' do
visit submit_form_path(slug: submitter.slug)
input = find_field('House number')
expect(input[:required]).to be_truthy
expect(input[:placeholder]).to eq 'Type here...'
fill_in 'House number', with: '4'
click_button 'Complete'
expect(page).to have_content('Form has been completed!')
submitter.reload
expect(submitter.completed_at).to be_present
expect(field_value(submitter, 'House number')).to eq 4
end
end
context 'when the multiple choice step' do
let(:template) { create(:template, account:, author:, only_field_types: %w[multiple]) }
let(:submission) { create(:submission, template:) }
let(:submitter) do
create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:)
end
it 'completes the form if the multiple choice is checked' do
visit submit_form_path(slug: submitter.slug)
%w[Red Green].each { |color| check color }
click_button 'Complete'
expect(page).to have_content('Form has been completed!')
submitter.reload
expect(submitter.completed_at).to be_present
expect(field_value(submitter, 'Colors')).to contain_exactly('Red', 'Green')
end
end
context 'when the select step' do
let(:template) { create(:template, account:, author:, only_field_types: %w[select]) }
let(:submission) { create(:submission, template:) }
let(:submitter) do
create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:)
end
it 'completes the form if the multiple choice is checked' do
visit submit_form_path(slug: submitter.slug)
select 'Female', from: 'Gender'
click_button 'Complete'
expect(page).to have_content('Form has been completed!')
submitter.reload
expect(submitter.completed_at).to be_present
expect(field_value(submitter, 'Gender')).to eq 'Female'
end
end
context 'when the initials step' do
let(:template) { create(:template, account:, author:, only_field_types: %w[initials]) }
let(:submission) { create(:submission, template:) }
let(:submitter) do
create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:)
end
it 'completes the form if the canvas is typed' do
visit submit_form_path(slug: submitter.slug)
find('#expand_form_button').click
fill_in 'initials_text_input', with: 'John Doe'
click_button 'Complete'
expect(page).to have_content('Document has been signed!')
submitter.reload
expect(submitter.completed_at).to be_present
expect(field_value(submitter, 'Initials')).to be_present
end
it 'completes the form if the canvas is drawn' do
visit submit_form_path(slug: submitter.slug)
find('#expand_form_button').click
click_link 'Draw'
draw_canvas
click_button 'Complete'
expect(page).to have_content('Document has been signed!')
submitter.reload
expect(submitter.completed_at).to be_present
expect(field_value(submitter, 'Initials')).to be_present
end
it 'completes the form if the initials is uploaded' do
visit submit_form_path(slug: submitter.slug)
find('#expand_form_button').click
find('span[data-tip="Click to upload"]').click
find('input[type="file"]', visible: false).attach_file(Rails.root.join('spec/fixtures/sample-image.png'))
click_button 'Complete'
expect(page).to have_content('Document has been signed!')
submitter.reload
expect(submitter.completed_at).to be_present
expect(field_value(submitter, 'Initials')).to be_present
end
end
context 'when the image step' do
let(:template) { create(:template, account:, author:, only_field_types: %w[image]) }
let(:submission) { create(:submission, template:) }
let(:submitter) do
create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:)
end
it 'completes the form if the image is uploaded' do
visit submit_form_path(slug: submitter.slug)
find('#expand_form_button').click
find('#dropzone').click
find('input[type="file"]', visible: false).attach_file(Rails.root.join('spec/fixtures/sample-image.png'))
click_button 'Complete'
expect(page).to have_content('Form has been completed!')
submitter.reload
expect(submitter.completed_at).to be_present
expect(field_value(submitter, 'Avatar')).to be_present
end
end
context 'when the file step' do
let(:template) { create(:template, account:, author:, only_field_types: %w[file]) }
let(:submission) { create(:submission, template:) }
let(:submitter) do
create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:)
end
it 'completes the form if the file is uploaded' do
visit submit_form_path(slug: submitter.slug)
find('#expand_form_button').click
find('#dropzone').click
find('input[type="file"]', visible: false).attach_file(Rails.root.join('spec/fixtures/sample-document.pdf'))
click_button 'Complete'
expect(page).to have_content('Form has been completed!')
submitter.reload
expect(submitter.completed_at).to be_present
expect(field_value(submitter, 'Attachment')).to be_present
end
end
context 'when the cells step' do
let(:template) { create(:template, account:, author:, only_field_types: %w[cells]) }
let(:submission) { create(:submission, template:) }
let(:submitter) do
create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:)
end
it 'completes the form if the field is filled' do
visit submit_form_path(slug: submitter.slug)
input = find_field('Cell code')
expect(input[:required]).to be_truthy
expect(input[:placeholder]).to eq 'Type here...'
fill_in 'Cell code', with: '456'
click_button 'Complete'
expect(page).to have_content('Form has been completed!')
submitter.reload
expect(submitter.completed_at).to be_present
expect(field_value(submitter, 'Cell code')).to eq '456'
end
end
context 'when the field with conditions' do
let(:template) { create(:template, account:, author:, only_field_types: ['text']) }
let(:submission) { create(:submission, :with_submitters, template:) }
let(:template_attachment) { template.schema.first }
let(:template_submitter) { submission.template_submitters.first }
let(:submitter) { submission.submitters.first }
let(:fields) do
[
{
'uuid' => 'da7e0d56-fdb0-441a-bbed-d0f6f2e10fd6',
'submitter_uuid' => submitter.uuid,
'name' => 'Full Name',
'type' => 'text',
'required' => false,
'preferences' => {},
'conditions' => [],
'areas' => [
{
'x' => 0.1117351575121163,
'y' => 0.08950650415231329,
'w' => 0.2,
'h' => 0.02857142857142857,
'attachment_uuid' => template_attachment['attachment_uuid'],
'page' => 0
}
]
},
{
'uuid' => 'd32ad52a-8f6b-4e32-b0d6-6258fb47440b',
'submitter_uuid' => submitter.uuid,
'name' => 'Email',
'type' => 'text',
'required' => false,
'preferences' => {},
'conditions' => [],
'validation' => { 'pattern' => '^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$' },
'areas' => [
{
'x' => 0.1097914983844911,
'y' => 0.1417641720258019,
'w' => 0.2,
'h' => 0.02857142857142857,
'attachment_uuid' => template_attachment['attachment_uuid'],
'page' => 0
}
]
},
{
'uuid' => 'c6e013ae-f9f6-4b3a-ad33-b7e772a0a49f',
'submitter_uuid' => submitter.uuid,
'name' => 'Phone',
'type' => 'text',
'required' => false,
'preferences' => {},
'areas' => [
{
'x' => 0.1100060581583199,
'y' => 0.2553160344676159,
'w' => 0.2,
'h' => 0.02857142857142857,
'attachment_uuid' => template_attachment['attachment_uuid'],
'page' => 0
}
]
},
{
'uuid' => '64523936-22fd-41f8-b997-ede8fbe467cc',
'submitter_uuid' => submitter.uuid,
'name' => 'Comment',
'type' => 'text',
'required' => false,
'preferences' => {},
'conditions' => [
{ 'field_uuid' => 'da7e0d56-fdb0-441a-bbed-d0f6f2e10fd6', 'action' => 'not_empty' },
{ 'field_uuid' => 'd32ad52a-8f6b-4e32-b0d6-6258fb47440b', 'action' => 'not_empty' },
{ 'field_uuid' => 'c6e013ae-f9f6-4b3a-ad33-b7e772a0a49f', 'action' => 'not_empty', 'operation' => 'or' }
],
'areas' => [
{
'x' => 0.1145875403877221,
'y' => 0.1982961365432846,
'w' => 0.2,
'h' => 0.02857142857142857,
'attachment_uuid' => template_attachment['attachment_uuid'],
'page' => 0
}
]
}
]
end
before do
template.update(fields:)
submission.update(template_fields: fields)
end
it 'completes the form and saves the conditional field when all required fields are filled' do
visit submit_form_path(slug: submitter.slug)
fill_in 'Full Name (optional)', with: 'John Doe'
click_button 'next'
fill_in 'Email (optional)', with: 'john.due@example.com'
click_button 'next'
fill_in 'Phone (optional)', with: '+1 (773) 229-8825'
click_button 'next'
fill_in 'Comment', with: 'This is a comment'
click_button 'Complete'
expect(page).to have_content('Form has been completed!')
submitter.reload
expect(submitter.completed_at).to be_present
expect(field_value(submitter, 'Full Name')).to eq 'John Doe'
expect(field_value(submitter, 'Email')).to eq 'john.due@example.com'
expect(field_value(submitter, 'Phone')).to eq '+1 (773) 229-8825'
expect(field_value(submitter, 'Comment')).to eq 'This is a comment'
end
it 'completes the form and saves the conditional field when minimum required fields are filled' do
visit submit_form_path(slug: submitter.slug)
fill_in 'Full Name (optional)', with: 'John Doe'
click_button 'next'
fill_in 'Email (optional)', with: 'john.due@example.com'
click_button 'next'
fill_in 'Phone (optional)', with: ''
click_button 'next'
fill_in 'Comment', with: 'This is a comment'
click_button 'Complete'
expect(page).to have_content('Form has been completed!')
submitter.reload
expect(submitter.completed_at).to be_present
expect(field_value(submitter, 'Full Name')).to eq 'John Doe'
expect(field_value(submitter, 'Email')).to eq 'john.due@example.com'
expect(field_value(submitter, 'Phone')).to be_empty
expect(field_value(submitter, 'Comment')).to eq 'This is a comment'
end
it 'completes the form without saving the conditional field when not enough fields are filled' do
visit submit_form_path(slug: submitter.slug)
fill_in 'Full Name (optional)', with: 'Jane Doe'
click_button 'next'
fill_in 'Email (optional)', with: ''
click_button 'next'
fill_in 'Phone (optional)', with: ''
click_button 'Complete'
expect(page).to have_content('Form has been completed!')
submitter.reload
expect(submitter.completed_at).to be_present
expect(field_value(submitter, 'Full Name')).to eq 'Jane Doe'
expect(field_value(submitter, 'Email')).to be_empty
expect(field_value(submitter, 'Phone')).to be_empty
expect(field_value(submitter, 'Comment')).to be_nil
end
it 'completes the form without saving the conditional field when only partial fields are filled' do
visit submit_form_path(slug: submitter.slug)
fill_in 'Full Name (optional)', with: ''
click_button 'next'
fill_in 'Email (optional)', with: 'john.due@example.com'
click_button 'next'
fill_in 'Phone (optional)', with: '+1 (773) 229-8825'
click_button 'Complete'
expect(page).to have_content('Form has been completed!')
submitter.reload
expect(submitter.completed_at).to be_present
expect(field_value(submitter, 'Full Name')).to be_empty
expect(field_value(submitter, 'Email')).to eq 'john.due@example.com'
expect(field_value(submitter, 'Phone')).to eq '+1 (773) 229-8825'
expect(field_value(submitter, 'Comment')).to be_nil
end
end
it 'sends completed email' do
template = create(:template, account:, author:, only_field_types: %w[text signature])
submission = create(:submission, template:)
submitter = create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:)
visit submit_form_path(slug: submitter.slug)
fill_in 'First Name', with: 'Adam'
click_on 'next'
click_link 'Type'
fill_in 'signature_text_input', with: 'Adam'
expect do
click_on 'Sign and Complete'
end.to change(ProcessSubmitterCompletionJob.jobs, :size).by(1)
end
end
-87
View File
@@ -1,87 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Submit Form' do
let(:account) { create(:account) }
let(:user) { create(:user, account:) }
let(:template) { create(:template, account:, author: user) }
before do
sign_in(user)
end
context 'when initialized by shared link' do
before do
visit start_form_path(slug: template.slug)
end
it 'shows start form page' do
expect(page).to have_content('You have been invited to submit a form')
expect(page).to have_content(template.name)
expect(page).to have_content("Invited by #{template.account.name}")
end
it 'complete the form' do
fill_in 'Email', with: 'john.dou@example.com'
click_button 'Start'
fill_in 'First Name', with: 'Adam'
click_on 'next'
click_on 'type_text_button'
fill_in 'signature_text_input', with: 'Adam'
expect do
click_on 'Sign and Complete'
end.not_to(change(Submitter, :count))
submitter = Submitter.find_by(email: 'john.dou@example.com')
expect(page).to have_button('Download')
expect(submitter.email).to eq('john.dou@example.com')
expect(submitter.ip).to eq('127.0.0.1')
expect(submitter.ua).to be_present
expect(submitter.opened_at).to be_present
expect(submitter.completed_at).to be_present
expect(submitter.values.values).to include('Adam')
end
end
context 'when initialized by shared email address' do
let(:submission) { create(:submission, template:, created_by_user: user) }
let(:submitters) { template.submitters.map { |s| create(:submitter, submission:, uuid: s['uuid']) } }
let(:submitter) { submitters.first }
before do
visit submit_form_path(slug: submitter.slug)
end
it 'completes the form' do
fill_in 'First Name', with: 'Sally'
click_on 'next'
click_on 'type_text_button'
fill_in 'signature_text_input', with: 'Sally'
click_on 'Sign and Complete'
submitter.reload
expect(page).to have_button('Download')
expect(submitter.ip).to eq('127.0.0.1')
expect(submitter.ua).to be_present
expect(submitter.opened_at).to be_present
expect(submitter.completed_at).to be_present
expect(submitter.values.values).to include('Sally')
end
it 'sends completed email' do
fill_in 'First Name', with: 'Adam'
click_on 'next'
click_on 'type_text_button'
fill_in 'signature_text_input', with: 'Adam'
expect do
click_on 'Sign and Complete'
end.to change(ProcessSubmitterCompletionJob.jobs, :size).by(1)
end
end
end
+95 -1
View File
@@ -5,7 +5,7 @@ require 'rails_helper'
RSpec.describe 'Template' do
let!(:account) { create(:account) }
let!(:user) { create(:user, account:) }
let!(:template) { create(:template, account:, author: user) }
let!(:template) { create(:template, account:, author: user, except_field_types: %w[phone payment]) }
before do
sign_in(user)
@@ -140,4 +140,98 @@ RSpec.describe 'Template' do
end
end
end
context 'when filtering submissions' do
let(:second_user) { create(:user, account:) }
it 'displays only submissions by the selected author' do
first_user_submissions = create_list(:submission, 5, :with_submitters, template:, created_by_user: user)
second_user_submissions = create_list(:submission, 6, :with_submitters, template:, created_by_user: second_user)
visit template_path(template)
(first_user_submissions + second_user_submissions).map(&:submitters).flatten.last(10).uniq.each do |submitter|
expect(page).to have_content(submitter.name)
end
page.find('.dropdown', text: 'Filter').click
click_link 'Author'
within '#modal' do
select second_user.full_name, from: 'author'
click_button 'Apply'
end
second_user_submissions.map(&:submitters).flatten.uniq.each do |submitter|
expect(page).to have_content(submitter.name)
end
first_user_submissions.map(&:submitters).flatten.uniq.each do |submitter|
expect(page).not_to have_content(submitter.name)
end
end
it 'displays submissions created within the selected date range' do
last_week_submissions = create_list(:submission, 5, :with_submitters, template:, created_by_user: user,
created_at: 9.days.ago)
this_week_submissions = create_list(:submission, 6, :with_submitters, template:, created_by_user: user,
created_at: 5.days.ago)
visit template_path(template)
(last_week_submissions + this_week_submissions).map(&:submitters).flatten.last(10).uniq.each do |submitter|
expect(page).to have_content(submitter.name)
end
page.find('.dropdown', text: 'Filter').click
click_link 'Created at'
within '#modal' do
fill_in 'From', with: I18n.l(10.days.ago, format: '%Y-%m-%d')
fill_in 'To', with: I18n.l(6.days.ago, format: '%Y-%m-%d')
click_button 'Apply'
end
last_week_submissions.map(&:submitters).flatten.uniq.each do |submitter|
expect(page).to have_content(submitter.name)
end
this_week_submissions.map(&:submitters).flatten.uniq.each do |submitter|
expect(page).not_to have_content(submitter.name)
end
end
it 'displays submissions completed within the selected date range' do
last_week_submissions = create_list(:submission, 5, :with_submitters, template:, created_by_user: user)
this_week_submissions = create_list(:submission, 6, :with_submitters, template:, created_by_user: user)
last_week_submissions.map(&:submitters).flatten.each do |submitter|
submitter.update!(completed_at: rand(6..10).days.ago)
end
this_week_submissions.map(&:submitters).flatten.each do |submitter|
submitter.update!(completed_at: rand(2..5).days.ago)
end
visit template_path(template)
(last_week_submissions + this_week_submissions).map(&:submitters).flatten.last(10).uniq.each do |submitter|
expect(page).to have_content(submitter.name)
end
page.find('.dropdown', text: 'Filter').click
click_link 'Completed at'
within '#modal' do
fill_in 'From', with: I18n.l(5.days.ago, format: '%Y-%m-%d')
fill_in 'To', with: I18n.l(1.day.ago, format: '%Y-%m-%d')
click_button 'Apply'
end
this_week_submissions.map(&:submitters).flatten.uniq.each do |submitter|
expect(page).to have_content(submitter.name)
end
last_week_submissions.map(&:submitters).flatten.uniq.each do |submitter|
expect(page).not_to have_content(submitter.name)
end
end
end
end
+208 -1
View File
@@ -259,6 +259,11 @@
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz#5b3329c9a58803d5df425e5785865881a81ca48d"
integrity sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==
"@babel/helper-string-parser@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c"
integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==
"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1":
version "7.19.1"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2"
@@ -269,6 +274,11 @@
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db"
integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==
"@babel/helper-validator-identifier@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7"
integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==
"@babel/helper-validator-option@^7.21.0":
version "7.21.0"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz#8224c7e13ace4bafdc4004da2cf064ef42673180"
@@ -317,6 +327,13 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.8.tgz#642af7d0333eab9c0ad70b14ac5e76dbde7bfdf8"
integrity sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA==
"@babel/parser@^7.23.5":
version "7.26.2"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.2.tgz#fd7b6f487cfea09889557ef5d4eeb9ff9a5abd11"
integrity sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==
dependencies:
"@babel/types" "^7.26.0"
"@babel/parser@^7.25.0", "@babel/parser@^7.25.3":
version "7.25.3"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.25.3.tgz#91fb126768d944966263f0657ab222a642b82065"
@@ -1005,11 +1022,44 @@
"@babel/helper-validator-identifier" "^7.24.7"
to-fast-properties "^2.0.0"
"@babel/types@^7.26.0":
version "7.26.0"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.0.tgz#deabd08d6b753bc8e0f198f8709fb575e31774ff"
integrity sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==
dependencies:
"@babel/helper-string-parser" "^7.25.9"
"@babel/helper-validator-identifier" "^7.25.9"
"@discoveryjs/json-ext@0.5.7", "@discoveryjs/json-ext@^0.5.0":
version "0.5.7"
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
"@eid-easy/eideasy-browser-client@2.104.1":
version "2.104.1"
resolved "https://registry.yarnpkg.com/@eid-easy/eideasy-browser-client/-/eideasy-browser-client-2.104.1.tgz#e492e0fc8ef1ff470c66522d530d5636264f30b3"
integrity sha512-d4fgqF5U+pWuPpfgbb/+BxI2CYThkbviAfl9NbLoKMOMO4bh7HZFxLdW5uU2O8yIeldGFunJWxEbMTLv/2PlTA==
dependencies:
axios "1.7.8"
jsencrypt "3.2.1"
lodash "^4.17.21"
serialize-error "^9.1.1"
"@eid-easy/eideasy-widget@^2.132.1":
version "2.132.1"
resolved "https://registry.yarnpkg.com/@eid-easy/eideasy-widget/-/eideasy-widget-2.132.1.tgz#634c077c55d7e582846dbeae47f6f37796b76bd6"
integrity sha512-bZNdKsxja4iIUm6B4x6pOyiBdoMNNUB3615oJgNUWallrfeiIbBQxLwG+m5nmivmHMgdaIQylmtfrhc6e7kgYA==
dependencies:
"@eid-easy/eideasy-browser-client" "2.104.1"
core-js "^3.8.3"
i18n-iso-countries "^6.7.0"
lodash.defaultsdeep "^4.6.1"
signature_pad "^4.1.4"
vue "^2.6.14"
vue-custom-element "^3.2.14"
vue-i18n "^8.23.0"
vue-select "^3.11.2"
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.3.0":
version "4.4.0"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
@@ -1440,6 +1490,17 @@
"@vue/compiler-core" "3.3.4"
"@vue/shared" "3.3.4"
"@vue/compiler-sfc@2.7.16":
version "2.7.16"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-2.7.16.tgz#ff81711a0fac9c68683d8bb00b63f857de77dc83"
integrity sha512-KWhJ9k5nXuNtygPU7+t1rX6baZeqOYLEforUPjgNDBnLicfHCoi48H87Q8XyLZOrNNsmhuwKqtpDQWjEFe6Ekg==
dependencies:
"@babel/parser" "^7.23.5"
postcss "^8.4.14"
source-map "^0.6.1"
optionalDependencies:
prettier "^1.18.2 || ^2.0.0"
"@vue/compiler-sfc@3.3.4":
version "3.3.4"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz#b19d942c71938893535b46226d602720593001df"
@@ -1826,6 +1887,11 @@ async@~3.2.3:
resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66"
integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
autocompleter@^9.1.0:
version "9.1.0"
resolved "https://registry.yarnpkg.com/autocompleter/-/autocompleter-9.1.0.tgz#c7248a8cc0c58376d0969734c40e29626d950f04"
@@ -1848,6 +1914,15 @@ available-typed-arrays@^1.0.5:
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==
axios@1.7.8:
version "1.7.8"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.8.tgz#1997b1496b394c21953e68c14aaa51b7b5de3d6e"
integrity sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
babel-eslint@^10.1.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.1.0.tgz#6968e568a910b78fb3779cdd8b6ac2f479943232"
@@ -2131,6 +2206,13 @@ colorette@^2.0.10, colorette@^2.0.14:
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a"
integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
commander@^10.0.1:
version "10.0.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06"
@@ -2238,6 +2320,11 @@ core-js-compat@^3.25.1:
dependencies:
browserslist "^4.21.5"
core-js@^3.8.3:
version "3.39.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.39.0.tgz#57f7647f4d2d030c32a72ea23a0555b2eaa30f83"
integrity sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==
core-util-is@~1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
@@ -2404,6 +2491,11 @@ csso@^5.0.5:
dependencies:
css-tree "~2.2.0"
csstype@^3.1.0:
version "3.1.3"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
csstype@^3.1.1:
version "3.1.2"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
@@ -2490,6 +2582,11 @@ define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0:
has-property-descriptors "^1.0.0"
object-keys "^1.1.1"
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
depd@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
@@ -2510,6 +2607,11 @@ detect-node@^2.0.4:
resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1"
integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==
diacritics@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/diacritics/-/diacritics-1.3.0.tgz#3efa87323ebb863e6696cebb0082d48ff3d6f7a1"
integrity sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==
didyoumean@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037"
@@ -3143,6 +3245,11 @@ follow-redirects@^1.0.0:
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
follow-redirects@^1.15.6:
version "1.15.9"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
for-each@^0.3.3:
version "0.3.3"
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
@@ -3150,6 +3257,15 @@ for-each@^0.3.3:
dependencies:
is-callable "^1.1.3"
form-data@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.1.tgz#ba1076daaaa5bfd7e99c1a6cb02aa0a5cff90d48"
integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
forwarded@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
@@ -3446,6 +3562,13 @@ human-signals@^2.1.0:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
i18n-iso-countries@^6.7.0:
version "6.8.0"
resolved "https://registry.yarnpkg.com/i18n-iso-countries/-/i18n-iso-countries-6.8.0.tgz#eebbc75594c6832aff86176c1bb38daa133d6dfd"
integrity sha512-jJs/+CA6+VUICFxqGcB0vFMERGfhfvyNk+8Vb9EagSZkl7kSpm/kT0VyhvzM/zixDWEV/+oN9L7v/GT9BwzoGg==
dependencies:
diacritics "1.3.0"
iconv-lite@0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@@ -3769,6 +3892,11 @@ js-yaml@^4.1.0:
dependencies:
argparse "^2.0.1"
jsencrypt@3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/jsencrypt/-/jsencrypt-3.2.1.tgz#09766983cc760088ff26b12fe7e574252af97727"
integrity sha512-k1sD5QV0KPn+D8uG9AdGzTQuamt82QZ3A3l6f7TRwMU6Oi2Vg0BsL+wZIQBONcraO1pc78ExMdvmBBJ8WhNYUA==
jsesc@^2.5.1:
version "2.5.2"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
@@ -3883,6 +4011,11 @@ lodash.debounce@^4.0.8:
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
lodash.defaultsdeep@^4.6.1:
version "4.6.1"
resolved "https://registry.yarnpkg.com/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz#512e9bd721d272d94e3d3a63653fa17516741ca6"
integrity sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==
lodash.get@^4.0:
version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
@@ -4016,7 +4149,7 @@ mime-db@1.52.0, "mime-db@>= 1.43.0 < 2":
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34:
mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34:
version "2.1.35"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
@@ -4099,6 +4232,11 @@ nanoid@^3.3.6:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
nanoid@^3.3.7:
version "3.3.8"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@@ -4352,6 +4490,11 @@ picocolors@^1.0.1:
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1"
integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==
picocolors@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
@@ -4661,11 +4804,25 @@ postcss@^8, postcss@^8.1.10, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.31:
picocolors "^1.0.0"
source-map-js "^1.0.2"
postcss@^8.4.14:
version "8.4.49"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.49.tgz#4ea479048ab059ab3ae61d082190fabfd994fe19"
integrity sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==
dependencies:
nanoid "^3.3.7"
picocolors "^1.1.1"
source-map-js "^1.2.1"
prelude-ls@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
"prettier@^1.18.2 || ^2.0.0":
version "2.8.8"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
process-nextick-args@~2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
@@ -4679,6 +4836,11 @@ proxy-addr@~2.0.7:
forwarded "0.2.0"
ipaddr.js "1.9.1"
proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
punycode@^2.1.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
@@ -5016,6 +5178,13 @@ send@0.18.0:
range-parser "~1.2.1"
statuses "2.0.1"
serialize-error@^9.1.1:
version "9.1.1"
resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-9.1.1.tgz#b66744b6287b538996a153103e67af7179d0831a"
integrity sha512-6uZQLGyUkNA4N+Zii9fYukmNu9PEA1F5rqcwXzN/3LtBjwl2dFBbVZ1Zyn08/CGkB4H440PIemdOQBt1Wvjbrg==
dependencies:
type-fest "^2.5.3"
serialize-javascript@^6.0.0, serialize-javascript@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c"
@@ -5102,6 +5271,11 @@ signal-exit@^3.0.2, signal-exit@^3.0.3:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
signature_pad@^4.1.4:
version "4.2.0"
resolved "https://registry.yarnpkg.com/signature_pad/-/signature_pad-4.2.0.tgz#7513cee8cb8afd6594d871c61cf4d61420601422"
integrity sha512-YLWysmaUBaC5wosAKkgbX7XI+LBv2w5L0QUcI6Jc4moHYzv9BUBJtAyNLpWzHjtjKTeWOH6bfP4a4pzf0UinfQ==
signature_pad@^4.1.5:
version "4.1.5"
resolved "https://registry.yarnpkg.com/signature_pad/-/signature_pad-4.1.5.tgz#d2ff3e9b21b479f46ae145e98e973b7928f9a22d"
@@ -5135,6 +5309,11 @@ sockjs@^0.3.24:
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
source-map-js@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
source-map-support@~0.5.20:
version "0.5.21"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
@@ -5500,6 +5679,11 @@ type-fest@^0.20.2:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
type-fest@^2.5.3:
version "2.19.0"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b"
integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==
type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
@@ -5600,6 +5784,11 @@ vary@~1.1.2:
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
vue-custom-element@^3.2.14:
version "3.3.0"
resolved "https://registry.yarnpkg.com/vue-custom-element/-/vue-custom-element-3.3.0.tgz#c20bb6108a16d1f7df3c550125551bf53d0e5a75"
integrity sha512-ePuy1EDDJd9/piwXLwsCyMQ964HsdhXPzypM9OX0r4JBa20EVN28U7RXeTWwXkoFKim/b3sP7xT2NEM0Di6tUQ==
vue-eslint-parser@^9.3.0:
version "9.3.0"
resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-9.3.0.tgz#775a974a0603c9a73d85fed8958ed9e814a4a816"
@@ -5613,6 +5802,11 @@ vue-eslint-parser@^9.3.0:
lodash "^4.17.21"
semver "^7.3.6"
vue-i18n@^8.23.0:
version "8.28.2"
resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-8.28.2.tgz#913558066e274395c0a9f40b2f3393d5c2636840"
integrity sha512-C5GZjs1tYlAqjwymaaCPDjCyGo10ajUphiwA922jKt9n7KPpqR7oM1PCwYzhB/E7+nT3wfdG3oRre5raIT1rKA==
vue-loader@^17.1.1:
version "17.1.1"
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-17.1.1.tgz#546c85eb844f98653921c02e8b0b060ebea19bfb"
@@ -5622,6 +5816,19 @@ vue-loader@^17.1.1:
hash-sum "^2.0.0"
watchpack "^2.4.0"
vue-select@^3.11.2:
version "3.20.3"
resolved "https://registry.yarnpkg.com/vue-select/-/vue-select-3.20.3.tgz#559b0ba2780171e4eeda455ebce5f3ce708380eb"
integrity sha512-aDaq4rPBrDavh7NDLgAP8e0G8UNElh6vO3itGWjhf1y9KaGRWtFw21cUkSRHGgj0V5FlCG4EaRb1xdE/C9J+eg==
vue@^2.6.14:
version "2.7.16"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.7.16.tgz#98c60de9def99c0e3da8dae59b304ead43b967c9"
integrity sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==
dependencies:
"@vue/compiler-sfc" "2.7.16"
csstype "^3.1.0"
vue@^3.3.2:
version "3.3.4"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.3.4.tgz#8ed945d3873667df1d0fcf3b2463ada028f88bd6"