Compare commits

...
87 Commits
Author SHA1 Message Date
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
Alex Turchyn cee471bfa5 Merge from docusealco/wip 2025-01-06 12:18:51 +02:00
Pete Matsyburka 81521dee81 add ability to reorder fields 2025-01-04 14:20:47 +02:00
Pete Matsyburka 3cac62d0fd fix typo 2025-01-04 01:42:25 +02:00
Pete Matsyburka 24ed3f2e7c filter submissions api 2025-01-04 01:31:54 +02:00
Alex Turchyn 02ad620c9f add translations 2025-01-04 01:10:22 +02:00
Alex Turchyn 9051d5a835 add a text hint under user roles 2025-01-03 18:39:50 +02:00
Alex Turchyn 0abd270f56 update translations 2025-01-03 01:09:29 +02:00
Pete Matsyburka fde9affd70 fix send to next 2025-01-02 23:32:24 +02:00
Pete Matsyburka 4edee2971a superadmin file download 2025-01-02 22:42:08 +02:00
Pete Matsyburka 313d7f8408 adjust response 2025-01-02 22:42:02 +02:00
Alex Turchyn db60810272 add email OTP 2025-01-01 14:38:24 +02:00
Pete Matsyburka b3507c1a3a adjust test toggle size 2024-12-31 19:57:41 +02:00
Alex Turchyn 19b5a07bac Add 'Test mode' switch to the profile dropdown 2024-12-31 19:36:14 +02:00
Pete Matsyburka 616aced4e4 fix spec 2024-12-30 21:38:02 +02:00
Pete Matsyburka c59f2a022a increase retries 2024-12-28 16:58:20 +02:00
101 changed files with 1269 additions and 469 deletions

No files matched your search

+9 -9
View File
@@ -7,13 +7,13 @@ jobs:
name: Rubocop name: Rubocop
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- name: Install Ruby - name: Install Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 3.4.1 ruby-version: 3.4.1
- name: Cache gems - name: Cache gems
uses: actions/cache@v1 uses: actions/cache@v4
with: with:
path: vendor/bundle path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }} key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
@@ -31,13 +31,13 @@ jobs:
name: Erblint name: Erblint
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- name: Install Ruby - name: Install Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 3.4.1 ruby-version: 3.4.1
- name: Cache gems - name: Cache gems
uses: actions/cache@v1 uses: actions/cache@v4
with: with:
path: vendor/bundle path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }} key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
@@ -55,7 +55,7 @@ jobs:
name: ESLint name: ESLint
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
@@ -63,7 +63,7 @@ jobs:
- name: Cache directory path - name: Cache directory path
id: yarn-cache-dir-path id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)" run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v1 - uses: actions/cache@v4
id: yarn-cache id: yarn-cache
with: with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }} path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -96,7 +96,7 @@ jobs:
--health-retries 5 --health-retries 5
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- name: Install Ruby - name: Install Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
@@ -110,12 +110,12 @@ jobs:
with: with:
chrome-version: 125 chrome-version: 125
- name: Cache node_modules - name: Cache node_modules
uses: actions/cache@v1 uses: actions/cache@v4
with: with:
path: node_modules path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
- name: Cache gems - name: Cache gems
uses: actions/cache@v1 uses: actions/cache@v4
with: with:
path: vendor/bundle path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }} key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
+5 -5
View File
@@ -7,7 +7,7 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-24.04-arm
steps: steps:
- name: Checkout code - name: Checkout code
@@ -25,22 +25,22 @@ jobs:
tags: | tags: |
type=semver,pattern={{version}} type=semver,pattern={{version}}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Create .version file - name: Create .version file
run: echo ${{ github.ref_name }} > .version run: echo ${{ github.ref_name }} > .version
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@v4 uses: docker/build-push-action@v6
with: with:
context: . context: .
push: true push: true
+24 -9
View File
@@ -223,9 +223,12 @@ GEM
concurrent-ruby (~> 1.1) concurrent-ruby (~> 1.1)
webrick (~> 1.7) webrick (~> 1.7)
websocket-driver (~> 0.7) websocket-driver (~> 0.7)
ffi (1.17.0) ffi (1.17.1)
ffi (1.17.0-aarch64-linux-gnu) ffi (1.17.1-aarch64-linux-gnu)
ffi (1.17.0-x86_64-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) geom2d (0.4.1)
globalid (1.2.1) globalid (1.2.1)
activesupport (>= 6.1) activesupport (>= 6.1)
@@ -334,12 +337,18 @@ GEM
net-smtp (0.5.0) net-smtp (0.5.0)
net-protocol net-protocol
nio4r (2.7.4) nio4r (2.7.4)
nokogiri (1.17.2) nokogiri (1.18.2)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.17.2-aarch64-linux) nokogiri (1.18.2-aarch64-linux-gnu)
racc (~> 1.4) 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) racc (~> 1.4)
oj (3.16.8) oj (3.16.8)
bigdecimal (>= 3.0) bigdecimal (>= 3.0)
@@ -520,10 +529,13 @@ GEM
simplecov-html (0.13.1) simplecov-html (0.13.1)
simplecov_json_formatter (0.1.4) simplecov_json_formatter (0.1.4)
smart_properties (1.17.0) smart_properties (1.17.0)
sqlite3 (2.4.1) sqlite3 (2.5.0)
mini_portile2 (~> 2.8.0) mini_portile2 (~> 2.8.0)
sqlite3 (2.4.1-aarch64-linux-gnu) sqlite3 (2.5.0-aarch64-linux-gnu)
sqlite3 (2.4.1-x86_64-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) stringio (3.1.2)
strip_attributes (1.14.1) strip_attributes (1.14.1)
activemodel (>= 3.0, < 9.0) activemodel (>= 3.0, < 9.0)
@@ -569,8 +581,11 @@ GEM
PLATFORMS PLATFORMS
aarch64-linux aarch64-linux
aarch64-linux-musl
arm64-darwin
ruby ruby
x86_64-linux x86_64-linux
x86_64-linux-musl
DEPENDENCIES DEPENDENCIES
annotate annotate
+1 -1
View File
@@ -50,7 +50,7 @@ class AccountsController < ApplicationController
# rubocop:disable Layout/LineLength # rubocop:disable Layout/LineLength
render turbo_stream: turbo_stream.replace( render turbo_stream: turbo_stream.replace(
:account_delete_button, :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 # rubocop:enable Layout/LineLength
end end
@@ -42,6 +42,7 @@ module Api
is_authorized = attachment.name.in?(%w[logo preview_images]) || is_authorized = attachment.name.in?(%w[logo preview_images]) ||
(current_user && attachment.record.account.id == current_user.account_id) || (current_user && attachment.record.account.id == current_user.account_id) ||
(current_user && !Docuseal.multitenant? && current_user.role == 'superadmin') ||
!attachment.record.account.account_configs !attachment.record.account.account_configs
.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value .find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value
+15 -1
View File
@@ -17,6 +17,8 @@ module Api
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] }) submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] })
end end
submissions = Submissions::Filter.call(submissions, current_user, params)
submissions = paginate(submissions.preload(:created_by_user, :submitters, submissions = paginate(submissions.preload(:created_by_user, :submitters,
template: :folder, template: :folder,
combined_document_attachment: :blob, combined_document_attachment: :blob,
@@ -116,7 +118,19 @@ module Api
end end
end end
json = { submitters: json } if request.path.ends_with?('/init') if request.path.ends_with?('/init')
json =
if submissions.size == 1
{
id: submissions.first.id,
submitters: json,
expire_at: submissions.first.expire_at,
created_at: submissions.first.created_at
}
else
{ submitters: json }
end
end
json json
end end
+4
View File
@@ -0,0 +1,4 @@
# frozen_string_literal: true
class PwaController < ActionController::Base
end
+1
View File
@@ -64,6 +64,7 @@ class StartFormController < ApplicationController
.or(template.submissions.where(expire_at: nil)).where(archived_at: nil)) .or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
.order(id: :desc) .order(id: :desc)
.where(declined_at: nil) .where(declined_at: nil)
.where(external_id: nil)
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel } .then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
.find_or_initialize_by(**submitter_params.compact_blank) .find_or_initialize_by(**submitter_params.compact_blank)
end end
@@ -20,7 +20,10 @@ class SubmissionsPreviewController < ApplicationController
@submission ||= Submission.find_by!(slug: params[:slug]) @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') raise ActionController::RoutingError, I18n.t('not_found')
end 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,7 +22,7 @@ class TemplatesPreferencesController < ApplicationController
preferences: %i[bcc_completed request_email_subject request_email_body preferences: %i[bcc_completed request_email_subject request_email_body
documents_copy_email_subject documents_copy_email_body documents_copy_email_subject documents_copy_email_body
documents_copy_email_enabled documents_copy_email_attach_audit 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_notification_email_attach_documents
completed_redirect_url completed_redirect_url
submitters_order submitters_order
+2 -1
View File
@@ -20,7 +20,8 @@ export default targetable(class extends HTMLElement {
fetch(this.dataset.src).then(async (response) => { fetch(this.dataset.src).then(async (response) => {
if (response.ok) { if (response.ok) {
const urls = await response.json() 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) { if (isSafariIos && urls.length > 1) {
this.downloadSafariIos(urls) this.downloadSafariIos(urls)
+22 -3
View File
@@ -191,13 +191,22 @@
class="whitespace-pre-wrap text-gray-400" class="whitespace-pre-wrap text-gray-400"
:class="{ 'w-full': field.preferences?.align }" :class="{ 'w-full': field.preferences?.align }"
>{{ field.name }}</span> >{{ 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(', ') }} {{ modelValue.join(', ') }}
</span> </span>
<span v-else-if="field.type === 'date'"> <span
v-else-if="field.type === 'date'"
:class="{ 'w-full': field.preferences?.align }"
>
{{ formattedDate }} {{ formattedDate }}
</span> </span>
<span v-else-if="field.type === 'number'"> <span
v-else-if="field.type === 'number'"
class="w-full"
>
{{ formatNumber(modelValue, field.preferences?.format) }} {{ formatNumber(modelValue, field.preferences?.format) }}
</span> </span>
<span <span
@@ -447,8 +456,18 @@ export default {
} }
}, },
formatNumber (number, format) { formatNumber (number, format) {
if (!number && number !== 0) {
return ''
}
if (format === 'comma') { if (format === 'comma') {
return new Intl.NumberFormat('en-US').format(number) 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') { } else if (format === 'dot') {
return new Intl.NumberFormat('de-DE').format(number) return new Intl.NumberFormat('de-DE').format(number)
} else if (format === 'space') { } else if (format === 'space') {
+19 -7
View File
@@ -31,7 +31,7 @@
:with-label="withLabel && !withFieldPlaceholder && step.length < 2" :with-label="withLabel && !withFieldPlaceholder && step.length < 2"
:is-value-set="step.some((f) => f.uuid in values)" :is-value-set="step.some((f) => f.uuid in values)"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
@click="$emit('focus-step', stepIndex)" @click="[$emit('focus-step', stepIndex), maybeScrollOnClick(field, area)]"
/> />
</Teleport> </Teleport>
</template> </template>
@@ -109,6 +109,14 @@ export default {
areaRefs: [] areaRefs: []
} }
}, },
computed: {
isMobileContainer () {
const root = this.$root.$el.parentNode.getRootNode()
const container = root.body || root.querySelector('div')
return container.style.overflow === 'hidden'
}
},
beforeUpdate () { beforeUpdate () {
this.areaRefs = [] this.areaRefs = []
}, },
@@ -121,14 +129,16 @@ export default {
this.scrollIntoArea(field.areas[0]) this.scrollIntoArea(field.areas[0])
} }
}, },
maybeScrollOnClick (field, area) {
if (['text', 'number', 'cells'].includes(field.type) && this.isMobileContainer) {
this.scrollIntoArea(area)
}
},
scrollIntoArea (area) { scrollIntoArea (area) {
const areaRef = this.areaRefs.find((a) => a.area === area) const areaRef = this.areaRefs.find((a) => a.area === area)
if (areaRef) { if (areaRef) {
const root = this.$root.$el.parentNode.getRootNode() if (this.isMobileContainer) {
const container = root.body || root.querySelector('div')
if (container.style.overflow === 'hidden') {
this.scrollInContainer(areaRef.$el) this.scrollInContainer(areaRef.$el)
} else { } else {
const targetRect = areaRef.$refs.scrollToElem.getBoundingClientRect() const targetRect = areaRef.$refs.scrollToElem.getBoundingClientRect()
@@ -157,13 +167,15 @@ export default {
const formContainer = root.getElementById('form_container') const formContainer = root.getElementById('form_container')
const container = root.body || root.querySelector('div') 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 boxRect = scrollbox.children[0].getBoundingClientRect()
const targetRect = target.getBoundingClientRect() const targetRect = target.getBoundingClientRect()
const targetTopRelativeToBox = targetRect.top - boxRect.top 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) { setAreaRef (el) {
if (el) { if (el) {
@@ -49,7 +49,7 @@
<MarkdownContent :string="field.description" /> <MarkdownContent :string="field.description" />
</div> </div>
<FileDropzone <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" :submitter-slug="submitterSlug"
:multiple="true" :multiple="true"
:dry-run="dryRun" :dry-run="dryRun"
+2 -1
View File
@@ -215,7 +215,8 @@ export default {
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`).then(async (response) => { fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`).then(async (response) => {
if (response.ok) { if (response.ok) {
const urls = await response.json() 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) { if (isSafariIos && urls.length > 1) {
this.downloadSafariIos(urls) this.downloadSafariIos(urls)
+11 -5
View File
@@ -1,22 +1,28 @@
<template> <template>
<div dir="auto"> <div dir="auto">
<div <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 }" :class="{ 'mb-2': !field.description }"
> >
<label <label
v-if="showFieldNames"
:for="field.uuid" :for="field.uuid"
class="label text-2xl" class="label text-xl sm:text-2xl py-0"
> >
<MarkdownContent <MarkdownContent
v-if="field.title" v-if="field.title"
:string="field.title" :string="field.title"
/> />
<template v-else> <template v-else>
{{ field.name && showFieldNames ? field.name : t('date') }} {{ field.name || t('date') }}
<template v-if="!field.required"> </template>
<template v-if="!field.required">
<span
class="ml-1"
:class="{ 'hidden sm:inline': (field.title || field.name || t('date') ).length > 10 }"
>
({{ t('optional') }}) ({{ t('optional') }})
</template> </span>
</template> </template>
</label> </label>
<button <button
+1 -1
View File
@@ -128,7 +128,7 @@ export default {
} }
}) })
} else { } else {
if (file.type === 'image/bmp') { if (file.type === 'image/bmp' || file.type === 'image/vnd.microsoft.icon') {
file = await this.convertBmpToPng(file) file = await this.convertBmpToPng(file)
} }
+30 -10
View File
@@ -15,7 +15,7 @@
/> />
<FieldAreas <FieldAreas
:steps="readonlyConditionalFields.map((e) => [e])" :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" :submitter="submitter"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:submittable="false" :submittable="false"
@@ -23,6 +23,7 @@
<FormulaFieldAreas <FormulaFieldAreas
v-if="formulaFields.length" v-if="formulaFields.length"
:fields="formulaFields" :fields="formulaFields"
:readonly-values="readonlyConditionalFieldValues"
:values="values" :values="values"
/> />
<Teleport <Teleport
@@ -163,7 +164,7 @@
v-if="showFieldNames && (currentField.name || currentField.title)" v-if="showFieldNames && (currentField.name || currentField.title)"
:for="currentField.uuid" :for="currentField.uuid"
dir="auto" 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 }" :class="{ 'mb-2': !currentField.description }"
> >
<MarkdownContent <MarkdownContent
@@ -172,7 +173,11 @@
/> />
<template v-else> <template v-else>
{{ currentField.name }} {{ 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> </template>
</label> </label>
<div <div
@@ -220,7 +225,7 @@
v-if="showFieldNames && (currentField.name || currentField.title)" v-if="showFieldNames && (currentField.name || currentField.title)"
:for="currentField.uuid" :for="currentField.uuid"
dir="auto" 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 }" :class="{ 'mb-2': !currentField.description }"
> >
<MarkdownContent <MarkdownContent
@@ -229,7 +234,11 @@
/> />
<template v-else> <template v-else>
{{ currentField.name }} {{ 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> </template>
</label> </label>
<div <div
@@ -465,7 +474,7 @@
</div> </div>
<div <div
v-if="(currentField.type !== 'payment' && currentField.type !== 'verification') || submittedValues[currentField.uuid]" v-if="(currentField.type !== 'payment' && currentField.type !== 'verification') || submittedValues[currentField.uuid]"
:class="currentField.type === 'signature' ? 'mt-2' : 'mt-6 md:mt-8'" :class="currentField.type === 'signature' ? 'mt-2' : 'mt-4 md:mt-6'"
> >
<button <button
id="submit_form_button" id="submit_form_button"
@@ -521,9 +530,9 @@
/> />
<div <div
v-if="stepFields.length < 80" 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 <a
v-for="(step, index) in stepFields" v-for="(step, index) in stepFields"
:key="step[0].uuid" :key="step[0].uuid"
@@ -855,7 +864,16 @@ export default {
}, },
computed: { computed: {
isMobile () { 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 () { attachmentConditionsIndex () {
return this.schema.reduce((acc, item) => { return this.schema.reduce((acc, item) => {
@@ -1103,7 +1121,9 @@ export default {
this.minimizeForm() 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(() => { this.$nextTick(() => {
const root = this.$root.$el.parentNode.getRootNode() const root = this.$root.$el.parentNode.getRootNode()
const scrollbox = root.getElementById('scrollbox') const scrollbox = root.getElementById('scrollbox')
@@ -38,6 +38,11 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
readonlyValues: {
type: Object,
required: false,
default: () => ({})
},
values: { values: {
type: Object, type: Object,
required: false, required: false,
@@ -87,7 +92,7 @@ export default {
}, },
calculateFormula (field) { calculateFormula (field) {
const transformedFormula = field.preferences.formula.replace(/{{(.*?)}}/g, (match, uuid) => { 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()) return this.math.evaluate(transformedFormula.toLowerCase())
+14 -4
View File
@@ -1,9 +1,19 @@
<template> <template>
<div v-if="modelValue"> <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 <label
class="label text-2xl" v-if="showFieldNames"
>{{ showFieldNames && field.name ? field.name : t('image') }}</label> :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 <button
class="btn btn-outline btn-sm" class="btn btn-outline btn-sm"
@click.prevent="remove" @click.prevent="remove"
@@ -35,7 +45,7 @@
<MarkdownContent :string="field.description" /> <MarkdownContent :string="field.description" />
</div> </div>
<FileDropzone <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" :submitter-slug="submitterSlug"
:dry-run="dryRun" :dry-run="dryRun"
:accept="'image/*'" :accept="'image/*'"
@@ -1,21 +1,22 @@
<template> <template>
<div dir="auto"> <div dir="auto">
<div <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 }" :class="{ 'mb-2': !field.description }"
> >
<label <label
class="label text-2xl" v-if="showFieldNames"
class="label text-xl sm:text-2xl py-0"
> >
<MarkdownContent <MarkdownContent
v-if="field.title" v-if="field.title"
:string="field.title" :string="field.title"
/> />
<template v-else> <template v-else>
{{ showFieldNames && field.name ? field.name : t('initials') }} {{ field.name || t('initials') }}
</template> </template>
</label> </label>
<div class="space-x-2 flex"> <div class="space-x-2 flex flex-none">
<span <span
v-if="isDrawInitials" v-if="isDrawInitials"
class="tooltip" class="tooltip"
@@ -40,7 +40,7 @@
> >
</div> </div>
<div <div
class="mt-6 md:mt-8" class="mt-4 md:mt-6"
> >
<button <button
type="submit" type="submit"
@@ -3,13 +3,19 @@
v-if="showFieldNames && (field.name || field.title)" v-if="showFieldNames && (field.name || field.title)"
:for="field.uuid" :for="field.uuid"
dir="auto" 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 }" :class="{ 'mb-2': !field.description }"
><MarkdownContent >
v-if="field.title" <MarkdownContent
:string="field.title" v-if="field.title"
/> :string="field.title"
/>
<template v-else>{{ field.name }}</template> <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> </label>
<div <div
v-if="field.description" v-if="field.description"
@@ -3,15 +3,18 @@
v-if="showFieldNames && (field.name || field.title)" v-if="showFieldNames && (field.name || field.title)"
:for="field.uuid" :for="field.uuid"
dir="auto" 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 }" :class="{ 'mb-2': !field.description }"
><MarkdownContent >
v-if="field.title" <MarkdownContent
:string="field.title" v-if="field.title"
/> :string="field.title"
/>
<template v-else>{{ field.name }}</template> <template v-else>{{ field.name }}</template>
<template v-if="!field.required"> <template v-if="!field.required">
({{ t('optional') }}) <span :class="{ 'hidden sm:inline': (field.title || field.name).length > 20 }">
({{ t('optional') }})
</span>
</template> </template>
</label> </label>
<div <div
@@ -1,7 +1,7 @@
<template> <template>
<label <label
v-if="!modelValue && !sessionId" 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 <MarkdownContent
v-if="field.title" v-if="field.title"
@@ -1,8 +1,9 @@
<template> <template>
<div> <div>
<label <label
v-if="showFieldNames"
:for="isCodeSent ? 'one_time_code' : field.uuid" :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 }" :class="{ 'mb-2': !field.description }"
> >
<MarkdownContent <MarkdownContent
@@ -10,10 +11,7 @@
:string="field.title" :string="field.title"
/> />
<template v-else> <template v-else>
{{ showFieldNames && field.name ? field.name : t('verified_phone_number') }} {{ field.name || t('verified_phone_number') }}
<template v-if="!field.required">
({{ t('optional') }})
</template>
</template> </template>
</label> </label>
<div <div
@@ -297,7 +295,7 @@ export default {
const data = await resp.json() const data = await resp.json()
if (resp.status === 422) { 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) { } else if (resp.status === 429) {
alert(data.error) alert(data.error)
} }
@@ -4,21 +4,22 @@
class="relative" class="relative"
> >
<div <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 }" :class="{ 'mb-2': !field.description }"
> >
<label <label
class="label text-2xl" v-if="showFieldNames"
class="label text-xl sm:text-2xl py-0"
> >
<MarkdownContent <MarkdownContent
v-if="field.title" v-if="field.title"
:string="field.title" :string="field.title"
/> />
<template v-else> <template v-else>
{{ showFieldNames && field.name ? field.name : t('signature') }} {{ field.name || t('signature') }}
</template> </template>
</label> </label>
<div class="space-x-2 flex"> <div class="space-x-2 flex flex-none">
<span <span
v-if="isTextSignature && format !== 'typed' && format !== 'upload'" v-if="isTextSignature && format !== 'typed' && format !== 'upload'"
class="tooltip" class="tooltip"
@@ -39,6 +40,7 @@
<span <span
v-else-if="withTypedSignature && format !== 'typed' && format !== 'drawn' && format !== 'upload'" v-else-if="withTypedSignature && format !== 'typed' && format !== 'drawn' && format !== 'upload'"
class="tooltip ml-2" class="tooltip ml-2"
:class="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
:data-tip="t('type_text')" :data-tip="t('type_text')"
> >
<a <a
@@ -56,6 +58,7 @@
<span <span
v-if="format !== 'typed' && format !== 'drawn' && format !== 'upload' && format !== 'drawn_or_typed'" v-if="format !== 'typed' && format !== 'drawn' && format !== 'upload' && format !== 'drawn_or_typed'"
class="tooltip" class="tooltip"
:class="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
:data-tip="t('take_photo')" :data-tip="t('take_photo')"
> >
<label <label
+9 -6
View File
@@ -3,15 +3,18 @@
v-if="showFieldNames && (field.name || field.title)" v-if="showFieldNames && (field.name || field.title)"
:for="field.uuid" :for="field.uuid"
dir="auto" 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 }" :class="{ 'mb-2': !field.description }"
><MarkdownContent >
v-if="field.title" <MarkdownContent
:string="field.title" v-if="field.title"
/> :string="field.title"
/>
<template v-else>{{ field.name }}</template> <template v-else>{{ field.name }}</template>
<template v-if="!field.required"> <template v-if="!field.required">
({{ t('optional') }}) <span :class="{ 'hidden sm:inline': (field.title || field.name).length > 20 }">
({{ t('optional') }})
</span>
</template> </template>
</label> </label>
<div <div
@@ -1,6 +1,6 @@
<template> <template>
<label <label
class="label text-2xl mb-2" class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
> >
<MarkdownContent <MarkdownContent
v-if="field.title" v-if="field.title"
@@ -31,6 +31,15 @@
class="animate-spin h-10" class="animate-spin h-10"
/> />
</div> </div>
<div v-else-if="redirectUrl">
<a
:href="redirectUrl"
target="_blank"
class="white-button w-full"
>
{{ t('verify_id') }}
</a>
</div>
<div <div
ref="widgetContainer" ref="widgetContainer"
/> />
@@ -87,6 +96,7 @@ export default {
return { return {
isCreatingCheckout: false, isCreatingCheckout: false,
isMathLoaded: false, isMathLoaded: false,
redirectUrl: '',
isLoading: false, isLoading: false,
eidEasyData: {} eidEasyData: {}
} }
@@ -125,12 +135,16 @@ export default {
async mounted () { async mounted () {
this.isLoading = true this.isLoading = true
Promise.all([ if (new URLSearchParams(window.location.search).get('submit') === 'true') {
import('@eid-easy/eideasy-widget'), this.$emit('submit')
this.start() } else {
]).finally(() => { Promise.all([
this.isLoading = false import('@eid-easy/eideasy-widget'),
}) this.start()
]).finally(() => {
this.isLoading = false
})
}
}, },
methods: { methods: {
start () { start () {
@@ -143,14 +157,26 @@ export default {
}).then(async (resp) => { }).then(async (resp) => {
this.eidEasyData = await resp.json() 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) { redirectUrl.searchParams.append('client_id', this.eidEasyData.client_id)
eidEasyWidget[key] = this.widgetSettings[key] 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 () { async submit () {
+32 -40
View File
@@ -10,7 +10,7 @@
<div <div
v-if="isSelected || isDraw" v-if="isSelected || isDraw"
class="top-0 bottom-0 right-0 left-0 absolute border border-1.5 pointer-events-none" 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 <div
v-if="field.type === 'cells' && (isSelected || isDraw)" v-if="field.type === 'cells' && (isSelected || isDraw)"
@@ -20,7 +20,7 @@
v-for="(cellW, index) in cells" v-for="(cellW, index) in cells"
:key="index" :key="index"
class="absolute top-0 bottom-0 border-r" 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) + '%' }" :style="{ left: (cellW / area.w * 100) + '%' }"
> >
<span <span
@@ -70,7 +70,7 @@
@keydown.enter.prevent="onNameEnter" @keydown.enter.prevent="onNameEnter"
@focus="onNameFocus" @focus="onNameFocus"
@blur="onNameBlur" @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 <div
v-if="isSettingsFocus || (isValueInput && field.type !== 'heading') || (isNameFocus && !['checkbox', 'phone'].includes(field.type))" v-if="isSettingsFocus || (isValueInput && field.type !== 'heading') || (isNameFocus && !['checkbox', 'phone'].includes(field.type))"
class="flex items-center ml-1.5" class="flex items-center ml-1.5"
@@ -161,7 +161,7 @@
ref="touchValueTarget" ref="touchValueTarget"
class="flex items-center h-full w-full" class="flex items-center h-full w-full"
dir="auto" 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" @click="focusValueInput"
> >
<span <span
@@ -212,7 +212,7 @@
:contenteditable="isValueInput" :contenteditable="isValueInput"
class="whitespace-pre-wrap outline-none empty:before:content-[attr(placeholder)] before:text-gray-400" class="whitespace-pre-wrap outline-none empty:before:content-[attr(placeholder)] before:text-gray-400"
:class="{ 'cursor-text': isValueInput }" :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" @blur="onDefaultValueBlur"
@paste.prevent="onPaste" @paste.prevent="onPaste"
@keydown.enter="onDefaultValueEnter" @keydown.enter="onDefaultValueEnter"
@@ -361,6 +361,7 @@ export default {
isMoved: false, isMoved: false,
renderDropdown: false, renderDropdown: false,
isNameFocus: false, isNameFocus: false,
isHeadingSelected: false,
textOverflowChars: 0, textOverflowChars: 0,
dragFrom: { x: 0, y: 0 } dragFrom: { x: 0, y: 0 }
} }
@@ -377,7 +378,7 @@ export default {
} }
}, },
isValueInput () { 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 () { modalContainerEl () {
return this.$el.getRootNode().querySelector('#docuseal_modal_container') return this.$el.getRootNode().querySelector('#docuseal_modal_container')
@@ -420,16 +421,6 @@ export default {
}, },
borderColors () { borderColors () {
return [ 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-red-500/80',
'border-sky-500/80', 'border-sky-500/80',
'border-emerald-500/80', 'border-emerald-500/80',
@@ -444,16 +435,6 @@ export default {
}, },
bgColors () { bgColors () {
return [ 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-red-100',
'bg-sky-100', 'bg-sky-100',
'bg-emerald-100', 'bg-emerald-100',
@@ -499,13 +480,13 @@ export default {
methods: { methods: {
buildDefaultName: Field.methods.buildDefaultName, buildDefaultName: Field.methods.buildDefaultName,
closeDropdown () { closeDropdown () {
document.activeElement.blur() this.$el.getRootNode().activeElement.blur()
}, },
maybeToggleDefaultValue () { maybeToggleDefaultValue () {
if (['text', 'number'].includes(this.field.type)) { if (['text', 'number'].includes(this.field.type)) {
this.isContenteditable = true this.isContenteditable = true
this.$nextTick(() => this.focusValueInput()) this.focusValueInput()
} else if (this.field.type === 'checkbox') { } else if (this.field.type === 'checkbox') {
this.field.readonly = !this.field.readonly this.field.readonly = !this.field.readonly
this.field.default_value === true ? delete this.field.default_value : this.field.default_value = true this.field.default_value === true ? delete this.field.default_value : this.field.default_value = true
@@ -527,20 +508,28 @@ export default {
} }
}, },
focusValueInput (e) { focusValueInput (e) {
if (this.$refs.defaultValue !== document.activeElement) { this.$nextTick(() => {
this.$refs.defaultValue.focus() if (this.$refs.defaultValue && this.$refs.defaultValue !== document.activeElement) {
this.$refs.defaultValue.focus()
if (this.$refs.defaultValue.innerText.length && this.$refs.defaultValue !== e?.target) { if (this.$refs.defaultValue.innerText.length && this.$refs.defaultValue !== e?.target) {
window.getSelection().collapse( window.getSelection().collapse(
this.$refs.defaultValue.firstChild, this.$refs.defaultValue.firstChild,
this.$refs.defaultValue.innerText.length this.$refs.defaultValue.innerText.length
) )
}
} }
} })
}, },
formatNumber (number, format) { formatNumber (number, format) {
if (format === 'comma') { if (format === 'comma') {
return new Intl.NumberFormat('en-US').format(number) 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') { } else if (format === 'dot') {
return new Intl.NumberFormat('de-DE').format(number) return new Intl.NumberFormat('de-DE').format(number)
} else if (format === 'space') { } else if (format === 'space') {
@@ -646,6 +635,7 @@ export default {
const text = this.$refs.defaultValue.innerText.trim() const text = this.$refs.defaultValue.innerText.trim()
this.isContenteditable = false this.isContenteditable = false
this.isHeadingSelected = false
if (text) { if (text) {
if (this.field.type === 'number') { if (this.field.type === 'number') {
@@ -763,10 +753,6 @@ export default {
this.selectedAreaRef.value = this.area 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.dragFrom = { x: rect.left - e.clientX, y: rect.top - e.clientY }
this.$el.getRootNode().addEventListener('mousemove', this.mouseMove) this.$el.getRootNode().addEventListener('mousemove', this.mouseMove)
@@ -801,6 +787,12 @@ export default {
this.save() this.save()
} }
if (this.field.type === 'heading') {
this.isHeadingSelected = !this.isMoved
this.focusValueInput()
}
this.isDragged = false this.isDragged = false
this.isMoved = false this.isMoved = false
+123 -19
View File
@@ -179,6 +179,7 @@
:with-arrows="template.schema.length > 1" :with-arrows="template.schema.length > 1"
:item="item" :item="item"
:document="sortedDocuments[index]" :document="sortedDocuments[index]"
:data-document-uuid="item.attachment_uuid"
:accept-file-types="acceptFileTypes" :accept-file-types="acceptFileTypes"
:with-replace-button="withUploadButton" :with-replace-button="withUploadButton"
:editable="editable" :editable="editable"
@@ -187,6 +188,7 @@
@remove="onDocumentRemove" @remove="onDocumentRemove"
@replace="onDocumentReplace" @replace="onDocumentReplace"
@up="moveDocument(item, -1)" @up="moveDocument(item, -1)"
@reorder="reorderFields"
@down="moveDocument(item, 1)" @down="moveDocument(item, 1)"
@change="save" @change="save"
/> />
@@ -220,8 +222,8 @@
</div> </div>
<div <div
id="pages_container" id="pages_container"
class="w-full overflow-y-hidden overflow-x-hidden mt-0.5 pt-0.5" class="w-full overflow-x-hidden mt-0.5 pt-0.5"
:class="isMobile ? 'overflow-y-auto' : 'md:overflow-y-auto'" :class="isMobile ? 'overflow-y-auto' : 'overflow-y-hidden md:overflow-y-auto'"
> >
<div <div
ref="documents" ref="documents"
@@ -265,6 +267,7 @@
:input-mode="inputMode" :input-mode="inputMode"
:default-fields="[...defaultRequiredFields, ...defaultFields]" :default-fields="[...defaultRequiredFields, ...defaultFields]"
:allow-draw="!onlyDefinedFields" :allow-draw="!onlyDefinedFields"
:data-document-uuid="document.uuid"
:default-submitters="defaultSubmitters" :default-submitters="defaultSubmitters"
:with-field-placeholder="withFieldPlaceholder" :with-field-placeholder="withFieldPlaceholder"
:draw-field="drawField" :draw-field="drawField"
@@ -363,6 +366,7 @@
:with-help="withHelp" :with-help="withHelp"
:default-submitters="defaultSubmitters" :default-submitters="defaultSubmitters"
:draw-field-type="drawFieldType" :draw-field-type="drawFieldType"
:with-fields-search="withFieldsSearch"
:default-fields="[...defaultRequiredFields, ...defaultFields]" :default-fields="[...defaultRequiredFields, ...defaultFields]"
:template="template" :template="template"
:default-required-fields="defaultRequiredFields" :default-required-fields="defaultRequiredFields"
@@ -620,6 +624,11 @@ export default {
required: false, required: false,
default: true default: true
}, },
withFieldsSearch: {
type: Boolean,
required: false,
default: null
},
withFieldsList: { withFieldsList: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -700,7 +709,9 @@ export default {
return this.locale.split('-')[0].toLowerCase() return this.locale.split('-')[0].toLowerCase()
}, },
isMobile () { 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 () { defaultDateFormat () {
const isUsBrowser = Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') const isUsBrowser = Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US')
@@ -769,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) => { this.defineSubmitters.forEach((name, index) => {
const submitter = (this.template.submitters[index] ||= {}) const submitter = (this.template.submitters[index] ||= {})
submitter.name = name || this.submitterDefaultNames[index] submitter.name = name || this.submitterDefaultNames[index]
if (existingSubmittersUuids.filter(Boolean).length) { if (defineSubmittersUuids.filter(Boolean).length || existingSubmittersUuids.filter(Boolean).length) {
submitter.uuid = existingSubmittersUuids[index] || submitter.uuid || v4() submitter.uuid = defineSubmittersUuids[index] || existingSubmittersUuids[index] || submitter.uuid || v4()
} else { } else {
submitter.uuid ||= v4() submitter.uuid ||= v4()
} }
@@ -818,6 +833,89 @@ export default {
this.documentRefs = [] this.documentRefs = []
}, },
methods: { methods: {
reorderFields (item) {
const itemFields = []
const fields = []
const fieldAreasIndex = {}
const attachmentUuids = this.template.schema.map((e) => e.attachment_uuid)
this.template.fields.forEach((f) => {
if (f.areas?.length) {
const firstArea = f.areas.reduce((min, a) => {
return attachmentUuids.indexOf(a.attachment_uuid) < attachmentUuids.indexOf(min.attachment_uuid) ? a : min
}, f.areas[0])
if (firstArea.attachment_uuid === item.attachment_uuid) {
itemFields.push(f)
} else {
fields.push(f)
}
} else {
fields.push(f)
}
})
const sortArea = (aArea, bArea) => {
if (aArea.attachment_uuid === bArea.attachment_uuid) {
if (aArea.page === bArea.page) {
if (Math.abs(aArea.y - bArea.y) < 0.01) {
if (aArea.x === bArea.x) {
return 0
} else {
return aArea.x - bArea.x
}
} else {
return aArea.y - bArea.y
}
} else {
return aArea.page - bArea.page
}
} else {
return attachmentUuids.indexOf(aArea.attachment_uuid) - attachmentUuids.indexOf(bArea.attachment_uuid)
}
}
itemFields.sort((aField, bField) => {
const aArea = (fieldAreasIndex[aField.uuid] ||= [...(aField.areas || [])].sort(sortArea)[0])
const bArea = (fieldAreasIndex[bField.uuid] ||= [...(bField.areas || [])].sort(sortArea)[0])
return sortArea(aArea, bArea)
})
const insertBeforeAttachmentUuids = attachmentUuids.slice(this.template.schema.indexOf(item) + 1)
let sortedFields = []
if (insertBeforeAttachmentUuids.length) {
const insertAfterField = fields.find((f) => {
if (f.areas?.length) {
return f.areas.find((a) => insertBeforeAttachmentUuids.includes(a.attachment_uuid))
} else {
return false
}
})
if (insertAfterField) {
fields.splice(fields.indexOf(insertAfterField), 0, ...itemFields)
sortedFields = fields
} else {
sortedFields = fields.concat(itemFields)
}
} else {
if (fields.length && itemFields.length && this.template.fields.indexOf(fields[0]) > this.template.fields.indexOf(itemFields[0])) {
sortedFields = itemFields.concat(fields)
} else {
sortedFields = fields.concat(itemFields)
}
}
if (this.template.fields.length === sortedFields.length) {
this.template.fields = sortedFields
this.save()
}
},
closeDropdown () { closeDropdown () {
document.activeElement.blur() document.activeElement.blur()
}, },
@@ -1228,7 +1326,11 @@ export default {
if (!this.fieldsDragFieldRef.value) { if (!this.fieldsDragFieldRef.value) {
if (['select', 'multiple', 'radio'].includes(field.type)) { 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)) { if (['stamp', 'heading'].includes(field.type)) {
@@ -1331,6 +1433,8 @@ export default {
const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid) 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) const areaRef = documentRef.pageRefs[area.page].areaRefs.find((ref) => ref.area === this.selectedAreaRef.value)
areaRef.isHeadingSelected = true
areaRef.focusValueInput() areaRef.focusValueInput()
}) })
} }
@@ -1418,29 +1522,29 @@ export default {
onDocumentRemove (item) { onDocumentRemove (item) {
if (window.confirm(this.t('are_you_sure_'))) { if (window.confirm(this.t('are_you_sure_'))) {
this.template.schema.splice(this.template.schema.indexOf(item), 1) this.template.schema.splice(this.template.schema.indexOf(item), 1)
}
const removedFieldUuids = [] const removedFieldUuids = []
this.template.fields.forEach((field) => { this.template.fields.forEach((field) => {
[...(field.areas || [])].forEach((area) => { [...(field.areas || [])].forEach((area) => {
if (area.attachment_uuid === item.attachment_uuid) { if (area.attachment_uuid === item.attachment_uuid) {
field.areas.splice(field.areas.indexOf(area), 1) field.areas.splice(field.areas.indexOf(area), 1)
removedFieldUuids.push(field.uuid) removedFieldUuids.push(field.uuid)
} }
})
}) })
})
this.template.fields = this.template.fields =
this.template.fields.filter((f) => !removedFieldUuids.includes(f.uuid) || f.areas?.length) this.template.fields.filter((f) => !removedFieldUuids.includes(f.uuid) || f.areas?.length)
this.save() this.save()
}
}, },
onDocumentReplace (data) { onDocumentReplace (data) {
const { replaceSchemaItem, schema, documents } = 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) this.template.documents.push(...documents)
if (data.fields) { if (data.fields) {
@@ -177,7 +177,7 @@ export default {
fields () { fields () {
if (this.item.submitter_uuid) { if (this.item.submitter_uuid) {
return this.template.fields.reduce((acc, f) => { 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) acc.push(f)
} }
@@ -7,6 +7,7 @@
:input-mode="inputMode" :input-mode="inputMode"
:number="index" :number="index"
:editable="editable" :editable="editable"
:data-page="index"
:areas="areasIndex[index]" :areas="areasIndex[index]"
:allow-draw="allowDraw" :allow-draw="allowDraw"
:is-drag="isDrag" :is-drag="isDrag"
+6 -6
View File
@@ -23,7 +23,7 @@
/> />
<Contenteditable <Contenteditable
ref="name" 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'" :editable="editable && !defaultField && field.type != 'heading'"
:icon-inline="true" :icon-inline="true"
:icon-width="18" :icon-width="18"
@@ -192,14 +192,14 @@
class="w-full input input-primary input-xs text-sm bg-transparent" class="w-full input input-primary input-xs text-sm bg-transparent"
:placeholder="`${t('option')} ${index + 1}`" :placeholder="`${t('option')} ${index + 1}`"
type="text" type="text"
:readonly="!editable" :readonly="!editable || defaultField"
required required
dir="auto" dir="auto"
@focus="maybeFocusOnOptionArea(option)" @focus="maybeFocusOnOptionArea(option)"
@blur="save" @blur="save"
> >
<button <button
v-if="editable" v-if="editable && !defaultField"
class="text-sm w-3.5" class="text-sm w-3.5"
tabindex="-1" tabindex="-1"
@click="removeOption(option)" @click="removeOption(option)"
@@ -208,11 +208,11 @@
</button> </button>
</div> </div>
<div <div
v-if="field.options && !editable" v-if="field.options && (!editable || defaultField)"
class="pb-1" class="pb-1"
/> />
<button <button
v-else-if="field.options && editable" v-else-if="field.options && editable && !defaultField"
class="text-center text-sm w-full pb-1" class="text-center text-sm w-full pb-1"
@click="addOption" @click="addOption"
> >
@@ -392,7 +392,7 @@ export default {
return this.sortedAreas[0] && this.$emit('scroll-to', this.sortedAreas[0]) return this.sortedAreas[0] && this.$emit('scroll-to', this.sortedAreas[0])
}, },
closeDropdown () { closeDropdown () {
document.activeElement.blur() this.$el.getRootNode().activeElement.blur()
}, },
addOption () { addOption () {
this.field.options.push({ value: '', uuid: v4() }) this.field.options.push({ value: '', uuid: v4() })
@@ -26,6 +26,33 @@
{{ t('format') }} {{ t('format') }}
</label> </label>
</div> </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 <div
v-if="['number', 'cells'].includes(field.type)" v-if="['number', 'cells'].includes(field.type)"
class="py-1.5 px-1 relative" class="py-1.5 px-1 relative"
@@ -229,7 +256,7 @@
</label> </label>
</div> </div>
<li <li
v-if="withRequired && field.type != 'phone' && field.type != 'stamp'" v-if="withRequired && field.type !== 'phone' && field.type !== 'stamp' && field.type !== 'verification'"
@click.stop @click.stop
> >
<label class="cursor-pointer py-1.5"> <label class="cursor-pointer py-1.5">
@@ -461,6 +488,9 @@ export default {
numberFormats () { numberFormats () {
return [ return [
'none', 'none',
'usd',
'eur',
'gbp',
'comma', 'comma',
'dot', 'dot',
'space' 'space'
@@ -541,6 +571,12 @@ export default {
formatNumber (number, format) { formatNumber (number, format) {
if (format === 'comma') { if (format === 'comma') {
return new Intl.NumberFormat('en-US').format(number) 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') { } else if (format === 'dot') {
return new Intl.NumberFormat('de-DE').format(number) return new Intl.NumberFormat('de-DE').format(number)
} else if (format === 'space') { } else if (format === 'space') {
@@ -9,7 +9,7 @@
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<span <span
class="w-3 h-3 flex-shrink-0 rounded-full" class="w-3 h-3 flex-shrink-0 rounded-full"
:class="colors[submitters.indexOf(selectedSubmitter)]" :class="colors[submitters.indexOf(selectedSubmitter) % colors.length]"
/> />
<Contenteditable <Contenteditable
v-model="selectedSubmitter.name" v-model="selectedSubmitter.name"
@@ -53,7 +53,7 @@
<span class="py-1 flex items-center"> <span class="py-1 flex items-center">
<span <span
class="rounded-full w-3 h-3 ml-1 mr-3" class="rounded-full w-3 h-3 ml-1 mr-3"
:class="colors[index]" :class="colors[index % colors.length]"
/> />
<span> <span>
{{ submitter.name }} {{ submitter.name }}
@@ -101,7 +101,7 @@
> >
<button <button
class="mx-1 w-3 h-3 rounded-full" class="mx-1 w-3 h-3 rounded-full"
:class="colors[submitters.indexOf(selectedSubmitter)]" :class="colors[submitters.indexOf(selectedSubmitter) % colors.length]"
/> />
</label> </label>
<label <label
@@ -113,7 +113,7 @@
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<span <span
class="w-3 h-3 rounded-full" class="w-3 h-3 rounded-full"
:class="colors[submitters.indexOf(selectedSubmitter)]" :class="colors[submitters.indexOf(selectedSubmitter) % colors.length]"
/> />
<Contenteditable <Contenteditable
v-model="selectedSubmitter.name" v-model="selectedSubmitter.name"
@@ -143,6 +143,7 @@
<li <li
v-for="(submitter, index) in submitters" v-for="(submitter, index) in submitters"
:key="submitter.uuid" :key="submitter.uuid"
class="w-full"
> >
<a <a
href="#" href="#"
@@ -153,7 +154,7 @@
<span class="py-1 flex items-center"> <span class="py-1 flex items-center">
<span <span
class="rounded-full w-3 h-3 ml-1 mr-3" class="rounded-full w-3 h-3 ml-1 mr-3"
:class="colors[index]" :class="colors[index % colors.length]"
/> />
<span> <span>
{{ submitter.name }} {{ submitter.name }}
@@ -191,7 +192,10 @@
</div> </div>
</a> </a>
</li> </li>
<li v-if="submitters.length < names.length && editable && allowAddNew"> <li
v-if="submitters.length < names.length && editable && allowAddNew"
class="w-full"
>
<a <a
href="#" href="#"
class="flex px-2" class="flex px-2"
@@ -215,6 +219,14 @@ import { IconUserPlus, IconTrashX, IconPlus, IconChevronUp, IconChevronDown } fr
import Contenteditable from './contenteditable' import Contenteditable from './contenteditable'
import { v4 } from 'uuid' 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 { export default {
name: 'FieldSubmitter', name: 'FieldSubmitter',
components: { components: {
@@ -275,16 +287,6 @@ export default {
computed: { computed: {
colors () { colors () {
return [ 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-red-500',
'bg-sky-500', 'bg-sky-500',
'bg-emerald-500', 'bg-emerald-500',
@@ -298,6 +300,14 @@ export default {
] ]
}, },
names () { names () {
const generatedNames = []
for (let i = 21; i < 101; i++) {
const suffix = getOrdinalSuffix(i)
generatedNames.push(`${i}${suffix} ${this.t('party')}`)
}
return [ return [
this.t('first_party'), this.t('first_party'),
this.t('second_party'), this.t('second_party'),
@@ -318,7 +328,8 @@ export default {
this.t('seventeenth_party'), this.t('seventeenth_party'),
this.t('eighteenth_party'), this.t('eighteenth_party'),
this.t('nineteenth_party'), this.t('nineteenth_party'),
this.t('twentieth_party') this.t('twentieth_party'),
...generatedNames
] ]
}, },
lastPartyIndex () { lastPartyIndex () {
@@ -372,7 +383,7 @@ export default {
this.$emit('new-submitter', newSubmitter) this.$emit('new-submitter', newSubmitter)
}, },
closeDropdown () { closeDropdown () {
document.activeElement.blur() this.$el.getRootNode().activeElement.blur()
} }
} }
} }
@@ -170,7 +170,7 @@ export default {
}, },
methods: { methods: {
closeDropdown () { closeDropdown () {
document.activeElement.blur() this.$el.getRootNode().activeElement.blur()
} }
} }
} }
+20 -4
View File
@@ -5,7 +5,7 @@
class="roles-dropdown w-full rounded-lg" class="roles-dropdown w-full rounded-lg"
:style="withStickySubmitters ? { backgroundColor } : {}" :style="withStickySubmitters ? { backgroundColor } : {}"
:submitters="submitters" :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" :editable="editable && !defaultSubmitters.length"
@new-submitter="save" @new-submitter="save"
@remove="removeSubmitter" @remove="removeSubmitter"
@@ -26,7 +26,7 @@
:field="field" :field="field"
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)" :type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
:editable="editable && (!fieldsDragFieldRef.value || fieldsDragFieldRef.value !== field)" :editable="editable && (!fieldsDragFieldRef.value || fieldsDragFieldRef.value !== field)"
:default-field="defaultFields.find((f) => f.name === field.name)" :default-field="defaultFieldsIndex[field.name]"
:draggable="editable" :draggable="editable"
@dragstart="fieldsDragFieldRef.value = field" @dragstart="fieldsDragFieldRef.value = field"
@dragend="fieldsDragFieldRef.value = null" @dragend="fieldsDragFieldRef.value = null"
@@ -229,6 +229,11 @@ export default {
type: Array, type: Array,
required: true required: true
}, },
withFieldsSearch: {
type: Boolean,
required: false,
default: null
},
template: { template: {
type: Object, type: Object,
required: true required: true
@@ -297,7 +302,18 @@ export default {
fieldNames: FieldType.computed.fieldNames, fieldNames: FieldType.computed.fieldNames,
fieldIcons: FieldType.computed.fieldIcons, fieldIcons: FieldType.computed.fieldIcons,
isShowFieldSearch () { 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 () { fieldIconsSorted () {
if (this.fieldTypes.length) { if (this.fieldTypes.length) {
@@ -320,7 +336,7 @@ export default {
}, },
filteredSubmitterDefaultFields () { filteredSubmitterDefaultFields () {
if (this.defaultFieldsSearch) { 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 { } else {
return this.submitterDefaultFields return this.submitterDefaultFields
} }
+18
View File
@@ -1,4 +1,7 @@
const en = { const en = {
party: 'Party',
method: 'Method',
reorder_fields: 'Reorder fields',
verify_id: 'Verify ID', 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.', obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Obtain qualified electronic signature (QeS) with the trusted provider. Click to learn more.',
editable: 'Editable', editable: 'Editable',
@@ -158,6 +161,9 @@ const en = {
} }
const es = { const es = {
party: 'Parte',
method: 'Método',
reorder_fields: 'Reordenar campos',
verify_id: 'Verificar ID', 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.', obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Obtenga una firma electrónica cualificada (QeS) con el proveedor de confianza. Haga clic para obtener más información.',
recurrent: 'Recurrente', recurrent: 'Recurrente',
@@ -317,6 +323,9 @@ const es = {
} }
const it = { const it = {
party: 'Parte',
method: 'Metodo',
reorder_fields: 'Riordina i campi',
verify_id: 'Verifica ID', 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ù.', obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Ottieni una firma elettronica qualificata (QeS) con il fornitore di fiducia. Clicca per saperne di più.',
ricorrente: 'Ricorrente', ricorrente: 'Ricorrente',
@@ -476,6 +485,9 @@ const it = {
} }
const pt = { const pt = {
party: 'Parte',
method: 'Método',
reorder_fields: 'Reorganizar campos',
verify_id: 'Verificar ID', 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.', obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Obtenha a assinatura eletrônica qualificada (QeS) com o provedor confiável. Clique para saber mais.',
recurrent: 'Recurrente', recurrent: 'Recurrente',
@@ -635,6 +647,9 @@ const pt = {
} }
const fr = { const fr = {
party: 'Partie',
method: 'Méthode',
reorder_fields: 'Réorganiser les champs',
verify_id: "Vérifier l'ID", 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.', obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Obtenez une signature électronique qualifiée (QeS) avec le fournisseur de confiance. Cliquez pour en savoir plus.',
recurrent: 'Récurrent', recurrent: 'Récurrent',
@@ -794,6 +809,9 @@ const fr = {
} }
const de = { const de = {
party: 'Partei',
method: 'Verfahren',
reorder_fields: 'Felder neu anordnen',
verify_id: 'ID überprüfen', 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.', obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Erhalten Sie eine qualifizierte elektronische Signatur (QeS) beim vertrauenswürdigen Anbieter. Klicken Sie hier, um mehr zu erfahren.',
wiederkehrend: 'Wiederkehrend', wiederkehrend: 'Wiederkehrend',
+13 -3
View File
@@ -1,6 +1,7 @@
<template> <template>
<div <div
class="relative cursor-crosshair select-none" class="relative select-none"
:class="{ 'cursor-crosshair': allowDraw }"
:style="drawField ? 'touch-action: none' : ''" :style="drawField ? 'touch-action: none' : ''"
> >
<img <img
@@ -25,7 +26,7 @@
:field="item.field" :field="item.field"
:editable="editable" :editable="editable"
:with-field-placeholder="withFieldPlaceholder" :with-field-placeholder="withFieldPlaceholder"
:default-field="defaultFields.find((f) => f.name === item.field.name)" :default-field="defaultFieldsIndex[item.field.name]"
:default-submitters="defaultSubmitters" :default-submitters="defaultSubmitters"
:max-page="totalPages - 1" :max-page="totalPages - 1"
@start-resize="resizeDirection = $event" @start-resize="resizeDirection = $event"
@@ -142,6 +143,13 @@ export default {
} }
}, },
computed: { computed: {
defaultFieldsIndex () {
return this.defaultFields.reduce((acc, field) => {
acc[field.name] = field
return acc
}, {})
},
defaultFieldType () { defaultFieldType () {
if (this.drawFieldType) { if (this.drawFieldType) {
return this.drawFieldType return this.drawFieldType
@@ -154,7 +162,9 @@ export default {
} }
}, },
isMobile () { 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 () { resizeDirectionClasses () {
return { return {
+44 -27
View File
@@ -56,23 +56,39 @@
</button> </button>
</div> </div>
<div <div
v-if="withArrows"
class="flex flex-col space-y-1" class="flex flex-col space-y-1"
> >
<button <span
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors" :data-tip="t('reorder_fields')"
style="width: 24px; height: 24px" class="tooltip tooltip-left before:text-xs"
@click.stop="$emit('up', item)"
> >
&uarr; <button
</button> class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors p-0"
<button @click.stop="$emit('reorder', item)"
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors" >
style="width: 24px; height: 24px" <IconSortDescending2
@click.stop="$emit('down', item)" :width="18"
> :height="18"
&darr; :stroke-width="1.6"
</button> />
</button>
</span>
<template v-if="withArrows">
<button
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
style="width: 24px; height: 24px"
@click.stop="$emit('up', item)"
>
&uarr;
</button>
<button
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
style="width: 24px; height: 24px"
@click.stop="$emit('down', item)"
>
&darr;
</button>
</template>
</div> </div>
</div> </div>
</div> </div>
@@ -88,23 +104,23 @@
@update:model-value="onUpdateName" @update:model-value="onUpdateName"
/> />
</div> </div>
<Teleport
v-if="isShowConditionsModal"
:to="modalContainerEl"
>
<ConditionsModal
:item="item"
:build-default-name="buildDefaultName"
@close="isShowConditionsModal = false"
/>
</Teleport>
</div> </div>
<Teleport
v-if="isShowConditionsModal"
:to="modalContainerEl"
>
<ConditionsModal
:item="item"
:build-default-name="buildDefaultName"
@close="isShowConditionsModal = false"
/>
</Teleport>
</template> </template>
<script> <script>
import Contenteditable from './contenteditable' import Contenteditable from './contenteditable'
import Upload from './upload' import Upload from './upload'
import { IconRouteAltLeft } from '@tabler/icons-vue' import { IconRouteAltLeft, IconSortDescending2 } from '@tabler/icons-vue'
import ConditionsModal from './conditions_modal' import ConditionsModal from './conditions_modal'
import ReplaceButton from './replace' import ReplaceButton from './replace'
import Field from './field' import Field from './field'
@@ -116,7 +132,8 @@ export default {
Contenteditable, Contenteditable,
IconRouteAltLeft, IconRouteAltLeft,
ConditionsModal, ConditionsModal,
ReplaceButton ReplaceButton,
IconSortDescending2
}, },
inject: ['t'], inject: ['t'],
props: { props: {
@@ -153,7 +170,7 @@ export default {
default: true default: true
} }
}, },
emits: ['scroll-to', 'change', 'remove', 'up', 'down', 'replace'], emits: ['scroll-to', 'change', 'remove', 'up', 'down', 'replace', 'reorder'],
data () { data () {
return { return {
isShowConditionsModal: false isShowConditionsModal: false
@@ -120,6 +120,8 @@ class ProcessSubmitterCompletionJob
submitter.submission.template_submitters.find do |e| submitter.submission.template_submitters.find do |e|
sub = submitter.submission.submitters.find { |s| s.uuid == e['uuid'] } sub = submitter.submission.submitters.find { |s| s.uuid == e['uuid'] }
next unless sub
sub.completed_at.blank? && sub.sent_at.blank? sub.completed_at.blank? && sub.sent_at.blank?
end end
@@ -5,9 +5,7 @@ class SendFormCompletedWebhookRequestJob
sidekiq_options queue: :webhooks sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.com Webhook' MAX_ATTEMPTS = 20
MAX_ATTEMPTS = 10
def perform(params = {}) def perform(params = {})
submitter = Submitter.find(params['submitter_id']) submitter = Submitter.find(params['submitter_id'])
@@ -21,19 +19,8 @@ class SendFormCompletedWebhookRequestJob
ActiveStorage::Current.url_options = Docuseal.default_url_options ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin resp = SendWebhookRequest.call(webhook_url, event_type: 'form.completed',
Faraday.post(webhook_url.url, data: Submitters::SerializeForWebhook.call(submitter))
{
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
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan)) (!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
@@ -5,8 +5,6 @@ class SendFormDeclinedWebhookRequestJob
sidekiq_options queue: :webhooks sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10 MAX_ATTEMPTS = 10
def perform(params = {}) def perform(params = {})
@@ -19,19 +17,8 @@ class SendFormDeclinedWebhookRequestJob
ActiveStorage::Current.url_options = Docuseal.default_url_options ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin resp = SendWebhookRequest.call(webhook_url, event_type: 'form.declined',
Faraday.post(webhook_url.url, data: Submitters::SerializeForWebhook.call(submitter))
{
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
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan)) (!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
@@ -5,8 +5,6 @@ class SendFormStartedWebhookRequestJob
sidekiq_options queue: :webhooks sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10 MAX_ATTEMPTS = 10
def perform(params = {}) def perform(params = {})
@@ -19,19 +17,8 @@ class SendFormStartedWebhookRequestJob
ActiveStorage::Current.url_options = Docuseal.default_url_options ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin resp = SendWebhookRequest.call(webhook_url, event_type: 'form.started',
Faraday.post(webhook_url.url, data: Submitters::SerializeForWebhook.call(submitter))
{
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
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan)) (!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
@@ -5,8 +5,6 @@ class SendFormViewedWebhookRequestJob
sidekiq_options queue: :webhooks sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10 MAX_ATTEMPTS = 10
def perform(params = {}) def perform(params = {})
@@ -19,19 +17,8 @@ class SendFormViewedWebhookRequestJob
ActiveStorage::Current.url_options = Docuseal.default_url_options ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin resp = SendWebhookRequest.call(webhook_url, event_type: 'form.viewed',
Faraday.post(webhook_url.url, data: Submitters::SerializeForWebhook.call(submitter))
{
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
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan)) (!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
@@ -5,8 +5,6 @@ class SendSubmissionArchivedWebhookRequestJob
sidekiq_options queue: :webhooks sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10 MAX_ATTEMPTS = 10
def perform(params = {}) def perform(params = {})
@@ -17,19 +15,8 @@ class SendSubmissionArchivedWebhookRequestJob
return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.archived') return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.archived')
resp = begin resp = SendWebhookRequest.call(webhook_url, event_type: 'submission.archived',
Faraday.post(webhook_url.url, data: submission.as_json(only: %i[id archived_at]))
{
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
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan)) (!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
@@ -5,8 +5,6 @@ class SendSubmissionCompletedWebhookRequestJob
sidekiq_options queue: :webhooks sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10 MAX_ATTEMPTS = 10
def perform(params = {}) def perform(params = {})
@@ -17,19 +15,8 @@ class SendSubmissionCompletedWebhookRequestJob
return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.completed') return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.completed')
resp = begin resp = SendWebhookRequest.call(webhook_url, event_type: 'submission.completed',
Faraday.post(webhook_url.url, data: Submissions::SerializeForApi.call(submission))
{
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
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan)) (!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
@@ -5,8 +5,6 @@ class SendSubmissionCreatedWebhookRequestJob
sidekiq_options queue: :webhooks sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10 MAX_ATTEMPTS = 10
def perform(params = {}) def perform(params = {})
@@ -17,19 +15,8 @@ class SendSubmissionCreatedWebhookRequestJob
return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.created') return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.created')
resp = begin resp = SendWebhookRequest.call(webhook_url, event_type: 'submission.created',
Faraday.post(webhook_url.url, data: Submissions::SerializeForApi.call(submission))
{
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
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan)) (!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) 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) mail = SubmitterMailer.invitation_email(submitter)
Submitters::ValidateSending.call(submitter, mail) Submitters::ValidateSending.call(submitter, mail)
@@ -17,6 +23,6 @@ class SendSubmitterInvitationEmailJob
SubmissionEvent.create!(submitter:, event_type: 'send_email') SubmissionEvent.create!(submitter:, event_type: 'send_email')
submitter.sent_at ||= Time.current submitter.sent_at ||= Time.current
submitter.save submitter.save!
end end
end end
@@ -5,8 +5,6 @@ class SendTemplateCreatedWebhookRequestJob
sidekiq_options queue: :webhooks sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10 MAX_ATTEMPTS = 10
def perform(params = {}) def perform(params = {})
@@ -17,19 +15,8 @@ class SendTemplateCreatedWebhookRequestJob
return if webhook_url.url.blank? || webhook_url.events.exclude?('template.created') return if webhook_url.url.blank? || webhook_url.events.exclude?('template.created')
resp = begin resp = SendWebhookRequest.call(webhook_url, event_type: 'template.created',
Faraday.post(webhook_url.url, data: Templates::SerializeForApi.call(template))
{
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
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan)) (!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
@@ -5,8 +5,6 @@ class SendTemplateUpdatedWebhookRequestJob
sidekiq_options queue: :webhooks sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10 MAX_ATTEMPTS = 10
def perform(params = {}) def perform(params = {})
@@ -17,19 +15,8 @@ class SendTemplateUpdatedWebhookRequestJob
return if webhook_url.url.blank? || webhook_url.events.exclude?('template.updated') return if webhook_url.url.blank? || webhook_url.events.exclude?('template.updated')
resp = begin resp = SendWebhookRequest.call(webhook_url, event_type: 'template.updated',
Faraday.post(webhook_url.url, data: Templates::SerializeForApi.call(template))
{
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
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan)) (!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
+4 -2
View File
@@ -121,7 +121,7 @@ class SubmitterMailer < ApplicationMailer
@body ||= @email_config.value['body'] if @email_config @body ||= @email_config.value['body'] if @email_config
assign_message_metadata('submitter_documents_copy', @submitter) 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 I18n.with_locale(@current_account.locale) do
subject = subject =
@@ -140,8 +140,10 @@ class SubmitterMailer < ApplicationMailer
private 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.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 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+@/, '@') reply_to = (submitter.submission.created_by_user || submitter.template.author)&.friendly_name&.sub(/\+\w+@/, '@')
Whitespace-only changes.
+1
View File
@@ -153,6 +153,7 @@
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
<%= render 'compliances' %>
<%= render 'integrations' %> <%= render 'integrations' %>
<% if can?(:manage, current_account) && Docuseal.multitenant? && true_user == current_user %> <% if can?(:manage, current_account) && Docuseal.multitenant? && true_user == current_user %>
<div class="px-1 mt-8"> <div class="px-1 mt-8">
@@ -0,0 +1 @@
<%= hidden_field_tag :redir, params[:redir] if params[:redir].present? %>
@@ -0,0 +1,4 @@
<div class="form-control">
<%= label_tag 'user[otp_attempt]', t('two_factor_code_from_authenticator_app'), class: 'label' %>
<%= text_field_tag 'user[otp_attempt]', nil, autofocus: true, autocomplete: 'off', placeholder: 'XXX-XXX', required: true, class: 'base-input' %>
</div>
+1 -3
View File
@@ -3,9 +3,7 @@
<%= render 'devise/shared/select_server' if Docuseal.multitenant? %> <%= render 'devise/shared/select_server' if Docuseal.multitenant? %>
<h1 class="text-4xl font-bold text-center mt-8"><%= t('sign_in') %></h1> <h1 class="text-4xl font-bold text-center mt-8"><%= t('sign_in') %></h1>
<%= form_for(resource, as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: session_path(resource_name)) do |f| %> <%= form_for(resource, as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: session_path(resource_name)) do |f| %>
<% if params[:redir].present? %> <%= render 'hidden_fields' %>
<%= hidden_field_tag :redir, params[:redir] %>
<% end %>
<div class="space-y-2" dir="auto"> <div class="space-y-2" dir="auto">
<div class="form-control"> <div class="form-control">
<%= f.label :email, t(:email), class: 'label' %> <%= f.label :email, t(:email), class: 'label' %>
+1 -6
View File
@@ -8,12 +8,7 @@
<% if params[:redir].present? %> <% if params[:redir].present? %>
<%= hidden_field_tag :redir, params[:redir] %> <%= hidden_field_tag :redir, params[:redir] %>
<% end %> <% end %>
<div class="space-y-2"> <%= render 'otp_form', **local_assigns %>
<div class="form-control">
<%= f.label :otp_attempt, t('two_factor_code_from_authenticator_app'), class: 'label' %>
<%= f.text_field :otp_attempt, autofocus: true, placeholder: 'XXX-XXX', required: true, class: 'base-input' %>
</div>
</div>
<div class="form-control"> <div class="form-control">
<%= f.button button_title(title: t('sign_in'), disabled_with: t('signing_in')), class: 'base-button' %> <%= f.button button_title(title: t('sign_in'), disabled_with: t('signing_in')), class: 'base-button' %>
</div> </div>
+1 -1
View File
@@ -173,7 +173,7 @@
<%= t('document_download_filename_format') %> <%= t('document_download_filename_format') %>
</span> </span>
<div class="mt-3"> <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>
</div> </div>
<% end %> <% end %>
+3
View File
@@ -2,6 +2,9 @@
<html data-theme="docuseal" lang="<%= I18n.locale %>"> <html data-theme="docuseal" lang="<%= I18n.locale %>">
<head> <head>
<%= render 'layouts/head_tags' %> <%= 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"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<%= csrf_meta_tags %> <%= csrf_meta_tags %>
<%= csp_meta_tag %> <%= csp_meta_tag %>
@@ -8,7 +8,7 @@
<div class="collapse-content"> <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| %> <%= 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.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"> <div class="form-control">
<%= ff.label :subject, t('subject'), class: 'label' %> <%= ff.label :subject, t('subject'), class: 'label' %>
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %> <%= 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' %> <%= ff.text_area :body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea> </autoresize-textarea>
</div> </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"> <div class="flex items-center justify-between pt-2.5 mx-1">
<span> <span>
<%= t('attach_documents') %> <%= 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"
}
+1 -1
View File
@@ -1 +1 @@
<% 'stats stat stat-figure stat-title stat-value text-accent' %> <% 'stats stat stat-figure stat-title stat-value text-accent w-fit hover:bg-white' %>
+5 -1
View File
@@ -2,5 +2,9 @@
--- ---
</p> </p>
<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> </p>
+11
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="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"> <div class="flex items-center space-x-4">
<a href="<%= root_path %>" class="text-2xl font-bold items-center flex space-x-2"> <a href="<%= root_path %>" class="text-2xl font-bold items-center flex space-x-2">
@@ -49,6 +50,16 @@
<% end %> <% end %>
</li> </li>
<% end %> <% end %>
<% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %>
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'w-full py-1' } do |f| %>
<label class="flex items-center pl-6 pr-4 py-2 border-y border-base-300 -ml-2 -mr-2" for="testing_toggle">
<%= f.check_box :testing_toggle, class: 'toggle', checked: current_account.testing?, onchange: 'this.form.requestSubmit()', style: 'height: 0.885rem; width: 1.35rem; --handleoffset: 0.395rem; margin-left: -2px; margin-right: 8px' %>
<span class="whitespace-nowrap">
<%= t('test_mode') %>
</span>
</label>
<% end %>
<% end %>
<li> <li>
<%= button_to destroy_user_session_path, method: :delete, data: { turbo: false }, class: 'flex items-center' do %> <%= button_to destroy_user_session_path, method: :delete, data: { turbo: false }, class: 'flex items-center' do %>
<%= svg_icon('logout', class: 'w-5 h-5 flex-shrink-0 stroke-2 mr-2 inline') %> <%= svg_icon('logout', class: 'w-5 h-5 flex-shrink-0 stroke-2 mr-2 inline') %>
Whitespace-only changes.
Whitespace-only changes.
+5 -5
View File
@@ -90,16 +90,16 @@
<% end %> <% end %>
<%= render 'shared/settings_nav_extra2' %> <%= render 'shared/settings_nav_extra2' %>
<% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %> <% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %>
<li> <%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'w-full' } do |f| %>
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'flex w-full' } do |f| %> <li>
<label class="flex items-center text-base hover:bg-base-300 w-full justify-between" for="testing_toggle"> <label class="flex items-center text-base hover:bg-base-300 w-full justify-between" for="testing_toggle">
<span class="mr-2 w-full"> <span class="mr-2 w-full">
<%= t('test_environment') %> <%= t('test_mode') %>
</span> </span>
<%= f.check_box :testing_toggle, class: 'toggle toggle-sm', checked: current_account.testing?, onchange: 'this.form.requestSubmit()' %> <%= f.check_box :testing_toggle, class: 'toggle toggle-sm', checked: current_account.testing?, onchange: 'this.form.requestSubmit()' %>
</label> </label>
<% end %> </li>
</li> <% end %>
<% end %> <% end %>
<% end %> <% end %>
</ul> </ul>
+1 -1
View File
@@ -2,7 +2,7 @@
<div class="hidden md:flex alert py-1 text-sm font-medium gap-x-2 whitespace-nowrap"> <div class="hidden md:flex alert py-1 text-sm font-medium gap-x-2 whitespace-nowrap">
<a href="<%= testing_api_settings_path %>" data-turbo-frame="modal" class="link font-semibold flex"> <a href="<%= testing_api_settings_path %>" data-turbo-frame="modal" class="link font-semibold flex">
<%= svg_icon('code_circle', class: 'w-5 h-5 mr-1') %> <%= svg_icon('code_circle', class: 'w-5 h-5 mr-1') %>
<span><%= t('testing_environment') %></span> <span><%= t('test_mode') %></span>
</a> </a>
<span> <span>
| |
+1 -1
View File
@@ -2,7 +2,7 @@
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'flex' } do |f| %> <%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'flex' } do |f| %>
<label class="flex items-center justify-between" for="testing_toggle"> <label class="flex items-center justify-between" for="testing_toggle">
<span class="mr-2 text-lg"> <span class="mr-2 text-lg">
<%= t('test_environment') %> <%= t('test_mode') %>
</span> </span>
<%= f.check_box :testing_toggle, class: 'toggle', checked: current_account.testing?, onchange: 'this.form.requestSubmit()' %> <%= f.check_box :testing_toggle, class: 'toggle', checked: current_account.testing?, onchange: 'this.form.requestSubmit()' %>
</label> </label>
+8 -1
View File
@@ -118,7 +118,7 @@
<div class="group border border-base-300 rounded-md px-2 py-1 mb-1"> <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 justify-between">
<div class="flex items-center space-x-1"> <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"> <span class="text-lg" dir="auto">
<%= (@submission.template_submitters || @submission.template.submitters).find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %> <%= (@submission.template_submitters || @submission.template.submitters).find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %>
</span> </span>
@@ -190,6 +190,11 @@
</a> </a>
</div> </div>
<% end %> <% 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> </div>
<div class="px-1.5 mb-4"> <div class="px-1.5 mb-4">
@@ -222,6 +227,8 @@
</div> </div>
<% elsif field['type'] == 'checkbox' %> <% elsif field['type'] == 'checkbox' %>
<%= svg_icon('check', class: 'w-6 h-6') %> <%= svg_icon('check', class: 'w-6 h-6') %>
<% elsif field['type'] == 'number' %>
<%= NumberUtils.format_number(value, field.dig('preferences', 'format')) %>
<% elsif field['type'] == 'date' %> <% elsif field['type'] == 'date' %>
<%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), @submission.account.locale) %> <%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), @submission.account.locale) %>
<% else %> <% else %>
@@ -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_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 %> <% 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 %> <% 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> <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> </download-button>
<% end %> <% end %>
</div> </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> <div class="divider uppercase"><%= t('or') %></div>
<toggle-submit class="block"> <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' %> <%= 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
@@ -23,7 +23,7 @@
<%= f.hidden_field :template_id, value: @template.id %> <%= f.hidden_field :template_id, value: @template.id %>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span> <span>
<%= t('share_template_with_test_environment') %> <%= t('share_template_with_test_mode') %>
</span> </span>
<%= f.check_box :value, class: 'toggle', checked: @template.template_sharings.exists?(account_id: current_account.testing_accounts), onchange: 'this.form.requestSubmit()' %> <%= f.check_box :value, class: 'toggle', checked: @template.template_sharings.exists?(account_id: current_account.testing_accounts), onchange: 'this.form.requestSubmit()' %>
</div> </div>
+2 -1
View File
@@ -2,7 +2,7 @@
<% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %> <% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %>
<div class="flex justify-between items-center w-full mb-4"> <div class="flex justify-between items-center w-full mb-4">
<div class="flex items-center flex-grow min-w-0"> <div class="flex items-center flex-grow min-w-0">
<% if has_archived || @pagy.count > 0 %> <% if has_archived || @pagy.count > 0 || @template_folders.present? %>
<div class="mr-2"> <div class="mr-2">
<%= render 'dashboard/toggle_view', selected: 'templates' %> <%= render 'dashboard/toggle_view', selected: 'templates' %>
</div> </div>
@@ -61,3 +61,4 @@
</div> </div>
</div> </div>
<% end %> <% end %>
<%= render 'shared/review_form' %>
@@ -118,7 +118,7 @@
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %> <%= 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> <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 %> <% 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"> <div class="form-control">
<%= ff.label :documents_copy_email_subject, t('email_subject'), class: 'label' %> <%= 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' %> <%= ff.text_field :documents_copy_email_subject, required: true, class: 'base-input', dir: 'auto' %>
@@ -134,6 +134,12 @@
<%= ff.text_area :documents_copy_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %> <%= ff.text_area :documents_copy_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea> </autoresize-textarea>
</div> </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"> <div class="flex items-center justify-between pt-2.5 px-1 mb-2">
<span> <span>
<%= t('attach_documents_to_the_email') %> <%= t('attach_documents_to_the_email') %>
@@ -308,7 +314,7 @@
<%= f.hidden_field :template_id, value: @template.id %> <%= f.hidden_field :template_id, value: @template.id %>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span> <span>
<%= t('share_template_with_test_environment') %> <%= t('share_template_with_test_mode') %>
</span> </span>
<%= f.check_box :value, class: 'toggle', checked: @template.template_sharings.exists?(account_id: current_account.testing_accounts), onchange: 'this.form.requestSubmit()' %> <%= f.check_box :value, class: 'toggle', checked: @template.template_sharings.exists?(account_id: current_account.testing_accounts), onchange: 'this.form.requestSubmit()' %>
</div> </div>
@@ -1,7 +1,7 @@
<%= render 'shared/turbo_modal', title: t('testing_environment') do %> <%= render 'shared/turbo_modal', title: t('test_mode') do %>
<div> <div>
<label class="text-sm font-semibold" for="api_key"> <label class="text-sm font-semibold" for="api_key">
x-Auth-Token X-Auth-Token
</label> </label>
<div class="flex gap-2 mb-4 mt-2"> <div class="flex gap-2 mb-4 mt-2">
<input id="api_key" type="text" value="<%= current_user.access_token.token %>" class="base-input w-full" autocomplete="off" readonly> <input id="api_key" type="text" value="<%= current_user.access_token.token %>" class="base-input w-full" autocomplete="off" readonly>
+7
View File
@@ -5,6 +5,13 @@
<option value="editor" disabled><%= t('editor') %></option> <option value="editor" disabled><%= t('editor') %></option>
<option value="viewer" disabled><%= t('viewer') %></option> <option value="viewer" disabled><%= t('viewer') %></option>
<% end %> <% end %>
<% if Docuseal.multitenant? %>
<label class="label">
<span class="label-text-alt">
<%= t('click_here_to_learn_more_about_user_roles_and_permissions_html') %>
</span>
</label>
<% end %>
<a class="text-sm mt-3 px-4 py-2 bg-base-300 rounded-full block" target="_blank" href="<%= Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}" %>"> <a class="text-sm mt-3 px-4 py-2 bg-base-300 rounded-full block" target="_blank" href="<%= Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}" %>">
<%= svg_icon('info_circle', class: 'w-4 h-4 inline align-text-bottom') %> <%= svg_icon('info_circle', class: 'w-4 h-4 inline align-text-bottom') %>
<%= t('unlock_more_user_roles_with_docuseal_pro') %> <%= t('unlock_more_user_roles_with_docuseal_pro') %>
+1 -1
View File
@@ -60,7 +60,7 @@
</span> </span>
</td> </td>
<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>
<td class="flex items-center space-x-2 justify-end"> <td class="flex items-center space-x-2 justify-end">
<% if params[:status].blank? && can?(:update, user) && user.archived_at.blank? %> <% if params[:status].blank? && can?(:update, user) && user.archived_at.blank? %>
+251 -30
View File
@@ -20,9 +20,11 @@ en: &en
language_ko: 한국어 language_ko: 한국어
hi_there: Hi there hi_there: Hi there
thanks: Thanks thanks: Thanks
reply_to: Reply to
pending_by_me: Pending by me pending_by_me: Pending by me
partially_completed: Partially completed partially_completed: Partially completed
unarchive: Unarchive unarchive: Unarchive
signed: Signed
first_party: 'First Party' first_party: 'First Party'
remove_filter: Remove filter remove_filter: Remove filter
add: Add add: Add
@@ -40,6 +42,7 @@ en: &en
hello_name: Hello %{name} hello_name: Hello %{name}
you_are_invited_to_product_name: You are invited to %{product_name} you_are_invited_to_product_name: You are invited to %{product_name}
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:' 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.' 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. sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Sign documents with trusted certificate provided by DocuSeal. Your documents and data are never shared with DocuSeal. PDF checksum is provided to generate a trusted signature.
you_have_been_invited_to_submit_the_name_form: 'You have been invited to submit the "%{name}" form.' you_have_been_invited_to_submit_the_name_form: 'You have been invited to submit the "%{name}" form.'
@@ -163,8 +166,8 @@ en: &en
schedule_account_for_deletion_: Schedule account for deletion? schedule_account_for_deletion_: Schedule account for deletion?
account_information_has_been_updated: Account information has been updated. account_information_has_been_updated: Account information has been updated.
should_be_a_valid_url: should be a valid URL 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_environment: Test Environment test_mode: Test mode
copy: Copy copy: Copy
copied: Copied copied: Copied
rotate: Rotate rotate: Rotate
@@ -321,7 +324,6 @@ en: &en
embedding: Embedding embedding: Embedding
background_jobs: Background Jobs background_jobs: Background Jobs
need_help_ask_a_question_: 'Need help? Ask a question:' need_help_ask_a_question_: 'Need help? Ask a question:'
testing_environment: Testing Environment
exit: Exit exit: Exit
leave: Leave leave: Leave
impersonated_as: Impersonated as impersonated_as: Impersonated as
@@ -426,7 +428,7 @@ en: &en
api_and_embedding: API and Embedding api_and_embedding: API and Embedding
template_id: Template ID template_id: Template ID
embedding_url: Embedding URL embedding_url: Embedding URL
share_template_with_test_environment: Share template with Test Environment share_template_with_test_mode: Share template with Test mode
share_template_with_all_tenants: Share template with all Tenants share_template_with_all_tenants: Share template with all Tenants
use_following_placeholders_text_: 'Use following placeholders text:' use_following_placeholders_text_: 'Use following placeholders text:'
upgrade_plan_to_add_more_users: Upgrade plan to add more users upgrade_plan_to_add_more_users: Upgrade plan to add more users
@@ -662,7 +664,36 @@ en: &en
policy_links: Policy Links policy_links: Policy Links
markdown_content_e_g: Markdown content, e.g. markdown_content_e_g: Markdown content, e.g.
privacy_policy: Privacy Policy privacy_policy: Privacy Policy
the_code_has_been_sent_to_your_email: The code has been sent to your email.
enter_the_verification_code_from_your_email: Enter the verification code from your email.
too_many_attempts: Too many attempts.
verification_code: Verification Code
resend_code: Resend Code
verify_new_sign_in: Verify new sign in
use_otp_code_to_sign_in_or_click_the_link_below_html: 'Use <b>%{code}</b> code to sign in or click the link below:'
complete_sign_in: Complete Sign In
please_reply_to_this_email_if_you_dont_recognize_this_sign_in_attempt: Please reply to this email if you don't recognize this sign in attempt.
docuseal_support: DocuSeal Support
use_the_edit_form_to_move_it_to_another_team: Use the edit form to move it to another team. use_the_edit_form_to_move_it_to_another_team: Use the edit form to move it to another team.
too_many_sms_attempts_try_again_in_a_few_seconds: Too many SMS attempts. Try again in a few seconds.
number_phone_number_is_invalid: "+%{number} phone number is invalid"
make_com_integration: Make.com Integration
zapier_integration: Zapier Integration
seamlessly_automate_your_document_signing_process_with_make_com: Seamlessly automate your document signing process with Make.com.
find_suitable_zapier_templates_to_automate_your_workflow: Find suitable Zapier templates to automate your workflow.
get_started: Get started
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: submission_event_names:
send_email_to_html: '<b>Email sent</b> to %{submitter_name}' send_email_to_html: '<b>Email sent</b> to %{submitter_name}'
send_reminder_email_to_html: '<b>Reminder email sent</b> to %{submitter_name}' send_reminder_email_to_html: '<b>Reminder email sent</b> to %{submitter_name}'
@@ -701,6 +732,8 @@ en: &en
read: Read your data read: Read your data
es: &es es: &es
signed: Firmado
reply_to: Responder a
partially_completed: Parcialmente completado partially_completed: Parcialmente completado
pending_by_me: Pendiente por mi pending_by_me: Pendiente por mi
add: Agregar add: Agregar
@@ -723,6 +756,7 @@ es: &es
hello_name: Hola %{name} hello_name: Hola %{name}
you_are_invited_to_product_name: Estás invitado a %{product_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:' 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>.' 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. 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 hi_there: Hola
@@ -846,8 +880,8 @@ es: &es
schedule_account_for_deletion_: ¿Programar la eliminación de la cuenta? 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. 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 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_environment: Entorno de prueba test_mode: Modo de prueba
copy: Copiar copy: Copiar
copied: Copiado copied: Copiado
rotate: Rotar rotate: Rotar
@@ -1004,7 +1038,6 @@ es: &es
embedding: Integración embedding: Integración
background_jobs: Trabajos asíncronos background_jobs: Trabajos asíncronos
need_help_ask_a_question_: '¿Necesitas ayuda? Haz una pregunta:' need_help_ask_a_question_: '¿Necesitas ayuda? Haz una pregunta:'
testing_environment: Entorno de pruebas
exit: Salir exit: Salir
leave: Salir leave: Salir
impersonated_as: Impersonado como impersonated_as: Impersonado como
@@ -1109,7 +1142,7 @@ es: &es
api_and_embedding: API e Integración api_and_embedding: API e Integración
template_id: ID de plantilla template_id: ID de plantilla
embedding_url: URL de integración embedding_url: URL de integración
share_template_with_test_environment: Compartir plantilla con el entorno de pruebas share_template_with_test_mode: Compartir plantilla con el modo de prueba
share_template_with_all_tenants: Compartir plantilla con todos los inquilinos share_template_with_all_tenants: Compartir plantilla con todos los inquilinos
use_following_placeholders_text_: 'Usa los siguientes marcadores de posición:' use_following_placeholders_text_: 'Usa los siguientes marcadores de posición:'
upgrade_plan_to_add_more_users: Actualiza el plan para agregar más usuarios upgrade_plan_to_add_more_users: Actualiza el plan para agregar más usuarios
@@ -1345,7 +1378,36 @@ es: &es
policy_links: Enlaces de Políticas policy_links: Enlaces de Políticas
markdown_content_e_g: Contenido Markdown, por ej. markdown_content_e_g: Contenido Markdown, por ej.
privacy_policy: Política de Privacidad privacy_policy: Política de Privacidad
the_code_has_been_sent_to_your_email: El código ha sido enviado a tu correo electrónico.
enter_the_verification_code_from_your_email: Ingresa el código de verificación de tu correo electrónico.
too_many_attempts: Demasiados intentos.
verification_code: Código de Verificación
resend_code: Reenviar Código
verify_new_sign_in: Verificar nuevo inicio de sesión
use_otp_code_to_sign_in_or_click_the_link_below_html: 'Usa el código <b>%{code}</b> para iniciar sesión o haz clic en el enlace a continuación:'
complete_sign_in: Completar Inicio de Sesión
please_reply_to_this_email_if_you_dont_recognize_this_sign_in_attempt: Responde a este correo electrónico si no reconoces este intento de inicio de sesión.
docuseal_support: Soporte DocuSeal
use_the_edit_form_to_move_it_to_another_team: Usa el formulario de edición para moverlo a otro equipo. use_the_edit_form_to_move_it_to_another_team: Usa el formulario de edición para moverlo a otro equipo.
too_many_sms_attempts_try_again_in_a_few_seconds: Demasiados intentos de SMS. Intenta de nuevo en unos segundos.
number_phone_number_is_invalid: "El número de teléfono +%{number} no es válido"
make_com_integration: Integración con Make.com
zapier_integration: Integración con Zapier
seamlessly_automate_your_document_signing_process_with_make_com: Automatiza sin problemas tu proceso de firma de documentos con Make.com.
find_suitable_zapier_templates_to_automate_your_workflow: Encuentra plantillas de Zapier adecuadas para automatizar tu flujo de trabajo.
get_started: Comenzar
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: submission_event_names:
send_email_to_html: '<b>Correo electrónico enviado</b> a %{submitter_name}' send_email_to_html: '<b>Correo electrónico enviado</b> a %{submitter_name}'
send_reminder_email_to_html: '<b>Correo de recordatorio enviado</b> a %{submitter_name}' send_reminder_email_to_html: '<b>Correo de recordatorio enviado</b> a %{submitter_name}'
@@ -1384,6 +1446,8 @@ es: &es
read: Leer tus datos read: Leer tus datos
it: &it it: &it
signed: Firmato
reply_to: Rispondi a
pending_by_me: In sospeso da me pending_by_me: In sospeso da me
add: Aggiungi add: Aggiungi
adding: Aggiungendo adding: Aggiungendo
@@ -1399,12 +1463,13 @@ it: &it
document_name: Nome del Documento document_name: Nome del Documento
unarchive: Ripristina unarchive: Ripristina
awaiting_completion_by_the_other_party: "In attesa di completamento da parte dell'altra parte" awaiting_completion_by_the_other_party: "In attesa di completamento da parte dell'altra parte"
enforce_recipients_order: Aplicar el orden de los destinatarios enforce_recipients_order: "Applicare l'ordine dei destinatari"
first_party: 'Prima parte' first_party: 'Prima parte'
docuseal_trusted_signature: "Firma Fiduciaria DocuSeal" docuseal_trusted_signature: "Firma Fiduciaria DocuSeal"
hello_name: Ciao %{name} hello_name: Ciao %{name}
you_are_invited_to_product_name: Sei stato invitato a %{product_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:' 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>.' 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." 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 hi_there: Ciao
@@ -1528,8 +1593,8 @@ it: &it
schedule_account_for_deletion_: "Programmare l'eliminazione dell'account?" schedule_account_for_deletion_: "Programmare l'eliminazione dell'account?"
account_information_has_been_updated: "Le informazioni dell'account sono state aggiornate." account_information_has_been_updated: "Le informazioni dell'account sono state aggiornate."
should_be_a_valid_url: deve essere un URL valido 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_environment: Ambiente di test test_mode: Modalità di test
copy: Copia copy: Copia
copied: Copiato copied: Copiato
rotate: Ruota rotate: Ruota
@@ -1686,7 +1751,6 @@ it: &it
embedding: Incorporazione embedding: Incorporazione
background_jobs: Attività in background background_jobs: Attività in background
need_help_ask_a_question_: 'Hai bisogno di aiuto? Fai una domanda:' need_help_ask_a_question_: 'Hai bisogno di aiuto? Fai una domanda:'
testing_environment: Ambiente di test
exit: Esci exit: Esci
leave: Lascia leave: Lascia
impersonated_as: Impersonato come impersonated_as: Impersonato come
@@ -1791,7 +1855,7 @@ it: &it
api_and_embedding: API e Incorporazione api_and_embedding: API e Incorporazione
template_id: ID modello template_id: ID modello
embedding_url: URL di incorporazione embedding_url: URL di incorporazione
share_template_with_test_environment: "Condividi il modello con l'ambiente di test" share_template_with_test_mode: Condividi modello con la modalità di test
share_template_with_all_tenants: Condividi il modello con tutti i tenant share_template_with_all_tenants: Condividi il modello con tutti i tenant
use_following_placeholders_text_: 'Usa i seguenti segnaposto:' use_following_placeholders_text_: 'Usa i seguenti segnaposto:'
upgrade_plan_to_add_more_users: Aggiorna il piano per aggiungere più utenti upgrade_plan_to_add_more_users: Aggiorna il piano per aggiungere più utenti
@@ -2027,7 +2091,36 @@ it: &it
policy_links: Collegamenti alle Politiche policy_links: Collegamenti alle Politiche
markdown_content_e_g: Contenuto Markdown, ad es. markdown_content_e_g: Contenuto Markdown, ad es.
privacy_policy: Politica sulla Privacy privacy_policy: Politica sulla Privacy
the_code_has_been_sent_to_your_email: Il codice è stato inviato alla tua e-mail.
enter_the_verification_code_from_your_email: Inserisci il codice di verifica dalla tua e-mail.
too_many_attempts: Troppe tentativi.
verification_code: Codice di Verifica
resend_code: Reinvia Codice
verify_new_sign_in: Verifica nuovo accesso
use_otp_code_to_sign_in_or_click_the_link_below_html: 'Usa il codice <b>%{code}</b> per accedere o clicca sul link qui sotto:'
complete_sign_in: "Completa l'Accesso"
please_reply_to_this_email_if_you_dont_recognize_this_sign_in_attempt: Rispondi a questa e-mail se non riconosci questo tentativo di accesso.
docuseal_support: Supporto DocuSeal
use_the_edit_form_to_move_it_to_another_team: Usa il modulo di modifica per spostarlo in un altro team. use_the_edit_form_to_move_it_to_another_team: Usa il modulo di modifica per spostarlo in un altro team.
too_many_sms_attempts_try_again_in_a_few_seconds: Troppi tentativi di SMS. Riprova tra qualche secondo.
number_phone_number_is_invalid: "Il numero di telefono +%{number} non è valido"
make_com_integration: Integrazione con Make.com
zapier_integration: Integrazione con Zapier
seamlessly_automate_your_document_signing_process_with_make_com: Automatizza senza problemi il tuo processo di firma dei documenti con Make.com.
find_suitable_zapier_templates_to_automate_your_workflow: Trova modelli Zapier adatti per automatizzare il tuo flusso di lavoro.
get_started: Inizia
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: submission_event_names:
send_email_to_html: '<b>E-mail inviato</b> a %{submitter_name}' send_email_to_html: '<b>E-mail inviato</b> a %{submitter_name}'
send_reminder_email_to_html: '<b>E-mail di promemoria inviato</b> a %{submitter_name}' send_reminder_email_to_html: '<b>E-mail di promemoria inviato</b> a %{submitter_name}'
@@ -2066,6 +2159,8 @@ it: &it
read: Leggi i tuoi dati read: Leggi i tuoi dati
fr: &fr fr: &fr
signed: Signé
reply_to: Répondre à
partially_completed: Partiellement complété partially_completed: Partiellement complété
pending_by_me: En attente par moi pending_by_me: En attente par moi
add: Ajouter add: Ajouter
@@ -2088,6 +2183,7 @@ fr: &fr
hello_name: Bonjour %{name} hello_name: Bonjour %{name}
you_are_invited_to_product_name: Vous êtes invité à %{product_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:' 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>.' 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. 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 hi_there: Bonjour
@@ -2212,8 +2308,8 @@ fr: &fr
schedule_account_for_deletion_: Programmer la suppression du compte? schedule_account_for_deletion_: Programmer la suppression du compte?
account_information_has_been_updated: Les informations du compte ont été mises à jour. account_information_has_been_updated: Les informations du compte ont été mises à jour.
should_be_a_valid_url: doit être une URL valide 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_environment: Mode de test test_mode: Mode test
copy: Copier copy: Copier
copied: Copié copied: Copié
rotate: Pivoter rotate: Pivoter
@@ -2370,7 +2466,6 @@ fr: &fr
embedding: Intégration embedding: Intégration
background_jobs: Tâches en arrière-plan background_jobs: Tâches en arrière-plan
need_help_ask_a_question_: "Besoin d'aide? Posez une question:" need_help_ask_a_question_: "Besoin d'aide? Posez une question:"
testing_environment: Environnement de test
exit: Quitter exit: Quitter
leave: Partir leave: Partir
impersonated_as: Usurpé comme impersonated_as: Usurpé comme
@@ -2475,7 +2570,7 @@ fr: &fr
api_and_embedding: API et intégration api_and_embedding: API et intégration
template_id: ID du modèle template_id: ID du modèle
embedding_url: "URL d'intégration" embedding_url: "URL d'intégration"
share_template_with_test_environment: "Partager le modèle avec l'environnement de test" share_template_with_test_mode: Partager le modèle avec le mode test
share_template_with_all_tenants: Partager le modèle avec tous les locataires share_template_with_all_tenants: Partager le modèle avec tous les locataires
use_following_placeholders_text_: 'Utilisez les espaces réservés suivants:' use_following_placeholders_text_: 'Utilisez les espaces réservés suivants:'
upgrade_plan_to_add_more_users: "Mettez à jour le plan pour ajouter plus d'utilisateurs" upgrade_plan_to_add_more_users: "Mettez à jour le plan pour ajouter plus d'utilisateurs"
@@ -2685,11 +2780,11 @@ fr: &fr
unverified: Non vérifié unverified: Non vérifié
document: Document document: Document
completed_at: Terminé le completed_at: Terminé le
edit_recipient: Modifica Destinatario edit_recipient: Modifier le destinataire
update_recipient: Aggiorna Destinatario update_recipient: Mettre à jour le destinataire
use_international_format_1xxx_: 'Utilizza il formato internazionale: +1xxx...' use_international_format_1xxx_: 'Utilisez un format international : +1xxx...'
submitter_cannot_be_updated: Il mittente non può essere aggiornato. submitter_cannot_be_updated: Le soumissionnaire ne peut pas être mis à jour.
at_least_one_field_must_be_filled: Almeno un campo deve essere compilato. at_least_one_field_must_be_filled: Au moins un champ doit être rempli.
archived_users: Utilisateurs Archivés archived_users: Utilisateurs Archivés
embedding_users: Utilisateurs Intégrés embedding_users: Utilisateurs Intégrés
view_embedding_users: Voir les Utilisateurs Intégrés view_embedding_users: Voir les Utilisateurs Intégrés
@@ -2711,7 +2806,36 @@ fr: &fr
policy_links: Liens des Politiques policy_links: Liens des Politiques
markdown_content_e_g: Contenu Markdown, par ex. markdown_content_e_g: Contenu Markdown, par ex.
privacy_policy: Politique de Confidentialité privacy_policy: Politique de Confidentialité
the_code_has_been_sent_to_your_email: Le code a été envoyé à votre e-mail.
enter_the_verification_code_from_your_email: Entrez le code de vérification de votre e-mail.
too_many_attempts: Trop de tentatives.
verification_code: Code de Vérification
resend_code: Renvoyer le Code
verify_new_sign_in: Vérifier la nouvelle connexion
use_otp_code_to_sign_in_or_click_the_link_below_html: 'Utilisez le code <b>%{code}</b> pour vous connecter ou cliquez sur le lien ci-dessous :'
complete_sign_in: Terminer la Connexion
please_reply_to_this_email_if_you_dont_recognize_this_sign_in_attempt: Veuillez répondre à cet e-mail si vous ne reconnaissez pas cette tentative de connexion.
docuseal_support: Support DocuSeal
use_the_edit_form_to_move_it_to_another_team: Utilisez le formulaire de modification pour le déplacer vers une autre équipe. use_the_edit_form_to_move_it_to_another_team: Utilisez le formulaire de modification pour le déplacer vers une autre équipe.
too_many_sms_attempts_try_again_in_a_few_seconds: Trop de tentatives de SMS. Réessayez dans quelques secondes.
number_phone_number_is_invalid: "Le numéro de téléphone +%{number} est invalide"
make_com_integration: Intégration avec Make.com
zapier_integration: Intégration avec Zapier
seamlessly_automate_your_document_signing_process_with_make_com: Automatisez facilement votre processus de signature de documents avec Make.com.
find_suitable_zapier_templates_to_automate_your_workflow: Trouvez des modèles Zapier adaptés pour automatiser votre flux de travail.
get_started: Commencer
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: submission_event_names:
send_email_to_html: '<b>E-mail envoyé</b> à %{submitter_name}' send_email_to_html: '<b>E-mail envoyé</b> à %{submitter_name}'
send_reminder_email_to_html: '<b>E-mail de rappel envoyé</b> à %{submitter_name}' send_reminder_email_to_html: '<b>E-mail de rappel envoyé</b> à %{submitter_name}'
@@ -2750,6 +2874,8 @@ fr: &fr
read: Lire vos données read: Lire vos données
pt: &pt pt: &pt
signed: Assinado
reply_to: Responder a
partially_completed: Parcialmente concluído partially_completed: Parcialmente concluído
pending_by_me: Pendente por mim pending_by_me: Pendente por mim
add: Adicionar add: Adicionar
@@ -2772,6 +2898,7 @@ pt: &pt
hello_name: Olá %{name} hello_name: Olá %{name}
you_are_invited_to_product_name: Você está convidado para %{product_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:' 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>.' 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. 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á hi_there: Olá
@@ -2895,8 +3022,8 @@ pt: &pt
schedule_account_for_deletion_: Agendar exclusão da conta? schedule_account_for_deletion_: Agendar exclusão da conta?
account_information_has_been_updated: As informações da conta foram atualizadas. account_information_has_been_updated: As informações da conta foram atualizadas.
should_be_a_valid_url: deve ser um URL válido 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_environment: Ambiente de teste test_mode: Modo de teste
copy: Copiar copy: Copiar
copied: Copiado copied: Copiado
rotate: Girar rotate: Girar
@@ -3053,7 +3180,6 @@ pt: &pt
embedding: Incorporação embedding: Incorporação
background_jobs: Trabalhos assíncronos background_jobs: Trabalhos assíncronos
need_help_ask_a_question_: 'Precisa de ajuda? Faça uma pergunta:' need_help_ask_a_question_: 'Precisa de ajuda? Faça uma pergunta:'
testing_environment: Ambiente de teste
exit: Sair exit: Sair
leave: Deixar leave: Deixar
impersonated_as: Impersonado como impersonated_as: Impersonado como
@@ -3158,7 +3284,7 @@ pt: &pt
api_and_embedding: API e Incorporação api_and_embedding: API e Incorporação
template_id: ID do modelo template_id: ID do modelo
embedding_url: URL de incorporação embedding_url: URL de incorporação
share_template_with_test_environment: Compartilhar modelo com ambiente de teste share_template_with_test_mode: Compartilhar modelo com o modo de teste
share_template_with_all_tenants: Compartilhar modelo com todos os locatários share_template_with_all_tenants: Compartilhar modelo com todos os locatários
use_following_placeholders_text_: 'Use os seguintes textos de substituição:' use_following_placeholders_text_: 'Use os seguintes textos de substituição:'
upgrade_plan_to_add_more_users: Faça upgrade do plano para adicionar mais usuários upgrade_plan_to_add_more_users: Faça upgrade do plano para adicionar mais usuários
@@ -3394,7 +3520,36 @@ pt: &pt
policy_links: Links de Políticas policy_links: Links de Políticas
markdown_content_e_g: Conteúdo Markdown, ex. markdown_content_e_g: Conteúdo Markdown, ex.
privacy_policy: Política de Privacidade privacy_policy: Política de Privacidade
the_code_has_been_sent_to_your_email: O código foi enviado para seu e-mail.
enter_the_verification_code_from_your_email: Insira o código de verificação do seu e-mail.
too_many_attempts: Muitas tentativas.
verification_code: Código de Verificação
resend_code: Reenviar Código
verify_new_sign_in: Verificar novo login
use_otp_code_to_sign_in_or_click_the_link_below_html: 'Use o código <b>%{code}</b> para entrar ou clique no link abaixo:'
complete_sign_in: Concluir Login
please_reply_to_this_email_if_you_dont_recognize_this_sign_in_attempt: Responda a este e-mail se você não reconhecer esta tentativa de login.
docuseal_support: Suporte DocuSeal
use_the_edit_form_to_move_it_to_another_team: Use o formulário de edição para movê-lo para outra equipe. use_the_edit_form_to_move_it_to_another_team: Use o formulário de edição para movê-lo para outra equipe.
too_many_sms_attempts_try_again_in_a_few_seconds: Muitas tentativas de SMS. Tente novamente em alguns segundos.
number_phone_number_is_invalid: "O número de telefone +%{number} é inválido"
make_com_integration: Integração com Make.com
zapier_integration: Integração com Zapier
seamlessly_automate_your_document_signing_process_with_make_com: Automatize perfeitamente seu processo de assinatura de documentos com o Make.com.
find_suitable_zapier_templates_to_automate_your_workflow: Encontre modelos Zapier adequados para automatizar seu fluxo de trabalho.
get_started: Começar
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: submission_event_names:
send_email_to_html: '<b>E-mail enviado</b> para %{submitter_name}' send_email_to_html: '<b>E-mail enviado</b> para %{submitter_name}'
send_reminder_email_to_html: '<b>E-mail de lembrete enviado</b> para %{submitter_name}' send_reminder_email_to_html: '<b>E-mail de lembrete enviado</b> para %{submitter_name}'
@@ -3433,6 +3588,8 @@ pt: &pt
read: Ler seus dados read: Ler seus dados
de: &de de: &de
signed: Unterschrieben
reply_to: Antworten auf
partially_completed: Teilweise abgeschlossen partially_completed: Teilweise abgeschlossen
pending_by_me: Ausstehend von mir pending_by_me: Ausstehend von mir
add: Hinzufügen add: Hinzufügen
@@ -3455,6 +3612,7 @@ de: &de
hello_name: Hallo %{name} hello_name: Hallo %{name}
you_are_invited_to_product_name: Sie sind eingeladen zu %{product_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:' 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>.' 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. 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 hi_there: Hallo
@@ -3578,8 +3736,8 @@ de: &de
schedule_account_for_deletion_: Konto zur Löschung einplanen? schedule_account_for_deletion_: Konto zur Löschung einplanen?
account_information_has_been_updated: Die Kontoinformationen wurden aktualisiert. account_information_has_been_updated: Die Kontoinformationen wurden aktualisiert.
should_be_a_valid_url: sollte eine gültige URL sein 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_environment: Testumgebung test_mode: Testmodus
copy: Kopieren copy: Kopieren
copied: Kopiert copied: Kopiert
rotate: Drehen rotate: Drehen
@@ -3736,7 +3894,6 @@ de: &de
embedding: Einbettung embedding: Einbettung
background_jobs: Hintergrundaufgaben background_jobs: Hintergrundaufgaben
need_help_ask_a_question_: 'Brauchen Sie Hilfe? Stellen Sie eine Frage:' need_help_ask_a_question_: 'Brauchen Sie Hilfe? Stellen Sie eine Frage:'
testing_environment: Testumgebung
exit: Beenden exit: Beenden
leave: Verlassen leave: Verlassen
impersonated_as: Angemeldet als impersonated_as: Angemeldet als
@@ -3841,7 +3998,7 @@ de: &de
api_and_embedding: API und Einbettung api_and_embedding: API und Einbettung
template_id: Vorlagen-ID template_id: Vorlagen-ID
embedding_url: Einbettungs-URL embedding_url: Einbettungs-URL
share_template_with_test_environment: Vorlage mit Testumgebung teilen share_template_with_test_mode: Vorlage mit dem Testmodus teilen
share_template_with_all_tenants: Vorlage mit allen Mietern teilen share_template_with_all_tenants: Vorlage mit allen Mietern teilen
use_following_placeholders_text_: 'Verwende die folgenden Platzhaltertexte:' use_following_placeholders_text_: 'Verwende die folgenden Platzhaltertexte:'
upgrade_plan_to_add_more_users: Upgrade des Plans, um weitere Benutzer hinzuzufügen upgrade_plan_to_add_more_users: Upgrade des Plans, um weitere Benutzer hinzuzufügen
@@ -4077,7 +4234,36 @@ de: &de
policy_links: Richtlinien-Links policy_links: Richtlinien-Links
markdown_content_e_g: Markdown-Inhalt, z. B. markdown_content_e_g: Markdown-Inhalt, z. B.
privacy_policy: Datenschutzrichtlinie privacy_policy: Datenschutzrichtlinie
the_code_has_been_sent_to_your_email: Der Code wurde an Ihre E-Mail gesendet.
enter_the_verification_code_from_your_email: Geben Sie den Verifizierungscode aus Ihrer E-Mail ein.
too_many_attempts: Zu viele Versuche.
verification_code: Verifizierungscode
resend_code: Code erneut senden
verify_new_sign_in: Neue Anmeldung überprüfen
use_otp_code_to_sign_in_or_click_the_link_below_html: 'Verwenden Sie den Code <b>%{code}</b>, um sich anzumelden, oder klicken Sie auf den untenstehenden Link:'
complete_sign_in: Anmeldung abschließen
please_reply_to_this_email_if_you_dont_recognize_this_sign_in_attempt: Bitte antworten Sie auf diese E-Mail, wenn Sie diesen Anmeldeversuch nicht erkennen.
docuseal_support: DocuSeal Support
use_the_edit_form_to_move_it_to_another_team: Verwenden Sie das Bearbeitungsformular, um ihn in ein anderes Team zu verschieben. use_the_edit_form_to_move_it_to_another_team: Verwenden Sie das Bearbeitungsformular, um ihn in ein anderes Team zu verschieben.
too_many_sms_attempts_try_again_in_a_few_seconds: Zu viele SMS-Versuche. Versuchen Sie es in ein paar Sekunden erneut.
number_phone_number_is_invalid: "Die Telefonnummer +%{number} ist ungültig"
make_com_integration: Make.com-Integration
zapier_integration: Zapier-Integration
seamlessly_automate_your_document_signing_process_with_make_com: Automatisieren Sie Ihren Dokumentenunterzeichnungsprozess nahtlos mit Make.com.
find_suitable_zapier_templates_to_automate_your_workflow: Finden Sie passende Zapier-Vorlagen, um Ihren Workflow zu automatisieren.
get_started: Loslegen
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: submission_event_names:
send_email_to_html: '<b>E-Mail gesendet</b> an %{submitter_name}' send_email_to_html: '<b>E-Mail gesendet</b> an %{submitter_name}'
send_reminder_email_to_html: '<b>Erinnerungs-E-Mail gesendet</b> an %{submitter_name}' send_reminder_email_to_html: '<b>Erinnerungs-E-Mail gesendet</b> an %{submitter_name}'
@@ -4168,6 +4354,11 @@ pl:
sign_up_with_microsoft: Zarejestruj się przez Microsoft sign_up_with_microsoft: Zarejestruj się przez Microsoft
by_creating_an_account_you_agree_to_our_html: 'Tworząc konto, akceptujesz naszą <a target="_blank" href="https://www.docuseal.com/privacy">Politykę Prywatności</a> i <a target="_blank" href="https://www.docuseal.com/terms">Regulamin</a>.' by_creating_an_account_you_agree_to_our_html: 'Tworząc konto, akceptujesz naszą <a target="_blank" href="https://www.docuseal.com/privacy">Politykę Prywatności</a> i <a target="_blank" href="https://www.docuseal.com/terms">Regulamin</a>.'
enter_email_to_continue: Wprowadź e-mail, aby kontynuować enter_email_to_continue: Wprowadź e-mail, aby kontynuować
the_code_has_been_sent_to_your_email: Kod został wysłany na Twój e-mail.
enter_the_verification_code_from_your_email: Wprowadź kod weryfikacyjny z Twojego e-maila.
too_many_attempts: Zbyt wiele prób.
verification_code: Kod Weryfikacyjny
resend_code: Wyślij Kod Ponownie
uk: uk:
awaiting_completion_by_the_other_party: "Очікує завершення з боку іншої сторони" awaiting_completion_by_the_other_party: "Очікує завершення з боку іншої сторони"
@@ -4222,6 +4413,11 @@ uk:
sign_up_with_microsoft: Зареєструватися через Microsoft sign_up_with_microsoft: Зареєструватися через Microsoft
by_creating_an_account_you_agree_to_our_html: 'Створюючи акаунт, ви погоджуєтесь з нашою <a target="_blank" href="https://www.docuseal.com/privacy">Політикою конфіденційності</a> і <a target="_blank" href="https://www.docuseal.com/terms">Умовами надання послуг</a>.' by_creating_an_account_you_agree_to_our_html: 'Створюючи акаунт, ви погоджуєтесь з нашою <a target="_blank" href="https://www.docuseal.com/privacy">Політикою конфіденційності</a> і <a target="_blank" href="https://www.docuseal.com/terms">Умовами надання послуг</a>.'
enter_email_to_continue: Введіть електронну пошту, щоб продовжити enter_email_to_continue: Введіть електронну пошту, щоб продовжити
the_code_has_been_sent_to_your_email: Код було надіслано на вашу електронну пошту.
enter_the_verification_code_from_your_email: Введіть код перевірки з вашої електронної пошти.
too_many_attempts: Забагато спроб.
verification_code: Код перевірки
resend_code: Відправити код знову
cs: cs:
awaiting_completion_by_the_other_party: "Čeká se na dokončení druhou stranou" awaiting_completion_by_the_other_party: "Čeká se na dokončení druhou stranou"
@@ -4276,6 +4472,11 @@ cs:
sign_up_with_microsoft: Zaregistrovat se pomocí Microsoftu sign_up_with_microsoft: Zaregistrovat se pomocí Microsoftu
by_creating_an_account_you_agree_to_our_html: 'Vytvořením účtu souhlasíte s našimi <a target="_blank" href="https://www.docuseal.com/privacy">Zásadami ochrany osobních údajů</a> a <a target="_blank" href="https://www.docuseal.com/terms">Podmínkami služby</a>.' by_creating_an_account_you_agree_to_our_html: 'Vytvořením účtu souhlasíte s našimi <a target="_blank" href="https://www.docuseal.com/privacy">Zásadami ochrany osobních údajů</a> a <a target="_blank" href="https://www.docuseal.com/terms">Podmínkami služby</a>.'
enter_email_to_continue: Zadejte e-mail pro pokračování enter_email_to_continue: Zadejte e-mail pro pokračování
the_code_has_been_sent_to_your_email: Kód byl odeslán na váš e-mail.
enter_the_verification_code_from_your_email: Zadejte ověřovací kód z vašeho e-mailu.
too_many_attempts: Příliš mnoho pokusů.
verification_code: Ověřovací Kód
resend_code: Znovu Odeslat Kód
he: he:
awaiting_completion_by_the_other_party: "המתנה להשלמה מצד הצד השני" awaiting_completion_by_the_other_party: "המתנה להשלמה מצד הצד השני"
@@ -4330,6 +4531,11 @@ he:
sign_up_with_microsoft: הירשם עם מיקרוסופט sign_up_with_microsoft: הירשם עם מיקרוסופט
by_creating_an_account_you_agree_to_our_html: 'על ידי יצירת חשבון, אתה מסכים ל<a target="_blank" href="https://www.docuseal.com/privacy">מדיניות הפרטיות</a> ול<a target="_blank" href="https://www.docuseal.com/terms">תנאי השירות</a> שלנו.' by_creating_an_account_you_agree_to_our_html: 'על ידי יצירת חשבון, אתה מסכים ל<a target="_blank" href="https://www.docuseal.com/privacy">מדיניות הפרטיות</a> ול<a target="_blank" href="https://www.docuseal.com/terms">תנאי השירות</a> שלנו.'
enter_email_to_continue: הכנס דוא"ל כדי להמשיך enter_email_to_continue: הכנס דוא"ל כדי להמשיך
the_code_has_been_sent_to_your_email: הקוד נשלח לדוא"ל שלך.
enter_the_verification_code_from_your_email: הזן את קוד האימות מדוא"ל שלך.
too_many_attempts: יותר מדי ניסיונות.
verification_code: קוד אימות
resend_code: שלח קוד מחדש
nl: nl:
awaiting_completion_by_the_other_party: "In afwachting van voltooiing door de andere partij" awaiting_completion_by_the_other_party: "In afwachting van voltooiing door de andere partij"
@@ -4384,6 +4590,11 @@ nl:
sign_up_with_microsoft: Aanmelden met Microsoft sign_up_with_microsoft: Aanmelden met Microsoft
by_creating_an_account_you_agree_to_our_html: 'Door een account aan te maken, ga je akkoord met ons <a target="_blank" href="https://www.docuseal.com/privacy">Privacybeleid</a> en onze <a target="_blank" href="https://www.docuseal.com/terms">Gebruiksvoorwaarden</a>.' by_creating_an_account_you_agree_to_our_html: 'Door een account aan te maken, ga je akkoord met ons <a target="_blank" href="https://www.docuseal.com/privacy">Privacybeleid</a> en onze <a target="_blank" href="https://www.docuseal.com/terms">Gebruiksvoorwaarden</a>.'
enter_email_to_continue: Voer e-mail in om door te gaan enter_email_to_continue: Voer e-mail in om door te gaan
the_code_has_been_sent_to_your_email: De code is naar uw e-mail verzonden.
enter_the_verification_code_from_your_email: Voer de verificatiecode uit uw e-mail in.
too_many_attempts: Te veel pogingen.
verification_code: Verificatiecode
resend_code: Code Opnieuw Verzenden
ar: ar:
awaiting_completion_by_the_other_party: "في انتظار إكتمال الطرف الآخر" awaiting_completion_by_the_other_party: "في انتظار إكتمال الطرف الآخر"
@@ -4438,6 +4649,11 @@ ar:
sign_up_with_microsoft: الاشتراك باستخدام مايكروسوفت sign_up_with_microsoft: الاشتراك باستخدام مايكروسوفت
by_creating_an_account_you_agree_to_our_html: 'من خلال إنشاء حساب، فإنك توافق على <a target="_blank" href="https://www.docuseal.com/privacy">سياسة الخصوصية</a> و<a target="_blank" href="https://www.docuseal.com/terms">شروط الخدمة</a> الخاصة بنا.' by_creating_an_account_you_agree_to_our_html: 'من خلال إنشاء حساب، فإنك توافق على <a target="_blank" href="https://www.docuseal.com/privacy">سياسة الخصوصية</a> و<a target="_blank" href="https://www.docuseal.com/terms">شروط الخدمة</a> الخاصة بنا.'
enter_email_to_continue: أدخل البريد الإلكتروني للمتابعة enter_email_to_continue: أدخل البريد الإلكتروني للمتابعة
the_code_has_been_sent_to_your_email: تم إرسال الرمز إلى بريدك الإلكتروني.
enter_the_verification_code_from_your_email: أدخل رمز التحقق من بريدك الإلكتروني.
too_many_attempts: عدد المحاولات كبير جدًا.
verification_code: رمز التحقق
resend_code: إعادة إرسال الرمز
ko: ko:
awaiting_completion_by_the_other_party: "다른 당사자의 완료를 기다리고 있습니다" awaiting_completion_by_the_other_party: "다른 당사자의 완료를 기다리고 있습니다"
@@ -4492,6 +4708,11 @@ ko:
sign_up_with_microsoft: Microsoft로 가입 sign_up_with_microsoft: Microsoft로 가입
by_creating_an_account_you_agree_to_our_html: '계정을 생성함으로써, 귀하는 우리의 <a target="_blank" href="https://www.docuseal.com/privacy">개인정보 보호정책</a> 및 <a target="_blank" href="https://www.docuseal.com/terms">서비스 약관</a>에 동의하는 것입니다.' by_creating_an_account_you_agree_to_our_html: '계정을 생성함으로써, 귀하는 우리의 <a target="_blank" href="https://www.docuseal.com/privacy">개인정보 보호정책</a> 및 <a target="_blank" href="https://www.docuseal.com/terms">서비스 약관</a>에 동의하는 것입니다.'
enter_email_to_continue: 계속하려면 이메일을 입력하세요 enter_email_to_continue: 계속하려면 이메일을 입력하세요
the_code_has_been_sent_to_your_email: 코드가 이메일로 전송되었습니다.
enter_the_verification_code_from_your_email: 이메일에서 인증 코드를 입력하세요.
too_many_attempts: 시도 횟수가 너무 많습니다.
verification_code: 인증 코드
resend_code: 코드 재전송
en-US: en-US:
<<: *en <<: *en
+2
View File
@@ -12,6 +12,7 @@ Rails.application.routes.draw do
root 'dashboard#index' root 'dashboard#index'
get 'up' => 'rails/health#show' get 'up' => 'rails/health#show'
get 'manifest' => 'pwa#manifest'
devise_for :users, devise_for :users,
path: '/', only: %i[sessions passwords omniauth_callbacks], path: '/', only: %i[sessions passwords omniauth_callbacks],
@@ -79,6 +80,7 @@ Rails.application.routes.draw do
resource :testing_account, only: %i[show destroy] resource :testing_account, only: %i[show destroy]
resources :testing_api_settings, only: %i[index] resources :testing_api_settings, only: %i[index]
resources :submitters_autocomplete, only: %i[index] resources :submitters_autocomplete, only: %i[index]
resources :submitters_resubmit, only: %i[update]
resources :template_folders_autocomplete, only: %i[index] resources :template_folders_autocomplete, only: %i[index]
resources :webhook_secret, only: %i[show update] resources :webhook_secret, only: %i[show update]
resources :webhook_preferences, only: %i[update] resources :webhook_preferences, only: %i[update]
+1 -1
View File
@@ -25,7 +25,7 @@ module Abilities
account_ids = [user.account_id, TemplateSharing::ALL_ID] account_ids = [user.account_id, TemplateSharing::ALL_ID]
template.template_sharings.any? do |e| 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 end
end end
+4
View File
@@ -153,6 +153,10 @@ module Accounts
EncryptedConfig.exists?(key: EncryptedConfig::EMAIL_SMTP_KEY) EncryptedConfig.exists?(key: EncryptedConfig::EMAIL_SMTP_KEY)
end end
def can_send_invitation_emails?(_account)
true
end
def normalize_timezone(timezone) def normalize_timezone(timezone)
tzinfo = TZInfo::Timezone.get(ActiveSupport::TimeZone::MAPPING[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) @default_pkcs ||= GenerateCertificate.load_pkcs(Docuseal::CERTS)
end end
def enable_pwa?
true
end
def trusted_certs def trusted_certs
@trusted_certs ||= @trusted_certs ||=
ENV['TRUSTED_CERTS'].to_s.gsub('\\n', "\n").split("\n\n").map do |base64| 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 = { FORMAT_LOCALES = {
'dot' => 'de', 'dot' => 'de',
'space' => 'fr', 'space' => 'fr',
'comma' => 'en' 'comma' => 'en',
'usd' => 'en',
'eur' => 'fr',
'gbp' => 'en'
}.freeze
CURRENCY_SYMBOLS = {
'usd' => '$',
'eur' => '€',
'gbp' => '£'
}.freeze }.freeze
module_function module_function
@@ -12,7 +21,9 @@ module NumberUtils
def format_number(number, format) def format_number(number, format)
locale = FORMAT_LOCALES[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:) ApplicationController.helpers.number_with_delimiter(number, locale:)
else else
number number
+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) def normalize_email(email)
return if email.blank? 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?(',') ||
return email.downcase if email.to_s.include?('.om') email.to_s.match?(/\.(?:gob|om|mm|cm|et|mo|nz|za|ie)\z/) ||
return email.downcase if email.to_s.include?('.mm') email.to_s.exclude?('.')
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?('.')
fixed_email = EmailTypo.call(email.delete_prefix('<')) 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 fixed_email
end end
@@ -135,31 +142,57 @@ module Submissions
values ||= submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } 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 end
item item
end end
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? return true if item['conditions'].blank?
item['conditions'].all? do |condition| item['conditions'].each_with_object([]) do |condition, acc|
result = result =
if fields_index[condition['field_uuid']]['submitter_uuid'] == include_submitter_uuid if fields_index[condition['field_uuid']]['submitter_uuid'] == include_submitter_uuid
submitter_conditions_acc << condition if submitter_conditions_acc
true true
else else
Submitters::SubmitValues.check_field_condition(condition, values, fields_index) Submitters::SubmitValues.check_field_condition(condition, values, fields_index)
end end
item['conditions'].each_with_object([]) do |c, acc| if condition['operation'] == 'or'
if c['operation'] == 'or' acc.push(acc.pop || result)
acc.push(acc.pop || result) else
else acc.push(result)
acc.push(result) end
end end.exclude?(false)
end.exclude?(false)
end
end end
end end
+10 -3
View File
@@ -31,7 +31,8 @@ module Submissions
next if uuid.blank? next if uuid.blank?
next if submitter_attrs.slice('email', 'phone', 'name').compact_blank.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? is_order_sent = submitters_order == 'random' || index.zero?
@@ -54,11 +55,17 @@ module Submissions
# rubocop:enable Metrics/BlockLength # rubocop:enable Metrics/BlockLength
def maybe_add_invite_submitters(submission, template) 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 item['invite_by_uuid'].blank? && item['optional_invite_by_uuid'].blank?
next if submission.template_submitters.any? { |e| e['uuid'] == item['uuid'] } 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
end end
+5 -1
View File
@@ -175,7 +175,7 @@ module Submissions
padding: [15, 0, 8, 0], padding: [15, 0, 8, 0],
position: :float) 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 }], 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) font_size: 9, padding: [15, 0, 10, 0], position: :float, text_align: :right)
end end
@@ -425,6 +425,10 @@ module Submissions
def maybe_add_background(_canvas, _submission, _page_size); end 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) def add_logo(column, _submission = nil)
column.image(PdfIcons.logo_io, width: 40, height: 40, position: :float) column.image(PdfIcons.logo_io, width: 40, height: 40, position: :float)
+13 -1
View File
@@ -123,10 +123,22 @@ module Submitters
filename = ReplaceEmailVariables.call(filename_format, submitter:) filename = ReplaceEmailVariables.call(filename_format, submitter:)
filename = filename.gsub('{document.name}', blob.filename.base) 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( filename = filename.gsub(
'{submission.completed_at}', '{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}" "#{filename}.#{blob.filename.extension}"
@@ -18,7 +18,7 @@ module Submitters
def find_or_create_signature_from_value(submitter, value, attachments) def find_or_create_signature_from_value(submitter, value, attachments)
_, attachment = Submitters::NormalizeValues.normalize_attachment_value(value, _, attachment = Submitters::NormalizeValues.normalize_attachment_value(value,
'signature', { 'type' => 'signature' },
submitter.account, submitter.account,
attachments, attachments,
submitter) submitter)
+13 -5
View File
@@ -40,7 +40,7 @@ module Submitters
if field['type'].in?(%w[initials signature image file stamp]) && value.present? if field['type'].in?(%w[initials signature image file stamp]) && value.present?
new_value, new_attachments = 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) attachments.push(*new_attachments)
@@ -109,17 +109,17 @@ module Submitters
.merge(fields.index_by { |e| e['name'].to_s.downcase }) .merge(fields.index_by { |e| e['name'].to_s.downcase })
end 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) if value.is_a?(Array)
new_attachments = value.map do |v| 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 attachments.find { |a| a.blob_id == new_attachment.blob_id } || new_attachment
end end
[new_attachments.map(&:uuid), new_attachments] [new_attachments.map(&:uuid), new_attachments]
else 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 } existing_attachment = attachments.find { |a| a.blob_id == new_attachment.blob_id }
@@ -129,7 +129,9 @@ module Submitters
end end
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 = blob =
if value.match?(%r{\Ahttps?://}) if value.match?(%r{\Ahttps?://})
find_or_create_blob_from_url(account, value) find_or_create_blob_from_url(account, value)
@@ -138,6 +140,8 @@ module Submitters
elsif (data = Base64.decode64(value.sub(BASE64_PREFIX_REGEXP, ''))) && elsif (data = Base64.decode64(value.sub(BASE64_PREFIX_REGEXP, ''))) &&
Marcel::MimeType.for(data).exclude?('octet-stream') Marcel::MimeType.for(data).exclude?('octet-stream')
find_or_create_blob_from_base64(account, data, type) 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 else
raise InvalidDefaultValue, "Invalid value, url, base64 or text < 60 chars is expected: #{value.first(200)}..." raise InvalidDefaultValue, "Invalid value, url, base64 or text < 60 chars is expected: #{value.first(200)}..."
end end
@@ -152,6 +156,10 @@ module Submitters
attachment attachment
end 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) def find_or_create_blob_from_base64(account, data, type)
checksum = Digest::MD5.base64digest(data) checksum = Digest::MD5.base64digest(data)
+20 -5
View File
@@ -54,8 +54,8 @@ module Submitters
submitter.ip = request.remote_ip submitter.ip = request.remote_ip
submitter.ua = request.user_agent submitter.ua = request.user_agent
submitter.values = merge_default_values(submitter) submitter.values = merge_default_values(submitter)
submitter.values = merge_formula_values(submitter)
submitter.values = maybe_remove_condition_values(submitter) submitter.values = maybe_remove_condition_values(submitter)
submitter.values = merge_formula_values(submitter)
submitter.values = submitter.values.transform_values do |v| submitter.values = submitter.values.transform_values do |v|
v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v
end end
@@ -186,10 +186,21 @@ module Submitters
attachments_index = attachments_index =
Submissions.filtered_conditions_schema(submitter.submission).index_by { |i| i['attachment_uuid'] } 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| submitter.submission.template_fields.each do |field|
next if field['submitter_uuid'] != submitter.uuid 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']] } if field['areas'].present? && field['areas'].none? { |area| attachments_index[area['attachment_uuid']] }
submitter.values.delete(field['uuid']) submitter.values.delete(field['uuid'])
@@ -199,10 +210,14 @@ module Submitters
submitter.values submitter.values
end end
def check_field_conditions(submitter, field, fields_uuid_index) def field_conditions_other_submitter?(submitter, field, fields_uuid_index)
return true if field['conditions'].blank? 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| field['conditions'].each_with_object([]) do |c, acc|
if c['operation'] == 'or' 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))
})
@@ -85,7 +85,7 @@ RSpec.describe SendFormCompletedWebhookRequestJob do
stub_request(:post, webhook_url.url).to_return(status: 401) stub_request(:post, webhook_url.url).to_return(status: 401)
expect do expect do
described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id, 'attempt' => 11) described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id, 'attempt' => 21)
end.not_to change(described_class.jobs, :size) end.not_to change(described_class.jobs, :size)
expect(WebMock).to have_requested(:post, webhook_url.url).once expect(WebMock).to have_requested(:post, webhook_url.url).once
@@ -83,7 +83,7 @@ RSpec.describe SendSubmissionCompletedWebhookRequestJob do
expect do expect do
described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id, described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id,
'attempt' => 11) 'attempt' => 21)
end.not_to change(described_class.jobs, :size) end.not_to change(described_class.jobs, :size)
expect(WebMock).to have_requested(:post, webhook_url.url).once expect(WebMock).to have_requested(:post, webhook_url.url).once
+4
View File
@@ -71,6 +71,10 @@ RSpec.configure do |config|
Sidekiq::Testing.inline! if example.metadata[:sidekiq] == :inline Sidekiq::Testing.inline! if example.metadata[:sidekiq] == :inline
end end
config.after do |example|
Sidekiq::Testing.fake! if example.metadata[:sidekiq] == :inline
end
config.before(multitenant: true) do config.before(multitenant: true) do
allow(Docuseal).to receive(:multitenant?).and_return(true) allow(Docuseal).to receive(:multitenant?).and_return(true)
end end
Loaded 100 of 101 files, more files were not shown because too many files have changed in this diff. Show more