Compare commits

..
60 Commits
Author SHA1 Message Date
Alex Turchyn 5a00d4ad5c Merge from docusealco/wip 2026-11-09 15:41:58 +02:00
Pete Matsyburka 30704b59c2 optimize query 2026-11-09 12:06:04 +02:00
Pete Matsyburka d56c903596 update readme 2026-11-08 16:05:24 +02:00
Pete Matsyburka 5353e8fa23 install app message 2026-11-08 10:58:10 +02:00
Pete Matsyburka 038d780265 sms fail event 2026-11-08 10:27:40 +02:00
Pete Matsyburka f3a8100734 app os 2026-11-08 09:59:56 +02:00
Pete Matsyburka dbf446efe1 block search bots 2026-11-08 09:48:22 +02:00
Pete Matsyburka 621431166b update security.md 2026-11-07 10:00:28 +02:00
Pete Matsyburka 92cfd5624d validate email 2026-11-07 09:37:32 +02:00
Pete Matsyburka e39e84adfa update gem 2026-11-05 23:04:12 +02:00
Pete Matsyburka 1b63b16691 adjust filename 2026-11-05 01:15:16 +02:00
Pete Matsyburka 256d14eb94 update rails 2026-11-04 14:32:43 +02:00
Pete Matsyburka 49463d611c unify native modals 2026-11-04 12:36:47 +02:00
Alex Turchyn 4fec8db589 fix native turbo modal 2026-11-04 12:36:47 +02:00
Pete Matsyburka 0bc620fa90 cleanup routes 2026-11-04 00:26:50 +02:00
Pete Matsyburka c0c5ba48a0 block active storage routes 2026-11-04 00:05:39 +02:00
Alex Turchyn 8fe0293ebe update hero image 2026-11-02 20:50:32 +02:00
Alex Turchyn 6c416dd76e Merge from docusealco/wip 2026-11-02 14:44:26 +02:00
Pete Matsyburka 1cc4ca6251 adjust events log 2026-11-02 12:28:21 +02:00
Pete Matsyburka a27423c845 update gem 2026-11-02 11:19:14 +02:00
Pete Matsyburka 2692b8a1b5 add index 2026-11-02 10:18:16 +02:00
Pete Matsyburka 7a75a6c887 optimize query 2026-11-02 09:48:37 +02:00
Pete Matsyburka 236b882558 update security.md 2026-11-01 13:42:44 +02:00
Pete Matsyburka 7ee648dacb add index 2026-11-01 12:38:59 +02:00
Pete Matsyburka 9bfa6f7f4a fix rubocop 2026-10-30 11:03:27 +02:00
Pete Matsyburka f9c12b9d78 mobile fixes 2026-10-29 21:24:07 +02:00
Pete Matsyburka 0a3f182e70 fix timezone 2026-10-29 10:51:45 +02:00
Pete Matsyburka 9c5c1f47f8 update gems 2026-10-29 10:36:11 +02:00
Pete Matsyburka 14c0ad2d16 add json log formatter 2026-10-29 09:59:21 +02:00
Pete Matsyburka 6736d8fe68 mobile fixes 2026-10-28 19:01:18 +02:00
Pete Matsyburka 4ecd830587 android fixes 2026-10-28 15:35:43 +02:00
Alex Turchyn a0ecc8e823 support the android app 2026-10-28 10:47:26 +02:00
Pete Matsyburka 8334b612a8 fix enable shared link 2026-10-28 09:33:46 +02:00
Pete Matsyburka 349c0b0537 optimize query 2026-10-27 15:44:08 +02:00
Pete Matsyburka 2af09afc66 add lang 2026-10-27 15:31:15 +02:00
Pete Matsyburka e22a3d9ef3 use uuid v7 2026-10-27 11:24:42 +02:00
Pete Matsyburka d3c92cecfa carry lang param 2026-10-26 17:57:03 +02:00
Pete Matsyburka af4672084d optimize query 2026-10-26 17:14:45 +02:00
Pete Matsyburka b7103cf010 cleanup 2026-10-26 16:33:55 +02:00
Alex Turchyn 13a1835c00 Merge from docusealco/wip 2026-10-26 12:27:46 +02:00
Pete Matsyburka c35407d7a9 cleanup routes 2026-10-25 21:43:38 +02:00
Pete Matsyburka e55ccc0b76 update dep 2026-10-25 16:09:58 +02:00
Pete Matsyburka b8d00870b4 optimize query 2026-10-25 15:02:22 +02:00
Pete Matsyburka 5413fbc248 add template_id to submitters 2026-10-25 09:39:47 +02:00
Pete Matsyburka 2d2eb7ce4d ruby 4.0.9 2026-10-23 23:42:14 +03:00
Pete Matsyburka ed2ddbf74e remove legacy route 2026-10-23 11:10:14 +03:00
Pete Matsyburka b99e5d1262 adjust autosave 2026-10-23 10:02:32 +03:00
Pete Matsyburka 87f6878eda fix brakeman 2026-10-23 08:29:39 +03:00
Pete Matsyburka 99b815eb4b fix generation 2026-10-23 08:25:07 +03:00
Pete Matsyburka e40a29b9ab update certs 2026-10-22 18:47:09 +03:00
Pete Matsyburka 151890bb72 add autosave config 2026-10-22 17:17:47 +03:00
Pete Matsyburka 78edd87cc1 add main tag 2026-10-22 15:01:54 +03:00
Pete Matsyburka 88c99a2b35 fix turbo morph 2026-10-22 14:48:03 +03:00
Pete Matsyburka 282b8cfa73 fix search 2026-10-22 11:02:24 +03:00
Pete Matsyburka e9888c7466 add validation 2026-10-21 15:41:53 +03:00
Pete Matsyburka 0647befb3a escape email variables 2026-10-21 09:54:29 +03:00
Pete Matsyburka 85aa3d0451 mask email field value 2026-10-21 09:35:38 +03:00
Pete Matsyburka 6d2b3bb654 adjust test webhook 2026-10-20 20:15:37 +03:00
Pete Matsyburka b394854e9d adjust send form 2026-10-20 09:50:21 +03:00
Pete Matsyburka 2e586e930e adjust signature validation 2026-10-19 19:03:26 +03:00
156 changed files with 867 additions and 569 deletions

No files matched your search

+4 -4
View File
@@ -12,7 +12,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 4.0.5
ruby-version: 4.0.9
- name: Cache gems
uses: actions/cache@v4
with:
@@ -37,7 +37,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 4.0.5
ruby-version: 4.0.9
- name: Cache gems
uses: actions/cache@v4
with:
@@ -89,7 +89,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 4.0.5
ruby-version: 4.0.9
- name: Cache gems
uses: actions/cache@v4
with:
@@ -113,7 +113,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
container:
image: ruby:4.0.5-alpine
image: ruby:4.0.9-alpine
services:
postgres:
+5 -4
View File
@@ -1,4 +1,4 @@
FROM ruby:4.0.5-alpine AS download
FROM ruby:4.0.9-alpine AS download
WORKDIR /fonts
@@ -17,7 +17,7 @@ RUN apk --no-cache add wget unzip && \
mkdir -p /pdfium-linux && \
unzip -q pdfium-linux.zip -d /pdfium-linux
FROM ruby:4.0.5-alpine AS webpack
FROM ruby:4.0.9-alpine AS webpack
ENV RAILS_ENV=production
ENV NODE_ENV=production
@@ -44,7 +44,7 @@ COPY ./app/views ./app/views
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
FROM ruby:4.0.5-alpine AS app
FROM ruby:4.0.9-alpine AS app
ENV RAILS_ENV=production
ENV BUNDLE_WITHOUT="development:test"
@@ -52,7 +52,8 @@ ENV OPENSSL_CONF=/etc/openssl_legacy.cnf
WORKDIR /app
RUN apk add --no-cache libpq vips redis onnxruntime leptonica jemalloc font-liberation font-carlito && \
RUN apk upgrade --no-cache ca-certificates-bundle && \
apk add --no-cache libpq vips redis onnxruntime leptonica jemalloc font-liberation font-carlito && \
rm -f /usr/bin/onnx_test_runner /usr/bin/onnxruntime_test
RUN addgroup -g 2000 docuseal && adduser -u 2000 -G docuseal -s /bin/sh -D -h /home/docuseal docuseal
+1 -1
View File
@@ -2,7 +2,7 @@
source 'https://rubygems.org'
ruby '4.0.5'
ruby '4.0.9'
gem 'addressable'
gem 'arabic-letter-connector', require: false
+94 -94
View File
@@ -3,29 +3,29 @@ GEM
specs:
action_text-trix (2.1.19)
railties
actioncable (8.1.3.1)
actionpack (= 8.1.3.1)
activesupport (= 8.1.3.1)
actioncable (8.1.4.1)
actionpack (= 8.1.4.1)
activesupport (= 8.1.4.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (8.1.3.1)
actionpack (= 8.1.3.1)
activejob (= 8.1.3.1)
activerecord (= 8.1.3.1)
activestorage (= 8.1.3.1)
activesupport (= 8.1.3.1)
actionmailbox (8.1.4.1)
actionpack (= 8.1.4.1)
activejob (= 8.1.4.1)
activerecord (= 8.1.4.1)
activestorage (= 8.1.4.1)
activesupport (= 8.1.4.1)
mail (>= 2.8.0)
actionmailer (8.1.3.1)
actionpack (= 8.1.3.1)
actionview (= 8.1.3.1)
activejob (= 8.1.3.1)
activesupport (= 8.1.3.1)
actionmailer (8.1.4.1)
actionpack (= 8.1.4.1)
actionview (= 8.1.4.1)
activejob (= 8.1.4.1)
activesupport (= 8.1.4.1)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (8.1.3.1)
actionview (= 8.1.3.1)
activesupport (= 8.1.3.1)
actionpack (8.1.4.1)
actionview (= 8.1.4.1)
activesupport (= 8.1.4.1)
nokogiri (>= 1.8.5)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
@@ -33,36 +33,36 @@ GEM
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (8.1.3.1)
actiontext (8.1.4.1)
action_text-trix (~> 2.1.15)
actionpack (= 8.1.3.1)
activerecord (= 8.1.3.1)
activestorage (= 8.1.3.1)
activesupport (= 8.1.3.1)
actionpack (= 8.1.4.1)
activerecord (= 8.1.4.1)
activestorage (= 8.1.4.1)
activesupport (= 8.1.4.1)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (8.1.3.1)
activesupport (= 8.1.3.1)
actionview (8.1.4.1)
activesupport (= 8.1.4.1)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (8.1.3.1)
activesupport (= 8.1.3.1)
rails-html-sanitizer (~> 1.6, >= 1.7.2)
activejob (8.1.4.1)
activesupport (= 8.1.4.1)
globalid (>= 0.3.6)
activemodel (8.1.3.1)
activesupport (= 8.1.3.1)
activerecord (8.1.3.1)
activemodel (= 8.1.3.1)
activesupport (= 8.1.3.1)
activemodel (8.1.4.1)
activesupport (= 8.1.4.1)
activerecord (8.1.4.1)
activemodel (= 8.1.4.1)
activesupport (= 8.1.4.1)
timeout (>= 0.4.0)
activestorage (8.1.3.1)
actionpack (= 8.1.3.1)
activejob (= 8.1.3.1)
activerecord (= 8.1.3.1)
activesupport (= 8.1.3.1)
activestorage (8.1.4.1)
actionpack (= 8.1.4.1)
activejob (= 8.1.4.1)
activerecord (= 8.1.4.1)
activesupport (= 8.1.4.1)
marcel (~> 1.0)
activesupport (8.1.3.1)
activesupport (8.1.4.1)
base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
@@ -116,7 +116,7 @@ GEM
erubi (~> 1.4)
parser (>= 2.4)
smart_properties
bigdecimal (4.1.2)
bigdecimal (4.1.3)
bindex (0.8.1)
bootsnap (1.23.0)
msgpack (~> 1.2)
@@ -149,14 +149,14 @@ GEM
crack (1.0.1)
bigdecimal
rexml
crass (1.0.7)
crass (1.0.8)
csv (3.3.5)
csv-safe (3.3.1)
csv (~> 3.0)
cuprite (0.17)
capybara (~> 3.0)
ferrum (~> 0.17.0)
date (3.5.1)
date (3.6.0)
debug (1.11.1)
irb (~> 1.10)
reline (>= 0.3.8)
@@ -179,7 +179,7 @@ GEM
dotenv (3.2.0)
drb (2.2.3)
email_typo (0.2.3)
erb (6.0.6)
erb (6.0.7)
erb_lint (0.9.0)
activesupport
better_html (>= 2.0.1)
@@ -195,8 +195,8 @@ GEM
railties (>= 6.1.0)
faker (3.6.1)
i18n (>= 1.8.11, < 2)
faraday (2.14.3)
faraday-net_http (>= 2.0, < 3.5)
faraday (2.15.1)
faraday-net_http (>= 2.0, < 3.6)
json
logger
faraday-follow_redirects (0.5.0)
@@ -264,14 +264,14 @@ GEM
strscan (>= 3.1.2)
i18n (1.15.2)
concurrent-ruby (~> 1.0)
io-console (0.8.2)
io-console (0.9.5)
irb (1.18.0)
pp (>= 0.6.0)
prism (>= 1.3.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jmespath (1.6.2)
json (2.21.2)
json (3.1.0)
jwt (3.2.0)
base64
language_server-protocol (3.17.0.5)
@@ -293,9 +293,9 @@ GEM
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
loofah (2.25.2)
loofah (2.25.3)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
nokogiri (>= 1.19.5)
mail (2.9.1)
logger
mini_mime (>= 0.1.1)
@@ -310,28 +310,28 @@ GEM
drb (~> 2.0)
prism (~> 1.5)
msgpack (1.8.4)
multi_json (1.19.1)
multi_json (1.21.2)
net-http (0.9.1)
uri (>= 0.11.1)
net-imap (0.6.6)
net-imap (0.6.8)
date
net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.2.2)
net-protocol (0.4.0)
timeout
net-smtp (0.5.1)
net-smtp (0.5.2)
net-protocol
nio4r (2.7.5)
nokogiri (1.19.4-aarch64-linux-gnu)
nokogiri (1.19.7-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.19.4-aarch64-linux-musl)
nokogiri (1.19.7-aarch64-linux-musl)
racc (~> 1.4)
nokogiri (1.19.4-arm64-darwin)
nokogiri (1.19.7-arm64-darwin)
racc (~> 1.4)
nokogiri (1.19.4-x86_64-linux-gnu)
nokogiri (1.19.7-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.19.4-x86_64-linux-musl)
nokogiri (1.19.7-x86_64-linux-musl)
racc (~> 1.4)
numo-narray-alt (0.10.3)
onnxruntime (0.10.1-aarch64-linux)
@@ -361,7 +361,7 @@ GEM
prettyprint
pretender (1.0.0)
actionpack (>= 7.2)
prettyprint (0.2.0)
prettyprint (0.2.1)
prism (1.9.0)
pry (0.16.0)
coderay (~> 1.1)
@@ -373,43 +373,43 @@ GEM
puma (8.0.2)
nio4r (~> 2.0)
racc (1.8.1)
rack (3.2.6)
rack (3.2.8)
rack-proxy (0.7.7)
rack
rack-session (2.1.2)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.2.0)
rack-test (2.3.0)
rack (>= 1.3)
rackup (2.3.1)
rack (>= 3)
rails (8.1.3.1)
actioncable (= 8.1.3.1)
actionmailbox (= 8.1.3.1)
actionmailer (= 8.1.3.1)
actionpack (= 8.1.3.1)
actiontext (= 8.1.3.1)
actionview (= 8.1.3.1)
activejob (= 8.1.3.1)
activemodel (= 8.1.3.1)
activerecord (= 8.1.3.1)
activestorage (= 8.1.3.1)
activesupport (= 8.1.3.1)
rails (8.1.4.1)
actioncable (= 8.1.4.1)
actionmailbox (= 8.1.4.1)
actionmailer (= 8.1.4.1)
actionpack (= 8.1.4.1)
actiontext (= 8.1.4.1)
actionview (= 8.1.4.1)
activejob (= 8.1.4.1)
activemodel (= 8.1.4.1)
activerecord (= 8.1.4.1)
activestorage (= 8.1.4.1)
activesupport (= 8.1.4.1)
bundler (>= 1.15.0)
railties (= 8.1.3.1)
railties (= 8.1.4.1)
rails-dom-testing (2.3.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.7.1)
loofah (~> 2.25, >= 2.25.2)
rails-html-sanitizer (1.7.2)
loofah (~> 2.25, >= 2.25.3)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
rails-i18n (8.1.0)
i18n (>= 0.7, < 2)
railties (>= 8.0.0, < 9)
railties (8.1.3.1)
actionpack (= 8.1.3.1)
activesupport (= 8.1.3.1)
railties (8.1.4.1)
actionpack (= 8.1.4.1)
activesupport (= 8.1.4.1)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
@@ -418,11 +418,11 @@ GEM
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.4.2)
rbs (4.1.0)
rbs (4.2.0)
logger
prism (>= 1.6.0)
tsort
rdoc (8.0.0)
rdoc (8.1.0)
erb
prism (>= 1.6.0)
rbs (>= 4.0.0)
@@ -430,7 +430,7 @@ GEM
redis-client (0.29.0)
connection_pool
regexp_parser (2.11.3)
reline (0.6.3)
reline (0.7.0)
io-console (~> 0.5)
representable (3.2.0)
declarative (< 0.1.0)
@@ -466,14 +466,14 @@ GEM
rspec-mocks (>= 3.13.0, < 5.0.0)
rspec-support (>= 3.13.0, < 5.0.0)
rspec-support (3.13.7)
rubocop (1.86.0)
json (~> 2.3)
rubocop (1.92.1)
json (>= 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (~> 1.10)
parallel (>= 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
regexp_parser (>= 2.11.3, < 3.0)
rubocop-ast (>= 1.49.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
@@ -535,7 +535,7 @@ GEM
thor (1.5.0)
timeout (0.6.1)
trailblazer-option (0.1.2)
trilogy (2.12.2)
trilogy (2.13.3)
bigdecimal
tsort (0.2.0)
turbo-rails (2.0.23)
@@ -555,7 +555,7 @@ GEM
unicode-emoji (~> 4.1)
unicode-emoji (4.2.0)
uniform_notifier (1.18.0)
uri (1.1.1)
uri (1.1.2)
useragent (0.16.11)
warden (1.2.9)
rack (>= 2.0.9)
@@ -572,22 +572,22 @@ GEM
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
wordcraft (0.8.0-aarch64-linux-gnu)
wordcraft (0.11.0-aarch64-linux-gnu)
ffi (>= 1.15)
wordcraft (0.8.0-aarch64-linux-musl)
wordcraft (0.11.0-aarch64-linux-musl)
ffi (>= 1.15)
wordcraft (0.8.0-arm64-darwin)
wordcraft (0.11.0-arm64-darwin)
ffi (>= 1.15)
wordcraft (0.8.0-x86_64-linux-gnu)
wordcraft (0.11.0-x86_64-linux-gnu)
ffi (>= 1.15)
wordcraft (0.8.0-x86_64-linux-musl)
wordcraft (0.11.0-x86_64-linux-musl)
ffi (>= 1.15)
xlsxtream (3.1.0)
zip_kit (>= 6.2, < 7)
xpath (3.2.0)
nokogiri (~> 1.8)
yaml (0.4.0)
zeitwerk (2.8.2)
zeitwerk (2.8.3)
zip_kit (6.3.4)
PLATFORMS
@@ -664,7 +664,7 @@ DEPENDENCIES
xlsxtream
RUBY VERSION
ruby 4.0.5
ruby 4.0.9
BUNDLED WITH
4.0.3
+3 -2
View File
@@ -30,7 +30,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
<a href="https://docuseal.com/sign_up">☁️ Try in Cloud</a>
</h2>
[![Demo](https://github.com/docusealco/docuseal/assets/5418788/d8703ea3-361a-423f-8bfe-eff1bd9dbe14)](https://demo.docuseal.tech)
[![Demo](https://github.com/user-attachments/assets/8344a341-96cf-4df3-aaae-5458a21a65b3)](https://demo.docuseal.tech)
## Features
- PDF form fields builder (WYSIWYG)
@@ -41,10 +41,11 @@ DocuSeal is an open source platform that provides secure and efficient digital d
- Automatic PDF eSignature
- PDF signature verification
- User management
- Mobile-optimized
- Mobile-optimized signing
- 7 UI languages with signing available in 14 languages
- API and Webhooks for integrations
- Easy to deploy in minutes
- Mobile apps for [iOS](https://apps.apple.com/us/app/docuseal-e-signature/id6823564236) and [Android](https://play.google.com/store/apps/details?id=com.docuseal.app)
## Pro Features
- Company logo and white-label
+2
View File
@@ -9,6 +9,8 @@ Out of scope vulnerabilities:
- CSRF
- DNSSEC, CAA, CSP headers
- DNS or email security related
- SSRF
- DoS
- Rate Limiting
Note: We reserve the right to classify any reported vulnerability as out of scope for the bug bounty program.
+1 -2
View File
@@ -54,14 +54,13 @@ class AccountsController < ApplicationController
email: true_user.email.sub('@', '+removed@'))
true_user.account.update!(archived_at: Time.current)
# rubocop:disable Layout/LineLength
# rubocop:disable-next Layout/LineLength
render turbo_stream: turbo_stream.replace(
:account_delete_button,
html: helpers.tag.p(I18n.t('your_account_will_be_permanently_deleted_within_2_months_please_contact_us_if_you_want_to_keep_your_account')) +
helpers.tag.br +
helpers.tag.p(I18n.t('your_email_address_has_been_released_immediately_you_can_now_be_added_to_your_company_team_without_waiting_for_the_deletion_period_to_end'))
)
# rubocop:enable Layout/LineLength
end
private
@@ -11,7 +11,7 @@ module Api
before_action :set_noindex_headers
before_action :set_security_headers
# rubocop:disable Metrics
# rubocop:disable-next Metrics
def show
blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
@@ -34,6 +34,12 @@ module Api
@record = attachment.record
@record = @record.record if @record.is_a?(ActiveStorage::Attachment)
if DetectBrowserDevice.search_bot?(request.user_agent)
Rollbar.warning("Search bot: #{@record.class.name} #{@record.id}") if defined?(Rollbar)
raise ActionController::RoutingError, 'Not Found'
end
authorization_check!(attachment, @record, exp)
if request.headers['Range'].present?
@@ -53,7 +59,6 @@ module Api
end
end
end
# rubocop:enable Metrics
private
@@ -1,53 +0,0 @@
# frozen_string_literal: true
module Api
class ActiveStorageBlobsProxyLegacyController < ApiBaseController
include ActiveStorage::Streaming
skip_before_action :authenticate_user!
skip_authorization_check
before_action :set_cors_headers
before_action :set_noindex_headers
before_action :set_security_headers
# rubocop:disable Metrics
def show
Rollbar.info('Blob legacy') if defined?(Rollbar)
blob = ActiveStorage::Blob.find_signed(params[:signed_blob_id] || params[:signed_id])
return head :not_found unless blob
if FilenameUtils.dangerous_extension(blob.filename)
Rollbar.error('Dangerous extension') if defined?(Rollbar)
return head :unprocessable_content
end
is_permitted = blob.attachments.any? do |a|
(current_user && a.record.account.id == current_user.account_id) ||
a.record.account.account_configs.any? { |e| e.key == 'legacy_blob_proxy' } ||
a.name == 'logo'
end
unless is_permitted
Rollbar.error("Blob account not found: #{blob.id}") if defined?(Rollbar)
return render json: { error: 'Not authenticated' }, status: :unauthorized
end
if request.headers['Range'].present?
send_blob_byte_range_data blob, request.headers['Range']
else
http_cache_forever public: true do
response.headers['Accept-Ranges'] = 'bytes'
response.headers['Content-Length'] = blob.byte_size.to_s
send_blob_stream blob, disposition: params[:disposition]
end
end
end
# rubocop:enable Metrics
end
end
@@ -1,74 +0,0 @@
# frozen_string_literal: true
module Api
class AttachmentsController < ActionController::API
include ActionController::Cookies
include ActiveStorage::SetCurrent
COOKIE_STORE_LIMIT = 10
def create
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
unless can_upload?(@submitter)
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content
end
file = params[:file]
if params[:type].in?(%w[initials signature])
image = ImageUtils.load_vips(file.read, content_type: file.content_type)
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
metadata = { analyzed: true, identified: true, width: image.width, height: image.height }
end
attachment = Submitters.create_attachment!(@submitter, file, metadata:)
if params[:remember_signature] == 'true' && @submitter.email.present?
cookies.permanent.encrypted[:signature_uuids] = build_new_cookie_signatures_json(@submitter, attachment)
end
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
rescue Submitters::MaliciousFileExtension => e
Rollbar.error(e) if defined?(Rollbar)
render json: { error: e.message }, status: :unprocessable_content
end
def can_upload?(submitter)
!submitter.declined_at? &&
!submitter.completed_at? &&
!submitter.submission.archived_at? &&
!submitter.submission.expired? &&
!submitter.submission.template&.archived_at?
end
def build_new_cookie_signatures_json(submitter, attachment)
values =
begin
JSON.parse(cookies.encrypted[:signature_uuids].presence || '{}')
rescue JSON::ParserError
{}
end
values[submitter.email] = attachment.uuid
values = values.to_a.last(COOKIE_STORE_LIMIT).to_h if values.size > COOKIE_STORE_LIMIT
values.to_json
end
end
end
@@ -22,7 +22,7 @@ module Api
render json: {
id: @submission.id,
documents: documents.map do |attachment|
{ name: attachment.filename.base, url: ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) }
{ name: FilenameUtils.basename(attachment.blob[:filename]), url: ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) }
end
}
end
@@ -1,15 +0,0 @@
# frozen_string_literal: true
module Api
class SubmitterEmailClicksController < ActionController::API
def create
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
if params[:t] == SubmissionEvents.build_tracking_param(@submitter, 'click_email')
SubmissionEvents.create_with_tracking_data(@submitter, 'click_email', request)
end
render json: {}
end
end
end
+20 -6
View File
@@ -36,7 +36,7 @@ module Api
render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true, params:)
end
# rubocop:disable Metrics/MethodLength
# rubocop:disable-next Metrics/MethodLength
def update
submission = @submitter.submission
role = submission.template_submitters.find { |e| e['uuid'] == @submitter.uuid }['name']
@@ -84,7 +84,6 @@ module Api
render json: { error: e.message }, status: :unprocessable_content
end
# rubocop:enable Metrics/MethodLength
def submitter_params
submitter_params = params.key?(:submitter) ? params.require(:submitter) : params
@@ -179,14 +178,29 @@ module Api
submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present?
submitters = submitters.where(slug: params[:slug]) if params[:slug].present?
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
if params[:template_id].present?
submitters = submitters.joins(:submission).where(submissions: { template_id: params[:template_id] })
end
submitters = filter_submitters_by_template_id(submitters, params) if params[:template_id].present?
maybe_filter_by_completed_at(submitters, params)
end
def filter_submitters_by_template_id(submitters, params)
return submitters.where(template_id: params[:template_id]) if params[:template_id].is_a?(Array)
template = Template.find_by(id: params[:template_id])
return submitters.none if template.nil? || !can?(:read, template)
if template.account_id == current_user.account_id &&
params[:completed_after].blank? && params[:completed_before].blank?
account_id_bind = Submitter.predicate_builder.build_bind_attribute('account_id', current_user.account_id)
submitters = submitters.unscope(where: :account_id)
.where((Submitter.arel_table[:account_id] + 0).eq(account_id_bind))
end
submitters.where(template_id: template.id)
end
def assign_external_id(submitter, attrs)
submitter.external_id = attrs[:application_key] if attrs.key?(:application_key)
submitter.external_id = attrs[:external_id] if attrs.key?(:external_id)
@@ -22,6 +22,7 @@ class ApplicationController < ActionController::Base
:true_ability,
:form_link_host,
:svg_icon,
:browser_os,
:turbo_native_ios?,
:turbo_native_android?
@@ -58,6 +59,10 @@ class ApplicationController < ActionController::Base
request.session[:impersonated_user_id] = user.uuid
end
def browser_os
@browser_os ||= DetectBrowserDevice.os(request.user_agent)
end
def pagy_auto(collection, **keyword_args)
if current_ability.can?(:manage, :countless)
pagy(:countless, collection, **keyword_args)
@@ -30,7 +30,7 @@ module Mcp
}
}.freeze
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
# rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength
def call
account = current_user.account
@@ -66,7 +66,9 @@ module Mcp
@template.save!
documents, = Templates::CreateAttachments.call(@template, { files: [file] }, extract_fields: true)
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
schema = documents.map do |doc|
{ attachment_uuid: doc.uuid, name: FilenameUtils.basename(doc.blob[:filename]) }
end
if @template.fields.blank?
@template.fields = Templates::ProcessDocument.normalize_attachment_fields(@template, documents)
@@ -87,6 +89,5 @@ module Mcp
edit_url: edit_template_url(@template)
)
end
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
end
end
@@ -66,7 +66,7 @@ module Mcp
}
}.freeze
# rubocop:disable Metrics
# rubocop:disable-next Metrics
def call
@template = Template.accessible_by(current_ability).find(mcp_params['template_id'])
@@ -115,6 +115,5 @@ module Mcp
rescue Submissions::CreateFromSubmitters::BaseError => e
render_tool_error(e.message)
end
# rubocop:enable Metrics
end
end
+1 -2
View File
@@ -1,9 +1,8 @@
# frozen_string_literal: true
class PasswordsController < Devise::PasswordsController
# rubocop:disable Rails/LexicallyScopedActionFilter
# rubocop:disable-next Rails/LexicallyScopedActionFilter
skip_before_action :require_no_authentication, only: %i[edit update]
# rubocop:enable Rails/LexicallyScopedActionFilter
around_action :with_browser_locale
@@ -53,7 +53,7 @@ class PreviewDocumentPageController < ActionController::API
File.open(file_path, File::RDWR | File::CREAT, 0o644) do |file|
file.flock(File::LOCK_EX)
# rubocop:disable Style/ZeroLengthPredicate
# rubocop:disable-next Style/ZeroLengthPredicate
if file.size.zero?
cleanup_stale_tempfiles
@@ -63,7 +63,6 @@ class PreviewDocumentPageController < ActionController::API
else
FileUtils.touch(file_path)
end
# rubocop:enable Style/ZeroLengthPredicate
file.flock(File::LOCK_UN)
@@ -14,7 +14,7 @@ class SendSubmissionEmailController < ApplicationController
template = Template.find_by!(slug: params[:template_slug])
@submitter =
Submitter.completed.where(submission: template.submissions).find_by(email: params[:email].to_s.downcase)
Submitter.completed.where(template_id: template.id).find_by(email: params[:email].to_s.downcase)
elsif params[:submission_slug]
submission = Submission.find_by!(slug: params[:submission_slug])
+20 -7
View File
@@ -34,7 +34,7 @@ class StartFormController < ApplicationController
@submitter = find_or_initialize_submitter(@template, submitter_params)
if @submitter.completed_at?
redirect_to start_form_completed_path(@template.slug, submitter_params.compact_blank)
redirect_to start_form_completed_path(@template.slug, submitter_params.compact_blank.merge(lang:))
else
if Templates.filter_undefined_submitters(@template.submitters).size > 1 && @submitter.new_record?
@error_message = multiple_submitters_error_message
@@ -59,7 +59,7 @@ class StartFormController < ApplicationController
Submitters::StartForm.assign_start_form_cookie(@submitter, request)
redirect_to submit_form_path(@submitter.slug)
redirect_to submit_form_path(@submitter.slug, lang:)
else
render :show, status: :unprocessable_content
end
@@ -80,7 +80,7 @@ class StartFormController < ApplicationController
raise ActionController::RoutingError, I18n.t('not_found') if required_params.any? { |_, v| v.blank? } ||
required_params.except('name').compact_blank.blank?
@submitter = Submitter.where(submission: @template.submissions)
@submitter = Submitter.where(template_id: @template.id)
.where.not(completed_at: nil)
.find_by!(required_params.except('name'))
end
@@ -92,8 +92,11 @@ class StartFormController < ApplicationController
blank_fields = required_fields.select { |key| submitter_params[key].blank? }
is_invalid_email = required_fields.include?('email') && submitter_params[:email].present? &&
!submitter_params[:email].match?(User::VALIDATE_EMAIL_REGEXP)
submitter =
if blank_fields.present?
if blank_fields.present? || is_invalid_email
Submitter.new(submitter_params)
else
Submitters::StartForm.find_or_initialize_submitter(
@@ -104,6 +107,12 @@ class StartFormController < ApplicationController
blank_fields.each { |key| submitter.errors.add(key.to_sym, :blank) }
if is_invalid_email
Rollbar.warning("Invalid start form email: #{submitter_params[:email]}") if defined?(Rollbar)
submitter.errors.add(:email, :invalid)
end
submitter
end
@@ -150,7 +159,7 @@ class StartFormController < ApplicationController
return render :show, status: :unprocessable_content if submitter.errors.present?
if Submitters::StartForm.verify_2fa_and_save_submitter(submitter, request, is_new_record: submitter.new_record?)
redirect_to submit_form_path(submitter.slug)
redirect_to submit_form_path(submitter.slug, lang:)
else
if defined?(Rollbar) && template.preferences['shared_link_2fa'] != true
Rollbar.info("2FA link requested: #{submitter.id}")
@@ -167,10 +176,14 @@ class StartFormController < ApplicationController
render :error, status: :unprocessable_content
rescue Submitters::UnableToSendCode, Submitters::InvalidOtp => e
redirect_to start_form_path(template.slug, params: submitter_params.merge(email_verification: true)),
redirect_to start_form_path(template.slug, lang:, params: submitter_params.merge(email_verification: true)),
alert: e.message
rescue RateLimit::LimitApproached
redirect_to start_form_path(template.slug, params: submitter_params.merge(email_verification: true)),
redirect_to start_form_path(template.slug, lang:, params: submitter_params.merge(email_verification: true)),
alert: I18n.t(:too_many_attempts)
end
def lang
params[:lang].presence
end
end
@@ -16,21 +16,21 @@ class StartFormEmail2faSendController < ApplicationController
.submitters.new(**submitter_params, account_id: @template.account_id)
if @submitter.email.blank?
return redirect_to start_form_path(@template.slug), alert: I18n.t(:provide_your_email_to_start)
return redirect_to start_form_path(@template.slug, lang:), alert: I18n.t(:provide_your_email_to_start)
end
Submitters.send_shared_link_email_verification_code(@submitter, request:)
redir_params = { notice: I18n.t(:code_has_been_resent) } if params[:resend]
redirect_to start_form_path(@template.slug, params: submitter_params.merge(email_verification: true)),
redirect_to start_form_path(@template.slug, lang:, params: submitter_params.merge(email_verification: true)),
**redir_params
rescue Submitters::BouncedEmail => e
@error_message = e.message
render 'start_form/error', status: :unprocessable_content
rescue Submitters::UnableToSendCode => e
redirect_to start_form_path(@template.slug, params: submitter_params.merge(email_verification: true)),
redirect_to start_form_path(@template.slug, lang:, params: submitter_params.merge(email_verification: true)),
alert: e.message
end
@@ -66,4 +66,8 @@ class StartFormEmail2faSendController < ApplicationController
attrs[:email] = Submissions.normalize_email(attrs[:email])
end
end
def lang
params[:lang].presence
end
end
@@ -12,6 +12,7 @@ class SubmissionEventsController < ApplicationController
'send_2fa_sms' => '2fa',
'send_2fa_email' => '2fa',
'send_sms' => 'send',
'fail_sms' => 'alert_circle',
'phone_verified' => 'phone_check',
'email_verified' => 'email_check',
'click_sms' => 'hand_click',
@@ -25,5 +25,7 @@ class SubmissionsExportController < ApplicationController
end
end
def new; end
def new
render layout: 'plain'
end
end
@@ -14,6 +14,6 @@ class SubmissionsFiltersController < ApplicationController
def show
return head :not_found unless ALLOWED_NAMES.include?(params[:name])
render params[:name]
render params[:name], layout: 'plain'
end
end
+16 -3
View File
@@ -8,6 +8,7 @@ class SubmitFormController < ApplicationController
skip_authorization_check
before_action :load_submitter, only: %i[show update completed]
before_action :maybe_render_not_found_for_search_bot, only: %i[show completed]
before_action :maybe_redirect_delegated, only: %i[show completed]
before_action :maybe_render_locked_page, only: :show
before_action :maybe_require_link_2fa, only: %i[show]
@@ -20,7 +21,7 @@ class SubmitFormController < ApplicationController
return render :email_2fa unless Submitters::AuthorizedForForm.pass_email_2fa?(@submitter, request)
if @submitter.completed_at? || submission.completed_at?
return redirect_to submit_form_completed_path(@submitter.slug)
return redirect_to submit_form_completed_path(@submitter.slug, lang:)
end
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
@@ -96,7 +97,7 @@ class SubmitFormController < ApplicationController
return if Submitters::AuthorizedForForm.call(@submitter, current_user, request)
redirect_to submit_form_path(params[:submit_form_slug])
redirect_to submit_form_path(params[:submit_form_slug], lang:)
end
def success; end
@@ -111,10 +112,18 @@ class SubmitFormController < ApplicationController
private
def maybe_render_not_found_for_search_bot
return unless DetectBrowserDevice.search_bot?(request.user_agent)
Rollbar.warning("Search bot: submitter #{@submitter&.id}") if defined?(Rollbar)
raise ActionController::RoutingError, I18n.t('not_found')
end
def maybe_require_link_2fa
return if Submitters::AuthorizedForForm.pass_link_2fa?(@submitter, current_user, request)
redirect_to start_form_path(@submitter.submission.template.slug)
redirect_to start_form_path(@submitter.submission.template.slug, lang:)
end
def maybe_render_locked_page
@@ -144,4 +153,8 @@ class SubmitFormController < ApplicationController
ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments)
.preload(:blob).index_by(&:uuid)
end
def lang
params[:lang].presence
end
end
@@ -20,17 +20,17 @@ class SubmitFormEmail2fasController < ApplicationController
{ value: @submitter.slug, expires: Submitters::StartForm::COOKIES_TTL.from_now,
**Submitters::StartForm::COOKIES_DEFAULTS }
redirect_to submit_form_path(@submitter.slug)
redirect_to submit_form_path(@submitter.slug, lang:)
else
redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:invalid_code)
redirect_to submit_form_path(@submitter.slug, status: :error, lang:), alert: I18n.t(:invalid_code)
end
rescue RateLimit::LimitApproached
redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:too_many_attempts)
redirect_to submit_form_path(@submitter.slug, status: :error, lang:), alert: I18n.t(:too_many_attempts)
end
def update
if @submitter.submission_events.where(event_type: 'send_2fa_email').exists?(created_at: 15.seconds.ago..)
return redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:rate_limit_exceeded)
return redirect_to submit_form_path(@submitter.slug, status: :error, lang:), alert: I18n.t(:rate_limit_exceeded)
end
RateLimit.call("send-email-code-#{@submitter.id}", limit: 2, ttl: 45.seconds, enabled: true)
@@ -38,19 +38,24 @@ class SubmitFormEmail2fasController < ApplicationController
if Docuseal.multitenant? && Submitters.email_bounced_recently?(@submitter.email)
Rollbar.warning("Bounced OTP email for submitter: #{@submitter.id}") if defined?(Rollbar)
return redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:verification_email_bounced)
return redirect_to submit_form_path(@submitter.slug, status: :error, lang:),
alert: I18n.t(:verification_email_bounced)
end
SendSubmitterVerificationEmailJob.perform_async('submitter_id' => @submitter.id, 'locale' => I18n.locale.to_s)
redir_params = params[:resend] ? { alert: I18n.t(:code_has_been_resent) } : {}
redirect_to submit_form_path(@submitter.slug, status: :sent), **redir_params
redirect_to submit_form_path(@submitter.slug, status: :sent, lang:), **redir_params
rescue RateLimit::LimitApproached
redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:too_many_attempts)
redirect_to submit_form_path(@submitter.slug, status: :error, lang:), alert: I18n.t(:too_many_attempts)
end
def load_submitter
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
end
def lang
params[:lang].presence
end
end
@@ -31,7 +31,7 @@ class TemplateDocumentsController < ApplicationController
documents, = Templates::CreateAttachments.call(@template, params, extract_fields: true)
schema = documents.map do |doc|
{ attachment_uuid: doc.uuid, name: doc.filename.base }
{ attachment_uuid: doc.uuid, name: FilenameUtils.basename(doc.blob[:filename]) }
end
render json: {
@@ -41,7 +41,9 @@ class TemplateFoldersController < ApplicationController
render_infinite_scroll(@pagy, @templates, @template_folders) if turbo_infinite_scroll?
end
def edit; end
def edit
render layout: 'plain'
end
def update
if @template_folder != current_account.default_template_folder &&
@@ -7,6 +7,8 @@ class TemplatesCloneController < ApplicationController
authorize!(:create, Template)
@template = Template.new(name: "#{@base_template.name} (#{I18n.t('clone')})")
render layout: 'plain'
end
def create
@@ -67,7 +67,7 @@ class TemplatesDebugController < ApplicationController
schema = documents.map do |doc|
{
attachment_uuid: doc.uuid,
name: doc.filename.base,
name: FilenameUtils.basename(doc.blob[:filename]),
dynamic: dynamic_documents.find { |e| e.uuid == doc.uuid }.present?
}
end
@@ -3,7 +3,9 @@
class TemplatesFoldersController < ApplicationController
load_and_authorize_resource :template
def edit; end
def edit
render layout: 'plain'
end
def update
authorize!(:update, @template)
@@ -17,7 +17,7 @@ class TemplatesUploadsController < ApplicationController
save_template!(@template, url_params)
documents, = Templates::CreateAttachments.call(@template, url_params || params, extract_fields: true)
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: FilenameUtils.basename(doc.blob[:filename]) } }
if @template.fields.blank?
@template.fields = Templates::ProcessDocument.normalize_attachment_fields(@template, documents)
+3 -1
View File
@@ -4,7 +4,9 @@ class UserInitialsController < ApplicationController
before_action :load_user_config
authorize_resource :user_config
def edit; end
def edit
render layout: 'plain'
end
def update
file = params[:file]
@@ -4,7 +4,9 @@ class UserSignaturesController < ApplicationController
before_action :load_user_config
authorize_resource :user_config
def edit; end
def edit
render layout: 'plain'
end
def update
file = params[:file]
+6 -2
View File
@@ -31,9 +31,13 @@ class UsersController < ApplicationController
end
end
def new; end
def new
render layout: turbo_native_app? ? 'plain' : true
end
def edit; end
def edit
render layout: turbo_native_app? ? 'plain' : true
end
def create
existing_user = User.accessible_by(current_ability).find_by(email: @user.email)
@@ -13,7 +13,9 @@ class WebhookSettingsController < ApplicationController
else
@webhook_events = @webhook_url.webhook_events
@webhook_events = @webhook_events.where(status: params[:status]) if %w[success error].include?(params[:status])
if %w[success error].include?(params[:status])
@webhook_events = @webhook_events.where(WebhookEvent.arel_table[:status].eq(params[:status]))
end
@pagy, @webhook_events = pagy(:countless, @webhook_events.order(id: :desc))
@@ -24,7 +26,9 @@ class WebhookSettingsController < ApplicationController
def show
@webhook_events = @webhook_url.webhook_events
@webhook_events = @webhook_events.where(status: params[:status]) if %w[success error].include?(params[:status])
if %w[success error].include?(params[:status])
@webhook_events = @webhook_events.where(WebhookEvent.arel_table[:status].eq(params[:status]))
end
@pagy, @webhook_events = pagy(:countless, @webhook_events.order(id: :desc))
end
@@ -66,7 +70,7 @@ class WebhookSettingsController < ApplicationController
SendTestWebhookRequestJob.perform_async(
'submitter_id' => submitter.id,
'event_uuid' => SecureRandom.uuid,
'event_uuid' => SecureRandom.uuid_v7,
'webhook_url_id' => @webhook_url.id
)
+1
View File
@@ -228,6 +228,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
template,
customFields: reactive(JSON.parse(this.dataset.customFields || '[]')),
dateFormats: JSON.parse(this.dataset.dateFormats || '[]'),
autosave: this.dataset.autosave !== 'false',
dynamicDocuments: reactive(JSON.parse(this.dataset.dynamicDocuments || '[]')),
backgroundColor: '#faf7f5',
locale: this.dataset.locale,
+1 -1
View File
@@ -55,7 +55,7 @@ export default targetable(class extends HTMLElement {
this.input.files = dataTransfer.files
if (this.input.webkitEntries.length) {
if (this.input.webkitEntries?.length) {
this.input.dataset.file = `${dataTransfer.files[0].name}`
}
+14 -2
View File
@@ -12,7 +12,14 @@ export default class extends HTMLElement {
toggle = (event) => {
if (event?.type === 'input' && event.target.matches(this.fieldSelector('name'))) return
const isNameOnly = (field) => this.isFilled('name') && !this.isFilled(field)
if (event?.type === 'focusin' && !this.isKeepSendUsed && this.isFocusFromName(event)) {
this.isKeepSendUsed = true
this.keepSendInput = event.target
} else if (event?.type === 'focusout' && event.target === this.keepSendInput) {
this.keepSendInput = null
}
const isNameOnly = (field) => this.isFilled('name') && !this.isFilled(field) && !this.keepSendInput?.matches(this.fieldSelector(field))
const isSend = (this.isChecked('send_email') && !isNameOnly('email')) ||
(this.isChecked('send_sms') && !isNameOnly('phone'))
@@ -29,12 +36,17 @@ export default class extends HTMLElement {
return [...this.form.querySelectorAll(this.fieldSelector(field))].some((input) => input.value.trim())
}
isFocusFromName ({ target, relatedTarget }) {
return !!relatedTarget?.matches(this.fieldSelector('name')) && !!relatedTarget.value.trim() &&
['email', 'phone'].some((field) => target.matches(this.fieldSelector(field)))
}
fieldSelector (field) {
return `[name$="[][${field}]"]`
}
get events () {
return ['change', 'input', 'linked-input.update']
return ['change', 'input', 'linked-input.update', 'focusin', 'focusout']
}
get form () {
+51 -4
View File
@@ -415,9 +415,9 @@
<a
:href="`/templates/${template.id}/form`"
:data-native-actions="template.submitters.length > 1 ? 'users' : null"
data-turbo="false"
data-turbo-prefetch="false"
class="flex items-center justify-center space-x-2"
@click.exact="isPreviewLoading = true"
@click.exact="onSaveAndPreviewClick"
>
<IconInnerShadowTop
v-if="isPreviewLoading"
@@ -640,6 +640,7 @@
:draw-field-type="drawFieldType"
:draw-custom-field="drawCustomField"
:draw-option="drawOption"
:autosave="autosave"
@update="onDynamicDocumentUpdate"
@draw="clearDrawField"
@add-custom-field="addCustomField"
@@ -771,7 +772,7 @@
class="sticky bottom-0 z-20"
>
<native-action
v-if="nativePlatform && withDocumentsList && (isBreakpointLg || isMobile) && sortedDocuments.length && editable"
v-if="nativePlatform && withDocumentsList && (isBreakpointLg || isMobile) && editable"
data-placement="drawer"
data-side="leading"
data-icon="files"
@@ -781,7 +782,7 @@
data-view="documents"
/>
<native-action
v-if="nativePlatform && withFieldsList && isBreakpointLg && sortedDocuments.length && editable"
v-if="nativePlatform && withFieldsList && isBreakpointLg && editable"
data-placement="drawer"
data-side="trailing"
data-icon="plus"
@@ -1379,6 +1380,7 @@ export default {
isPreviewLoading: false,
isLoadingBlankPage: false,
isSaving: false,
hasUnsavedChanges: false,
isDetectingPageFields: false,
detectFieldsQueue: [],
detectingAnalyzingProgress: null,
@@ -1662,6 +1664,11 @@ export default {
window.addEventListener('dragleave', this.onWindowDragLeave)
if (!this.autosave) {
window.addEventListener('beforeunload', this.onBeforeUnload)
document.addEventListener('turbo:before-visit', this.onBeforeVisit)
}
this.$nextTick(() => {
if (document.location.search?.includes('stripe_connect_success')) {
document.querySelector('form[action="/auth/stripe_connect"]')?.closest('.dropdown')?.querySelector('label')?.focus()
@@ -1685,6 +1692,11 @@ export default {
this.resizeObserver.disconnect()
window.removeEventListener('dragleave', this.onWindowDragLeave)
if (!this.autosave) {
window.removeEventListener('beforeunload', this.onBeforeUnload)
document.removeEventListener('turbo:before-visit', this.onBeforeVisit)
}
},
beforeUpdate () {
this.documentRefs = []
@@ -2241,6 +2253,37 @@ export default {
this.insertField(field)
}
},
onSaveAndPreviewClick (event) {
this.isPreviewLoading = true
if (!this.autosave) {
event.preventDefault()
const dynamicDocumentRefs = this.documentRefs.filter((ref) => ref.isDynamic)
dynamicDocumentRefs.forEach((ref) => ref.update())
this.rebuildVariablesSchema({ disable: false })
Promise.all([this.save({ force: true }), ...dynamicDocumentRefs.map((ref) => ref.saveBody())]).then(() => {
window.Turbo.visit(`/templates/${this.template.id}/form`)
})
}
},
onBeforeUnload (event) {
if (this.hasUnsavedChanges) {
event.preventDefault()
event.returnValue = ''
return ''
}
},
onBeforeVisit (event) {
if (this.hasUnsavedChanges && !window.confirm(this.t('close_without_saving_changes'))) {
event.preventDefault()
}
},
closeDropdown () {
document.activeElement.blur()
},
@@ -4036,6 +4079,8 @@ export default {
}
if (!this.autosave && !force) {
this.hasUnsavedChanges = true
return Promise.resolve({})
}
@@ -4058,6 +4103,8 @@ export default {
}),
headers: { 'Content-Type': 'application/json' }
}).then(() => {
this.hasUnsavedChanges = false
if (this.onSave) {
this.onSave(this.template)
}
@@ -7,7 +7,7 @@
class="absolute top-0 bottom-0 right-0 left-0"
@click.prevent="$emit('close')"
/>
<div :class="inline ? 'px-4 pt-3 pb-6' : 'modal-box pt-4 pb-6 px-6 mt-20 w-full max-w-xl'">
<div :class="inline ? (nativePlatform === 'android' ? 'pt-1.5 pb-6' : 'pt-3 pb-6') : 'modal-box pt-4 pb-6 px-6 mt-20 w-full max-w-xl'" :style="inline ? { paddingInline: '18px' } : null">
<div
v-if="!inline"
class="flex justify-between items-center border-b pb-2 mb-2 font-medium"
@@ -33,7 +33,7 @@
>{{ t('available_in_pro') }}</a>
</div>
<form @submit.prevent="validateSaveAndClose">
<div class="my-4">
<div :class="inline && nativePlatform === 'android' ? 'mt-1 mb-4' : 'my-4'">
<div
v-for="(condition, cindex) in conditions"
:key="cindex"
@@ -168,7 +168,7 @@
<script>
export default {
name: 'ConditionModal',
inject: ['t', 'template', 'withConditions'],
inject: ['t', 'template', 'withConditions', 'nativePlatform'],
props: {
item: {
type: Object,
@@ -7,7 +7,7 @@
class="absolute top-0 bottom-0 right-0 left-0"
@click.prevent="$emit('close')"
/>
<div :class="inline ? 'px-4 pt-3 pb-6' : 'modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl'">
<div :class="inline ? (nativePlatform === 'android' ? 'pt-1.5 pb-6' : 'pt-3 pb-6') : 'modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl'" :style="inline ? { paddingInline: '18px' } : null">
<div
v-if="!inline"
class="flex justify-between items-center border-b pb-2 mb-2 font-medium"
@@ -27,7 +27,7 @@
<div>
<label
dir="auto"
class="label text-sm"
:class="['label text-sm', { 'pt-0 mt-1': inline && nativePlatform === 'android' }]"
for="description_field"
>
{{ t('description') }}
@@ -71,7 +71,7 @@
<script>
export default {
name: 'DescriptionModal',
inject: ['t', 'template'],
inject: ['t', 'template', 'nativePlatform'],
props: {
field: {
type: Object,
@@ -36,7 +36,7 @@
<CropView
v-if="cropPageItem"
class="absolute inset-0 z-10 bg-base-100"
:style="inline ? { paddingTop: '70px' } : null"
:style="inline && nativePlatform === 'ios' ? { paddingTop: '70px' } : null"
:template-id="template.id"
:page="cropPageItem"
:image-url="thumbUrl(cropPageItem)"
@@ -47,7 +47,7 @@
<RedactView
v-else-if="redactPageItem"
class="absolute inset-0 z-10 bg-base-100"
:style="inline ? { paddingTop: '70px' } : null"
:style="inline && nativePlatform === 'ios' ? { paddingTop: '70px' } : null"
:template-id="template.id"
:page="redactPageItem"
:image-url="thumbUrl(redactPageItem)"
@@ -63,14 +63,14 @@
>
<div
class="flex-1 overflow-y-auto overflow-x-hidden px-4 md:px-6 py-4 space-y-4"
:style="inline ? { paddingTop: '86px' } : null"
:style="inline ? { paddingInline: '18px', ...(nativePlatform === 'ios' ? { paddingTop: '86px' } : {}) } : null"
>
<div
v-for="(doc, docIndex) in layout"
:key="doc.attachmentUuid"
:data-uuid="doc.attachmentUuid"
class="relative"
:style="inline ? { scrollMarginTop: '86px' } : null"
:style="inline && nativePlatform === 'ios' ? { scrollMarginTop: '86px' } : null"
>
<div
class="flex items-center pb-2"
@@ -191,7 +191,7 @@
:key="page.id"
:data-page-id="page.id"
class="relative min-w-0 cursor-pointer group"
:style="inline ? { scrollMarginTop: '86px' } : null"
:style="inline && nativePlatform === 'ios' ? { scrollMarginTop: '86px' } : null"
draggable="true"
@click="selectedPageId = page.id"
@mousedown="onPageMousedown(page, $event)"
@@ -260,7 +260,7 @@
</div>
<div
class="flex-none px-4 border-t pt-3 flex flex-col gap-3 md:w-56 md:border-t-0 md:border-l md:py-4 md:gap-0"
:class="{ 'pb-3': !inline }"
:class="{ 'pb-3': !inline || nativePlatform === 'android' }"
>
<div class="grid grid-cols-4 gap-2 md:block md:space-y-2">
<button
@@ -419,7 +419,7 @@ export default {
IconEraser,
IconCrop
},
inject: ['t', 'baseFetch'],
inject: ['t', 'baseFetch', 'nativePlatform'],
props: {
template: {
type: Object,
@@ -84,6 +84,11 @@ export default {
required: false,
default: true
},
autosave: {
type: Boolean,
required: false,
default: true
},
selectedSubmitter: {
type: Object,
required: false,
@@ -279,7 +284,7 @@ export default {
this.updatedSectionEditors.set(section.id, editor)
this.saveTimer = setTimeout(() => this.updateSectionsAndSave(), 1000)
this.saveTimer = setTimeout(() => this.autosave ? this.updateSectionsAndSave() : this.updateSections(), 1000)
},
loadListsStyle () {
this.listsStyle = this.bodyDom.querySelector('body > style[data-lists]')?.textContent || null
@@ -299,7 +304,9 @@ export default {
updateVariablesSchema () {
this.document.variables_schema = buildVariablesSchema(this.bodyDom.body)
},
updateSectionsAndSave () {
updateSections () {
delete this.saveTimer
this.updatedSectionEditors.forEach((editor, sectionId) => {
const target = this.bodyDom.getElementById(sectionId)
@@ -315,6 +322,9 @@ export default {
this.updateVariablesSchema()
this.$emit('update', this.document)
},
updateSectionsAndSave () {
this.updateSections()
return this.saveBody()
},
@@ -608,10 +608,10 @@ export default {
return !['stamp', 'heading', 'strikethrough'].includes(this.field.type)
},
showCondition () {
return this.withCondition && (this.withConditions || this.nativePlatform !== 'ios') && !['stamp', 'heading'].includes(this.field.type)
return this.withCondition && (this.withConditions || !this.nativePlatform) && !['stamp', 'heading'].includes(this.field.type)
},
showFormula () {
return (this.field.type === 'number' || this.field.type === 'date' || this.field.type === 'payment' || !!this.field.preferences?.formula) && (this.withFormula || this.nativePlatform !== 'ios')
return (this.field.type === 'number' || this.field.type === 'date' || this.field.type === 'payment' || !!this.field.preferences?.formula) && (this.withFormula || !this.nativePlatform)
},
showRequired () {
return this.withRequired && !['phone', 'stamp', 'verification', 'strikethrough', 'heading'].includes(this.field.type)
@@ -484,7 +484,7 @@
</label>
</li>
<li
v-if="withCondition && (withConditions || nativePlatform !== 'ios') && field.type != 'stamp' && field.type != 'heading'"
v-if="withCondition && (withConditions || !nativePlatform) && field.type != 'stamp' && field.type != 'heading'"
class="field-settings-condition"
>
<label
@@ -500,7 +500,7 @@
</label>
</li>
<li
v-if="(field.type == 'number' || field.type == 'date' || field.preferences?.formula) && (withFormula || nativePlatform !== 'ios')"
v-if="(field.type == 'number' || field.type == 'date' || field.preferences?.formula) && (withFormula || !nativePlatform)"
class="field-settings-formula"
>
<label
+1 -1
View File
@@ -275,7 +275,7 @@
</div>
</button>
<div
v-else-if="type == 'phone' && nativePlatform !== 'ios' && (fieldTypes.length === 0 || fieldTypes.includes(type))"
v-else-if="type == 'phone' && !nativePlatform && (fieldTypes.length === 0 || fieldTypes.includes(type))"
class="tooltip tooltip-bottom flex"
:class="{'tooltip-bottom-end': withPayment, 'tooltip-bottom': !withPayment }"
:data-tip="t('unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification')"
@@ -7,7 +7,7 @@
class="absolute top-0 bottom-0 right-0 left-0"
@click.prevent="$emit('close')"
/>
<div :class="inline ? 'px-4 pt-3 pb-6' : 'modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl'">
<div :class="inline ? (nativePlatform === 'android' ? 'pt-1.5 pb-6' : 'pt-3 pb-6') : 'modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl'" :style="inline ? { paddingInline: '18px' } : null">
<div
v-if="!inline"
class="flex justify-between items-center border-b pb-2 mb-2 font-medium"
@@ -21,7 +21,7 @@
@click.prevent="$emit('close')"
>&times;</a>
</div>
<div class="mt-4">
<div :class="inline && nativePlatform === 'android' ? 'mt-1' : 'mt-4'">
<div>
<div class="flex flex-wrap items-center gap-1.5">
<span>
@@ -200,7 +200,7 @@ export default {
components: {
IconChevronDown
},
inject: ['t', 'template'],
inject: ['t', 'template', 'nativePlatform'],
props: {
field: {
type: Object,
@@ -7,7 +7,7 @@
class="absolute top-0 bottom-0 right-0 left-0"
@click.prevent="$emit('close')"
/>
<div :class="inline ? 'px-4 pt-3 pb-6' : 'modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl'">
<div :class="inline ? (nativePlatform === 'android' ? 'pt-1.5 pb-6' : 'pt-3 pb-6') : 'modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl'" :style="inline ? { paddingInline: '18px' } : null">
<div
v-if="!inline"
class="flex justify-between items-center border-b pb-2 mb-2 font-medium"
@@ -32,7 +32,7 @@
class="link"
>{{ t('available_in_pro') }}</a>
</div>
<div class="flex flex-wrap mb-2 gap-y-1 pt-1">
<div :class="['flex flex-wrap mb-2 gap-y-1', inline && nativePlatform === 'android' ? 'mt-1' : 'pt-1']">
<button
v-for="f in fields"
:key="f.uuid"
@@ -152,7 +152,7 @@ export default {
IconCodePlus,
IconMathFunction
},
inject: ['t', 'template', 'withFormula'],
inject: ['t', 'template', 'withFormula', 'nativePlatform'],
props: {
field: {
type: Object,
+7
View File
@@ -70,6 +70,7 @@ const en = {
remove_condition: 'Remove condition',
add_condition: 'Add condition',
are_you_sure_: 'Are you sure?',
close_without_saving_changes: 'Close without saving changes?',
sign_yourself: 'Sign Yourself',
set_signing_date: 'Set signing date',
signing_date: 'Signing Date',
@@ -371,6 +372,7 @@ const es = {
down: 'Abajo',
set_signing_date: 'Establecer fecha de firma',
are_you_sure_: '¿Estás seguro?',
close_without_saving_changes: '¿Cerrar sin guardar los cambios?',
sign_yourself: 'Firma tú mismo',
checked: 'Marcado',
current_date: 'Fecha actual',
@@ -652,6 +654,7 @@ const it = {
remove_condition: 'Rimuovi condizione',
add_condition: 'Aggiungi condizione',
are_you_sure_: 'Sei sicuro?',
close_without_saving_changes: 'Chiudere senza salvare le modifiche?',
sign_yourself: 'Firma te stesso',
set_signing_date: 'Imposta data di firma',
signing_date: 'Data di firma',
@@ -953,6 +956,7 @@ const pt = {
down: 'Para baixo',
set_signing_date: 'Definir data de assinatura',
are_you_sure_: 'Tem certeza?',
close_without_saving_changes: 'Fechar sem salvar as alterações?',
sign_yourself: 'Assine você mesmo',
checked: 'Marcado',
current_date: 'Data atual',
@@ -1234,6 +1238,7 @@ const fr = {
remove_condition: 'Supprimer la condition',
add_condition: 'Ajouter une condition',
are_you_sure_: 'Êtes-vous sûr ?',
close_without_saving_changes: 'Fermer sans enregistrer les modifications ?',
sign_yourself: 'Signer vous-même',
set_signing_date: 'Définir la date de signature',
signing_date: 'Date de signature',
@@ -1525,6 +1530,7 @@ const de = {
remove_condition: 'Bedingung entfernen',
add_condition: 'Bedingung hinzufügen',
are_you_sure_: 'Sind Sie sicher?',
close_without_saving_changes: 'Ohne Speichern der Änderungen schließen?',
sign_yourself: 'Selbst unterschreiben',
set_signing_date: 'Unterzeichnungsdatum festlegen',
signing_date: 'Unterzeichnungsdatum',
@@ -1816,6 +1822,7 @@ const nl = {
remove_condition: 'Voorwaarde verwijderen',
add_condition: 'Voorwaarde toevoegen',
are_you_sure_: 'Weet u het zeker?',
close_without_saving_changes: 'Sluiten zonder wijzigingen op te slaan?',
sign_yourself: 'Zelf ondertekenen',
set_signing_date: 'Ondertekeningsdatum instellen',
signing_date: 'Ondertekeningsdatum',
@@ -203,7 +203,7 @@
>{{ t('learn_more') }}</a>
</div>
<li
v-if="withFormula || nativePlatform !== 'ios'"
v-if="withFormula || !nativePlatform"
class="field-settings-formula mb-1"
>
<label
@@ -233,7 +233,7 @@
</label>
</li>
<li
v-if="withCondition && (withConditions || nativePlatform !== 'ios')"
v-if="withCondition && (withConditions || !nativePlatform)"
class="field-settings-condition mt-1"
>
<label
@@ -5,7 +5,7 @@
class="absolute top-0 bottom-0 right-0 left-0"
@click.prevent="$emit('close')"
/>
<div :class="inline ? 'px-4 pt-3 pb-6' : 'modal-box pt-4 pb-6 mt-20 w-full'">
<div :class="inline ? (nativePlatform === 'android' ? 'pt-1.5 pb-6' : 'pt-3 pb-6') : 'modal-box pt-4 pb-6 mt-20 w-full'" :style="inline ? { paddingInline: '18px' } : null">
<div
v-if="!inline"
class="flex justify-between items-center border-b pb-2 mb-3 font-medium"
@@ -63,7 +63,7 @@ import { IconUser, IconInnerShadowTop } from '@tabler/icons-vue'
export default {
name: 'RevisionsModal',
components: { IconUser, IconInnerShadowTop },
inject: ['t', 'baseFetch'],
inject: ['t', 'baseFetch', 'nativePlatform'],
props: {
template: {
type: Object,
@@ -259,7 +259,7 @@ export default {
return true
},
showCondition () {
return this.withCondition && (this.withConditions || this.nativePlatform !== 'ios')
return this.withCondition && (this.withConditions || !this.nativePlatform)
},
fieldNames: FieldType.computed.fieldNames,
fieldLabels: FieldType.computed.fieldLabels
+2 -2
View File
@@ -100,7 +100,7 @@ class ProcessSubmitterCompletionJob
WebhookUrls.for_account_id(submitter.account_id, %w[form.completed submission.completed]).each do |webhook|
if webhook.events.include?('form.completed')
event_uuids['form.completed'] ||= SecureRandom.uuid
event_uuids['form.completed'] ||= SecureRandom.uuid_v7
SendFormCompletedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
'event_uuid' => event_uuids['form.completed'],
@@ -109,7 +109,7 @@ class ProcessSubmitterCompletionJob
next unless webhook.events.include?('submission.completed') && is_last
event_uuids['submission.completed'] ||= SecureRandom.uuid
event_uuids['submission.completed'] ||= SecureRandom.uuid_v7
SendSubmissionCompletedWebhookRequestJob.perform_async('submission_id' => submitter.submission_id,
'event_uuid' => event_uuids['submission.completed'],
+8 -6
View File
@@ -30,14 +30,16 @@ class SendTestWebhookRequestJob
raise LocalhostError, "Can't send to localhost." if uri.host.in?(SendWebhookRequest::LOCALHOSTS)
end
Faraday.post(webhook_url.url,
{
event_type: 'form.completed',
timestamp: Time.current.iso8601,
data: Submitters::SerializeForWebhook.call(submitter)
}.to_json,
body_json = {
event_type: 'form.completed',
timestamp: Time.current.iso8601,
data: Submitters::SerializeForWebhook.call(submitter)
}.to_json
Faraday.post(webhook_url.url, body_json,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT,
'X-Docuseal-Signature' => WebhookUrls::Signatures.sign(webhook_url.hmac_secret, body: body_json),
**webhook_url.secret.to_h)
end
end
+2 -1
View File
@@ -15,7 +15,8 @@
#
# Indexes
#
# index_accounts_on_uuid (uuid) UNIQUE
# index_accounts_on_created_at (created_at)
# index_accounts_on_uuid (uuid) UNIQUE
#
class Account < ApplicationRecord
attribute :uuid, :string, default: -> { SecureRandom.uuid }
+1
View File
@@ -61,6 +61,7 @@ class AccountConfig < ApplicationRecord
DOCUMENT_FILENAME_FORMAT_KEY = 'document_filename_format'
TEMPLATE_CUSTOM_FIELDS_KEY = 'template_custom_fields'
TEMPLATE_DATE_FORMATS_KEY = 'template_date_formats'
TEMPLATE_BUILDER_AUTOSAVE_KEY = 'template_builder_autosave'
POLICY_LINKS_KEY = 'policy_links'
ENABLE_MCP_KEY = 'enable_mcp'
+1
View File
@@ -45,6 +45,7 @@ class SubmissionEvent < ApplicationRecord
send_email: 'send_email',
bounce_email: 'bounce_email',
complaint_email: 'complaint_email',
fail_sms: 'fail_sms',
send_reminder_email: 'send_reminder_email',
send_sms: 'send_sms',
send_2fa_sms: 'send_2fa_sms',
+9 -1
View File
@@ -25,15 +25,17 @@
# account_id :bigint not null
# external_id :string
# submission_id :bigint not null
# template_id :bigint
#
# Indexes
#
# index_submitters_on_account_id_and_completed_at (account_id,completed_at) WHERE (completed_at IS NOT NULL)
# index_submitters_on_account_id_and_id (account_id,id)
# index_submitters_on_email (email)
# index_submitters_on_email_and_template_id (email,template_id) WHERE (email IS NOT NULL)
# index_submitters_on_external_id (external_id)
# index_submitters_on_slug (slug) UNIQUE
# index_submitters_on_submission_id (submission_id)
# index_submitters_on_template_id_and_id (template_id,id)
#
# Foreign Keys
#
@@ -68,6 +70,8 @@ class Submitter < ApplicationRecord
scope :completed, -> { where.not(completed_at: nil) }
before_validation :set_template_id, on: :create
after_destroy :anonymize_email_events, if: -> { Docuseal.multitenant? }
def status
@@ -124,6 +128,10 @@ class Submitter < ApplicationRecord
private
def set_template_id
self.template_id = submission&.template_id
end
def anonymize_email_events
email_events.each do |event|
event.update!(email: Digest::MD5.base64digest(event.email))
+3 -1
View File
@@ -56,6 +56,8 @@ class User < ApplicationRecord
FULL_EMAIL_REGEXP =
/\A[a-z0-9][.']?(?:(?:[a-z0-9_-]+[.+'])*[a-z0-9_-]+)*@(?:[a-z0-9]+[.-])*[a-z0-9]+\.[a-z]{2,}\z/i
VALIDATE_EMAIL_REGEXP = /\A[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\z/
has_one_attached :signature
has_one_attached :initials
@@ -78,7 +80,7 @@ class User < ApplicationRecord
scope :archived, -> { where.not(archived_at: nil) }
scope :admins, -> { where(role: ADMIN_ROLE) }
validates :email, format: { with: /\A[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\z/ }
validates :email, format: { with: VALIDATE_EMAIL_REGEXP }
def access_token
super || build_access_token.tap(&:save!)
+1 -1
View File
@@ -22,7 +22,7 @@
# index_webhook_events_on_webhook_url_id_and_id (webhook_url_id,id)
#
class WebhookEvent < ApplicationRecord
attribute :uuid, :string, default: -> { SecureRandom.uuid }
attribute :uuid, :string, default: -> { SecureRandom.uuid_v7 }
belongs_to :webhook_url, optional: true
belongs_to :account, optional: true
+2 -2
View File
@@ -133,7 +133,7 @@
<% end %>
<% end %>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::ALLOW_TO_DECLINE_KEY) %>
<% if can?(:manage, account_config) && (!turbo_native_ios? || !Docuseal.multitenant? || can?(:manage, :disable_decline)) %>
<% if can?(:manage, account_config) && (!turbo_native_app? || !Docuseal.multitenant? || can?(:manage, :disable_decline)) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %>
<div class="flex items-center justify-between gap-4 py-2.5">
@@ -156,7 +156,7 @@
<% end %>
<% end %>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::ALLOW_TO_DELEGATE_KEY) %>
<% if can?(:manage, account_config) && (!turbo_native_ios? || !Docuseal.multitenant? || can?(:manage, :delegate_form)) %>
<% if can?(:manage, account_config) && (!turbo_native_app? || !Docuseal.multitenant? || can?(:manage, :delegate_form)) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %>
<div class="flex items-center justify-between gap-4 py-2.5">
@@ -1,4 +1,4 @@
<% unless turbo_native_ios? %>
<% unless turbo_native_app? %>
<tr class="group">
<td>
<div class="flex items-center space-x-1">
+17
View File
@@ -7,4 +7,21 @@
-webkit-user-drag: none;
-webkit-touch-callout: none;
}
<% if turbo_native_android? %>
input[type="date"] {
appearance: none;
-webkit-appearance: none;
}
input[type="date"]::-webkit-calendar-picker-indicator {
display: none;
}
@media (max-width: 767px) {
div:has(> #account_settings_menu) + * {
margin-top: 0 !important;
}
}
<% end %>
</style>
+4 -6
View File
@@ -28,15 +28,13 @@
<open-modal src="<%= url_for(**url_params, params: modal_uri.query_values) %>"></open-modal>
<% end %>
<% end %>
<% unless content_for?(:native_modal) %>
<turbo-frame id="modal"></turbo-frame>
<% end %>
<turbo-frame id="modal"></turbo-frame>
<turbo-frame id="drawer"></turbo-frame>
<%= render turbo_native_app? ? 'shared/native_navbar' : 'shared/navbar' %>
<% if flash.present? %><%= render 'shared/flash' %><% end %>
<div class="max-w-6xl mx-auto px-4 md:px-2 mb-8">
<main class="max-w-6xl mx-auto px-4 md:px-2 mb-8">
<%= yield %>
</div>
</main>
<% if flash.present? %><%= render 'shared/flash' %><% end %>
<%= render 'shared/body_scripts' %>
</body>
</html>
+3 -1
View File
@@ -13,6 +13,8 @@
<%= stylesheet_pack_tag 'form', media: 'all', data: { turbo_track: 'dynamic' } %>
</head>
<body>
<%= yield %>
<main>
<%= yield %>
</main>
</body>
</html>
+4 -2
View File
@@ -13,8 +13,10 @@
<%= stylesheet_pack_tag 'application', media: 'all', data: { turbo_track: 'dynamic' } %>
</head>
<body>
<% if flash.present? %><%= render 'shared/flash' %><% end %>
<%= yield %>
<main>
<%= yield %>
</main>
<turbo-frame id="modal"></turbo-frame>
<% if flash.present? %><%= render 'shared/flash' %><% end %>
</body>
</html>
@@ -22,7 +22,7 @@
<% end %>
</div>
<%= render 'bcc_form', config: @bcc_config %>
<% unless can?(:manage, :personalization_restricted) || (turbo_native_ios? && !can?(:manage, :email_reminders)) %>
<% unless can?(:manage, :personalization_restricted) || (turbo_native_app? && !can?(:manage, :email_reminders)) %>
<div class="flex justify-between items-end mb-4 mt-8">
<h2 class="text-2xl md:text-3xl font-bold">
<%= t('sign_request_email_reminders') %>
@@ -10,7 +10,7 @@
<%= render 'documents_copy_email_form' %>
<%= render 'submitter_completed_email_form' %>
</div>
<% if !turbo_native_ios? || can?(:manage, :custom_logo) %>
<% if !turbo_native_app? || can?(:manage, :custom_logo) %>
<p class="text-3xl md:text-4xl font-bold mb-4 mt-8">
<%= t('company_logo') %>
</p>
@@ -0,0 +1,13 @@
<div id="mobile_app_alert" class="px-4 md:px-2 py-2 text-center bg-yellow-300 text-base-content w-full flex items-center justify-center gap-2">
<span>
<%= t('get_the_docuseal_app_for_a_better_mobile_experience') %>
<%= link_to t('install_app'), browser_os == 'ios' ? Docuseal::APP_STORE_URL : Docuseal::GOOGLE_PLAY_URL, class: 'link font-medium', target: '_blank', rel: 'noopener noreferrer nofollow' %>
</span>
<toggle-cookies data-key="mobile_app_alert" data-value="hidden" class="flex">
<remove-on-event data-selector-id="mobile_app_alert" class="flex">
<button type="button" class="btn btn-ghost btn-circle btn-sm" aria-label="<%= t('close') %>">
<%= svg_icon('x', class: 'w-5 h-5') %>
</button>
</remove-on-event>
</toggle-cookies>
</div>
+8 -6
View File
@@ -1,11 +1,13 @@
<script type="application/json" id="native_path_configuration"><%= render partial: 'layouts/native_path_configuration', formats: :json %></script>
<% if signed_in? %>
<native-action data-placement="profile" data-icon="user">
<%= link_to t('profile'), settings_profile_index_path %>
</native-action>
<native-action data-placement="profile" data-icon="settings">
<%= link_to t('settings'), settings_path %>
</native-action>
<% unless turbo_native_android? %>
<native-action data-placement="profile" data-icon="user">
<%= link_to t('profile'), settings_profile_index_path %>
</native-action>
<native-action data-placement="profile" data-icon="settings">
<%= link_to t('settings'), settings_path %>
</native-action>
<% end %>
<% if current_account.testing? && (can?(:manage, EncryptedConfig) || current_user != true_user) %>
<native-action data-placement="test_mode" data-icon="code" data-label="<%= t('exit_test_mode') %>">
<%= button_to '', testing_account_path, method: :delete, form: { data: { turbo_action: 'replace' } } %>
+3 -2
View File
@@ -1,5 +1,6 @@
<%= render 'shared/navbar_warning' %>
<div class="max-w-6xl mb-4 mx-auto px-4 md:px-2 py-3 flex items-center justify-between">
<%= render 'shared/mobile_app_alert' if signed_in? && cookies[:mobile_app_alert].blank? && (browser_os == 'ios' || browser_os == 'android') %>
<nav class="max-w-6xl mb-4 mx-auto px-4 md:px-2 py-3 flex items-center justify-between">
<div class="flex items-center space-x-4">
<a href="<%= root_path %>" class="text-2xl font-bold items-center flex space-x-2">
<%= render 'shared/title' %>
@@ -106,4 +107,4 @@
<% end %>
</div>
<% end %>
</div>
</nav>
+3 -3
View File
@@ -43,7 +43,7 @@
<%= link_to t('storage'), settings_storage_index_path, class: 'text-base hover:bg-base-300', data: nav_link_data %>
</li>
<% end %>
<% if can?(:read, EncryptedConfig.new(key: 'submitter_invitation_sms', account: current_account)) && true_user == current_user && (!turbo_native_ios? || can?(:manage, :sms_verification)) %>
<% if can?(:read, EncryptedConfig.new(key: 'submitter_invitation_sms', account: current_account)) && true_user == current_user && (!turbo_native_app? || can?(:manage, :sms_verification)) %>
<li>
<%= link_to 'SMS', settings_sms_path, class: 'text-base hover:bg-base-300', data: nav_link_data %>
</li>
@@ -84,7 +84,7 @@
</li>
<% end %>
<% end %>
<% if !Docuseal.demo? && !turbo_native_ios? && can?(:manage, EncryptedConfig) && (current_user != true_user || !current_account.linked_account_account) %>
<% if !Docuseal.demo? && !turbo_native_app? && can?(:manage, EncryptedConfig) && (current_user != true_user || !current_account.linked_account_account) %>
<li>
<%= content_for(:pro_link) || link_to(Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}", class: 'text-base hover:bg-base-300', data: { turbo: false }) do %>
<%= t('plans') %>
@@ -107,7 +107,7 @@
<% end %>
</li>
<% end %>
<% if (!Docuseal.multitenant? || can?(:manage, :saml_sso)) && can?(:read, EncryptedConfig.new(key: 'saml_configs', account: current_account)) && true_user == current_user && (!turbo_native_ios? || can?(:manage, :saml_sso)) %>
<% if (!Docuseal.multitenant? || can?(:manage, :saml_sso)) && can?(:read, EncryptedConfig.new(key: 'saml_configs', account: current_account)) && true_user == current_user && (!turbo_native_app? || can?(:manage, :saml_sso)) %>
<li>
<%= link_to 'SSO', settings_sso_index_path, class: 'text-base hover:bg-base-300', data: nav_link_data %>
</li>
@@ -1,7 +1,9 @@
<form action="<%= url_for %>" method="get" class="dropdown dropdown-top hidden md:inline">
<label tabindex="0" class="btn btn-sm h-10">
<%= svg_icon('arrow_sort', class: 'w-5 h-5') %>
</label>
<% unless turbo_native_app? %>
<label tabindex="0" class="btn btn-sm h-10">
<%= svg_icon('arrow_sort', class: 'w-5 h-5') %>
</label>
<% end %>
<ul tabindex="0" class="dropdown-content z-[10] menu p-2 shadow bg-base-100 rounded-box mb-1 min-w-48">
<native-action class="contents" data-menu="<%= t('sort_by') %>" data-menu-icon="arrow_sort" data-icon="sort_descending_numbers" data-selected="<%= selected_order == 'created_at' %>">
<toggle-cookies data-value="created_at" data-key="dashboard_templates_order">
+2 -2
View File
@@ -1,7 +1,7 @@
<% if turbo_native_app? && !turbo_frame_request? %>
<% if turbo_native_app? %>
<% content_for(:native_title, local_assigns[:title]) if local_assigns[:title] %>
<turbo-frame id="drawer">
<native-modal class="block w-full" data-close-after-submit="<%= local_assigns.key?(:close_after_submit) ? local_assigns[:close_after_submit] : true %>">
<native-modal class="block max-w-lg mx-auto <%= turbo_native_android? ? 'pb-4' : 'pb-6' %>" data-close-after-submit="<%= local_assigns.key?(:close_after_submit) ? local_assigns[:close_after_submit] : true %>">
<%= yield %>
</native-modal>
</turbo-frame>
+2 -3
View File
@@ -1,8 +1,7 @@
<% if turbo_native_ios? || (turbo_native_app? && !turbo_frame_request?) %>
<% content_for(:native_modal, 'true') %>
<% if turbo_native_app? %>
<% content_for(:native_title, local_assigns[:title]) if local_assigns[:title] %>
<turbo-frame id="modal">
<native-modal class="block max-w-lg mx-auto pb-6" data-close-after-submit="<%= local_assigns.key?(:close_after_submit) ? local_assigns[:close_after_submit] : true %>">
<native-modal class="block max-w-lg mx-auto <%= turbo_native_android? ? 'pb-4' : 'pb-6' %>" data-close-after-submit="<%= local_assigns.key?(:close_after_submit) ? local_assigns[:close_after_submit] : true %>">
<%= yield %>
</native-modal>
</turbo-frame>
+8 -6
View File
@@ -1,4 +1,11 @@
<% if turbo_frame_request? %>
<% if turbo_native_app? %>
<% content_for(:native_title, local_assigns[:title]) if local_assigns[:title] %>
<turbo-frame id="modal">
<native-modal class="block max-w-lg mx-auto <%= turbo_native_android? ? 'pb-4' : 'pb-6' %>" data-close-after-submit="<%= local_assigns.key?(:close_after_submit) ? local_assigns[:close_after_submit] : true %>">
<%= yield %>
</native-modal>
</turbo-frame>
<% elsif turbo_frame_request? %>
<turbo-frame id="modal">
<turbo-modal class="modal modal-open items-start !animate-none overflow-y-auto !justify-normal md:!justify-center" data-close-after-submit="<%= local_assigns.key?(:close_after_submit) ? local_assigns[:close_after_submit] : true %>">
<div class="absolute top-0 bottom-0 right-0 left-0" data-action="click:turbo-modal#close"></div>
@@ -17,11 +24,6 @@
</div>
</turbo-modal>
</turbo-frame>
<% elsif turbo_native_app? %>
<% content_for(:native_title, local_assigns[:title]) if local_assigns[:title] %>
<native-modal class="block w-full [&>.px-5]:px-4" data-close-after-submit="<%= local_assigns.key?(:close_after_submit) ? local_assigns[:close_after_submit] : true %>">
<%= yield %>
</native-modal>
<% else %>
<% if local_assigns[:title] %>
<div class="sticky top-0 z-10 flex justify-between bg-base-100 py-2 px-5 items-center border-b pb-2 font-medium">
+2 -2
View File
@@ -1,4 +1,4 @@
<main class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<div class="flex items-center justify-center">
@@ -32,5 +32,5 @@
</toggle-submit>
<% end %>
</div>
</main>
</div>
<%= render 'shared/attribution', link_path: '/start', account: @submitter.account %>
@@ -2,7 +2,7 @@
<% I18n.with_locale(@template.account.locale) do %>
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @template.account.name)) %>
<% end %>
<main class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<div class="flex items-center justify-center">
@@ -23,7 +23,7 @@
<div>
<%= t('we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue') %>
</div>
<%= form_for '', url: @resubmit_submitter ? resubmit_form_path : start_form_path(@template.slug), method: :put, html: { class: 'space-y-4', id: 'code_form' } do |f| %>
<%= form_for '', url: @resubmit_submitter ? resubmit_form_path : start_form_path(@template.slug, lang: params[:lang].presence), method: :put, html: { class: 'space-y-4', id: 'code_form' } do |f| %>
<div dir="auto" class="form-control !mt-0">
<%= f.hidden_field 'resubmit', value: params[:resubmit] %>
<%= f.hidden_field 'submitter[name]', value: params[:name] || params.dig(:submitter, :name) %>
@@ -52,7 +52,7 @@
<% if @resubmit_submitter %>
<%= button_to t(:re_send_email), resubmit_form_path, params: { resubmit: @resubmit_submitter.slug }, method: :put, id: 'resend_code', class: 'hidden' %>
<% else %>
<%= button_to t(:re_send_email), start_form_email_2fa_send_index_path, params: { slug: @template.slug, resend: true, submitter: { name: params[:name] || params.dig(:submitter, :name), email: params[:email] || params.dig(:submitter, :email), phone: params[:phone] || params.dig(:submitter, :phone) } }.compact, method: :post, id: 'resend_code', class: 'hidden' %>
<%= button_to t(:re_send_email), start_form_email_2fa_send_index_path, params: { slug: @template.slug, resend: true, submitter: { name: params[:name] || params.dig(:submitter, :name), email: params[:email] || params.dig(:submitter, :email), phone: params[:phone] || params.dig(:submitter, :phone) }, lang: params[:lang] }.compact, method: :post, id: 'resend_code', class: 'hidden' %>
<% end %>
</div>
</main>
</div>
+2 -2
View File
@@ -2,7 +2,7 @@
<% I18n.with_locale(@template.account.locale) do %>
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @template.account.name)) %>
<% end %>
<main class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<div class="flex items-center justify-center">
@@ -28,4 +28,4 @@
<%= link_to t('back'), @resubmit_submitter ? submit_form_path(@resubmit_submitter.slug) : start_form_path(@template.slug, @submitter.slice(:name, :email, :phone).compact_blank), class: 'base-button' %>
</div>
</div>
</main>
</div>
+2 -2
View File
@@ -2,7 +2,7 @@
<% I18n.with_locale(@template.account.locale) do %>
<% content_for(:html_description, t('share_link_is_currently_disabled')) %>
<% end %>
<main class="max-w-md space-y-6 mx-auto px-2 mt-12 mb-4">
<div class="max-w-md space-y-6 mx-auto px-2 mt-12 mb-4">
<div class="text-center w-full space-y-6">
<%= render 'start_form/banner' %>
<p class="text-xl font-semibold text-center">
@@ -27,5 +27,5 @@
<%= button_to button_title(title: t('enable_shared_link'), icon: svg_icon('lock_open', class: 'w-6 h-6')), template_share_link_path(@template), params: { template: { shared_link: true }, redir: start_form_path(slug: @template.slug) }, method: :post, class: 'white-button w-full' %>
</toggle-submit>
<% end %>
</main>
</div>
<%= render 'shared/attribution', link_path: '/start', account: @template.account %>
+3 -3
View File
@@ -2,7 +2,7 @@
<% I18n.with_locale(@template.account.locale) do %>
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @template.account.name)) %>
<% end %>
<main class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<div class="text-center w-full space-y-6">
@@ -28,7 +28,7 @@
</div>
</div>
<% if !@template.archived_at? && !@template.account.archived_at? %>
<%= form_for @submitter, url: @resubmit_submitter ? resubmit_form_path : start_form_path(@template.slug), data: { turbo_frame: :_top }, method: :put, html: { class: 'space-y-4' } do |f| %>
<%= form_for @submitter, url: @resubmit_submitter ? resubmit_form_path : start_form_path(@template.slug, lang: params[:lang].presence), data: { turbo_frame: :_top }, method: :put, html: { class: 'space-y-4' } do |f| %>
<%= hidden_field_tag :resubmit, @resubmit_submitter.slug if @resubmit_submitter %>
<% if @error_message %>
<div class="alert items-start bg-base-100 border border-red-400 text-red-400">
@@ -66,4 +66,4 @@
<%= render 'shared/attribution', link_path: '/start', account: @template.account %>
<%= render 'start_form/policy', account: @template.account %>
</div>
</main>
</div>
+6 -2
View File
@@ -43,14 +43,18 @@
</div>
<p class="text-base-content/80 mt-1">
<% if event.event_type == 'complete_verification' %>
<%= t('submission_event_names.complete_verification_by_html', provider: event.data['method'], submitter_name:) %>
<% identity_name = [event.data['firstname'], event.data['lastname']].compact_blank.join(' ') %>
<%= t('submission_event_names.complete_verification_by_html', provider: event.data['method'], submitter_name: identity_name.presence || submitter_name) %>
<% elsif event.event_type == 'invite_party' && (invited_submitter = @submission.submitters.find { |e| e.uuid == event.data['uuid'] }) && (name = @submission.template_submitters.find { |e| e['uuid'] == event.data['uuid'] }&.dig('name')) %>
<%= t('submission_event_names.invite_party_by_html', invited_submitter_name: [invited_submitter.name || invited_submitter.email || invited_submitter.phone, name].join(' '), submitter_name:) %>
<% elsif event.event_type == 'delegate_form' %>
<% delegate_from = event.data['old_email'].presence || versions.rfind { |v| v.created_at <= event.event_timestamp }&.then { |v| v.name || v.phone } %>
<%= t('submission_event_names.delegate_form_by_html', from: delegate_from, to: event.data['email']) %>
<% elsif event.event_type.include?('send_') %>
<% elsif event.event_type.include?('send_') || event.event_type == 'fail_sms' %>
<%= t("submission_event_names.#{event.event_type}_to_html", submitter_name:) %>
<% if event.data['error_code'].present? %>
<span class="text-base-content/60">(<%= t('error') %>: <%= event.data['error_code'] %>)</span>
<% end %>
<% elsif event.event_type.start_with?('bounce_') || event.event_type.start_with?('complaint_') %>
<%= t("submission_event_names.#{event.event_type}_html", submitter_name:) %>
<% else %>
+1 -1
View File
@@ -1,4 +1,4 @@
<% unless turbo_native_ios? %>
<% unless turbo_native_app? %>
<toggle-on-checked data-names="is_custom_message" class="block">
<div class="form-control">
<%= render 'sms_settings/placeholder' %>
@@ -1,4 +1,4 @@
<% unless turbo_native_ios? %>
<% unless turbo_native_app? %>
<div class="mt-2 mb-1">
<div class="tooltip tooltip-no-touch w-full" data-tip="<%= t('unlock_with_docuseal_pro') %>">
<%= link_to submitter.sent_at? ? t('re_send_sms') : t('send_sms'), Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}", class: 'btn btn-sm btn-primary text-gray-400 w-full' %>
+4 -4
View File
@@ -6,14 +6,14 @@
<% recipient_form_fields = Accounts.load_recipient_form_fields(current_account) if prefillable_fields.blank? %>
<% can_send_emails = Accounts.can_send_emails?(current_account) %>
<% only_detailed = require_phone_2fa || require_email_2fa || prefillable_fields.present? || recipient_form_fields.present? %>
<% with_list = @template.variables_schema.blank? && (!turbo_native_ios? || can?(:manage, :bulk_send)) %>
<% with_phone = !turbo_native_ios? || can?(:manage, :sms_verification) %>
<% with_list = @template.variables_schema.blank? && (!turbo_native_app? || can?(:manage, :bulk_send)) %>
<% with_phone = !turbo_native_app? || can?(:manage, :sms_verification) %>
<% default_tab = 'email' if default_tab == 'phone' && !with_phone %>
<% variables_form = render 'variables_form', schema: @template.variables_schema if @template.variables_schema.present? && @template.variables_schema.any? { |_, v| !v['disabled'] } %>
<%= render 'shared/turbo_modal_large', title: params[:selfsign] ? t('add_recipients') : t('add_new_recipients') do %>
<% options = [only_detailed ? nil : [t('via_email'), 'email'], only_detailed || !with_phone ? nil : [t('via_phone'), 'phone'], [t('detailed'), 'detailed'], with_list ? [t('upload_list'), 'list'] : nil].compact %>
<% if options.size > 1 %>
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center px-2 block <%= 'mt-4' unless turbo_native_app? %>">
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center px-2 block <%= turbo_native_app? ? ('mt-1.5' if turbo_native_android?) : 'mt-4' %> <%= 'max-sm:hidden' if options.one? { |_, value| value != 'list' } %>">
<div class="flex justify-center">
<% options.each_with_index do |(label, value), index| %>
<%= content_tag(value == 'list' ? 'span' : 'toggle-cookies', data: { value:, key: 'add_recipients_tab' }) do %>
@@ -26,7 +26,7 @@
</div>
</toggle-visible>
<% end %>
<div class="px-5 mb-5 mt-5">
<div class="px-5 mt-5 <%= 'mb-5' unless turbo_native_app? %>" style="<%= 'padding-inline: 18px' if turbo_native_app? %>">
<% unless only_detailed %>
<div id="email" class="<%= 'hidden' if default_tab != 'email' %>">
<%= render 'email_form', template: @template, variables_form:, can_send_emails:, viewer_submitter_uuids: %>
+4 -4
View File
@@ -11,7 +11,7 @@
<% with_timestamp_seconds = configs.find { |e| e.key == AccountConfig::WITH_TIMESTAMP_SECONDS_KEY }&.value == true %>
<% with_signature_id_reason = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false %>
<% with_signature_id_completed_at = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID_COMPLETED_AT_KEY }&.value == true %>
<main style="max-width: 1600px" class="mx-auto pl-4">
<div style="max-width: 1600px" class="mx-auto pl-4">
<% unless native_tab == 'recipients' %>
<div class="flex justify-between items-center pr-4 sticky top-0 md:relative z-10 bg-base-100 <%= 'py-1.5' unless turbo_native_app? %>">
<% unless turbo_native_app? %>
@@ -134,9 +134,9 @@
<% document = @submission.schema_documents.find { |a| item['attachment_uuid'] == a.uuid } %>
<% if document.preview_images.first %>
<scroll-to data-selector-id="page-<%= document.uuid %>-0" class="block cursor-pointer">
<img src="<%= (document.preview_images.find { |e| e.filename.base.to_i.zero? } || document.preview_images.first).url(time: service_url_time) %>" width="<%= document.preview_images.first.metadata['width'] %>" height="<%= document.preview_images.first.metadata['height'] %>" class="rounded border" loading="lazy" alt="<%= item['name'].presence || document.filename.base %>">
<img src="<%= (document.preview_images.find { |e| e.filename.base.to_i.zero? } || document.preview_images.first).url(time: service_url_time) %>" width="<%= document.preview_images.first.metadata['width'] %>" height="<%= document.preview_images.first.metadata['height'] %>" class="rounded border" loading="lazy" alt="<%= item['name'].presence || FilenameUtils.basename(document.blob[:filename]) %>">
<div class="pb-2 pt-1.5 text-center" dir="auto">
<%= item['name'].presence || document.filename.base %>
<%= item['name'].presence || FilenameUtils.basename(document.blob[:filename]) %>
</div>
</scroll-to>
<% end %>
@@ -394,7 +394,7 @@
</label>
</toggle-visible>
<% end %>
</main>
</div>
<% unless request.headers['HTTP_X_TURBO_REQUEST_ID'] %>
<%= render 'scripts/autosize_field' %>
<% end %>
+1 -1
View File
@@ -1,6 +1,6 @@
<% filter_params = params.permit(:q, :archived, *Submissions::Filter::ALLOWED_PARAMS) %>
<%= render 'shared/turbo_modal', title: t('export'), close_after_submit: false do %>
<div class="space-y-2">
<div class="space-y-2 <%= 'mt-1.5' if turbo_native_android? %>" style="<%= 'padding-inline: 18px' if turbo_native_app? %>">
<%= 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="enabled">
@@ -1,7 +1,7 @@
<% path_uri = Addressable::URI.parse(params[:path].to_s) %>
<% path = url_for(**Rails.application.routes.recognize_path(path_uri.path, method: :get)) %>
<%= render 'shared/turbo_modal', title: local_assigns[:title] do %>
<%= form_for '', url: path, method: :get, data: { turbo_frame: :_top }, html: { autocomplete: :off } do |f| %>
<%= form_for '', url: path, method: :get, data: { turbo_frame: :_top }, html: { autocomplete: :off, style: ('padding-inline: 18px' if turbo_native_app?) } do |f| %>
<%= hidden_field_tag :q, params[:q] if params[:q].present? %>
<%= hidden_field_tag :scope, params[:scope] if params[:scope].present? %>
<%= hidden_field_tag :archived, params[:archived] if params[:archived].present? %>
@@ -3,11 +3,11 @@
<div class="flex flex-col md:flex-row gap-2">
<div class="form-control w-full">
<%= label_tag 'completed_at_from', t('from'), for: 'date_from', class: 'label text-sm' %>
<%= date_field_tag 'completed_at_from', params[:completed_at_from], id: 'date_from', class: 'base-input !h-10', autocomplete: 'off' %>
<%= date_field_tag 'completed_at_from', params[:completed_at_from], id: 'date_from', class: "base-input !h-10 #{'w-full' if turbo_native_android?}", autocomplete: 'off' %>
</div>
<div class="form-control w-full">
<%= label_tag 'completed_at_to', t('to'), for: 'date_to', class: 'label text-sm' %>
<%= date_field_tag 'completed_at_to', params[:completed_at_to], id: 'date_to', class: 'base-input !h-10', autocomplete: 'off' %>
<%= date_field_tag 'completed_at_to', params[:completed_at_to], id: 'date_to', class: "base-input !h-10 #{'w-full' if turbo_native_android?}", autocomplete: 'off' %>
</div>
</div>
<%= render 'date_buttons' %>
@@ -3,11 +3,11 @@
<div class="flex flex-col md:flex-row gap-2">
<div class="form-control w-full">
<%= label_tag 'created_at_from', t('from'), for: 'date_from', class: 'label text-sm' %>
<%= date_field_tag 'created_at_from', params[:created_at_from], id: 'date_from', class: 'base-input !h-10', autocomplete: 'off' %>
<%= date_field_tag 'created_at_from', params[:created_at_from], id: 'date_from', class: "base-input !h-10 #{'w-full' if turbo_native_android?}", autocomplete: 'off' %>
</div>
<div class="form-control w-full">
<%= label_tag 'created_at_to', t('to'), for: 'date_to', class: 'label text-sm' %>
<%= date_field_tag 'created_at_to', params[:created_at_to], id: 'date_to', class: 'base-input !h-10', autocomplete: 'off' %>
<%= date_field_tag 'created_at_to', params[:created_at_to], id: 'date_to', class: "base-input !h-10 #{'w-full' if turbo_native_android?}", autocomplete: 'off' %>
</div>
</div>
<%= render 'date_buttons' %>
+2 -2
View File
@@ -1,5 +1,5 @@
<% completed_at = @submitter.completed_at || @submitter.submission.completed_at %>
<main class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<div class="flex items-center justify-center">
@@ -51,5 +51,5 @@
</toggle-submit>
<% end %>
</div>
</main>
</div>
<%= render 'shared/attribution', link_path: '/start', account: @submitter.account %>
+5 -4
View File
@@ -3,7 +3,7 @@
<% I18n.with_locale(@submitter.account.locale) do %>
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @submitter.account.name)) %>
<% end %>
<main class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<div class="flex items-center justify-center">
@@ -34,6 +34,7 @@
<%= form_for '', url: submit_form_email_2fa_path, method: :post, html: { class: 'space-y-4', id: 'code_form' } do |f| %>
<div dir="auto" class="form-control !mt-0">
<%= f.hidden_field :submitter_slug, value: @submitter.slug %>
<%= f.hidden_field :lang, value: params[:lang] if params[:lang].present? %>
<%= f.text_field :one_time_code, required: true, inputmode: 'numeric', class: 'base-input text-center', placeholder: 'XXXXXX' %>
<div class="flex justify-between items-center mt-1">
<span>
@@ -55,7 +56,7 @@
<%= f.button button_title(title: t('submit')), class: 'base-button' %>
</toggle-submit>
<% end %>
<%= button_to t(:re_send_email), submit_form_email_2fa_path, params: { submitter_slug: @submitter.slug, resend: true }, method: :put, form: { id: 'resend_code_form', class: 'hidden' } %>
<%= button_to t(:re_send_email), submit_form_email_2fa_path, params: { submitter_slug: @submitter.slug, resend: true, lang: params[:lang] }.compact_blank, method: :put, form: { id: 'resend_code_form', class: 'hidden' } %>
<% else %>
<% if params[:t] %>
<fetch-form data-onload="true">
@@ -63,11 +64,11 @@
</fetch-form>
<% end %>
<toggle-submit dir="auto" class="form-control">
<%= button_to button_title(title: t('send_verification_code')), submit_form_email_2fa_path, params: { submitter_slug: @submitter.slug }, method: :put, class: 'base-button w-full' %>
<%= button_to button_title(title: t('send_verification_code')), submit_form_email_2fa_path, params: { submitter_slug: @submitter.slug, lang: params[:lang] }.compact_blank, method: :put, class: 'base-button w-full' %>
</toggle-submit>
<% end %>
<% else %>
<div><%= t('please_contact_the_requester_to_specify_your_email_for_two_factor_authentication') %></div>
<% end %>
</div>
</main>
</div>
+2 -2
View File
@@ -14,7 +14,7 @@
<% delegate_modal_id = nil %>
<% banner_html = turbo_native_app? ? '' : local_assigns[:banner_html] %>
<div style="max-height: -webkit-fill-available;">
<main id="scrollbox">
<div id="scrollbox">
<div id="sr_only_content"></div>
<div class="mx-auto block pb-72" style="max-width: 1000px">
<%# flex block w-full sticky top-0 z-50 space-x-2 items-center bg-yellow-100 p-2 border-y border-yellow-200 transition-transform duration-300 %>
@@ -172,7 +172,7 @@
<% end %>
<% end %>
</div>
</main>
</div>
</div>
<div class="fixed bottom-0 w-full h-0 z-50">
<div class="mx-auto" style="max-width: 1000px">
+1 -1
View File
@@ -1,5 +1,5 @@
<%= render 'shared/turbo_modal_large', title: t('edit_recipient') do %>
<div class="px-5 mb-5 mt-4">
<div class="px-5 mt-4 <%= 'mb-5' unless turbo_native_app? %>" style="<%= 'padding-inline: 18px' if turbo_native_app? %>">
<%= form_for '', url: submitter_path(@submitter), method: :patch, html: { autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
<div class="mb-2">
<submitter-item class="form-control">
+2 -2
View File
@@ -1,6 +1,6 @@
<%= render 'shared/turbo_modal', title: t('rename_folder') do %>
<%= form_for @template_folder, url: folder_path(@template_folder), data: { turbo_frame: :_top }, html: { autocomplete: :off } do |f| %>
<div class="form-control my-6">
<%= form_for @template_folder, url: folder_path(@template_folder), data: { turbo_frame: :_top }, html: { autocomplete: :off, style: ('padding-inline: 18px' if turbo_native_app?) } do |f| %>
<div class="form-control <%= turbo_native_android? ? 'mt-1.5 mb-6' : 'my-6' %>">
<%= f.text_field :name, required: true, placeholder: "#{t('folder_name')}...", class: 'base-input w-full', autofocus: true, dir: 'auto' %>
</div>
<div class="form-control">
+3 -2
View File
@@ -7,7 +7,8 @@
<%= button_to nil, user_configs_path, method: :post, params: { user_config: { key: UserConfig::SHOW_APP_TOUR, value: true } }, class: 'hidden', id: 'start_tour_button' %>
<% end %>
<% end %>
<% account_configs = current_account.account_configs.where(key: [AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY, AccountConfig::TEMPLATE_DATE_FORMATS_KEY]).to_a %>
<% account_configs = current_account.account_configs.where(key: [AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY, AccountConfig::TEMPLATE_DATE_FORMATS_KEY, AccountConfig::TEMPLATE_BUILDER_AUTOSAVE_KEY]).to_a %>
<% custom_fields = account_configs.find { |e| e.key == AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY }&.value || [] %>
<% date_formats = account_configs.find { |e| e.key == AccountConfig::TEMPLATE_DATE_FORMATS_KEY }&.value || [] %>
<template-builder data-native-platform="<%= turbo_native_ios? ? 'ios' : (turbo_native_android? ? 'android' : '') %>" data-accept-file-types="image/*, application/pdf, application/zip, application/json, <%= Templates::CreateAttachments.document_extensions.join(', ') %>" class="grid" data-template="<%= @template_data.to_json %>" data-custom-fields="<%= custom_fields.to_json %>" data-date-formats="<%= date_formats.to_json %>" data-with-sign-yourself-button="<%= !@template.archived_at? %>" data-with-fields-detection="true" data-with-send-button="<%= !@template.archived_at? && can?(:create, @template.submissions.new(account: current_account)) %>" data-with-revisions-menu="<%= @template.template_versions.exists? %>" data-locale="<%= I18n.locale %>" data-show-tour-start-form="<%= @show_tour_start_form %>"></template-builder>
<% autosave = account_configs.find { |e| e.key == AccountConfig::TEMPLATE_BUILDER_AUTOSAVE_KEY }&.value != false %>
<template-builder data-native-platform="<%= turbo_native_ios? ? 'ios' : (turbo_native_android? ? 'android' : '') %>" data-accept-file-types="image/*, application/pdf, application/zip, application/json, <%= Templates::CreateAttachments.document_extensions.join(', ') %>" class="grid" data-template="<%= @template_data.to_json %>" data-custom-fields="<%= custom_fields.to_json %>" data-date-formats="<%= date_formats.to_json %>" data-autosave="<%= autosave %>" data-with-sign-yourself-button="<%= !@template.archived_at? %>" data-with-fields-detection="true" data-with-send-button="<%= !@template.archived_at? && can?(:create, @template.submissions.new(account: current_account)) %>" data-with-revisions-menu="<%= @template.template_versions.exists? %>" data-locale="<%= I18n.locale %>" data-show-tour-start-form="<%= @show_tour_start_form %>"></template-builder>
+1 -1
View File
@@ -1,5 +1,5 @@
<%= render 'shared/turbo_modal', title: @base_template ? t('clone_template') : t('new_document_template') do %>
<div class="<%= 'px-4' if turbo_native_app? %>">
<div style="<%= 'padding-inline: 18px' if turbo_native_app? %>">
<%= render 'templates/file_form' %>
</div>
<% end %>
+3 -1
View File
@@ -5,7 +5,9 @@
<% is_show_header = submissions_count != 0 || params[:q].present? || params[:status].present? || filter_params.present? %>
<%= render 'title', template: @template, with_export: is_show_header %>
<% if turbo_native_app? %>
<native-action data-placement="search" data-native="search" data-label="<%= t('search') %>" data-url="<%= search_index_path(filter_params.to_h.merge({ 'scope' => 'submissions', 'template_id' => @template.id, 'archived' => ('true' if @template.archived_at?) }.compact)) %>"></native-action>
<% if !turbo_native_android? || submissions_count >= 5 || params[:q].present? || params[:status].present? || filter_params.present? %>
<native-action data-placement="search" data-native="search" data-label="<%= t('search') %>" data-url="<%= search_index_path(filter_params.to_h.merge({ 'scope' => 'submissions', 'template_id' => @template.id, 'archived' => ('true' if @template.archived_at?) }.compact)) %>"></native-action>
<% end %>
<% if !@template.archived_at? && can?(:create, Submission) %>
<native-action data-placement="footer" data-icon="send" data-haptic="true" data-label="<%= t('send_to_recipients') %>">
<a href="<%= new_template_submission_path(@template) %>" data-turbo-frame="modal" data-native-modal="<%= 'medium' if @template.submitters.size < 3 %>"></a>
+5 -4
View File
@@ -1,11 +1,12 @@
<%= form_for @template, url: template_clone_index_path(@base_template), data: { turbo_frame: :_top }, html: { autocomplete: :off } do |f| %>
<%= form_for @template, url: template_clone_index_path(@base_template), data: { turbo_frame: :_top }, html: { autocomplete: :off, style: ('padding-inline: 18px' if turbo_native_app?) } do |f| %>
<% accounts = Account.accessible_by(true_ability).where.not(id: true_user.account.testing_accounts).where(User.where(User.arel_table[:account_id].eq(Account.arel_table[:id])).arel.exists).active %>
<% if (can?(:manage, :tenants) || true_user != current_user) && accounts.where.not(id: current_account.id).exists? %>
<div class="form-control -mb-2 mt-2">
<% with_accounts = (can?(:manage, :tenants) || true_user != current_user) && accounts.where.not(id: current_account.id).exists? %>
<% if with_accounts %>
<div class="form-control -mb-2 <%= turbo_native_android? ? 'mt-1.5' : 'mt-2' %>">
<%= select_tag :account_id, options_for_select([current_account, *accounts.order(:name)].uniq.map { |e| [e.name, e.id] }, current_account.id), required: true, class: 'base-select' %>
</div>
<% end %>
<div class="form-control mt-4">
<div class="form-control <%= turbo_native_android? && !with_accounts ? 'mt-1.5' : 'mt-4' %>">
<%= f.text_field :name, required: true, placeholder: t('document_name'), class: 'base-input', dir: 'auto' %>
</div>
<div class="mt-3 mb-4 flex items-center justify-between">
Loaded 100 of 156 files, more files were not shown because too many files have changed in this diff. Show more