Compare commits

...
95 Commits
Author SHA1 Message Date
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
Alex Turchyn 1b52a99662 Merge from docusealco/wip 2025-02-17 22:55:09 +02:00
Pete Matsyburka f5facfa6b9 use ubuntu latest build 2025-02-17 22:49:14 +02:00
Pete Matsyburka 21c6144dc6 fix document controller 2025-02-17 18:45:52 +02:00
Pete Matsyburka 2c155f0e42 use ip to reopen 2025-02-17 12:46:44 +02:00
Alex Turchyn 33cc0a8ab5 Merge from docusealco/wip 2025-02-17 12:36:04 +02:00
Pete Matsyburka 1e65a7e52b fix combined 2025-02-14 22:28:59 +02:00
Pete Matsyburka 14e2dbe1aa detailed party email form 2025-02-14 18:12:15 +02:00
Pete Matsyburka a823a6ac56 fix template clone 2025-02-14 18:12:09 +02:00
Pete Matsyburka cfc4e001ba refactor annots 2025-02-14 12:33:49 +02:00
Pete Matsyburka 496cc855f6 refactor 2025-02-14 11:00:12 +02:00
Pete Matsyburka bfb2db07a5 fix events modal 2025-02-13 16:46:57 +02:00
Pete Matsyburka 9f4f868246 use erb_lint 2025-02-13 13:19:33 +02:00
Pete Matsyburka f91f02f786 remove seats info 2025-02-13 00:10:43 +02:00
Pete Matsyburka 1da72e3e7a update gem 2025-02-12 21:31:56 +02:00
Pete Matsyburka 2e2c0e087d skip missing font 2025-02-12 20:38:46 +02:00
Pete Matsyburka 0c72f58ab0 fix combined 2025-02-12 20:38:46 +02:00
Alex Turchyn d9d969cb79 add translations 2025-02-12 14:31:49 +02:00
Pete Matsyburka 8d685d1f73 adjust event log 2025-02-11 22:37:47 +02:00
Alex Turchyn e406620044 add 'Event Log' modal 2025-02-11 22:23:51 +02:00
Pete Matsyburka a315674421 update gem 2025-02-11 11:18:15 +02:00
Alex Turchyn 8d953f210f add attachment count attribute to template factory 2025-02-11 11:16:32 +02:00
Alex Turchyn 7439ef07ad Merge from docusealco/wip 2025-02-10 13:08:17 +02:00
Pete Matsyburka fc4b4de6ed customize template email per party 2025-02-09 14:13:04 +02:00
Alex Turchyn 96e4733f87 add submissions filtering by 'sent' and 'opened' statuses 2025-02-07 20:20:37 +02:00
Pete Matsyburka 8b6731a289 show logo 2025-02-07 14:48:04 +02:00
Pete Matsyburka f93ac68e9c adjust field drag 2025-02-07 14:39:23 +02:00
Alex Turchyn 439df4879c add stamp text logo 2025-02-07 14:15:35 +02:00
Alex Turchyn 83b6ce8574 fix logos 2025-02-07 14:15:18 +02:00
Pete Matsyburka 6ea07bc13b show minimize on mobile if with description 2025-02-06 13:49:09 +02:00
Alex Turchyn a15624690b show unauthorized message in test environment 2025-02-06 13:41:16 +02:00
Pete Matsyburka 974bb85fb7 rescue malformed PDF 2025-02-06 12:55:27 +02:00
Pete Matsyburka 5dc80601b2 courier font 2025-02-05 18:17:55 +02:00
Pete Matsyburka df2e53bc40 font mono 2025-02-05 16:13:31 +02:00
115 changed files with 2081 additions and 736 deletions

No files matched your search

+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:
- ''
View File
File renamed without changes.
+28 -1
View File
@@ -49,7 +49,7 @@ jobs:
bundle config path vendor/bundle bundle config path vendor/bundle
bundle install --jobs 4 --retry 4 bundle install --jobs 4 --retry 4
- name: Run Erblint - name: Run Erblint
run: bundle exec erblint ./app run: bundle exec erb_lint ./app
eslint: eslint:
name: ESLint name: ESLint
@@ -77,6 +77,33 @@ jobs:
run: | run: |
./node_modules/eslint/bin/eslint.js "app/javascript/**/*.js" ./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: rspec:
name: RSpec name: RSpec
runs-on: ubuntu-latest runs-on: ubuntu-latest
+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 WORKDIR /fonts
@@ -6,7 +6,7 @@ RUN apk --no-cache add fontforge wget && wget https://github.com/satbyy/go-noto-
RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")' RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")'
FROM ruby:3.4.1-alpine as webpack FROM ruby:3.4.1-alpine AS webpack
ENV RAILS_ENV=production ENV RAILS_ENV=production
ENV NODE_ENV=production ENV NODE_ENV=production
@@ -32,7 +32,7 @@ COPY ./app/views ./app/views
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
FROM ruby:3.4.1-alpine as app FROM ruby:3.4.1-alpine AS app
ENV RAILS_ENV=production ENV RAILS_ENV=production
ENV BUNDLE_WITHOUT="development:test" ENV BUNDLE_WITHOUT="development:test"
+3 -1
View File
@@ -46,7 +46,6 @@ gem 'twitter_cldr', require: false
gem 'tzinfo-data' gem 'tzinfo-data'
group :development, :test do group :development, :test do
gem 'annotate'
gem 'better_html' gem 'better_html'
gem 'bullet' gem 'bullet'
gem 'debug' gem 'debug'
@@ -63,6 +62,9 @@ group :development, :test do
end end
group :development do group :development do
gem 'annotaterb'
gem 'brakeman', require: false
gem 'foreman', require: false
gem 'letter_opener_web' gem 'letter_opener_web'
gem 'web-console' gem 'web-console'
end end
+16 -13
View File
@@ -74,9 +74,7 @@ GEM
uri (>= 0.13.1) uri (>= 0.13.1)
addressable (2.8.7) addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0) public_suffix (>= 2.0.2, < 7.0)
annotate (2.6.5) annotaterb (4.14.0)
activerecord (>= 2.3.0)
rake (>= 0.8.7)
arabic-letter-connector (0.1.1) arabic-letter-connector (0.1.1)
ast (2.4.2) ast (2.4.2)
aws-eventstream (1.3.0) aws-eventstream (1.3.0)
@@ -120,6 +118,8 @@ GEM
bindex (0.8.1) bindex (0.8.1)
bootsnap (1.18.4) bootsnap (1.18.4)
msgpack (~> 1.2) msgpack (~> 1.2)
brakeman (7.0.0)
racc
builder (3.3.0) builder (3.3.0)
bullet (8.0.0) bullet (8.0.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
@@ -229,6 +229,7 @@ GEM
ffi (1.17.1-arm64-darwin) ffi (1.17.1-arm64-darwin)
ffi (1.17.1-x86_64-linux-gnu) ffi (1.17.1-x86_64-linux-gnu)
ffi (1.17.1-x86_64-linux-musl) ffi (1.17.1-x86_64-linux-musl)
foreman (0.88.1)
geom2d (0.4.1) geom2d (0.4.1)
globalid (1.2.1) globalid (1.2.1)
activesupport (>= 6.1) activesupport (>= 6.1)
@@ -327,7 +328,7 @@ GEM
mysql2 (0.5.6) mysql2 (0.5.6)
net-http-persistent (4.0.5) net-http-persistent (4.0.5)
connection_pool (~> 2.2) connection_pool (~> 2.2)
net-imap (0.5.3) net-imap (0.5.6)
date date
net-protocol net-protocol
net-pop (0.1.2) net-pop (0.1.2)
@@ -337,18 +338,18 @@ GEM
net-smtp (0.5.0) net-smtp (0.5.0)
net-protocol net-protocol
nio4r (2.7.4) nio4r (2.7.4)
nokogiri (1.18.2) nokogiri (1.18.5)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.2-aarch64-linux-gnu) nokogiri (1.18.5-aarch64-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.2-aarch64-linux-musl) nokogiri (1.18.5-aarch64-linux-musl)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.2-arm64-darwin) nokogiri (1.18.5-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.2-x86_64-linux-gnu) nokogiri (1.18.5-x86_64-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.2-x86_64-linux-musl) nokogiri (1.18.5-x86_64-linux-musl)
racc (~> 1.4) racc (~> 1.4)
oj (3.16.8) oj (3.16.8)
bigdecimal (>= 3.0) bigdecimal (>= 3.0)
@@ -386,7 +387,7 @@ GEM
puma (6.5.0) puma (6.5.0)
nio4r (~> 2.0) nio4r (~> 2.0)
racc (1.8.1) racc (1.8.1)
rack (3.1.8) rack (3.1.12)
rack-proxy (0.7.7) rack-proxy (0.7.7)
rack rack
rack-session (2.0.0) rack-session (2.0.0)
@@ -558,7 +559,7 @@ GEM
unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4) unicode-emoji (4.0.4)
uniform_notifier (1.16.0) uniform_notifier (1.16.0)
uri (1.0.2) uri (1.0.3)
useragent (0.16.11) useragent (0.16.11)
warden (1.2.9) warden (1.2.9)
rack (>= 2.0.9) rack (>= 2.0.9)
@@ -588,13 +589,14 @@ PLATFORMS
x86_64-linux-musl x86_64-linux-musl
DEPENDENCIES DEPENDENCIES
annotate annotaterb
arabic-letter-connector arabic-letter-connector
aws-sdk-s3 aws-sdk-s3
aws-sdk-secretsmanager aws-sdk-secretsmanager
azure-storage-blob azure-storage-blob
better_html better_html
bootsnap bootsnap
brakeman
bullet bullet
cancancan cancancan
capybara capybara
@@ -610,6 +612,7 @@ DEPENDENCIES
faker faker
faraday faraday
faraday-follow_redirects faraday-follow_redirects
foreman
google-cloud-storage google-cloud-storage
hexapdf hexapdf
image_processing image_processing
@@ -39,7 +39,7 @@ class AccountConfigsController < ApplicationController
end end
def account_config_params 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]) attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
end end
end end
@@ -10,7 +10,7 @@ module Api
last_submitter = @submission.submitters.max_by(&:completed_at) last_submitter = @submission.submitters.max_by(&:completed_at)
if last_submitter.documents_attachments.blank? if last_submitter.documents_attachments.blank?
last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(submitter) last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(last_submitter)
end end
last_submitter.documents_attachments last_submitter.documents_attachments
@@ -179,7 +179,7 @@ module Api
message: %i[subject body], message: %i[subject body],
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role, submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last, :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, fields: [:name, :uuid, :default_value, :value, :title, :description,
:readonly, :validation_pattern, :invalid_message, :readonly, :validation_pattern, :invalid_message,
{ default_value: [], value: [], preferences: {} }] }]] { default_value: [], value: [], preferences: {} }] }]]
+7 -1
View File
@@ -146,9 +146,15 @@ module Api
if attrs[:completed] if attrs[:completed]
submitter.values = Submitters::SubmitValues.merge_default_values(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) 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| submitter.values = submitter.values.transform_values do |v|
v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v
end end
+2
View File
@@ -34,6 +34,8 @@ module Api
} }
end end
} }
rescue HexaPDF::MalformedPDFError
render json: { error: 'Malformed PDF' }, status: :unprocessable_entity
end end
end end
end end
+1 -1
View File
@@ -30,7 +30,7 @@ class ApplicationController < ActionController::Base
redirect_to request.referer, alert: 'Too many requests', status: :too_many_requests redirect_to request.referer, alert: 'Too many requests', status: :too_many_requests
end end
if Rails.env.production? if Rails.env.production? || Rails.env.test?
rescue_from CanCan::AccessDenied do |e| rescue_from CanCan::AccessDenied do |e|
Rollbar.warning(e) if defined?(Rollbar) Rollbar.warning(e) if defined?(Rollbar)
@@ -39,7 +39,7 @@ class NotificationsSettingsController < ApplicationController
end end
def email_config_params 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]) attrs[:key] = nil unless attrs[:key].in?([AccountConfig::BCC_EMAILS, AccountConfig::SUBMITTER_REMINDERS])
end end
end end
@@ -50,7 +50,7 @@ class PersonalizationSettingsController < ApplicationController
end end
def account_config_params 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) return attrs if attrs[:value].is_a?(String)
@@ -9,8 +9,6 @@ class SendSubmissionEmailController < ApplicationController
SEND_DURATION = 30.minutes SEND_DURATION = 30.minutes
def success; end
def create def create
@submitter = @submitter =
if params[:template_slug] if params[:template_slug]
@@ -31,7 +29,7 @@ class SendSubmissionEmailController < ApplicationController
end end
respond_to do |f| respond_to do |f|
f.html { redirect_to success_send_submission_email_index_path } f.html { render :success }
f.json { head :ok } f.json { head :ok }
end end
end end
+13 -2
View File
@@ -25,7 +25,7 @@ class StartFormController < ApplicationController
redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email]) redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email])
else else
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record? 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 return render :show
end end
@@ -34,6 +34,8 @@ class StartFormController < ApplicationController
assign_submission_attributes(@submitter, @template) assign_submission_attributes(@submitter, @template)
Submissions::AssignDefinedSubmitters.call(@submitter.submission) Submissions::AssignDefinedSubmitters.call(@submitter.submission)
else
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
end end
if @submitter.save if @submitter.save
@@ -65,8 +67,9 @@ class StartFormController < ApplicationController
.order(id: :desc) .order(id: :desc)
.where(declined_at: nil) .where(declined_at: nil)
.where(external_id: nil) .where(external_id: nil)
.where(ip: [nil, request.remote_ip])
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel } .then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
.find_or_initialize_by(**submitter_params.compact_blank) .find_or_initialize_by(email: submitter_params[:email], **submitter_params.compact_blank)
end end
def assign_submission_attributes(submitter, template) def assign_submission_attributes(submitter, template)
@@ -114,4 +117,12 @@ class StartFormController < ApplicationController
@template = Template.find_by!(slug:) @template = Template.find_by!(slug:)
end 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 end
@@ -0,0 +1,25 @@
# frozen_string_literal: true
class SubmissionEventsController < ApplicationController
SUBMISSION_EVENT_ICONS = {
'view_form' => 'eye',
'start_form' => 'player_play',
'complete_form' => 'check',
'send_email' => 'mail_forward',
'click_email' => 'hand_click',
'api_complete_form' => 'check',
'send_reminder_email' => 'mail_forward',
'send_2fa_sms' => '2fa',
'send_sms' => 'send',
'phone_verified' => 'phone_check',
'click_sms' => 'hand_click',
'decline_form' => 'x',
'start_verification' => 'player_play',
'complete_verification' => 'check',
'invite_party' => 'user_plus'
}.freeze
load_and_authorize_resource :submission
def index; end
end
@@ -20,7 +20,7 @@ class SubmissionsPreviewController < ApplicationController
@submission ||= Submission.find_by!(slug: params[:slug]) @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 && if !@submission.submitters.all?(&:completed_at?) && !signature_valid &&
(!current_user || !current_ability.can?(:read, @submission)) (!current_user || !current_ability.can?(:read, @submission))
@@ -40,6 +40,7 @@ class SubmissionsPreviewController < ApplicationController
def completed def completed
@submission = Submission.find_by!(slug: params[:submissions_preview_slug]) @submission = Submission.find_by!(slug: params[:submissions_preview_slug])
@template = @submission.template
render :completed, layout: 'form' render :completed, layout: 'form'
end end
@@ -6,9 +6,10 @@ class TemplatesDebugController < ApplicationController
def show def show
attachment = @template.documents.first 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'] ||= {}
attachment.metadata['pdf']['fields'] = fields attachment.metadata['pdf']['fields'] = fields
@@ -28,8 +28,10 @@ class TemplatesPreferencesController < ApplicationController
submitters_order submitters_order
completed_notification_email_subject completed_notification_email_body completed_notification_email_subject completed_notification_email_body
completed_notification_email_enabled completed_notification_email_attach_audit] + completed_notification_email_enabled completed_notification_email_attach_audit] +
[completed_message: %i[title body]] [completed_message: %i[title body],
submitters: [%i[uuid request_email_subject request_email_body]]]
).tap do |attrs| ).tap do |attrs|
attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1'
attrs[:preferences] = attrs[:preferences].transform_values do |value| attrs[:preferences] = attrs[:preferences].transform_values do |value|
if %w[true false].include?(value) if %w[true false].include?(value)
value == 'true' value == 'true'
+1 -1
View File
@@ -26,7 +26,7 @@ class UserConfigsController < ApplicationController
end end
def user_config_params 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]) attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
end end
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? 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 = 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? 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) authorize!(:manage, account)
@user.account = account @user.account = account
end 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') redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_updated')
else else
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity
@@ -84,8 +83,11 @@ class UsersController < ApplicationController
def user_params def user_params
if params.key?(:user) if params.key?(:user)
params.require(:user).permit(:email, :first_name, :last_name, :password, permitted_params = %i[email first_name last_name password archived_at]
:role, :archived_at, :account_id)
permitted_params << :role if role_valid?(params.dig(:user, :role))
params.require(:user).permit(permitted_params)
else else
{} {}
end end
@@ -3,6 +3,8 @@ export default class extends HTMLElement {
this.resize() this.resize()
this.textarea.addEventListener('input', () => this.resize()) this.textarea.addEventListener('input', () => this.resize())
this.observeVisibility()
} }
resize () { resize () {
@@ -11,6 +13,28 @@ export default class extends HTMLElement {
} }
} }
observeVisibility () {
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.resize()
this.observer.unobserve(this.textarea)
}
})
},
{
threshold: 0.1
}
)
this.observer.observe(this.textarea)
}
disconnectedCallback () {
this.observer.unobserve(this.textarea)
}
get textarea () { get textarea () {
return this.querySelector('textarea') return this.querySelector('textarea')
} }
+23 -7
View File
@@ -1,9 +1,9 @@
<template> <template>
<div <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" dir="auto"
:style="computedStyle" :style="computedStyle"
:class="{ 'font-serif': field.preferences?.font === 'Times', 'text-[1.6vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'field-area-active outline-red-500 outline-dashed outline-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }" :class="{ '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 <div
v-if="(!withFieldPlaceholder || !field.name || field.type === 'cells') && !isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid" v-if="(!withFieldPlaceholder || !field.name || field.type === 'cells') && !isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
@@ -168,7 +168,7 @@
<div <div
v-else-if="field.type === 'cells'" v-else-if="field.type === 'cells'"
class="w-full flex items-center" class="w-full flex items-center"
:class="{ 'justify-end': field.preferences?.align === 'right' }" :class="{ 'justify-end': field.preferences?.align === 'right', ...fontClasses }"
> >
<div <div
v-for="(char, index) in modelValue" v-for="(char, index) in modelValue"
@@ -184,7 +184,7 @@
ref="textContainer" ref="textContainer"
dir="auto" dir="auto"
class="flex items-center px-0.5 w-full" class="flex items-center px-0.5 w-full"
:class="alignClasses[field.preferences?.align]" :class="{ ...alignClasses, ...fontClasses }"
> >
<span <span
v-if="field && field.name && withFieldPlaceholder && !modelValue && modelValue !== 0" v-if="field && field.name && withFieldPlaceholder && !modelValue && modelValue !== 0"
@@ -326,10 +326,26 @@ export default {
} }
}, },
alignClasses () { alignClasses () {
if (!this.field.preferences) {
return {}
}
return { return {
center: 'text-center', 'text-center': this.field.preferences.align === 'center',
left: 'text-left', 'text-left': this.field.preferences.align === 'left',
right: 'text-right' '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 () { option () {
@@ -13,7 +13,7 @@
> >
<a <a
v-if="val" 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" :href="attachmentsIndex[val].url"
target="_blank" target="_blank"
> >
@@ -26,7 +26,10 @@
{{ attachmentsIndex[val].filename }} {{ attachmentsIndex[val].filename }}
</span> </span>
</a> </a>
<button @click.prevent="removeAttachment(val)"> <button
class="remove-attachment-button"
@click.prevent="removeAttachment(val)"
>
<IconTrashX <IconTrashX
:width="18" :width="18"
:heigh="19" :heigh="19"
@@ -44,7 +47,7 @@
<div <div
v-if="field.description && !modelValue.length" v-if="field.description && !modelValue.length"
dir="auto" dir="auto"
class="mb-3 px-1" class="mb-3 px-1 field-description-text"
> >
<MarkdownContent :string="field.description" /> <MarkdownContent :string="field.description" />
</div> </div>
+6 -6
View File
@@ -1,7 +1,7 @@
<template> <template>
<div <div
id="form_completed" 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" dir="auto"
> >
<div class="font-medium text-2xl flex items-center space-x-1.5 mx-auto"> <div class="font-medium text-2xl flex items-center space-x-1.5 mx-auto">
@@ -10,13 +10,13 @@
:width="30" :width="30"
:height="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')) }} {{ completedMessage.title || (hasSignatureFields ? (hasMultipleDocuments ? t('documents_have_been_signed') : t('document_has_been_signed')) : t('form_has_been_completed')) }}
</span> </span>
</div> </div>
<div <div
v-if="completedMessage.body" v-if="completedMessage.body"
class="mt-2" class="mt-2 completed-form-message-body"
> >
<MarkdownContent <MarkdownContent
:string="completedMessage.body" :string="completedMessage.body"
@@ -27,7 +27,7 @@
v-if="completedButton.url" v-if="completedButton.url"
:href="sanitizeHref(completedButton.url)" :href="sanitizeHref(completedButton.url)"
rel="noopener noreferrer nofollow" 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> <span>
{{ completedButton.title || 'Back to Website' }} {{ completedButton.title || 'Back to Website' }}
@@ -35,7 +35,7 @@
</a> </a>
<button <button
v-if="canSendEmail && !isDemo && withSendCopyButton" 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" :disabled="isSendingCopy"
@click.prevent="sendCopyToEmail" @click.prevent="sendCopyToEmail"
> >
@@ -50,7 +50,7 @@
</button> </button>
<button <button
v-if="!isWebView && withDownloadButton" 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" :disabled="isDownloading"
@click.prevent="download" @click.prevent="download"
> >
+3 -3
View File
@@ -7,7 +7,7 @@
<label <label
v-if="showFieldNames" v-if="showFieldNames"
:for="field.uuid" :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 <MarkdownContent
v-if="field.title" v-if="field.title"
@@ -26,7 +26,7 @@
</template> </template>
</label> </label>
<button <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')]" @click.prevent="[setCurrentDate(), $emit('focus')]"
> >
<IconCalendarCheck :width="16" /> <IconCalendarCheck :width="16" />
@@ -35,7 +35,7 @@
</div> </div>
<div <div
v-if="field.description" v-if="field.description"
class="mb-3 px-1" class="mb-3 px-1 field-description-text"
dir="auto" dir="auto"
> >
<MarkdownContent :string="field.description" /> <MarkdownContent :string="field.description" />
+1 -1
View File
@@ -7,7 +7,7 @@
> >
<label <label
:for="inputId" :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 }" :class="{ 'opacity-50': isLoading }"
> >
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center"> <div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
+27 -22
View File
@@ -67,7 +67,7 @@
<button <button
v-if="!isFormVisible" v-if="!isFormVisible"
id="expand_form_button" 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%" style="width: 96%; margin-left: 2%"
@click.prevent="[isFormVisible = true, scrollIntoField(currentField)]" @click.prevent="[isFormVisible = true, scrollIntoField(currentField)]"
> >
@@ -93,14 +93,15 @@
<div <div
v-show="isFormVisible" v-show="isFormVisible"
id="form_container" 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 }" :class="{ 'md:bottom-4': isBreakpointMd }"
:style="{ backgroundColor: backgroundColor }" :style="{ backgroundColor: backgroundColor }"
> >
<button <button
v-if="!isCompleted" v-if="!isCompleted"
id="minimize_form_button" id="minimize_form_button"
class="absolute right-0 mr-2 mt-2 top-0 hidden md:block" 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')" :title="t('minimize')"
@click.prevent="minimizeForm" @click.prevent="minimizeForm"
> >
@@ -118,7 +119,7 @@
ref="form" ref="form"
:action="submitPath" :action="submitPath"
method="post" method="post"
class="mx-auto" class="mx-auto steps-form"
:style="{ maxWidth: isBreakpointMd ? '582px' : '' }" :style="{ maxWidth: isBreakpointMd ? '582px' : '' }"
@submit.prevent="submitStep" @submit.prevent="submitStep"
> >
@@ -164,7 +165,7 @@
v-if="showFieldNames && (currentField.name || currentField.title)" v-if="showFieldNames && (currentField.name || currentField.title)"
:for="currentField.uuid" :for="currentField.uuid"
dir="auto" dir="auto"
class="label text-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 }" :class="{ 'mb-2': !currentField.description }"
> >
<MarkdownContent <MarkdownContent
@@ -187,7 +188,7 @@
<div <div
v-if="currentField.description" v-if="currentField.description"
dir="auto" dir="auto"
class="mb-3 px-1" class="mb-3 px-1 field-description-text"
> >
<MarkdownContent :string="currentField.description" /> <MarkdownContent :string="currentField.description" />
</div> </div>
@@ -225,7 +226,7 @@
v-if="showFieldNames && (currentField.name || currentField.title)" v-if="showFieldNames && (currentField.name || currentField.title)"
:for="currentField.uuid" :for="currentField.uuid"
dir="auto" dir="auto"
class="label text-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 }" :class="{ 'mb-2': !currentField.description }"
> >
<MarkdownContent <MarkdownContent
@@ -244,7 +245,7 @@
<div <div
v-if="currentField.description" v-if="currentField.description"
dir="auto" dir="auto"
class="mb-3 px-1" class="mb-3 px-1 field-description-text"
> >
<MarkdownContent :string="currentField.description" /> <MarkdownContent :string="currentField.description" />
</div> </div>
@@ -269,7 +270,7 @@
> >
<label <label
:for="option.uuid" :for="option.uuid"
class="flex items-center space-x-3" class="flex items-center space-x-3 radio-label"
> >
<input <input
:id="option.uuid" :id="option.uuid"
@@ -303,7 +304,7 @@
<div <div
v-if="currentField.description" v-if="currentField.description"
dir="auto" dir="auto"
class="mb-3 px-1" class="mb-3 px-1 field-description-text"
> >
<MarkdownContent :string="currentField.description" /> <MarkdownContent :string="currentField.description" />
</div> </div>
@@ -337,7 +338,7 @@
> >
<label <label
:for="field.uuid" :for="field.uuid"
class="flex items-center space-x-3" class="flex items-center space-x-3 checkbox-label"
> >
<input <input
type="hidden" type="hidden"
@@ -480,7 +481,7 @@
id="submit_form_button" id="submit_form_button"
ref="submitButton" ref="submitButton"
type="submit" type="submit"
class="base-button w-full flex justify-center" class="base-button w-full flex justify-center submit-form-button"
:disabled="isButtonDisabled" :disabled="isButtonDisabled"
> >
<span class="flex"> <span class="flex">
@@ -532,13 +533,13 @@
v-if="stepFields.length < 80" v-if="stepFields.length < 80"
class="flex justify-center mt-3 sm:mt-4 mb-0 sm:mb-1" 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 <a
v-for="(step, index) in stepFields" v-for="(step, index) in stepFields"
:key="step[0].uuid" :key="step[0].uuid"
href="#" href="#"
class="inline border border-base-300 h-3 w-3 rounded-full mx-1 mt-1" 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)]" @click.prevent="isCompleted ? '' : [saveStep(), goToStep(index, true)]"
/> />
</div> </div>
@@ -1036,7 +1037,7 @@ export default {
}, []) }, [])
}, },
formulaFields () { 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 () { attachmentsIndex () {
return this.attachments.reduce((acc, a) => { return this.attachments.reduce((acc, a) => {
@@ -1190,22 +1191,24 @@ export default {
return false return false
} }
const defaultValue = !field || isEmpty(field.default_value) ? null : field.default_value
if (['empty', 'unchecked'].includes(condition.action)) { 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)) { } 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) { } else if (['equal', 'contains'].includes(condition.action) && field) {
if (field.options) { if (field.options) {
const option = field.options.find((o) => o.uuid === condition.value) 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))) return values.includes(this.optionValue(option, field.options.indexOf(option)))
} else { } 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) { } else if (['not_equal', 'does_not_contain'].includes(condition.action) && field) {
const option = field.options.find((o) => o.uuid === condition.value) 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))) return !values.includes(this.optionValue(option, field.options.indexOf(option)))
} else { } else {
@@ -1298,7 +1301,9 @@ export default {
if (!this.isCompleted) { if (!this.isCompleted) {
if (scrollToArea) { if (scrollToArea) {
this.scrollIntoField(this.currentField) this.$nextTick(() => {
setTimeout(() => this.scrollIntoField(this.currentField), 1)
})
} }
this.enableScrollIntoField = false this.enableScrollIntoField = false
@@ -1315,7 +1320,7 @@ export default {
const currentFieldUuids = this.currentStepFields.map((f) => f.uuid) const currentFieldUuids = this.currentStepFields.map((f) => f.uuid)
const currentFieldType = this.currentField.type 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 return
} }
@@ -4,7 +4,7 @@
<label <label
v-if="showFieldNames" v-if="showFieldNames"
:for="field.uuid" :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 <MarkdownContent
v-if="field.title" v-if="field.title"
@@ -15,7 +15,7 @@
</template> </template>
</label> </label>
<button <button
class="btn btn-outline btn-sm" class="btn btn-outline btn-sm reupload-button"
@click.prevent="remove" @click.prevent="remove"
> >
<IconReload :width="16" /> <IconReload :width="16" />
@@ -25,7 +25,7 @@
<div> <div>
<img <img
:src="attachmentsIndex[modelValue].url" :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> </div>
<input <input
@@ -40,7 +40,7 @@
<div <div
v-if="field.description" v-if="field.description"
dir="auto" dir="auto"
class="mb-3 px-1" class="mb-3 px-1 field-description-text"
> >
<MarkdownContent :string="field.description" /> <MarkdownContent :string="field.description" />
</div> </div>
@@ -6,7 +6,7 @@
> >
<label <label
v-if="showFieldNames" 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 <MarkdownContent
v-if="field.title" v-if="field.title"
@@ -25,7 +25,7 @@
<a <a
id="type_text_button" id="type_text_button"
href="#" href="#"
class="btn btn-outline font-medium btn-sm" class="btn btn-outline font-medium btn-sm type-text-button"
@click.prevent="toggleTextInput" @click.prevent="toggleTextInput"
> >
<IconTextSize :width="16" /> <IconTextSize :width="16" />
@@ -42,7 +42,7 @@
<a <a
id="type_text_button" id="type_text_button"
href="#" href="#"
class="btn btn-outline font-medium btn-sm" class="btn btn-outline font-medium btn-sm type-text-button"
@click.prevent="toggleTextInput" @click.prevent="toggleTextInput"
> >
<IconSignature :width="16" /> <IconSignature :width="16" />
@@ -55,9 +55,7 @@
class="tooltip" class="tooltip"
:data-tip="t('click_to_upload')" :data-tip="t('click_to_upload')"
> >
<label <label class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap upload-image-button">
class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap"
>
<IconUpload :width="16" /> <IconUpload :width="16" />
<input <input
:key="uploadImageInputKey" :key="uploadImageInputKey"
@@ -74,7 +72,7 @@
<a <a
v-if="modelValue || computedPreviousValue" v-if="modelValue || computedPreviousValue"
href="#" href="#"
class="btn font-medium btn-outline btn-sm" class="btn font-medium btn-outline btn-sm clear-canvas-button"
@click.prevent="remove" @click.prevent="remove"
> >
<IconReload :width="16" /> <IconReload :width="16" />
@@ -83,7 +81,7 @@
<a <a
v-else v-else
href="#" href="#"
class="btn font-medium btn-outline btn-sm" class="btn font-medium btn-outline btn-sm clear-canvas-button"
@click.prevent="clear" @click.prevent="clear"
> >
<IconReload :width="16" /> <IconReload :width="16" />
@@ -105,7 +103,7 @@
<div <div
v-if="field.description" v-if="field.description"
dir="auto" dir="auto"
class="mb-3 px-1" class="mb-3 px-1 field-description-text"
> >
<MarkdownContent :string="field.description" /> <MarkdownContent :string="field.description" />
</div> </div>
@@ -123,7 +121,7 @@
<canvas <canvas
v-show="!modelValue && !computedPreviousValue" v-show="!modelValue && !computedPreviousValue"
ref="canvas" ref="canvas"
class="bg-white border border-base-300 rounded-2xl w-full" class="bg-white border border-base-300 rounded-2xl w-full draw-canvas"
/> />
<input <input
v-if="!isDrawInitials && !modelValue && !computedPreviousValue" v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
@@ -3,7 +3,7 @@
v-if="showFieldNames && (field.name || field.title)" v-if="showFieldNames && (field.name || field.title)"
:for="field.uuid" :for="field.uuid"
dir="auto" dir="auto"
class="label text-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 }" :class="{ 'mb-2': !field.description }"
> >
<MarkdownContent <MarkdownContent
@@ -20,7 +20,7 @@
<div <div
v-if="field.description" v-if="field.description"
dir="auto" dir="auto"
class="mb-3 px-1" class="mb-3 px-1 field-description-text"
> >
<MarkdownContent :string="field.description" /> <MarkdownContent :string="field.description" />
</div> </div>
@@ -50,7 +50,7 @@
> >
<label <label
:for="option.uuid" :for="option.uuid"
class="flex items-center space-x-3" class="flex items-center space-x-3 checkbox-label"
@click="scrollIntoField(field)" @click="scrollIntoField(field)"
> >
<input <input
@@ -3,7 +3,7 @@
v-if="showFieldNames && (field.name || field.title)" v-if="showFieldNames && (field.name || field.title)"
:for="field.uuid" :for="field.uuid"
dir="auto" dir="auto"
class="label text-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 }" :class="{ 'mb-2': !field.description }"
> >
<MarkdownContent <MarkdownContent
@@ -24,7 +24,7 @@
<div <div
v-if="field.description" v-if="field.description"
dir="auto" dir="auto"
class="mb-3 px-1" class="mb-3 px-1 field-description-text"
> >
<MarkdownContent :string="field.description" /> <MarkdownContent :string="field.description" />
</div> </div>
@@ -1,7 +1,7 @@
<template> <template>
<label <label
v-if="!modelValue && !sessionId" 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 <MarkdownContent
v-if="field.title" v-if="field.title"
@@ -34,7 +34,7 @@
<button <button
v-if="sessionId" v-if="sessionId"
disabled disabled
class="base-button w-full" class="base-button w-full modal-save-button"
> >
<IconLoader <IconLoader
width="22" width="22"
@@ -3,7 +3,7 @@
<label <label
v-if="showFieldNames" v-if="showFieldNames"
:for="isCodeSent ? 'one_time_code' : field.uuid" :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 }" :class="{ 'mb-2': !field.description }"
> >
<MarkdownContent <MarkdownContent
@@ -17,7 +17,7 @@
<div <div
v-if="field.description" v-if="field.description"
dir="auto" dir="auto"
class="mb-3 px-1" class="mb-3 px-1 field-description-text"
> >
<MarkdownContent :string="field.description" /> <MarkdownContent :string="field.description" />
</div> </div>
@@ -45,7 +45,7 @@
<a <a
v-if="!defaultValue" v-if="!defaultValue"
href="#" href="#"
class="link" class="link change-phone-number-link"
@click.prevent="isCodeSent = false" @click.prevent="isCodeSent = false"
> >
{{ t('change_phone_number') }} {{ t('change_phone_number') }}
@@ -59,7 +59,7 @@
<a <a
v-else v-else
href="#" href="#"
class="link" class="link resend-code-link"
@click.prevent="resendCode" @click.prevent="resendCode"
> >
{{ isResendLoading ? t('sending') : t('resend_code') }} {{ isResendLoading ? t('sending') : t('resend_code') }}
@@ -68,13 +68,13 @@
</div> </div>
<div <div
v-show="!isCodeSent" 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 <div
id="country_code" id="country_code"
class="relative inline-block" 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 }} {{ selectedCountry.flag }} +{{ selectedCountry.dial }}
</div> </div>
<select <select
@@ -9,7 +9,7 @@
> >
<label <label
v-if="showFieldNames" 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 <MarkdownContent
v-if="field.title" v-if="field.title"
@@ -28,7 +28,7 @@
<a <a
id="type_text_button" id="type_text_button"
href="#" href="#"
class="btn btn-outline btn-sm font-medium" class="btn btn-outline btn-sm font-medium type-text-button"
@click.prevent="[toggleTextInput(), hideQr()]" @click.prevent="[toggleTextInput(), hideQr()]"
> >
<IconSignature :width="16" /> <IconSignature :width="16" />
@@ -46,7 +46,7 @@
<a <a
id="type_text_button" id="type_text_button"
href="#" 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()]" @click.prevent="[toggleTextInput(), hideQr()]"
> >
<IconTextSize :width="16" /> <IconTextSize :width="16" />
@@ -61,9 +61,7 @@
:class="{ 'hidden sm:inline': modelValue || computedPreviousValue }" :class="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
:data-tip="t('take_photo')" :data-tip="t('take_photo')"
> >
<label <label class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap upload-image-button">
class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap"
>
<IconCamera :width="16" /> <IconCamera :width="16" />
<input <input
:key="uploadImageInputKey" :key="uploadImageInputKey"
@@ -80,7 +78,7 @@
<a <a
v-if="modelValue || computedPreviousValue" v-if="modelValue || computedPreviousValue"
href="#" href="#"
class="btn btn-outline btn-sm font-medium" class="btn btn-outline btn-sm font-medium reupload-button"
@click.prevent="remove" @click.prevent="remove"
> >
<IconReload :width="16" /> <IconReload :width="16" />
@@ -119,7 +117,7 @@
<div <div
v-if="field.description" v-if="field.description"
dir="auto" dir="auto"
class="mb-3 px-1" class="mb-3 px-1 field-description-text"
> >
<MarkdownContent :string="field.description" /> <MarkdownContent :string="field.description" />
</div> </div>
@@ -167,7 +165,7 @@
v-show="!modelValue && !computedPreviousValue" v-show="!modelValue && !computedPreviousValue"
ref="canvas" ref="canvas"
style="padding: 1px; 0" 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 <div
v-if="isShowQr" v-if="isShowQr"
+3 -3
View File
@@ -3,7 +3,7 @@
v-if="showFieldNames && (field.name || field.title)" v-if="showFieldNames && (field.name || field.title)"
:for="field.uuid" :for="field.uuid"
dir="auto" dir="auto"
class="label text-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 }" :class="{ 'mb-2': !field.description }"
> >
<MarkdownContent <MarkdownContent
@@ -24,7 +24,7 @@
<div <div
v-if="field.description" v-if="field.description"
dir="auto" dir="auto"
class="mb-3 px-1" class="mb-3 px-1 field-description-text"
> >
<MarkdownContent :string="field.description" /> <MarkdownContent :string="field.description" />
</div> </div>
@@ -67,7 +67,7 @@
> >
<a <a
href="#" href="#"
class="btn btn-ghost btn-circle btn-sm" class="btn btn-ghost btn-circle btn-sm toggle-multiline-text-button"
@click.prevent="toggleTextArea" @click.prevent="toggleTextArea"
> >
<IconAlignBoxLeftTop /> <IconAlignBoxLeftTop />
@@ -1,7 +1,5 @@
<template> <template>
<label <label class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label">
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
>
<MarkdownContent <MarkdownContent
v-if="field.title" v-if="field.title"
:string="field.title" :string="field.title"
@@ -11,13 +9,13 @@
<div <div
v-if="field.description" v-if="field.description"
dir="auto" dir="auto"
class="mb-4 px-1" class="mb-4 px-1 field-description-text"
> >
<MarkdownContent :string="field.description" /> <MarkdownContent :string="field.description" />
</div> </div>
<div <div
v-if="emptyValueRequiredStep && emptyValueRequiredStep[0] !== field" 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') }} {{ t('complete_all_required_fields_to_proceed_with_identity_verification') }}
</div> </div>
+37 -10
View File
@@ -1,6 +1,6 @@
<template> <template>
<div <div
class="absolute overflow-visible group" class="absolute overflow-visible group field-area-container"
:style="positionStyle" :style="positionStyle"
:class="{ 'z-[1]': isMoved || isDragged }" :class="{ 'z-[1]': isMoved || isDragged }"
@pointerdown.stop @pointerdown.stop
@@ -33,7 +33,7 @@
</div> </div>
<div <div
v-if="field?.type && (isSelected || isNameFocus)" 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" style="top: -25px; height: 25px"
@mousedown.stop @mousedown.stop
@pointerdown.stop @pointerdown.stop
@@ -41,7 +41,7 @@
<FieldSubmitter <FieldSubmitter
v-if="field.type != 'heading'" v-if="field.type != 'heading'"
v-model="field.submitter_uuid" v-model="field.submitter_uuid"
class="border-r" class="border-r roles-dropdown"
:compact="true" :compact="true"
:editable="editable && (!defaultField || defaultField.role !== submitter?.name)" :editable="editable && (!defaultField || defaultField.role !== submitter?.name)"
:allow-add-new="!defaultSubmitters.length" :allow-add-new="!defaultSubmitters.length"
@@ -108,7 +108,7 @@
>{{ t('editable') }}</label> >{{ t('editable') }}</label>
<span <span
v-if="field.type !== 'payment' && !isValueInput" v-if="field.type !== 'payment' && !isValueInput"
class="dropdown dropdown-end" class="dropdown dropdown-end field-area-settings-dropdown"
@mouseenter="renderDropdown = true" @mouseenter="renderDropdown = true"
@touchstart="renderDropdown = true" @touchstart="renderDropdown = true"
> >
@@ -141,6 +141,7 @@
:with-required="false" :with-required="false"
:with-areas="false" :with-areas="false"
@click-formula="isShowFormulaModal = true" @click-formula="isShowFormulaModal = true"
@click-font="isShowFontModal = true"
@click-description="isShowDescriptionModal = true" @click-description="isShowDescriptionModal = true"
@click-condition="isShowConditionsModal = true" @click-condition="isShowConditionsModal = true"
@scroll-to="[selectedAreaRef.value = $event, $emit('scroll-to', $event)]" @scroll-to="[selectedAreaRef.value = $event, $emit('scroll-to', $event)]"
@@ -159,9 +160,9 @@
</div> </div>
<div <div
ref="touchValueTarget" ref="touchValueTarget"
class="flex items-center h-full w-full" class="flex items-center h-full w-full field-area"
dir="auto" dir="auto"
:class="[isValueInput ? 'bg-opacity-50' : 'bg-opacity-80', field.type === 'heading' ? 'bg-gray-50' : bgColors[submitterIndex % bgColors.length], isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? (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" @click="focusValueInput"
> >
<span <span
@@ -253,6 +254,17 @@
@close="isShowFormulaModal = false" @close="isShowFormulaModal = false"
/> />
</Teleport> </Teleport>
<Teleport
v-if="isShowFontModal"
:to="modalContainerEl"
>
<FontModal
:field="field"
:editable="editable && !defaultField"
:build-default-name="buildDefaultName"
@close="isShowFontModal = false"
/>
</Teleport>
<Teleport <Teleport
v-if="isShowConditionsModal" v-if="isShowConditionsModal"
:to="modalContainerEl" :to="modalContainerEl"
@@ -283,6 +295,7 @@ import FieldType from './field_type'
import Field from './field' import Field from './field'
import FieldSettings from './field_settings' import FieldSettings from './field_settings'
import FormulaModal from './formula_modal' import FormulaModal from './formula_modal'
import FontModal from './font_modal'
import ConditionsModal from './conditions_modal' import ConditionsModal from './conditions_modal'
import DescriptionModal from './description_modal' import DescriptionModal from './description_modal'
import { IconX, IconCheck, IconDotsVertical } from '@tabler/icons-vue' import { IconX, IconCheck, IconDotsVertical } from '@tabler/icons-vue'
@@ -295,6 +308,7 @@ export default {
IconCheck, IconCheck,
FieldSettings, FieldSettings,
FormulaModal, FormulaModal,
FontModal,
IconDotsVertical, IconDotsVertical,
DescriptionModal, DescriptionModal,
ConditionsModal, ConditionsModal,
@@ -352,6 +366,7 @@ export default {
data () { data () {
return { return {
isShowFormulaModal: false, isShowFormulaModal: false,
isShowFontModal: false,
isShowConditionsModal: false, isShowConditionsModal: false,
isContenteditable: false, isContenteditable: false,
isSettingsFocus: false, isSettingsFocus: false,
@@ -386,11 +401,19 @@ export default {
defaultName () { defaultName () {
return this.buildDefaultName(this.field, this.template.fields) return this.buildDefaultName(this.field, this.template.fields)
}, },
alignClasses () { fontClasses () {
if (!this.field.preferences) {
return {}
}
return { return {
center: 'justify-center', 'justify-center': this.field.preferences.align === 'center',
left: 'justify-start', 'justify-start': this.field.preferences.align === 'left',
right: 'justify-end' '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 () { optionIndexText () {
@@ -483,6 +506,10 @@ export default {
this.$el.getRootNode().activeElement.blur() this.$el.getRootNode().activeElement.blur()
}, },
maybeToggleDefaultValue () { maybeToggleDefaultValue () {
if (!this.editable) {
return
}
if (['text', 'number'].includes(this.field.type)) { if (['text', 'number'].includes(this.field.type)) {
this.isContenteditable = true this.isContenteditable = true
+20 -15
View File
@@ -32,7 +32,7 @@
<div <div
v-if="$slots.buttons || withTitle" v-if="$slots.buttons || withTitle"
id="title_container" 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 }" :class="{ sticky: withStickySubmitters || isBreakpointLg }"
:style="{ backgroundColor }" :style="{ backgroundColor }"
> >
@@ -47,7 +47,7 @@
v-if="withTitle" v-if="withTitle"
:model-value="template.name" :model-value="template.name"
:editable="editable" :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" :icon-stroke-width="2.3"
@update:model-value="updateName" @update:model-value="updateName"
/> />
@@ -163,7 +163,7 @@
</div> </div>
<div <div
id="main_container" 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%]')" :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 <div
@@ -205,7 +205,7 @@
<button <button
v-if="sortedDocuments.length && editable && withAddPageButton" v-if="sortedDocuments.length && editable && withAddPageButton"
id="add_blank_page_button" id="add_blank_page_button"
class="btn btn-outline w-full" class="btn btn-outline w-full add-blank-page-button"
@click.prevent="addBlankPage" @click.prevent="addBlankPage"
> >
<IconInnerShadowTop <IconInnerShadowTop
@@ -239,7 +239,7 @@
<button <button
v-if="withAddPageButton" v-if="withAddPageButton"
id="add_blank_page_button" 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" @click.prevent="addBlankPage"
> >
<IconInnerShadowTop <IconInnerShadowTop
@@ -266,7 +266,7 @@
:is-drag="!!dragField" :is-drag="!!dragField"
:input-mode="inputMode" :input-mode="inputMode"
:default-fields="[...defaultRequiredFields, ...defaultFields]" :default-fields="[...defaultRequiredFields, ...defaultFields]"
:allow-draw="!onlyDefinedFields" :allow-draw="!onlyDefinedFields || drawField"
:data-document-uuid="document.uuid" :data-document-uuid="document.uuid"
:default-submitters="defaultSubmitters" :default-submitters="defaultSubmitters"
:with-field-placeholder="withFieldPlaceholder" :with-field-placeholder="withFieldPlaceholder"
@@ -307,7 +307,7 @@
<button <button
v-if="withAddPageButton" v-if="withAddPageButton"
id="add_blank_page_button" 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" @click.prevent="addBlankPage"
> >
<IconInnerShadowTop <IconInnerShadowTop
@@ -327,7 +327,7 @@
<div <div
v-if="withFieldsList && !isMobile" v-if="withFieldsList && !isMobile"
id="fields_list_container" 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'" :class="drawField ? 'overflow-hidden' : 'overflow-y-auto overflow-x-hidden'"
> >
<div <div
@@ -335,13 +335,13 @@
class="sticky inset-0 h-full z-20" class="sticky inset-0 h-full z-20"
:style="{ backgroundColor }" :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> <p>
{{ t('draw_field_on_the_document') }} {{ t('draw_field_on_the_document') }}
</p> </p>
<div> <div>
<button <button
class="base-button" class="base-button cancel-draw-button"
@click="clearDrawField" @click="clearDrawField"
> >
{{ t('cancel') }} {{ t('cancel') }}
@@ -408,7 +408,10 @@
@select="startFieldDraw($event)" @select="startFieldDraw($event)"
/> />
</div> </div>
<div id="docuseal_modal_container" /> <div
id="docuseal_modal_container"
class="modal-container"
/>
</div> </div>
</template> </template>
@@ -1316,6 +1319,10 @@ export default {
} }
}, },
onDropfield (area) { onDropfield (area) {
if (!this.editable) {
return
}
const field = this.fieldsDragFieldRef.value || { const field = this.fieldsDragFieldRef.value || {
name: '', name: '',
uuid: v4(), uuid: v4(),
@@ -1338,9 +1345,8 @@ export default {
} }
if (field.type === 'date') { if (field.type === 'date') {
field.preferences = { field.preferences ||= {}
format: this.defaultDateFormat field.preferences.format ||= this.defaultDateFormat
}
} }
} }
@@ -1401,7 +1407,6 @@ export default {
const lastArea = field.areas[field.areas.length - 1] const lastArea = field.areas[field.areas.length - 1]
if (lastArea) { if (lastArea) {
fieldArea.x -= lastArea.w / 2
fieldArea.w = lastArea.w fieldArea.w = lastArea.w
fieldArea.h = lastArea.h fieldArea.h = lastArea.h
} }
@@ -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="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"> <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) }} {{ t('condition') }} - {{ item.name || buildDefaultName(item, template.fields) }}
</span> </span>
<a <a
href="#" href="#"
class="text-xl" class="text-xl modal-close-button"
@click.prevent="$emit('close')" @click.prevent="$emit('close')"
>&times;</a> >&times;</a>
</div> </div>
@@ -50,9 +50,9 @@
v-if="conditions.length > 1" v-if="conditions.length > 1"
class="flex justify-between mx-1" class="flex justify-between mx-1"
> >
<span class="text-sm"> <label class="text-sm">
{{ t('condition') }} {{ cindex + 1 }} {{ t('condition') }} {{ cindex + 1 }}
</span> </label>
<a <a
href="#" href="#"
class="link text-sm" class="link text-sm"
@@ -131,9 +131,7 @@
class="inline float-right link text-right mb-3 px-2" class="inline float-right link text-right mb-3 px-2"
@click.prevent="conditions.push({})" @click.prevent="conditions.push({})"
> + {{ t('add_condition') }}</a> > + {{ t('add_condition') }}</a>
<button <button class="base-button w-full mt-2 modal-save-button">
class="base-button w-full mt-2"
>
{{ t('save') }} {{ t('save') }}
</button> </button>
</form> </form>
+4 -4
View File
@@ -1,7 +1,7 @@
<template> <template>
<div class="flex space-x-2"> <div class="flex space-x-2">
<Contenteditable <Contenteditable
class="w-full block mr-6" class="w-full block mr-6 document-preview-name"
:model-value="item.name" :model-value="item.name"
:icon-width="16" :icon-width="16"
@update:model-value="onUpdateName" @update:model-value="onUpdateName"
@@ -15,7 +15,7 @@
/> />
<button <button
v-if="withArrows" 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" style="width: 24px; height: 24px"
@click.stop="$emit('up', item)" @click.stop="$emit('up', item)"
> >
@@ -23,14 +23,14 @@
</button> </button>
<button <button
v-if="withArrows" 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" style="width: 24px; height: 24px"
@click.stop="$emit('down', item)" @click.stop="$emit('down', item)"
> >
&darr; &darr;
</button> </button>
<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" style="width: 24px; height: 24px"
@click.stop="$emit('remove', item)" @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="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"> <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) }} {{ field.name || buildDefaultName(field, template.fields) }}
</span> </span>
<a <a
href="#" href="#"
class="text-xl" class="text-xl modal-close-button"
@click.prevent="$emit('close')" @click.prevent="$emit('close')"
>&times;</a> >&times;</a>
</div> </div>
@@ -55,9 +55,7 @@
> >
</div> </div>
</div> </div>
<button <button class="base-button w-full mt-4 modal-save-button">
class="base-button w-full mt-4"
>
{{ t('save') }} {{ t('save') }}
</button> </button>
</form> </form>
+18 -3
View File
@@ -3,7 +3,7 @@
class="list-field group mb-2" class="list-field group mb-2"
> >
<div <div
class="border border-base-300 rounded rounded-tr-none relative group" class="border border-base-300 rounded rounded-tr-none relative group fields-list-item"
:style="{ backgroundColor: backgroundColor }" :style="{ backgroundColor: backgroundColor }"
> >
<div class="flex items-center justify-between relative group/contenteditable-container"> <div class="flex items-center justify-between relative group/contenteditable-container">
@@ -98,7 +98,7 @@
/> />
<span <span
v-else-if="field.type !== 'heading'" v-else-if="field.type !== 'heading'"
class="dropdown dropdown-end" class="dropdown dropdown-end field-settings-dropdown"
@mouseenter="renderDropdown = true" @mouseenter="renderDropdown = true"
@touchstart="renderDropdown = true" @touchstart="renderDropdown = true"
> >
@@ -127,6 +127,7 @@
:editable="editable" :editable="editable"
:background-color="dropdownBgColor" :background-color="dropdownBgColor"
@click-formula="isShowFormulaModal = true" @click-formula="isShowFormulaModal = true"
@click-font="isShowFontModal = true"
@click-description="isShowDescriptionModal = true" @click-description="isShowDescriptionModal = true"
@click-condition="isShowConditionsModal = true" @click-condition="isShowConditionsModal = true"
@set-draw="$emit('set-draw', $event)" @set-draw="$emit('set-draw', $event)"
@@ -136,7 +137,7 @@
</ul> </ul>
</span> </span>
<button <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')" :title="t('remove')"
@click="$emit('remove', field)" @click="$emit('remove', field)"
> >
@@ -231,6 +232,17 @@
@close="isShowFormulaModal = false" @close="isShowFormulaModal = false"
/> />
</Teleport> </Teleport>
<Teleport
v-if="isShowFontModal"
:to="modalContainerEl"
>
<FontModal
:field="field"
:editable="editable && !defaultField"
:build-default-name="buildDefaultName"
@close="isShowFontModal = false"
/>
</Teleport>
<Teleport <Teleport
v-if="isShowConditionsModal" v-if="isShowConditionsModal"
:to="modalContainerEl" :to="modalContainerEl"
@@ -261,6 +273,7 @@ import FieldType from './field_type'
import PaymentSettings from './payment_settings' import PaymentSettings from './payment_settings'
import FieldSettings from './field_settings' import FieldSettings from './field_settings'
import FormulaModal from './formula_modal' import FormulaModal from './formula_modal'
import FontModal from './font_modal'
import ConditionsModal from './conditions_modal' import ConditionsModal from './conditions_modal'
import DescriptionModal from './description_modal' import DescriptionModal from './description_modal'
import { IconRouteAltLeft, IconMathFunction, IconNewSection, IconTrashX, IconSettings } from '@tabler/icons-vue' import { IconRouteAltLeft, IconMathFunction, IconNewSection, IconTrashX, IconSettings } from '@tabler/icons-vue'
@@ -275,6 +288,7 @@ export default {
PaymentSettings, PaymentSettings,
IconNewSection, IconNewSection,
FormulaModal, FormulaModal,
FontModal,
DescriptionModal, DescriptionModal,
ConditionsModal, ConditionsModal,
IconRouteAltLeft, IconRouteAltLeft,
@@ -305,6 +319,7 @@ export default {
isNameFocus: false, isNameFocus: false,
showPaymentModal: false, showPaymentModal: false,
isShowFormulaModal: false, isShowFormulaModal: false,
isShowFontModal: false,
isShowConditionsModal: false, isShowConditionsModal: false,
isShowDescriptionModal: false, isShowDescriptionModal: false,
renderDropdown: false renderDropdown: false
@@ -54,7 +54,7 @@
</label> </label>
</div> </div>
<div <div
v-if="['number', 'cells'].includes(field.type)" v-if="['cells'].includes(field.type)"
class="py-1.5 px-1 relative" class="py-1.5 px-1 relative"
@click.stop @click.stop
> >
@@ -330,6 +330,19 @@
v-if="field.type != 'stamp'" v-if="field.type != 'stamp'"
class="pb-0.5 mt-0.5" 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 <li
v-if="field.type != 'stamp'" v-if="field.type != 'stamp'"
> >
@@ -427,7 +440,7 @@
</template> </template>
<script> <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 { export default {
name: 'FieldSettings', name: 'FieldSettings',
@@ -438,6 +451,7 @@ export default {
IconRouteAltLeft, IconRouteAltLeft,
IconCopy, IconCopy,
IconNewSection, IconNewSection,
IconTypography,
IconX IconX
}, },
inject: ['template', 'save', 't'], inject: ['template', 'save', 't'],
@@ -472,7 +486,7 @@ export default {
default: true 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 () { data () {
return { return {
} }
@@ -5,7 +5,7 @@
@touchstart="renderDropdown = true" @touchstart="renderDropdown = true"
> >
<div class="flex space-x-2 items-end"> <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"> <div class="flex items-center space-x-2">
<span <span
class="w-3 h-3 flex-shrink-0 rounded-full" class="w-3 h-3 flex-shrink-0 rounded-full"
@@ -22,7 +22,7 @@
/> />
</div> </div>
</div> </div>
<div class="dropdown dropdown-top dropdown-end"> <div class="dropdown dropdown-top dropdown-end roles-dropdown-mobile">
<label <label
tabindex="0" tabindex="0"
class="bg-base-100 cursor-pointer rounded-md p-2 border border-base-300 w-full flex justify-center" class="bg-base-100 cursor-pointer rounded-md p-2 border border-base-300 w-full flex justify-center"
@@ -1,6 +1,6 @@
<template> <template>
<span <span
class="dropdown" class="dropdown field-types-dropdown"
@mouseenter="renderDropdown = true" @mouseenter="renderDropdown = true"
@touchstart="renderDropdown = true" @touchstart="renderDropdown = true"
> >
+6 -6
View File
@@ -2,7 +2,7 @@
<div :class="withStickySubmitters ? 'sticky top-0 z-[1]' : ''"> <div :class="withStickySubmitters ? 'sticky top-0 z-[1]' : ''">
<FieldSubmitter <FieldSubmitter
:model-value="selectedSubmitter.uuid" :model-value="selectedSubmitter.uuid"
class="roles-dropdown w-full rounded-lg" class="roles-dropdown w-full rounded-lg roles-dropdown"
:style="withStickySubmitters ? { backgroundColor } : {}" :style="withStickySubmitters ? { backgroundColor } : {}"
:submitters="submitters" :submitters="submitters"
:menu-style="{ overflow: 'auto', display: 'flex', flexDirection: 'row', maxHeight: 'calc(100vh - 120px)', backgroundColor: ['', null, 'transparent'].includes(backgroundColor) ? 'white' : backgroundColor }" :menu-style="{ overflow: 'auto', display: 'flex', flexDirection: 'row', maxHeight: 'calc(100vh - 120px)', backgroundColor: ['', null, 'transparent'].includes(backgroundColor) ? 'white' : backgroundColor }"
@@ -74,7 +74,7 @@
<div <div
:style="{ backgroundColor }" :style="{ backgroundColor }"
draggable="true" 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 })" @dragstart="onDragstart({ type: 'text', ...field })"
@dragend="$emit('drag-end')" @dragend="$emit('drag-end')"
> >
@@ -104,7 +104,7 @@
</div> </div>
<div <div
v-if="editable && !onlyDefinedFields" 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 <template
v-for="(icon, type) in fieldIconsSorted" v-for="(icon, type) in fieldIconsSorted"
@@ -113,7 +113,7 @@
<button <button
v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification')" v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification')"
draggable="true" 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 }" :style="{ backgroundColor }"
:class="drawFieldType === type ? 'border-base-content/40' : 'border-base-300 hover:border-base-content/20'" :class="drawFieldType === type ? 'border-base-content/40' : 'border-base-300 hover:border-base-content/20'"
@dragstart="onDragstart({ type: type })" @dragstart="onDragstart({ type: type })"
@@ -142,7 +142,7 @@
<a <a
href="https://www.docuseal.com/pricing" href="https://www.docuseal.com/pricing"
target="_blank" 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 }" :style="{ backgroundColor }"
> >
<div class="w-0 absolute left-0"> <div class="w-0 absolute left-0">
@@ -168,7 +168,7 @@
<a <a
href="https://www.docuseal.com/contact" href="https://www.docuseal.com/contact"
target="_blank" 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 }" :style="{ backgroundColor }"
> >
<div class="w-0 absolute left-0"> <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="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"> <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) }} {{ t('formula') }} - {{ field.name || buildDefaultName(field, template.fields) }}
</span> </span>
<a <a
href="#" href="#"
class="text-xl" class="text-xl modal-close-button"
@click.prevent="$emit('close')" @click.prevent="$emit('close')"
>&times;</a> >&times;</a>
</div> </div>
@@ -111,7 +111,7 @@
</div> </div>
</div> </div>
<button <button
class="base-button w-full" class="base-button w-full modal-save-button"
@click.prevent="validateSaveAndClose" @click.prevent="validateSaveAndClose"
> >
{{ t('save') }} {{ t('save') }}
+6
View File
@@ -1,4 +1,5 @@
const en = { const en = {
font: 'Font',
party: 'Party', party: 'Party',
method: 'Method', method: 'Method',
reorder_fields: 'Reorder fields', reorder_fields: 'Reorder fields',
@@ -161,6 +162,7 @@ const en = {
} }
const es = { const es = {
fuente: 'Fuente',
party: 'Parte', party: 'Parte',
method: 'Método', method: 'Método',
reorder_fields: 'Reordenar campos', reorder_fields: 'Reordenar campos',
@@ -323,6 +325,7 @@ const es = {
} }
const it = { const it = {
font: 'Carattere',
party: 'Parte', party: 'Parte',
method: 'Metodo', method: 'Metodo',
reorder_fields: 'Riordina i campi', reorder_fields: 'Riordina i campi',
@@ -485,6 +488,7 @@ const it = {
} }
const pt = { const pt = {
fonte: 'Fonte',
party: 'Parte', party: 'Parte',
method: 'Método', method: 'Método',
reorder_fields: 'Reorganizar campos', reorder_fields: 'Reorganizar campos',
@@ -647,6 +651,7 @@ const pt = {
} }
const fr = { const fr = {
font: 'Police',
party: 'Partie', party: 'Partie',
method: 'Méthode', method: 'Méthode',
reorder_fields: 'Réorganiser les champs', reorder_fields: 'Réorganiser les champs',
@@ -809,6 +814,7 @@ const fr = {
} }
const de = { const de = {
font: 'Schriftart',
party: 'Partei', party: 'Partei',
method: 'Verfahren', method: 'Verfahren',
reorder_fields: 'Felder neu anordnen', reorder_fields: 'Felder neu anordnen',
@@ -1,6 +1,6 @@
<template> <template>
<div class="absolute text-center w-full bottom-0 pr-3 mb-4"> <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"> <div class="flex items-center space-x-2">
<component <component
:is="fieldIcons[drawField.type]" :is="fieldIcons[drawField.type]"
@@ -1,7 +1,5 @@
<template> <template>
<span <span class="dropdown dropdown-top dropdown-end absolute bottom-4 right-4 z-10 fields-dropdown-mobile">
class="dropdown dropdown-top dropdown-end absolute bottom-4 right-4 z-10"
>
<label <label
class="btn btn-neutral text-white btn-circle btn-lg group" class="btn btn-neutral text-white btn-circle btn-lg group"
tabindex="0" tabindex="0"
@@ -1,6 +1,6 @@
<template> <template>
<span <span
class="dropdown dropdown-end" class="dropdown dropdown-end field-settings-dropdown"
:class="{ 'dropdown-open': ((!field.preferences?.price && !field.preferences?.formula) || !isConnected) && !isLoading }" :class="{ 'dropdown-open': ((!field.preferences?.price && !field.preferences?.formula) || !isConnected) && !isLoading }"
> >
<label <label
+6 -6
View File
@@ -23,7 +23,7 @@
> >
<div> <div>
<button <button
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors p-0" 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" @click.stop="isShowConditionsModal = true"
> >
<IconRouteAltLeft <IconRouteAltLeft
@@ -48,7 +48,7 @@
> >
<div> <div>
<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" style="width: 24px; height: 24px"
@click.stop="$emit('remove', item)" @click.stop="$emit('remove', item)"
> >
@@ -63,7 +63,7 @@
class="tooltip tooltip-left before:text-xs" class="tooltip tooltip-left before:text-xs"
> >
<button <button
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors p-0" 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)" @click.stop="$emit('reorder', item)"
> >
<IconSortDescending2 <IconSortDescending2
@@ -75,14 +75,14 @@
</span> </span>
<template v-if="withArrows"> <template v-if="withArrows">
<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" style="width: 24px; height: 24px"
@click.stop="$emit('up', item)" @click.stop="$emit('up', item)"
> >
&uarr; &uarr;
</button> </button>
<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" style="width: 24px; height: 24px"
@click.stop="$emit('down', item)" @click.stop="$emit('down', item)"
> >
@@ -94,7 +94,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="flex pb-2 pt-1.5"> <div class="flex pb-2 pt-1.5 document-preview-name">
<Contenteditable <Contenteditable
:model-value="item.name" :model-value="item.name"
:icon-width="16" :icon-width="16"
+1 -1
View File
@@ -1,7 +1,7 @@
<template> <template>
<label <label
:for="inputId" :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 }" :class="{ 'opacity-100': isLoading || isProcessing }"
> >
{{ message }} {{ message }}
+1 -1
View File
@@ -3,7 +3,7 @@
<label <label
id="add_document_button" id="add_document_button"
:for="inputId" :for="inputId"
class="btn btn-outline w-full" class="btn btn-outline w-full add-document-button"
:class="{ 'btn-disabled': isLoading || isProcessing }" :class="{ 'btn-disabled': isLoading || isProcessing }"
> >
<IconInnerShadowTop <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
@@ -5,7 +5,7 @@ class SendFormCompletedWebhookRequestJob
sidekiq_options queue: :webhooks sidekiq_options queue: :webhooks
MAX_ATTEMPTS = 20 MAX_ATTEMPTS = 12
def perform(params = {}) def perform(params = {})
submitter = Submitter.find(params['submitter_id']) 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
+29 -10
View File
@@ -14,8 +14,20 @@ class SubmitterMailer < ApplicationMailer
@email_message = submitter.account.email_messages.find_by(uuid: submitter.preferences['email_message_uuid']) @email_message = submitter.account.email_messages.find_by(uuid: submitter.preferences['email_message_uuid'])
end end
@body = @email_message&.body.presence || @submitter.template.preferences['request_email_body'].presence template_submitters_index =
@subject = @email_message&.subject.presence || @submitter.template.preferences['request_email_subject'].presence if @email_message.blank?
build_submitter_preferences_index(@submitter)
else
{}
end
@body = @email_message&.body.presence ||
template_submitters_index.dig(@submitter.uuid, 'request_email_body').presence ||
@submitter.template.preferences['request_email_body'].presence
@subject = @email_message&.subject.presence ||
template_submitters_index.dig(@submitter.uuid, 'request_email_subject').presence ||
@submitter.template.preferences['request_email_subject'].presence
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) @email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY)
@@ -24,14 +36,7 @@ class SubmitterMailer < ApplicationMailer
reply_to = build_submitter_reply_to(@submitter) reply_to = build_submitter_reply_to(@submitter)
I18n.with_locale(@current_account.locale) do I18n.with_locale(@current_account.locale) do
subject = subject = build_invite_subject(@subject, @email_config, submitter)
if @email_config || @subject
ReplaceEmailVariables.call(@subject || @email_config.value['subject'], submitter:)
elsif @submitter.with_signature_fields?
I18n.t(:you_are_invited_to_sign_a_document)
else
I18n.t(:you_are_invited_to_submit_a_form)
end
mail( mail(
to: @submitter.friendly_name, to: @submitter.friendly_name,
@@ -196,6 +201,20 @@ class SubmitterMailer < ApplicationMailer
user.role == 'integration' ? user.friendly_name.sub(/\+\w+@/, '@') : user.friendly_name user.role == 'integration' ? user.friendly_name.sub(/\+\w+@/, '@') : user.friendly_name
end end
def build_invite_subject(subject, email_config, submitter)
if email_config || subject
ReplaceEmailVariables.call(subject || email_config.value['subject'], submitter:)
elsif submitter.with_signature_fields?
I18n.t(:you_are_invited_to_sign_a_document)
else
I18n.t(:you_are_invited_to_submit_a_form)
end
end
def build_submitter_preferences_index(submitter)
submitter.template.preferences['submitters'].to_a.index_by { |e| e['uuid'] }
end
def add_attachments_with_size_limit(submitter, storage_attachments, current_size, filename_format = nil) def add_attachments_with_size_limit(submitter, storage_attachments, current_size, filename_format = nil)
total_size = current_size total_size = current_size
+11
View File
@@ -55,6 +55,7 @@ class Submitter < ApplicationRecord
has_many_attached :attachments has_many_attached :attachments
has_many_attached :preview_documents has_many_attached :preview_documents
has_many :template_accesses, through: :template 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 :document_generation_events, dependent: :destroy
has_many :submission_events, dependent: :destroy has_many :submission_events, dependent: :destroy
@@ -63,6 +64,8 @@ class Submitter < ApplicationRecord
scope :completed, -> { where.not(completed_at: nil) } scope :completed, -> { where.not(completed_at: nil) }
after_destroy :anonymize_email_events, if: -> { Docuseal.multitenant? }
def status def status
if declined_at? if declined_at?
'declined' 'declined'
@@ -108,4 +111,12 @@ class Submitter < ApplicationRecord
fields.any? { |f| f['submitter_uuid'] == uuid && signature_field_types.include?(f['type']) } fields.any? { |f| f['submitter_uuid'] == uuid && signature_field_types.include?(f['type']) }
end end
end end
private
def anonymize_email_events
email_events.each do |event|
event.update!(email: Digest::MD5.base64digest(event.email))
end
end
end end
+4 -2
View File
@@ -28,10 +28,12 @@ class WebhookUrl < ApplicationRecord
form.started form.started
form.completed form.completed
form.declined form.declined
submission.created
submission.completed
submission.expired
submission.archived
template.created template.created
template.updated template.updated
submission.created
submission.archived
].freeze ].freeze
belongs_to :account belongs_to :account
+2 -2
View File
@@ -63,7 +63,7 @@
<span> <span>
<%= t('add_signature_id_to_the_documents') %> <%= t('add_signature_id_to_the_documents') %>
</span> </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> </div>
<% end %> <% end %>
<% end %> <% end %>
@@ -75,7 +75,7 @@
<span> <span>
<%= t('require_signing_reason') %> <%= t('require_signing_reason') %>
</span> </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> </div>
<% end %> <% end %>
<% end %> <% end %>
+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
<path d="M7 16h-4l3.47 -4.66a2 2 0 1 0 -3.47 -1.54"></path>
<path d="M10 16v-8h4"></path> <path d="M10 12l3 0"></path>
<path d="M17 16v-6a2 2 0 0 1 4 0v6"></path>
<path d="M17 13l4 0"></path>
</svg>

After

Width:  |  Height:  |  Size: 426 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
</svg>

After

Width:  |  Height:  |  Size: 397 B

+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
<path d="M8 13v-8.5a1.5 1.5 0 0 1 3 0v7.5"></path>
<path d="M11 11.5v-2a1.5 1.5 0 0 1 3 0v2.5"></path>
<path d="M14 10.5a1.5 1.5 0 0 1 3 0v1.5"></path>
<path d="M17 11.5a1.5 1.5 0 0 1 3 0v4.5a6 6 0 0 1 -6 6h-2h.208a6 6 0 0 1 -5.012 -2.7l-.196 -.3c-.312 -.479 -1.407 -2.388 -3.286 -5.728a1.5 1.5 0 0 1 .536 -2.022a1.867 1.867 0 0 1 2.28 .28l1.47 1.47"></path>
<path d="M5 3l-1 -1"></path>
<path d="M4 7h-1"></path>
<path d="M14 3l1 -1"></path>
<path d="M15 6h1"></path>
</svg>

After

Width:  |  Height:  |  Size: 711 B

+11
View File
@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
<path d="M4 12h.01"></path>
<path d="M4 6h.01"></path>
<path d="M4 18h.01"></path>
<path d="M8 18h2"></path>
<path d="M8 12h2"></path>
<path d="M8 6h2"></path>
<path d="M14 6h6"></path>
<path d="M14 12h6"></path>
<path d="M14 18h6"></path>
</svg>

After

Width:  |  Height:  |  Size: 484 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
<path d="M3 9l9 6l9 -6l-9 -6l-9 6"></path>
<path d="M21 9v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-10"></path>
<path d="M3 19l6 -6"></path>
<path d="M15 13l6 6"></path>
</svg>

After

Width:  |  Height:  |  Size: 401 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
<path d="M5 4h4l2 5l-2.5 1.5a11 11 0 0 0 5 5l1.5 -2.5l5 2v4a2 2 0 0 1 -2 2a16 16 0 0 1 -15 -15a2 2 0 0 1 2 -2"></path>
<path d="M15 6l2 2l4 -4"></path>
</svg>

After

Width:  |  Height:  |  Size: 382 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
<path d="M7 4v16l13 -8z"></path>
</svg>

After

Width:  |  Height:  |  Size: 261 B

@@ -2,7 +2,7 @@
<div class="space-y-6 mx-auto"> <div class="space-y-6 mx-auto">
<div class="space-y-6"> <div class="space-y-6">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<%= render 'start_form/docuseal_logo' %> <%= render 'start_form/banner' %>
</div> </div>
<div class="text-center text-4xl font-bold"> <div class="text-center text-4xl font-bold">
<%= t('email_has_been_sent') %> <%= t('email_has_been_sent') %>
+3 -1
View File
@@ -30,7 +30,9 @@
<div dir="auto" class="form-control !mt-0"> <div dir="auto" class="form-control !mt-0">
<%= f.label :email, t('email'), class: 'label' %> <%= 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') %> <%= 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> </div>
<toggle-submit dir="auto" class="form-control"> <toggle-submit dir="auto" class="form-control">
<%= f.button button_title(title: t('start'), disabled_with: t('starting')), class: 'base-button' %> <%= f.button button_title(title: t('start'), disabled_with: t('starting')), class: 'base-button' %>
@@ -0,0 +1,49 @@
<% event_colors = %w[bg-red-200 bg-sky-200 bg-emerald-200 bg-yellow-200 bg-purple-200 bg-pink-200 bg-cyan-200 bg-orange-200 bg-lime-200 bg-indigo-200] %>
<% submitters_uuids = (@submission.template_submitters || @submission.template.submitters).pluck('uuid') %>
<%= render 'shared/turbo_modal_large', title: t('event_log') do %>
<div class="pl-8 pr-4 py-4">
<ol class="relative border-s border-base-300 space-y-6">
<li class="ml-7">
<span class="absolute flex items-center justify-center w-7 h-7 rounded-full -start-3.5 ring-8 ring-base-100 text-base-content bg-gray-200">
<%= svg_icon('file_text', class: 'w-4 h-4') %>
</span>
<p class="text-sm leading-none text-base-content/60 pt-1.5">
<%= l(@submission.created_at.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) %>
</p>
<p class="text-base-content/80 mt-1">
<% if @submission.source == 'invite' %>
<%= t('submission_created_by_email_html', email: @submission.created_by_user.email) %>
<% elsif @submission.created_by_user %>
<%= t('submission_created_by_email_via_source_html', email: @submission.created_by_user.email, source: t("submission_sources.#{@submission.source}")) %>
<% else %>
<%= t('submission_created_via_source_html', source: t("submission_sources.#{@submission.source}")) %>
<% end %>
</p>
</li>
<% @submission.submission_events.order(:event_timestamp).each do |event| %>
<% submitter = @submission.submitters.find { |e| e.id == event.submitter_id } %>
<% bg_class = event_colors[submitters_uuids.index(submitter.uuid) % event_colors.length] %>
<% submitter_name = event.event_type.include?('sms') || event.event_type.include?('phone') ? (event.data['phone'] || submitter.phone) : (submitter.name || submitter.email || submitter.phone) %>
<li class="ml-7">
<span class="absolute flex items-center justify-center w-7 h-7 rounded-full -start-3.5 ring-8 ring-base-100 text-base-content <%= bg_class %>">
<%= svg_icon(SubmissionEventsController::SUBMISSION_EVENT_ICONS.fetch(event.event_type, 'circle_dot'), class: 'w-4 h-4') %>
</span>
<p class="text-sm leading-none text-base-content/60 pt-1.5">
<%= l(event.event_timestamp.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) %>
</p>
<p class="text-base-content/80 mt-1">
<% if event.event_type == 'complete_verification' %>
<%= t('submission_event_names.complete_verification_by_html', provider: event.data['method'], submitter_name:) %>
<% elsif event.event_type == 'invite_party' && (invited_submitter = @submission.submitters.find { |e| e.uuid == event.data['uuid'] }) && (name = @submission.template_submitters.find { |e| e['uuid'] == event.data['uuid'] }&.dig('name')) %>
<%= t('submission_event_names.invite_party_by_html', invited_submitter_name: [invited_submitter.name || invited_submitter.email || invited_submitter.phone, name].join(' '), submitter_name:) %>
<% elsif event.event_type.include?('send_') %>
<%= t("submission_event_names.#{event.event_type}_to_html", submitter_name:) %>
<% else %>
<%= t("submission_event_names.#{event.event_type}_by_html", submitter_name:) %>
<% end %>
</p>
</li>
<% end %>
</ol>
</div>
<% end %>
+52 -4
View File
@@ -1,3 +1,4 @@
<% submitter_preferences_index = template.preferences['submitters'].to_a.index_by { |e| e['uuid'] } %>
<div class="form-control"> <div class="form-control">
<% can_send_emails = Accounts.can_send_emails?(current_account) %> <% can_send_emails = Accounts.can_send_emails?(current_account) %>
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
@@ -36,10 +37,18 @@
<% config = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) %> <% config = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) %>
<div id="message_field" class="card card-compact bg-base-300/40 hidden"> <div id="message_field" class="card card-compact bg-base-300/40 hidden">
<div class="card-body"> <div class="card-body">
<div class="form-control space-y-2"> <%= tag.input id: toggle_uuid = SecureRandom.uuid, value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: local_assigns[:message_per_submitter] != false && template.preferences['submitters'].to_a.size > 1 %>
<div class="peer-checked:hidden form-control space-y-2">
<div class="form-control"> <div class="form-control">
<%= f.label :subject, t('subject'), class: 'label' %> <div class="flex justify-between">
<%= f.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || template.preferences['request_email_subject'].presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %> <%= f.label :subject, t('subject'), class: 'label' %>
<% if template.submitters.size > 1 && template.submitters.size < 5 && local_assigns[:message_per_submitter] != false %>
<label for="<%= toggle_uuid %>" class="label underline">
<%= t('edit_per_party') %>
</label>
<% end %>
</div>
<%= f.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_subject').presence || template.preferences['request_email_subject'].presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<div class="flex items-center"> <div class="flex items-center">
@@ -49,7 +58,7 @@
</span> </span>
</div> </div>
<autoresize-textarea> <autoresize-textarea>
<%= f.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || template.preferences['request_email_body'].presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %> <%= f.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_body').presence || template.preferences['request_email_body'].presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
</autoresize-textarea> </autoresize-textarea>
<% unless local_assigns.fetch(:disable_save_as_default_template_option, false) %> <% unless local_assigns.fetch(:disable_save_as_default_template_option, false) %>
<label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer"> <label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer">
@@ -60,5 +69,44 @@
</div> </div>
<%= render 'submissions/message_fields' %> <%= render 'submissions/message_fields' %>
</div> </div>
<% if template.submitters.size > 1 && template.submitters.size < 5 && local_assigns[:message_per_submitter] != false %>
<div class="hidden peer-checked:block form-control space-y-2">
<% uuid = SecureRandom.uuid %>
<% options = template.submitters.map { |e| [e['name'], "request_email_#{uuid}_#{e['uuid']}"] } %>
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="flex relative px-1">
<ul class="tabs w-full flex flex-nowrap">
<% options.each_with_index do |(label, val), index| %>
<div class="w-full">
<%= f.radio_button :selected, val, checked: index.zero?, id: "#{val}_radio", data: { action: 'click:toggle-visible#trigger' }, class: 'hidden peer' %>
<%= f.label :selected, label, value: val, for: "#{val}_radio", class: 'tab w-full tab-lifted peer-checked:tab-active !bg-transparent' %>
</div>
<% end %>
</ul>
</toggle-visible>
<% template.submitters.each_with_index do |submitter, index| %>
<%= fields_for :submitter_preferences, nil, index: submitter['uuid'] do |ff| %>
<div id="request_email_<%= uuid %>_<%= submitter['uuid'] %>" class="<%= 'hidden' if index != 0 %>">
<div class="form-control">
<div class="flex justify-between">
<%= ff.label :subject, t('subject'), class: 'label' %>
</div>
<%= ff.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_subject').presence || template.preferences['request_email_subject'].presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :message, t('body'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_body').presence || template.preferences['request_email_body'].presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
</autoresize-textarea>
</div>
</div>
<% end %>
<% end %>
</div>
<% end %>
</div> </div>
</div> </div>
+3 -1
View File
@@ -1,6 +1,8 @@
<% align = field.dig('preferences', 'align') %> <% align = field.dig('preferences', 'align') %>
<% color = field.dig('preferences', 'color') %> <% color = field.dig('preferences', 'color') %>
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-serif' if field.dig('preferences', 'font') == 'Times' %> <%= align == 'right' ? 'justify-end' : (align == 'center' ? 'justify-center' : '') %>" style="<%= "color: #{color}; " if color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size'].to_i * 1.23}pt); line-height: `clamp(6pt, 2.0vw, #{(field['preferences']['font_size'].to_i * 1.23) + 3}pt)`" if field.dig('preferences', 'font_size') %>"> <% font = field.dig('preferences', 'font') %>
<% 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' %> <% if field['type'] == 'signature' %>
<div class="flex flex-col justify-between h-full overflow-hidden"> <div class="flex flex-col justify-between h-full overflow-hidden">
<div class="flex-grow flex overflow-hidden" style="min-height: 50%"> <div class="flex-grow flex overflow-hidden" style="min-height: 50%">
+31 -6
View File
@@ -19,6 +19,11 @@
<%= svg_icon('external_link', class: 'w-6 h-6') %> <%= svg_icon('external_link', class: 'w-6 h-6') %>
<span class="hidden md:inline"><%= t('audit_log') %></span> <span class="hidden md:inline"><%= t('audit_log') %></span>
</a> </a>
<% 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>
<% end %>
<% end %> <% end %>
<% if last_submitter %> <% if last_submitter %>
<% if is_all_completed || !is_combined_enabled %> <% if is_all_completed || !is_combined_enabled %>
@@ -100,7 +105,18 @@
<% value = values[field['uuid']] %> <% value = values[field['uuid']] %>
<% value ||= field['default_value'] if field['type'] == 'heading' %> <% value ||= field['default_value'] if field['type'] == 'heading' %>
<% next if value.blank? %> <% 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 %> <% end %>
</div> </div>
</div> </div>
@@ -227,12 +243,21 @@
</div> </div>
<% elsif field['type'] == 'checkbox' %> <% elsif field['type'] == 'checkbox' %>
<%= svg_icon('check', class: 'w-6 h-6') %> <%= svg_icon('check', class: 'w-6 h-6') %>
<% elsif field['type'] == 'number' %>
<%= NumberUtils.format_number(value, field.dig('preferences', 'format')) %>
<% elsif field['type'] == 'date' %>
<%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), @submission.account.locale) %>
<% else %> <% else %>
<%= Array.wrap(value).join(', ') %> <% 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 %> <% end %>
</div> </div>
</div> </div>
@@ -1,5 +1,5 @@
<% query_params = params.permit(:q).merge(filter_params) %> <% query_params = params.permit(:q).merge(filter_params) %>
<% if icon = { 'declined' => 'x_circle', 'expired' => 'clock_cancel', 'partially_completed' => 'clock_edit' }[params[:status]] %> <% if icon = { 'declined' => 'x_circle', 'expired' => 'clock_cancel', 'partially_completed' => 'clock_edit', 'sent' => 'send', 'opened' => 'mail_opened' }[params[:status]] %>
<div class="flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-34 border-neutral-700"> <div class="flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-34 border-neutral-700">
<%= link_to submissions_filter_path('status', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 w-full pr-1 md:max-w-[140px]' do %> <%= link_to submissions_filter_path('status', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 w-full pr-1 md:max-w-[140px]' do %>
<%= svg_icon(icon, class: 'w-5 h-5 shrink-0') %> <%= svg_icon(icon, class: 'w-5 h-5 shrink-0') %>
@@ -2,7 +2,7 @@
<div class="flex flex-col md:flex-row gap-2 mt-5"> <div class="flex flex-col md:flex-row gap-2 mt-5">
<div class="form-control w-full"> <div class="form-control w-full">
<div id="status" class="radio-select grid grid-cols-2 gap-2 px-1"> <div id="status" class="radio-select grid grid-cols-2 gap-2 px-1">
<% ['', 'pending', 'completed', 'partially_completed', 'declined', 'expired'].each do |status| %> <% ['', 'pending', 'completed', 'partially_completed', 'sent', 'opened', 'declined', 'expired'].each do |status| %>
<label class="radio-label cursor-pointer inline-flex items-center space-x-2"> <label class="radio-label cursor-pointer inline-flex items-center space-x-2">
<%= radio_button_tag 'status', status, params[:status] == status || (status == '' && params[:status].blank?), class: 'base-radio' %> <%= radio_button_tag 'status', status, params[:status] == status || (status == '' && params[:status].blank?), class: 'base-radio' %>
<span><%= t(status.presence || 'all') %></span> <span><%= t(status.presence || 'all') %></span>
@@ -2,11 +2,9 @@
<div class="max-w-md mx-auto px-2 mt-12 mb-4"> <div class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto"> <div class="space-y-6 mx-auto">
<div class="space-y-6"> <div class="space-y-6">
<% if Docuseal.multitenant? %> <div class="flex items-center justify-center">
<div class="flex items-center justify-center"> <%= render 'start_form/banner' %>
<%= render 'start_form/docuseal_logo' %> </div>
</div>
<% end %>
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4"> <div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
<div class="flex items-center"> <div class="flex items-center">
<div class="mr-3"> <div class="mr-3">
@@ -0,0 +1 @@
<%= render partial: 'submissions/annotation', collection: annots, as: :annot %>
+3 -3
View File
@@ -47,8 +47,8 @@
<div class="relative my-4 shadow-md"> <div class="relative my-4 shadow-md">
<img loading="lazy" src="<%= page.url %>" width="<%= page.metadata['width'] %>" height="<%= page.metadata['height'] %>"> <img loading="lazy" src="<%= page.url %>" width="<%= page.metadata['width'] %>" height="<%= page.metadata['height'] %>">
<div id="page-<%= [document.uuid, index].join('-') %>" class="top-0 bottom-0 left-0 right-0 absolute"> <div id="page-<%= [document.uuid, index].join('-') %>" class="top-0 bottom-0 left-0 right-0 absolute">
<% document_annots_index[index]&.each do |annot| %> <% if annots = document_annots_index[index] %>
<%= render 'submissions/annotation', annot: %> <%= render 'submit_form/annotations', annots: %>
<% end %> <% end %>
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %> <% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
<% value = values[field['uuid']].presence || (field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submitter.submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %> <% value = values[field['uuid']].presence || (field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submitter.submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
@@ -58,7 +58,7 @@
<% next if field['conditions'].present? && values[field['uuid']].blank? && field['submitter_uuid'] != @submitter.uuid %> <% 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['conditions'].present? && field['submitter_uuid'] == @submitter.uuid %>
<% next if field.dig('preferences', 'formula').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 %> <% end %>
</div> </div>
</div> </div>
+1 -1
View File
@@ -17,7 +17,7 @@
</submitter-item> </submitter-item>
</div> </div>
<div> <div>
<%= render 'submissions/send_email', f:, template: @submitter.template, submitter: @submitter, resend_email: @submitter.sent_at?, submitter_email_message: @submitter_email_message, disable_save_as_default_template_option: true %> <%= render 'submissions/send_email', f:, template: @submitter.template, submitter: @submitter, resend_email: @submitter.sent_at?, submitter_email_message: @submitter_email_message, disable_save_as_default_template_option: true, message_per_submitter: false %>
<%= render 'submissions/send_sms', f:, resend_sms: @submitter.sent_at? %> <%= render 'submissions/send_sms', f:, resend_sms: @submitter.sent_at? %>
</div> </div>
<div class="form-control mt-4"> <div class="form-control mt-4">
+66 -16
View File
@@ -83,21 +83,71 @@
<div class="collapse-content"> <div class="collapse-content">
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %> <%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %>
<toggle-on-submit data-element-id="email_saved_alert1"></toggle-on-submit> <toggle-on-submit data-element-id="email_saved_alert1"></toggle-on-submit>
<%= f.fields_for :preferences, Struct.new(:request_email_subject, :request_email_body).new(*(@template.preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence || AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY).value.values_at('subject', 'body'))) do |ff| %> <%= tag.input id: 'request_email_per_submitter', value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: @template.preferences['submitters'].to_a.size > 1 %>
<div class="form-control"> <div class="peer-checked:hidden">
<%= ff.label :request_email_subject, t('email_subject'), class: 'label' %> <%= f.fields_for :preferences, Struct.new(:request_email_subject, :request_email_body).new(*(@template.preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence || AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY).value.values_at('subject', 'body'))) do |ff| %>
<%= ff.text_field :request_email_subject, required: true, class: 'base-input', dir: 'auto' %> <div class="form-control">
</div> <div class="flex justify-between">
<div class="form-control"> <%= ff.label :request_email_subject, t('email_subject'), class: 'label' %>
<div class="flex items-center"> <% if @template.submitters.size > 1 && @template.submitters.size < 5 %>
<%= ff.label :request_email_body, t('email_body'), class: 'label' %> <label for="request_email_per_submitter" class="label underline">
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>"> <%= t('edit_per_party') %>
<%= svg_icon('info_circle', class: 'w-4 h-4') %> </label>
</span> <% end %>
</div>
<%= ff.text_field :request_email_subject, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
<autoresize-textarea> <div class="form-control">
<%= ff.text_area :request_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %> <div class="flex items-center">
</autoresize-textarea> <%= ff.label :request_email_body, t('email_body'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :request_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<% end %>
</div>
<% if @template.submitters.size > 1 && @template.submitters.size < 5 %>
<div class="hidden peer-checked:block">
<% options = @template.submitters.map { |e| [e['name'], "request_email_#{e['uuid']}"] } %>
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="flex relative px-1">
<ul class="tabs w-full flex flex-nowrap mb-2">
<% options.each_with_index do |(label, val), index| %>
<div class="w-full">
<%= f.radio_button :selected, val, checked: index.zero?, id: "#{val}_radio", data: { action: 'click:toggle-visible#trigger' }, class: 'hidden peer' %>
<%= f.label :selected, label, value: val, for: "#{val}_radio", class: 'tab w-full tab-lifted peer-checked:tab-active' %>
</div>
<% end %>
</ul>
</toggle-visible>
<%= f.fields_for :preferences do |ff| %>
<% @template.submitters.each_with_index do |submitter, index| %>
<div id="request_email_<%= submitter['uuid'] %>" class="<%= 'hidden' if index != 0 %>">
<% submitter_preferences = f.object.preferences['submitters'].to_a.find { |e| e['uuid'] == submitter['uuid'] } || {} %>
<%= ff.fields_for :submitters, Struct.new(:request_email_subject, :request_email_body).new(*(submitter_preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence || @template.preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence || AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY).value.values_at('subject', 'body'))), index: nil do |fff| %>
<%= fff.hidden_field :uuid, value: submitter['uuid'] %>
<div class="form-control">
<%= fff.label :request_email_subject, t('email_subject'), class: 'label' %>
<%= fff.text_field :request_email_subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= fff.label :request_email_body, t('email_body'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= fff.text_area :request_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<% end %>
</div>
<% end %>
<% end %>
</div> </div>
<% end %> <% end %>
<div class="form-control pt-2"> <div class="form-control pt-2">
@@ -235,12 +285,12 @@
<div class="form-control"> <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 %> <%= 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 %> <% 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 %> <% else %>
<toggle-attribute data-target-id="<%= email_field_uuid = SecureRandom.uuid %>" data-class-name="hidden" data-value="email"> <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' %> <%= 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> </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 %> <% end %>
</div> </div>
<% if @template.submitters.size == 2 %> <% if @template.submitters.size == 2 %>
+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 end
config.active_storage.resolve_model_to_route = :rails_storage_proxy if ENV['ACTIVE_STORAGE_PUBLIC'] != 'true' 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. # Mount Action Cable outside main process or domain.
# config.action_cable.mount_path = nil # config.action_cable.mount_path = nil
+1 -1
View File
@@ -187,7 +187,7 @@ Devise.setup do |config|
# ==> Configuration for :rememberable # ==> Configuration for :rememberable
# The time the user will be remembered without asking for credentials again. # 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. # Invalidates all the remember me tokens when the user signs out.
config.expire_all_remember_me_on_sign_out = true config.expire_all_remember_me_on_sign_out = true
+78
View File
@@ -20,6 +20,7 @@ en: &en
language_ko: 한국어 language_ko: 한국어
hi_there: Hi there hi_there: Hi there
thanks: Thanks thanks: Thanks
edit_per_party: Edit per party
reply_to: Reply to reply_to: Reply to
pending_by_me: Pending by me pending_by_me: Pending by me
partially_completed: Partially completed partially_completed: Partially completed
@@ -694,6 +695,18 @@ en: &en
overdue_payment: Overdue Payment overdue_payment: Overdue Payment
your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: Your Pro Plan has been suspended due to unpaid invoices. You can update your payment details to settle the invoice and continue using DocuSeal or cancel your subscription. your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: Your Pro Plan has been suspended due to unpaid invoices. You can update your payment details to settle the invoice and continue using DocuSeal or cancel your subscription.
manage_subscription: Manage Subscription manage_subscription: Manage Subscription
submission_created_by_email_html: '<b>Submission created</b> by %{email}'
submission_created_by_email_via_source_html: '<b>Submission created</b> by %{email} via %{source}'
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
embed: Embedding
invie: Invite
link: Link
submission_event_names: submission_event_names:
send_email_to_html: '<b>Email sent</b> to %{submitter_name}' send_email_to_html: '<b>Email sent</b> to %{submitter_name}'
send_reminder_email_to_html: '<b>Reminder email sent</b> to %{submitter_name}' send_reminder_email_to_html: '<b>Reminder email sent</b> to %{submitter_name}'
@@ -732,6 +745,7 @@ en: &en
read: Read your data read: Read your data
es: &es es: &es
edit_per_party: Editar por parte
signed: Firmado signed: Firmado
reply_to: Responder a reply_to: Responder a
partially_completed: Parcialmente completado partially_completed: Parcialmente completado
@@ -1408,6 +1422,18 @@ es: &es
overdue_payment: Pago Atrasado overdue_payment: Pago Atrasado
your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: Tu plan Pro ha sido suspendido debido a facturas impagas. Puedes actualizar tus datos de pago para liquidar la factura y seguir usando DocuSeal o cancelar tu suscripción. your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: Tu plan Pro ha sido suspendido debido a facturas impagas. Puedes actualizar tus datos de pago para liquidar la factura y seguir usando DocuSeal o cancelar tu suscripción.
manage_subscription: Gestionar Suscripción manage_subscription: Gestionar Suscripción
submission_created_by_email_html: '<b>Envío creado</b> por %{email}'
submission_created_by_email_via_source_html: '<b>Envío creado</b> por %{email} a través de %{source}'
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
embed: Integración
invite: Invitación
link: Enlace
submission_event_names: submission_event_names:
send_email_to_html: '<b>Correo electrónico enviado</b> a %{submitter_name}' send_email_to_html: '<b>Correo electrónico enviado</b> a %{submitter_name}'
send_reminder_email_to_html: '<b>Correo de recordatorio enviado</b> a %{submitter_name}' send_reminder_email_to_html: '<b>Correo de recordatorio enviado</b> a %{submitter_name}'
@@ -1446,6 +1472,7 @@ es: &es
read: Leer tus datos read: Leer tus datos
it: &it it: &it
edit_per_party: Modifica per partito
signed: Firmato signed: Firmato
reply_to: Rispondi a reply_to: Rispondi a
pending_by_me: In sospeso da me pending_by_me: In sospeso da me
@@ -2121,6 +2148,18 @@ it: &it
overdue_payment: Pagamento Scaduto overdue_payment: Pagamento Scaduto
your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: Il tuo piano Pro è stato sospeso a causa di fatture non pagate. Puoi aggiornare i tuoi dati di pagamento per saldare la fattura e continuare a utilizzare DocuSeal o annullare l'abbonamento. your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: Il tuo piano Pro è stato sospeso a causa di fatture non pagate. Puoi aggiornare i tuoi dati di pagamento per saldare la fattura e continuare a utilizzare DocuSeal o annullare l'abbonamento.
manage_subscription: Gestisci Abbonamento manage_subscription: Gestisci Abbonamento
submission_created_by_email_html: '<b>Invio creato</b> da %{email}'
submission_created_by_email_via_source_html: '<b>Invio creato</b> da %{email} tramite %{source}'
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
embed: Incorporamento
invite: Invito
link: Link
submission_event_names: submission_event_names:
send_email_to_html: '<b>E-mail inviato</b> a %{submitter_name}' send_email_to_html: '<b>E-mail inviato</b> a %{submitter_name}'
send_reminder_email_to_html: '<b>E-mail di promemoria inviato</b> a %{submitter_name}' send_reminder_email_to_html: '<b>E-mail di promemoria inviato</b> a %{submitter_name}'
@@ -2159,6 +2198,7 @@ it: &it
read: Leggi i tuoi dati read: Leggi i tuoi dati
fr: &fr fr: &fr
edit_per_party: Éditer par partie
signed: Signé signed: Signé
reply_to: Répondre à reply_to: Répondre à
partially_completed: Partiellement complété partially_completed: Partiellement complété
@@ -2836,6 +2876,18 @@ fr: &fr
overdue_payment: Paiement En Retard overdue_payment: Paiement En Retard
your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: Votre plan Pro a été suspendu en raison de factures impayées. Vous pouvez mettre à jour vos informations de paiement pour régler la facture et continuer à utiliser DocuSeal ou annuler votre abonnement. your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: Votre plan Pro a été suspendu en raison de factures impayées. Vous pouvez mettre à jour vos informations de paiement pour régler la facture et continuer à utiliser DocuSeal ou annuler votre abonnement.
manage_subscription: Gérer l'Abonnement manage_subscription: Gérer l'Abonnement
submission_created_by_email_html: '<b>Soumission créée</b> par %{email}'
submission_created_by_email_via_source_html: '<b>Soumission créée</b> par %{email} via %{source}'
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 l’utilisation d’un lien de partage, car il n’est pas clair quel signataire est responsable de quels champs. Pour résoudre ce problème, suivez ce <a href="https://www.docuseal.com/resources/pre-filling-recipients" class="link font-bold" rel="noopener noreferrer nofollow" target="_blank">guide</a> pour définir les détails du signataire par défaut.'
submission_sources:
api: API
bulk: Envoi en masse
embed: Intégration
invite: Invitation
link: Lien
submission_event_names: submission_event_names:
send_email_to_html: '<b>E-mail envoyé</b> à %{submitter_name}' send_email_to_html: '<b>E-mail envoyé</b> à %{submitter_name}'
send_reminder_email_to_html: '<b>E-mail de rappel envoyé</b> à %{submitter_name}' send_reminder_email_to_html: '<b>E-mail de rappel envoyé</b> à %{submitter_name}'
@@ -2874,6 +2926,7 @@ fr: &fr
read: Lire vos données read: Lire vos données
pt: &pt pt: &pt
edit_per_party: Edita por festa
signed: Assinado signed: Assinado
reply_to: Responder a reply_to: Responder a
partially_completed: Parcialmente concluído partially_completed: Parcialmente concluído
@@ -3550,6 +3603,18 @@ pt: &pt
overdue_payment: Pagamento Atrasado overdue_payment: Pagamento Atrasado
your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: Seu plano Pro foi suspenso devido a faturas não pagas. Você pode atualizar seus dados de pagamento para quitar a fatura e continuar usando o DocuSeal ou cancelar sua assinatura. your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: Seu plano Pro foi suspenso devido a faturas não pagas. Você pode atualizar seus dados de pagamento para quitar a fatura e continuar usando o DocuSeal ou cancelar sua assinatura.
manage_subscription: Gerenciar Assinatura manage_subscription: Gerenciar Assinatura
submission_created_by_email_html: '<b>Envio criado</b> por %{email}'
submission_created_by_email_via_source_html: '<b>Envio criado</b> por %{email} via %{source}'
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
embed: Incorporação
invite: Convite
link: Link
submission_event_names: submission_event_names:
send_email_to_html: '<b>E-mail enviado</b> para %{submitter_name}' send_email_to_html: '<b>E-mail enviado</b> para %{submitter_name}'
send_reminder_email_to_html: '<b>E-mail de lembrete enviado</b> para %{submitter_name}' send_reminder_email_to_html: '<b>E-mail de lembrete enviado</b> para %{submitter_name}'
@@ -3588,6 +3653,7 @@ pt: &pt
read: Ler seus dados read: Ler seus dados
de: &de de: &de
edit_per_party: Bearbeiten pro Partei
signed: Unterschrieben signed: Unterschrieben
reply_to: Antworten auf reply_to: Antworten auf
partially_completed: Teilweise abgeschlossen partially_completed: Teilweise abgeschlossen
@@ -4264,6 +4330,18 @@ de: &de
overdue_payment: Überfällige Zahlung overdue_payment: Überfällige Zahlung
your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: Ihr Pro-Plan wurde aufgrund unbezahlter Rechnungen ausgesetzt. Sie können Ihre Zahlungsdaten aktualisieren, um die Rechnung zu begleichen und DocuSeal weiterhin zu nutzen, oder Ihr Abonnement kündigen. your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: Ihr Pro-Plan wurde aufgrund unbezahlter Rechnungen ausgesetzt. Sie können Ihre Zahlungsdaten aktualisieren, um die Rechnung zu begleichen und DocuSeal weiterhin zu nutzen, oder Ihr Abonnement kündigen.
manage_subscription: Abonnement Verwalten manage_subscription: Abonnement Verwalten
submission_created_by_email_html: '<b>Übermittlung erstellt</b> von %{email}'
submission_created_by_email_via_source_html: '<b>Übermittlung erstellt</b> durch %{email} über %{source}'
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
embed: Einbettung
invite: Einladung
link: Link
submission_event_names: submission_event_names:
send_email_to_html: '<b>E-Mail gesendet</b> an %{submitter_name}' send_email_to_html: '<b>E-Mail gesendet</b> an %{submitter_name}'
send_reminder_email_to_html: '<b>Erinnerungs-E-Mail gesendet</b> an %{submitter_name}' send_reminder_email_to_html: '<b>Erinnerungs-E-Mail gesendet</b> an %{submitter_name}'
+8 -2
View File
@@ -33,8 +33,14 @@ environment ENV.fetch('RAILS_ENV', 'development')
# the concurrency of the application would be max `threads` * `workers`. # the concurrency of the application would be max `threads` * `workers`.
# Workers do not work on JRuby or Windows (both of which do not support # Workers do not work on JRuby or Windows (both of which do not support
# processes). # 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. # Use the `preload_app!` method when specifying a `workers` number.
# This directive tells Puma to first boot the application and load code # This directive tells Puma to first boot the application and load code
+4 -3
View File
@@ -72,6 +72,7 @@ Rails.application.routes.draw do
resources :submissions, only: %i[index], controller: 'submissions_dashboard' resources :submissions, only: %i[index], controller: 'submissions_dashboard'
resources :submissions, only: %i[show destroy] do resources :submissions, only: %i[show destroy] do
resources :unarchive, only: %i[create], controller: 'submissions_unarchive' resources :unarchive, only: %i[create], controller: 'submissions_unarchive'
resources :events, only: %i[index], controller: 'submission_events'
end end
resources :submitters, only: %i[edit update] resources :submitters, only: %i[edit update]
resources :console_redirect, only: %i[index] resources :console_redirect, only: %i[index]
@@ -121,6 +122,8 @@ Rails.application.routes.draw do
get '/disk/:encoded_key/*filename' => 'active_storage/disk#show', as: :rails_disk_service 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 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 post '/direct_uploads' => 'active_storage/direct_uploads#create', as: :rails_direct_uploads
ActiveSupport.run_load_hooks(:multitenant_routes, self)
end end
resources :start_form, only: %i[show update], path: 'd', param: 'slug' do resources :start_form, only: %i[show update], path: 'd', param: 'slug' do
@@ -145,9 +148,7 @@ Rails.application.routes.draw do
get :completed get :completed
end end
resources :send_submission_email, only: %i[create] do resources :send_submission_email, only: %i[create]
get :success, on: :collection
end
resources :submitters, only: %i[], param: 'slug' do resources :submitters, only: %i[], param: 'slug' do
resources :download, only: %i[index], controller: 'submissions_download' resources :download, only: %i[index], controller: 'submissions_download'
@@ -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. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" 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 t.index ["submitter_id"], name: "index_completed_submitters_on_submitter_id", unique: true
end 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| create_table "document_generation_events", force: :cascade do |t|
t.bigint "submitter_id", null: false t.bigint "submitter_id", null: false
t.string "event_name", null: false t.string "event_name", null: false
+11 -10
View File
@@ -8,19 +8,20 @@ module DownloadUtils
module_function module_function
def call(url) def call(url)
uri = Addressable::URI.parse(url) uri = begin
URI(url)
if Docuseal.multitenant? rescue URI::Error
raise UnableToDownload, "Error loading: #{uri.display_uri}. Only HTTPS is allowed." if uri.scheme != 'https' Addressable::URI.parse(url).normalize
if uri.host.in?(LOCALHOSTS)
raise UnableToDownload, "Error loading: #{uri.display_uri}. Can't download from localhost."
end
end 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 resp
end end
+8
View File
@@ -24,6 +24,10 @@ module PdfIcons
StringIO.new(logo_new_data) StringIO.new(logo_new_data)
end end
def stamp_logo_io
StringIO.new(stamp_logo_data)
end
def check_data def check_data
@check_data ||= PATH.join('check.png').read @check_data ||= PATH.join('check.png').read
end end
@@ -39,4 +43,8 @@ module PdfIcons
def logo_new_data def logo_new_data
@logo_new_data ||= PATH.join('logo_new.png').read @logo_new_data ||= PATH.join('logo_new.png').read
end end
def stamp_logo_data
@stamp_logo_data ||= PATH.join('stamp-logo.png').read
end
end end
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

+25 -4
View File
@@ -15,7 +15,10 @@ module ReplaceEmailVariables
SUBMITTER_SLUG = /\{+submitter\.slug\}+/i SUBMITTER_SLUG = /\{+submitter\.slug\}+/i
SUBMISSION_LINK = /\{+submission\.link\}+/i SUBMISSION_LINK = /\{+submission\.link\}+/i
SUBMISSION_ID = /\{+submission\.id\}+/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_LINKS = /\{+documents\.links\}+/i
DOCUMENTS_LINK = /\{+documents\.link\}+/i DOCUMENTS_LINK = /\{+documents\.link\}+/i
@@ -37,13 +40,25 @@ module ReplaceEmailVariables
text = replace(text, SUBMISSION_LINK, html_escape:) do text = replace(text, SUBMISSION_LINK, html_escape:) do
submitter.submission ? build_submission_link(submitter.submission) : '' submitter.submission ? build_submission_link(submitter.submission) : ''
end 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_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, DOCUMENTS_LINK, html_escape:) { build_documents_links_text(submitter, sig) }
text = replace(text, ACCOUNT_NAME, html_escape:) { submitter.submission.account.name } 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_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, 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+@/, '@') } replace(text, SENDER_EMAIL, html_escape:) { submitter.submission.created_by_user&.email.to_s.sub(/\+\w+@/, '@') }
end end
# rubocop:enable Metrics # rubocop:enable Metrics
@@ -54,12 +69,18 @@ module ReplaceEmailVariables
) )
end 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) def replace(text, var, html_escape: false)
text.gsub(var) do text.gsub(var) do
if html_escape if html_escape
ERB::Util.html_escape(yield) ERB::Util.html_escape(yield(Regexp.last_match))
else else
yield yield(Regexp.last_match)
end end
end end
end end
+19 -1
View File
@@ -3,10 +3,28 @@
module SendWebhookRequest module SendWebhookRequest
USER_AGENT = 'DocuSeal.com Webhook' 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 module_function
def call(webhook_url, event_type:, data:) 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['Content-Type'] = 'application/json'
req.headers['User-Agent'] = USER_AGENT req.headers['User-Agent'] = USER_AGENT
req.headers.merge!(webhook_url.secret.to_h) if webhook_url.secret.present? req.headers.merge!(webhook_url.secret.to_h) if webhook_url.secret.present?
+1
View File
@@ -107,6 +107,7 @@ module Submissions
def normalize_email(email) def normalize_email(email)
return if email.blank? return if email.blank?
return if email.is_a?(Numeric)
return email.downcase if email.to_s.include?(',') || return email.downcase if email.to_s.include?(',') ||
email.to_s.match?(/\.(?:gob|om|mm|cm|et|mo|nz|za|ie)\z/) || email.to_s.match?(/\.(?:gob|om|mm|cm|et|mo|nz|za|ie)\z/) ||
+127 -17
View File
@@ -6,11 +6,11 @@ module Submissions
module_function module_function
# rubocop:disable Metrics/BlockLength # rubocop:disable Metrics
def call(template:, user:, submissions_attrs:, source:, submitters_order:, params: {}) def call(template:, user:, submissions_attrs:, source:, submitters_order:, params: {})
preferences = Submitters.normalize_preferences(user.account, user, 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 = Submitters.normalize_preferences(user.account, user, attrs)
submission_preferences = preferences.merge(submission_preferences) submission_preferences = preferences.merge(submission_preferences)
@@ -23,24 +23,44 @@ module Submissions
expire_at: attrs[:expire_at], expire_at: attrs[:expire_at],
template_submitters: [], submitters_order:) 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| 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? submission.template_schema = submission.template.schema if submission.template_schema.blank?
next if submitter_attrs.slice('email', 'phone', 'name').compact_blank.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') submission.template_submitters << template_submitter.except('optional_invite_by_uuid', 'invite_by_uuid')
is_order_sent = submitters_order == 'random' || index.zero? is_order_sent = submitters_order == 'random' || index.zero?
build_submitter(submission:, attrs: submitter_attrs, uuid:, build_submitter(submission:, attrs: submitter_attrs,
is_order_sent:, user:, uuid:, is_order_sent:, user:, params:,
preferences: preferences.merge(submission_preferences)) preferences: preferences.merge(submission_preferences))
end end
maybe_set_template_fields(submission, attrs[:submitters])
if submission.submitters.size > template.submitters.size if submission.submitters.size > template.submitters.size
raise BaseError, 'Defined more signing parties than in template' raise BaseError, 'Defined more signing parties than in template'
end end
@@ -51,8 +71,20 @@ module Submissions
submission.tap(&:save!) submission.tap(&:save!)
end 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 end
# rubocop:enable Metrics/BlockLength
def maybe_add_invite_submitters(submission, template) def maybe_add_invite_submitters(submission, template)
template.submitters.each_with_index do |item, index| template.submitters.each_with_index do |item, index|
@@ -69,11 +101,23 @@ module Submissions
end end
end end
def submitter_message_preferences(uuid, params)
return {} if params[:request_email_per_submitter] != '1'
return {} if params[:is_custom_message] != '1'
{
'subject' => params.dig('submitter_preferences', uuid, 'subject'),
'body' => params.dig('submitter_preferences', uuid, 'body')
}.compact_blank
end
def maybe_set_template_fields(submission, submitters_attrs, default_submitter_uuid: nil) def maybe_set_template_fields(submission, submitters_attrs, default_submitter_uuid: nil)
template_fields = (submission.template_fields || submission.template.fields).deep_dup 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| 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_readonly_fields_param(submitter_attrs[:readonly_fields], template_fields, submitter_uuid)
process_field_values_param(submitter_attrs[:values], template_fields, submitter_uuid) process_field_values_param(submitter_attrs[:values], template_fields, submitter_uuid)
@@ -90,6 +134,65 @@ module Submissions
submission submission
end 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) def process_readonly_fields_param(readonly_fields, template_fields, submitter_uuid)
return if readonly_fields.blank? return if readonly_fields.blank?
@@ -168,16 +271,17 @@ module Submissions
field field
end end
def find_submitter_uuid(template, attrs, index) def find_submitter_uuid(submitters, attrs, index)
uuid = attrs[:uuid].presence 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 end
def build_submitter(submission:, attrs:, uuid:, is_order_sent:, user:, preferences:) def build_submitter(submission:, attrs:, uuid:, is_order_sent:, user:, preferences:, params:)
email = Submissions.normalize_email(attrs[:email]) email = Submissions.normalize_email(attrs[:email])
submitter_preferences = Submitters.normalize_preferences(submission.account, user, attrs) submitter_preferences = Submitters.normalize_preferences(submission.account, user,
attrs.merge(submitter_message_preferences(uuid, params)))
values = attrs[:values] || {} values = attrs[:values] || {}
phone_field_uuid = find_phone_field(submission, values)&.dig('uuid') phone_field_uuid = find_phone_field(submission, values)&.dig('uuid')
@@ -214,9 +318,15 @@ module Submissions
def assign_completed_attributes(submitter) def assign_completed_attributes(submitter)
submitter.values = Submitters::SubmitValues.merge_default_values(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) 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| submitter.values = submitter.values.transform_values do |v|
v == '{{date}}' ? Time.current.in_time_zone(submitter.submission.account.timezone).to_date.to_s : v v == '{{date}}' ? Time.current.in_time_zone(submitter.submission.account.timezone).to_date.to_s : v
end end
+36 -21
View File
@@ -37,30 +37,45 @@ module Submissions
submissions.where(created_by_user_id: user&.id || -1) submissions.where(created_by_user_id: user&.id || -1)
end end
# rubocop:disable Metrics/MethodLength
def filter_by_status(submissions, filters) def filter_by_status(submissions, filters)
submissions = submissions.pending if filters[:status] == 'pending' case filters[:status]
submissions = submissions.completed if filters[:status] == 'completed' when 'pending'
submissions = submissions.declined if filters[:status] == 'declined' submissions.pending
submissions = submissions.expired if filters[:status] == 'expired' when 'completed'
submissions.completed
if filters[:status] == 'partially_completed' when 'declined'
submissions = submissions.declined
submissions.joins(:submitters) when 'expired'
.group(:id) submissions.expired
.having(Arel::Nodes::NamedFunction.new( when 'sent'
'COUNT', [Arel::Nodes::NamedFunction.new('NULLIF', submissions.joins(:submitters)
[Submitter.arel_table[:completed_at].eq(nil), .where(submitters: { opened_at: nil, completed_at: nil, declined_at: nil })
Arel::Nodes.build_quoted(false)])] .where.not(submitters: { sent_at: nil })
).gt(0)) .group(:id)
.having(Arel::Nodes::NamedFunction.new( when 'opened'
'COUNT', [Arel::Nodes::NamedFunction.new('NULLIF', submissions.joins(:submitters)
[Submitter.arel_table[:completed_at].not_eq(nil), .where(submitters: { completed_at: nil, declined_at: nil })
Arel::Nodes.build_quoted(false)])] .where.not(submitters: { opened_at: nil })
).gt(0)) .group(:id)
when 'partially_completed'
submissions.joins(:submitters)
.group(:id)
.having(Arel::Nodes::NamedFunction.new(
'COUNT', [Arel::Nodes::NamedFunction.new('NULLIF',
[Submitter.arel_table[:completed_at].eq(nil),
Arel::Nodes.build_quoted(false)])]
).gt(0))
.having(Arel::Nodes::NamedFunction.new(
'COUNT', [Arel::Nodes::NamedFunction.new('NULLIF',
[Submitter.arel_table[:completed_at].not_eq(nil),
Arel::Nodes.build_quoted(false)])]
).gt(0))
else
submissions
end end
submissions
end end
# rubocop:enable Metrics/MethodLength
def filter_by_created_at(submissions, filters) def filter_by_created_at(submissions, filters)
submissions = submissions.where(created_at: filters[:created_at_from]..) if filters[:created_at_from].present? submissions = submissions.where(created_at: filters[:created_at_from]..) if filters[:created_at_from].present?
Loaded 100 of 115 files, more files were not shown because too many files have changed in this diff. Show more