Compare commits

...

74 Commits

Author SHA1 Message Date
Alex Turchyn 1f48be135c Merge from docusealco/wip 2025-03-31 13:02:51 +03:00
Pete Matsyburka 1cae12b728 adjust validation 2025-03-29 23:15:45 +02:00
Alex Turchyn 86d2cd93f8 fix signature uploading 2025-03-29 15:44:19 +02:00
Alex Turchyn b0110d9340 update signature validation 2025-03-29 14:50:26 +02:00
Alex Turchyn 7e70f4fb14 update translations 2025-03-29 13:37:21 +02:00
Pete Matsyburka 28e3d65e63 improve required field handling 2025-03-28 22:42:27 +02:00
Pete Matsyburka f1a2b71b59 fix smtp 2025-03-27 19:50:09 +02:00
Pete Matsyburka 47c7270911 enforce signing order 2025-03-27 19:23:20 +02:00
Pete Matsyburka 4e1fa336a2 add personalization options 2025-03-27 19:23:19 +02:00
Pete Matsyburka 244bc32136 fix preview url 2025-03-27 00:52:35 +02:00
Pete Matsyburka 42c30bdfd0 ensure generated doc completed 2025-03-26 13:14:59 +02:00
Alex Turchyn 0abf48ddba update preview image 2025-03-26 13:14:07 +02:00
Alex Turchyn ef05f030de Merge from docusealco/wip 2025-03-24 14:03:04 +02:00
Pete Matsyburka b8c36ff09c update gems 2025-03-24 09:48:37 +02:00
Pete Matsyburka 1ba8a9a374 fix pdf fields extract 2025-03-24 09:44:10 +02:00
Pete Matsyburka 661e9b69fb do not validate values param 2025-03-21 13:42:20 +02:00
Pete Matsyburka a1aeabccff fix scroll 2025-03-20 21:07:28 +02:00
Pete Matsyburka 0e5e0e7772 fix pdf upload 2025-03-19 17:57:46 +02:00
Alex Turchyn 2a74fdd095 add field area classes 2025-03-19 15:33:02 +02:00
Alex Turchyn ac426e55b2 add missing CSS class for custom fields 2025-03-18 22:24:54 +02:00
Pete Matsyburka ad2b35673a fix class 2025-03-18 20:31:00 +02:00
Pete Matsyburka af1e8fa6f2 adjust i18n 2025-03-18 17:34:31 +02:00
Pete Matsyburka 5800cb6c5a fix default value condition 2025-03-18 17:34:31 +02:00
Alex Turchyn 64d3360e82 add ui classes 2025-03-18 17:34:11 +02:00
Alex Turchyn 3ae98bf03e improve multiple submitters error message 2025-03-18 17:31:40 +02:00
Alex Turchyn 65e81d90f4 fix acro radio fields 2025-03-17 22:38:42 +02:00
Pete Matsyburka 4fb9637e2d add merge roles param 2025-03-17 13:30:07 +02:00
Pete Matsyburka 6b3726dbd5 update gem 2025-03-15 15:01:46 +02:00
Pete Matsyburka c5b0f2c76c fix require value validation 2025-03-13 19:21:28 +02:00
Pete Matsyburka ea18f9e28a fix combined pdf 2025-03-13 14:21:53 +02:00
Pete Matsyburka d229c75978 fix draw field 2025-03-12 19:14:00 +02:00
Pete Matsyburka ecbe3c3e4f PRESIGNED_URLS_EXPIRE_MINUTES 2025-03-12 14:24:33 +02:00
Pete Matsyburka 38a5cb968b extract pdf date fields 2025-03-12 10:23:24 +02:00
Pete Matsyburka a30ee9e961 fix perform at 2025-03-11 11:28:22 +02:00
Pete Matsyburka eba27004dc font format date number 2025-03-11 10:51:04 +02:00
Pete Matsyburka 09849acd2f add submission.expired webhook 2025-03-10 22:53:56 +02:00
Pete Matsyburka 19e9047a39 reduce max attempts 2025-03-10 15:54:21 +02:00
Pete Matsyburka 82b6bdd9ad update gem 2025-03-10 15:05:09 +02:00
Alex Turchyn 5764b4dac1 Merge from docusealco/wip 2025-03-10 13:13:01 +02:00
Pete Matsyburka 3affd67d24 add font field preferences 2025-03-09 12:46:28 +02:00
Pete Matsyburka d5a98a0f9e web concurrency auto 2025-03-08 16:11:11 +02:00
Pete Matsyburka 9629cc7226 make remember configurable 2025-03-05 20:44:32 +02:00
Pete Matsyburka 06415ad869 add routes hook 2025-03-05 18:29:39 +02:00
Pete Matsyburka 3f872da435 fix mask 2025-03-05 18:07:38 +02:00
Pete Matsyburka bc1d5a9825 add font type 2025-03-05 12:12:16 +02:00
Pete Matsyburka 72c8ed9a00 update gem 2025-03-04 22:17:01 +02:00
Pete Matsyburka 3fec433846 fix builder editable 2025-03-04 19:46:55 +02:00
Pete Matsyburka 3b572702d7 add mask size 2025-03-04 01:02:18 +02:00
Alex Turchyn febdc0b48d add specs for masked fields 2025-03-03 22:43:27 +02:00
Pete Matsyburka 8773bb2f62 disable cfr toggle 2025-03-03 13:21:10 +02:00
Pete Matsyburka 55ad70fabc fix builder default date format 2025-03-03 11:24:38 +02:00
Pete Matsyburka 23dcd62672 add field mask 2025-03-03 00:18:56 +02:00
Alex Turchyn 42371d7ce3 Merge from docusealco/wip 2025-03-02 15:37:12 +02:00
Pete Matsyburka 4116746be3 destroy email events 2025-02-28 12:12:02 +02:00
Pete Matsyburka b2b2856ac0 handle required pdf fields 2025-02-27 21:37:32 +02:00
Alex Turchyn b65bca213f add brakeman to CI 2025-02-27 18:43:57 +02:00
Pete Matsyburka 7a72e2cbb7 allow http 2025-02-27 10:59:32 +02:00
Pete Matsyburka 3a73398a1e fix email 2025-02-27 10:01:33 +02:00
Pete Matsyburka cce20d9230 fix rubocop 2025-02-25 14:39:11 +02:00
Pete Matsyburka 8b72c91951 add console audit tables 2025-02-25 13:16:11 +02:00
Pete Matsyburka b0d8e47a75 fix annotate model 2025-02-25 13:10:33 +02:00
Pete Matsyburka 85824faa2b https webhooks 2025-02-25 10:33:16 +02:00
Pete Matsyburka 8a507339af add brakeman 2025-02-25 00:54:05 +02:00
Pete Matsyburka 4636849118 fix permitted params 2025-02-24 23:59:30 +02:00
Pete Matsyburka cc52cae9fc fix not found page 2025-02-24 14:19:48 +02:00
Alex Turchyn b6bb4d2c8b Merge from docusealco/wip 2025-02-24 14:05:38 +02:00
Pete Matsyburka 5032676bd2 fix url download 2025-02-24 13:24:16 +02:00
Pete Matsyburka 146b37f8cd fix clone 2025-02-21 22:21:31 +02:00
Pete Matsyburka 47b3a2d09e multiple recipient emails 2025-02-21 21:49:33 +02:00
Pete Matsyburka f2306b334b update gem 2025-02-20 13:02:58 +02:00
Pete Matsyburka 3affa65571 fix formula condition 2025-02-20 12:13:52 +02:00
Pete Matsyburka 6f3be16826 fix dockerfile 2025-02-18 16:50:48 +02:00
Pete Matsyburka 07ce76537b use ubuntu arm 2025-02-18 16:50:01 +02:00
Pete Matsyburka a1fb38b8cb add email variables 2025-02-18 12:18:59 +02:00
105 changed files with 1648 additions and 504 deletions
+58
View File
@@ -0,0 +1,58 @@
---
:position: before
:position_in_additional_file_patterns: before
:position_in_class: before
:position_in_factory: before
:position_in_fixture: before
:position_in_routes: before
:position_in_serializer: before
:position_in_test: before
:classified_sort: true
:exclude_controllers: true
:exclude_factories: true
:exclude_fixtures: false
:exclude_helpers: true
:exclude_scaffolds: true
:exclude_serializers: false
:exclude_sti_subclasses: false
:exclude_tests: false
:force: false
:format_markdown: false
:format_rdoc: false
:format_yard: false
:frozen: false
:ignore_model_sub_dir: false
:ignore_unknown_models: false
:include_version: false
:show_check_constraints: false
:show_complete_foreign_keys: false
:show_foreign_keys: true
:show_indexes: true
:simple_indexes: false
:sort: false
:timestamp: false
:trace: false
:with_comment: true
:with_column_comments: true
:with_table_comments: true
:active_admin: false
:command:
:debug: false
:hide_default_column_types: ''
:hide_limit_column_types: ''
:ignore_columns:
:ignore_routes:
:models: true
:routes: false
:skip_on_db_migrate: false
:target_action: :do_annotations
:wrapper:
:wrapper_close:
:wrapper_open:
:classes_default_to_s: []
:additional_file_patterns: []
:model_dir:
- app/models
:require: []
:root_dir:
- ''
+27
View File
@@ -77,6 +77,33 @@ jobs:
run: |
./node_modules/eslint/bin/eslint.js "app/javascript/**/*.js"
brakeman:
name: Brakeman
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.4.1
- name: Cache gems
uses: actions/cache@v4
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gem-
- name: Install gems
run: |
gem install bundler
bundle config path vendor/bundle
bundle install --jobs 4 --retry 4
yarn install
sudo apt-get update
sudo apt-get install libvips
- name: Run Brakeman
run: bundle exec brakeman -q --exit-on-warn
rspec:
name: RSpec
runs-on: ubuntu-latest
+1 -1
View File
@@ -7,7 +7,7 @@ on:
jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04-arm
steps:
- name: Checkout code
+3 -3
View File
@@ -1,4 +1,4 @@
FROM ruby:3.4.1-alpine as fonts
FROM ruby:3.4.1-alpine AS 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")'
FROM ruby:3.4.1-alpine as webpack
FROM ruby:3.4.1-alpine AS webpack
ENV RAILS_ENV=production
ENV NODE_ENV=production
@@ -32,7 +32,7 @@ COPY ./app/views ./app/views
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
FROM ruby:3.4.1-alpine as app
FROM ruby:3.4.1-alpine AS app
ENV RAILS_ENV=production
ENV BUNDLE_WITHOUT="development:test"
+3 -1
View File
@@ -46,7 +46,6 @@ gem 'twitter_cldr', require: false
gem 'tzinfo-data'
group :development, :test do
gem 'annotate'
gem 'better_html'
gem 'bullet'
gem 'debug'
@@ -63,6 +62,9 @@ group :development, :test do
end
group :development do
gem 'annotaterb'
gem 'brakeman', require: false
gem 'foreman', require: false
gem 'letter_opener_web'
gem 'web-console'
end
+15 -12
View File
@@ -74,9 +74,7 @@ GEM
uri (>= 0.13.1)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
annotate (2.6.5)
activerecord (>= 2.3.0)
rake (>= 0.8.7)
annotaterb (4.14.0)
arabic-letter-connector (0.1.1)
ast (2.4.2)
aws-eventstream (1.3.0)
@@ -120,6 +118,8 @@ GEM
bindex (0.8.1)
bootsnap (1.18.4)
msgpack (~> 1.2)
brakeman (7.0.0)
racc
builder (3.3.0)
bullet (8.0.0)
activesupport (>= 3.0.0)
@@ -229,6 +229,7 @@ GEM
ffi (1.17.1-arm64-darwin)
ffi (1.17.1-x86_64-linux-gnu)
ffi (1.17.1-x86_64-linux-musl)
foreman (0.88.1)
geom2d (0.4.1)
globalid (1.2.1)
activesupport (>= 6.1)
@@ -337,18 +338,18 @@ GEM
net-smtp (0.5.0)
net-protocol
nio4r (2.7.4)
nokogiri (1.18.2)
nokogiri (1.18.5)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.18.2-aarch64-linux-gnu)
nokogiri (1.18.5-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.2-aarch64-linux-musl)
nokogiri (1.18.5-aarch64-linux-musl)
racc (~> 1.4)
nokogiri (1.18.2-arm64-darwin)
nokogiri (1.18.5-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.2-x86_64-linux-gnu)
nokogiri (1.18.5-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.2-x86_64-linux-musl)
nokogiri (1.18.5-x86_64-linux-musl)
racc (~> 1.4)
oj (3.16.8)
bigdecimal (>= 3.0)
@@ -386,7 +387,7 @@ GEM
puma (6.5.0)
nio4r (~> 2.0)
racc (1.8.1)
rack (3.1.10)
rack (3.1.12)
rack-proxy (0.7.7)
rack
rack-session (2.0.0)
@@ -558,7 +559,7 @@ GEM
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
uniform_notifier (1.16.0)
uri (1.0.2)
uri (1.0.3)
useragent (0.16.11)
warden (1.2.9)
rack (>= 2.0.9)
@@ -588,13 +589,14 @@ PLATFORMS
x86_64-linux-musl
DEPENDENCIES
annotate
annotaterb
arabic-letter-connector
aws-sdk-s3
aws-sdk-secretsmanager
azure-storage-blob
better_html
bootsnap
brakeman
bullet
cancancan
capybara
@@ -610,6 +612,7 @@ DEPENDENCIES
faker
faraday
faraday-follow_redirects
foreman
google-cloud-storage
hexapdf
image_processing
@@ -15,6 +15,7 @@ class AccountConfigsController < ApplicationController
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
AccountConfig::FORCE_SSO_AUTH_KEY,
AccountConfig::FLATTEN_RESULT_PDF_KEY,
AccountConfig::ENFORCE_SIGNING_ORDER_KEY,
AccountConfig::WITH_SIGNATURE_ID,
AccountConfig::COMBINE_PDF_RESULT_KEY,
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
@@ -39,7 +40,7 @@ class AccountConfigsController < ApplicationController
end
def account_config_params
params.required(:account_config).permit!.tap do |attrs|
params.required(:account_config).permit(:key, :value, { value: {} }, { value: [] }).tap do |attrs|
attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
end
end
@@ -179,7 +179,7 @@ module Api
message: %i[subject body],
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
fields: [:name, :uuid, :default_value, :value, :title, :description,
:readonly, :validation_pattern, :invalid_message,
{ default_value: [], value: [], preferences: {} }] }]]
+7 -1
View File
@@ -146,9 +146,15 @@ module Api
if attrs[:completed]
submitter.values = Submitters::SubmitValues.merge_default_values(submitter)
submitter.values = Submitters::SubmitValues.merge_formula_values(submitter)
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
formula_values = Submitters::SubmitValues.build_formula_values(submitter)
if formula_values.present?
submitter.values = submitter.values.merge(formula_values)
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
end
submitter.values = submitter.values.transform_values do |v|
v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v
end
@@ -39,7 +39,7 @@ class NotificationsSettingsController < ApplicationController
end
def email_config_params
params.require(:account_config).permit!.tap do |attrs|
params.require(:account_config).permit(:key, :value, { value: {} }, { value: [] }).tap do |attrs|
attrs[:key] = nil unless attrs[:key].in?([AccountConfig::BCC_EMAILS, AccountConfig::SUBMITTER_REMINDERS])
end
end
@@ -50,7 +50,7 @@ class PersonalizationSettingsController < ApplicationController
end
def account_config_params
attrs = params.require(:account_config).permit!
attrs = params.require(:account_config).permit(:key, :value, { value: {} }, { value: [] })
return attrs if attrs[:value].is_a?(String)
+9 -1
View File
@@ -25,7 +25,7 @@ class StartFormController < ApplicationController
redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email])
else
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record?
@error_message = I18n.t('not_found')
@error_message = multiple_submitters_error_message
return render :show
end
@@ -117,4 +117,12 @@ class StartFormController < ApplicationController
@template = Template.find_by!(slug:)
end
def multiple_submitters_error_message
if current_user&.account_id == @template.account_id
helpers.t('this_submission_has_multiple_signers_which_prevents_the_use_of_a_sharing_link_html')
else
I18n.t('not_found')
end
end
end
@@ -23,9 +23,9 @@ class SubmissionsDownloadController < ApplicationController
last_submitter = submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
Submissions::EnsureResultGenerated.call(last_submitter)
return head :not_found unless last_submitter
return head :not_found unless last_submitter.completed_at?
Submissions::EnsureResultGenerated.call(last_submitter)
if last_submitter.completed_at < TTL.ago && !signature_valid && !current_user_submitter?(last_submitter)
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
@@ -20,7 +20,7 @@ class SubmissionsPreviewController < ApplicationController
@submission ||= Submission.find_by!(slug: params[:slug])
raise ActionController::RoutingError if @submission.account.archived_at?
raise ActionController::RoutingError, I18n.t('not_found') if @submission.account.archived_at?
if !@submission.submitters.all?(&:completed_at?) && !signature_valid &&
(!current_user || !current_ability.can?(:read, @submission))
+9 -3
View File
@@ -20,7 +20,11 @@ class SubmitFormController < ApplicationController
@submitter.account.archived_at?
return render :expired if submission.expired?
return render :declined if @submitter.declined_at?
return render :awaiting if submission.template.preferences['submitters_order'] == 'preserved' &&
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
return render :awaiting if (@form_configs[:enforce_signing_order] ||
submission.template.preferences['submitters_order'] == 'preserved') &&
!Submitters.current_submitter_order?(@submitter)
Submitters.preload_with_pages(@submitter)
@@ -29,8 +33,6 @@ class SubmitFormController < ApplicationController
@attachments_index = build_attachments_index(submission)
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
return unless @form_configs[:prefill_signature]
if (user_signature = UserConfigs.load_signature(current_user))
@@ -70,6 +72,10 @@ class SubmitFormController < ApplicationController
Submitters::SubmitValues.call(submitter, params, request)
head :ok
rescue Submitters::SubmitValues::RequiredFieldError => e
Rollbar.warning("Required field #{submitter.id}: #{e.message}") if defined?(Rollbar)
render json: { field_uuid: e.message }, status: :unprocessable_entity
rescue Submitters::SubmitValues::ValidationError => e
render json: { error: e.message }, status: :unprocessable_entity
end
@@ -6,9 +6,10 @@ class TemplatesDebugController < ApplicationController
def show
attachment = @template.documents.first
pdf = HexaPDF::Document.new(io: StringIO.new(attachment.download))
data = attachment.download
pdf = HexaPDF::Document.new(io: StringIO.new(data))
fields = Templates::FindAcroFields.call(pdf, attachment)
fields = Templates::FindAcroFields.call(pdf, attachment, data)
attachment.metadata['pdf'] ||= {}
attachment.metadata['pdf']['fields'] = fields
+1 -1
View File
@@ -26,7 +26,7 @@ class UserConfigsController < ApplicationController
end
def user_config_params
params.required(:user_config).permit!.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])
end
end
+7 -5
View File
@@ -45,17 +45,16 @@ class UsersController < ApplicationController
return redirect_to settings_users_path, notice: I18n.t('unable_to_update_user') if Docuseal.demo?
attrs = user_params.compact_blank.merge(user_params.slice(:archived_at))
attrs.delete(:role) if !role_valid?(attrs[:role]) || current_user == @user
if params.dig(:user, :account_id).present?
account = Account.accessible_by(current_ability).find(params[:user][:account_id])
account = Account.accessible_by(current_ability).find(params.dig(:user, :account_id))
authorize!(:manage, account)
@user.account = account
end
if @user.update(attrs)
if @user.update(attrs.except(current_user == @user ? :role : nil))
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_updated')
else
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity
@@ -84,8 +83,11 @@ class UsersController < ApplicationController
def user_params
if params.key?(:user)
params.require(:user).permit(:email, :first_name, :last_name, :password,
:role, :archived_at, :account_id)
permitted_params = %i[email first_name last_name password archived_at]
permitted_params << :role if role_valid?(params.dig(:user, :role))
params.require(:user).permit(permitted_params)
else
{}
end
-2
View File
@@ -35,8 +35,6 @@ import IndeterminateCheckbox from './elements/indeterminate_checkbox'
import * as TurboInstantClick from './lib/turbo_instant_click'
import './images/preview.png'
TurboInstantClick.start()
document.addEventListener('turbo:before-cache', () => {
+20 -17
View File
@@ -1,5 +1,6 @@
import SignaturePad from 'signature_pad'
import { cropCanvasAndExportToPNG } from './submission_form/crop_canvas'
import { isValidSignatureCanvas } from './submission_form/validate_signature'
window.customElements.define('draw-signature', class extends HTMLElement {
connectedCallback () {
@@ -43,6 +44,8 @@ window.customElements.define('draw-signature', class extends HTMLElement {
return response
})
}).catch(error => {
console.log(error)
}).finally(() => {
this.submitButton.disabled = false
})
@@ -65,26 +68,26 @@ window.customElements.define('draw-signature', class extends HTMLElement {
}
async submitImage () {
return new Promise((resolve, reject) => {
cropCanvasAndExportToPNG(this.canvas, { errorOnTooSmall: true }).then(async (blob) => {
const file = new File([blob], 'signature.png', { type: 'image/png' })
if (!isValidSignatureCanvas(this.pad.toData())) {
alert('Signature is too small or simple. Please redraw.')
const formData = new FormData()
return Promise.reject(new Error('Image too small or simple'))
}
formData.append('file', file)
formData.append('submitter_slug', this.dataset.slug)
formData.append('name', 'attachments')
formData.append('remember_signature', 'true')
return cropCanvasAndExportToPNG(this.canvas).then(async (blob) => {
const file = new File([blob], 'signature.png', { type: 'image/png' })
return fetch('/api/attachments', {
method: 'POST',
body: formData
}).then((resp) => resp.json()).then((attachment) => {
return resolve(attachment)
})
}).catch((error) => {
return reject(error)
})
const formData = new FormData()
formData.append('file', file)
formData.append('submitter_slug', this.dataset.slug)
formData.append('name', 'attachments')
formData.append('remember_signature', 'true')
return fetch('/api/attachments', {
method: 'POST',
body: formData
}).then(resp => resp.json())
})
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

+23 -7
View File
@@ -1,9 +1,9 @@
<template>
<div
class="field-area flex absolute lg:text-base -outline-offset-1"
class="flex absolute lg:text-base -outline-offset-1 field-area"
dir="auto"
:style="computedStyle"
:class="{ 'font-mono': field.preferences?.font === 'Courier', 'font-serif': field.preferences?.font === 'Times', 'text-[1.6vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'field-area-active outline-red-500 outline-dashed outline-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
:class="{ 'text-[1.6vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'outline-red-500 outline-dashed outline-2 z-10 field-area-active': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
>
<div
v-if="(!withFieldPlaceholder || !field.name || field.type === 'cells') && !isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
@@ -168,7 +168,7 @@
<div
v-else-if="field.type === 'cells'"
class="w-full flex items-center"
:class="{ 'justify-end': field.preferences?.align === 'right' }"
:class="{ 'justify-end': field.preferences?.align === 'right', ...fontClasses }"
>
<div
v-for="(char, index) in modelValue"
@@ -184,7 +184,7 @@
ref="textContainer"
dir="auto"
class="flex items-center px-0.5 w-full"
:class="alignClasses[field.preferences?.align]"
:class="{ ...alignClasses, ...fontClasses }"
>
<span
v-if="field && field.name && withFieldPlaceholder && !modelValue && modelValue !== 0"
@@ -326,10 +326,26 @@ export default {
}
},
alignClasses () {
if (!this.field.preferences) {
return {}
}
return {
center: 'text-center',
left: 'text-left',
right: 'text-right'
'text-center': this.field.preferences.align === 'center',
'text-left': this.field.preferences.align === 'left',
'text-right': this.field.preferences.align === 'right'
}
},
fontClasses () {
if (!this.field.preferences) {
return {}
}
return {
'font-mono': this.field.preferences.font === 'Courier',
'font-serif': this.field.preferences.font === 'Times',
'font-bold': ['bold_italic', 'bold'].includes(this.field.preferences.font_type),
italic: ['bold_italic', 'italic'].includes(this.field.preferences.font_type)
}
},
option () {
@@ -13,7 +13,7 @@
>
<a
v-if="val"
class="flex items-center space-x-1.5 w-full"
class="flex items-center space-x-1.5 w-full attachment-file-name"
:href="attachmentsIndex[val].url"
target="_blank"
>
@@ -26,7 +26,10 @@
{{ attachmentsIndex[val].filename }}
</span>
</a>
<button @click.prevent="removeAttachment(val)">
<button
class="remove-attachment-button"
@click.prevent="removeAttachment(val)"
>
<IconTrashX
:width="18"
:heigh="19"
@@ -44,7 +47,7 @@
<div
v-if="field.description && !modelValue.length"
dir="auto"
class="mb-3 px-1"
class="mb-3 px-1 field-description-text"
>
<MarkdownContent :string="field.description" />
</div>
+6 -6
View File
@@ -1,7 +1,7 @@
<template>
<div
id="form_completed"
class="mx-auto max-w-md flex flex-col"
class="mx-auto max-w-md flex flex-col completed-form"
dir="auto"
>
<div class="font-medium text-2xl flex items-center space-x-1.5 mx-auto">
@@ -10,13 +10,13 @@
:width="30"
:height="30"
/>
<span>
<span class="completed-form-message-title">
{{ completedMessage.title || (hasSignatureFields ? (hasMultipleDocuments ? t('documents_have_been_signed') : t('document_has_been_signed')) : t('form_has_been_completed')) }}
</span>
</div>
<div
v-if="completedMessage.body"
class="mt-2"
class="mt-2 completed-form-message-body"
>
<MarkdownContent
:string="completedMessage.body"
@@ -27,7 +27,7 @@
v-if="completedButton.url"
:href="sanitizeHref(completedButton.url)"
rel="noopener noreferrer nofollow"
class="white-button flex items-center w-full"
class="white-button flex items-center w-full completed-form-completed-button"
>
<span>
{{ completedButton.title || 'Back to Website' }}
@@ -35,7 +35,7 @@
</a>
<button
v-if="canSendEmail && !isDemo && withSendCopyButton"
class="white-button !h-auto flex items-center space-x-1 w-full"
class="white-button !h-auto flex items-center space-x-1 w-full completed-form-send-copy-button"
:disabled="isSendingCopy"
@click.prevent="sendCopyToEmail"
>
@@ -50,7 +50,7 @@
</button>
<button
v-if="!isWebView && withDownloadButton"
class="base-button flex items-center space-x-1 w-full"
class="base-button flex items-center space-x-1 w-full completed-form-download-button"
:disabled="isDownloading"
@click.prevent="download"
>
@@ -1,4 +1,4 @@
function cropCanvasAndExportToPNG (canvas, { errorOnTooSmall } = { errorOnTooSmall: false }) {
function cropCanvasAndExportToPNG (canvas) {
const ctx = canvas.getContext('2d')
const width = canvas.width
@@ -33,10 +33,6 @@ function cropCanvasAndExportToPNG (canvas, { errorOnTooSmall } = { errorOnTooSma
croppedCanvas.height = croppedHeight
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)
return new Promise((resolve, reject) => {
+3 -3
View File
@@ -7,7 +7,7 @@
<label
v-if="showFieldNames"
:for="field.uuid"
class="label text-xl sm:text-2xl py-0"
class="label text-xl sm:text-2xl py-0 field-name-label"
>
<MarkdownContent
v-if="field.title"
@@ -26,7 +26,7 @@
</template>
</label>
<button
class="btn btn-outline btn-sm !normal-case font-normal"
class="btn btn-outline btn-sm !normal-case font-normal set-current-date-button"
@click.prevent="[setCurrentDate(), $emit('focus')]"
>
<IconCalendarCheck :width="16" />
@@ -35,7 +35,7 @@
</div>
<div
v-if="field.description"
class="mb-3 px-1"
class="mb-3 px-1 field-description-text"
dir="auto"
>
<MarkdownContent :string="field.description" />
+1 -1
View File
@@ -7,7 +7,7 @@
>
<label
:for="inputId"
class="w-full relative bg-base-300 hover:bg-base-200 rounded-md border border-base-content border-dashed"
class="w-full relative bg-base-300 hover:bg-base-200 rounded-md border border-base-content border-dashed file-dropzone"
:class="{ 'opacity-50': isLoading }"
>
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
+42 -28
View File
@@ -67,7 +67,7 @@
<button
v-if="!isFormVisible"
id="expand_form_button"
class="btn btn-neutral flex text-white absolute bottom-0 w-full mb-3"
class="btn btn-neutral flex text-white absolute bottom-0 w-full mb-3 expand-form-button"
style="width: 96%; margin-left: 2%"
@click.prevent="[isFormVisible = true, scrollIntoField(currentField)]"
>
@@ -93,14 +93,14 @@
<div
v-show="isFormVisible"
id="form_container"
class="shadow-md bg-base-100 absolute bottom-0 w-full border-base-200 border p-4 rounded"
class="shadow-md bg-base-100 absolute bottom-0 w-full border-base-200 border p-4 rounded form-container"
:class="{ 'md:bottom-4': isBreakpointMd }"
:style="{ backgroundColor: backgroundColor }"
>
<button
v-if="!isCompleted"
id="minimize_form_button"
class="absolute right-0 top-0"
class="absolute right-0 top-0 minimize-form-button"
:class="currentField?.description?.length > 100 ? 'mr-1 mt-1 md:mr-2 md:mt-2': 'mr-2 mt-2 hidden md:block'"
:title="t('minimize')"
@click.prevent="minimizeForm"
@@ -119,7 +119,7 @@
ref="form"
:action="submitPath"
method="post"
class="mx-auto"
class="mx-auto steps-form"
:style="{ maxWidth: isBreakpointMd ? '582px' : '' }"
@submit.prevent="submitStep"
>
@@ -165,7 +165,7 @@
v-if="showFieldNames && (currentField.name || currentField.title)"
:for="currentField.uuid"
dir="auto"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
:class="{ 'mb-2': !currentField.description }"
>
<MarkdownContent
@@ -188,7 +188,7 @@
<div
v-if="currentField.description"
dir="auto"
class="mb-3 px-1"
class="mb-3 px-1 field-description-text"
>
<MarkdownContent :string="currentField.description" />
</div>
@@ -226,7 +226,7 @@
v-if="showFieldNames && (currentField.name || currentField.title)"
:for="currentField.uuid"
dir="auto"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
:class="{ 'mb-2': !currentField.description }"
>
<MarkdownContent
@@ -245,7 +245,7 @@
<div
v-if="currentField.description"
dir="auto"
class="mb-3 px-1"
class="mb-3 px-1 field-description-text"
>
<MarkdownContent :string="currentField.description" />
</div>
@@ -270,7 +270,7 @@
>
<label
:for="option.uuid"
class="flex items-center space-x-3"
class="flex items-center space-x-3 radio-label"
>
<input
:id="option.uuid"
@@ -304,7 +304,7 @@
<div
v-if="currentField.description"
dir="auto"
class="mb-3 px-1"
class="mb-3 px-1 field-description-text"
>
<MarkdownContent :string="currentField.description" />
</div>
@@ -338,7 +338,7 @@
>
<label
:for="field.uuid"
class="flex items-center space-x-3"
class="flex items-center space-x-3 checkbox-label"
>
<input
type="hidden"
@@ -481,7 +481,7 @@
id="submit_form_button"
ref="submitButton"
type="submit"
class="base-button w-full flex justify-center"
class="base-button w-full flex justify-center submit-form-button"
:disabled="isButtonDisabled"
>
<span class="flex">
@@ -533,13 +533,13 @@
v-if="stepFields.length < 80"
class="flex justify-center mt-3 sm:mt-4 mb-0 sm:mb-1"
>
<div class="flex items-center flex-wrap">
<div class="flex items-center flex-wrap steps-progress">
<a
v-for="(step, index) in stepFields"
:key="step[0].uuid"
href="#"
class="inline border border-base-300 h-3 w-3 rounded-full mx-1 mt-1"
:class="{ 'bg-base-300': index === currentStep, 'bg-base-content': (index < currentStep && stepFields[index].every((f) => !f.required || ![null, undefined, ''].includes(values[f.uuid]))) || isCompleted, 'bg-white': index > currentStep }"
:class="{ 'bg-base-300 steps-progress-current': index === currentStep, 'bg-base-content': (index < currentStep && stepFields[index].every((f) => !f.required || ![null, undefined, ''].includes(values[f.uuid]))) || isCompleted, 'bg-white': index > currentStep }"
@click.prevent="isCompleted ? '' : [saveStep(), goToStep(index, true)]"
/>
</div>
@@ -1037,7 +1037,7 @@ export default {
}, [])
},
formulaFields () {
return this.fields.filter((f) => f.preferences?.formula && f.type !== 'payment')
return this.fields.filter((f) => f.preferences?.formula && f.type !== 'payment' && this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f))
},
attachmentsIndex () {
return this.attachments.reduce((acc, a) => {
@@ -1191,22 +1191,24 @@ export default {
return false
}
const defaultValue = !field || isEmpty(field.default_value) ? null : field.default_value
if (['empty', 'unchecked'].includes(condition.action)) {
return isEmpty(this.values[condition.field_uuid])
return isEmpty(this.values[condition.field_uuid] ?? defaultValue)
} else if (['not_empty', 'checked'].includes(condition.action)) {
return !isEmpty(this.values[condition.field_uuid])
return !isEmpty(this.values[condition.field_uuid] ?? defaultValue)
} else if (['equal', 'contains'].includes(condition.action) && field) {
if (field.options) {
const option = field.options.find((o) => o.uuid === condition.value)
const values = [this.values[condition.field_uuid]].flat()
const values = [this.values[condition.field_uuid] ?? defaultValue].flat()
return values.includes(this.optionValue(option, field.options.indexOf(option)))
} else {
return [this.values[condition.field_uuid]].flat().includes(condition.value)
return [this.values[condition.field_uuid] ?? defaultValue].flat().includes(condition.value)
}
} else if (['not_equal', 'does_not_contain'].includes(condition.action) && field) {
const option = field.options.find((o) => o.uuid === condition.value)
const values = [this.values[condition.field_uuid]].flat()
const values = [this.values[condition.field_uuid] ?? defaultValue].flat()
return !values.includes(this.optionValue(option, field.options.indexOf(option)))
} else {
@@ -1299,7 +1301,9 @@ export default {
if (!this.isCompleted) {
if (scrollToArea) {
this.scrollIntoField(this.currentField)
this.$nextTick(() => {
setTimeout(() => this.scrollIntoField(this.currentField), 1)
})
}
this.enableScrollIntoField = false
@@ -1316,7 +1320,7 @@ export default {
const currentFieldUuids = this.currentStepFields.map((f) => f.uuid)
const currentFieldType = this.currentField.type
if (!formData && !this.$refs.form.checkValidity()) {
if (!formData && !this.$refs.form.checkValidity() && currentFieldUuids.every((fieldUuid) => isEmpty(this.submittedValues[fieldUuid]) || !isEmpty(this.values[fieldUuid]))) {
return
}
@@ -1400,7 +1404,21 @@ export default {
if (response.status === 422 || response.status === 500) {
const data = await response.json()
if (data.error) {
if (data.field_uuid) {
const field = this.fieldsUuidIndex[data.field_uuid]
if (field) {
const step = this.stepFields.findIndex((fields) => fields.includes(field))
if (step !== -1) {
this.goToStep(step, this.autoscrollFields)
this.showFillAllRequiredFields = true
}
}
return Promise.reject(new Error('Required field: ' + data.field_uuid))
} else if (data.error) {
const i18nKey = data.error.replace(/\s+/g, '_').toLowerCase()
alert(this.t(i18nKey) !== i18nKey ? this.t(i18nKey) : data.error)
@@ -1435,11 +1453,7 @@ export default {
this.isSubmittingComplete = false
})
}).catch(error => {
if (error?.message === 'Image too small') {
alert(this.t('signature_is_too_small_please_redraw'))
} else {
console.log(error)
}
console.log(error)
}).finally(() => {
this.isSubmitting = false
this.isSubmittingComplete = false
+13 -13
View File
@@ -93,7 +93,7 @@ const en = {
reupload: 'Reupload',
upload: 'Upload',
files: 'Files',
signature_is_too_small_please_redraw: 'Signature is too small. Please redraw.',
signature_is_too_small_or_simple_please_redraw: 'Signature is too small or simple. Please redraw.',
wait_countdown_seconds: 'Wait {countdown} seconds'
}
@@ -191,7 +191,7 @@ const es = {
reupload: 'Volver a subir',
upload: 'Subir',
files: 'Archivos',
signature_is_too_small_please_redraw: 'La firma es demasiado pequeña. Por favor, dibújala de nuevo.',
signature_is_too_small_or_simple_please_redraw: 'La firma es demasiado pequeña o simple. Por favor, vuelve a dibujarla.',
wait_countdown_seconds: 'Espera {countdown} segundos'
}
@@ -289,7 +289,7 @@ const it = {
reupload: 'Ricarica',
upload: 'Carica',
files: 'File',
signature_is_too_small_please_redraw: 'La firma è troppo piccola. Ridisegnala per favore.',
signature_is_too_small_or_simple_please_redraw: 'La firma è troppo piccola o semplice. Ridisegnala, per favore.',
wait_countdown_seconds: 'Attendi {countdown} secondi'
}
@@ -387,7 +387,7 @@ const de = {
reupload: 'Erneut hochladen',
upload: 'Hochladen',
files: 'Dateien',
signature_is_too_small_please_redraw: 'Die Unterschrift ist zu klein. Bitte erneut zeichnen.',
signature_is_too_small_or_simple_please_redraw: 'Die Unterschrift ist zu klein oder zu einfach. Bitte erneut zeichnen.',
wait_countdown_seconds: 'Warte {countdown} Sekunden'
}
@@ -485,7 +485,7 @@ const fr = {
reupload: 'Recharger',
upload: 'Télécharger',
files: 'Fichiers',
signature_is_too_small_please_redraw: 'La signature est trop petite. Veuillez la redessiner.',
signature_is_too_small_or_simple_please_redraw: 'La signature est trop petite ou trop simple. Veuillez la redessiner.',
wait_countdown_seconds: 'Attendez {countdown} secondes'
}
@@ -583,7 +583,7 @@ const pl = {
reupload: 'Ponowne przesłanie',
upload: 'Przesyłanie',
files: 'Pliki',
signature_is_too_small_please_redraw: 'Podpis jest zbyt mały. Proszę narysować go ponownie.'
signature_is_too_small_or_simple_please_redraw: 'Podpis jest zbyt mały lub zbyt prosty. Proszę narysować go ponownie.'
}
const uk = {
@@ -680,7 +680,7 @@ const uk = {
reupload: 'Перезавантажити',
upload: 'Завантажити',
files: 'Файли',
signature_is_too_small_please_redraw: 'Підпис занадто малий. Будь ласка, перемалюйте його.',
signature_is_too_small_or_simple_please_redraw: 'Підпис занадто маленький або надто простий. Будь ласка, перемалюйте.',
wait_countdown_seconds: 'Зачекайте {countdown} секунд'
}
@@ -778,7 +778,7 @@ const cs = {
reupload: 'Znovu nahrát',
upload: 'Nahrát',
files: 'Soubory',
signature_is_too_small_please_redraw: 'Podpis je příliš malý. Prosím, překreslete ho.',
signature_is_too_small_or_simple_please_redraw: 'Podpis je příliš malý nebo jednoduchý. Nakreslete jej prosím znovu.',
wait_countdown_seconds: 'Počkejte {countdown} sekund'
}
@@ -876,7 +876,7 @@ const pt = {
reupload: 'Reenviar',
upload: 'Carregar',
files: 'Arquivos',
signature_is_too_small_please_redraw: 'A assinatura é muito pequena. Por favor, redesenhe-a.',
signature_is_too_small_or_simple_please_redraw: 'A assinatura é muito pequena ou simples. Por favor, redesenhe.',
wait_countdown_seconds: 'Aguarde {countdown} segundos'
}
@@ -975,7 +975,7 @@ const he = {
reupload: 'העלה שוב',
upload: 'העלאה',
files: 'קבצים',
signature_is_too_small_please_redraw: 'החתימה קטנה מדי. אנא צייר מחדש.',
signature_is_too_small_or_simple_please_redraw: 'החתימה קטנה או פשוטה מדי. אנא חתום מחדש.',
wait_countdown_seconds: 'המתן {countdown} שניות'
}
@@ -1074,7 +1074,7 @@ const nl = {
reupload: 'Opnieuw uploaden',
upload: 'Uploaden',
files: 'Bestanden',
signature_is_too_small_please_redraw: 'De handtekening is te klein. Teken deze opnieuw, alstublieft.',
signature_is_too_small_or_simple_please_redraw: 'De handtekening is te klein of te eenvoudig. Teken opnieuw.',
wait_countdown_seconds: 'Wacht {countdown} seconden'
}
@@ -1172,7 +1172,7 @@ const ar = {
reupload: 'إعادة التحميل',
upload: 'تحميل',
files: 'الملفات',
signature_is_too_small_please_redraw: 'التوقيع صغير جدًا. يرجى إعادة الرسم.',
signature_is_too_small_or_simple_please_redraw: 'التوقيع صغير جدًا أو بسيط جدًا. يرجى إعادة رسمه.',
wait_countdown_seconds: 'انتظر {countdown} ثانية'
}
@@ -1269,7 +1269,7 @@ const ko = {
reupload: '다시 업로드',
upload: '업로드',
files: '파일',
signature_is_too_small_please_redraw: '서명이 너무 작니다. 다시 그려주세요.',
signature_is_too_small_or_simple_please_redraw: '서명이 너무 작거나 단순합니다. 다시 그려주세요.',
wait_countdown_seconds: '{countdown}초 기다리세요'
}
@@ -4,7 +4,7 @@
<label
v-if="showFieldNames"
:for="field.uuid"
class="label text-xl sm:text-2xl py-0"
class="label text-xl sm:text-2xl py-0 field-name-label"
>
<MarkdownContent
v-if="field.title"
@@ -15,7 +15,7 @@
</template>
</label>
<button
class="btn btn-outline btn-sm"
class="btn btn-outline btn-sm reupload-button"
@click.prevent="remove"
>
<IconReload :width="16" />
@@ -25,7 +25,7 @@
<div>
<img
:src="attachmentsIndex[modelValue].url"
class="h-52 border border-base-300 rounded mx-auto"
class="h-52 border border-base-300 rounded mx-auto uploaded-image-preview"
>
</div>
<input
@@ -40,7 +40,7 @@
<div
v-if="field.description"
dir="auto"
class="mb-3 px-1"
class="mb-3 px-1 field-description-text"
>
<MarkdownContent :string="field.description" />
</div>
@@ -6,7 +6,7 @@
>
<label
v-if="showFieldNames"
class="label text-xl sm:text-2xl py-0"
class="label text-xl sm:text-2xl py-0 field-name-label"
>
<MarkdownContent
v-if="field.title"
@@ -25,7 +25,7 @@
<a
id="type_text_button"
href="#"
class="btn btn-outline font-medium btn-sm"
class="btn btn-outline font-medium btn-sm type-text-button"
@click.prevent="toggleTextInput"
>
<IconTextSize :width="16" />
@@ -42,7 +42,7 @@
<a
id="type_text_button"
href="#"
class="btn btn-outline font-medium btn-sm"
class="btn btn-outline font-medium btn-sm type-text-button"
@click.prevent="toggleTextInput"
>
<IconSignature :width="16" />
@@ -55,9 +55,7 @@
class="tooltip"
:data-tip="t('click_to_upload')"
>
<label
class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap"
>
<label class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap upload-image-button">
<IconUpload :width="16" />
<input
:key="uploadImageInputKey"
@@ -74,7 +72,7 @@
<a
v-if="modelValue || computedPreviousValue"
href="#"
class="btn font-medium btn-outline btn-sm"
class="btn font-medium btn-outline btn-sm clear-canvas-button"
@click.prevent="remove"
>
<IconReload :width="16" />
@@ -83,7 +81,7 @@
<a
v-else
href="#"
class="btn font-medium btn-outline btn-sm"
class="btn font-medium btn-outline btn-sm clear-canvas-button"
@click.prevent="clear"
>
<IconReload :width="16" />
@@ -105,7 +103,7 @@
<div
v-if="field.description"
dir="auto"
class="mb-3 px-1"
class="mb-3 px-1 field-description-text"
>
<MarkdownContent :string="field.description" />
</div>
@@ -120,11 +118,17 @@
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
class="mx-auto bg-white border border-base-300 rounded max-h-44"
>
<canvas
v-show="!modelValue && !computedPreviousValue"
ref="canvas"
class="bg-white border border-base-300 rounded-2xl w-full"
/>
<div class="relative">
<div
v-if="!isDrawInitials"
class="absolute top-0 right-0 left-0 bottom-0"
/>
<canvas
v-show="!modelValue && !computedPreviousValue"
ref="canvas"
class="bg-white border border-base-300 rounded-2xl w-full draw-canvas"
/>
</div>
<input
v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
id="initials_text_input"
@@ -3,7 +3,7 @@
v-if="showFieldNames && (field.name || field.title)"
:for="field.uuid"
dir="auto"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
:class="{ 'mb-2': !field.description }"
>
<MarkdownContent
@@ -20,7 +20,7 @@
<div
v-if="field.description"
dir="auto"
class="mb-3 px-1"
class="mb-3 px-1 field-description-text"
>
<MarkdownContent :string="field.description" />
</div>
@@ -50,7 +50,7 @@
>
<label
:for="option.uuid"
class="flex items-center space-x-3"
class="flex items-center space-x-3 checkbox-label"
@click="scrollIntoField(field)"
>
<input
@@ -3,7 +3,7 @@
v-if="showFieldNames && (field.name || field.title)"
:for="field.uuid"
dir="auto"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
:class="{ 'mb-2': !field.description }"
>
<MarkdownContent
@@ -24,7 +24,7 @@
<div
v-if="field.description"
dir="auto"
class="mb-3 px-1"
class="mb-3 px-1 field-description-text"
>
<MarkdownContent :string="field.description" />
</div>
@@ -1,7 +1,7 @@
<template>
<label
v-if="!modelValue && !sessionId"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
>
<MarkdownContent
v-if="field.title"
@@ -34,7 +34,7 @@
<button
v-if="sessionId"
disabled
class="base-button w-full"
class="base-button w-full modal-save-button"
>
<IconLoader
width="22"
@@ -3,7 +3,7 @@
<label
v-if="showFieldNames"
:for="isCodeSent ? 'one_time_code' : field.uuid"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
:class="{ 'mb-2': !field.description }"
>
<MarkdownContent
@@ -17,7 +17,7 @@
<div
v-if="field.description"
dir="auto"
class="mb-3 px-1"
class="mb-3 px-1 field-description-text"
>
<MarkdownContent :string="field.description" />
</div>
@@ -45,7 +45,7 @@
<a
v-if="!defaultValue"
href="#"
class="link"
class="link change-phone-number-link"
@click.prevent="isCodeSent = false"
>
{{ t('change_phone_number') }}
@@ -59,7 +59,7 @@
<a
v-else
href="#"
class="link"
class="link resend-code-link"
@click.prevent="resendCode"
>
{{ isResendLoading ? t('sending') : t('resend_code') }}
@@ -68,13 +68,13 @@
</div>
<div
v-show="!isCodeSent"
class="flex w-full rounded-full outline-neutral-content outline-2 outline-offset-2 focus-within:outline"
class="flex w-full rounded-full outline-neutral-content outline-2 outline-offset-2 focus-within:outline phone-number-input-container"
>
<div
id="country_code"
class="relative inline-block"
>
<div class="btn bg-base-200 border border-neutral-300 text-2xl whitespace-nowrap font-normal rounded-l-full">
<div class="btn bg-base-200 border border-neutral-300 text-2xl whitespace-nowrap font-normal rounded-l-full country-code-select-label">
{{ selectedCountry.flag }} +{{ selectedCountry.dial }}
</div>
<select
@@ -9,7 +9,7 @@
>
<label
v-if="showFieldNames"
class="label text-xl sm:text-2xl py-0"
class="label text-xl sm:text-2xl py-0 field-name-label"
>
<MarkdownContent
v-if="field.title"
@@ -28,7 +28,7 @@
<a
id="type_text_button"
href="#"
class="btn btn-outline btn-sm font-medium"
class="btn btn-outline btn-sm font-medium type-text-button"
@click.prevent="[toggleTextInput(), hideQr()]"
>
<IconSignature :width="16" />
@@ -46,7 +46,7 @@
<a
id="type_text_button"
href="#"
class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap"
class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap type-text-button"
@click.prevent="[toggleTextInput(), hideQr()]"
>
<IconTextSize :width="16" />
@@ -61,9 +61,7 @@
:class="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
:data-tip="t('take_photo')"
>
<label
class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap"
>
<label class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap upload-image-button">
<IconCamera :width="16" />
<input
:key="uploadImageInputKey"
@@ -80,7 +78,7 @@
<a
v-if="modelValue || computedPreviousValue"
href="#"
class="btn btn-outline btn-sm font-medium"
class="btn btn-outline btn-sm font-medium reupload-button"
@click.prevent="remove"
>
<IconReload :width="16" />
@@ -119,7 +117,7 @@
<div
v-if="field.description"
dir="auto"
class="mb-3 px-1"
class="mb-3 px-1 field-description-text"
>
<MarkdownContent :string="field.description" />
</div>
@@ -167,7 +165,7 @@
v-show="!modelValue && !computedPreviousValue"
ref="canvas"
style="padding: 1px; 0"
class="bg-white border border-base-300 rounded-2xl w-full"
class="bg-white border border-base-300 rounded-2xl w-full draw-canvas"
/>
<div
v-if="isShowQr"
@@ -294,6 +292,7 @@
<script>
import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2, IconQrcode, IconX } from '@tabler/icons-vue'
import { cropCanvasAndExportToPNG } from './crop_canvas'
import { isValidSignatureCanvas } from './validate_signature'
import SignaturePad from 'signature_pad'
import AppearsOn from './appears_on'
import FileDropzone from './dropzone'
@@ -600,6 +599,7 @@ export default {
},
drawImage (event) {
this.remove()
this.clear()
this.isSignatureStarted = true
this.drawOnCanvas(event.target.files[0], this.$refs.canvas)
@@ -682,8 +682,14 @@ export default {
return Promise.resolve({})
}
return new Promise((resolve, reject) => {
cropCanvasAndExportToPNG(this.$refs.canvas, { errorOnTooSmall: true }).then(async (blob) => {
if (this.isSignatureStarted && this.pad.toData().length > 0 && !isValidSignatureCanvas(this.pad.toData())) {
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
return Promise.reject(new Error('Image too small or simple'))
}
return new Promise((resolve) => {
cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => {
const file = new File([blob], 'signature.png', { type: 'image/png' })
if (this.dryRun) {
@@ -719,12 +725,6 @@ export default {
return resolve(attachment)
})
}
}).catch((error) => {
if (error.message === 'Image too small' && this.field.required === false) {
return resolve({})
} else {
return reject(error)
}
})
})
}
+3 -3
View File
@@ -3,7 +3,7 @@
v-if="showFieldNames && (field.name || field.title)"
:for="field.uuid"
dir="auto"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
:class="{ 'mb-2': !field.description }"
>
<MarkdownContent
@@ -24,7 +24,7 @@
<div
v-if="field.description"
dir="auto"
class="mb-3 px-1"
class="mb-3 px-1 field-description-text"
>
<MarkdownContent :string="field.description" />
</div>
@@ -67,7 +67,7 @@
>
<a
href="#"
class="btn btn-ghost btn-circle btn-sm"
class="btn btn-ghost btn-circle btn-sm toggle-multiline-text-button"
@click.prevent="toggleTextArea"
>
<IconAlignBoxLeftTop />
@@ -0,0 +1,38 @@
function isValidSignatureCanvas (data) {
if (data.length === 0) return false
const strokes = data.filter(stroke => Array.isArray(stroke.points) && stroke.points.length > 2)
if (strokes.length === 0) return false
let skippedStraightLine = 0
const validStrokes = strokes.filter(stroke => {
const points = stroke.points
const first = points[0]
const last = points[points.length - 1]
const A = last.y - first.y
const B = first.x - last.x
const C = last.x * first.y - first.x * last.y
const lineLength = Math.sqrt(A * A + B * B)
const totalDeviation = points.reduce((sum, p) => {
const distanceToLine = Math.abs(A * p.x + B * p.y + C) / lineLength
return sum + distanceToLine
}, 0)
const avgDeviation = totalDeviation / points.length
if (avgDeviation < 3 && skippedStraightLine < 2) {
skippedStraightLine++
return false
}
return true
})
return validStrokes.length > 0
}
export { isValidSignatureCanvas }
@@ -1,7 +1,5 @@
<template>
<label
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
>
<label class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label">
<MarkdownContent
v-if="field.title"
:string="field.title"
@@ -11,13 +9,13 @@
<div
v-if="field.description"
dir="auto"
class="mb-4 px-1"
class="mb-4 px-1 field-description-text"
>
<MarkdownContent :string="field.description" />
</div>
<div
v-if="emptyValueRequiredStep && emptyValueRequiredStep[0] !== field"
class="px-1"
class="px-1 field-description-text"
>
{{ t('complete_all_required_fields_to_proceed_with_identity_verification') }}
</div>
+37 -10
View File
@@ -1,6 +1,6 @@
<template>
<div
class="absolute overflow-visible group"
class="absolute overflow-visible group field-area-container"
:style="positionStyle"
:class="{ 'z-[1]': isMoved || isDragged }"
@pointerdown.stop
@@ -33,7 +33,7 @@
</div>
<div
v-if="field?.type && (isSelected || isNameFocus)"
class="absolute bg-white rounded-t border overflow-visible whitespace-nowrap flex z-10"
class="absolute bg-white rounded-t border overflow-visible whitespace-nowrap flex z-10 field-area-controls"
style="top: -25px; height: 25px"
@mousedown.stop
@pointerdown.stop
@@ -41,7 +41,7 @@
<FieldSubmitter
v-if="field.type != 'heading'"
v-model="field.submitter_uuid"
class="border-r"
class="border-r roles-dropdown"
:compact="true"
:editable="editable && (!defaultField || defaultField.role !== submitter?.name)"
:allow-add-new="!defaultSubmitters.length"
@@ -108,7 +108,7 @@
>{{ t('editable') }}</label>
<span
v-if="field.type !== 'payment' && !isValueInput"
class="dropdown dropdown-end"
class="dropdown dropdown-end field-area-settings-dropdown"
@mouseenter="renderDropdown = true"
@touchstart="renderDropdown = true"
>
@@ -141,6 +141,7 @@
:with-required="false"
:with-areas="false"
@click-formula="isShowFormulaModal = true"
@click-font="isShowFontModal = true"
@click-description="isShowDescriptionModal = true"
@click-condition="isShowConditionsModal = true"
@scroll-to="[selectedAreaRef.value = $event, $emit('scroll-to', $event)]"
@@ -159,9 +160,9 @@
</div>
<div
ref="touchValueTarget"
class="flex items-center h-full w-full"
class="flex items-center h-full w-full field-area"
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) ? (alignClasses[field.preferences?.align] || '') : 'justify-center']"
:class="[isValueInput ? 'bg-opacity-50' : 'bg-opacity-80', field.type === 'heading' ? 'bg-gray-50' : bgColors[submitterIndex % bgColors.length], isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? fontClasses : 'justify-center']"
@click="focusValueInput"
>
<span
@@ -253,6 +254,17 @@
@close="isShowFormulaModal = false"
/>
</Teleport>
<Teleport
v-if="isShowFontModal"
:to="modalContainerEl"
>
<FontModal
:field="field"
:editable="editable && !defaultField"
:build-default-name="buildDefaultName"
@close="isShowFontModal = false"
/>
</Teleport>
<Teleport
v-if="isShowConditionsModal"
:to="modalContainerEl"
@@ -283,6 +295,7 @@ import FieldType from './field_type'
import Field from './field'
import FieldSettings from './field_settings'
import FormulaModal from './formula_modal'
import FontModal from './font_modal'
import ConditionsModal from './conditions_modal'
import DescriptionModal from './description_modal'
import { IconX, IconCheck, IconDotsVertical } from '@tabler/icons-vue'
@@ -295,6 +308,7 @@ export default {
IconCheck,
FieldSettings,
FormulaModal,
FontModal,
IconDotsVertical,
DescriptionModal,
ConditionsModal,
@@ -352,6 +366,7 @@ export default {
data () {
return {
isShowFormulaModal: false,
isShowFontModal: false,
isShowConditionsModal: false,
isContenteditable: false,
isSettingsFocus: false,
@@ -386,11 +401,19 @@ export default {
defaultName () {
return this.buildDefaultName(this.field, this.template.fields)
},
alignClasses () {
fontClasses () {
if (!this.field.preferences) {
return {}
}
return {
center: 'justify-center',
left: 'justify-start',
right: 'justify-end'
'justify-center': this.field.preferences.align === 'center',
'justify-start': this.field.preferences.align === 'left',
'justify-end': this.field.preferences.align === 'right',
'font-mono': this.field.preferences.font === 'Courier',
'font-serif': this.field.preferences.font === 'Times',
'font-bold': ['bold_italic', 'bold'].includes(this.field.preferences.font_type),
italic: ['bold_italic', 'italic'].includes(this.field.preferences.font_type)
}
},
optionIndexText () {
@@ -483,6 +506,10 @@ export default {
this.$el.getRootNode().activeElement.blur()
},
maybeToggleDefaultValue () {
if (!this.editable) {
return
}
if (['text', 'number'].includes(this.field.type)) {
this.isContenteditable = true
+20 -14
View File
@@ -32,7 +32,7 @@
<div
v-if="$slots.buttons || withTitle"
id="title_container"
class="flex justify-between py-1.5 items-center pr-4 top-0 z-10"
class="flex justify-between py-1.5 items-center pr-4 top-0 z-10 title-container"
:class="{ sticky: withStickySubmitters || isBreakpointLg }"
:style="{ backgroundColor }"
>
@@ -47,7 +47,7 @@
v-if="withTitle"
:model-value="template.name"
:editable="editable"
class="text-xl md:text-3xl font-semibold focus:text-clip"
class="text-xl md:text-3xl font-semibold focus:text-clip template-name"
:icon-stroke-width="2.3"
@update:model-value="updateName"
/>
@@ -163,7 +163,7 @@
</div>
<div
id="main_container"
class="flex"
class="flex main-container"
:class="$slots.buttons || withTitle ? (isMobile ? 'max-h-[calc(100%_-_60px)]' : 'md:max-h-[calc(100%_-_60px)]') : (isMobile ? 'max-h-[100%]' : 'md:max-h-[100%]')"
>
<div
@@ -205,7 +205,7 @@
<button
v-if="sortedDocuments.length && editable && withAddPageButton"
id="add_blank_page_button"
class="btn btn-outline w-full"
class="btn btn-outline w-full add-blank-page-button"
@click.prevent="addBlankPage"
>
<IconInnerShadowTop
@@ -239,7 +239,7 @@
<button
v-if="withAddPageButton"
id="add_blank_page_button"
class="btn btn-outline w-full mt-4"
class="btn btn-outline w-full mt-4 add-blank-page-button"
@click.prevent="addBlankPage"
>
<IconInnerShadowTop
@@ -266,7 +266,7 @@
:is-drag="!!dragField"
:input-mode="inputMode"
:default-fields="[...defaultRequiredFields, ...defaultFields]"
:allow-draw="!onlyDefinedFields"
:allow-draw="!onlyDefinedFields || drawField"
:data-document-uuid="document.uuid"
:default-submitters="defaultSubmitters"
:with-field-placeholder="withFieldPlaceholder"
@@ -307,7 +307,7 @@
<button
v-if="withAddPageButton"
id="add_blank_page_button"
class="btn btn-outline w-full mt-4"
class="btn btn-outline w-full mt-4 add-blank-page-button"
@click.prevent="addBlankPage"
>
<IconInnerShadowTop
@@ -327,7 +327,7 @@
<div
v-if="withFieldsList && !isMobile"
id="fields_list_container"
class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block"
class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block fields-list-container"
:class="drawField ? 'overflow-hidden' : 'overflow-y-auto overflow-x-hidden'"
>
<div
@@ -335,13 +335,13 @@
class="sticky inset-0 h-full z-20"
:style="{ backgroundColor }"
>
<div class="bg-base-200 rounded-lg p-5 text-center space-y-4">
<div class="bg-base-200 rounded-lg p-5 text-center space-y-4 draw-field-container">
<p>
{{ t('draw_field_on_the_document') }}
</p>
<div>
<button
class="base-button"
class="base-button cancel-draw-button"
@click="clearDrawField"
>
{{ t('cancel') }}
@@ -408,7 +408,10 @@
@select="startFieldDraw($event)"
/>
</div>
<div id="docuseal_modal_container" />
<div
id="docuseal_modal_container"
class="modal-container"
/>
</div>
</template>
@@ -1316,6 +1319,10 @@ export default {
}
},
onDropfield (area) {
if (!this.editable) {
return
}
const field = this.fieldsDragFieldRef.value || {
name: '',
uuid: v4(),
@@ -1338,9 +1345,8 @@ export default {
}
if (field.type === 'date') {
field.preferences = {
format: this.defaultDateFormat
}
field.preferences ||= {}
field.preferences.format ||= this.defaultDateFormat
}
}
@@ -8,12 +8,12 @@
/>
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span>
<span class="modal-title">
{{ t('condition') }} - {{ item.name || buildDefaultName(item, template.fields) }}
</span>
<a
href="#"
class="text-xl"
class="text-xl modal-close-button"
@click.prevent="$emit('close')"
>&times;</a>
</div>
@@ -50,9 +50,9 @@
v-if="conditions.length > 1"
class="flex justify-between mx-1"
>
<span class="text-sm">
<label class="text-sm">
{{ t('condition') }} {{ cindex + 1 }}
</span>
</label>
<a
href="#"
class="link text-sm"
@@ -131,9 +131,7 @@
class="inline float-right link text-right mb-3 px-2"
@click.prevent="conditions.push({})"
> + {{ t('add_condition') }}</a>
<button
class="base-button w-full mt-2"
>
<button class="base-button w-full mt-2 modal-save-button">
{{ t('save') }}
</button>
</form>
+4 -4
View File
@@ -1,7 +1,7 @@
<template>
<div class="flex space-x-2">
<Contenteditable
class="w-full block mr-6"
class="w-full block mr-6 document-preview-name"
:model-value="item.name"
:icon-width="16"
@update:model-value="onUpdateName"
@@ -15,7 +15,7 @@
/>
<button
v-if="withArrows"
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors document-control-button"
style="width: 24px; height: 24px"
@click.stop="$emit('up', item)"
>
@@ -23,14 +23,14 @@
</button>
<button
v-if="withArrows"
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors document-control-button"
style="width: 24px; height: 24px"
@click.stop="$emit('down', item)"
>
&darr;
</button>
<button
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors document-control-button"
style="width: 24px; height: 24px"
@click.stop="$emit('remove', item)"
>
@@ -8,12 +8,12 @@
/>
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span>
<span class="modal-title">
{{ field.name || buildDefaultName(field, template.fields) }}
</span>
<a
href="#"
class="text-xl"
class="text-xl modal-close-button"
@click.prevent="$emit('close')"
>&times;</a>
</div>
@@ -55,9 +55,7 @@
>
</div>
</div>
<button
class="base-button w-full mt-4"
>
<button class="base-button w-full mt-4 modal-save-button">
{{ t('save') }}
</button>
</form>
+18 -3
View File
@@ -3,7 +3,7 @@
class="list-field group mb-2"
>
<div
class="border border-base-300 rounded rounded-tr-none relative group"
class="border border-base-300 rounded rounded-tr-none relative group fields-list-item"
:style="{ backgroundColor: backgroundColor }"
>
<div class="flex items-center justify-between relative group/contenteditable-container">
@@ -98,7 +98,7 @@
/>
<span
v-else-if="field.type !== 'heading'"
class="dropdown dropdown-end"
class="dropdown dropdown-end field-settings-dropdown"
@mouseenter="renderDropdown = true"
@touchstart="renderDropdown = true"
>
@@ -127,6 +127,7 @@
:editable="editable"
:background-color="dropdownBgColor"
@click-formula="isShowFormulaModal = true"
@click-font="isShowFontModal = true"
@click-description="isShowDescriptionModal = true"
@click-condition="isShowConditionsModal = true"
@set-draw="$emit('set-draw', $event)"
@@ -136,7 +137,7 @@
</ul>
</span>
<button
class="relative text-transparent group-hover:text-base-content pr-1"
class="relative text-transparent group-hover:text-base-content pr-1 field-remove-button"
:title="t('remove')"
@click="$emit('remove', field)"
>
@@ -231,6 +232,17 @@
@close="isShowFormulaModal = false"
/>
</Teleport>
<Teleport
v-if="isShowFontModal"
:to="modalContainerEl"
>
<FontModal
:field="field"
:editable="editable && !defaultField"
:build-default-name="buildDefaultName"
@close="isShowFontModal = false"
/>
</Teleport>
<Teleport
v-if="isShowConditionsModal"
:to="modalContainerEl"
@@ -261,6 +273,7 @@ import FieldType from './field_type'
import PaymentSettings from './payment_settings'
import FieldSettings from './field_settings'
import FormulaModal from './formula_modal'
import FontModal from './font_modal'
import ConditionsModal from './conditions_modal'
import DescriptionModal from './description_modal'
import { IconRouteAltLeft, IconMathFunction, IconNewSection, IconTrashX, IconSettings } from '@tabler/icons-vue'
@@ -275,6 +288,7 @@ export default {
PaymentSettings,
IconNewSection,
FormulaModal,
FontModal,
DescriptionModal,
ConditionsModal,
IconRouteAltLeft,
@@ -305,6 +319,7 @@ export default {
isNameFocus: false,
showPaymentModal: false,
isShowFormulaModal: false,
isShowFontModal: false,
isShowConditionsModal: false,
isShowDescriptionModal: false,
renderDropdown: false
@@ -54,7 +54,7 @@
</label>
</div>
<div
v-if="['number', 'cells'].includes(field.type)"
v-if="['cells'].includes(field.type)"
class="py-1.5 px-1 relative"
@click.stop
>
@@ -330,6 +330,19 @@
v-if="field.type != 'stamp'"
class="pb-0.5 mt-0.5"
>
<li v-if="['text', 'number', 'date'].includes(field.type)">
<label
class="label-text cursor-pointer text-center w-full flex items-center"
@click="$emit('click-font')"
>
<IconTypography
width="18"
/>
<span class="text-sm">
{{ t('font') }}
</span>
</label>
</li>
<li
v-if="field.type != 'stamp'"
>
@@ -427,7 +440,7 @@
</template>
<script>
import { IconRouteAltLeft, IconShape, IconX, IconMathFunction, IconNewSection, IconInfoCircle, IconCopy } from '@tabler/icons-vue'
import { IconRouteAltLeft, IconTypography, IconShape, IconX, IconMathFunction, IconNewSection, IconInfoCircle, IconCopy } from '@tabler/icons-vue'
export default {
name: 'FieldSettings',
@@ -438,6 +451,7 @@ export default {
IconRouteAltLeft,
IconCopy,
IconNewSection,
IconTypography,
IconX
},
inject: ['template', 'save', 't'],
@@ -472,7 +486,7 @@ export default {
default: true
}
},
emits: ['set-draw', 'scroll-to', 'click-formula', 'click-description', 'click-condition', 'remove-area'],
emits: ['set-draw', 'scroll-to', 'click-formula', 'click-description', 'click-condition', 'click-font', 'remove-area'],
data () {
return {
}
@@ -5,7 +5,7 @@
@touchstart="renderDropdown = true"
>
<div class="flex space-x-2 items-end">
<div class="group/contenteditable-container bg-base-100 rounded-md p-2 border border-base-300 w-full flex justify-between items-end">
<div class="group/contenteditable-container bg-base-100 rounded-md p-2 border border-base-300 w-full flex justify-between items-end roles-dropdown-label-mobile">
<div class="flex items-center space-x-2">
<span
class="w-3 h-3 flex-shrink-0 rounded-full"
@@ -22,7 +22,7 @@
/>
</div>
</div>
<div class="dropdown dropdown-top dropdown-end">
<div class="dropdown dropdown-top dropdown-end roles-dropdown-mobile">
<label
tabindex="0"
class="bg-base-100 cursor-pointer rounded-md p-2 border border-base-300 w-full flex justify-center"
@@ -1,6 +1,6 @@
<template>
<span
class="dropdown"
class="dropdown field-types-dropdown"
@mouseenter="renderDropdown = true"
@touchstart="renderDropdown = true"
>
+6 -6
View File
@@ -2,7 +2,7 @@
<div :class="withStickySubmitters ? 'sticky top-0 z-[1]' : ''">
<FieldSubmitter
:model-value="selectedSubmitter.uuid"
class="roles-dropdown w-full rounded-lg"
class="roles-dropdown w-full rounded-lg roles-dropdown"
:style="withStickySubmitters ? { backgroundColor } : {}"
:submitters="submitters"
:menu-style="{ overflow: 'auto', display: 'flex', flexDirection: 'row', maxHeight: 'calc(100vh - 120px)', backgroundColor: ['', null, 'transparent'].includes(backgroundColor) ? 'white' : backgroundColor }"
@@ -74,7 +74,7 @@
<div
:style="{ backgroundColor }"
draggable="true"
class="default-field border border-base-300 rounded rounded-tr-none relative group mb-2"
class="border border-base-300 rounded rounded-tr-none relative group mb-2 default-field fields-list-item"
@dragstart="onDragstart({ type: 'text', ...field })"
@dragend="$emit('drag-end')"
>
@@ -104,7 +104,7 @@
</div>
<div
v-if="editable && !onlyDefinedFields"
class="grid grid-cols-3 gap-1 pb-2"
class="grid grid-cols-3 gap-1 pb-2 fields-grid"
>
<template
v-for="(icon, type) in fieldIconsSorted"
@@ -113,7 +113,7 @@
<button
v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification')"
draggable="true"
class="field-type-button group flex items-center justify-center border border-dashed w-full rounded relative"
class="field-type-button group flex items-center justify-center border border-dashed w-full rounded relative fields-grid-item"
:style="{ backgroundColor }"
:class="drawFieldType === type ? 'border-base-content/40' : 'border-base-300 hover:border-base-content/20'"
@dragstart="onDragstart({ type: type })"
@@ -142,7 +142,7 @@
<a
href="https://www.docuseal.com/pricing"
target="_blank"
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative fields-grid-item"
:style="{ backgroundColor }"
>
<div class="w-0 absolute left-0">
@@ -168,7 +168,7 @@
<a
href="https://www.docuseal.com/contact"
target="_blank"
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative fields-grid-item"
:style="{ backgroundColor }"
>
<div class="w-0 absolute left-0">
@@ -0,0 +1,290 @@
<template>
<div
class="modal modal-open items-start !animate-none overflow-y-auto"
>
<div
class="absolute top-0 bottom-0 right-0 left-0"
@click.prevent="$emit('close')"
/>
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span class="modal-title">
{{ t('font') }} - {{ field.name || buildDefaultName(field, template.fields) }}
</span>
<a
href="#"
class="text-xl modal-close-button"
@click.prevent="$emit('close')"
>&times;</a>
</div>
<div class="mt-4">
<div>
<div class="flex items-center space-x-1.5">
<span>
<div class="dropdown modal-field-font-dropdown">
<label
tabindex="0"
class="base-input flex items-center justify-between"
style="height: 32px; padding-right: 0; width: 120px"
:class="fonts.find((f) => f.value === preferences.font)?.class"
>
<span style="margin-top: 1px">
{{ preferences.font || 'Default' }}
</span>
<IconChevronDown
class="ml-2 mr-2 mt-0.5"
width="18"
height="18"
/>
</label>
<div
tabindex="0"
class="dropdown-content p-0 mt-1 block z-10 menu shadow bg-white border border-base-300 rounded-md w-52"
>
<div
v-for="(font, index) in fonts"
:key="index"
:value="font.value"
:class="{ 'bg-base-300': preferences.font == font.value, [font.class]: true }"
class="hover:bg-base-300 px-2 py-1.5 cursor-pointer"
@click="[font.value ? preferences.font = font.value : delete preferences.font, closeDropdown()]"
>
{{ font.label }}
</div>
</div>
</div>
</span>
<span class="relative">
<select
class="select input-bordered bg-white select-sm text-center pl-2"
style="font-size: 16px; width: 86px; text-align-last: center;"
@change="$event.target.value ? preferences.font_size = parseInt($event.target.value) : delete preferences.font_size"
>
<option
:selected="!preferences.font_size"
value=""
>
Auto
</option>
<option
v-for="size in sizes"
:key="size"
:value="size"
:selected="size === preferences.font_size"
>
{{ size }}
</option>
</select>
<span
class="border-l pl-1.5 absolute bg-white bottom-0 pointer-events-none text-sm h-5"
style="right: 13px; top: 7px"
>
pt
</span>
</span>
<span>
<div
class="join"
style="height: 32px"
>
<button
v-for="(type, index) in types"
:key="index"
class="btn btn-sm join-item bg-white input-bordered hover:border-base-content/20 hover:bg-base-200/50 px-2"
:class="{ '!bg-base-300': preferences.font_type?.includes(type.value) }"
@click="setFontType(type.value)"
>
<component :is="type.icon" />
</button>
</div>
</span>
<span>
<div
class="join"
style="height: 32px"
>
<button
v-for="(align, index) in aligns"
:key="index"
class="btn btn-sm join-item bg-white input-bordered hover:border-base-content/20 hover:bg-base-200/50 px-2"
:class="{ '!bg-base-300': preferences.align === align.value }"
@click="align.value && preferences.align != align.value ? preferences.align = align.value : delete preferences.align"
>
<component :is="align.icon" />
</button>
</div>
</span>
<span>
<select
class="input input-bordered bg-white input-sm text-lg rounded-md"
style="-webkit-appearance: none; -moz-appearance: none; text-indent: 0px; text-overflow: ''; padding: 0px 6px; height: 32px"
@change="$event.target.value ? preferences.color = $event.target.value : delete preferences.color"
>
<option
v-for="(color, index) in colors"
:key="index"
:value="color.value"
:selected="color.value == preferences.color"
>
{{ color.label }}
</option>
</select>
</span>
</div>
</div>
<div class="mt-4">
<div
class="flex items-center border border-base-content/20 rounded-xl bg-white px-4 h-16 modal-field-font-preview"
:style="{
color: preferences.color || 'black',
fontSize: (preferences.font_size || 12) + 'pt',
}"
:class="textClasses"
>
<span
contenteditable="true"
class="outline-none"
>
{{ field.default_value || field.name || buildDefaultName(field, template.fields) }}
</span>
</div>
</div>
<div class="mt-4">
<button
class="base-button w-full modal-save-button"
@click.prevent="saveAndClose"
>
{{ t('save') }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { IconChevronDown, IconBold, IconItalic, IconAlignLeft, IconAlignRight, IconAlignCenter } from '@tabler/icons-vue'
export default {
name: 'FontModal',
components: {
IconChevronDown
},
inject: ['t', 'save', 'template'],
props: {
field: {
type: Object,
required: true
},
editable: {
type: Boolean,
required: false,
default: true
},
buildDefaultName: {
type: Function,
required: true
}
},
emits: ['close'],
data () {
return {
preferences: {}
}
},
computed: {
fonts () {
return [
{ value: null, label: 'Default' },
{ value: 'Times', label: 'Times', class: 'font-serif' },
{ value: 'Courier', label: 'Courier', class: 'font-mono' }
]
},
types () {
return [
{ icon: IconBold, value: 'bold' },
{ icon: IconItalic, value: 'italic' }
]
},
aligns () {
return [
{ icon: IconAlignLeft, value: 'left' },
{ icon: IconAlignCenter, value: 'center' },
{ icon: IconAlignRight, value: 'right' }
]
},
sizes () {
return [...Array(23).keys()].map(i => i + 6)
},
colors () {
return [
{ label: '⬛', value: 'black' },
{ label: '🟦', value: 'blue' },
{ label: '🟥', value: 'red' }
]
},
textClasses () {
return {
'font-mono': this.preferences.font === 'Courier',
'font-serif': this.preferences.font === 'Times',
'justify-center': this.preferences.align === 'center',
'justify-start': this.preferences.align === 'left',
'justify-end': this.preferences.align === 'right',
'font-bold': ['bold_italic', 'bold'].includes(this.preferences.font_type),
italic: ['bold_italic', 'italic'].includes(this.preferences.font_type)
}
},
keys () {
return ['font_type', 'font_size', 'color', 'align', 'font']
}
},
created () {
this.preferences = this.keys.reduce((acc, key) => {
acc[key] = this.field.preferences?.[key]
return acc
}, {})
},
methods: {
closeDropdown () {
this.$el.getRootNode().activeElement.blur()
},
setFontType (value) {
if (value === 'bold') {
if (this.preferences.font_type === 'bold') {
delete this.preferences.font_type
} else if (this.preferences.font_type === 'italic') {
this.preferences.font_type = 'bold_italic'
} else if (this.preferences.font_type === 'bold_italic') {
this.preferences.font_type = 'italic'
} else {
this.preferences.font_type = value
}
}
if (value === 'italic') {
if (this.preferences.font_type === 'italic') {
delete this.preferences.font_type
} else if (this.preferences.font_type === 'bold') {
this.preferences.font_type = 'bold_italic'
} else if (this.preferences.font_type === 'bold_italic') {
this.preferences.font_type = 'bold'
} else {
this.preferences.font_type = value
}
}
},
saveAndClose () {
this.field.preferences ||= {}
this.keys.forEach((key) => delete this.field.preferences[key])
Object.assign(this.field.preferences, this.preferences)
this.save()
this.$emit('close')
}
}
}
</script>
@@ -8,12 +8,12 @@
/>
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span>
<span class="modal-title">
{{ t('formula') }} - {{ field.name || buildDefaultName(field, template.fields) }}
</span>
<a
href="#"
class="text-xl"
class="text-xl modal-close-button"
@click.prevent="$emit('close')"
>&times;</a>
</div>
@@ -111,7 +111,7 @@
</div>
</div>
<button
class="base-button w-full"
class="base-button w-full modal-save-button"
@click.prevent="validateSaveAndClose"
>
{{ t('save') }}
+6
View File
@@ -1,4 +1,5 @@
const en = {
font: 'Font',
party: 'Party',
method: 'Method',
reorder_fields: 'Reorder fields',
@@ -161,6 +162,7 @@ const en = {
}
const es = {
fuente: 'Fuente',
party: 'Parte',
method: 'Método',
reorder_fields: 'Reordenar campos',
@@ -323,6 +325,7 @@ const es = {
}
const it = {
font: 'Carattere',
party: 'Parte',
method: 'Metodo',
reorder_fields: 'Riordina i campi',
@@ -485,6 +488,7 @@ const it = {
}
const pt = {
fonte: 'Fonte',
party: 'Parte',
method: 'Método',
reorder_fields: 'Reorganizar campos',
@@ -647,6 +651,7 @@ const pt = {
}
const fr = {
font: 'Police',
party: 'Partie',
method: 'Méthode',
reorder_fields: 'Réorganiser les champs',
@@ -809,6 +814,7 @@ const fr = {
}
const de = {
font: 'Schriftart',
party: 'Partei',
method: 'Verfahren',
reorder_fields: 'Felder neu anordnen',
@@ -1,6 +1,6 @@
<template>
<div class="absolute text-center w-full bottom-0 pr-3 mb-4">
<span class="w-full bg-base-200 px-4 py-2 rounded-md inline-flex space-x-2 mx-auto items-center justify-between mb-2 z-20">
<span class="w-full bg-base-200 px-4 py-2 rounded-md inline-flex space-x-2 mx-auto items-center justify-between mb-2 z-20 draw-field-container-mobile">
<div class="flex items-center space-x-2">
<component
:is="fieldIcons[drawField.type]"
@@ -1,7 +1,5 @@
<template>
<span
class="dropdown dropdown-top dropdown-end absolute bottom-4 right-4 z-10"
>
<span class="dropdown dropdown-top dropdown-end absolute bottom-4 right-4 z-10 fields-dropdown-mobile">
<label
class="btn btn-neutral text-white btn-circle btn-lg group"
tabindex="0"
@@ -1,6 +1,6 @@
<template>
<span
class="dropdown dropdown-end"
class="dropdown dropdown-end field-settings-dropdown"
:class="{ 'dropdown-open': ((!field.preferences?.price && !field.preferences?.formula) || !isConnected) && !isLoading }"
>
<label
+6 -6
View File
@@ -23,7 +23,7 @@
>
<div>
<button
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors p-0"
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors p-0 document-control-button"
@click.stop="isShowConditionsModal = true"
>
<IconRouteAltLeft
@@ -48,7 +48,7 @@
>
<div>
<button
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors document-control-button"
style="width: 24px; height: 24px"
@click.stop="$emit('remove', item)"
>
@@ -63,7 +63,7 @@
class="tooltip tooltip-left before:text-xs"
>
<button
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors p-0"
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors p-0 document-control-button"
@click.stop="$emit('reorder', item)"
>
<IconSortDescending2
@@ -75,14 +75,14 @@
</span>
<template v-if="withArrows">
<button
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors document-control-button"
style="width: 24px; height: 24px"
@click.stop="$emit('up', item)"
>
&uarr;
</button>
<button
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors document-control-button"
style="width: 24px; height: 24px"
@click.stop="$emit('down', item)"
>
@@ -94,7 +94,7 @@
</div>
</div>
</div>
<div class="flex pb-2 pt-1.5">
<div class="flex pb-2 pt-1.5 document-preview-name">
<Contenteditable
:model-value="item.name"
:icon-width="16"
+1 -1
View File
@@ -1,7 +1,7 @@
<template>
<label
:for="inputId"
class="btn btn-neutral btn-xs text-white transition-none"
class="btn btn-neutral btn-xs text-white transition-none replace-document-button"
:class="{ 'opacity-100': isLoading || isProcessing }"
>
{{ message }}
+1 -1
View File
@@ -3,7 +3,7 @@
<label
id="add_document_button"
:for="inputId"
class="btn btn-outline w-full"
class="btn btn-outline w-full add-document-button"
:class="{ 'btn-disabled': isLoading || isProcessing }"
>
<IconInnerShadowTop
@@ -0,0 +1,19 @@
# frozen_string_literal: true
class ProcessSubmissionExpiredJob
include Sidekiq::Job
def perform(params = {})
submission = Submission.find(params['submission_id'])
return if submission.archived_at?
return if submission.template.archived_at?
return if submission.submitters.where.not(declined_at: nil).exists?
return unless submission.submitters.exists?(completed_at: nil)
WebhookUrls.for_account_id(submission.account_id, %w[submission.expired]).each do |webhook|
SendSubmissionExpiredWebhookRequestJob.perform_async('submission_id' => submission.id,
'webhook_url_id' => webhook.id)
end
end
end
+19 -8
View File
@@ -94,11 +94,27 @@ class ProcessSubmitterCompletionJob
end
end
to = build_to_addresses(submitter)
maybe_enqueue_copy_emails(submitter)
end
return if to.blank? || submitter.template.preferences['documents_copy_email_enabled'] == false
def maybe_enqueue_copy_emails(submitter)
return if submitter.template.preferences['documents_copy_email_enabled'] == false
SubmitterMailer.documents_copy_email(submitter, to:).deliver_later!
configs = AccountConfigs.find_or_initialize_for_key(submitter.account,
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
return if configs.value['enabled'] == false
to = submitter.submission.submitters.reject { |e| e.preferences['send_email'] == false }
.sort_by(&:completed_at).select(&:email?).map(&:friendly_name)
return if to.blank?
if configs.value['bcc_recipients'] == true
to.each { |to| SubmitterMailer.documents_copy_email(submitter, to:).deliver_later! }
else
SubmitterMailer.documents_copy_email(submitter, to: to.join(', ')).deliver_later!
end
end
def build_bcc_addresses(submission)
@@ -110,11 +126,6 @@ class ProcessSubmitterCompletionJob
bcc.to_s.scan(User::EMAIL_REGEXP)
end
def build_to_addresses(submitter)
submitter.submission.submitters.reject { |e| e.preferences['send_email'] == false }
.sort_by(&:completed_at).select(&:email?).map(&:friendly_name).join(', ')
end
def enqueue_next_submitter_request_notification(submitter)
next_submitter_item =
submitter.submission.template_submitters.find do |e|
@@ -5,7 +5,7 @@ class SendFormCompletedWebhookRequestJob
sidekiq_options queue: :webhooks
MAX_ATTEMPTS = 20
MAX_ATTEMPTS = 12
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
@@ -0,0 +1,31 @@
# frozen_string_literal: true
class SendSubmissionExpiredWebhookRequestJob
include Sidekiq::Job
sidekiq_options queue: :webhooks
MAX_ATTEMPTS = 10
def perform(params = {})
submission = Submission.find(params['submission_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i
return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.expired')
resp = SendWebhookRequest.call(webhook_url, event_type: 'submission.expired',
data: Submissions::SerializeForApi.call(submission))
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
SendSubmissionExpiredWebhookRequestJob.perform_in((2**attempt).minutes, {
'submission_id' => submission.id,
'webhook_url_id' => webhook_url.id,
'attempt' => attempt + 1,
'last_status' => resp&.status.to_i
})
end
end
end
+1
View File
@@ -30,6 +30,7 @@ class AccountConfig < ApplicationRecord
ALLOW_TO_RESUBMIT = 'allow_to_resubmit'
ALLOW_TO_DECLINE_KEY = 'allow_to_decline'
SUBMITTER_REMINDERS = 'submitter_reminders'
ENFORCE_SIGNING_ORDER_KEY = 'enforce_signing_order'
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
FORM_COMPLETED_MESSAGE_KEY = 'form_completed_message'
FORM_WITH_CONFETTI_KEY = 'form_with_confetti'
+11
View File
@@ -55,6 +55,7 @@ class Submitter < ApplicationRecord
has_many_attached :attachments
has_many_attached :preview_documents
has_many :template_accesses, through: :template
has_many :email_events, as: :emailable, dependent: (Docuseal.multitenant? ? nil : :destroy)
has_many :document_generation_events, dependent: :destroy
has_many :submission_events, dependent: :destroy
@@ -63,6 +64,8 @@ class Submitter < ApplicationRecord
scope :completed, -> { where.not(completed_at: nil) }
after_destroy :anonymize_email_events, if: -> { Docuseal.multitenant? }
def status
if declined_at?
'declined'
@@ -108,4 +111,12 @@ class Submitter < ApplicationRecord
fields.any? { |f| f['submitter_uuid'] == uuid && signature_field_types.include?(f['type']) }
end
end
private
def anonymize_email_events
email_events.each do |event|
event.update!(email: Digest::MD5.base64digest(event.email))
end
end
end
+4 -2
View File
@@ -28,10 +28,12 @@ class WebhookUrl < ApplicationRecord
form.started
form.completed
form.declined
submission.created
submission.completed
submission.expired
submission.archived
template.created
template.updated
submission.created
submission.archived
].freeze
belongs_to :account
+16 -2
View File
@@ -63,7 +63,7 @@
<span>
<%= t('add_signature_id_to_the_documents') %>
</span>
<%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()' %>
<%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()', disabled: can?(:manage, :cfr) %>
</div>
<% end %>
<% end %>
@@ -75,7 +75,7 @@
<span>
<%= t('require_signing_reason') %>
</span>
<%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()' %>
<%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()', disabled: can?(:manage, :cfr) %>
</div>
<% end %>
<% end %>
@@ -151,6 +151,20 @@
</div>
<% end %>
<% end %>
<% if !Docuseal.multitenant? || can?(:manage, :personalization_advanced) %>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::ENFORCE_SIGNING_ORDER_KEY) %>
<% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %>
<div class="flex items-center justify-between py-2.5">
<span>
<%= t('always_enforce_signing_order') %>
</span>
<%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()' %>
</div>
<% end %>
<% end %>
<% end %>
</div>
<% end %>
<%= render 'compliances' %>
@@ -8,7 +8,7 @@
<div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<%= f.hidden_field :key %>
<%= f.fields_for :value, Struct.new(:subject, :body, :reply_to, :attach_audit_log, :attach_documents).new(*f.object.value.values_at('subject', 'body', 'reply_to', 'attach_audit_log', 'attach_documents')) do |ff| %>
<%= f.fields_for :value, Struct.new(:subject, :body, :reply_to, :attach_audit_log, :attach_documents, :bcc_recipients, :enabled).new(*f.object.value.values_at('subject', 'body', 'reply_to', 'attach_audit_log', 'attach_documents', 'bcc_recipients', 'enabled')) do |ff| %>
<div class="form-control">
<%= ff.label :subject, t('subject'), class: 'label' %>
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
@@ -24,23 +24,41 @@
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<% if can?(:manage, :reply_to) %>
<% if can?(:manage, :reply_to) || can?(:manage, :personalization_advanced) %>
<div class="form-control">
<%= ff.label :reply_to, t('reply_to'), class: 'label' %>
<%= ff.email_field :reply_to, class: 'base-input', dir: 'auto', placeholder: t(:email) %>
</div>
<% end %>
<div class="flex items-center justify-between pt-2.5 mx-1">
<span>
<%= t('attach_documents') %>
</span>
<%= ff.check_box :attach_documents, { checked: ff.object.attach_documents != false, class: 'toggle' }, 'true', 'false' %>
</div>
<div class="flex items-center justify-between pb-2.5 mx-1">
<span>
<%= t('attach_audit_log_pdf') %>
</span>
<%= ff.check_box :attach_audit_log, { checked: ff.object.attach_audit_log != false, class: 'toggle' }, 'true', 'false' %>
<div class="space-y-3.5">
<div class="flex items-center justify-between mx-1">
<span>
<%= t('attach_documents') %>
</span>
<%= ff.check_box :attach_documents, { checked: ff.object.attach_documents != false, class: 'toggle' }, 'true', 'false' %>
</div>
<div class="flex items-center justify-between mx-1">
<span>
<%= t('attach_audit_log_pdf') %>
</span>
<%= ff.check_box :attach_audit_log, { checked: ff.object.attach_audit_log != false, class: 'toggle' }, 'true', 'false' %>
</div>
<% unless Docuseal.multitenant? %>
<div class="flex items-center justify-between mx-1">
<span>
<%= t('bcc_recipients') %>
</span>
<%= ff.check_box :bcc_recipients, { checked: ff.object.bcc_recipients == true, class: 'toggle' }, 'true', 'false' %>
</div>
<% end %>
<% if !Docuseal.multitenant? || can?(:manage, :personalization_advanced) %>
<div class="flex items-center justify-between mx-1">
<span>
<%= t('send_emails_automatically_on_completion') %>
</span>
<%= ff.check_box :enabled, { checked: ff.object.enabled != false, class: 'toggle' }, 'true', 'false' %>
</div>
<% end %>
</div>
<% end %>
<div class="form-control pt-2">
+2 -2
View File
@@ -13,10 +13,10 @@
<meta property="og:image" content="">
<meta name="twitter:image" content="">
<% else %>
<meta property="og:image" content="<%= image_pack_url('images/preview.png') %>">
<meta property="og:image" content="<%= root_url %>preview.png">
<meta property="og:image:width" content="800">
<meta property="og:image:height" content="800">
<meta name="twitter:image" content="<%= image_pack_url('images/preview.png') %>">
<meta name="twitter:image" content="<%= root_url %>preview.png">
<% end %>
<meta name="twitter:card" content="summary">
<meta name="twitter:creator" content="@docusealco">
+3 -1
View File
@@ -30,7 +30,9 @@
<div dir="auto" class="form-control !mt-0">
<%= f.label :email, t('email'), class: 'label' %>
<%= f.email_field :email, value: current_user&.email || params[:email] || @submitter.email, required: true, class: 'base-input', placeholder: t('provide_your_email_to_start') %>
<%= @error_message %>
<% if @error_message %>
<span class="label-text-alt text-red-400 mt-1"><%= @error_message %></span>
<% end %>
</div>
<toggle-submit dir="auto" class="form-control">
<%= f.button button_title(title: t('start'), disabled_with: t('starting')), class: 'base-button' %>
+2 -1
View File
@@ -1,7 +1,8 @@
<% align = field.dig('preferences', 'align') %>
<% color = field.dig('preferences', 'color') %>
<% font = field.dig('preferences', 'font') %>
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-mono' if font == 'Courier' %> <%= 'font-serif' if font == 'Times' %> <%= align == 'right' ? 'justify-end' : (align == 'center' ? 'justify-center' : '') %>" style="<%= "color: #{color}; " if color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size'].to_i * 1.23}pt); line-height: `clamp(6pt, 2.0vw, #{(field['preferences']['font_size'].to_i * 1.23) + 3}pt)`" if field.dig('preferences', 'font_size') %>">
<% 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') %>">
<% if field['type'] == 'signature' %>
<div class="flex flex-col justify-between h-full overflow-hidden">
<div class="flex-grow flex overflow-hidden" style="min-height: 50%">
+27 -7
View File
@@ -19,7 +19,7 @@
<%= svg_icon('external_link', class: 'w-6 h-6') %>
<span class="hidden md:inline"><%= t('audit_log') %></span>
</a>
<% else %>
<% elsif signed_in? %>
<%= link_to submission_events_path(@submission), class: 'white-button', data: { turbo_frame: :modal } do %>
<%= svg_icon('logs', class: 'w-6 h-6') %>
<span class="hidden md:block"><%= t('event_log') %></span>
@@ -105,7 +105,18 @@
<% value = values[field['uuid']] %>
<% value ||= field['default_value'] if field['type'] == 'heading' %>
<% next if value.blank? %>
<%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: %>
<% if (mask = field.dig('preferences', 'mask').presence) && signed_in? && can?(:read, @submission) %>
<span class="group">
<span class="hidden group-hover:inline">
<%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: %>
</span>
<span class="group-hover:hidden">
<%= render 'submissions/value', area:, field:, attachments_index:, value: Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', '), locale: @submission.account.locale, timezone: @submission.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: %>
</span>
</span>
<% else %>
<%= render 'submissions/value', area:, field:, attachments_index:, value: mask.present? ? Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', ') : value, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: %>
<% end %>
<% end %>
</div>
</div>
@@ -232,12 +243,21 @@
</div>
<% elsif field['type'] == 'checkbox' %>
<%= svg_icon('check', class: 'w-6 h-6') %>
<% elsif field['type'] == 'number' %>
<%= NumberUtils.format_number(value, field.dig('preferences', 'format')) %>
<% elsif field['type'] == 'date' %>
<%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), @submission.account.locale) %>
<% else %>
<div class="whitespace-pre-wrap"><%= Array.wrap(value).join(', ') %></div>
<% if field['type'] == 'number' %>
<% value = NumberUtils.format_number(value, field.dig('preferences', 'format')) %>
<% elsif field['type'] == 'date' %>
<% value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), @submission.account.locale) %>
<% end %>
<% if (mask = field.dig('preferences', 'mask').presence) %>
<% if signed_in? && can?(:read, @submission) %>
<div class="[&:not(:hover)]:after:content-[attr(data-value)] block whitespace-pre-wrap group" data-value="<%= Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', ') %>"><span class="hidden group-hover:block"><%= Array.wrap(value).join(', ') %></span></div>
<% else %>
<div class="whitespace-pre-wrap"><%= Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', ') %></div>
<% end %>
<% else %>
<div class="whitespace-pre-wrap"><%= Array.wrap(value).join(', ') %></div>
<% end %>
<% end %>
</div>
</div>
+1 -1
View File
@@ -58,7 +58,7 @@
<% next if field['conditions'].present? && values[field['uuid']].blank? && field['submitter_uuid'] != @submitter.uuid %>
<% next if field['conditions'].present? && field['submitter_uuid'] == @submitter.uuid %>
<% next if field.dig('preferences', 'formula').present? && field['submitter_uuid'] == @submitter.uuid %>
<%= render 'submissions/value', area:, field:, attachments_index: @attachments_index, value:, locale: @submitter.account.locale, timezone: @submitter.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: @form_configs[:with_signature_id] %>
<%= render 'submissions/value', area:, field:, attachments_index: @attachments_index, value: field.dig('preferences', 'mask').present? ? TextUtils.mask_value(value, field.dig('preferences', 'mask')) : value, locale: @submitter.account.locale, timezone: @submitter.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: @form_configs[:with_signature_id] %>
<% end %>
</div>
</div>
+14 -12
View File
@@ -206,7 +206,7 @@
<span>
<%= t('send_emails_automatically_on_completion') %>
</span>
<%= ff.check_box :documents_copy_email_enabled, { checked: ff.object.documents_copy_email_enabled != false, class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %>
<%= ff.check_box :documents_copy_email_enabled, { checked: ff.object.documents_copy_email_enabled != false && configs['enabled'] != false, class: 'toggle', onchange: 'this.form.requestSubmit()', disabled: configs['enabled'] == false }, 'true', 'false' %>
</div>
<% end %>
<div class="form-control pt-2">
@@ -285,12 +285,12 @@
<div class="form-control">
<%= ff.text_field :name, class: 'w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer mb-2', autocomplete: 'off', placeholder: "#{index + 1}#{(index + 1).ordinal} Party", required: true %>
<% if @template.submitters.size == 2 %>
<%= ff.email_field :email, class: 'base-input', autocomplete: 'off', placeholder: t('default_email'), disabled: ff.object.is_requester || ff.object.invite_by_uuid.present? || ff.object.optional_invite_by_uuid.present?, id: field_uuid = SecureRandom.uuid %>
<%= tag.input name: ff.field_name(:email), value: ff.object.email, type: :email, class: 'base-input', multiple: true, autocomplete: 'off', placeholder: t('default_email'), disabled: ff.object.is_requester || ff.object.invite_by_uuid.present? || ff.object.optional_invite_by_uuid.present?, id: field_uuid = SecureRandom.uuid %>
<% else %>
<toggle-attribute data-target-id="<%= email_field_uuid = SecureRandom.uuid %>" data-class-name="hidden" data-value="email">
<%= ff.select :option, [[t('not_specified'), 'not_set'], [t('submission_requester'), 'is_requester'], [t('specified_email'), 'email'], *(@template.submitters - [submitter]).flat_map { |e| [[t('invite_by_name', name: e['name']), "invite_by_#{e['uuid']}"], [t('invite_by_name', name: e['name']) + " (#{t(:optional).capitalize})", "optional_invite_by_#{e['uuid']}"]] }, *(@template.submitters - [submitter]).map { |e| [t('same_as_name', name: e['name']), "linked_to_#{e['uuid']}"] }], {}, class: 'base-select mb-3' %>
</toggle-attribute>
<%= ff.email_field :email, class: "base-input #{'hidden' if item.option != 'email'}", autocomplete: 'off', placeholder: t('default_email'), id: email_field_uuid %>
<%= tag.input name: ff.field_name(:email), type: :email, value: ff.object.email, multiple: true, class: "base-input #{'hidden' if item.option != 'email'}", autocomplete: 'off', placeholder: t('default_email'), id: email_field_uuid %>
<% end %>
</div>
<% if @template.submitters.size == 2 %>
@@ -322,15 +322,17 @@
<% end %>
</div>
<% end %>
<%= 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 pt-4 mt-4 justify-between border-t w-full">
<span>
<%= t('enforce_recipients_order') %>
</span>
<%= f.fields_for :preferences, Struct.new(:submitters_order).new(@template.preferences['submitters_order']) do |ff| %>
<%= ff.check_box :submitters_order, { class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'preserved', '' %>
<% end %>
</div>
<% unless current_account.account_configs.exists?(key: AccountConfig::ENFORCE_SIGNING_ORDER_KEY, value: true) %>
<%= 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 pt-4 mt-4 justify-between border-t w-full">
<span>
<%= t('enforce_recipients_order') %>
</span>
<%= f.fields_for :preferences, Struct.new(:submitters_order).new(@template.preferences['submitters_order']) do |ff| %>
<%= ff.check_box :submitters_order, { class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'preserved', '' %>
<% end %>
</div>
<% end %>
<% end %>
<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 %>
+1 -1
View File
@@ -1,7 +1,7 @@
<div class="flex-wrap space-y-4 md:flex md:flex-nowrap md:space-y-0 md:space-x-10">
<%= render 'shared/settings_nav' %>
<div class="md:flex-grow">
<div class="flex flex-col md:flex-row gap-2 md:justify-between md:items-end mb-4 min-h-12">
<div class="flex flex-col md:flex-row md:flex-wrap gap-2 md:justify-between md:items-end mb-4 min-h-12">
<h1 class="text-4xl font-bold">
<% if params[:status] == 'archived' %>
<%= t('archived_users') %>
+8
View File
@@ -0,0 +1,8 @@
{
"ignored_warnings": [
{
"fingerprint": "25f4ce5fee1e1180fa1919dc4ee78db3ab3457a956e4679503aa745771a43836",
"note": "Permitted parameters are necessary for creating submitters via API"
}
]
}
+1 -1
View File
@@ -49,7 +49,7 @@ Rails.application.configure do
end
config.active_storage.resolve_model_to_route = :rails_storage_proxy if ENV['ACTIVE_STORAGE_PUBLIC'] != 'true'
config.active_storage.service_urls_expire_in = 4.hours
config.active_storage.service_urls_expire_in = ENV.fetch('PRESIGNED_URLS_EXPIRE_MINUTES', '240').to_i.minutes
# Mount Action Cable outside main process or domain.
# config.action_cable.mount_path = nil
+1 -1
View File
@@ -187,7 +187,7 @@ Devise.setup do |config|
# ==> Configuration for :rememberable
# The time the user will be remembered without asking for credentials again.
config.remember_for = 2.years
config.remember_for = ENV.fetch('SESSION_REMEMBER_DAYS', '730').to_i.days
# Invalidates all the remember me tokens when the user signs out.
config.expire_all_remember_me_on_sign_out = true
+24 -18
View File
@@ -20,6 +20,8 @@ en: &en
language_ko: 한국어
hi_there: Hi there
thanks: Thanks
bcc_recipients: BCC recipients
always_enforce_signing_order: Always enforce the signing order
edit_per_party: Edit per party
reply_to: Reply to
pending_by_me: Pending by me
@@ -432,7 +434,7 @@ en: &en
share_template_with_test_mode: Share template with Test mode
share_template_with_all_tenants: Share template with all Tenants
use_following_placeholders_text_: 'Use following placeholders text:'
upgrade_plan_to_add_more_users: Upgrade plan to add more users
users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: '%{users_count}/%{total_users_count} Pro users limit was reached. To invite additional users, please purchase more Pro user seats via the "Manage plan" button.'
move_into_folder: Move Into Folder
new_folder_name: New Folder Name
exit_preview: Exit Preview
@@ -473,8 +475,6 @@ en: &en
team_accounts: Team Accounts
tenant_account: Tenant account
tenant_accounts: Tenant Accounts
upgrade_plan: Upgrade Plan
add_user: Add User
impersonate: Impersonate
loading: Loading
documents: Documents
@@ -700,6 +700,7 @@ en: &en
submission_created_via_source_html: '<b>Submission created</b> via %{source}'
pro_user_seats_used: Pro user seats used
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.
submission_sources:
api: API
bulk: Bulk Send
@@ -744,6 +745,8 @@ en: &en
read: Read your data
es: &es
always_enforce_signing_order: Siempre imponer el orden de firma
bcc_recipients: Destinatarios CCO
edit_per_party: Editar por parte
signed: Firmado
reply_to: Responder a
@@ -1158,7 +1161,7 @@ es: &es
share_template_with_test_mode: Compartir plantilla con el modo de prueba
share_template_with_all_tenants: Compartir plantilla con todos los inquilinos
use_following_placeholders_text_: 'Usa los siguientes marcadores de posición:'
upgrade_plan_to_add_more_users: Actualiza el plan para agregar más usuarios
users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'Se alcanzó el límite de %{users_count}/%{total_users_count} usuarios Pro. Para invitar a más usuarios, compra más plazas Pro usando el botón "Gestionar plan".'
move_into_folder: Mover a la carpeta
new_folder_name: Nuevo nombre de la carpeta
exit_preview: Salir de la vista previa
@@ -1199,8 +1202,6 @@ es: &es
team_accounts: Cuentas de equipo
tenant_account: Cuenta de inquilino
tenant_accounts: Cuentas de inquilino
upgrade_plan: Actualizar plan
add_user: Agregar usuario
impersonate: Suplantar
loading: Cargando
documents: Documentos
@@ -1426,6 +1427,7 @@ es: &es
submission_created_via_source_html: '<b>Envío creado</b> a través de %{source}'
pro_user_seats_used: Plazas de usuario Pro en uso
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.'
submission_sources:
api: API
bulk: Envío masivo
@@ -1470,6 +1472,8 @@ es: &es
read: Leer tus datos
it: &it
always_enforce_signing_order: Applicare sempre l'ordine di firma
bcc_recipients: Destinatari BCC
edit_per_party: Modifica per partito
signed: Firmato
reply_to: Rispondi a
@@ -1883,7 +1887,7 @@ it: &it
share_template_with_test_mode: Condividi modello con la modalità di test
share_template_with_all_tenants: Condividi il modello con tutti i tenant
use_following_placeholders_text_: 'Usa i seguenti segnaposto:'
upgrade_plan_to_add_more_users: Aggiorna il piano per aggiungere più utenti
users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'È stato raggiunto il limite di %{users_count}/%{total_users_count} utenti Pro. Per invitare altri utenti, acquista più posti Pro tramite il pulsante "Gestisci piano".'
move_into_folder: Sposta nella cartella
new_folder_name: Nuovo nome della cartella
exit_preview: "Esci dall'anteprima"
@@ -1924,8 +1928,6 @@ it: &it
team_accounts: Account di squadra
tenant_account: Account tenant
tenant_accounts: Account tenant
upgrade_plan: Aggiorna piano
add_user: Aggiungi utente
impersonate: Impersona
loading: Caricamento in corso
documents: Documenti
@@ -2151,6 +2153,7 @@ it: &it
submission_created_via_source_html: '<b>Invio creato</b> tramite %{source}'
pro_user_seats_used: Posti utente Pro in uso
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."
submission_sources:
api: API
bulk: Invio massivo
@@ -2195,6 +2198,8 @@ it: &it
read: Leggi i tuoi dati
fr: &fr
always_enforce_signing_order: Toujours appliquer l'ordre de signature
bcc_recipients: Destinataires en CCI
edit_per_party: Éditer par partie
signed: Signé
reply_to: Répondre à
@@ -2610,7 +2615,7 @@ fr: &fr
share_template_with_test_mode: Partager le modèle avec le mode test
share_template_with_all_tenants: Partager le modèle avec tous les locataires
use_following_placeholders_text_: 'Utilisez les espaces réservés suivants:'
upgrade_plan_to_add_more_users: "Mettez à jour le plan pour ajouter plus d'utilisateurs"
users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'La limite de %{users_count}/%{total_users_count} utilisateurs Pro a été atteinte. Pour inviter d''autres utilisateurs, veuillez acheter plus de places Pro via le bouton "Gérer le plan".'
move_into_folder: Déplacer dans le dossier
new_folder_name: Nouveau nom du dossier
exit_preview: "Quitter l'aperçu"
@@ -2651,8 +2656,6 @@ fr: &fr
team_accounts: "Comptes d'équipe"
tenant_account: Compte locataire
tenant_accounts: Comptes locataires
upgrade_plan: Mettre à jour le plan
add_user: Ajouter un utilisateur
impersonate: Usurper
loading: Chargement en cours
documents: Documents
@@ -2878,6 +2881,7 @@ fr: &fr
submission_created_via_source_html: '<b>Soumission créée</b> via %{source}'
pro_user_seats_used: Places utilisateur Pro en cours d'utilisation
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 lutilisation dun lien de partage, car il nest 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.'
submission_sources:
api: API
bulk: Envoi en masse
@@ -2922,6 +2926,8 @@ fr: &fr
read: Lire vos données
pt: &pt
always_enforce_signing_order: Sempre impor a ordem de assinatura
bcc_recipients: Destinatários BCC
edit_per_party: Edita por festa
signed: Assinado
reply_to: Responder a
@@ -3336,7 +3342,7 @@ pt: &pt
share_template_with_test_mode: Compartilhar modelo com o modo de teste
share_template_with_all_tenants: Compartilhar modelo com todos os locatários
use_following_placeholders_text_: 'Use os seguintes textos de substituição:'
upgrade_plan_to_add_more_users: Faça upgrade do plano para adicionar mais usuários
users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'O limite de %{users_count}/%{total_users_count} usuários Pro foi atingido. Para convidar mais usuários, adquira mais licenças Pro através do botão "Gerenciar plano".'
move_into_folder: Mover para pasta
new_folder_name: Novo nome da pasta
exit_preview: Sair da pré-visualização
@@ -3377,8 +3383,6 @@ pt: &pt
team_accounts: Contas de equipe
tenant_account: Conta de locatário
tenant_accounts: Contas de locatário
upgrade_plan: Fazer upgrade do plano
add_user: Adicionar usuário
impersonate: Usurpar
loading: Carregando
documents: Documentos
@@ -3604,6 +3608,7 @@ pt: &pt
submission_created_via_source_html: '<b>Envio criado</b> via %{source}'
pro_user_seats_used: Lugares de usuário Pro em uso
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.'
submission_sources:
api: API
bulk: Envio em massa
@@ -3648,6 +3653,8 @@ pt: &pt
read: Ler seus dados
de: &de
always_enforce_signing_order: Immer die Reihenfolge der Unterschriften erzwingen
bcc_recipients: BCC-Empfänger
edit_per_party: Bearbeiten pro Partei
signed: Unterschrieben
reply_to: Antworten auf
@@ -4062,7 +4069,7 @@ de: &de
share_template_with_test_mode: Vorlage mit dem Testmodus teilen
share_template_with_all_tenants: Vorlage mit allen Mietern teilen
use_following_placeholders_text_: 'Verwende die folgenden Platzhaltertexte:'
upgrade_plan_to_add_more_users: Upgrade des Plans, um weitere Benutzer hinzuzufügen
users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'Das Limit von %{users_count}/%{total_users_count} Pro-Benutzern wurde erreicht. Um weitere Benutzer einzuladen, kaufen Sie bitte zusätzliche Pro-Benutzerplätze über die Schaltfläche "Plan verwalten".'
move_into_folder: In Ordner verschieben
new_folder_name: Neuer Ordnername
exit_preview: Vorschau beenden
@@ -4103,8 +4110,6 @@ de: &de
team_accounts: Teamkonten
tenant_account: Mieterkonto
tenant_accounts: Mieterkonten
upgrade_plan: Plan upgraden
add_user: Benutzer hinzufügen
impersonate: Nachahmen
loading: Wird geladen
documents: Dokumente
@@ -4330,6 +4335,7 @@ de: &de
submission_created_via_source_html: '<b>Übermittlung erstellt</b> über %{source}'
pro_user_seats_used: Verwendete Pro-Benutzerplätze
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.'
submission_sources:
api: API
bulk: Massenversand
+8 -2
View File
@@ -33,8 +33,14 @@ environment ENV.fetch('RAILS_ENV', 'development')
# the concurrency of the application would be max `threads` * `workers`.
# Workers do not work on JRuby or Windows (both of which do not support
# processes).
#
workers ENV.fetch('WEB_CONCURRENCY', 0)
if ENV['WEB_CONCURRENCY_AUTO'] == 'true'
require 'etc'
workers Etc.nprocessors
else
workers ENV.fetch('WEB_CONCURRENCY', 0)
end
# Use the `preload_app!` method when specifying a `workers` number.
# This directive tells Puma to first boot the application and load code
+2
View File
@@ -122,6 +122,8 @@ Rails.application.routes.draw do
get '/disk/:encoded_key/*filename' => 'active_storage/disk#show', as: :rails_disk_service
put '/disk/:encoded_token' => 'active_storage/disk#update', as: :update_rails_disk_service
post '/direct_uploads' => 'active_storage/direct_uploads#create', as: :rails_direct_uploads
ActiveSupport.run_load_hooks(:multitenant_routes, self)
end
resources :start_form, only: %i[show update], path: 'd', param: 'slug' do
@@ -0,0 +1,37 @@
# frozen_string_literal: true
class CreateConsole1984Tables < ActiveRecord::Migration[7.0]
def change
create_table :console1984_sessions do |t|
t.text :reason
t.references :user, null: false, index: false
t.timestamps
t.index :created_at
t.index %i[user_id created_at]
end
create_table :console1984_users do |t|
t.string :username, null: false
t.timestamps
t.index [:username]
end
create_table :console1984_commands do |t|
t.text :statements
t.references :sensitive_access
t.references :session, null: false, index: false
t.timestamps
t.index %i[session_id created_at sensitive_access_id], name: 'on_session_and_sensitive_chronologically'
end
create_table :console1984_sensitive_accesses do |t|
t.text :justification
t.references :session, null: false
t.timestamps
end
end
end
+35 -1
View File
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.2].define(version: 2024_12_07_172237) do
ActiveRecord::Schema[8.0].define(version: 2025_02_25_111255) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -112,6 +112,40 @@ ActiveRecord::Schema[7.2].define(version: 2024_12_07_172237) do
t.index ["submitter_id"], name: "index_completed_submitters_on_submitter_id", unique: true
end
create_table "console1984_commands", force: :cascade do |t|
t.text "statements"
t.bigint "sensitive_access_id"
t.bigint "session_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["sensitive_access_id"], name: "index_console1984_commands_on_sensitive_access_id"
t.index ["session_id", "created_at", "sensitive_access_id"], name: "on_session_and_sensitive_chronologically"
end
create_table "console1984_sensitive_accesses", force: :cascade do |t|
t.text "justification"
t.bigint "session_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["session_id"], name: "index_console1984_sensitive_accesses_on_session_id"
end
create_table "console1984_sessions", force: :cascade do |t|
t.text "reason"
t.bigint "user_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["created_at"], name: "index_console1984_sessions_on_created_at"
t.index ["user_id", "created_at"], name: "index_console1984_sessions_on_user_id_and_created_at"
end
create_table "console1984_users", force: :cascade do |t|
t.string "username", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["username"], name: "index_console1984_users_on_username"
end
create_table "document_generation_events", force: :cascade do |t|
t.bigint "submitter_id", null: false
t.string "event_name", null: false
+1 -1
View File
@@ -28,7 +28,7 @@ module ActionMailerConfigsInterceptor
end
unless Docuseal.multitenant?
email_configs = EncryptedConfig.find_by(key: EncryptedConfig::EMAIL_SMTP_KEY)
email_configs = EncryptedConfig.order(:account_id).find_by(key: EncryptedConfig::EMAIL_SMTP_KEY)
if email_configs
message.delivery_method(:smtp, build_smtp_configs_hash(email_configs))
+11 -10
View File
@@ -8,19 +8,20 @@ module DownloadUtils
module_function
def call(url)
uri = Addressable::URI.parse(url)
if Docuseal.multitenant?
raise UnableToDownload, "Error loading: #{uri.display_uri}. Only HTTPS is allowed." if uri.scheme != 'https'
if uri.host.in?(LOCALHOSTS)
raise UnableToDownload, "Error loading: #{uri.display_uri}. Can't download from localhost."
end
uri = begin
URI(url)
rescue URI::Error
Addressable::URI.parse(url).normalize
end
resp = conn.get(uri.display_uri.to_s)
if Docuseal.multitenant?
raise UnableToDownload, "Error loading: #{uri}. Only HTTPS is allowed." if uri.scheme != 'https'
raise UnableToDownload, "Error loading: #{uri}. Can't download from localhost." if uri.host.in?(LOCALHOSTS)
end
raise UnableToDownload, "Error loading: #{uri.display_uri}" if resp.status >= 400
resp = conn.get(uri)
raise UnableToDownload, "Error loading: #{uri}" if resp.status >= 400
resp
end
+25 -4
View File
@@ -15,7 +15,10 @@ module ReplaceEmailVariables
SUBMITTER_SLUG = /\{+submitter\.slug\}+/i
SUBMISSION_LINK = /\{+submission\.link\}+/i
SUBMISSION_ID = /\{+submission\.id\}+/i
SUBMISSION_SUBMITTERS = /\{+submission\.submitters\}+/i
SUBMITTERS = /\{+(?:submission\.)?submitters\}+/i
SUBMITTERS_N_EMAIL = /\{+submitters\[(?<index>\d+)\]\.email\}+/i
SUBMITTERS_N_NAME = /\{+submitters\[(?<index>\d+)\]\.name\}+/i
SUBMITTERS_N_FIRST_NAME = /\{+submitters\[(?<index>\d+)\]\.first_name\}+/i
DOCUMENTS_LINKS = /\{+documents\.links\}+/i
DOCUMENTS_LINK = /\{+documents\.link\}+/i
@@ -37,13 +40,25 @@ module ReplaceEmailVariables
text = replace(text, SUBMISSION_LINK, html_escape:) do
submitter.submission ? build_submission_link(submitter.submission) : ''
end
text = replace(text, SUBMISSION_SUBMITTERS, html_escape:) { build_submission_submitters(submitter.submission) }
text = replace(text, SUBMITTERS, html_escape:) { build_submission_submitters(submitter.submission) }
text = replace(text, DOCUMENTS_LINKS, html_escape:) { build_documents_links_text(submitter, sig) }
text = replace(text, DOCUMENTS_LINK, html_escape:) { build_documents_links_text(submitter, sig) }
text = replace(text, ACCOUNT_NAME, html_escape:) { submitter.submission.account.name }
text = replace(text, SENDER_NAME, html_escape:) { submitter.submission.created_by_user&.full_name }
text = replace(text, SENDER_FIRST_NAME, html_escape:) { submitter.submission.created_by_user&.first_name }
text = replace(text, SUBMITTERS_N_NAME, html_escape:) do |match|
build_submitters_n_field(submitter.submission, match[:index].to_i - 1, :name)
end
text = replace(text, SUBMITTERS_N_EMAIL, html_escape:) do |match|
build_submitters_n_field(submitter.submission, match[:index].to_i - 1, :email)
end
text = replace(text, SUBMITTERS_N_FIRST_NAME, html_escape:) do |match|
build_submitters_n_field(submitter.submission, match[:index].to_i - 1, :first_name)
end
replace(text, SENDER_EMAIL, html_escape:) { submitter.submission.created_by_user&.email.to_s.sub(/\+\w+@/, '@') }
end
# rubocop:enable Metrics
@@ -54,12 +69,18 @@ module ReplaceEmailVariables
)
end
def build_submitters_n_field(submission, index, field_name)
uuid = (submission.template_submitters || submission.template.submitters).dig(index, 'uuid')
submission.submitters.find { |s| s.uuid == uuid }.try(field_name)
end
def replace(text, var, html_escape: false)
text.gsub(var) do
if html_escape
ERB::Util.html_escape(yield)
ERB::Util.html_escape(yield(Regexp.last_match))
else
yield
yield(Regexp.last_match)
end
end
end
+19 -1
View File
@@ -3,10 +3,28 @@
module SendWebhookRequest
USER_AGENT = 'DocuSeal.com Webhook'
LOCALHOSTS = %w[0.0.0.0 127.0.0.1 localhost].freeze
HttpsError = Class.new(StandardError)
LocalhostError = Class.new(StandardError)
module_function
def call(webhook_url, event_type:, data:)
Faraday.post(webhook_url.url) do |req|
uri = begin
URI(webhook_url.url)
rescue URI::Error
Addressable::URI.parse(webhook_url.url).normalize
end
if Docuseal.multitenant?
raise HttpsError, 'Only HTTPS is allowed.' if uri.scheme != 'https' &&
!AccountConfig.exists?(key: :allow_http,
account_id: webhook_url.account_id)
raise LocalhostError, "Can't send to localhost." if uri.host.in?(LOCALHOSTS)
end
Faraday.post(uri) do |req|
req.headers['Content-Type'] = 'application/json'
req.headers['User-Agent'] = USER_AGENT
req.headers.merge!(webhook_url.secret.to_h) if webhook_url.secret.present?
+17
View File
@@ -107,6 +107,7 @@ module Submissions
def normalize_email(email)
return if email.blank?
return if email.is_a?(Numeric)
return email.downcase if email.to_s.include?(',') ||
email.to_s.match?(/\.(?:gob|om|mm|cm|et|mo|nz|za|ie)\z/) ||
@@ -195,4 +196,20 @@ module Submissions
end
end.exclude?(false)
end
def regenerate_documents(submission)
submitters = submission.submitters.where.not(completed_at: nil).preload(:documents_attachments)
submitters.each { |submitter| submitter.documents.each(&:destroy!) }
submission.submitters.where.not(completed_at: nil).order(:completed_at).each do |submitter|
GenerateResultAttachments.call(submitter)
end
return if submission.combined_document_attachment.blank?
submission.combined_document_attachment.destroy!
Submissions::GenerateCombinedAttachment.call(submission.submitters.completed.order(:completed_at).last)
end
end
+112 -13
View File
@@ -6,11 +6,11 @@ module Submissions
module_function
# rubocop:disable Metrics/BlockLength
# rubocop:disable Metrics
def call(template:, user:, submissions_attrs:, source:, submitters_order:, params: {})
preferences = Submitters.normalize_preferences(user.account, user, params)
Array.wrap(submissions_attrs).filter_map do |attrs|
submissions = Array.wrap(submissions_attrs).filter_map do |attrs|
submission_preferences = Submitters.normalize_preferences(user.account, user, attrs)
submission_preferences = preferences.merge(submission_preferences)
@@ -23,15 +23,33 @@ module Submissions
expire_at: attrs[:expire_at],
template_submitters: [], submitters_order:)
maybe_set_template_fields(submission, attrs[:submitters])
template_submitters = template.submitters.deep_dup
attrs[:submitters].each_with_index do |submitter_attrs, index|
uuid = find_submitter_uuid(template, submitter_attrs, index)
if submitter_attrs[:roles].present? && submitter_attrs[:roles].size > 1
template_submitter, template_submitters, submission.template_fields =
merge_submitters_and_fields(submitter_attrs, template_submitters,
submission.template_fields || submission.template.fields)
next if uuid.blank?
next if submitter_attrs.slice('email', 'phone', 'name').compact_blank.blank?
submission.template_schema = submission.template.schema if submission.template_schema.blank?
uuid = template_submitter['uuid']
else
if submitter_attrs[:roles].present? && submitter_attrs[:roles].size == 1
submitter_attrs[:role] = submitter_attrs[:roles].first
end
uuid = find_submitter_uuid(template_submitters, submitter_attrs, index)
next if uuid.blank?
next if submitter_attrs.slice('email', 'phone', 'name').compact_blank.blank?
submission.template_fields = submission.template.fields if submitter_attrs[:completed].present? &&
submission.template_fields.blank?
template_submitter = template_submitters.find { |e| e['uuid'] == uuid }
end
template_submitter = template.submitters.find { |e| e['uuid'] == uuid }
submission.template_submitters << template_submitter.except('optional_invite_by_uuid', 'invite_by_uuid')
is_order_sent = submitters_order == 'random' || index.zero?
@@ -41,6 +59,8 @@ module Submissions
preferences: preferences.merge(submission_preferences))
end
maybe_set_template_fields(submission, attrs[:submitters])
if submission.submitters.size > template.submitters.size
raise BaseError, 'Defined more signing parties than in template'
end
@@ -51,8 +71,20 @@ module Submissions
submission.tap(&:save!)
end
maybe_enqueue_expire_at(submissions)
submissions
end
# rubocop:enable Metrics
def maybe_enqueue_expire_at(submissions)
submissions.each do |submission|
next unless submission.expire_at?
ProcessSubmissionExpiredJob.perform_at(submission.expire_at, 'submission_id' => submission.id)
end
end
# rubocop:enable Metrics/BlockLength
def maybe_add_invite_submitters(submission, template)
template.submitters.each_with_index do |item, index|
@@ -82,8 +114,10 @@ module Submissions
def maybe_set_template_fields(submission, submitters_attrs, default_submitter_uuid: nil)
template_fields = (submission.template_fields || submission.template.fields).deep_dup
submitters = submission.template_submitters || submission.template.submitters
submitters_attrs.each_with_index do |submitter_attrs, index|
submitter_uuid = default_submitter_uuid || find_submitter_uuid(submission.template, submitter_attrs, index)
submitter_uuid = default_submitter_uuid || find_submitter_uuid(submitters, submitter_attrs, index)
process_readonly_fields_param(submitter_attrs[:readonly_fields], template_fields, submitter_uuid)
process_field_values_param(submitter_attrs[:values], template_fields, submitter_uuid)
@@ -100,6 +134,65 @@ module Submissions
submission
end
def merge_submitters_and_fields(submitter_attrs, template_submitters, template_fields)
selected_submitters = submitter_attrs[:roles].map do |role|
template_submitters.find { |e| e['name'].to_s.casecmp(role).zero? } ||
raise(BaseError, "#{role} role doesn't exist")
end
merge_role_uuids = selected_submitters.pluck('uuid')
old_role_uuids = template_submitters.pluck('uuid')
name = submitter_attrs[:role].presence || selected_submitters.pluck('name').join(' / ')
merged_submitter, template_submitters =
build_merged_submitter(template_submitters, role_uuids: merge_role_uuids, name:)
field_names_index = {}
sorted_fields = template_fields.sort_by { |e| old_role_uuids.index(e['submitter_uuid']) }
sorted_fields.each do |field|
next unless merge_role_uuids.include?(field['submitter_uuid'])
if (existing_field = field_names_index[field['name']])
existing_field['areas'] ||= []
existing_field['areas'].push(*field['areas'])
template_fields.delete(field)
else
field['submitter_uuid'] = merged_submitter['uuid']
field_names_index[field['name']] = field if field['name'].present?
end
end
[merged_submitter, template_submitters, template_fields]
end
def build_merged_submitter(submitters, role_uuids:, name:)
new_uuid = Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, role_uuids.sort.join(':'))
merged_submitter = nil
submitters =
submitters.filter_map do |submitter|
submitter['optional_invite_by_uuid'] = new_uuid if role_uuids.include?(submitter['optional_invite_by_uuid'])
submitter['invite_by_uuid'] = new_uuid if role_uuids.include?(submitter['invite_by_uuid'])
submitter['linked_to_uuid'] = new_uuid if role_uuids.include?(submitter['linked_to_uuid'])
if role_uuids.include?(submitter['uuid'])
next if merged_submitter
merged_submitter = submitter.deep_dup
merged_submitter['uuid'] = new_uuid
merged_submitter['name'] = name
merged_submitter.delete('linked_to_uuid')
end
submitter
end
[merged_submitter, submitters]
end
def process_readonly_fields_param(readonly_fields, template_fields, submitter_uuid)
return if readonly_fields.blank?
@@ -178,11 +271,11 @@ module Submissions
field
end
def find_submitter_uuid(template, attrs, index)
def find_submitter_uuid(submitters, attrs, index)
uuid = attrs[:uuid].presence
uuid ||= template.submitters.find { |e| e['name'].to_s.casecmp(attrs[:role].to_s).zero? }&.dig('uuid')
uuid ||= submitters.find { |e| e['name'].to_s.casecmp(attrs[:role].to_s).zero? }&.dig('uuid')
uuid || template.submitters[index]&.dig('uuid')
uuid || submitters[index]&.dig('uuid')
end
def build_submitter(submission:, attrs:, uuid:, is_order_sent:, user:, preferences:, params:)
@@ -225,9 +318,15 @@ module Submissions
def assign_completed_attributes(submitter)
submitter.values = Submitters::SubmitValues.merge_default_values(submitter)
submitter.values = Submitters::SubmitValues.merge_formula_values(submitter)
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
formula_values = Submitters::SubmitValues.build_formula_values(submitter)
if formula_values.present?
submitter.values = submitter.values.merge(formula_values)
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
end
submitter.values = submitter.values.transform_values do |v|
v == '{{date}}' ? Time.current.in_time_zone(submitter.submission.account.timezone).to_date.to_s : v
end
@@ -7,11 +7,15 @@ module Submissions
CHECK_COMPLETE_TIMEOUT = 90.seconds
WaitForCompleteTimeout = Class.new(StandardError)
NotCompletedYet = Class.new(StandardError)
module_function
def call(submitter)
return [] unless submitter
raise NotCompletedYet unless submitter.completed_at?
return submitter.documents if ApplicationRecord.uncached { submitter.document_generation_events.complete.exists? }
events =
+5 -4
View File
@@ -359,13 +359,14 @@ module Submissions
value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), account.locale)
end
if field['type'] == 'number'
value = NumberUtils.format_number(value,
field.dig('preferences', 'format'))
end
value = NumberUtils.format_number(value, field.dig('preferences', 'format')) if field['type'] == 'number'
value = value.join(', ') if value.is_a?(Array)
if (mask = field.dig('preferences', 'mask').presence)
value = TextUtils.mask_value(value, mask)
end
composer.formatted_text_box([{ text: TextUtils.maybe_rtl_reverse(value.to_s.presence || 'n/a') }],
text_align: value.to_s.match?(RTL_REGEXP) ? :right : :left,
padding: [0, 0, 10, 0])
@@ -23,15 +23,9 @@ module Submissions
**Submissions::GenerateResultAttachments.build_signing_params(submitter, pkcs, tsa_url)
}
begin
pdf.sign(io, **sign_params)
rescue HexaPDF::MalformedPDFError => e
Rollbar.error(e) if defined?(Rollbar)
pdf.sign(io, write_options: { incremental: false }, **sign_params)
end
sign_pdf(io, pdf, sign_params)
else
pdf.write(io, incremental: true, validate: false)
pdf.write(io, incremental: true, validate: true)
end
Submissions::GenerateResultAttachments.maybe_enable_ltv(io, sign_params)
@@ -45,6 +39,20 @@ module Submissions
)
end
def sign_pdf(io, pdf, sign_params)
pdf.sign(io, **sign_params)
rescue HexaPDF::MalformedPDFError => e
Rollbar.error(e) if defined?(Rollbar)
pdf.sign(io, write_options: { incremental: false }, **sign_params)
rescue HexaPDF::Error => e
Rollbar.error(e) if defined?(Rollbar)
pdf.validate(auto_correct: true)
pdf.sign(io, write_options: { validate: false }, **sign_params)
end
def build_combined_pdf(submitter)
pdfs_index = Submissions::GenerateResultAttachments.generate_pdfs(submitter)
+42 -6
View File
@@ -4,12 +4,29 @@ module Submissions
module GenerateResultAttachments
FONT_SIZE = 11
FONT_PATH = '/fonts/GoNotoKurrent-Regular.ttf'
FONT_BOLD_PATH = '/fonts/GoNotoKurrent-Bold.ttf'
FONT_NAME = if File.exist?(FONT_PATH)
FONT_PATH
else
'Helvetica'
end
FONT_BOLD_NAME = if File.exist?(FONT_BOLD_PATH)
FONT_BOLD_PATH
else
'Helvetica'
end
FONT_ITALIC_NAME = 'Helvetica'
FONT_BOLD_ITALIC_NAME = 'Helvetica'
FONT_VARIANS = {
none: FONT_NAME,
bold: FONT_BOLD_NAME,
italic: FONT_ITALIC_NAME,
bold_italic: FONT_BOLD_ITALIC_NAME
}.freeze
SIGN_REASON = 'Signed by %<name>s with DocuSeal.com'
RTL_REGEXP = TextUtils::RTL_REGEXP
@@ -18,12 +35,15 @@ module Submissions
TEXT_TOP_MARGIN = 1
MAX_PAGE_ROTATE = 20
COURIER_FONT = 'Courier'
A4_SIZE = [595, 842].freeze
TESTING_FOOTER = 'Testing Document - NOT LEGALLY BINDING'
DEFAULT_FONTS = %w[Times Helvetica Courier].freeze
FONTS_LINE_HEIGHT = {
'Times' => 1.4,
'Helvetica' => 1.4,
'Courier' => 1.6
}.freeze
MISSING_GLYPH_REPLACE = {
'▪' => '-',
@@ -192,8 +212,16 @@ module Submissions
fill_color = field.dig('preferences', 'color').presence
font_name = field.dig('preferences', 'font')
font_variant = (field.dig('preferences', 'font_type').presence || 'none').to_sym
font_name = FONT_NAME unless font_name.in?(DEFAULT_FONTS)
font = pdf.fonts.add(font_name)
if font_variant != :none && font_name == FONT_NAME
font_name = FONT_VARIANS[font_variant]
font_variant = nil unless font_name.in?(DEFAULT_FONTS)
end
font = pdf.fonts.add(font_name, variant: font_variant)
value = submitter.values[field['uuid']]
value = field['default_value'] if field['type'] == 'heading'
@@ -391,6 +419,10 @@ module Submissions
when ->(type) { type == 'cells' && !area['cell_w'].to_f.zero? }
cell_width = area['cell_w'] * width
if (mask = field.dig('preferences', 'mask').presence)
value = TextUtils.mask_value(value, mask)
end
chars = TextUtils.maybe_rtl_reverse(value).chars
chars = chars.reverse if field.dig('preferences', 'align') == 'right'
@@ -440,8 +472,12 @@ module Submissions
value = TextUtils.maybe_rtl_reverse(Array.wrap(value).join(', '))
if (mask = field.dig('preferences', 'mask').presence)
value = TextUtils.mask_value(value, mask)
end
text_params = { font:, fill_color:, font_size: }
text_params[:line_height] = text_params[:font_size] * 1.6 if font_name == COURIER_FONT
text_params[:line_height] = text_params[:font_size] * (FONTS_LINE_HEIGHT[font_name] || 1)
text = HexaPDF::Layout::TextFragment.create(value, **text_params)
@@ -450,7 +486,7 @@ module Submissions
if preferences_font_size.blank? && box_height > (area['h'] * height) + 1
text_params[:font_size] = (font_size / 1.4).to_i
text_params[:line_height] = text_params[:font_size] * 1.6 if font_name == COURIER_FONT
text_params[:line_height] = text_params[:font_size] * (FONTS_LINE_HEIGHT[font_name] || 1)
text = HexaPDF::Layout::TextFragment.create(value, **text_params)
@@ -461,7 +497,7 @@ module Submissions
if preferences_font_size.blank? && box_height > (area['h'] * height) + 1
text_params[:font_size] = (font_size / 1.9).to_i
text_params[:line_height] = text_params[:font_size] * 1.6 if font_name == COURIER_FONT
text_params[:line_height] = text_params[:font_size] * (FONTS_LINE_HEIGHT[font_name] || 1)
text = HexaPDF::Layout::TextFragment.create(value, **text_params)
+5 -2
View File
@@ -19,7 +19,9 @@ module Submissions
end
def normalize_submitter_params!(submitter_params, template, index = nil, for_submitter: nil)
default_values = submitter_params[:values] || {}
with_values = submitter_params[:values].present?
default_values = with_values ? submitter_params[:values] : {}
submitter_params[:fields]&.each do |f|
default_values[f[:name].presence || f[:uuid]] = f[:default_value] if f.key?(:default_value)
@@ -33,8 +35,9 @@ module Submissions
default_values,
submitter_name: submitter_params[:role] ||
template.submitters.dig(index, 'name'),
role_names: submitter_params[:roles],
for_submitter:,
throw_errors: true)
throw_errors: !with_values)
submitter_params[:values] = values
+3
View File
@@ -8,6 +8,7 @@ module Submitters
AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
AccountConfig::WITH_SIGNATURE_ID,
AccountConfig::ALLOW_TO_DECLINE_KEY,
AccountConfig::ENFORCE_SIGNING_ORDER_KEY,
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
AccountConfig::REUSE_SIGNATURE_KEY,
AccountConfig::ALLOW_TYPED_SIGNATURE,
@@ -27,6 +28,7 @@ module Submitters
with_decline = find_safe_value(configs, AccountConfig::ALLOW_TO_DECLINE_KEY) != false
with_signature_id = find_safe_value(configs, AccountConfig::WITH_SIGNATURE_ID) == true
require_signing_reason = find_safe_value(configs, AccountConfig::REQUIRE_SIGNING_REASON_KEY) == true
enforce_signing_order = find_safe_value(configs, AccountConfig::ENFORCE_SIGNING_ORDER_KEY) == true
policy_links = find_safe_value(configs, AccountConfig::POLICY_LINKS_KEY)
attrs = {
@@ -36,6 +38,7 @@ module Submitters
reuse_signature:,
with_decline:,
policy_links:,
enforce_signing_order:,
completed_message:,
require_signing_reason:,
prefill_signature:,
+45 -22
View File
@@ -17,38 +17,47 @@ module Submitters
module_function
def call(template, values, submitter_name: nil, for_submitter: nil, throw_errors: false)
fields = fetch_fields(template, submitter_name:, for_submitter:)
def call(template, values, submitter_name: nil, role_names: nil, for_submitter: nil, throw_errors: false)
fields =
if role_names.present?
fetch_roles_fields(template, roles: role_names)
else
fetch_fields(template, submitter_name:, for_submitter:)
end
fields_uuid_index = fields.index_by { |e| e['uuid'] }
fields_name_index = build_fields_index(fields)
attachments = []
normalized_values = values.to_h.filter_map do |key, value|
if fields_uuid_index[key].blank?
original_key = key
key = fields_name_index[key]&.dig('uuid') || fields_name_index[key.to_s.downcase]&.dig('uuid')
raise(UnknownFieldName, "Unknown field: #{original_key}") if key.blank? && throw_errors
end
normalized_values = values.to_h.each_with_object({}) do |(key, value), acc|
next if key.blank?
field = fields_uuid_index[key]
uuid_field = fields_uuid_index[key]
if field['type'].in?(%w[initials signature image file stamp]) && value.present?
new_value, new_attachments =
normalize_attachment_value(value, field, template.account, attachments, for_submitter)
value_fields = [uuid_field] if uuid_field
attachments.push(*new_attachments)
if value_fields.blank?
value_fields = fields_name_index[key].presence || fields_name_index[key.to_s.downcase]
value = new_value
raise(UnknownFieldName, "Unknown field: #{key}") if value_fields.blank? && throw_errors
end
[key, normalize_value(field, value)]
end.to_h
next if value_fields.blank?
value_fields.each do |field|
if field['type'].in?(%w[initials signature image file stamp]) && value.present?
new_value, new_attachments =
normalize_attachment_value(value, field, template.account, attachments, for_submitter)
attachments.push(*new_attachments)
value = new_value
end
acc[field['uuid']] = normalize_value(field, value)
end
end
[normalized_values, attachments]
end
@@ -103,10 +112,24 @@ module Submitters
end
end
def fetch_roles_fields(template, roles:)
submitters = roles.map do |submitter_name|
template.submitters.find { |e| e['name'] == submitter_name } ||
raise(UnknownSubmitterName,
"Unknown submitter role: #{submitter_name}. Template defines #{template.submitters.pluck('name')}")
end
role_uuids = submitters.pluck('uuid')
template.fields.select do |e|
role_uuids.include?(e['submitter_uuid'])
end
end
def build_fields_index(fields)
fields.index_by { |e| e['name'] }
.merge(fields.index_by { |e| e['name'].to_s.parameterize.underscore })
.merge(fields.index_by { |e| e['name'].to_s.downcase })
fields.group_by { |e| e['name'] }
.merge(fields.group_by { |e| e['name'].to_s.parameterize.underscore })
.merge(fields.group_by { |e| e['name'].to_s.downcase })
end
def normalize_attachment_value(value, field, account, attachments, for_submitter = nil)
+78 -27
View File
@@ -3,12 +3,14 @@
module Submitters
module SubmitValues
ValidationError = Class.new(StandardError)
RequiredFieldError = Class.new(StandardError)
VARIABLE_REGEXP = /\{\{?(\w+)\}\}?/
NONEDITABLE_FIELD_TYPES = %w[stamp heading].freeze
module_function
def call(submitter, params, request)
def call(submitter, params, request, validate_required: true)
Submissions.update_template_fields!(submitter.submission) if submitter.submission.template_fields.blank?
unless submitter.submission_events.exists?(event_type: 'start_form')
@@ -20,7 +22,7 @@ module Submitters
end
end
update_submitter!(submitter, params, request)
update_submitter!(submitter, params, request, validate_required:)
submitter.submission.save!
@@ -29,13 +31,13 @@ module Submitters
submitter
end
def update_submitter!(submitter, params, request)
def update_submitter!(submitter, params, request, validate_required: true)
values = normalized_values(params)
submitter.values.merge!(values)
submitter.opened_at ||= Time.current
assign_completed_attributes(submitter, request) if params[:completed] == 'true'
assign_completed_attributes(submitter, request, validate_required:) if params[:completed] == 'true'
ApplicationRecord.transaction do
maybe_set_signature_reason!(values, submitter, params)
@@ -49,17 +51,36 @@ module Submitters
submitter
end
def assign_completed_attributes(submitter, request)
def assign_completed_attributes(submitter, request, validate_required: true)
submitter.completed_at = Time.current
submitter.ip = request.remote_ip
submitter.ua = request.user_agent
submitter.values = merge_default_values(submitter)
submitter.values = maybe_remove_condition_values(submitter)
submitter.values = merge_formula_values(submitter)
required_field_uuids_acc = Set.new
submitter.values = maybe_remove_condition_values(submitter, required_field_uuids_acc:)
formula_values = build_formula_values(submitter)
if formula_values.present?
submitter.values = submitter.values.merge(formula_values)
submitter.values = maybe_remove_condition_values(submitter, required_field_uuids_acc:)
end
submitter.values = submitter.values.transform_values do |v|
v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v
end
required_field_uuids_acc.each do |uuid|
next if submitter.values[uuid].present?
raise RequiredFieldError, uuid if validate_required
Rollbar.warning("Required field #{submitter.id}: #{uuid}") if defined?(Rollbar)
end
submitter
end
@@ -149,7 +170,7 @@ module Submitters
default_values.compact_blank.merge(submitter.values)
end
def merge_formula_values(submitter)
def build_formula_values(submitter)
computed_values = submitter.submission.template_fields.each_with_object({}) do |field, acc|
next if field['submitter_uuid'] != submitter.uuid
next if field['type'] == 'payment'
@@ -161,7 +182,7 @@ module Submitters
acc[field['uuid']] = calculate_formula_value(formula, submitter.values.merge(acc.compact_blank))
end
submitter.values.merge(computed_values.compact_blank)
computed_values.compact_blank
end
def calculate_formula_value(_formula, _values)
@@ -180,37 +201,67 @@ module Submitters
with_time:)
end
def maybe_remove_condition_values(submitter)
fields_uuid_index = submitter.submission.template_fields.index_by { |e| e['uuid'] }
def maybe_remove_condition_values(submitter, required_field_uuids_acc: nil)
submission = submitter.submission
fields_uuid_index = submission.template_fields.index_by { |e| e['uuid'] }
submitters_values = nil
has_other_submitters = submission.template_submitters.size > 1
has_document_conditions =
(submission.template_schema || submission.template.schema).any? { |e| e['conditions'].present? }
attachments_index =
Submissions.filtered_conditions_schema(submitter.submission).index_by { |i| i['attachment_uuid'] }
submitter_values = nil
is_other_submitter_conditions = submitter.submission.template_submitters.size > 1
submitter.submission.template_fields.each do |field|
next if field['submitter_uuid'] != submitter.uuid
submitter_values ||= submitter.values
is_other_submitter_conditions &&= field_conditions_other_submitter?(submitter, field, fields_uuid_index)
if is_other_submitter_conditions
submitter_values = submitter.submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) }
if has_document_conditions
Submissions.filtered_conditions_schema(submission).index_by { |i| i['attachment_uuid'] }
end
submitter.values.delete(field['uuid']) unless check_field_conditions(submitter_values, field, fields_uuid_index)
submission.template_fields.each do |field|
next if field['submitter_uuid'] != submitter.uuid
if field['areas'].present? && field['areas'].none? { |area| attachments_index[area['attachment_uuid']] }
required_field_uuids_acc.add(field['uuid']) if required_field_uuids_acc && required_editable_field?(field)
if has_document_conditions && !check_field_areas_attachments(field, attachments_index)
submitter.values.delete(field['uuid'])
required_field_uuids_acc.delete(field['uuid'])
end
if has_other_submitters && !submitters_values &&
field_conditions_other_submitter?(submitter, field, fields_uuid_index)
submitters_values = merge_submitters_values(submitter)
end
unless check_field_conditions(submitters_values || submitter.values, field, fields_uuid_index)
submitter.values.delete(field['uuid'])
required_field_uuids_acc.delete(field['uuid'])
end
end
submitter.values
end
def required_editable_field?(field)
return false if NONEDITABLE_FIELD_TYPES.include?(field['type'])
field['required'].present? && field['readonly'].blank?
end
def check_field_areas_attachments(field, attachments_index)
return true if field['areas'].blank?
field['areas'].any? { |area| attachments_index[area['attachment_uuid']] }
end
def merge_submitters_values(submitter)
submitter.submission.submitters
.reduce({}) { |acc, sub| acc.merge(sub.values) }
.merge(submitter.values)
end
def field_conditions_other_submitter?(submitter, field, fields_uuid_index)
return false if field['conditions'].blank?
field['conditions'].to_a.any? do |c|
fields_uuid_index.dig(c['field_uuid'], 'submitter_uuid') != submitter.uuid
end
+10
View File
@@ -0,0 +1,10 @@
# frozen_string_literal: true
# This rake task was added by annotate_rb gem.
# Can set `ANNOTATERB_SKIP_ON_DB_TASKS` to be anything to skip this
if Rails.env.development? && ENV['ANNOTATERB_SKIP_ON_DB_TASKS'].nil?
require 'annotate_rb'
AnnotateRb::Core.load_rake_tasks
end
-57
View File
@@ -1,57 +0,0 @@
# frozen_string_literal: true
if Rails.env.development?
require 'annotate'
task set_annotation_options: :environment do
Annotate.set_defaults(
'active_admin' => 'false',
'additional_file_patterns' => [],
'routes' => 'false',
'models' => 'true',
'position_in_routes' => 'before',
'position_in_class' => 'before',
'position_in_test' => 'before',
'position_in_fixture' => 'before',
'position_in_factory' => 'before',
'position_in_serializer' => 'before',
'show_foreign_keys' => 'true',
'show_complete_foreign_keys' => 'false',
'show_indexes' => 'true',
'simple_indexes' => 'false',
'model_dir' => 'app/models',
'root_dir' => '',
'include_version' => 'false',
'require' => '',
'exclude_tests' => 'true',
'exclude_fixtures' => 'true',
'exclude_factories' => 'true',
'exclude_serializers' => 'false',
'exclude_scaffolds' => 'true',
'exclude_controllers' => 'true',
'exclude_helpers' => 'true',
'exclude_sti_subclasses' => 'false',
'ignore_model_sub_dir' => 'false',
'ignore_columns' => nil,
'ignore_routes' => nil,
'ignore_unknown_models' => 'false',
'hide_limit_column_types' => 'integer,bigint,boolean',
'hide_default_column_types' => 'json,jsonb,hstore',
'skip_on_db_migrate' => 'false',
'format_bare' => 'true',
'format_rdoc' => 'false',
'format_yard' => 'false',
'format_markdown' => 'false',
'sort' => 'false',
'force' => 'false',
'frozen' => 'false',
'classified_sort' => 'true',
'trace' => 'false',
'wrapper_open' => nil,
'wrapper_close' => nil,
'with_comment' => 'true'
)
end
Annotate.load_tasks
end
+9 -5
View File
@@ -9,7 +9,6 @@ module Templates
template.external_id = external_id
template.author = author
template.preferences = original_template.preferences.deep_dup
template.name = name.presence || "#{original_template.name} (#{I18n.t('clone')})"
if folder_name.present?
@@ -18,10 +17,11 @@ module Templates
template.folder_id = original_template.folder_id
end
template.submitters, template.fields, template.schema =
template.submitters, template.fields, template.schema, template.preferences =
update_submitters_and_fields_and_schema(original_template.submitters.deep_dup,
original_template.fields.deep_dup,
original_template.schema.deep_dup)
original_template.schema.deep_dup,
original_template.preferences.deep_dup)
if name.present? && template.schema.size == 1 &&
original_template.schema.first['name'] == original_template.name &&
@@ -33,7 +33,7 @@ module Templates
end
# rubocop:disable Metrics, Style/CombinableLoops
def update_submitters_and_fields_and_schema(cloned_submitters, cloned_fields, cloned_schema)
def update_submitters_and_fields_and_schema(cloned_submitters, cloned_fields, cloned_schema, cloned_preferences)
submitter_uuids_replacements = {}
field_uuids_replacements = {}
@@ -58,6 +58,10 @@ module Templates
end
end
cloned_preferences['submitters'].to_a.each do |submitter|
submitter['uuid'] = submitter_uuids_replacements[submitter['uuid']]
end
cloned_fields.each do |field|
new_field_uuid = SecureRandom.uuid
@@ -88,7 +92,7 @@ module Templates
end
end
[cloned_submitters, cloned_fields, cloned_schema]
[cloned_submitters, cloned_fields, cloned_schema, cloned_preferences]
end
# rubocop:enable Metrics, Style/CombinableLoops
end
+26 -10
View File
@@ -21,6 +21,8 @@ module Templates
Escolher
)\b/ix
DATE_FORMAT_REGEXP = %r{[myd]{2,4}[-\\/\s.][myd]{2,4}[-\\/\s.][myd]{2,4}}i
FIELD_ALIGNMENT = {
0 => 'left',
1 => 'center',
@@ -30,8 +32,8 @@ module Templates
module_function
# rubocop:disable Metrics
def call(pdf, attachment)
return [] unless pdf.acro_form
def call(pdf, attachment, data)
return [] if pdf.acro_form.blank? && data.exclude?('/Form')
fields, annots_index = build_fields_with_pages(pdf)
@@ -101,7 +103,7 @@ module Templates
{
uuid: SecureRandom.uuid,
required: false,
required: field.flags.include?(:required),
preferences: {},
areas:,
**field_properties
@@ -147,7 +149,7 @@ module Templates
options: build_options(field[:Opt], 'radio'),
default_value: selected_option
}
elsif field.field_type == :Btn && field.concrete_field_type == :check_box &&
elsif field.field_type == :Btn && %i[check_box radio_button].include?(field.concrete_field_type) &&
field[:Kids].present? && field[:Kids].size > 1 && field.allowed_values.size > 1
selected_option = (field.allowed_values || []).find { |v| v == field.field_value }
@@ -187,15 +189,29 @@ module Templates
default_value: field.field_value
}
elsif field.field_type == :Tx
{
**attrs,
type: 'text',
default_value: field.field_value
}
if field[:AA] && ((field[:AA][:F] && field[:AA][:F][:JS].include?('AFDate_')) ||
(field[:AA][:K] && field[:AA][:F][:JS].include?('AFDate_')))
if (format = field[:AA][:F][:JS][DATE_FORMAT_REGEXP])
attrs[:preferences] ||= {}
attrs[:preferences][:format] = format.upcase
end
{
**attrs,
type: 'date',
default_value: field.field_value
}
else
{
**attrs,
type: 'text',
default_value: field.field_value
}
end
elsif field.field_type == :Sig
{
**attrs,
type: 'signature'
type: field.try(:field_name).to_s.downcase.include?('initials') ? 'initials' : 'signature'
}
else
{}

Some files were not shown because too many files have changed in this diff Show More