Compare commits

...

104 Commits

Author SHA1 Message Date
Alex Turchyn 93f84abce4 Merge from docusealco/wip 2025-09-29 13:14:29 +03:00
Pete Matsyburka 3c6a32caba payment link 2025-09-29 10:23:11 +03:00
Pete Matsyburka 72c08e5fdb fix settings 2025-09-29 08:59:36 +03:00
Pete Matsyburka 022d80b5ce adjust reminders 2025-09-29 07:31:46 +03:00
Pete Matsyburka f48da51802 10k stars 2025-09-27 16:12:00 +03:00
Pete Matsyburka 709d04b33f toggle submit element 2025-09-27 11:40:16 +03:00
Pete Matsyburka 3a251dd063 development url ttl 2025-09-27 11:08:14 +03:00
Pete Matsyburka 9cc92b59b3 add strikethrough 2025-09-27 10:40:29 +03:00
Pete Matsyburka e0469b10a7 hide mobile folder upload 2025-09-26 09:35:32 +03:00
Pete Matsyburka 39eb67b162 refactor csp 2025-09-26 08:20:03 +03:00
Pete Matsyburka 8d9bea3b0f adjust date format 2025-09-25 20:32:41 +03:00
Pete Matsyburka 7015d8dde8 fix csp 2025-09-25 18:30:07 +03:00
Pete Matsyburka 6b54072cb5 check formula field condition 2025-09-25 14:00:51 +03:00
Pete Matsyburka b898708306 fix formula condition 2025-09-25 13:12:36 +03:00
Pete Matsyburka 1b1cf36839 fix verification step 2025-09-25 10:40:33 +03:00
Pete Matsyburka 18b89edc19 fix search input 2025-09-25 09:01:04 +03:00
Pete Matsyburka 70c7ef4247 adjust reminders 2025-09-25 08:47:31 +03:00
Pete Matsyburka a8196709be remove csp meta tag 2025-09-24 23:23:42 +03:00
Pete Matsyburka a9d249ccf0 fix variables schema 2025-09-24 19:51:37 +03:00
Pete Matsyburka cd83d918e5 fix csp 2025-09-24 15:45:04 +03:00
Pete Matsyburka b64a84a362 add csp 2025-09-24 12:14:18 +03:00
Pete Matsyburka f4426a8ee0 adjust payment title 2025-09-23 10:05:51 +03:00
Pete Matsyburka df8c44d2ce quantity formula 2025-09-23 09:03:43 +03:00
Pete Matsyburka 65ce3d4822 add use direct file links 2025-09-22 17:52:24 +03:00
Alex Turchyn e5b1d94579 Merge from docusealco/wip 2025-09-22 13:11:28 +03:00
Pete Matsyburka 0b4754ca24 update email events index 2025-09-22 08:40:12 +03:00
Pete Matsyburka b3dc8d55be fix submission name 2025-09-19 21:36:05 +03:00
Pete Matsyburka e5ab7667b9 adjust verification step 2025-09-18 14:15:16 +03:00
Pete Matsyburka 2a4e6435f1 adjust api settings 2025-09-18 10:24:38 +03:00
Pete Matsyburka 804431c44a adjust account preferences 2025-09-18 10:05:04 +03:00
Alex Turchyn f3b26bf7a0 add tooltips to account preferences 2025-09-18 09:26:21 +03:00
Pete Matsyburka aa27d90017 update gem 2025-09-18 08:48:48 +03:00
Pete Matsyburka 0fa8cf2dd0 fix result generatrion 2025-09-18 08:33:34 +03:00
Pete Matsyburka 634b5c37ba fix form fields import 2025-09-17 13:26:58 +03:00
Pete Matsyburka 6e16f81fc6 add folder filter 2025-09-16 16:19:36 +03:00
Pete Matsyburka ffc4024f70 rubocop 2025-09-16 13:03:02 +03:00
Alex Turchyn a6981185b2 Merge from docusealco/wip 2025-09-15 13:13:58 +03:00
Pete Matsyburka 0361fabb66 fix combined download 2025-09-15 11:39:43 +03:00
Pete Matsyburka 04e7f101be add lock events 2025-09-15 10:10:39 +03:00
Pete Matsyburka 86d680cfdf fix variables schema 2025-09-15 08:57:54 +03:00
Pete Matsyburka 7bc2152057 expire disk storage urls 2025-09-13 18:10:47 +03:00
Pete Matsyburka 74ed9a5040 update docs 2025-09-13 11:23:34 +03:00
Alex Turchyn 30115fc0dc adjust login OTP form 2025-09-13 09:34:50 +03:00
Pete Matsyburka 8e8e5e0e29 add template variables 2025-09-12 18:22:45 +03:00
Alex Turchyn b58ce1b571 add translations 2025-09-10 08:25:36 +03:00
Pete Matsyburka df5c4e4210 fix email 2fa resubmit 2025-09-09 19:29:09 +03:00
Pete Matsyburka ba2e0999f1 add with file links option 2025-09-09 09:53:21 +03:00
Pete Matsyburka a1f5dd08c7 fix typo 2025-09-08 14:42:25 +03:00
Alex Turchyn c1123fef63 Merge from docusealco/wip 2025-09-08 13:05:04 +03:00
Pete Matsyburka 4dc6149530 add reset password link 2025-09-06 14:46:51 +03:00
Pete Matsyburka 1245ff2cce disable edit password 2025-09-06 10:41:51 +03:00
Pete Matsyburka 284204fd78 refactor, use invalid class 2025-09-06 10:18:01 +03:00
Alex Turchyn f7be74eb73 improve user password reset 2025-09-06 09:21:59 +03:00
Pete Matsyburka aeea619059 skip annotation 2025-09-05 15:30:58 +03:00
Pete Matsyburka ea2b7f20e7 count segments 2025-09-05 14:59:20 +03:00
Alex Turchyn f40f1d25de reveal API key with user password 2025-09-05 09:58:16 +03:00
Pete Matsyburka c04bb2d7cf allow to disable user 2fa 2025-09-04 21:19:43 +03:00
Pete Matsyburka 4c1ccd65bf fix condition field value 2025-09-04 18:16:00 +03:00
Pete Matsyburka 54e064b0fc fix fields sort 2025-09-04 16:13:29 +03:00
Pete Matsyburka 9a8b72883c fix relation 2025-09-03 12:50:16 +03:00
Pete Matsyburka 5d75ee2e47 filter export 2025-09-02 21:17:25 +03:00
Pete Matsyburka 7357fa4871 adjust settings nav links 2025-09-01 18:34:23 +03:00
Pete Matsyburka f6850b5427 add completed submitters verification method 2025-09-01 14:35:56 +03:00
Pete Matsyburka 1e9a181e60 fix typo 2025-09-01 14:01:40 +03:00
Pete Matsyburka 7224a1ccec adjust expire link 2025-09-01 13:45:28 +03:00
Alex Turchyn 9b935a8180 Merge from docusealco/wip 2025-09-01 12:47:09 +03:00
Pete Matsyburka c4a693846e expire download links 2025-09-01 11:59:02 +03:00
Pete Matsyburka 02c7cdcd97 fix rotate 2025-08-28 17:43:39 +03:00
Pete Matsyburka 36bee92e8e fix condition remove 2025-08-28 10:31:19 +03:00
Pete Matsyburka 6542804f44 fix markdown 2025-08-27 22:01:49 +03:00
Pete Matsyburka d805fd614c private template placeholder 2025-08-26 12:38:41 +03:00
Alex Turchyn 21299dc65f Merge pull from docusealco/wip 2025-08-25 12:37:42 +03:00
Pete Matsyburka 0df0946c2a fix markdown 2025-08-25 10:11:36 +03:00
Pete Matsyburka 1bb38d50a1 adjust formula font 2025-08-24 18:16:43 +03:00
Pete Matsyburka 242987cb41 icon size 2025-08-24 14:25:36 +03:00
Pete Matsyburka 5456abde54 adjust date input 2025-08-24 10:08:47 +03:00
Pete Matsyburka 4a07427440 adjust paste radio 2025-08-24 09:27:51 +03:00
Pete Matsyburka cf60dae2ff fix modal title 2025-08-23 21:46:51 +03:00
Pete Matsyburka 659997c6c1 add 2 options by default 2025-08-23 21:33:46 +03:00
Pete Matsyburka 389a47a152 expand options 2025-08-23 21:16:07 +03:00
Pete Matsyburka b6b81ca487 editable readonly custom field 2025-08-23 20:52:57 +03:00
Pete Matsyburka 719c1786f1 set default value 2025-08-23 20:16:14 +03:00
Pete Matsyburka 4685bac4b3 fix serializers 2025-08-23 11:28:26 +03:00
Pete Matsyburka a05275cc08 update json 2025-08-23 10:41:49 +03:00
Pete Matsyburka 1abf1b1658 fix formula 2025-08-22 18:11:09 +03:00
Pete Matsyburka 8893dc5698 pwa orientation any 2025-08-22 18:11:05 +03:00
Pete Matsyburka f8467b2da1 nested formula 2025-08-21 14:36:49 +03:00
Pete Matsyburka f39b8b000e use csv safe 2025-08-21 13:58:53 +03:00
Pete Matsyburka 961036fa31 update widget 2025-08-21 10:22:06 +03:00
Pete Matsyburka 8b9056894e sanitize url 2025-08-20 21:31:15 +03:00
Pete Matsyburka 7f979e9396 add number validation 2025-08-20 18:32:20 +03:00
Pete Matsyburka 8db1192d0e update hexapdf 2025-08-19 16:34:59 +03:00
Pete Matsyburka 3bd515951a stamp without completed 2025-08-19 12:40:45 +03:00
Pete Matsyburka 2bd5b9676b add with verification 2025-08-19 12:25:23 +03:00
Alex Turchyn 993698ec01 Merge from docusealco/wip 2025-08-18 12:33:13 +03:00
Pete Matsyburka fcdc44ddbe update rails 2025-08-14 09:44:04 +03:00
Pete Matsyburka 0cb6b0ae12 fix zip 2025-08-13 11:18:49 +03:00
Pete Matsyburka 0388927c5b fix readonly date 2025-08-12 22:33:38 +03:00
Pete Matsyburka 2c8f5d2129 debug file 2025-08-12 20:01:52 +03:00
Pete Matsyburka 2bffbf83c6 handle zip upload 2025-08-12 13:07:16 +03:00
Pete Matsyburka ec01529014 show readonly fields 2025-08-11 16:58:14 +03:00
Pete Matsyburka ea1abf5662 fix test email 2025-08-08 16:33:19 +03:00
Pete Matsyburka fcd5263af1 add hour minute variable 2025-08-07 11:33:11 +03:00
Pete Matsyburka 1fa8616dbc fix submitters order 2025-08-04 14:46:46 +03:00
211 changed files with 11486 additions and 1365 deletions
+3
View File
@@ -68,6 +68,9 @@ RSpec/ExampleLength:
RSpec/MultipleMemoizedHelpers:
Max: 15
RSpec/AnyInstance:
Enabled: false
Metrics/BlockNesting:
Max: 5
+1 -1
View File
@@ -50,7 +50,7 @@ ENV OPENSSL_CONF=/app/openssl_legacy.cnf
WORKDIR /app
RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev@edge redis libheif@edge vips-heif@edge gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev@edge yaml-dev redis libheif@edge vips-heif@edge gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
\n\
+1
View File
@@ -11,6 +11,7 @@ gem 'azure-storage-blob', require: false
gem 'bootsnap', require: false
gem 'cancancan'
gem 'csv'
gem 'csv-safe'
gem 'devise'
gem 'devise-two-factor'
gem 'dotenv', require: false
+96 -83
View File
@@ -1,29 +1,29 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (8.0.1)
actionpack (= 8.0.1)
activesupport (= 8.0.1)
actioncable (8.0.2.1)
actionpack (= 8.0.2.1)
activesupport (= 8.0.2.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (8.0.1)
actionpack (= 8.0.1)
activejob (= 8.0.1)
activerecord (= 8.0.1)
activestorage (= 8.0.1)
activesupport (= 8.0.1)
actionmailbox (8.0.2.1)
actionpack (= 8.0.2.1)
activejob (= 8.0.2.1)
activerecord (= 8.0.2.1)
activestorage (= 8.0.2.1)
activesupport (= 8.0.2.1)
mail (>= 2.8.0)
actionmailer (8.0.1)
actionpack (= 8.0.1)
actionview (= 8.0.1)
activejob (= 8.0.1)
activesupport (= 8.0.1)
actionmailer (8.0.2.1)
actionpack (= 8.0.2.1)
actionview (= 8.0.2.1)
activejob (= 8.0.2.1)
activesupport (= 8.0.2.1)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (8.0.1)
actionview (= 8.0.1)
activesupport (= 8.0.1)
actionpack (8.0.2.1)
actionview (= 8.0.2.1)
activesupport (= 8.0.2.1)
nokogiri (>= 1.8.5)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
@@ -31,35 +31,35 @@ GEM
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (8.0.1)
actionpack (= 8.0.1)
activerecord (= 8.0.1)
activestorage (= 8.0.1)
activesupport (= 8.0.1)
actiontext (8.0.2.1)
actionpack (= 8.0.2.1)
activerecord (= 8.0.2.1)
activestorage (= 8.0.2.1)
activesupport (= 8.0.2.1)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (8.0.1)
activesupport (= 8.0.1)
actionview (8.0.2.1)
activesupport (= 8.0.2.1)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (8.0.1)
activesupport (= 8.0.1)
activejob (8.0.2.1)
activesupport (= 8.0.2.1)
globalid (>= 0.3.6)
activemodel (8.0.1)
activesupport (= 8.0.1)
activerecord (8.0.1)
activemodel (= 8.0.1)
activesupport (= 8.0.1)
activemodel (8.0.2.1)
activesupport (= 8.0.2.1)
activerecord (8.0.2.1)
activemodel (= 8.0.2.1)
activesupport (= 8.0.2.1)
timeout (>= 0.4.0)
activestorage (8.0.1)
actionpack (= 8.0.1)
activejob (= 8.0.1)
activerecord (= 8.0.1)
activesupport (= 8.0.1)
activestorage (8.0.2.1)
actionpack (= 8.0.2.1)
activejob (= 8.0.2.1)
activerecord (= 8.0.2.1)
activesupport (= 8.0.2.1)
marcel (~> 1.0)
activesupport (8.0.1)
activesupport (8.0.2.1)
base64
benchmark (>= 0.3)
bigdecimal
@@ -104,9 +104,9 @@ GEM
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
net-http-persistent (~> 4.0)
nokogiri (~> 1, >= 1.10.8)
base64 (0.2.0)
base64 (0.3.0)
bcrypt (3.1.20)
benchmark (0.4.0)
benchmark (0.4.1)
better_html (2.1.1)
actionview (>= 6.0)
activesupport (>= 6.0)
@@ -114,7 +114,7 @@ GEM
erubi (~> 1.4)
parser (>= 2.4)
smart_properties
bigdecimal (3.1.8)
bigdecimal (3.2.2)
bindex (0.8.1)
bootsnap (1.18.4)
msgpack (~> 1.2)
@@ -141,8 +141,8 @@ GEM
cldr-plurals-runtime-rb (1.1.0)
cmdparse (3.0.7)
coderay (1.1.3)
concurrent-ruby (1.3.4)
connection_pool (2.4.1)
concurrent-ruby (1.3.5)
connection_pool (2.5.3)
crack (1.0.0)
bigdecimal
rexml
@@ -150,6 +150,8 @@ GEM
css_parser (1.21.0)
addressable
csv (3.3.2)
csv-safe (3.3.1)
csv (~> 3.0)
cuprite (0.15.1)
capybara (~> 3.0)
ferrum (~> 0.15.0)
@@ -174,8 +176,9 @@ GEM
rake (>= 12.0.0, < 14.0.0)
docile (1.4.1)
dotenv (3.1.7)
drb (2.2.1)
drb (2.2.3)
email_typo (0.2.3)
erb (5.0.2)
erb_lint (0.7.0)
activesupport
better_html (>= 2.0.1)
@@ -270,23 +273,25 @@ GEM
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
hashdiff (1.1.2)
hexapdf (1.0.3)
hexapdf (1.4.0)
cmdparse (~> 3.0, >= 3.0.3)
geom2d (~> 0.4, >= 0.4.1)
openssl (>= 2.2.1)
strscan (>= 3.1.2)
htmlentities (4.3.4)
httpclient (2.8.3)
i18n (1.14.6)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
image_processing (1.13.0)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
io-console (0.8.0)
irb (1.14.3)
io-console (0.8.1)
irb (1.15.2)
pp (>= 0.6.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jmespath (1.6.2)
json (2.9.1)
json (2.13.2)
jwt (2.9.3)
base64
language_server-protocol (3.17.0.3)
@@ -300,13 +305,13 @@ GEM
letter_opener (~> 1.9)
railties (>= 6.1)
rexml
logger (1.6.4)
logger (1.7.0)
lograge (0.14.0)
actionpack (>= 4)
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
loofah (2.23.1)
loofah (2.24.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
@@ -320,7 +325,7 @@ GEM
mini_magick (4.13.2)
mini_mime (1.1.5)
mini_portile2 (2.8.9)
minitest (5.25.4)
minitest (5.25.5)
msgpack (1.7.5)
multi_json (1.15.0)
multipart-post (2.4.1)
@@ -328,14 +333,14 @@ GEM
mysql2 (0.5.6)
net-http-persistent (4.0.5)
connection_pool (~> 2.2)
net-imap (0.5.6)
net-imap (0.5.9)
date
net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.2.2)
timeout
net-smtp (0.5.0)
net-smtp (0.5.1)
net-protocol
nio4r (2.7.4)
nokogiri (1.18.9)
@@ -351,13 +356,13 @@ GEM
racc (~> 1.4)
nokogiri (1.18.9-x86_64-linux-musl)
racc (~> 1.4)
oj (3.16.8)
oj (3.16.11)
bigdecimal (>= 3.0)
ostruct (>= 0.2)
openssl (3.3.0)
orm_adapter (0.5.0)
os (1.1.4)
ostruct (0.6.1)
ostruct (0.6.3)
package_json (0.1.0)
pagy (9.3.3)
parallel (1.26.3)
@@ -365,6 +370,8 @@ GEM
ast (~> 2.4.1)
racc
pg (1.5.9)
pp (0.6.2)
prettyprint
premailer (1.27.0)
addressable
css_parser (>= 1.19.0)
@@ -375,42 +382,44 @@ GEM
premailer (~> 1.7, >= 1.7.9)
pretender (0.5.0)
actionpack (>= 6.1)
prettyprint (0.2.0)
pry (0.15.0)
coderay (~> 1.1)
method_source (~> 1.0)
pry-rails (0.3.11)
pry (>= 0.13.0)
psych (5.2.2)
psych (5.2.6)
date
stringio
public_suffix (6.0.1)
puma (6.5.0)
nio4r (~> 2.0)
racc (1.8.1)
rack (3.1.16)
rack (3.2.0)
rack-proxy (0.7.7)
rack
rack-session (2.0.0)
rack-session (2.1.1)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.1.0)
rack-test (2.2.0)
rack (>= 1.3)
rackup (2.2.1)
rack (>= 3)
rails (8.0.1)
actioncable (= 8.0.1)
actionmailbox (= 8.0.1)
actionmailer (= 8.0.1)
actionpack (= 8.0.1)
actiontext (= 8.0.1)
actionview (= 8.0.1)
activejob (= 8.0.1)
activemodel (= 8.0.1)
activerecord (= 8.0.1)
activestorage (= 8.0.1)
activesupport (= 8.0.1)
rails (8.0.2.1)
actioncable (= 8.0.2.1)
actionmailbox (= 8.0.2.1)
actionmailer (= 8.0.2.1)
actionpack (= 8.0.2.1)
actiontext (= 8.0.2.1)
actionview (= 8.0.2.1)
activejob (= 8.0.2.1)
activemodel (= 8.0.2.1)
activerecord (= 8.0.2.1)
activestorage (= 8.0.2.1)
activesupport (= 8.0.2.1)
bundler (>= 1.15.0)
railties (= 8.0.1)
rails-dom-testing (2.2.0)
railties (= 8.0.2.1)
rails-dom-testing (2.3.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
@@ -424,22 +433,23 @@ GEM
actionview (> 3.1)
activesupport (> 3.1)
railties (> 3.1)
railties (8.0.1)
actionpack (= 8.0.1)
activesupport (= 8.0.1)
railties (8.0.2.1)
actionpack (= 8.0.2.1)
activesupport (= 8.0.2.1)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.2.1)
rdoc (6.10.0)
rake (13.3.0)
rdoc (6.14.2)
erb
psych (>= 4.0.0)
redis-client (0.23.0)
connection_pool
regexp_parser (2.9.3)
reline (0.6.0)
reline (0.6.2)
io-console (~> 0.5)
representable (3.2.0)
declarative (< 0.1.0)
@@ -451,7 +461,7 @@ GEM
actionpack (>= 5.2)
railties (>= 5.2)
retriable (3.1.2)
rexml (3.4.0)
rexml (3.4.4)
rotp (6.3.0)
rouge (4.5.2)
rqrcode (2.2.0)
@@ -538,9 +548,10 @@ GEM
sqlite3 (2.5.0-arm64-darwin)
sqlite3 (2.5.0-x86_64-linux-gnu)
sqlite3 (2.5.0-x86_64-linux-musl)
stringio (3.1.2)
stringio (3.1.7)
strip_attributes (1.14.1)
activemodel (>= 3.0, < 9.0)
strscan (3.1.5)
thor (1.4.0)
timeout (0.4.3)
trailblazer-option (0.1.2)
@@ -574,12 +585,13 @@ GEM
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.9.1)
websocket-driver (0.7.6)
websocket-driver (0.8.0)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.7.1)
zeitwerk (2.7.3)
PLATFORMS
aarch64-linux
@@ -602,6 +614,7 @@ DEPENDENCIES
cancancan
capybara
csv
csv-safe
cuprite
debug
devise
@@ -15,9 +15,11 @@ class AccountConfigsController < ApplicationController
AccountConfig::ESIGNING_PREFERENCE_KEY,
AccountConfig::FORM_WITH_CONFETTI_KEY,
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY,
AccountConfig::FORCE_SSO_AUTH_KEY,
AccountConfig::FLATTEN_RESULT_PDF_KEY,
AccountConfig::ENFORCE_SIGNING_ORDER_KEY,
AccountConfig::WITH_FILE_LINKS_KEY,
AccountConfig::WITH_SIGNATURE_ID,
AccountConfig::COMBINE_PDF_RESULT_KEY,
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
+2 -2
View File
@@ -27,7 +27,7 @@ class AccountsController < ApplicationController
unless URI.parse(@encrypted_config.value.to_s).class.in?([URI::HTTP, URI::HTTPS])
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url'))
return render :show, status: :unprocessable_entity
return render :show, status: :unprocessable_content
end
@encrypted_config.save!
@@ -39,7 +39,7 @@ class AccountsController < ApplicationController
redirect_to settings_account_path, notice: I18n.t('account_information_has_been_updated')
end
rescue ActiveRecord::RecordInvalid
render :show, status: :unprocessable_entity
render :show, status: :unprocessable_content
end
def destroy
@@ -13,7 +13,7 @@ module Api
def show
blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
if blob_uuid.blank? || (purp.present? && purp != 'blob') || (exp && exp < Time.current.to_i)
if blob_uuid.blank? || purp != 'blob'
Rollbar.error('Blob not found') if defined?(Rollbar)
return head :not_found
@@ -24,8 +24,9 @@ module Api
attachment = blob.attachments.take
@record = attachment.record
@record = @record.record if @record.is_a?(ActiveStorage::Attachment)
authorization_check!(attachment) if exp.blank?
authorization_check!(attachment, @record, exp)
if request.headers['Range'].present?
send_blob_byte_range_data blob, request.headers['Range']
@@ -41,16 +42,22 @@ module Api
private
def authorization_check!(attachment)
is_authorized = attachment.name.in?(%w[logo preview_images]) ||
(current_user && attachment.record.account.id == current_user.account_id) ||
(current_user && !Docuseal.multitenant? && current_user.role == 'superadmin') ||
!attachment.record.account.account_configs
.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value
def authorization_check!(attachment, record, exp)
return if attachment.name == 'logo'
return if exp.to_i >= Time.current.to_i
return if current_user && current_ability.can?(:read, record)
return if is_authorized
if exp.blank?
configs = record.account.account_configs.where(key: [AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY])
Rollbar.error('Blob aunauthorized') if defined?(Rollbar)
require_auth = configs.any? { |c| c.key == AccountConfig::DOWNLOAD_LINKS_AUTH_KEY && c.value }
require_ttl = configs.none? { |c| c.key == AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY && c.value == false }
return if !require_ttl && !require_auth
end
Rollbar.error('Blob unauthorized') if defined?(Rollbar)
raise CanCan::AccessDenied
end
+2 -2
View File
@@ -16,7 +16,7 @@ module Api
check_authorization
rescue_from Params::BaseValidator::InvalidParameterError do |e|
render json: { error: e.message }, status: :unprocessable_entity
render json: { error: e.message }, status: :unprocessable_content
end
rescue_from RateLimit::LimitApproached do |e|
@@ -33,7 +33,7 @@ module Api
rescue_from JSON::ParserError do |e|
Rollbar.warning(e) if defined?(Rollbar)
render json: { error: "JSON parse error: #{e.message}" }, status: :unprocessable_entity
render json: { error: "JSON parse error: #{e.message}" }, status: :unprocessable_content
end
end
@@ -16,14 +16,14 @@ module Api
if ImageUtils.blank?(image)
Rollbar.error("Empty signature: #{submitter.id}") if defined?(Rollbar)
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_entity
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_content
end
if ImageUtils.error?(image)
Rollbar.error("Error signature: #{submitter.id}") if defined?(Rollbar)
return render json: { error: "#{params[:type]} error, try to sign on another device" },
status: :unprocessable_entity
status: :unprocessable_content
end
end
@@ -18,12 +18,14 @@ module Api
field: :completed_at
)
expires_at = Accounts.link_expires_at(current_account)
render json: {
data: submitters.map do |s|
{
event_type: 'form.completed',
timestamp: s.completed_at,
data: Submitters::SerializeForWebhook.call(s)
data: Submitters::SerializeForWebhook.call(s, expires_at:)
}
end,
pagination: {
@@ -34,10 +34,12 @@ module Api
associations: [:blob]
).call
expires_at = Accounts.link_expires_at(current_account)
render json: {
id: @submission.id,
documents: documents.map do |attachment|
{ name: attachment.filename.base, url: ActiveStorage::Blob.proxy_url(attachment.blob) }
{ name: attachment.filename.base, url: ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) }
end
}
end
@@ -14,16 +14,19 @@ module Api
:created_by_user, :submission_events,
template: :folder,
submitters: { documents_attachments: :blob, attachments_attachments: :blob },
audit_trail_attachment: :blob
audit_trail_attachment: :blob,
combined_document_attachment: :blob
),
field: :completed_at)
expires_at = Accounts.link_expires_at(current_account)
render json: {
data: submissions.map do |s|
{
event_type: 'submission.completed',
timestamp: s.completed_at,
data: Submissions::SerializeForApi.call(s, s.submitters)
data: Submissions::SerializeForApi.call(s, s.submitters, expires_at:)
}
end,
pagination: {
@@ -18,10 +18,12 @@ module Api
combined_document_attachment: :blob,
audit_trail_attachment: :blob))
expires_at = Accounts.link_expires_at(current_account)
render json: {
data: submissions.map do |s|
Submissions::SerializeForApi.call(s, s.submitters, params,
with_events: false, with_documents: false, with_values: false)
with_events: false, with_documents: false, with_values: false, expires_at:)
end,
pagination: {
count: submissions.size,
@@ -41,7 +43,7 @@ module Api
end
if @submission.audit_trail_attachment.blank? && submitters.all?(&:completed_at?)
@submission.audit_trail_attachment = Submissions::GenerateAuditTrail.call(@submission)
@submission.audit_trail_attachment = Submissions::EnsureAuditGenerated.call(@submission)
end
render json: Submissions::SerializeForApi.call(@submission, submitters, params)
@@ -50,12 +52,12 @@ module Api
def create
Params::SubmissionCreateValidator.call(params)
return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil?
return render json: { error: 'Template not found' }, status: :unprocessable_content if @template.nil?
if @template.fields.blank?
Rollbar.warning("Template does not contain fields: #{@template.id}") if defined?(Rollbar)
return render json: { error: 'Template does not contain fields' }, status: :unprocessable_entity
return render json: { error: 'Template does not contain fields' }, status: :unprocessable_content
end
params[:send_email] = true unless params.key?(:send_email)
@@ -82,7 +84,7 @@ module Api
DownloadUtils::UnableToDownload => e
Rollbar.warning(e) if defined?(Rollbar)
render json: { error: e.message }, status: :unprocessable_entity
render json: { error: e.message }, status: :unprocessable_content
end
def destroy
@@ -182,6 +184,7 @@ module Api
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
:require_phone_2fa, :expire_at, :name,
{
variables: {},
message: %i[subject body],
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
@@ -189,7 +192,7 @@ module Api
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
fields: [:name, :uuid, :default_value, :value, :title, :description,
:readonly, :required, :validation_pattern, :invalid_message,
{ default_value: [], value: [], preferences: {} }] }]]
{ default_value: [], value: [], preferences: {}, validation: {} }] }]]
}
]
+5 -3
View File
@@ -14,9 +14,11 @@ module Api
documents_attachments: :blob, attachments_attachments: :blob)
)
expires_at = Accounts.link_expires_at(current_account)
render json: {
data: submitters.map do |s|
Submitters::SerializeForApi.call(s, with_template: true, with_events: true, params:)
Submitters::SerializeForApi.call(s, with_template: true, with_events: true, params:, expires_at:)
end,
pagination: {
count: submitters.size,
@@ -34,7 +36,7 @@ module Api
def update
if @submitter.completed_at?
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_entity
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_content
end
submission = @submitter.submission
@@ -74,7 +76,7 @@ module Api
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
Rollbar.warning(e) if defined?(Rollbar)
render json: { error: e.message }, status: :unprocessable_entity
render json: { error: e.message }, status: :unprocessable_content
end
def submitter_params
@@ -26,13 +26,15 @@ module Api
original_template: @template,
documents: params[:documents])
Templates.maybe_assign_access(cloned_template)
cloned_template.save!
WebhookUrls.enqueue_events(cloned_template, 'template.created')
SearchEntries.enqueue_reindex(cloned_template)
render json: Templates::SerializeForApi.call(cloned_template, schema_documents)
render json: Templates::SerializeForApi.call(cloned_template, schema_documents:)
end
end
end
+8 -6
View File
@@ -24,13 +24,14 @@ module Api
name: :preview_images)
.preload(:blob)
expires_at = Accounts.link_expires_at(current_account)
render json: {
data: templates.map do |t|
Templates::SerializeForApi.call(
t,
schema_documents.select { |e| e.record_id == t.id },
preview_image_attachments
)
Templates::SerializeForApi.call(t,
schema_documents: schema_documents.select { |e| e.record_id == t.id },
preview_image_attachments:,
expires_at:)
end,
pagination: {
count: templates.size,
@@ -111,9 +112,10 @@ module Api
:required, :readonly, :default_value,
:title, :description, :prefillable,
{ preferences: {},
default_value: [],
conditions: [%i[field_uuid value action operation]],
options: [%i[value uuid]],
validation: %i[message pattern],
validation: %i[message pattern min max step],
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
}
]
+3 -3
View File
@@ -7,8 +7,8 @@ module Api
def merge
files = params[:files] || []
return render json: { error: 'Files are required' }, status: :unprocessable_entity if files.blank?
return render json: { error: 'At least 2 files are required' }, status: :unprocessable_entity if files.size < 2
return render json: { error: 'Files are required' }, status: :unprocessable_content if files.blank?
return render json: { error: 'At least 2 files are required' }, status: :unprocessable_content if files.size < 2
render json: {
data: Base64.encode64(PdfUtils.merge(files.map { |base64| StringIO.new(Base64.decode64(base64)) }).string)
@@ -35,7 +35,7 @@ module Api
end
}
rescue HexaPDF::MalformedPDFError
render json: { error: 'Malformed PDF' }, status: :unprocessable_entity
render json: { error: 'Malformed PDF' }, status: :unprocessable_content
end
end
end
+19
View File
@@ -13,6 +13,8 @@ class ApplicationController < ActionController::Base
before_action :maybe_redirect_to_setup, unless: :signed_in?
before_action :authenticate_user!, unless: :devise_controller?
before_action :set_csp, if: -> { request.get? && !request.headers['HTTP_X_TURBO'] }
helper_method :button_title,
:current_account,
:form_link_host,
@@ -123,4 +125,21 @@ class ApplicationController < ActionController::Base
redirect_to request.url.gsub('.co/', '.com/'), allow_other_host: true, status: :moved_permanently
end
def set_csp
request.content_security_policy = current_content_security_policy.tap do |policy|
policy.default_src :self
policy.script_src :self
policy.style_src :self, :unsafe_inline
policy.img_src :self, :https, :http, :blob, :data
policy.font_src :self, :https, :http, :blob, :data
policy.manifest_src :self
policy.media_src :self
policy.frame_src :self
policy.worker_src :self, :blob
policy.connect_src :self
policy.directives['connect-src'] << 'ws:' if Rails.env.development?
end
end
end
@@ -15,12 +15,12 @@ class EmailSmtpSettingsController < ApplicationController
redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved')
else
render :index, status: :unprocessable_entity
render :index, status: :unprocessable_content
end
rescue StandardError => e
flash[:alert] = e.message
render :index, status: :unprocessable_entity
render :index, status: :unprocessable_content
end
private
+2 -2
View File
@@ -53,7 +53,7 @@ class EsignSettingsController < ApplicationController
@cert_record.errors.add(:name, I18n.t('already_exists'))
return render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'),
status: :unprocessable_entity
status: :unprocessable_content
end
save_new_cert!(@encrypted_config, @cert_record)
@@ -64,7 +64,7 @@ class EsignSettingsController < ApplicationController
@cert_record.errors.add(:password, e.message)
render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), status: :unprocessable_entity
render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), status: :unprocessable_content
end
def update
+2 -2
View File
@@ -24,7 +24,7 @@ class MfaSetupController < ApplicationController
@error_message = I18n.t('code_is_invalid')
render turbo_stream: turbo_stream.replace(:mfa_form, partial: 'mfa_setup/form'), status: :unprocessable_entity
render turbo_stream: turbo_stream.replace(:mfa_form, partial: 'mfa_setup/form'), status: :unprocessable_content
end
end
@@ -36,7 +36,7 @@ class MfaSetupController < ApplicationController
else
@error_message = I18n.t('code_is_invalid')
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_entity
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_content
end
end
+10
View File
@@ -1,6 +1,10 @@
# frozen_string_literal: true
class PasswordsController < Devise::PasswordsController
# rubocop:disable Rails/LexicallyScopedActionFilter
skip_before_action :require_no_authentication, only: %i[edit update]
# rubocop:enable Rails/LexicallyScopedActionFilter
class Current < ActiveSupport::CurrentAttributes
attribute :user
end
@@ -16,4 +20,10 @@ class PasswordsController < Devise::PasswordsController
Current.user = resource
end
end
private
def after_resetting_password_path_for(_)
new_session_path(resource_name)
end
end
+4 -4
View File
@@ -11,16 +11,16 @@ class ProfileController < ApplicationController
if current_user.update(contact_params)
redirect_to settings_profile_index_path, notice: I18n.t('contact_information_has_been_update')
else
render :index, status: :unprocessable_entity
render :index, status: :unprocessable_content
end
end
def update_password
if current_user.update(password_params)
if current_user.update_with_password(password_params)
bypass_sign_in(current_user)
redirect_to settings_profile_index_path, notice: I18n.t('password_has_been_changed')
else
render :index, status: :unprocessable_entity
render :index, status: :unprocessable_content
end
end
@@ -31,6 +31,6 @@ class ProfileController < ApplicationController
end
def password_params
params.require(:user).permit(:password, :password_confirmation)
params.require(:user).permit(:password, :password_confirmation, :current_password)
end
end
@@ -0,0 +1,21 @@
# frozen_string_literal: true
class RevealAccessTokenController < ApplicationController
def show
authorize!(:manage, current_user.access_token)
end
def create
authorize!(:manage, current_user.access_token)
if current_user.valid_password?(params[:password])
render turbo_stream: turbo_stream.replace(:access_token_container,
partial: 'reveal_access_token/access_token',
locals: { token: current_user.access_token.token })
else
render turbo_stream: turbo_stream.replace(:modal, template: 'reveal_access_token/show',
locals: { error_message: I18n.t('wrong_password') }),
status: :unprocessable_content
end
end
end
+1 -1
View File
@@ -16,7 +16,7 @@ class SessionsController < Devise::SessionsController
end
if User.exists?(email:, otp_required_for_login: true) && sign_in_params[:otp_attempt].blank?
return render :otp, locals: { resource: User.new(sign_in_params) }, status: :unprocessable_entity
return render :otp, locals: { resource: User.new(sign_in_params) }, status: :unprocessable_content
end
super
+3 -3
View File
@@ -23,10 +23,10 @@ class SetupController < ApplicationController
unless URI.parse(encrypted_config_params[:value].to_s).class.in?([URI::HTTP, URI::HTTPS])
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url'))
return render :index, status: :unprocessable_entity
return render :index, status: :unprocessable_content
end
return render :index, status: :unprocessable_entity unless @account.valid?
return render :index, status: :unprocessable_content unless @account.valid?
if @user.save
encrypted_configs = [
@@ -42,7 +42,7 @@ class SetupController < ApplicationController
redirect_to newsletter_path
else
render :index, status: :unprocessable_entity
render :index, status: :unprocessable_content
end
end
+4 -4
View File
@@ -42,7 +42,7 @@ class StartFormController < ApplicationController
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record?
@error_message = multiple_submitters_error_message
return render :show, status: :unprocessable_entity
return render :show, status: :unprocessable_content
end
if (is_new_record = @submitter.new_record?)
@@ -60,7 +60,7 @@ class StartFormController < ApplicationController
redirect_to submit_form_path(@submitter.slug)
else
render :show, status: :unprocessable_entity
render :show, status: :unprocessable_content
end
end
end
@@ -206,7 +206,7 @@ class StartFormController < ApplicationController
end
def handle_require_2fa(submitter, is_new_record:)
return render :show, status: :unprocessable_entity if submitter.errors.present?
return render :show, status: :unprocessable_content if submitter.errors.present?
is_otp_verified = Submitters.verify_link_otp!(params[:one_time_code], submitter)
@@ -223,7 +223,7 @@ class StartFormController < ApplicationController
redirect_to submit_form_path(submitter.slug)
else
render :show, status: :unprocessable_entity
render :show, status: :unprocessable_content
end
else
Submitters.send_shared_link_email_verification_code(submitter, request:)
@@ -13,7 +13,7 @@ class StorageSettingsController < ApplicationController
redirect_to settings_storage_index_path, notice: I18n.t('changes_have_been_saved')
else
render :index, status: :unprocessable_entity
render :index, status: :unprocessable_content
end
end
+1 -1
View File
@@ -66,7 +66,7 @@ class SubmissionsController < ApplicationController
rescue Submissions::CreateFromSubmitters::BaseError => e
render turbo_stream: turbo_stream.replace(:submitters_error, partial: 'submissions/error',
locals: { error: e.message }),
status: :unprocessable_entity
status: :unprocessable_content
end
def destroy
@@ -70,7 +70,7 @@ class SubmissionsDownloadController < ApplicationController
return if submitter.submission.submitters.order(:completed_at).last != submitter
attachment = submitter.submission.combined_document_attachment
attachment ||= Submissions::GenerateCombinedAttachment.call(submitter)
attachment ||= Submissions::EnsureCombinedGenerated.call(submitter)
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
@@ -10,11 +10,16 @@ class SubmissionsExportController < ApplicationController
attachments_attachments: :blob })
.order(id: :asc)
submissions = Submissions.search(current_user, submissions, params[:q], search_values: true)
submissions = Submissions::Filter.call(submissions, current_user, params)
expires_at = Accounts.link_expires_at(current_account)
if params[:format] == 'csv'
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format]),
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format], expires_at:),
filename: "#{@template.name}.csv"
elsif params[:format] == 'xlsx'
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format]),
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format], expires_at:),
filename: "#{@template.name}.xlsx"
end
end
@@ -3,6 +3,7 @@
class SubmissionsFiltersController < ApplicationController
ALLOWED_NAMES = %w[
author
folder
completed_at
status
created_at
+6 -6
View File
@@ -48,20 +48,20 @@ class SubmitFormController < ApplicationController
def update
if @submitter.completed_at?
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_entity
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_content
end
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at?
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_entity
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content
end
if @submitter.submission.expired?
return render json: { error: I18n.t('form_has_been_expired') }, status: :unprocessable_entity
return render json: { error: I18n.t('form_has_been_expired') }, status: :unprocessable_content
end
if @submitter.declined_at?
return render json: { error: I18n.t('form_has_been_declined') },
status: :unprocessable_entity
status: :unprocessable_content
end
Submitters::SubmitValues.call(@submitter, params, request)
@@ -70,9 +70,9 @@ class SubmitFormController < ApplicationController
rescue Submitters::SubmitValues::RequiredFieldError => e
Rollbar.warning("Required field #{@submitter.id}: #{e.message}") if defined?(Rollbar)
render json: { field_uuid: e.message }, status: :unprocessable_entity
render json: { field_uuid: e.message }, status: :unprocessable_content
rescue Submitters::SubmitValues::ValidationError => e
render json: { error: e.message }, status: :unprocessable_entity
render json: { error: e.message }, status: :unprocessable_content
end
def completed; end
@@ -11,13 +11,13 @@ class SubmitFormDownloadController < ApplicationController
return redirect_to submitter_download_index_path(@submitter.slug) if @submitter.completed_at?
return head :unprocessable_entity if @submitter.declined_at? ||
@submitter.submission.archived_at? ||
@submitter.submission.expired? ||
@submitter.submission.template&.archived_at? ||
AccountConfig.exists?(account_id: @submitter.account_id,
key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
value: false)
return head :unprocessable_content if @submitter.declined_at? ||
@submitter.submission.archived_at? ||
@submitter.submission.expired? ||
@submitter.submission.template&.archived_at? ||
AccountConfig.exists?(account_id: @submitter.account_id,
key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
value: false)
last_completed_submitter = @submitter.submission.submitters
.where.not(id: @submitter.id)
@@ -7,7 +7,7 @@ class SubmitFormInviteController < ApplicationController
def create
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return head :unprocessable_entity unless can_invite?(submitter)
return head :unprocessable_content unless can_invite?(submitter)
invite_submitters = filter_invite_submitters(submitter, 'invite_by_uuid')
optional_invite_submitters = filter_invite_submitters(submitter, 'optional_invite_by_uuid')
@@ -34,7 +34,7 @@ class SubmitFormInviteController < ApplicationController
head :ok
else
head :unprocessable_entity
head :unprocessable_content
end
end
@@ -5,7 +5,7 @@ class TemplateDocumentsController < ApplicationController
def create
if params[:blobs].blank? && params[:files].blank?
return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_entity
return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_content
end
old_fields_hash = @template.fields.hash
@@ -28,6 +28,6 @@ class TemplateDocumentsController < ApplicationController
)
}
rescue Templates::CreateAttachments::PdfEncrypted
render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_entity
render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_content
end
end
@@ -4,7 +4,7 @@ class TemplatesCloneAndReplaceController < ApplicationController
load_and_authorize_resource :template
def create
return head :unprocessable_entity if params[:files].blank?
return head :unprocessable_content if params[:files].blank?
ActiveRecord::Associations::Preloader.new(
records: [@template],
@@ -17,6 +17,8 @@ class TemplatesCloneAndReplaceController < ApplicationController
documents = Templates::ReplaceAttachments.call(cloned_template, params, extract_fields: true)
Templates.maybe_assign_access(cloned_template)
cloned_template.save!
Templates::CloneAttachments.call(template: cloned_template, original_template: @template,
@@ -31,7 +33,7 @@ class TemplatesCloneAndReplaceController < ApplicationController
rescue Templates::CreateAttachments::PdfEncrypted
respond_to do |f|
f.html { render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password) }
f.json { render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_entity }
f.json { render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_content }
end
end
end
+5 -2
View File
@@ -69,6 +69,8 @@ class TemplatesController < ApplicationController
@template.account = current_account
end
Templates.maybe_assign_access(@template)
if @template.save
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
@@ -78,7 +80,7 @@ class TemplatesController < ApplicationController
maybe_redirect_to_template(@template)
else
render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_entity
render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_content
end
end
@@ -122,9 +124,10 @@ class TemplatesController < ApplicationController
:required, :readonly, :default_value,
:title, :description, :prefillable,
{ preferences: {},
default_value: [],
conditions: [%i[field_uuid value action operation]],
options: [%i[value uuid]],
validation: %i[message pattern],
validation: %i[message pattern min max step],
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
)
end
@@ -3,6 +3,8 @@
class TemplatesDebugController < ApplicationController
load_and_authorize_resource :template
DEBUG_FILE = ''
def show
attachment = @template.documents.first
@@ -16,6 +18,8 @@ class TemplatesDebugController < ApplicationController
@template.update!(fields: Templates::ProcessDocument.normalize_attachment_fields(@template, [attachment]))
debug_file if DEBUG_FILE.present?
ActiveRecord::Associations::Preloader.new(
records: [@template],
associations: [schema_documents: { preview_images_attachments: :blob }]
@@ -31,4 +35,27 @@ class TemplatesDebugController < ApplicationController
render 'templates/edit', layout: 'plain'
end
def debug_file
tempfile = Tempfile.new
tempfile.binmode
tempfile.write(File.read(DEBUG_FILE))
tempfile.rewind
filename = File.basename(DEBUG_FILE)
file = ActionDispatch::Http::UploadedFile.new(
tempfile:,
filename:,
type: Marcel::MimeType.for(tempfile)
)
params = { files: [file] }
documents = Templates::CreateAttachments.call(@template, params)
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
@template.update!(schema:)
end
end
@@ -46,6 +46,8 @@ class TemplatesUploadsController < ApplicationController
template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
template.name = File.basename((url_params || params)[:files].first.original_filename, '.*')
Templates.maybe_assign_access(template)
template.save!
template
+6 -5
View File
@@ -27,9 +27,10 @@ class UsersController < ApplicationController
if User.accessible_by(current_ability).exists?(email: @user.email)
@user.errors.add(:email, I18n.t('already_exists'))
return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity
return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_content
end
@user.password = SecureRandom.hex if @user.password.blank?
@user.role = User::ADMIN_ROLE unless role_valid?(@user.role)
if @user.save
@@ -37,7 +38,7 @@ class UsersController < ApplicationController
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_invited')
else
render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity
render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_content
end
end
@@ -54,10 +55,10 @@ class UsersController < ApplicationController
@user.account = account
end
if @user.update(attrs.except(current_user == @user ? :role : nil))
if @user.update(attrs.except(*(current_user == @user ? %i[password otp_required_for_login role] : %i[password])))
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_updated')
else
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_content
end
end
@@ -83,7 +84,7 @@ class UsersController < ApplicationController
def user_params
if params.key?(:user)
permitted_params = %i[email first_name last_name password archived_at]
permitted_params = %i[email first_name last_name password archived_at otp_required_for_login]
permitted_params << :role if role_valid?(params.dig(:user, :role))
@@ -0,0 +1,20 @@
# frozen_string_literal: true
class UsersSendResetPasswordController < ApplicationController
load_and_authorize_resource :user
LIMIT_DURATION = 10.minutes
def update
authorize!(:manage, @user)
if @user.reset_password_sent_at && @user.reset_password_sent_at > LIMIT_DURATION.ago
redirect_back fallback_location: settings_users_path, notice: I18n.t('email_has_been_sent_already')
else
@user.send_reset_password_instructions
redirect_back fallback_location: settings_users_path,
notice: I18n.t('an_email_with_password_reset_instructions_has_been_sent')
end
end
end
+23
View File
@@ -23,6 +23,7 @@ import SignatureForm from './elements/signature_form'
import SubmitForm from './elements/submit_form'
import PromptPassword from './elements/prompt_password'
import EmailsTextarea from './elements/emails_textarea'
import ToggleSubmit from './elements/toggle_submit'
import ToggleOnSubmit from './elements/toggle_on_submit'
import CheckOnClick from './elements/check_on_click'
import PasswordInput from './elements/password_input'
@@ -34,11 +35,20 @@ import MaskedInput from './elements/masked_input'
import SetDateButton from './elements/set_date_button'
import IndeterminateCheckbox from './elements/indeterminate_checkbox'
import AppTour from './elements/app_tour'
import AppTourStart from './elements/app_tour_start'
import DashboardDropzone from './elements/dashboard_dropzone'
import RequiredCheckboxGroup from './elements/required_checkbox_group'
import PageContainer from './elements/page_container'
import EmailEditor from './elements/email_editor'
import MountOnClick from './elements/mount_on_click'
import RemoveOnEvent from './elements/remove_on_event'
import ScrollTo from './elements/scroll_to'
import SetValue from './elements/set_value'
import ReviewForm from './elements/review_form'
import ShowOnValue from './elements/show_on_value'
import CustomValidation from './elements/custom_validation'
import ToggleClasses from './elements/toggle_classes'
import AutosizeField from './elements/autosize_field'
import * as TurboInstantClick from './lib/turbo_instant_click'
@@ -55,6 +65,9 @@ document.addEventListener('keyup', (e) => {
})
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
document.addEventListener('turbo:before-fetch-request', (event) => {
event.detail.fetchOptions.headers['X-Turbo'] = 'true'
})
document.addEventListener('turbo:submit-end', async (event) => {
const resp = event.detail?.formSubmission?.result?.fetchResponse?.response
@@ -97,6 +110,7 @@ safeRegisterElement('submit-form', SubmitForm)
safeRegisterElement('prompt-password', PromptPassword)
safeRegisterElement('emails-textarea', EmailsTextarea)
safeRegisterElement('toggle-cookies', ToggleCookies)
safeRegisterElement('toggle-submit', ToggleSubmit)
safeRegisterElement('toggle-on-submit', ToggleOnSubmit)
safeRegisterElement('password-input', PasswordInput)
safeRegisterElement('search-input', SearchInput)
@@ -107,12 +121,21 @@ safeRegisterElement('masked-input', MaskedInput)
safeRegisterElement('set-date-button', SetDateButton)
safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox)
safeRegisterElement('app-tour', AppTour)
safeRegisterElement('app-tour-start', AppTourStart)
safeRegisterElement('dashboard-dropzone', DashboardDropzone)
safeRegisterElement('check-on-click', CheckOnClick)
safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup)
safeRegisterElement('page-container', PageContainer)
safeRegisterElement('email-editor', EmailEditor)
safeRegisterElement('mount-on-click', MountOnClick)
safeRegisterElement('remove-on-event', RemoveOnEvent)
safeRegisterElement('scroll-to', ScrollTo)
safeRegisterElement('set-value', SetValue)
safeRegisterElement('review-form', ReviewForm)
safeRegisterElement('show-on-value', ShowOnValue)
safeRegisterElement('custom-validation', CustomValidation)
safeRegisterElement('toggle-classes', ToggleClasses)
safeRegisterElement('autosize-field', AutosizeField)
safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () {
+2 -2
View File
@@ -19,7 +19,7 @@ button .disabled {
display: none;
}
button[disabled] .disabled {
button[disabled] .disabled, button.btn-disabled .disabled {
display: initial;
}
@@ -27,7 +27,7 @@ button .enabled {
display: initial;
}
button[disabled] .enabled {
button[disabled] .enabled, button.btn-disabled .enabled {
display: none;
}
@@ -0,0 +1,7 @@
export default class extends HTMLElement {
connectedCallback () {
this.querySelector('form').addEventListener('submit', () => {
window.app_tour.start()
})
}
}
+21
View File
@@ -0,0 +1,21 @@
export default class extends HTMLElement {
connectedCallback () {
const originalFontValue = this.field.style.fontSize
if (this.field.scrollHeight > this.field.clientHeight) {
this.field.style.fontSize = `calc(${originalFontValue} / 1.5)`
this.field.style.lineHeight = `calc(${this.field.style.fontSize} * 1.3)`
if (this.field.scrollHeight > this.field.clientHeight) {
this.field.style.fontSize = `calc(${originalFontValue} / 2.0)`
this.field.style.lineHeight = `calc(${this.field.style.fontSize} * 1.3)`
}
}
this.field.classList.remove('hidden')
}
get field () {
return this.closest('field-value')
}
}
@@ -0,0 +1,14 @@
export default class extends HTMLElement {
connectedCallback () {
const input = this.querySelector('input')
const invalidMessage = this.dataset.invalidMessage || ''
input.addEventListener('invalid', () => {
input.setCustomValidity(input.value ? invalidMessage : '')
})
input.addEventListener('input', () => {
input.setCustomValidity('')
})
}
}
@@ -0,0 +1,15 @@
export default class extends HTMLElement {
connectedCallback () {
const eventType = this.dataset.on || 'click'
const selector = document.getElementById(this.dataset.selectorId) || this
const eventElement = eventType === 'submit' ? this.querySelector('form') : this
eventElement.addEventListener(eventType, (event) => {
if (eventType === 'click') {
event.preventDefault()
}
selector.remove()
})
}
}
+19
View File
@@ -0,0 +1,19 @@
export default class extends HTMLElement {
connectedCallback () {
this.querySelectorAll('input[type="radio"]').forEach(radio => {
radio.addEventListener('change', (event) => {
const rating = parseInt(event.target.value)
if (rating === 10) {
window.review_comment.value = ''
window.review_comment.classList.add('hidden')
window.review_submit.classList.add('hidden')
event.target.form.submit()
} else {
window.review_comment.classList.remove('hidden')
window.review_submit.classList.remove('hidden')
}
})
})
}
}
+10
View File
@@ -0,0 +1,10 @@
export default class extends HTMLElement {
connectedCallback () {
this.selector = document.getElementById(this.dataset.selectorId)
this.addEventListener('click', () => {
this.selector.scrollIntoView({ behavior: 'smooth', block: 'start' })
history.replaceState(null, null, `#${this.dataset.selectorId}`)
})
}
}
+12
View File
@@ -13,6 +13,14 @@ export default class extends HTMLElement {
this.input.classList.remove('w-60')
}
})
this.button.addEventListener('click', (event) => {
if (!this.input.value && document.activeElement !== this.input) {
event.preventDefault()
this.input.focus()
}
})
}
get input () {
@@ -22,4 +30,8 @@ export default class extends HTMLElement {
get title () {
return document.querySelector(this.dataset.title)
}
get button () {
return this.querySelector('button')
}
}
+11
View File
@@ -0,0 +1,11 @@
export default class extends HTMLElement {
connectedCallback () {
const input = this.dataset.inputId ? document.getElementById(this.dataset.inputId) : this.querySelector('input')
this.firstElementChild.addEventListener(this.dataset.on || 'click', () => {
if (this.dataset.emptyOnly !== 'true' || !input.value) {
input.value = this.dataset.value
}
})
}
}
+17
View File
@@ -0,0 +1,17 @@
export default class extends HTMLElement {
connectedCallback () {
this.addEventListener('change', (event) => {
const targetValue = this.dataset.value
const selectorId = this.dataset.selectorId
const targetElement = document.getElementById(selectorId)
if (event.target.value === targetValue) {
targetElement.classList.remove('hidden')
} else {
targetElement.classList.add('hidden')
targetElement.value = ''
event.target.form.requestSubmit()
}
})
}
}
+18 -2
View File
@@ -1,11 +1,27 @@
export default class extends HTMLElement {
connectedCallback () {
const form = this.querySelector('form') || (this.querySelector('input, button, select') || this.lastElementChild).form
if (this.dataset.interval) {
this.interval = setInterval(() => {
this.querySelector('form').requestSubmit()
form.requestSubmit()
}, parseInt(this.dataset.interval))
} else if (this.dataset.on) {
this.lastElementChild.addEventListener(this.dataset.on, (event) => {
if (this.dataset.disable === 'true') {
form.querySelector('[type="submit"]')?.setAttribute('disabled', true)
}
if (this.dataset.submitIfValue === 'true') {
if (event.target.value) {
form.requestSubmit()
}
} else {
form.requestSubmit()
}
})
} else {
this.querySelector('form').requestSubmit()
form.requestSubmit()
}
}
+11
View File
@@ -0,0 +1,11 @@
export default class extends HTMLElement {
connectedCallback () {
const button = this.querySelector('a, button')
button.addEventListener('click', () => {
this.dataset.classes.split(' ').forEach((cls) => {
button.classList.toggle(cls)
})
})
}
}
+9 -3
View File
@@ -4,9 +4,15 @@ export default actionable(class extends HTMLElement {
trigger (event) {
const elementIds = JSON.parse(this.dataset.elementIds)
elementIds.forEach((elementId) => {
document.getElementById(elementId).classList.toggle('hidden', (event.target.dataset.toggleId || event.target.value) !== elementId)
})
if (event.target.type === 'checkbox') {
elementIds.forEach((elementId) => {
document.getElementById(elementId)?.classList.toggle('hidden')
})
} else {
elementIds.forEach((elementId) => {
document.getElementById(elementId).classList.toggle('hidden', (event.target.dataset.toggleId || event.target.value) !== elementId)
})
}
if (this.dataset.focusId) {
document.getElementById(this.dataset.focusId)?.focus()
+2
View File
@@ -6,6 +6,7 @@ import ToggleSubmit from './elements/toggle_submit'
import FetchForm from './elements/fetch_form'
import ScrollButtons from './elements/scroll_buttons'
import PageContainer from './elements/page_container'
import SubmitForm from './elements/submit_form'
const safeRegisterElement = (name, element, options = {}) => !window.customElements.get(name) && window.customElements.define(name, element, options)
@@ -14,6 +15,7 @@ safeRegisterElement('toggle-submit', ToggleSubmit)
safeRegisterElement('fetch-form', FetchForm)
safeRegisterElement('scroll-buttons', ScrollButtons)
safeRegisterElement('page-container', PageContainer)
safeRegisterElement('submit-form', SubmitForm)
safeRegisterElement('submission-form', class extends HTMLElement {
connectedCallback () {
this.appElem = document.createElement('div')
+2 -2
View File
@@ -19,7 +19,7 @@ button .disabled {
display: none;
}
button[disabled] .disabled {
button[disabled] .disabled, button.btn-disabled .disabled {
display: initial;
}
@@ -27,7 +27,7 @@ button .enabled {
display: initial;
}
button[disabled] .enabled {
button[disabled] .enabled, button.btn-disabled .enabled {
display: none;
}
+1 -1
View File
@@ -56,7 +56,7 @@ function mouseoverListener (event) {
const requestOptions = {
credentials: 'same-origin',
headers: { Accept: 'text/html, application/xhtml+xml', 'VND.PREFETCH': 'true' },
headers: { Accept: 'text/html, application/xhtml+xml', 'VND.PREFETCH': 'true', 'X-Turbo': 'true' },
method: 'GET',
redirect: 'follow'
}
+78 -6
View File
@@ -29,8 +29,15 @@
{{ optionValue(option) }}
</template>
<template v-else>
{{ field.title || field.name || fieldNames[field.type] }}
<template v-if="field.type === 'checkbox' && !field.name">
<MarkdownContent
v-if="field.title"
:text-only="true"
:string="field.title"
/>
<template v-else>
{{ field.name || fieldNames[field.type] }}
</template>
<template v-if="field.type === 'checkbox' && !field.name && !field.title">
{{ fieldIndex + 1 }}
</template>
<template v-else-if="!field.required && field.type !== 'checkbox'">
@@ -212,6 +219,48 @@
>
{{ formatNumber(modelValue, field.preferences?.format) }}
</span>
<span
v-else-if="field.type === 'strikethrough'"
class="w-full h-full flex items-center justify-center"
>
<svg
v-if="(((1000.0 / pageWidth) * pageHeight) * area.h) < 40.0"
xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
>
<line
x1="0"
y1="50%"
x2="100%"
y2="50%"
:stroke="field.preferences?.color || 'red'"
:stroke-width="strikethroughWidth"
/>
</svg>
<svg
v-else
xmlns="http://www.w3.org/2000/svg"
:style="{ overflow: 'visible', width: `calc(100% - ${strikethroughWidth})`, height: `calc(100% - ${strikethroughWidth})` }"
>
<line
x1="0"
y1="0"
x2="100%"
y2="100%"
:stroke="field.preferences?.color || 'red'"
:stroke-width="strikethroughWidth"
/>
<line
x1="100%"
y1="0"
x2="0"
y2="100%"
:stroke="field.preferences?.color || 'red'"
:stroke-width="strikethroughWidth"
/>
</svg>
</span>
<span
v-else
class="whitespace-pre-wrap"
@@ -222,12 +271,14 @@
</template>
<script>
import MarkdownContent from './markdown_content'
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconId } from '@tabler/icons-vue'
export default {
name: 'FieldArea',
components: {
IconPaperclip,
MarkdownContent,
IconCheck
},
inject: ['t'],
@@ -251,6 +302,16 @@ export default {
required: false,
default: false
},
pageWidth: {
type: Number,
required: false,
default: 0
},
pageHeight: {
type: Number,
required: false,
default: 0
},
isValueSet: {
type: Boolean,
required: false,
@@ -333,6 +394,13 @@ export default {
verification: this.t('verify_id')
}
},
strikethroughWidth () {
if (this.isInlineSize) {
return '0.6cqmin'
} else {
return 'clamp(0px, 0.5vw, 6px)'
}
},
isShowSignatureId () {
if ([true, false].includes(this.field.preferences?.with_signature_id)) {
return this.field.preferences.with_signature_id
@@ -422,10 +490,14 @@ export default {
},
formattedDate () {
if (this.field.type === 'date' && this.modelValue) {
return this.formatDate(
this.modelValue === '{{date}}' ? new Date() : new Date(this.modelValue),
this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
)
try {
return this.formatDate(
this.modelValue === '{{date}}' ? new Date() : new Date(this.modelValue),
this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
)
} catch {
return this.modelValue
}
} else {
return ''
}
+30 -23
View File
@@ -11,30 +11,37 @@
v-for="(area, areaIndex) in field.areas"
:key="areaIndex"
>
<Teleport
v-if="findPageElementForArea(area)"
:to="findPageElementForArea(area)"
<template
v-for="(pageElem, index) in [findPageElementForArea(area)]"
:key="index"
>
<FieldArea
:ref="setAreaRef"
v-model="values[field.uuid]"
:values="values"
:field="field"
:area="area"
:submittable="submittable"
:field-index="fieldIndex"
:is-inline-size="isInlineSize"
:scroll-padding="scrollPadding"
:submitter="submitter"
:with-field-placeholder="withFieldPlaceholder"
:with-signature-id="withSignatureId"
:is-active="currentStep === step"
:with-label="withLabel && !withFieldPlaceholder && step.length < 2"
:is-value-set="step.some((f) => f.uuid in values)"
:attachments-index="attachmentsIndex"
@click="[$emit('focus-step', stepIndex), maybeScrollOnClick(field, area)]"
/>
</Teleport>
<Teleport
v-if="pageElem"
:to="pageElem"
>
<FieldArea
:ref="setAreaRef"
v-model="values[field.uuid]"
:values="values"
:field="field"
:area="area"
:submittable="submittable"
:page-width="1400"
:page-height="(1400.0 / pageElem.offsetWidth) * pageElem.offsetHeight"
:field-index="fieldIndex"
:is-inline-size="isInlineSize"
:scroll-padding="scrollPadding"
:submitter="submitter"
:with-field-placeholder="withFieldPlaceholder"
:with-signature-id="withSignatureId"
:is-active="currentStep === step"
:with-label="withLabel && !withFieldPlaceholder && step.length < 2"
:is-value-set="step.some((f) => f.uuid in values)"
:attachments-index="attachmentsIndex"
@click="[$emit('focus-step', stepIndex), maybeScrollOnClick(field, area)]"
/>
</Teleport>
</template>
</template>
</template>
</template>
+3 -6
View File
@@ -25,7 +25,7 @@
<div class="space-y-3 mt-5">
<a
v-if="completedButton.url"
:href="sanitizeHref(completedButton.url)"
:href="sanitizeUrl(completedButton.url)"
rel="noopener noreferrer nofollow"
class="white-button flex items-center w-full completed-form-completed-button"
>
@@ -102,6 +102,7 @@
<script>
import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop, IconLogin } from '@tabler/icons-vue'
import MarkdownContent from './markdown_content'
import { sanitizeUrl } from '@braintree/sanitize-url'
export default {
name: 'FormCompleted',
@@ -198,6 +199,7 @@ export default {
})
},
methods: {
sanitizeUrl,
sendCopyToEmail () {
this.isSendingCopy = true
@@ -252,11 +254,6 @@ export default {
this.isDownloading = false
})
},
sanitizeHref (href) {
if (href && href.trim().match(/^((?:https?:\/\/)|\/)/)) {
return href.replace(/javascript:/g, '')
}
},
downloadSafariIos (urls) {
const fileRequests = urls.map((url) => {
return fetch(url).then(async (resp) => {
@@ -26,6 +26,7 @@
</template>
</label>
<button
v-if="withToday"
class="btn btn-outline btn-sm !normal-case font-normal set-current-date-button"
@click.prevent="[setCurrentDate(), $emit('focus')]"
>
@@ -46,6 +47,8 @@
:id="field.uuid"
ref="input"
v-model="value"
:min="validationMin"
:max="validationMax"
class="base-input !text-2xl text-center w-full"
:required="field.required"
type="date"
@@ -89,6 +92,44 @@ export default {
},
emits: ['update:model-value', 'focus', 'submit'],
computed: {
dateNowString () {
const today = new Date()
const yyyy = today.getFullYear()
const mm = String(today.getMonth() + 1).padStart(2, '0')
const dd = String(today.getDate()).padStart(2, '0')
return `${yyyy}-${mm}-${dd}`
},
validationMin () {
if (this.field.validation?.min) {
return ['{{date}}', '{date}'].includes(this.field.validation.min) ? this.dateNowString : this.field.validation.min
} else {
return ''
}
},
validationMax () {
if (this.field.validation?.max) {
return ['{{date}}', '{date}'].includes(this.field.validation.max) ? this.dateNowString : this.field.validation.max
} else {
return ''
}
},
withToday () {
const todayDate = new Date().setHours(0, 0, 0, 0)
if (this.validationMin) {
if (new Date(this.validationMin).setHours(0, 0, 0, 0) <= todayDate) {
return this.validationMax ? (new Date(this.validationMax).setHours(0, 0, 0, 0) >= todayDate) : true
} else {
return false
}
} else if (this.validationMax) {
return new Date(this.validationMax).setHours(0, 0, 0, 0) >= todayDate
} else {
return true
}
},
value: {
set (value) {
this.$emit('update:model-value', value)
+24 -7
View File
@@ -23,7 +23,7 @@
<FormulaFieldAreas
v-if="formulaFields.length"
:fields="formulaFields"
:readonly-values="readonlyConditionalFieldValues"
:readonly-values="readonlyFieldValues"
:values="values"
/>
<Teleport
@@ -349,10 +349,10 @@
:id="field.uuid"
type="checkbox"
class="base-checkbox !h-7 !w-7"
:oninvalid="`this.setCustomValidity('${t('please_check_the_box_to_continue')}')`"
:onchange="`this.setCustomValidity(validity.valueMissing ? '${t('please_check_the_box_to_continue')}' : '');`"
:required="field.required"
:checked="!!values[field.uuid]"
@invalid="$event.target.setCustomValidity(t('please_check_the_box_to_continue'))"
@change="$event.target.setCustomValidity($event.target.validity.valueMissing ? t('please_check_the_box_to_continue') : '')"
@click="[scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
>
<span
@@ -454,7 +454,9 @@
v-model="values[currentField.uuid]"
:field="currentField"
:submitter-slug="submitterSlug"
:fields="formulaFields"
:values="values"
:readonly-values="readonlyFieldValues"
@attached="attachments.push($event)"
@focus="scrollIntoField(currentField)"
@submit="!isSubmitting && submitStep()"
@@ -572,6 +574,7 @@ import FormCompleted from './completed'
import { IconInnerShadowTop, IconArrowsDiagonal, IconWritingSign, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
import AppearsOn from './appears_on'
import i18n from './i18n'
import { sanitizeUrl } from '@braintree/sanitize-url'
const isEmpty = (obj) => {
if (obj == null) return true
@@ -871,7 +874,14 @@ export default {
},
readonlyConditionalFieldValues () {
return this.readonlyConditionalFields.reduce((acc, f) => {
acc[f.uuid] = (this.values[f.uuid] || f.default_value)
acc[f.uuid] = isEmpty(this.values[f.uuid]) ? f.default_value : this.values[f.uuid]
return acc
}, {})
},
readonlyFieldValues () {
return this.readonlyFields.reduce((acc, f) => {
acc[f.uuid] = isEmpty(this.values[f.uuid]) ? f.default_value : this.values[f.uuid]
return acc
}, {})
@@ -971,7 +981,10 @@ export default {
return this.currentStepFields[0]
},
readonlyConditionalFields () {
return this.fields.filter((f) => f.readonly && f.conditions?.length && this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f))
return this.readonlyFields.filter((f) => f.conditions?.length)
},
readonlyFields () {
return this.fields.filter((f) => f.readonly && this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f))
},
stepFields () {
const verificationFields = []
@@ -1014,7 +1027,11 @@ export default {
const aArea = (fieldAreasIndex[aField.uuid] ||= [...(aField.areas || [])].sort(sortArea)[0])
const bArea = (fieldAreasIndex[bField.uuid] ||= [...(bField.areas || [])].sort(sortArea)[0])
return sortArea(aArea, bArea)
if (aArea && bArea) {
return sortArea(aArea, bArea)
} else {
return 0
}
})
}
@@ -1476,7 +1493,7 @@ export default {
}
if (this.completedRedirectUrl) {
window.location.href = this.completedRedirectUrl
window.location.href = sanitizeUrl(this.completedRedirectUrl)
}
}
}
@@ -58,6 +58,13 @@ export default {
computed: {
isInlineSize () {
return CSS.supports('container-type: size')
},
fieldsUuidIndex () {
return this.fields.reduce((acc, field) => {
acc[field.uuid] = field
return acc
}, {})
}
},
async mounted () {
@@ -96,8 +103,19 @@ export default {
findPageElementForArea (area) {
return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${area.attachment_uuid}-${area.page}`)
},
normalizeFormula (formula, depth = 0) {
if (depth > 10) return formula
return formula.replace(/{{(.*?)}}/g, (match, uuid) => {
if (this.fieldsUuidIndex[uuid]) {
return `(${this.normalizeFormula(this.fieldsUuidIndex[uuid].preferences.formula, depth + 1)})`
} else {
return match
}
})
},
calculateFormula (field) {
const transformedFormula = field.preferences.formula.replace(/{{(.*?)}}/g, (match, uuid) => {
const transformedFormula = this.normalizeFormula(field.preferences.formula).replace(/{{(.*?)}}/g, (match, uuid) => {
return this.readonlyValues[uuid] || this.values[uuid] || 0.0
})
@@ -1,35 +1,32 @@
<template>
<span>
<template
v-for="(item, index) in items"
:key="index"
>
<a
v-if="item.startsWith('<a') && item.endsWith('</a>')"
:href="sanitizeHref(extractAttr(item, 'href'))"
rel="noopener noreferrer nofollow"
:class="extractAttr(item, 'class') || 'link'"
target="_blank"
>
{{ extractText(item) }}
</a>
<b
v-else-if="item.startsWith('<b>') || item.startsWith('<strong>')"
>
{{ extractText(item) }}
</b>
<i
v-else-if="item.startsWith('<i>') || item.startsWith('<em>')"
>
{{ extractText(item) }}
</i>
<br
v-else-if="item === '<br>' || item === '\n'"
>
<span v-if="textOnly">
{{ dom.body.textContent }}
</span>
<template v-else>
<template
v-else
v-for="(item, index) in nodes || dom.body.childNodes"
:key="index"
>
{{ item }}
<a
v-if="item.tagName === 'A' && item.getAttribute('href') !== 'undefined'"
:href="sanitizeUrl(item.getAttribute('href'))"
rel="noopener noreferrer nofollow"
:class="item.getAttribute('class') || 'link'"
target="_blank"
>
<MarkdownContent :nodes="item.childNodes" />
</a>
<component
:is="item.tagName"
v-else-if="safeTags.includes(item.tagName)"
>
<MarkdownContent :nodes="item.childNodes" />
</component>
<br v-else-if="item.tagName === 'BR' || item.nodeValue === '\n'">
<template v-else>
{{ item.textContent }}
</template>
</template>
</template>
</span>
@@ -37,8 +34,7 @@
<script>
import snarkdown from 'snarkdown'
const htmlSplitRegexp = /(<a.+?<\/a>|<i>.+?<\/i>|<b>.+?<\/b>|<em>.+?<\/em>|<strong>.+?<\/strong>|<br>)/
import { sanitizeUrl } from '@braintree/sanitize-url'
export default {
name: 'MarkdownContent',
@@ -47,10 +43,23 @@ export default {
type: String,
required: false,
default: ''
},
nodes: {
type: [Array, Object],
require: false,
default: null
},
textOnly: {
type: Boolean,
required: false,
default: false
}
},
computed: {
items () {
safeTags () {
return ['UL', 'I', 'EM', 'B', 'STRONG', 'P']
},
dom () {
const linkParts = this.string.split(/(https?:\/\/[^\s)]+)/g)
const text = linkParts.map((part, index) => {
@@ -61,25 +70,11 @@ export default {
}
}).join('')
return snarkdown(text.replace(/\n/g, '<br>')).split(htmlSplitRegexp)
return new DOMParser().parseFromString(snarkdown(text.replace(/\n/g, '<br>')), 'text/html')
}
},
methods: {
sanitizeHref (href) {
if (href && href.trim().match(/^((?:https?:\/\/)|\/)/)) {
return href.replace(/javascript:/g, '')
}
},
extractAttr (text, attr) {
if (text.includes(attr)) {
return text.split(attr).pop().split('"')[1]
}
},
extractText (text) {
if (text) {
return text.match(/>(.+?)</)?.[1]
}
}
sanitizeUrl
}
}
</script>
@@ -39,8 +39,10 @@
:id="field.uuid"
v-model="number"
type="number"
:step="field.validation?.step || 'any'"
:min="field.validation?.min"
:max="field.validation?.max"
class="base-input !text-2xl w-full"
step="any"
:required="field.required"
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
:name="`values[${field.uuid}]`"
@@ -92,10 +92,20 @@ export default {
type: Object,
required: true
},
readonlyValues: {
type: Object,
required: false,
default: () => ({})
},
values: {
type: Object,
required: true
},
fields: {
type: Array,
required: false,
default: () => []
},
submitterSlug: {
type: String,
required: true
@@ -109,6 +119,13 @@ export default {
}
},
computed: {
fieldsUuidIndex () {
return this.fields.reduce((acc, field) => {
acc[field.uuid] = field
return acc
}, {})
},
queryParams () {
return new URLSearchParams(window.location.search)
},
@@ -116,6 +133,10 @@ export default {
return this.queryParams.get('stripe_session_id')
},
defaultName () {
if (this.field.preferences?.price_id || this.field.preferences?.payment_link_id) {
return ''
}
const { price, currency } = this.field.preferences || {}
const formatter = new Intl.NumberFormat([], {
@@ -178,12 +199,23 @@ export default {
},
methods: {
calculateFormula () {
const transformedFormula = this.field.preferences.formula.replace(/{{(.*?)}}/g, (match, uuid) => {
return this.values[uuid] || 0.0
const transformedFormula = this.normalizeFormula(this.field.preferences.formula).replace(/{{(.*?)}}/g, (match, uuid) => {
return this.readonlyValues[uuid] || this.values[uuid] || 0.0
})
return this.math.evaluate(transformedFormula.toLowerCase())
},
normalizeFormula (formula, depth = 0) {
if (depth > 10) return formula
return formula.replace(/{{(.*?)}}/g, (match, uuid) => {
if (this.fieldsUuidIndex[uuid]) {
return `(${this.normalizeFormula(this.fieldsUuidIndex[uuid].preferences.formula, depth + 1)})`
} else {
return match
}
})
},
async submit () {
if (this.sessionId) {
return fetch(this.baseUrl + '/api/stripe_payments/' + this.sessionId, {
@@ -14,7 +14,7 @@
<MarkdownContent :string="field.description" />
</div>
<div
v-if="emptyValueRequiredStep && emptyValueRequiredStep[0] !== field"
v-if="isRequiredFieldEmpty"
class="px-1 field-description-text"
>
{{ t('complete_all_required_fields_to_proceed_with_identity_verification') }}
@@ -100,6 +100,9 @@ export default {
}
},
computed: {
isRequiredFieldEmpty () {
return this.emptyValueRequiredStep && this.emptyValueRequiredStep[0] !== this.field
},
countryCode () {
const browserTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
const browserTz = browserTimeZone.split('/')[1]
@@ -131,17 +134,19 @@ export default {
}
},
async mounted () {
this.isLoading = true
if (!this.isRequiredFieldEmpty) {
this.isLoading = true
if (new URLSearchParams(window.location.search).get('submit') === 'true') {
this.$emit('submit')
} else {
Promise.all([
import('@eid-easy/eideasy-widget'),
this.start()
]).finally(() => {
this.isLoading = false
})
if (new URLSearchParams(window.location.search).get('submit') === 'true') {
this.$emit('submit')
} else {
Promise.all([
import('@eid-easy/eideasy-widget'),
this.start()
]).finally(() => {
this.isLoading = false
})
}
}
},
methods: {
@@ -156,6 +161,10 @@ export default {
}).then(async (resp) => {
this.eidEasyData = await resp.json()
if (this.eidEasyData.check_completed) {
this.$emit('submit')
}
if (this.eidEasyData.available_methods[0] === 'itsme-qes-signature' &&
this.eidEasyData.available_methods.length === 1) {
const redirectUrl = new URL('https://id.eideasy.com/sign_contract_external')
@@ -166,7 +175,7 @@ export default {
redirectUrl.searchParams.append('lang', this.locale)
this.redirectUrl = redirectUrl.toString()
} else {
} else if (this.$refs.widgetContainer) {
const eidEasyWidget = document.createElement('eideasy-widget')
for (const key in this.widgetSettings) {
@@ -179,15 +188,23 @@ export default {
})
},
async submit () {
return fetch(this.baseUrl + '/api/identity_verification', {
const resp = await fetch(this.baseUrl + '/api/identity_verification', {
method: 'POST',
body: JSON.stringify({
submitter_slug: this.submitterSlug
}),
headers: { 'Content-Type': 'application/json' }
}).then(async (resp) => {
return resp
})
if (resp.status === 404) {
throw new Error('Verification not completed yet')
}
if (resp.ok) {
return resp
} else {
throw new Error('Verification failed')
}
}
}
}
+199 -35
View File
@@ -10,7 +10,7 @@
<div
v-if="isSelected || isDraw"
class="top-0 bottom-0 right-0 left-0 absolute border border-1.5 pointer-events-none"
:class="field.type === 'heading' ? '' : borderColors[submitterIndex % borderColors.length]"
:class="activeBorderClasses"
/>
<div
v-if="field.type === 'cells' && (isSelected || isDraw)"
@@ -39,7 +39,7 @@
@pointerdown.stop
>
<FieldSubmitter
v-if="field.type != 'heading'"
v-if="field.type != 'heading' && field.type != 'strikethrough'"
v-model="field.submitter_uuid"
class="border-r roles-dropdown"
:compact="true"
@@ -72,11 +72,11 @@
@blur="onNameBlur"
>{{ optionIndexText }} {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || defaultName }}</span>
<div
v-if="isSettingsFocus || (isValueInput && field.type !== 'heading') || (isNameFocus && !['checkbox', 'phone'].includes(field.type))"
v-if="isSettingsFocus || isSelectInput || (isValueInput && field.type !== 'heading') || (isNameFocus && !['checkbox', 'phone'].includes(field.type))"
class="flex items-center ml-1.5"
>
<input
v-if="!isValueInput"
v-if="!isValueInput && !isSelectInput"
:id="`required-checkbox-${field.uuid}`"
v-model="field.required"
type="checkbox"
@@ -84,14 +84,14 @@
@mousedown.prevent
>
<label
v-if="!isValueInput"
v-if="!isValueInput && !isSelectInput"
:for="`required-checkbox-${field.uuid}`"
class="label text-xs"
@click.prevent="field.required = !field.required"
@mousedown.prevent
>{{ t('required') }}</label>
<input
v-if="isValueInput"
v-if="isValueInput || isSelectInput"
:id="`readonly-checkbox-${field.uuid}`"
type="checkbox"
class="checkbox checkbox-xs no-animation rounded"
@@ -100,7 +100,7 @@
@mousedown.prevent
>
<label
v-if="isValueInput"
v-if="isValueInput || isSelectInput"
:for="`readonly-checkbox-${field.uuid}`"
class="label text-xs"
@click.prevent="field.readonly = !(field.readonly ?? true)"
@@ -164,30 +164,81 @@
ref="touchValueTarget"
class="flex h-full w-full field-area"
dir="auto"
:class="[isValueInput ? 'bg-opacity-50' : 'bg-opacity-80', field.type === 'heading' ? 'bg-gray-50' : bgColors[submitterIndex % bgColors.length], isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? fontClasses : 'justify-center items-center']"
:class="[isValueInput ? 'cursor-text' : '', isValueInput || isCheckboxInput || isSelectInput ? 'bg-opacity-50' : 'bg-opacity-80', bgClasses, isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? fontClasses : 'justify-center items-center']"
@click="focusValueInput"
>
<span
v-if="field"
class="flex justify-center items-center space-x-1"
:class="{ 'w-full': ['cells', 'checkbox'].includes(field.type), 'h-full': !isValueInput && !isDefaultValuePresent }"
:class="{ 'w-full': isWFullType, 'h-full': !isValueInput && (!isDefaultValuePresent || field.type === 'strikethrough') }"
>
<div
v-if="isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas && field.type !== 'checkbox')"
:class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type) }"
v-if="field.type === 'strikethrough'"
class="w-full h-full flex items-center justify-center"
>
<svg
v-if="(((basePageWidth / pageWidth) * pageHeight) * area.h) < 41.6"
xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
>
<line
x1="0"
y1="50%"
x2="100%"
y2="50%"
:stroke="field.preferences?.color || 'red'"
:stroke-width="strikethroughWidth"
/>
</svg>
<svg
v-else
xmlns="http://www.w3.org/2000/svg"
:style="{ overflow: 'visible', width: `calc(100% - ${strikethroughWidth})`, height: `calc(100% - ${strikethroughWidth})` }"
>
<line
x1="0"
y1="0"
x2="100%"
y2="100%"
:stroke="field.preferences?.color || 'red'"
:stroke-width="strikethroughWidth"
/>
<line
x1="100%"
y1="0"
x2="0"
y2="100%"
:stroke="field.preferences?.color || 'red'"
:stroke-width="strikethroughWidth"
/>
</svg>
</div>
<div
v-else-if="isDefaultValuePresent || isValueInput || isSelectInput || (withFieldPlaceholder && field.areas && field.type !== 'checkbox')"
:class="{ 'w-full h-full': isWFullType }"
:style="fontStyle"
>
<div
ref="textContainer"
class="flex items-center px-0.5"
:style="{ color: field.preferences?.color }"
:class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type) }"
:class="{ 'w-full h-full': isWFullType }"
>
<IconCheck
v-if="field.type == 'checkbox'"
class="aspect-square mx-auto"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
/>
<template
v-else-if="(field.type === 'radio' || field.type === 'multiple') && field?.areas?.length > 1"
>
<IconCheck
v-if="field.type === 'multiple' ? field.default_value.includes(buildAreaOptionValue(area)) : buildAreaOptionValue(area) === field.default_value"
class="aspect-square mx-auto"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
/>
</template>
<span
v-else-if="field.type === 'number' && !isValueInput && (field.default_value || field.default_value == 0)"
class="whitespace-pre-wrap"
@@ -210,14 +261,39 @@
{{ char }}
</div>
</div>
<select
v-else-if="isSelectInput"
ref="defaultValueSelect"
class="bg-transparent outline-none focus:outline-none w-full"
@change="[field.default_value = $event.target.value, field.readonly = !!field.default_value?.length, save()]"
@focus="selectedAreaRef.value = area"
@keydown.enter="onDefaultValueEnter"
>
<option
:disabled="!field.default_value?.length"
:selected="!field.default_value?.length"
:value="''"
>
{{ t(field.default_value?.length ? 'none' : 'select') }}
</option>
<option
v-for="(option, index) in field.options"
:key="index"
:selected="field.default_value === option.value"
:value="option.value"
>
{{ option.value }}
</option>
</select>
<span
v-else
ref="defaultValue"
:contenteditable="isValueInput"
class="whitespace-pre-wrap outline-none empty:before:content-[attr(placeholder)] before:text-gray-400"
class="whitespace-pre-wrap outline-none empty:before:content-[attr(placeholder)] before:text-base-content/30"
:class="{ 'cursor-text': isValueInput }"
:placeholder="withFieldPlaceholder && !isValueInput ? defaultField?.title || field.title || field.name || defaultName : t('type_value')"
:placeholder="withFieldPlaceholder && !isValueInput ? defaultField?.title || field.title || field.name || defaultName : (field.type === 'date' ? field.preferences?.format || t('type_value') : t('type_value'))"
@blur="onDefaultValueBlur"
@focus="selectedAreaRef.value = area"
@paste.prevent="onPaste"
@keydown.enter="onDefaultValueEnter"
>{{ field.default_value }}</span>
@@ -225,7 +301,7 @@
</div>
<component
:is="fieldIcons[field.type]"
v-else
v-else-if="!isCheckboxInput"
width="100%"
height="100%"
class="max-h-10 opacity-50"
@@ -233,12 +309,12 @@
</span>
</div>
<div
v-if="!isValueInput"
v-if="!isValueInput && !isSelectInput"
ref="touchTarget"
class="absolute top-0 bottom-0 right-0 left-0"
:class="isDragged ? 'cursor-grab' : 'cursor-pointer'"
@dblclick="maybeToggleDefaultValue"
@click="maybeToggleCheckboxValue"
@click="inputMode && maybeToggleCheckboxValue()"
/>
<span
v-if="field?.type && editable"
@@ -253,6 +329,7 @@
<FormulaModal
:field="field"
:editable="editable && !defaultField"
:default-field="defaultField"
:build-default-name="buildDefaultName"
@close="isShowFormulaModal = false"
/>
@@ -264,6 +341,7 @@
<FontModal
:field="field"
:editable="editable && !defaultField"
:default-field="defaultField"
:build-default-name="buildDefaultName"
@close="isShowFontModal = false"
/>
@@ -275,6 +353,7 @@
<ConditionsModal
:item="field"
:build-default-name="buildDefaultName"
:default-field="defaultField"
@close="isShowConditionsModal = false"
/>
</Teleport>
@@ -285,6 +364,7 @@
<DescriptionModal
:field="field"
:editable="editable && !defaultField"
:default-field="defaultField"
:build-default-name="buildDefaultName"
@close="isShowDescriptionModal = false"
/>
@@ -359,6 +439,16 @@ export default {
required: false,
default: false
},
pageWidth: {
type: Number,
required: false,
default: 0
},
pageHeight: {
type: Number,
required: false,
default: 0
},
defaultSubmitters: {
type: Array,
required: false,
@@ -398,6 +488,34 @@ export default {
fieldNames: FieldType.computed.fieldNames,
fieldLabels: FieldType.computed.fieldLabels,
fieldIcons: FieldType.computed.fieldIcons,
bgClasses () {
if (this.field.type === 'heading') {
return 'bg-gray-50'
} else if (this.field.type === 'strikethrough') {
return 'bg-transparent'
} else {
return this.bgColors[this.submitterIndex % this.bgColors.length]
}
},
activeBorderClasses () {
if (this.field.type === 'heading') {
return ''
} else if (this.field.type === 'strikethrough') {
return 'border-dashed border-gray-300'
} else {
return this.borderColors[this.submitterIndex % this.borderColors.length]
}
},
isWFullType () {
return ['cells', 'checkbox', 'radio', 'multiple', 'select', 'strikethrough'].includes(this.field.type)
},
strikethroughWidth () {
if (this.isInlineSize) {
return '0.6cqmin'
} else {
return 'clamp(0px, 0.5vw, 6px)'
}
},
fontStyle () {
let fontSize = ''
@@ -417,24 +535,37 @@ export default {
return { fontSize, lineHeight: `calc(${fontSize} * ${this.lineHeight})` }
},
optionsUuidIndex () {
return this.field.options.reduce((acc, option) => {
acc[option.uuid] = option
return acc
}, {})
},
fontSizePx () {
return parseInt(this.field?.preferences?.font_size || 11) * this.fontScale
},
lineHeight () {
return 1.3
},
basePageWidth () {
return 1040.0
},
fontScale () {
return 1040 / 612.0
return this.basePageWidth / 612.0
},
isDefaultValuePresent () {
if (this.field?.type === 'radio' && this.field?.areas?.length > 1) {
return false
} else {
return this.field?.default_value || this.field?.default_value === 0
}
return this.field?.default_value || this.field?.default_value === 0
},
isSelectInput () {
return this.inputMode && (this.field.type === 'select' || (this.field.type === 'radio' && this.field.areas?.length < 2))
},
isCheckboxInput () {
return this.inputMode && (this.field.type === 'checkbox' || (['radio', 'multiple'].includes(this.field.type) && this.area.option_uuid))
},
isValueInput () {
return (this.field.type === 'heading' && this.isHeadingSelected) || this.isContenteditable || (this.inputMode && ['text', 'number', 'date'].includes(this.field.type))
return (this.field.type === 'heading' && this.isHeadingSelected) || this.isContenteditable ||
(this.inputMode && (['text', 'number'].includes(this.field.type) || (this.field.type === 'date' && this.field.default_value !== '{{date}}')))
},
modalContainerEl () {
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
@@ -549,6 +680,11 @@ export default {
closeDropdown () {
this.$el.getRootNode().activeElement.blur()
},
buildAreaOptionValue (area) {
const option = this.optionsUuidIndex[area.option_uuid]
return option.value || `${this.t('option')} ${this.field.options.indexOf(option) + 1}`
},
maybeToggleDefaultValue () {
if (!this.editable) {
return
@@ -558,22 +694,41 @@ export default {
this.isContenteditable = true
this.focusValueInput()
} else if (this.field.type === 'checkbox') {
this.field.readonly = !this.field.readonly
this.field.default_value === true ? delete this.field.default_value : this.field.default_value = true
this.save()
} else if (this.field.type === 'date') {
this.field.readonly = !this.field.readonly
this.field.default_value === '{{date}}' ? delete this.field.default_value : this.field.default_value = '{{date}}'
this.save()
} else {
this.maybeToggleCheckboxValue()
}
},
maybeToggleCheckboxValue () {
if (this.inputMode && this.field.type === 'checkbox') {
this.field.readonly = !this.field.readonly
if (this.field.type === 'checkbox') {
this.field.default_value === true ? delete this.field.default_value : this.field.default_value = true
this.field.readonly = this.field.default_value === true
this.save()
} else if (this.field.type === 'radio' && this.area.option_uuid) {
const value = this.buildAreaOptionValue(this.area)
this.field.default_value === value ? delete this.field.default_value : this.field.default_value = value
this.field.readonly = !!this.field.default_value?.length
this.save()
} else if (this.field.type === 'multiple' && this.area.option_uuid) {
const value = this.buildAreaOptionValue(this.area)
if (this.field.default_value?.includes(value)) {
this.field.default_value.splice(this.field.default_value.indexOf(value), 1)
if (!this.field.default_value?.length) delete this.field.default_value
} else {
Array.isArray(this.field.default_value) ? this.field.default_value.push(value) : this.field.default_value = [value]
}
this.field.readonly = !!this.field.default_value?.length
this.save()
}
@@ -667,10 +822,15 @@ export default {
delete this.field.options
}
if (['heading'].includes(this.field.type)) {
if (this.field.type === 'heading') {
this.field.readonly = true
}
if (this.field.type === 'strikethrough') {
this.field.readonly = true
this.field.default_value = true
}
if (['select', 'multiple', 'radio'].includes(this.field.type)) {
this.field.options ||= [{ value: '', uuid: v4() }]
}
@@ -749,7 +909,7 @@ export default {
}
},
drag (e) {
if (e.target.id === 'mask') {
if (e.target.id === 'mask' && this.editable) {
this.isDragged = true
this.area.x = (e.offsetX - this.dragFrom.x) / e.target.clientWidth
@@ -765,7 +925,9 @@ export default {
e.preventDefault()
this.isDragged = true
if (this.editable) {
this.isDragged = true
}
const rect = e.target.getBoundingClientRect()
@@ -818,7 +980,9 @@ export default {
e.preventDefault()
this.isDragged = true
if (this.editable) {
this.isDragged = true
}
const rect = e.target.getBoundingClientRect()
+59 -19
View File
@@ -402,7 +402,10 @@
:style="{ backgroundColor }"
>
<div class="bg-base-200 rounded-lg p-5 text-center space-y-4 draw-field-container">
<p>
<p v-if="(drawField?.type || drawFieldType) === 'strikethrough'">
{{ t('draw_strikethrough_the_document') }}
</p>
<p v-else>
{{ t('draw_field_on_the_document') }}
</p>
<div>
@@ -413,7 +416,7 @@
{{ t('cancel') }}
</button>
<a
v-if="!drawField && !drawOption && !['stamp', 'signature', 'initials', 'heading'].includes(drawField?.type || drawFieldType)"
v-if="!drawField && !drawOption && !['stamp', 'signature', 'initials', 'heading', 'strikethrough'].includes(drawField?.type || drawFieldType)"
href="#"
class="link block mt-3 text-sm"
@click.prevent="[addField(drawFieldType), drawField = null, drawOption = null, withSelectedFieldType ? '' : drawFieldType = '', showDrawField = false]"
@@ -660,7 +663,7 @@ export default {
acceptFileTypes: {
type: String,
required: false,
default: 'image/*, application/pdf'
default: 'image/*, application/pdf, application/zip'
},
baseUrl: {
type: String,
@@ -1065,7 +1068,7 @@ export default {
}
if (['select', 'multiple', 'radio'].includes(type)) {
field.options = [{ value: '', uuid: v4() }]
field.options = [{ value: '', uuid: v4() }, { value: '', uuid: v4() }]
}
if (type === 'stamp') {
@@ -1084,6 +1087,11 @@ export default {
}
}
if (field.type === 'strikethrough') {
field.readonly = true
field.default_value = true
}
if (type === 'signature' && [true, false].includes(this.withSignatureId)) {
field.preferences ||= {}
field.preferences.with_signature_id = this.withSignatureId
@@ -1109,7 +1117,7 @@ export default {
}
if (['select', 'multiple', 'radio'].includes(type)) {
field.options = [{ value: '', uuid: v4() }]
field.options = [{ value: '', uuid: v4() }, { value: '', uuid: v4() }]
}
if (type === 'stamp') {
@@ -1265,24 +1273,31 @@ export default {
if (!field.areas.length) {
this.template.fields.splice(this.template.fields.indexOf(field), 1)
this.template.fields.forEach((f) => {
(f.conditions || []).forEach((c) => {
this.removeFieldConditions(field)
}
this.save()
},
removeFieldConditions (field) {
this.template.fields.forEach((f) => {
if (f.conditions) {
f.conditions.forEach((c) => {
if (c.field_uuid === field.uuid) {
f.conditions.splice(f.conditions.indexOf(c), 1)
}
})
})
}
})
this.template.schema.forEach((item) => {
(item.conditions || []).forEach((c) => {
this.template.schema.forEach((item) => {
if (item.conditions) {
item.conditions.forEach((c) => {
if (c.field_uuid === field.uuid) {
item.conditions.splice(item.conditions.indexOf(c), 1)
}
})
})
}
this.save()
}
})
},
pasteField () {
const field = this.template.fields.find((f) => f.areas?.includes(this.copiedArea))
@@ -1301,7 +1316,13 @@ export default {
this.copiedArea.option_uuid ||= field.options[0].uuid
area.option_uuid = v4()
field.options.push({ uuid: area.option_uuid })
const lastOption = field.options[field.options.length - 1]
if (!field.areas.find((a) => lastOption.uuid === a.option_uuid)) {
area.option_uuid = lastOption.uuid
} else {
field.options.push({ uuid: area.option_uuid })
}
field.areas.push(area)
} else {
@@ -1344,6 +1365,9 @@ export default {
} else if (type === 'initials') {
area.w = pageMask.clientWidth / 10 / pageMask.clientWidth
area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth)
} else if (type === 'strikethrough') {
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
area.h = (pageMask.clientWidth / 70 / pageMask.clientWidth)
} else {
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth)
@@ -1477,7 +1501,7 @@ export default {
if (this.dragField?.options?.length) {
field.options = this.dragField.options.map(option => ({ value: option, uuid: v4() }))
} else {
field.options = [{ value: '', uuid: v4() }]
field.options = [{ value: '', uuid: v4() }, { value: '', uuid: v4() }]
}
}
@@ -1487,8 +1511,12 @@ export default {
field.default_value = '{{date}}'
}
if (['stamp', 'heading'].includes(field.type)) {
if (['stamp', 'heading', 'strikethrough'].includes(field.type)) {
field.readonly = true
if (field.type === 'strikethrough') {
field.default_value = true
}
}
if (field.type === 'date') {
@@ -1576,6 +1604,11 @@ export default {
w: area.maskW / 10 / area.maskW,
h: area.maskW / 35 / area.maskW
}
} else if (fieldType === 'strikethrough') {
baseArea = {
w: area.maskW / 5 / area.maskW,
h: area.maskW / 70 / area.maskW
}
} else {
baseArea = {
w: area.maskW / 5 / area.maskW,
@@ -1709,8 +1742,15 @@ export default {
})
})
this.template.fields =
this.template.fields.filter((f) => !removedFieldUuids.includes(f.uuid) || f.areas?.length)
this.template.fields = this.template.fields.reduce((acc, f) => {
if (removedFieldUuids.includes(f.uuid) && !f.areas?.length) {
this.removeFieldConditions(f)
} else {
acc.push(f)
}
return acc
}, [])
this.save()
}
@@ -9,7 +9,7 @@
<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('condition') }} - {{ item.name || buildDefaultName(item, template.fields) }}
{{ t('condition') }} - {{ (defaultField ? (defaultField.title || item.title || item.name) : item.name) || buildDefaultName(item, template.fields) }}
</span>
<a
href="#"
@@ -160,6 +160,11 @@ export default {
type: Object,
required: true
},
defaultField: {
type: Object,
required: false,
default: null
},
buildDefaultName: {
type: Function,
required: true
@@ -172,10 +177,13 @@ export default {
}
},
computed: {
excludeTypes () {
return ['heading', 'strikethrough']
},
fields () {
if (this.item.submitter_uuid) {
return this.template.fields.reduce((acc, f) => {
if (f !== this.item && (!f.conditions?.length || !f.conditions.find((c) => c.field_uuid === this.item.uuid))) {
if (f !== this.item && !this.excludeTypes.includes(f.type) && (!f.conditions?.length || !f.conditions.find((c) => c.field_uuid === this.item.uuid))) {
acc.push(f)
}
+1 -1
View File
@@ -66,7 +66,7 @@ export default {
acceptFileTypes: {
type: String,
required: false,
default: 'image/*, application/pdf'
default: 'image/*, application/pdf, application/zip'
},
withReplaceButton: {
type: Boolean,
@@ -9,7 +9,7 @@
<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">
{{ field.name || buildDefaultName(field, template.fields) }}
{{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field, template.fields) }}
</span>
<a
href="#"
@@ -73,6 +73,11 @@ export default {
type: Object,
required: true
},
defaultField: {
type: Object,
required: false,
default: null
},
editable: {
type: Boolean,
required: false,
+3 -3
View File
@@ -107,7 +107,7 @@ export default {
acceptFileTypes: {
type: String,
required: false,
default: 'image/*, application/pdf'
default: 'image/*, application/pdf, application/zip'
}
},
emits: ['success', 'error', 'loading'],
@@ -131,7 +131,7 @@ export default {
message () {
if (this.isLoading) {
return this.t('uploading')
} else if (this.acceptFileTypes === 'image/*, application/pdf') {
} else if (this.acceptFileTypes === 'image/*, application/pdf, application/zip') {
return this.title || this.t('add_pdf_documents_or_images')
} else {
return this.title || this.t('add_documents_or_images')
@@ -146,7 +146,7 @@ export default {
methods: {
upload: Upload.methods.upload,
onDropFiles (e) {
if (this.acceptFileTypes !== 'image/*, application/pdf' || [...e.dataTransfer.files].every((f) => f.type.match(/(?:image\/)|(?:application\/pdf)/))) {
if (this.acceptFileTypes !== 'image/*, application/pdf, application/zip' || [...e.dataTransfer.files].every((f) => f.type.match(/(?:image\/)|(?:application\/pdf)|(?:application\/zip)/))) {
this.$refs.input.files = e.dataTransfer.files
this.upload()
+38 -7
View File
@@ -14,7 +14,7 @@
<div class="flex items-center p-1 space-x-1">
<FieldType
v-model="field.type"
:editable="editable && !defaultField && field.type != 'heading'"
:editable="editable && !defaultField"
:button-width="20"
:menu-classes="'mt-1.5'"
:menu-style="{ backgroundColor: dropdownBgColor }"
@@ -97,7 +97,7 @@
@click-formula="isShowFormulaModal = true"
/>
<span
v-else-if="field.type !== 'heading'"
v-else
class="dropdown dropdown-end field-settings-dropdown"
@mouseenter="renderDropdown = true"
@touchstart="renderDropdown = true"
@@ -151,7 +151,7 @@
</div>
</div>
<div
v-if="field.options && withOptions"
v-if="field.options && withOptions && (isExpandOptions || field.options.length < 5)"
ref="options"
class="border-t border-base-300 mx-2 pt-2 space-y-1.5"
draggable="true"
@@ -216,12 +216,30 @@
/>
<button
v-else-if="field.options && editable && !defaultField"
class="text-center text-sm w-full pb-1"
class="field-add-option text-center text-sm w-full pb-1"
@click="addOption"
>
+ {{ t('add_option') }}
</button>
</div>
<div
v-else-if="field.options && withOptions && !isExpandOptions && field.options.length > 4"
class="border-t border-base-300 mx-2 space-y-1.5"
>
<button
class="field-expand-options text-center text-sm w-full py-1 flex space-x-0.5 justify-center items-center"
@click="isExpandOptions = true"
>
<span class="lowercase">
{{ field.options.length }} {{ t('options') }}
</span>
<IconChevronDown
class="ml-2 mr-2 mt-0.5"
width="15"
height="15"
/>
</button>
</div>
</div>
<Teleport
v-if="isShowFormulaModal"
@@ -230,6 +248,7 @@
<FormulaModal
:field="field"
:editable="editable && !defaultField"
:default-field="defaultField"
:build-default-name="buildDefaultName"
@close="isShowFormulaModal = false"
/>
@@ -241,6 +260,7 @@
<FontModal
:field="field"
:editable="editable && !defaultField"
:default-field="defaultField"
:build-default-name="buildDefaultName"
@close="isShowFontModal = false"
/>
@@ -251,6 +271,7 @@
>
<ConditionsModal
:item="field"
:default-field="defaultField"
:build-default-name="buildDefaultName"
@close="isShowConditionsModal = false"
/>
@@ -262,6 +283,7 @@
<DescriptionModal
:field="field"
:editable="editable && !defaultField"
:default-field="defaultField"
:build-default-name="buildDefaultName"
@close="isShowDescriptionModal = false"
/>
@@ -278,7 +300,7 @@ import FormulaModal from './formula_modal'
import FontModal from './font_modal'
import ConditionsModal from './conditions_modal'
import DescriptionModal from './description_modal'
import { IconRouteAltLeft, IconMathFunction, IconNewSection, IconTrashX, IconSettings } from '@tabler/icons-vue'
import { IconRouteAltLeft, IconMathFunction, IconNewSection, IconTrashX, IconSettings, IconChevronDown } from '@tabler/icons-vue'
import { v4 } from 'uuid'
export default {
@@ -288,6 +310,7 @@ export default {
IconSettings,
FieldSettings,
PaymentSettings,
IconChevronDown,
IconNewSection,
FormulaModal,
FontModal,
@@ -333,6 +356,7 @@ export default {
emits: ['set-draw', 'remove', 'scroll-to'],
data () {
return {
isExpandOptions: false,
isNameFocus: false,
showPaymentModal: false,
isShowFormulaModal: false,
@@ -397,7 +421,7 @@ export default {
} else {
const typeIndex = fields.filter((f) => f.type === field.type).indexOf(field)
if (field.type === 'heading') {
if (field.type === 'heading' || field.type === 'strikethrough') {
return `${this.fieldNames[field.type]} ${typeIndex + 1}`
} else {
return `${this.fieldLabels[field.type]} ${typeIndex + 1}`
@@ -427,6 +451,8 @@ export default {
this.$el.getRootNode().activeElement.blur()
},
addOption () {
this.isExpandOptions = true
this.field.options.push({ value: '', uuid: v4() })
this.$nextTick(() => {
@@ -459,10 +485,15 @@ export default {
this.field.options ||= [{ value: '', uuid: v4() }]
}
if (['heading'].includes(this.field.type)) {
if (this.field.type === 'heading') {
this.field.readonly = true
}
if (this.field.type === 'strikethrough') {
this.field.readonly = true
this.field.default_value = true
}
(this.field.areas || []).forEach((area) => {
if (this.field.type === 'cells') {
area.cell_w = area.w * 2 / Math.floor(area.w / area.h)
@@ -1,31 +1,4 @@
<template>
<div
v-if="field.type === 'number'"
class="py-1.5 px-1 relative"
@click.stop
>
<select
:placeholder="t('format')"
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
@change="[field.preferences ||= {}, field.preferences.format = $event.target.value, save()]"
>
<option
v-for="format in numberFormats"
:key="format"
:value="format"
:selected="format === field.preferences?.format || (format === 'none' && !field.preferences?.format)"
>
{{ formatNumber(123456789.567, format) }}
</option>
</select>
<label
:style="{ backgroundColor }"
class="absolute -top-1 left-2.5 px-1 h-4"
style="font-size: 8px"
>
{{ t('format') }}
</label>
</div>
<div
v-if="field.type === 'verification'"
class="py-1.5 px-1 relative"
@@ -217,6 +190,77 @@
</label>
</div>
</div>
<div
v-if="field.type === 'number'"
class="py-1.5 px-1 relative flex space-x-1"
@click.stop
>
<div class="w-1/2 relative">
<input
:placeholder="t('min')"
type="number"
min="0"
:value="field.validation?.min"
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
@input="[field.validation ||= {}, $event.target.value ? field.validation.min = $event.target.value : delete field.validation.min]"
@blur="save"
>
<label
v-if="field.validation?.min"
:style="{ backgroundColor }"
class="absolute -top-2.5 left-1.5 px-1 h-4"
style="font-size: 8px"
>
{{ t('min') }}
</label>
</div>
<div class="w-1/2 relative">
<input
:placeholder="t('max')"
type="number"
min="1"
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
:value="field.validation?.max"
@input="[field.validation ||= {}, $event.target.value ? field.validation.max = $event.target.value : delete field.validation.max]"
@blur="save"
>
<label
v-if="field.validation?.max"
:style="{ backgroundColor }"
class="absolute -top-2.5 left-1.5 px-1 h-4"
style="font-size: 8px"
>
{{ t('max') }}
</label>
</div>
</div>
<div
v-if="field.type === 'number'"
class="py-1.5 px-1 relative"
@click.stop
>
<select
:placeholder="t('format')"
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
@change="[field.preferences ||= {}, field.preferences.format = $event.target.value, save()]"
>
<option
v-for="format in numberFormats"
:key="format"
:value="format"
:selected="format === field.preferences?.format || (format === 'none' && !field.preferences?.format)"
>
{{ formatNumber(123456789.567, format) }}
</option>
</select>
<label
:style="{ backgroundColor }"
class="absolute -top-1 left-2.5 px-1 h-4"
style="font-size: 8px"
>
{{ t('format') }}
</label>
</div>
<div
v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation"
class="py-1.5 px-1 relative"
@@ -336,7 +380,7 @@
</label>
</li>
<li
v-if="withRequired && field.type !== 'phone' && field.type !== 'stamp' && field.type !== 'verification'"
v-if="withRequired && field.type !== 'phone' && field.type !== 'stamp' && field.type !== 'verification' && field.type !== 'strikethrough' && field.type !== 'heading'"
@click.stop
>
<label class="cursor-pointer py-1.5">
@@ -393,7 +437,7 @@
</label>
</li>
<li
v-if="['text', 'number'].includes(field.type) && !defaultField"
v-if="['text', 'number'].includes(field.type)"
@click.stop
>
<label class="cursor-pointer py-1.5">
@@ -401,6 +445,7 @@
v-model="field.readonly"
type="checkbox"
class="toggle toggle-xs"
:disabled="!editable || (defaultField && [true, false].includes(defaultField.readonly))"
@update:model-value="save"
>
<span class="label-text">{{ t('read_only') }}</span>
@@ -425,7 +470,7 @@
v-if="field.type != 'stamp'"
class="pb-0.5 mt-0.5"
>
<li v-if="['text', 'number', 'date', 'select'].includes(field.type)">
<li v-if="['text', 'number', 'date', 'select', 'heading'].includes(field.type)">
<label
class="label-text cursor-pointer text-center w-full flex items-center"
@click="$emit('click-font')"
@@ -439,7 +484,7 @@
</label>
</li>
<li
v-if="field.type != 'stamp'"
v-if="field.type != 'stamp' && field.type != 'heading' && field.type != 'strikethrough'"
>
<label
class="label-text cursor-pointer text-center w-full flex items-center"
@@ -454,7 +499,7 @@
</label>
</li>
<li
v-if="field.type != 'stamp'"
v-if="field.type != 'stamp' && field.type != 'heading'"
>
<label
class="label-text cursor-pointer text-center w-full flex items-center"
@@ -51,7 +51,7 @@
</template>
<script>
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading, IconId, IconCalendarCheck } from '@tabler/icons-vue'
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading, IconId, IconCalendarCheck, IconStrikethrough } from '@tabler/icons-vue'
export default {
name: 'FiledTypeDropdown',
@@ -97,6 +97,7 @@ export default {
fieldNames () {
return {
heading: this.t('heading'),
strikethrough: this.t('strikeout'),
text: this.t('text'),
signature: this.t('signature'),
initials: this.t('initials'),
@@ -139,6 +140,7 @@ export default {
fieldIcons () {
return {
heading: IconHeading,
strikethrough: IconStrikethrough,
text: IconTextSize,
signature: IconWritingSign,
initials: IconLetterCaseUpper,
@@ -158,6 +160,9 @@ export default {
verification: IconId
}
},
skipTypes () {
return ['heading', 'datenow', 'strikethrough']
},
fieldIconsSorted () {
if (this.fieldTypes.length) {
return this.fieldTypes.reduce((acc, type) => {
@@ -166,7 +171,7 @@ export default {
return acc
}, {})
} else {
return Object.fromEntries(Object.entries(this.fieldIcons).filter(([key]) => key !== 'heading' && key !== 'datenow'))
return Object.fromEntries(Object.entries(this.fieldIcons).filter(([key]) => !this.skipTypes.includes(key)))
}
}
},
+4 -1
View File
@@ -351,6 +351,9 @@ export default {
return acc
}, {})
},
skipTypes () {
return ['heading', 'datenow', 'strikethrough']
},
fieldIconsSorted () {
if (this.fieldTypes.length) {
return this.fieldTypes.reduce((acc, type) => {
@@ -359,7 +362,7 @@ export default {
return acc
}, {})
} else {
return Object.fromEntries(Object.entries(this.fieldIcons).filter(([key]) => key !== 'heading' && key !== 'datenow'))
return Object.fromEntries(Object.entries(this.fieldIcons).filter(([key]) => !this.skipTypes.includes(key)))
}
},
submitterFields () {
@@ -9,7 +9,7 @@
<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) }}
{{ t('font') }} - {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field, template.fields) }}
</span>
<a
href="#"
@@ -202,6 +202,11 @@ export default {
type: Object,
required: true
},
defaultField: {
type: Object,
required: false,
default: null
},
editable: {
type: Boolean,
required: false,
@@ -9,7 +9,7 @@
<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('formula') }} - {{ field.name || buildDefaultName(field, template.fields) }}
{{ t('formula') }} - {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field, template.fields) }}
</span>
<a
href="#"
@@ -28,14 +28,21 @@
class="link"
>{{ t('available_in_pro') }}</a>
</div>
<div class="flex-inline mb-2 gap-2 space-y-1">
<div class="flex flex-wrap mb-2 gap-y-1 pt-1">
<button
v-for="f in fields"
:key="f.uuid"
class="mr-1 btn btn-neutral btn-outline border-base-content/20 btn-sm normal-case font-normal bg-white !rounded-xl"
class="mr-1 flex btn btn-neutral btn-outline border-base-content/20 btn-sm normal-case font-normal bg-white !rounded-xl"
@click.prevent="insertTextUnderCursor(`{{${f.name || buildDefaultName(f, template.fields)}}}`)"
>
<IconMathFunction
v-if="f.preferences?.formula"
width="17"
height="17"
stroke-width="1.5"
/>
<IconCodePlus
v-else
width="20"
height="20"
stroke-width="1.5"
@@ -57,7 +64,7 @@
<div class="mb-3 mt-1">
<div
target="blank"
class="text-sm mb-2 inline space-x-2"
class="text-sm mb-2 inline space-x-2 font-mono"
>
<button
class="bg-base-200 px-2 rounded-xl"
@@ -83,12 +90,6 @@
>
/
</button>
<button
class="bg-base-200 px-2 rounded-xl"
@click="insertTextUnderCursor(' % ')"
>
%
</button>
<button
class="bg-base-200 px-2 rounded-xl"
@click="insertTextUnderCursor('^')"
@@ -122,12 +123,13 @@
</template>
<script>
import { IconCodePlus } from '@tabler/icons-vue'
import { IconCodePlus, IconMathFunction } from '@tabler/icons-vue'
export default {
name: 'FormulaModal',
components: {
IconCodePlus
IconCodePlus,
IconMathFunction
},
inject: ['t', 'save', 'template', 'withFormula'],
props: {
@@ -135,6 +137,11 @@ export default {
type: Object,
required: true
},
defaultField: {
type: Object,
required: false,
default: null
},
editable: {
type: Boolean,
required: false,
@@ -154,7 +161,7 @@ export default {
computed: {
fields () {
return this.template.fields.reduce((acc, f) => {
if (f !== this.field && ['number'].includes(f.type) && (!f.preferences?.formula || f.submitter_uuid !== this.field.submitter_uuid)) {
if (f !== this.field && ['number'].includes(f.type) && (!f.preferences?.formula || !f.preferences.formula.includes(this.field.uuid))) {
acc.push(f)
}
@@ -224,9 +231,11 @@ export default {
this.formula = newText
textarea.setSelectionRange(cursorPos + textToInsert.length, cursorPos + textToInsert.length)
this.$nextTick(() => {
textarea.setSelectionRange(cursorPos + textToInsert.length, cursorPos + textToInsert.length)
textarea.focus()
textarea.focus()
})
},
resizeTextarea () {
const textarea = this.$refs.textarea
@@ -78,7 +78,7 @@ export default {
acceptFileTypes: {
type: String,
required: false,
default: 'image/*, application/pdf'
default: 'image/*, application/pdf, application/zip'
}
},
emits: ['add', 'replace', 'replace-and-clone', 'error'],
+30
View File
@@ -1,4 +1,8 @@
const en = {
payment_link: 'Payment link',
strikeout: 'Strikeout',
draw_strikethrough_the_document: 'Draw strikethrough the document',
quantity: 'Quantity',
prefillable: 'Prefillable',
signature_id: 'Signature ID',
error_message: 'Error message',
@@ -83,6 +87,7 @@ const en = {
copy_to_all_pages: 'Copy to All Pages',
add_option: 'Add option',
option: 'Option',
options: 'Options',
condition: 'Condition',
first_party: 'First Party',
second_party: 'Second Party',
@@ -175,6 +180,10 @@ const en = {
}
const es = {
payment_link: 'Enlace de pago',
strikeout: 'Tachar',
draw_strikethrough_the_document: 'Dibujar una línea de tachado en el documento',
quantity: 'Cantidad',
prefillable: 'Rellenable',
signature_id: 'ID de Firma',
error_message: 'Mensaje de error',
@@ -255,6 +264,7 @@ const es = {
copy_to_all_pages: 'Copiar a todas las páginas',
add_option: 'Agregar opción',
option: 'Opción',
options: 'Opciones',
first_party: 'Primera Parte',
second_party: 'Segunda Parte',
third_party: 'Tercera Parte',
@@ -351,6 +361,10 @@ const es = {
}
const it = {
payment_link: 'Link di pagamento',
strikeout: 'Barrato',
draw_strikethrough_the_document: 'Disegna una linea barrata sul documento',
quantity: 'Quantità',
prefillable: 'Precompilabile',
signature_id: 'ID firma',
error_message: 'Messaggio di errore',
@@ -435,6 +449,7 @@ const it = {
copy_to_all_pages: 'Copia in tutte le pagine',
add_option: 'Aggiungi opzione',
option: 'Opzione',
options: 'Opzioni',
condition: 'Condizione',
first_party: 'Prima parte',
second_party: 'Seconda parte',
@@ -527,6 +542,10 @@ const it = {
}
const pt = {
payment_link: 'Link de pagamento',
strikeout: 'Tachado',
draw_strikethrough_the_document: 'Desenhe uma linha de tachado no documento',
quantity: 'Quantidade',
prefillable: 'Pré-preenchível',
signature_id: 'ID da Assinatura',
error_message: 'Mensagem de erro',
@@ -607,6 +626,7 @@ const pt = {
copy_to_all_pages: 'Copiar para todas as páginas',
add_option: 'Adicionar opção',
option: 'Opção',
options: 'Opções',
first_party: 'Primeira Parte',
second_party: 'Segunda Parte',
third_party: 'Terceira Parte',
@@ -703,6 +723,10 @@ const pt = {
}
const fr = {
payment_link: 'Lien de paiement',
strikeout: 'Barrer',
draw_strikethrough_the_document: 'Tracer une ligne de suppression sur le document',
quantity: 'Quantité',
prefillable: 'Pré-remplissable',
signature_id: 'ID de signature',
error_message: 'Message d\'erreur',
@@ -782,6 +806,7 @@ const fr = {
copy_to_all_pages: 'Copier sur toutes les pages',
add_option: 'Ajouter une option',
option: 'Option',
options: 'Options',
first_party: 'Première partie',
second_party: 'Deuxième partie',
third_party: 'Troisième partie',
@@ -879,6 +904,10 @@ const fr = {
}
const de = {
payment_link: 'Zahlungslink',
strikeout: 'Streichung',
draw_strikethrough_the_document: 'Ziehe eine Streichung auf das Dokument',
quantity: 'Menge',
prefillable: 'Vorausfüllbar',
signature_id: 'Signatur-ID',
error_message: 'Fehlermeldung',
@@ -958,6 +987,7 @@ const de = {
copy_to_all_pages: 'Auf alle Seiten kopieren',
add_option: 'Option hinzufügen',
option: 'Option',
options: 'Options',
first_party: 'Erste Partei',
second_party: 'Zweite Partei',
third_party: 'Dritte Partei',
+5 -1
View File
@@ -1,7 +1,7 @@
<template>
<div
class="relative select-none mb-4 before:border before:rounded before:top-0 before:bottom-0 before:left-0 before:right-0 before:absolute"
:class="{ 'cursor-crosshair': allowDraw, 'touch-none': !!drawField }"
:class="{ 'cursor-crosshair': allowDraw && editable, 'touch-none': !!drawField }"
style="container-type: size"
:style="{ aspectRatio: `${width} / ${height}`}"
>
@@ -24,6 +24,8 @@
:ref="setAreaRefs"
:area="item.area"
:input-mode="inputMode"
:page-width="width"
:page-height="height"
:field="item.field"
:editable="editable"
:with-field-placeholder="withFieldPlaceholder"
@@ -40,6 +42,8 @@
<FieldArea
v-if="newArea"
:is-draw="true"
:page-width="width"
:page-height="height"
:field="{ submitter_uuid: selectedSubmitter.uuid, type: drawField?.type || dragFieldPlaceholder?.type || defaultFieldType }"
:area="newArea"
/>
@@ -1,7 +1,7 @@
<template>
<span
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 && !field.preferences?.price_id && !field.preferences?.payment_link_id) || !isConnected) && !isLoading }"
>
<label
tabindex="0"
@@ -21,7 +21,7 @@
@click="closeDropdown"
>
<div
v-if="!('price_id' in field.preferences)"
v-if="!('price_id' in field.preferences) && !('payment_link_id' in field.preferences)"
class="py-1.5 px-1 relative"
@click.stop
>
@@ -52,10 +52,9 @@
@click.stop
>
<input
v-if="field.preferences.formula"
type="number"
:placeholder="t('price')"
disabled="true"
v-if="'payment_link_id' in field.preferences"
v-model="field.preferences.payment_link_id"
placeholder="plink_XXXXX"
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
@blur="save"
>
@@ -66,6 +65,14 @@
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
@blur="save"
>
<input
v-else-if="field.preferences.formula"
type="number"
:placeholder="t('price')"
disabled="true"
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
@blur="save"
>
<input
v-else
v-model="field.preferences.price"
@@ -75,29 +82,41 @@
@blur="save"
>
<label
v-if="field.preferences.price && !field.preferences.formula"
v-if="(field.preferences.price || field.preferences.price_id || field.preferences.payment_link_id) && (!field.preferences.formula || ('price_id' in field.preferences) || ('payment_link_id' in field.preferences))"
:style="{ backgroundColor: backgroundColor }"
class="absolute -top-1 left-2.5 px-1 h-4"
style="font-size: 8px"
>
{{ t('price') }}
{{ 'payment_link_id' in field.preferences ? t('payment_link') : t('price') }}
</label>
<div class="flex items-center justify-center">
<a
href="#"
class="hover:underline"
style="font-size: 11px"
:class="{'underline': !('price_id' in field.preferences)}"
@click="delete field.preferences.price_id"
:class="{'underline': !('payment_link_id' in field.preferences)}"
@click="[delete field.preferences.price_id, delete field.preferences.payment_link_id]"
>{{ t('one_off') }}</a>
<span class="h-2.5 border-l border-base-content mx-1" />
<template
v-if="field.preferences.price_id"
>
<a
href="#"
class="hover:underline"
style="font-size: 11px"
:class="{'underline': ('price_id' in field.preferences)}"
@click="field.preferences.payment_link_id ??= ''"
>{{ t('recurrent') }}</a>
<span class="h-2.5 border-l border-base-content mx-1" />
</template>
<a
href="#"
class="hover:underline"
style="font-size: 11px"
:class="{'underline': ('price_id' in field.preferences)}"
@click="field.preferences.price_id ??= ''"
>{{ t('recurrent') }}</a>
:class="{'underline': ('payment_link_id' in field.preferences)}"
@click="[delete field.preferences.price_id, field.preferences.payment_link_id ??= '']"
>{{ t('payment_link') }}</a>
</div>
</div>
<div
@@ -184,7 +203,6 @@
>{{ t('learn_more') }}</a>
</div>
<li
v-if="!('price_id' in field.preferences)"
class="mb-1"
>
<label
@@ -195,7 +213,7 @@
width="18"
/>
<span class="text-sm">
{{ t('formula') }}
{{ 'payment_link_id' in field.preferences ? t('quantity') : t('formula') }}
</span>
</label>
</li>
+1 -1
View File
@@ -157,7 +157,7 @@ export default {
acceptFileTypes: {
type: String,
required: false,
default: 'image/*, application/pdf'
default: 'image/*, application/pdf, application/zip'
},
withReplaceButton: {
type: Boolean,
+1 -1
View File
@@ -35,7 +35,7 @@ export default {
acceptFileTypes: {
type: String,
required: false,
default: 'image/*, application/pdf'
default: 'image/*, application/pdf, application/zip'
}
},
emits: ['success'],
+1 -1
View File
@@ -57,7 +57,7 @@ export default {
acceptFileTypes: {
type: String,
required: false,
default: 'image/*, application/pdf'
default: 'image/*, application/pdf, application/zip'
}
},
emits: ['success', 'error'],
+13 -5
View File
@@ -14,10 +14,10 @@ class ProcessSubmitterCompletionJob
if is_all_completed && submitter.completed_at == submitter.submission.submitters.maximum(:completed_at)
if submitter.submission.account.account_configs.exists?(key: AccountConfig::COMBINE_PDF_RESULT_KEY, value: true)
Submissions::GenerateCombinedAttachment.call(submitter)
Submissions::EnsureCombinedGenerated.call(submitter)
end
Submissions::GenerateAuditTrail.call(submitter.submission)
Submissions::EnsureAuditGenerated.call(submitter.submission)
enqueue_completed_emails(submitter)
end
@@ -38,12 +38,19 @@ class ProcessSubmitterCompletionJob
submission = submitter.submission
complete_verification_events, sms_events =
submitter.submission_events.where(event_type: %i[send_sms send_2fa_sms complete_verification])
.partition { |e| e.event_type == 'complete_verification' }
complete_verification_event = complete_verification_events.first
completed_submitter.assign_attributes(
submission_id: submitter.submission_id,
account_id: submission.account_id,
template_id: submission.template_id,
source: submission.source,
sms_count: submitter.submission_events.where(event_type: %w[send_sms send_2fa_sms]).count,
sms_count: sms_events.sum { |e| e.data['segments'] || 1 },
verification_method: complete_verification_event&.data&.dig('method'),
completed_at: submitter.completed_at
)
@@ -146,7 +153,8 @@ class ProcessSubmitterCompletionJob
current_group_index = submitter_groups.find { |_, group| group.any? { |s| s['uuid'] == submitter.uuid } }&.first
if submitter_groups[current_group_index + 1] &&
submitters_index.values_at(*submitter_groups[current_group_index].pluck('uuid')).all?(&:completed_at?)
submitters_index.values_at(*submitter_groups[current_group_index].pluck('uuid'))
.compact.all?(&:completed_at?)
submitter_groups[current_group_index + 1]
end
else
@@ -159,7 +167,7 @@ class ProcessSubmitterCompletionJob
end
end
next_submitters = submitters_index.values_at(*Array.wrap(next_submitter_items).pluck('uuid'))
next_submitters = submitters_index.values_at(*Array.wrap(next_submitter_items).pluck('uuid')).compact
Submitters.send_signature_requests(next_submitters)
end
+2
View File
@@ -38,9 +38,11 @@ class AccountConfig < ApplicationRecord
FORM_PREFILL_SIGNATURE_KEY = 'form_prefill_signature'
ESIGNING_PREFERENCE_KEY = 'esigning_preference'
DOWNLOAD_LINKS_AUTH_KEY = 'download_links_auth'
DOWNLOAD_LINKS_EXPIRE_KEY = 'download_links_expire'
FORCE_SSO_AUTH_KEY = 'force_sso_auth'
FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf'
WITH_SIGNATURE_ID = 'with_signature_id'
WITH_FILE_LINKS_KEY = 'with_file_links'
WITH_SIGNATURE_ID_REASON_KEY = 'with_signature_id_reason'
WITH_AUDIT_VALUES_KEY = 'with_audit_values'
WITH_SUBMITTER_TIMEZONE_KEY = 'with_submitter_timezone'
+12 -11
View File
@@ -4,16 +4,17 @@
#
# Table name: completed_submitters
#
# id :bigint not null, primary key
# completed_at :datetime not null
# sms_count :integer not null
# source :string not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# submission_id :bigint not null
# submitter_id :bigint not null
# template_id :bigint
# id :bigint not null, primary key
# completed_at :datetime not null
# sms_count :integer not null
# source :string not null
# verification_method :string
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# submission_id :bigint not null
# submitter_id :bigint not null
# template_id :bigint
#
# Indexes
#
@@ -29,5 +30,5 @@ class CompletedSubmitter < ApplicationRecord
has_many :completed_documents, dependent: :destroy,
primary_key: :submitter_id,
foreign_key: :submitter_id,
inverse_of: :submitter
inverse_of: :completed_submitter
end
+1 -1
View File
@@ -20,7 +20,7 @@
#
# index_email_events_on_account_id_and_event_datetime (account_id,event_datetime)
# index_email_events_on_email (email)
# index_email_events_on_email_event_types (email) WHERE ((event_type)::text = ANY ((ARRAY['bounce'::character varying, 'soft_bounce'::character varying, 'complaint'::character varying, 'soft_complaint'::character varying])::text[]))
# index_email_events_on_email_event_types (email) WHERE ((event_type)::text = ANY ((ARRAY['bounce'::character varying, 'soft_bounce'::character varying, 'permanent_bounce'::character varying, 'complaint'::character varying, 'soft_complaint'::character varying])::text[]))
# index_email_events_on_emailable (emailable_type,emailable_id)
# index_email_events_on_message_id (message_id)
#
+25
View File
@@ -0,0 +1,25 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: lock_events
#
# id :bigint not null, primary key
# event_name :string not null
# key :string not null
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_lock_events_on_event_name_and_key (event_name,key) UNIQUE WHERE ((event_name)::text = ANY ((ARRAY['start'::character varying, 'complete'::character varying])::text[]))
# index_lock_events_on_key (key)
#
class LockEvent < ApplicationRecord
enum :event_name, {
complete: 'complete',
fail: 'fail',
start: 'start',
retry: 'retry'
}, scope: false
end
+8 -4
View File
@@ -15,6 +15,8 @@
# template_fields :text
# template_schema :text
# template_submitters :text
# variables :text
# variables_schema :text
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
@@ -50,6 +52,8 @@ class Submission < ApplicationRecord
serialize :template_fields, coder: JSON
serialize :template_schema, coder: JSON
serialize :template_submitters, coder: JSON
serialize :variables_schema, coder: JSON
serialize :variables, coder: JSON
serialize :preferences, coder: JSON
attribute :source, :string, default: 'link'
@@ -114,16 +118,16 @@ class Submission < ApplicationRecord
@fields_uuid_index ||= (template_fields || template.fields).index_by { |f| f['uuid'] }
end
def audit_trail_url
def audit_trail_url(expires_at: nil)
return if audit_trail.blank?
ActiveStorage::Blob.proxy_url(audit_trail.blob)
ActiveStorage::Blob.proxy_url(audit_trail.blob, expires_at:)
end
alias audit_log_url audit_trail_url
def combined_document_url
def combined_document_url(expires_at: nil)
return if combined_document.blank?
ActiveStorage::Blob.proxy_url(combined_document.blob)
ActiveStorage::Blob.proxy_url(combined_document.blob, expires_at:)
end
end
+18 -16
View File
@@ -4,22 +4,23 @@
#
# Table name: templates
#
# id :bigint not null, primary key
# archived_at :datetime
# fields :text not null
# name :string not null
# preferences :text not null
# schema :text not null
# shared_link :boolean default(FALSE), not null
# slug :string not null
# source :text not null
# submitters :text not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# author_id :bigint not null
# external_id :string
# folder_id :bigint not null
# id :bigint not null, primary key
# archived_at :datetime
# fields :text not null
# name :string not null
# preferences :text not null
# schema :text not null
# shared_link :boolean default(FALSE), not null
# slug :string not null
# source :text not null
# submitters :text not null
# variables_schema :text
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# author_id :bigint not null
# external_id :string
# folder_id :bigint not null
#
# Indexes
#
@@ -57,6 +58,7 @@ class Template < ApplicationRecord
serialize :preferences, coder: JSON
serialize :fields, coder: JSON
serialize :variables_schema, coder: JSON
serialize :schema, coder: JSON
serialize :submitters, coder: JSON
+136 -53
View File
@@ -50,11 +50,16 @@
<% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %>
<div class="flex items-center justify-between py-2.5">
<span>
<%= t('force_2fa_with_authenticator_app') %>
</span>
<%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()' %>
<div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1">
<span class="text-left"><%= t('force_2fa_with_authenticator_app') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('require_two_factor_authentication_2fa_with_an_authenticator_app_e_g_google_authenticator_authy_all_users_signing_documents_must_pass_the_second_factor_verification_using_a_secure_code_in_addition_to_their_password') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
</span>
</div>
<submit-form data-on="change" class="flex">
<%= f.check_box :value, class: 'toggle', checked: account_config.value %>
</submit-form>
</div>
<% end %>
<% end %>
@@ -62,11 +67,16 @@
<% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %>
<div class="flex items-center justify-between py-2.5">
<span>
<%= t('add_signature_id_to_the_documents') %>
</span>
<%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()', disabled: can?(:manage, :cfr) %>
<div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1">
<span class="text-left"><%= t('add_signature_id_to_the_documents') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('add_a_unique_signature_id_and_timestamp_to_each_signature_for_audit_and_traceability_purposes_along_with_the_timestamp_part_of_docuseals_21_cfr_part_11_compliance_settings') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
</span>
</div>
<submit-form data-on="change" class="flex">
<%= f.check_box :value, class: 'toggle', checked: account_config.value, disabled: can?(:manage, :cfr) %>
</submit-form>
</div>
<% end %>
<% end %>
@@ -74,11 +84,16 @@
<% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %>
<div class="flex items-center justify-between py-2.5">
<span>
<%= t('require_signing_reason') %>
</span>
<%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()', disabled: can?(:manage, :cfr) %>
<div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1">
<span class="text-left"><%= t('require_signing_reason') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('require_signer_to_provide_a_reason_for_signing_before_completing_their_signature_e_g_approvals_certifications_part_of_docuseals_21_cfr_part_11_compliance_settings') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
</span>
</div>
<submit-form data-on="change" class="flex">
<%= f.check_box :value, class: 'toggle', checked: account_config.value, disabled: can?(:manage, :cfr) %>
</submit-form>
</div>
<% end %>
<% end %>
@@ -86,11 +101,16 @@
<% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %>
<div class="flex items-center justify-between py-2.5">
<span>
<%= t('allow_typed_text_signatures') %>
</span>
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false, onchange: 'this.form.requestSubmit()' %>
<div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1">
<span class="text-left"><%= t('allow_typed_text_signatures') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('allow_signers_to_create_signatures_by_typing_their_name_instead_of_drawing_or_uploading_one') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
</span>
</div>
<submit-form data-on="change" class="flex">
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false %>
</submit-form>
</div>
<% end %>
<% end %>
@@ -98,11 +118,16 @@
<% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %>
<div class="flex items-center justify-between py-2.5">
<span>
<%= t('allow_to_resubmit_completed_forms') %>
</span>
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false, onchange: 'this.form.requestSubmit()' %>
<div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1">
<span class="text-left"><%= t('allow_to_resubmit_completed_forms') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('allow_signers_to_resubmit_forms_after_completion_useful_when_corrections_or_multiple_submissions_are_needed') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
</span>
</div>
<submit-form data-on="change" class="flex">
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false %>
</submit-form>
</div>
<% end %>
<% end %>
@@ -110,11 +135,16 @@
<% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %>
<div class="flex items-center justify-between py-2.5">
<span>
<%= t('allow_to_decline_documents') %>
</span>
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false, onchange: 'this.form.requestSubmit()' %>
<div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1">
<span class="text-left"><%= t('allow_to_decline_documents') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('allow_recipients_to_decline_signing_a_document_the_decline_reason_notification_will_be_sent_to_the_signature_requester') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
</span>
</div>
<submit-form data-on="change" class="flex">
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false %>
</submit-form>
</div>
<% end %>
<% end %>
@@ -122,11 +152,33 @@
<% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %>
<div class="flex items-center justify-between py-2.5">
<span>
<%= t('remember_and_pre_fill_signatures') %>
</span>
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false, onchange: 'this.form.requestSubmit()' %>
<div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1">
<span class="text-left"><%= t('remember_and_pre_fill_signatures') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('save_a_users_signature_and_automatically_pre_fill_it_in_future_signing_sessions') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
</span>
</div>
<submit-form data-on="change" class="flex">
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false %>
</submit-form>
</div>
<% end %>
<% end %>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY) %>
<% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %>
<div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1">
<span class="text-left"><%= t('expirable_file_download_links') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('make_document_download_links_expire_after_40_minutes_to_prevent_long_term_access_and_enhance_security') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
</span>
</div>
<submit-form data-on="change" class="flex">
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false %>
</submit-form>
</div>
<% end %>
<% end %>
@@ -134,11 +186,16 @@
<% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %>
<div class="flex items-center justify-between py-2.5">
<span>
<%= t('require_authentication_for_file_download_links') %>
</span>
<%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()' %>
<div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1">
<span class="text-left"><%= t('require_authentication_for_file_download_links') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('require_authentication_with_user_login_or_api_key_to_access_the_document_download_links') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
</span>
</div>
<submit-form data-on="change" class="flex">
<%= f.check_box :value, class: 'toggle', checked: account_config.value %>
</submit-form>
</div>
<% end %>
<% end %>
@@ -146,11 +203,16 @@
<% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %>
<div class="flex items-center justify-between py-2.5">
<span>
<%= t('combine_completed_documents_and_audit_log') %>
</span>
<%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()' %>
<div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1">
<span class="text-left"><%= t('combine_completed_documents_and_audit_log') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('combine_signed_documents_and_the_audit_log_into_a_single_pdf_file_for_easier_recordkeeping_and_compliance') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
</span>
</div>
<submit-form data-on="change" class="flex">
<%= f.check_box :value, class: 'toggle', checked: account_config.value %>
</submit-form>
</div>
<% end %>
<% end %>
@@ -159,22 +221,43 @@
<% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %>
<div class="flex items-center justify-between py-2.5">
<span>
<%= t('always_enforce_signing_order') %>
</span>
<%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()' %>
<div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1">
<span class="text-left"><%= t('always_enforce_signing_order') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('make_the_recipients_signing_order_always_enforced_so_that_the_second_signer_can_start_signing_their_part_only_after_the_first_signer_has_completed_signing') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
</span>
</div>
<submit-form data-on="change" class="flex">
<%= f.check_box :value, class: 'toggle', checked: account_config.value %>
</submit-form>
</div>
<% end %>
<% end %>
<% end %>
<% if can?(:manage, :personalization_advanced) %>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::WITH_FILE_LINKS_KEY) %>
<% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %>
<div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1">
<span class="text-left"><%= t('use_direct_file_attachment_links_in_the_documents') %></span>
</div>
<submit-form data-on="change" class="flex">
<%= f.check_box :value, class: 'toggle', checked: account_config.value %>
</submit-form>
</div>
<% end %>
<% end %>
<% end %>
<%= render 'extra_preferences' %>
<% if !Docuseal.multitenant? && SearchEntry.table_exists? && (!Docuseal.fulltext_search? || params[:reindex] == 'true') && can?(:manage, EncryptedConfig) %>
<div class="flex items-center justify-between py-2.5">
<div class="flex items-center justify-between gap-4 py-2.5">
<span>
Efficient search with search index
<%= t('efficient_search_with_search_index') %>
</span>
<%= button_to params[:reindex] == 'true' ? 'Reindex' : 'Build Search Index', settings_search_entries_reindex_index_path, method: :post, class: 'btn btn-sm btn-neutral text-white px-4' %>
<%= button_to params[:reindex] == 'true' ? t('reindex') : t('build_search_index'), settings_search_entries_reindex_index_path, method: :post, class: 'btn btn-sm btn-neutral text-white px-4' %>
</div>
<% end %>
</div>
+18 -7
View File
@@ -11,10 +11,21 @@
<div class="flex flex-col md:flex-row gap-4">
<div class="flex w-full space-x-4">
<% token = current_user.access_token.token %>
<masked-input class="block w-full" data-token="<%= token %>">
<input id="api_key" type="text" value="<%= token.sub(token[5..], '*' * token[5..].size) %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
</masked-input>
<%= render 'shared/clipboard_copy', icon: 'copy', text: token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
<% obscured_token = current_user.access_token.token.sub(token[5..], '*' * token[5..].size) %>
<% if current_account.testing? %>
<masked-input class="block w-full" data-token="<%= token %>">
<input id="api_key" type="text" value="<%= obscured_token %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
</masked-input>
<%= render 'shared/clipboard_copy', icon: 'copy', text: token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
<% else %>
<a id="access_token_container" href="<%= settings_reveal_access_token_path %>" data-turbo-frame="modal" class="flex w-full space-x-4">
<input id="api_key" type="text" value="<%= obscured_token %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
<div class="base-button">
<%= svg_icon('copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline"><%= t('copy') %></span>
</div>
</a>
<% end %>
</div>
<%= button_to button_title(title: t('rotate'), disabled_with: t('rotate'), icon: svg_icon('reload', class: 'w-6 h-6')), settings_api_index_path, class: 'white-button w-full', data: { turbo_confirm: t('remove_existing_api_token_and_generated_a_new_one_are_you_sure_') } %>
</div>
@@ -35,7 +46,7 @@
<div class="collapse-content" style="display: inherit">
<div class="mockup-code overflow-hidden">
<% text = capture do %>curl --location '<%= api_submissions_url %>' \
--header 'X-Auth-Token: <%= current_user.access_token.token %>' \
--header 'X-Auth-Token: API_TOKEN' \
--data-raw '{
"template_id": <%= current_account.templates.last&.id || 1 %>,
"submitters": [
@@ -71,7 +82,7 @@
<div class="collapse-content" style="display: inherit">
<div class="mockup-code overflow-hidden">
<% text = capture do %>curl --location '<%= api_submissions_emails_url %>' \
--header 'X-Auth-Token: <%= current_user.access_token.token %>' \
--header 'X-Auth-Token: API_TOKEN' \
--data-raw '{
"template_id": <%= current_account.templates.last&.id || 1 %>,
"emails": "<%= current_user.email.sub('@', '+test@') %>, <%= current_user.email.sub('@', '+test2@') %>"
@@ -97,7 +108,7 @@
<div class="collapse-content" style="display: inherit">
<div class="mockup-code overflow-hidden">
<% text = capture do %>curl '<%= api_template_url(current_account.templates&.last || 1) %>' \
--header 'X-Auth-Token: <%= current_user.access_token.token %>'<% end.to_str %>
--header 'X-Auth-Token: API_TOKEN'<% end.to_str %>
<span class="top-0 right-0 absolute">
<%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
</span>
+16 -9
View File
@@ -2,15 +2,22 @@
<h1 class="text-4xl font-bold text-center mt-8">
<%= t('sign_in') %>
</h1>
<%= form_for(resource, as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: session_path(resource_name)) do |f| %>
<%= f.hidden_field :email %>
<%= f.hidden_field :password %>
<% if params[:redir].present? %>
<%= hidden_field_tag :redir, params[:redir] %>
<% end %>
<%= render 'otp_form', **local_assigns %>
<div class="form-control">
<%= f.button button_title(title: t('sign_in'), disabled_with: t('signing_in')), class: 'base-button' %>
<% if local_assigns[:access_error].present? %>
<div class="alert mt-6">
<%= svg_icon('x_circle', class: 'w-6 h-6 text-red-500') %>
<span><%= local_assigns[:access_error] %></span>
</div>
<% else %>
<%= form_for(resource, as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: session_path(resource_name)) do |f| %>
<%= f.hidden_field :email %>
<%= f.hidden_field :password %>
<% if params[:redir].present? %>
<%= hidden_field_tag :redir, params[:redir] %>
<% end %>
<%= render 'otp_form', **local_assigns %>
<div class="form-control">
<%= f.button button_title(title: t('sign_in'), disabled_with: t('signing_in')), class: 'base-button' %>
</div>
<% end %>
<% end %>
</div>
+3 -1
View File
@@ -14,6 +14,8 @@
<% if params[:redir].present? %>
<%= hidden_field_tag :redir, params[:redir] %>
<% end %>
<%= select_tag :lang, options_for_select((I18n.available_locales - %i[en pt-PT de-DE fr-FR it-IT es-ES]).map { |code| [t("language_#{code}"), code] }, I18n.locale), onchange: 'this.form.requestSubmit();', class: 'select select-sm border-base-content/30 text-base' %>
<submit-form data-on="change">
<%= select_tag :lang, options_for_select((I18n.available_locales - %i[en pt-PT de-DE fr-FR it-IT es-ES]).map { |code| [t("language_#{code}"), code] }, I18n.locale), class: 'select select-sm border-base-content/30 text-base' %>
</submit-form>
<% end %>
</div>
@@ -1,52 +0,0 @@
<% eu_server = request.host == 'docuseal.eu' %>
<server-selector>
<div id="global_server_selector" class="text-center hidden">
<div class="join">
<a href="https://docuseal.com<%= request.fullpath.gsub('docuseal.eu', 'docuseal.com') %>" class="btn bg-base-200 join-item w-40 <%= 'bg-base-300' unless eu_server %>">
<%= svg_icon 'world', class: 'w-5 h-5' %>
Global
</a>
<a href="https://docuseal.eu<%= request.fullpath.gsub('docuseal.com', 'docuseal.eu') %>" class="relative btn bg-base-200 join-item w-40 <%= 'bg-base-300' if eu_server %>">
<%= svg_icon 'eu_flag', class: 'w-5 h-5' %>
Europe
<% unless eu_server %>
<span id="eu_server_alert" class="absolute flex space-x-0.5 hidden" style="top: -1.5rem;">
<span class="text-xs font-normal leading-none text-base-content normal-case">
<%= t('eu_data_residency') %>
</span>
<%= svg_icon 'corner_right_down', class: 'w-4 h-5 stroke-1 shrink-0 pt-1' %>
</span>
<% end %>
</a>
</div>
</div>
<div id="server_selector" class="flex justify-center <%= 'hidden' unless eu_server %>">
<div class="dropdown">
<label tabindex="0" class="relative btn btn-sm bg-transparent font-medium normal-case border-base-content/20 justify-start" style="width: 141px; padding: 0 20px">
<% if eu_server %>
<%= svg_icon 'eu_flag', class: 'w-5 h-5' %>
<span>EU Cloud</span>
<% else %>
<%= svg_icon 'usa_flag', class: 'w-5 h-5' %>
<span>US Cloud</span>
<% end %>
<%= svg_icon 'chevron_down', class: 'mr-1 w-4 h-4 absolute right-1' %>
</label>
<ul tabindex="0" class="dropdown-content z-[1] menu border border-base-content/20 mt-1 bg-base-100 rounded-box w-36">
<li>
<a href="https://docuseal.com<%= request.fullpath.gsub('docuseal.eu', 'docuseal.com') %>" class="flex items-center space-x-2 <%= 'bg-base-300' unless eu_server %>">
<%= svg_icon 'usa_flag', class: 'w-5 h-5' %>
US Cloud
</a>
</li>
<li>
<a href="https://docuseal.eu<%= request.fullpath.gsub('docuseal.com', 'docuseal.eu') %>" class="flex items-center space-x-2 <%= 'bg-base-300' if eu_server %>">
<%= svg_icon 'eu_flag', class: 'w-5 h-5' %>
EU Cloud
</a>
</li>
</ul>
</div>
</div>
</server-selector>
<%= render 'scripts/server_selector' %>

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