Compare commits

...
66 Commits
Author SHA1 Message Date
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
Alex Turchyn f627277b45 Merge from docusealco/wip 2025-08-04 12:59:10 +03:00
Pete Matsyburka 809c3ea270 fix template 2fa email 2025-07-31 19:23:27 +03:00
Pete Matsyburka 9caa9d79d4 fix tooltip 2025-07-31 13:32:28 +03:00
Pete Matsyburka a8f1c1c323 add new signature formats 2025-07-31 13:13:31 +03:00
Pete Matsyburka 59fbea5c5f fix migration 2025-07-30 18:45:28 +03:00
Pete Matsyburka d3273d879a add order ui 2025-07-30 10:22:38 +03:00
Pete Matsyburka ea227e82c9 force smtp auto 2025-07-29 22:49:20 +03:00
Pete Matsyburka 676b330aa3 add submitters order param 2025-07-29 17:47:47 +03:00
Pete Matsyburka 8d3295e125 better signature validation 2025-07-29 13:51:11 +03:00
Pete Matsyburka 8cb74e0bcc fix build 2025-07-28 16:55:21 +03:00
127 changed files with 1526 additions and 574 deletions

No files matched your search

+7 -2
View File
@@ -12,10 +12,15 @@ on:
type: string type: string
required: false required: false
default: tonistiigi/binfmt:latest default: tonistiigi/binfmt:latest
os:
description: OS
type: string
required: false
default: ubuntu-24.04-arm
jobs: jobs:
build: build:
runs-on: ubuntu-24.04-arm runs-on: ${{ inputs.os }}
timeout-minutes: 20 timeout-minutes: 20
steps: steps:
@@ -29,7 +34,7 @@ jobs:
uses: docker/metadata-action@v4 uses: docker/metadata-action@v4
with: with:
images: docuseal/docuseal images: docuseal/docuseal
tags: type=semver,pattern={{version}} tags: latest,${{ inputs.version }}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
+1 -1
View File
@@ -50,7 +50,7 @@ ENV OPENSSL_CONF=/app/openssl_legacy.cnf
WORKDIR /app 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\ RUN echo $'.include = /etc/ssl/openssl.cnf\n\
\n\ \n\
+1
View File
@@ -11,6 +11,7 @@ gem 'azure-storage-blob', require: false
gem 'bootsnap', require: false gem 'bootsnap', require: false
gem 'cancancan' gem 'cancancan'
gem 'csv' gem 'csv'
gem 'csv-safe'
gem 'devise' gem 'devise'
gem 'devise-two-factor' gem 'devise-two-factor'
gem 'dotenv', require: false gem 'dotenv', require: false
+95 -82
View File
@@ -1,29 +1,29 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (8.0.1) actioncable (8.0.2.1)
actionpack (= 8.0.1) actionpack (= 8.0.2.1)
activesupport (= 8.0.1) activesupport (= 8.0.2.1)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
actionmailbox (8.0.1) actionmailbox (8.0.2.1)
actionpack (= 8.0.1) actionpack (= 8.0.2.1)
activejob (= 8.0.1) activejob (= 8.0.2.1)
activerecord (= 8.0.1) activerecord (= 8.0.2.1)
activestorage (= 8.0.1) activestorage (= 8.0.2.1)
activesupport (= 8.0.1) activesupport (= 8.0.2.1)
mail (>= 2.8.0) mail (>= 2.8.0)
actionmailer (8.0.1) actionmailer (8.0.2.1)
actionpack (= 8.0.1) actionpack (= 8.0.2.1)
actionview (= 8.0.1) actionview (= 8.0.2.1)
activejob (= 8.0.1) activejob (= 8.0.2.1)
activesupport (= 8.0.1) activesupport (= 8.0.2.1)
mail (>= 2.8.0) mail (>= 2.8.0)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
actionpack (8.0.1) actionpack (8.0.2.1)
actionview (= 8.0.1) actionview (= 8.0.2.1)
activesupport (= 8.0.1) activesupport (= 8.0.2.1)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
rack (>= 2.2.4) rack (>= 2.2.4)
rack-session (>= 1.0.1) rack-session (>= 1.0.1)
@@ -31,35 +31,35 @@ GEM
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
useragent (~> 0.16) useragent (~> 0.16)
actiontext (8.0.1) actiontext (8.0.2.1)
actionpack (= 8.0.1) actionpack (= 8.0.2.1)
activerecord (= 8.0.1) activerecord (= 8.0.2.1)
activestorage (= 8.0.1) activestorage (= 8.0.2.1)
activesupport (= 8.0.1) activesupport (= 8.0.2.1)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (8.0.1) actionview (8.0.2.1)
activesupport (= 8.0.1) activesupport (= 8.0.2.1)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.11) erubi (~> 1.11)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
activejob (8.0.1) activejob (8.0.2.1)
activesupport (= 8.0.1) activesupport (= 8.0.2.1)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (8.0.1) activemodel (8.0.2.1)
activesupport (= 8.0.1) activesupport (= 8.0.2.1)
activerecord (8.0.1) activerecord (8.0.2.1)
activemodel (= 8.0.1) activemodel (= 8.0.2.1)
activesupport (= 8.0.1) activesupport (= 8.0.2.1)
timeout (>= 0.4.0) timeout (>= 0.4.0)
activestorage (8.0.1) activestorage (8.0.2.1)
actionpack (= 8.0.1) actionpack (= 8.0.2.1)
activejob (= 8.0.1) activejob (= 8.0.2.1)
activerecord (= 8.0.1) activerecord (= 8.0.2.1)
activesupport (= 8.0.1) activesupport (= 8.0.2.1)
marcel (~> 1.0) marcel (~> 1.0)
activesupport (8.0.1) activesupport (8.0.2.1)
base64 base64
benchmark (>= 0.3) benchmark (>= 0.3)
bigdecimal bigdecimal
@@ -104,9 +104,9 @@ GEM
faraday_middleware (~> 1.0, >= 1.0.0.rc1) faraday_middleware (~> 1.0, >= 1.0.0.rc1)
net-http-persistent (~> 4.0) net-http-persistent (~> 4.0)
nokogiri (~> 1, >= 1.10.8) nokogiri (~> 1, >= 1.10.8)
base64 (0.2.0) base64 (0.3.0)
bcrypt (3.1.20) bcrypt (3.1.20)
benchmark (0.4.0) benchmark (0.4.1)
better_html (2.1.1) better_html (2.1.1)
actionview (>= 6.0) actionview (>= 6.0)
activesupport (>= 6.0) activesupport (>= 6.0)
@@ -114,7 +114,7 @@ GEM
erubi (~> 1.4) erubi (~> 1.4)
parser (>= 2.4) parser (>= 2.4)
smart_properties smart_properties
bigdecimal (3.1.8) bigdecimal (3.2.2)
bindex (0.8.1) bindex (0.8.1)
bootsnap (1.18.4) bootsnap (1.18.4)
msgpack (~> 1.2) msgpack (~> 1.2)
@@ -141,8 +141,8 @@ GEM
cldr-plurals-runtime-rb (1.1.0) cldr-plurals-runtime-rb (1.1.0)
cmdparse (3.0.7) cmdparse (3.0.7)
coderay (1.1.3) coderay (1.1.3)
concurrent-ruby (1.3.4) concurrent-ruby (1.3.5)
connection_pool (2.4.1) connection_pool (2.5.3)
crack (1.0.0) crack (1.0.0)
bigdecimal bigdecimal
rexml rexml
@@ -150,6 +150,8 @@ GEM
css_parser (1.21.0) css_parser (1.21.0)
addressable addressable
csv (3.3.2) csv (3.3.2)
csv-safe (3.3.1)
csv (~> 3.0)
cuprite (0.15.1) cuprite (0.15.1)
capybara (~> 3.0) capybara (~> 3.0)
ferrum (~> 0.15.0) ferrum (~> 0.15.0)
@@ -174,8 +176,9 @@ GEM
rake (>= 12.0.0, < 14.0.0) rake (>= 12.0.0, < 14.0.0)
docile (1.4.1) docile (1.4.1)
dotenv (3.1.7) dotenv (3.1.7)
drb (2.2.1) drb (2.2.3)
email_typo (0.2.3) email_typo (0.2.3)
erb (5.0.2)
erb_lint (0.7.0) erb_lint (0.7.0)
activesupport activesupport
better_html (>= 2.0.1) better_html (>= 2.0.1)
@@ -270,23 +273,25 @@ GEM
os (>= 0.9, < 2.0) os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a) signet (>= 0.16, < 2.a)
hashdiff (1.1.2) hashdiff (1.1.2)
hexapdf (1.0.3) hexapdf (1.4.0)
cmdparse (~> 3.0, >= 3.0.3) cmdparse (~> 3.0, >= 3.0.3)
geom2d (~> 0.4, >= 0.4.1) geom2d (~> 0.4, >= 0.4.1)
openssl (>= 2.2.1) openssl (>= 2.2.1)
strscan (>= 3.1.2)
htmlentities (4.3.4) htmlentities (4.3.4)
httpclient (2.8.3) httpclient (2.8.3)
i18n (1.14.6) i18n (1.14.7)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
image_processing (1.13.0) image_processing (1.13.0)
mini_magick (>= 4.9.5, < 5) mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3) ruby-vips (>= 2.0.17, < 3)
io-console (0.8.0) io-console (0.8.1)
irb (1.14.3) irb (1.15.2)
pp (>= 0.6.0)
rdoc (>= 4.0.0) rdoc (>= 4.0.0)
reline (>= 0.4.2) reline (>= 0.4.2)
jmespath (1.6.2) jmespath (1.6.2)
json (2.9.1) json (2.13.2)
jwt (2.9.3) jwt (2.9.3)
base64 base64
language_server-protocol (3.17.0.3) language_server-protocol (3.17.0.3)
@@ -300,13 +305,13 @@ GEM
letter_opener (~> 1.9) letter_opener (~> 1.9)
railties (>= 6.1) railties (>= 6.1)
rexml rexml
logger (1.6.4) logger (1.7.0)
lograge (0.14.0) lograge (0.14.0)
actionpack (>= 4) actionpack (>= 4)
activesupport (>= 4) activesupport (>= 4)
railties (>= 4) railties (>= 4)
request_store (~> 1.0) request_store (~> 1.0)
loofah (2.23.1) loofah (2.24.1)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
mail (2.8.1) mail (2.8.1)
@@ -320,7 +325,7 @@ GEM
mini_magick (4.13.2) mini_magick (4.13.2)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.9) mini_portile2 (2.8.9)
minitest (5.25.4) minitest (5.25.5)
msgpack (1.7.5) msgpack (1.7.5)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.4.1) multipart-post (2.4.1)
@@ -328,14 +333,14 @@ GEM
mysql2 (0.5.6) mysql2 (0.5.6)
net-http-persistent (4.0.5) net-http-persistent (4.0.5)
connection_pool (~> 2.2) connection_pool (~> 2.2)
net-imap (0.5.6) net-imap (0.5.9)
date date
net-protocol net-protocol
net-pop (0.1.2) net-pop (0.1.2)
net-protocol net-protocol
net-protocol (0.2.2) net-protocol (0.2.2)
timeout timeout
net-smtp (0.5.0) net-smtp (0.5.1)
net-protocol net-protocol
nio4r (2.7.4) nio4r (2.7.4)
nokogiri (1.18.9) nokogiri (1.18.9)
@@ -351,13 +356,13 @@ GEM
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.9-x86_64-linux-musl) nokogiri (1.18.9-x86_64-linux-musl)
racc (~> 1.4) racc (~> 1.4)
oj (3.16.8) oj (3.16.11)
bigdecimal (>= 3.0) bigdecimal (>= 3.0)
ostruct (>= 0.2) ostruct (>= 0.2)
openssl (3.3.0) openssl (3.3.0)
orm_adapter (0.5.0) orm_adapter (0.5.0)
os (1.1.4) os (1.1.4)
ostruct (0.6.1) ostruct (0.6.3)
package_json (0.1.0) package_json (0.1.0)
pagy (9.3.3) pagy (9.3.3)
parallel (1.26.3) parallel (1.26.3)
@@ -365,6 +370,8 @@ GEM
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
pg (1.5.9) pg (1.5.9)
pp (0.6.2)
prettyprint
premailer (1.27.0) premailer (1.27.0)
addressable addressable
css_parser (>= 1.19.0) css_parser (>= 1.19.0)
@@ -375,42 +382,44 @@ GEM
premailer (~> 1.7, >= 1.7.9) premailer (~> 1.7, >= 1.7.9)
pretender (0.5.0) pretender (0.5.0)
actionpack (>= 6.1) actionpack (>= 6.1)
prettyprint (0.2.0)
pry (0.15.0) pry (0.15.0)
coderay (~> 1.1) coderay (~> 1.1)
method_source (~> 1.0) method_source (~> 1.0)
pry-rails (0.3.11) pry-rails (0.3.11)
pry (>= 0.13.0) pry (>= 0.13.0)
psych (5.2.2) psych (5.2.6)
date date
stringio stringio
public_suffix (6.0.1) public_suffix (6.0.1)
puma (6.5.0) puma (6.5.0)
nio4r (~> 2.0) nio4r (~> 2.0)
racc (1.8.1) racc (1.8.1)
rack (3.1.16) rack (3.2.0)
rack-proxy (0.7.7) rack-proxy (0.7.7)
rack rack
rack-session (2.0.0) rack-session (2.1.1)
base64 (>= 0.1.0)
rack (>= 3.0.0) rack (>= 3.0.0)
rack-test (2.1.0) rack-test (2.2.0)
rack (>= 1.3) rack (>= 1.3)
rackup (2.2.1) rackup (2.2.1)
rack (>= 3) rack (>= 3)
rails (8.0.1) rails (8.0.2.1)
actioncable (= 8.0.1) actioncable (= 8.0.2.1)
actionmailbox (= 8.0.1) actionmailbox (= 8.0.2.1)
actionmailer (= 8.0.1) actionmailer (= 8.0.2.1)
actionpack (= 8.0.1) actionpack (= 8.0.2.1)
actiontext (= 8.0.1) actiontext (= 8.0.2.1)
actionview (= 8.0.1) actionview (= 8.0.2.1)
activejob (= 8.0.1) activejob (= 8.0.2.1)
activemodel (= 8.0.1) activemodel (= 8.0.2.1)
activerecord (= 8.0.1) activerecord (= 8.0.2.1)
activestorage (= 8.0.1) activestorage (= 8.0.2.1)
activesupport (= 8.0.1) activesupport (= 8.0.2.1)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 8.0.1) railties (= 8.0.2.1)
rails-dom-testing (2.2.0) rails-dom-testing (2.3.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
minitest minitest
nokogiri (>= 1.6) nokogiri (>= 1.6)
@@ -424,22 +433,23 @@ GEM
actionview (> 3.1) actionview (> 3.1)
activesupport (> 3.1) activesupport (> 3.1)
railties (> 3.1) railties (> 3.1)
railties (8.0.1) railties (8.0.2.1)
actionpack (= 8.0.1) actionpack (= 8.0.2.1)
activesupport (= 8.0.1) activesupport (= 8.0.2.1)
irb (~> 1.13) irb (~> 1.13)
rackup (>= 1.0.0) rackup (>= 1.0.0)
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0, >= 1.2.2) thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.2.1) rake (13.3.0)
rdoc (6.10.0) rdoc (6.14.2)
erb
psych (>= 4.0.0) psych (>= 4.0.0)
redis-client (0.23.0) redis-client (0.23.0)
connection_pool connection_pool
regexp_parser (2.9.3) regexp_parser (2.9.3)
reline (0.6.0) reline (0.6.2)
io-console (~> 0.5) io-console (~> 0.5)
representable (3.2.0) representable (3.2.0)
declarative (< 0.1.0) declarative (< 0.1.0)
@@ -538,9 +548,10 @@ GEM
sqlite3 (2.5.0-arm64-darwin) sqlite3 (2.5.0-arm64-darwin)
sqlite3 (2.5.0-x86_64-linux-gnu) sqlite3 (2.5.0-x86_64-linux-gnu)
sqlite3 (2.5.0-x86_64-linux-musl) sqlite3 (2.5.0-x86_64-linux-musl)
stringio (3.1.2) stringio (3.1.7)
strip_attributes (1.14.1) strip_attributes (1.14.1)
activemodel (>= 3.0, < 9.0) activemodel (>= 3.0, < 9.0)
strscan (3.1.5)
thor (1.4.0) thor (1.4.0)
timeout (0.4.3) timeout (0.4.3)
trailblazer-option (0.1.2) trailblazer-option (0.1.2)
@@ -574,12 +585,13 @@ GEM
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0) hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.9.1) webrick (1.9.1)
websocket-driver (0.7.6) websocket-driver (0.8.0)
base64
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
zeitwerk (2.7.1) zeitwerk (2.7.3)
PLATFORMS PLATFORMS
aarch64-linux aarch64-linux
@@ -602,6 +614,7 @@ DEPENDENCIES
cancancan cancancan
capybara capybara
csv csv
csv-safe
cuprite cuprite
debug debug
devise devise
@@ -15,6 +15,7 @@ class AccountConfigsController < ApplicationController
AccountConfig::ESIGNING_PREFERENCE_KEY, AccountConfig::ESIGNING_PREFERENCE_KEY,
AccountConfig::FORM_WITH_CONFETTI_KEY, AccountConfig::FORM_WITH_CONFETTI_KEY,
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY, AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY,
AccountConfig::FORCE_SSO_AUTH_KEY, AccountConfig::FORCE_SSO_AUTH_KEY,
AccountConfig::FLATTEN_RESULT_PDF_KEY, AccountConfig::FLATTEN_RESULT_PDF_KEY,
AccountConfig::ENFORCE_SIGNING_ORDER_KEY, AccountConfig::ENFORCE_SIGNING_ORDER_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]) 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')) @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 end
@encrypted_config.save! @encrypted_config.save!
@@ -39,7 +39,7 @@ class AccountsController < ApplicationController
redirect_to settings_account_path, notice: I18n.t('account_information_has_been_updated') redirect_to settings_account_path, notice: I18n.t('account_information_has_been_updated')
end end
rescue ActiveRecord::RecordInvalid rescue ActiveRecord::RecordInvalid
render :show, status: :unprocessable_entity render :show, status: :unprocessable_content
end end
def destroy def destroy
@@ -13,7 +13,7 @@ module Api
def show def show
blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid]) 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) Rollbar.error('Blob not found') if defined?(Rollbar)
return head :not_found return head :not_found
@@ -24,8 +24,9 @@ module Api
attachment = blob.attachments.take attachment = blob.attachments.take
@record = attachment.record @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? if request.headers['Range'].present?
send_blob_byte_range_data blob, request.headers['Range'] send_blob_byte_range_data blob, request.headers['Range']
@@ -41,16 +42,22 @@ module Api
private private
def authorization_check!(attachment) def authorization_check!(attachment, record, exp)
is_authorized = attachment.name.in?(%w[logo preview_images]) || return if attachment.name == 'logo'
(current_user && attachment.record.account.id == current_user.account_id) || return if exp.to_i >= Time.current.to_i
(current_user && !Docuseal.multitenant? && current_user.role == 'superadmin') || return if current_user && current_ability.can?(:read, record)
!attachment.record.account.account_configs
.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value
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 raise CanCan::AccessDenied
end end
+2 -2
View File
@@ -16,7 +16,7 @@ module Api
check_authorization check_authorization
rescue_from Params::BaseValidator::InvalidParameterError do |e| rescue_from Params::BaseValidator::InvalidParameterError do |e|
render json: { error: e.message }, status: :unprocessable_entity render json: { error: e.message }, status: :unprocessable_content
end end
rescue_from RateLimit::LimitApproached do |e| rescue_from RateLimit::LimitApproached do |e|
@@ -33,7 +33,7 @@ module Api
rescue_from JSON::ParserError do |e| rescue_from JSON::ParserError do |e|
Rollbar.warning(e) if defined?(Rollbar) 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
end end
+14 -3
View File
@@ -10,10 +10,21 @@ module Api
def create def create
submitter = Submitter.find_by!(slug: params[:submitter_slug]) submitter = Submitter.find_by!(slug: params[:submitter_slug])
if params[:type].in?(%w[initials signature]) && ImageUtils.blank?(Vips::Image.new_from_file(params[:file].path)) if params[:type].in?(%w[initials signature])
Rollbar.error("Empty signature: #{submitter.id}") if defined?(Rollbar) image = Vips::Image.new_from_file(params[:file].path)
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_entity if ImageUtils.blank?(image)
Rollbar.error("Empty signature: #{submitter.id}") if defined?(Rollbar)
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_content
end
end end
attachment = Submitters.create_attachment!(submitter, params) attachment = Submitters.create_attachment!(submitter, params)
@@ -18,12 +18,14 @@ module Api
field: :completed_at field: :completed_at
) )
expires_at = Accounts.link_expires_at(current_account)
render json: { render json: {
data: submitters.map do |s| data: submitters.map do |s|
{ {
event_type: 'form.completed', event_type: 'form.completed',
timestamp: s.completed_at, timestamp: s.completed_at,
data: Submitters::SerializeForWebhook.call(s) data: Submitters::SerializeForWebhook.call(s, expires_at:)
} }
end, end,
pagination: { pagination: {
@@ -34,10 +34,12 @@ module Api
associations: [:blob] associations: [:blob]
).call ).call
expires_at = Accounts.link_expires_at(current_account)
render json: { render json: {
id: @submission.id, id: @submission.id,
documents: documents.map do |attachment| 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
} }
end end
@@ -14,16 +14,19 @@ module Api
:created_by_user, :submission_events, :created_by_user, :submission_events,
template: :folder, template: :folder,
submitters: { documents_attachments: :blob, attachments_attachments: :blob }, submitters: { documents_attachments: :blob, attachments_attachments: :blob },
audit_trail_attachment: :blob audit_trail_attachment: :blob,
combined_document_attachment: :blob
), ),
field: :completed_at) field: :completed_at)
expires_at = Accounts.link_expires_at(current_account)
render json: { render json: {
data: submissions.map do |s| data: submissions.map do |s|
{ {
event_type: 'submission.completed', event_type: 'submission.completed',
timestamp: s.completed_at, timestamp: s.completed_at,
data: Submissions::SerializeForApi.call(s, s.submitters) data: Submissions::SerializeForApi.call(s, s.submitters, expires_at:)
} }
end, end,
pagination: { pagination: {
@@ -18,10 +18,12 @@ module Api
combined_document_attachment: :blob, combined_document_attachment: :blob,
audit_trail_attachment: :blob)) audit_trail_attachment: :blob))
expires_at = Accounts.link_expires_at(current_account)
render json: { render json: {
data: submissions.map do |s| data: submissions.map do |s|
Submissions::SerializeForApi.call(s, s.submitters, params, 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, end,
pagination: { pagination: {
count: submissions.size, count: submissions.size,
@@ -50,12 +52,12 @@ module Api
def create def create
Params::SubmissionCreateValidator.call(params) 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? if @template.fields.blank?
Rollbar.warning("Template does not contain fields: #{@template.id}") if defined?(Rollbar) 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 end
params[:send_email] = true unless params.key?(:send_email) params[:send_email] = true unless params.key?(:send_email)
@@ -82,7 +84,7 @@ module Api
DownloadUtils::UnableToDownload => e DownloadUtils::UnableToDownload => e
Rollbar.warning(e) if defined?(Rollbar) Rollbar.warning(e) if defined?(Rollbar)
render json: { error: e.message }, status: :unprocessable_entity render json: { error: e.message }, status: :unprocessable_content
end end
def destroy def destroy
@@ -185,11 +187,11 @@ module Api
message: %i[subject body], message: %i[subject body],
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role, submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last, :completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
:require_phone_2fa, :require_phone_2fa, :order,
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body], { metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
fields: [:name, :uuid, :default_value, :value, :title, :description, fields: [:name, :uuid, :default_value, :value, :title, :description,
:readonly, :required, :validation_pattern, :invalid_message, :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) documents_attachments: :blob, attachments_attachments: :blob)
) )
expires_at = Accounts.link_expires_at(current_account)
render json: { render json: {
data: submitters.map do |s| 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, end,
pagination: { pagination: {
count: submitters.size, count: submitters.size,
@@ -34,7 +36,7 @@ module Api
def update def update
if @submitter.completed_at? 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 end
submission = @submitter.submission submission = @submitter.submission
@@ -74,7 +76,7 @@ module Api
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
Rollbar.warning(e) if defined?(Rollbar) Rollbar.warning(e) if defined?(Rollbar)
render json: { error: e.message }, status: :unprocessable_entity render json: { error: e.message }, status: :unprocessable_content
end end
def submitter_params def submitter_params
@@ -26,13 +26,15 @@ module Api
original_template: @template, original_template: @template,
documents: params[:documents]) documents: params[:documents])
Templates.maybe_assign_access(cloned_template)
cloned_template.save! cloned_template.save!
WebhookUrls.enqueue_events(cloned_template, 'template.created') WebhookUrls.enqueue_events(cloned_template, 'template.created')
SearchEntries.enqueue_reindex(cloned_template) 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 end
end end
+9 -7
View File
@@ -24,13 +24,14 @@ module Api
name: :preview_images) name: :preview_images)
.preload(:blob) .preload(:blob)
expires_at = Accounts.link_expires_at(current_account)
render json: { render json: {
data: templates.map do |t| data: templates.map do |t|
Templates::SerializeForApi.call( Templates::SerializeForApi.call(t,
t, schema_documents: schema_documents.select { |e| e.record_id == t.id },
schema_documents.select { |e| e.record_id == t.id }, preview_image_attachments:,
preview_image_attachments expires_at:)
)
end, end,
pagination: { pagination: {
count: templates.size, count: templates.size,
@@ -106,14 +107,15 @@ module Api
:external_id, :external_id,
:shared_link, :shared_link,
{ {
submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email]], submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email order]],
fields: [[:uuid, :submitter_uuid, :name, :type, fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value, :required, :readonly, :default_value,
:title, :description, :prefillable, :title, :description, :prefillable,
{ preferences: {}, { preferences: {},
default_value: [],
conditions: [%i[field_uuid value action operation]], conditions: [%i[field_uuid value action operation]],
options: [%i[value uuid]], 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]] }]] 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 def merge
files = params[:files] || [] files = params[:files] || []
return render json: { error: 'Files are required' }, status: :unprocessable_entity if files.blank? 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_entity if files.size < 2 return render json: { error: 'At least 2 files are required' }, status: :unprocessable_content if files.size < 2
render json: { render json: {
data: Base64.encode64(PdfUtils.merge(files.map { |base64| StringIO.new(Base64.decode64(base64)) }).string) data: Base64.encode64(PdfUtils.merge(files.map { |base64| StringIO.new(Base64.decode64(base64)) }).string)
@@ -35,7 +35,7 @@ module Api
end end
} }
rescue HexaPDF::MalformedPDFError rescue HexaPDF::MalformedPDFError
render json: { error: 'Malformed PDF' }, status: :unprocessable_entity render json: { error: 'Malformed PDF' }, status: :unprocessable_content
end end
end end
end end
@@ -15,12 +15,12 @@ class EmailSmtpSettingsController < ApplicationController
redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved') redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved')
else else
render :index, status: :unprocessable_entity render :index, status: :unprocessable_content
end end
rescue StandardError => e rescue StandardError => e
flash[:alert] = e.message flash[:alert] = e.message
render :index, status: :unprocessable_entity render :index, status: :unprocessable_content
end end
private private
+2 -2
View File
@@ -53,7 +53,7 @@ class EsignSettingsController < ApplicationController
@cert_record.errors.add(:name, I18n.t('already_exists')) @cert_record.errors.add(:name, I18n.t('already_exists'))
return render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), return render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'),
status: :unprocessable_entity status: :unprocessable_content
end end
save_new_cert!(@encrypted_config, @cert_record) save_new_cert!(@encrypted_config, @cert_record)
@@ -64,7 +64,7 @@ class EsignSettingsController < ApplicationController
@cert_record.errors.add(:password, e.message) @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 end
def update def update
+2 -2
View File
@@ -24,7 +24,7 @@ class MfaSetupController < ApplicationController
@error_message = I18n.t('code_is_invalid') @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
end end
@@ -36,7 +36,7 @@ class MfaSetupController < ApplicationController
else else
@error_message = I18n.t('code_is_invalid') @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
end end
+10
View File
@@ -1,6 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
class PasswordsController < Devise::PasswordsController 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 class Current < ActiveSupport::CurrentAttributes
attribute :user attribute :user
end end
@@ -16,4 +20,10 @@ class PasswordsController < Devise::PasswordsController
Current.user = resource Current.user = resource
end end
end end
private
def after_resetting_password_path_for(_)
new_session_path(resource_name)
end
end end
+4 -4
View File
@@ -11,16 +11,16 @@ class ProfileController < ApplicationController
if current_user.update(contact_params) if current_user.update(contact_params)
redirect_to settings_profile_index_path, notice: I18n.t('contact_information_has_been_update') redirect_to settings_profile_index_path, notice: I18n.t('contact_information_has_been_update')
else else
render :index, status: :unprocessable_entity render :index, status: :unprocessable_content
end end
end end
def update_password def update_password
if current_user.update(password_params) if current_user.update_with_password(password_params)
bypass_sign_in(current_user) bypass_sign_in(current_user)
redirect_to settings_profile_index_path, notice: I18n.t('password_has_been_changed') redirect_to settings_profile_index_path, notice: I18n.t('password_has_been_changed')
else else
render :index, status: :unprocessable_entity render :index, status: :unprocessable_content
end end
end end
@@ -31,6 +31,6 @@ class ProfileController < ApplicationController
end end
def password_params def password_params
params.require(:user).permit(:password, :password_confirmation) params.require(:user).permit(:password, :password_confirmation, :current_password)
end end
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 end
if User.exists?(email:, otp_required_for_login: true) && sign_in_params[:otp_attempt].blank? 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 end
super 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]) 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')) @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 end
return render :index, status: :unprocessable_entity unless @account.valid? return render :index, status: :unprocessable_content unless @account.valid?
if @user.save if @user.save
encrypted_configs = [ encrypted_configs = [
@@ -42,7 +42,7 @@ class SetupController < ApplicationController
redirect_to newsletter_path redirect_to newsletter_path
else else
render :index, status: :unprocessable_entity render :index, status: :unprocessable_content
end end
end end
+4 -4
View File
@@ -42,7 +42,7 @@ class StartFormController < ApplicationController
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record? if filter_undefined_submitters(@template).size > 1 && @submitter.new_record?
@error_message = multiple_submitters_error_message @error_message = multiple_submitters_error_message
return render :show, status: :unprocessable_entity return render :show, status: :unprocessable_content
end end
if (is_new_record = @submitter.new_record?) if (is_new_record = @submitter.new_record?)
@@ -60,7 +60,7 @@ class StartFormController < ApplicationController
redirect_to submit_form_path(@submitter.slug) redirect_to submit_form_path(@submitter.slug)
else else
render :show, status: :unprocessable_entity render :show, status: :unprocessable_content
end end
end end
end end
@@ -206,7 +206,7 @@ class StartFormController < ApplicationController
end end
def handle_require_2fa(submitter, is_new_record:) 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) 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) redirect_to submit_form_path(submitter.slug)
else else
render :show, status: :unprocessable_entity render :show, status: :unprocessable_content
end end
else else
Submitters.send_shared_link_email_verification_code(submitter, request:) 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') redirect_to settings_storage_index_path, notice: I18n.t('changes_have_been_saved')
else else
render :index, status: :unprocessable_entity render :index, status: :unprocessable_content
end end
end end
+1 -1
View File
@@ -66,7 +66,7 @@ class SubmissionsController < ApplicationController
rescue Submissions::CreateFromSubmitters::BaseError => e rescue Submissions::CreateFromSubmitters::BaseError => e
render turbo_stream: turbo_stream.replace(:submitters_error, partial: 'submissions/error', render turbo_stream: turbo_stream.replace(:submitters_error, partial: 'submissions/error',
locals: { error: e.message }), locals: { error: e.message }),
status: :unprocessable_entity status: :unprocessable_content
end end
def destroy def destroy
@@ -10,11 +10,16 @@ class SubmissionsExportController < ApplicationController
attachments_attachments: :blob }) attachments_attachments: :blob })
.order(id: :asc) .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' 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" filename: "#{@template.name}.csv"
elsif params[:format] == 'xlsx' 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" filename: "#{@template.name}.xlsx"
end end
end end
+6 -6
View File
@@ -48,20 +48,20 @@ class SubmitFormController < ApplicationController
def update def update
if @submitter.completed_at? if @submitter.completed_at?
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_entity return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_content
end end
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at? 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 end
if @submitter.submission.expired? if @submitter.submission.expired?
return render json: { error: I18n.t('form_has_been_expired') }, status: :unprocessable_entity return render json: { error: I18n.t('form_has_been_expired') }, status: :unprocessable_content
end end
if @submitter.declined_at? if @submitter.declined_at?
return render json: { error: I18n.t('form_has_been_declined') }, return render json: { error: I18n.t('form_has_been_declined') },
status: :unprocessable_entity status: :unprocessable_content
end end
Submitters::SubmitValues.call(@submitter, params, request) Submitters::SubmitValues.call(@submitter, params, request)
@@ -70,9 +70,9 @@ class SubmitFormController < ApplicationController
rescue Submitters::SubmitValues::RequiredFieldError => e rescue Submitters::SubmitValues::RequiredFieldError => e
Rollbar.warning("Required field #{@submitter.id}: #{e.message}") if defined?(Rollbar) Rollbar.warning("Required field #{@submitter.id}: #{e.message}") if defined?(Rollbar)
render json: { field_uuid: e.message }, status: :unprocessable_entity render json: { field_uuid: e.message }, status: :unprocessable_content
rescue Submitters::SubmitValues::ValidationError => e rescue Submitters::SubmitValues::ValidationError => e
render json: { error: e.message }, status: :unprocessable_entity render json: { error: e.message }, status: :unprocessable_content
end end
def completed; 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 redirect_to submitter_download_index_path(@submitter.slug) if @submitter.completed_at?
return head :unprocessable_entity if @submitter.declined_at? || return head :unprocessable_content if @submitter.declined_at? ||
@submitter.submission.archived_at? || @submitter.submission.archived_at? ||
@submitter.submission.expired? || @submitter.submission.expired? ||
@submitter.submission.template&.archived_at? || @submitter.submission.template&.archived_at? ||
AccountConfig.exists?(account_id: @submitter.account_id, AccountConfig.exists?(account_id: @submitter.account_id,
key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY, key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
value: false) value: false)
last_completed_submitter = @submitter.submission.submitters last_completed_submitter = @submitter.submission.submitters
.where.not(id: @submitter.id) .where.not(id: @submitter.id)
@@ -7,7 +7,7 @@ class SubmitFormInviteController < ApplicationController
def create def create
submitter = Submitter.find_by!(slug: params[:submit_form_slug]) 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') invite_submitters = filter_invite_submitters(submitter, 'invite_by_uuid')
optional_invite_submitters = filter_invite_submitters(submitter, 'optional_invite_by_uuid') optional_invite_submitters = filter_invite_submitters(submitter, 'optional_invite_by_uuid')
@@ -34,7 +34,7 @@ class SubmitFormInviteController < ApplicationController
head :ok head :ok
else else
head :unprocessable_entity head :unprocessable_content
end end
end end
@@ -5,7 +5,7 @@ class TemplateDocumentsController < ApplicationController
def create def create
if params[:blobs].blank? && params[:files].blank? 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 end
old_fields_hash = @template.fields.hash old_fields_hash = @template.fields.hash
@@ -28,6 +28,6 @@ class TemplateDocumentsController < ApplicationController
) )
} }
rescue Templates::CreateAttachments::PdfEncrypted 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
end end
@@ -4,7 +4,7 @@ class TemplatesCloneAndReplaceController < ApplicationController
load_and_authorize_resource :template load_and_authorize_resource :template
def create def create
return head :unprocessable_entity if params[:files].blank? return head :unprocessable_content if params[:files].blank?
ActiveRecord::Associations::Preloader.new( ActiveRecord::Associations::Preloader.new(
records: [@template], records: [@template],
@@ -17,6 +17,8 @@ class TemplatesCloneAndReplaceController < ApplicationController
documents = Templates::ReplaceAttachments.call(cloned_template, params, extract_fields: true) documents = Templates::ReplaceAttachments.call(cloned_template, params, extract_fields: true)
Templates.maybe_assign_access(cloned_template)
cloned_template.save! cloned_template.save!
Templates::CloneAttachments.call(template: cloned_template, original_template: @template, Templates::CloneAttachments.call(template: cloned_template, original_template: @template,
@@ -31,7 +33,7 @@ class TemplatesCloneAndReplaceController < ApplicationController
rescue Templates::CreateAttachments::PdfEncrypted rescue Templates::CreateAttachments::PdfEncrypted
respond_to do |f| respond_to do |f|
f.html { render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password) } 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 end
end end
+6 -3
View File
@@ -69,6 +69,8 @@ class TemplatesController < ApplicationController
@template.account = current_account @template.account = current_account
end end
Templates.maybe_assign_access(@template)
if @template.save if @template.save
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
@@ -78,7 +80,7 @@ class TemplatesController < ApplicationController
maybe_redirect_to_template(@template) maybe_redirect_to_template(@template)
else 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
end end
@@ -117,14 +119,15 @@ class TemplatesController < ApplicationController
params.require(:template).permit( params.require(:template).permit(
:name, :name,
{ schema: [[:attachment_uuid, :name, { conditions: [%i[field_uuid value action operation]] }]], { schema: [[:attachment_uuid, :name, { conditions: [%i[field_uuid value action operation]] }]],
submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid optional_invite_by_uuid email]], submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid optional_invite_by_uuid email order]],
fields: [[:uuid, :submitter_uuid, :name, :type, fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value, :required, :readonly, :default_value,
:title, :description, :prefillable, :title, :description, :prefillable,
{ preferences: {}, { preferences: {},
default_value: [],
conditions: [%i[field_uuid value action operation]], conditions: [%i[field_uuid value action operation]],
options: [%i[value uuid]], 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]] }]] } areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
) )
end end
@@ -3,6 +3,8 @@
class TemplatesDebugController < ApplicationController class TemplatesDebugController < ApplicationController
load_and_authorize_resource :template load_and_authorize_resource :template
DEBUG_FILE = ''
def show def show
attachment = @template.documents.first attachment = @template.documents.first
@@ -16,6 +18,8 @@ class TemplatesDebugController < ApplicationController
@template.update!(fields: Templates::ProcessDocument.normalize_attachment_fields(@template, [attachment])) @template.update!(fields: Templates::ProcessDocument.normalize_attachment_fields(@template, [attachment]))
debug_file if DEBUG_FILE.present?
ActiveRecord::Associations::Preloader.new( ActiveRecord::Associations::Preloader.new(
records: [@template], records: [@template],
associations: [schema_documents: { preview_images_attachments: :blob }] associations: [schema_documents: { preview_images_attachments: :blob }]
@@ -31,4 +35,27 @@ class TemplatesDebugController < ApplicationController
render 'templates/edit', layout: 'plain' render 'templates/edit', layout: 'plain'
end 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 end
@@ -9,6 +9,10 @@ class TemplatesRecipientsController < ApplicationController
@template.submitters = @template.submitters =
submitters_params.map { |s| s.reject { |_, v| v.is_a?(String) && v.blank? } } submitters_params.map { |s| s.reject { |_, v| v.is_a?(String) && v.blank? } }
if @template.submitters.each_with_index.all? { |s, index| s['order'] == index }
@template.submitters.each { |s| s.delete('order') }
end
@template.save! @template.save!
render json: { submitters: @template.submitters } render json: { submitters: @template.submitters }
@@ -18,7 +22,7 @@ class TemplatesRecipientsController < ApplicationController
def submitters_params def submitters_params
permit_params = { submitters: [%i[name uuid is_requester optional_invite_by_uuid permit_params = { submitters: [%i[name uuid is_requester optional_invite_by_uuid
invite_by_uuid linked_to_uuid email option]] } invite_by_uuid linked_to_uuid email option order]] }
params.require(:template).permit(permit_params).fetch(:submitters, {}).values.filter_map do |s| params.require(:template).permit(permit_params).fetch(:submitters, {}).values.filter_map do |s|
next if s[:uuid].blank? next if s[:uuid].blank?
@@ -29,6 +33,7 @@ class TemplatesRecipientsController < ApplicationController
s.delete(:is_requester) s.delete(:is_requester)
end end
s[:order] = s[:order].to_i if s[:order].present?
s.delete(:invite_by_uuid) if s[:invite_by_uuid].blank? s.delete(:invite_by_uuid) if s[:invite_by_uuid].blank?
s.delete(:optional_invite_by_uuid) if s[:optional_invite_by_uuid].blank? s.delete(:optional_invite_by_uuid) if s[:optional_invite_by_uuid].blank?
@@ -46,6 +46,8 @@ class TemplatesUploadsController < ApplicationController
template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name]) template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
template.name = File.basename((url_params || params)[:files].first.original_filename, '.*') template.name = File.basename((url_params || params)[:files].first.original_filename, '.*')
Templates.maybe_assign_access(template)
template.save! template.save!
template template
+6 -5
View File
@@ -27,9 +27,10 @@ class UsersController < ApplicationController
if User.accessible_by(current_ability).exists?(email: @user.email) if User.accessible_by(current_ability).exists?(email: @user.email)
@user.errors.add(:email, I18n.t('already_exists')) @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 end
@user.password = SecureRandom.hex if @user.password.blank?
@user.role = User::ADMIN_ROLE unless role_valid?(@user.role) @user.role = User::ADMIN_ROLE unless role_valid?(@user.role)
if @user.save if @user.save
@@ -37,7 +38,7 @@ class UsersController < ApplicationController
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_invited') redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_invited')
else 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
end end
@@ -54,10 +55,10 @@ class UsersController < ApplicationController
@user.account = account @user.account = account
end 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') redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_updated')
else else
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_content
end end
end end
@@ -83,7 +84,7 @@ class UsersController < ApplicationController
def user_params def user_params
if params.key?(:user) 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)) 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
+2
View File
@@ -38,6 +38,7 @@ import DashboardDropzone from './elements/dashboard_dropzone'
import RequiredCheckboxGroup from './elements/required_checkbox_group' import RequiredCheckboxGroup from './elements/required_checkbox_group'
import PageContainer from './elements/page_container' import PageContainer from './elements/page_container'
import EmailEditor from './elements/email_editor' import EmailEditor from './elements/email_editor'
import MountOnClick from './elements/mount_on_click'
import * as TurboInstantClick from './lib/turbo_instant_click' import * as TurboInstantClick from './lib/turbo_instant_click'
@@ -111,6 +112,7 @@ safeRegisterElement('check-on-click', CheckOnClick)
safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup) safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup)
safeRegisterElement('page-container', PageContainer) safeRegisterElement('page-container', PageContainer)
safeRegisterElement('email-editor', EmailEditor) safeRegisterElement('email-editor', EmailEditor)
safeRegisterElement('mount-on-click', MountOnClick)
safeRegisterElement('template-builder', class extends HTMLElement { safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () { connectedCallback () {
+11
View File
@@ -0,0 +1,11 @@
export default class extends HTMLElement {
connectedCallback () {
this.addEventListener('click', () => {
document.body.append(this.template.content)
})
}
get template () {
return document.getElementById(this.dataset.templateId)
}
}
+19 -6
View File
@@ -29,8 +29,15 @@
{{ optionValue(option) }} {{ optionValue(option) }}
</template> </template>
<template v-else> <template v-else>
{{ field.title || field.name || fieldNames[field.type] }} <MarkdownContent
<template v-if="field.type === 'checkbox' && !field.name"> 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 }} {{ fieldIndex + 1 }}
</template> </template>
<template v-else-if="!field.required && field.type !== 'checkbox'"> <template v-else-if="!field.required && field.type !== 'checkbox'">
@@ -222,12 +229,14 @@
</template> </template>
<script> <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' import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconId } from '@tabler/icons-vue'
export default { export default {
name: 'FieldArea', name: 'FieldArea',
components: { components: {
IconPaperclip, IconPaperclip,
MarkdownContent,
IconCheck IconCheck
}, },
inject: ['t'], inject: ['t'],
@@ -422,10 +431,14 @@ export default {
}, },
formattedDate () { formattedDate () {
if (this.field.type === 'date' && this.modelValue) { if (this.field.type === 'date' && this.modelValue) {
return this.formatDate( try {
this.modelValue === '{{date}}' ? new Date() : new Date(this.modelValue), return this.formatDate(
this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY') 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 { } else {
return '' return ''
} }
+3 -6
View File
@@ -25,7 +25,7 @@
<div class="space-y-3 mt-5"> <div class="space-y-3 mt-5">
<a <a
v-if="completedButton.url" v-if="completedButton.url"
:href="sanitizeHref(completedButton.url)" :href="sanitizeUrl(completedButton.url)"
rel="noopener noreferrer nofollow" rel="noopener noreferrer nofollow"
class="white-button flex items-center w-full completed-form-completed-button" class="white-button flex items-center w-full completed-form-completed-button"
> >
@@ -102,6 +102,7 @@
<script> <script>
import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop, IconLogin } from '@tabler/icons-vue' import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop, IconLogin } from '@tabler/icons-vue'
import MarkdownContent from './markdown_content' import MarkdownContent from './markdown_content'
import { sanitizeUrl } from '@braintree/sanitize-url'
export default { export default {
name: 'FormCompleted', name: 'FormCompleted',
@@ -198,6 +199,7 @@ export default {
}) })
}, },
methods: { methods: {
sanitizeUrl,
sendCopyToEmail () { sendCopyToEmail () {
this.isSendingCopy = true this.isSendingCopy = true
@@ -252,11 +254,6 @@ export default {
this.isDownloading = false this.isDownloading = false
}) })
}, },
sanitizeHref (href) {
if (href && href.trim().match(/^((?:https?:\/\/)|\/)/)) {
return href.replace(/javascript:/g, '')
}
},
downloadSafariIos (urls) { downloadSafariIos (urls) {
const fileRequests = urls.map((url) => { const fileRequests = urls.map((url) => {
return fetch(url).then(async (resp) => { return fetch(url).then(async (resp) => {
@@ -26,6 +26,7 @@
</template> </template>
</label> </label>
<button <button
v-if="withToday"
class="btn btn-outline btn-sm !normal-case font-normal set-current-date-button" class="btn btn-outline btn-sm !normal-case font-normal set-current-date-button"
@click.prevent="[setCurrentDate(), $emit('focus')]" @click.prevent="[setCurrentDate(), $emit('focus')]"
> >
@@ -46,6 +47,8 @@
:id="field.uuid" :id="field.uuid"
ref="input" ref="input"
v-model="value" v-model="value"
:min="validationMin"
:max="validationMax"
class="base-input !text-2xl text-center w-full" class="base-input !text-2xl text-center w-full"
:required="field.required" :required="field.required"
type="date" type="date"
@@ -89,6 +92,44 @@ export default {
}, },
emits: ['update:model-value', 'focus', 'submit'], emits: ['update:model-value', 'focus', 'submit'],
computed: { 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: { value: {
set (value) { set (value) {
this.$emit('update:model-value', value) this.$emit('update:model-value', value)
+7 -2
View File
@@ -572,6 +572,7 @@ import FormCompleted from './completed'
import { IconInnerShadowTop, IconArrowsDiagonal, IconWritingSign, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue' import { IconInnerShadowTop, IconArrowsDiagonal, IconWritingSign, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
import AppearsOn from './appears_on' import AppearsOn from './appears_on'
import i18n from './i18n' import i18n from './i18n'
import { sanitizeUrl } from '@braintree/sanitize-url'
const isEmpty = (obj) => { const isEmpty = (obj) => {
if (obj == null) return true if (obj == null) return true
@@ -1014,7 +1015,11 @@ export default {
const aArea = (fieldAreasIndex[aField.uuid] ||= [...(aField.areas || [])].sort(sortArea)[0]) const aArea = (fieldAreasIndex[aField.uuid] ||= [...(aField.areas || [])].sort(sortArea)[0])
const bArea = (fieldAreasIndex[bField.uuid] ||= [...(bField.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 +1481,7 @@ export default {
} }
if (this.completedRedirectUrl) { if (this.completedRedirectUrl) {
window.location.href = this.completedRedirectUrl window.location.href = sanitizeUrl(this.completedRedirectUrl)
} }
} }
} }
@@ -58,6 +58,13 @@ export default {
computed: { computed: {
isInlineSize () { isInlineSize () {
return CSS.supports('container-type: size') return CSS.supports('container-type: size')
},
fieldsUuidIndex () {
return this.fields.reduce((acc, field) => {
acc[field.uuid] = field
return acc
}, {})
} }
}, },
async mounted () { async mounted () {
@@ -96,8 +103,19 @@ export default {
findPageElementForArea (area) { findPageElementForArea (area) {
return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${area.attachment_uuid}-${area.page}`) 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) { 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 return this.readonlyValues[uuid] || this.values[uuid] || 0.0
}) })
@@ -1,35 +1,32 @@
<template> <template>
<span> <span>
<template <span v-if="textOnly">
v-for="(item, index) in items" {{ dom.body.textContent }}
:key="index" </span>
> <template v-else>
<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'"
>
<template <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>
</template> </template>
</span> </span>
@@ -37,8 +34,7 @@
<script> <script>
import snarkdown from 'snarkdown' import snarkdown from 'snarkdown'
import { sanitizeUrl } from '@braintree/sanitize-url'
const htmlSplitRegexp = /(<a.+?<\/a>|<i>.+?<\/i>|<b>.+?<\/b>|<em>.+?<\/em>|<strong>.+?<\/strong>|<br>)/
export default { export default {
name: 'MarkdownContent', name: 'MarkdownContent',
@@ -47,10 +43,23 @@ export default {
type: String, type: String,
required: false, required: false,
default: '' default: ''
},
nodes: {
type: [Array, Object],
require: false,
default: null
},
textOnly: {
type: Boolean,
required: false,
default: false
} }
}, },
computed: { computed: {
items () { safeTags () {
return ['UL', 'I', 'EM', 'B', 'STRONG', 'P']
},
dom () {
const linkParts = this.string.split(/(https?:\/\/[^\s)]+)/g) const linkParts = this.string.split(/(https?:\/\/[^\s)]+)/g)
const text = linkParts.map((part, index) => { const text = linkParts.map((part, index) => {
@@ -61,25 +70,11 @@ export default {
} }
}).join('') }).join('')
return snarkdown(text.replace(/\n/g, '<br>')).split(htmlSplitRegexp) return new DOMParser().parseFromString(snarkdown(text.replace(/\n/g, '<br>')), 'text/html')
} }
}, },
methods: { methods: {
sanitizeHref (href) { sanitizeUrl
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]
}
}
} }
} }
</script> </script>
@@ -39,8 +39,10 @@
:id="field.uuid" :id="field.uuid"
v-model="number" v-model="number"
type="number" type="number"
:step="field.validation?.step || 'any'"
:min="field.validation?.min"
:max="field.validation?.max"
class="base-input !text-2xl w-full" class="base-input !text-2xl w-full"
step="any"
:required="field.required" :required="field.required"
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`" :placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
:name="`values[${field.uuid}]`" :name="`values[${field.uuid}]`"
@@ -21,7 +21,7 @@
</label> </label>
<div class="space-x-2 flex flex-none"> <div class="space-x-2 flex flex-none">
<span <span
v-if="isTextSignature && format !== 'typed' && format !== 'upload'" v-if="isTextSignature && format !== 'typed_or_upload' && format !== 'typed' && format !== 'upload'"
class="tooltip" class="tooltip"
:data-tip="t('draw_signature')" :data-tip="t('draw_signature')"
> >
@@ -38,7 +38,7 @@
</a> </a>
</span> </span>
<span <span
v-else-if="withTypedSignature && format !== 'typed' && format !== 'drawn' && format !== 'upload'" v-else-if="withTypedSignature && format !== 'drawn_or_upload' && format !== 'typed_or_upload' && format !== 'typed' && format !== 'drawn' && format !== 'upload'"
class="tooltip ml-2" class="tooltip ml-2"
:class="{ 'hidden sm:inline': modelValue || computedPreviousValue }" :class="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
:data-tip="t('type_text')" :data-tip="t('type_text')"
@@ -85,8 +85,8 @@
{{ t(format === 'upload' ? 'reupload' : 'redraw') }} {{ t(format === 'upload' ? 'reupload' : 'redraw') }}
</a> </a>
<span <span
v-if="withQrButton && !modelValue && !computedPreviousValue && format !== 'typed' && format !== 'upload'" v-if="withQrButton && !modelValue && !computedPreviousValue && format !== 'typed_or_upload' && format !== 'typed' && format !== 'upload'"
class=" tooltip" class="tooltip before:translate-x-[-90%]"
:data-tip="t('drawn_signature_on_a_touchscreen_device')" :data-tip="t('drawn_signature_on_a_touchscreen_device')"
> >
<a <a
@@ -395,7 +395,7 @@ export default {
isShowQr: false, isShowQr: false,
isOtherReason: false, isOtherReason: false,
isUsePreviousValue: true, isUsePreviousValue: true,
isTextSignature: this.field.preferences?.format === 'typed', isTextSignature: this.field.preferences?.format === 'typed' || this.field.preferences?.format === 'typed_or_upload',
uploadImageInputKey: Math.random().toString() uploadImageInputKey: Math.random().toString()
} }
}, },
+110 -31
View File
@@ -72,11 +72,11 @@
@blur="onNameBlur" @blur="onNameBlur"
>{{ optionIndexText }} {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || defaultName }}</span> >{{ optionIndexText }} {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || defaultName }}</span>
<div <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" class="flex items-center ml-1.5"
> >
<input <input
v-if="!isValueInput" v-if="!isValueInput && !isSelectInput"
:id="`required-checkbox-${field.uuid}`" :id="`required-checkbox-${field.uuid}`"
v-model="field.required" v-model="field.required"
type="checkbox" type="checkbox"
@@ -84,14 +84,14 @@
@mousedown.prevent @mousedown.prevent
> >
<label <label
v-if="!isValueInput" v-if="!isValueInput && !isSelectInput"
:for="`required-checkbox-${field.uuid}`" :for="`required-checkbox-${field.uuid}`"
class="label text-xs" class="label text-xs"
@click.prevent="field.required = !field.required" @click.prevent="field.required = !field.required"
@mousedown.prevent @mousedown.prevent
>{{ t('required') }}</label> >{{ t('required') }}</label>
<input <input
v-if="isValueInput" v-if="isValueInput || isSelectInput"
:id="`readonly-checkbox-${field.uuid}`" :id="`readonly-checkbox-${field.uuid}`"
type="checkbox" type="checkbox"
class="checkbox checkbox-xs no-animation rounded" class="checkbox checkbox-xs no-animation rounded"
@@ -100,7 +100,7 @@
@mousedown.prevent @mousedown.prevent
> >
<label <label
v-if="isValueInput" v-if="isValueInput || isSelectInput"
:for="`readonly-checkbox-${field.uuid}`" :for="`readonly-checkbox-${field.uuid}`"
class="label text-xs" class="label text-xs"
@click.prevent="field.readonly = !(field.readonly ?? true)" @click.prevent="field.readonly = !(field.readonly ?? true)"
@@ -164,30 +164,39 @@
ref="touchValueTarget" ref="touchValueTarget"
class="flex h-full w-full field-area" class="flex h-full w-full field-area"
dir="auto" dir="auto"
:class="[isValueInput ? 'bg-opacity-50' : 'bg-opacity-80', field.type === 'heading' ? 'bg-gray-50' : bgColors[submitterIndex % bgColors.length], isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? fontClasses : 'justify-center items-center']" :class="[isValueInput ? 'cursor-text' : '', isValueInput || isCheckboxInput || isSelectInput ? 'bg-opacity-50' : 'bg-opacity-80', field.type === 'heading' ? 'bg-gray-50' : bgColors[submitterIndex % bgColors.length], isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? fontClasses : 'justify-center items-center']"
@click="focusValueInput" @click="focusValueInput"
> >
<span <span
v-if="field" v-if="field"
class="flex justify-center items-center space-x-1" class="flex justify-center items-center space-x-1"
:class="{ 'w-full': ['cells', 'checkbox'].includes(field.type), 'h-full': !isValueInput && !isDefaultValuePresent }" :class="{ 'w-full': isWFullType, 'h-full': !isValueInput && !isDefaultValuePresent }"
> >
<div <div
v-if="isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas && field.type !== 'checkbox')" v-if="isDefaultValuePresent || isValueInput || isSelectInput || (withFieldPlaceholder && field.areas && field.type !== 'checkbox')"
:class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type) }" :class="{ 'w-full h-full': isWFullType }"
:style="fontStyle" :style="fontStyle"
> >
<div <div
ref="textContainer" ref="textContainer"
class="flex items-center px-0.5" class="flex items-center px-0.5"
:style="{ color: field.preferences?.color }" :style="{ color: field.preferences?.color }"
:class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type) }" :class="{ 'w-full h-full': isWFullType }"
> >
<IconCheck <IconCheck
v-if="field.type == 'checkbox'" v-if="field.type == 'checkbox'"
class="aspect-square mx-auto" class="aspect-square mx-auto"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }" :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 <span
v-else-if="field.type === 'number' && !isValueInput && (field.default_value || field.default_value == 0)" v-else-if="field.type === 'number' && !isValueInput && (field.default_value || field.default_value == 0)"
class="whitespace-pre-wrap" class="whitespace-pre-wrap"
@@ -210,14 +219,39 @@
{{ char }} {{ char }}
</div> </div>
</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 <span
v-else v-else
ref="defaultValue" ref="defaultValue"
:contenteditable="isValueInput" :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 }" :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" @blur="onDefaultValueBlur"
@focus="selectedAreaRef.value = area"
@paste.prevent="onPaste" @paste.prevent="onPaste"
@keydown.enter="onDefaultValueEnter" @keydown.enter="onDefaultValueEnter"
>{{ field.default_value }}</span> >{{ field.default_value }}</span>
@@ -225,7 +259,7 @@
</div> </div>
<component <component
:is="fieldIcons[field.type]" :is="fieldIcons[field.type]"
v-else v-else-if="!isCheckboxInput"
width="100%" width="100%"
height="100%" height="100%"
class="max-h-10 opacity-50" class="max-h-10 opacity-50"
@@ -233,12 +267,12 @@
</span> </span>
</div> </div>
<div <div
v-if="!isValueInput" v-if="!isValueInput && !isSelectInput"
ref="touchTarget" ref="touchTarget"
class="absolute top-0 bottom-0 right-0 left-0" class="absolute top-0 bottom-0 right-0 left-0"
:class="isDragged ? 'cursor-grab' : 'cursor-pointer'" :class="isDragged ? 'cursor-grab' : 'cursor-pointer'"
@dblclick="maybeToggleDefaultValue" @dblclick="maybeToggleDefaultValue"
@click="maybeToggleCheckboxValue" @click="inputMode && maybeToggleCheckboxValue()"
/> />
<span <span
v-if="field?.type && editable" v-if="field?.type && editable"
@@ -253,6 +287,7 @@
<FormulaModal <FormulaModal
:field="field" :field="field"
:editable="editable && !defaultField" :editable="editable && !defaultField"
:default-field="defaultField"
:build-default-name="buildDefaultName" :build-default-name="buildDefaultName"
@close="isShowFormulaModal = false" @close="isShowFormulaModal = false"
/> />
@@ -264,6 +299,7 @@
<FontModal <FontModal
:field="field" :field="field"
:editable="editable && !defaultField" :editable="editable && !defaultField"
:default-field="defaultField"
:build-default-name="buildDefaultName" :build-default-name="buildDefaultName"
@close="isShowFontModal = false" @close="isShowFontModal = false"
/> />
@@ -275,6 +311,7 @@
<ConditionsModal <ConditionsModal
:item="field" :item="field"
:build-default-name="buildDefaultName" :build-default-name="buildDefaultName"
:default-field="defaultField"
@close="isShowConditionsModal = false" @close="isShowConditionsModal = false"
/> />
</Teleport> </Teleport>
@@ -285,6 +322,7 @@
<DescriptionModal <DescriptionModal
:field="field" :field="field"
:editable="editable && !defaultField" :editable="editable && !defaultField"
:default-field="defaultField"
:build-default-name="buildDefaultName" :build-default-name="buildDefaultName"
@close="isShowDescriptionModal = false" @close="isShowDescriptionModal = false"
/> />
@@ -398,6 +436,9 @@ export default {
fieldNames: FieldType.computed.fieldNames, fieldNames: FieldType.computed.fieldNames,
fieldLabels: FieldType.computed.fieldLabels, fieldLabels: FieldType.computed.fieldLabels,
fieldIcons: FieldType.computed.fieldIcons, fieldIcons: FieldType.computed.fieldIcons,
isWFullType () {
return ['cells', 'checkbox', 'radio', 'multiple', 'select'].includes(this.field.type)
},
fontStyle () { fontStyle () {
let fontSize = '' let fontSize = ''
@@ -417,6 +458,13 @@ export default {
return { fontSize, lineHeight: `calc(${fontSize} * ${this.lineHeight})` } return { fontSize, lineHeight: `calc(${fontSize} * ${this.lineHeight})` }
}, },
optionsUuidIndex () {
return this.field.options.reduce((acc, option) => {
acc[option.uuid] = option
return acc
}, {})
},
fontSizePx () { fontSizePx () {
return parseInt(this.field?.preferences?.font_size || 11) * this.fontScale return parseInt(this.field?.preferences?.font_size || 11) * this.fontScale
}, },
@@ -427,14 +475,17 @@ export default {
return 1040 / 612.0 return 1040 / 612.0
}, },
isDefaultValuePresent () { isDefaultValuePresent () {
if (this.field?.type === 'radio' && this.field?.areas?.length > 1) { return this.field?.default_value || this.field?.default_value === 0
return false },
} else { isSelectInput () {
return this.field?.default_value || this.field?.default_value === 0 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 () { 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 () { modalContainerEl () {
return this.$el.getRootNode().querySelector('#docuseal_modal_container') return this.$el.getRootNode().querySelector('#docuseal_modal_container')
@@ -549,6 +600,11 @@ export default {
closeDropdown () { closeDropdown () {
this.$el.getRootNode().activeElement.blur() 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 () { maybeToggleDefaultValue () {
if (!this.editable) { if (!this.editable) {
return return
@@ -558,22 +614,41 @@ export default {
this.isContenteditable = true this.isContenteditable = true
this.focusValueInput() 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') { } else if (this.field.type === 'date') {
this.field.readonly = !this.field.readonly this.field.readonly = !this.field.readonly
this.field.default_value === '{{date}}' ? delete this.field.default_value : this.field.default_value = '{{date}}' this.field.default_value === '{{date}}' ? delete this.field.default_value : this.field.default_value = '{{date}}'
this.save() this.save()
} else {
this.maybeToggleCheckboxValue()
} }
}, },
maybeToggleCheckboxValue () { maybeToggleCheckboxValue () {
if (this.inputMode && this.field.type === 'checkbox') { 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.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() this.save()
} }
@@ -749,7 +824,7 @@ export default {
} }
}, },
drag (e) { drag (e) {
if (e.target.id === 'mask') { if (e.target.id === 'mask' && this.editable) {
this.isDragged = true this.isDragged = true
this.area.x = (e.offsetX - this.dragFrom.x) / e.target.clientWidth this.area.x = (e.offsetX - this.dragFrom.x) / e.target.clientWidth
@@ -765,7 +840,9 @@ export default {
e.preventDefault() e.preventDefault()
this.isDragged = true if (this.editable) {
this.isDragged = true
}
const rect = e.target.getBoundingClientRect() const rect = e.target.getBoundingClientRect()
@@ -818,7 +895,9 @@ export default {
e.preventDefault() e.preventDefault()
this.isDragged = true if (this.editable) {
this.isDragged = true
}
const rect = e.target.getBoundingClientRect() const rect = e.target.getBoundingClientRect()
+36 -16
View File
@@ -660,7 +660,7 @@ export default {
acceptFileTypes: { acceptFileTypes: {
type: String, type: String,
required: false, required: false,
default: 'image/*, application/pdf' default: 'image/*, application/pdf, application/zip'
}, },
baseUrl: { baseUrl: {
type: String, type: String,
@@ -1065,7 +1065,7 @@ export default {
} }
if (['select', 'multiple', 'radio'].includes(type)) { if (['select', 'multiple', 'radio'].includes(type)) {
field.options = [{ value: '', uuid: v4() }] field.options = [{ value: '', uuid: v4() }, { value: '', uuid: v4() }]
} }
if (type === 'stamp') { if (type === 'stamp') {
@@ -1109,7 +1109,7 @@ export default {
} }
if (['select', 'multiple', 'radio'].includes(type)) { if (['select', 'multiple', 'radio'].includes(type)) {
field.options = [{ value: '', uuid: v4() }] field.options = [{ value: '', uuid: v4() }, { value: '', uuid: v4() }]
} }
if (type === 'stamp') { if (type === 'stamp') {
@@ -1265,24 +1265,31 @@ export default {
if (!field.areas.length) { if (!field.areas.length) {
this.template.fields.splice(this.template.fields.indexOf(field), 1) this.template.fields.splice(this.template.fields.indexOf(field), 1)
this.template.fields.forEach((f) => { this.removeFieldConditions(field)
(f.conditions || []).forEach((c) => { }
this.save()
},
removeFieldConditions (field) {
this.template.fields.forEach((f) => {
if (f.conditions) {
f.conditions.forEach((c) => {
if (c.field_uuid === field.uuid) { if (c.field_uuid === field.uuid) {
f.conditions.splice(f.conditions.indexOf(c), 1) f.conditions.splice(f.conditions.indexOf(c), 1)
} }
}) })
}) }
})
this.template.schema.forEach((item) => { this.template.schema.forEach((item) => {
(item.conditions || []).forEach((c) => { if (item.conditions) {
item.conditions.forEach((c) => {
if (c.field_uuid === field.uuid) { if (c.field_uuid === field.uuid) {
item.conditions.splice(item.conditions.indexOf(c), 1) item.conditions.splice(item.conditions.indexOf(c), 1)
} }
}) })
}) }
} })
this.save()
}, },
pasteField () { pasteField () {
const field = this.template.fields.find((f) => f.areas?.includes(this.copiedArea)) const field = this.template.fields.find((f) => f.areas?.includes(this.copiedArea))
@@ -1301,7 +1308,13 @@ export default {
this.copiedArea.option_uuid ||= field.options[0].uuid this.copiedArea.option_uuid ||= field.options[0].uuid
area.option_uuid = v4() 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) field.areas.push(area)
} else { } else {
@@ -1477,7 +1490,7 @@ export default {
if (this.dragField?.options?.length) { if (this.dragField?.options?.length) {
field.options = this.dragField.options.map(option => ({ value: option, uuid: v4() })) field.options = this.dragField.options.map(option => ({ value: option, uuid: v4() }))
} else { } else {
field.options = [{ value: '', uuid: v4() }] field.options = [{ value: '', uuid: v4() }, { value: '', uuid: v4() }]
} }
} }
@@ -1709,8 +1722,15 @@ export default {
}) })
}) })
this.template.fields = this.template.fields = this.template.fields.reduce((acc, f) => {
this.template.fields.filter((f) => !removedFieldUuids.includes(f.uuid) || f.areas?.length) if (removedFieldUuids.includes(f.uuid) && !f.areas?.length) {
this.removeFieldConditions(f)
} else {
acc.push(f)
}
return acc
}, [])
this.save() 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="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium"> <div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span class="modal-title"> <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> </span>
<a <a
href="#" href="#"
@@ -160,6 +160,11 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
defaultField: {
type: Object,
required: false,
default: null
},
buildDefaultName: { buildDefaultName: {
type: Function, type: Function,
required: true required: true
+1 -1
View File
@@ -66,7 +66,7 @@ export default {
acceptFileTypes: { acceptFileTypes: {
type: String, type: String,
required: false, required: false,
default: 'image/*, application/pdf' default: 'image/*, application/pdf, application/zip'
}, },
withReplaceButton: { withReplaceButton: {
type: Boolean, 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="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium"> <div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span class="modal-title"> <span class="modal-title">
{{ field.name || buildDefaultName(field, template.fields) }} {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field, template.fields) }}
</span> </span>
<a <a
href="#" href="#"
@@ -73,6 +73,11 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
defaultField: {
type: Object,
required: false,
default: null
},
editable: { editable: {
type: Boolean, type: Boolean,
required: false, required: false,
+3 -3
View File
@@ -107,7 +107,7 @@ export default {
acceptFileTypes: { acceptFileTypes: {
type: String, type: String,
required: false, required: false,
default: 'image/*, application/pdf' default: 'image/*, application/pdf, application/zip'
} }
}, },
emits: ['success', 'error', 'loading'], emits: ['success', 'error', 'loading'],
@@ -131,7 +131,7 @@ export default {
message () { message () {
if (this.isLoading) { if (this.isLoading) {
return this.t('uploading') 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') return this.title || this.t('add_pdf_documents_or_images')
} else { } else {
return this.title || this.t('add_documents_or_images') return this.title || this.t('add_documents_or_images')
@@ -146,7 +146,7 @@ export default {
methods: { methods: {
upload: Upload.methods.upload, upload: Upload.methods.upload,
onDropFiles (e) { 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.$refs.input.files = e.dataTransfer.files
this.upload() this.upload()
+29 -3
View File
@@ -151,7 +151,7 @@
</div> </div>
</div> </div>
<div <div
v-if="field.options && withOptions" v-if="field.options && withOptions && (isExpandOptions || field.options.length < 5)"
ref="options" ref="options"
class="border-t border-base-300 mx-2 pt-2 space-y-1.5" class="border-t border-base-300 mx-2 pt-2 space-y-1.5"
draggable="true" draggable="true"
@@ -216,12 +216,30 @@
/> />
<button <button
v-else-if="field.options && editable && !defaultField" 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" @click="addOption"
> >
+ {{ t('add_option') }} + {{ t('add_option') }}
</button> </button>
</div> </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> </div>
<Teleport <Teleport
v-if="isShowFormulaModal" v-if="isShowFormulaModal"
@@ -230,6 +248,7 @@
<FormulaModal <FormulaModal
:field="field" :field="field"
:editable="editable && !defaultField" :editable="editable && !defaultField"
:default-field="defaultField"
:build-default-name="buildDefaultName" :build-default-name="buildDefaultName"
@close="isShowFormulaModal = false" @close="isShowFormulaModal = false"
/> />
@@ -241,6 +260,7 @@
<FontModal <FontModal
:field="field" :field="field"
:editable="editable && !defaultField" :editable="editable && !defaultField"
:default-field="defaultField"
:build-default-name="buildDefaultName" :build-default-name="buildDefaultName"
@close="isShowFontModal = false" @close="isShowFontModal = false"
/> />
@@ -251,6 +271,7 @@
> >
<ConditionsModal <ConditionsModal
:item="field" :item="field"
:default-field="defaultField"
:build-default-name="buildDefaultName" :build-default-name="buildDefaultName"
@close="isShowConditionsModal = false" @close="isShowConditionsModal = false"
/> />
@@ -262,6 +283,7 @@
<DescriptionModal <DescriptionModal
:field="field" :field="field"
:editable="editable && !defaultField" :editable="editable && !defaultField"
:default-field="defaultField"
:build-default-name="buildDefaultName" :build-default-name="buildDefaultName"
@close="isShowDescriptionModal = false" @close="isShowDescriptionModal = false"
/> />
@@ -278,7 +300,7 @@ import FormulaModal from './formula_modal'
import FontModal from './font_modal' import FontModal from './font_modal'
import ConditionsModal from './conditions_modal' import ConditionsModal from './conditions_modal'
import DescriptionModal from './description_modal' import DescriptionModal from './description_modal'
import { IconRouteAltLeft, IconMathFunction, IconNewSection, IconTrashX, IconSettings } from '@tabler/icons-vue' import { IconRouteAltLeft, IconMathFunction, IconNewSection, IconTrashX, IconSettings, IconChevronDown } from '@tabler/icons-vue'
import { v4 } from 'uuid' import { v4 } from 'uuid'
export default { export default {
@@ -288,6 +310,7 @@ export default {
IconSettings, IconSettings,
FieldSettings, FieldSettings,
PaymentSettings, PaymentSettings,
IconChevronDown,
IconNewSection, IconNewSection,
FormulaModal, FormulaModal,
FontModal, FontModal,
@@ -333,6 +356,7 @@ export default {
emits: ['set-draw', 'remove', 'scroll-to'], emits: ['set-draw', 'remove', 'scroll-to'],
data () { data () {
return { return {
isExpandOptions: false,
isNameFocus: false, isNameFocus: false,
showPaymentModal: false, showPaymentModal: false,
isShowFormulaModal: false, isShowFormulaModal: false,
@@ -427,6 +451,8 @@ export default {
this.$el.getRootNode().activeElement.blur() this.$el.getRootNode().activeElement.blur()
}, },
addOption () { addOption () {
this.isExpandOptions = true
this.field.options.push({ value: '', uuid: v4() }) this.field.options.push({ value: '', uuid: v4() })
this.$nextTick(() => { this.$nextTick(() => {
@@ -1,31 +1,4 @@
<template> <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 <div
v-if="field.type === 'verification'" v-if="field.type === 'verification'"
class="py-1.5 px-1 relative" class="py-1.5 px-1 relative"
@@ -217,6 +190,77 @@
</label> </label>
</div> </div>
</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 <div
v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation" v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation"
class="py-1.5 px-1 relative" class="py-1.5 px-1 relative"
@@ -304,7 +348,7 @@
{{ t('any') }} {{ t('any') }}
</option> </option>
<option <option
v-for="type in ['drawn', 'typed', 'drawn_or_typed', 'upload']" v-for="type in ['drawn', 'typed', 'drawn_or_typed', 'drawn_or_upload', 'upload']"
:key="type" :key="type"
:value="type" :value="type"
:selected="field.preferences?.format === type" :selected="field.preferences?.format === type"
@@ -393,7 +437,7 @@
</label> </label>
</li> </li>
<li <li
v-if="['text', 'number'].includes(field.type) && !defaultField" v-if="['text', 'number'].includes(field.type)"
@click.stop @click.stop
> >
<label class="cursor-pointer py-1.5"> <label class="cursor-pointer py-1.5">
@@ -401,6 +445,7 @@
v-model="field.readonly" v-model="field.readonly"
type="checkbox" type="checkbox"
class="toggle toggle-xs" class="toggle toggle-xs"
:disabled="!editable || (defaultField && [true, false].includes(defaultField.readonly))"
@update:model-value="save" @update:model-value="save"
> >
<span class="label-text">{{ t('read_only') }}</span> <span class="label-text">{{ t('read_only') }}</span>
@@ -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="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium"> <div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span class="modal-title"> <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> </span>
<a <a
href="#" href="#"
@@ -202,6 +202,11 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
defaultField: {
type: Object,
required: false,
default: null
},
editable: { editable: {
type: Boolean, type: Boolean,
required: false, 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="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium"> <div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span class="modal-title"> <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> </span>
<a <a
href="#" href="#"
@@ -28,14 +28,21 @@
class="link" class="link"
>{{ t('available_in_pro') }}</a> >{{ t('available_in_pro') }}</a>
</div> </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 <button
v-for="f in fields" v-for="f in fields"
:key="f.uuid" :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)}}}`)" @click.prevent="insertTextUnderCursor(`{{${f.name || buildDefaultName(f, template.fields)}}}`)"
> >
<IconMathFunction
v-if="f.preferences?.formula"
width="17"
height="17"
stroke-width="1.5"
/>
<IconCodePlus <IconCodePlus
v-else
width="20" width="20"
height="20" height="20"
stroke-width="1.5" stroke-width="1.5"
@@ -57,7 +64,7 @@
<div class="mb-3 mt-1"> <div class="mb-3 mt-1">
<div <div
target="blank" target="blank"
class="text-sm mb-2 inline space-x-2" class="text-sm mb-2 inline space-x-2 font-mono"
> >
<button <button
class="bg-base-200 px-2 rounded-xl" class="bg-base-200 px-2 rounded-xl"
@@ -83,12 +90,6 @@
> >
/ /
</button> </button>
<button
class="bg-base-200 px-2 rounded-xl"
@click="insertTextUnderCursor(' % ')"
>
%
</button>
<button <button
class="bg-base-200 px-2 rounded-xl" class="bg-base-200 px-2 rounded-xl"
@click="insertTextUnderCursor('^')" @click="insertTextUnderCursor('^')"
@@ -122,12 +123,13 @@
</template> </template>
<script> <script>
import { IconCodePlus } from '@tabler/icons-vue' import { IconCodePlus, IconMathFunction } from '@tabler/icons-vue'
export default { export default {
name: 'FormulaModal', name: 'FormulaModal',
components: { components: {
IconCodePlus IconCodePlus,
IconMathFunction
}, },
inject: ['t', 'save', 'template', 'withFormula'], inject: ['t', 'save', 'template', 'withFormula'],
props: { props: {
@@ -135,6 +137,11 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
defaultField: {
type: Object,
required: false,
default: null
},
editable: { editable: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -154,7 +161,7 @@ export default {
computed: { computed: {
fields () { fields () {
return this.template.fields.reduce((acc, f) => { return this.template.fields.reduce((acc, f) => {
if (f !== this.field && ['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) acc.push(f)
} }
@@ -224,9 +231,11 @@ export default {
this.formula = newText 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 () { resizeTextarea () {
const textarea = this.$refs.textarea const textarea = this.$refs.textarea
@@ -78,7 +78,7 @@ export default {
acceptFileTypes: { acceptFileTypes: {
type: String, type: String,
required: false, required: false,
default: 'image/*, application/pdf' default: 'image/*, application/pdf, application/zip'
} }
}, },
emits: ['add', 'replace', 'replace-and-clone', 'error'], emits: ['add', 'replace', 'replace-and-clone', 'error'],
+12
View File
@@ -60,6 +60,7 @@ const en = {
any: 'Any', any: 'Any',
drawn: 'Drawn', drawn: 'Drawn',
drawn_or_typed: 'Drawn or Typed', drawn_or_typed: 'Drawn or Typed',
drawn_or_upload: 'Drawn or Upload',
upload: 'Upload', upload: 'Upload',
formula: 'Formula', formula: 'Formula',
typed: 'Typed', typed: 'Typed',
@@ -82,6 +83,7 @@ const en = {
copy_to_all_pages: 'Copy to All Pages', copy_to_all_pages: 'Copy to All Pages',
add_option: 'Add option', add_option: 'Add option',
option: 'Option', option: 'Option',
options: 'Options',
condition: 'Condition', condition: 'Condition',
first_party: 'First Party', first_party: 'First Party',
second_party: 'Second Party', second_party: 'Second Party',
@@ -254,6 +256,7 @@ const es = {
copy_to_all_pages: 'Copiar a todas las páginas', copy_to_all_pages: 'Copiar a todas las páginas',
add_option: 'Agregar opción', add_option: 'Agregar opción',
option: 'Opción', option: 'Opción',
options: 'Opciones',
first_party: 'Primera Parte', first_party: 'Primera Parte',
second_party: 'Segunda Parte', second_party: 'Segunda Parte',
third_party: 'Tercera Parte', third_party: 'Tercera Parte',
@@ -322,6 +325,7 @@ const es = {
any: 'Cualquier', any: 'Cualquier',
drawn: 'Dibujado', drawn: 'Dibujado',
drawn_or_typed: 'Dibujado o Escrito', drawn_or_typed: 'Dibujado o Escrito',
drawn_or_upload: 'Dibujado o Subido',
upload: 'Subir', upload: 'Subir',
typed: 'Escrito', typed: 'Escrito',
none: 'Ninguno', none: 'Ninguno',
@@ -410,6 +414,7 @@ const it = {
any: 'Qualsiasi', any: 'Qualsiasi',
drawn: 'Disegnato', drawn: 'Disegnato',
drawn_or_typed: 'Disegnato o Digitato', drawn_or_typed: 'Disegnato o Digitato',
drawn_or_upload: 'Disegnato o Caricato',
upload: 'Caricare', upload: 'Caricare',
formula: 'Formula', formula: 'Formula',
typed: 'Digitato', typed: 'Digitato',
@@ -432,6 +437,7 @@ const it = {
copy_to_all_pages: 'Copia in tutte le pagine', copy_to_all_pages: 'Copia in tutte le pagine',
add_option: 'Aggiungi opzione', add_option: 'Aggiungi opzione',
option: 'Opzione', option: 'Opzione',
options: 'Opzioni',
condition: 'Condizione', condition: 'Condizione',
first_party: 'Prima parte', first_party: 'Prima parte',
second_party: 'Seconda parte', second_party: 'Seconda parte',
@@ -604,6 +610,7 @@ const pt = {
copy_to_all_pages: 'Copiar para todas as páginas', copy_to_all_pages: 'Copiar para todas as páginas',
add_option: 'Adicionar opção', add_option: 'Adicionar opção',
option: 'Opção', option: 'Opção',
options: 'Opções',
first_party: 'Primeira Parte', first_party: 'Primeira Parte',
second_party: 'Segunda Parte', second_party: 'Segunda Parte',
third_party: 'Terceira Parte', third_party: 'Terceira Parte',
@@ -672,6 +679,7 @@ const pt = {
any: 'Qualquer', any: 'Qualquer',
drawn: 'Desenhado', drawn: 'Desenhado',
drawn_or_typed: 'Desenhado ou Digitado', drawn_or_typed: 'Desenhado ou Digitado',
drawn_or_upload: 'Desenhado ou Enviado',
upload: 'Carregar', upload: 'Carregar',
typed: 'Digitado', typed: 'Digitado',
none: 'Nenhum', none: 'Nenhum',
@@ -778,6 +786,7 @@ const fr = {
copy_to_all_pages: 'Copier sur toutes les pages', copy_to_all_pages: 'Copier sur toutes les pages',
add_option: 'Ajouter une option', add_option: 'Ajouter une option',
option: 'Option', option: 'Option',
options: 'Options',
first_party: 'Première partie', first_party: 'Première partie',
second_party: 'Deuxième partie', second_party: 'Deuxième partie',
third_party: 'Troisième partie', third_party: 'Troisième partie',
@@ -847,6 +856,7 @@ const fr = {
any: 'Tout', any: 'Tout',
drawn: 'Dessiné', drawn: 'Dessiné',
drawn_or_typed: 'Dessiné ou Tapé', drawn_or_typed: 'Dessiné ou Tapé',
drawn_or_upload: 'Dessiné ou Téléchargé',
upload: 'Téléverser', upload: 'Téléverser',
typed: 'Tapé', typed: 'Tapé',
none: 'Aucun', none: 'Aucun',
@@ -953,6 +963,7 @@ const de = {
copy_to_all_pages: 'Auf alle Seiten kopieren', copy_to_all_pages: 'Auf alle Seiten kopieren',
add_option: 'Option hinzufügen', add_option: 'Option hinzufügen',
option: 'Option', option: 'Option',
options: 'Options',
first_party: 'Erste Partei', first_party: 'Erste Partei',
second_party: 'Zweite Partei', second_party: 'Zweite Partei',
third_party: 'Dritte Partei', third_party: 'Dritte Partei',
@@ -1022,6 +1033,7 @@ const de = {
any: 'Jede', any: 'Jede',
drawn: 'Gezeichnet', drawn: 'Gezeichnet',
drawn_or_typed: 'Gezeichnet oder getippt', drawn_or_typed: 'Gezeichnet oder getippt',
drawn_or_upload: 'Gezeichnet oder hochgeladen',
upload: 'Upload', upload: 'Upload',
typed: 'Getippt', typed: 'Getippt',
none: 'Keine', none: 'Keine',
+1 -1
View File
@@ -1,7 +1,7 @@
<template> <template>
<div <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="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="container-type: size"
:style="{ aspectRatio: `${width} / ${height}`}" :style="{ aspectRatio: `${width} / ${height}`}"
> >
+1 -1
View File
@@ -157,7 +157,7 @@ export default {
acceptFileTypes: { acceptFileTypes: {
type: String, type: String,
required: false, required: false,
default: 'image/*, application/pdf' default: 'image/*, application/pdf, application/zip'
}, },
withReplaceButton: { withReplaceButton: {
type: Boolean, type: Boolean,
+1 -1
View File
@@ -35,7 +35,7 @@ export default {
acceptFileTypes: { acceptFileTypes: {
type: String, type: String,
required: false, required: false,
default: 'image/*, application/pdf' default: 'image/*, application/pdf, application/zip'
} }
}, },
emits: ['success'], emits: ['success'],
+1 -1
View File
@@ -57,7 +57,7 @@ export default {
acceptFileTypes: { acceptFileTypes: {
type: String, type: String,
required: false, required: false,
default: 'image/*, application/pdf' default: 'image/*, application/pdf, application/zip'
} }
}, },
emits: ['success', 'error'], emits: ['success', 'error'],
+31 -10
View File
@@ -38,12 +38,19 @@ class ProcessSubmitterCompletionJob
submission = submitter.submission 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( completed_submitter.assign_attributes(
submission_id: submitter.submission_id, submission_id: submitter.submission_id,
account_id: submission.account_id, account_id: submission.account_id,
template_id: submission.template_id, template_id: submission.template_id,
source: submission.source, 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 completed_at: submitter.completed_at
) )
@@ -135,19 +142,33 @@ class ProcessSubmitterCompletionJob
end end
def enqueue_next_submitter_request_notification(submitter) def enqueue_next_submitter_request_notification(submitter)
next_submitter_item = submission = submitter.submission
submitter.submission.template_submitters.find do |e| submitters_index = submission.submitters.index_by(&:uuid)
sub = submitter.submission.submitters.find { |s| s.uuid == e['uuid'] }
next unless sub next_submitter_items =
if submission.template_submitters.any? { |s| s['order'] }
submitter_groups =
submission.template_submitters.group_by.with_index { |s, index| s['order'] || index }
sub.completed_at.blank? && sub.sent_at.blank? 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'))
.compact.all?(&:completed_at?)
submitter_groups[current_group_index + 1]
end
else
submission.template_submitters.find do |e|
sub = submitters_index[e['uuid']]
next unless sub
sub.completed_at.blank? && sub.sent_at.blank?
end
end end
return unless next_submitter_item next_submitters = submitters_index.values_at(*Array.wrap(next_submitter_items).pluck('uuid')).compact
next_submitter = submitter.submission.submitters.find { |s| s.uuid == next_submitter_item['uuid'] } Submitters.send_signature_requests(next_submitters)
Submitters.send_signature_requests([next_submitter])
end end
end end
+1
View File
@@ -2,6 +2,7 @@
class TemplateMailer < ApplicationMailer class TemplateMailer < ApplicationMailer
def otp_verification_email(template, email:) def otp_verification_email(template, email:)
@current_account = template.account
@template = template @template = template
@otp_code = EmailVerificationCodes.generate([email.downcase.strip, template.slug].join(':')) @otp_code = EmailVerificationCodes.generate([email.downcase.strip, template.slug].join(':'))
+1
View File
@@ -38,6 +38,7 @@ class AccountConfig < ApplicationRecord
FORM_PREFILL_SIGNATURE_KEY = 'form_prefill_signature' FORM_PREFILL_SIGNATURE_KEY = 'form_prefill_signature'
ESIGNING_PREFERENCE_KEY = 'esigning_preference' ESIGNING_PREFERENCE_KEY = 'esigning_preference'
DOWNLOAD_LINKS_AUTH_KEY = 'download_links_auth' DOWNLOAD_LINKS_AUTH_KEY = 'download_links_auth'
DOWNLOAD_LINKS_EXPIRE_KEY = 'download_links_expire'
FORCE_SSO_AUTH_KEY = 'force_sso_auth' FORCE_SSO_AUTH_KEY = 'force_sso_auth'
FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf' FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf'
WITH_SIGNATURE_ID = 'with_signature_id' WITH_SIGNATURE_ID = 'with_signature_id'
+12 -11
View File
@@ -4,16 +4,17 @@
# #
# Table name: completed_submitters # Table name: completed_submitters
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# completed_at :datetime not null # completed_at :datetime not null
# sms_count :integer not null # sms_count :integer not null
# source :string not null # source :string not null
# created_at :datetime not null # verification_method :string
# updated_at :datetime not null # created_at :datetime not null
# account_id :bigint not null # updated_at :datetime not null
# submission_id :bigint not null # account_id :bigint not null
# submitter_id :bigint not null # submission_id :bigint not null
# template_id :bigint # submitter_id :bigint not null
# template_id :bigint
# #
# Indexes # Indexes
# #
@@ -29,5 +30,5 @@ class CompletedSubmitter < ApplicationRecord
has_many :completed_documents, dependent: :destroy, has_many :completed_documents, dependent: :destroy,
primary_key: :submitter_id, primary_key: :submitter_id,
foreign_key: :submitter_id, foreign_key: :submitter_id,
inverse_of: :submitter inverse_of: :completed_submitter
end end
+4 -4
View File
@@ -114,16 +114,16 @@ class Submission < ApplicationRecord
@fields_uuid_index ||= (template_fields || template.fields).index_by { |f| f['uuid'] } @fields_uuid_index ||= (template_fields || template.fields).index_by { |f| f['uuid'] }
end end
def audit_trail_url def audit_trail_url(expires_at: nil)
return if audit_trail.blank? return if audit_trail.blank?
ActiveStorage::Blob.proxy_url(audit_trail.blob) ActiveStorage::Blob.proxy_url(audit_trail.blob, expires_at:)
end end
alias audit_log_url audit_trail_url alias audit_log_url audit_trail_url
def combined_document_url def combined_document_url(expires_at: nil)
return if combined_document.blank? return if combined_document.blank?
ActiveStorage::Blob.proxy_url(combined_document.blob) ActiveStorage::Blob.proxy_url(combined_document.blob, expires_at:)
end end
end end
+12
View File
@@ -130,6 +130,18 @@
</div> </div>
<% end %> <% end %>
<% 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 py-2.5">
<span>
<%= t('expirable_file_download_links') %>
</span>
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false, onchange: 'this.form.requestSubmit()' %>
</div>
<% end %>
<% end %>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY) %> <% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY) %>
<% if can?(:manage, account_config) %> <% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %> <%= form_for account_config, url: account_configs_path, method: :post do |f| %>
+15 -4
View File
@@ -11,10 +11,21 @@
<div class="flex flex-col md:flex-row gap-4"> <div class="flex flex-col md:flex-row gap-4">
<div class="flex w-full space-x-4"> <div class="flex w-full space-x-4">
<% token = current_user.access_token.token %> <% token = current_user.access_token.token %>
<masked-input class="block w-full" data-token="<%= token %>"> <% obscured_token = current_user.access_token.token.sub(token[5..], '*' * token[5..].size) %>
<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> <% if current_account.testing? %>
</masked-input> <masked-input class="block w-full" data-token="<%= token %>">
<%= 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') %> <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> </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_') } %> <%= 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> </div>
+11 -9
View File
@@ -35,17 +35,19 @@
<%= ff.select :authentication, options_for_select([%w[Plain plain], %w[Login login], %w[CRAM-MD5 cram_md5]], value.fetch('authentication', 'plain')), { prompt: true }, required: true, class: 'base-select' %> <%= ff.select :authentication, options_for_select([%w[Plain plain], %w[Login login], %w[CRAM-MD5 cram_md5]], value.fetch('authentication', 'plain')), { prompt: true }, required: true, class: 'base-select' %>
</div> </div>
</div> </div>
<div class="form-control"> <% if !Docuseal.multitenant? || can?(:manage, :personalization_advanced) %>
<%= ff.label :security_label, 'SMTP Security', class: 'label' %> <div class="form-control">
<div class="flex items-center space-x-6"> <%= ff.label :security_label, 'SMTP Security', class: 'label' %>
<% [%w[Auto none], %w[SSL ssl], %w[TLS tls], %w[Noverify noverify]].each do |(label, val)| %> <div class="flex items-center space-x-6">
<%= ff.label :security, value: val, for: "#{val}_radio", class: 'label' do %> <% [%w[Auto none], %w[SSL ssl], %w[TLS tls], %w[Noverify noverify]].each do |(label, val)| %>
<%= ff.radio_button :security, val, checked: (value['security'].blank? && val == 'none') || value['security'] == val, id: "#{val}_radio", class: 'base-radio mr-2' %> <%= ff.label :security, value: val, for: "#{val}_radio", class: 'label' do %>
<%= label %> <%= ff.radio_button :security, val, checked: (value['security'].blank? && val == 'none') || value['security'] == val, id: "#{val}_radio", class: 'base-radio mr-2' %>
<%= label %>
<% end %>
<% end %> <% end %>
<% end %> </div>
</div> </div>
</div> <% end %>
<div class="form-control"> <div class="form-control">
<%= ff.label :from_email, t('send_from_email'), class: 'label' %> <%= ff.label :from_email, t('send_from_email'), class: 'label' %>
<%= ff.email_field :from_email, value: value['from_email'], required: !Docuseal.multitenant?, class: 'base-input' %> <%= ff.email_field :from_email, value: value['from_email'], required: !Docuseal.multitenant?, class: 'base-input' %>
+21 -11
View File
@@ -54,19 +54,29 @@
<p class="text-2xl font-bold mt-8 mb-4"> <p class="text-2xl font-bold mt-8 mb-4">
<%= t('change_password') %> <%= t('change_password') %>
</p> </p>
<%= form_for current_user, url: update_password_settings_profile_index_path, method: :patch, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %> <%= form_for current_user, url: update_password_settings_profile_index_path, method: :patch, html: { autocomplete: 'off' } do |f| %>
<div class="form-control"> <%= f.label :password, t('new_password'), class: 'label' %>
<%= f.label :password, t('new_password'), class: 'label' %> <%= f.password_field :password, autocomplete: 'off', class: 'base-input peer w-full', required: true %>
<%= f.password_field :password, autocomplete: 'off', class: 'base-input' %> <div class="<%= 'peer-invalid:hidden' if current_user.errors.blank? %> space-y-4 mt-4">
</div> <div class="form-control">
<div class="form-control"> <%= f.label :password_confirmation, t('confirm_password'), class: 'label' %>
<%= f.label :password_confirmation, t('confirm_password'), class: 'label' %> <%= f.password_field :password_confirmation, autocomplete: 'off', class: 'base-input' %>
<%= f.password_field :password_confirmation, autocomplete: 'off', class: 'base-input' %> </div>
</div> <div class="form-control">
<div class="form-control pt-2"> <%= f.label :current_password, t('current_password'), class: 'label' %>
<%= f.button button_title(title: t('update'), disabled_with: t('updating')), class: 'base-button' %> <%= f.password_field :current_password, autocomplete: 'current-password', class: 'base-input' %>
<% if Accounts.can_send_emails?(current_account) %>
<span class="label-text-alt mt-1">
<%= t('dont_remember_your_current_password_click_here_to_reset_it_html') %>
</span>
<% end %>
</div>
<div class="form-control">
<%= f.button button_title(title: t('update'), disabled_with: t('updating')), class: 'base-button' %>
</div>
</div> </div>
<% end %> <% end %>
<%= button_to nil, user_send_reset_password_path(current_user), id: 'resend_password_button', method: :put, class: 'hidden', data: { turbo_confirm: t('are_you_sure_') } %>
<p class="text-2xl font-bold mt-8 mb-4"> <p class="text-2xl font-bold mt-8 mb-4">
<%= t('two_factor_authentication') %> <%= t('two_factor_authentication') %>
</p> </p>
+1 -1
View File
@@ -17,7 +17,7 @@
"start_url": "/", "start_url": "/",
"display": "standalone", "display": "standalone",
"scope": "/", "scope": "/",
"orientation": "natural", "orientation": "any",
"description": "<%= Docuseal.product_name %> is an open source platform that provides secure and efficient digital document signing and processing.", "description": "<%= Docuseal.product_name %> is an open source platform that provides secure and efficient digital document signing and processing.",
"categories": ["productivity", "utilities"], "categories": ["productivity", "utilities"],
"theme_color": "#FAF7F4", "theme_color": "#FAF7F4",
@@ -0,0 +1,2 @@
<input id="api_key" type="text" value="<%= token %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
<%= 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') %>
@@ -0,0 +1,14 @@
<%= render 'shared/turbo_modal', title: t('reveal_api_key') do %>
<%= form_tag settings_reveal_access_token_path, enctype: 'multipart/form-data', data: { turbo_frame: :_top } do %>
<div class="form-control">
<%= label_tag :password, t('enter_your_password_to_reveal_the_api_key'), class: 'label' %>
<%= password_field_tag :password, nil, class: 'base-input', autocomplete: 'current-password', required: true, autofocus: true, placeholder: t('password') %>
<% if local_assigns[:error_message].present? %>
<span class="label-text-alt text-red-400 mt-1"><%= local_assigns[:error_message] %></span>
<% end %>
</div>
<div class="form-control mt-4">
<%= submit_tag t('submit'), class: 'base-button' %>
</div>
<% end %>
<% end %>
+3 -1
View File
@@ -56,6 +56,8 @@
<%= link_to 'API', settings_api_index_path, class: 'text-base hover:bg-base-300' %> <%= link_to 'API', settings_api_index_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
<% end %> <% end %>
<% end %>
<% if Docuseal.demo? || !Docuseal.multitenant? || (current_user != true_user && !current_account.testing?) %>
<% if can?(:read, WebhookUrl) %> <% if can?(:read, WebhookUrl) %>
<li> <li>
<%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %> <%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %>
@@ -70,7 +72,7 @@
<% end %> <% end %>
</li> </li>
<% end %> <% end %>
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user != true_user || !current_account.testing?) %> <% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user == true_user || current_account.testing?) %>
<li> <li>
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}#{'/test' if current_account.testing?}/api") : "#{Docuseal::CONSOLE_URL}/on_premises", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %> <%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}#{'/test' if current_account.testing?}/api") : "#{Docuseal::CONSOLE_URL}/on_premises", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
<% if Docuseal.multitenant? %> API <% else %> <%= t('console') %> <% end %> <% if Docuseal.multitenant? %> API <% else %> <%= t('console') %> <% end %>
@@ -1,4 +1,4 @@
<% if template.preferences['submitters_order'] == 'preserved' %> <% if template.preferences['submitters_order'] == 'preserved' || template.submitters.any? { |s| s['order'] } %>
<%= f.hidden_field :preserve_order, value: '1' %> <%= f.hidden_field :preserve_order, value: '1' %>
<% elsif template.submitters.size > 1 %> <% elsif template.submitters.size > 1 %>
<div class="form-control"> <div class="form-control">
+2 -2
View File
@@ -109,7 +109,7 @@
<%= render 'submissions/annotation', annot: %> <%= render 'submissions/annotation', annot: %>
<% end %> <% end %>
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %> <% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
<% value = values[field['uuid']] %> <% value = values[field['uuid']].presence || (field['default_value'] != '{{date}}' && field['readonly'] == true && field['conditions'].blank? && field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
<% value ||= field['default_value'] if field['type'] == 'heading' %> <% value ||= field['default_value'] if field['type'] == 'heading' %>
<% next if value.blank? %> <% next if value.blank? %>
<% submitter = submitters_index[field['submitter_uuid']] %> <% submitter = submitters_index[field['submitter_uuid']] %>
@@ -238,7 +238,7 @@
<div class="px-1.5 mb-4"> <div class="px-1.5 mb-4">
<% submitter_fields_index[item['uuid']].to_a.each_with_index do |field, index| %> <% submitter_fields_index[item['uuid']].to_a.each_with_index do |field, index| %>
<% submitter_field_counters[field['type']] += 1 %> <% submitter_field_counters[field['type']] += 1 %>
<% value = values[field['uuid']] %> <% value = values[field['uuid']].presence || (field['default_value'] != '{{date}}' && field['readonly'] == true && field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
<% next if value.blank? %> <% next if value.blank? %>
<% next if field['type'] == 'heading' %> <% next if field['type'] == 'heading' %>
<div class="pt-2.5 border-b border-base-300"> <div class="pt-2.5 border-b border-base-300">
+3 -2
View File
@@ -1,6 +1,7 @@
<% filter_params = params.permit(:q, *Submissions::Filter::ALLOWED_PARAMS) %>
<%= render 'shared/turbo_modal', title: t('export'), close_after_submit: false do %> <%= render 'shared/turbo_modal', title: t('export'), close_after_submit: false do %>
<div class="space-y-2"> <div class="space-y-2">
<%= button_to template_submissions_export_index_path(@template), params: { format: :xlsx }, method: :get, data: { turbo_frame: :_top } do %> <%= button_to template_submissions_export_index_path(@template), params: { format: :xlsx, **filter_params }, method: :get, data: { turbo_frame: :_top } do %>
<div class="flex items-center p-4 text-left rounded-2xl border border-neutral-300 hover:cursor-pointer hover:bg-neutral hover:text-gray-300"> <div class="flex items-center p-4 text-left rounded-2xl border border-neutral-300 hover:cursor-pointer hover:bg-neutral hover:text-gray-300">
<div class="enabled"> <div class="enabled">
<%= svg_icon('download', class: 'w-12 h-12 stroke-2 mr-2') %> <%= svg_icon('download', class: 'w-12 h-12 stroke-2 mr-2') %>
@@ -14,7 +15,7 @@
</div> </div>
</div> </div>
<% end %> <% end %>
<%= button_to template_submissions_export_index_path(@template), params: { format: :csv }, method: :get, data: { turbo_frame: :_top } do %> <%= button_to template_submissions_export_index_path(@template), params: { format: :csv, **filter_params }, method: :get, data: { turbo_frame: :_top } do %>
<div class="flex items-center text-left p-4 rounded-2xl border border-neutral-300 hover:cursor-pointer hover:bg-neutral hover:text-gray-300"> <div class="flex items-center text-left p-4 rounded-2xl border border-neutral-300 hover:cursor-pointer hover:bg-neutral hover:text-gray-300">
<div class="enabled"> <div class="enabled">
<%= svg_icon('download', class: 'w-12 h-12 stroke-2 mr-2') %> <%= svg_icon('download', class: 'w-12 h-12 stroke-2 mr-2') %>
+1 -1
View File
@@ -77,7 +77,7 @@
<%= render 'submit_form/annotations', annots: %> <%= render 'submit_form/annotations', annots: %>
<% end %> <% end %>
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %> <% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
<% value = values[field['uuid']].presence || (field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submitter.submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %> <% value = values[field['uuid']].presence || (field['readonly'] == true && field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submitter.submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
<% next if value.blank? %> <% next if value.blank? %>
<% next if !field['readonly'] && field['submitter_uuid'] == @submitter.uuid %> <% next if !field['readonly'] && field['submitter_uuid'] == @submitter.uuid %>
<% next if value == '{{date}}' && field['submitter_uuid'] != @submitter.uuid %> <% next if value == '{{date}}' && field['submitter_uuid'] != @submitter.uuid %>
+1 -1
View File
@@ -23,7 +23,7 @@
</div> </div>
</span> </span>
</div> </div>
<input id="file_dropzone_input" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="image/*, application/pdf<%= ', .docx, .doc, .xlsx, .xls, .odt, .rtf' if Docuseal.multitenant? %>" multiple> <input id="file_dropzone_input" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="image/*, application/pdf, application/zip<%= ", #{Templates::CreateAttachments::DOCUMENT_EXTENSIONS.join(', ')}" if Docuseal.advanced_formats? %>" multiple>
</div> </div>
</label> </label>
</file-dropzone> </file-dropzone>
+1 -1
View File
@@ -14,6 +14,6 @@
</span> </span>
</button> </button>
<input type="hidden" name="form_id" value="<%= form_id %>"> <input type="hidden" name="form_id" value="<%= form_id %>">
<input id="upload_template" name="files[]" class="hidden" onchange="this.form.requestSubmit()" type="file" accept="image/*, application/pdf<%= ', .docx, .doc, .xlsx, .xls, .odt, .rtf' if Docuseal.advanced_formats? %>" multiple> <input id="upload_template" name="files[]" class="hidden" onchange="this.form.requestSubmit()" type="file" accept="image/*, application/pdf, application/zip<%= ", #{Templates::CreateAttachments::DOCUMENT_EXTENSIONS.join(', ')}" if Docuseal.advanced_formats? %>" multiple>
<input hidden name="folder_name" value="<%= local_assigns[:folder_name] %>"> <input hidden name="folder_name" value="<%= local_assigns[:folder_name] %>">
<% end %> <% end %>
+1 -1
View File
@@ -13,7 +13,7 @@
<% if params[:q].present? || params[:status].present? || filter_params.present? || @pagy.pages > 1 %> <% if params[:q].present? || params[:status].present? || filter_params.present? || @pagy.pages > 1 %>
<%= render 'shared/search_input', title_selector: 'h2' %> <%= render 'shared/search_input', title_selector: 'h2' %>
<% end %> <% end %>
<%= link_to new_template_submissions_export_path(@template), class: 'hidden md:flex btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %> <%= link_to new_template_submissions_export_path(@template, params.permit(:q, *Submissions::Filter::ALLOWED_PARAMS)), class: 'hidden md:flex btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('download', class: 'w-6 h-6 stroke-2') %> <%= svg_icon('download', class: 'w-6 h-6 stroke-2') %>
<span><%= t('export') %></span> <span><%= t('export') %></span>
<% end %> <% end %>
@@ -1,4 +1,5 @@
<% close_on_submit = local_assigns.fetch(:close_on_submit, true) %> <% close_on_submit = local_assigns.fetch(:close_on_submit, true) %>
<% is_order_set = template.submitters.any? { |s| s['order'] } %>
<%= form_for template, url: template_recipients_path(template), method: :post, html: { autocomplete: 'off', class: 'mt-1', id: :submitters_form }, data: { close_on_submit: } do |f| %> <%= form_for template, url: template_recipients_path(template), method: :post, html: { autocomplete: 'off', class: 'mt-1', id: :submitters_form }, data: { close_on_submit: } do |f| %>
<% unless close_on_submit %> <% unless close_on_submit %>
<toggle-on-submit data-element-id="form_saved_alert"></toggle-on-submit> <toggle-on-submit data-element-id="form_saved_alert"></toggle-on-submit>
@@ -6,11 +7,24 @@
<div class="space-y-3 divide-y"> <div class="space-y-3 divide-y">
<% template.submitters.each_with_index do |submitter, index| %> <% template.submitters.each_with_index do |submitter, index| %>
<div class="<%= 'pt-3' if index.positive? %>"> <div class="<%= 'pt-3' if index.positive? %>">
<%= f.fields_for :submitters, item = Struct.new(:name, :uuid, :is_requester, :email, :invite_by_uuid, :optional_invite_by_uuid, :linked_to_uuid, :option).new(*submitter.values_at('name', 'uuid', 'is_requester', 'email', 'invite_by_uuid', 'optional_invite_by_uuid', 'linked_to_uuid')), index: do |ff| %> <%= f.fields_for :submitters, item = Struct.new(:name, :uuid, :is_requester, :email, :invite_by_uuid, :optional_invite_by_uuid, :linked_to_uuid, :order, :option).new(*submitter.values_at('name', 'uuid', 'is_requester', 'email', 'invite_by_uuid', 'optional_invite_by_uuid', 'linked_to_uuid', 'order')), index: do |ff| %>
<% item.option = item.is_requester.present? ? 'is_requester' : (item.email.present? ? 'email' : (item.linked_to_uuid.present? ? "linked_to_#{item.linked_to_uuid}" : (item.invite_by_uuid.present? ? "invite_by_#{item.invite_by_uuid}" : (item.optional_invite_by_uuid.present? ? "optional_invite_by_#{item.optional_invite_by_uuid}" : '')))) %> <% item.option = item.is_requester.present? ? 'is_requester' : (item.email.present? ? 'email' : (item.linked_to_uuid.present? ? "linked_to_#{item.linked_to_uuid}" : (item.invite_by_uuid.present? ? "invite_by_#{item.invite_by_uuid}" : (item.optional_invite_by_uuid.present? ? "optional_invite_by_#{item.optional_invite_by_uuid}" : '')))) %>
<%= ff.hidden_field :uuid %> <%= ff.hidden_field :uuid %>
<div class="form-control"> <div class="form-control">
<%= ff.text_field :name, class: 'w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer mb-2', autocomplete: 'off', placeholder: "#{index + 1}#{(index + 1).ordinal} Party", required: true %> <div class="flex justify-between">
<%= ff.text_field :name, class: 'w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer mb-2', autocomplete: 'off', placeholder: "#{index + 1}#{(index + 1).ordinal} Party", required: true %>
<% if template.submitters.size > 2 %>
<div id="order_<%= submitter['uuid'] %>" class="mr-0.5">
<% if is_order_set %>
<%= ff.select :order, options_for_select(template.submitters.map.with_index { |_, i| [(i + 1).ordinalize, i] }, submitter['order'].presence || index), {}, class: 'select select-xs text-sm input-bordered bg-white pl-3.5' %>
<% elsif index == 0 %>
<mount-on-click data-template-id="order_fields" class="link whitespace-nowrap text-sm mt-1 mr-1 block">
<%= t('edit_order') %>
</mount-on-click>
<% end %>
</div>
<% end %>
</div>
<% if template.submitters.size == 2 %> <% if template.submitters.size == 2 %>
<%= tag.input name: ff.field_name(:email), value: ff.object.email, type: :email, class: 'base-input', multiple: true, autocomplete: 'off', placeholder: t('default_email'), disabled: ff.object.is_requester || ff.object.invite_by_uuid.present? || ff.object.optional_invite_by_uuid.present?, id: field_uuid = SecureRandom.uuid %> <%= tag.input name: ff.field_name(:email), value: ff.object.email, type: :email, class: 'base-input', multiple: true, autocomplete: 'off', placeholder: t('default_email'), disabled: ff.object.is_requester || ff.object.invite_by_uuid.present? || ff.object.optional_invite_by_uuid.present?, id: field_uuid = SecureRandom.uuid %>
<% else %> <% else %>
@@ -85,3 +99,16 @@
</div> </div>
<% end %> <% end %>
</div> </div>
<% if template.submitters.size > 2 && !is_order_set %>
<template id="order_fields">
<% template.submitters.each_with_index do |submitter, index| %>
<turbo-stream action="replace" target="order_<%= submitter['uuid'] %>">
<template>
<div id="order_<%= submitter['uuid'] %>">
<%= select_tag "template[submitters][#{index}][order]", options_for_select(template.submitters.map.with_index { |_, i| [(i + 1).ordinalize, i] }, submitter['order'].presence || index), class: 'select select-xs text-sm input-bordered bg-white pl-3.5' %>
</div>
</template>
</turbo-stream>
<% end %>
</template>
<% end %>
+29 -11
View File
@@ -1,22 +1,37 @@
<%= form_for user, html: { class: 'space-y-4' }, data: { turbo_frame: :_top } do |f| %> <%= form_for user, html: { class: 'space-y-4' }, data: { turbo_frame: :_top } do |f| %>
<div class="space-y-2"> <div class="space-y-2">
<div class="form-control"> <div class="flex space-x-4">
<%= f.label :first_name, t('first_name'), class: 'label' %> <div class="w-full">
<%= f.text_field :first_name, required: true, class: 'base-input', dir: 'auto' %> <%= f.label :first_name, t('first_name'), class: 'label' %>
</div> <%= f.text_field :first_name, required: true, class: 'base-input w-full', dir: 'auto' %>
<div class="form-control"> </div>
<%= f.label :last_name, t('last_name'), class: 'label' %> <div class="w-full">
<%= f.text_field :last_name, required: true, class: 'base-input', dir: 'auto' %> <%= f.label :last_name, t('last_name'), class: 'label' %>
<%= f.text_field :last_name, required: true, class: 'base-input w-full', dir: 'auto' %>
</div>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= f.label :email, t('email'), class: 'label' %> <%= f.label :email, t('email'), class: 'label' %>
<%= f.email_field :email, required: true, class: 'base-input' %> <%= f.email_field :email, required: true, class: 'base-input' %>
<% if user.persisted? && Accounts.can_send_emails?(current_account) %>
<span class="label-text-alt mt-2 mx-1">
<%= t('click_here_to_send_a_reset_password_email_html') %>
</span>
<% end %>
</div> </div>
<div class="form-control"> <% if user.new_record? && !Docuseal.multitenant? %>
<%= f.label :password, t('password'), class: 'label' %> <div class="form-control">
<%= f.password_field :password, required: user.new_record?, class: 'base-input' %> <%= f.label :password, t('password'), class: 'label' %>
</div> <%= f.password_field :password, class: 'base-input' %>
</div>
<% end %>
<% if f.object != current_user %> <% if f.object != current_user %>
<% if user.otp_required_for_login %>
<div class="form-control">
<%= f.label :otp_required_for_login, t('two_factor_authentication'), class: 'label' %>
<%= f.select :otp_required_for_login, [[t('enabled'), true], [t('disabled'), false]], { include_blank: false }, class: 'base-select' %>
</div>
<% end %>
<%= render 'role_select', f: %> <%= render 'role_select', f: %>
<% end %> <% end %>
<% if local_assigns[:extra_fields_html].present? %> <% if local_assigns[:extra_fields_html].present? %>
@@ -27,3 +42,6 @@
<%= f.button button_title, class: 'base-button' %> <%= f.button button_title, class: 'base-button' %>
</div> </div>
<% end %> <% end %>
<% if user.persisted? %>
<%= button_to nil, user_send_reset_password_path(user), id: 'resend_password_button', method: :put, class: 'hidden', data: { turbo_confirm: t('are_you_sure_'), turbo_frame: :_top } %>
<% end %>
+1 -1
View File
@@ -4,7 +4,7 @@
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center mb-4"> <div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center mb-4">
<h1 class="text-4xl font-bold">Webhook</h1> <h1 class="text-4xl font-bold">Webhook</h1>
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center"> <div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center">
<% if params[:action] == 'index' %> <% if params[:action] == 'index' && (current_user == true_user || current_account.testing?) %>
<%= render 'shared/test_mode_toggle' %> <%= render 'shared/test_mode_toggle' %>
<% end %> <% end %>
<% if @webhook_url.persisted? && params[:action] == 'index' %> <% if @webhook_url.persisted? && params[:action] == 'index' %>
+1 -1
View File
@@ -321,7 +321,7 @@ Devise.setup do |config|
# apps is `200 OK` and `302 Found respectively`, but new apps are generated with # apps is `200 OK` and `302 Found respectively`, but new apps are generated with
# these new defaults that match Hotwire/Turbo behavior. # these new defaults that match Hotwire/Turbo behavior.
# Note: These might become the new default in future versions of Devise. # Note: These might become the new default in future versions of Devise.
config.responder.error_status = :unprocessable_entity config.responder.error_status = :unprocessable_content
config.responder.redirect_status = :see_other config.responder.redirect_status = :see_other
# ==> Configuration for :registerable # ==> Configuration for :registerable
+78 -1
View File
@@ -24,6 +24,12 @@ en: &en
thanks: Thanks thanks: Thanks
private: Private private: Private
select: Select select: Select
enabled: Enabled
disabled: Disabled
party: Party
click_here_to_send_a_reset_password_email_html: '<label class="link" for="resend_password_button">Click here</label> to send a reset password email.'
edit_order: Edit Order
expirable_file_download_links: Expirable file download links
invite_form_fields: Invite form fields invite_form_fields: Invite form fields
default_parties: Default parties default_parties: Default parties
authenticate_embedded_form_preview_with_token: Authenticate embedded form preview with token authenticate_embedded_form_preview_with_token: Authenticate embedded form preview with token
@@ -34,13 +40,14 @@ en: &en
bcc_recipients: BCC recipients bcc_recipients: BCC recipients
resend_pending: Re-send pending resend_pending: Re-send pending
always_enforce_signing_order: Always enforce the signing order always_enforce_signing_order: Always enforce the signing order
create_templates_with_private_access_by_default: Create templates with private access by default
ensure_unique_recipients: Ensure unique recipients ensure_unique_recipients: Ensure unique recipients
edit_per_party: Edit per party edit_per_party: Edit per party
reply_to: Reply to reply_to: Reply to
pending_by_me: Pending by me pending_by_me: Pending by me
partially_completed: Partially completed partially_completed: Partially completed
require_phone_2fa_to_open: Require phone 2FA to open require_phone_2fa_to_open: Require phone 2FA to open
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: The sender has requested a two factor authentication via one time password sent to your <b>%{phone}</b> phone number. the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: The sender has requested two-factor authentication via a one-time password sent to your <b>%{phone}</b> phone number.
send_verification_code: Send verification code send_verification_code: Send verification code
code_has_been_resent: Code has been re-sent code_has_been_resent: Code has been re-sent
invalid_code: Invalid code invalid_code: Invalid code
@@ -792,6 +799,12 @@ en: &en
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" has been completed by <strong>{submission.submitters}</strong>' template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" has been completed by <strong>{submission.submitters}</strong>'
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Please check the copy of your "<strong>{template.name}</strong>" in the email attachments.' please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Please check the copy of your "<strong>{template.name}</strong>" in the email attachments.'
you_have_been_invited_to_sign_the_template_name_html: 'You have been invited to sign the "<strong>{template.name}</strong>".' you_have_been_invited_to_sign_the_template_name_html: 'You have been invited to sign the "<strong>{template.name}</strong>".'
reveal_api_key: Reveal API Key
enter_your_password_to_reveal_the_api_key: Enter your password to reveal the API key
wrong_password: Wrong password.
current_password: Current password
dont_remember_your_current_password_click_here_to_reset_it_html: 'Don''t remember your current password? <label class="link" for="resend_password_button">Click here</label> to reset it.'
an_email_with_password_reset_instructions_has_been_sent: An email with password reset instructions has been sent.
submission_sources: submission_sources:
api: API api: API
bulk: Bulk Send bulk: Bulk Send
@@ -898,6 +911,12 @@ en: &en
range_without_total: "%{from}-%{to} events" range_without_total: "%{from}-%{to} events"
es: &es es: &es
enabled: Habilitado
disabled: Deshabilitado
expirable_file_download_links: Enlaces de descarga de archivos con vencimiento
create_templates_with_private_access_by_default: Crear plantillas con acceso privado por defecto
party: Parte
edit_order: Edita Pedido
select: Seleccionar select: Seleccionar
invite_form_fields: Invitar campos del formulario invite_form_fields: Invitar campos del formulario
pro: Pro pro: Pro
@@ -1670,6 +1689,12 @@ es: &es
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" ha sido completado por <strong>{submission.submitters}</strong>' template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" ha sido completado por <strong>{submission.submitters}</strong>'
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Por favor, revisa la copia de tu "<strong>{template.name}</strong>" en los archivos adjuntos del correo electrónico.' please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Por favor, revisa la copia de tu "<strong>{template.name}</strong>" en los archivos adjuntos del correo electrónico.'
you_have_been_invited_to_sign_the_template_name_html: 'Has sido invitado a firmar el "<strong>{template.name}</strong>".' you_have_been_invited_to_sign_the_template_name_html: 'Has sido invitado a firmar el "<strong>{template.name}</strong>".'
reveal_api_key: Revelar clave API
enter_your_password_to_reveal_the_api_key: Introduce tu contraseña para revelar la clave API
wrong_password: Contraseña incorrecta.
current_password: Contraseña actual
dont_remember_your_current_password_click_here_to_reset_it_html: '¿No recuerdas tu contraseña actual? <label class="link" for="resend_password_button">Haz clic aquí</label> para restablecerla.'
an_email_with_password_reset_instructions_has_been_sent: Se enviará un correo electrónico con las instrucciones para restablecer tu contraseña en unos minutos.
submission_sources: submission_sources:
api: API api: API
bulk: Envío masivo bulk: Envío masivo
@@ -1776,6 +1801,13 @@ es: &es
range_without_total: "%{from}-%{to} eventos" range_without_total: "%{from}-%{to} eventos"
it: &it it: &it
click_here_to_send_a_reset_password_email_html: '<label class="link" for="resend_password_button">Clicca qui</label> per inviare una email per reimpostare la password.'
enabled: Abilitato
disabled: Disabilitato
expirable_file_download_links: Link di download di file con scadenza
create_templates_with_private_access_by_default: Crea modelli con accesso privato per impostazione predefinita
party: Parte
edit_order: Modifica Ordine
select: Seleziona select: Seleziona
invite_form_fields: Invita campi modulo invite_form_fields: Invita campi modulo
pro: Pro pro: Pro
@@ -2548,6 +2580,12 @@ it: &it
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" è stato completato da <strong>{submission.submitters}</strong>' template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" è stato completato da <strong>{submission.submitters}</strong>'
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Per favore, controlla la copia del tuo "<strong>{template.name}</strong>" negli allegati dell''email.' please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Per favore, controlla la copia del tuo "<strong>{template.name}</strong>" negli allegati dell''email.'
you_have_been_invited_to_sign_the_template_name_html: 'Sei stato invitato a firmare il "<strong>{template.name}</strong>".' you_have_been_invited_to_sign_the_template_name_html: 'Sei stato invitato a firmare il "<strong>{template.name}</strong>".'
reveal_api_key: Mostra chiave API
enter_your_password_to_reveal_the_api_key: Inserisci la tua password per mostrare la chiave API
wrong_password: Password errata.
current_password: Password attuale
dont_remember_your_current_password_click_here_to_reset_it_html: 'Non ricordi la tua password attuale? <label class="link" for="resend_password_button">Clicca qui</label> per reimpostarla.'
an_email_with_password_reset_instructions_has_been_sent: Un'email con le istruzioni per reimpostare la password ti è stata inviata e arriverà entro pochi minuti.
submission_sources: submission_sources:
api: API api: API
bulk: Invio massivo bulk: Invio massivo
@@ -2654,6 +2692,13 @@ it: &it
range_without_total: "%{from}-%{to} eventi" range_without_total: "%{from}-%{to} eventi"
fr: &fr fr: &fr
click_here_to_send_a_reset_password_email_html: '<label class="link" for="resend_password_button">Cliquez ici</label> pour envoyer un e-mail de réinitialisation du mot de passe.'
enabled: Activé
disabled: Désactivé
expirable_file_download_links: Liens de téléchargement de fichiers expirables
create_templates_with_private_access_by_default: Créer des modèles avec un accès privé par défaut
party: Partie
edit_order: Modifier la commande
select: Sélectionner select: Sélectionner
invite_form_fields: Inviter des champs de formulaire invite_form_fields: Inviter des champs de formulaire
pro: Pro pro: Pro
@@ -3429,6 +3474,12 @@ fr: &fr
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" a été complété par <strong>{submission.submitters}</strong>' template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" a été complété par <strong>{submission.submitters}</strong>'
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Veuillez vérifier la copie de votre "<strong>{template.name}</strong>" dans les pièces jointes de l’e-mail.' please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Veuillez vérifier la copie de votre "<strong>{template.name}</strong>" dans les pièces jointes de l’e-mail.'
you_have_been_invited_to_sign_the_template_name_html: 'Vous avez été invité à signer le "<strong>{template.name}</strong>".' you_have_been_invited_to_sign_the_template_name_html: 'Vous avez été invité à signer le "<strong>{template.name}</strong>".'
reveal_api_key: Révéler la clé API
enter_your_password_to_reveal_the_api_key: Entrez votre mot de passe pour révéler la clé API
wrong_password: Mot de passe incorrect.
current_password: Mot de passe actuel
dont_remember_your_current_password_click_here_to_reset_it_html: 'Vous ne vous souvenez plus de votre mot de passe actuel ? <label class="link" for="resend_password_button">Cliquez ici</label> pour le réinitialiser.'
an_email_with_password_reset_instructions_has_been_sent: Un e-mail contenant les instructions pour réinitialiser votre mot de passe vous sera envoyé dans quelques minutes.
submission_sources: submission_sources:
api: API api: API
bulk: Envoi en masse bulk: Envoi en masse
@@ -3535,6 +3586,13 @@ fr: &fr
range_without_total: "%{from} à %{to} événements" range_without_total: "%{from} à %{to} événements"
pt: &pt pt: &pt
click_here_to_send_a_reset_password_email_html: '<label class="link" for="resend_password_button">Clique aqui</label> para enviar um e-mail de redefinição de senha.'
enabled: Ativado
disabled: Desativado
expirable_file_download_links: Links de download de arquivos com expiração
create_templates_with_private_access_by_default: Criar modelos com acesso privado por padrão
party: Parte
edit_order: Edita Pedido
select: Selecionar select: Selecionar
invite_form_fields: Convidar campos do formulário invite_form_fields: Convidar campos do formulário
pro: Pro pro: Pro
@@ -4308,6 +4366,12 @@ pt: &pt
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" foi concluído por <strong>{submission.submitters}</strong>' template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" foi concluído por <strong>{submission.submitters}</strong>'
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Por favor, verifique a cópia do seu "<strong>{template.name}</strong>" nos anexos do e-mail.' please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Por favor, verifique a cópia do seu "<strong>{template.name}</strong>" nos anexos do e-mail.'
you_have_been_invited_to_sign_the_template_name_html: 'Você foi convidado a assinar o "<strong>{template.name}</strong>".' you_have_been_invited_to_sign_the_template_name_html: 'Você foi convidado a assinar o "<strong>{template.name}</strong>".'
reveal_api_key: Revelar chave API
enter_your_password_to_reveal_the_api_key: Insira sua senha para revelar a chave API
wrong_password: Senha incorreta.
current_password: Senha atual
dont_remember_your_current_password_click_here_to_reset_it_html: 'Não se lembra da sua senha atual? <label class="link" for="resend_password_button">Clique aqui</label> para redefini-la.'
an_email_with_password_reset_instructions_has_been_sent: Um e-mail com instruções para redefinir sua senha será enviado em alguns minutos.
submission_sources: submission_sources:
api: API api: API
bulk: Envio em massa bulk: Envio em massa
@@ -4414,6 +4478,13 @@ pt: &pt
range_without_total: "%{from}-%{to} eventos" range_without_total: "%{from}-%{to} eventos"
de: &de de: &de
click_here_to_send_a_reset_password_email_html: '<label class="link" for="resend_password_button">Klicken Sie hier</label>, um eine E-Mail zum Zurücksetzen des Passworts zu senden.'
enabled: Aktiviert
disabled: Deaktiviert
expirable_file_download_links: Ablaufbare Datei-Download-Links
create_templates_with_private_access_by_default: Vorlagen standardmäßig mit privatem Zugriff erstellen
party: Partei
edit_order: Bestellung bearbeiten
select: Auswählen select: Auswählen
invite_form_fields: Formularfelder einladen invite_form_fields: Formularfelder einladen
pro: Pro pro: Pro
@@ -5187,6 +5258,12 @@ de: &de
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" wurde von <strong>{submission.submitters}</strong> abgeschlossen' template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" wurde von <strong>{submission.submitters}</strong> abgeschlossen'
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Bitte prüfen Sie die Kopie Ihres "<strong>{template.name}</strong>" in den E-Mail-Anhängen.' please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Bitte prüfen Sie die Kopie Ihres "<strong>{template.name}</strong>" in den E-Mail-Anhängen.'
you_have_been_invited_to_sign_the_template_name_html: 'Du wurdest eingeladen, "<strong>{template.name}</strong>" zu unterschreiben.' you_have_been_invited_to_sign_the_template_name_html: 'Du wurdest eingeladen, "<strong>{template.name}</strong>" zu unterschreiben.'
reveal_api_key: API-Schlüssel anzeigen
enter_your_password_to_reveal_the_api_key: Gib dein Passwort ein, um den API-Schlüssel anzuzeigen
wrong_password: Falsches Passwort.
current_password: Aktuelles Passwort
dont_remember_your_current_password_click_here_to_reset_it_html: 'Sie erinnern sich nicht an Ihr aktuelles Passwort? <label class="link" for="resend_password_button">Klicken Sie hier</label>, um es zurückzusetzen.'
an_email_with_password_reset_instructions_has_been_sent: Eine E-Mail mit Anweisungen zum Zurücksetzen Ihres Passworts wurde Ihnen in wenigen Minuten zugesendet.
submission_sources: submission_sources:
api: API api: API
bulk: Massenversand bulk: Massenversand
+4 -1
View File
@@ -65,7 +65,9 @@ Rails.application.routes.draw do
resources :setup, only: %i[index create] resources :setup, only: %i[index create]
resource :newsletter, only: %i[show update] resource :newsletter, only: %i[show update]
resources :enquiries, only: %i[create] resources :enquiries, only: %i[create]
resources :users, only: %i[new create edit update destroy] resources :users, only: %i[new create edit update destroy] do
resource :send_reset_password, only: %i[update], controller: 'users_send_reset_password'
end
resource :user_signature, only: %i[edit update destroy] resource :user_signature, only: %i[edit update destroy]
resource :user_initials, only: %i[edit update destroy] resource :user_initials, only: %i[edit update destroy]
resources :submissions_archived, only: %i[index], path: 'submissions/archived' resources :submissions_archived, only: %i[index], path: 'submissions/archived'
@@ -179,6 +181,7 @@ Rails.application.routes.draw do
defaults: { status: :integration } defaults: { status: :integration }
resource :personalization, only: %i[show create], controller: 'personalization_settings' resource :personalization, only: %i[show create], controller: 'personalization_settings'
resources :api, only: %i[index create], controller: 'api_settings' resources :api, only: %i[index create], controller: 'api_settings'
resource :reveal_access_token, only: %i[show create], controller: 'reveal_access_token'
resources :webhooks, only: %i[index show new create update destroy], controller: 'webhook_settings' do resources :webhooks, only: %i[index show new create update destroy], controller: 'webhook_settings' do
post :resend post :resend
@@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class PopulateSubmitterAccountId < ActiveRecord::Migration[7.1] class PopulateSubmitterAccountId < ActiveRecord::Migration[7.1]
disable_ddl_transaction disable_ddl_transaction!
class MigrationSubmitter < ApplicationRecord class MigrationSubmitter < ApplicationRecord
self.table_name = 'submitters' self.table_name = 'submitters'
@@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class PopulateCompletedSubmittersAndDocuments < ActiveRecord::Migration[7.2] class PopulateCompletedSubmittersAndDocuments < ActiveRecord::Migration[7.2]
disable_ddl_transaction disable_ddl_transaction!
class MigrationSubmitter < ApplicationRecord class MigrationSubmitter < ApplicationRecord
self.table_name = 'submitters' self.table_name = 'submitters'
@@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class PopulateWebhookUrls < ActiveRecord::Migration[7.2] class PopulateWebhookUrls < ActiveRecord::Migration[7.2]
disable_ddl_transaction disable_ddl_transaction!
class MigrationWebhookUrl < ActiveRecord::Base class MigrationWebhookUrl < ActiveRecord::Base
self.table_name = 'webhook_urls' self.table_name = 'webhook_urls'
@@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class AddSharedLinkToTemplates < ActiveRecord::Migration[8.0] class AddSharedLinkToTemplates < ActiveRecord::Migration[8.0]
disable_ddl_transaction disable_ddl_transaction!
class MigrationTemplate < ActiveRecord::Base class MigrationTemplate < ActiveRecord::Base
self.table_name = 'templates' self.table_name = 'templates'
@@ -1,11 +1,11 @@
# frozen_string_literal: true # frozen_string_literal: true
class CreateSearchEnties < ActiveRecord::Migration[8.0] class CreateSearchEnties < ActiveRecord::Migration[8.0]
disable_ddl_transaction!
def up def up
return unless adapter_name == 'PostgreSQL' return unless adapter_name == 'PostgreSQL'
enable_extension 'btree_gin'
create_table :search_entries do |t| create_table :search_entries do |t|
t.references :record, null: false, polymorphic: true, index: false t.references :record, null: false, polymorphic: true, index: false
t.bigint :account_id, null: false t.bigint :account_id, null: false
@@ -14,12 +14,24 @@ class CreateSearchEnties < ActiveRecord::Migration[8.0]
t.timestamps t.timestamps
end end
add_index :search_entries, %i[account_id tsvector], using: :gin, where: "record_type = 'Submitter'", begin
name: 'index_search_entries_on_account_id_tsvector_submitter' enable_extension 'btree_gin'
add_index :search_entries, %i[account_id tsvector], using: :gin, where: "record_type = 'Submission'", rescue StandardError
name: 'index_search_entries_on_account_id_tsvector_submission' nil
add_index :search_entries, %i[account_id tsvector], using: :gin, where: "record_type = 'Template'", end
name: 'index_search_entries_on_account_id_tsvector_template'
btree_gin_enabled = extension_enabled?('btree_gin')
add_index :search_entries, btree_gin_enabled ? %i[account_id tsvector] : :tsvector,
using: :gin, where: "record_type = 'Submitter'",
name: 'index_search_entries_on_account_id_tsvector_submitter'
add_index :search_entries, btree_gin_enabled ? %i[account_id tsvector] : :tsvector,
using: :gin, where: "record_type = 'Submission'",
name: 'index_search_entries_on_account_id_tsvector_submission'
add_index :search_entries, btree_gin_enabled ? %i[account_id tsvector] : :tsvector,
using: :gin, where: "record_type = 'Template'",
name: 'index_search_entries_on_account_id_tsvector_template'
add_index :search_entries, %i[record_id record_type], unique: true add_index :search_entries, %i[record_id record_type], unique: true
end end
@@ -28,6 +40,10 @@ class CreateSearchEnties < ActiveRecord::Migration[8.0]
drop_table :search_entries drop_table :search_entries
disable_extension 'btree_gin' begin
disable_extension 'btree_gin'
rescue StandardError
nil
end
end end
end end
@@ -4,13 +4,18 @@ class AddNgramToSearchIndex < ActiveRecord::Migration[8.0]
def change def change
return unless adapter_name == 'PostgreSQL' return unless adapter_name == 'PostgreSQL'
btree_gin_enabled = extension_enabled?('btree_gin')
add_column :search_entries, :ngram, :tsvector add_column :search_entries, :ngram, :tsvector
add_index :search_entries, %i[account_id ngram], using: :gin, where: "record_type = 'Submitter'", add_index :search_entries, btree_gin_enabled ? %i[account_id tsvector] : :tsvector,
name: 'index_search_entries_on_account_id_ngram_submitter' using: :gin, where: "record_type = 'Submitter'",
add_index :search_entries, %i[account_id ngram], using: :gin, where: "record_type = 'Submission'", name: 'index_search_entries_on_account_id_ngram_submitter'
name: 'index_search_entries_on_account_id_ngram_submission' add_index :search_entries, btree_gin_enabled ? %i[account_id tsvector] : :tsvector,
add_index :search_entries, %i[account_id ngram], using: :gin, where: "record_type = 'Template'", using: :gin, where: "record_type = 'Submission'",
name: 'index_search_entries_on_account_id_ngram_template' name: 'index_search_entries_on_account_id_ngram_submission'
add_index :search_entries, btree_gin_enabled ? %i[account_id tsvector] : :tsvector,
using: :gin, where: "record_type = 'Template'",
name: 'index_search_entries_on_account_id_ngram_template'
end end
end end
@@ -0,0 +1,30 @@
# frozen_string_literal: true
class PopulateExpireLinkConfigs < ActiveRecord::Migration[8.0]
disable_ddl_transaction!
class MigrationAccount < ActiveRecord::Base
self.table_name = 'accounts'
end
class MigrationAccountConfig < ActiveRecord::Base
self.table_name = 'account_configs'
serialize :value, coder: JSON
end
def up
MigrationAccount.find_each do |account|
config = MigrationAccountConfig.find_or_initialize_by(key: 'download_links_expire', account_id: account.id)
next if config.persisted?
config.value = false
config.save!
end
end
def down
nil
end
end
@@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddVerificationMethodToCompletedSubmitters < ActiveRecord::Migration[8.0]
def change
add_column :completed_submitters, :verification_method, :string
end
end
Loaded 100 of 127 files, more files were not shown because too many files have changed in this diff. Show more