Compare commits

...
84 Commits
Author SHA1 Message Date
Alex Turchyn 7439ef07ad Merge from docusealco/wip 2025-02-10 13:08:17 +02:00
Pete Matsyburka fc4b4de6ed customize template email per party 2025-02-09 14:13:04 +02:00
Alex Turchyn 96e4733f87 add submissions filtering by 'sent' and 'opened' statuses 2025-02-07 20:20:37 +02:00
Pete Matsyburka 8b6731a289 show logo 2025-02-07 14:48:04 +02:00
Pete Matsyburka f93ac68e9c adjust field drag 2025-02-07 14:39:23 +02:00
Alex Turchyn 439df4879c add stamp text logo 2025-02-07 14:15:35 +02:00
Alex Turchyn 83b6ce8574 fix logos 2025-02-07 14:15:18 +02:00
Pete Matsyburka 6ea07bc13b show minimize on mobile if with description 2025-02-06 13:49:09 +02:00
Alex Turchyn a15624690b show unauthorized message in test environment 2025-02-06 13:41:16 +02:00
Pete Matsyburka 974bb85fb7 rescue malformed PDF 2025-02-06 12:55:27 +02:00
Pete Matsyburka 5dc80601b2 courier font 2025-02-05 18:17:55 +02:00
Pete Matsyburka df2e53bc40 font mono 2025-02-05 16:13:31 +02:00
Alex Turchyn 6e685bbc32 Merge from docusealco/wip 2025-02-05 13:44:10 +02:00
Pete Matsyburka 0b3ca0de96 fix ci 2025-02-04 19:37:42 +02:00
Pete Matsyburka aed3f1f141 fix build 2025-02-04 18:47:15 +02:00
Pete Matsyburka 1a7981e919 update actions 2025-02-04 16:55:24 +02:00
Pete Matsyburka 4add805371 fix field align 2025-02-04 16:19:49 +02:00
Alex Turchyn 66c18317b7 Merge from docusealco/wip 2025-02-04 16:11:47 +02:00
Pete Matsyburka 9edbb885b4 use arm runner 2025-02-04 15:51:31 +02:00
Alex Turchyn c67c80d6bc Merge from docusealco/wip 2025-02-04 13:44:17 +02:00
Pete Matsyburka 0399531e0c html image 2025-02-03 18:38:52 +02:00
Pete Matsyburka 54426556d9 field conditions 2025-02-02 21:36:38 +02:00
Alex Turchyn fa74ee1755 add Progressive Web App 2025-02-02 14:31:42 +02:00
Pete Matsyburka eb53af12c4 adjust form 2025-02-02 12:36:42 +02:00
Pete Matsyburka 38efcb99c0 fix verification 2025-01-31 21:32:15 +02:00
Pete Matsyburka a92d914674 adjust form 2025-01-31 20:11:57 +02:00
Pete Matsyburka 219a698aa5 adjust i18n 2025-01-31 15:26:53 +02:00
Alex Turchyn 5bed6448dc make custom fields titles smaller on mobile devices 2025-01-31 15:19:04 +02:00
Alex Turchyn 6d2a8ca3d3 update translations 2025-01-31 14:43:06 +02:00
Alex Turchyn bfb2e0043d update email attribution footer for Test Mode 2025-01-31 14:43:06 +02:00
Pete Matsyburka 922101b12c fix dashboard toggle 2025-01-31 12:03:52 +02:00
Pete Matsyburka 9ff41a0c4a scroll on mobile field click 2025-01-29 16:08:09 +02:00
Pete Matsyburka 071f187605 fix template conditions 2025-01-29 00:56:14 +02:00
Pete Matsyburka ab6cd0e89c adjust send email job 2025-01-28 18:52:12 +02:00
Pete Matsyburka 97c0dad7ff add with fields search 2025-01-28 14:13:06 +02:00
Pete Matsyburka bc0917a190 fix sign in date 2025-01-28 11:16:24 +02:00
Alex Turchyn 5718acecff add translations 2025-01-28 11:15:33 +02:00
Alex Turchyn 395d43dbeb Merge from docusealco/wip 2025-01-27 13:21:32 +02:00
Pete Matsyburka 37de5e0d0d add resubmit from dashboard 2025-01-25 20:22:48 +02:00
Pete Matsyburka 44e848a1f0 fix heading move 2025-01-25 00:07:20 +02:00
Pete Matsyburka f6d59d731c fix email typo 2025-01-24 13:32:12 +02:00
Pete Matsyburka 87b039e709 fix submitter names 2025-01-23 22:21:01 +02:00
Pete Matsyburka e0c6a4e08b fix submission preview page 2025-01-23 17:11:50 +02:00
Pete Matsyburka 80b06550c2 adjust template link 2025-01-23 11:39:43 +02:00
Pete Matsyburka ec8e35ed7a fix email typo 2025-01-22 23:22:34 +02:00
Alex Turchyn 4dc325a6bb change open/read timeout for webhook requests 2025-01-22 20:58:47 +02:00
Pete Matsyburka d52c57df72 adjust i18n 2025-01-22 11:49:04 +02:00
Alex Turchyn 0cb2557447 add warning message to navbar 2025-01-21 13:10:27 +02:00
Pete Matsyburka cd05c1fd19 search field by title 2025-01-20 23:08:19 +02:00
Pete Matsyburka 5458e82b6e increase parties limit 2025-01-20 21:01:13 +02:00
Pete Matsyburka c59c58624b convert icon to png 2025-01-20 14:46:43 +02:00
Alex Turchyn 2da059d440 Merge from docusealco/wip 2025-01-20 12:48:34 +02:00
Pete Matsyburka b0aabd9e79 add signed option in filename 2025-01-19 16:16:00 +02:00
Pete Matsyburka f1cd60dfad error message 2025-01-19 16:02:40 +02:00
Alex Turchyn a264a8522a add compliances section 2025-01-18 16:39:20 +02:00
Pete Matsyburka 794e339425 add verification settings 2025-01-18 16:35:07 +02:00
Pete Matsyburka 9aaf61e658 add number currency format 2025-01-17 21:08:13 +02:00
Pete Matsyburka 37faa7ece6 do not remove condition on replace 2025-01-17 17:11:10 +02:00
Pete Matsyburka 666e3fde4c fix document remove 2025-01-17 17:06:37 +02:00
Pete Matsyburka 995e040a4e Revert "do not remove condition on replace"
This reverts commit c24ae1751e.
2025-01-17 16:55:11 +02:00
Pete Matsyburka 185ac2cbee refactor 2025-01-17 15:52:49 +02:00
Pete Matsyburka a4e372ff5c fix android scroll 2025-01-17 01:49:36 +02:00
Pete Matsyburka b4834c7674 fix filename date 2025-01-16 16:30:06 +02:00
Pete Matsyburka c24ae1751e do not remove condition on replace 2025-01-16 01:05:06 +02:00
Pete Matsyburka 9bd0ad70c4 add default field options 2025-01-16 00:40:22 +02:00
Pete Matsyburka f210f1d8bf Revert "fix scrolling to fields on Android devices"
This reverts commit e3f3895aac.
2025-01-15 13:57:30 +02:00
Alex Turchyn 92b61992f2 fix vertical scrolling for mobile devices 2025-01-15 02:52:31 +02:00
Alex Turchyn e3f3895aac fix scrolling to fields on Android devices 2025-01-14 23:23:09 +02:00
Alex Turchyn c696890cf5 fix dropdowns in Firefox 2025-01-14 01:57:20 +02:00
Alex Turchyn 288d204d8e Merge from docusealco/wip 2025-01-13 13:00:50 +02:00
Pete Matsyburka 5a96f4f1eb adjust ios mobile 2025-01-11 14:02:04 +02:00
Alex Turchyn 5ff4f9889e add user reviews 2025-01-11 13:09:38 +02:00
Alex Turchyn 78fb93fd02 fix detection of unknown mobile IOS devices 2025-01-11 13:06:21 +02:00
Pete Matsyburka 3c83d459bd add document page attributes 2025-01-10 20:33:23 +02:00
Pete Matsyburka 6cdb8e7132 fix draw cursor 2025-01-10 20:33:16 +02:00
Pete Matsyburka e2b93cbcab fix colors 2025-01-10 15:12:09 +02:00
Pete Matsyburka a59cc7c44e verification redirect 2025-01-10 11:50:54 +02:00
Pete Matsyburka 1984b99e04 fix formula with conditions 2025-01-09 22:25:46 +02:00
Pete Matsyburka fe3e8fba3e fix readonly formula values 2025-01-09 12:26:56 +02:00
Pete Matsyburka 97ecdf2ff0 fix invite submitters api order 2025-01-09 01:09:26 +02:00
Alex Turchyn e4c62087d0 fix translations 2025-01-08 18:17:46 +02:00
Pete Matsyburka 1f9faddfd0 add reply to 2025-01-07 20:38:56 +02:00
Pete Matsyburka 6dff6483bf fix default field title 2025-01-07 17:30:21 +02:00
Pete Matsyburka 2e43eb189f fix define submitters order 2025-01-07 13:04:59 +02:00
100 changed files with 1135 additions and 497 deletions

No files matched your search

+9 -9
View File
@@ -7,13 +7,13 @@ jobs:
name: Rubocop
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.4.1
- name: Cache gems
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
@@ -31,13 +31,13 @@ jobs:
name: Erblint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.4.1
- name: Cache gems
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
@@ -55,7 +55,7 @@ jobs:
name: ESLint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v1
with:
@@ -63,7 +63,7 @@ jobs:
- name: Cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v1
- uses: actions/cache@v4
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -96,7 +96,7 @@ jobs:
--health-retries 5
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
@@ -110,12 +110,12 @@ jobs:
with:
chrome-version: 125
- name: Cache node_modules
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
- name: Cache gems
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
+5 -5
View File
@@ -7,7 +7,7 @@ on:
jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04-arm
steps:
- name: Checkout code
@@ -25,22 +25,22 @@ jobs:
tags: |
type=semver,pattern={{version}}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Create .version file
run: echo ${{ github.ref_name }} > .version
- name: Login to Docker Hub
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
uses: docker/build-push-action@v6
with:
context: .
push: true
+24 -9
View File
@@ -223,9 +223,12 @@ GEM
concurrent-ruby (~> 1.1)
webrick (~> 1.7)
websocket-driver (~> 0.7)
ffi (1.17.0)
ffi (1.17.0-aarch64-linux-gnu)
ffi (1.17.0-x86_64-linux-gnu)
ffi (1.17.1)
ffi (1.17.1-aarch64-linux-gnu)
ffi (1.17.1-aarch64-linux-musl)
ffi (1.17.1-arm64-darwin)
ffi (1.17.1-x86_64-linux-gnu)
ffi (1.17.1-x86_64-linux-musl)
geom2d (0.4.1)
globalid (1.2.1)
activesupport (>= 6.1)
@@ -334,12 +337,18 @@ GEM
net-smtp (0.5.0)
net-protocol
nio4r (2.7.4)
nokogiri (1.17.2)
nokogiri (1.18.2)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.17.2-aarch64-linux)
nokogiri (1.18.2-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.17.2-x86_64-linux)
nokogiri (1.18.2-aarch64-linux-musl)
racc (~> 1.4)
nokogiri (1.18.2-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.2-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.2-x86_64-linux-musl)
racc (~> 1.4)
oj (3.16.8)
bigdecimal (>= 3.0)
@@ -520,10 +529,13 @@ GEM
simplecov-html (0.13.1)
simplecov_json_formatter (0.1.4)
smart_properties (1.17.0)
sqlite3 (2.4.1)
sqlite3 (2.5.0)
mini_portile2 (~> 2.8.0)
sqlite3 (2.4.1-aarch64-linux-gnu)
sqlite3 (2.4.1-x86_64-linux-gnu)
sqlite3 (2.5.0-aarch64-linux-gnu)
sqlite3 (2.5.0-aarch64-linux-musl)
sqlite3 (2.5.0-arm64-darwin)
sqlite3 (2.5.0-x86_64-linux-gnu)
sqlite3 (2.5.0-x86_64-linux-musl)
stringio (3.1.2)
strip_attributes (1.14.1)
activemodel (>= 3.0, < 9.0)
@@ -569,8 +581,11 @@ GEM
PLATFORMS
aarch64-linux
aarch64-linux-musl
arm64-darwin
ruby
x86_64-linux
x86_64-linux-musl
DEPENDENCIES
annotate
+1 -1
View File
@@ -50,7 +50,7 @@ class AccountsController < ApplicationController
# rubocop:disable Layout/LineLength
render turbo_stream: turbo_stream.replace(
:account_delete_button,
html: helpers.tag.p(I18n.t('your_account_removal_request_will_be_processed_within_2_weeks_please_contact_us_if_you_want_to_keep_your_account'))
html: helpers.tag.p(I18n.t('your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account'))
)
# rubocop:enable Layout/LineLength
end
+2
View File
@@ -34,6 +34,8 @@ module Api
}
end
}
rescue HexaPDF::MalformedPDFError
render json: { error: 'Malformed PDF' }, status: :unprocessable_entity
end
end
end
+1 -1
View File
@@ -30,7 +30,7 @@ class ApplicationController < ActionController::Base
redirect_to request.referer, alert: 'Too many requests', status: :too_many_requests
end
if Rails.env.production?
if Rails.env.production? || Rails.env.test?
rescue_from CanCan::AccessDenied do |e|
Rollbar.warning(e) if defined?(Rollbar)
+4
View File
@@ -0,0 +1,4 @@
# frozen_string_literal: true
class PwaController < ActionController::Base
end
@@ -9,8 +9,6 @@ class SendSubmissionEmailController < ApplicationController
SEND_DURATION = 30.minutes
def success; end
def create
@submitter =
if params[:template_slug]
@@ -31,7 +29,7 @@ class SendSubmissionEmailController < ApplicationController
end
respond_to do |f|
f.html { redirect_to success_send_submission_email_index_path }
f.html { render :success }
f.json { head :ok }
end
end
+1
View File
@@ -64,6 +64,7 @@ class StartFormController < ApplicationController
.or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
.order(id: :desc)
.where(declined_at: nil)
.where(external_id: nil)
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
.find_or_initialize_by(**submitter_params.compact_blank)
end
@@ -20,7 +20,10 @@ class SubmissionsPreviewController < ApplicationController
@submission ||= Submission.find_by!(slug: params[:slug])
if @submission.account.archived_at? || (!@submission.submitters.all?(&:completed_at?) && current_user.blank?)
raise ActionController::RoutingError if @submission.account.archived_at?
if !@submission.submitters.all?(&:completed_at?) && !signature_valid &&
(!current_user || !current_ability.can?(:read, @submission))
raise ActionController::RoutingError, I18n.t('not_found')
end
@@ -37,6 +40,7 @@ class SubmissionsPreviewController < ApplicationController
def completed
@submission = Submission.find_by!(slug: params[:submissions_preview_slug])
@template = @submission.template
render :completed, layout: 'form'
end
@@ -0,0 +1,58 @@
# frozen_string_literal: true
class SubmittersResubmitController < ApplicationController
load_and_authorize_resource :submitter, parent: false
def update
return redirect_to submit_form_path(slug: @submitter.slug) if @submitter.email != current_user.email
submission = @submitter.template.submissions.new(created_by_user: current_user,
submitters_order: :preserved,
**@submitter.submission.slice(:template_fields,
:account_id,
:template_schema,
:template_submitters,
:preferences))
@submitter.submission.submitters.each do |submitter|
new_submitter = submission.submitters.new(submitter.slice(:uuid, :email, :phone, :name,
:preferences, :metadata, :account_id))
next unless submitter.uuid == @submitter.uuid
assign_submitter_values(new_submitter, submitter)
@new_submitter ||= new_submitter
end
submission.save!
redirect_to submit_form_path(slug: @new_submitter.slug)
end
private
def assign_submitter_values(new_submitter, submitter)
attachments_index = submitter.attachments.index_by(&:uuid)
submitter.submission.template_fields.each do |field|
next if field['submitter_uuid'] != submitter.uuid
next if field['default_value'] == '{{date}}'
next if field['type'] == 'stamp'
next if field['type'] == 'signature'
next if field.dig('preferences', 'formula').present?
value = submitter.values[field['uuid']]
next if value.blank?
if field['type'].in?(%w[image file initials])
Array.wrap(value).each do |attachment_uuid|
new_submitter.attachments << attachments_index[attachment_uuid].dup
end
end
new_submitter.values[field['uuid']] = value
end
end
end
@@ -22,14 +22,16 @@ 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
documents_copy_email_attach_documents documents_copy_email_reply_to
completed_notification_email_attach_documents
completed_redirect_url
submitters_order
completed_notification_email_subject completed_notification_email_body
completed_notification_email_enabled completed_notification_email_attach_audit] +
[completed_message: %i[title body]]
[completed_message: %i[title body],
submitters: [%i[uuid request_email_subject request_email_body]]]
).tap do |attrs|
attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1'
attrs[:preferences] = attrs[:preferences].transform_values do |value|
if %w[true false].include?(value)
value == 'true'
@@ -3,6 +3,8 @@ export default class extends HTMLElement {
this.resize()
this.textarea.addEventListener('input', () => this.resize())
this.observeVisibility()
}
resize () {
@@ -11,6 +13,28 @@ export default class extends HTMLElement {
}
}
observeVisibility () {
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.resize()
this.observer.unobserve(this.textarea)
}
})
},
{
threshold: 0.1
}
)
this.observer.observe(this.textarea)
}
disconnectedCallback () {
this.observer.unobserve(this.textarea)
}
get textarea () {
return this.querySelector('textarea')
}
+2 -1
View File
@@ -20,7 +20,8 @@ export default targetable(class extends HTMLElement {
fetch(this.dataset.src).then(async (response) => {
if (response.ok) {
const urls = await response.json()
const isSafariIos = /iPhone|iPad|iPod/i.test(navigator.userAgent)
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
const isSafariIos = isMobileSafariIos || /iPhone|iPad|iPod/i.test(navigator.userAgent)
if (isSafariIos && urls.length > 1) {
this.downloadSafariIos(urls)
+23 -4
View File
@@ -3,7 +3,7 @@
class="field-area flex absolute lg:text-base -outline-offset-1"
dir="auto"
:style="computedStyle"
:class="{ 'font-serif': field.preferences?.font === 'Times', 'text-[1.6vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'field-area-active outline-red-500 outline-dashed outline-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
:class="{ 'font-mono': field.preferences?.font === 'Courier', 'font-serif': field.preferences?.font === 'Times', 'text-[1.6vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'field-area-active outline-red-500 outline-dashed outline-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
>
<div
v-if="(!withFieldPlaceholder || !field.name || field.type === 'cells') && !isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
@@ -191,13 +191,22 @@
class="whitespace-pre-wrap text-gray-400"
:class="{ 'w-full': field.preferences?.align }"
>{{ field.name }}</span>
<span v-else-if="Array.isArray(modelValue)">
<span
v-else-if="Array.isArray(modelValue)"
:class="{ 'w-full': field.preferences?.align }"
>
{{ modelValue.join(', ') }}
</span>
<span v-else-if="field.type === 'date'">
<span
v-else-if="field.type === 'date'"
:class="{ 'w-full': field.preferences?.align }"
>
{{ formattedDate }}
</span>
<span v-else-if="field.type === 'number'">
<span
v-else-if="field.type === 'number'"
class="w-full"
>
{{ formatNumber(modelValue, field.preferences?.format) }}
</span>
<span
@@ -447,8 +456,18 @@ export default {
}
},
formatNumber (number, format) {
if (!number && number !== 0) {
return ''
}
if (format === 'comma') {
return new Intl.NumberFormat('en-US').format(number)
} else if (format === 'usd') {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
} else if (format === 'gbp') {
return new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
} else if (format === 'eur') {
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
} else if (format === 'dot') {
return new Intl.NumberFormat('de-DE').format(number)
} else if (format === 'space') {
+19 -7
View File
@@ -31,7 +31,7 @@
:with-label="withLabel && !withFieldPlaceholder && step.length < 2"
:is-value-set="step.some((f) => f.uuid in values)"
:attachments-index="attachmentsIndex"
@click="$emit('focus-step', stepIndex)"
@click="[$emit('focus-step', stepIndex), maybeScrollOnClick(field, area)]"
/>
</Teleport>
</template>
@@ -109,6 +109,14 @@ export default {
areaRefs: []
}
},
computed: {
isMobileContainer () {
const root = this.$root.$el.parentNode.getRootNode()
const container = root.body || root.querySelector('div')
return container.style.overflow === 'hidden'
}
},
beforeUpdate () {
this.areaRefs = []
},
@@ -121,14 +129,16 @@ export default {
this.scrollIntoArea(field.areas[0])
}
},
maybeScrollOnClick (field, area) {
if (['text', 'number', 'cells'].includes(field.type) && this.isMobileContainer) {
this.scrollIntoArea(area)
}
},
scrollIntoArea (area) {
const areaRef = this.areaRefs.find((a) => a.area === area)
if (areaRef) {
const root = this.$root.$el.parentNode.getRootNode()
const container = root.body || root.querySelector('div')
if (container.style.overflow === 'hidden') {
if (this.isMobileContainer) {
this.scrollInContainer(areaRef.$el)
} else {
const targetRect = areaRef.$refs.scrollToElem.getBoundingClientRect()
@@ -157,13 +167,15 @@ export default {
const formContainer = root.getElementById('form_container')
const container = root.body || root.querySelector('div')
const padding = 64
const isAndroid = /android/i.test(navigator.userAgent)
const padding = isAndroid ? 128 : 64
const scrollboxTop = isAndroid ? scrollbox.getBoundingClientRect().top : 0
const boxRect = scrollbox.children[0].getBoundingClientRect()
const targetRect = target.getBoundingClientRect()
const targetTopRelativeToBox = targetRect.top - boxRect.top
scrollbox.scrollTo({ top: targetTopRelativeToBox - container.offsetHeight + formContainer.offsetHeight + target.offsetHeight + padding, behavior: 'smooth' })
scrollbox.scrollTo({ top: targetTopRelativeToBox + scrollboxTop - container.offsetHeight + formContainer.offsetHeight + target.offsetHeight + padding, behavior: 'smooth' })
},
setAreaRef (el) {
if (el) {
@@ -49,7 +49,7 @@
<MarkdownContent :string="field.description" />
</div>
<FileDropzone
:message="`${t('upload')} ${field.name || t('files')}${field.required ? '' : ` (${t('optional')})`}`"
:message="`${t('upload')} ${(field.title || field.name) || t('files')}${field.required ? '' : ` (${t('optional')})`}`"
:submitter-slug="submitterSlug"
:multiple="true"
:dry-run="dryRun"
+2 -1
View File
@@ -215,7 +215,8 @@ export default {
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`).then(async (response) => {
if (response.ok) {
const urls = await response.json()
const isSafariIos = /iPhone|iPad|iPod/i.test(navigator.userAgent)
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
const isSafariIos = isMobileSafariIos || /iPhone|iPad|iPod/i.test(navigator.userAgent)
if (isSafariIos && urls.length > 1) {
this.downloadSafariIos(urls)
+11 -5
View File
@@ -1,22 +1,28 @@
<template>
<div dir="auto">
<div
class="flex justify-between items-center w-full"
class="flex justify-between items-end w-full mb-3.5 sm:mb-4"
:class="{ 'mb-2': !field.description }"
>
<label
v-if="showFieldNames"
:for="field.uuid"
class="label text-2xl"
class="label text-xl sm:text-2xl py-0"
>
<MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>
{{ field.name && showFieldNames ? field.name : t('date') }}
<template v-if="!field.required">
{{ field.name || t('date') }}
</template>
<template v-if="!field.required">
<span
class="ml-1"
:class="{ 'hidden sm:inline': (field.title || field.name || t('date') ).length > 10 }"
>
({{ t('optional') }})
</template>
</span>
</template>
</label>
<button
+1 -1
View File
@@ -128,7 +128,7 @@ export default {
}
})
} else {
if (file.type === 'image/bmp') {
if (file.type === 'image/bmp' || file.type === 'image/vnd.microsoft.icon') {
file = await this.convertBmpToPng(file)
}
+32 -11
View File
@@ -15,7 +15,7 @@
/>
<FieldAreas
:steps="readonlyConditionalFields.map((e) => [e])"
:values="readonlyConditionalFields.reduce((acc, f) => { acc[f.uuid] = (values[f.uuid] || f.default_value); return acc }, {})"
:values="readonlyConditionalFieldValues"
:submitter="submitter"
:attachments-index="attachmentsIndex"
:submittable="false"
@@ -23,6 +23,7 @@
<FormulaFieldAreas
v-if="formulaFields.length"
:fields="formulaFields"
:readonly-values="readonlyConditionalFieldValues"
:values="values"
/>
<Teleport
@@ -99,7 +100,8 @@
<button
v-if="!isCompleted"
id="minimize_form_button"
class="absolute right-0 mr-2 mt-2 top-0 hidden md:block"
class="absolute right-0 top-0"
:class="currentField?.description?.length > 100 ? 'mr-1 mt-1 md:mr-2 md:mt-2': 'mr-2 mt-2 hidden md:block'"
:title="t('minimize')"
@click.prevent="minimizeForm"
>
@@ -163,7 +165,7 @@
v-if="showFieldNames && (currentField.name || currentField.title)"
:for="currentField.uuid"
dir="auto"
class="label text-2xl"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
:class="{ 'mb-2': !currentField.description }"
>
<MarkdownContent
@@ -172,7 +174,11 @@
/>
<template v-else>
{{ currentField.name }}
<template v-if="!currentField.required">({{ t('optional') }})</template>
</template>
<template v-if="!currentField.required">
<span :class="{ 'hidden sm:inline': (currentField.title || currentField.name).length > 20 }">
({{ t('optional') }})
</span>
</template>
</label>
<div
@@ -220,7 +226,7 @@
v-if="showFieldNames && (currentField.name || currentField.title)"
:for="currentField.uuid"
dir="auto"
class="label text-2xl"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
:class="{ 'mb-2': !currentField.description }"
>
<MarkdownContent
@@ -229,7 +235,11 @@
/>
<template v-else>
{{ currentField.name }}
<template v-if="!currentField.required">({{ t('optional') }})</template>
</template>
<template v-if="!currentField.required">
<span :class="{ 'hidden sm:inline': (currentField.title || currentField.name).length > 20 }">
({{ t('optional') }})
</span>
</template>
</label>
<div
@@ -465,7 +475,7 @@
</div>
<div
v-if="(currentField.type !== 'payment' && currentField.type !== 'verification') || submittedValues[currentField.uuid]"
:class="currentField.type === 'signature' ? 'mt-2' : 'mt-6 md:mt-8'"
:class="currentField.type === 'signature' ? 'mt-2' : 'mt-4 md:mt-6'"
>
<button
id="submit_form_button"
@@ -521,9 +531,9 @@
/>
<div
v-if="stepFields.length < 80"
class="flex justify-center"
class="flex justify-center mt-3 sm:mt-4 mb-0 sm:mb-1"
>
<div class="flex items-center mt-4 mb-1 flex-wrap">
<div class="flex items-center flex-wrap">
<a
v-for="(step, index) in stepFields"
:key="step[0].uuid"
@@ -855,7 +865,16 @@ export default {
},
computed: {
isMobile () {
return /android|iphone|ipad/i.test(navigator.userAgent)
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
return isMobileSafariIos || /android|iphone|ipad/i.test(navigator.userAgent)
},
readonlyConditionalFieldValues () {
return this.readonlyConditionalFields.reduce((acc, f) => {
acc[f.uuid] = (this.values[f.uuid] || f.default_value)
return acc
}, {})
},
attachmentConditionsIndex () {
return this.schema.reduce((acc, item) => {
@@ -1103,7 +1122,9 @@ export default {
this.minimizeForm()
}
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
const isMobile = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit|android/i.test(navigator.userAgent)
if (isMobile || /iPhone|iPad|iPod/i.test(navigator.userAgent)) {
this.$nextTick(() => {
const root = this.$root.$el.parentNode.getRootNode()
const scrollbox = root.getElementById('scrollbox')
@@ -38,6 +38,11 @@ export default {
required: false,
default: () => []
},
readonlyValues: {
type: Object,
required: false,
default: () => ({})
},
values: {
type: Object,
required: false,
@@ -87,7 +92,7 @@ export default {
},
calculateFormula (field) {
const transformedFormula = field.preferences.formula.replace(/{{(.*?)}}/g, (match, uuid) => {
return this.values[uuid] || 0.0
return this.readonlyValues[uuid] || this.values[uuid] || 0.0
})
return this.math.evaluate(transformedFormula.toLowerCase())
+14 -4
View File
@@ -1,9 +1,19 @@
<template>
<div v-if="modelValue">
<div class="flex justify-between items-center w-full mb-2">
<div class="flex justify-between items-end w-full mb-3.5 md:mb-4">
<label
class="label text-2xl"
>{{ showFieldNames && field.name ? field.name : t('image') }}</label>
v-if="showFieldNames"
:for="field.uuid"
class="label text-xl sm:text-2xl py-0"
>
<MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>
{{ field.name || t('image') }}
</template>
</label>
<button
class="btn btn-outline btn-sm"
@click.prevent="remove"
@@ -35,7 +45,7 @@
<MarkdownContent :string="field.description" />
</div>
<FileDropzone
:message="`${t('upload')} ${field.name || t('image')}${field.required ? '' : ` (${t('optional')})`}`"
:message="`${t('upload')} ${(field.title || field.name) || t('image')}${field.required ? '' : ` (${t('optional')})`}`"
:submitter-slug="submitterSlug"
:dry-run="dryRun"
:accept="'image/*'"
@@ -1,21 +1,22 @@
<template>
<div dir="auto">
<div
class="flex justify-between items-center w-full"
class="flex justify-between items-end w-full mb-3.5 md:mb-4"
:class="{ 'mb-2': !field.description }"
>
<label
class="label text-2xl"
v-if="showFieldNames"
class="label text-xl sm:text-2xl py-0"
>
<MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>
{{ showFieldNames && field.name ? field.name : t('initials') }}
{{ field.name || t('initials') }}
</template>
</label>
<div class="space-x-2 flex">
<div class="space-x-2 flex flex-none">
<span
v-if="isDrawInitials"
class="tooltip"
@@ -40,7 +40,7 @@
>
</div>
<div
class="mt-6 md:mt-8"
class="mt-4 md:mt-6"
>
<button
type="submit"
@@ -3,13 +3,19 @@
v-if="showFieldNames && (field.name || field.title)"
:for="field.uuid"
dir="auto"
class="label text-2xl"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
:class="{ 'mb-2': !field.description }"
><MarkdownContent
v-if="field.title"
:string="field.title"
/>
>
<MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>{{ field.name }}</template>
<template v-if="!field.required">
<span :class="{ 'hidden sm:inline': (field.title || field.name).length > 20 }">
({{ t('optional') }})
</span>
</template>
</label>
<div
v-if="field.description"
@@ -3,15 +3,18 @@
v-if="showFieldNames && (field.name || field.title)"
:for="field.uuid"
dir="auto"
class="label text-2xl"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
:class="{ 'mb-2': !field.description }"
><MarkdownContent
v-if="field.title"
:string="field.title"
/>
>
<MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>{{ field.name }}</template>
<template v-if="!field.required">
({{ t('optional') }})
<span :class="{ 'hidden sm:inline': (field.title || field.name).length > 20 }">
({{ t('optional') }})
</span>
</template>
</label>
<div
@@ -1,7 +1,7 @@
<template>
<label
v-if="!modelValue && !sessionId"
class="label text-2xl mb-2"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
>
<MarkdownContent
v-if="field.title"
@@ -1,8 +1,9 @@
<template>
<div>
<label
v-if="showFieldNames"
:for="isCodeSent ? 'one_time_code' : field.uuid"
class="label text-2xl"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
:class="{ 'mb-2': !field.description }"
>
<MarkdownContent
@@ -10,10 +11,7 @@
:string="field.title"
/>
<template v-else>
{{ showFieldNames && field.name ? field.name : t('verified_phone_number') }}
<template v-if="!field.required">
({{ t('optional') }})
</template>
{{ field.name || t('verified_phone_number') }}
</template>
</label>
<div
@@ -297,7 +295,7 @@ export default {
const data = await resp.json()
if (resp.status === 422) {
alert(this.t('number_phone_is_invalid').replace('{number}', this.fullInternationalPhoneValue))
alert(data.error || this.t('number_phone_is_invalid').replace('{number}', this.fullInternationalPhoneValue))
} else if (resp.status === 429) {
alert(data.error)
}
@@ -4,21 +4,22 @@
class="relative"
>
<div
class="flex justify-between items-center w-full"
class="flex justify-between items-end w-full mb-3.5 md:mb-4"
:class="{ 'mb-2': !field.description }"
>
<label
class="label text-2xl"
v-if="showFieldNames"
class="label text-xl sm:text-2xl py-0"
>
<MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>
{{ showFieldNames && field.name ? field.name : t('signature') }}
{{ field.name || t('signature') }}
</template>
</label>
<div class="space-x-2 flex">
<div class="space-x-2 flex flex-none">
<span
v-if="isTextSignature && format !== 'typed' && format !== 'upload'"
class="tooltip"
@@ -39,6 +40,7 @@
<span
v-else-if="withTypedSignature && format !== 'typed' && format !== 'drawn' && format !== 'upload'"
class="tooltip ml-2"
:class="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
:data-tip="t('type_text')"
>
<a
@@ -56,6 +58,7 @@
<span
v-if="format !== 'typed' && format !== 'drawn' && format !== 'upload' && format !== 'drawn_or_typed'"
class="tooltip"
:class="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
:data-tip="t('take_photo')"
>
<label
+9 -6
View File
@@ -3,15 +3,18 @@
v-if="showFieldNames && (field.name || field.title)"
:for="field.uuid"
dir="auto"
class="label text-2xl"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
:class="{ 'mb-2': !field.description }"
><MarkdownContent
v-if="field.title"
:string="field.title"
/>
>
<MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>{{ field.name }}</template>
<template v-if="!field.required">
({{ t('optional') }})
<span :class="{ 'hidden sm:inline': (field.title || field.name).length > 20 }">
({{ t('optional') }})
</span>
</template>
</label>
<div
@@ -1,6 +1,6 @@
<template>
<label
class="label text-2xl mb-2"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
>
<MarkdownContent
v-if="field.title"
@@ -31,6 +31,15 @@
class="animate-spin h-10"
/>
</div>
<div v-else-if="redirectUrl">
<a
:href="redirectUrl"
target="_blank"
class="white-button w-full"
>
{{ t('verify_id') }}
</a>
</div>
<div
ref="widgetContainer"
/>
@@ -87,6 +96,7 @@ export default {
return {
isCreatingCheckout: false,
isMathLoaded: false,
redirectUrl: '',
isLoading: false,
eidEasyData: {}
}
@@ -125,12 +135,16 @@ export default {
async mounted () {
this.isLoading = true
Promise.all([
import('@eid-easy/eideasy-widget'),
this.start()
]).finally(() => {
this.isLoading = false
})
if (new URLSearchParams(window.location.search).get('submit') === 'true') {
this.$emit('submit')
} else {
Promise.all([
import('@eid-easy/eideasy-widget'),
this.start()
]).finally(() => {
this.isLoading = false
})
}
},
methods: {
start () {
@@ -143,14 +157,26 @@ export default {
}).then(async (resp) => {
this.eidEasyData = await resp.json()
const eidEasyWidget = document.createElement('eideasy-widget')
if (this.eidEasyData.available_methods[0] === 'itsme-qes-signature' &&
this.eidEasyData.available_methods.length === 1) {
const redirectUrl = new URL('https://id.eideasy.com/sign_contract_external')
for (const key in this.widgetSettings) {
eidEasyWidget[key] = this.widgetSettings[key]
redirectUrl.searchParams.append('client_id', this.eidEasyData.client_id)
redirectUrl.searchParams.append('doc_id', this.eidEasyData.doc_id)
redirectUrl.searchParams.append('country', this.countryCode)
redirectUrl.searchParams.append('lang', this.locale)
this.redirectUrl = redirectUrl.toString()
} else {
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)
}
this.$refs.widgetContainer.innerHTML = ''
this.$refs.widgetContainer.appendChild(eidEasyWidget)
})
},
async submit () {
+32 -40
View File
@@ -10,7 +10,7 @@
<div
v-if="isSelected || isDraw"
class="top-0 bottom-0 right-0 left-0 absolute border border-1.5 pointer-events-none"
:class="field.type === 'heading' ? '' : borderColors[submitterIndex]"
:class="field.type === 'heading' ? '' : borderColors[submitterIndex % borderColors.length]"
/>
<div
v-if="field.type === 'cells' && (isSelected || isDraw)"
@@ -20,7 +20,7 @@
v-for="(cellW, index) in cells"
:key="index"
class="absolute top-0 bottom-0 border-r"
:class="field.type === 'heading' ? '' : borderColors[submitterIndex]"
:class="field.type === 'heading' ? '' : borderColors[submitterIndex % borderColors.length]"
:style="{ left: (cellW / area.w * 100) + '%' }"
>
<span
@@ -70,7 +70,7 @@
@keydown.enter.prevent="onNameEnter"
@focus="onNameFocus"
@blur="onNameBlur"
>{{ optionIndexText }} {{ (defaultField ? (field.title || field.name) : field.name) || defaultName }}</span>
>{{ optionIndexText }} {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || defaultName }}</span>
<div
v-if="isSettingsFocus || (isValueInput && field.type !== 'heading') || (isNameFocus && !['checkbox', 'phone'].includes(field.type))"
class="flex items-center ml-1.5"
@@ -161,7 +161,7 @@
ref="touchValueTarget"
class="flex items-center h-full w-full"
dir="auto"
:class="[isValueInput ? 'bg-opacity-50' : 'bg-opacity-80', field.type === 'heading' ? 'bg-gray-50' : bgColors[submitterIndex], isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? (alignClasses[field.preferences?.align] || '') : 'justify-center']"
:class="[isValueInput ? 'bg-opacity-50' : 'bg-opacity-80', field.type === 'heading' ? 'bg-gray-50' : bgColors[submitterIndex % bgColors.length], isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? (alignClasses[field.preferences?.align] || '') : 'justify-center']"
@click="focusValueInput"
>
<span
@@ -212,7 +212,7 @@
:contenteditable="isValueInput"
class="whitespace-pre-wrap outline-none empty:before:content-[attr(placeholder)] before:text-gray-400"
:class="{ 'cursor-text': isValueInput }"
:placeholder="withFieldPlaceholder && !isValueInput ? field.name || defaultName : t('type_value')"
:placeholder="withFieldPlaceholder && !isValueInput ? defaultField?.title || field.title || field.name || defaultName : t('type_value')"
@blur="onDefaultValueBlur"
@paste.prevent="onPaste"
@keydown.enter="onDefaultValueEnter"
@@ -361,6 +361,7 @@ export default {
isMoved: false,
renderDropdown: false,
isNameFocus: false,
isHeadingSelected: false,
textOverflowChars: 0,
dragFrom: { x: 0, y: 0 }
}
@@ -377,7 +378,7 @@ export default {
}
},
isValueInput () {
return (this.field.type === 'heading' && this.isSelected) || this.isContenteditable || (this.inputMode && ['text', 'number', 'date'].includes(this.field.type))
return (this.field.type === 'heading' && this.isHeadingSelected) || this.isContenteditable || (this.inputMode && ['text', 'number', 'date'].includes(this.field.type))
},
modalContainerEl () {
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
@@ -420,16 +421,6 @@ export default {
},
borderColors () {
return [
'border-red-500/80',
'border-sky-500/80',
'border-emerald-500/80',
'border-yellow-300/80',
'border-purple-600/80',
'border-pink-500/80',
'border-cyan-500/80',
'border-orange-500/80',
'border-lime-500/80',
'border-indigo-500/80',
'border-red-500/80',
'border-sky-500/80',
'border-emerald-500/80',
@@ -444,16 +435,6 @@ export default {
},
bgColors () {
return [
'bg-red-100',
'bg-sky-100',
'bg-emerald-100',
'bg-yellow-100',
'bg-purple-100',
'bg-pink-100',
'bg-cyan-100',
'bg-orange-100',
'bg-lime-100',
'bg-indigo-100',
'bg-red-100',
'bg-sky-100',
'bg-emerald-100',
@@ -499,13 +480,13 @@ export default {
methods: {
buildDefaultName: Field.methods.buildDefaultName,
closeDropdown () {
document.activeElement.blur()
this.$el.getRootNode().activeElement.blur()
},
maybeToggleDefaultValue () {
if (['text', 'number'].includes(this.field.type)) {
this.isContenteditable = true
this.$nextTick(() => this.focusValueInput())
this.focusValueInput()
} else if (this.field.type === 'checkbox') {
this.field.readonly = !this.field.readonly
this.field.default_value === true ? delete this.field.default_value : this.field.default_value = true
@@ -527,20 +508,28 @@ export default {
}
},
focusValueInput (e) {
if (this.$refs.defaultValue !== document.activeElement) {
this.$refs.defaultValue.focus()
this.$nextTick(() => {
if (this.$refs.defaultValue && this.$refs.defaultValue !== document.activeElement) {
this.$refs.defaultValue.focus()
if (this.$refs.defaultValue.innerText.length && this.$refs.defaultValue !== e?.target) {
window.getSelection().collapse(
this.$refs.defaultValue.firstChild,
this.$refs.defaultValue.innerText.length
)
if (this.$refs.defaultValue.innerText.length && this.$refs.defaultValue !== e?.target) {
window.getSelection().collapse(
this.$refs.defaultValue.firstChild,
this.$refs.defaultValue.innerText.length
)
}
}
}
})
},
formatNumber (number, format) {
if (format === 'comma') {
return new Intl.NumberFormat('en-US').format(number)
} else if (format === 'usd') {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
} else if (format === 'gbp') {
return new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
} else if (format === 'eur') {
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
} else if (format === 'dot') {
return new Intl.NumberFormat('de-DE').format(number)
} else if (format === 'space') {
@@ -646,6 +635,7 @@ export default {
const text = this.$refs.defaultValue.innerText.trim()
this.isContenteditable = false
this.isHeadingSelected = false
if (text) {
if (this.field.type === 'number') {
@@ -763,10 +753,6 @@ export default {
this.selectedAreaRef.value = this.area
if (this.field.type === 'heading') {
this.$nextTick(() => this.focusValueInput())
}
this.dragFrom = { x: rect.left - e.clientX, y: rect.top - e.clientY }
this.$el.getRootNode().addEventListener('mousemove', this.mouseMove)
@@ -801,6 +787,12 @@ export default {
this.save()
}
if (this.field.type === 'heading') {
this.isHeadingSelected = !this.isMoved
this.focusValueInput()
}
this.isDragged = false
this.isMoved = false
+39 -20
View File
@@ -179,6 +179,7 @@
:with-arrows="template.schema.length > 1"
:item="item"
:document="sortedDocuments[index]"
:data-document-uuid="item.attachment_uuid"
:accept-file-types="acceptFileTypes"
:with-replace-button="withUploadButton"
:editable="editable"
@@ -221,8 +222,8 @@
</div>
<div
id="pages_container"
class="w-full overflow-y-hidden overflow-x-hidden mt-0.5 pt-0.5"
:class="isMobile ? 'overflow-y-auto' : 'md:overflow-y-auto'"
class="w-full overflow-x-hidden mt-0.5 pt-0.5"
:class="isMobile ? 'overflow-y-auto' : 'overflow-y-hidden md:overflow-y-auto'"
>
<div
ref="documents"
@@ -266,6 +267,7 @@
:input-mode="inputMode"
:default-fields="[...defaultRequiredFields, ...defaultFields]"
:allow-draw="!onlyDefinedFields"
:data-document-uuid="document.uuid"
:default-submitters="defaultSubmitters"
:with-field-placeholder="withFieldPlaceholder"
:draw-field="drawField"
@@ -364,6 +366,7 @@
:with-help="withHelp"
:default-submitters="defaultSubmitters"
:draw-field-type="drawFieldType"
:with-fields-search="withFieldsSearch"
:default-fields="[...defaultRequiredFields, ...defaultFields]"
:template="template"
:default-required-fields="defaultRequiredFields"
@@ -621,6 +624,11 @@ export default {
required: false,
default: true
},
withFieldsSearch: {
type: Boolean,
required: false,
default: null
},
withFieldsList: {
type: Boolean,
required: false,
@@ -701,7 +709,9 @@ export default {
return this.locale.split('-')[0].toLowerCase()
},
isMobile () {
return /android|iphone|ipad/i.test(navigator.userAgent)
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
return isMobileSafariIos || /android|iphone|ipad/i.test(navigator.userAgent)
},
defaultDateFormat () {
const isUsBrowser = Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US')
@@ -770,13 +780,17 @@ export default {
}
})
const defineSubmittersUuids = this.defineSubmitters.map((name) => {
return this.template.submitters.find(e => e.name === name)?.uuid
})
this.defineSubmitters.forEach((name, index) => {
const submitter = (this.template.submitters[index] ||= {})
submitter.name = name || this.submitterDefaultNames[index]
if (existingSubmittersUuids.filter(Boolean).length) {
submitter.uuid = existingSubmittersUuids[index] || submitter.uuid || v4()
if (defineSubmittersUuids.filter(Boolean).length || existingSubmittersUuids.filter(Boolean).length) {
submitter.uuid = defineSubmittersUuids[index] || existingSubmittersUuids[index] || submitter.uuid || v4()
} else {
submitter.uuid ||= v4()
}
@@ -1312,7 +1326,11 @@ export default {
if (!this.fieldsDragFieldRef.value) {
if (['select', 'multiple', 'radio'].includes(field.type)) {
field.options = [{ value: '', uuid: v4() }]
if (this.dragField?.options?.length) {
field.options = this.dragField.options.map(option => ({ value: option, uuid: v4() }))
} else {
field.options = [{ value: '', uuid: v4() }]
}
}
if (['stamp', 'heading'].includes(field.type)) {
@@ -1383,7 +1401,6 @@ export default {
const lastArea = field.areas[field.areas.length - 1]
if (lastArea) {
fieldArea.x -= lastArea.w / 2
fieldArea.w = lastArea.w
fieldArea.h = lastArea.h
}
@@ -1415,6 +1432,8 @@ export default {
const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid)
const areaRef = documentRef.pageRefs[area.page].areaRefs.find((ref) => ref.area === this.selectedAreaRef.value)
areaRef.isHeadingSelected = true
areaRef.focusValueInput()
})
}
@@ -1502,29 +1521,29 @@ export default {
onDocumentRemove (item) {
if (window.confirm(this.t('are_you_sure_'))) {
this.template.schema.splice(this.template.schema.indexOf(item), 1)
}
const removedFieldUuids = []
const removedFieldUuids = []
this.template.fields.forEach((field) => {
[...(field.areas || [])].forEach((area) => {
if (area.attachment_uuid === item.attachment_uuid) {
field.areas.splice(field.areas.indexOf(area), 1)
this.template.fields.forEach((field) => {
[...(field.areas || [])].forEach((area) => {
if (area.attachment_uuid === item.attachment_uuid) {
field.areas.splice(field.areas.indexOf(area), 1)
removedFieldUuids.push(field.uuid)
}
removedFieldUuids.push(field.uuid)
}
})
})
})
this.template.fields =
this.template.fields.filter((f) => !removedFieldUuids.includes(f.uuid) || f.areas?.length)
this.template.fields =
this.template.fields.filter((f) => !removedFieldUuids.includes(f.uuid) || f.areas?.length)
this.save()
this.save()
}
},
onDocumentReplace (data) {
const { replaceSchemaItem, schema, documents } = data
this.template.schema.splice(this.template.schema.indexOf(replaceSchemaItem), 1, schema[0])
this.template.schema.splice(this.template.schema.indexOf(replaceSchemaItem), 1, { ...replaceSchemaItem, ...schema[0] })
this.template.documents.push(...documents)
if (data.fields) {
@@ -177,7 +177,7 @@ export default {
fields () {
if (this.item.submitter_uuid) {
return this.template.fields.reduce((acc, f) => {
if (f !== this.item && f.submitter_uuid === this.item.submitter_uuid) {
if (f !== this.item) {
acc.push(f)
}
@@ -7,6 +7,7 @@
:input-mode="inputMode"
:number="index"
:editable="editable"
:data-page="index"
:areas="areasIndex[index]"
:allow-draw="allowDraw"
:is-drag="isDrag"
+6 -6
View File
@@ -23,7 +23,7 @@
/>
<Contenteditable
ref="name"
:model-value="(defaultField ? (field.title || field.name) : field.name) || defaultName"
:model-value="(defaultField ? (defaultField.title || field.title || field.name) : field.name) || defaultName"
:editable="editable && !defaultField && field.type != 'heading'"
:icon-inline="true"
:icon-width="18"
@@ -192,14 +192,14 @@
class="w-full input input-primary input-xs text-sm bg-transparent"
:placeholder="`${t('option')} ${index + 1}`"
type="text"
:readonly="!editable"
:readonly="!editable || defaultField"
required
dir="auto"
@focus="maybeFocusOnOptionArea(option)"
@blur="save"
>
<button
v-if="editable"
v-if="editable && !defaultField"
class="text-sm w-3.5"
tabindex="-1"
@click="removeOption(option)"
@@ -208,11 +208,11 @@
</button>
</div>
<div
v-if="field.options && !editable"
v-if="field.options && (!editable || defaultField)"
class="pb-1"
/>
<button
v-else-if="field.options && editable"
v-else-if="field.options && editable && !defaultField"
class="text-center text-sm w-full pb-1"
@click="addOption"
>
@@ -392,7 +392,7 @@ export default {
return this.sortedAreas[0] && this.$emit('scroll-to', this.sortedAreas[0])
},
closeDropdown () {
document.activeElement.blur()
this.$el.getRootNode().activeElement.blur()
},
addOption () {
this.field.options.push({ value: '', uuid: v4() })
@@ -26,6 +26,33 @@
{{ t('format') }}
</label>
</div>
<div
v-if="field.type === 'verification'"
class="py-1.5 px-1 relative"
@click.stop
>
<select
:placeholder="t('method')"
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
@change="[field.preferences ||= {}, field.preferences.method = $event.target.value, save()]"
>
<option
v-for="method in ['QeS', 'AeS']"
:key="method"
:value="method.toLowerCase()"
:selected="method.toLowerCase() === field.preferences?.method || (method === 'QeS' && !field.preferences?.method)"
>
{{ method }}
</option>
</select>
<label
:style="{ backgroundColor }"
class="absolute -top-1 left-2.5 px-1 h-4"
style="font-size: 8px"
>
{{ t('method') }}
</label>
</div>
<div
v-if="['number', 'cells'].includes(field.type)"
class="py-1.5 px-1 relative"
@@ -229,7 +256,7 @@
</label>
</div>
<li
v-if="withRequired && field.type != 'phone' && field.type != 'stamp'"
v-if="withRequired && field.type !== 'phone' && field.type !== 'stamp' && field.type !== 'verification'"
@click.stop
>
<label class="cursor-pointer py-1.5">
@@ -461,6 +488,9 @@ export default {
numberFormats () {
return [
'none',
'usd',
'eur',
'gbp',
'comma',
'dot',
'space'
@@ -541,6 +571,12 @@ export default {
formatNumber (number, format) {
if (format === 'comma') {
return new Intl.NumberFormat('en-US').format(number)
} else if (format === 'usd') {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
} else if (format === 'gbp') {
return new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
} else if (format === 'eur') {
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
} else if (format === 'dot') {
return new Intl.NumberFormat('de-DE').format(number)
} else if (format === 'space') {
@@ -9,7 +9,7 @@
<div class="flex items-center space-x-2">
<span
class="w-3 h-3 flex-shrink-0 rounded-full"
:class="colors[submitters.indexOf(selectedSubmitter)]"
:class="colors[submitters.indexOf(selectedSubmitter) % colors.length]"
/>
<Contenteditable
v-model="selectedSubmitter.name"
@@ -53,7 +53,7 @@
<span class="py-1 flex items-center">
<span
class="rounded-full w-3 h-3 ml-1 mr-3"
:class="colors[index]"
:class="colors[index % colors.length]"
/>
<span>
{{ submitter.name }}
@@ -101,7 +101,7 @@
>
<button
class="mx-1 w-3 h-3 rounded-full"
:class="colors[submitters.indexOf(selectedSubmitter)]"
:class="colors[submitters.indexOf(selectedSubmitter) % colors.length]"
/>
</label>
<label
@@ -113,7 +113,7 @@
<div class="flex items-center space-x-2">
<span
class="w-3 h-3 rounded-full"
:class="colors[submitters.indexOf(selectedSubmitter)]"
:class="colors[submitters.indexOf(selectedSubmitter) % colors.length]"
/>
<Contenteditable
v-model="selectedSubmitter.name"
@@ -143,6 +143,7 @@
<li
v-for="(submitter, index) in submitters"
:key="submitter.uuid"
class="w-full"
>
<a
href="#"
@@ -153,7 +154,7 @@
<span class="py-1 flex items-center">
<span
class="rounded-full w-3 h-3 ml-1 mr-3"
:class="colors[index]"
:class="colors[index % colors.length]"
/>
<span>
{{ submitter.name }}
@@ -191,7 +192,10 @@
</div>
</a>
</li>
<li v-if="submitters.length < names.length && editable && allowAddNew">
<li
v-if="submitters.length < names.length && editable && allowAddNew"
class="w-full"
>
<a
href="#"
class="flex px-2"
@@ -215,6 +219,14 @@ import { IconUserPlus, IconTrashX, IconPlus, IconChevronUp, IconChevronDown } fr
import Contenteditable from './contenteditable'
import { v4 } from 'uuid'
function getOrdinalSuffix (num) {
if (num % 10 === 1 && num % 100 !== 11) return 'st'
if (num % 10 === 2 && num % 100 !== 12) return 'nd'
if (num % 10 === 3 && num % 100 !== 13) return 'rd'
return 'th'
}
export default {
name: 'FieldSubmitter',
components: {
@@ -275,16 +287,6 @@ export default {
computed: {
colors () {
return [
'bg-red-500',
'bg-sky-500',
'bg-emerald-500',
'bg-yellow-300',
'bg-purple-600',
'bg-pink-500',
'bg-cyan-500',
'bg-orange-500',
'bg-lime-500',
'bg-indigo-500',
'bg-red-500',
'bg-sky-500',
'bg-emerald-500',
@@ -298,6 +300,14 @@ export default {
]
},
names () {
const generatedNames = []
for (let i = 21; i < 101; i++) {
const suffix = getOrdinalSuffix(i)
generatedNames.push(`${i}${suffix} ${this.t('party')}`)
}
return [
this.t('first_party'),
this.t('second_party'),
@@ -318,7 +328,8 @@ export default {
this.t('seventeenth_party'),
this.t('eighteenth_party'),
this.t('nineteenth_party'),
this.t('twentieth_party')
this.t('twentieth_party'),
...generatedNames
]
},
lastPartyIndex () {
@@ -372,7 +383,7 @@ export default {
this.$emit('new-submitter', newSubmitter)
},
closeDropdown () {
document.activeElement.blur()
this.$el.getRootNode().activeElement.blur()
}
}
}
@@ -170,7 +170,7 @@ export default {
},
methods: {
closeDropdown () {
document.activeElement.blur()
this.$el.getRootNode().activeElement.blur()
}
}
}
+20 -4
View File
@@ -5,7 +5,7 @@
class="roles-dropdown w-full rounded-lg"
:style="withStickySubmitters ? { backgroundColor } : {}"
:submitters="submitters"
:menu-style="{ backgroundColor: ['', null, 'transparent'].includes(backgroundColor) ? 'white' : backgroundColor }"
:menu-style="{ overflow: 'auto', display: 'flex', flexDirection: 'row', maxHeight: 'calc(100vh - 120px)', backgroundColor: ['', null, 'transparent'].includes(backgroundColor) ? 'white' : backgroundColor }"
:editable="editable && !defaultSubmitters.length"
@new-submitter="save"
@remove="removeSubmitter"
@@ -26,7 +26,7 @@
:field="field"
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
:editable="editable && (!fieldsDragFieldRef.value || fieldsDragFieldRef.value !== field)"
:default-field="defaultFields.find((f) => f.name === field.name)"
:default-field="defaultFieldsIndex[field.name]"
:draggable="editable"
@dragstart="fieldsDragFieldRef.value = field"
@dragend="fieldsDragFieldRef.value = null"
@@ -229,6 +229,11 @@ export default {
type: Array,
required: true
},
withFieldsSearch: {
type: Boolean,
required: false,
default: null
},
template: {
type: Object,
required: true
@@ -297,7 +302,18 @@ export default {
fieldNames: FieldType.computed.fieldNames,
fieldIcons: FieldType.computed.fieldIcons,
isShowFieldSearch () {
return this.submitterDefaultFields.length > 15
if (this.withFieldsSearch === false) {
return false
} else {
return this.submitterDefaultFields.length > 15
}
},
defaultFieldsIndex () {
return this.defaultFields.reduce((acc, field) => {
acc[field.name] = field
return acc
}, {})
},
fieldIconsSorted () {
if (this.fieldTypes.length) {
@@ -320,7 +336,7 @@ export default {
},
filteredSubmitterDefaultFields () {
if (this.defaultFieldsSearch) {
return this.submitterDefaultFields.filter((f) => f.name.toLowerCase().includes(this.defaultFieldsSearch.toLowerCase()))
return this.submitterDefaultFields.filter((f) => (f.title || f.name).toLowerCase().includes(this.defaultFieldsSearch.toLowerCase()))
} else {
return this.submitterDefaultFields
}
+12
View File
@@ -1,4 +1,6 @@
const en = {
party: 'Party',
method: 'Method',
reorder_fields: 'Reorder fields',
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.',
@@ -159,6 +161,8 @@ const en = {
}
const es = {
party: 'Parte',
method: 'Método',
reorder_fields: 'Reordenar campos',
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.',
@@ -319,6 +323,8 @@ const es = {
}
const it = {
party: 'Parte',
method: 'Metodo',
reorder_fields: 'Riordina i campi',
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ù.',
@@ -479,6 +485,8 @@ const it = {
}
const pt = {
party: 'Parte',
method: 'Método',
reorder_fields: 'Reorganizar campos',
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.',
@@ -639,6 +647,8 @@ const pt = {
}
const fr = {
party: 'Partie',
method: 'Méthode',
reorder_fields: 'Réorganiser les champs',
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.',
@@ -799,6 +809,8 @@ const fr = {
}
const de = {
party: 'Partei',
method: 'Verfahren',
reorder_fields: 'Felder neu anordnen',
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.',
+13 -3
View File
@@ -1,6 +1,7 @@
<template>
<div
class="relative cursor-crosshair select-none"
class="relative select-none"
:class="{ 'cursor-crosshair': allowDraw }"
:style="drawField ? 'touch-action: none' : ''"
>
<img
@@ -25,7 +26,7 @@
:field="item.field"
:editable="editable"
:with-field-placeholder="withFieldPlaceholder"
:default-field="defaultFields.find((f) => f.name === item.field.name)"
:default-field="defaultFieldsIndex[item.field.name]"
:default-submitters="defaultSubmitters"
:max-page="totalPages - 1"
@start-resize="resizeDirection = $event"
@@ -142,6 +143,13 @@ export default {
}
},
computed: {
defaultFieldsIndex () {
return this.defaultFields.reduce((acc, field) => {
acc[field.name] = field
return acc
}, {})
},
defaultFieldType () {
if (this.drawFieldType) {
return this.drawFieldType
@@ -154,7 +162,9 @@ export default {
}
},
isMobile () {
return /android|iphone|ipad/i.test(navigator.userAgent)
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
return isMobileSafariIos || /android|iphone|ipad/i.test(navigator.userAgent)
},
resizeDirectionClasses () {
return {
+10 -10
View File
@@ -104,17 +104,17 @@
@update:model-value="onUpdateName"
/>
</div>
<Teleport
v-if="isShowConditionsModal"
:to="modalContainerEl"
>
<ConditionsModal
:item="item"
:build-default-name="buildDefaultName"
@close="isShowConditionsModal = false"
/>
</Teleport>
</div>
<Teleport
v-if="isShowConditionsModal"
:to="modalContainerEl"
>
<ConditionsModal
:item="item"
:build-default-name="buildDefaultName"
@close="isShowConditionsModal = false"
/>
</Teleport>
</template>
<script>
@@ -5,8 +5,6 @@ class SendFormCompletedWebhookRequestJob
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 20
def perform(params = {})
@@ -21,19 +19,8 @@ class SendFormCompletedWebhookRequestJob
ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin
Faraday.post(webhook_url.url,
{
event_type: 'form.completed',
timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter)
}.to_json,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT,
**webhook_url.secret.to_h)
rescue Faraday::Error
nil
end
resp = SendWebhookRequest.call(webhook_url, event_type: 'form.completed',
data: Submitters::SerializeForWebhook.call(submitter))
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
@@ -5,8 +5,6 @@ class SendFormDeclinedWebhookRequestJob
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
@@ -19,19 +17,8 @@ class SendFormDeclinedWebhookRequestJob
ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin
Faraday.post(webhook_url.url,
{
event_type: 'form.declined',
timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter)
}.to_json,
**webhook_url.secret.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
nil
end
resp = SendWebhookRequest.call(webhook_url, event_type: 'form.declined',
data: Submitters::SerializeForWebhook.call(submitter))
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
@@ -5,8 +5,6 @@ class SendFormStartedWebhookRequestJob
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
@@ -19,19 +17,8 @@ class SendFormStartedWebhookRequestJob
ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin
Faraday.post(webhook_url.url,
{
event_type: 'form.started',
timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter)
}.to_json,
**webhook_url.secret.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
nil
end
resp = SendWebhookRequest.call(webhook_url, event_type: 'form.started',
data: Submitters::SerializeForWebhook.call(submitter))
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
@@ -5,8 +5,6 @@ class SendFormViewedWebhookRequestJob
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
@@ -19,19 +17,8 @@ class SendFormViewedWebhookRequestJob
ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin
Faraday.post(webhook_url.url,
{
event_type: 'form.viewed',
timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter)
}.to_json,
**webhook_url.secret.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
nil
end
resp = SendWebhookRequest.call(webhook_url, event_type: 'form.viewed',
data: Submitters::SerializeForWebhook.call(submitter))
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
@@ -5,8 +5,6 @@ class SendSubmissionArchivedWebhookRequestJob
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
@@ -17,19 +15,8 @@ class SendSubmissionArchivedWebhookRequestJob
return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.archived')
resp = begin
Faraday.post(webhook_url.url,
{
event_type: 'submission.archived',
timestamp: Time.current,
data: submission.as_json(only: %i[id archived_at])
}.to_json,
**webhook_url.secret.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
nil
end
resp = SendWebhookRequest.call(webhook_url, event_type: 'submission.archived',
data: submission.as_json(only: %i[id archived_at]))
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
@@ -5,8 +5,6 @@ class SendSubmissionCompletedWebhookRequestJob
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
@@ -17,19 +15,8 @@ class SendSubmissionCompletedWebhookRequestJob
return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.completed')
resp = begin
Faraday.post(webhook_url.url,
{
event_type: 'submission.completed',
timestamp: Time.current,
data: Submissions::SerializeForApi.call(submission)
}.to_json,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT,
**webhook_url.secret.to_h)
rescue Faraday::Error
nil
end
resp = SendWebhookRequest.call(webhook_url, event_type: 'submission.completed',
data: Submissions::SerializeForApi.call(submission))
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
@@ -5,8 +5,6 @@ class SendSubmissionCreatedWebhookRequestJob
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
@@ -17,19 +15,8 @@ class SendSubmissionCreatedWebhookRequestJob
return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.created')
resp = begin
Faraday.post(webhook_url.url,
{
event_type: 'submission.created',
timestamp: Time.current,
data: Submissions::SerializeForApi.call(submission)
}.to_json,
**webhook_url.secret.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
nil
end
resp = SendWebhookRequest.call(webhook_url, event_type: 'submission.created',
data: Submissions::SerializeForApi.call(submission))
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
@@ -8,6 +8,12 @@ class SendSubmitterInvitationEmailJob
return if submitter.submission.source == 'invite' && !Accounts.can_send_emails?(submitter.account, on_events: true)
unless Accounts.can_send_invitation_emails?(submitter.account)
Rollbar.warning("Skip email: #{submitter.account.id}") if defined?(Rollbar)
return
end
mail = SubmitterMailer.invitation_email(submitter)
Submitters::ValidateSending.call(submitter, mail)
@@ -17,6 +23,6 @@ class SendSubmitterInvitationEmailJob
SubmissionEvent.create!(submitter:, event_type: 'send_email')
submitter.sent_at ||= Time.current
submitter.save
submitter.save!
end
end
@@ -5,8 +5,6 @@ class SendTemplateCreatedWebhookRequestJob
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
@@ -17,19 +15,8 @@ class SendTemplateCreatedWebhookRequestJob
return if webhook_url.url.blank? || webhook_url.events.exclude?('template.created')
resp = begin
Faraday.post(webhook_url.url,
{
event_type: 'template.created',
timestamp: Time.current,
data: Templates::SerializeForApi.call(template)
}.to_json,
**webhook_url.secret.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
nil
end
resp = SendWebhookRequest.call(webhook_url, event_type: 'template.created',
data: Templates::SerializeForApi.call(template))
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
@@ -5,8 +5,6 @@ class SendTemplateUpdatedWebhookRequestJob
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
@@ -17,19 +15,8 @@ class SendTemplateUpdatedWebhookRequestJob
return if webhook_url.url.blank? || webhook_url.events.exclude?('template.updated')
resp = begin
Faraday.post(webhook_url.url,
{
event_type: 'template.updated',
timestamp: Time.current,
data: Templates::SerializeForApi.call(template)
}.to_json,
**webhook_url.secret.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
nil
end
resp = SendWebhookRequest.call(webhook_url, event_type: 'template.updated',
data: Templates::SerializeForApi.call(template))
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
+33 -12
View File
@@ -14,8 +14,20 @@ class SubmitterMailer < ApplicationMailer
@email_message = submitter.account.email_messages.find_by(uuid: submitter.preferences['email_message_uuid'])
end
@body = @email_message&.body.presence || @submitter.template.preferences['request_email_body'].presence
@subject = @email_message&.subject.presence || @submitter.template.preferences['request_email_subject'].presence
template_submitters_index =
if @email_message.blank?
build_submitter_preferences_index(@submitter)
else
{}
end
@body = @email_message&.body.presence ||
template_submitters_index.dig(@submitter.uuid, 'request_email_body').presence ||
@submitter.template.preferences['request_email_body'].presence
@subject = @email_message&.subject.presence ||
template_submitters_index.dig(@submitter.uuid, 'request_email_subject').presence ||
@submitter.template.preferences['request_email_subject'].presence
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY)
@@ -24,14 +36,7 @@ class SubmitterMailer < ApplicationMailer
reply_to = build_submitter_reply_to(@submitter)
I18n.with_locale(@current_account.locale) do
subject =
if @email_config || @subject
ReplaceEmailVariables.call(@subject || @email_config.value['subject'], submitter:)
elsif @submitter.with_signature_fields?
I18n.t(:you_are_invited_to_sign_a_document)
else
I18n.t(:you_are_invited_to_submit_a_form)
end
subject = build_invite_subject(@subject, @email_config, submitter)
mail(
to: @submitter.friendly_name,
@@ -121,7 +126,7 @@ class SubmitterMailer < ApplicationMailer
@body ||= @email_config.value['body'] if @email_config
assign_message_metadata('submitter_documents_copy', @submitter)
reply_to = build_submitter_reply_to(submitter)
reply_to = build_submitter_reply_to(submitter, email_config: @email_config, documents_copy_email: true)
I18n.with_locale(@current_account.locale) do
subject =
@@ -140,8 +145,10 @@ class SubmitterMailer < ApplicationMailer
private
def build_submitter_reply_to(submitter)
def build_submitter_reply_to(submitter, email_config: nil, documents_copy_email: nil)
reply_to = submitter.preferences['reply_to'].presence
reply_to ||= submitter.template.preferences['documents_copy_email_reply_to'].presence if documents_copy_email
reply_to ||= email_config.value['reply_to'].presence if email_config
if reply_to.blank? && (submitter.submission.created_by_user || submitter.template.author)&.email != submitter.email
reply_to = (submitter.submission.created_by_user || submitter.template.author)&.friendly_name&.sub(/\+\w+@/, '@')
@@ -194,6 +201,20 @@ class SubmitterMailer < ApplicationMailer
user.role == 'integration' ? user.friendly_name.sub(/\+\w+@/, '@') : user.friendly_name
end
def build_invite_subject(subject, email_config, submitter)
if email_config || subject
ReplaceEmailVariables.call(subject || email_config.value['subject'], submitter:)
elsif submitter.with_signature_fields?
I18n.t(:you_are_invited_to_sign_a_document)
else
I18n.t(:you_are_invited_to_submit_a_form)
end
end
def build_submitter_preferences_index(submitter)
submitter.template.preferences['submitters'].to_a.index_by { |e| e['uuid'] }
end
def add_attachments_with_size_limit(submitter, storage_attachments, current_size, filename_format = nil)
total_size = current_size
Whitespace-only changes.
+1
View File
@@ -153,6 +153,7 @@
<% end %>
</div>
<% end %>
<%= render 'compliances' %>
<%= render 'integrations' %>
<% if can?(:manage, current_account) && Docuseal.multitenant? && true_user == current_user %>
<div class="px-1 mt-8">
+1 -1
View File
@@ -173,7 +173,7 @@
<%= 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()' %>
<%= f.select :value, [["#{I18n.t('document_name')}.pdf", '{document.name}'], ["#{I18n.t('document_name')} - #{I18n.t(:signed)}.pdf", '{document.name} - {submission.status}'], ["#{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 %>
+6
View File
@@ -0,0 +1,6 @@
<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="M3 9l9 6l9 -6l-9 -6l-9 6"></path>
<path d="M21 9v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-10"></path>
<path d="M3 19l6 -6"></path>
<path d="M15 13l6 6"></path>
</svg>

After

Width:  |  Height:  |  Size: 401 B

+3
View File
@@ -2,6 +2,9 @@
<html data-theme="docuseal" lang="<%= I18n.locale %>">
<head>
<%= render 'layouts/head_tags' %>
<% if Docuseal.enable_pwa? %>
<link rel="manifest" href="/manifest.json">
<% end %>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
@@ -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, :attach_documents).new(*f.object.value.values_at('subject', 'body', 'attach_audit_log', 'attach_documents')) do |ff| %>
<%= f.fields_for :value, Struct.new(:subject, :body, :reply_to, :attach_audit_log, :attach_documents).new(*f.object.value.values_at('subject', 'body', 'reply_to', '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,6 +24,12 @@
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<% if can?(:manage, :reply_to) %>
<div class="form-control">
<%= ff.label :reply_to, t('reply_to'), class: 'label' %>
<%= ff.email_field :reply_to, class: 'base-input', dir: 'auto', placeholder: t(:email) %>
</div>
<% end %>
<div class="flex items-center justify-between pt-2.5 mx-1">
<span>
<%= t('attach_documents') %>
+25
View File
@@ -0,0 +1,25 @@
{
"name": "<%= Docuseal.product_name %>",
"short_name": "<%= Docuseal.product_name %>",
"id": "/",
"icons": [
{
"src": "/logo.svg",
"type": "image/svg+xml",
"sizes": "any"
},
{
"src": "/apple-touch-icon.png",
"type": "image/png",
"sizes": "192x192"
}
],
"start_url": "/",
"display": "standalone",
"scope": "/",
"orientation": "natural",
"description": "<%= Docuseal.product_name %> is an open source platform that provides secure and efficient digital document signing and processing.",
"categories": ["productivity", "utilities"],
"theme_color": "#FAF7F4",
"background_color": "#FAF7F4"
}
@@ -2,7 +2,7 @@
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<div class="flex items-center justify-center">
<%= render 'start_form/docuseal_logo' %>
<%= render 'start_form/banner' %>
</div>
<div class="text-center text-4xl font-bold">
<%= t('email_has_been_sent') %>
+5 -1
View File
@@ -2,5 +2,9 @@
---
</p>
<p>
<%= t('sent_using_product_name_free_document_signing_html', product_url: "#{Docuseal::PRODUCT_EMAIL_URL}/start", product_name: Docuseal.product_name) %>
<% if @current_account&.testing? %>
<%= t('sent_using_product_name_in_testing_mode_html', product_url: "#{Docuseal::PRODUCT_EMAIL_URL}/start", product_name: Docuseal.product_name) %>
<% else %>
<%= t('sent_using_product_name_free_document_signing_html', product_url: "#{Docuseal::PRODUCT_EMAIL_URL}/start", product_name: Docuseal.product_name) %>
<% end %>
</p>
+1
View File
@@ -1,3 +1,4 @@
<%= render 'shared/navbar_warning' %>
<div class="max-w-6xl mb-4 mx-auto px-4 md:px-2 py-3 flex items-center justify-between">
<div class="flex items-center space-x-4">
<a href="<%= root_path %>" class="text-2xl font-bold items-center flex space-x-2">
Whitespace-only changes.
Whitespace-only changes.
+51 -4
View File
@@ -1,3 +1,4 @@
<% submitter_preferences_index = template.preferences['submitters'].to_a.index_by { |e| e['uuid'] } %>
<div class="form-control">
<% can_send_emails = Accounts.can_send_emails?(current_account) %>
<div class="flex justify-between items-center">
@@ -36,10 +37,18 @@
<% config = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) %>
<div id="message_field" class="card card-compact bg-base-300/40 hidden">
<div class="card-body">
<div class="form-control space-y-2">
<%= tag.input id: 'request_email_per_submitter', value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: local_assigns[:message_per_submitter] != false && template.preferences['submitters'].to_a.size > 1 %>
<div class="peer-checked:hidden form-control space-y-2">
<div class="form-control">
<%= f.label :subject, t('subject'), class: 'label' %>
<%= f.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || template.preferences['request_email_subject'].presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
<div class="flex justify-between">
<%= f.label :subject, t('subject'), class: 'label' %>
<% if template.submitters.size > 1 && template.submitters.size < 5 && local_assigns[:message_per_submitter] != false %>
<label for="request_email_per_submitter" class="label underline">
<%= t('edit_per_party') %>
</label>
<% end %>
</div>
<%= f.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_subject').presence || template.preferences['request_email_subject'].presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
@@ -49,7 +58,7 @@
</span>
</div>
<autoresize-textarea>
<%= f.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || template.preferences['request_email_body'].presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
<%= f.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_body').presence || template.preferences['request_email_body'].presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
</autoresize-textarea>
<% unless local_assigns.fetch(:disable_save_as_default_template_option, false) %>
<label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer">
@@ -60,5 +69,43 @@
</div>
<%= render 'submissions/message_fields' %>
</div>
<% if template.submitters.size > 1 && template.submitters.size < 5 && local_assigns[:message_per_submitter] != false %>
<div class="hidden peer-checked:block form-control space-y-2">
<% options = template.submitters.map { |e| [e['name'], "request_email_#{e['uuid']}"] } %>
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="flex relative px-1">
<ul class="tabs w-full flex flex-nowrap">
<% options.each_with_index do |(label, val), index| %>
<div class="w-full">
<%= f.radio_button :selected, val, checked: index.zero?, id: "#{val}_radio", data: { action: 'click:toggle-visible#trigger' }, class: 'hidden peer' %>
<%= f.label :selected, label, value: val, for: "#{val}_radio", class: 'tab w-full tab-lifted peer-checked:tab-active !bg-transparent' %>
</div>
<% end %>
</ul>
</toggle-visible>
<% template.submitters.each_with_index do |submitter, index| %>
<%= fields_for :submitter_preferences, nil, index: submitter['uuid'] do |ff| %>
<div id="request_email_<%= submitter['uuid'] %>" class="<%= 'hidden' if index != 0 %>">
<div class="form-control">
<div class="flex justify-between">
<%= ff.label :subject, t('subject'), class: 'label' %>
</div>
<%= ff.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_subject').presence || template.preferences['request_email_subject'].presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :message, t('body'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_body').presence || template.preferences['request_email_body'].presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
</autoresize-textarea>
</div>
</div>
<% end %>
<% end %>
</div>
<% end %>
</div>
</div>
+2 -1
View File
@@ -1,6 +1,7 @@
<% align = field.dig('preferences', 'align') %>
<% color = field.dig('preferences', 'color') %>
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-serif' if field.dig('preferences', 'font') == 'Times' %> <%= align == 'right' ? 'justify-end' : (align == 'center' ? 'justify-center' : '') %>" style="<%= "color: #{color}; " if color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size'].to_i * 1.23}pt); line-height: `clamp(6pt, 2.0vw, #{(field['preferences']['font_size'].to_i * 1.23) + 3}pt)`" if field.dig('preferences', 'font_size') %>">
<% font = field.dig('preferences', 'font') %>
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-mono' if font == 'Courier' %> <%= 'font-serif' if font == 'Times' %> <%= align == 'right' ? 'justify-end' : (align == 'center' ? 'justify-center' : '') %>" style="<%= "color: #{color}; " if color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size'].to_i * 1.23}pt); line-height: `clamp(6pt, 2.0vw, #{(field['preferences']['font_size'].to_i * 1.23) + 3}pt)`" if field.dig('preferences', 'font_size') %>">
<% if field['type'] == 'signature' %>
<div class="flex flex-col justify-between h-full overflow-hidden">
<div class="flex-grow flex overflow-hidden" style="min-height: 50%">
+9 -2
View File
@@ -118,7 +118,7 @@
<div class="group border border-base-300 rounded-md px-2 py-1 mb-1">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-1">
<span class="mx-1 w-3 h-3 shrink-0 rounded-full <%= colors[index] %>"></span>
<span class="mx-1 w-3 h-3 shrink-0 rounded-full <%= colors[index % 10] %>"></span>
<span class="text-lg" dir="auto">
<%= (@submission.template_submitters || @submission.template.submitters).find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %>
</span>
@@ -190,6 +190,11 @@
</a>
</div>
<% end %>
<% if signed_in? && submitter && submitter.completed_at? && submitter.email == current_user.email && submitter.completed_at > 1.month.ago && can?(:update, @submission) %>
<div class="mt-2 mb-1">
<%= button_to t('resubmit'), submitters_resubmit_path(submitter), method: :put, class: 'btn btn-sm btn-primary w-full', form: { target: '_blank' }, data: { turbo: false } %>
</div>
<% end %>
</div>
</div>
<div class="px-1.5 mb-4">
@@ -222,10 +227,12 @@
</div>
<% elsif field['type'] == 'checkbox' %>
<%= svg_icon('check', class: 'w-6 h-6') %>
<% elsif field['type'] == 'number' %>
<%= NumberUtils.format_number(value, field.dig('preferences', 'format')) %>
<% elsif field['type'] == 'date' %>
<%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), @submission.account.locale) %>
<% else %>
<%= Array.wrap(value).join(', ') %>
<div class="whitespace-pre-wrap"><%= Array.wrap(value).join(', ') %></div>
<% end %>
</div>
</div>
@@ -1,5 +1,5 @@
<% query_params = params.permit(:q).merge(filter_params) %>
<% if icon = { 'declined' => 'x_circle', 'expired' => 'clock_cancel', 'partially_completed' => 'clock_edit' }[params[:status]] %>
<% if icon = { 'declined' => 'x_circle', 'expired' => 'clock_cancel', 'partially_completed' => 'clock_edit', 'sent' => 'send', 'opened' => 'mail_opened' }[params[:status]] %>
<div class="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">
<%= link_to submissions_filter_path('status', 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(icon, class: 'w-5 h-5 shrink-0') %>
@@ -2,7 +2,7 @@
<div class="flex flex-col md:flex-row gap-2 mt-5">
<div class="form-control w-full">
<div id="status" class="radio-select grid grid-cols-2 gap-2 px-1">
<% ['', 'pending', 'completed', 'partially_completed', 'declined', 'expired'].each do |status| %>
<% ['', 'pending', 'completed', 'partially_completed', 'sent', 'opened', 'declined', 'expired'].each do |status| %>
<label class="radio-label cursor-pointer inline-flex items-center space-x-2">
<%= radio_button_tag 'status', status, params[:status] == status || (status == '' && params[:status].blank?), class: 'base-radio' %>
<span><%= t(status.presence || 'all') %></span>
@@ -2,11 +2,9 @@
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<% if Docuseal.multitenant? %>
<div class="flex items-center justify-center">
<%= render 'start_form/docuseal_logo' %>
</div>
<% end %>
<div class="flex items-center justify-center">
<%= render 'start_form/banner' %>
</div>
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
<div class="flex items-center">
<div class="mr-3">
@@ -1,5 +1,5 @@
<% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid created_at], methods: %i[url filename content_type]) %>
<% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %>
<% data_fields = Submissions.filtered_conditions_fields(submitter).to_json %>
<% invite_submitters = (submitter.submission.template_submitters || submitter.submission.template.submitters).select { |s| s['invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } }.to_json %>
<% optional_invite_submitters = (submitter.submission.template_submitters || submitter.submission.template.submitters).select { |s| s['optional_invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } }.to_json %>
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-schema="<%= schema.to_json %>" data-reuse-signature="<%= configs[:reuse_signature] %>" data-require-signing-reason="<%= configs[:require_signing_reason] %>" data-with-signature-id="<%= configs[:with_signature_id] %>" data-with-confetti="<%= configs[:with_confetti] %>" data-completed-redirect-url="<%= submitter.preferences['completed_redirect_url'].presence || submitter.submission.template.preferences['completed_redirect_url'] %>" data-completed-message="<%= (configs[:completed_message]&.compact_blank.presence || submitter.submission.template.preferences['completed_message'] || {}).to_json %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.preferences.key?('go_to_last') ? submitter.preferences['go_to_last'] : submitter.opened_at? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(submitter.submission.account) %>" data-optional-invite-submitters="<%= optional_invite_submitters %>" data-invite-submitters="<%= invite_submitters %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>" data-previous-signature-value="<%= local_assigns[:signature_attachment]&.uuid %>" data-remember-signature="<%= configs[:prefill_signature] %>" data-dry-run="<%= local_assigns[:dry_run] %>" data-expand="<%= local_assigns[:expand] %>" data-scroll-padding="<%= local_assigns[:scroll_padding] %>" data-language="<%= I18n.locale.to_s.split('-').first %>"></submission-form>
+2 -1
View File
@@ -42,7 +42,8 @@
</download-button>
<% end %>
</div>
<% if Templates.filter_undefined_submitters(@submitter.submission.template).size == 1 && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false && !@submitter.template.archived_at? %>
<% undefined_submitters = Templates.filter_undefined_submitters(@submitter.submission.template) %>
<% if undefined_submitters.size == 1 && undefined_submitters.first['uuid'] == @submitter.uuid && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false && !@submitter.template.archived_at? %>
<div class="divider uppercase"><%= t('or') %></div>
<toggle-submit class="block">
<%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@submitter.submission.template.slug), params: { submitter: { email: @submitter.email, phone: @submitter.phone, name: @submitter.name }, resubmit: @submitter.slug }, method: :put, class: 'white-button w-full' %>
+1 -1
View File
@@ -17,7 +17,7 @@
</submitter-item>
</div>
<div>
<%= render 'submissions/send_email', f:, template: @submitter.template, submitter: @submitter, resend_email: @submitter.sent_at?, submitter_email_message: @submitter_email_message, disable_save_as_default_template_option: true %>
<%= render 'submissions/send_email', f:, template: @submitter.template, submitter: @submitter, resend_email: @submitter.sent_at?, submitter_email_message: @submitter_email_message, disable_save_as_default_template_option: true, message_per_submitter: false %>
<%= render 'submissions/send_sms', f:, resend_sms: @submitter.sent_at? %>
</div>
<div class="form-control mt-4">
+2 -1
View File
@@ -2,7 +2,7 @@
<% 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">
<% if has_archived || @pagy.count > 0 %>
<% if has_archived || @pagy.count > 0 || @template_folders.present? %>
<div class="mr-2">
<%= render 'dashboard/toggle_view', selected: 'templates' %>
</div>
@@ -61,3 +61,4 @@
</div>
</div>
<% end %>
<%= render 'shared/review_form' %>
+71 -15
View File
@@ -83,21 +83,71 @@
<div class="collapse-content">
<%= 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_alert1"></toggle-on-submit>
<%= f.fields_for :preferences, Struct.new(:request_email_subject, :request_email_body).new(*(@template.preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence || AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY).value.values_at('subject', 'body'))) do |ff| %>
<div class="form-control">
<%= ff.label :request_email_subject, t('email_subject'), class: 'label' %>
<%= ff.text_field :request_email_subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :request_email_body, t('email_body'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
<%= tag.input id: 'request_email_per_submitter', value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: @template.preferences['submitters'].to_a.size > 1 %>
<div class="peer-checked:hidden">
<%= f.fields_for :preferences, Struct.new(:request_email_subject, :request_email_body).new(*(@template.preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence || AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY).value.values_at('subject', 'body'))) do |ff| %>
<div class="form-control">
<div class="flex justify-between">
<%= ff.label :request_email_subject, t('email_subject'), class: 'label' %>
<% if @template.submitters.size > 1 && @template.submitters.size < 5 %>
<label for="request_email_per_submitter" class="label underline">
<%= t('edit_per_party') %>
</label>
<% end %>
</div>
<%= ff.text_field :request_email_subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<autoresize-textarea>
<%= ff.text_area :request_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :request_email_body, t('email_body'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :request_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<% end %>
</div>
<% if @template.submitters.size > 1 && @template.submitters.size < 5 %>
<div class="hidden peer-checked:block">
<% options = @template.submitters.map { |e| [e['name'], "request_email_#{e['uuid']}"] } %>
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="flex relative px-1">
<ul class="tabs w-full flex flex-nowrap mb-2">
<% options.each_with_index do |(label, val), index| %>
<div class="w-full">
<%= f.radio_button :selected, val, checked: index.zero?, id: "#{val}_radio", data: { action: 'click:toggle-visible#trigger' }, class: 'hidden peer' %>
<%= f.label :selected, label, value: val, for: "#{val}_radio", class: 'tab w-full tab-lifted peer-checked:tab-active' %>
</div>
<% end %>
</ul>
</toggle-visible>
<%= f.fields_for :preferences do |ff| %>
<% @template.submitters.each_with_index do |submitter, index| %>
<div id="request_email_<%= submitter['uuid'] %>" class="<%= 'hidden' if index != 0 %>">
<% submitter_preferences = f.object.preferences['submitters'].to_a.find { |e| e['uuid'] == submitter['uuid'] } || {} %>
<%= ff.fields_for :submitters, Struct.new(:request_email_subject, :request_email_body).new(*(submitter_preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence || @template.preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence || AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY).value.values_at('subject', 'body'))), index: nil do |fff| %>
<%= fff.hidden_field :uuid, value: submitter['uuid'] %>
<div class="form-control">
<%= fff.label :request_email_subject, t('email_subject'), class: 'label' %>
<%= fff.text_field :request_email_subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= fff.label :request_email_body, t('email_body'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= fff.text_area :request_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<% end %>
</div>
<% end %>
<% end %>
</div>
<% end %>
<div class="form-control pt-2">
@@ -118,7 +168,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, :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| %>
<%= f.fields_for :preferences, Struct.new(:documents_copy_email_reply_to, :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_reply_to'].presence || configs['reply_to'], @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' %>
@@ -134,6 +184,12 @@
<%= ff.text_area :documents_copy_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<% if can?(:manage, :reply_to) %>
<div class="form-control">
<%= ff.label :documents_copy_email_reply_to, t('reply_to'), class: 'label' %>
<%= ff.email_field :documents_copy_email_reply_to, class: 'base-input', dir: 'auto', placeholder: t(:email) %>
</div>
<% end %>
<div class="flex items-center justify-between pt-2.5 px-1 mb-2">
<span>
<%= t('attach_documents_to_the_email') %>
+1 -1
View File
@@ -60,7 +60,7 @@
</span>
</td>
<td>
<%= user.last_sign_in_at ? l(user.last_sign_in_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) : '-' %>
<%= user.current_sign_in_at ? l(user.current_sign_in_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) : '-' %>
</td>
<td class="flex items-center space-x-2 justify-end">
<% if params[:status].blank? && can?(:update, user) && user.archived_at.blank? %>
+90 -12
View File
@@ -20,9 +20,12 @@ en: &en
language_ko: 한국어
hi_there: Hi there
thanks: Thanks
edit_per_party: Edit per party
reply_to: Reply to
pending_by_me: Pending by me
partially_completed: Partially completed
unarchive: Unarchive
signed: Signed
first_party: 'First Party'
remove_filter: Remove filter
add: Add
@@ -40,6 +43,7 @@ en: &en
hello_name: Hello %{name}
you_are_invited_to_product_name: You are invited to %{product_name}
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'You have been invited to %{account_name} %{product_name}. Please sign up using the link below:'
sent_using_product_name_in_testing_mode_html: 'Sent using <a href="%{product_url}">%{product_name}</a> in testing mode'
sent_using_product_name_free_document_signing_html: 'Sent using <a href="%{product_url}">%{product_name}</a> free document signing.'
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Sign documents with trusted certificate provided by DocuSeal. Your documents and data are never shared with DocuSeal. PDF checksum is provided to generate a trusted signature.
you_have_been_invited_to_submit_the_name_form: 'You have been invited to submit the "%{name}" form.'
@@ -163,7 +167,7 @@ en: &en
schedule_account_for_deletion_: Schedule account for deletion?
account_information_has_been_updated: Account information has been updated.
should_be_a_valid_url: should be a valid URL
your_account_removal_request_will_be_processed_within_2_weeks_please_contact_us_if_you_want_to_keep_your_account: Your account removal request will be processed within 2 weeks. Please contact us if you want to keep your account.
your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Your account removal request will be processed within 2 months. Please contact us if you want to keep your account.
test_mode: Test mode
copy: Copy
copied: Copied
@@ -682,6 +686,15 @@ en: &en
click_here_to_learn_more_about_user_roles_and_permissions_html: '<a href="https://www.docuseal.com/resources/manage-users-and-roles" class="link" rel="noopener noreferrer nofollow" target="_blank">Click here</a> to learn more about user roles and permissions.'
count_10_signature_request_emails_sent_this_month_upgrade_to_pro_to_send_unlimited_signature_request_email: '%{count} / 10 signature request emails sent this month. Upgrade to Pro to send unlimited signature request email.'
test_mode_emails_limit_will_be_reset_within_24_hours: Test mode emails limit will be reset within 24 hours.
on_a_scale_of_1_to_10_how_satisfied_are_you_with_the_docuseal_product_: On a scale of 1 to 10, how satisfied are you with the DocuSeal product?
tell_us_more_about_your_experience: Tell us more about your experience
extremely_dissatisfied: Extremely Dissatisfied
extremely_satisfied: Extremely Satisfied
your_pro_plan_payment_is_overdue: Your Pro plan payment is overdue.
click_here_to_update_your_payment_details_and_clear_the_invoice_to_ensure_uninterrupted_service_html: <a href="%{url}" class="link">Click here</a> to update your payment details and clear the invoice to ensure uninterrupted service.
overdue_payment: Overdue Payment
your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: Your Pro Plan has been suspended due to unpaid invoices. You can update your payment details to settle the invoice and continue using DocuSeal or cancel your subscription.
manage_subscription: Manage Subscription
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}'
@@ -720,6 +733,9 @@ en: &en
read: Read your data
es: &es
edit_per_party: Editar por parte
signed: Firmado
reply_to: Responder a
partially_completed: Parcialmente completado
pending_by_me: Pendiente por mi
add: Agregar
@@ -742,6 +758,7 @@ es: &es
hello_name: Hola %{name}
you_are_invited_to_product_name: Estás invitado a %{product_name}
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Has sido invitado a %{account_name} %{product_name}. Por favor, regístrate usando el enlace a continuación:'
sent_using_product_name_in_testing_mode_html: 'Enviado usando <a href="%{product_url}">%{product_name}</a> en Modo de Prueba'
sent_using_product_name_free_document_signing_html: 'Enviado usando la firma de documentos gratuita de <a href="%{product_url}">%{product_name}</a>.'
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Firme documentos con un certificado de confianza proporcionado por DocuSeal. Sus documentos y datos nunca se comparten con DocuSeal. Se proporciona un checksum de PDF para generar una firma de confianza.
hi_there: Hola
@@ -865,7 +882,7 @@ es: &es
schedule_account_for_deletion_: ¿Programar la eliminación de la cuenta?
account_information_has_been_updated: La información de la cuenta ha sido actualizada.
should_be_a_valid_url: debe ser una URL válida
your_account_removal_request_will_be_processed_within_2_weeks_please_contact_us_if_you_want_to_keep_your_account: Tu solicitud de eliminación de cuenta se procesará en un plazo de 2 semanas. Por favor contáctanos si deseas mantener tu cuenta.
your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Tu solicitud de eliminación de cuenta se procesará en un plazo de 2 meses. Por favor contáctanos si deseas mantener tu cuenta.
test_mode: Modo de prueba
copy: Copiar
copied: Copiado
@@ -1384,6 +1401,15 @@ es: &es
click_here_to_learn_more_about_user_roles_and_permissions_html: '<a href="https://www.docuseal.com/resources/manage-users-and-roles" class="link" rel="noopener noreferrer nofollow" target="_blank">Haz clic aquí</a> para obtener más información sobre los roles y permisos de usuario.'
count_10_signature_request_emails_sent_this_month_upgrade_to_pro_to_send_unlimited_signature_request_email: '%{count} / 10 correos electrónicos de solicitud de firma enviados este mes. Mejora a Pro para enviar solicitudes de firma ilimitadas.'
test_mode_emails_limit_will_be_reset_within_24_hours: El límite de correos electrónicos en modo de prueba se restablecerá en 24 horas.
on_a_scale_of_1_to_10_how_satisfied_are_you_with_the_docuseal_product_: 'En una escala del 1 al 10, ¿qué tan satisfecho estás con el producto DocuSeal?'
tell_us_more_about_your_experience: Cuéntanos más sobre tu experiencia
extremely_dissatisfied: Extremadamente insatisfecho
extremely_satisfied: Extremadamente satisfecho
your_pro_plan_payment_is_overdue: El pago de tu plan Pro está atrasado.
click_here_to_update_your_payment_details_and_clear_the_invoice_to_ensure_uninterrupted_service_html: '<a href="%{url}" class="link">Haz clic aquí</a> para actualizar tus datos de pago y liquidar la factura para garantizar un servicio ininterrumpido.'
overdue_payment: Pago Atrasado
your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: Tu plan Pro ha sido suspendido debido a facturas impagas. Puedes actualizar tus datos de pago para liquidar la factura y seguir usando DocuSeal o cancelar tu suscripción.
manage_subscription: Gestionar Suscripción
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}'
@@ -1422,6 +1448,9 @@ es: &es
read: Leer tus datos
it: &it
edit_per_party: Modifica per partito
signed: Firmato
reply_to: Rispondi a
pending_by_me: In sospeso da me
add: Aggiungi
adding: Aggiungendo
@@ -1437,12 +1466,13 @@ it: &it
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
enforce_recipients_order: "Applicare l'ordine dei destinatari"
first_party: 'Prima parte'
docuseal_trusted_signature: "Firma Fiduciaria DocuSeal"
hello_name: Ciao %{name}
you_are_invited_to_product_name: Sei stato invitato a %{product_name}
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Sei stato invitato a %{account_name} %{product_name}. Registrati utilizzando il link qui sotto:'
sent_using_product_name_in_testing_mode_html: 'Inviato utilizzando <a href="%{product_url}">%{product_name}</a> in Modalità di Test'
sent_using_product_name_free_document_signing_html: 'Inviato utilizzando la firma di documenti gratuita di <a href="%{product_url}">%{product_name}</a>.'
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: "Firma documenti con un certificato di fiducia fornito da DocuSeal. I tuoi documenti e i tuoi dati non vengono mai condivisi con DocuSeal. Il checksum PDF è fornito per generare una firma di fiducia."
hi_there: Ciao
@@ -1566,7 +1596,7 @@ it: &it
schedule_account_for_deletion_: "Programmare l'eliminazione dell'account?"
account_information_has_been_updated: "Le informazioni dell'account sono state aggiornate."
should_be_a_valid_url: deve essere un URL valido
your_account_removal_request_will_be_processed_within_2_weeks_please_contact_us_if_you_want_to_keep_your_account: "La tua richiesta di rimozione dell'account sarà elaborata entro 2 settimane. Contattaci se desideri mantenere il tuo account."
your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account: "La tua richiesta di rimozione dell'account sarà elaborata entro 2 mesi. Contattaci se desideri mantenere il tuo account."
test_mode: Modalità di test
copy: Copia
copied: Copiato
@@ -2085,6 +2115,15 @@ it: &it
click_here_to_learn_more_about_user_roles_and_permissions_html: '<a href="https://www.docuseal.com/resources/manage-users-and-roles" class="link" rel="noopener noreferrer nofollow" target="_blank">Fai clic qui</a> per saperne di più sui ruoli e le autorizzazioni degli utenti.'
count_10_signature_request_emails_sent_this_month_upgrade_to_pro_to_send_unlimited_signature_request_email: '%{count} / 10 e-mail di richiesta di firma inviati questo mese. Passa a Pro per inviare richieste di firma illimitate.'
test_mode_emails_limit_will_be_reset_within_24_hours: Il limite di e-mail in modalità di test verrà ripristinato entro 24 ore.
on_a_scale_of_1_to_10_how_satisfied_are_you_with_the_docuseal_product_: 'Su una scala da 1 a 10, quanto sei soddisfatto del prodotto DocuSeal?'
tell_us_more_about_your_experience: Raccontaci di più sulla tua esperienza
extremely_dissatisfied: Estremamente insoddisfatto
extremely_satisfied: Estremamente soddisfatto
your_pro_plan_payment_is_overdue: Il pagamento del tuo piano Pro è in ritardo.
click_here_to_update_your_payment_details_and_clear_the_invoice_to_ensure_uninterrupted_service_html: '<a href="%{url}" class="link">Fai clic qui</a> per aggiornare i tuoi dati di pagamento e saldare la fattura per garantire un servizio ininterrotto.'
overdue_payment: Pagamento Scaduto
your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: Il tuo piano Pro è stato sospeso a causa di fatture non pagate. Puoi aggiornare i tuoi dati di pagamento per saldare la fattura e continuare a utilizzare DocuSeal o annullare l'abbonamento.
manage_subscription: Gestisci Abbonamento
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}'
@@ -2123,6 +2162,9 @@ it: &it
read: Leggi i tuoi dati
fr: &fr
edit_per_party: Éditer par partie
signed: Signé
reply_to: Répondre à
partially_completed: Partiellement complété
pending_by_me: En attente par moi
add: Ajouter
@@ -2145,6 +2187,7 @@ fr: &fr
hello_name: Bonjour %{name}
you_are_invited_to_product_name: Vous êtes invité à %{product_name}
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Vous avez été invité à %{account_name} %{product_name}. Veuillez vous inscrire en utilisant le lien ci-dessous:'
sent_using_product_name_in_testing_mode_html: 'Envoyé en utilisant <a href="%{product_url}">%{product_name}</a> en Mode Test'
sent_using_product_name_free_document_signing_html: 'Envoyé en utilisant la signature de documents gratuite de <a href="%{product_url}">%{product_name}</a>.'
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Signez des documents avec un certificat de confiance fourni par DocuSeal. Vos documents et données ne sont jamais partagés avec DocuSeal. Un checksum PDF est fourni pour générer une signature de confiance.
hi_there: Bonjour
@@ -2269,7 +2312,7 @@ fr: &fr
schedule_account_for_deletion_: Programmer la suppression du compte?
account_information_has_been_updated: Les informations du compte ont été mises à jour.
should_be_a_valid_url: doit être une URL valide
your_account_removal_request_will_be_processed_within_2_weeks_please_contact_us_if_you_want_to_keep_your_account: Votre demande de suppression du compte sera traitée dans un délai de 2 semaines. Veuillez nous contacter si vous souhaitez conserver votre compte.
your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Votre demande de suppression du compte sera traitée dans un délai de 2 mois. Veuillez nous contacter si vous souhaitez conserver votre compte.
test_mode: Mode test
copy: Copier
copied: Copié
@@ -2741,11 +2784,11 @@ fr: &fr
unverified: Non vérifié
document: Document
completed_at: Terminé le
edit_recipient: Modifica Destinatario
update_recipient: Aggiorna Destinatario
use_international_format_1xxx_: 'Utilizza il formato internazionale: +1xxx...'
submitter_cannot_be_updated: Il mittente non può essere aggiornato.
at_least_one_field_must_be_filled: Almeno un campo deve essere compilato.
edit_recipient: Modifier le destinataire
update_recipient: Mettre à jour le destinataire
use_international_format_1xxx_: 'Utilisez un format international : +1xxx...'
submitter_cannot_be_updated: Le soumissionnaire ne peut pas être mis à jour.
at_least_one_field_must_be_filled: Au moins un champ doit être rempli.
archived_users: Utilisateurs Archivés
embedding_users: Utilisateurs Intégrés
view_embedding_users: Voir les Utilisateurs Intégrés
@@ -2788,6 +2831,15 @@ fr: &fr
click_here_to_learn_more_about_user_roles_and_permissions_html: '<a href="https://www.docuseal.com/resources/manage-users-and-roles" class="link" rel="noopener noreferrer nofollow" target="_blank">Cliquez ici</a> pour en savoir plus sur les rôles et les autorisations des utilisateurs.'
count_10_signature_request_emails_sent_this_month_upgrade_to_pro_to_send_unlimited_signature_request_email: '%{count} / 10 e-mails de demande de signature envoyés ce mois-ci. Passez à Pro pour envoyer des demandes de signature illimitées.'
test_mode_emails_limit_will_be_reset_within_24_hours: La limite d'e-mails en mode test sera réinitialisée dans les 24 heures.
on_a_scale_of_1_to_10_how_satisfied_are_you_with_the_docuseal_product_: 'Sur une échelle de 1 à 10, à quel point êtes-vous satisfait du produit DocuSeal?'
tell_us_more_about_your_experience: Parlez-nous davantage de votre expérience
extremely_dissatisfied: Extrêmement insatisfait
extremely_satisfied: Extrêmement satisfait
your_pro_plan_payment_is_overdue: Le paiement de votre plan Pro est en retard.
click_here_to_update_your_payment_details_and_clear_the_invoice_to_ensure_uninterrupted_service_html: '<a href="%{url}" class="link">Cliquez ici</a> pour mettre à jour vos informations de paiement et régler la facture afin de garantir un service ininterrompu.'
overdue_payment: Paiement En Retard
your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: Votre plan Pro a été suspendu en raison de factures impayées. Vous pouvez mettre à jour vos informations de paiement pour régler la facture et continuer à utiliser DocuSeal ou annuler votre abonnement.
manage_subscription: Gérer l'Abonnement
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}'
@@ -2826,6 +2878,9 @@ fr: &fr
read: Lire vos données
pt: &pt
edit_per_party: Edita por festa
signed: Assinado
reply_to: Responder a
partially_completed: Parcialmente concluído
pending_by_me: Pendente por mim
add: Adicionar
@@ -2848,6 +2903,7 @@ pt: &pt
hello_name: Olá %{name}
you_are_invited_to_product_name: Você está convidado para %{product_name}
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Você foi convidado para %{account_name} %{product_name}. Inscreva-se usando o link abaixo:'
sent_using_product_name_in_testing_mode_html: 'Enviado usando <a href="%{product_url}">%{product_name}</a> no Modo de Teste'
sent_using_product_name_free_document_signing_html: 'Enviado usando a assinatura gratuita de documentos de <a href="%{product_url}">%{product_name}</a>.'
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Assine documentos com certificado confiável fornecido pela DocuSeal. Seus documentos e dados nunca são compartilhados com a DocuSeal. O checksum do PDF é fornecido para gerar uma assinatura confiável.
hi_there: Olá
@@ -2971,7 +3027,7 @@ pt: &pt
schedule_account_for_deletion_: Agendar exclusão da conta?
account_information_has_been_updated: As informações da conta foram atualizadas.
should_be_a_valid_url: deve ser um URL válido
your_account_removal_request_will_be_processed_within_2_weeks_please_contact_us_if_you_want_to_keep_your_account: Seu pedido de remoção da conta será processado em até 2 semanas. Entre em contato conosco se você quiser manter sua conta.
your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Seu pedido de remoção da conta será processado em até 2 meses. Entre em contato conosco se você quiser manter sua conta.
test_mode: Modo de teste
copy: Copiar
copied: Copiado
@@ -3490,6 +3546,15 @@ pt: &pt
click_here_to_learn_more_about_user_roles_and_permissions_html: '<a href="https://www.docuseal.com/resources/manage-users-and-roles" class="link" rel="noopener noreferrer nofollow" target="_blank">Clique aqui</a> para saber mais sobre os papéis e permissões dos usuários.'
count_10_signature_request_emails_sent_this_month_upgrade_to_pro_to_send_unlimited_signature_request_email: '%{count} / 10 e-mails de solicitação de assinatura enviados este mês. Faça upgrade para Pro para enviar solicitações de assinatura ilimitadas.'
test_mode_emails_limit_will_be_reset_within_24_hours: O limite de e-mails no modo de teste será redefinido em 24 horas.
on_a_scale_of_1_to_10_how_satisfied_are_you_with_the_docuseal_product_: 'Em uma escala de 1 a 10, quão satisfeito você está com o produto DocuSeal?'
tell_us_more_about_your_experience: Conte-nos mais sobre sua experiência
extremely_dissatisfied: Extremamente insatisfeito
extremely_satisfied: Extremamente satisfeito
your_pro_plan_payment_is_overdue: O pagamento do seu plano Pro está atrasado.
click_here_to_update_your_payment_details_and_clear_the_invoice_to_ensure_uninterrupted_service_html: '<a href="%{url}" class="link">Clique aqui</a> para atualizar seus dados de pagamento e quitar a fatura para garantir um serviço ininterrupto.'
overdue_payment: Pagamento Atrasado
your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: Seu plano Pro foi suspenso devido a faturas não pagas. Você pode atualizar seus dados de pagamento para quitar a fatura e continuar usando o DocuSeal ou cancelar sua assinatura.
manage_subscription: Gerenciar Assinatura
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}'
@@ -3528,6 +3593,9 @@ pt: &pt
read: Ler seus dados
de: &de
edit_per_party: Bearbeiten pro Partei
signed: Unterschrieben
reply_to: Antworten auf
partially_completed: Teilweise abgeschlossen
pending_by_me: Ausstehend von mir
add: Hinzufügen
@@ -3550,6 +3618,7 @@ de: &de
hello_name: Hallo %{name}
you_are_invited_to_product_name: Sie sind eingeladen zu %{product_name}
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Sie wurden zu %{account_name} %{product_name} eingeladen. Bitte registrieren Sie sich über den folgenden Link:'
sent_using_product_name_in_testing_mode_html: 'Gesendet über <a href="%{product_url}">%{product_name}</a> im Testmodus'
sent_using_product_name_free_document_signing_html: 'Gesendet mit der kostenlosen Dokumentensignierung von <a href="%{product_url}">%{product_name}</a>.'
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Unterzeichnen Sie Dokumente mit einem vertrauenswürdigen Zertifikat von DocuSeal. Ihre Dokumente und Daten werden niemals mit DocuSeal geteilt. Eine PDF-Prüfziffer wird bereitgestellt, um eine vertrauenswürdige Signatur zu generieren.
hi_there: Hallo
@@ -3673,7 +3742,7 @@ de: &de
schedule_account_for_deletion_: Konto zur Löschung einplanen?
account_information_has_been_updated: Die Kontoinformationen wurden aktualisiert.
should_be_a_valid_url: sollte eine gültige URL sein
your_account_removal_request_will_be_processed_within_2_weeks_please_contact_us_if_you_want_to_keep_your_account: Deine Anfrage zur Kontolöschung wird innerhalb von 2 Wochen bearbeitet. Bitte kontaktiere uns, wenn du dein Konto behalten möchtest.
your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Deine Anfrage zur Kontolöschung wird innerhalb von 2 Monaten bearbeitet. Bitte kontaktiere uns, wenn du dein Konto behalten möchtest.
test_mode: Testmodus
copy: Kopieren
copied: Kopiert
@@ -4192,6 +4261,15 @@ de: &de
click_here_to_learn_more_about_user_roles_and_permissions_html: '<a href="https://www.docuseal.com/resources/manage-users-and-roles" class="link" rel="noopener noreferrer nofollow" target="_blank">Klicken Sie hier</a>, um mehr über Benutzerrollen und -berechtigungen zu erfahren.'
count_10_signature_request_emails_sent_this_month_upgrade_to_pro_to_send_unlimited_signature_request_email: '%{count} / 10 Signaturanfrage-E-Mails wurden diesen Monat gesendet. Upgraden Sie zu Pro, um unbegrenzte Signaturanfragen zu senden.'
test_mode_emails_limit_will_be_reset_within_24_hours: Das Limit für E-Mails im Testmodus wird innerhalb von 24 Stunden zurückgesetzt.
on_a_scale_of_1_to_10_how_satisfied_are_you_with_the_docuseal_product_: 'Auf einer Skala von 1 bis 10, wie zufrieden sind Sie mit dem DocuSeal-Produkt?'
tell_us_more_about_your_experience: Erzählen Sie uns mehr über Ihre Erfahrung
extremely_dissatisfied: Extrem unzufrieden
extremely_satisfied: Extrem zufrieden
your_pro_plan_payment_is_overdue: Ihre Zahlung für den Pro-Plan ist überfällig.
click_here_to_update_your_payment_details_and_clear_the_invoice_to_ensure_uninterrupted_service_html: '<a href="%{url}" class="link">Klicken Sie hier</a>, um Ihre Zahlungsdaten zu aktualisieren und die Rechnung zu begleichen, um einen unterbrechungsfreien Service sicherzustellen.'
overdue_payment: Überfällige Zahlung
your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: Ihr Pro-Plan wurde aufgrund unbezahlter Rechnungen ausgesetzt. Sie können Ihre Zahlungsdaten aktualisieren, um die Rechnung zu begleichen und DocuSeal weiterhin zu nutzen, oder Ihr Abonnement kündigen.
manage_subscription: Abonnement Verwalten
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}'
+3 -3
View File
@@ -12,6 +12,7 @@ Rails.application.routes.draw do
root 'dashboard#index'
get 'up' => 'rails/health#show'
get 'manifest' => 'pwa#manifest'
devise_for :users,
path: '/', only: %i[sessions passwords omniauth_callbacks],
@@ -79,6 +80,7 @@ Rails.application.routes.draw do
resource :testing_account, only: %i[show destroy]
resources :testing_api_settings, only: %i[index]
resources :submitters_autocomplete, only: %i[index]
resources :submitters_resubmit, only: %i[update]
resources :template_folders_autocomplete, only: %i[index]
resources :webhook_secret, only: %i[show update]
resources :webhook_preferences, only: %i[update]
@@ -143,9 +145,7 @@ Rails.application.routes.draw do
get :completed
end
resources :send_submission_email, only: %i[create] do
get :success, on: :collection
end
resources :send_submission_email, only: %i[create]
resources :submitters, only: %i[], param: 'slug' do
resources :download, only: %i[index], controller: 'submissions_download'
+1 -1
View File
@@ -25,7 +25,7 @@ module Abilities
account_ids = [user.account_id, TemplateSharing::ALL_ID]
template.template_sharings.any? do |e|
e.account_id.in?(account_ids) && (ability.nil? || e.ability == ability)
e.account_id.in?(account_ids) && (ability.nil? || e.ability == 'manage' || e.ability == ability)
end
end
end
+4
View File
@@ -153,6 +153,10 @@ module Accounts
EncryptedConfig.exists?(key: EncryptedConfig::EMAIL_SMTP_KEY)
end
def can_send_invitation_emails?(_account)
true
end
def normalize_timezone(timezone)
tzinfo = TZInfo::Timezone.get(ActiveSupport::TimeZone::MAPPING[timezone] || timezone)
+4
View File
@@ -69,6 +69,10 @@ module Docuseal
@default_pkcs ||= GenerateCertificate.load_pkcs(Docuseal::CERTS)
end
def enable_pwa?
true
end
def trusted_certs
@trusted_certs ||=
ENV['TRUSTED_CERTS'].to_s.gsub('\\n', "\n").split("\n\n").map do |base64|
+13 -2
View File
@@ -4,7 +4,16 @@ module NumberUtils
FORMAT_LOCALES = {
'dot' => 'de',
'space' => 'fr',
'comma' => 'en'
'comma' => 'en',
'usd' => 'en',
'eur' => 'fr',
'gbp' => 'en'
}.freeze
CURRENCY_SYMBOLS = {
'usd' => '$',
'eur' => '€',
'gbp' => '£'
}.freeze
module_function
@@ -12,7 +21,9 @@ module NumberUtils
def format_number(number, format)
locale = FORMAT_LOCALES[format]
if locale
if CURRENCY_SYMBOLS[format]
ApplicationController.helpers.number_to_currency(number, locale:, precision: 2, unit: CURRENCY_SYMBOLS[format])
elsif locale
ApplicationController.helpers.number_with_delimiter(number, locale:)
else
number
+8
View File
@@ -24,6 +24,10 @@ module PdfIcons
StringIO.new(logo_new_data)
end
def stamp_logo_io
StringIO.new(stamp_logo_data)
end
def check_data
@check_data ||= PATH.join('check.png').read
end
@@ -39,4 +43,8 @@ module PdfIcons
def logo_new_data
@logo_new_data ||= PATH.join('logo_new.png').read
end
def stamp_logo_data
@stamp_logo_data ||= PATH.join('stamp-logo.png').read
end
end
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

+26
View File
@@ -0,0 +1,26 @@
# frozen_string_literal: true
module SendWebhookRequest
USER_AGENT = 'DocuSeal.com Webhook'
module_function
def call(webhook_url, event_type:, data:)
Faraday.post(webhook_url.url) do |req|
req.headers['Content-Type'] = 'application/json'
req.headers['User-Agent'] = USER_AGENT
req.headers.merge!(webhook_url.secret.to_h) if webhook_url.secret.present?
req.body = {
event_type: event_type,
timestamp: Time.current,
data: data
}.to_json
req.options.read_timeout = 8
req.options.open_timeout = 8
end
rescue Faraday::Error
nil
end
end
+55 -22
View File
@@ -107,20 +107,27 @@ module Submissions
def normalize_email(email)
return if email.blank?
return email.downcase if email.to_s.include?(',')
return email.downcase if email.to_s.include?('.gob')
return email.downcase if email.to_s.include?('.om')
return email.downcase if email.to_s.include?('.mm')
return email.downcase if email.to_s.include?('.cm')
return email.downcase if email.to_s.include?('.et')
return email.downcase if email.to_s.include?('.mo')
return email.downcase if email.to_s.include?('.nz')
return email.downcase if email.to_s.include?('.za')
return email.downcase unless email.to_s.include?('.')
return email.downcase if email.to_s.include?(',') ||
email.to_s.match?(/\.(?:gob|om|mm|cm|et|mo|nz|za|ie)\z/) ||
email.to_s.exclude?('.')
fixed_email = EmailTypo.call(email.delete_prefix('<'))
Rails.logger.info("Fixed email #{email.split('@').last}") if fixed_email != email.downcase.delete_prefix('<').strip
return fixed_email if fixed_email == email
domain = email.to_s.split('@').last.to_s.downcase
fixed_domain = fixed_email.to_s.split('@').last
return email.downcase if domain == fixed_domain
if DidYouMean::Levenshtein.distance(domain, fixed_domain) > 3
Rails.logger.info("Skipped email fix #{domain}")
return email.downcase
end
Rails.logger.info("Fixed email #{domain}") if fixed_email != email.downcase.delete_prefix('<').strip
fixed_email
end
@@ -135,31 +142,57 @@ module Submissions
values ||= submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) }
next unless check_document_conditions(item, values, fields_uuid_index, include_submitter_uuid:)
next unless check_item_conditions(item, values, fields_uuid_index, include_submitter_uuid:)
end
item
end
end
def check_document_conditions(item, values, fields_index, include_submitter_uuid: nil)
def filtered_conditions_fields(submitter, only_submitter_fields: true)
fields = submitter.submission.template_fields || submitter.submission.template.fields
fields_uuid_index = nil
values = nil
fields.filter_map do |field|
next if field['submitter_uuid'] != submitter.uuid && only_submitter_fields
if field['conditions'].present?
fields_uuid_index ||= fields.index_by { |f| f['uuid'] }
values ||= submitter.submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) }
submitter_conditions = []
next unless check_item_conditions(field, values, fields_uuid_index,
include_submitter_uuid: submitter.uuid,
submitter_conditions_acc: submitter_conditions)
field = field.merge('conditions' => submitter_conditions) if submitter_conditions != field['conditions']
end
field
end
end
def check_item_conditions(item, values, fields_index, include_submitter_uuid: nil, submitter_conditions_acc: nil)
return true if item['conditions'].blank?
item['conditions'].all? do |condition|
item['conditions'].each_with_object([]) do |condition, acc|
result =
if fields_index[condition['field_uuid']]['submitter_uuid'] == include_submitter_uuid
submitter_conditions_acc << condition if submitter_conditions_acc
true
else
Submitters::SubmitValues.check_field_condition(condition, values, fields_index)
end
item['conditions'].each_with_object([]) do |c, acc|
if c['operation'] == 'or'
acc.push(acc.pop || result)
else
acc.push(result)
end
end.exclude?(false)
end
if condition['operation'] == 'or'
acc.push(acc.pop || result)
else
acc.push(result)
end
end.exclude?(false)
end
end
+25 -7
View File
@@ -31,12 +31,13 @@ module Submissions
next if uuid.blank?
next if submitter_attrs.slice('email', 'phone', 'name').compact_blank.blank?
submission.template_submitters << template.submitters.find { |e| e['uuid'] == uuid }
template_submitter = template.submitters.find { |e| e['uuid'] == uuid }
submission.template_submitters << template_submitter.except('optional_invite_by_uuid', 'invite_by_uuid')
is_order_sent = submitters_order == 'random' || index.zero?
build_submitter(submission:, attrs: submitter_attrs, uuid:,
is_order_sent:, user:,
build_submitter(submission:, attrs: submitter_attrs,
uuid:, is_order_sent:, user:, params:,
preferences: preferences.merge(submission_preferences))
end
@@ -54,14 +55,30 @@ module Submissions
# rubocop:enable Metrics/BlockLength
def maybe_add_invite_submitters(submission, template)
template.submitters.each do |item|
template.submitters.each_with_index do |item, index|
next if item['invite_by_uuid'].blank? && item['optional_invite_by_uuid'].blank?
next if submission.template_submitters.any? { |e| e['uuid'] == item['uuid'] }
submission.template_submitters << item
if index.zero?
submission.template_submitters.insert(1, item)
elsif submission.template_submitters.size > index
submission.template_submitters.insert(index, item)
else
submission.template_submitters << item
end
end
end
def submitter_message_preferences(uuid, params)
return {} if params[:request_email_per_submitter] != '1'
return {} if params[:is_custom_message] != '1'
{
'subject' => params.dig('submitter_preferences', uuid, 'subject'),
'body' => params.dig('submitter_preferences', uuid, 'body')
}.compact_blank
end
def maybe_set_template_fields(submission, submitters_attrs, default_submitter_uuid: nil)
template_fields = (submission.template_fields || submission.template.fields).deep_dup
@@ -168,9 +185,10 @@ module Submissions
uuid || template.submitters[index]&.dig('uuid')
end
def build_submitter(submission:, attrs:, uuid:, is_order_sent:, user:, preferences:)
def build_submitter(submission:, attrs:, uuid:, is_order_sent:, user:, preferences:, params:)
email = Submissions.normalize_email(attrs[:email])
submitter_preferences = Submitters.normalize_preferences(submission.account, user, attrs)
submitter_preferences = Submitters.normalize_preferences(submission.account, user,
attrs.merge(submitter_message_preferences(uuid, params)))
values = attrs[:values] || {}
phone_field_uuid = find_phone_field(submission, values)&.dig('uuid')
+36 -21
View File
@@ -37,30 +37,45 @@ module Submissions
submissions.where(created_by_user_id: user&.id || -1)
end
# rubocop:disable Metrics/MethodLength
def filter_by_status(submissions, filters)
submissions = submissions.pending if filters[:status] == 'pending'
submissions = submissions.completed if filters[:status] == 'completed'
submissions = submissions.declined if filters[:status] == 'declined'
submissions = submissions.expired if filters[:status] == 'expired'
if filters[:status] == 'partially_completed'
submissions =
submissions.joins(:submitters)
.group(:id)
.having(Arel::Nodes::NamedFunction.new(
'COUNT', [Arel::Nodes::NamedFunction.new('NULLIF',
[Submitter.arel_table[:completed_at].eq(nil),
Arel::Nodes.build_quoted(false)])]
).gt(0))
.having(Arel::Nodes::NamedFunction.new(
'COUNT', [Arel::Nodes::NamedFunction.new('NULLIF',
[Submitter.arel_table[:completed_at].not_eq(nil),
Arel::Nodes.build_quoted(false)])]
).gt(0))
case filters[:status]
when 'pending'
submissions.pending
when 'completed'
submissions.completed
when 'declined'
submissions.declined
when 'expired'
submissions.expired
when 'sent'
submissions.joins(:submitters)
.where(submitters: { opened_at: nil, completed_at: nil, declined_at: nil })
.where.not(submitters: { sent_at: nil })
.group(:id)
when 'opened'
submissions.joins(:submitters)
.where(submitters: { completed_at: nil, declined_at: nil })
.where.not(submitters: { opened_at: nil })
.group(:id)
when 'partially_completed'
submissions.joins(:submitters)
.group(:id)
.having(Arel::Nodes::NamedFunction.new(
'COUNT', [Arel::Nodes::NamedFunction.new('NULLIF',
[Submitter.arel_table[:completed_at].eq(nil),
Arel::Nodes.build_quoted(false)])]
).gt(0))
.having(Arel::Nodes::NamedFunction.new(
'COUNT', [Arel::Nodes::NamedFunction.new('NULLIF',
[Submitter.arel_table[:completed_at].not_eq(nil),
Arel::Nodes.build_quoted(false)])]
).gt(0))
else
submissions
end
submissions
end
# rubocop:enable Metrics/MethodLength
def filter_by_created_at(submissions, filters)
submissions = submissions.where(created_at: filters[:created_at_from]..) if filters[:created_at_from].present?
+5 -1
View File
@@ -175,7 +175,7 @@ module Submissions
padding: [15, 0, 8, 0],
position: :float)
unless submission.source.in?(%w[embed api])
if show_verify?(submission)
column.formatted_text([{ link: verify_url, text: I18n.t('verify'), style: :link }],
font_size: 9, padding: [15, 0, 10, 0], position: :float, text_align: :right)
end
@@ -425,6 +425,10 @@ module Submissions
def maybe_add_background(_canvas, _submission, _page_size); end
def show_verify?(submission)
!submission.source.in?(%w[embed api])
end
def add_logo(column, _submission = nil)
column.image(PdfIcons.logo_io, width: 40, height: 40, position: :float)
+16 -12
View File
@@ -18,6 +18,8 @@ module Submissions
TEXT_TOP_MARGIN = 1
MAX_PAGE_ROTATE = 20
COURIER_FONT = 'Courier'
A4_SIZE = [595, 842].freeze
TESTING_FOOTER = 'Testing Document - NOT LEGALLY BINDING'
@@ -188,7 +190,8 @@ module Submissions
fill_color = field.dig('preferences', 'color').presence
font = pdf.fonts.add(field.dig('preferences', 'font').presence || FONT_NAME)
font_name = field.dig('preferences', 'font').presence || FONT_NAME
font = pdf.fonts.add(font_name)
value = submitter.values[field['uuid']]
value = field['default_value'] if field['type'] == 'heading'
@@ -435,18 +438,19 @@ module Submissions
value = TextUtils.maybe_rtl_reverse(Array.wrap(value).join(', '))
text = HexaPDF::Layout::TextFragment.create(value, font:,
fill_color:,
font_size:)
text_params = { font:, fill_color:, font_size: }
text_params[:line_height] = text_params[:font_size] * 1.6 if font_name == COURIER_FONT
text = HexaPDF::Layout::TextFragment.create(value, **text_params)
lines = layouter.fit([text], area['w'] * width, height).lines
box_height = lines.sum(&:height)
if preferences_font_size.blank? && box_height > (area['h'] * height) + 1
text = HexaPDF::Layout::TextFragment.create(value,
font:,
fill_color:,
font_size: (font_size / 1.4).to_i)
text_params[:font_size] = (font_size / 1.4).to_i
text_params[:line_height] = text_params[:font_size] * 1.6 if font_name == COURIER_FONT
text = HexaPDF::Layout::TextFragment.create(value, **text_params)
lines = layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width, height).lines
@@ -454,10 +458,10 @@ module Submissions
end
if preferences_font_size.blank? && box_height > (area['h'] * height) + 1
text = HexaPDF::Layout::TextFragment.create(value,
font:,
fill_color:,
font_size: (font_size / 1.9).to_i)
text_params[:font_size] = (font_size / 1.9).to_i
text_params[:line_height] = text_params[:font_size] * 1.6 if font_name == COURIER_FONT
text = HexaPDF::Layout::TextFragment.create(value, **text_params)
lines = layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width, height).lines
+13 -1
View File
@@ -123,10 +123,22 @@ module Submitters
filename = ReplaceEmailVariables.call(filename_format, submitter:)
filename = filename.gsub('{document.name}', blob.filename.base)
filename = filename.gsub(' - {submission.status}') do
if submitter.submission.submitters.all?(&:completed_at?)
status =
if submitter.submission.template_fields.any? { |f| f['type'] == 'signature' }
I18n.t(:signed)
else
I18n.t(:completed)
end
" - #{status}"
end
end
filename = filename.gsub(
'{submission.completed_at}',
I18n.l(submitter.completed_at.beginning_of_year.in_time_zone(submitter.account.timezone), format: :short)
I18n.l(submitter.completed_at.in_time_zone(submitter.account.timezone), format: :short)
)
"#{filename}.#{blob.filename.extension}"
+1 -1
View File
@@ -98,7 +98,7 @@ module Submitters
end
def load_logo(_submitter)
PdfIcons.logo_io
PdfIcons.stamp_logo_io
end
end
end
@@ -18,7 +18,7 @@ module Submitters
def find_or_create_signature_from_value(submitter, value, attachments)
_, attachment = Submitters::NormalizeValues.normalize_attachment_value(value,
'signature',
{ 'type' => 'signature' },
submitter.account,
attachments,
submitter)
+13 -5
View File
@@ -40,7 +40,7 @@ module Submitters
if field['type'].in?(%w[initials signature image file stamp]) && value.present?
new_value, new_attachments =
normalize_attachment_value(value, field['type'], template.account, attachments, for_submitter)
normalize_attachment_value(value, field, template.account, attachments, for_submitter)
attachments.push(*new_attachments)
@@ -109,17 +109,17 @@ module Submitters
.merge(fields.index_by { |e| e['name'].to_s.downcase })
end
def normalize_attachment_value(value, type, account, attachments, for_submitter = nil)
def normalize_attachment_value(value, field, account, attachments, for_submitter = nil)
if value.is_a?(Array)
new_attachments = value.map do |v|
new_attachment = find_or_build_attachment(v, type, account, for_submitter)
new_attachment = find_or_build_attachment(v, field, account, for_submitter)
attachments.find { |a| a.blob_id == new_attachment.blob_id } || new_attachment
end
[new_attachments.map(&:uuid), new_attachments]
else
new_attachment = find_or_build_attachment(value, type, account, for_submitter)
new_attachment = find_or_build_attachment(value, field, account, for_submitter)
existing_attachment = attachments.find { |a| a.blob_id == new_attachment.blob_id }
@@ -129,7 +129,9 @@ module Submitters
end
end
def find_or_build_attachment(value, type, account, for_submitter = nil)
def find_or_build_attachment(value, field, account, for_submitter = nil)
type = field['type']
blob =
if value.match?(%r{\Ahttps?://})
find_or_create_blob_from_url(account, value)
@@ -138,6 +140,8 @@ module Submitters
elsif (data = Base64.decode64(value.sub(BASE64_PREFIX_REGEXP, ''))) &&
Marcel::MimeType.for(data).exclude?('octet-stream')
find_or_create_blob_from_base64(account, data, type)
elsif type == 'image' && (value.starts_with?('<html>') || value.starts_with?('<!DOCTYPE'))
find_or_create_blob_from_html(account, value, field)
else
raise InvalidDefaultValue, "Invalid value, url, base64 or text < 60 chars is expected: #{value.first(200)}..."
end
@@ -152,6 +156,10 @@ module Submitters
attachment
end
def find_or_create_blob_from_html(_account, value, _field)
raise InvalidDefaultValue, "HTML content is not allowed: #{value.first(200)}..."
end
def find_or_create_blob_from_base64(account, data, type)
checksum = Digest::MD5.base64digest(data)
+20 -5
View File
@@ -54,8 +54,8 @@ module Submitters
submitter.ip = request.remote_ip
submitter.ua = request.user_agent
submitter.values = merge_default_values(submitter)
submitter.values = merge_formula_values(submitter)
submitter.values = maybe_remove_condition_values(submitter)
submitter.values = merge_formula_values(submitter)
submitter.values = submitter.values.transform_values do |v|
v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v
end
@@ -186,10 +186,21 @@ module Submitters
attachments_index =
Submissions.filtered_conditions_schema(submitter.submission).index_by { |i| i['attachment_uuid'] }
submitter_values = nil
is_other_submitter_conditions = submitter.submission.template_submitters.size > 1
submitter.submission.template_fields.each do |field|
next if field['submitter_uuid'] != submitter.uuid
submitter.values.delete(field['uuid']) unless check_field_conditions(submitter, field, fields_uuid_index)
submitter_values ||= submitter.values
is_other_submitter_conditions &&= field_conditions_other_submitter?(submitter, field, fields_uuid_index)
if is_other_submitter_conditions
submitter_values = submitter.submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) }
end
submitter.values.delete(field['uuid']) unless check_field_conditions(submitter_values, field, fields_uuid_index)
if field['areas'].present? && field['areas'].none? { |area| attachments_index[area['attachment_uuid']] }
submitter.values.delete(field['uuid'])
@@ -199,10 +210,14 @@ module Submitters
submitter.values
end
def check_field_conditions(submitter, field, fields_uuid_index)
return true if field['conditions'].blank?
def field_conditions_other_submitter?(submitter, field, fields_uuid_index)
field['conditions'].to_a.any? do |c|
fields_uuid_index.dig(c['field_uuid'], 'submitter_uuid') != submitter.uuid
end
end
submitter_values = submitter.values
def check_field_conditions(submitter_values, field, fields_uuid_index)
return true if field['conditions'].blank?
field['conditions'].each_with_object([]) do |c, acc|
if c['operation'] == 'or'
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 11 KiB

+11
View File
@@ -0,0 +1,11 @@
self.addEventListener('install', () => {
console.log('DocuSeal App installed')
})
self.addEventListener('activate', () => {
console.log('DocuSeal App activated')
})
self.addEventListener('fetch', (event) => {
event.respondWith(fetch(event.request))
})