Compare commits

..
46 Commits
Author SHA1 Message Date
Alex Turchyn 786c80bee7 Merge pull from docusealco/wip 2025-04-28 12:46:30 +03:00
Pete Matsyburka 98dcce299e resend 2025-04-28 08:59:41 +03:00
Pete Matsyburka cbff242a5d adjust condition fields 2025-04-25 12:14:49 +03:00
Pete Matsyburka 1876b3efba update readme 2025-04-24 19:44:49 +03:00
Alex Turchyn 51091f05f0 add Japanese language to the signing form 2025-04-24 19:38:46 +03:00
Pete Matsyburka d76eee2ae4 add valign 2025-04-24 19:33:26 +03:00
Pete Matsyburka d7bfa96d57 hide order on mobile 2025-04-24 12:59:31 +03:00
Pete Matsyburka be60608211 fix remove listener 2025-04-23 21:24:18 +03:00
Pete Matsyburka 4e5abb3c09 fix query 2025-04-23 12:30:06 +03:00
Pete Matsyburka 7fee82ded2 fix order queries 2025-04-23 10:58:40 +03:00
Alex Turchyn e1cd62ff40 add folders/templates sorting 2025-04-22 21:16:17 +03:00
Pete Matsyburka b498291b62 update gem 2025-04-22 16:20:35 +03:00
Pete Matsyburka def3009846 fix cert disabled 2025-04-22 15:52:03 +03:00
Pete Matsyburka 9d43fd9aa5 fix fields 2025-04-18 18:18:57 +03:00
Pete Matsyburka 22a21fb037 fix field focus 2025-04-18 13:02:17 +03:00
Pete Matsyburka 2a9c375273 adjust header 2025-04-18 12:57:43 +03:00
Alex Turchyn 847e70ec1b improve signing form header 2025-04-18 12:16:11 +03:00
Pete Matsyburka 7964d6e0f0 improve signature validation 2025-04-18 11:34:10 +03:00
Pete Matsyburka d0259791bf add field value email variable 2025-04-16 21:19:13 +03:00
Pete Matsyburka a9dcaeccbc fix erblint 2025-04-16 11:35:24 +03:00
Pete Matsyburka 501b91a603 removed email 2025-04-16 11:35:20 +03:00
Pete Matsyburka 2b5f772feb update ai link 2025-04-15 21:40:22 +03:00
Pete Matsyburka 7cada0af8b older devises 2025-04-15 19:58:31 +03:00
Pete Matsyburka 67be785e69 add slug filter 2025-04-15 13:22:02 +03:00
Pete Matsyburka d1cfc64adc adjust api response 2025-04-14 12:14:24 +03:00
Pete Matsyburka 4e5157d1ee add text valign 2025-04-11 16:25:32 +03:00
Pete Matsyburka 5e27cc36c6 fix default font 2025-04-10 13:22:10 +03:00
Pete Matsyburka 6e81a3be3f fix i18n 2025-04-10 11:51:40 +03:00
Pete Matsyburka 858b2f33fd ensure unique recipients 2025-04-10 00:56:16 +03:00
Alex Turchyn 6adcc87b2b add translations 2025-04-09 20:45:17 +03:00
Alex Turchyn e1d5a5b6a6 prevent an infinite loop of field conditions 2025-04-09 14:04:42 +03:00
Pete Matsyburka f090e79002 fix required 2025-04-09 13:51:43 +03:00
Pete Matsyburka 56b32e35a2 make last name optional 2025-04-08 19:12:04 +03:00
Pete Matsyburka b40c27cdbf fix specs 2025-04-08 16:21:45 +03:00
Pete Matsyburka c4505e67f0 require 2fa placeholder 2025-04-08 15:24:17 +03:00
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
96 changed files with 1994 additions and 274 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
+7 -7
View File
@@ -338,18 +338,18 @@ GEM
net-smtp (0.5.0) net-smtp (0.5.0)
net-protocol net-protocol
nio4r (2.7.4) nio4r (2.7.4)
nokogiri (1.18.5) nokogiri (1.18.8)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.5-aarch64-linux-gnu) nokogiri (1.18.8-aarch64-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.5-aarch64-linux-musl) nokogiri (1.18.8-aarch64-linux-musl)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.5-arm64-darwin) nokogiri (1.18.8-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.5-x86_64-linux-gnu) nokogiri (1.18.8-x86_64-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.5-x86_64-linux-musl) nokogiri (1.18.8-x86_64-linux-musl)
racc (~> 1.4) racc (~> 1.4)
oj (3.16.8) oj (3.16.8)
bigdecimal (>= 3.0) bigdecimal (>= 3.0)
@@ -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
+1 -1
View File
@@ -42,7 +42,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
- PDF signature verification - PDF signature verification
- Users management - Users management
- Mobile-optimized - Mobile-optimized
- 6 UI languages with signing available in 13 languages - 6 UI languages with signing available in 14 languages
- API and Webhooks for integrations - API and Webhooks for integrations
- Easy to deploy in minutes - Easy to deploy in minutes
+1 -1
View File
@@ -45,7 +45,7 @@ class AccountsController < ApplicationController
def destroy def destroy
authorize!(:manage, current_account) authorize!(:manage, current_account)
true_user.update!(locked_at: Time.current) true_user.update!(locked_at: Time.current, email: true_user.email.sub('@', '+removed@'))
# rubocop:disable Layout/LineLength # rubocop:disable Layout/LineLength
render turbo_stream: turbo_stream.replace( render turbo_stream: turbo_stream.replace(
+16 -7
View File
@@ -11,13 +11,7 @@ module Api
def index def index
submissions = Submissions.search(@submissions, params[:q]) submissions = Submissions.search(@submissions, params[:q])
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present? submissions = filter_submissions(submissions, params)
if params[:template_folder].present?
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] })
end
submissions = Submissions::Filter.call(submissions, current_user, params)
submissions = paginate(submissions.preload(:created_by_user, :submitters, submissions = paginate(submissions.preload(:created_by_user, :submitters,
template: :folder, template: :folder,
@@ -111,6 +105,21 @@ module Api
private private
def filter_submissions(submissions, params)
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
submissions = submissions.where(slug: params[:slug]) if params[:slug].present?
if params[:template_folder].present?
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] })
end
if params.key?(:archived)
submissions = params[:archived].in?(['true', true]) ? submissions.archived : submissions.active
end
Submissions::Filter.call(submissions, current_user, params)
end
def build_create_json(submissions) def build_create_json(submissions)
json = submissions.flat_map do |submission| json = submissions.flat_map do |submission|
submission.submitters.map do |s| submission.submitters.map do |s|
+14 -9
View File
@@ -7,15 +7,7 @@ module Api
def index def index
submitters = Submitters.search(@submitters, params[:q]) submitters = Submitters.search(@submitters, params[:q])
submitters = submitters.where(external_id: params[:application_key]) if params[:application_key].present? submitters = filter_submitters(submitters, params)
submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present?
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
if params[:template_id].present?
submitters = submitters.joins(:submission).where(submission: { template_id: params[:template_id] })
end
submitters = maybe_filder_by_completed_at(submitters, params)
submitters = paginate( submitters = paginate(
submitters.preload(:template, :submission, :submission_events, submitters.preload(:template, :submission, :submission_events,
@@ -163,6 +155,19 @@ module Api
submitter submitter
end end
def filter_submitters(submitters, params)
submitters = submitters.where(external_id: params[:application_key]) if params[:application_key].present?
submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present?
submitters = submitters.where(slug: params[:slug]) if params[:slug].present?
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
if params[:template_id].present?
submitters = submitters.joins(:submission).where(submission: { template_id: params[:template_id] })
end
maybe_filder_by_completed_at(submitters, params)
end
def assign_external_id(submitter, attrs) def assign_external_id(submitter, attrs)
submitter.external_id = attrs[:application_key] if attrs.key?(:application_key) submitter.external_id = attrs[:application_key] if attrs.key?(:application_key)
submitter.external_id = attrs[:external_id] if attrs.key?(:external_id) submitter.external_id = attrs[:external_id] if attrs.key?(:external_id)
@@ -90,6 +90,7 @@ module Api
templates = params[:archived].in?(['true', true]) ? templates.archived : templates.active templates = params[:archived].in?(['true', true]) ? templates.archived : templates.active
templates = templates.where(external_id: params[:application_key]) if params[:application_key].present? templates = templates.where(external_id: params[:application_key]) if params[:application_key].present?
templates = templates.where(external_id: params[:external_id]) if params[:external_id].present? templates = templates.where(external_id: params[:external_id]) if params[:external_id].present?
templates = templates.where(slug: params[:slug]) if params[:slug].present?
templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present? templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present?
templates templates
+2
View File
@@ -11,6 +11,8 @@ class StartFormController < ApplicationController
before_action :load_template before_action :load_template
def show def show
raise ActionController::RoutingError, I18n.t('not_found') if @template.preferences['require_phone_2fa'] == true
@submitter = @template.submissions.new(account_id: @template.account_id) @submitter = @template.submissions.new(account_id: @template.account_id)
.submitters.new(uuid: (filter_undefined_submitters(@template).first || .submitters.new(uuid: (filter_undefined_submitters(@template).first ||
@template.submitters.first)['uuid']) @template.submitters.first)['uuid'])
@@ -57,6 +57,11 @@ class SubmissionsController < ApplicationController
Submissions.send_signature_requests(submissions) Submissions.send_signature_requests(submissions)
redirect_to template_path(@template), notice: I18n.t('new_recipients_have_been_added') redirect_to template_path(@template), notice: I18n.t('new_recipients_have_been_added')
rescue Submissions::CreateFromSubmitters::BaseError => e
render turbo_stream: turbo_stream.replace(:submitters_error,
partial: 'submissions/error',
locals: { error: e.message }),
status: :unprocessable_entity
end end
def destroy def destroy
+13 -13
View File
@@ -7,11 +7,11 @@ class SubmitFormController < ApplicationController
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
skip_authorization_check skip_authorization_check
before_action :load_submitter, only: %i[show update completed]
CONFIG_KEYS = [].freeze CONFIG_KEYS = [].freeze
def show def show
@submitter = Submitter.find_by!(slug: params[:slug])
submission = @submitter.submission submission = @submitter.submission
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at? return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
@@ -50,44 +50,44 @@ class SubmitFormController < ApplicationController
end end
def update def update
submitter = Submitter.find_by!(slug: params[:slug]) if @submitter.completed_at?
if submitter.completed_at?
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_entity return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_entity
end end
if submitter.template.archived_at? || submitter.submission.archived_at? if @submitter.template.archived_at? || @submitter.submission.archived_at?
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_entity return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_entity
end end
if submitter.submission.expired? if @submitter.submission.expired?
return render json: { error: I18n.t('form_has_been_expired') }, status: :unprocessable_entity return render json: { error: I18n.t('form_has_been_expired') }, status: :unprocessable_entity
end end
if submitter.declined_at? if @submitter.declined_at?
return render json: { error: I18n.t('form_has_been_declined') }, return render json: { error: I18n.t('form_has_been_declined') },
status: :unprocessable_entity status: :unprocessable_entity
end end
Submitters::SubmitValues.call(submitter, params, request) Submitters::SubmitValues.call(@submitter, params, request)
head :ok head :ok
rescue Submitters::SubmitValues::RequiredFieldError => e rescue Submitters::SubmitValues::RequiredFieldError => e
Rollbar.warning("Required field #{submitter.id}: #{e.message}") if defined?(Rollbar) Rollbar.warning("Required field #{@submitter.id}: #{e.message}") if defined?(Rollbar)
render json: { field_uuid: e.message }, status: :unprocessable_entity 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
def completed def completed; end
@submitter = Submitter.completed.find_by!(slug: params[:submit_form_slug])
end
def success; end def success; end
private private
def load_submitter
@submitter = Submitter.find_by!(slug: params[:slug] || params[:submit_form_slug])
end
def build_attachments_index(submission) def build_attachments_index(submission)
ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments) ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments)
.preload(:blob).index_by(&:uuid) .preload(:blob).index_by(&:uuid)
@@ -5,8 +5,9 @@ class TemplateFoldersController < ApplicationController
def show def show
@templates = @template_folder.templates.active.accessible_by(current_ability) @templates = @template_folder.templates.active.accessible_by(current_ability)
.preload(:author, :template_accesses).order(id: :desc) .preload(:author, :template_accesses)
@templates = Templates.search(@templates, params[:q]) @templates = Templates.search(@templates, params[:q])
@templates = Templates::Order.call(@templates, current_user, cookies.permanent[:dashboard_templates_order])
@pagy, @templates = pagy(@templates, limit: 12) @pagy, @templates = pagy(@templates, limit: 12)
end end
@@ -9,9 +9,11 @@ class TemplatesDashboardController < ApplicationController
FOLDERS_PER_PAGE = 18 FOLDERS_PER_PAGE = 18
def index def index
@template_folders = @template_folders.where(id: @templates.active.select(:folder_id)).order(id: :desc) @template_folders = @template_folders.where(id: @templates.active.select(:folder_id))
@template_folders = TemplateFolders.search(@template_folders, params[:q]) @template_folders = TemplateFolders.search(@template_folders, params[:q])
@template_folders = sort_template_folders(@template_folders, current_user,
cookies.permanent[:dashboard_templates_order])
@pagy, @template_folders = pagy( @pagy, @template_folders = pagy(
@template_folders, @template_folders,
@@ -24,6 +26,7 @@ class TemplatesDashboardController < ApplicationController
else else
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME } @template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
@templates = filter_templates(@templates) @templates = filter_templates(@templates)
@templates = Templates::Order.call(@templates, current_user, cookies.permanent[:dashboard_templates_order])
limit = limit =
if @template_folders.size < 4 if @template_folders.size < 4
@@ -39,7 +42,7 @@ class TemplatesDashboardController < ApplicationController
private private
def filter_templates(templates) def filter_templates(templates)
rel = templates.active.preload(:author, :template_accesses).order(id: :desc) rel = templates.active.preload(:author, :template_accesses)
if params[:q].blank? if params[:q].blank?
if Docuseal.multitenant? && !current_account.testing? if Docuseal.multitenant? && !current_account.testing?
@@ -54,4 +57,38 @@ class TemplatesDashboardController < ApplicationController
Templates.search(rel, params[:q]) Templates.search(rel, params[:q])
end end
def sort_template_folders(template_folders, current_user, order)
case order
when 'used_at'
subquery =
Template.left_joins(:submissions)
.group(:folder_id)
.where(account_id: current_user.account_id)
.select(
:folder_id,
Template.arel_table[:updated_at].maximum.as('updated_at_max'),
Submission.arel_table[:created_at].maximum.as('submission_created_at_max')
)
template_folders = template_folders.joins(
Template.arel_table
.join(subquery.arel.as('templates'), Arel::Nodes::OuterJoin)
.on(TemplateFolder.arel_table[:id].eq(Template.arel_table[:folder_id]))
.join_sources
)
template_folders.order(
Arel::Nodes::Case.new
.when(Template.arel_table[:submission_created_at_max].gt(Template.arel_table[:updated_at_max]))
.then(Template.arel_table[:submission_created_at_max])
.else(Template.arel_table[:updated_at_max])
.desc
)
when 'name'
template_folders.order(name: :asc)
else
template_folders.order(id: :desc)
end
end
end end
@@ -24,8 +24,8 @@ class TemplatesPreferencesController < ApplicationController
documents_copy_email_enabled documents_copy_email_attach_audit documents_copy_email_enabled documents_copy_email_attach_audit
documents_copy_email_attach_documents documents_copy_email_reply_to documents_copy_email_attach_documents documents_copy_email_reply_to
completed_notification_email_attach_documents completed_notification_email_attach_documents
completed_redirect_url completed_redirect_url validate_unique_submitters
submitters_order submitters_order require_phone_2fa
completed_notification_email_subject completed_notification_email_body completed_notification_email_subject completed_notification_email_body
completed_notification_email_enabled completed_notification_email_attach_audit] + completed_notification_email_enabled completed_notification_email_attach_audit] +
[completed_message: %i[title body], [completed_message: %i[title body],
+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 -1
View File
@@ -32,6 +32,7 @@ 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'
@@ -99,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 () {
@@ -124,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)
+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 })
})
}
}
+36
View File
@@ -0,0 +1,36 @@
export default class extends HTMLElement {
connectedCallback () {
this.form.addEventListener('submit', (e) => {
e.preventDefault()
this.submit()
})
if (this.dataset.onload === 'true') {
this.form.querySelector('button').click()
}
}
submit () {
fetch(this.form.action, {
method: this.form.method,
body: new FormData(this.form)
}).then(async (resp) => {
if (!resp.ok) {
try {
const data = JSON.parse(await resp.text())
if (data.error) {
alert(data.error)
}
} catch (err) {
console.error(err)
}
}
})
}
get form () {
return this.querySelector('form')
}
}
+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
} }
+62
View File
@@ -0,0 +1,62 @@
export default class extends HTMLElement {
connectedCallback () {
this.header = document.querySelector('#signing_form_header')
window.addEventListener('scroll', this.onScroll)
window.addEventListener('resize', this.onResize)
if (!this.isNarrow() && this.isHeaderNotVisible()) {
this.showButtons({ animate: false })
}
}
disconnectedCallback () {
window.removeEventListener('scroll', this.onScroll)
window.removeEventListener('resize', this.onResize)
}
onResize = () => {
if (this.isNarrow()) {
this.hideButtons(true)
} else if (this.isHeaderNotVisible()) {
this.showButtons()
}
}
isNarrow () {
return window.innerWidth < 1230
}
onScroll = () => {
if (this.isHeaderNotVisible() && !this.isNarrow()) {
this.showButtons()
} else {
this.hideButtons()
}
}
isHeaderNotVisible () {
const rect = this.header.getBoundingClientRect()
return rect.bottom <= 0 || rect.top >= window.innerHeight
}
showButtons ({ animate } = { animate: true }) {
if (animate) {
this.classList.add('transition-transform', 'duration-300')
}
this.classList.remove('hidden', '-translate-y-10', 'opacity-0')
this.classList.add('translate-y-0', 'opacity-100')
}
hideButtons () {
this.classList.remove('translate-y-0', 'opacity-100')
this.classList.add('-translate-y-10', 'opacity-0')
setTimeout(() => {
if (this.classList.contains('-translate-y-10')) {
this.classList.add('hidden')
}
}, 300)
}
}
+4
View File
@@ -3,11 +3,15 @@ import { createApp, reactive } from 'vue'
import Form from './submission_form/form' import Form from './submission_form/form'
import DownloadButton from './elements/download_button' import DownloadButton from './elements/download_button'
import ToggleSubmit from './elements/toggle_submit' import ToggleSubmit from './elements/toggle_submit'
import FetchForm from './elements/fetch_form'
import ScrollButtons from './elements/scroll_buttons'
const safeRegisterElement = (name, element, options = {}) => !window.customElements.get(name) && window.customElements.define(name, element, options) const safeRegisterElement = (name, element, options = {}) => !window.customElements.get(name) && window.customElements.define(name, element, options)
safeRegisterElement('download-button', DownloadButton) safeRegisterElement('download-button', DownloadButton)
safeRegisterElement('toggle-submit', ToggleSubmit) safeRegisterElement('toggle-submit', ToggleSubmit)
safeRegisterElement('fetch-form', FetchForm)
safeRegisterElement('scroll-buttons', ScrollButtons)
safeRegisterElement('submission-form', class extends HTMLElement { safeRegisterElement('submission-form', class extends HTMLElement {
connectedCallback () { connectedCallback () {
this.appElem = document.createElement('div') this.appElem = document.createElement('div')
+6 -3
View File
@@ -183,7 +183,7 @@
v-else v-else
ref="textContainer" ref="textContainer"
dir="auto" dir="auto"
class="flex items-center px-0.5 w-full" class="flex px-0.5 w-full"
:class="{ ...alignClasses, ...fontClasses }" :class="{ ...alignClasses, ...fontClasses }"
> >
<span <span
@@ -327,13 +327,16 @@ export default {
}, },
alignClasses () { alignClasses () {
if (!this.field.preferences) { if (!this.field.preferences) {
return {} return { 'items-center': true }
} }
return { return {
'text-center': this.field.preferences.align === 'center', 'text-center': this.field.preferences.align === 'center',
'text-left': this.field.preferences.align === 'left', 'text-left': this.field.preferences.align === 'left',
'text-right': this.field.preferences.align === 'right' 'text-right': this.field.preferences.align === 'right',
'items-center': !this.field.preferences.valign || this.field.preferences.valign === 'center',
'items-start': this.field.preferences.valign === 'top',
'items-end': this.field.preferences.valign === 'bottom'
} }
}, },
fontClasses () { fontClasses () {
+3 -3
View File
@@ -193,9 +193,9 @@ export default {
}) })
} }
if (window.decline_button) { document.querySelectorAll('#decline_button').forEach((button) => {
window.decline_button.setAttribute('disabled', 'true') button.setAttribute('disabled', 'true')
} })
}, },
methods: { methods: {
sendCopyToEmail () { sendCopyToEmail () {
@@ -1,4 +1,4 @@
function cropCanvasAndExportToPNG (canvas) { function cropCanvasAndExportToPNG (canvas, { errorOnTooSmall } = { errorOnTooSmall: false }) {
const ctx = canvas.getContext('2d') const ctx = canvas.getContext('2d')
const width = canvas.width const width = canvas.width
@@ -33,6 +33,10 @@ function cropCanvasAndExportToPNG (canvas) {
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) => {
+1 -1
View File
@@ -11,7 +11,7 @@
:with-label="!isAnonymousChecboxes && showFieldNames" :with-label="!isAnonymousChecboxes && showFieldNames"
:current-step="currentStepFields" :current-step="currentStepFields"
:scroll-padding="scrollPadding" :scroll-padding="scrollPadding"
@focus-step="[saveStep(), goToStep($event, false, true), currentField.type !== 'checkbox' ? isFormVisible = true : '']" @focus-step="[saveStep(), currentField.type !== 'checkbox' ? isFormVisible = true : '', goToStep($event, false, true)]"
/> />
<FieldAreas <FieldAreas
:steps="readonlyConditionalFields.map((e) => [e])" :steps="readonlyConditionalFields.map((e) => [e])"
+117 -6
View File
@@ -148,6 +148,7 @@ const es = {
sign_now: 'Firmar ahora', sign_now: 'Firmar ahora',
type_here_: 'Escribe aquí...', type_here_: 'Escribe aquí...',
optional: 'opcional', optional: 'opcional',
option: 'Opción',
appears_on: 'Aparece en', appears_on: 'Aparece en',
page: 'Página', page: 'Página',
select_your_option: 'Selecciona tu opción', select_your_option: 'Selecciona tu opción',
@@ -245,6 +246,7 @@ const it = {
sign_now: 'Firma ora', sign_now: 'Firma ora',
type_here_: 'Digita qui...', type_here_: 'Digita qui...',
optional: 'opzionale', optional: 'opzionale',
option: 'Opzione',
appears_on: 'Compare su', appears_on: 'Compare su',
page: 'Pagina', page: 'Pagina',
take_photo: 'Scattare una foto', take_photo: 'Scattare una foto',
@@ -265,7 +267,7 @@ const it = {
document_has_been_signed: 'Il documento è stato firmato!', document_has_been_signed: 'Il documento è stato firmato!',
documents_have_been_signed: 'I documenti sono stati firmati!', documents_have_been_signed: 'I documenti sono stati firmati!',
create_a_free_account: 'Crea un Account Gratuito', create_a_free_account: 'Crea un Account Gratuito',
powered_by: 'Desarrollado por', powered_by: 'Fornito da',
please_check_the_box_to_continue: 'Si prega di spuntare la casella per continuare.', please_check_the_box_to_continue: 'Si prega di spuntare la casella per continuare.',
open_source_documents_software: 'software di documenti open source', open_source_documents_software: 'software di documenti open source',
verified_phone_number: 'Verifica numero di telefono', verified_phone_number: 'Verifica numero di telefono',
@@ -343,6 +345,7 @@ const de = {
sign_now: 'Jetzt unterschreiben', sign_now: 'Jetzt unterschreiben',
type_here_: 'Hier eingeben...', type_here_: 'Hier eingeben...',
optional: 'optional', optional: 'optional',
option: 'Option',
appears_on: 'Erscheint auf', appears_on: 'Erscheint auf',
page: 'Seite', page: 'Seite',
take_photo: 'Foto aufnehmen', take_photo: 'Foto aufnehmen',
@@ -393,8 +396,8 @@ const de = {
const fr = { const fr = {
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour continuer la vérification de l'identité.", complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour continuer la vérification de l'identité.",
verif_id: "Vérification de l'ID", verify_id: "Vérification de l'ID",
verif_identite: "Vérification de l'identité", identity_verification: "Vérification de l'identité",
complete: 'Terminer', complete: 'Terminer',
fill_all_required_fields_to_complete: 'Veuillez remplir tous les champs obligatoires pour compléter', fill_all_required_fields_to_complete: 'Veuillez remplir tous les champs obligatoires pour compléter',
sign_and_complete: 'Signer et Terminer', sign_and_complete: 'Signer et Terminer',
@@ -441,6 +444,7 @@ const fr = {
sign_now: 'Signer maintenant', sign_now: 'Signer maintenant',
type_here_: 'Tapez ici...', type_here_: 'Tapez ici...',
optional: 'facultatif', optional: 'facultatif',
option: 'Option',
appears_on: 'Apparaît sur', appears_on: 'Apparaît sur',
page: 'Page', page: 'Page',
take_photo: 'Prendre une photo', take_photo: 'Prendre une photo',
@@ -539,6 +543,7 @@ const pl = {
sign_now: 'Podpisz teraz', sign_now: 'Podpisz teraz',
type_here_: 'Wpisz tutaj...', type_here_: 'Wpisz tutaj...',
optional: 'opcjonalny', optional: 'opcjonalny',
option: 'Opcja',
appears_on: 'Pojawia się na', appears_on: 'Pojawia się na',
page: 'Strona', page: 'Strona',
select_your_option: 'Wybierz swoją opcję', select_your_option: 'Wybierz swoją opcję',
@@ -583,7 +588,8 @@ const pl = {
reupload: 'Ponowne przesłanie', reupload: 'Ponowne przesłanie',
upload: 'Przesyłanie', upload: 'Przesyłanie',
files: 'Pliki', files: 'Pliki',
signature_is_too_small_or_simple_please_redraw: 'Podpis jest zbyt mały lub zbyt prosty. Proszę narysować go ponownie.' signature_is_too_small_or_simple_please_redraw: 'Podpis jest zbyt mały lub zbyt prosty. Proszę narysować go ponownie.',
wait_countdown_seconds: 'Poczekaj {countdown} sekund'
} }
const uk = { const uk = {
@@ -636,6 +642,7 @@ const uk = {
sign_now: 'Підписати зараз', sign_now: 'Підписати зараз',
type_here_: 'Введіть тут', type_here_: 'Введіть тут',
optional: 'необов’язково', optional: 'необов’язково',
option: 'Опція',
appears_on: "З'являється на", appears_on: "З'являється на",
page: 'Сторінка', page: 'Сторінка',
take_photo: 'Зробити фото', take_photo: 'Зробити фото',
@@ -658,7 +665,7 @@ const uk = {
create_a_free_account: 'Створити безкоштовний обліковий запис', create_a_free_account: 'Створити безкоштовний обліковий запис',
powered_by: 'Працює на базі', powered_by: 'Працює на базі',
please_check_the_box_to_continue: 'Будь ласка, позначте прапорець, щоб продовжити.', please_check_the_box_to_continue: 'Будь ласка, позначте прапорець, щоб продовжити.',
open_source_documents_software: 'відкритий програмний засіб для документів', open_source_documents_software: 'відкрите програмне забезпечення для документів',
verified_phone_number: 'Підтвердіть номер телефону', verified_phone_number: 'Підтвердіть номер телефону',
use_international_format: 'Використовуйте міжнародний формат: +1xxx', use_international_format: 'Використовуйте міжнародний формат: +1xxx',
six_digits_code: '6-значний код', six_digits_code: '6-значний код',
@@ -734,6 +741,7 @@ const cs = {
sign_now: 'Podepsat nyní', sign_now: 'Podepsat nyní',
type_here_: 'Zadejte zde', type_here_: 'Zadejte zde',
optional: 'volitelné', optional: 'volitelné',
option: 'Možnost',
appears_on: 'Zobrazuje se na', appears_on: 'Zobrazuje se na',
page: 'Stránka', page: 'Stránka',
select_your_option: 'Vyberte svou volbu', select_your_option: 'Vyberte svou volbu',
@@ -832,6 +840,7 @@ const pt = {
sign_now: 'Assinar agora', sign_now: 'Assinar agora',
type_here_: 'Digite aqui', type_here_: 'Digite aqui',
optional: 'opcional', optional: 'opcional',
option: 'Opção',
appears_on: 'Aparece em', appears_on: 'Aparece em',
page: 'Página', page: 'Página',
take_photo: 'Tirar foto', take_photo: 'Tirar foto',
@@ -1097,6 +1106,7 @@ const ar = {
select_a_reason: 'اختر سببًا', select_a_reason: 'اختر سببًا',
value_is_invalid: 'القيمة غير صالحة', value_is_invalid: 'القيمة غير صالحة',
verification_code_is_invalid: 'رمز التحقق غير صالح', verification_code_is_invalid: 'رمز التحقق غير صالح',
already_paid: 'تم الدفع بالفعل',
drawn_signature_on_a_touchscreen_device: 'توقيع مرسوم على جهاز بشاشة تعمل باللمس', drawn_signature_on_a_touchscreen_device: 'توقيع مرسوم على جهاز بشاشة تعمل باللمس',
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'امسح رمز الاستجابة السريعة باستخدام تطبيق الكاميرا لفتح النموذج على الهاتف المحمول ورسم توقيعك', scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'امسح رمز الاستجابة السريعة باستخدام تطبيق الكاميرا لفتح النموذج على الهاتف المحمول ورسم توقيعك',
by_clicking_you_agree_to_the: 'بالنقر فوق "{button}"، أنت توافق على', by_clicking_you_agree_to_the: 'بالنقر فوق "{button}"، أنت توافق على',
@@ -1198,6 +1208,8 @@ const ko = {
by_clicking_you_agree_to_the: '"{button}"를 클릭함으로써, 다음에 동의하게 됩니다', by_clicking_you_agree_to_the: '"{button}"를 클릭함으로써, 다음에 동의하게 됩니다',
electronic_signature_disclosure: '전자 서명 공개', electronic_signature_disclosure: '전자 서명 공개',
esignature_disclosure: '전자 서명 공개', esignature_disclosure: '전자 서명 공개',
value_is_invalid: '값이 올바르지 않습니다',
verification_code_is_invalid: '인증 코드가 올바르지 않습니다',
already_paid: '이미 지불됨', already_paid: '이미 지불됨',
text: '텍스트', text: '텍스트',
signature: '서명', signature: '서명',
@@ -1273,6 +1285,105 @@ const ko = {
wait_countdown_seconds: '{countdown}초 기다리세요' wait_countdown_seconds: '{countdown}초 기다리세요'
} }
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar, ko } const ja = {
complete_all_required_fields_to_proceed_with_identity_verification: '本人確認を進めるには、すべての必須項目を入力してください。',
verify_id: '本人確認',
identity_verification: '本人確認',
complete: '完了',
fill_all_required_fields_to_complete: '完了するにはすべての必須項目を入力してください',
sign_and_complete: '署名して完了',
text: 'テキスト',
by_clicking_you_agree_to_the: '"{button}" をクリックすることで、次に同意したことになります:',
electronic_signature_disclosure: '電子署名に関する開示',
esignature_disclosure: '電子署名開示',
signature: '署名',
initials: 'イニシャル',
drawn_signature_on_a_touchscreen_device: 'タッチスクリーンデバイスで描かれた署名',
approved: '承認済み',
reviewed: '確認済み',
other: 'その他',
authored_by_me: '自分が作成',
invite: '招待',
email: 'メール',
approved_by: '承認者',
reviewed_by: '確認者',
authored_by: '作成者',
select_a_reason: '理由を選択',
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'QRコードをカメラアプリで読み取り、モバイルでフォームを開いて署名を描いてください',
date: '日付',
number: '数値',
value_is_invalid: '無効な値です',
verification_code_is_invalid: '認証コードが無効です',
already_paid: 'すでに支払い済み',
image: '画像',
pay: '支払う',
take_photo: '写真を撮る',
number_phone_is_invalid: '{number} の電話番号は無効です',
file: 'ファイル',
digitally_signed_by: '電子署名者:',
reason: '理由',
select: '選択',
checkbox: 'チェックボックス',
multiple: '複数選択',
radio: 'ラジオボタン',
cells: 'セル',
stamp: 'スタンプ',
minimize: '最小化',
payment: '支払い',
phone: '電話',
start_now: '今すぐ開始',
continue: '続行',
sign_now: '今すぐ署名',
type_here_: 'ここに入力...',
optional: '任意',
option: 'オプション',
appears_on: '表示ページ',
page: 'ページ',
select_your_option: 'オプションを選択',
complete_hightlighted_checkboxes_and_click: 'ハイライトされたチェックボックスを完了し、クリックしてください',
submit: '送信',
next: '次へ',
click_to_upload: 'クリックしてアップロード',
or_drag_and_drop_files: 'またはファイルをドラッグ&ドロップ',
send_copy_via_email: 'メールでコピーを送信',
download: 'ダウンロード',
clear: 'クリア',
redraw: '描き直す',
draw_initials: 'イニシャルを描く',
type_signature_here: 'ここに署名を入力',
type_initial_here: 'ここにイニシャルを入力',
form_has_been_completed: 'フォームが完了しました!',
document_has_been_signed: 'ドキュメントが署名されました!',
documents_have_been_signed: 'ドキュメントがすべて署名されました!',
create_a_free_account: '無料アカウントを作成',
powered_by: '提供元:',
please_check_the_box_to_continue: '続行するにはボックスにチェックを入れてください。',
open_source_documents_software: 'オープンソースのドキュメントソフトウェア',
verified_phone_number: '電話番号を認証',
use_international_format: '国際形式を使用してください:+1xxx',
six_digits_code: '6桁のコード',
change_phone_number: '電話番号を変更',
sending: '送信中...',
resend_code: 'コードを再送信',
verification_code_has_been_resent: '認証コードがSMSで再送信されました',
please_fill_all_required_fields: 'すべての必須項目を入力してください',
set_today: '今日の日付を設定',
toggle_multiline_text: '複数行テキスト切り替え',
draw_signature: '署名を描く',
type_initial: 'イニシャルを入力',
draw: '描く',
type: '入力',
type_text: 'テキストを入力',
email_has_been_sent: 'メールが送信されました',
processing: '処理中',
pay_with_strip: 'Stripeで支払う',
reupload: '再アップロード',
upload: 'アップロード',
files: 'ファイル',
signature_is_too_small_or_simple_please_redraw: '署名が小さすぎるか単純すぎます。もう一度描いてください。',
wait_countdown_seconds: '{countdown} 秒お待ちください'
}
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar, ko, ja }
export default i18n export default i18n
@@ -339,8 +339,8 @@ export default {
return Promise.resolve({}) return Promise.resolve({})
} }
return new Promise((resolve) => { return new Promise((resolve, reject) => {
cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => { cropCanvasAndExportToPNG(this.$refs.canvas, { errorOnTooSmall: true }).then(async (blob) => {
const file = new File([blob], 'initials.png', { type: 'image/png' }) const file = new File([blob], 'initials.png', { type: 'image/png' })
if (this.dryRun) { if (this.dryRun) {
@@ -373,6 +373,14 @@ export default {
return resolve(attachment) return resolve(attachment)
}) })
} }
}).catch((error) => {
if (this.field.required === true) {
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
return reject(error)
} else {
return resolve({})
}
}) })
}) })
} }
@@ -683,13 +683,17 @@ export default {
} }
if (this.isSignatureStarted && this.pad.toData().length > 0 && !isValidSignatureCanvas(this.pad.toData())) { if (this.isSignatureStarted && this.pad.toData().length > 0 && !isValidSignatureCanvas(this.pad.toData())) {
alert(this.t('signature_is_too_small_or_simple_please_redraw')) if (this.field.required === true || this.pad.toData().length > 0) {
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
return Promise.reject(new Error('Image too small or simple')) return Promise.reject(new Error('Image too small or simple'))
} else {
Promise.resolve({})
}
} }
return new Promise((resolve) => { return new Promise((resolve, reject) => {
cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => { cropCanvasAndExportToPNG(this.$refs.canvas, { errorOnTooSmall: true }).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) {
@@ -725,6 +729,14 @@ export default {
return resolve(attachment) return resolve(attachment)
}) })
} }
}).catch((error) => {
if (this.field.required === true) {
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
return reject(error)
} else {
return resolve({})
}
}) })
}) })
} }
+7 -4
View File
@@ -160,15 +160,15 @@
</div> </div>
<div <div
ref="touchValueTarget" ref="touchValueTarget"
class="flex items-center h-full w-full field-area" class="flex h-full w-full field-area"
dir="auto" dir="auto"
:class="[isValueInput ? 'bg-opacity-50' : 'bg-opacity-80', field.type === 'heading' ? 'bg-gray-50' : bgColors[submitterIndex % bgColors.length], isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? fontClasses : 'justify-center']" :class="[isValueInput ? 'bg-opacity-50' : 'bg-opacity-80', field.type === 'heading' ? 'bg-gray-50' : bgColors[submitterIndex % bgColors.length], isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? fontClasses : 'justify-center items-center']"
@click="focusValueInput" @click="focusValueInput"
> >
<span <span
v-if="field" v-if="field"
class="flex justify-center items-center space-x-1" class="flex justify-center items-center space-x-1"
:class="{ 'w-full': ['cells', 'checkbox'].includes(field.type), 'h-full': !isValueInput }" :class="{ 'w-full': ['cells', 'checkbox'].includes(field.type), 'h-full': !isValueInput && !isDefaultValuePresent }"
> >
<div <div
v-if="isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas && field.type !== 'checkbox')" v-if="isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas && field.type !== 'checkbox')"
@@ -403,10 +403,13 @@ export default {
}, },
fontClasses () { fontClasses () {
if (!this.field.preferences) { if (!this.field.preferences) {
return {} return { 'items-center': true }
} }
return { return {
'items-center': !this.field.preferences.valign || this.field.preferences.valign === 'center',
'items-start': this.field.preferences.valign === 'top',
'items-end': this.field.preferences.valign === 'bottom',
'justify-center': this.field.preferences.align === 'center', 'justify-center': this.field.preferences.align === 'center',
'justify-start': this.field.preferences.align === 'left', 'justify-start': this.field.preferences.align === 'left',
'justify-end': this.field.preferences.align === 'right', 'justify-end': this.field.preferences.align === 'right',
+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
@@ -175,7 +175,7 @@ export default {
fields () { fields () {
if (this.item.submitter_uuid) { if (this.item.submitter_uuid) {
return this.template.fields.reduce((acc, f) => { return this.template.fields.reduce((acc, f) => {
if (f !== this.item) { if (f !== this.item && (!f.conditions?.length || !f.conditions.find((c) => c.field_uuid === this.item.uuid))) {
acc.push(f) acc.push(f)
} }
@@ -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)
}
} }
} }
}, },
+44 -8
View File
@@ -57,7 +57,7 @@
<span class="relative"> <span class="relative">
<select <select
class="select input-bordered bg-white select-sm text-center pl-2" class="select input-bordered bg-white select-sm text-center pl-2"
style="font-size: 16px; width: 86px; text-align-last: center;" style="font-size: 16px; line-height: 12px; width: 86px; text-align-last: center;"
@change="$event.target.value ? preferences.font_size = parseInt($event.target.value) : delete preferences.font_size" @change="$event.target.value ? preferences.font_size = parseInt($event.target.value) : delete preferences.font_size"
> >
<option <option
@@ -77,12 +77,12 @@
</select> </select>
<span <span
class="border-l pl-1.5 absolute bg-white bottom-0 pointer-events-none text-sm h-5" class="border-l pl-1.5 absolute bg-white bottom-0 pointer-events-none text-sm h-5"
style="right: 13px; top: 7px" style="right: 13px; top: 6px"
> >
pt pt
</span> </span>
</span> </span>
<span> <span class="flex">
<div <div
class="join" class="join"
style="height: 32px" style="height: 32px"
@@ -98,7 +98,7 @@
</button> </button>
</div> </div>
</span> </span>
<span> <span class="flex">
<div <div
class="join" class="join"
style="height: 32px" style="height: 32px"
@@ -114,6 +114,32 @@
</button> </button>
</div> </div>
</span> </span>
<span class="flex">
<div class="dropdown modal-field-font-dropdown">
<label
tabindex="0"
class="cursor-pointer flex bg-white border input-bordered rounded-md h-8 items-center justify-center px-1"
style="-webkit-appearance: none; -moz-appearance: none;"
>
<component :is="valigns.find((v) => v.value === (preferences.valign || 'center'))?.icon" />
</label>
<div
tabindex="0"
class="dropdown-content p-0 mt-1 block z-10 menu shadow bg-white border border-base-300 rounded-md"
>
<div
v-for="(valign, index) in valigns"
:key="index"
:value="valign.value"
:class="{ 'bg-base-300': preferences.valign == valign.value }"
class="hover:bg-base-300 px-2 py-1.5 cursor-pointer"
@click="[valign.value ? preferences.valign = valign.value : delete preferences.valign, closeDropdown()]"
>
<component :is="valign.icon" />
</div>
</div>
</div>
</span>
<span> <span>
<select <select
class="input input-bordered bg-white input-sm text-lg rounded-md" class="input input-bordered bg-white input-sm text-lg rounded-md"
@@ -134,7 +160,7 @@
</div> </div>
<div class="mt-4"> <div class="mt-4">
<div <div
class="flex items-center border border-base-content/20 rounded-xl bg-white px-4 h-16 modal-field-font-preview" class="flex border border-base-content/20 rounded-xl bg-white px-4 h-16 modal-field-font-preview"
:style="{ :style="{
color: preferences.color || 'black', color: preferences.color || 'black',
fontSize: (preferences.font_size || 12) + 'pt', fontSize: (preferences.font_size || 12) + 'pt',
@@ -143,7 +169,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>
@@ -163,7 +189,7 @@
</template> </template>
<script> <script>
import { IconChevronDown, IconBold, IconItalic, IconAlignLeft, IconAlignRight, IconAlignCenter } from '@tabler/icons-vue' import { IconChevronDown, IconBold, IconItalic, IconAlignLeft, IconAlignRight, IconAlignCenter, IconAlignBoxCenterTop, IconAlignBoxCenterBottom, IconAlignBoxCenterMiddle } from '@tabler/icons-vue'
export default { export default {
name: 'FontModal', name: 'FontModal',
@@ -213,6 +239,13 @@ export default {
{ icon: IconAlignRight, value: 'right' } { icon: IconAlignRight, value: 'right' }
] ]
}, },
valigns () {
return [
{ icon: IconAlignBoxCenterTop, value: 'top' },
{ icon: IconAlignBoxCenterMiddle, value: 'center' },
{ icon: IconAlignBoxCenterBottom, value: 'bottom' }
]
},
sizes () { sizes () {
return [...Array(23).keys()].map(i => i + 6) return [...Array(23).keys()].map(i => i + 6)
}, },
@@ -230,12 +263,15 @@ export default {
'justify-center': this.preferences.align === 'center', 'justify-center': this.preferences.align === 'center',
'justify-start': this.preferences.align === 'left', 'justify-start': this.preferences.align === 'left',
'justify-end': this.preferences.align === 'right', 'justify-end': this.preferences.align === 'right',
'items-center': !this.preferences.valign || this.preferences.valign === 'center',
'items-start': this.preferences.valign === 'top',
'items-end': this.preferences.valign === 'bottom',
'font-bold': ['bold_italic', 'bold'].includes(this.preferences.font_type), 'font-bold': ['bold_italic', 'bold'].includes(this.preferences.font_type),
italic: ['bold_italic', 'italic'].includes(this.preferences.font_type) italic: ['bold_italic', 'italic'].includes(this.preferences.font_type)
} }
}, },
keys () { keys () {
return ['font_type', 'font_size', 'color', 'align', 'font'] return ['font_type', 'font_size', 'color', 'align', 'valign', 'font']
} }
}, },
created () { created () {
@@ -154,7 +154,7 @@ export default {
computed: { computed: {
fields () { fields () {
return this.template.fields.reduce((acc, f) => { return this.template.fields.reduce((acc, f) => {
if (f !== this.field && f.submitter_uuid === this.field.submitter_uuid && ['number'].includes(f.type) && !f.preferences?.formula) { if (f !== this.field && ['number'].includes(f.type) && (!f.preferences?.formula || f.submitter_uuid !== this.field.submitter_uuid)) {
acc.push(f) acc.push(f)
} }
+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,
+5
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])
@@ -89,6 +90,10 @@ class Submission < ApplicationRecord
expire_at && expire_at <= Time.current expire_at && expire_at <= Time.current
end end
def fields_uuid_index
@fields_uuid_index ||= (template_fields || template.fields).index_by { |f| f['uuid'] }
end
def audit_trail_url def audit_trail_url
return if audit_trail.blank? return if audit_trail.blank?
+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
+2 -1
View File
@@ -174,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') %>
+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#2c3e50" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 9l4 -4l4 4m-4 -4v14" />
<path d="M21 15l-4 4l-4 -4m4 4v-14" />
</svg>

After

Width:  |  Height:  |  Size: 357 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M12 19h-7a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v5.5" /><path d="M19 22v-6" /><path d="M22 19l-3 -3l-3 3" /><path d="M3 7l9 6l9 -6" />
</svg>

After

Width:  |  Height:  |  Size: 437 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
<path d="M15 10v-5c0 -1.38 .62 -2 2 -2s2 .62 2 2v5m0 -3h-4"></path>
<path d="M19 21h-4l4 -7h-4"></path> <path d="M4 15l3 3l3 -3"></path>
<path d="M7 6v12"></path>
</svg>

After

Width:  |  Height:  |  Size: 395 B

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
<path d="M4 15l3 3l3 -3"></path>
<path d="M7 6v12"></path>
<path d="M17 14a2 2 0 0 1 2 2v3a2 2 0 1 1 -4 0v-3a2 2 0 0 1 2 -2z"></path>
<path d="M17 5m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"></path>
<path d="M19 5v3a2 2 0 0 1 -2 2h-1.5"></path>
</svg>

After

Width:  |  Height:  |  Size: 474 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
<path d="M10 15l-3 3l-3 -3"></path>
<path d="M7 6v12"></path>
<path d="M14 18.333c0 .369 .298 .667 .667 .667h2.666a.667 .667 0 0 0 .667 -.667v-2.666a.667 .667 0 0 0 -.667 -.667h-2.666a.667 .667 0 0 0 -.667 .667v2.666z"></path>
<path d="M14 10.833c0 .645 .522 1.167 1.167 1.167h4.666c.645 0 1.167 -.522 1.167 -1.167v-4.666c0 -.645 -.522 -1.167 -1.167 -1.167h-4.666c-.645 0 -1.167 .522 -1.167 1.167v4.666z"></path>
</svg>

After

Width:  |  Height:  |  Size: 647 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M16 18a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2zm0 -12a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2zm-7 12a6 6 0 0 1 6 -6a6 6 0 0 1 -6 -6a6 6 0 0 1 -6 6a6 6 0 0 1 6 6z" />
</svg>

After

Width:  |  Height:  |  Size: 497 B

+1 -1
View File
@@ -12,7 +12,7 @@
</div> </div>
<div class="form-control"> <div class="form-control">
<%= f.label :last_name, t('last_name'), class: 'label' %> <%= f.label :last_name, t('last_name'), class: 'label' %>
<%= f.text_field :last_name, required: true, class: 'base-input', dir: 'auto' %> <%= f.text_field :last_name, required: false, class: 'base-input', dir: 'auto' %>
</div> </div>
</div> </div>
<div class="form-control"> <div class="form-control">
+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_by(key: UserConfig::SHOW_APP_TOUR) || UserConfig.new(key: UserConfig::SHOW_APP_TOUR, user: current_user) %>
<% 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') %>
+1 -1
View File
@@ -1,4 +1,4 @@
<% uuid = SecureRandom.uuid %> <% uuid = local_assigns[:uuid] || SecureRandom.uuid %>
<label id="<%= local_assigns[:button_id] %>" for="<%= uuid %>" class="<%= local_assigns[:btn_class] %>"><%= local_assigns[:btn_text] %></label> <label id="<%= local_assigns[:button_id] %>" for="<%= uuid %>" class="<%= local_assigns[:btn_class] %>"><%= local_assigns[:btn_text] %></label>
<input type="checkbox" id="<%= uuid %>" class="modal-toggle"> <input type="checkbox" id="<%= uuid %>" class="modal-toggle">
<div id="<%= local_assigns[:id] %>" class="modal items-start !animate-none overflow-y-auto"> <div id="<%= local_assigns[:id] %>" class="modal items-start !animate-none overflow-y-auto">
+9 -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">
@@ -50,6 +50,14 @@
<% end %> <% end %>
</li> </li>
<% end %> <% end %>
<% if Docuseal.multitenant? || current_user.role == 'superadmin' %>
<li>
<%= link_to Docuseal::CHATGPT_URL, target: 'blank', class: 'flex items-center' do %>
<%= svg_icon('sparkles', class: 'w-5 h-5 flex-shrink-0 stroke-2') %>
<span class="mr-1 whitespace-nowrap"><%= t('ask_ai') %></span>
<% end %>
</li>
<% end %>
<% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %> <% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %>
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'w-full py-1' } do |f| %> <%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'w-full py-1' } do |f| %>
<label class="flex items-center pl-6 pr-4 py-2 border-y border-base-300 -ml-2 -mr-2" for="testing_toggle"> <label class="flex items-center pl-6 pr-4 py-2 border-y border-base-300 -ml-2 -mr-2" for="testing_toggle">
+17 -14
View File
@@ -5,20 +5,23 @@
<%= @pagy.from %>-<%= local_assigns.fetch(:to, @pagy.to) %> of <%= local_assigns.fetch(:count, @pagy.count) %> <%= local_assigns[:items_name] || 'items' %> <%= @pagy.from %>-<%= local_assigns.fetch(:to, @pagy.to) %> of <%= local_assigns.fetch(:count, @pagy.count) %> <%= local_assigns[:items_name] || 'items' %>
<%= local_assigns[:left_additional_html] %> <%= local_assigns[:left_additional_html] %>
</div> </div>
<div class="join"> <div class="flex items-center space-x-1.5">
<% if @pagy.prev %> <%= local_assigns[:right_additional_html] %>
<%== link.call(@pagy.prev, '«', classes: 'join-item btn min-h-full h-10') %> <div class="join">
<% else %> <% if @pagy.prev %>
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">«</span> <%== link.call(@pagy.prev, '«', classes: 'join-item btn min-h-full h-10') %>
<% end %> <% else %>
<span class="join-item btn uppercase min-h-full h-10"> <span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">«</span>
<%= t('page_number', number: @pagy.page) %> <% end %>
</span> <span class="join-item btn font-medium uppercase min-h-full h-10">
<% if @pagy.next %> <%= t('page_number', number: @pagy.page) %>
<%== link.call(@pagy.next, '»', classes: 'join-item btn min-h-full h-10') %> </span>
<% else %> <% if @pagy.next %>
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">»</span> <%== link.call(@pagy.next, '»', classes: 'join-item btn min-h-full h-10') %>
<% end %> <% else %>
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">»</span>
<% end %>
</div>
</div> </div>
</div> </div>
<% end %> <% 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>
@@ -0,0 +1,34 @@
<% dashboard_templates_order = cookies.permanent[:dashboard_templates_order] || 'created_at' %>
<form action="<%= url_for %>" method="get" class="dropdown dropdown-top hidden md:inline">
<label tabindex="0" class="btn btn-sm h-10">
<%= svg_icon('arrow_sort', class: 'w-5 h-5') %>
</label>
<ul tabindex="0" class="dropdown-content z-[10] menu p-2 shadow bg-base-100 rounded-box mb-1 min-w-48">
<toggle-cookies data-value="created_at" data-key="dashboard_templates_order">
<li>
<button class="<%= 'bg-base-200' if dashboard_templates_order == 'created_at' %>">
<%= svg_icon('sort_descending_numbers', class: 'w-4 h-4') %>
<span class="whitespace-nowrap"><%= t('newest_first') %></span>
</button>
</li>
</toggle-cookies>
<% if local_assigns[:with_recently_used] != false %>
<toggle-cookies data-value="used_at" data-key="dashboard_templates_order">
<li>
<button class="<%= 'bg-base-200' if dashboard_templates_order == 'used_at' %>">
<%= svg_icon('sort_descending_small_big', class: 'w-4 h-4') %>
<span class="whitespace-nowrap"><%= t('recently_used') %></span>
</button>
</li>
</toggle-cookies>
<% end %>
<toggle-cookies data-value="name" data-key="dashboard_templates_order">
<li>
<button class="<%= 'bg-base-200' if dashboard_templates_order == 'name' %>">
<%= svg_icon('sort_ascending_letters', class: 'w-4 h-4') %>
<span class="whitespace-nowrap"><%= t('name_a_z') %></span>
</button>
</li>
</toggle-cookies>
</ul>
</form>
@@ -31,7 +31,7 @@
</submitters-autocomplete> </submitters-autocomplete>
<submitters-autocomplete data-field="phone"> <submitters-autocomplete data-field="phone">
<linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>"> <linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
<input type="tel" pattern="^\+[0-9\s\-]+$" oninvalid="this.value ? this.setCustomValidity('<%= t('use_international_format_1xxx_') %>') : ''" oninput="this.setCustomValidity('')" name="submission[1][submitters][][phone]" autocomplete="off" class="base-input !h-10 mt-1.5 w-full" placeholder="<%= "#{t('phone')} (#{t('optional')})" %>" id="detailed_phone_<%= item['uuid'] %>"> <%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('#{t('use_international_format_1xxx_')}') : ''", oninput: "this.setCustomValidity('')", name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: local_assigns[:require_phone_2fa] == true ? t(:phone) : "#{t('phone')} (#{t('optional')})", id: "detailed_phone_#{item['uuid']}", required: local_assigns[:require_phone_2fa] == true %>
</linked-input> </linked-input>
</submitters-autocomplete> </submitters-autocomplete>
</div> </div>
+7
View File
@@ -0,0 +1,7 @@
<div id="submitters_error">
<% if local_assigns[:error] %>
<div class="text-center text-red-500">
<%= local_assigns[:error] %>
</div>
<% end %>
</div>
+9 -6
View File
@@ -1,8 +1,9 @@
<% align = field.dig('preferences', 'align') %> <% align = field.dig('preferences', 'align') %>
<% valign = field.dig('preferences', 'valign') %>
<% color = field.dig('preferences', 'color') %> <% color = field.dig('preferences', 'color') %>
<% font = field.dig('preferences', 'font') %> <% font = field.dig('preferences', 'font') %>
<% font_type = field.dig('preferences', 'font_type') %> <% font_type = field.dig('preferences', 'font_type') %>
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-mono' if font == 'Courier' %> <%= 'font-serif' if font == 'Times' %> <%= 'font-bold' if font_type == 'bold' || font_type == 'bold_italic' %> <%= 'italic' if font_type == 'italic' || font_type == 'bold_italic' %> <%= align == 'right' ? 'justify-end' : (align == 'center' ? 'justify-center' : '') %>" style="<%= "color: #{color}; " if color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size'].to_i * 1.23}pt); line-height: `clamp(6pt, 2.0vw, #{(field['preferences']['font_size'].to_i * 1.23) + 3}pt)`" if field.dig('preferences', 'font_size') %>"> <field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-mono' if font == 'Courier' %> <%= 'font-serif' if font == 'Times' %> <%= 'font-bold' if font_type == 'bold' || font_type == 'bold_italic' %> <%= 'italic' if font_type == 'italic' || font_type == 'bold_italic' %> <%= align == 'right' ? 'text-right' : (align == 'center' ? 'text-center' : '') %>" style="<%= "color: #{color}; " if color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size'].to_i * 1.23}pt); line-height: `clamp(6pt, 2.0vw, #{(field['preferences']['font_size'].to_i * 1.23) + 3}pt)`" if field.dig('preferences', 'font_size') %>">
<% if field['type'] == 'signature' %> <% if field['type'] == 'signature' %>
<div class="flex flex-col justify-between h-full overflow-hidden"> <div class="flex flex-col justify-between h-full overflow-hidden">
<div class="flex-grow flex overflow-hidden" style="min-height: 50%"> <div class="flex-grow flex overflow-hidden" style="min-height: 50%">
@@ -61,17 +62,19 @@
</div> </div>
<% elsif field['type'] == 'date' %> <% elsif field['type'] == 'date' %>
<autosize-field></autosize-field> <autosize-field></autosize-field>
<div class="flex items-center px-0.5"> <div class="flex w-full px-0.5 <%= valign == 'top' ? 'items-start' : (valign == 'bottom' ? 'items-end' : 'items-center') %>">
<% value = Time.current.in_time_zone(local_assigns[:timezone]).to_date.to_s if value == '{{date}}' %> <% value = Time.current.in_time_zone(local_assigns[:timezone]).to_date.to_s if value == '{{date}}' %>
<%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), local_assigns[:locale]) %> <div class="w-full"><%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), local_assigns[:locale]) %></div>
</div> </div>
<% elsif field['type'] == 'number' %> <% elsif field['type'] == 'number' %>
<autosize-field></autosize-field> <autosize-field></autosize-field>
<div class="flex items-center px-0.5 whitespace-nowrap"> <div class="flex w-full px-0.5 whitespace-nowrap <%= valign == 'top' ? 'items-start' : (valign == 'bottom' ? 'items-end' : 'items-center') %>">
<%= NumberUtils.format_number(value, field.dig('preferences', 'format')) %> <div class="w-full"><%= NumberUtils.format_number(value, field.dig('preferences', 'format')) %></div>
</div> </div>
<% else %> <% else %>
<autosize-field></autosize-field> <autosize-field></autosize-field>
<div class="flex items-center px-0.5 whitespace-pre-wrap"><%= Array.wrap(value).join(', ') %></div> <div class="flex w-full px-0.5 whitespace-pre-wrap <%= valign == 'top' ? 'items-start' : (valign == 'bottom' ? 'items-end' : 'items-center') %>">
<div class="w-full"><%= Array.wrap(value).join(', ') %></div>
</div>
<% end %> <% end %>
</field-value> </field-value>
+14 -10
View File
@@ -1,10 +1,11 @@
<% require_phone_2fa = @template.preferences['require_phone_2fa'] == true %>
<%= render 'shared/turbo_modal_large', title: params[:selfsign] ? t('add_recipients') : t('add_new_recipients') do %> <%= render 'shared/turbo_modal_large', title: params[:selfsign] ? t('add_recipients') : t('add_new_recipients') do %>
<% options = [[t('via_email'), 'email'], [t('via_phone'), 'phone'], [t('detailed'), 'detailed'], [t('upload_list'), 'list']].compact %> <% options = [require_phone_2fa ? nil : [t('via_email'), 'email'], require_phone_2fa ? nil : [t('via_phone'), 'phone'], [t('detailed'), 'detailed'], [t('upload_list'), 'list']].compact %>
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center px-2 mt-4 block"> <toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center px-2 mt-4 block">
<div class="flex justify-center"> <div class="flex justify-center">
<% options.each_with_index do |(label, value), index| %> <% options.each_with_index do |(label, value), index| %>
<div> <div>
<%= radio_button_tag 'option', value, value == 'email', class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %> <%= radio_button_tag 'option', value, value == (require_phone_2fa ? 'detailed' : 'email'), class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %>
<label for="option_<%= value %>" class="block bg-base-200 md:min-w-[112px] text-sm font-semibold whitespace-nowrap py-1.5 px-4 peer-checked:bg-base-300 <%= 'hidden sm:inline-block' if value == 'list' %> <%= 'rounded-l-3xl' if index.zero? %> <%= 'rounded-r-3xl sm:rounded-r-none' if value == 'detailed' %> <%= 'rounded-r-3xl' if index == options.size - 1 %>"> <label for="option_<%= value %>" class="block bg-base-200 md:min-w-[112px] text-sm font-semibold whitespace-nowrap py-1.5 px-4 peer-checked:bg-base-300 <%= 'hidden sm:inline-block' if value == 'list' %> <%= 'rounded-l-3xl' if index.zero? %> <%= 'rounded-r-3xl sm:rounded-r-none' if value == 'detailed' %> <%= 'rounded-r-3xl' if index == options.size - 1 %>">
<%= label %> <%= label %>
</label> </label>
@@ -13,18 +14,21 @@
</div> </div>
</toggle-visible> </toggle-visible>
<div class="px-5 mb-5 mt-4"> <div class="px-5 mb-5 mt-4">
<div id="email"> <% unless require_phone_2fa %>
<%= render 'email_form', template: @template %> <div id="email">
</div> <%= render 'email_form', template: @template %>
<div id="phone" class="hidden"> </div>
<%= render 'phone_form', template: @template %> <div id="phone" class="hidden">
</div> <%= render 'phone_form', template: @template %>
<div id="detailed" class="hidden"> </div>
<%= render 'detailed_form', template: @template %> <% end %>
<div id="detailed" class="<%= 'hidden' unless require_phone_2fa %>">
<%= render 'detailed_form', template: @template, require_phone_2fa: %>
</div> </div>
<div id="list" class="hidden"> <div id="list" class="hidden">
<%= render 'list_form', template: @template %> <%= render 'list_form', template: @template %>
</div> </div>
<%= render 'submissions/error' %>
</div> </div>
<%= content_for(:modal_extra) %> <%= content_for(:modal_extra) %>
<% end %> <% end %>
@@ -57,6 +57,7 @@
<div class="flex items-end flex-col md:flex-row gap-2 w-full md:w-fit"> <div class="flex items-end flex-col md:flex-row gap-2 w-full md:w-fit">
<%= render 'submissions_filters/applied_filters', filter_params: %> <%= render 'submissions_filters/applied_filters', filter_params: %>
<%= render 'submissions_filters/filter_button', filter_params: %> <%= render 'submissions_filters/filter_button', filter_params: %>
<%= render 'submissions_filters/actions', filter_params: %>
</div> </div>
</div> </div>
<% end %> <% end %>
Whitespace-only changes.
+24 -3
View File
@@ -8,17 +8,18 @@
<div style="max-height: -webkit-fill-available;"> <div style="max-height: -webkit-fill-available;">
<div id="scrollbox"> <div id="scrollbox">
<div class="mx-auto block pb-72" style="max-width: 1000px"> <div class="mx-auto block pb-72" style="max-width: 1000px">
<%# flex block w-full sticky top-0 z-50 space-x-2 items-center bg-yellow-100 p-2 border-y border-yellow-200 %> <%# flex block w-full sticky top-0 z-50 space-x-2 items-center bg-yellow-100 p-2 border-y border-yellow-200 transition-transform duration-300 %>
<%= local_assigns[:banner_html] || capture do %> <%= local_assigns[:banner_html] || capture do %>
<%= render('submit_form/banner') %> <%= render('submit_form/banner') %>
<div class="sticky top-0 z-50 bg-base-100 py-2 px-2 flex items-center md:-mx-[8px]" style="margin-bottom: -16px"> <div id="signing_form_header" class="sticky min-[1230px]:static top-0 z-50 bg-base-100 py-2 px-2 flex items-center md:-mx-[8px]" style="margin-bottom: -16px">
<div class="text-xl md:text-2xl font-medium focus:text-clip" style="width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"> <div class="text-xl md:text-2xl font-medium focus:text-clip" style="width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
<%= @submitter.submission.template.name %> <%= @submitter.submission.template.name %>
</div> </div>
<div class="flex items-center space-x-2" style="margin-left: 20px; flex-shrink: 0"> <div class="flex items-center space-x-2" style="margin-left: 20px; flex-shrink: 0">
<% if @form_configs[:with_decline] %> <% if @form_configs[:with_decline] %>
<% decline_modal_checkbox_uuid = SecureRandom.uuid %>
<div> <div>
<%= render 'shared/html_modal', title: t(:decline), btn_text: t(:decline), btn_class: 'btn btn-sm !px-5', button_id: 'decline_button' do %> <%= render 'shared/html_modal', title: t(:decline), btn_text: t(:decline), btn_class: 'btn btn-sm !px-5', button_id: 'decline_button', uuid: decline_modal_checkbox_uuid do %>
<%= render 'submit_form/decline_form', submitter: @submitter %> <%= render 'submit_form/decline_form', submitter: @submitter %>
<% end %> <% end %>
</div> </div>
@@ -35,6 +36,26 @@
</download-button> </download-button>
</div> </div>
</div> </div>
<scroll-buttons class="fixed right-5 top-2 hidden md:flex gap-1 z-50 ease-in-out opacity-0 -translate-y-10">
<% if @form_configs[:with_decline] %>
<label id="decline_button" for="<%= decline_modal_checkbox_uuid %>" class="btn btn-sm px-0">
<span class="min-[1366px]:inline hidden px-3">
<%= t(:decline) %>
</span>
<span class="inline min-[1366px]:hidden px-2">
<%= svg_icon('x', class: 'w-5 h-5') %>
</span>
</label>
<% end %>
<download-button data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm px-2">
<span data-target="download-button.defaultButton">
<%= svg_icon('download', class: 'w-5 h-5') %>
</span>
<span class="hidden" data-target="download-button.loadingButton">
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin') %>
</span>
</download-button>
</scroll-buttons>
<% end %> <% end %>
<% schema.each do |item| %> <% schema.each do |item| %>
<% document = @submitter.submission.template_schema_documents.find { |a| a.uuid == item['attachment_uuid'] } %> <% document = @submitter.submission.template_schema_documents.find { |a| a.uuid == item['attachment_uuid'] } %>
+6 -1
View File
@@ -35,7 +35,12 @@
<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 %>
</div> </div>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates' %> <% templates_order_select_html = capture do %>
<% if params[:q].blank? && @pagy.pages > 1 %>
<%= render('shared/templates_order_select', with_recently_used: @pagy.count < 10_000) %>
<% end %>
<% end %>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', right_additional_html: templates_order_select_html %>
<% elsif params[:q].present? %> <% elsif params[:q].present? %>
<div class="text-center"> <div class="text-center">
<div class="mt-16 text-3xl font-semibold"> <div class="mt-16 text-3xl font-semibold">
+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>
+6 -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 %>
@@ -61,6 +61,7 @@
<div class="flex items-end flex-col md:flex-row gap-2 w-full md:w-fit"> <div class="flex items-end flex-col md:flex-row gap-2 w-full md:w-fit">
<%= render 'submissions_filters/applied_filters', filter_params: %> <%= render 'submissions_filters/applied_filters', filter_params: %>
<%= render 'submissions_filters/filter_button', filter_params: %> <%= render 'submissions_filters/filter_button', filter_params: %>
<%= render 'submissions_filters/actions', filter_params: filter_params.merge(template_id: @template.id) %>
</div> </div>
</div> </div>
<% end %> <% end %>
@@ -80,18 +81,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 +118,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)) %>
+30 -2
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">
@@ -33,6 +34,11 @@
</div> </div>
<% end %> <% end %>
<% end %> <% end %>
<% templates_order_select_html = capture do %>
<% if params[:q].blank? && @pagy.pages > 1 %>
<%= render('shared/templates_order_select', with_recently_used: @pagy.count < 10_000) %>
<% end %>
<% end %>
<% if @template_folders.present? %> <% if @template_folders.present? %>
<div class="grid gap-4 md:grid-cols-3 <%= 'mb-6' if @templates.present? %>"> <div class="grid gap-4 md:grid-cols-3 <%= 'mb-6' if @templates.present? %>">
<%= render partial: 'template_folders/folder', collection: @template_folders, as: :folder %> <%= render partial: 'template_folders/folder', collection: @template_folders, as: :folder %>
@@ -41,14 +47,36 @@
<% 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? %>
<% if @pagy.pages > 1 %> <% if @pagy.pages > 1 %>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', left_additional_html: view_archived_html %> <%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', left_additional_html: view_archived_html, right_additional_html: templates_order_select_html %>
<% else %> <% else %>
<div class="mt-2"> <div class="mt-2">
<%= view_archived_html %> <%= view_archived_html %>
Whitespace-only changes.
+14 -1
View File
@@ -51,7 +51,7 @@
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %> <%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %>
<toggle-on-submit data-element-id="form_saved_alert"></toggle-on-submit> <toggle-on-submit data-element-id="form_saved_alert"></toggle-on-submit>
<% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY).value %> <% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY).value %>
<%= f.fields_for :preferences, Struct.new(:completed_redirect_url, :completed_message).new(@template.preferences['completed_redirect_url'].presence, Struct.new(:title, :body).new(*(@template.preferences['completed_message'] || {}).values_at('title', 'body'))) do |ff| %> <%= f.fields_for :preferences, Struct.new(:completed_redirect_url, :completed_message, :require_phone_2fa).new(@template.preferences['completed_redirect_url'].presence, Struct.new(:title, :body).new(*(@template.preferences['completed_message'] || {}).values_at('title', 'body')), @template.preferences['require_phone_2fa'] == true) do |ff| %>
<div class="form-control mb-2"> <div class="form-control mb-2">
<%= ff.label :completed_redirect_url, t('redirect_on_completion_url'), class: 'label' %> <%= ff.label :completed_redirect_url, t('redirect_on_completion_url'), class: 'label' %>
<%= ff.url_field :completed_redirect_url, required: false, class: 'base-input', dir: 'auto' %> <%= ff.url_field :completed_redirect_url, required: false, class: 'base-input', dir: 'auto' %>
@@ -64,6 +64,7 @@
</autoresize-textarea> </autoresize-textarea>
</div> </div>
<% end %> <% end %>
<%= render 'templates_preferences/form_fields', ff: %>
<% end %> <% end %>
<div class="form-control pt-2"> <div class="form-control pt-2">
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %> <%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
@@ -334,6 +335,18 @@
</div> </div>
<% end %> <% end %>
<% end %> <% end %>
<% if can?(:manage, :personalization_advanced) %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-2' }, data: { close_on_submit: false } do |f| %>
<div class="flex items-center mt-4 justify-between w-full">
<span>
<%= t('ensure_unique_recipients') %>
</span>
<%= f.fields_for :preferences, Struct.new(:validate_unique_submitters).new(@template.preferences['validate_unique_submitters']) do |ff| %>
<%= ff.check_box :validate_unique_submitters, { class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', '' %>
<% end %>
</div>
<% 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 %>
</div> </div>
+2 -1
View File
@@ -25,7 +25,8 @@ module DocuSeal
config.active_storage.draw_routes = ENV['MULTITENANT'] != 'true' config.active_storage.draw_routes = ENV['MULTITENANT'] != 'true'
config.i18n.available_locales = %i[en en-US en-GB es-ES fr-FR pt-PT de-DE it-IT es it de fr pl uk cs pt he nl ar ko] config.i18n.available_locales = %i[en en-US en-GB es-ES fr-FR pt-PT de-DE it-IT
es it de fr pl uk cs pt he nl ar ko ja]
config.i18n.fallbacks = [:en] config.i18n.fallbacks = [:en]
config.exceptions_app = ->(env) { ErrorsController.action(:show).call(env) } config.exceptions_app = ->(env) { ErrorsController.action(:show).call(env) }
+462 -7
View File
@@ -18,14 +18,22 @@ en: &en
language_nl: Nederlands language_nl: Nederlands
language_ar: العربية language_ar: العربية
language_ko: 한국어 language_ko: 한국어
language_ja: 日本語
hi_there: Hi there hi_there: Hi there
thanks: Thanks thanks: Thanks
bcc_recipients: BCC recipients bcc_recipients: BCC recipients
resend_pending: Re-send pending
always_enforce_signing_order: Always enforce the signing order always_enforce_signing_order: Always enforce the signing order
ensure_unique_recipients: Ensure unique recipients
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
partially_completed: Partially completed partially_completed: Partially completed
require_phone_2fa_to_open: Require phone 2FA to open
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: The sender has requested a two factor authentication via one time password sent to your <b>%{phone}</b> phone number.
send_verification_code: Send verification code
code_has_been_resent: Code has been re-sent
invalid_code: Invalid code
unarchive: Unarchive unarchive: Unarchive
signed: Signed signed: Signed
first_party: 'First Party' first_party: 'First Party'
@@ -47,6 +55,8 @@ en: &en
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'You have been invited to %{account_name} %{product_name}. Please sign up using the link below:' you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'You have been invited to %{account_name} %{product_name}. Please sign up using the link below:'
sent_using_product_name_in_testing_mode_html: 'Sent using <a href="%{product_url}">%{product_name}</a> in testing mode' sent_using_product_name_in_testing_mode_html: 'Sent using <a href="%{product_url}">%{product_name}</a> in testing mode'
sent_using_product_name_free_document_signing_html: 'Sent using <a href="%{product_url}">%{product_name}</a> free document signing.' sent_using_product_name_free_document_signing_html: 'Sent using <a href="%{product_url}">%{product_name}</a> free document signing.'
sent_with_docuseal_pro_html: 'Sent with <a href="%{product_url}">DocuSeal Pro</a>'
show_send_with_docuseal_pro_attribution_in_emails_html: Show "Sent with <span class="link">DocuSeal Pro</span>" attribution in emails
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Sign documents with trusted certificate provided by DocuSeal. Your documents and data are never shared with DocuSeal. PDF checksum is provided to generate a trusted signature. sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Sign documents with trusted certificate provided by DocuSeal. Your documents and data are never shared with DocuSeal. PDF checksum is provided to generate a trusted signature.
you_have_been_invited_to_submit_the_name_form: 'You have been invited to submit the "%{name}" form.' you_have_been_invited_to_submit_the_name_form: 'You have been invited to submit the "%{name}" form.'
you_have_been_invited_to_sign_the_name: 'You have been invited to sign the "%{name}".' you_have_been_invited_to_sign_the_name: 'You have been invited to sign the "%{name}".'
@@ -166,7 +176,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.
@@ -701,6 +711,12 @@ 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
name_a_z: Name A-Z
recently_used: Recently used
newest_first: Newest first
submission_sources: submission_sources:
api: API api: API
bulk: Bulk Send bulk: Bulk Send
@@ -739,12 +755,59 @@ 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
resend_pending: Reenviar pendiente
ensure_unique_recipients: Asegurar destinatarios únicos
require_phone_2fa_to_open: Requiere 2FA por teléfono para abrir
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: El remitente ha solicitado una autenticación de dos factores mediante una contraseña de un solo uso enviada a su número de teléfono <b>%{phone}</b>.
send_verification_code: Enviar código de verificación
code_has_been_resent: El código ha sido reenviado
invalid_code: Código inválido
always_enforce_signing_order: Siempre imponer el orden de firma always_enforce_signing_order: Siempre imponer el orden de firma
bcc_recipients: Destinatarios CCO bcc_recipients: Destinatarios CCO
edit_per_party: Editar por parte edit_per_party: Editar por parte
@@ -774,6 +837,8 @@ es: &es
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Has sido invitado a %{account_name} %{product_name}. Por favor, regístrate usando el enlace a continuación:' you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Has sido invitado a %{account_name} %{product_name}. Por favor, regístrate usando el enlace a continuación:'
sent_using_product_name_in_testing_mode_html: 'Enviado usando <a href="%{product_url}">%{product_name}</a> en Modo de Prueba' sent_using_product_name_in_testing_mode_html: 'Enviado usando <a href="%{product_url}">%{product_name}</a> en Modo de Prueba'
sent_using_product_name_free_document_signing_html: 'Enviado usando la firma de documentos gratuita de <a href="%{product_url}">%{product_name}</a>.' sent_using_product_name_free_document_signing_html: 'Enviado usando la firma de documentos gratuita de <a href="%{product_url}">%{product_name}</a>.'
sent_with_docuseal_pro_html: 'Enviado con <a href="%{product_url}">DocuSeal Pro</a>'
show_send_with_docuseal_pro_attribution_in_emails_html: Mostrar el mensaje "Enviado con <span class="link">DocuSeal Pro</span>" en los correos electrónicos
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Firme documentos con un certificado de confianza proporcionado por DocuSeal. Sus documentos y datos nunca se comparten con DocuSeal. Se proporciona un checksum de PDF para generar una firma de confianza. sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Firme documentos con un certificado de confianza proporcionado por DocuSeal. Sus documentos y datos nunca se comparten con DocuSeal. Se proporciona un checksum de PDF para generar una firma de confianza.
hi_there: Hola hi_there: Hola
thanks: Gracias thanks: Gracias
@@ -893,7 +958,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.
@@ -1428,6 +1493,12 @@ 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
name_a_z: Nombre A-Z
recently_used: Usado recientemente
newest_first: Más reciente primero
submission_sources: submission_sources:
api: API api: API
bulk: Envío masivo bulk: Envío masivo
@@ -1466,12 +1537,59 @@ 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
resend_pending: Reinvia in sospeso
ensure_unique_recipients: Assicurarsi destinatari unici
require_phone_2fa_to_open: Richiedi l'autenticazione a due fattori tramite telefono per aprire
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: Il mittente ha richiesto un'autenticazione a due fattori tramite una password monouso inviata al tuo numero di telefono <b>%{phone}</b>.
send_verification_code: Invia codice di verifica
code_has_been_resent: Il codice è stato inviato di nuovo
invalid_code: Codice non valido
always_enforce_signing_order: Applicare sempre l'ordine di firma always_enforce_signing_order: Applicare sempre l'ordine di firma
bcc_recipients: Destinatari BCC bcc_recipients: Destinatari BCC
edit_per_party: Modifica per partito edit_per_party: Modifica per partito
@@ -1500,6 +1618,8 @@ it: &it
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Sei stato invitato a %{account_name} %{product_name}. Registrati utilizzando il link qui sotto:' you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Sei stato invitato a %{account_name} %{product_name}. Registrati utilizzando il link qui sotto:'
sent_using_product_name_in_testing_mode_html: 'Inviato utilizzando <a href="%{product_url}">%{product_name}</a> in Modalità di Test' sent_using_product_name_in_testing_mode_html: 'Inviato utilizzando <a href="%{product_url}">%{product_name}</a> in Modalità di Test'
sent_using_product_name_free_document_signing_html: 'Inviato utilizzando la firma di documenti gratuita di <a href="%{product_url}">%{product_name}</a>.' sent_using_product_name_free_document_signing_html: 'Inviato utilizzando la firma di documenti gratuita di <a href="%{product_url}">%{product_name}</a>.'
sent_with_docuseal_pro_html: 'Inviato con <a href="%{product_url}">DocuSeal Pro</a>'
show_send_with_docuseal_pro_attribution_in_emails_html: Mostra la dicitura "Inviato con <span class="link">DocuSeal Pro</span>" nelle email
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: "Firma documenti con un certificato di fiducia fornito da DocuSeal. I tuoi documenti e i tuoi dati non vengono mai condivisi con DocuSeal. Il checksum PDF è fornito per generare una firma di fiducia." sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: "Firma documenti con un certificato di fiducia fornito da DocuSeal. I tuoi documenti e i tuoi dati non vengono mai condivisi con DocuSeal. Il checksum PDF è fornito per generare una firma di fiducia."
hi_there: Ciao hi_there: Ciao
thanks: Grazie thanks: Grazie
@@ -1619,7 +1739,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."
@@ -2154,6 +2274,12 @@ 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
name_a_z: Nome A-Z
recently_used: Recentemente usato
newest_first: Più recenti prima
submission_sources: submission_sources:
api: API api: API
bulk: Invio massivo bulk: Invio massivo
@@ -2192,12 +2318,59 @@ 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
resend_pending: Renvoyer en attente
ensure_unique_recipients: Assurer l'unicité des destinataires
require_phone_2fa_to_open: Requiert une 2FA par téléphone pour ouvrir
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: L'expéditeur a demandé une authentification à deux facteurs via un mot de passe à usage unique envoyé à votre numéro de téléphone <b>%{phone}</b>.
send_verification_code: Envoyer le code de vérification
code_has_been_resent: Le code a été renvoyé
invalid_code: Code invalide
always_enforce_signing_order: Toujours appliquer l'ordre de signature always_enforce_signing_order: Toujours appliquer l'ordre de signature
bcc_recipients: Destinataires en CCI bcc_recipients: Destinataires en CCI
edit_per_party: Éditer par partie edit_per_party: Éditer par partie
@@ -2227,6 +2400,8 @@ fr: &fr
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Vous avez été invité à %{account_name} %{product_name}. Veuillez vous inscrire en utilisant le lien ci-dessous:' you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Vous avez été invité à %{account_name} %{product_name}. Veuillez vous inscrire en utilisant le lien ci-dessous:'
sent_using_product_name_in_testing_mode_html: 'Envoyé en utilisant <a href="%{product_url}">%{product_name}</a> en Mode Test' sent_using_product_name_in_testing_mode_html: 'Envoyé en utilisant <a href="%{product_url}">%{product_name}</a> en Mode Test'
sent_using_product_name_free_document_signing_html: 'Envoyé en utilisant la signature de documents gratuite de <a href="%{product_url}">%{product_name}</a>.' sent_using_product_name_free_document_signing_html: 'Envoyé en utilisant la signature de documents gratuite de <a href="%{product_url}">%{product_name}</a>.'
sent_with_docuseal_pro_html: 'Envoyé avec <a href="%{product_url}">DocuSeal Pro</a>'
show_send_with_docuseal_pro_attribution_in_emails_html: Afficher "Envoyé avec <span class="link">DocuSeal Pro</span>" dans les e-mails
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Signez des documents avec un certificat de confiance fourni par DocuSeal. Vos documents et données ne sont jamais partagés avec DocuSeal. Un checksum PDF est fourni pour générer une signature de confiance. sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Signez des documents avec un certificat de confiance fourni par DocuSeal. Vos documents et données ne sont jamais partagés avec DocuSeal. Un checksum PDF est fourni pour générer une signature de confiance.
hi_there: Bonjour hi_there: Bonjour
thanks: Merci thanks: Merci
@@ -2347,7 +2522,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.
@@ -2882,6 +3057,12 @@ 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
name_a_z: Nom A-Z
recently_used: Récemment utilisé
newest_first: Le plus récent d'abord
submission_sources: submission_sources:
api: API api: API
bulk: Envoi en masse bulk: Envoi en masse
@@ -2920,12 +3101,59 @@ 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
resend_pending: Re-enviar pendente
ensure_unique_recipients: Garantir destinatários únicos
require_phone_2fa_to_open: Necessário autenticação de dois fatores via telefone para abrir
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: O remetente solicitou uma autenticação de dois fatores via senha de uso único enviada para seu número <b>%{phone}</b>.
send_verification_code: Enviar código de verificação
code_has_been_resent: Código foi reenviado
invalid_code: Código inválido
always_enforce_signing_order: Sempre impor a ordem de assinatura always_enforce_signing_order: Sempre impor a ordem de assinatura
bcc_recipients: Destinatários BCC bcc_recipients: Destinatários BCC
edit_per_party: Edita por festa edit_per_party: Edita por festa
@@ -2955,6 +3183,8 @@ pt: &pt
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Você foi convidado para %{account_name} %{product_name}. Inscreva-se usando o link abaixo:' you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Você foi convidado para %{account_name} %{product_name}. Inscreva-se usando o link abaixo:'
sent_using_product_name_in_testing_mode_html: 'Enviado usando <a href="%{product_url}">%{product_name}</a> no Modo de Teste' sent_using_product_name_in_testing_mode_html: 'Enviado usando <a href="%{product_url}">%{product_name}</a> no Modo de Teste'
sent_using_product_name_free_document_signing_html: 'Enviado usando a assinatura gratuita de documentos de <a href="%{product_url}">%{product_name}</a>.' sent_using_product_name_free_document_signing_html: 'Enviado usando a assinatura gratuita de documentos de <a href="%{product_url}">%{product_name}</a>.'
sent_with_docuseal_pro_html: 'Enviado com <a href="%{product_url}">DocuSeal Pro</a>'
show_send_with_docuseal_pro_attribution_in_emails_html: Mostrar "Enviado com <span class="link">DocuSeal Pro</span>" nos e-mails
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Assine documentos com certificado confiável fornecido pela DocuSeal. Seus documentos e dados nunca são compartilhados com a DocuSeal. O checksum do PDF é fornecido para gerar uma assinatura confiável. sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Assine documentos com certificado confiável fornecido pela DocuSeal. Seus documentos e dados nunca são compartilhados com a DocuSeal. O checksum do PDF é fornecido para gerar uma assinatura confiável.
hi_there: Olá hi_there: Olá
thanks: Obrigado thanks: Obrigado
@@ -3074,7 +3304,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.
@@ -3223,7 +3453,7 @@ pt: &pt
verify_pdf: Verificar PDF verify_pdf: Verificar PDF
sign_out: Sair sign_out: Sair
page_number: 'Página %{number}' page_number: 'Página %{number}'
powered_by: Oferecido por powered_by: Desenvolvido por
count_documents_signed_with_html: '<b>%{count}</b> documentos assinados com' count_documents_signed_with_html: '<b>%{count}</b> documentos assinados com'
storage: Armazenamento storage: Armazenamento
notifications: Notificações notifications: Notificações
@@ -3609,6 +3839,12 @@ 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
name_a_z: Nome A-Z
recently_used: Recentemente usado
newest_first: Mais recente primeiro
submission_sources: submission_sources:
api: API api: API
bulk: Envio em massa bulk: Envio em massa
@@ -3647,12 +3883,60 @@ 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
resend_pending: Ausstehende erneut senden
ensure_unique_recipients: Stellen Sie einzigartige Empfänger sicher
require_phone_2fa_to_open: Telefon-2FA zum Öffnen erforderlich
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: Der Absender hat eine Zwei-Faktor-Authentifizierung per Einmalpasswort angefordert, das an Ihre <b>%{phone}</b>-Telefonnummer gesendet wurde.
send_verification_code: Bestätigungscode senden
code_has_been_resent: Code wurde erneut gesendet
invalid_code: Ungültiger Code
always_enforce_signing_order: Immer die Reihenfolge der Unterschriften erzwingen always_enforce_signing_order: Immer die Reihenfolge der Unterschriften erzwingen
bcc_recipients: BCC-Empfänger bcc_recipients: BCC-Empfänger
edit_per_party: Bearbeiten pro Partei edit_per_party: Bearbeiten pro Partei
@@ -3682,6 +3966,8 @@ de: &de
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Sie wurden zu %{account_name} %{product_name} eingeladen. Bitte registrieren Sie sich über den folgenden Link:' you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Sie wurden zu %{account_name} %{product_name} eingeladen. Bitte registrieren Sie sich über den folgenden Link:'
sent_using_product_name_in_testing_mode_html: 'Gesendet über <a href="%{product_url}">%{product_name}</a> im Testmodus' sent_using_product_name_in_testing_mode_html: 'Gesendet über <a href="%{product_url}">%{product_name}</a> im Testmodus'
sent_using_product_name_free_document_signing_html: 'Gesendet mit der kostenlosen Dokumentensignierung von <a href="%{product_url}">%{product_name}</a>.' sent_using_product_name_free_document_signing_html: 'Gesendet mit der kostenlosen Dokumentensignierung von <a href="%{product_url}">%{product_name}</a>.'
sent_with_docuseal_pro_html: Gesendet mit <a href="%{product_url}">DocuSeal Pro</a>
show_send_with_docuseal_pro_attribution_in_emails_html: '"Gesendet mit <span class="link">DocuSeal Pro</span>" in E-Mails anzeigen'
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Unterzeichnen Sie Dokumente mit einem vertrauenswürdigen Zertifikat von DocuSeal. Ihre Dokumente und Daten werden niemals mit DocuSeal geteilt. Eine PDF-Prüfziffer wird bereitgestellt, um eine vertrauenswürdige Signatur zu generieren. sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Unterzeichnen Sie Dokumente mit einem vertrauenswürdigen Zertifikat von DocuSeal. Ihre Dokumente und Daten werden niemals mit DocuSeal geteilt. Eine PDF-Prüfziffer wird bereitgestellt, um eine vertrauenswürdige Signatur zu generieren.
hi_there: Hallo hi_there: Hallo
thanks: Danke thanks: Danke
@@ -3801,7 +4087,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.
@@ -4336,6 +4622,12 @@ 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
name_a_z: Name A-Z
recently_used: Kürzlich verwendet
newest_first: Neueste zuerst
submission_sources: submission_sources:
api: API api: API
bulk: Massenversand bulk: Massenversand
@@ -4374,12 +4666,57 @@ 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
read: Lese deine Daten read: Lese deine Daten
pl: pl:
require_phone_2fa_to_open: Wymagaj uwierzytelniania telefonicznego 2FA do otwarcia
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: Nadawca zażądał uwierzytelnienia dwuetapowego poprzez jednorazowe hasło wysłane na Twój <b>%{phone}</b> numer telefonu.
send_verification_code: Wyślij kod weryfikacyjny
code_has_been_resent: Kod został wysłany ponownie
invalid_code: Niepoprawny kod
awaiting_completion_by_the_other_party: "Oczekuje na dokończenie przez drugą stronę" awaiting_completion_by_the_other_party: "Oczekuje na dokończenie przez drugą stronę"
view: Widok view: Widok
hi_there: Cześć, hi_there: Cześć,
@@ -4437,8 +4774,16 @@ pl:
too_many_attempts: Zbyt wiele prób. too_many_attempts: Zbyt wiele prób.
verification_code: Kod Weryfikacyjny verification_code: Kod Weryfikacyjny
resend_code: Wyślij Kod Ponownie resend_code: Wyślij Kod Ponownie
powered_by: 'Napędzany prze'
count_documents_signed_with_html: '<b>%{count}</b> dokumentów podpisanych za pomocą'
open_source_documents_software: 'oprogramowanie do dokumentów open source'
uk: uk:
require_phone_2fa_to_open: Вимагати двофакторну автентифікацію через телефон для відкриття
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: Відправник запросив двофакторну автентифікацію за допомогою одноразового пароля, відправленого на ваш номер телефону <b>%{phone}</b>.
send_verification_code: Надіслати код підтвердження
code_has_been_resent: Код повторно надіслано
invalid_code: Невірний код
awaiting_completion_by_the_other_party: "Очікує завершення з боку іншої сторони" awaiting_completion_by_the_other_party: "Очікує завершення з боку іншої сторони"
view: Переглянути view: Переглянути
hi_there: Привіт, hi_there: Привіт,
@@ -4496,8 +4841,16 @@ uk:
too_many_attempts: Забагато спроб. too_many_attempts: Забагато спроб.
verification_code: Код перевірки verification_code: Код перевірки
resend_code: Відправити код знову resend_code: Відправити код знову
powered_by: 'Працює на базі'
count_documents_signed_with_html: '<b>%{count}</b> документів підписано за допомогою'
open_source_documents_software: 'відкрите програмне забезпечення для документів'
cs: cs:
require_phone_2fa_to_open: Vyžadovat otevření pomocí telefonního 2FA
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: Odesílatel požádal o dvoufaktorové ověření pomocí jednorázového hesla zaslaného na vaše telefonní číslo <b>%{phone}</b>.
send_verification_code: Odeslat ověřovací kód
code_has_been_resent: Kód byl znovu odeslán
invalid_code: Neplatný kód
awaiting_completion_by_the_other_party: "Čeká se na dokončení druhou stranou" awaiting_completion_by_the_other_party: "Čeká se na dokončení druhou stranou"
view: Zobrazit view: Zobrazit
hi_there: Ahoj, hi_there: Ahoj,
@@ -4555,8 +4908,16 @@ cs:
too_many_attempts: Příliš mnoho pokusů. too_many_attempts: Příliš mnoho pokusů.
verification_code: Ověřovací Kód verification_code: Ověřovací Kód
resend_code: Znovu Odeslat Kód resend_code: Znovu Odeslat Kód
powered_by: 'Poháněno'
count_documents_signed_with_html: '<b>%{count}</b> dokumentů podepsáno pomocí'
open_source_documents_software: 'open source software pro dokumenty'
he: he:
require_phone_2fa_to_open: דרוש אימות דו-שלבי באמצעות טלפון לפתיחה
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: השולח ביקש אימות דו-שלבי באמצעות סיסמה חד פעמית שנשלחה למספר הטלפון שלך <b>%{phone}</b>.
send_verification_code: שלח קוד אימות
code_has_been_resent: הקוד נשלח מחדש
invalid_code: קוד שגוי
awaiting_completion_by_the_other_party: "המתנה להשלמה מצד הצד השני" awaiting_completion_by_the_other_party: "המתנה להשלמה מצד הצד השני"
view: תצוגה view: תצוגה
hi_there: שלום, hi_there: שלום,
@@ -4614,8 +4975,16 @@ he:
too_many_attempts: יותר מדי ניסיונות. too_many_attempts: יותר מדי ניסיונות.
verification_code: קוד אימות verification_code: קוד אימות
resend_code: שלח קוד מחדש resend_code: שלח קוד מחדש
powered_by: 'מופעל על ידי'
count_documents_signed_with_html: '<b>%{count}</b> מסמכים נחתמו באמצעות'
open_source_documents_software: 'תוכנה בקוד פתוח למסמכים'
nl: nl:
require_phone_2fa_to_open: Vereis telefoon 2FA om te openen
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: De afzender heeft gevraagd om tweefactorauthenticatie via een eenmalig wachtwoord, verzonden naar uw <b>%{phone}</b> telefoonnummer.
send_verification_code: Verificatiecode verzenden
code_has_been_resent: Code is opnieuw verzonden
invalid_code: Invalid code
awaiting_completion_by_the_other_party: "In afwachting van voltooiing door de andere partij" awaiting_completion_by_the_other_party: "In afwachting van voltooiing door de andere partij"
view: Bekijken view: Bekijken
hi_there: Hallo, hi_there: Hallo,
@@ -4673,8 +5042,16 @@ nl:
too_many_attempts: Te veel pogingen. too_many_attempts: Te veel pogingen.
verification_code: Verificatiecode verification_code: Verificatiecode
resend_code: Code Opnieuw Verzenden resend_code: Code Opnieuw Verzenden
powered_by: 'Aangedreven door'
count_documents_signed_with_html: '<b>%{count}</b> documenten ondertekend met'
open_source_documents_software: 'open source documenten software'
ar: ar:
require_phone_2fa_to_open: "تطلب فتح عبر تحقق الهاتف ذو العاملين"
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: "المرسل طلب تحقق ذو عاملين عبر كلمة مرور لمرة واحدة مرسل إلى رقم هاتفك <b>%{phone}</b>."
send_verification_code: "إرسال رمز التحقق"
code_has_been_resent: "تم إعادة إرسال الرمز"
invalid_code: "رمز غير صالح"
awaiting_completion_by_the_other_party: "في انتظار إكتمال الطرف الآخر" awaiting_completion_by_the_other_party: "في انتظار إكتمال الطرف الآخر"
view: عرض view: عرض
hi_there: مرحبا, hi_there: مرحبا,
@@ -4732,8 +5109,16 @@ ar:
too_many_attempts: عدد المحاولات كبير جدًا. too_many_attempts: عدد المحاولات كبير جدًا.
verification_code: رمز التحقق verification_code: رمز التحقق
resend_code: إعادة إرسال الرمز resend_code: إعادة إرسال الرمز
powered_by: 'مشغل بواسطة'
count_documents_signed_with_html: '<b>%{count}</b> مستندات تم توقيعها باستخدام'
open_source_documents_software: 'برنامج مستندات مفتوح المصدر'
ko: ko:
require_phone_2fa_to_open: 휴대폰 2FA를 열 때 요구함
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: 발신자가 <b>%{phone}</b> 전화번호로 보내진 일회용 비밀번호를 통해 이중 인증을 요청했습니다.
send_verification_code: 인증 코드 보내기
code_has_been_resent: 코드가 재전송되었습니다.
invalid_code: 잘못된 코드
awaiting_completion_by_the_other_party: "다른 당사자의 완료를 기다리고 있습니다" awaiting_completion_by_the_other_party: "다른 당사자의 완료를 기다리고 있습니다"
view: 보기 view: 보기
hi_there: 안녕하세요, hi_there: 안녕하세요,
@@ -4791,6 +5176,76 @@ ko:
too_many_attempts: 시도 횟수가 너무 많습니다. too_many_attempts: 시도 횟수가 너무 많습니다.
verification_code: 인증 코드 verification_code: 인증 코드
resend_code: 코드 재전송 resend_code: 코드 재전송
powered_by: '제공:'
count_documents_signed_with_html: '<b>%{count}</b>개의 문서가 다음을 통해 서명됨'
open_source_documents_software: '오픈소스 문서 소프트웨어'
ja:
require_phone_2fa_to_open: 電話による2段階認証が必要です
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: 送信者は、<b>%{phone}</b> に送信されたワンタイムパスワードによる2段階認証を要求しました。
send_verification_code: 認証コードを送信
code_has_been_resent: コードが再送信されました
invalid_code: 無効なコードです
awaiting_completion_by_the_other_party: 他の当事者による完了を待機中
view: 表示
hi_there: こんにちは
download: ダウンロード
decline: 辞退
declined: 辞退済み
decline_reason: 辞退の理由
provide_a_reason: 理由を入力してください
notify_the_sender_with_the_reason_you_declined: 辞退理由を送信者に通知してください
form_has_been_declined_on_html: '<span class="font-semibold">%{time}</span> にフォームが辞退されました'
name_declined_by_submitter: '"%{name}" は %{submitter} により辞退されました'
name_declined_by_submitter_with_the_following_reason: '"%{name}" は %{submitter} により次の理由で辞退されました:'
email: メール
digitally_signed_by: 電子署名者
role: 役割
provide_your_email_to_start: 開始するにはメールアドレスを入力してください
start: 開始
reason: 理由
starting: 開始中
form_expired_at_html: '<span class="font-semibold">%{time}</span> にフォームの有効期限が切れました'
invited_by_html: '<span class="font-semibold">%{name}</span> により招待されました'
you_have_been_invited_to_submit_a_form: フォームの提出に招待されました
verification_code_code: '認証コード: %{code}'
signed_on_time: '%{time} に署名済み'
completed_on_time: '%{time} に完了'
document_has_been_signed_already: ドキュメントはすでに署名されています
form_has_been_submitted_already: フォームはすでに送信されています
send_copy_to_email: メールにコピーを送信
sending: 送信中
resubmit: 再送信
form_has_been_deleted_by_html: '<span class="font-semibold">%{name}</span> によりフォームが削除されました。'
or: または
download_documents: ドキュメントをダウンロード
downloading: ダウンロード中
completed_successfully: 正常に完了しました
password: パスワード
sign_in: サインイン
signing_in: サインイン中
sign_in_with_microsoft: Microsoftでサインイン
sign_in_with_google: Googleでサインイン
forgot_your_password_: パスワードをお忘れですか?
create_free_account: 無料アカウントを作成
already_have_an_account: すでにアカウントをお持ちですか?
first_name: 名
last_name: 姓
sign_up: 登録
signing_up: 登録中
profile_details: プロフィールの詳細
sign_up_with_google: Googleで登録
sign_up_with_microsoft: Microsoftで登録
by_creating_an_account_you_agree_to_our_html: '<a target="_blank" href="https://www.docuseal.com/privacy">プライバシーポリシー</a>および<a target="_blank" href="https://www.docuseal.com/terms">利用規約</a>に同意の上、アカウントを作成します。'
enter_email_to_continue: 続行するにはメールを入力してください
the_code_has_been_sent_to_your_email: コードがあなたのメールに送信されました
enter_the_verification_code_from_your_email: メールに記載された認証コードを入力してください
too_many_attempts: 試行回数が多すぎます
verification_code: 認証コード
resend_code: コードを再送信
powered_by: '提供元:'
count_documents_signed_with_html: '<b>%{count}</b> 件のドキュメントが以下で署名されました'
open_source_documents_software: 'オープンソースのドキュメントソフトウェア'
en-US: en-US:
<<: *en <<: *en
+3 -1
View File
@@ -8,7 +8,9 @@ class ApiPathConsiderJsonMiddleware
def call(env) def call(env)
if env['PATH_INFO'].starts_with?('/api') && if env['PATH_INFO'].starts_with?('/api') &&
(!env['PATH_INFO'].ends_with?('/documents') || env['REQUEST_METHOD'] != 'POST') && (!env['PATH_INFO'].ends_with?('/documents') || env['REQUEST_METHOD'] != 'POST') &&
!env['PATH_INFO'].ends_with?('/attachments') !env['PATH_INFO'].ends_with?('/attachments') &&
!env['PATH_INFO'].ends_with?('/submitter_sms_clicks') &&
!env['PATH_INFO'].ends_with?('/submitter_email_clicks')
env['CONTENT_TYPE'] = 'application/json' env['CONTENT_TYPE'] = 'application/json'
end end
+1 -1
View File
@@ -12,7 +12,7 @@ module Docuseal
DISCORD_URL = 'https://discord.gg/qygYCDGck9' DISCORD_URL = 'https://discord.gg/qygYCDGck9'
TWITTER_URL = 'https://twitter.com/docusealco' TWITTER_URL = 'https://twitter.com/docusealco'
TWITTER_HANDLE = '@docusealco' TWITTER_HANDLE = '@docusealco'
CHATGPT_URL = 'https://chatgpt.com/g/g-9hg8AAw0r-docuseal' CHATGPT_URL = "#{PRODUCT_URL}/chat".freeze
SUPPORT_EMAIL = 'support@docuseal.com' SUPPORT_EMAIL = 'support@docuseal.com'
HOST = ENV.fetch('HOST', 'localhost') HOST = ENV.fetch('HOST', 'localhost')
AATL_CERT_NAME = 'docuseal_aatl' AATL_CERT_NAME = 'docuseal_aatl'
+30 -2
View File
@@ -19,6 +19,7 @@ module ReplaceEmailVariables
SUBMITTERS_N_EMAIL = /\{+submitters\[(?<index>\d+)\]\.email\}+/i SUBMITTERS_N_EMAIL = /\{+submitters\[(?<index>\d+)\]\.email\}+/i
SUBMITTERS_N_NAME = /\{+submitters\[(?<index>\d+)\]\.name\}+/i SUBMITTERS_N_NAME = /\{+submitters\[(?<index>\d+)\]\.name\}+/i
SUBMITTERS_N_FIRST_NAME = /\{+submitters\[(?<index>\d+)\]\.first_name\}+/i SUBMITTERS_N_FIRST_NAME = /\{+submitters\[(?<index>\d+)\]\.first_name\}+/i
SUBMITTERS_N_FIELD_VALUE = /\{+submitters\[(?<index>\d+)\]\.(?<field_name>[^}]+)\}+/i
DOCUMENTS_LINKS = /\{+documents\.links\}+/i DOCUMENTS_LINKS = /\{+documents\.links\}+/i
DOCUMENTS_LINK = /\{+documents\.link\}+/i DOCUMENTS_LINK = /\{+documents\.link\}+/i
@@ -59,6 +60,10 @@ module ReplaceEmailVariables
build_submitters_n_field(submitter.submission, match[:index].to_i - 1, :first_name) build_submitters_n_field(submitter.submission, match[:index].to_i - 1, :first_name)
end end
text = replace(text, SUBMITTERS_N_FIELD_VALUE, html_escape:) do |match|
build_submitters_n_field(submitter.submission, match[:index].to_i - 1, :values, match[:field_name].to_s.strip)
end
replace(text, SENDER_EMAIL, html_escape:) { submitter.submission.created_by_user&.email.to_s.sub(/\+\w+@/, '@') } replace(text, SENDER_EMAIL, html_escape:) { submitter.submission.created_by_user&.email.to_s.sub(/\+\w+@/, '@') }
end end
# rubocop:enable Metrics # rubocop:enable Metrics
@@ -69,10 +74,33 @@ module ReplaceEmailVariables
) )
end end
def build_submitters_n_field(submission, index, field_name) def build_submitters_n_field(submission, index, field_name, value_name = nil)
uuid = (submission.template_submitters || submission.template.submitters).dig(index, 'uuid') uuid = (submission.template_submitters || submission.template.submitters).dig(index, 'uuid')
submission.submitters.find { |s| s.uuid == uuid }.try(field_name) submitter = submission.submitters.find { |s| s.uuid == uuid }
return unless submitter
value = submitter.try(field_name)
if value_name
field = (submission.template_fields || submission.template.fields).find { |e| e['name'] == value_name }
return unless field
value =
if field['type'].in?(%w[image signature initials stamp payment file])
attachment_uuid = Array.wrap(value[field['uuid']]).first
attachment = submitter.attachments.find { |e| e.uuid == attachment_uuid }
ActiveStorage::Blob.proxy_url(attachment.blob) if attachment
else
value[field&.dig('uuid')]
end
end
value
end end
def replace(text, var, html_escape: false) def replace(text, var, html_escape: false)
+6 -11
View File
@@ -134,16 +134,11 @@ module Submissions
end end
def filtered_conditions_schema(submission, values: nil, include_submitter_uuid: nil) def filtered_conditions_schema(submission, values: nil, include_submitter_uuid: nil)
fields_uuid_index = nil
(submission.template_schema || submission.template.schema).filter_map do |item| (submission.template_schema || submission.template.schema).filter_map do |item|
if item['conditions'].present? if item['conditions'].present?
fields_uuid_index ||=
(submission.template_fields || submission.template.fields).index_by { |f| f['uuid'] }
values ||= submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } values ||= submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) }
next unless check_item_conditions(item, values, fields_uuid_index, include_submitter_uuid:) next unless check_item_conditions(item, values, submission.fields_uuid_index, include_submitter_uuid:)
end end
item item
@@ -151,21 +146,21 @@ module Submissions
end end
def filtered_conditions_fields(submitter, only_submitter_fields: true) def filtered_conditions_fields(submitter, only_submitter_fields: true)
fields = submitter.submission.template_fields || submitter.submission.template.fields submission = submitter.submission
fields = submission.template_fields || submission.template.fields
fields_uuid_index = nil
values = nil values = nil
fields.filter_map do |field| fields.filter_map do |field|
next if field['submitter_uuid'] != submitter.uuid && only_submitter_fields next if field['submitter_uuid'] != submitter.uuid && only_submitter_fields
if field['conditions'].present? if field['conditions'].present?
fields_uuid_index ||= fields.index_by { |f| f['uuid'] } values ||= submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) }
values ||= submitter.submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) }
submitter_conditions = [] submitter_conditions = []
next unless check_item_conditions(field, values, fields_uuid_index, next unless check_item_conditions(field, values, submission.fields_uuid_index,
include_submitter_uuid: submitter.uuid, include_submitter_uuid: submitter.uuid,
submitter_conditions_acc: submitter_conditions) submitter_conditions_acc: submitter_conditions)
@@ -65,6 +65,12 @@ module Submissions
raise BaseError, 'Defined more signing parties than in template' raise BaseError, 'Defined more signing parties than in template'
end end
if template.preferences['validate_unique_submitters'] == true
submission_emails = submission.submitters.filter_map(&:email)
raise BaseError, 'Recipient emails should differ' if submission_emails.uniq.size != submission_emails.size
end
next if submission.submitters.blank? next if submission.submitters.blank?
maybe_add_invite_submitters(submission, template) maybe_add_invite_submitters(submission, template)
+2 -2
View File
@@ -50,12 +50,12 @@ module Submissions
} }
document.sign(io, **sign_params) document.sign(io, **sign_params)
Submissions::GenerateResultAttachments.maybe_enable_ltv(io, sign_params)
else else
document.write(io) document.write(io)
end end
Submissions::GenerateResultAttachments.maybe_enable_ltv(io, sign_params)
ActiveStorage::Attachment.create!( ActiveStorage::Attachment.create!(
blob: ActiveStorage::Blob.create_and_upload!( blob: ActiveStorage::Blob.create_and_upload!(
io: io.tap(&:rewind), filename: "#{I18n.t('audit_log')} - #{submission.template.name}.pdf" io: io.tap(&:rewind), filename: "#{I18n.t('audit_log')} - #{submission.template.name}.pdf"
@@ -24,12 +24,12 @@ module Submissions
} }
sign_pdf(io, pdf, sign_params) sign_pdf(io, pdf, sign_params)
Submissions::GenerateResultAttachments.maybe_enable_ltv(io, sign_params)
else else
pdf.write(io, incremental: true, validate: true) pdf.write(io, incremental: true, validate: true)
end end
Submissions::GenerateResultAttachments.maybe_enable_ltv(io, sign_params)
ActiveStorage::Attachment.create!( ActiveStorage::Attachment.create!(
blob: ActiveStorage::Blob.create_and_upload!( blob: ActiveStorage::Blob.create_and_upload!(
io: io.tap(&:rewind), filename: "#{submission.template.name}.pdf" io: io.tap(&:rewind), filename: "#{submission.template.name}.pdf"
+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
+14 -4
View File
@@ -221,7 +221,7 @@ module Submissions
font_variant = nil unless font_name.in?(DEFAULT_FONTS) font_variant = nil unless font_name.in?(DEFAULT_FONTS)
end end
font = pdf.fonts.add(font_name, variant: font_variant) font = pdf.fonts.add(font_name, variant: font_variant, custom_encoding: font_name.in?(DEFAULT_FONTS))
value = submitter.values[field['uuid']] value = submitter.values[field['uuid']]
value = field['default_value'] if field['type'] == 'heading' value = field['default_value'] if field['type'] == 'heading'
@@ -229,8 +229,9 @@ module Submissions
text_align = field.dig('preferences', 'align').to_s.to_sym.presence || text_align = field.dig('preferences', 'align').to_s.to_sym.presence ||
(value.to_s.match?(RTL_REGEXP) ? :right : :left) (value.to_s.match?(RTL_REGEXP) ? :right : :left)
layouter = HexaPDF::Layout::TextLayouter.new(text_valign: :center, text_align:, text_valign = (field.dig('preferences', 'valign').to_s.presence || 'center').to_sym
font:, font_size:)
layouter = HexaPDF::Layout::TextLayouter.new(text_valign:, text_align:, font:, font_size:)
next if Array.wrap(value).compact_blank.blank? next if Array.wrap(value).compact_blank.blank?
@@ -515,10 +516,19 @@ module Submissions
0 0
end end
align_y_diff =
if text_valign == :top
0
elsif text_valign == :bottom
height_diff + TEXT_TOP_MARGIN
else
height_diff / 2
end
layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width, layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width,
height_diff.positive? ? box_height : area['h'] * height) height_diff.positive? ? box_height : area['h'] * height)
.draw(canvas, (area['x'] * width) - right_align_x_adjustment + TEXT_LEFT_MARGIN, .draw(canvas, (area['x'] * width) - right_align_x_adjustment + TEXT_LEFT_MARGIN,
height - (area['y'] * height) + height_diff - TEXT_TOP_MARGIN) height - (area['y'] * height) + align_y_diff - TEXT_TOP_MARGIN)
end end
end end
end end
+1
View File
@@ -97,6 +97,7 @@ module Submitters
def send_signature_requests(submitters, delay_seconds: nil) def send_signature_requests(submitters, delay_seconds: nil)
submitters.each_with_index do |submitter, index| submitters.each_with_index do |submitter, index|
next if submitter.email.blank? next if submitter.email.blank?
next if submitter.declined_at?
next if submitter.preferences['send_email'] == false next if submitter.preferences['send_email'] == false
if delay_seconds if delay_seconds
+2 -1
View File
@@ -49,7 +49,8 @@ module Submitters
def serialize_events(events) def serialize_events(events)
events.map do |event| events.map do |event|
event.as_json(only: %i[id submitter_id event_type event_timestamp]).merge('data' => event.data.slice('reason')) event.as_json(only: %i[id submitter_id event_type event_timestamp])
.merge('data' => event.data.slice('reason', 'firstname', 'lastname', 'method', 'country'))
end end
end end
end end
+12 -5
View File
@@ -171,6 +171,8 @@ module Submitters
end end
def build_formula_values(submitter) def build_formula_values(submitter)
submission_values = nil
computed_values = submitter.submission.template_fields.each_with_object({}) do |field, acc| computed_values = submitter.submission.template_fields.each_with_object({}) do |field, acc|
next if field['submitter_uuid'] != submitter.uuid next if field['submitter_uuid'] != submitter.uuid
next if field['type'] == 'payment' next if field['type'] == 'payment'
@@ -179,7 +181,14 @@ module Submitters
next if formula.blank? next if formula.blank?
acc[field['uuid']] = calculate_formula_value(formula, submitter.values.merge(acc.compact_blank)) submission_values ||=
if submitter.submission.template_submitters.size > 1
merge_submitters_values(submitter)
else
submitter.values
end
acc[field['uuid']] = calculate_formula_value(formula, submission_values.merge(acc.compact_blank))
end end
computed_values.compact_blank computed_values.compact_blank
@@ -204,8 +213,6 @@ module Submitters
def maybe_remove_condition_values(submitter, required_field_uuids_acc: nil) def maybe_remove_condition_values(submitter, required_field_uuids_acc: nil)
submission = submitter.submission submission = submitter.submission
fields_uuid_index = submission.template_fields.index_by { |e| e['uuid'] }
submitters_values = nil submitters_values = nil
has_other_submitters = submission.template_submitters.size > 1 has_other_submitters = submission.template_submitters.size > 1
@@ -228,11 +235,11 @@ module Submitters
end end
if has_other_submitters && !submitters_values && if has_other_submitters && !submitters_values &&
field_conditions_other_submitter?(submitter, field, fields_uuid_index) field_conditions_other_submitter?(submitter, field, submission.fields_uuid_index)
submitters_values = merge_submitters_values(submitter) submitters_values = merge_submitters_values(submitter)
end end
unless check_field_conditions(submitters_values || submitter.values, field, fields_uuid_index) unless check_field_conditions(submitters_values || submitter.values, field, submission.fields_uuid_index)
submitter.values.delete(field['uuid']) submitter.values.delete(field['uuid'])
required_field_uuids_acc.delete(field['uuid']) required_field_uuids_acc.delete(field['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
+35
View File
@@ -0,0 +1,35 @@
# frozen_string_literal: true
module Templates
module Order
module_function
def call(templates, current_user, order)
case order
when 'used_at'
subquery = Submission.select(:template_id, Submission.arel_table[:created_at].maximum.as('created_at'))
.where(account_id: current_user.account_id)
.group(:template_id)
templates = templates.joins(
Template.arel_table
.join(subquery.arel.as('submissions'), Arel::Nodes::OuterJoin)
.on(Template.arel_table[:id].eq(Submission.arel_table[:template_id]))
.join_sources
)
templates.order(
Arel::Nodes::Case.new
.when(Submission.arel_table[:created_at].gt(Template.arel_table[:updated_at]))
.then(Submission.arel_table[:created_at])
.else(Template.arel_table[:updated_at])
.desc
)
when 'name'
templates.order(name: :asc)
else
templates.order(id: :desc)
end
end
end
end
+2 -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",
@@ -56,7 +57,7 @@
] ]
}, },
"browserslist": [ "browserslist": [
"last 3 years" "last 5 years"
], ],
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "^7.21.8", "@babel/eslint-parser": "^7.21.8",
+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
+3
View File
@@ -506,6 +506,9 @@ RSpec.describe 'Signing Form', type: :system do
find('#expand_form_button').click find('#expand_form_button').click
find('span[data-tip="Click to upload"]').click find('span[data-tip="Click to upload"]').click
find('input[type="file"]', visible: false).attach_file(Rails.root.join('spec/fixtures/sample-image.png')) find('input[type="file"]', visible: false).attach_file(Rails.root.join('spec/fixtures/sample-image.png'))
sleep 0.1
click_button 'Complete' click_button 'Complete'
expect(page).to have_content('Document has been signed!') expect(page).to have_content('Document has been signed!')
+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"