Compare commits

...
23 Commits
Author SHA1 Message Date
Alex Turchyn d0799b8cc1 Merge from docusealco/wip 2025-04-07 12:58:13 +03:00
Pete Matsyburka 889fcad859 fix drop position 2025-04-05 12:06:29 +03:00
Pete Matsyburka c133fef99d ruby 3.4.2 2025-04-04 17:20:52 +03:00
Alex Turchyn cfdc978a2a add app tour 2025-04-04 11:40:55 +03:00
Pete Matsyburka bff1651966 fix long font preview 2025-04-03 17:59:05 +03:00
Alex Turchyn b97854f2fe improve account deletion confirmation message 2025-04-03 12:56:48 +03:00
Alex Turchyn 7a4629bf79 improve drag'n'drop in the builder 2025-04-02 16:51:39 +03:00
Pete Matsyburka 2573d8cb19 fix n+1 2025-04-01 10:23:31 +03:00
Pete Matsyburka dd559e90f1 add archived filter 2025-04-01 10:23:30 +03:00
Alex Turchyn e5db61e358 add API specs for completed submitter 2025-03-31 22:40:05 +03:00
Pete Matsyburka 69376c0faf fix acro form fields 2025-03-31 21:50:04 +03:00
Alex Turchyn 1f48be135c Merge from docusealco/wip 2025-03-31 13:02:51 +03:00
Pete Matsyburka 1cae12b728 adjust validation 2025-03-29 23:15:45 +02:00
Alex Turchyn 86d2cd93f8 fix signature uploading 2025-03-29 15:44:19 +02:00
Alex Turchyn b0110d9340 update signature validation 2025-03-29 14:50:26 +02:00
Alex Turchyn 7e70f4fb14 update translations 2025-03-29 13:37:21 +02:00
Pete Matsyburka 28e3d65e63 improve required field handling 2025-03-28 22:42:27 +02:00
Pete Matsyburka f1a2b71b59 fix smtp 2025-03-27 19:50:09 +02:00
Pete Matsyburka 47c7270911 enforce signing order 2025-03-27 19:23:20 +02:00
Pete Matsyburka 4e1fa336a2 add personalization options 2025-03-27 19:23:19 +02:00
Pete Matsyburka 244bc32136 fix preview url 2025-03-27 00:52:35 +02:00
Pete Matsyburka 42c30bdfd0 ensure generated doc completed 2025-03-26 13:14:59 +02:00
Alex Turchyn 0abf48ddba update preview image 2025-03-26 13:14:07 +02:00
64 changed files with 1452 additions and 277 deletions

No files matched your search

+4 -4
View File
@@ -11,7 +11,7 @@ jobs:
- 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.2
- name: Cache gems - name: Cache gems
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
@@ -35,7 +35,7 @@ jobs:
- 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.2
- name: Cache gems - name: Cache gems
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
@@ -85,7 +85,7 @@ jobs:
- 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.2
- name: Cache gems - name: Cache gems
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
@@ -127,7 +127,7 @@ jobs:
- 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.2
- name: Set up Node - name: Set up Node
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
+3 -3
View File
@@ -1,4 +1,4 @@
FROM ruby:3.4.1-alpine AS fonts FROM ruby:3.4.2-alpine AS fonts
WORKDIR /fonts WORKDIR /fonts
@@ -6,7 +6,7 @@ RUN apk --no-cache add fontforge wget && wget https://github.com/satbyy/go-noto-
RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")' RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")'
FROM ruby:3.4.1-alpine AS webpack FROM ruby:3.4.2-alpine AS webpack
ENV RAILS_ENV=production ENV RAILS_ENV=production
ENV NODE_ENV=production ENV NODE_ENV=production
@@ -32,7 +32,7 @@ COPY ./app/views ./app/views
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
FROM ruby:3.4.1-alpine AS app FROM ruby:3.4.2-alpine AS app
ENV RAILS_ENV=production ENV RAILS_ENV=production
ENV BUNDLE_WITHOUT="development:test" ENV BUNDLE_WITHOUT="development:test"
+1 -1
View File
@@ -2,7 +2,7 @@
source 'https://rubygems.org' source 'https://rubygems.org'
ruby '3.4.1' ruby '3.4.2'
gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic' gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic'
gem 'aws-sdk-s3', require: false gem 'aws-sdk-s3', require: false
+1 -1
View File
@@ -652,7 +652,7 @@ DEPENDENCIES
webmock webmock
RUBY VERSION RUBY VERSION
ruby 3.4.1p0 ruby 3.4.2p28
BUNDLED WITH BUNDLED WITH
2.5.3 2.5.3
@@ -15,6 +15,7 @@ class AccountConfigsController < ApplicationController
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY, AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
AccountConfig::FORCE_SSO_AUTH_KEY, AccountConfig::FORCE_SSO_AUTH_KEY,
AccountConfig::FLATTEN_RESULT_PDF_KEY, AccountConfig::FLATTEN_RESULT_PDF_KEY,
AccountConfig::ENFORCE_SIGNING_ORDER_KEY,
AccountConfig::WITH_SIGNATURE_ID, AccountConfig::WITH_SIGNATURE_ID,
AccountConfig::COMBINE_PDF_RESULT_KEY, AccountConfig::COMBINE_PDF_RESULT_KEY,
AccountConfig::REQUIRE_SIGNING_REASON_KEY, AccountConfig::REQUIRE_SIGNING_REASON_KEY,
@@ -17,6 +17,10 @@ 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
if params.key?(:archived)
submissions = params[:archived].in?(['true', true]) ? submissions.archived : submissions.active
end
submissions = Submissions::Filter.call(submissions, current_user, params) submissions = Submissions::Filter.call(submissions, current_user, params)
submissions = paginate(submissions.preload(:created_by_user, :submitters, submissions = paginate(submissions.preload(:created_by_user, :submitters,
@@ -23,9 +23,9 @@ class SubmissionsDownloadController < ApplicationController
last_submitter = submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last last_submitter = submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
Submissions::EnsureResultGenerated.call(last_submitter) return head :not_found unless last_submitter
return head :not_found unless last_submitter.completed_at? Submissions::EnsureResultGenerated.call(last_submitter)
if last_submitter.completed_at < TTL.ago && !signature_valid && !current_user_submitter?(last_submitter) if last_submitter.completed_at < TTL.ago && !signature_valid && !current_user_submitter?(last_submitter)
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar) Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
+9 -3
View File
@@ -20,7 +20,11 @@ class SubmitFormController < ApplicationController
@submitter.account.archived_at? @submitter.account.archived_at?
return render :expired if submission.expired? return render :expired if submission.expired?
return render :declined if @submitter.declined_at? return render :declined if @submitter.declined_at?
return render :awaiting if submission.template.preferences['submitters_order'] == 'preserved' &&
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
return render :awaiting if (@form_configs[:enforce_signing_order] ||
submission.template.preferences['submitters_order'] == 'preserved') &&
!Submitters.current_submitter_order?(@submitter) !Submitters.current_submitter_order?(@submitter)
Submitters.preload_with_pages(@submitter) Submitters.preload_with_pages(@submitter)
@@ -29,8 +33,6 @@ class SubmitFormController < ApplicationController
@attachments_index = build_attachments_index(submission) @attachments_index = build_attachments_index(submission)
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
return unless @form_configs[:prefill_signature] return unless @form_configs[:prefill_signature]
if (user_signature = UserConfigs.load_signature(current_user)) if (user_signature = UserConfigs.load_signature(current_user))
@@ -70,6 +72,10 @@ class SubmitFormController < ApplicationController
Submitters::SubmitValues.call(submitter, params, request) Submitters::SubmitValues.call(submitter, params, request)
head :ok head :ok
rescue Submitters::SubmitValues::RequiredFieldError => e
Rollbar.warning("Required field #{submitter.id}: #{e.message}") if defined?(Rollbar)
render json: { field_uuid: e.message }, status: :unprocessable_entity
rescue Submitters::SubmitValues::ValidationError => e rescue Submitters::SubmitValues::ValidationError => e
render json: { error: e.message }, status: :unprocessable_entity render json: { error: e.message }, status: :unprocessable_entity
end end
+3 -1
View File
@@ -5,7 +5,8 @@ class UserConfigsController < ApplicationController
authorize_resource :user_config authorize_resource :user_config
ALLOWED_KEYS = [ ALLOWED_KEYS = [
UserConfig::RECEIVE_COMPLETED_EMAIL UserConfig::RECEIVE_COMPLETED_EMAIL,
UserConfig::SHOW_APP_TOUR
].freeze ].freeze
InvalidKey = Class.new(StandardError) InvalidKey = Class.new(StandardError)
@@ -28,6 +29,7 @@ class UserConfigsController < ApplicationController
def user_config_params def user_config_params
params.required(:user_config).permit(:key, :value, { value: {} }, { value: [] }).tap do |attrs| params.required(:user_config).permit(:key, :value, { value: {} }, { value: [] }).tap do |attrs|
attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0]) attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
attrs[:value] = attrs[:value] == 'true' if attrs[:value].in?(%w[true false])
end end
end end
end end
+4 -3
View File
@@ -32,11 +32,10 @@ import CheckboxGroup from './elements/checkbox_group'
import MaskedInput from './elements/masked_input' import MaskedInput from './elements/masked_input'
import SetDateButton from './elements/set_date_button' import SetDateButton from './elements/set_date_button'
import IndeterminateCheckbox from './elements/indeterminate_checkbox' import IndeterminateCheckbox from './elements/indeterminate_checkbox'
import AppTour from './elements/app_tour'
import * as TurboInstantClick from './lib/turbo_instant_click' import * as TurboInstantClick from './lib/turbo_instant_click'
import './images/preview.png'
TurboInstantClick.start() TurboInstantClick.start()
document.addEventListener('turbo:before-cache', () => { document.addEventListener('turbo:before-cache', () => {
@@ -101,6 +100,7 @@ safeRegisterElement('checkbox-group', CheckboxGroup)
safeRegisterElement('masked-input', MaskedInput) safeRegisterElement('masked-input', MaskedInput)
safeRegisterElement('set-date-button', SetDateButton) safeRegisterElement('set-date-button', SetDateButton)
safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox) safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox)
safeRegisterElement('app-tour', AppTour)
safeRegisterElement('template-builder', class extends HTMLElement { safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () { connectedCallback () {
@@ -126,7 +126,8 @@ safeRegisterElement('template-builder', class extends HTMLElement {
withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false', withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false',
withConditions: this.dataset.withConditions === 'true', withConditions: this.dataset.withConditions === 'true',
currencies: (this.dataset.currencies || '').split(',').filter(Boolean), currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
acceptFileTypes: this.dataset.acceptFileTypes acceptFileTypes: this.dataset.acceptFileTypes,
showTourStartForm: this.dataset.showTourStartForm === 'true'
}) })
this.component = this.app.mount(this.appElem) this.component = this.app.mount(this.appElem)
+20 -17
View File
@@ -1,5 +1,6 @@
import SignaturePad from 'signature_pad' import SignaturePad from 'signature_pad'
import { cropCanvasAndExportToPNG } from './submission_form/crop_canvas' import { cropCanvasAndExportToPNG } from './submission_form/crop_canvas'
import { isValidSignatureCanvas } from './submission_form/validate_signature'
window.customElements.define('draw-signature', class extends HTMLElement { window.customElements.define('draw-signature', class extends HTMLElement {
connectedCallback () { connectedCallback () {
@@ -43,6 +44,8 @@ window.customElements.define('draw-signature', class extends HTMLElement {
return response return response
}) })
}).catch(error => {
console.log(error)
}).finally(() => { }).finally(() => {
this.submitButton.disabled = false this.submitButton.disabled = false
}) })
@@ -65,26 +68,26 @@ window.customElements.define('draw-signature', class extends HTMLElement {
} }
async submitImage () { async submitImage () {
return new Promise((resolve, reject) => { if (!isValidSignatureCanvas(this.pad.toData())) {
cropCanvasAndExportToPNG(this.canvas, { errorOnTooSmall: true }).then(async (blob) => { alert('Signature is too small or simple. Please redraw.')
const file = new File([blob], 'signature.png', { type: 'image/png' })
const formData = new FormData() return Promise.reject(new Error('Image too small or simple'))
}
formData.append('file', file) return cropCanvasAndExportToPNG(this.canvas).then(async (blob) => {
formData.append('submitter_slug', this.dataset.slug) const file = new File([blob], 'signature.png', { type: 'image/png' })
formData.append('name', 'attachments')
formData.append('remember_signature', 'true')
return fetch('/api/attachments', { const formData = new FormData()
method: 'POST',
body: formData formData.append('file', file)
}).then((resp) => resp.json()).then((attachment) => { formData.append('submitter_slug', this.dataset.slug)
return resolve(attachment) formData.append('name', 'attachments')
}) formData.append('remember_signature', 'true')
}).catch((error) => {
return reject(error) return fetch('/api/attachments', {
}) method: 'POST',
body: formData
}).then(resp => resp.json())
}) })
} }
+338
View File
@@ -0,0 +1,338 @@
export default class extends HTMLElement {
async connectedCallback () {
this.tourType = this.dataset.type
this.nextPagePath = this.dataset.nextPagePath
this.I18n = JSON.parse(this.dataset.i18n || '{}')
if (this.dataset.showTour === 'true') this.start()
}
async start () {
if (window.innerWidth < 768) return
const [{ driver }] = await Promise.all([
import('driver.js'),
import('driver.js/dist/driver.css')
])
this.driverObj = driver({
showProgress: true,
nextBtnText: this.I18n.next,
prevBtnText: this.I18n.previous,
doneBtnText: this.I18n.done,
onDestroyStarted: () => {
this.disableAppGuide().finally(() => { this.destroy() })
},
onHighlightStarted: (element) => {
if (element) {
const clickHandler = () => {
this.disableAppGuide().finally(() => { this.destroy() })
element.removeEventListener('click', clickHandler)
}
element.addEventListener('click', clickHandler)
}
}
})
if (this.tourType === 'dashboard') {
this.showDashboardTour()
} else if (this.tourType === 'builder') {
this.showTemplateBuilderTour()
} else if (this.tourType === 'account') {
this.showAccountTour()
} else if (this.tourType === 'template') {
this.showTemplateTour()
}
}
disconnectedCallback () {
if (this.driverObj) this.destroy()
}
destroy () {
if (this.builderTemplate) this.builderTemplate.fields.shift()
if (this.driverObj) this.driverObj.destroy()
}
showTemplateTour () {
const steps = [
{
element: '#share_link_clipboard',
popover: {
title: this.I18n.copy_and_share_link,
description: this.I18n.copy_and_share_link_description,
side: 'bottom',
align: 'end'
}
},
{
element: '#sign_yourself_button',
popover: {
title: this.I18n.sign_the_document,
description: this.I18n.sign_the_document_description,
side: 'top',
align: 'center'
}
},
{
element: '#send_to_recipients_button',
popover: {
title: this.I18n.send_for_signing,
description: this.I18n.add_recipients_description,
side: 'top',
align: 'center'
}
},
{
element: '#add_recipients_button',
popover: {
title: this.I18n.add_recipients,
description: this.I18n.add_recipients_description,
side: 'bottom',
align: 'end'
}
},
{
element: '#account_settings_button',
popover: {
title: this.I18n.settings,
description: this.I18n.settings_template_description,
side: 'right',
align: 'start',
showButtons: this.nextPagePath ? ['next', 'previous', 'close'] : ['previous', 'close'],
onNextClick: () => {
if (this.nextPagePath) {
window.Turbo.visit(this.nextPagePath)
}
}
}
}
].filter((step) => document.querySelector(step.element))
this.driverObj.setSteps(steps)
this.driverObj.drive()
}
showDashboardTour () {
this.driverObj.setSteps([
{
element: '#templates_submissions_toggle',
popover: {
title: this.I18n.template_and_submissions,
description: this.I18n.template_and_submissions_description,
side: 'right',
align: 'start'
}
},
{
element: '#templates_upload_button',
popover: {
title: this.I18n.upload_a_pdf_file,
description: this.I18n.upload_a_pdf_file_description,
side: 'left',
align: 'start',
showButtons: this.nextPagePath ? ['next', 'previous', 'close'] : ['previous', 'close'],
onNextClick: () => {
if (this.nextPagePath) {
window.Turbo.visit(this.nextPagePath)
}
}
},
onHighlightStarted: () => {}
}
])
this.driverObj.drive()
}
showAccountTour () {
this.driverObj.setSteps([
{
element: '#account_settings_menu',
popover: {
title: this.I18n.settings,
description: this.I18n.settings_account_description,
side: 'right',
align: 'start'
}
},
{
element: '#support_channels',
popover: {
title: this.I18n.support,
description: this.I18n.support_description,
side: 'left',
align: 'start'
}
}
].filter((step) => document.querySelector(step.element)))
this.driverObj.drive()
}
showTemplateBuilderTour () {
const builderComponent = document.querySelector('template-builder')?.component
this.builderTemplate = builderComponent?.template
if (this.builderTemplate) {
this.builderTemplate.fields.unshift({
uuid: 'b387399b-88dc-4345-9d37-743e97a9b2b3',
submitter_uuid: this.builderTemplate.submitters[0].uuid,
name: 'First Name',
type: 'text'
})
builderComponent.$nextTick(() => {
this.driverObj.setSteps([
{
element: '.roles-dropdown',
popover: {
title: this.I18n.select_a_signer_party,
description: this.I18n.select_a_signer_party_description,
side: 'left',
align: 'start',
onPopoverRender: () => {
const rolesDropdown = document.querySelector('.roles-dropdown')
rolesDropdown.dispatchEvent(new Event('mouseenter', { bubbles: true, cancelable: true }))
rolesDropdown.classList.add('dropdown-open')
}
}
},
{
element: '.roles-dropdown .dropdown-content',
popover: {
title: this.I18n.available_parties,
description: this.I18n.available_parties_description,
side: 'left',
align: 'start',
onPopoverRender: () => {
document.querySelector('.roles-dropdown .dropdown-content').classList.remove('driver-active-element')
},
onNextClick: () => {
document.querySelector('.roles-dropdown').classList.remove('dropdown-open')
this.driverObj.moveNext()
}
}
},
{
element: '#field-types-grid',
popover: {
title: this.I18n.available_field_types,
description: this.I18n.available_field_types_description,
side: 'right',
align: 'start',
onPrevClick: () => {
document.querySelector('.roles-dropdown').classList.add('dropdown-open')
this.driverObj.movePrevious()
}
}
},
{
element: '#text_type_field_button',
popover: {
title: this.I18n.text_input_field,
description: this.I18n.text_input_field_description,
side: 'left',
align: 'start'
}
},
{
element: '#signature_type_field_button',
popover: {
title: this.I18n.signature_field,
description: this.I18n.signature_field_description,
side: 'left',
align: 'start'
}
},
{
element: '.fields',
popover: {
title: this.I18n.added_fields,
description: this.I18n.added_fields_description,
side: 'right',
align: 'start'
}
},
{
element: '.list-field label:has(svg.tabler-icon-settings)',
popover: {
title: this.I18n.open_field_settings,
description: this.I18n.open_field_settings_description,
side: 'bottom',
align: 'end',
onPopoverRender: () => {
const settingsDropdown = document.querySelector('.list-field div:first-child span:has(svg.tabler-icon-settings)')
document.querySelectorAll('.list-field div:first-child .text-transparent').forEach((e) => e.classList.remove('text-transparent'))
settingsDropdown.dispatchEvent(new Event('mouseenter', { bubbles: true, cancelable: true }))
settingsDropdown.classList.add('dropdown-open')
}
}
},
{
element: '.list-field div:first-child span:has(svg.tabler-icon-settings) .dropdown-content',
popover: {
title: this.I18n.field_settings,
description: this.I18n.field_settings_description,
side: 'left',
align: 'start',
onPopoverRender: () => {
document.querySelector('.list-field div:first-child span:has(svg.tabler-icon-settings) .dropdown-content').classList.remove('driver-active-element')
},
onNextClick: () => {
document.querySelector('.list-field div:first-child span:has(svg.tabler-icon-settings)').classList.remove('dropdown-open')
this.driverObj.moveNext()
}
}
},
{
element: '#send_button',
popover: {
title: this.I18n.send_document,
description: this.I18n.send_document_description,
side: 'bottom',
align: 'end',
onPrevClick: () => {
document.querySelector('.list-field div:first-child span:has(svg.tabler-icon-settings)').classList.add('dropdown-open')
this.driverObj.movePrevious()
}
}
},
{
element: '#sign_yourself_button',
popover: {
title: this.I18n.sign_yourself,
description: this.I18n.sign_yourself_description,
side: 'bottom',
align: 'end',
onNextClick: () => {
if (this.nextPagePath) {
window.Turbo.visit(this.nextPagePath)
} else {
this.destroy()
}
}
}
}
])
this.driverObj.drive()
})
}
}
async disableAppGuide () {
return fetch('/user_configs', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({ key: 'show_app_tour', value: false })
})
}
}
+1 -1
View File
@@ -35,7 +35,7 @@ export default actionable(targetable(class extends HTMLElement {
} }
toggleLoading = (e) => { toggleLoading = (e) => {
if (e && e.target && !e.target.contains(this)) { if (e && e.target && (!e.target.contains(this) || !e.detail?.formSubmission?.formElement?.contains(this))) {
return return
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

@@ -1,4 +1,4 @@
function cropCanvasAndExportToPNG (canvas, { errorOnTooSmall } = { errorOnTooSmall: false }) { function cropCanvasAndExportToPNG (canvas) {
const ctx = canvas.getContext('2d') const ctx = canvas.getContext('2d')
const width = canvas.width const width = canvas.width
@@ -33,10 +33,6 @@ function cropCanvasAndExportToPNG (canvas, { errorOnTooSmall } = { errorOnTooSma
croppedCanvas.height = croppedHeight croppedCanvas.height = croppedHeight
const croppedCtx = croppedCanvas.getContext('2d') const croppedCtx = croppedCanvas.getContext('2d')
if (errorOnTooSmall && (croppedWidth < 20 || croppedHeight < 20)) {
return Promise.reject(new Error('Image too small'))
}
croppedCtx.drawImage(canvas, leftmost, topmost, croppedWidth, croppedHeight, 0, 0, croppedWidth, croppedHeight) croppedCtx.drawImage(canvas, leftmost, topmost, croppedWidth, croppedHeight, 0, 0, croppedWidth, croppedHeight)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
+16 -6
View File
@@ -1404,7 +1404,21 @@ export default {
if (response.status === 422 || response.status === 500) { if (response.status === 422 || response.status === 500) {
const data = await response.json() const data = await response.json()
if (data.error) { if (data.field_uuid) {
const field = this.fieldsUuidIndex[data.field_uuid]
if (field) {
const step = this.stepFields.findIndex((fields) => fields.includes(field))
if (step !== -1) {
this.goToStep(step, this.autoscrollFields)
this.showFillAllRequiredFields = true
}
}
return Promise.reject(new Error('Required field: ' + data.field_uuid))
} else if (data.error) {
const i18nKey = data.error.replace(/\s+/g, '_').toLowerCase() const i18nKey = data.error.replace(/\s+/g, '_').toLowerCase()
alert(this.t(i18nKey) !== i18nKey ? this.t(i18nKey) : data.error) alert(this.t(i18nKey) !== i18nKey ? this.t(i18nKey) : data.error)
@@ -1439,11 +1453,7 @@ export default {
this.isSubmittingComplete = false this.isSubmittingComplete = false
}) })
}).catch(error => { }).catch(error => {
if (error?.message === 'Image too small') { console.log(error)
alert(this.t('signature_is_too_small_please_redraw'))
} else {
console.log(error)
}
}).finally(() => { }).finally(() => {
this.isSubmitting = false this.isSubmitting = false
this.isSubmittingComplete = false this.isSubmittingComplete = false
+13 -13
View File
@@ -93,7 +93,7 @@ const en = {
reupload: 'Reupload', reupload: 'Reupload',
upload: 'Upload', upload: 'Upload',
files: 'Files', files: 'Files',
signature_is_too_small_please_redraw: 'Signature is too small. Please redraw.', signature_is_too_small_or_simple_please_redraw: 'Signature is too small or simple. Please redraw.',
wait_countdown_seconds: 'Wait {countdown} seconds' wait_countdown_seconds: 'Wait {countdown} seconds'
} }
@@ -191,7 +191,7 @@ const es = {
reupload: 'Volver a subir', reupload: 'Volver a subir',
upload: 'Subir', upload: 'Subir',
files: 'Archivos', files: 'Archivos',
signature_is_too_small_please_redraw: 'La firma es demasiado pequeña. Por favor, dibújala de nuevo.', signature_is_too_small_or_simple_please_redraw: 'La firma es demasiado pequeña o simple. Por favor, vuelve a dibujarla.',
wait_countdown_seconds: 'Espera {countdown} segundos' wait_countdown_seconds: 'Espera {countdown} segundos'
} }
@@ -289,7 +289,7 @@ const it = {
reupload: 'Ricarica', reupload: 'Ricarica',
upload: 'Carica', upload: 'Carica',
files: 'File', files: 'File',
signature_is_too_small_please_redraw: 'La firma è troppo piccola. Ridisegnala per favore.', signature_is_too_small_or_simple_please_redraw: 'La firma è troppo piccola o semplice. Ridisegnala, per favore.',
wait_countdown_seconds: 'Attendi {countdown} secondi' wait_countdown_seconds: 'Attendi {countdown} secondi'
} }
@@ -387,7 +387,7 @@ const de = {
reupload: 'Erneut hochladen', reupload: 'Erneut hochladen',
upload: 'Hochladen', upload: 'Hochladen',
files: 'Dateien', files: 'Dateien',
signature_is_too_small_please_redraw: 'Die Unterschrift ist zu klein. Bitte erneut zeichnen.', signature_is_too_small_or_simple_please_redraw: 'Die Unterschrift ist zu klein oder zu einfach. Bitte erneut zeichnen.',
wait_countdown_seconds: 'Warte {countdown} Sekunden' wait_countdown_seconds: 'Warte {countdown} Sekunden'
} }
@@ -485,7 +485,7 @@ const fr = {
reupload: 'Recharger', reupload: 'Recharger',
upload: 'Télécharger', upload: 'Télécharger',
files: 'Fichiers', files: 'Fichiers',
signature_is_too_small_please_redraw: 'La signature est trop petite. Veuillez la redessiner.', signature_is_too_small_or_simple_please_redraw: 'La signature est trop petite ou trop simple. Veuillez la redessiner.',
wait_countdown_seconds: 'Attendez {countdown} secondes' wait_countdown_seconds: 'Attendez {countdown} secondes'
} }
@@ -583,7 +583,7 @@ const pl = {
reupload: 'Ponowne przesłanie', reupload: 'Ponowne przesłanie',
upload: 'Przesyłanie', upload: 'Przesyłanie',
files: 'Pliki', files: 'Pliki',
signature_is_too_small_please_redraw: 'Podpis jest zbyt mały. Proszę narysować go ponownie.' signature_is_too_small_or_simple_please_redraw: 'Podpis jest zbyt mały lub zbyt prosty. Proszę narysować go ponownie.'
} }
const uk = { const uk = {
@@ -680,7 +680,7 @@ const uk = {
reupload: 'Перезавантажити', reupload: 'Перезавантажити',
upload: 'Завантажити', upload: 'Завантажити',
files: 'Файли', files: 'Файли',
signature_is_too_small_please_redraw: 'Підпис занадто малий. Будь ласка, перемалюйте його.', signature_is_too_small_or_simple_please_redraw: 'Підпис занадто маленький або надто простий. Будь ласка, перемалюйте.',
wait_countdown_seconds: 'Зачекайте {countdown} секунд' wait_countdown_seconds: 'Зачекайте {countdown} секунд'
} }
@@ -778,7 +778,7 @@ const cs = {
reupload: 'Znovu nahrát', reupload: 'Znovu nahrát',
upload: 'Nahrát', upload: 'Nahrát',
files: 'Soubory', files: 'Soubory',
signature_is_too_small_please_redraw: 'Podpis je příliš malý. Prosím, překreslete ho.', signature_is_too_small_or_simple_please_redraw: 'Podpis je příliš malý nebo jednoduchý. Nakreslete jej prosím znovu.',
wait_countdown_seconds: 'Počkejte {countdown} sekund' wait_countdown_seconds: 'Počkejte {countdown} sekund'
} }
@@ -876,7 +876,7 @@ const pt = {
reupload: 'Reenviar', reupload: 'Reenviar',
upload: 'Carregar', upload: 'Carregar',
files: 'Arquivos', files: 'Arquivos',
signature_is_too_small_please_redraw: 'A assinatura é muito pequena. Por favor, redesenhe-a.', signature_is_too_small_or_simple_please_redraw: 'A assinatura é muito pequena ou simples. Por favor, redesenhe.',
wait_countdown_seconds: 'Aguarde {countdown} segundos' wait_countdown_seconds: 'Aguarde {countdown} segundos'
} }
@@ -975,7 +975,7 @@ const he = {
reupload: 'העלה שוב', reupload: 'העלה שוב',
upload: 'העלאה', upload: 'העלאה',
files: 'קבצים', files: 'קבצים',
signature_is_too_small_please_redraw: 'החתימה קטנה מדי. אנא צייר מחדש.', signature_is_too_small_or_simple_please_redraw: 'החתימה קטנה או פשוטה מדי. אנא חתום מחדש.',
wait_countdown_seconds: 'המתן {countdown} שניות' wait_countdown_seconds: 'המתן {countdown} שניות'
} }
@@ -1074,7 +1074,7 @@ const nl = {
reupload: 'Opnieuw uploaden', reupload: 'Opnieuw uploaden',
upload: 'Uploaden', upload: 'Uploaden',
files: 'Bestanden', files: 'Bestanden',
signature_is_too_small_please_redraw: 'De handtekening is te klein. Teken deze opnieuw, alstublieft.', signature_is_too_small_or_simple_please_redraw: 'De handtekening is te klein of te eenvoudig. Teken opnieuw.',
wait_countdown_seconds: 'Wacht {countdown} seconden' wait_countdown_seconds: 'Wacht {countdown} seconden'
} }
@@ -1172,7 +1172,7 @@ const ar = {
reupload: 'إعادة التحميل', reupload: 'إعادة التحميل',
upload: 'تحميل', upload: 'تحميل',
files: 'الملفات', files: 'الملفات',
signature_is_too_small_please_redraw: 'التوقيع صغير جدًا. يرجى إعادة الرسم.', signature_is_too_small_or_simple_please_redraw: 'التوقيع صغير جدًا أو بسيط جدًا. يرجى إعادة رسمه.',
wait_countdown_seconds: 'انتظر {countdown} ثانية' wait_countdown_seconds: 'انتظر {countdown} ثانية'
} }
@@ -1269,7 +1269,7 @@ const ko = {
reupload: '다시 업로드', reupload: '다시 업로드',
upload: '업로드', upload: '업로드',
files: '파일', files: '파일',
signature_is_too_small_please_redraw: '서명이 너무 작습니다. 다시 그려주세요.', signature_is_too_small_or_simple_please_redraw: '서명이 너무 작거나 단순합니다. 다시 그려주세요.',
wait_countdown_seconds: '{countdown}초 기다리세요' wait_countdown_seconds: '{countdown}초 기다리세요'
} }
@@ -118,11 +118,17 @@
:src="attachmentsIndex[modelValue || computedPreviousValue].url" :src="attachmentsIndex[modelValue || computedPreviousValue].url"
class="mx-auto bg-white border border-base-300 rounded max-h-44" class="mx-auto bg-white border border-base-300 rounded max-h-44"
> >
<canvas <div class="relative">
v-show="!modelValue && !computedPreviousValue" <div
ref="canvas" v-if="!isDrawInitials"
class="bg-white border border-base-300 rounded-2xl w-full draw-canvas" class="absolute top-0 right-0 left-0 bottom-0"
/> />
<canvas
v-show="!modelValue && !computedPreviousValue"
ref="canvas"
class="bg-white border border-base-300 rounded-2xl w-full draw-canvas"
/>
</div>
<input <input
v-if="!isDrawInitials && !modelValue && !computedPreviousValue" v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
id="initials_text_input" id="initials_text_input"
@@ -292,6 +292,7 @@
<script> <script>
import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2, IconQrcode, IconX } from '@tabler/icons-vue' import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2, IconQrcode, IconX } from '@tabler/icons-vue'
import { cropCanvasAndExportToPNG } from './crop_canvas' import { cropCanvasAndExportToPNG } from './crop_canvas'
import { isValidSignatureCanvas } from './validate_signature'
import SignaturePad from 'signature_pad' import SignaturePad from 'signature_pad'
import AppearsOn from './appears_on' import AppearsOn from './appears_on'
import FileDropzone from './dropzone' import FileDropzone from './dropzone'
@@ -598,6 +599,7 @@ export default {
}, },
drawImage (event) { drawImage (event) {
this.remove() this.remove()
this.clear()
this.isSignatureStarted = true this.isSignatureStarted = true
this.drawOnCanvas(event.target.files[0], this.$refs.canvas) this.drawOnCanvas(event.target.files[0], this.$refs.canvas)
@@ -680,8 +682,14 @@ export default {
return Promise.resolve({}) return Promise.resolve({})
} }
return new Promise((resolve, reject) => { if (this.isSignatureStarted && this.pad.toData().length > 0 && !isValidSignatureCanvas(this.pad.toData())) {
cropCanvasAndExportToPNG(this.$refs.canvas, { errorOnTooSmall: true }).then(async (blob) => { alert(this.t('signature_is_too_small_or_simple_please_redraw'))
return Promise.reject(new Error('Image too small or simple'))
}
return new Promise((resolve) => {
cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => {
const file = new File([blob], 'signature.png', { type: 'image/png' }) const file = new File([blob], 'signature.png', { type: 'image/png' })
if (this.dryRun) { if (this.dryRun) {
@@ -717,12 +725,6 @@ export default {
return resolve(attachment) return resolve(attachment)
}) })
} }
}).catch((error) => {
if (error.message === 'Image too small' && this.field.required === false) {
return resolve({})
} else {
return reject(error)
}
}) })
}) })
} }
@@ -0,0 +1,38 @@
function isValidSignatureCanvas (data) {
if (data.length === 0) return false
const strokes = data.filter(stroke => Array.isArray(stroke.points) && stroke.points.length > 2)
if (strokes.length === 0) return false
let skippedStraightLine = 0
const validStrokes = strokes.filter(stroke => {
const points = stroke.points
const first = points[0]
const last = points[points.length - 1]
const A = last.y - first.y
const B = first.x - last.x
const C = last.x * first.y - first.x * last.y
const lineLength = Math.sqrt(A * A + B * B)
const totalDeviation = points.reduce((sum, p) => {
const distanceToLine = Math.abs(A * p.x + B * p.y + C) / lineLength
return sum + distanceToLine
}, 0)
const avgDeviation = totalDeviation / points.length
if (avgDeviation < 3 && skippedStraightLine < 2) {
skippedStraightLine++
return false
}
return true
})
return validStrokes.length > 0
}
export { isValidSignatureCanvas }
+113 -57
View File
@@ -1,9 +1,18 @@
<template> <template>
<div <div
ref="dragContainer"
style="max-width: 1600px" style="max-width: 1600px"
class="mx-auto pl-3 h-full" class="mx-auto pl-3 h-full"
:class="isMobile ? 'pl-4' : 'md:pl-4'" :class="isMobile ? 'pl-4' : 'md:pl-4'"
@dragover="onDragover"
> >
<DragPlaceholder
ref="dragPlaceholder"
:field="fieldsDragFieldRef.value || toRaw(dragField)"
:is-field="template.fields.includes(fieldsDragFieldRef.value)"
:is-default="defaultFields.includes(toRaw(dragField))"
:is-required="defaultRequiredFields.includes(toRaw(dragField))"
/>
<div <div
v-if="pendingFieldAttachmentUuids.length && editable" v-if="pendingFieldAttachmentUuids.length && editable"
class="top-1.5 sticky h-0 z-20 max-w-2xl mx-auto" class="top-1.5 sticky h-0 z-20 max-w-2xl mx-auto"
@@ -60,6 +69,7 @@
<template v-else> <template v-else>
<a <a
v-if="withSignYourselfButton" v-if="withSignYourselfButton"
id="sign_yourself_button"
:href="template.submitters.length > 1 ? `/templates/${template.id}/submissions/new?selfsign=true` : `/d/${template.slug}`" :href="template.submitters.length > 1 ? `/templates/${template.id}/submissions/new?selfsign=true` : `/d/${template.slug}`"
class="btn btn-primary btn-ghost text-base hidden md:flex" class="btn btn-primary btn-ghost text-base hidden md:flex"
:target="template.submitters.length > 1 ? '' : '_blank'" :target="template.submitters.length > 1 ? '' : '_blank'"
@@ -76,6 +86,7 @@
</a> </a>
<a <a
v-if="withSendButton" v-if="withSendButton"
id="send_button"
:href="`/templates/${template.id}/submissions/new?with_link=true`" :href="`/templates/${template.id}/submissions/new?with_link=true`"
data-turbo-frame="modal" data-turbo-frame="modal"
class="white-button md:!px-6" class="white-button md:!px-6"
@@ -91,6 +102,7 @@
</a> </a>
<span <span
v-if="editable" v-if="editable"
id="save_button_container"
class="flex" class="flex"
> >
<button <button
@@ -269,6 +281,7 @@
:allow-draw="!onlyDefinedFields || drawField" :allow-draw="!onlyDefinedFields || drawField"
:data-document-uuid="document.uuid" :data-document-uuid="document.uuid"
:default-submitters="defaultSubmitters" :default-submitters="defaultSubmitters"
:drag-field-placeholder="fieldsDragFieldRef.value || dragField"
:with-field-placeholder="withFieldPlaceholder" :with-field-placeholder="withFieldPlaceholder"
:draw-field="drawField" :draw-field="drawField"
:draw-field-type="drawFieldType" :draw-field-type="drawFieldType"
@@ -374,12 +387,14 @@
:with-sticky-submitters="withStickySubmitters" :with-sticky-submitters="withStickySubmitters"
:only-defined-fields="onlyDefinedFields" :only-defined-fields="onlyDefinedFields"
:editable="editable" :editable="editable"
:show-tour-start-form="showTourStartForm"
@add-field="addField" @add-field="addField"
@set-draw="[drawField = $event.field, drawOption = $event.option]" @set-draw="[drawField = $event.field, drawOption = $event.option]"
@set-draw-type="[drawFieldType = $event, showDrawField = true]" @set-draw-type="[drawFieldType = $event, showDrawField = true]"
@set-drag="dragField = $event" @set-drag="dragField = $event"
@set-drag-placeholder="$refs.dragPlaceholder.dragPlaceholder = $event"
@change-submitter="selectedSubmitter = $event" @change-submitter="selectedSubmitter = $event"
@drag-end="dragField = null" @drag-end="[dragField = null, $refs.dragPlaceholder.dragPlaceholder = null]"
@scroll-to-area="scrollToArea" @scroll-to-area="scrollToArea"
/> />
</div> </div>
@@ -418,6 +433,7 @@
<script> <script>
import Upload from './upload' import Upload from './upload'
import Dropzone from './dropzone' import Dropzone from './dropzone'
import DragPlaceholder from './drag_placeholder'
import Fields from './fields' import Fields from './fields'
import MobileDrawField from './mobile_draw_field' import MobileDrawField from './mobile_draw_field'
import Document from './document' import Document from './document'
@@ -429,13 +445,14 @@ import MobileFields from './mobile_fields'
import FieldSubmitter from './field_submitter' import FieldSubmitter from './field_submitter'
import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments } from '@tabler/icons-vue' import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments } from '@tabler/icons-vue'
import { v4 } from 'uuid' import { v4 } from 'uuid'
import { ref, computed } from 'vue' import { ref, computed, toRaw } from 'vue'
import * as i18n from './i18n' import * as i18n from './i18n'
export default { export default {
name: 'TemplateBuilder', name: 'TemplateBuilder',
components: { components: {
Upload, Upload,
DragPlaceholder,
Document, Document,
Fields, Fields,
IconInfoCircle, IconInfoCircle,
@@ -460,6 +477,7 @@ export default {
template: this.template, template: this.template,
save: this.save, save: this.save,
t: this.t, t: this.t,
assignDropAreaSize: this.assignDropAreaSize,
currencies: this.currencies, currencies: this.currencies,
locale: this.locale, locale: this.locale,
baseFetch: this.baseFetch, baseFetch: this.baseFetch,
@@ -686,6 +704,11 @@ export default {
type: Object, type: Object,
required: false, required: false,
default: () => ({ headers: {} }) default: () => ({ headers: {} })
},
showTourStartForm: {
type: Boolean,
required: false,
default: false
} }
}, },
data () { data () {
@@ -836,6 +859,17 @@ export default {
this.documentRefs = [] this.documentRefs = []
}, },
methods: { methods: {
toRaw,
onDragover (e) {
if (this.$refs.dragPlaceholder?.dragPlaceholder) {
this.$refs.dragPlaceholder.isMask = e.target.id === 'mask'
const ref = this.$refs.dragPlaceholder.dragPlaceholder
ref.x = e.clientX - ref.offsetX
ref.y = e.clientY - ref.offsetY
}
},
reorderFields (item) { reorderFields (item) {
const itemFields = [] const itemFields = []
const fields = [] const fields = []
@@ -1319,6 +1353,10 @@ export default {
} }
}, },
onDropfield (area) { onDropfield (area) {
if (this.$refs.dragPlaceholder) {
this.$refs.dragPlaceholder.dragPlaceholder = null
}
if (!this.editable) { if (!this.editable) {
return return
} }
@@ -1331,6 +1369,10 @@ export default {
...this.dragField ...this.dragField
} }
if (!field.type) {
field.type = 'text'
}
if (!this.fieldsDragFieldRef.value) { if (!this.fieldsDragFieldRef.value) {
if (['select', 'multiple', 'radio'].includes(field.type)) { if (['select', 'multiple', 'radio'].includes(field.type)) {
if (this.dragField?.options?.length) { if (this.dragField?.options?.length) {
@@ -1357,70 +1399,18 @@ export default {
attachment_uuid: area.attachment_uuid attachment_uuid: area.attachment_uuid
} }
const previousField = [...this.template.fields].reverse().find((f) => f.type === field.type) this.assignDropAreaSize(fieldArea, field, area)
let baseArea
if (this.selectedField?.type === field.type) {
baseArea = this.selectedAreaRef.value
} else if (previousField?.areas?.length) {
baseArea = previousField.areas[previousField.areas.length - 1]
} else {
if (['checkbox'].includes(field.type)) {
baseArea = {
w: area.maskW / 30 / area.maskW,
h: area.maskW / 30 / area.maskW * (area.maskW / area.maskH)
}
} else if (field.type === 'image') {
baseArea = {
w: area.maskW / 5 / area.maskW,
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH)
}
} else if (field.type === 'signature' || field.type === 'stamp' || field.type === 'verification') {
baseArea = {
w: area.maskW / 5 / area.maskW,
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2
}
} else if (field.type === 'initials') {
baseArea = {
w: area.maskW / 10 / area.maskW,
h: area.maskW / 35 / area.maskW
}
} else {
baseArea = {
w: area.maskW / 5 / area.maskW,
h: area.maskW / 35 / area.maskW
}
}
}
fieldArea.w = baseArea.w
fieldArea.h = baseArea.h
fieldArea.y = fieldArea.y - baseArea.h / 2
if (field.type === 'cells') {
fieldArea.cell_w = baseArea.cell_w || (baseArea.w / 5)
}
field.areas ||= []
const lastArea = field.areas[field.areas.length - 1]
if (lastArea) {
fieldArea.w = lastArea.w
fieldArea.h = lastArea.h
}
if (field.width) { if (field.width) {
fieldArea.w = field.width / area.maskW
delete field.width delete field.width
} }
if (field.height) { if (field.height) {
fieldArea.h = field.height / area.maskH
delete field.height delete field.height
} }
field.areas ||= []
field.areas.push(fieldArea) field.areas.push(fieldArea)
this.selectedAreaRef.value = fieldArea this.selectedAreaRef.value = fieldArea
@@ -1444,6 +1434,72 @@ export default {
}) })
} }
}, },
assignDropAreaSize (fieldArea, field, area) {
const fieldType = field.type || 'text'
const previousField = [...this.template.fields].reverse().find((f) => f.type === fieldType)
let baseArea
if (this.selectedField?.type === fieldType) {
baseArea = this.selectedAreaRef.value
} else if (previousField?.areas?.length) {
baseArea = previousField.areas[previousField.areas.length - 1]
} else {
if (['checkbox'].includes(fieldType)) {
baseArea = {
w: area.maskW / 30 / area.maskW,
h: area.maskW / 30 / area.maskW * (area.maskW / area.maskH)
}
} else if (fieldType === 'image') {
baseArea = {
w: area.maskW / 5 / area.maskW,
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH)
}
} else if (fieldType === 'signature' || fieldType === 'stamp' || fieldType === 'verification') {
baseArea = {
w: area.maskW / 5 / area.maskW,
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2
}
} else if (fieldType === 'initials') {
baseArea = {
w: area.maskW / 10 / area.maskW,
h: area.maskW / 35 / area.maskW
}
} else {
baseArea = {
w: area.maskW / 5 / area.maskW,
h: area.maskW / 35 / area.maskW
}
}
}
fieldArea.w = baseArea.w
fieldArea.h = baseArea.h
if (fieldType === 'cells') {
fieldArea.cell_w = baseArea.cell_w || (baseArea.w / 5)
}
if (field.areas?.length) {
const lastArea = field.areas[field.areas.length - 1]
if (lastArea) {
fieldArea.w = lastArea.w
fieldArea.h = lastArea.h
}
}
if (field.width) {
fieldArea.w = field.width / area.maskW
}
if (field.height) {
fieldArea.h = field.height / area.maskH
}
fieldArea.y = fieldArea.y - fieldArea.h / 2
},
addBlankPage () { addBlankPage () {
this.isLoadingBlankPage = true this.isLoadingBlankPage = true
@@ -13,6 +13,7 @@
:is-drag="isDrag" :is-drag="isDrag"
:with-field-placeholder="withFieldPlaceholder" :with-field-placeholder="withFieldPlaceholder"
:default-fields="defaultFields" :default-fields="defaultFields"
:drag-field-placeholder="dragFieldPlaceholder"
:default-submitters="defaultSubmitters" :default-submitters="defaultSubmitters"
:draw-field="drawField" :draw-field="drawField"
:draw-field-type="drawFieldType" :draw-field-type="drawFieldType"
@@ -39,6 +40,11 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
dragFieldPlaceholder: {
type: Object,
required: false,
default: null
},
inputMode: { inputMode: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -0,0 +1,115 @@
<template>
<Field
v-if="dragPlaceholder && isField && !isMask && field"
ref="dragPlaceholder"
:style="dragPlaceholderStyle"
:field="field"
:with-options="false"
class="fixed z-20 pointer-events-none"
:editable="false"
/>
<div
v-else-if="dragPlaceholder && (isDefault || isRequired) && !isMask && field"
ref="dragPlaceholder"
:style="[dragPlaceholderStyle, { backgroundColor: backgroundColor }]"
class="fixed z-20 border border-base-300 rounded group default-field fields-list-item pointer-events-none"
>
<div class="flex items-center justify-between relative cursor-grab">
<div class="flex items-center p-1 space-x-1">
<IconDrag />
<component
:is="fieldIcons[field.type || 'text']"
:stroke-width="1.6"
:width="20"
/>
<span class="block pl-0.5">
{{ field.title || field.name }}
</span>
</div>
<span
v-if="isRequired"
:data-tip="t('required')"
class="text-red-400 text-3xl pr-1.5 tooltip tooltip-left h-8"
>
*
</span>
</div>
</div>
<button
v-else-if="dragPlaceholder && !isMask && field"
ref="dragPlaceholder"
class="fixed field-type-button z-20 flex items-center justify-center border border-dashed w-full rounded border-base-content/20 opacity-90 pointer-events-none"
:style="[dragPlaceholderStyle, { backgroundColor }]"
>
<div
class="flex items-console cursor-grab h-full absolute left-0 bg-base-200/50"
>
<IconDrag class="my-auto" />
</div>
<div class="flex items-center flex-col px-2 py-2">
<component :is="fieldIcons[field.type || 'text']" />
<span class="text-xs mt-1">
{{ fieldNames[field.type || 'text'] }}
</span>
</div>
</button>
</template>
<script>
import Field from './field'
import IconDrag from './icon_drag'
import FieldType from './field_type'
export default {
name: 'DragPlaceholder',
components: {
Field,
IconDrag
},
inject: ['t', 'backgroundColor'],
props: {
field: {
type: Object,
required: false,
default: null
},
isDefault: {
type: Boolean,
required: false,
default: false
},
isRequired: {
type: Boolean,
required: false,
default: false
},
isField: {
type: Boolean,
required: false,
default: false
}
},
data () {
return {
isMask: false,
dragPlaceholder: null
}
},
computed: {
dragPlaceholderStyle () {
if (this.dragPlaceholder) {
return {
left: this.dragPlaceholder.x + 'px',
top: this.dragPlaceholder.y + 'px',
width: this.dragPlaceholder.w + 'px',
height: this.dragPlaceholder.h + 'px'
}
} else {
return {}
}
},
fieldNames: FieldType.computed.fieldNames,
fieldIcons: FieldType.computed.fieldIcons
}
}
</script>
+7 -2
View File
@@ -3,7 +3,7 @@
class="list-field group mb-2" class="list-field group mb-2"
> >
<div <div
class="border border-base-300 rounded rounded-tr-none relative group fields-list-item" class="border border-base-300 rounded relative group fields-list-item"
:style="{ backgroundColor: backgroundColor }" :style="{ backgroundColor: backgroundColor }"
> >
<div class="flex items-center justify-between relative group/contenteditable-container"> <div class="flex items-center justify-between relative group/contenteditable-container">
@@ -149,7 +149,7 @@
</div> </div>
</div> </div>
<div <div
v-if="field.options" v-if="field.options && withOptions"
ref="options" ref="options"
class="border-t border-base-300 mx-2 pt-2 space-y-1.5" class="border-t border-base-300 mx-2 pt-2 space-y-1.5"
draggable="true" draggable="true"
@@ -302,6 +302,11 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
withOptions: {
type: Boolean,
required: false,
default: true
},
defaultField: { defaultField: {
type: Object, type: Object,
required: false, required: false,
+77 -19
View File
@@ -25,11 +25,11 @@
:data-uuid="field.uuid" :data-uuid="field.uuid"
: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"
:default-field="defaultFieldsIndex[field.name]" :default-field="defaultFieldsIndex[field.name]"
:draggable="editable" :draggable="editable"
@dragstart="fieldsDragFieldRef.value = field" @dragstart="[fieldsDragFieldRef.value = field, removeDragOverlay($event), setDragPlaceholder($event)]"
@dragend="fieldsDragFieldRef.value = null" @dragend="[fieldsDragFieldRef.value = null, $emit('set-drag-placeholder', null)]"
@remove="removeField" @remove="removeField"
@scroll-to="$emit('scroll-to-area', $event)" @scroll-to="$emit('scroll-to-area', $event)"
@set-draw="$emit('set-draw', $event)" @set-draw="$emit('set-draw', $event)"
@@ -74,8 +74,8 @@
<div <div
:style="{ backgroundColor }" :style="{ backgroundColor }"
draggable="true" draggable="true"
class="border border-base-300 rounded rounded-tr-none relative group mb-2 default-field fields-list-item" class="border border-base-300 rounded relative group mb-2 default-field fields-list-item"
@dragstart="onDragstart({ type: 'text', ...field })" @dragstart="onDragstart($event, field)"
@dragend="$emit('drag-end')" @dragend="$emit('drag-end')"
> >
<div class="flex items-center justify-between relative cursor-grab"> <div class="flex items-center justify-between relative cursor-grab">
@@ -104,6 +104,7 @@
</div> </div>
<div <div
v-if="editable && !onlyDefinedFields" v-if="editable && !onlyDefinedFields"
id="field-types-grid"
class="grid grid-cols-3 gap-1 pb-2 fields-grid" class="grid grid-cols-3 gap-1 pb-2 fields-grid"
> >
<template <template
@@ -112,11 +113,12 @@
> >
<button <button
v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification')" v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification')"
:id="`${type}_type_field_button`"
draggable="true" draggable="true"
class="field-type-button group flex items-center justify-center border border-dashed w-full rounded relative fields-grid-item" class="field-type-button group flex items-center justify-center border border-dashed w-full rounded relative fields-grid-item"
:style="{ backgroundColor }" :style="{ backgroundColor }"
:class="drawFieldType === type ? 'border-base-content/40' : 'border-base-300 hover:border-base-content/20'" :class="drawFieldType === type ? 'border-base-content/40' : 'border-base-300 hover:border-base-content/20'"
@dragstart="onDragstart({ type: type })" @dragstart="onDragstart($event, { type: type })"
@dragend="$emit('drag-end')" @dragend="$emit('drag-end')"
@click="['file', 'payment', 'verification'].includes(type) ? $emit('add-field', type) : $emit('set-draw-type', type)" @click="['file', 'payment', 'verification'].includes(type) ? $emit('add-field', type) : $emit('set-draw-type', type)"
> >
@@ -189,7 +191,7 @@
</template> </template>
</div> </div>
<div <div
v-if="fields.length < 4 && editable && withHelp" v-if="fields.length < 4 && editable && withHelp && !showTourStartForm"
class="text-xs p-2 border border-base-200 rounded" class="text-xs p-2 border border-base-200 rounded"
> >
<ul class="list-disc list-outside ml-3"> <ul class="list-disc list-outside ml-3">
@@ -204,6 +206,23 @@
</li> </li>
</ul> </ul>
</div> </div>
<div
v-show="fields.length < 4 && editable && withHelp && showTourStartForm"
class="rounded py-2 px-4 w-full border border-dashed border-base-300"
>
<div class="text-center text-sm">
{{ t('start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document') }}
</div>
<div class="flex justify-center">
<label
for="start_tour_button"
class="btn btn-sm btn-warning w-40 mt-2"
@click="startTour"
>
{{ t('start_tour') }}
</label>
</div>
</div>
</template> </template>
<script> <script>
@@ -290,9 +309,14 @@ export default {
selectedSubmitter: { selectedSubmitter: {
type: Object, type: Object,
required: true required: true
},
showTourStartForm: {
type: Boolean,
required: false,
default: false
} }
}, },
emits: ['add-field', 'set-draw', 'set-draw-type', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter'], emits: ['add-field', 'set-draw', 'set-draw-type', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter', 'set-drag-placeholder'],
data () { data () {
return { return {
defaultFieldsSearch: '' defaultFieldsSearch: ''
@@ -343,22 +367,56 @@ export default {
} }
}, },
methods: { methods: {
onDragstart (field) { onDragstart (event, field) {
this.removeDragOverlay(event)
this.setDragPlaceholder(event)
this.$emit('set-drag', field) this.$emit('set-drag', field)
}, },
setDragPlaceholder (event) {
this.$emit('set-drag-placeholder', {
offsetX: event.offsetX,
offsetY: event.offsetY,
x: event.clientX - event.offsetX,
y: event.clientY - event.offsetY,
w: event.currentTarget.clientWidth + 2,
h: event.currentTarget.clientHeight + 2
})
},
removeDragOverlay (event) {
const root = this.$el.getRootNode()
const hiddenEl = document.createElement('div')
hiddenEl.style.width = '1px'
hiddenEl.style.height = '1px'
hiddenEl.style.opacity = '0'
hiddenEl.style.position = 'fixed'
root.querySelector('#docuseal_modal_container').appendChild(hiddenEl)
event.dataTransfer.setDragImage(hiddenEl, 0, 0)
setTimeout(() => { hiddenEl.remove() }, 1000)
},
startTour () {
document.querySelector('app-tour').start()
},
onFieldDragover (e) { onFieldDragover (e) {
const targetField = e.target.closest('[data-uuid]') if (this.fieldsDragFieldRef.value) {
const dragField = this.$refs.fields.querySelector(`[data-uuid="${this.fieldsDragFieldRef.value.uuid}"]`) const targetField = e.target.closest('[data-uuid]')
const dragField = this.$refs.fields.querySelector(`[data-uuid="${this.fieldsDragFieldRef.value.uuid}"]`)
if (dragField && targetField && targetField !== dragField) { if (dragField && targetField && targetField !== dragField) {
const fields = Array.from(this.$refs.fields.children) const fields = Array.from(this.$refs.fields.children)
const currentIndex = fields.indexOf(dragField) const currentIndex = fields.indexOf(dragField)
const targetIndex = fields.indexOf(targetField) const targetIndex = fields.indexOf(targetField)
if (currentIndex < targetIndex) { if (currentIndex < targetIndex) {
targetField.after(dragField) targetField.after(dragField)
} else { } else {
targetField.before(dragField) targetField.before(dragField)
}
} }
} }
}, },
@@ -143,7 +143,7 @@
> >
<span <span
contenteditable="true" contenteditable="true"
class="outline-none" class="outline-none whitespace-nowrap truncate"
> >
{{ field.default_value || field.name || buildDefaultName(field, template.fields) }} {{ field.default_value || field.name || buildDefaultName(field, template.fields) }}
</span> </span>
+18 -6
View File
@@ -158,7 +158,9 @@ const en = {
some_fields_are_missing_in_the_formula: 'Some fields are missing in the formula.', some_fields_are_missing_in_the_formula: 'Some fields are missing in the formula.',
learn_more: 'Learn more', learn_more: 'Learn more',
and: 'and', and: 'and',
or: 'or' or: 'or',
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Start a quick tour to learn how to create an send your first document',
start_tour: 'Start Tour'
} }
const es = { const es = {
@@ -321,7 +323,9 @@ const es = {
some_fields_are_missing_in_the_formula: 'Faltan algunos campos en la fórmula.', some_fields_are_missing_in_the_formula: 'Faltan algunos campos en la fórmula.',
learn_more: 'Aprende más', learn_more: 'Aprende más',
and: 'y', and: 'y',
or: 'o' or: 'o',
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Inicia una guía rápida para aprender a crear y enviar tu primer documento.',
start_tour: 'Iniciar guía'
} }
const it = { const it = {
@@ -484,7 +488,9 @@ const it = {
some_fields_are_missing_in_the_formula: 'Alcuni campi mancano nella formula.', some_fields_are_missing_in_the_formula: 'Alcuni campi mancano nella formula.',
learn_more: 'Scopri di più', learn_more: 'Scopri di più',
and: 'e', and: 'e',
or: 'o' or: 'o',
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Inizia un tour rapido per imparare a creare e inviare il tuo primo documento.',
start_tour: 'Inizia il tour'
} }
const pt = { const pt = {
@@ -647,7 +653,9 @@ const pt = {
some_fields_are_missing_in_the_formula: 'Faltam alguns campos na fórmula.', some_fields_are_missing_in_the_formula: 'Faltam alguns campos na fórmula.',
learn_more: 'Saiba mais', learn_more: 'Saiba mais',
and: 'e', and: 'e',
or: 'ou' or: 'ou',
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Comece um tour rápido para aprender a criar e enviar seu primeiro documento.',
start_tour: 'Iniciar tour'
} }
const fr = { const fr = {
@@ -810,7 +818,9 @@ const fr = {
some_fields_are_missing_in_the_formula: 'Certains champs manquent dans la formule.', some_fields_are_missing_in_the_formula: 'Certains champs manquent dans la formule.',
learn_more: 'En savoir plus', learn_more: 'En savoir plus',
and: 'et', and: 'et',
or: 'ou' or: 'ou',
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Lancez une visite rapide pour apprendre à créer et envoyer votre premier document.',
start_tour: 'Démarrer'
} }
const de = { const de = {
@@ -973,7 +983,9 @@ const de = {
some_fields_are_missing_in_the_formula: 'Einige Felder fehlen in der Formel.', some_fields_are_missing_in_the_formula: 'Einige Felder fehlen in der Formel.',
learn_more: 'Erfahren Sie mehr', learn_more: 'Erfahren Sie mehr',
and: 'und', and: 'und',
or: 'oder' or: 'oder',
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Starte eine kurze Tour, um zu lernen, wie du dein erstes Dokument erstellst und versendest.',
start_tour: 'Starten'
} }
export { en, es, it, pt, fr, de } export { en, es, it, pt, fr, de }
+27 -3
View File
@@ -37,7 +37,7 @@
<FieldArea <FieldArea
v-if="newArea" v-if="newArea"
:is-draw="true" :is-draw="true"
:field="{ submitter_uuid: selectedSubmitter.uuid, type: drawField?.type || defaultFieldType }" :field="{ submitter_uuid: selectedSubmitter.uuid, type: drawField?.type || dragFieldPlaceholder?.type || defaultFieldType }"
:area="newArea" :area="newArea"
/> />
</div> </div>
@@ -49,7 +49,9 @@
:class="{ 'z-10': !isMobile, 'cursor-grab': isDrag, 'cursor-nwse-resize': drawField, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }" :class="{ 'z-10': !isMobile, 'cursor-grab': isDrag, 'cursor-nwse-resize': drawField, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }"
@pointermove="onPointermove" @pointermove="onPointermove"
@pointerdown="onStartDraw" @pointerdown="onStartDraw"
@dragover.prevent @dragover.prevent="onDragover"
@dragenter="onDragenter"
@dragleave="newArea = null"
@drop="onDrop" @drop="onDrop"
@pointerup="onPointerup" @pointerup="onPointerup"
/> />
@@ -64,12 +66,17 @@ export default {
components: { components: {
FieldArea FieldArea
}, },
inject: ['fieldTypes', 'defaultDrawFieldType', 'fieldsDragFieldRef'], inject: ['fieldTypes', 'defaultDrawFieldType', 'fieldsDragFieldRef', 'assignDropAreaSize'],
props: { props: {
image: { image: {
type: Object, type: Object,
required: true required: true
}, },
dragFieldPlaceholder: {
type: Object,
required: false,
default: null
},
areas: { areas: {
type: Array, type: Array,
required: false, required: false,
@@ -192,7 +199,24 @@ export default {
this.areaRefs.push(el) this.areaRefs.push(el)
} }
}, },
onDragenter (e) {
this.newArea = {}
this.assignDropAreaSize(this.newArea, this.dragFieldPlaceholder, {
maskW: this.$refs.mask.clientWidth,
maskH: this.$refs.mask.clientHeight
})
this.newArea.x = (e.offsetX - 6) / this.$refs.mask.clientWidth
this.newArea.y = e.offsetY / this.$refs.mask.clientHeight - this.newArea.h / 2
},
onDragover (e) {
this.newArea.x = (e.offsetX - 6) / this.$refs.mask.clientWidth
this.newArea.y = e.offsetY / this.$refs.mask.clientHeight - this.newArea.h / 2
},
onDrop (e) { onDrop (e) {
this.newArea = null
this.$emit('drop-field', { this.$emit('drop-field', {
x: e.offsetX, x: e.offsetX,
y: e.offsetY, y: e.offsetY,
+19 -8
View File
@@ -94,11 +94,27 @@ class ProcessSubmitterCompletionJob
end end
end end
to = build_to_addresses(submitter) maybe_enqueue_copy_emails(submitter)
end
return if to.blank? || submitter.template.preferences['documents_copy_email_enabled'] == false def maybe_enqueue_copy_emails(submitter)
return if submitter.template.preferences['documents_copy_email_enabled'] == false
SubmitterMailer.documents_copy_email(submitter, to:).deliver_later! configs = AccountConfigs.find_or_initialize_for_key(submitter.account,
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
return if configs.value['enabled'] == false
to = submitter.submission.submitters.reject { |e| e.preferences['send_email'] == false }
.sort_by(&:completed_at).select(&:email?).map(&:friendly_name)
return if to.blank?
if configs.value['bcc_recipients'] == true
to.each { |to| SubmitterMailer.documents_copy_email(submitter, to:).deliver_later! }
else
SubmitterMailer.documents_copy_email(submitter, to: to.join(', ')).deliver_later!
end
end end
def build_bcc_addresses(submission) def build_bcc_addresses(submission)
@@ -110,11 +126,6 @@ class ProcessSubmitterCompletionJob
bcc.to_s.scan(User::EMAIL_REGEXP) bcc.to_s.scan(User::EMAIL_REGEXP)
end end
def build_to_addresses(submitter)
submitter.submission.submitters.reject { |e| e.preferences['send_email'] == false }
.sort_by(&:completed_at).select(&:email?).map(&:friendly_name).join(', ')
end
def enqueue_next_submitter_request_notification(submitter) def enqueue_next_submitter_request_notification(submitter)
next_submitter_item = next_submitter_item =
submitter.submission.template_submitters.find do |e| submitter.submission.template_submitters.find do |e|
+1
View File
@@ -30,6 +30,7 @@ class AccountConfig < ApplicationRecord
ALLOW_TO_RESUBMIT = 'allow_to_resubmit' ALLOW_TO_RESUBMIT = 'allow_to_resubmit'
ALLOW_TO_DECLINE_KEY = 'allow_to_decline' ALLOW_TO_DECLINE_KEY = 'allow_to_decline'
SUBMITTER_REMINDERS = 'submitter_reminders' SUBMITTER_REMINDERS = 'submitter_reminders'
ENFORCE_SIGNING_ORDER_KEY = 'enforce_signing_order'
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button' FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
FORM_COMPLETED_MESSAGE_KEY = 'form_completed_message' FORM_COMPLETED_MESSAGE_KEY = 'form_completed_message'
FORM_WITH_CONFETTI_KEY = 'form_with_confetti' FORM_WITH_CONFETTI_KEY = 'form_with_confetti'
+1
View File
@@ -64,6 +64,7 @@ class Submission < ApplicationRecord
through: :template, source: :documents_attachments through: :template, source: :documents_attachments
scope :active, -> { where(archived_at: nil) } scope :active, -> { where(archived_at: nil) }
scope :archived, -> { where.not(archived_at: nil) }
scope :pending, -> { joins(:submitters).where(submitters: { completed_at: nil }).group(:id) } scope :pending, -> { joins(:submitters).where(submitters: { completed_at: nil }).group(:id) }
scope :completed, lambda { scope :completed, lambda {
where.not(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id]) where.not(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
+1
View File
@@ -25,6 +25,7 @@ class UserConfig < ApplicationRecord
INITIALS_KEY = 'initials' INITIALS_KEY = 'initials'
RECEIVE_COMPLETED_EMAIL = 'receive_completed_email' RECEIVE_COMPLETED_EMAIL = 'receive_completed_email'
RECEIVE_DECLINED_EMAIL = 'receive_declined_email' RECEIVE_DECLINED_EMAIL = 'receive_declined_email'
SHOW_APP_TOUR = 'show_app_tour'
belongs_to :user belongs_to :user
+16 -1
View File
@@ -151,6 +151,20 @@
</div> </div>
<% end %> <% end %>
<% end %> <% end %>
<% if !Docuseal.multitenant? || can?(:manage, :personalization_advanced) %>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::ENFORCE_SIGNING_ORDER_KEY) %>
<% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %>
<div class="flex items-center justify-between py-2.5">
<span>
<%= t('always_enforce_signing_order') %>
</span>
<%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()' %>
</div>
<% end %>
<% end %>
<% end %>
</div> </div>
<% end %> <% end %>
<%= render 'compliances' %> <%= render 'compliances' %>
@@ -160,9 +174,10 @@
<h2 class="text-2xl font-bold mb-2"> <h2 class="text-2xl font-bold mb-2">
<%= t('danger_zone') %> <%= t('danger_zone') %>
</h2> </h2>
<%= button_to button_title(title: t('delete_my_account')), settings_account_path, class: 'btn btn-outline btn-error block', data: { turbo_confirm: t('schedule_account_for_deletion_') }, method: :delete, id: :account_delete_button %> <%= button_to button_title(title: t('delete_my_account')), settings_account_path, class: 'btn btn-outline btn-error block', data: { turbo_confirm: t('you_are_scheduling_your_account_for_deletion_after_deletion_your_data_will_be_permanently_removed_and_cannot_be_recovered_click_ok_if_you_would_like_to_continue') }, method: :delete, id: :account_delete_button %>
</div> </div>
<% end %> <% end %>
</div> </div>
<div class="w-0 md:w-52"></div> <div class="w-0 md:w-52"></div>
</div> </div>
<%= render 'shared/app_tour', type: 'account' %>
+1 -1
View File
@@ -1,4 +1,4 @@
<form action="<%= root_path %>" method="get" class="bg-base-200 px-1.5 rounded-xl py-1 whitespace-nowrap"> <form action="<%= root_path %>" method="get" id="templates_submissions_toggle" class="bg-base-200 px-1.5 rounded-xl py-1 whitespace-nowrap">
<toggle-cookies data-value="templates" data-key="dashboard_view" class="sm:tooltip tooltip-top" data-tip="<%= t('templates') %>"> <toggle-cookies data-value="templates" data-key="dashboard_view" class="sm:tooltip tooltip-top" data-tip="<%= t('templates') %>">
<button class="<%= local_assigns[:selected] == 'submissions' ? 'btn !border !rounded-lg btn-square !p-0 !btn-sm !h-8 !w-9' : 'btn btn-neutral !rounded-lg btn-square !p-0 hover:text-neutral-300 !btn-sm !h-8 !w-9 disabled:btn-neutral' %>"> <button class="<%= local_assigns[:selected] == 'submissions' ? 'btn !border !rounded-lg btn-square !p-0 !btn-sm !h-8 !w-9' : 'btn btn-neutral !rounded-lg btn-square !p-0 hover:text-neutral-300 !btn-sm !h-8 !w-9 disabled:btn-neutral' %>">
<%= svg_icon('layout_grid', class: 'w-6 h-6 stroke-2') %> <%= svg_icon('layout_grid', class: 'w-6 h-6 stroke-2') %>
@@ -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, :reply_to, :attach_audit_log, :attach_documents).new(*f.object.value.values_at('subject', 'body', 'reply_to', 'attach_audit_log', 'attach_documents')) do |ff| %> <%= f.fields_for :value, Struct.new(:subject, :body, :reply_to, :attach_audit_log, :attach_documents, :bcc_recipients, :enabled).new(*f.object.value.values_at('subject', 'body', 'reply_to', 'attach_audit_log', 'attach_documents', 'bcc_recipients', 'enabled')) 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,23 +24,41 @@
<%= 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) %> <% if can?(:manage, :reply_to) || can?(:manage, :personalization_advanced) %>
<div class="form-control"> <div class="form-control">
<%= ff.label :reply_to, t('reply_to'), class: 'label' %> <%= ff.label :reply_to, t('reply_to'), class: 'label' %>
<%= ff.email_field :reply_to, class: 'base-input', dir: 'auto', placeholder: t(:email) %> <%= ff.email_field :reply_to, class: 'base-input', dir: 'auto', placeholder: t(:email) %>
</div> </div>
<% end %> <% end %>
<div class="flex items-center justify-between pt-2.5 mx-1"> <div class="space-y-3.5">
<span> <div class="flex items-center justify-between mx-1">
<%= t('attach_documents') %> <span>
</span> <%= t('attach_documents') %>
<%= ff.check_box :attach_documents, { checked: ff.object.attach_documents != false, class: 'toggle' }, 'true', 'false' %> </span>
</div> <%= ff.check_box :attach_documents, { checked: ff.object.attach_documents != false, class: 'toggle' }, 'true', 'false' %>
<div class="flex items-center justify-between pb-2.5 mx-1"> </div>
<span> <div class="flex items-center justify-between mx-1">
<%= t('attach_audit_log_pdf') %> <span>
</span> <%= t('attach_audit_log_pdf') %>
<%= ff.check_box :attach_audit_log, { checked: ff.object.attach_audit_log != false, class: 'toggle' }, 'true', 'false' %> </span>
<%= ff.check_box :attach_audit_log, { checked: ff.object.attach_audit_log != false, class: 'toggle' }, 'true', 'false' %>
</div>
<% unless Docuseal.multitenant? %>
<div class="flex items-center justify-between mx-1">
<span>
<%= t('bcc_recipients') %>
</span>
<%= ff.check_box :bcc_recipients, { checked: ff.object.bcc_recipients == true, class: 'toggle' }, 'true', 'false' %>
</div>
<% end %>
<% if !Docuseal.multitenant? || can?(:manage, :personalization_advanced) %>
<div class="flex items-center justify-between mx-1">
<span>
<%= t('send_emails_automatically_on_completion') %>
</span>
<%= ff.check_box :enabled, { checked: ff.object.enabled != false, class: 'toggle' }, 'true', 'false' %>
</div>
<% end %>
</div> </div>
<% end %> <% end %>
<div class="form-control pt-2"> <div class="form-control pt-2">
+6
View File
@@ -0,0 +1,6 @@
<% if current_user.created_at > 2.weeks.ago || params[:tour] == 'true' %>
<% user_config = current_user.user_configs.find_or_initialize_by(key: UserConfig::SHOW_APP_TOUR) %>
<% if user_config.new_record? || user_config.value || params[:tour] == 'true' %>
<app-tour data-show-tour="<%= params[:tour] == 'true' || user_config.value %>" data-type="<%= local_assigns[:type] %>" data-next-page-path="<%= local_assigns[:next_page_path] %>" data-i18n="<%= t('app_tour').to_json %>"></app-tour>
<% end %>
<% end %>
+1 -1
View File
@@ -1 +1 @@
<% 'stats stat stat-figure stat-title stat-value text-accent w-fit hover:bg-white' %> <% 'stats stat stat-figure stat-title stat-value text-accent w-fit hover:bg-white dropdown-open' %>
+1 -1
View File
@@ -1,5 +1,5 @@
<clipboard-copy data-text="<%= text %>"> <clipboard-copy data-text="<%= text %>">
<label class="<%= local_assigns[:class] %>"> <label id="<%= local_assigns[:id] %>" class="<%= local_assigns[:class] %>">
<input type="radio" class="peer hidden"> <input type="radio" class="peer hidden">
<span class="peer-checked:hidden flex items-center space-x-2"> <span class="peer-checked:hidden flex items-center space-x-2">
<%= svg_icon(local_assigns[:icon] || 'link', class: local_assigns[:icon_class] || 'w-6 h-6 text-white') %> <%= svg_icon(local_assigns[:icon] || 'link', class: local_assigns[:icon_class] || 'w-6 h-6 text-white') %>
+2 -2
View File
@@ -13,10 +13,10 @@
<meta property="og:image" content=""> <meta property="og:image" content="">
<meta name="twitter:image" content=""> <meta name="twitter:image" content="">
<% else %> <% else %>
<meta property="og:image" content="<%= image_pack_url('images/preview.png') %>"> <meta property="og:image" content="<%= root_url %>preview.png">
<meta property="og:image:width" content="800"> <meta property="og:image:width" content="800">
<meta property="og:image:height" content="800"> <meta property="og:image:height" content="800">
<meta name="twitter:image" content="<%= image_pack_url('images/preview.png') %>"> <meta name="twitter:image" content="<%= root_url %>preview.png">
<% end %> <% end %>
<meta name="twitter:card" content="summary"> <meta name="twitter:card" content="summary">
<meta name="twitter:creator" content="@docusealco"> <meta name="twitter:creator" content="@docusealco">
+1 -1
View File
@@ -20,7 +20,7 @@
<% else %> <% else %>
<div class="flex items-center justify-center space-x-4 mr-1"> <div class="flex items-center justify-center space-x-4 mr-1">
<%= render 'shared/navbar_buttons' %> <%= render 'shared/navbar_buttons' %>
<%= link_to t('settings'), settings_profile_index_path, class: 'hidden md:inline-flex font-medium text-lg' %> <%= link_to t('settings'), settings_profile_index_path, class: 'hidden md:inline-flex font-medium text-lg', id: 'account_settings_button' %>
</div> </div>
<% end %> <% end %>
<div class="dropdown dropdown-end"> <div class="dropdown dropdown-end">
+2 -2
View File
@@ -1,6 +1,6 @@
<div class="block w-full md:w-52 flex-none"> <div class="block w-full md:w-52 flex-none">
<menu-active> <menu-active>
<ul class="menu px-0"> <ul id="account_settings_menu" class="menu px-0">
<li class="menu-title py-0 !bg-transparent mb-3 -mt-5"><a href="<%= '/' %>" class="!bg-transparent !text-neutral font-medium">&larr; <%= t('back') %></a></li> <li class="menu-title py-0 !bg-transparent mb-3 -mt-5"><a href="<%= '/' %>" class="!bg-transparent !text-neutral font-medium">&larr; <%= t('back') %></a></li>
<li class="menu-title py-0 !bg-transparent"> <li class="menu-title py-0 !bg-transparent">
<span class="!bg-transparent"><%= t('settings') %></span> <span class="!bg-transparent"><%= t('settings') %></span>
@@ -105,7 +105,7 @@
</ul> </ul>
</menu-active> </menu-active>
<% if Docuseal.multitenant? || cannot?(:manage, :tenants) %> <% if Docuseal.multitenant? || cannot?(:manage, :tenants) %>
<div class="mx-4 border-t border-base-300 hidden md:block"> <div id="support_channels" class="mx-4 border-t border-base-300 hidden md:block">
<div class="text-sm mt-3"> <div class="text-sm mt-3">
<%= t('need_help_ask_a_question_') %> <%= t('need_help_ask_a_question_') %>
</div> </div>
+2 -2
View File
@@ -81,7 +81,7 @@
<% elsif !submission.archived_at? && !template.archived_at? && !submission.expired? && !submitter.declined_at? %> <% elsif !submission.archived_at? && !template.archived_at? && !submission.expired? && !submitter.declined_at? %>
<% if current_user.email == submitter.email %> <% if current_user.email == submitter.email %>
<div class="flex-1 md:flex-none md:w-36 flex"> <div class="flex-1 md:flex-none md:w-36 flex">
<a href="<%= submit_form_url(slug: submitter.slug) %>" data-turbo="false" target="_blank" class="btn btn-sm btn-neutral btn-outline bg-white w-full md:w-36 flex z-[1]"> <a href="<%= submit_form_url(slug: submitter.slug) %>" data-turbo="false" target="_blank" id="sign_yourself_button" class="btn btn-sm btn-neutral btn-outline bg-white w-full md:w-36 flex z-[1]">
<span class="flex items-center justify-center space-x-1 md:space-x-2"> <span class="flex items-center justify-center space-x-1 md:space-x-2">
<% if t('sign_now').length < 12 %> <% if t('sign_now').length < 12 %>
<%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %> <%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %>
@@ -167,7 +167,7 @@
<% elsif !template.archived_at? && !submission.archived_at? && !is_submission_completed && !submission.expired? && !submitter.declined_at? %> <% elsif !template.archived_at? && !submission.archived_at? && !is_submission_completed && !submission.expired? && !submitter.declined_at? %>
<div class="relative flex items-center space-x-3"> <div class="relative flex items-center space-x-3">
<% if current_user.email == submitter.email %> <% if current_user.email == submitter.email %>
<a href="<%= submit_form_url(slug: submitter.slug) %>" data-turbo="false" target="_blank" class="absolute md:relative top-0 right-0 btn btn-xs btn-outline btn-neutral bg-white w-28 md:w-36 z-[1]"> <a href="<%= submit_form_url(slug: submitter.slug) %>" data-turbo="false" target="_blank" id="sign_yourself_button" class="absolute md:relative top-0 right-0 btn btn-xs btn-outline btn-neutral bg-white w-28 md:w-36 z-[1]">
<span class="flex items-center justify-center space-x-1 md:space-x-2"> <span class="flex items-center justify-center space-x-1 md:space-x-2">
<% if t('sign_now').length < 12 %> <% if t('sign_now').length < 12 %>
<%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %> <%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %>
+1 -1
View File
@@ -49,7 +49,7 @@
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
<%= render 'shared/clipboard_copy', text: start_form_url(slug: @template.slug), class: 'absolute md:relative bottom-0 right-0 btn btn-xs md:btn-sm whitespace-nowrap btn-neutral text-white mt-1 px-2', icon_class: 'w-4 h-4 md:w-6 md:h-6 text-white', copy_title: t('link'), copied_title: t('copied'), copy_title_md: t('link'), copied_title_md: t('copied') %> <%= render 'shared/clipboard_copy', text: start_form_url(slug: @template.slug), id: 'share_link_clipboard', class: 'absolute md:relative bottom-0 right-0 btn btn-xs md:btn-sm whitespace-nowrap btn-neutral text-white mt-1 px-2', icon_class: 'w-4 h-4 md:w-6 md:h-6 text-white', copy_title: t('link'), copied_title: t('copied'), copy_title_md: t('link'), copied_title_md: t('copied') %>
</div> </div>
<% end %> <% end %>
</div> </div>
+1 -1
View File
@@ -1,5 +1,5 @@
<%= form_for '', url: templates_upload_path, id: form_id = SecureRandom.uuid, method: :post, class: 'inline', html: { enctype: 'multipart/form-data' } do %> <%= form_for '', url: templates_upload_path, id: form_id = SecureRandom.uuid, method: :post, class: 'inline', html: { enctype: 'multipart/form-data' } do %>
<button type="submit" class="btn btn-ghost text-base" onclick="[event.preventDefault(), window.upload_template.click()]"> <button id="templates_upload_button" type="submit" class="btn btn-ghost text-base" onclick="[event.preventDefault(), window.upload_template.click()]">
<span class="enabled"> <span class="enabled">
<span class="flex items-center justify-center space-x-2"> <span class="flex items-center justify-center space-x-2">
<%= svg_icon('upload', class: 'w-6 h-6 stroke-2') %> <%= svg_icon('upload', class: 'w-6 h-6 stroke-2') %>
+9 -1
View File
@@ -1 +1,9 @@
<template-builder class="grid" data-template="<%= @template_data %>" data-with-sign-yourself-button="<%= !@template.archived_at? %>" data-with-send-button="<%= !@template.archived_at? && can?(:create, @template.submissions.new(account: current_account)) %>" data-locale="<%= I18n.locale %>"></template-builder> <% if current_user.created_at > 2.weeks.ago || params[:tour] == 'true' %>
<% user_config = current_user.user_configs.find_or_initialize_by(key: UserConfig::SHOW_APP_TOUR) %>
<% @show_tour_start_form = user_config.new_record? && !params.key?(:tour) %>
<% if user_config.new_record? || user_config.value || params[:tour] == 'true' %>
<app-tour data-show-tour="<%= params[:tour] == 'true' || user_config.value %>" data-type="builder" data-next-page-path="<%= template_path(@template, params.permit(:tour)) %>" data-i18n="<%= t('app_tour').to_json %>"></app-tour>
<%= button_to nil, user_configs_path, method: :post, params: { user_config: { key: UserConfig::SHOW_APP_TOUR, value: true } }, class: 'hidden', id: 'start_tour_button' %>
<% end %>
<% end %>
<template-builder class="grid" data-template="<%= @template_data %>" data-with-sign-yourself-button="<%= !@template.archived_at? %>" data-with-send-button="<%= !@template.archived_at? && can?(:create, @template.submissions.new(account: current_account)) %>" data-locale="<%= I18n.locale %>" data-show-tour-start-form="<%= @show_tour_start_form %>"></template-builder>
+5 -4
View File
@@ -18,7 +18,7 @@
<span><%= t('export') %></span> <span><%= t('export') %></span>
<% end %> <% end %>
<% if !@template.archived_at? && can?(:create, Submission) %> <% if !@template.archived_at? && can?(:create, Submission) %>
<%= link_to new_template_submission_path(@template), class: 'white-button !border', data: { turbo_frame: 'modal' } do %> <%= link_to new_template_submission_path(@template), id: 'add_recipients_button', class: 'white-button !border', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %> <%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<%= t('add_recipients_html') %> <%= t('add_recipients_html') %>
<% end %> <% end %>
@@ -80,18 +80,18 @@
<p><%= t('send_an_invitation_to_fill_and_complete_the_form') %></p> <p><%= t('send_an_invitation_to_fill_and_complete_the_form') %></p>
<div class="space-y-2 flex flex-col"> <div class="space-y-2 flex flex-col">
<% if can?(:create, Submission) %> <% if can?(:create, Submission) %>
<%= link_to new_template_submission_path(@template, with_link: true), class: 'base-button mt-6', data: { turbo_frame: 'modal' } do %> <%= link_to new_template_submission_path(@template, with_link: true), id: 'send_to_recipients_button', class: 'base-button mt-6', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %> <%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span class="mr-1"><%= t('send_to_recipients') %></span> <span class="mr-1"><%= t('send_to_recipients') %></span>
<% end %> <% end %>
<% end %> <% end %>
<% if @template.submitters.size == 1 %> <% if @template.submitters.size == 1 %>
<%= link_to start_form_url(slug: @template.slug), class: 'white-button mt-6', target: '_blank', rel: 'noopener' do %> <%= link_to start_form_url(slug: @template.slug), id: 'sign_yourself_button', class: 'white-button mt-6', target: '_blank', rel: 'noopener' do %>
<%= svg_icon('writing', class: 'w-6 h-6') %> <%= svg_icon('writing', class: 'w-6 h-6') %>
<span class="mr-1"><%= t('sign_it_yourself') %></span> <span class="mr-1"><%= t('sign_it_yourself') %></span>
<% end %> <% end %>
<% else %> <% else %>
<%= link_to new_template_submission_path(@template, selfsign: true), class: 'white-button mt-6', data: { turbo_frame: 'modal' } do %> <%= link_to new_template_submission_path(@template, selfsign: true), id: 'sign_yourself_button', class: 'white-button mt-6', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('writing', class: 'w-6 h-6') %> <%= svg_icon('writing', class: 'w-6 h-6') %>
<span class="mr-1"><%= t('sign_it_yourself') %></span> <span class="mr-1"><%= t('sign_it_yourself') %></span>
<% end %> <% end %>
@@ -117,3 +117,4 @@
<%= view_archived_html %> <%= view_archived_html %>
</div> </div>
<% end %> <% end %>
<%= render 'shared/app_tour', type: 'template', next_page_path: settings_account_path(params.permit(:tour)) %>
+24 -1
View File
@@ -1,4 +1,5 @@
<% has_archived = current_account.templates.where.not(archived_at: nil).exists? %> <% has_archived = current_account.templates.where.not(archived_at: nil).exists? %>
<% show_dropzone = params[:q].blank? && @pagy.pages == 1 && ((@template_folders.size < 10 && @templates.size.zero?) || (@template_folders.size < 7 && @templates.size < 4) || (@template_folders.size < 4 && @templates.size < 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">
@@ -41,9 +42,31 @@
<% if @templates.present? %> <% if @templates.present? %>
<div class="grid gap-4 md:grid-cols-3"> <div class="grid gap-4 md:grid-cols-3">
<%= render partial: 'templates/template', collection: @templates %> <%= render partial: 'templates/template', collection: @templates %>
<% if show_dropzone && current_user.created_at > 2.weeks.ago || params[:tour] == 'true' %>
<% user_config = current_user.user_configs.find_or_initialize_by(key: UserConfig::SHOW_APP_TOUR) %>
<% if user_config.new_record? || user_config.value || params[:tour] == 'true' %>
<div class="hidden md:block">
<app-tour id="app_tour" data-show-tour="<%= params[:tour] == 'true' || user_config.value %>" data-type="dashboard" data-next-page-path="<%= @templates.first && can?(:edit, @templates.first) ? edit_template_path(@templates.first, params.permit(:tour)) : settings_account_path %>" data-i18n="<%= t('app_tour').to_json %>"></app-tour>
<% if user_config.new_record? && !params.key?(:tour) %>
<div class="h-36 rounded-2xl pt-3 px-7 w-full border border-dashed border-base-300">
<div class="text-xl text-center font-semibold text-base-content">
<%= t('welcome_to_docuseal') %>
</div>
<div class="my-2 text-center text-xs text-base-content/70">
<%= t('start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document') %>
</div>
<div class="flex gap-2 mt-3 w-full">
<%= button_to button_title(title: t('skip'), icon_disabled: svg_icon('loader', class: 'w-4 h-4 animate-spin')), user_configs_path, params: { user_config: { key: UserConfig::SHOW_APP_TOUR, value: false } }, class: 'btn btn-sm btn-outline w-full', form_class: 'flex-1', method: :post, form: { onsubmit: 'window.app_tour.parentNode.remove()' } %>
<%= button_to t('start_tour'), user_configs_path, params: { user_config: { key: UserConfig::SHOW_APP_TOUR, value: true } }, class: 'btn btn-sm btn-warning w-full', form_class: 'flex-1', method: :post, form: { onsubmit: 'window.app_tour.start()' } %>
</div>
</div>
<% end %>
</div>
<% end %>
<% end %>
</div> </div>
<% end %> <% end %>
<% if params[:q].blank? && @pagy.pages == 1 && ((@template_folders.size < 10 && @templates.size.zero?) || (@template_folders.size < 7 && @templates.size < 4) || (@template_folders.size < 4 && @templates.size < 7)) %> <% if show_dropzone %>
<%= render 'templates/dropzone' %> <%= render 'templates/dropzone' %>
<% end %> <% end %>
<% if @templates.present? || params[:q].blank? %> <% if @templates.present? || params[:q].blank? %>
+12 -10
View File
@@ -206,7 +206,7 @@
<span> <span>
<%= t('send_emails_automatically_on_completion') %> <%= t('send_emails_automatically_on_completion') %>
</span> </span>
<%= ff.check_box :documents_copy_email_enabled, { checked: ff.object.documents_copy_email_enabled != false, class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %> <%= ff.check_box :documents_copy_email_enabled, { checked: ff.object.documents_copy_email_enabled != false && configs['enabled'] != false, class: 'toggle', onchange: 'this.form.requestSubmit()', disabled: configs['enabled'] == false }, 'true', 'false' %>
</div> </div>
<% end %> <% end %>
<div class="form-control pt-2"> <div class="form-control pt-2">
@@ -322,15 +322,17 @@
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-2' }, data: { close_on_submit: false } do |f| %> <% unless current_account.account_configs.exists?(key: AccountConfig::ENFORCE_SIGNING_ORDER_KEY, value: true) %>
<div class="flex items-center pt-4 mt-4 justify-between border-t w-full"> <%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-2' }, data: { close_on_submit: false } do |f| %>
<span> <div class="flex items-center pt-4 mt-4 justify-between border-t w-full">
<%= t('enforce_recipients_order') %> <span>
</span> <%= t('enforce_recipients_order') %>
<%= f.fields_for :preferences, Struct.new(:submitters_order).new(@template.preferences['submitters_order']) do |ff| %> </span>
<%= ff.check_box :submitters_order, { class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'preserved', '' %> <%= f.fields_for :preferences, Struct.new(:submitters_order).new(@template.preferences['submitters_order']) do |ff| %>
<% end %> <%= ff.check_box :submitters_order, { class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'preserved', '' %>
</div> <% end %>
</div>
<% end %>
<% end %> <% end %>
<div class="form-control mt-5 pb-2"> <div class="form-control mt-5 pb-2">
<%= button_tag button_title(title: t('save'), disabled_with: t('updating')), class: 'base-button', form: :submitters_form %> <%= button_tag button_title(title: t('save'), disabled_with: t('updating')), class: 'base-button', form: :submitters_form %>
+1 -1
View File
@@ -1,7 +1,7 @@
<div class="flex-wrap space-y-4 md:flex md:flex-nowrap md:space-y-0 md:space-x-10"> <div class="flex-wrap space-y-4 md:flex md:flex-nowrap md:space-y-0 md:space-x-10">
<%= render 'shared/settings_nav' %> <%= render 'shared/settings_nav' %>
<div class="md:flex-grow"> <div class="md:flex-grow">
<div class="flex flex-col md:flex-row gap-2 md:justify-between md:items-end mb-4 min-h-12"> <div class="flex flex-col md:flex-row md:flex-wrap gap-2 md:justify-between md:items-end mb-4 min-h-12">
<h1 class="text-4xl font-bold"> <h1 class="text-4xl font-bold">
<% if params[:status] == 'archived' %> <% if params[:status] == 'archived' %>
<%= t('archived_users') %> <%= t('archived_users') %>
+283 -24
View File
@@ -20,6 +20,8 @@ en: &en
language_ko: 한국어 language_ko: 한국어
hi_there: Hi there hi_there: Hi there
thanks: Thanks thanks: Thanks
bcc_recipients: BCC recipients
always_enforce_signing_order: Always enforce the signing order
edit_per_party: Edit per party edit_per_party: Edit per party
reply_to: Reply to reply_to: Reply to
pending_by_me: Pending by me pending_by_me: Pending by me
@@ -164,7 +166,7 @@ en: &en
connect_salesforce: Connect Salesforce connect_salesforce: Connect Salesforce
danger_zone: Danger Zone danger_zone: Danger Zone
delete_my_account: Delete my account delete_my_account: Delete my account
schedule_account_for_deletion_: Schedule account for deletion? you_are_scheduling_your_account_for_deletion_after_deletion_your_data_will_be_permanently_removed_and_cannot_be_recovered_click_ok_if_you_would_like_to_continue: "You are scheduling your account for deletion. After deletion, your data will be permanently removed and cannot be recovered.\n\nClick OK if you would like to continue."
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_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. 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.
@@ -432,7 +434,7 @@ en: &en
share_template_with_test_mode: Share template with Test mode 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 users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: '%{users_count}/%{total_users_count} Pro users limit was reached. To invite additional users, please purchase more Pro user seats via the "Manage plan" button.'
move_into_folder: Move Into Folder move_into_folder: Move Into Folder
new_folder_name: New Folder Name new_folder_name: New Folder Name
exit_preview: Exit Preview exit_preview: Exit Preview
@@ -473,8 +475,6 @@ en: &en
team_accounts: Team Accounts team_accounts: Team Accounts
tenant_account: Tenant account tenant_account: Tenant account
tenant_accounts: Tenant Accounts tenant_accounts: Tenant Accounts
upgrade_plan: Upgrade Plan
add_user: Add User
impersonate: Impersonate impersonate: Impersonate
loading: Loading loading: Loading
documents: Documents documents: Documents
@@ -701,6 +701,9 @@ en: &en
pro_user_seats_used: Pro user seats used pro_user_seats_used: Pro user seats used
manage_plan: Manage plan manage_plan: Manage plan
this_submission_has_multiple_signers_which_prevents_the_use_of_a_sharing_link_html: This submission has multiple signers, which prevents the use of a sharing link as it's unclear which signer is responsible for specific fields. To resolve this, follow this <a href="https://www.docuseal.com/resources/pre-filling-recipients" class="link font-bold" rel="noopener noreferrer nofollow" target="_blank">guide</a> to define the default signer details. this_submission_has_multiple_signers_which_prevents_the_use_of_a_sharing_link_html: This submission has multiple signers, which prevents the use of a sharing link as it's unclear which signer is responsible for specific fields. To resolve this, follow this <a href="https://www.docuseal.com/resources/pre-filling-recipients" class="link font-bold" rel="noopener noreferrer nofollow" target="_blank">guide</a> to define the default signer details.
welcome_to_docuseal: Welcome to DocuSeal
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: Start a quick tour to learn how to create an send your first document
start_tour: Start Tour
submission_sources: submission_sources:
api: API api: API
bulk: Bulk Send bulk: Bulk Send
@@ -739,12 +742,54 @@ en: &en
or: Or or: Or
download: download download: download
a_sample_spreadsheet_to_fill_and_import: a sample spreadsheet to fill and import a_sample_spreadsheet_to_fill_and_import: a sample spreadsheet to fill and import
app_tour:
start: Start
previous: Previous
next: Next
template_and_submissions: 'Templates and Submissions'
template_and_submissions_description: "You can select the view that best suits your workflow. Choose the 'Templates' view if you create reusable document templates, or the 'Submissions' view when you sign individual documents or review the status of each signing request."
upload_a_pdf_file: 'Upload a PDF file'
upload_a_pdf_file_description: 'Upload a document to use it for creating a signing form template.'
select_a_signer_party: 'Select a signer party'
select_a_signer_party_description: 'This dropdown allows you to select a signing party role or add a new party to the document when it needs to be signed by two or more parties.'
available_parties: 'Available parties'
available_parties_description: 'This list contains all available parties with their role names. Once selected, you can add and configure fields for the signer party.'
available_field_types: 'Available field types'
available_field_types_description: 'This section contains all available field types that can be added to the document for data input or signature collection.'
text_input_field: 'Text input field'
text_input_field_description: 'This field allows users to enter text-based information, such as names or emails.'
signature_field: 'Signature field'
signature_field_description: 'This field allows for collecting signatures from the signers.'
added_fields: 'Added fields'
added_fields_description: 'This list displays all fields added to the document. You can reorder, edit, or remove them.'
open_field_settings: 'Open field settings'
open_field_settings_description: 'On the right, next to the field name, there is a settings icon that opens the field settings.'
field_settings: 'Field settings'
field_settings_description: 'Each field can be customized through settings to meet your requirements, such as adjusting the font size, making it a required field, or adding a description.'
send_document: 'Send document for signing'
send_document_description: 'Before sending the document for signing, you can configure recipients, add their email addresses or phone numbers, and customize the email content.'
sign_yourself: 'Sign the document yourself'
sign_yourself_description: 'This button allows you to add your signature to the documents that require your signature.'
copy_and_share_link: 'Copy & share link'
copy_and_share_link_description: 'Copy this link to share the document. Anyone with the link can sign it after entering their email.'
sign_the_document: 'Sign the document'
sign_the_document_description: 'If you are one of the signers signer, this button allows you to sign the document yourself.'
send_for_signing: 'Send for signing'
add_recipients: 'Add recipients'
add_recipients_description: 'Add new recipients for the document to be signed. You can enter their email addresses or phone numbers.'
settings: 'Settings'
settings_account_description: 'With comprehensive account settings, you can customize the eSigning experience and invite more users.'
support: 'Support'
support_description: 'You can use our self-service AI assistant or email us at support@docuseal.com if you have any questions.'
settings_template_description: 'Use this button to access your account settings page.'
doorkeeper: doorkeeper:
scopes: scopes:
write: Update your data write: Update your data
read: Read your data read: Read your data
es: &es es: &es
always_enforce_signing_order: Siempre imponer el orden de firma
bcc_recipients: Destinatarios CCO
edit_per_party: Editar por parte edit_per_party: Editar por parte
signed: Firmado signed: Firmado
reply_to: Responder a reply_to: Responder a
@@ -891,7 +936,7 @@ es: &es
connect_salesforce: Conectar Salesforce connect_salesforce: Conectar Salesforce
danger_zone: Zona de peligro danger_zone: Zona de peligro
delete_my_account: Eliminar mi cuenta delete_my_account: Eliminar mi cuenta
schedule_account_for_deletion_: ¿Programar la eliminación de la cuenta? you_are_scheduling_your_account_for_deletion_after_deletion_your_data_will_be_permanently_removed_and_cannot_be_recovered_click_ok_if_you_would_like_to_continue: "Estás programando la eliminación de tu cuenta. Después de la eliminación, tus datos se eliminarán permanentemente y no podrán recuperarse.\n\nHaz clic en OK si deseas continuar."
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_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. 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.
@@ -1159,7 +1204,7 @@ es: &es
share_template_with_test_mode: Compartir plantilla con el modo de prueba 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 users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'Se alcanzó el límite de %{users_count}/%{total_users_count} usuarios Pro. Para invitar a más usuarios, compra más plazas Pro usando el botón "Gestionar plan".'
move_into_folder: Mover a la carpeta move_into_folder: Mover a la carpeta
new_folder_name: Nuevo nombre de la carpeta new_folder_name: Nuevo nombre de la carpeta
exit_preview: Salir de la vista previa exit_preview: Salir de la vista previa
@@ -1200,8 +1245,6 @@ es: &es
team_accounts: Cuentas de equipo team_accounts: Cuentas de equipo
tenant_account: Cuenta de inquilino tenant_account: Cuenta de inquilino
tenant_accounts: Cuentas de inquilino tenant_accounts: Cuentas de inquilino
upgrade_plan: Actualizar plan
add_user: Agregar usuario
impersonate: Suplantar impersonate: Suplantar
loading: Cargando loading: Cargando
documents: Documentos documents: Documentos
@@ -1428,6 +1471,9 @@ es: &es
pro_user_seats_used: Plazas de usuario Pro en uso pro_user_seats_used: Plazas de usuario Pro en uso
manage_plan: Gestionar plan manage_plan: Gestionar plan
this_submission_has_multiple_signers_which_prevents_the_use_of_a_sharing_link_html: 'Este envío tiene múltiples firmantes, lo que impide el uso de un enlace para compartir, ya que no está claro qué firmante es responsable de los campos específicos. Para resolver esto, sigue esta <a href="https://www.docuseal.com/resources/pre-filling-recipients" class="link font-bold" rel="noopener noreferrer nofollow" target="_blank">guía</a> para definir los detalles predeterminados del firmante.' this_submission_has_multiple_signers_which_prevents_the_use_of_a_sharing_link_html: 'Este envío tiene múltiples firmantes, lo que impide el uso de un enlace para compartir, ya que no está claro qué firmante es responsable de los campos específicos. Para resolver esto, sigue esta <a href="https://www.docuseal.com/resources/pre-filling-recipients" class="link font-bold" rel="noopener noreferrer nofollow" target="_blank">guía</a> para definir los detalles predeterminados del firmante.'
welcome_to_docuseal: Bienvenido a DocuSeal
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: Inicia una guía rápida para aprender a crear y enviar tu primer documento.
start_tour: Iniciar guía
submission_sources: submission_sources:
api: API api: API
bulk: Envío masivo bulk: Envío masivo
@@ -1466,12 +1512,54 @@ es: &es
or: O or: O
download: descarga download: descarga
a_sample_spreadsheet_to_fill_and_import: una hoja de cálculo de ejemplo para completar e importar. a_sample_spreadsheet_to_fill_and_import: una hoja de cálculo de ejemplo para completar e importar.
app_tour:
start: Comenzar
previous: Anterior
next: Siguiente
template_and_submissions: 'Plantillas y Envíos'
template_and_submissions_description: "Puedes seleccionar la vista que mejor se adapte a tu flujo de trabajo. Elige la vista de 'Plantillas' si creas plantillas reutilizables de documentos, o la vista de 'Envíos' cuando firmas documentos individuales o revisas el estado de cada solicitud de firma."
upload_a_pdf_file: 'Subir un archivo PDF'
upload_a_pdf_file_description: 'Sube un documento para usarlo como plantilla de formulario de firma.'
select_a_signer_party: 'Seleccionar parte firmante'
select_a_signer_party_description: 'Este menú desplegable te permite seleccionar un rol de firmante o agregar una nueva parte al documento cuando debe ser firmado por dos o más partes.'
available_parties: 'Partes disponibles'
available_parties_description: 'Esta lista contiene todas las partes disponibles con sus roles. Una vez seleccionada, puedes añadir y configurar campos para esa parte.'
available_field_types: 'Tipos de campo disponibles'
available_field_types_description: 'Esta sección contiene todos los tipos de campo que se pueden añadir al documento para entrada de datos o recolección de firmas.'
text_input_field: 'Campo de texto'
text_input_field_description: 'Este campo permite a los usuarios introducir información de texto, como nombres o correos electrónicos.'
signature_field: 'Campo de firma'
signature_field_description: 'Este campo permite recopilar firmas de los firmantes.'
added_fields: 'Campos añadidos'
added_fields_description: 'Esta lista muestra todos los campos añadidos al documento. Puedes reordenarlos, editarlos o eliminarlos.'
open_field_settings: 'Abrir configuración del campo'
open_field_settings_description: 'A la derecha del nombre del campo, hay un icono de ajustes que abre la configuración del campo.'
field_settings: 'Configuración del campo'
field_settings_description: 'Cada campo puede personalizarse según tus necesidades, como ajustar el tamaño de fuente, hacerlo obligatorio o añadir una descripción.'
send_document: 'Enviar documento para firmar'
send_document_description: 'Antes de enviar el documento para firmar, puedes configurar destinatarios, añadir correos electrónicos o teléfonos, y personalizar el contenido del correo.'
sign_yourself: 'Firma el documento tú mismo'
sign_yourself_description: 'Este botón te permite añadir tu firma a documentos que requieren tu firma.'
copy_and_share_link: 'Copiar y compartir enlace'
copy_and_share_link_description: 'Copia este enlace para compartir el documento. Cualquiera con el enlace puede firmarlo tras ingresar su correo electrónico.'
sign_the_document: 'Firmar el documento'
sign_the_document_description: 'Si eres uno de los firmantes, este botón te permite firmar el documento tú mismo.'
send_for_signing: 'Enviar para firma'
add_recipients: 'Agregar destinatarios'
add_recipients_description: 'Agrega nuevos destinatarios para que el documento sea firmado. Puedes ingresar sus correos electrónicos o números de teléfono.'
settings: 'Configuración'
settings_account_description: 'Con una configuración completa de la cuenta, puedes personalizar la experiencia de firma y añadir más usuarios.'
support: 'Soporte'
support_description: 'Puedes usar nuestro asistente de IA o escribirnos a support@docuseal.com si tienes preguntas.'
settings_template_description: 'Usa este botón para acceder a la configuración de tu cuenta.'
doorkeeper: doorkeeper:
scopes: scopes:
write: Actualizar tus datos write: Actualizar tus datos
read: Leer tus datos read: Leer tus datos
it: &it it: &it
always_enforce_signing_order: Applicare sempre l'ordine di firma
bcc_recipients: Destinatari BCC
edit_per_party: Modifica per partito edit_per_party: Modifica per partito
signed: Firmato signed: Firmato
reply_to: Rispondi a reply_to: Rispondi a
@@ -1617,7 +1705,7 @@ it: &it
connect_salesforce: Connetti Salesforce connect_salesforce: Connetti Salesforce
danger_zone: Zona di pericolo danger_zone: Zona di pericolo
delete_my_account: Elimina il mio account delete_my_account: Elimina il mio account
schedule_account_for_deletion_: "Programmare l'eliminazione dell'account?" you_are_scheduling_your_account_for_deletion_after_deletion_your_data_will_be_permanently_removed_and_cannot_be_recovered_click_ok_if_you_would_like_to_continue: "Stai programmando l'eliminazione del tuo account. Dopo l'eliminazione, i tuoi dati saranno rimossi in modo permanente e non potranno essere recuperati.\n\nFai clic su OK se desideri continuare."
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_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." 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."
@@ -1885,7 +1973,7 @@ it: &it
share_template_with_test_mode: Condividi modello con la modalità 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 users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'È stato raggiunto il limite di %{users_count}/%{total_users_count} utenti Pro. Per invitare altri utenti, acquista più posti Pro tramite il pulsante "Gestisci piano".'
move_into_folder: Sposta nella cartella move_into_folder: Sposta nella cartella
new_folder_name: Nuovo nome della cartella new_folder_name: Nuovo nome della cartella
exit_preview: "Esci dall'anteprima" exit_preview: "Esci dall'anteprima"
@@ -1926,8 +2014,6 @@ it: &it
team_accounts: Account di squadra team_accounts: Account di squadra
tenant_account: Account tenant tenant_account: Account tenant
tenant_accounts: Account tenant tenant_accounts: Account tenant
upgrade_plan: Aggiorna piano
add_user: Aggiungi utente
impersonate: Impersona impersonate: Impersona
loading: Caricamento in corso loading: Caricamento in corso
documents: Documenti documents: Documenti
@@ -2154,6 +2240,9 @@ it: &it
pro_user_seats_used: Posti utente Pro in uso pro_user_seats_used: Posti utente Pro in uso
manage_plan: Gestisci piano manage_plan: Gestisci piano
this_submission_has_multiple_signers_which_prevents_the_use_of_a_sharing_link_html: "Questa sottomissione ha più firmatari, il che impedisce l'uso di un link di condivisione poiché non è chiaro quale firmatario sia responsabile di specifici campi. Per risolvere questo problema, segui questa <a href=\"https://www.docuseal.com/resources/pre-filling-recipients\" class=\"link font-bold\" rel=\"noopener noreferrer nofollow\" target=\"_blank\">guida</a> per definire i dettagli predefiniti del firmatario." this_submission_has_multiple_signers_which_prevents_the_use_of_a_sharing_link_html: "Questa sottomissione ha più firmatari, il che impedisce l'uso di un link di condivisione poiché non è chiaro quale firmatario sia responsabile di specifici campi. Per risolvere questo problema, segui questa <a href=\"https://www.docuseal.com/resources/pre-filling-recipients\" class=\"link font-bold\" rel=\"noopener noreferrer nofollow\" target=\"_blank\">guida</a> per definire i dettagli predefiniti del firmatario."
welcome_to_docuseal: Benvenuto in DocuSeal
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: Inizia un tour rapido per imparare a creare e inviare il tuo primo documento.
start_tour: Inizia il tour
submission_sources: submission_sources:
api: API api: API
bulk: Invio massivo bulk: Invio massivo
@@ -2192,12 +2281,54 @@ it: &it
or: Oppure or: Oppure
download: scarica download: scarica
a_sample_spreadsheet_to_fill_and_import: un foglio di calcolo di esempio da compilare e importare. a_sample_spreadsheet_to_fill_and_import: un foglio di calcolo di esempio da compilare e importare.
app_tour:
start: Inizia
previous: Precedente
next: Successivo
template_and_submissions: 'Modelli e Invii'
template_and_submissions_description: "Puoi selezionare la vista che meglio si adatta al tuo flusso di lavoro. Scegli la vista 'Modelli' se crei modelli riutilizzabili di documenti, oppure la vista 'Invii' quando firmi documenti singoli o controlli lo stato di ogni richiesta di firma."
upload_a_pdf_file: 'Carica un file PDF'
upload_a_pdf_file_description: 'Carica un documento da utilizzare per creare un modello di modulo di firma.'
select_a_signer_party: 'Seleziona una parte firmataria'
select_a_signer_party_description: 'Questo menu a discesa consente di selezionare un ruolo di firmatario o aggiungere una nuova parte al documento quando deve essere firmato da due o più parti.'
available_parties: 'Parti disponibili'
available_parties_description: 'Questo elenco contiene tutte le parti disponibili con i relativi ruoli. Una volta selezionata, puoi aggiungere e configurare i campi per la parte firmataria.'
available_field_types: 'Tipi di campo disponibili'
available_field_types_description: "Questa sezione contiene tutti i tipi di campo disponibili che possono essere aggiunti al documento per l'inserimento di dati o la raccolta delle firme."
text_input_field: 'Campo di testo'
text_input_field_description: 'Questo campo consente agli utenti di inserire informazioni testuali, come nomi o email.'
signature_field: 'Campo firma'
signature_field_description: 'Questo campo consente di raccogliere firme dai firmatari.'
added_fields: 'Campi aggiunti'
added_fields_description: 'Questo elenco mostra tutti i campi aggiunti al documento. Puoi riordinarli, modificarli o rimuoverli.'
open_field_settings: 'Apri le impostazioni del campo'
open_field_settings_description: "A destra del nome del campo c'è un'icona delle impostazioni che apre il menu di configurazione."
field_settings: 'Impostazioni del campo'
field_settings_description: 'Ogni campo può essere personalizzato tramite le impostazioni per soddisfare le tue esigenze, come modificare la dimensione del testo, renderlo obbligatorio o aggiungere una descrizione.'
send_document: 'Invia documento per la firma'
send_document_description: "Prima di inviare il documento per la firma, puoi configurare i destinatari, aggiungere email o numeri di telefono e personalizzare il contenuto dell'email."
sign_yourself: 'Firma il documento personalmente'
sign_yourself_description: 'Questo pulsante ti consente di aggiungere la tua firma ai documenti che richiedono la tua firma.'
copy_and_share_link: 'Copia e condividi il link'
copy_and_share_link_description: 'Copia questo link per condividere il documento. Chiunque abbia il link potrà firmarlo dopo aver inserito la propria email.'
sign_the_document: 'Firma il documento'
sign_the_document_description: 'Se sei uno dei firmatari, questo pulsante ti consente di firmare il documento personalmente.'
send_for_signing: 'Invia per la firma'
add_recipients: 'Aggiungi destinatari'
add_recipients_description: 'Aggiungi nuovi destinatari per la firma del documento. Puoi inserire il loro indirizzo email o numero di telefono.'
settings: 'Impostazioni'
settings_account_description: "Con impostazioni complete dell'account, puoi personalizzare l'esperienza di firma elettronica e invitare altri utenti."
support: 'Supporto'
support_description: 'Puoi usare il nostro assistente AI oppure scriverci a support@docuseal.com se hai domande.'
settings_template_description: "Usa questo pulsante per accedere alla pagina delle impostazioni dell'account."
doorkeeper: doorkeeper:
scopes: scopes:
write: Aggiorna i tuoi dati write: Aggiorna i tuoi dati
read: Leggi i tuoi dati read: Leggi i tuoi dati
fr: &fr fr: &fr
always_enforce_signing_order: Toujours appliquer l'ordre de signature
bcc_recipients: Destinataires en CCI
edit_per_party: Éditer par partie edit_per_party: Éditer par partie
signed: Signé signed: Signé
reply_to: Répondre à reply_to: Répondre à
@@ -2345,7 +2476,7 @@ fr: &fr
connect_salesforce: Connecter Salesforce connect_salesforce: Connecter Salesforce
danger_zone: Zone dangereuse danger_zone: Zone dangereuse
delete_my_account: Supprimer mon compte delete_my_account: Supprimer mon compte
schedule_account_for_deletion_: Programmer la suppression du compte? you_are_scheduling_your_account_for_deletion_after_deletion_your_data_will_be_permanently_removed_and_cannot_be_recovered_click_ok_if_you_would_like_to_continue: "Vous programmez la suppression de votre compte. Après suppression, vos données seront définitivement supprimées et ne pourront pas être récupérées.\n\nCliquez sur OK si vous souhaitez continuer."
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_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. 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.
@@ -2613,7 +2744,7 @@ fr: &fr
share_template_with_test_mode: Partager le modèle avec le mode 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" users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'La limite de %{users_count}/%{total_users_count} utilisateurs Pro a été atteinte. Pour inviter d''autres utilisateurs, veuillez acheter plus de places Pro via le bouton "Gérer le plan".'
move_into_folder: Déplacer dans le dossier move_into_folder: Déplacer dans le dossier
new_folder_name: Nouveau nom du dossier new_folder_name: Nouveau nom du dossier
exit_preview: "Quitter l'aperçu" exit_preview: "Quitter l'aperçu"
@@ -2654,8 +2785,6 @@ fr: &fr
team_accounts: "Comptes d'équipe" team_accounts: "Comptes d'équipe"
tenant_account: Compte locataire tenant_account: Compte locataire
tenant_accounts: Comptes locataires tenant_accounts: Comptes locataires
upgrade_plan: Mettre à jour le plan
add_user: Ajouter un utilisateur
impersonate: Usurper impersonate: Usurper
loading: Chargement en cours loading: Chargement en cours
documents: Documents documents: Documents
@@ -2882,6 +3011,9 @@ fr: &fr
pro_user_seats_used: Places utilisateur Pro en cours d'utilisation pro_user_seats_used: Places utilisateur Pro en cours d'utilisation
manage_plan: Gérer le plan manage_plan: Gérer le plan
this_submission_has_multiple_signers_which_prevents_the_use_of_a_sharing_link_html: 'Cette soumission comporte plusieurs signataires, ce qui empêche l’utilisation d’un lien de partage, car il n’est pas clair quel signataire est responsable de quels champs. Pour résoudre ce problème, suivez ce <a href="https://www.docuseal.com/resources/pre-filling-recipients" class="link font-bold" rel="noopener noreferrer nofollow" target="_blank">guide</a> pour définir les détails du signataire par défaut.' this_submission_has_multiple_signers_which_prevents_the_use_of_a_sharing_link_html: 'Cette soumission comporte plusieurs signataires, ce qui empêche l’utilisation d’un lien de partage, car il n’est pas clair quel signataire est responsable de quels champs. Pour résoudre ce problème, suivez ce <a href="https://www.docuseal.com/resources/pre-filling-recipients" class="link font-bold" rel="noopener noreferrer nofollow" target="_blank">guide</a> pour définir les détails du signataire par défaut.'
welcome_to_docuseal: Bienvenue sur DocuSeal
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: Lancez une visite rapide pour apprendre à créer et envoyer votre premier document.
start_tour: Démarrer
submission_sources: submission_sources:
api: API api: API
bulk: Envoi en masse bulk: Envoi en masse
@@ -2920,12 +3052,54 @@ fr: &fr
or: Un or: Un
download: téléchargez download: téléchargez
a_sample_spreadsheet_to_fill_and_import: un modèle de feuille de calcul à remplir et importer. a_sample_spreadsheet_to_fill_and_import: un modèle de feuille de calcul à remplir et importer.
app_tour:
start: Démarrer
previous: Précédent
next: Suivant
template_and_submissions: 'Modèles et Soumissions'
template_and_submissions_description: "Vous pouvez sélectionner la vue qui correspond le mieux à votre flux de travail. Choisissez la vue 'Modèles' si vous créez des modèles de documents réutilisables, ou la vue 'Soumissions' lorsque vous signez des documents individuels ou vérifiez l'état de chaque demande de signature."
upload_a_pdf_file: 'Téléverser un fichier PDF'
upload_a_pdf_file_description: "Téléversez un document pour l'utiliser comme modèle de formulaire de signature."
select_a_signer_party: 'Sélectionner une partie signataire'
select_a_signer_party_description: "Ce menu déroulant vous permet de sélectionner un rôle de signataire ou d'ajouter une nouvelle partie au document lorsqu'il doit être signé par deux parties ou plus."
available_parties: 'Parties disponibles'
available_parties_description: 'Cette liste contient toutes les parties disponibles avec leurs rôles. Une fois sélectionnée, vous pouvez ajouter et configurer les champs pour cette partie.'
available_field_types: 'Types de champs disponibles'
available_field_types_description: 'Cette section contient tous les types de champs pouvant être ajoutés au document pour la saisie de données ou la collecte de signatures.'
text_input_field: 'Champ de texte'
text_input_field_description: 'Ce champ permet aux utilisateurs de saisir des informations textuelles telles que des noms ou des adresses e-mail.'
signature_field: 'Champ de signature'
signature_field_description: 'Ce champ permet de collecter les signatures des signataires.'
added_fields: 'Champs ajoutés'
added_fields_description: 'Cette liste affiche tous les champs ajoutés au document. Vous pouvez les réorganiser, les modifier ou les supprimer.'
open_field_settings: 'Ouvrir les paramètres du champ'
open_field_settings_description: "À droite du nom du champ se trouve une icône permettant d'ouvrir les paramètres du champ."
field_settings: 'Paramètres du champ'
field_settings_description: "Chaque champ peut être personnalisé à l'aide des paramètres pour répondre à vos besoins, comme ajuster la taille de la police, le rendre obligatoire ou ajouter une description."
send_document: 'Envoyer le document pour signature'
send_document_description: "Avant d'envoyer le document pour signature, vous pouvez configurer les destinataires, ajouter leurs e-mails ou numéros de téléphone et personnaliser le contenu de l'e-mail."
sign_yourself: 'Signer le document vous-même'
sign_yourself_description: "Ce bouton vous permet d'ajouter votre propre signature aux documents qui en nécessitent une."
copy_and_share_link: 'Copier et partager le lien'
copy_and_share_link_description: 'Copiez ce lien pour partager le document. Toute personne disposant du lien pourra le signer après avoir saisi son e-mail.'
sign_the_document: 'Signer le document'
sign_the_document_description: 'Si vous êtes l’un des signataires, ce bouton vous permet de signer le document vous-même.'
send_for_signing: 'Envoyer pour signature'
add_recipients: 'Ajouter des destinataires'
add_recipients_description: 'Ajoutez de nouveaux destinataires pour que le document soit signé. Vous pouvez saisir leur adresse e-mail ou leur numéro de téléphone.'
settings: 'Paramètres'
settings_account_description: "Grâce à des paramètres de compte complets, vous pouvez personnaliser l'expérience de signature électronique et inviter d'autres utilisateurs."
support: 'Assistance'
support_description: 'Vous pouvez utiliser notre assistant IA ou nous écrire à support@docuseal.com si vous avez des questions.'
settings_template_description: 'Utilisez ce bouton pour accéder à la page des paramètres de votre compte.'
doorkeeper: doorkeeper:
scopes: scopes:
write: Mettre à jour vos données write: Mettre à jour vos données
read: Lire vos données read: Lire vos données
pt: &pt pt: &pt
always_enforce_signing_order: Sempre impor a ordem de assinatura
bcc_recipients: Destinatários BCC
edit_per_party: Edita por festa edit_per_party: Edita por festa
signed: Assinado signed: Assinado
reply_to: Responder a reply_to: Responder a
@@ -3072,7 +3246,7 @@ pt: &pt
connect_salesforce: Conectar Salesforce connect_salesforce: Conectar Salesforce
danger_zone: Zona de perigo danger_zone: Zona de perigo
delete_my_account: Excluir minha conta delete_my_account: Excluir minha conta
schedule_account_for_deletion_: Agendar exclusão da conta? you_are_scheduling_your_account_for_deletion_after_deletion_your_data_will_be_permanently_removed_and_cannot_be_recovered_click_ok_if_you_would_like_to_continue: "Você está agendando a exclusão da sua conta. Após a exclusão, seus dados serão permanentemente removidos e não poderão ser recuperados.\n\nClique em OK se desejar continuar."
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_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. 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.
@@ -3340,7 +3514,7 @@ pt: &pt
share_template_with_test_mode: Compartilhar modelo com o modo 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 users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'O limite de %{users_count}/%{total_users_count} usuários Pro foi atingido. Para convidar mais usuários, adquira mais licenças Pro através do botão "Gerenciar plano".'
move_into_folder: Mover para pasta move_into_folder: Mover para pasta
new_folder_name: Novo nome da pasta new_folder_name: Novo nome da pasta
exit_preview: Sair da pré-visualização exit_preview: Sair da pré-visualização
@@ -3381,8 +3555,6 @@ pt: &pt
team_accounts: Contas de equipe team_accounts: Contas de equipe
tenant_account: Conta de locatário tenant_account: Conta de locatário
tenant_accounts: Contas de locatário tenant_accounts: Contas de locatário
upgrade_plan: Fazer upgrade do plano
add_user: Adicionar usuário
impersonate: Usurpar impersonate: Usurpar
loading: Carregando loading: Carregando
documents: Documentos documents: Documentos
@@ -3609,6 +3781,9 @@ pt: &pt
pro_user_seats_used: Lugares de usuário Pro em uso pro_user_seats_used: Lugares de usuário Pro em uso
manage_plan: Gerenciar plano manage_plan: Gerenciar plano
this_submission_has_multiple_signers_which_prevents_the_use_of_a_sharing_link_html: 'Este envio tem vários signatários, o que impede o uso de um link de compartilhamento, pois não está claro qual signatário é responsável por quais campos. Para resolver isso, siga este <a href="https://www.docuseal.com/resources/pre-filling-recipients" class="link font-bold" rel="noopener noreferrer nofollow" target="_blank">guia</a> para definir os detalhes padrão do signatário.' this_submission_has_multiple_signers_which_prevents_the_use_of_a_sharing_link_html: 'Este envio tem vários signatários, o que impede o uso de um link de compartilhamento, pois não está claro qual signatário é responsável por quais campos. Para resolver isso, siga este <a href="https://www.docuseal.com/resources/pre-filling-recipients" class="link font-bold" rel="noopener noreferrer nofollow" target="_blank">guia</a> para definir os detalhes padrão do signatário.'
welcome_to_docuseal: Bem-vindo ao DocuSeal
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: Comece um tour rápido para aprender a criar e enviar seu primeiro documento.
start_tour: Iniciar tour
submission_sources: submission_sources:
api: API api: API
bulk: Envio em massa bulk: Envio em massa
@@ -3647,12 +3822,55 @@ pt: &pt
or: Ou or: Ou
download: baixe download: baixe
a_sample_spreadsheet_to_fill_and_import: uma planilha de exemplo para preencher e importar. a_sample_spreadsheet_to_fill_and_import: uma planilha de exemplo para preencher e importar.
app_tour:
start: Iniciar
previous: Anterior
next: Próximo
template_and_submissions: 'Modelos e Envios'
template_and_submissions_description: "Você pode selecionar a visualização que melhor se adapta ao seu fluxo de trabalho. Escolha a visualização 'Modelos' se criar modelos de documentos reutilizáveis, ou 'Envios' ao assinar documentos individuais ou revisar o status de cada solicitação de assinatura."
upload_a_pdf_file: 'Enviar um arquivo PDF'
upload_a_pdf_file_description: 'Envie um documento para usá-lo como modelo de formulário de assinatura.'
select_a_signer_party: 'Selecionar parte assinante'
select_a_signer_party_description: 'Este menu suspenso permite selecionar um papel de assinante ou adicionar uma nova parte ao documento quando ele precisa ser assinado por duas ou mais partes.'
available_parties: 'Partes disponíveis'
available_parties_description: 'Esta lista contém todas as partes disponíveis com seus respectivos papéis. Após selecionar, você poderá adicionar e configurar os campos para essa parte.'
available_field_types: 'Tipos de campo disponíveis'
available_field_types_description: 'Esta seção contém todos os tipos de campos que podem ser adicionados ao documento para entrada de dados ou coleta de assinaturas.'
text_input_field: 'Campo de texto'
text_input_field_description: 'Este campo permite que os usuários insiram informações baseadas em texto, como nomes ou e-mails.'
signature_field: 'Campo de assinatura'
signature_field_description: 'Este campo permite coletar assinaturas dos signatários.'
added_fields: 'Campos adicionados'
added_fields_description: 'Esta lista exibe todos os campos adicionados ao documento. Você pode reordená-los, editá-los ou removê-los.'
open_field_settings: 'Abrir configurações do campo'
open_field_settings_description: 'À direita do nome do campo há um ícone de configurações que abre as configurações do campo.'
field_settings: 'Configurações do campo'
field_settings_description: 'Cada campo pode ser personalizado de acordo com suas necessidades, como ajustar o tamanho da fonte, torná-lo obrigatório ou adicionar uma descrição.'
send_document: 'Enviar documento para assinatura'
send_document_description: 'Antes de enviar o documento para assinatura, você pode configurar os destinatários, adicionar e-mails ou números de telefone e personalizar o conteúdo do e-mail.'
sign_yourself: 'Assinar o documento você mesmo'
sign_yourself_description: 'Este botão permite que você adicione sua própria assinatura aos documentos que exigem sua assinatura.'
copy_and_share_link: 'Copiar e compartilhar link'
copy_and_share_link_description: 'Copie este link para compartilhar o documento. Qualquer pessoa com o link pode assiná-lo após inserir seu e-mail.'
sign_the_document: 'Assinar o documento'
sign_the_document_description: 'Se você for um dos signatários, este botão permite que você assine o documento.'
send_for_signing: 'Enviar para assinatura'
add_recipients: 'Adicionar destinatários'
add_recipients_description: 'Adicione novos destinatários para que o documento seja assinado. Você pode inserir seus e-mails ou números de telefone.'
settings: 'Configurações'
settings_account_description: 'Com configurações completas de conta, você pode personalizar a experiência de assinatura eletrônica e convidar mais usuários.'
support: 'Suporte'
support_description: 'Você pode usar nosso assistente de IA ou nos escrever para support@docuseal.com se tiver dúvidas.'
settings: 'Configurações'
settings_template_description: 'Use este botão para acessar a página de configurações da sua conta.'
doorkeeper: doorkeeper:
scopes: scopes:
write: Atualizar seus dados write: Atualizar seus dados
read: Ler seus dados read: Ler seus dados
de: &de de: &de
always_enforce_signing_order: Immer die Reihenfolge der Unterschriften erzwingen
bcc_recipients: BCC-Empfänger
edit_per_party: Bearbeiten pro Partei edit_per_party: Bearbeiten pro Partei
signed: Unterschrieben signed: Unterschrieben
reply_to: Antworten auf reply_to: Antworten auf
@@ -3799,7 +4017,7 @@ de: &de
connect_salesforce: Salesforce verbinden connect_salesforce: Salesforce verbinden
danger_zone: Gefahrenzone danger_zone: Gefahrenzone
delete_my_account: Mein Konto löschen delete_my_account: Mein Konto löschen
schedule_account_for_deletion_: Konto zur Löschung einplanen? you_are_scheduling_your_account_for_deletion_after_deletion_your_data_will_be_permanently_removed_and_cannot_be_recovered_click_ok_if_you_would_like_to_continue: "Du planst die Löschung deines Kontos. Nach der Löschung werden deine Daten dauerhaft entfernt und können nicht wiederhergestellt werden.\n\nKlicke auf OK, wenn du fortfahren möchtest."
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_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. 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.
@@ -4067,7 +4285,7 @@ de: &de
share_template_with_test_mode: Vorlage mit dem Testmodus 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 users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'Das Limit von %{users_count}/%{total_users_count} Pro-Benutzern wurde erreicht. Um weitere Benutzer einzuladen, kaufen Sie bitte zusätzliche Pro-Benutzerplätze über die Schaltfläche "Plan verwalten".'
move_into_folder: In Ordner verschieben move_into_folder: In Ordner verschieben
new_folder_name: Neuer Ordnername new_folder_name: Neuer Ordnername
exit_preview: Vorschau beenden exit_preview: Vorschau beenden
@@ -4108,8 +4326,6 @@ de: &de
team_accounts: Teamkonten team_accounts: Teamkonten
tenant_account: Mieterkonto tenant_account: Mieterkonto
tenant_accounts: Mieterkonten tenant_accounts: Mieterkonten
upgrade_plan: Plan upgraden
add_user: Benutzer hinzufügen
impersonate: Nachahmen impersonate: Nachahmen
loading: Wird geladen loading: Wird geladen
documents: Dokumente documents: Dokumente
@@ -4336,6 +4552,9 @@ de: &de
pro_user_seats_used: Verwendete Pro-Benutzerplätze pro_user_seats_used: Verwendete Pro-Benutzerplätze
manage_plan: Plan verwalten manage_plan: Plan verwalten
this_submission_has_multiple_signers_which_prevents_the_use_of_a_sharing_link_html: 'Diese Übermittlung hat mehrere Unterzeichner, was die Nutzung eines Freigabelinks verhindert, da unklar ist, welcher Unterzeichner für welche Felder verantwortlich ist. Um dies zu lösen, folgen Sie dieser <a href="https://www.docuseal.com/resources/pre-filling-recipients" class="link font-bold" rel="noopener noreferrer nofollow" target="_blank">Anleitung</a>, um die Standarddetails des Unterzeichners festzulegen.' this_submission_has_multiple_signers_which_prevents_the_use_of_a_sharing_link_html: 'Diese Übermittlung hat mehrere Unterzeichner, was die Nutzung eines Freigabelinks verhindert, da unklar ist, welcher Unterzeichner für welche Felder verantwortlich ist. Um dies zu lösen, folgen Sie dieser <a href="https://www.docuseal.com/resources/pre-filling-recipients" class="link font-bold" rel="noopener noreferrer nofollow" target="_blank">Anleitung</a>, um die Standarddetails des Unterzeichners festzulegen.'
welcome_to_docuseal: Willkommen bei DocuSeal
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: Starte eine kurze Tour, um zu lernen, wie du dein erstes Dokument erstellst und versendest.
start_tour: Starten
submission_sources: submission_sources:
api: API api: API
bulk: Massenversand bulk: Massenversand
@@ -4374,6 +4593,46 @@ de: &de
or: Oder or: Oder
download: laden download: laden
a_sample_spreadsheet_to_fill_and_import: Sie eine Beispieltabelle herunter, um sie auszufüllen und zu importieren. a_sample_spreadsheet_to_fill_and_import: Sie eine Beispieltabelle herunter, um sie auszufüllen und zu importieren.
app_tour:
start: Start
previous: Zurück
next: Weiter
template_and_submissions: 'Vorlagen und Übermittlungen'
template_and_submissions_description: "Du kannst die Ansicht wählen, die am besten zu deinem Arbeitsablauf passt. Wähle die Ansicht 'Vorlagen', wenn du wiederverwendbare Dokumentenvorlagen erstellst, oder 'Übermittlungen', wenn du einzelne Dokumente unterschreibst oder den Status jeder Signaturanfrage überprüfen möchtest."
upload_a_pdf_file: 'PDF-Datei hochladen'
upload_a_pdf_file_description: 'Lade ein Dokument hoch, um es als Vorlage für ein Signaturformular zu verwenden.'
select_a_signer_party: 'Signaturpartei auswählen'
select_a_signer_party_description: 'Dieses Dropdown-Menü ermöglicht es dir, eine Signaturrolle auszuwählen oder eine neue Partei zum Dokument hinzuzufügen, wenn es von zwei oder mehr Parteien unterzeichnet werden muss.'
available_parties: 'Verfügbare Parteien'
available_parties_description: 'Diese Liste enthält alle verfügbaren Parteien mit ihren Rollennamen. Nach der Auswahl kannst du Felder für die Partei hinzufügen und konfigurieren.'
available_field_types: 'Verfügbare Feldtypen'
available_field_types_description: 'Dieser Bereich enthält alle verfügbaren Feldtypen, die dem Dokument für Dateneingabe oder Signaturerfassung hinzugefügt werden können.'
text_input_field: 'Textfeld'
text_input_field_description: 'Dieses Feld ermöglicht es Benutzern, textbasierte Informationen wie Namen oder E-Mails einzugeben.'
signature_field: 'Signaturfeld'
signature_field_description: 'Dieses Feld dient der Erfassung von Signaturen der Unterzeichner.'
added_fields: 'Hinzugefügte Felder'
added_fields_description: 'Diese Liste zeigt alle dem Dokument hinzugefügten Felder. Du kannst sie neu anordnen, bearbeiten oder entfernen.'
open_field_settings: 'Feld-Einstellungen öffnen'
open_field_settings_description: 'Rechts neben dem Feldnamen befindet sich ein Zahnradsymbol, um die Einstellungen des Feldes zu öffnen.'
field_settings: 'Feld-Einstellungen'
field_settings_description: 'Jedes Feld kann individuell angepasst werden, z. B. durch Schriftgröße, Pflichtfeld-Option oder Beschreibung.'
send_document: 'Dokument zum Unterschreiben senden'
send_document_description: 'Bevor du das Dokument zum Unterschreiben sendest, kannst du Empfänger hinzufügen, ihre E-Mail-Adressen oder Telefonnummern eingeben und den E-Mail-Inhalt anpassen.'
sign_yourself: 'Dokument selbst unterschreiben'
sign_yourself_description: 'Mit diesem Button kannst du deine eigene Unterschrift zu Dokumenten hinzufügen, die deine Signatur erfordern.'
copy_and_share_link: 'Link kopieren und teilen'
copy_and_share_link_description: 'Kopiere diesen Link, um das Dokument zu teilen. Jeder mit dem Link kann es unterschreiben, nachdem er seine E-Mail-Adresse eingegeben hat.'
sign_the_document: 'Dokument unterschreiben'
sign_the_document_description: 'Wenn du einer der Unterzeichner bist, kannst du das Dokument mit diesem Button selbst unterschreiben.'
send_for_signing: 'Zum Unterschreiben senden'
add_recipients: 'Empfänger hinzufügen'
add_recipients_description: 'Füge neue Empfänger hinzu, damit das Dokument unterschrieben werden kann. Du kannst deren E-Mail-Adressen oder Telefonnummern eingeben.'
settings: 'Einstellungen'
settings_account_description: 'Mit umfangreichen Kontoeinstellungen kannst du das Signiererlebnis anpassen und weitere Benutzer einladen.'
support: 'Support'
support_description: 'Du kannst unseren KI-Assistenten nutzen oder uns bei Fragen an support@docuseal.com schreiben.'
settings_template_description: 'Mit diesem Button gelangst du zu den Kontoeinstellungen.'
doorkeeper: doorkeeper:
scopes: scopes:
write: Aktualisiere deine Daten write: Aktualisiere deine Daten
+1 -1
View File
@@ -28,7 +28,7 @@ module ActionMailerConfigsInterceptor
end end
unless Docuseal.multitenant? unless Docuseal.multitenant?
email_configs = EncryptedConfig.find_by(key: EncryptedConfig::EMAIL_SMTP_KEY) email_configs = EncryptedConfig.order(:account_id).find_by(key: EncryptedConfig::EMAIL_SMTP_KEY)
if email_configs if email_configs
message.delivery_method(:smtp, build_smtp_configs_hash(email_configs)) message.delivery_method(:smtp, build_smtp_configs_hash(email_configs))
+16
View File
@@ -196,4 +196,20 @@ module Submissions
end end
end.exclude?(false) end.exclude?(false)
end end
def regenerate_documents(submission)
submitters = submission.submitters.where.not(completed_at: nil).preload(:documents_attachments)
submitters.each { |submitter| submitter.documents.each(&:destroy!) }
submission.submitters.where.not(completed_at: nil).order(:completed_at).each do |submitter|
GenerateResultAttachments.call(submitter)
end
return if submission.combined_document_attachment.blank?
submission.combined_document_attachment.destroy!
Submissions::GenerateCombinedAttachment.call(submission.submitters.completed.order(:completed_at).last)
end
end end
@@ -7,11 +7,15 @@ module Submissions
CHECK_COMPLETE_TIMEOUT = 90.seconds CHECK_COMPLETE_TIMEOUT = 90.seconds
WaitForCompleteTimeout = Class.new(StandardError) WaitForCompleteTimeout = Class.new(StandardError)
NotCompletedYet = Class.new(StandardError)
module_function module_function
def call(submitter) def call(submitter)
return [] unless submitter return [] unless submitter
raise NotCompletedYet unless submitter.completed_at?
return submitter.documents if ApplicationRecord.uncached { submitter.document_generation_events.complete.exists? } return submitter.documents if ApplicationRecord.uncached { submitter.document_generation_events.complete.exists? }
events = events =
+2 -1
View File
@@ -58,7 +58,8 @@ module Submissions
end end
def build_table_rows(submissions) def build_table_rows(submissions)
submissions.map do |submission| submissions.preload(submitters: [attachments_attachments: :blob, documents_attachments: :blob])
.find_each.map do |submission|
submission_data = [] submission_data = []
submitters_count = submission.submitters.size submitters_count = submission.submitters.size
+3
View File
@@ -8,6 +8,7 @@ module Submitters
AccountConfig::FORM_PREFILL_SIGNATURE_KEY, AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
AccountConfig::WITH_SIGNATURE_ID, AccountConfig::WITH_SIGNATURE_ID,
AccountConfig::ALLOW_TO_DECLINE_KEY, AccountConfig::ALLOW_TO_DECLINE_KEY,
AccountConfig::ENFORCE_SIGNING_ORDER_KEY,
AccountConfig::REQUIRE_SIGNING_REASON_KEY, AccountConfig::REQUIRE_SIGNING_REASON_KEY,
AccountConfig::REUSE_SIGNATURE_KEY, AccountConfig::REUSE_SIGNATURE_KEY,
AccountConfig::ALLOW_TYPED_SIGNATURE, AccountConfig::ALLOW_TYPED_SIGNATURE,
@@ -27,6 +28,7 @@ module Submitters
with_decline = find_safe_value(configs, AccountConfig::ALLOW_TO_DECLINE_KEY) != false with_decline = find_safe_value(configs, AccountConfig::ALLOW_TO_DECLINE_KEY) != false
with_signature_id = find_safe_value(configs, AccountConfig::WITH_SIGNATURE_ID) == true with_signature_id = find_safe_value(configs, AccountConfig::WITH_SIGNATURE_ID) == true
require_signing_reason = find_safe_value(configs, AccountConfig::REQUIRE_SIGNING_REASON_KEY) == true require_signing_reason = find_safe_value(configs, AccountConfig::REQUIRE_SIGNING_REASON_KEY) == true
enforce_signing_order = find_safe_value(configs, AccountConfig::ENFORCE_SIGNING_ORDER_KEY) == true
policy_links = find_safe_value(configs, AccountConfig::POLICY_LINKS_KEY) policy_links = find_safe_value(configs, AccountConfig::POLICY_LINKS_KEY)
attrs = { attrs = {
@@ -36,6 +38,7 @@ module Submitters
reuse_signature:, reuse_signature:,
with_decline:, with_decline:,
policy_links:, policy_links:,
enforce_signing_order:,
completed_message:, completed_message:,
require_signing_reason:, require_signing_reason:,
prefill_signature:, prefill_signature:,
+68 -25
View File
@@ -3,12 +3,14 @@
module Submitters module Submitters
module SubmitValues module SubmitValues
ValidationError = Class.new(StandardError) ValidationError = Class.new(StandardError)
RequiredFieldError = Class.new(StandardError)
VARIABLE_REGEXP = /\{\{?(\w+)\}\}?/ VARIABLE_REGEXP = /\{\{?(\w+)\}\}?/
NONEDITABLE_FIELD_TYPES = %w[stamp heading].freeze
module_function module_function
def call(submitter, params, request) def call(submitter, params, request, validate_required: true)
Submissions.update_template_fields!(submitter.submission) if submitter.submission.template_fields.blank? Submissions.update_template_fields!(submitter.submission) if submitter.submission.template_fields.blank?
unless submitter.submission_events.exists?(event_type: 'start_form') unless submitter.submission_events.exists?(event_type: 'start_form')
@@ -20,7 +22,7 @@ module Submitters
end end
end end
update_submitter!(submitter, params, request) update_submitter!(submitter, params, request, validate_required:)
submitter.submission.save! submitter.submission.save!
@@ -29,13 +31,13 @@ module Submitters
submitter submitter
end end
def update_submitter!(submitter, params, request) def update_submitter!(submitter, params, request, validate_required: true)
values = normalized_values(params) values = normalized_values(params)
submitter.values.merge!(values) submitter.values.merge!(values)
submitter.opened_at ||= Time.current submitter.opened_at ||= Time.current
assign_completed_attributes(submitter, request) if params[:completed] == 'true' assign_completed_attributes(submitter, request, validate_required:) if params[:completed] == 'true'
ApplicationRecord.transaction do ApplicationRecord.transaction do
maybe_set_signature_reason!(values, submitter, params) maybe_set_signature_reason!(values, submitter, params)
@@ -49,25 +51,36 @@ module Submitters
submitter submitter
end end
def assign_completed_attributes(submitter, request) def assign_completed_attributes(submitter, request, validate_required: true)
submitter.completed_at = Time.current submitter.completed_at = Time.current
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 = maybe_remove_condition_values(submitter)
required_field_uuids_acc = Set.new
submitter.values = maybe_remove_condition_values(submitter, required_field_uuids_acc:)
formula_values = build_formula_values(submitter) formula_values = build_formula_values(submitter)
if formula_values.present? if formula_values.present?
submitter.values = submitter.values.merge(formula_values) submitter.values = submitter.values.merge(formula_values)
submitter.values = maybe_remove_condition_values(submitter) submitter.values = maybe_remove_condition_values(submitter, required_field_uuids_acc:)
end end
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
required_field_uuids_acc.each do |uuid|
next if submitter.values[uuid].present?
raise RequiredFieldError, uuid if validate_required
Rollbar.warning("Required field #{submitter.id}: #{uuid}") if defined?(Rollbar)
end
submitter submitter
end end
@@ -188,37 +201,67 @@ module Submitters
with_time:) with_time:)
end end
def maybe_remove_condition_values(submitter) def maybe_remove_condition_values(submitter, required_field_uuids_acc: nil)
fields_uuid_index = submitter.submission.template_fields.index_by { |e| e['uuid'] } submission = submitter.submission
fields_uuid_index = submission.template_fields.index_by { |e| e['uuid'] }
submitters_values = nil
has_other_submitters = submission.template_submitters.size > 1
has_document_conditions =
(submission.template_schema || submission.template.schema).any? { |e| e['conditions'].present? }
attachments_index = attachments_index =
Submissions.filtered_conditions_schema(submitter.submission).index_by { |i| i['attachment_uuid'] } if has_document_conditions
Submissions.filtered_conditions_schema(submission).index_by { |i| i['attachment_uuid'] }
submitter_values = nil
is_other_submitter_conditions = submitter.submission.template_submitters.size > 1
submitter.submission.template_fields.each do |field|
next if field['submitter_uuid'] != submitter.uuid
submitter_values ||= 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 end
submitter.values.delete(field['uuid']) unless check_field_conditions(submitter_values, field, fields_uuid_index) submission.template_fields.each do |field|
next if field['submitter_uuid'] != submitter.uuid
if field['areas'].present? && field['areas'].none? { |area| attachments_index[area['attachment_uuid']] } required_field_uuids_acc.add(field['uuid']) if required_field_uuids_acc && required_editable_field?(field)
if has_document_conditions && !check_field_areas_attachments(field, attachments_index)
submitter.values.delete(field['uuid']) submitter.values.delete(field['uuid'])
required_field_uuids_acc.delete(field['uuid'])
end
if has_other_submitters && !submitters_values &&
field_conditions_other_submitter?(submitter, field, fields_uuid_index)
submitters_values = merge_submitters_values(submitter)
end
unless check_field_conditions(submitters_values || submitter.values, field, fields_uuid_index)
submitter.values.delete(field['uuid'])
required_field_uuids_acc.delete(field['uuid'])
end end
end end
submitter.values submitter.values
end end
def required_editable_field?(field)
return false if NONEDITABLE_FIELD_TYPES.include?(field['type'])
field['required'].present? && field['readonly'].blank?
end
def check_field_areas_attachments(field, attachments_index)
return true if field['areas'].blank?
field['areas'].any? { |area| attachments_index[area['attachment_uuid']] }
end
def merge_submitters_values(submitter)
submitter.submission.submitters
.reduce({}) { |acc, sub| acc.merge(sub.values) }
.merge(submitter.values)
end
def field_conditions_other_submitter?(submitter, field, fields_uuid_index) def field_conditions_other_submitter?(submitter, field, fields_uuid_index)
return false if field['conditions'].blank?
field['conditions'].to_a.any? do |c| field['conditions'].to_a.any? do |c|
fields_uuid_index.dig(c['field_uuid'], 'submitter_uuid') != submitter.uuid fields_uuid_index.dig(c['field_uuid'], 'submitter_uuid') != submitter.uuid
end end
+1 -1
View File
@@ -190,7 +190,7 @@ module Templates
} }
elsif field.field_type == :Tx elsif field.field_type == :Tx
if field[:AA] && ((field[:AA][:F] && field[:AA][:F][:JS].include?('AFDate_')) || if field[:AA] && ((field[:AA][:F] && field[:AA][:F][:JS].include?('AFDate_')) ||
(field[:AA][:K] && field[:AA][:F][:JS].include?('AFDate_'))) (field[:AA][:K] && field[:AA][:K][:JS].include?('AFDate_')))
if (format = field[:AA][:F][:JS][DATE_FORMAT_REGEXP]) if (format = field[:AA][:F][:JS][DATE_FORMAT_REGEXP])
attrs[:preferences] ||= {} attrs[:preferences] ||= {}
attrs[:preferences][:format] = format.upcase attrs[:preferences][:format] = format.upcase
+1
View File
@@ -21,6 +21,7 @@
"css-loader": "^6.7.3", "css-loader": "^6.7.3",
"css-minimizer-webpack-plugin": "^5.0.0", "css-minimizer-webpack-plugin": "^5.0.0",
"daisyui": "^3.9.4", "daisyui": "^3.9.4",
"driver.js": "^1.3.5",
"mathjs": "^12.4.0", "mathjs": "^12.4.0",
"mini-css-extract-plugin": "^2.7.5", "mini-css-extract-plugin": "^2.7.5",
"postcss": "^8.4.31", "postcss": "^8.4.31",
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

+16 -1
View File
@@ -104,6 +104,21 @@ describe 'Submission API', type: :request do
expect(response.parsed_body).to eq(JSON.parse(create_submission_body(submission).to_json)) expect(response.parsed_body).to eq(JSON.parse(create_submission_body(submission).to_json))
end end
it 'creates a submission when the submitter is marked as completed' do
post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: {
template_id: templates[0].id,
submitters: [{ role: 'First Party', email: 'john.doe@example.com', completed: true }]
}.to_json
expect(response).to have_http_status(:ok)
submission = Submission.last
submitter = submission.submitters.first
expect(submitter.status).to eq('completed')
expect(submitter.completed_at).not_to be_nil
end
it 'creates a submission when some submitter roles are not provided' do it 'creates a submission when some submitter roles are not provided' do
post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: { post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: {
template_id: multiple_submitters_template.id, template_id: multiple_submitters_template.id,
@@ -395,7 +410,7 @@ describe 'Submission API', type: :request do
preferences: { send_email: true, send_sms: false }, preferences: { send_email: true, send_sms: false },
role: submitter.template.submitters.find { |s| s['uuid'] == submitter.uuid }['name'], role: submitter.template.submitters.find { |s| s['uuid'] == submitter.uuid }['name'],
embed_src: "#{Docuseal::DEFAULT_APP_URL}/s/#{submitter.slug}", embed_src: "#{Docuseal::DEFAULT_APP_URL}/s/#{submitter.slug}",
values: [] values: Submitters::SerializeForWebhook.build_values_array(submitter)
} }
end end
end end
+19 -2
View File
@@ -95,6 +95,23 @@ describe 'Submitter API', type: :request do
expect(submitter.email).to eq('john.doe+updated@example.com') expect(submitter.email).to eq('john.doe+updated@example.com')
expect(response.parsed_body).to eq(JSON.parse(update_submitter_body(submitter).to_json)) expect(response.parsed_body).to eq(JSON.parse(update_submitter_body(submitter).to_json))
end end
it 'marks a submitter as completed' do
submitter = create(:submission, :with_submitters, :with_events,
template: templates[0],
created_by_user: author).submitters.first
put "/api/submitters/#{submitter.id}", headers: { 'x-auth-token': author.access_token.token }, params: {
completed: true
}.to_json
expect(response).to have_http_status(:ok)
submitter.reload
expect(submitter.status).to eq('completed')
expect(submitter.completed_at).not_to be_nil
end
end end
private private
@@ -134,8 +151,8 @@ describe 'Submitter API', type: :request do
data: event.data.slice(:reason) data: event.data.slice(:reason)
} }
end, end,
values: [], values: Submitters::SerializeForWebhook.build_values_array(submitter),
documents: [], documents: Submitters::SerializeForWebhook.build_documents_array(submitter),
role: submitter.template.submitters.find { |s| s['uuid'] == submitter.uuid }['name'] role: submitter.template.submitters.find { |s| s['uuid'] == submitter.uuid }['name']
} }
end end
+38 -10
View File
@@ -4,21 +4,49 @@ module SigningFormHelper
module_function module_function
def draw_canvas def draw_canvas
page.find('canvas').click([], { x: 150, y: 100 })
page.execute_script <<~JS page.execute_script <<~JS
const canvas = document.getElementsByTagName('canvas')[0]; const canvas = document.getElementsByTagName('canvas')[0];
const ctx = canvas.getContext('2d'); const rect = canvas.getBoundingClientRect();
ctx.beginPath(); const startX = rect.left + 50;
ctx.moveTo(150, 100); const startY = rect.top + 100;
ctx.lineTo(450, 100);
ctx.stroke();
ctx.beginPath(); const amplitude = 20;
ctx.moveTo(150, 100); const wavelength = 30;
ctx.lineTo(150, 150); const length = 300;
ctx.stroke();
function dispatchPointerEvent(type, x, y) {
const event = new PointerEvent(type, {
pointerId: 1,
pointerType: 'pen',
isPrimary: true,
clientX: x,
clientY: y,
bubbles: true,
pressure: 0.5
});
canvas.dispatchEvent(event);
}
dispatchPointerEvent('pointerdown', startX, startY);
let x = 0;
function drawStep() {
if (x > length) {
dispatchPointerEvent('pointerup', startX + x, startY);
return;
}
const y = startY + amplitude * Math.sin((x / wavelength) * 2 * Math.PI);
dispatchPointerEvent('pointermove', startX + x, y);
x += 5;
requestAnimationFrame(drawStep);
}
drawStep();
JS JS
sleep 0.5 sleep 0.5
end end
+13
View File
@@ -360,6 +360,19 @@ RSpec.describe 'Signing Form', type: :system do
expect(field_value(submitter, 'Signature')).to be_present expect(field_value(submitter, 'Signature')).to be_present
end end
it 'shows an error message if the canvas is not drawn or too simple' do
visit submit_form_path(slug: submitter.slug)
find('#expand_form_button').click
page.find('canvas').click([], { x: 150, y: 100 })
alert_text = page.accept_alert do
click_button 'Sign and Complete'
end
expect(alert_text).to eq 'Signature is too small or simple. Please redraw.'
end
it 'completes the form if the canvas is typed' do it 'completes the form if the canvas is typed' do
visit submit_form_path(slug: submitter.slug) visit submit_form_path(slug: submitter.slug)
+5
View File
@@ -2678,6 +2678,11 @@ domutils@^3.0.1:
domelementtype "^2.3.0" domelementtype "^2.3.0"
domhandler "^5.0.3" domhandler "^5.0.3"
driver.js@^1.3.5:
version "1.3.5"
resolved "https://registry.yarnpkg.com/driver.js/-/driver.js-1.3.5.tgz#70695fd2b3b15a3db409c8a37d9b3a563a7c176a"
integrity sha512-exkp49hXuujvTOZ3zYgySWRlEAa8/3nA8glYjtuZjmkTdsQITXivBsW1ytyhKQx3WkeYaovlnvVcLbtTaN86kA==
duplexer@^0.1.2: duplexer@^0.1.2:
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6"