Compare commits

...
70 Commits
Author SHA1 Message Date
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
Alex Turchyn 8f8f69f0bc Merge from docusealco/wip 2026-10-19 14:57:12 +03:00
Pete Matsyburka 04663d55eb update deps 2026-10-19 11:10:48 +03:00
Pete Matsyburka 93d0b935e2 fix dynamic document 2026-10-17 13:17:52 +03:00
Pete Matsyburka 829b2b2855 disable jemalloc 2026-10-17 10:42:20 +03:00
Alex Turchyn cf6de444f4 disable the review form for native apps 2026-10-16 23:22:32 +03:00
Pete Matsyburka 126f5d58a2 convert docx 2026-10-16 20:18:10 +03:00
Pete Matsyburka 09b93067f4 fix close modal 2026-10-16 19:56:36 +03:00
Pete Matsyburka f14458f238 fix generate 2026-10-16 19:56:36 +03:00
Pete Matsyburka 445139f05c adjust mobile nav 2026-10-16 19:56:36 +03:00
Pete Matsyburka e5e4965f05 adjust kba 2026-10-16 19:56:36 +03:00
Pete Matsyburka 1c8f7b0316 use jemalloc 2026-10-15 15:28:54 +03:00
Alex Turchyn 9fe3656652 fix icons shrinking 2026-10-14 08:11:45 +03:00
Pete Matsyburka 17ae46bd61 make remember signature permanent 2026-10-13 16:45:42 +03:00
Pete Matsyburka a036483938 tenants test flag 2026-10-13 09:23:38 +03:00
Alex Turchyn 3c65b0e4ba Merge from docusealco/wip 2026-10-12 15:29:17 +03:00
Pete Matsyburka ced5ab67ab adjust generate previews 2026-10-12 09:33:42 +03:00
Pete Matsyburka 28c4177128 adjsut detailed send button 2026-10-11 17:52:54 +03:00
Pete Matsyburka 5d86a08b5f update send modal 2026-10-10 15:57:08 +03:00
Pete Matsyburka a69fb81429 fix multiselect 2026-10-09 12:57:12 +03:00
Pete Matsyburka e6ac3380d4 dynamic editor image drag 2026-10-09 12:25:21 +03:00
Pete Matsyburka 3a535e7112 adjust date timezone 2026-10-08 16:06:41 +03:00
Pete Matsyburka 7464f9cfad download event log 2026-10-08 10:04:12 +03:00
Pete Matsyburka 58e24ef758 update submitter slug 2026-10-08 10:04:01 +03:00
Pete Matsyburka c1d809f989 update demo 2026-10-07 09:00:58 +03:00
Pete Matsyburka 80b24f0e2a show signature when completed 2026-10-06 12:12:17 +03:00
Pete Matsyburka 2e253904a3 invalid email error 2026-10-06 11:01:04 +03:00
Pete Matsyburka a7f381caf0 update gem 2026-10-05 12:31:08 +03:00
169 changed files with 1167 additions and 660 deletions

No files matched your search

+4 -4
View File
@@ -12,7 +12,7 @@ jobs:
- name: Install Ruby - name: Install Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 4.0.5 ruby-version: 4.0.9
- name: Cache gems - name: Cache gems
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
@@ -37,7 +37,7 @@ jobs:
- name: Install Ruby - name: Install Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 4.0.5 ruby-version: 4.0.9
- name: Cache gems - name: Cache gems
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
@@ -89,7 +89,7 @@ jobs:
- name: Install Ruby - name: Install Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 4.0.5 ruby-version: 4.0.9
- name: Cache gems - name: Cache gems
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
@@ -113,7 +113,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15 timeout-minutes: 15
container: container:
image: ruby:4.0.5-alpine image: ruby:4.0.9-alpine
services: services:
postgres: postgres:
+8 -5
View File
@@ -1,4 +1,4 @@
FROM ruby:4.0.5-alpine AS download FROM ruby:4.0.9-alpine AS download
WORKDIR /fonts WORKDIR /fonts
@@ -17,7 +17,7 @@ RUN apk --no-cache add wget unzip && \
mkdir -p /pdfium-linux && \ mkdir -p /pdfium-linux && \
unzip -q pdfium-linux.zip -d /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 RAILS_ENV=production
ENV NODE_ENV=production ENV NODE_ENV=production
@@ -44,7 +44,7 @@ COPY ./app/views ./app/views
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker 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 RAILS_ENV=production
ENV BUNDLE_WITHOUT="development:test" ENV BUNDLE_WITHOUT="development:test"
@@ -52,7 +52,8 @@ ENV OPENSSL_CONF=/etc/openssl_legacy.cnf
WORKDIR /app WORKDIR /app
RUN apk add --no-cache libpq vips redis onnxruntime leptonica && \ 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 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 RUN addgroup -g 2000 docuseal && adduser -u 2000 -G docuseal -s /bin/sh -D -h /home/docuseal docuseal
@@ -91,7 +92,7 @@ COPY --chown=docuseal:docuseal --from=download /model.onnx /app/tmp/model.onnx
COPY --chown=docuseal:docuseal --from=webpack /app/public/packs ./public/packs COPY --chown=docuseal:docuseal --from=webpack /app/public/packs ./public/packs
RUN mkdir -p /app/public/fonts && ln -s /fonts/DancingScript-Regular.otf /app/public/fonts/ && \ RUN mkdir -p /app/public/fonts && ln -s /fonts/DancingScript-Regular.otf /app/public/fonts/ && \
mkdir -p /usr/share/fonts/noto && ln -s /fonts/GoNotoKurrent-Regular.ttf /usr/share/fonts/noto/ && ln -s /fonts/GoNotoKurrent-Bold.ttf /usr/share/fonts/noto/ && fc-cache -f && \ mkdir -p /usr/share/fonts/noto/bold && ln -s /fonts/GoNotoKurrent-Regular.ttf /usr/share/fonts/noto/ && ln -s /fonts/GoNotoKurrent-Bold.ttf /usr/share/fonts/noto/bold/ && fc-cache -f && \
bundle exec bootsnap precompile -j 1 --gemfile app/ lib/ && \ bundle exec bootsnap precompile -j 1 --gemfile app/ lib/ && \
chown -R docuseal:docuseal /app/tmp/cache chown -R docuseal:docuseal /app/tmp/cache
@@ -100,6 +101,8 @@ ENV HOME=/home/docuseal
ENV WORKDIR=/data/docuseal ENV WORKDIR=/data/docuseal
ENV VIPS_MAX_COORD=17000 ENV VIPS_MAX_COORD=17000
ENV VIPS_BLOCK_UNTRUSTED=1 ENV VIPS_BLOCK_UNTRUSTED=1
# ENV LD_PRELOAD=/usr/lib/libjemalloc.so.2
ENV MALLOC_CONF=dirty_decay_ms:1000,narenas:2,background_thread:true
EXPOSE 3000 EXPOSE 3000
CMD ["/app/bin/bundle", "exec", "puma", "-C", "/app/config/puma.rb", "--dir", "/app"] CMD ["/app/bin/bundle", "exec", "puma", "-C", "/app/config/puma.rb", "--dir", "/app"]
+2 -1
View File
@@ -2,7 +2,7 @@
source 'https://rubygems.org' source 'https://rubygems.org'
ruby '4.0.5' ruby '4.0.9'
gem 'addressable' gem 'addressable'
gem 'arabic-letter-connector', require: false gem 'arabic-letter-connector', require: false
@@ -45,6 +45,7 @@ gem 'trilogy', require: false
gem 'turbo-rails' gem 'turbo-rails'
gem 'twitter_cldr', require: false gem 'twitter_cldr', require: false
gem 'tzinfo-data' gem 'tzinfo-data'
gem 'wordcraft', require: false, group: :ce
gem 'xlsxtream', require: false gem 'xlsxtream', require: false
group :development, :test do group :development, :test do
+94 -83
View File
@@ -3,29 +3,29 @@ GEM
specs: specs:
action_text-trix (2.1.19) action_text-trix (2.1.19)
railties railties
actioncable (8.1.3.1) actioncable (8.1.4)
actionpack (= 8.1.3.1) actionpack (= 8.1.4)
activesupport (= 8.1.3.1) activesupport (= 8.1.4)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
actionmailbox (8.1.3.1) actionmailbox (8.1.4)
actionpack (= 8.1.3.1) actionpack (= 8.1.4)
activejob (= 8.1.3.1) activejob (= 8.1.4)
activerecord (= 8.1.3.1) activerecord (= 8.1.4)
activestorage (= 8.1.3.1) activestorage (= 8.1.4)
activesupport (= 8.1.3.1) activesupport (= 8.1.4)
mail (>= 2.8.0) mail (>= 2.8.0)
actionmailer (8.1.3.1) actionmailer (8.1.4)
actionpack (= 8.1.3.1) actionpack (= 8.1.4)
actionview (= 8.1.3.1) actionview (= 8.1.4)
activejob (= 8.1.3.1) activejob (= 8.1.4)
activesupport (= 8.1.3.1) activesupport (= 8.1.4)
mail (>= 2.8.0) mail (>= 2.8.0)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
actionpack (8.1.3.1) actionpack (8.1.4)
actionview (= 8.1.3.1) actionview (= 8.1.4)
activesupport (= 8.1.3.1) activesupport (= 8.1.4)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
rack (>= 2.2.4) rack (>= 2.2.4)
rack-session (>= 1.0.1) rack-session (>= 1.0.1)
@@ -33,36 +33,36 @@ GEM
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
useragent (~> 0.16) useragent (~> 0.16)
actiontext (8.1.3.1) actiontext (8.1.4)
action_text-trix (~> 2.1.15) action_text-trix (~> 2.1.15)
actionpack (= 8.1.3.1) actionpack (= 8.1.4)
activerecord (= 8.1.3.1) activerecord (= 8.1.4)
activestorage (= 8.1.3.1) activestorage (= 8.1.4)
activesupport (= 8.1.3.1) activesupport (= 8.1.4)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (8.1.3.1) actionview (8.1.4)
activesupport (= 8.1.3.1) activesupport (= 8.1.4)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.11) erubi (~> 1.11)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
activejob (8.1.3.1) activejob (8.1.4)
activesupport (= 8.1.3.1) activesupport (= 8.1.4)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (8.1.3.1) activemodel (8.1.4)
activesupport (= 8.1.3.1) activesupport (= 8.1.4)
activerecord (8.1.3.1) activerecord (8.1.4)
activemodel (= 8.1.3.1) activemodel (= 8.1.4)
activesupport (= 8.1.3.1) activesupport (= 8.1.4)
timeout (>= 0.4.0) timeout (>= 0.4.0)
activestorage (8.1.3.1) activestorage (8.1.4)
actionpack (= 8.1.3.1) actionpack (= 8.1.4)
activejob (= 8.1.3.1) activejob (= 8.1.4)
activerecord (= 8.1.3.1) activerecord (= 8.1.4)
activesupport (= 8.1.3.1) activesupport (= 8.1.4)
marcel (~> 1.0) marcel (~> 1.0)
activesupport (8.1.3.1) activesupport (8.1.4)
base64 base64
bigdecimal bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1) concurrent-ruby (~> 1.0, >= 1.3.1)
@@ -116,7 +116,7 @@ GEM
erubi (~> 1.4) erubi (~> 1.4)
parser (>= 2.4) parser (>= 2.4)
smart_properties smart_properties
bigdecimal (4.1.2) bigdecimal (4.1.3)
bindex (0.8.1) bindex (0.8.1)
bootsnap (1.23.0) bootsnap (1.23.0)
msgpack (~> 1.2) msgpack (~> 1.2)
@@ -149,14 +149,14 @@ GEM
crack (1.0.1) crack (1.0.1)
bigdecimal bigdecimal
rexml rexml
crass (1.0.7) crass (1.0.8)
csv (3.3.5) csv (3.3.5)
csv-safe (3.3.1) csv-safe (3.3.1)
csv (~> 3.0) csv (~> 3.0)
cuprite (0.17) cuprite (0.17)
capybara (~> 3.0) capybara (~> 3.0)
ferrum (~> 0.17.0) ferrum (~> 0.17.0)
date (3.5.1) date (3.6.0)
debug (1.11.1) debug (1.11.1)
irb (~> 1.10) irb (~> 1.10)
reline (>= 0.3.8) reline (>= 0.3.8)
@@ -179,7 +179,7 @@ GEM
dotenv (3.2.0) dotenv (3.2.0)
drb (2.2.3) drb (2.2.3)
email_typo (0.2.3) email_typo (0.2.3)
erb (6.0.6) erb (6.0.7)
erb_lint (0.9.0) erb_lint (0.9.0)
activesupport activesupport
better_html (>= 2.0.1) better_html (>= 2.0.1)
@@ -195,8 +195,8 @@ GEM
railties (>= 6.1.0) railties (>= 6.1.0)
faker (3.6.1) faker (3.6.1)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
faraday (2.14.3) faraday (2.15.1)
faraday-net_http (>= 2.0, < 3.5) faraday-net_http (>= 2.0, < 3.6)
json json
logger logger
faraday-follow_redirects (0.5.0) faraday-follow_redirects (0.5.0)
@@ -264,14 +264,14 @@ GEM
strscan (>= 3.1.2) strscan (>= 3.1.2)
i18n (1.15.2) i18n (1.15.2)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
io-console (0.8.2) io-console (0.9.5)
irb (1.18.0) irb (1.18.0)
pp (>= 0.6.0) pp (>= 0.6.0)
prism (>= 1.3.0) prism (>= 1.3.0)
rdoc (>= 4.0.0) rdoc (>= 4.0.0)
reline (>= 0.4.2) reline (>= 0.4.2)
jmespath (1.6.2) jmespath (1.6.2)
json (2.21.2) json (3.1.0)
jwt (3.2.0) jwt (3.2.0)
base64 base64
language_server-protocol (3.17.0.5) language_server-protocol (3.17.0.5)
@@ -310,17 +310,17 @@ GEM
drb (~> 2.0) drb (~> 2.0)
prism (~> 1.5) prism (~> 1.5)
msgpack (1.8.4) msgpack (1.8.4)
multi_json (1.19.1) multi_json (1.21.2)
net-http (0.9.1) net-http (0.9.1)
uri (>= 0.11.1) uri (>= 0.11.1)
net-imap (0.6.6) net-imap (0.6.8)
date date
net-protocol net-protocol
net-pop (0.1.2) net-pop (0.1.2)
net-protocol net-protocol
net-protocol (0.2.2) net-protocol (0.4.0)
timeout timeout
net-smtp (0.5.1) net-smtp (0.5.2)
net-protocol net-protocol
nio4r (2.7.5) nio4r (2.7.5)
nokogiri (1.19.4-aarch64-linux-gnu) nokogiri (1.19.4-aarch64-linux-gnu)
@@ -361,7 +361,7 @@ GEM
prettyprint prettyprint
pretender (1.0.0) pretender (1.0.0)
actionpack (>= 7.2) actionpack (>= 7.2)
prettyprint (0.2.0) prettyprint (0.2.1)
prism (1.9.0) prism (1.9.0)
pry (0.16.0) pry (0.16.0)
coderay (~> 1.1) coderay (~> 1.1)
@@ -373,30 +373,30 @@ GEM
puma (8.0.2) puma (8.0.2)
nio4r (~> 2.0) nio4r (~> 2.0)
racc (1.8.1) racc (1.8.1)
rack (3.2.6) rack (3.2.7)
rack-proxy (0.7.7) rack-proxy (0.7.7)
rack rack
rack-session (2.1.2) rack-session (2.1.2)
base64 (>= 0.1.0) base64 (>= 0.1.0)
rack (>= 3.0.0) rack (>= 3.0.0)
rack-test (2.2.0) rack-test (2.3.0)
rack (>= 1.3) rack (>= 1.3)
rackup (2.3.1) rackup (2.3.1)
rack (>= 3) rack (>= 3)
rails (8.1.3.1) rails (8.1.4)
actioncable (= 8.1.3.1) actioncable (= 8.1.4)
actionmailbox (= 8.1.3.1) actionmailbox (= 8.1.4)
actionmailer (= 8.1.3.1) actionmailer (= 8.1.4)
actionpack (= 8.1.3.1) actionpack (= 8.1.4)
actiontext (= 8.1.3.1) actiontext (= 8.1.4)
actionview (= 8.1.3.1) actionview (= 8.1.4)
activejob (= 8.1.3.1) activejob (= 8.1.4)
activemodel (= 8.1.3.1) activemodel (= 8.1.4)
activerecord (= 8.1.3.1) activerecord (= 8.1.4)
activestorage (= 8.1.3.1) activestorage (= 8.1.4)
activesupport (= 8.1.3.1) activesupport (= 8.1.4)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 8.1.3.1) railties (= 8.1.4)
rails-dom-testing (2.3.0) rails-dom-testing (2.3.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
minitest minitest
@@ -407,9 +407,9 @@ GEM
rails-i18n (8.1.0) rails-i18n (8.1.0)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 8.0.0, < 9) railties (>= 8.0.0, < 9)
railties (8.1.3.1) railties (8.1.4)
actionpack (= 8.1.3.1) actionpack (= 8.1.4)
activesupport (= 8.1.3.1) activesupport (= 8.1.4)
irb (~> 1.13) irb (~> 1.13)
rackup (>= 1.0.0) rackup (>= 1.0.0)
rake (>= 12.2) rake (>= 12.2)
@@ -418,11 +418,11 @@ GEM
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.4.2) rake (13.4.2)
rbs (4.1.0) rbs (4.2.0)
logger logger
prism (>= 1.6.0) prism (>= 1.6.0)
tsort tsort
rdoc (8.0.0) rdoc (8.1.0)
erb erb
prism (>= 1.6.0) prism (>= 1.6.0)
rbs (>= 4.0.0) rbs (>= 4.0.0)
@@ -430,7 +430,7 @@ GEM
redis-client (0.29.0) redis-client (0.29.0)
connection_pool connection_pool
regexp_parser (2.11.3) regexp_parser (2.11.3)
reline (0.6.3) reline (0.7.0)
io-console (~> 0.5) io-console (~> 0.5)
representable (3.2.0) representable (3.2.0)
declarative (< 0.1.0) declarative (< 0.1.0)
@@ -466,14 +466,14 @@ GEM
rspec-mocks (>= 3.13.0, < 5.0.0) rspec-mocks (>= 3.13.0, < 5.0.0)
rspec-support (>= 3.13.0, < 5.0.0) rspec-support (>= 3.13.0, < 5.0.0)
rspec-support (3.13.7) rspec-support (3.13.7)
rubocop (1.86.0) rubocop (1.92.1)
json (~> 2.3) json (>= 2.3)
language_server-protocol (~> 3.17.0.2) language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0) lint_roller (~> 1.1.0)
parallel (~> 1.10) parallel (>= 1.10)
parser (>= 3.3.0.2) parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0) 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) rubocop-ast (>= 1.49.0, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0) unicode-display_width (>= 2.4.0, < 4.0)
@@ -524,11 +524,11 @@ GEM
simplecov-html (0.13.2) simplecov-html (0.13.2)
simplecov_json_formatter (0.1.4) simplecov_json_formatter (0.1.4)
smart_properties (1.17.0) smart_properties (1.17.0)
sqlite3 (2.9.5-aarch64-linux-gnu) sqlite3 (2.9.6-aarch64-linux-gnu)
sqlite3 (2.9.5-aarch64-linux-musl) sqlite3 (2.9.6-aarch64-linux-musl)
sqlite3 (2.9.5-arm64-darwin) sqlite3 (2.9.6-arm64-darwin)
sqlite3 (2.9.5-x86_64-linux-gnu) sqlite3 (2.9.6-x86_64-linux-gnu)
sqlite3 (2.9.5-x86_64-linux-musl) sqlite3 (2.9.6-x86_64-linux-musl)
strip_attributes (2.0.1) strip_attributes (2.0.1)
activemodel (>= 3.0, < 9.0) activemodel (>= 3.0, < 9.0)
strscan (3.1.8) strscan (3.1.8)
@@ -555,7 +555,7 @@ GEM
unicode-emoji (~> 4.1) unicode-emoji (~> 4.1)
unicode-emoji (4.2.0) unicode-emoji (4.2.0)
uniform_notifier (1.18.0) uniform_notifier (1.18.0)
uri (1.1.1) uri (1.1.2)
useragent (0.16.11) useragent (0.16.11)
warden (1.2.9) warden (1.2.9)
rack (>= 2.0.9) rack (>= 2.0.9)
@@ -572,12 +572,22 @@ GEM
base64 base64
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
wordcraft (0.11.0-aarch64-linux-gnu)
ffi (>= 1.15)
wordcraft (0.11.0-aarch64-linux-musl)
ffi (>= 1.15)
wordcraft (0.11.0-arm64-darwin)
ffi (>= 1.15)
wordcraft (0.11.0-x86_64-linux-gnu)
ffi (>= 1.15)
wordcraft (0.11.0-x86_64-linux-musl)
ffi (>= 1.15)
xlsxtream (3.1.0) xlsxtream (3.1.0)
zip_kit (>= 6.2, < 7) zip_kit (>= 6.2, < 7)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
yaml (0.4.0) yaml (0.4.0)
zeitwerk (2.8.2) zeitwerk (2.8.3)
zip_kit (6.3.4) zip_kit (6.3.4)
PLATFORMS PLATFORMS
@@ -650,10 +660,11 @@ DEPENDENCIES
tzinfo-data tzinfo-data
web-console web-console
webmock webmock
wordcraft
xlsxtream xlsxtream
RUBY VERSION RUBY VERSION
ruby 4.0.5 ruby 4.0.9
BUNDLED WITH BUNDLED WITH
4.0.3 4.0.3
+1
View File
@@ -9,6 +9,7 @@ Out of scope vulnerabilities:
- CSRF - CSRF
- DNSSEC, CAA, CSP headers - DNSSEC, CAA, CSP headers
- DNS or email security related - DNS or email security related
- SSRF
- Rate Limiting - Rate Limiting
Note: We reserve the right to classify any reported vulnerability as out of scope for the bug bounty program. 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@')) email: true_user.email.sub('@', '+removed@'))
true_user.account.update!(archived_at: Time.current) true_user.account.update!(archived_at: Time.current)
# rubocop:disable Layout/LineLength # rubocop:disable-next Layout/LineLength
render turbo_stream: turbo_stream.replace( render turbo_stream: turbo_stream.replace(
:account_delete_button, :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')) + 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.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')) 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 end
private private
@@ -11,7 +11,7 @@ module Api
before_action :set_noindex_headers before_action :set_noindex_headers
before_action :set_security_headers before_action :set_security_headers
# rubocop:disable Metrics # rubocop:disable-next Metrics
def show def show
blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid]) blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
@@ -53,7 +53,6 @@ module Api
end end
end end
end end
# rubocop:enable Metrics
private 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.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
@@ -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:) render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true, params:)
end end
# rubocop:disable Metrics/MethodLength # rubocop:disable-next Metrics/MethodLength
def update def update
submission = @submitter.submission submission = @submitter.submission
role = submission.template_submitters.find { |e| e['uuid'] == @submitter.uuid }['name'] 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 render json: { error: e.message }, status: :unprocessable_content
end end
# rubocop:enable Metrics/MethodLength
def submitter_params def submitter_params
submitter_params = params.key?(:submitter) ? params.require(: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(external_id: params[:external_id]) if params[:external_id].present?
submitters = submitters.where(slug: params[:slug]) if params[:slug].present? submitters = submitters.where(slug: params[:slug]) if params[:slug].present?
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present? submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
submitters = filter_submitters_by_template_id(submitters, params) if params[:template_id].present?
if params[:template_id].present?
submitters = submitters.joins(:submission).where(submissions: { template_id: params[:template_id] })
end
maybe_filter_by_completed_at(submitters, params) maybe_filter_by_completed_at(submitters, params)
end 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) def assign_external_id(submitter, attrs)
submitter.external_id = attrs[:application_key] if attrs.key?(:application_key) submitter.external_id = attrs[:application_key] if attrs.key?(:application_key)
submitter.external_id = attrs[:external_id] if attrs.key?(:external_id) submitter.external_id = attrs[:external_id] if attrs.key?(:external_id)
+6 -1
View File
@@ -118,7 +118,12 @@ class ApplicationController < ActionController::Base
end end
def sign_in_for_demo def sign_in_for_demo
sign_in(User.active.order('random()').take) unless signed_in? return if signed_in?
account = Account.create!(name: "Demo #{SecureRandom.hex(3)}")
sign_in(account.users.create!(first_name: 'Demo', last_name: 'User',
email: "#{SecureRandom.hex}@docuseal.com", password: SecureRandom.hex))
end end
def current_account def current_account
@@ -30,7 +30,7 @@ module Mcp
} }
}.freeze }.freeze
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength # rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength
def call def call
account = current_user.account account = current_user.account
@@ -87,6 +87,5 @@ module Mcp
edit_url: edit_template_url(@template) edit_url: edit_template_url(@template)
) )
end end
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
end end
end end
@@ -66,7 +66,7 @@ module Mcp
} }
}.freeze }.freeze
# rubocop:disable Metrics # rubocop:disable-next Metrics
def call def call
@template = Template.accessible_by(current_ability).find(mcp_params['template_id']) @template = Template.accessible_by(current_ability).find(mcp_params['template_id'])
@@ -115,6 +115,5 @@ module Mcp
rescue Submissions::CreateFromSubmitters::BaseError => e rescue Submissions::CreateFromSubmitters::BaseError => e
render_tool_error(e.message) render_tool_error(e.message)
end end
# rubocop:enable Metrics
end end
end end
+1 -2
View File
@@ -1,9 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class PasswordsController < Devise::PasswordsController class PasswordsController < Devise::PasswordsController
# rubocop:disable Rails/LexicallyScopedActionFilter # rubocop:disable-next Rails/LexicallyScopedActionFilter
skip_before_action :require_no_authentication, only: %i[edit update] skip_before_action :require_no_authentication, only: %i[edit update]
# rubocop:enable Rails/LexicallyScopedActionFilter
around_action :with_browser_locale 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.open(file_path, File::RDWR | File::CREAT, 0o644) do |file|
file.flock(File::LOCK_EX) file.flock(File::LOCK_EX)
# rubocop:disable Style/ZeroLengthPredicate # rubocop:disable-next Style/ZeroLengthPredicate
if file.size.zero? if file.size.zero?
cleanup_stale_tempfiles cleanup_stale_tempfiles
@@ -63,7 +63,6 @@ class PreviewDocumentPageController < ActionController::API
else else
FileUtils.touch(file_path) FileUtils.touch(file_path)
end end
# rubocop:enable Style/ZeroLengthPredicate
file.flock(File::LOCK_UN) file.flock(File::LOCK_UN)
@@ -14,7 +14,7 @@ class SendSubmissionEmailController < ApplicationController
template = Template.find_by!(slug: params[:template_slug]) template = Template.find_by!(slug: params[:template_slug])
@submitter = @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] elsif params[:submission_slug]
submission = Submission.find_by!(slug: params[:submission_slug]) submission = Submission.find_by!(slug: params[:submission_slug])
+10 -6
View File
@@ -34,7 +34,7 @@ class StartFormController < ApplicationController
@submitter = find_or_initialize_submitter(@template, submitter_params) @submitter = find_or_initialize_submitter(@template, submitter_params)
if @submitter.completed_at? 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 else
if Templates.filter_undefined_submitters(@template.submitters).size > 1 && @submitter.new_record? if Templates.filter_undefined_submitters(@template.submitters).size > 1 && @submitter.new_record?
@error_message = multiple_submitters_error_message @error_message = multiple_submitters_error_message
@@ -59,7 +59,7 @@ class StartFormController < ApplicationController
Submitters::StartForm.assign_start_form_cookie(@submitter, request) Submitters::StartForm.assign_start_form_cookie(@submitter, request)
redirect_to submit_form_path(@submitter.slug) redirect_to submit_form_path(@submitter.slug, lang:)
else else
render :show, status: :unprocessable_content render :show, status: :unprocessable_content
end end
@@ -80,7 +80,7 @@ class StartFormController < ApplicationController
raise ActionController::RoutingError, I18n.t('not_found') if required_params.any? { |_, v| v.blank? } || raise ActionController::RoutingError, I18n.t('not_found') if required_params.any? { |_, v| v.blank? } ||
required_params.except('name').compact_blank.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) .where.not(completed_at: nil)
.find_by!(required_params.except('name')) .find_by!(required_params.except('name'))
end end
@@ -150,7 +150,7 @@ class StartFormController < ApplicationController
return render :show, status: :unprocessable_content if submitter.errors.present? 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?) 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 else
if defined?(Rollbar) && template.preferences['shared_link_2fa'] != true if defined?(Rollbar) && template.preferences['shared_link_2fa'] != true
Rollbar.info("2FA link requested: #{submitter.id}") Rollbar.info("2FA link requested: #{submitter.id}")
@@ -167,10 +167,14 @@ class StartFormController < ApplicationController
render :error, status: :unprocessable_content render :error, status: :unprocessable_content
rescue Submitters::UnableToSendCode, Submitters::InvalidOtp => e 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 alert: e.message
rescue RateLimit::LimitApproached 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) alert: I18n.t(:too_many_attempts)
end end
def lang
params[:lang].presence
end
end end
@@ -16,21 +16,21 @@ class StartFormEmail2faSendController < ApplicationController
.submitters.new(**submitter_params, account_id: @template.account_id) .submitters.new(**submitter_params, account_id: @template.account_id)
if @submitter.email.blank? 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 end
Submitters.send_shared_link_email_verification_code(@submitter, request:) Submitters.send_shared_link_email_verification_code(@submitter, request:)
redir_params = { notice: I18n.t(:code_has_been_resent) } if params[:resend] 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 **redir_params
rescue Submitters::BouncedEmail => e rescue Submitters::BouncedEmail => e
@error_message = e.message @error_message = e.message
render 'start_form/error', status: :unprocessable_content render 'start_form/error', status: :unprocessable_content
rescue Submitters::UnableToSendCode => e 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 alert: e.message
end end
@@ -66,4 +66,8 @@ class StartFormEmail2faSendController < ApplicationController
attrs[:email] = Submissions.normalize_email(attrs[:email]) attrs[:email] = Submissions.normalize_email(attrs[:email])
end end
end end
def lang
params[:lang].presence
end
end end
@@ -66,6 +66,8 @@ class SubmissionsController < ApplicationController
create_submissions(@template, submissions_params, params) create_submissions(@template, submissions_params, params)
end end
raise Submissions::CreateFromSubmitters::BaseError, 'Invalid email address' if submissions.blank?
WebhookUrls.enqueue_events(submissions, 'submission.created') WebhookUrls.enqueue_events(submissions, 'submission.created')
Submissions.send_signature_requests(submissions) Submissions.send_signature_requests(submissions)
@@ -25,5 +25,7 @@ class SubmissionsExportController < ApplicationController
end end
end end
def new; end def new
render layout: turbo_native_android? ? 'plain' : true
end
end end
@@ -14,6 +14,6 @@ class SubmissionsFiltersController < ApplicationController
def show def show
return head :not_found unless ALLOWED_NAMES.include?(params[:name]) return head :not_found unless ALLOWED_NAMES.include?(params[:name])
render params[:name] render params[:name], layout: turbo_native_android? ? 'plain' : true
end end
end end
+7 -3
View File
@@ -20,7 +20,7 @@ class SubmitFormController < ApplicationController
return render :email_2fa unless Submitters::AuthorizedForForm.pass_email_2fa?(@submitter, request) return render :email_2fa unless Submitters::AuthorizedForForm.pass_email_2fa?(@submitter, request)
if @submitter.completed_at? || submission.completed_at? 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 end
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS) @form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
@@ -96,7 +96,7 @@ class SubmitFormController < ApplicationController
return if Submitters::AuthorizedForForm.call(@submitter, current_user, request) 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 end
def success; end def success; end
@@ -114,7 +114,7 @@ class SubmitFormController < ApplicationController
def maybe_require_link_2fa def maybe_require_link_2fa
return if Submitters::AuthorizedForForm.pass_link_2fa?(@submitter, current_user, request) 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 end
def maybe_render_locked_page def maybe_render_locked_page
@@ -144,4 +144,8 @@ class SubmitFormController < ApplicationController
ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments) ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments)
.preload(:blob).index_by(&:uuid) .preload(:blob).index_by(&:uuid)
end end
def lang
params[:lang].presence
end
end end
@@ -20,17 +20,17 @@ class SubmitFormEmail2fasController < ApplicationController
{ value: @submitter.slug, expires: Submitters::StartForm::COOKIES_TTL.from_now, { value: @submitter.slug, expires: Submitters::StartForm::COOKIES_TTL.from_now,
**Submitters::StartForm::COOKIES_DEFAULTS } **Submitters::StartForm::COOKIES_DEFAULTS }
redirect_to submit_form_path(@submitter.slug) redirect_to submit_form_path(@submitter.slug, lang:)
else 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 end
rescue RateLimit::LimitApproached 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 end
def update def update
if @submitter.submission_events.where(event_type: 'send_2fa_email').exists?(created_at: 15.seconds.ago..) 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 end
RateLimit.call("send-email-code-#{@submitter.id}", limit: 2, ttl: 45.seconds, enabled: true) 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) if Docuseal.multitenant? && Submitters.email_bounced_recently?(@submitter.email)
Rollbar.warning("Bounced OTP email for submitter: #{@submitter.id}") if defined?(Rollbar) 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 end
SendSubmitterVerificationEmailJob.perform_async('submitter_id' => @submitter.id, 'locale' => I18n.locale.to_s) SendSubmitterVerificationEmailJob.perform_async('submitter_id' => @submitter.id, 'locale' => I18n.locale.to_s)
redir_params = params[:resend] ? { alert: I18n.t(:code_has_been_resent) } : {} 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 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 end
def load_submitter def load_submitter
@submitter = Submitter.find_by!(slug: params[:submitter_slug]) @submitter = Submitter.find_by!(slug: params[:submitter_slug])
end end
def lang
params[:lang].presence
end
end end
@@ -34,7 +34,7 @@ class SubmitFormUploadController < ActionController::API
attachment = Submitters.create_attachment!(@submitter, file, metadata:) attachment = Submitters.create_attachment!(@submitter, file, metadata:)
if params[:remember_signature] == 'true' && @submitter.email.present? if params[:remember_signature] == 'true' && @submitter.email.present?
cookies.encrypted[:signature_uuids] = build_new_cookie_signatures_json(@submitter, attachment) cookies.permanent.encrypted[:signature_uuids] = build_new_cookie_signatures_json(@submitter, attachment)
end end
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type]) render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
+17
View File
@@ -81,9 +81,26 @@ class SubmittersController < ApplicationController
submitter.name = attrs[:name] if attrs.key?(:name) submitter.name = attrs[:name] if attrs.key?(:name)
if submitter_changed?(submitter)
submitter.submitter_versions.new(slug: submitter.slug, email: submitter.email_was,
name: submitter.name_was, phone: submitter.phone_was)
submitter.slug = SecureRandom.base58(14)
submitter.values = Submitters.fetch_values_for_delegate(submitter)
end
submitter submitter
end end
def submitter_changed?(submitter)
if submitter.email_was.blank? && submitter.phone_was.blank?
submitter.name_was.present? && submitter.name_changed?
else
(submitter.email_was.present? && submitter.email_changed?) ||
(submitter.phone_was.present? && submitter.phone_changed?)
end
end
def submitter_params def submitter_params
params.require(:submitter).permit(:email, :name, :phone).transform_values(&:strip) params.require(:submitter).permit(:email, :name, :phone).transform_values(&:strip)
end end
@@ -41,7 +41,9 @@ class TemplateFoldersController < ApplicationController
render_infinite_scroll(@pagy, @templates, @template_folders) if turbo_infinite_scroll? render_infinite_scroll(@pagy, @templates, @template_folders) if turbo_infinite_scroll?
end end
def edit; end def edit
render layout: turbo_native_android? ? 'plain' : true
end
def update def update
if @template_folder != current_account.default_template_folder && if @template_folder != current_account.default_template_folder &&
@@ -8,7 +8,9 @@ class TemplateSharingsTestingController < ApplicationController
end end
def create def create
testing_account = Accounts.find_or_create_testing_user(true_user.account).account account = can?(:manage, :tenants_test) ? current_account : true_user.account
testing_account = Accounts.find_or_create_testing_user(account).account
if params[:value] == '1' if params[:value] == '1'
TemplateSharing.create!(ability: :manage, account: testing_account, template: @template) TemplateSharing.create!(ability: :manage, account: testing_account, template: @template)
@@ -7,6 +7,8 @@ class TemplatesCloneController < ApplicationController
authorize!(:create, Template) authorize!(:create, Template)
@template = Template.new(name: "#{@base_template.name} (#{I18n.t('clone')})") @template = Template.new(name: "#{@base_template.name} (#{I18n.t('clone')})")
render layout: turbo_native_android? ? 'plain' : true
end end
def create def create
@@ -3,7 +3,9 @@
class TemplatesFoldersController < ApplicationController class TemplatesFoldersController < ApplicationController
load_and_authorize_resource :template load_and_authorize_resource :template
def edit; end def edit
render layout: turbo_native_android? ? 'plain' : true
end
def update def update
authorize!(:update, @template) authorize!(:update, @template)
+3 -1
View File
@@ -4,7 +4,9 @@ class UserInitialsController < ApplicationController
before_action :load_user_config before_action :load_user_config
authorize_resource :user_config authorize_resource :user_config
def edit; end def edit
render layout: turbo_native_android? ? 'plain' : true
end
def update def update
file = params[:file] file = params[:file]
@@ -4,7 +4,9 @@ class UserSignaturesController < ApplicationController
before_action :load_user_config before_action :load_user_config
authorize_resource :user_config authorize_resource :user_config
def edit; end def edit
render layout: turbo_native_android? ? 'plain' : true
end
def update def update
file = params[:file] file = params[:file]
+6 -2
View File
@@ -31,9 +31,13 @@ class UsersController < ApplicationController
end end
end end
def new; end def new
render layout: turbo_native_android? ? 'plain' : true
end
def edit; end def edit
render layout: turbo_native_android? ? 'plain' : true
end
def create def create
existing_user = User.accessible_by(current_ability).find_by(email: @user.email) existing_user = User.accessible_by(current_ability).find_by(email: @user.email)
@@ -66,7 +66,7 @@ class WebhookSettingsController < ApplicationController
SendTestWebhookRequestJob.perform_async( SendTestWebhookRequestJob.perform_async(
'submitter_id' => submitter.id, 'submitter_id' => submitter.id,
'event_uuid' => SecureRandom.uuid, 'event_uuid' => SecureRandom.uuid_v7,
'webhook_url_id' => @webhook_url.id 'webhook_url_id' => @webhook_url.id
) )
+7
View File
@@ -35,6 +35,9 @@ import PromptPassword from './elements/prompt_password'
import EmailsTextarea from './elements/emails_textarea' import EmailsTextarea from './elements/emails_textarea'
import ToggleSubmit from './elements/toggle_submit' import ToggleSubmit from './elements/toggle_submit'
import ToggleOnSubmit from './elements/toggle_on_submit' import ToggleOnSubmit from './elements/toggle_on_submit'
import ToggleOnChecked from './elements/toggle_on_checked'
import ToggleSendTitle from './elements/toggle_send_title'
import AppendTemplate from './elements/append_template'
import CheckOnClick from './elements/check_on_click' import CheckOnClick from './elements/check_on_click'
import PasswordInput from './elements/password_input' import PasswordInput from './elements/password_input'
import SearchInput from './elements/search_input' import SearchInput from './elements/search_input'
@@ -159,6 +162,9 @@ safeRegisterElement('emails-textarea', EmailsTextarea)
safeRegisterElement('toggle-cookies', ToggleCookies) safeRegisterElement('toggle-cookies', ToggleCookies)
safeRegisterElement('toggle-submit', ToggleSubmit) safeRegisterElement('toggle-submit', ToggleSubmit)
safeRegisterElement('toggle-on-submit', ToggleOnSubmit) safeRegisterElement('toggle-on-submit', ToggleOnSubmit)
safeRegisterElement('toggle-on-checked', ToggleOnChecked)
safeRegisterElement('toggle-send-title', ToggleSendTitle)
safeRegisterElement('append-template', AppendTemplate)
safeRegisterElement('password-input', PasswordInput) safeRegisterElement('password-input', PasswordInput)
safeRegisterElement('search-input', SearchInput) safeRegisterElement('search-input', SearchInput)
safeRegisterElement('toggle-attribute', ToggleAttribute) safeRegisterElement('toggle-attribute', ToggleAttribute)
@@ -222,6 +228,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
template, template,
customFields: reactive(JSON.parse(this.dataset.customFields || '[]')), customFields: reactive(JSON.parse(this.dataset.customFields || '[]')),
dateFormats: JSON.parse(this.dataset.dateFormats || '[]'), dateFormats: JSON.parse(this.dataset.dateFormats || '[]'),
autosave: this.dataset.autosave !== 'false',
dynamicDocuments: reactive(JSON.parse(this.dataset.dynamicDocuments || '[]')), dynamicDocuments: reactive(JSON.parse(this.dataset.dynamicDocuments || '[]')),
backgroundColor: '#faf7f5', backgroundColor: '#faf7f5',
locale: this.dataset.locale, locale: this.dataset.locale,
@@ -0,0 +1,29 @@
export default class extends HTMLElement {
connectedCallback () {
this.addEventListener('click', this.onClick)
}
disconnectedCallback () {
this.removeEventListener('click', this.onClick)
}
onClick = (e) => {
e.preventDefault()
const target = document.getElementById(this.dataset.targetId)
const item = this.querySelector('template').content.firstElementChild.cloneNode(true)
item.querySelector('a').addEventListener('click', (e) => {
e.preventDefault()
item.remove()
target.classList.toggle('hidden', !target.childElementCount)
})
target.append(item)
target.classList.remove('hidden')
item.querySelector('input').focus()
}
}
+18 -1
View File
@@ -2,7 +2,7 @@ import { actionable } from '@github/catalyst/lib/actionable'
import { targets, targetable } from '@github/catalyst/lib/targetable' import { targets, targetable } from '@github/catalyst/lib/targetable'
export default actionable(targetable(class extends HTMLElement { export default actionable(targetable(class extends HTMLElement {
static [targets.static] = ['items'] static [targets.static] = ['items', 'groups']
addItem (e) { addItem (e) {
e.preventDefault() e.preventDefault()
@@ -24,11 +24,28 @@ export default actionable(targetable(class extends HTMLElement {
duplicateItem.querySelectorAll('a.hidden').forEach((button) => button.classList.toggle('hidden')) duplicateItem.querySelectorAll('a.hidden').forEach((button) => button.classList.toggle('hidden'))
originalItem.parentNode.append(duplicateItem) originalItem.parentNode.append(duplicateItem)
this.toggleMultipleClasses()
duplicateItem.querySelector("input:not([type='hidden'])")?.focus()
} }
removeItem (e) { removeItem (e) {
e.preventDefault() e.preventDefault()
this.items.find((item) => item.contains(e.target))?.remove() this.items.find((item) => item.contains(e.target))?.remove()
this.toggleMultipleClasses()
}
toggleMultipleClasses () {
const isMultiple = this.items.length > 1
const elements = [...this.items, ...this.groups]
elements.forEach((element) => {
const classes = element.dataset.multipleClasses || this.dataset.multipleClasses
classes?.split(' ').forEach((cls) => element.classList.toggle(cls, isMultiple))
})
} }
})) }))
+1 -1
View File
@@ -22,7 +22,7 @@ export default class extends HTMLElement {
} }
async flashFrom (fetchResponse) { async flashFrom (fetchResponse) {
const html = await fetchResponse?.responseHTML const html = await fetchResponse?.responseHTML.catch(() => null)
const flash = html && new DOMParser().parseFromString(html, 'text/html').getElementById('native_flash') const flash = html && new DOMParser().parseFromString(html, 'text/html').getElementById('native_flash')
if (flash?.dataset.alert) return { alert: flash.dataset.alert } if (flash?.dataset.alert) return { alert: flash.dataset.alert }
+1 -1
View File
@@ -55,7 +55,7 @@ export default targetable(class extends HTMLElement {
this.input.files = dataTransfer.files this.input.files = dataTransfer.files
if (this.input.webkitEntries.length) { if (this.input.webkitEntries?.length) {
this.input.dataset.file = `${dataTransfer.files[0].name}` this.input.dataset.file = `${dataTransfer.files[0].name}`
} }
@@ -0,0 +1,23 @@
export default class extends HTMLElement {
connectedCallback () {
this.form.addEventListener('change', this.toggle)
this.toggle()
}
disconnectedCallback () {
this.form.removeEventListener('change', this.toggle)
}
toggle = () => {
const selector = this.dataset.names.split(' ').map((name) => `[name="${name}"]:checked:enabled`).join(', ')
const isChecked = !!this.form.querySelector(selector)
this.firstElementChild.classList.toggle('hidden', !isChecked)
this.lastElementChild.classList.toggle('hidden', isChecked)
}
get form () {
return this.closest('form')
}
}
@@ -0,0 +1,55 @@
export default class extends HTMLElement {
connectedCallback () {
this.events.forEach((event) => this.form.addEventListener(event, this.toggle))
this.toggle()
}
disconnectedCallback () {
this.events.forEach((event) => this.form.removeEventListener(event, this.toggle))
}
toggle = (event) => {
if (event?.type === 'input' && event.target.matches(this.fieldSelector('name'))) return
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'))
this.firstElementChild.classList.toggle('hidden', !isSend)
this.lastElementChild.classList.toggle('hidden', isSend)
}
isChecked (name) {
return !!this.form.querySelector(`[name="${name}"]:checked:enabled`)
}
isFilled (field) {
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', 'focusin', 'focusout']
}
get form () {
return this.closest('form')
}
}
+3
View File
@@ -24,6 +24,9 @@ const ICONS = {
user: 'person', user: 'person',
users: 'person.2', users: 'person.2',
users_plus: 'person.badge.plus', users_plus: 'person.badge.plus',
user_plus: 'person.badge.plus',
eye_plus: 'eye',
mail_plus: 'envelope',
photo: 'photo', photo: 'photo',
settings: 'gearshape', settings: 'gearshape',
logout: 'rectangle.portrait.and.arrow.right', logout: 'rectangle.portrait.and.arrow.right',
+16 -7
View File
@@ -3,11 +3,19 @@
v-if="!error" v-if="!error"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label" class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
> >
<MarkdownContent <span class="flex items-end">
v-if="field.title" <MarkdownContent
:string="field.title" v-if="field.title"
/> :string="field.title"
<template v-else>{{ field.name || 'Knowledge Based Authentication' }}</template> />
<template v-else>{{ field.name || 'Knowledge Based Authentication' }}</template>
<span
class="tooltip tooltip-bottom ml-2 mb-1"
data-tip="To verify your identity, the details you enter are checked against third-party records, which may include credit bureau data, and used to generate questions based on those records. Only these identity details are shared with the identity verification provider; this document and your tax return information are not shared with it or with any credit reporting company. If the check is recorded on your credit report, it appears as a soft inquiry, which does not affect your credit score and is not shown to lenders. This check does not give the sender of this document or the IRS access to your credit report."
>
<IconInfoCircle class="w-4 h-4 text-neutral-500" />
</span>
</span>
<span <span
v-if="questions" v-if="questions"
class="float-right font-normal text-neutral-500 mt-1 whitespace-nowrap text-right text-sm" class="float-right font-normal text-neutral-500 mt-1 whitespace-nowrap text-right text-sm"
@@ -282,13 +290,14 @@
<script> <script>
import MarkdownContent from './markdown_content' import MarkdownContent from './markdown_content'
import { IconInnerShadowTop } from '@tabler/icons-vue' import { IconInnerShadowTop, IconInfoCircle } from '@tabler/icons-vue'
export default { export default {
name: 'KbaStep', name: 'KbaStep',
components: { components: {
MarkdownContent, MarkdownContent,
IconInnerShadowTop IconInnerShadowTop,
IconInfoCircle
}, },
inject: ['baseUrl', 't'], inject: ['baseUrl', 't'],
props: { props: {
+1 -3
View File
@@ -319,7 +319,7 @@ export default {
default: false default: false
} }
}, },
emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to', 'add-custom-field', 'click-title', 'multi-select'], emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to', 'add-custom-field', 'click-title'],
data () { data () {
return { return {
isContenteditable: false, isContenteditable: false,
@@ -798,8 +798,6 @@ export default {
this.selectedAreasRef.value.splice(this.selectedAreasRef.value.indexOf(this.area), 1) this.selectedAreasRef.value.splice(this.selectedAreasRef.value.indexOf(this.area), 1)
} }
this.$emit('multi-select', e)
return return
} }
+67 -7
View File
@@ -3,8 +3,10 @@
ref="dragContainer" ref="dragContainer"
style="max-width: 1600px" style="max-width: 1600px"
:class="nativeView ? (isSheetView ? 'bg-base-100' : 'px-4') : 'mx-auto pl-3 md:pl-4 h-full'" :class="nativeView ? (isSheetView ? 'bg-base-100' : 'px-4') : 'mx-auto pl-3 md:pl-4 h-full'"
@dragstart="isInternalDrag = true"
@dragend="isInternalDrag = false"
@dragover="onDragover" @dragover="onDragover"
@drop="isDragFile = false" @drop="[isDragFile = false, isInternalDrag = false]"
> >
<HoverDropzone <HoverDropzone
v-if="sortedDocuments.length && withUploadButton && editable && !isSheetView" v-if="sortedDocuments.length && withUploadButton && editable && !isSheetView"
@@ -413,9 +415,9 @@
<a <a
:href="`/templates/${template.id}/form`" :href="`/templates/${template.id}/form`"
:data-native-actions="template.submitters.length > 1 ? 'users' : null" :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" class="flex items-center justify-center space-x-2"
@click.exact="isPreviewLoading = true" @click.exact="onSaveAndPreviewClick"
> >
<IconInnerShadowTop <IconInnerShadowTop
v-if="isPreviewLoading" v-if="isPreviewLoading"
@@ -638,6 +640,7 @@
:draw-field-type="drawFieldType" :draw-field-type="drawFieldType"
:draw-custom-field="drawCustomField" :draw-custom-field="drawCustomField"
:draw-option="drawOption" :draw-option="drawOption"
:autosave="autosave"
@update="onDynamicDocumentUpdate" @update="onDynamicDocumentUpdate"
@draw="clearDrawField" @draw="clearDrawField"
@add-custom-field="addCustomField" @add-custom-field="addCustomField"
@@ -769,7 +772,7 @@
class="sticky bottom-0 z-20" class="sticky bottom-0 z-20"
> >
<native-action <native-action
v-if="nativePlatform && withDocumentsList && (isBreakpointLg || isMobile) && sortedDocuments.length && editable" v-if="nativePlatform && withDocumentsList && (isBreakpointLg || isMobile) && editable"
data-placement="drawer" data-placement="drawer"
data-side="leading" data-side="leading"
data-icon="files" data-icon="files"
@@ -779,7 +782,7 @@
data-view="documents" data-view="documents"
/> />
<native-action <native-action
v-if="nativePlatform && withFieldsList && isBreakpointLg && sortedDocuments.length && editable" v-if="nativePlatform && withFieldsList && isBreakpointLg && editable"
data-placement="drawer" data-placement="drawer"
data-side="trailing" data-side="trailing"
data-icon="plus" data-icon="plus"
@@ -1377,6 +1380,7 @@ export default {
isPreviewLoading: false, isPreviewLoading: false,
isLoadingBlankPage: false, isLoadingBlankPage: false,
isSaving: false, isSaving: false,
hasUnsavedChanges: false,
isDetectingPageFields: false, isDetectingPageFields: false,
detectFieldsQueue: [], detectFieldsQueue: [],
detectingAnalyzingProgress: null, detectingAnalyzingProgress: null,
@@ -1390,6 +1394,7 @@ export default {
drawOption: null, drawOption: null,
dragField: null, dragField: null,
isDragFile: false, isDragFile: false,
isInternalDrag: false,
isMathLoaded: false, isMathLoaded: false,
isRevisionsModalOpen: false, isRevisionsModalOpen: false,
isDocumentsDrawerOpen: false, isDocumentsDrawerOpen: false,
@@ -1659,6 +1664,11 @@ export default {
window.addEventListener('dragleave', this.onWindowDragLeave) window.addEventListener('dragleave', this.onWindowDragLeave)
if (!this.autosave) {
window.addEventListener('beforeunload', this.onBeforeUnload)
document.addEventListener('turbo:before-visit', this.onBeforeVisit)
}
this.$nextTick(() => { this.$nextTick(() => {
if (document.location.search?.includes('stripe_connect_success')) { if (document.location.search?.includes('stripe_connect_success')) {
document.querySelector('form[action="/auth/stripe_connect"]')?.closest('.dropdown')?.querySelector('label')?.focus() document.querySelector('form[action="/auth/stripe_connect"]')?.closest('.dropdown')?.querySelector('label')?.focus()
@@ -1682,6 +1692,11 @@ export default {
this.resizeObserver.disconnect() this.resizeObserver.disconnect()
window.removeEventListener('dragleave', this.onWindowDragLeave) window.removeEventListener('dragleave', this.onWindowDragLeave)
if (!this.autosave) {
window.removeEventListener('beforeunload', this.onBeforeUnload)
document.removeEventListener('turbo:before-visit', this.onBeforeVisit)
}
}, },
beforeUpdate () { beforeUpdate () {
this.documentRefs = [] this.documentRefs = []
@@ -2039,8 +2054,18 @@ export default {
ref.x = e.clientX - ref.offsetX ref.x = e.clientX - ref.offsetX
ref.y = e.clientY - ref.offsetY ref.y = e.clientY - ref.offsetY
} else if (e.dataTransfer?.types?.includes('Files') && !this.editModalDocumentUuid) { } else if (e.dataTransfer?.types?.includes('Files') && !this.editModalDocumentUuid && !this.isInternalDrag) {
this.isDragFile = true const items = Array.from(e.dataTransfer.items)
if (this.dynamicDocuments.length && items.length && items.every((item) => item.type.startsWith('image/'))) {
if (!e.defaultPrevented) {
e.preventDefault()
e.dataTransfer.dropEffect = 'none'
}
} else {
this.isDragFile = true
}
} }
}, },
onWindowDragLeave (event) { onWindowDragLeave (event) {
@@ -2228,6 +2253,37 @@ export default {
this.insertField(field) 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 () { closeDropdown () {
document.activeElement.blur() document.activeElement.blur()
}, },
@@ -4023,6 +4079,8 @@ export default {
} }
if (!this.autosave && !force) { if (!this.autosave && !force) {
this.hasUnsavedChanges = true
return Promise.resolve({}) return Promise.resolve({})
} }
@@ -4045,6 +4103,8 @@ export default {
}), }),
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
}).then(() => { }).then(() => {
this.hasUnsavedChanges = false
if (this.onSave) { if (this.onSave) {
this.onSave(this.template) this.onSave(this.template)
} }
@@ -7,7 +7,7 @@
class="absolute top-0 bottom-0 right-0 left-0" class="absolute top-0 bottom-0 right-0 left-0"
@click.prevent="$emit('close')" @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' ? 'px-4 pt-1.5 pb-6' : 'px-4 pt-3 pb-6') : 'modal-box pt-4 pb-6 px-6 mt-20 w-full max-w-xl'">
<div <div
v-if="!inline" v-if="!inline"
class="flex justify-between items-center border-b pb-2 mb-2 font-medium" class="flex justify-between items-center border-b pb-2 mb-2 font-medium"
@@ -33,7 +33,7 @@
>{{ t('available_in_pro') }}</a> >{{ t('available_in_pro') }}</a>
</div> </div>
<form @submit.prevent="validateSaveAndClose"> <form @submit.prevent="validateSaveAndClose">
<div class="my-4"> <div :class="inline && nativePlatform === 'android' ? 'mt-1 mb-4' : 'my-4'">
<div <div
v-for="(condition, cindex) in conditions" v-for="(condition, cindex) in conditions"
:key="cindex" :key="cindex"
@@ -168,7 +168,7 @@
<script> <script>
export default { export default {
name: 'ConditionModal', name: 'ConditionModal',
inject: ['t', 'template', 'withConditions'], inject: ['t', 'template', 'withConditions', 'nativePlatform'],
props: { props: {
item: { item: {
type: Object, type: Object,
@@ -7,7 +7,7 @@
class="absolute top-0 bottom-0 right-0 left-0" class="absolute top-0 bottom-0 right-0 left-0"
@click.prevent="$emit('close')" @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' ? 'px-4 pt-1.5 pb-6' : '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 <div
v-if="!inline" v-if="!inline"
class="flex justify-between items-center border-b pb-2 mb-2 font-medium" class="flex justify-between items-center border-b pb-2 mb-2 font-medium"
@@ -27,7 +27,7 @@
<div> <div>
<label <label
dir="auto" dir="auto"
class="label text-sm" :class="['label text-sm', { 'pt-0 mt-1': inline && nativePlatform === 'android' }]"
for="description_field" for="description_field"
> >
{{ t('description') }} {{ t('description') }}
@@ -71,7 +71,7 @@
<script> <script>
export default { export default {
name: 'DescriptionModal', name: 'DescriptionModal',
inject: ['t', 'template'], inject: ['t', 'template', 'nativePlatform'],
props: { props: {
field: { field: {
type: Object, type: Object,
@@ -36,7 +36,7 @@
<CropView <CropView
v-if="cropPageItem" v-if="cropPageItem"
class="absolute inset-0 z-10 bg-base-100" 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" :template-id="template.id"
:page="cropPageItem" :page="cropPageItem"
:image-url="thumbUrl(cropPageItem)" :image-url="thumbUrl(cropPageItem)"
@@ -47,7 +47,7 @@
<RedactView <RedactView
v-else-if="redactPageItem" v-else-if="redactPageItem"
class="absolute inset-0 z-10 bg-base-100" 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" :template-id="template.id"
:page="redactPageItem" :page="redactPageItem"
:image-url="thumbUrl(redactPageItem)" :image-url="thumbUrl(redactPageItem)"
@@ -63,14 +63,14 @@
> >
<div <div
class="flex-1 overflow-y-auto overflow-x-hidden px-4 md:px-6 py-4 space-y-4" 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 && nativePlatform === 'ios' ? { paddingTop: '86px' } : null"
> >
<div <div
v-for="(doc, docIndex) in layout" v-for="(doc, docIndex) in layout"
:key="doc.attachmentUuid" :key="doc.attachmentUuid"
:data-uuid="doc.attachmentUuid" :data-uuid="doc.attachmentUuid"
class="relative" class="relative"
:style="inline ? { scrollMarginTop: '86px' } : null" :style="inline && nativePlatform === 'ios' ? { scrollMarginTop: '86px' } : null"
> >
<div <div
class="flex items-center pb-2" class="flex items-center pb-2"
@@ -191,7 +191,7 @@
:key="page.id" :key="page.id"
:data-page-id="page.id" :data-page-id="page.id"
class="relative min-w-0 cursor-pointer group" class="relative min-w-0 cursor-pointer group"
:style="inline ? { scrollMarginTop: '86px' } : null" :style="inline && nativePlatform === 'ios' ? { scrollMarginTop: '86px' } : null"
draggable="true" draggable="true"
@click="selectedPageId = page.id" @click="selectedPageId = page.id"
@mousedown="onPageMousedown(page, $event)" @mousedown="onPageMousedown(page, $event)"
@@ -260,7 +260,7 @@
</div> </div>
<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="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"> <div class="grid grid-cols-4 gap-2 md:block md:space-y-2">
<button <button
@@ -419,7 +419,7 @@ export default {
IconEraser, IconEraser,
IconCrop IconCrop
}, },
inject: ['t', 'baseFetch'], inject: ['t', 'baseFetch', 'nativePlatform'],
props: { props: {
template: { template: {
type: Object, type: Object,
@@ -84,6 +84,11 @@ export default {
required: false, required: false,
default: true default: true
}, },
autosave: {
type: Boolean,
required: false,
default: true
},
selectedSubmitter: { selectedSubmitter: {
type: Object, type: Object,
required: false, required: false,
@@ -279,7 +284,7 @@ export default {
this.updatedSectionEditors.set(section.id, editor) this.updatedSectionEditors.set(section.id, editor)
this.saveTimer = setTimeout(() => this.updateSectionsAndSave(), 1000) this.saveTimer = setTimeout(() => this.autosave ? this.updateSectionsAndSave() : this.updateSections(), 1000)
}, },
loadListsStyle () { loadListsStyle () {
this.listsStyle = this.bodyDom.querySelector('body > style[data-lists]')?.textContent || null this.listsStyle = this.bodyDom.querySelector('body > style[data-lists]')?.textContent || null
@@ -299,7 +304,9 @@ export default {
updateVariablesSchema () { updateVariablesSchema () {
this.document.variables_schema = buildVariablesSchema(this.bodyDom.body) this.document.variables_schema = buildVariablesSchema(this.bodyDom.body)
}, },
updateSectionsAndSave () { updateSections () {
delete this.saveTimer
this.updatedSectionEditors.forEach((editor, sectionId) => { this.updatedSectionEditors.forEach((editor, sectionId) => {
const target = this.bodyDom.getElementById(sectionId) const target = this.bodyDom.getElementById(sectionId)
@@ -315,6 +322,9 @@ export default {
this.updateVariablesSchema() this.updateVariablesSchema()
this.$emit('update', this.document) this.$emit('update', this.document)
},
updateSectionsAndSave () {
this.updateSections()
return this.saveBody() return this.saveBody()
}, },
@@ -27,13 +27,17 @@ tiptapStylesheet.replaceSync(
font-feature-settings: "liga" 0; font-feature-settings: "liga" 0;
display: flex; display: flex;
flex-flow: column nowrap; flex-flow: column nowrap;
min-height: inherit; flex-grow: 1;
} }
.ProseMirror > article { .ProseMirror > article:last-of-type {
margin-bottom: auto; margin-bottom: auto;
} }
.ProseMirror p.doc-trailing-break::after {
content: none;
}
.ProseMirror [contenteditable="false"] { .ProseMirror [contenteditable="false"] {
white-space: normal; white-space: normal;
} }
@@ -639,6 +639,20 @@ export default {
if (!file) return if (!file) return
const image = await this.uploadImage(file)
if (image) {
this.editor.chain().focus().insertContent(image).run()
}
},
async insertImageFiles (files, pos) {
const images = (await Promise.all(files.map(this.uploadImage))).filter(Boolean)
if (images.length) {
this.editor.chain().focus().insertContentAt(pos, images).run()
}
},
async uploadImage (file) {
const formData = new FormData() const formData = new FormData()
formData.append('file', file) formData.append('file', file)
@@ -656,9 +670,7 @@ export default {
await this.$nextTick() await this.$nextTick()
this.editor.chain().focus() return { type: 'image', attrs: { htmlAttrs: { src: `blob:${data.uuid}`, style: 'max-width: 100%' } } }
.insertContent({ type: 'image', attrs: { htmlAttrs: { src: `blob:${data.uuid}`, style: 'max-width: 100%' } } })
.run()
}, },
setDynamicMenuCoords (editor) { setDynamicMenuCoords (editor) {
const { from, to } = editor.state.selection const { from, to } = editor.state.selection
@@ -975,7 +987,7 @@ export default {
onSelectedAreaChange () { onSelectedAreaChange () {
this.save() this.save()
}, },
onFieldDrop (view, event, _slice, moved) { onFieldDrop (view, event, slice, moved) {
this.isAreaDrag = false this.isAreaDrag = false
if (moved) { if (moved) {
@@ -983,8 +995,9 @@ export default {
} }
const draggedField = this.fieldsDragFieldRef?.value || this.customDragFieldRef?.value || this.dragField const draggedField = this.fieldsDragFieldRef?.value || this.customDragFieldRef?.value || this.dragField
const imageFiles = slice.size ? [] : Array.from(event.dataTransfer.files).filter((file) => file.type.startsWith('image/'))
if (!draggedField) return false if (!draggedField && !imageFiles.length) return false
event.preventDefault() event.preventDefault()
@@ -992,14 +1005,18 @@ export default {
if (!pos) return false if (!pos) return false
this.insertFieldAtRange({ if (draggedField) {
sourceField: draggedField, this.insertFieldAtRange({
existingField: this.fieldsDragFieldRef?.value, sourceField: draggedField,
from: pos.pos existingField: this.fieldsDragFieldRef?.value,
}) from: pos.pos
})
this.fieldsDragFieldRef.value = null this.fieldsDragFieldRef.value = null
this.customDragFieldRef.value = null this.customDragFieldRef.value = null
} else {
this.insertImageFiles(imageFiles, pos.pos)
}
return true return true
}, },
@@ -608,10 +608,10 @@ export default {
return !['stamp', 'heading', 'strikethrough'].includes(this.field.type) return !['stamp', 'heading', 'strikethrough'].includes(this.field.type)
}, },
showCondition () { 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 () { 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 () { showRequired () {
return this.withRequired && !['phone', 'stamp', 'verification', 'strikethrough', 'heading'].includes(this.field.type) return this.withRequired && !['phone', 'stamp', 'verification', 'strikethrough', 'heading'].includes(this.field.type)
@@ -484,7 +484,7 @@
</label> </label>
</li> </li>
<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" class="field-settings-condition"
> >
<label <label
@@ -500,7 +500,7 @@
</label> </label>
</li> </li>
<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" class="field-settings-formula"
> >
<label <label
+1 -1
View File
@@ -275,7 +275,7 @@
</div> </div>
</button> </button>
<div <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 tooltip-bottom flex"
:class="{'tooltip-bottom-end': withPayment, 'tooltip-bottom': !withPayment }" :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')" :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" class="absolute top-0 bottom-0 right-0 left-0"
@click.prevent="$emit('close')" @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' ? 'px-4 pt-1.5 pb-6' : '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 <div
v-if="!inline" v-if="!inline"
class="flex justify-between items-center border-b pb-2 mb-2 font-medium" class="flex justify-between items-center border-b pb-2 mb-2 font-medium"
@@ -21,7 +21,7 @@
@click.prevent="$emit('close')" @click.prevent="$emit('close')"
>&times;</a> >&times;</a>
</div> </div>
<div class="mt-4"> <div :class="inline && nativePlatform === 'android' ? 'mt-1' : 'mt-4'">
<div> <div>
<div class="flex flex-wrap items-center gap-1.5"> <div class="flex flex-wrap items-center gap-1.5">
<span> <span>
@@ -200,7 +200,7 @@ export default {
components: { components: {
IconChevronDown IconChevronDown
}, },
inject: ['t', 'template'], inject: ['t', 'template', 'nativePlatform'],
props: { props: {
field: { field: {
type: Object, type: Object,
@@ -7,7 +7,7 @@
class="absolute top-0 bottom-0 right-0 left-0" class="absolute top-0 bottom-0 right-0 left-0"
@click.prevent="$emit('close')" @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' ? 'px-4 pt-1.5 pb-6' : '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 <div
v-if="!inline" v-if="!inline"
class="flex justify-between items-center border-b pb-2 mb-2 font-medium" class="flex justify-between items-center border-b pb-2 mb-2 font-medium"
@@ -32,7 +32,7 @@
class="link" class="link"
>{{ t('available_in_pro') }}</a> >{{ t('available_in_pro') }}</a>
</div> </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 <button
v-for="f in fields" v-for="f in fields"
:key="f.uuid" :key="f.uuid"
@@ -152,7 +152,7 @@ export default {
IconCodePlus, IconCodePlus,
IconMathFunction IconMathFunction
}, },
inject: ['t', 'template', 'withFormula'], inject: ['t', 'template', 'withFormula', 'nativePlatform'],
props: { props: {
field: { field: {
type: Object, type: Object,
+7
View File
@@ -70,6 +70,7 @@ const en = {
remove_condition: 'Remove condition', remove_condition: 'Remove condition',
add_condition: 'Add condition', add_condition: 'Add condition',
are_you_sure_: 'Are you sure?', are_you_sure_: 'Are you sure?',
close_without_saving_changes: 'Close without saving changes?',
sign_yourself: 'Sign Yourself', sign_yourself: 'Sign Yourself',
set_signing_date: 'Set signing date', set_signing_date: 'Set signing date',
signing_date: 'Signing Date', signing_date: 'Signing Date',
@@ -371,6 +372,7 @@ const es = {
down: 'Abajo', down: 'Abajo',
set_signing_date: 'Establecer fecha de firma', set_signing_date: 'Establecer fecha de firma',
are_you_sure_: '¿Estás seguro?', are_you_sure_: '¿Estás seguro?',
close_without_saving_changes: '¿Cerrar sin guardar los cambios?',
sign_yourself: 'Firma tú mismo', sign_yourself: 'Firma tú mismo',
checked: 'Marcado', checked: 'Marcado',
current_date: 'Fecha actual', current_date: 'Fecha actual',
@@ -652,6 +654,7 @@ const it = {
remove_condition: 'Rimuovi condizione', remove_condition: 'Rimuovi condizione',
add_condition: 'Aggiungi condizione', add_condition: 'Aggiungi condizione',
are_you_sure_: 'Sei sicuro?', are_you_sure_: 'Sei sicuro?',
close_without_saving_changes: 'Chiudere senza salvare le modifiche?',
sign_yourself: 'Firma te stesso', sign_yourself: 'Firma te stesso',
set_signing_date: 'Imposta data di firma', set_signing_date: 'Imposta data di firma',
signing_date: 'Data di firma', signing_date: 'Data di firma',
@@ -953,6 +956,7 @@ const pt = {
down: 'Para baixo', down: 'Para baixo',
set_signing_date: 'Definir data de assinatura', set_signing_date: 'Definir data de assinatura',
are_you_sure_: 'Tem certeza?', are_you_sure_: 'Tem certeza?',
close_without_saving_changes: 'Fechar sem salvar as alterações?',
sign_yourself: 'Assine você mesmo', sign_yourself: 'Assine você mesmo',
checked: 'Marcado', checked: 'Marcado',
current_date: 'Data atual', current_date: 'Data atual',
@@ -1234,6 +1238,7 @@ const fr = {
remove_condition: 'Supprimer la condition', remove_condition: 'Supprimer la condition',
add_condition: 'Ajouter une condition', add_condition: 'Ajouter une condition',
are_you_sure_: 'Êtes-vous sûr ?', are_you_sure_: 'Êtes-vous sûr ?',
close_without_saving_changes: 'Fermer sans enregistrer les modifications ?',
sign_yourself: 'Signer vous-même', sign_yourself: 'Signer vous-même',
set_signing_date: 'Définir la date de signature', set_signing_date: 'Définir la date de signature',
signing_date: 'Date de signature', signing_date: 'Date de signature',
@@ -1525,6 +1530,7 @@ const de = {
remove_condition: 'Bedingung entfernen', remove_condition: 'Bedingung entfernen',
add_condition: 'Bedingung hinzufügen', add_condition: 'Bedingung hinzufügen',
are_you_sure_: 'Sind Sie sicher?', are_you_sure_: 'Sind Sie sicher?',
close_without_saving_changes: 'Ohne Speichern der Änderungen schließen?',
sign_yourself: 'Selbst unterschreiben', sign_yourself: 'Selbst unterschreiben',
set_signing_date: 'Unterzeichnungsdatum festlegen', set_signing_date: 'Unterzeichnungsdatum festlegen',
signing_date: 'Unterzeichnungsdatum', signing_date: 'Unterzeichnungsdatum',
@@ -1816,6 +1822,7 @@ const nl = {
remove_condition: 'Voorwaarde verwijderen', remove_condition: 'Voorwaarde verwijderen',
add_condition: 'Voorwaarde toevoegen', add_condition: 'Voorwaarde toevoegen',
are_you_sure_: 'Weet u het zeker?', are_you_sure_: 'Weet u het zeker?',
close_without_saving_changes: 'Sluiten zonder wijzigingen op te slaan?',
sign_yourself: 'Zelf ondertekenen', sign_yourself: 'Zelf ondertekenen',
set_signing_date: 'Ondertekeningsdatum instellen', set_signing_date: 'Ondertekeningsdatum instellen',
signing_date: 'Ondertekeningsdatum', signing_date: 'Ondertekeningsdatum',
-8
View File
@@ -57,7 +57,6 @@
@scroll-to="$emit('scroll-to', $event)" @scroll-to="$emit('scroll-to', $event)"
@add-custom-field="$emit('add-custom-field', $event)" @add-custom-field="$emit('add-custom-field', $event)"
@contextmenu="openAreaContextMenu($event, item.area, item.field)" @contextmenu="openAreaContextMenu($event, item.area, item.field)"
@multi-select="openMultiSelectContextMenu"
@click-title="closeContextMenu" @click-title="closeContextMenu"
/> />
<FieldArea <FieldArea
@@ -450,13 +449,6 @@ export default {
areas: this.selectedAreasRef.value areas: this.selectedAreasRef.value
} }
}, },
openMultiSelectContextMenu (event) {
if (this.selectedAreasRef.value.length >= 2) {
this.openSelectionContextMenu(event)
} else {
this.closeContextMenu()
}
},
handleSelectionCopy () { handleSelectionCopy () {
this.$emit('copy-selected-areas') this.$emit('copy-selected-areas')
@@ -203,7 +203,7 @@
>{{ t('learn_more') }}</a> >{{ t('learn_more') }}</a>
</div> </div>
<li <li
v-if="withFormula || nativePlatform !== 'ios'" v-if="withFormula || !nativePlatform"
class="field-settings-formula mb-1" class="field-settings-formula mb-1"
> >
<label <label
@@ -233,7 +233,7 @@
</label> </label>
</li> </li>
<li <li
v-if="withCondition && (withConditions || nativePlatform !== 'ios')" v-if="withCondition && (withConditions || !nativePlatform)"
class="field-settings-condition mt-1" class="field-settings-condition mt-1"
> >
<label <label
@@ -5,7 +5,7 @@
class="absolute top-0 bottom-0 right-0 left-0" class="absolute top-0 bottom-0 right-0 left-0"
@click.prevent="$emit('close')" @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' ? 'px-4 pt-1.5 pb-6' : 'px-4 pt-3 pb-6') : 'modal-box pt-4 pb-6 mt-20 w-full'">
<div <div
v-if="!inline" v-if="!inline"
class="flex justify-between items-center border-b pb-2 mb-3 font-medium" 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 { export default {
name: 'RevisionsModal', name: 'RevisionsModal',
components: { IconUser, IconInnerShadowTop }, components: { IconUser, IconInnerShadowTop },
inject: ['t', 'baseFetch'], inject: ['t', 'baseFetch', 'nativePlatform'],
props: { props: {
template: { template: {
type: Object, type: Object,
@@ -259,7 +259,7 @@ export default {
return true return true
}, },
showCondition () { showCondition () {
return this.withCondition && (this.withConditions || this.nativePlatform !== 'ios') return this.withCondition && (this.withConditions || !this.nativePlatform)
}, },
fieldNames: FieldType.computed.fieldNames, fieldNames: FieldType.computed.fieldNames,
fieldLabels: FieldType.computed.fieldLabels 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| WebhookUrls.for_account_id(submitter.account_id, %w[form.completed submission.completed]).each do |webhook|
if webhook.events.include?('form.completed') 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, SendFormCompletedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
'event_uuid' => event_uuids['form.completed'], 'event_uuid' => event_uuids['form.completed'],
@@ -109,7 +109,7 @@ class ProcessSubmitterCompletionJob
next unless webhook.events.include?('submission.completed') && is_last 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, SendSubmissionCompletedWebhookRequestJob.perform_async('submission_id' => submitter.submission_id,
'event_uuid' => event_uuids['submission.completed'], '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) raise LocalhostError, "Can't send to localhost." if uri.host.in?(SendWebhookRequest::LOCALHOSTS)
end end
Faraday.post(webhook_url.url, body_json = {
{ event_type: 'form.completed',
event_type: 'form.completed', timestamp: Time.current.iso8601,
timestamp: Time.current.iso8601, data: Submitters::SerializeForWebhook.call(submitter)
data: Submitters::SerializeForWebhook.call(submitter) }.to_json
}.to_json,
Faraday.post(webhook_url.url, body_json,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT, 'User-Agent' => USER_AGENT,
'X-Docuseal-Signature' => WebhookUrls::Signatures.sign(webhook_url.hmac_secret, body: body_json),
**webhook_url.secret.to_h) **webhook_url.secret.to_h)
end end
end end
+2 -1
View File
@@ -15,7 +15,8 @@
# #
# Indexes # Indexes
# #
# index_accounts_on_uuid (uuid) UNIQUE # index_accounts_on_created_at (created_at)
# index_accounts_on_uuid (uuid) UNIQUE
# #
class Account < ApplicationRecord class Account < ApplicationRecord
attribute :uuid, :string, default: -> { SecureRandom.uuid } attribute :uuid, :string, default: -> { SecureRandom.uuid }
+1
View File
@@ -61,6 +61,7 @@ class AccountConfig < ApplicationRecord
DOCUMENT_FILENAME_FORMAT_KEY = 'document_filename_format' DOCUMENT_FILENAME_FORMAT_KEY = 'document_filename_format'
TEMPLATE_CUSTOM_FIELDS_KEY = 'template_custom_fields' TEMPLATE_CUSTOM_FIELDS_KEY = 'template_custom_fields'
TEMPLATE_DATE_FORMATS_KEY = 'template_date_formats' TEMPLATE_DATE_FORMATS_KEY = 'template_date_formats'
TEMPLATE_BUILDER_AUTOSAVE_KEY = 'template_builder_autosave'
POLICY_LINKS_KEY = 'policy_links' POLICY_LINKS_KEY = 'policy_links'
ENABLE_MCP_KEY = 'enable_mcp' ENABLE_MCP_KEY = 'enable_mcp'
+9 -1
View File
@@ -25,15 +25,17 @@
# account_id :bigint not null # account_id :bigint not null
# external_id :string # external_id :string
# submission_id :bigint not null # submission_id :bigint not null
# template_id :bigint
# #
# Indexes # 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_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_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_external_id (external_id)
# index_submitters_on_slug (slug) UNIQUE # index_submitters_on_slug (slug) UNIQUE
# index_submitters_on_submission_id (submission_id) # index_submitters_on_submission_id (submission_id)
# index_submitters_on_template_id_and_id (template_id,id)
# #
# Foreign Keys # Foreign Keys
# #
@@ -68,6 +70,8 @@ class Submitter < ApplicationRecord
scope :completed, -> { where.not(completed_at: nil) } scope :completed, -> { where.not(completed_at: nil) }
before_validation :set_template_id, on: :create
after_destroy :anonymize_email_events, if: -> { Docuseal.multitenant? } after_destroy :anonymize_email_events, if: -> { Docuseal.multitenant? }
def status def status
@@ -124,6 +128,10 @@ class Submitter < ApplicationRecord
private private
def set_template_id
self.template_id = submission&.template_id
end
def anonymize_email_events def anonymize_email_events
email_events.each do |event| email_events.each do |event|
event.update!(email: Digest::MD5.base64digest(event.email)) event.update!(email: Digest::MD5.base64digest(event.email))
+1 -1
View File
@@ -22,7 +22,7 @@
# index_webhook_events_on_webhook_url_id_and_id (webhook_url_id,id) # index_webhook_events_on_webhook_url_id_and_id (webhook_url_id,id)
# #
class WebhookEvent < ApplicationRecord class WebhookEvent < ApplicationRecord
attribute :uuid, :string, default: -> { SecureRandom.uuid } attribute :uuid, :string, default: -> { SecureRandom.uuid_v7 }
belongs_to :webhook_url, optional: true belongs_to :webhook_url, optional: true
belongs_to :account, optional: true belongs_to :account, optional: true
+3 -3
View File
@@ -133,7 +133,7 @@
<% end %> <% end %>
<% end %> <% end %>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::ALLOW_TO_DECLINE_KEY) %> <% 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| %> <%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %> <%= f.hidden_field :key %>
<div class="flex items-center justify-between gap-4 py-2.5"> <div class="flex items-center justify-between gap-4 py-2.5">
@@ -156,7 +156,7 @@
<% end %> <% end %>
<% end %> <% end %>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::ALLOW_TO_DELEGATE_KEY) %> <% 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| %> <%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %> <%= f.hidden_field :key %>
<div class="flex items-center justify-between gap-4 py-2.5"> <div class="flex items-center justify-between gap-4 py-2.5">
@@ -196,7 +196,7 @@
<% end %> <% end %>
<% end %> <% end %>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY) %> <% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY) %>
<% if can?(:manage, account_config) %> <% if !Docuseal.demo? && can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %> <%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %> <%= f.hidden_field :key %>
<div class="flex items-center justify-between gap-4 py-2.5"> <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"> <tr class="group">
<td> <td>
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
<path d="M12 18c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" />
<path d="M16 19h6" />
<path d="M19 16v6" />
</svg>

After

Width:  |  Height:  |  Size: 460 B

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 19h-7a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v5.5" />
<path d="M16 19h6" />
<path d="M19 16v6" />
<path d="M3 7l9 6l9 -6" />
</svg>

After

Width:  |  Height:  |  Size: 441 B

+2 -3
View File
@@ -1,8 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M5 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0" /> <path d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0" />
<path d="M3 21v-2a4 4 0 0 1 4 -4h4c.96 0 1.84 .338 2.53 .901" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
<path d="M16 19h6" /> <path d="M16 19h6" />
<path d="M19 16v6" /> <path d="M19 16v6" />
<path d="M6 21v-2a4 4 0 0 1 4 -4h4" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 488 B

After

Width:  |  Height:  |  Size: 421 B

+17
View File
@@ -7,4 +7,21 @@
-webkit-user-drag: none; -webkit-user-drag: none;
-webkit-touch-callout: 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> </style>
+3 -3
View File
@@ -33,10 +33,10 @@
<% end %> <% end %>
<turbo-frame id="drawer"></turbo-frame> <turbo-frame id="drawer"></turbo-frame>
<%= render turbo_native_app? ? 'shared/native_navbar' : 'shared/navbar' %> <%= render turbo_native_app? ? 'shared/native_navbar' : 'shared/navbar' %>
<% if flash.present? %><%= render 'shared/flash' %><% end %> <main class="max-w-6xl mx-auto px-4 md:px-2 mb-8">
<div class="max-w-6xl mx-auto px-4 md:px-2 mb-8">
<%= yield %> <%= yield %>
</div> </main>
<% if flash.present? %><%= render 'shared/flash' %><% end %>
<%= render 'shared/body_scripts' %> <%= render 'shared/body_scripts' %>
</body> </body>
</html> </html>
+3 -1
View File
@@ -13,6 +13,8 @@
<%= stylesheet_pack_tag 'form', media: 'all', data: { turbo_track: 'dynamic' } %> <%= stylesheet_pack_tag 'form', media: 'all', data: { turbo_track: 'dynamic' } %>
</head> </head>
<body> <body>
<%= yield %> <main>
<%= yield %>
</main>
</body> </body>
</html> </html>
+4 -2
View File
@@ -13,8 +13,10 @@
<%= stylesheet_pack_tag 'application', media: 'all', data: { turbo_track: 'dynamic' } %> <%= stylesheet_pack_tag 'application', media: 'all', data: { turbo_track: 'dynamic' } %>
</head> </head>
<body> <body>
<% if flash.present? %><%= render 'shared/flash' %><% end %> <main>
<%= yield %> <%= yield %>
</main>
<turbo-frame id="modal"></turbo-frame> <turbo-frame id="modal"></turbo-frame>
<% if flash.present? %><%= render 'shared/flash' %><% end %>
</body> </body>
</html> </html>
@@ -22,7 +22,7 @@
<% end %> <% end %>
</div> </div>
<%= render 'bcc_form', config: @bcc_config %> <%= 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"> <div class="flex justify-between items-end mb-4 mt-8">
<h2 class="text-2xl md:text-3xl font-bold"> <h2 class="text-2xl md:text-3xl font-bold">
<%= t('sign_request_email_reminders') %> <%= t('sign_request_email_reminders') %>
@@ -10,7 +10,7 @@
<%= render 'documents_copy_email_form' %> <%= render 'documents_copy_email_form' %>
<%= render 'submitter_completed_email_form' %> <%= render 'submitter_completed_email_form' %>
</div> </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"> <p class="text-3xl md:text-4xl font-bold mb-4 mt-8">
<%= t('company_logo') %> <%= t('company_logo') %>
</p> </p>
+1 -1
View File
@@ -3,7 +3,7 @@
<div class="w-full text-center"> <div class="w-full text-center">
<span class="font-bold"><%= t('demo_environment') %></span> <span class="font-bold"><%= t('demo_environment') %></span>
<br> <br>
<%= t('create_a_new_template_document_form_or_submit_the_existing_one_html', new_template_link: new_template_path, start_form_link: start_form_url(slug: ::Template.first&.slug)) %> 😊 <%= t('create_a_new_template_document_form_html', new_template_link: new_template_path) %>
</div> </div>
</div> </div>
</div> </div>
+10 -8
View File
@@ -1,11 +1,13 @@
<script type="application/json" id="native_path_configuration"><%= render partial: 'layouts/native_path_configuration', formats: :json %></script> <script type="application/json" id="native_path_configuration"><%= render partial: 'layouts/native_path_configuration', formats: :json %></script>
<% if signed_in? %> <% if signed_in? %>
<native-action data-placement="profile" data-icon="user"> <% unless turbo_native_android? %>
<%= link_to t('profile'), settings_profile_index_path %> <native-action data-placement="profile" data-icon="user">
</native-action> <%= link_to t('profile'), settings_profile_index_path %>
<native-action data-placement="profile" data-icon="settings"> </native-action>
<%= link_to t('settings'), settings_path %> <native-action data-placement="profile" data-icon="settings">
</native-action> <%= link_to t('settings'), settings_path %>
</native-action>
<% end %>
<% if current_account.testing? && (can?(:manage, EncryptedConfig) || current_user != true_user) %> <% 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') %>"> <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' } } %> <%= button_to '', testing_account_path, method: :delete, form: { data: { turbo_action: 'replace' } } %>
@@ -22,13 +24,13 @@
<% end %> <% end %>
<% if can?(:create, ::Template) && controller_name.in?(%w[templates_dashboard submissions_dashboard template_folders templates_archived submissions_archived]) %> <% if can?(:create, ::Template) && controller_name.in?(%w[templates_dashboard submissions_dashboard template_folders templates_archived submissions_archived]) %>
<% folder_name = @template_folder&.full_name %> <% folder_name = @template_folder&.full_name %>
<native-action data-placement="create" data-icon="upload" data-native="upload" data-label="<%= t('upload') %>" data-url="<%= templates_upload_path(folder_name: folder_name) %>" data-accept="image/*, application/pdf, application/zip, application/json<%= ", #{Templates::CreateAttachments::DOCUMENT_EXTENSIONS.join(', ')}" if Docuseal.advanced_formats? %>"></native-action> <native-action data-placement="create" data-icon="upload" data-native="upload" data-label="<%= t('upload') %>" data-url="<%= templates_upload_path(folder_name: folder_name) %>" data-accept="image/*, application/pdf, application/zip, application/json, <%= Templates::CreateAttachments.document_extensions.join(', ') %>"></native-action>
<native-action data-placement="create" data-icon="scan" data-native="scan" data-label="<%= t('scan') %>" data-url="<%= templates_upload_path(folder_name: folder_name) %>"></native-action> <native-action data-placement="create" data-icon="scan" data-native="scan" data-label="<%= t('scan') %>" data-url="<%= templates_upload_path(folder_name: folder_name) %>"></native-action>
<native-action data-placement="create" data-icon="pencil_plus"> <native-action data-placement="create" data-icon="pencil_plus">
<%= link_to t('create'), new_template_path(folder_name: folder_name), data: { turbo_frame: :modal, native_modal: :medium } %> <%= link_to t('create'), new_template_path(folder_name: folder_name), data: { turbo_frame: :modal, native_modal: :medium } %>
</native-action> </native-action>
<% unless controller_name.in?(%w[templates_archived submissions_archived]) %> <% unless controller_name.in?(%w[templates_archived submissions_archived]) %>
<native-action data-placement="menu" data-menu="<%= t('new') %>" data-menu-icon="pencil_plus" data-icon="upload" data-native="upload" data-label="<%= t('upload') %>" data-url="<%= templates_upload_path(folder_name: folder_name) %>" data-accept="image/*, application/pdf, application/zip, application/json<%= ", #{Templates::CreateAttachments::DOCUMENT_EXTENSIONS.join(', ')}" if Docuseal.advanced_formats? %>"></native-action> <native-action data-placement="menu" data-menu="<%= t('new') %>" data-menu-icon="pencil_plus" data-icon="upload" data-native="upload" data-label="<%= t('upload') %>" data-url="<%= templates_upload_path(folder_name: folder_name) %>" data-accept="image/*, application/pdf, application/zip, application/json, <%= Templates::CreateAttachments.document_extensions.join(', ') %>"></native-action>
<native-action data-placement="menu" data-menu="<%= t('new') %>" data-menu-icon="pencil_plus" data-icon="scan" data-native="scan" data-label="<%= t('scan') %>" data-url="<%= templates_upload_path(folder_name: folder_name) %>"></native-action> <native-action data-placement="menu" data-menu="<%= t('new') %>" data-menu-icon="pencil_plus" data-icon="scan" data-native="scan" data-label="<%= t('scan') %>" data-url="<%= templates_upload_path(folder_name: folder_name) %>"></native-action>
<native-action data-placement="menu" data-menu="<%= t('new') %>" data-menu-icon="pencil_plus" data-icon="pencil_plus"> <native-action data-placement="menu" data-menu="<%= t('new') %>" data-menu-icon="pencil_plus" data-icon="pencil_plus">
<%= link_to t('create'), new_template_path(folder_name: folder_name), data: { turbo_frame: :modal, native_modal: :medium } %> <%= link_to t('create'), new_template_path(folder_name: folder_name), data: { turbo_frame: :modal, native_modal: :medium } %>
+2 -2
View File
@@ -1,5 +1,5 @@
<%= render 'shared/navbar_warning' %> <%= 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"> <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"> <div class="flex items-center space-x-4">
<a href="<%= root_path %>" class="text-2xl font-bold items-center flex space-x-2"> <a href="<%= root_path %>" class="text-2xl font-bold items-center flex space-x-2">
<%= render 'shared/title' %> <%= render 'shared/title' %>
@@ -106,4 +106,4 @@
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
</div> </nav>
+5 -5
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 %> <%= link_to t('storage'), settings_storage_index_path, class: 'text-base hover:bg-base-300', data: nav_link_data %>
</li> </li>
<% end %> <% 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> <li>
<%= link_to 'SMS', settings_sms_path, class: 'text-base hover:bg-base-300', data: nav_link_data %> <%= link_to 'SMS', settings_sms_path, class: 'text-base hover:bg-base-300', data: nav_link_data %>
</li> </li>
@@ -84,7 +84,7 @@
</li> </li>
<% end %> <% end %>
<% 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> <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 %> <%= 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') %> <%= t('plans') %>
@@ -93,21 +93,21 @@
</li> </li>
<% end %> <% end %>
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user == true_user || current_account.testing?) %> <% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user == true_user || current_account.testing?) %>
<% if Docuseal.multitenant? || !turbo_native_ios? %> <% unless turbo_native_app? %>
<li> <li>
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}#{'/test' if current_account.testing?}/api") : "#{Docuseal::CONSOLE_URL}/on_premises", class: 'text-base hover:bg-base-300', data: { turbo: false } do %> <%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}#{'/test' if current_account.testing?}/api") : "#{Docuseal::CONSOLE_URL}/on_premises", class: 'text-base hover:bg-base-300', data: { turbo: false } do %>
<% if Docuseal.multitenant? %> API <% else %> <%= t('console') %> <% end %> <% if Docuseal.multitenant? %> API <% else %> <%= t('console') %> <% end %>
<% end %> <% end %>
</li> </li>
<% end %> <% end %>
<% if Docuseal.multitenant? %> <% if Docuseal.multitenant? && !turbo_native_app? %>
<li> <li>
<%= link_to console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}#{'/test' if current_account.testing?}/embedding/form"), class: 'text-base hover:bg-base-300', data: { turbo: false } do %> <%= link_to console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}#{'/test' if current_account.testing?}/embedding/form"), class: 'text-base hover:bg-base-300', data: { turbo: false } do %>
<%= t('embedding') %> <%= t('embedding') %>
<% end %> <% end %>
</li> </li>
<% end %> <% 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> <li>
<%= link_to 'SSO', settings_sso_index_path, class: 'text-base hover:bg-base-300', data: nav_link_data %> <%= link_to 'SSO', settings_sso_index_path, class: 'text-base hover:bg-base-300', data: nav_link_data %>
</li> </li>
@@ -1,7 +1,9 @@
<form action="<%= url_for %>" method="get" class="dropdown dropdown-top hidden md:inline"> <form action="<%= url_for %>" method="get" class="dropdown dropdown-top hidden md:inline">
<label tabindex="0" class="btn btn-sm h-10"> <% unless turbo_native_app? %>
<%= svg_icon('arrow_sort', class: 'w-5 h-5') %> <label tabindex="0" class="btn btn-sm h-10">
</label> <%= 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"> <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' %>"> <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"> <toggle-cookies data-value="created_at" data-key="dashboard_templates_order">
+1 -1
View File
@@ -2,7 +2,7 @@
<% content_for(:native_modal, 'true') %> <% content_for(:native_modal, 'true') %>
<% content_for(:native_title, local_assigns[:title]) if local_assigns[:title] %> <% content_for(:native_title, local_assigns[:title]) if local_assigns[:title] %>
<turbo-frame id="modal"> <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 %> <%= yield %>
</native-modal> </native-modal>
</turbo-frame> </turbo-frame>
+3 -3
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 mx-auto">
<div class="space-y-6"> <div class="space-y-6">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
@@ -12,7 +12,7 @@
<div> <div>
<p dir="auto" class="text-lg font-bold mb-1"><%= @submitter.submission.template&.name %></p> <p dir="auto" class="text-lg font-bold mb-1"><%= @submitter.submission.template&.name %></p>
<p dir="auto" class="text-sm"> <p dir="auto" class="text-sm">
<%= t(@submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(@submitter.completed_at.to_date, format: :long)) %> <%= t(@submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(@submitter.completed_at.in_time_zone(@submitter.account.timezone).to_date, format: :long)) %>
</p> </p>
</div> </div>
</div> </div>
@@ -32,5 +32,5 @@
</toggle-submit> </toggle-submit>
<% end %> <% end %>
</div> </div>
</main> </div>
<%= render 'shared/attribution', link_path: '/start', account: @submitter.account %> <%= render 'shared/attribution', link_path: '/start', account: @submitter.account %>
@@ -2,7 +2,7 @@
<% I18n.with_locale(@template.account.locale) do %> <% 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)) %> <% 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 %> <% 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 mx-auto">
<div class="space-y-6"> <div class="space-y-6">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
@@ -23,7 +23,7 @@
<div> <div>
<%= t('we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue') %> <%= t('we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue') %>
</div> </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"> <div dir="auto" class="form-control !mt-0">
<%= f.hidden_field 'resubmit', value: params[:resubmit] %> <%= f.hidden_field 'resubmit', value: params[:resubmit] %>
<%= f.hidden_field 'submitter[name]', value: params[:name] || params.dig(:submitter, :name) %> <%= f.hidden_field 'submitter[name]', value: params[:name] || params.dig(:submitter, :name) %>
@@ -52,7 +52,7 @@
<% if @resubmit_submitter %> <% if @resubmit_submitter %>
<%= button_to t(:re_send_email), resubmit_form_path, params: { resubmit: @resubmit_submitter.slug }, method: :put, id: 'resend_code', class: 'hidden' %> <%= button_to t(:re_send_email), resubmit_form_path, params: { resubmit: @resubmit_submitter.slug }, method: :put, id: 'resend_code', class: 'hidden' %>
<% else %> <% 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 %> <% end %>
</div> </div>
</main> </div>
+2 -2
View File
@@ -2,7 +2,7 @@
<% I18n.with_locale(@template.account.locale) do %> <% 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)) %> <% 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 %> <% 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 mx-auto">
<div class="space-y-6"> <div class="space-y-6">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
@@ -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' %> <%= 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>
</div> </div>
</main> </div>
+2 -2
View File
@@ -2,7 +2,7 @@
<% I18n.with_locale(@template.account.locale) do %> <% I18n.with_locale(@template.account.locale) do %>
<% content_for(:html_description, t('share_link_is_currently_disabled')) %> <% content_for(:html_description, t('share_link_is_currently_disabled')) %>
<% end %> <% 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"> <div class="text-center w-full space-y-6">
<%= render 'start_form/banner' %> <%= render 'start_form/banner' %>
<p class="text-xl font-semibold text-center"> <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' %> <%= 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> </toggle-submit>
<% end %> <% end %>
</main> </div>
<%= render 'shared/attribution', link_path: '/start', account: @template.account %> <%= render 'shared/attribution', link_path: '/start', account: @template.account %>
+3 -3
View File
@@ -2,7 +2,7 @@
<% I18n.with_locale(@template.account.locale) do %> <% 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)) %> <% 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 %> <% 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 mx-auto">
<div class="space-y-6"> <div class="space-y-6">
<div class="text-center w-full space-y-6"> <div class="text-center w-full space-y-6">
@@ -28,7 +28,7 @@
</div> </div>
</div> </div>
<% if !@template.archived_at? && !@template.account.archived_at? %> <% 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 %> <%= hidden_field_tag :resubmit, @resubmit_submitter.slug if @resubmit_submitter %>
<% if @error_message %> <% if @error_message %>
<div class="alert items-start bg-base-100 border border-red-400 text-red-400"> <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 'shared/attribution', link_path: '/start', account: @template.account %>
<%= render 'start_form/policy', account: @template.account %> <%= render 'start_form/policy', account: @template.account %>
</div> </div>
</main> </div>
+2 -1
View File
@@ -43,7 +43,8 @@
</div> </div>
<p class="text-base-content/80 mt-1"> <p class="text-base-content/80 mt-1">
<% if event.event_type == 'complete_verification' %> <% 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')) %> <% 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:) %> <%= 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' %> <% elsif event.event_type == 'delegate_form' %>
+70
View File
@@ -0,0 +1,70 @@
<% fields_id = "add_new_fields_#{SecureRandom.hex(3)}" %>
<div id="<%= fields_id %>" class="space-y-3 mt-4 hidden" data-targets="dynamic-list.groups" data-multiple-classes="px-3 pt-3 pb-4 rounded-xl border border-dashed border-base-300"></div>
<native-menu class="<%= turbo_native_app? ? 'block' : 'dropdown block max-sm:hidden' %> mt-4 -mb-2">
<label tabindex="0" class="btn btn-ghost btn-sm normal-case font-normal w-full flex items-center justify-center border-dashed border-base-300 hover:border-base-content/20 !bg-transparent">
<%= svg_icon('users_plus', class: 'w-4 h-4 stroke-2') %>
<span><%= t('add_new') %></span>
</label>
<ul tabindex="0" class="<%= turbo_native_app? ? 'hidden' : 'z-10 dropdown-content p-2 mt-1 shadow menu bg-base-100 rounded-box' %>" style="left: 50%; --tw-translate-x: -50%">
<li>
<native-menu-action data-icon="user_plus" data-label="<%= t('recipients') %>" class="grid-cols-1 p-0">
<a href="#" class="flex items-center gap-2 px-4 py-2" data-action="click:dynamic-list#addItem">
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
<span class="whitespace-nowrap"><%= t('recipients') %></span>
</a>
</native-menu-action>
</li>
<append-template class="contents" data-target-id="<%= fields_id %>">
<li>
<native-menu-action data-icon="eye_plus" data-label="<%= t('cc_view_only') %>" class="grid-cols-1 p-0">
<a href="#" class="flex items-center gap-2 px-4 py-2">
<%= svg_icon('eye_plus', class: 'w-4 h-4 stroke-2') %>
<span class="whitespace-nowrap"><%= t('cc_view_only') %></span>
</a>
</native-menu-action>
</li>
<template>
<submitter-item class="form-control">
<label class="label pt-0 pb-1">
<span class="label-text"><%= t('cc_view_only') %></span>
</label>
<div class="relative">
<submitters-autocomplete data-field="email">
<input type="email" name="cc_recipients[]" autocomplete="off" class="base-input !h-10 w-full pr-10" placeholder="<%= t('email') %>">
</submitters-autocomplete>
<a href="#" class="absolute right-2 top-2 block w-6 h-6 text-neutral-700 p-1 hover:text-base-content">
<%= svg_icon('trash', class: 'w-4 h-4') %>
</a>
</div>
</submitter-item>
</template>
</append-template>
<append-template class="contents" data-target-id="<%= fields_id %>">
<li>
<native-menu-action data-icon="mail_plus" data-label="<%= t('bcc_completed') %>" class="grid-cols-1 p-0">
<a href="#" class="flex items-center gap-2 px-4 py-2">
<%= svg_icon('mail_plus', class: 'w-4 h-4 stroke-2') %>
<span class="whitespace-nowrap"><%= t('bcc_completed') %></span>
</a>
</native-menu-action>
</li>
<template>
<div class="form-control">
<label class="label pt-0 pb-1">
<span class="label-text"><%= t('bcc_completed') %></span>
</label>
<div class="relative">
<input type="email" name="bcc_completed[]" autocomplete="off" class="base-input !h-10 w-full pr-10" placeholder="<%= t('email') %>">
<a href="#" class="absolute right-2 top-2 block w-6 h-6 text-neutral-700 p-1 hover:text-base-content">
<%= svg_icon('trash', class: 'w-4 h-4') %>
</a>
</div>
<% default_bcc_completed = template.preferences['bcc_completed'].presence || current_account.account_configs.find_by(key: AccountConfig::BCC_EMAILS)&.value %>
<% if default_bcc_completed.present? %>
<input type="hidden" name="bcc_completed[]" value="<%= default_bcc_completed %>">
<% end %>
</div>
</template>
</append-template>
</ul>
</native-menu>
+14 -12
View File
@@ -1,12 +1,12 @@
<% has_phone_field = false %> <% has_phone_field = false %>
<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %> <%= form_for '', url: template_submissions_path(template), html: { autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
<% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %> <% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %>
<dynamic-list class="space-y-4"> <dynamic-list class="block" data-multiple-classes="px-3 <%= submitters.size > 1 ? 'pt-3 pb-4' : 'py-4' %> rounded-xl border border-dashed border-base-300">
<div class="space-y-4"> <div class="space-y-4">
<div class="relative <%= 'p-4 rounded-xl bg-base-300/40 max-sm:p-0 max-sm:bg-transparent' unless local_assigns[:variables_form] %>" data-targets="dynamic-list.items"> <div class="relative" data-targets="dynamic-list.items">
<div> <div>
<div class="absolute right-4 top-5"> <div class="absolute top-5" style="right: 12px">
<a href="#" data-action="click:dynamic-list#removeItem" class="<%= submitters.size == 1 ? 'right-2 top-1' : '-top-3' %> relative block w-6 h-6 rounded-lg text-neutral-700 text-center bg-base-300 p-1 hidden hover:bg-neutral hover:text-white"> <a href="#" data-action="click:dynamic-list#removeItem" class="<%= submitters.size == 1 ? 'right-2 top-1' : '-top-3' %> relative block w-6 h-6 text-neutral-700 p-1 hidden hover:text-base-content">
<%= svg_icon('trash', class: 'w-4 h-4') %> <%= svg_icon('trash', class: 'w-4 h-4') %>
</a> </a>
</div> </div>
@@ -86,14 +86,11 @@
</div> </div>
</div> </div>
<% if params[:selfsign].blank? && local_assigns[:prefillable_fields].blank? && local_assigns[:recipient_form_fields].blank? && local_assigns[:variables_form].blank? %> <% if params[:selfsign].blank? && local_assigns[:prefillable_fields].blank? && local_assigns[:recipient_form_fields].blank? && local_assigns[:variables_form].blank? %>
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center max-sm:hidden" data-action="click:dynamic-list#addItem"> <%= render 'add_new', template: %>
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
<span><%= t('add_new') %></span>
</a>
<% end %> <% end %>
</dynamic-list> </dynamic-list>
<%= local_assigns[:variables_form] %> <%= local_assigns[:variables_form] %>
<div> <div class="mt-4">
<%= render('submitters_order', f:, template:) if can_send_emails %> <%= render('submitters_order', f:, template:) if can_send_emails %>
<%= render 'send_email', f:, template:, can_send_emails:, viewer_submitter_uuids: local_assigns[:viewer_submitter_uuids] %> <%= render 'send_email', f:, template:, can_send_emails:, viewer_submitter_uuids: local_assigns[:viewer_submitter_uuids] %>
<% if has_phone_field %> <% if has_phone_field %>
@@ -101,7 +98,12 @@
<% end %> <% end %>
<%= render 'extra_fields', f: %> <%= render 'extra_fields', f: %>
</div> </div>
<div class="form-control"> <div class="form-control mt-4">
<%= f.button button_title(title: t('add_recipients')), class: 'base-button' %> <%= f.button class: 'base-button' do %>
<toggle-send-title>
<span><%= button_title(title: t('send'), disabled_with: t('sending')) %></span>
<span class="hidden"><%= button_title(title: t('add_recipients')) %></span>
</toggle-send-title>
<% end %>
</div> </div>
<% end %> <% end %>
+15 -13
View File
@@ -1,4 +1,4 @@
<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %> <%= form_for '', url: template_submissions_path(template), html: { autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
<% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %> <% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %>
<% if submitters.size == 1 && local_assigns[:variables_form] %> <% if submitters.size == 1 && local_assigns[:variables_form] %>
<% item = submitters.first %> <% item = submitters.first %>
@@ -26,18 +26,18 @@
<emails-textarea data-bulk-enabled="<%= Docuseal.demo? || !Docuseal.multitenant? || can?(:manage, :bulk_send) %>" data-limit="<%= Docuseal.multitenant? ? (can?(:manage, :bulk_send) ? 40 : 1) : nil %>"> <emails-textarea data-bulk-enabled="<%= Docuseal.demo? || !Docuseal.multitenant? || can?(:manage, :bulk_send) %>" data-limit="<%= Docuseal.multitenant? ? (can?(:manage, :bulk_send) ? 40 : 1) : nil %>">
<submitters-autocomplete data-field="email" class="block relative"> <submitters-autocomplete data-field="email" class="block relative">
<autoresize-textarea> <autoresize-textarea>
<%= f.text_area :emails, required: true, class: 'base-textarea w-full !text-lg', placeholder: "#{t('type_emails_here')}...", rows: 2 %> <%= f.text_area :emails, required: true, class: 'base-textarea block w-full !text-lg', placeholder: "#{t('type_emails_here')}...", rows: 2 %>
</autoresize-textarea> </autoresize-textarea>
</submitters-autocomplete> </submitters-autocomplete>
</emails-textarea> </emails-textarea>
</submitter-item> </submitter-item>
<% else %> <% else %>
<dynamic-list class="space-y-4"> <dynamic-list class="block" data-multiple-classes="px-3 pt-3 pb-4 rounded-xl border border-dashed border-base-300">
<div class="space-y-4"> <div class="space-y-4">
<div class="relative <%= 'p-4 rounded-xl bg-base-300/40 max-sm:p-0 max-sm:bg-transparent' unless local_assigns[:variables_form] %>" data-targets="dynamic-list.items"> <div class="relative" data-targets="dynamic-list.items">
<div> <div>
<div class="absolute right-4 top-5"> <div class="absolute top-5" style="right: 12px">
<a href="#" data-action="click:dynamic-list#removeItem" class="-top-3 relative block w-6 h-6 rounded-lg text-neutral-700 text-center bg-base-300 p-1 hidden hover:bg-neutral hover:text-white"> <a href="#" data-action="click:dynamic-list#removeItem" class="-top-3 relative block w-6 h-6 text-neutral-700 p-1 hidden hover:text-base-content">
<%= svg_icon('trash', class: 'w-4 h-4') %> <%= svg_icon('trash', class: 'w-4 h-4') %>
</a> </a>
</div> </div>
@@ -60,22 +60,24 @@
</div> </div>
</div> </div>
<% if params[:selfsign].blank? && local_assigns[:variables_form].blank? %> <% if params[:selfsign].blank? && local_assigns[:variables_form].blank? %>
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center max-sm:hidden" data-action="click:dynamic-list#addItem"> <%= render 'add_new', template: %>
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
<span><%= t('add_new') %></span>
</a>
<% end %> <% end %>
</dynamic-list> </dynamic-list>
<% end %> <% end %>
<%= local_assigns[:variables_form] %> <%= local_assigns[:variables_form] %>
<div> <div class="mt-4">
<%= render('submitters_order', f:, template:) if can_send_emails %> <%= render('submitters_order', f:, template:) if can_send_emails %>
<%= render 'send_email', f:, template:, can_send_emails:, viewer_submitter_uuids: local_assigns[:viewer_submitter_uuids] %> <%= render 'send_email', f:, template:, can_send_emails:, viewer_submitter_uuids: local_assigns[:viewer_submitter_uuids] %>
<%= render 'extra_fields', f: %> <%= render 'extra_fields', f: %>
</div> </div>
<div class="form-control"> <div class="form-control mt-4">
<%= content_for(:submit_button) || capture do %> <%= content_for(:submit_button) || capture do %>
<%= f.button button_title(title: t('add_recipients')), class: 'base-button' %> <%= f.button class: 'base-button' do %>
<toggle-on-checked data-names="send_email send_sms">
<span><%= button_title(title: t('send'), disabled_with: t('sending')) %></span>
<span class="hidden"><%= button_title(title: t('add_recipients')) %></span>
</toggle-on-checked>
<% end %>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
+14 -12
View File
@@ -1,11 +1,11 @@
<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %> <%= form_for '', url: template_submissions_path(template), html: { autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
<% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %> <% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %>
<dynamic-list class="space-y-4"> <dynamic-list class="block" data-multiple-classes="px-3 <%= submitters.size > 1 ? 'pt-3 pb-4' : 'py-4' %> rounded-xl border border-dashed border-base-300">
<div class="space-y-4"> <div class="space-y-4">
<div class="relative <%= 'p-4 rounded-xl bg-base-300/40 max-sm:p-0 max-sm:bg-transparent' unless local_assigns[:variables_form] %>" data-targets="dynamic-list.items"> <div class="relative" data-targets="dynamic-list.items">
<div> <div>
<div class="absolute right-4 top-5"> <div class="absolute top-5" style="right: 12px">
<a href="#" data-action="click:dynamic-list#removeItem" class="<%= submitters.size == 1 ? 'right-2 top-1' : '-top-3' %> relative block w-6 h-6 rounded-lg text-neutral-700 text-center bg-base-300 p-1 hidden hover:bg-neutral hover:text-white"> <a href="#" data-action="click:dynamic-list#removeItem" class="<%= submitters.size == 1 ? 'right-2 top-1' : '-top-3' %> relative block w-6 h-6 text-neutral-700 p-1 hidden hover:text-base-content">
<%= svg_icon('trash', class: 'w-4 h-4') %> <%= svg_icon('trash', class: 'w-4 h-4') %>
</a> </a>
</div> </div>
@@ -48,19 +48,21 @@
</div> </div>
</div> </div>
<% if params[:selfsign].blank? && local_assigns[:variables_form].blank? %> <% if params[:selfsign].blank? && local_assigns[:variables_form].blank? %>
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center max-sm:hidden" data-action="click:dynamic-list#addItem"> <%= render 'add_new', template: %>
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
<span><%= t('add_new') %></span>
</a>
<% end %> <% end %>
</dynamic-list> </dynamic-list>
<%= local_assigns[:variables_form] %> <%= local_assigns[:variables_form] %>
<div> <div class="mt-4">
<%= render 'submitters_order', f:, template: %> <%= render 'submitters_order', f:, template: %>
<%= render 'send_sms', f:, checked: true %> <%= render 'send_sms', f:, checked: true %>
<%= render 'extra_phone_fields', f: %> <%= render 'extra_phone_fields', f: %>
</div> </div>
<div class="form-control"> <div class="form-control mt-4">
<%= f.button button_title(title: t('add_recipients')), class: 'base-button' %> <%= f.button class: 'base-button' do %>
<toggle-on-checked data-names="send_email send_sms">
<span><%= button_title(title: t('send'), disabled_with: t('sending')) %></span>
<span class="hidden"><%= button_title(title: t('add_recipients')) %></span>
</toggle-on-checked>
<% end %>
</div> </div>
<% end %> <% end %>
@@ -14,7 +14,7 @@
<toggle-visible data-element-ids="<%= [message_field_id].to_json %>" class="flex"> <toggle-visible data-element-ids="<%= [message_field_id].to_json %>" class="flex">
<label> <label>
<%= f.check_box :is_custom_message, checked: false, class: 'hidden peer', data: { action: 'change:toggle-visible#trigger', type: 'checkbox' } %> <%= f.check_box :is_custom_message, checked: false, class: 'hidden peer', data: { action: 'change:toggle-visible#trigger', type: 'checkbox' } %>
<span class="link peer-checked:hidden"><%= t('edit_message') %></span> <span class="link link-hover peer-checked:hidden"><%= t('edit_message') %></span>
</label> </label>
</toggle-visible> </toggle-visible>
<% end %> <% end %>
+6 -4
View File
@@ -1,5 +1,7 @@
<% unless turbo_native_ios? %> <% unless turbo_native_app? %>
<div class="form-control"> <toggle-on-checked data-names="is_custom_message" class="block">
<%= render 'sms_settings/placeholder' %> <div class="form-control">
</div> <%= render 'sms_settings/placeholder' %>
</div>
</toggle-on-checked>
<% end %> <% end %>
@@ -1,4 +1,4 @@
<% unless turbo_native_ios? %> <% unless turbo_native_app? %>
<div class="mt-2 mb-1"> <div class="mt-2 mb-1">
<div class="tooltip tooltip-no-touch w-full" data-tip="<%= t('unlock_with_docuseal_pro') %>"> <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' %> <%= 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? %> <% recipient_form_fields = Accounts.load_recipient_form_fields(current_account) if prefillable_fields.blank? %>
<% can_send_emails = Accounts.can_send_emails?(current_account) %> <% can_send_emails = Accounts.can_send_emails?(current_account) %>
<% only_detailed = require_phone_2fa || require_email_2fa || prefillable_fields.present? || recipient_form_fields.present? %> <% 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_list = @template.variables_schema.blank? && (!turbo_native_app? || can?(:manage, :bulk_send)) %>
<% with_phone = !turbo_native_ios? || can?(:manage, :sms_verification) %> <% with_phone = !turbo_native_app? || can?(:manage, :sms_verification) %>
<% default_tab = 'email' if default_tab == 'phone' && !with_phone %> <% 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'] } %> <% 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 %> <%= 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 %> <% 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 %> <% 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"> <div class="flex justify-center">
<% options.each_with_index do |(label, value), index| %> <% options.each_with_index do |(label, value), index| %>
<%= content_tag(value == 'list' ? 'span' : 'toggle-cookies', data: { value:, key: 'add_recipients_tab' }) do %> <%= content_tag(value == 'list' ? 'span' : 'toggle-cookies', data: { value:, key: 'add_recipients_tab' }) do %>
@@ -26,7 +26,7 @@
</div> </div>
</toggle-visible> </toggle-visible>
<% end %> <% end %>
<div class="px-5 mb-5 mt-4"> <div class="px-5 mb-5 mt-5">
<% unless only_detailed %> <% unless only_detailed %>
<div id="email" class="<%= 'hidden' if default_tab != 'email' %>"> <div id="email" class="<%= 'hidden' if default_tab != 'email' %>">
<%= render 'email_form', template: @template, variables_form:, can_send_emails:, viewer_submitter_uuids: %> <%= render 'email_form', template: @template, variables_form:, can_send_emails:, viewer_submitter_uuids: %>
+20 -14
View File
@@ -11,7 +11,7 @@
<% with_timestamp_seconds = configs.find { |e| e.key == AccountConfig::WITH_TIMESTAMP_SECONDS_KEY }&.value == true %> <% 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_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 %> <% 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' %> <% 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? %>"> <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? %> <% unless turbo_native_app? %>
@@ -54,7 +54,8 @@
<% if @submission.submitters.to_a.any?(&:completed_at?) %> <% if @submission.submitters.to_a.any?(&:completed_at?) %>
<% show_combined_download = @submission.completed_at? && !is_combined_enabled %> <% show_combined_download = @submission.completed_at? && !is_combined_enabled %>
<% show_unarchive = signed_in? && can?(:destroy, @submission) && @submission.archived_at? && @submission.completed_at? %> <% show_unarchive = signed_in? && can?(:destroy, @submission) && @submission.archived_at? && @submission.completed_at? %>
<% show_download_dropdown = show_combined_download || show_unarchive %> <% show_event_log_download = signed_in? && can?(:manage, :event_log_download) && can?(:read, @submission) %>
<% show_download_dropdown = show_combined_download || show_event_log_download || show_unarchive %>
<% if @submission.completed_at? || !is_combined_enabled %> <% if @submission.completed_at? || !is_combined_enabled %>
<div class="join relative"> <div class="join relative">
<native-action data-label="<%= t('download') %>" data-icon="download"> <native-action data-label="<%= t('download') %>" data-icon="download">
@@ -95,6 +96,9 @@
</li> </li>
</native-action> </native-action>
<% end %> <% end %>
<% if show_event_log_download %>
<%= render 'submissions/download_event_log' %>
<% end %>
<% if show_unarchive %> <% if show_unarchive %>
<native-action data-label="<%= t('unarchive') %>" data-icon="rotate" data-section="archive"> <native-action data-label="<%= t('unarchive') %>" data-icon="rotate" data-section="archive">
<li> <li>
@@ -163,6 +167,7 @@
<% value = values[field['uuid']].presence || (field['default_value'] != '{{date}}' && field['readonly'] == true && field['conditions'].blank? && field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %> <% value = values[field['uuid']].presence || (field['default_value'] != '{{date}}' && field['readonly'] == true && field['conditions'].blank? && field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
<% value ||= field['default_value'] if field['type'] == 'heading' %> <% value ||= field['default_value'] if field['type'] == 'heading' %>
<% submitter = submitters_index[field['submitter_uuid']] %> <% submitter = submitters_index[field['submitter_uuid']] %>
<% value = nil if field['type'] == 'signature' && submitter && !submitter.completed_at? %>
<% if value.present? %> <% if value.present? %>
<% if (mask = field.dig('preferences', 'mask').presence) && signed_in? && can?(:read, @submission) %> <% if (mask = field.dig('preferences', 'mask').presence) && signed_in? && can?(:read, @submission) %>
<span class="group"> <span class="group">
@@ -222,39 +227,39 @@
</div> </div>
<% if submitter&.name.present? %> <% if submitter&.name.present? %>
<div class="flex items-center space-x-1 mt-1"> <div class="flex items-center space-x-1 mt-1">
<%= svg_icon('user', class: 'w-5 h-5') %> <%= svg_icon('user', class: 'w-5 h-5 shrink-0') %>
<span> <span class="truncate">
<%= submitter&.name %> <%= submitter&.name %>
</span> </span>
</div> </div>
<% end %> <% end %>
<% if submitter&.email.present? %> <% if submitter&.email.present? %>
<div class="flex items-center space-x-1 mt-1"> <div class="flex items-center space-x-1 mt-1">
<%= svg_icon('mail', class: 'w-5 h-5') %> <%= svg_icon('mail', class: 'w-5 h-5 shrink-0') %>
<span> <span class="truncate">
<%= submitter.email || 'N/A' %> <%= submitter.email || 'N/A' %>
</span> </span>
</div> </div>
<% end %> <% end %>
<% if submitter&.phone.present? %> <% if submitter&.phone.present? %>
<div class="flex items-center space-x-1 mt-1"> <div class="flex items-center space-x-1 mt-1">
<%= svg_icon('phone', class: 'w-5 h-5') %> <%= svg_icon('phone', class: 'w-5 h-5 shrink-0') %>
<span> <span class="truncate">
<%= submitter.phone %> <%= submitter.phone %>
</span> </span>
</div> </div>
<% end %> <% end %>
<div class="flex items-center space-x-1 mt-1"> <div class="flex items-center space-x-1 mt-1">
<% if submitter&.viewer? %> <% if submitter&.viewer? %>
<%= svg_icon(submitter.opened_at? ? 'file_check' : 'file_unknown', class: 'w-5 h-5') %> <%= svg_icon(submitter.opened_at? ? 'file_check' : 'file_unknown', class: 'w-5 h-5 shrink-0') %>
<% elsif submitter && !submitter.completed_at? && @submission.completed_at? %> <% elsif submitter && !submitter.completed_at? && @submission.completed_at? %>
<%= svg_icon('writing_off', class: 'w-5 h-5') %> <%= svg_icon('writing_off', class: 'w-5 h-5 shrink-0') %>
<% elsif @submission.expire_at? && submitter && !submitter.completed_at? %> <% elsif @submission.expire_at? && submitter && !submitter.completed_at? %>
<%= svg_icon('clock_exclamation', class: 'w-5 h-5') %> <%= svg_icon('clock_exclamation', class: 'w-5 h-5 shrink-0') %>
<% else %> <% else %>
<%= svg_icon('writing', class: 'w-5 h-5') %> <%= svg_icon('writing', class: 'w-5 h-5 shrink-0') %>
<% end %> <% end %>
<span> <span class="truncate">
<% if submitter&.declined_at? %> <% if submitter&.declined_at? %>
<%= t('declined_on_time', time: l(submitter.declined_at.in_time_zone(@submission.account.timezone), format: :short, locale: @submission.account.locale)) %> <%= t('declined_on_time', time: l(submitter.declined_at.in_time_zone(@submission.account.timezone), format: :short, locale: @submission.account.locale)) %>
<% elsif submitter&.viewer? %> <% elsif submitter&.viewer? %>
@@ -320,6 +325,7 @@
<% value = values[field['uuid']].presence || (field['default_value'] != '{{date}}' && field['readonly'] == true && field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %> <% value = values[field['uuid']].presence || (field['default_value'] != '{{date}}' && field['readonly'] == true && field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
<% next if value.blank? %> <% next if value.blank? %>
<% next if field['type'] == 'heading' || field['type'] == 'strikethrough' %> <% next if field['type'] == 'heading' || field['type'] == 'strikethrough' %>
<% next if field['type'] == 'signature' && submitter && !submitter.completed_at? %>
<div class="pt-2.5 border-b border-base-300"> <div class="pt-2.5 border-b border-base-300">
<div class="text-xs font-medium uppercase mb-0.5" dir="auto"> <div class="text-xs font-medium uppercase mb-0.5" dir="auto">
<%= field['name'].presence || "#{t("#{field['type']}_field")} #{submitter_field_counters[field['type']]}" %> <%= field['name'].presence || "#{t("#{field['type']}_field")} #{submitter_field_counters[field['type']]}" %>
@@ -388,7 +394,7 @@
</label> </label>
</toggle-visible> </toggle-visible>
<% end %> <% end %>
</main> </div>
<% unless request.headers['HTTP_X_TURBO_REQUEST_ID'] %> <% unless request.headers['HTTP_X_TURBO_REQUEST_ID'] %>
<%= render 'scripts/autosize_field' %> <%= render 'scripts/autosize_field' %>
<% end %> <% end %>
+1 -1
View File
@@ -1,6 +1,6 @@
<% filter_params = params.permit(:q, :archived, *Submissions::Filter::ALLOWED_PARAMS) %> <% filter_params = params.permit(:q, :archived, *Submissions::Filter::ALLOWED_PARAMS) %>
<%= render 'shared/turbo_modal', title: t('export'), close_after_submit: false do %> <%= render 'shared/turbo_modal', title: t('export'), close_after_submit: false do %>
<div class="space-y-2"> <div class="space-y-2 <%= 'mt-1.5 px-4' if turbo_native_android? %>">
<%= button_to template_submissions_export_index_path(@template), params: { format: :xlsx, **filter_params }, method: :get, data: { turbo_frame: :_top } do %> <%= button_to template_submissions_export_index_path(@template), params: { format: :xlsx, **filter_params }, method: :get, data: { turbo_frame: :_top } do %>
<div class="flex items-center p-4 text-left rounded-2xl border border-neutral-300 hover:cursor-pointer hover:bg-neutral hover:text-gray-300"> <div class="flex items-center p-4 text-left rounded-2xl border border-neutral-300 hover:cursor-pointer hover:bg-neutral hover:text-gray-300">
<div class="enabled"> <div class="enabled">
Loaded 100 of 169 files, more files were not shown because too many files have changed in this diff. Show more