Compare commits

...
155 Commits
Author SHA1 Message Date
Alex Turchyn 6e2430e5a2 Merge from docusealco/wip 2026-09-14 15:54:52 +03:00
Pete Matsyburka 85fbae1813 validate webhook header 2026-09-14 15:07:56 +03:00
Pete Matsyburka 9941122d90 adjust i18n 2026-09-14 11:08:40 +03:00
Pete Matsyburka 71be6db358 adjust delegated values 2026-09-12 18:34:39 +03:00
Pete Matsyburka 994c1b3155 keep pasted field roles 2026-09-12 18:29:56 +03:00
Pete Matsyburka 92b297a620 fix friendly name 2026-09-11 13:00:26 +03:00
Alex Turchyn bd03498b0f adjust show archived 2026-09-11 09:56:10 +03:00
Pete Matsyburka 660b24710e do not email archived user 2026-09-10 19:17:27 +03:00
Pete Matsyburka 1416cf72e8 sms retry 30 seconds 2026-09-10 15:29:27 +03:00
Alex Turchyn ae7b0ce608 fix settings collapse spacing 2026-09-09 16:09:19 +03:00
Pete Matsyburka a3828ec390 xlsx write native boolean 2026-09-09 13:35:20 +03:00
Pete Matsyburka b8c1691bd7 optimize xlsx export 2026-09-09 12:46:28 +03:00
Pete Matsyburka 4b00c896e4 reset signature on delegate 2026-09-08 18:52:12 +03:00
Pete Matsyburka 781da32a07 replace filename slashes 2026-09-08 13:53:35 +03:00
Pete Matsyburka 307036eae6 fix invite party 2026-09-08 09:12:47 +03:00
Pete Matsyburka 9944a9b1a5 adjust bind address 2026-09-07 17:57:07 +03:00
Alex Turchyn 11856a17ac Merge from docusealco/wip 2026-09-07 12:53:23 +03:00
Pete Matsyburka 7b4c74d711 safe page cycle 2026-09-06 19:25:34 +03:00
Pete Matsyburka e0b0d4e803 add authorize 2026-09-06 19:25:34 +03:00
Pete Matsyburka ec04a95e36 fix pdf validation 2026-09-06 09:58:19 +03:00
Pete Matsyburka 887d80f6d7 raw page size 2026-09-05 13:09:09 +03:00
Pete Matsyburka 9ad281d457 handle 2fa email bounce 2026-09-04 09:01:51 +03:00
Pete Matsyburka e316d2006f subject only email message 2026-09-03 18:59:33 +03:00
Pete Matsyburka f9669b0e16 process pdf v2 2026-09-03 11:48:54 +03:00
Pete Matsyburka 785eb789ad fix undo autosave false 2026-09-02 20:42:08 +03:00
Pete Matsyburka 436d947f6a detect fields autosave false 2026-09-02 20:08:42 +03:00
Pete Matsyburka 033ce3a169 fix text align 2026-09-02 20:02:57 +03:00
Alex Turchyn c216e43d24 Merge from docusealco/wip 2026-08-31 11:35:27 +03:00
Pete Matsyburka d2373c1578 fix normalize email 2026-08-31 10:13:17 +03:00
Pete Matsyburka 474603e1c9 hide list form buttons 2026-08-30 18:51:35 +03:00
Pete Matsyburka 1fc1444b8f optimize load bmp 2026-08-30 15:49:54 +03:00
Pete Matsyburka 47361da26f adjust validate required 2026-08-30 15:36:49 +03:00
Pete Matsyburka 11dea042ad adjust test mode auth 2026-08-30 12:07:05 +03:00
Pete Matsyburka 8756931638 render locked page 2026-08-30 12:01:54 +03:00
Pete Matsyburka 180b201dcf fix mfa rate limit 2026-08-30 12:01:54 +03:00
Pete Matsyburka a1c30c2093 update tools verify 2026-08-30 11:04:45 +03:00
Pete Matsyburka 66bf6af7ba adjust verify pdf signature 2026-08-30 10:56:34 +03:00
Pete Matsyburka 1a65e7bcff adjust download route 2026-08-29 21:53:01 +03:00
Pete Matsyburka 59b5f03948 fix buttons order 2026-08-29 20:18:34 +03:00
Pete Matsyburka 4626518c83 fix search 2026-08-29 20:06:14 +03:00
Pete Matsyburka c18e263f34 adjust verify pdf signature 2026-08-29 20:06:13 +03:00
Pete Matsyburka c3dae1bfe4 add size checks 2026-08-29 16:29:52 +03:00
Pete Matsyburka b44152fd52 fix search index 2026-08-29 16:29:52 +03:00
Pete Matsyburka 2889a7c937 squish name 2026-08-29 16:29:52 +03:00
Pete Matsyburka 91e65e4a19 adjust prefill 2026-08-29 16:29:52 +03:00
Pete Matsyburka 2a1a2cbd82 fix generation 2026-08-28 19:15:48 +03:00
Pete Matsyburka 99b3247cf7 archive account 2026-08-25 18:07:48 +03:00
Pete Matsyburka fa91c6d6be fix mobile filters 2026-08-25 15:12:19 +03:00
Alex Turchyn 27a0d10525 Merge from docusealco/wip 2026-08-25 13:46:49 +03:00
Pete Matsyburka d25f0812ca reset decline values 2026-08-24 16:38:05 +03:00
Pete Matsyburka 9e2aa6f8c8 pass fetch options 2026-08-24 14:43:13 +03:00
Pete Matsyburka 11c76bf554 refactor 2026-08-24 09:47:34 +03:00
Pete Matsyburka e5733d88c0 remove uuids scope 2026-08-23 22:15:02 +03:00
Pete Matsyburka 6d38c9f375 adjust resubmit 2026-08-23 21:23:27 +03:00
Pete Matsyburka 91ec29ad2c add i18n 2026-08-23 19:23:05 +03:00
Pete Matsyburka d374c7fa10 fix log 2026-08-23 19:04:18 +03:00
Pete Matsyburka ca0daf6d0b normalize 2fa email 2026-08-23 18:52:12 +03:00
Pete Matsyburka 076c3d805a fix find_or_initialize_submitter 2026-08-23 18:49:32 +03:00
Pete Matsyburka 1f149ac67a new form if fields changed 2026-08-23 18:01:00 +03:00
Pete Matsyburka 616f7b4fd8 adjsut resubmit 2026-08-23 17:48:04 +03:00
Pete Matsyburka 6f4527038b fix redirect 2026-08-23 17:38:17 +03:00
Pete Matsyburka 8ed2883504 adjust email 2fa form 2026-08-23 17:34:17 +03:00
Pete Matsyburka 651b84a460 fix redirect status 2026-08-23 17:24:44 +03:00
Pete Matsyburka 73c4f158ff add self source 2026-08-23 17:19:50 +03:00
Pete Matsyburka 6f1cc34433 adjust log 2026-08-23 16:51:21 +03:00
Pete Matsyburka a77029ffcb start form remember session 2026-08-23 16:51:15 +03:00
Pete Matsyburka 0480572ef5 refactor start form 2026-08-23 15:45:43 +03:00
Pete Matsyburka 807463c465 optimize shared link 2026-08-23 15:45:43 +03:00
Pete Matsyburka 0f61053976 remove secrets yml 2026-08-23 11:50:12 +03:00
Pete Matsyburka b8cf525034 fix cycle safe 2026-08-21 18:56:22 +03:00
Pete Matsyburka fef203daed adjust notification settings 2026-08-21 17:45:19 +03:00
Pete Matsyburka 41b38cc594 fix js error on redirect 2026-08-21 12:00:29 +03:00
Pete Matsyburka 4e4354af99 fix RTL reverse 2026-08-20 17:26:54 +03:00
Pete Matsyburka 113b61588b process pdf from doc 2026-08-20 13:21:24 +03:00
Pete Matsyburka c1aac6b763 add stats 2026-08-19 12:58:24 +03:00
Alex Turchyn f85fd3cff7 add personalization tooltips 2026-08-18 14:24:55 +03:00
Alex Turchyn 004a22c1c8 Merge from docusealco/wip 2026-08-17 12:31:40 +03:00
Pete Matsyburka 69343b1bdd fix send completed emails 2026-08-17 09:16:20 +03:00
Pete Matsyburka 81a306928c fix ci 2026-08-16 10:17:48 +03:00
Alex Turchyn 2bf3d42ed6 add translations 2026-08-16 09:51:41 +03:00
Pete Matsyburka eec08266c9 fix page preview open 2026-08-16 09:45:04 +03:00
Pete Matsyburka cbbc75b485 fix CI 2026-08-15 22:09:08 +03:00
Pete Matsyburka 3d93bbe5aa improve flatten 2026-08-15 21:08:51 +03:00
Pete Matsyburka 5308e3e15b process attachment v2 2026-08-15 21:08:35 +03:00
Pete Matsyburka 22c146d564 adjust pdfium 2026-08-15 19:08:54 +03:00
Pete Matsyburka c8d3af9841 import acro form 2026-08-15 19:08:54 +03:00
Pete Matsyburka ee1d45d461 update dep 2026-08-15 19:08:54 +03:00
Pete Matsyburka 27d075ed63 optimize merge pdf 2026-08-15 19:08:54 +03:00
Pete Matsyburka df63acf0c8 optimize decrypt 2026-08-15 19:08:52 +03:00
Alex Turchyn 185827c951 add Pdfium acro fields detector 2026-08-14 15:29:41 +03:00
Pete Matsyburka e361cbabf4 adjust verify signature 2026-08-14 15:29:41 +03:00
Pete Matsyburka e867bf5c48 use json serializer 2026-08-14 12:16:25 +03:00
Pete Matsyburka 726d0ea4f3 validate url 2026-08-13 15:46:43 +03:00
Pete Matsyburka 90be34b243 update gem 2026-08-11 22:05:14 +03:00
Pete Matsyburka e87c06d811 uniq bcc addresses 2026-08-11 17:23:28 +03:00
Alex Turchyn 54454d1dfc Merge from docusealco/wip 2026-08-11 17:16:45 +03:00
Pete Matsyburka 726174deff authorize reminder config 2026-08-11 08:55:17 +03:00
Pete Matsyburka 52eb8ac863 add log 2026-08-10 15:21:22 +03:00
Pete Matsyburka 97d91d8f27 fix view only download 2026-08-10 12:06:42 +03:00
Pete Matsyburka dd358a8401 fix infinite loop 2026-08-10 11:48:05 +03:00
Pete Matsyburka 8e254b7d3c fix infinite loop 2026-08-10 06:01:20 +03:00
Pete Matsyburka b9018fa442 add completed download scope 2026-08-09 20:14:31 +03:00
Pete Matsyburka 09310f3690 guard save template message 2026-08-09 19:53:19 +03:00
Pete Matsyburka 2dd38260ba guard email 2fa send 2026-08-09 19:51:07 +03:00
Pete Matsyburka ffb3247cdc add authorize user 2026-08-09 18:37:53 +03:00
Pete Matsyburka c59c470321 sanitize search query 2026-08-09 16:01:12 +03:00
Pete Matsyburka f35510ffd6 adjust with reason 2026-08-08 09:00:35 +03:00
Pete Matsyburka eeec852b3b adjust preview cache 2026-08-07 21:54:55 +03:00
Pete Matsyburka 485b7f2c8f fix dynamic variable parse 2026-08-07 14:19:28 +03:00
Alex Turchyn b6498ec049 add PayPal translations 2026-08-07 10:12:52 +03:00
Pete Matsyburka dc1de72e18 fix mysql migrations 2026-08-06 17:50:03 +03:00
Pete Matsyburka 7dddef4c94 fix date time formats 2026-08-04 11:07:13 +03:00
Alex Turchyn a2d8b85549 Merge from docusealco/wip 2026-08-03 12:53:50 +03:00
Pete Matsyburka 4d68783b82 add counters 2026-08-03 10:05:09 +03:00
Pete Matsyburka b05c3b2030 fix autocomplete 2026-08-03 09:41:08 +03:00
Pete Matsyburka 784ae9aa4b fix dynamic document preview 2026-08-03 08:19:35 +03:00
Pete Matsyburka 03b5956cbc bcc limit 2026-08-03 07:42:53 +03:00
Pete Matsyburka 2876c9a18e adjust pagination 2026-08-01 08:38:49 +03:00
Pete Matsyburka c4b7be3925 fix mobile tabs 2026-07-31 12:46:34 +03:00
Pete Matsyburka e2ffbdac1d update gem 2026-07-31 11:43:40 +03:00
Pete Matsyburka 0d197bb2eb fix mobile modal 2026-07-31 10:57:37 +03:00
Pete Matsyburka 55089bee7f adjust pagination 2026-07-31 10:53:34 +03:00
Pete Matsyburka b96fe74db1 adjust filters 2026-07-31 10:46:24 +03:00
Pete Matsyburka 2fc1ba3037 fix layout 2026-07-31 09:23:18 +03:00
Pete Matsyburka c67d37207e update deps 2026-07-30 20:51:46 +03:00
Pete Matsyburka dba50cd1bf mobile fixes 2026-07-30 20:21:40 +03:00
Pete Matsyburka edf2c0d9ed update gem 2026-07-30 18:47:44 +03:00
Pete Matsyburka 35f7c36f28 rename settings index 2026-07-30 18:13:55 +03:00
Pete Matsyburka 3a24ecac63 Revert "break long names in template and folder titles"
This reverts commit 59fe8a7917.
2026-07-30 18:13:55 +03:00
Pete Matsyburka 50ec7682b5 fix open modal mobile 2026-07-30 18:13:54 +03:00
Pete Matsyburka 3667993caf center settings footer 2026-07-30 16:54:36 +03:00
Alex Turchyn 68e8cd1188 fix settings nav offset 2026-07-30 16:45:56 +03:00
Alex Turchyn 3eebf6d2cf unify tooltip and breakpoint classes 2026-07-30 16:45:56 +03:00
Alex Turchyn 34d0e00910 use numeric keyboard for otp code inputs 2026-07-30 16:45:56 +03:00
Alex Turchyn b8ab7190d2 use dvh for modal and drawer 2026-07-30 16:45:56 +03:00
Alex Turchyn 59fe8a7917 break long names in template and folder titles 2026-07-30 16:45:56 +03:00
Alex Turchyn d56070827b fix broken svg 2026-07-30 16:45:56 +03:00
Alex Turchyn 74dbd4bed3 fix trusted signature text 2026-07-30 16:45:56 +03:00
Alex Turchyn 807e82b21e open modals as pages on mobile devices 2026-07-30 16:45:56 +03:00
Alex Turchyn 66862e93c8 unify breakpoint classes 2026-07-30 16:45:56 +03:00
Alex Turchyn 270461ad3b add mobile filters 2026-07-30 16:45:56 +03:00
Alex Turchyn 3edd1ab32d fix invalid classes and html attributes 2026-07-30 16:45:56 +03:00
Alex Turchyn e77998e53c scale down settings titles on mobile 2026-07-30 16:45:56 +03:00
Alex Turchyn 1421bc2bce fix horizontal scroll from preferences tooltip 2026-07-30 16:45:56 +03:00
Alex Turchyn cbbbfdda80 add mobile settings navigation 2026-07-30 16:45:56 +03:00
Alex Turchyn 2d510081be fix esign signature row layout 2026-07-30 16:45:56 +03:00
Alex Turchyn 704893ee1d show view archived link in mobile pagination 2026-07-30 16:45:56 +03:00
Alex Turchyn 88612913a6 wrap smtp security radios on small screens 2026-07-30 16:45:56 +03:00
Alex Turchyn ce6823ed38 hide tooltip wrappers with hidden content 2026-07-30 16:45:56 +03:00
Alex Turchyn 9074d23b3c fix uneven template action buttons 2026-07-30 16:45:56 +03:00
Alex Turchyn 76d0e76fd5 no touch tooltips on nav elements 2026-07-30 16:45:56 +03:00
Pete Matsyburka 5ec8e4ccc7 text formula 2026-07-30 11:55:59 +03:00
Alex TurchynandPete Matsyburka b8c29324b8 paypal configs
Co-authored-by: Pete Matsyburka <pete@docuseal.com>
2026-07-30 07:56:07 +03:00
Pete Matsyburka 972f38242e update rails 2026-07-29 21:53:03 +03:00
Pete Matsyburka 2c6ddd7f90 rate limit remove 2fa 2026-07-27 15:48:04 +03:00
180 changed files with 4278 additions and 1608 deletions

No files matched your search

+19 -25
View File
@@ -111,48 +111,39 @@ jobs:
rspec:
name: RSpec
runs-on: ubuntu-latest
timeout-minutes: 10
timeout-minutes: 15
container:
image: ruby:4.0.5-alpine
services:
postgres:
image: postgres:14
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: docuseal_test
ports: ["5432:5432"]
options: >-
--health-cmd pg_isready
--health-cmd "pg_isready -U postgres -d docuseal_test"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Install packages
run: apk add --no-cache build-base git libpq-dev yaml-dev nodejs yarn tar zstd gzip wget unzip vips leptonica chromium
- uses: actions/checkout@v4
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 4.0.5
- name: Set up Node
uses: actions/setup-node@v1
with:
node-version: 20.19.0
- name: Install Chrome
uses: browser-actions/setup-chrome@latest
with:
chrome-version: 125
- name: Cache node_modules
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
key: ${{ runner.os }}-musl-node-${{ hashFiles('**/yarn.lock') }}
- name: Cache gems
uses: actions/cache@v4
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
key: ${{ runner.os }}-musl-gem-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gem-
${{ runner.os }}-musl-gem-
- name: Install dependencies
env:
RAILS_ENV: test
@@ -161,17 +152,20 @@ jobs:
bundle config path vendor/bundle
bundle install --jobs 4 --retry 4
yarn install
sudo apt-get update
sudo apt-get install -y libvips liblept5
wget -O pdfium-linux.tgz "https://github.com/bblanchon/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz"
sudo tar -xzf pdfium-linux.tgz --strip-components=1 -C /usr/lib lib/libpdfium.so
rm -f pdfium-linux.tgz
wget -O pdfium-linux.zip "https://github.com/docusealco/pdfium-binaries/releases/download/20260813/pdfium-musl-$(uname -m).zip"
case "$(uname -m)" in
x86_64) echo "c5c7dde243ecb66ab0819c8193515ef38ad53549fe260f3c2dfd93ea56eda2e7 pdfium-linux.zip" ;;
aarch64) echo "64c4483449b1b4dccc696ad0c5c96e0b7f74dcc57b4f23c676b7a70671b0bbb5 pdfium-linux.zip" ;;
esac | sha256sum -c -
unzip -q pdfium-linux.zip -d /tmp/pdfium-linux
cp /tmp/pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so
rm -rf pdfium-linux.zip /tmp/pdfium-linux
- name: Run
env:
RAILS_ENV: test
NODE_ENV: test
COVERAGE: true
DATABASE_URL: postgres://postgres:postgres@localhost:5432/docuseal_test
DATABASE_URL: postgres://postgres:postgres@postgres:5432/docuseal_test
run: |
bundle exec rake db:create
bundle exec rake db:migrate
+3 -3
View File
@@ -9,10 +9,10 @@ RUN apk --no-cache add wget unzip && \
wget https://raw.githubusercontent.com/impallari/DancingScript/master/OFL.txt && \
wget https://raw.githubusercontent.com/notofonts/noto-fonts/refs/heads/main/LICENSE && \
wget -O /model.onnx "https://github.com/docusealco/fields-detection/releases/download/2.0.0/model_704_int8.onnx" && \
wget -O pdfium-linux.zip "https://github.com/docusealco/pdfium-binaries/releases/download/20260613/pdfium-musl-$(uname -m).zip" && \
wget -O pdfium-linux.zip "https://github.com/docusealco/pdfium-binaries/releases/download/20260813/pdfium-musl-$(uname -m).zip" && \
case "$(uname -m)" in \
x86_64) echo "2c953ff72ee2dda07e7fc577e25841cc3d6464468a7c5adfaea574efcbc3b90b pdfium-linux.zip" ;; \
aarch64) echo "23bbe287d2753fdb05741c7660647eb0ef0d2e4da2ce0722bfa9d9d455bd64e2 pdfium-linux.zip" ;; \
x86_64) echo "c5c7dde243ecb66ab0819c8193515ef38ad53549fe260f3c2dfd93ea56eda2e7 pdfium-linux.zip" ;; \
aarch64) echo "64c4483449b1b4dccc696ad0c5c96e0b7f74dcc57b4f23c676b7a70671b0bbb5 pdfium-linux.zip" ;; \
esac | sha256sum -c - && \
mkdir -p /pdfium-linux && \
unzip -q pdfium-linux.zip -d /pdfium-linux
+2 -1
View File
@@ -36,7 +36,7 @@ gem 'rotp'
gem 'rouge', require: false
gem 'rqrcode', require: false
gem 'ruby-vips'
gem 'rubyXL', require: false
gem 'rubyzip', require: false
gem 'shakapacker'
gem 'sidekiq'
gem 'sqlite3', require: false
@@ -45,6 +45,7 @@ gem 'trilogy', require: false
gem 'turbo-rails'
gem 'twitter_cldr', require: false
gem 'tzinfo-data'
gem 'xlsxtream', require: false
group :development, :test do
gem 'better_html'
+84 -82
View File
@@ -1,31 +1,31 @@
GEM
remote: https://rubygems.org/
specs:
action_text-trix (2.1.18)
action_text-trix (2.1.19)
railties
actioncable (8.1.3)
actionpack (= 8.1.3)
activesupport (= 8.1.3)
actioncable (8.1.3.1)
actionpack (= 8.1.3.1)
activesupport (= 8.1.3.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (8.1.3)
actionpack (= 8.1.3)
activejob (= 8.1.3)
activerecord (= 8.1.3)
activestorage (= 8.1.3)
activesupport (= 8.1.3)
actionmailbox (8.1.3.1)
actionpack (= 8.1.3.1)
activejob (= 8.1.3.1)
activerecord (= 8.1.3.1)
activestorage (= 8.1.3.1)
activesupport (= 8.1.3.1)
mail (>= 2.8.0)
actionmailer (8.1.3)
actionpack (= 8.1.3)
actionview (= 8.1.3)
activejob (= 8.1.3)
activesupport (= 8.1.3)
actionmailer (8.1.3.1)
actionpack (= 8.1.3.1)
actionview (= 8.1.3.1)
activejob (= 8.1.3.1)
activesupport (= 8.1.3.1)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (8.1.3)
actionview (= 8.1.3)
activesupport (= 8.1.3)
actionpack (8.1.3.1)
actionview (= 8.1.3.1)
activesupport (= 8.1.3.1)
nokogiri (>= 1.8.5)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
@@ -33,36 +33,36 @@ GEM
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (8.1.3)
actiontext (8.1.3.1)
action_text-trix (~> 2.1.15)
actionpack (= 8.1.3)
activerecord (= 8.1.3)
activestorage (= 8.1.3)
activesupport (= 8.1.3)
actionpack (= 8.1.3.1)
activerecord (= 8.1.3.1)
activestorage (= 8.1.3.1)
activesupport (= 8.1.3.1)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (8.1.3)
activesupport (= 8.1.3)
actionview (8.1.3.1)
activesupport (= 8.1.3.1)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (8.1.3)
activesupport (= 8.1.3)
activejob (8.1.3.1)
activesupport (= 8.1.3.1)
globalid (>= 0.3.6)
activemodel (8.1.3)
activesupport (= 8.1.3)
activerecord (8.1.3)
activemodel (= 8.1.3)
activesupport (= 8.1.3)
activemodel (8.1.3.1)
activesupport (= 8.1.3.1)
activerecord (8.1.3.1)
activemodel (= 8.1.3.1)
activesupport (= 8.1.3.1)
timeout (>= 0.4.0)
activestorage (8.1.3)
actionpack (= 8.1.3)
activejob (= 8.1.3)
activerecord (= 8.1.3)
activesupport (= 8.1.3)
activestorage (8.1.3.1)
actionpack (= 8.1.3.1)
activejob (= 8.1.3.1)
activerecord (= 8.1.3.1)
activesupport (= 8.1.3.1)
marcel (~> 1.0)
activesupport (8.1.3)
activesupport (8.1.3.1)
base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
@@ -144,7 +144,7 @@ GEM
cldr-plurals-runtime-rb (1.1.0)
cmdparse (3.0.7)
coderay (1.1.3)
concurrent-ruby (1.3.7)
concurrent-ruby (1.3.8)
connection_pool (3.0.2)
crack (1.0.1)
bigdecimal
@@ -179,7 +179,7 @@ GEM
dotenv (3.2.0)
drb (2.2.3)
email_typo (0.2.3)
erb (6.0.4)
erb (6.0.6)
erb_lint (0.9.0)
activesupport
better_html (>= 2.0.1)
@@ -217,7 +217,7 @@ GEM
foreman (0.90.0)
thor (~> 1.4)
geom2d (0.4.1)
globalid (1.3.0)
globalid (1.4.0)
activesupport (>= 6.1)
google-apis-core (1.0.2)
addressable (~> 2.8, >= 2.8.7)
@@ -262,7 +262,7 @@ GEM
geom2d (~> 0.4, >= 0.4.1)
openssl (>= 2.2.1)
strscan (>= 3.1.2)
i18n (1.14.8)
i18n (1.15.2)
concurrent-ruby (~> 1.0)
io-console (0.8.2)
irb (1.18.0)
@@ -271,7 +271,7 @@ GEM
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jmespath (1.6.2)
json (2.19.9)
json (2.21.2)
jwt (3.2.0)
base64
language_server-protocol (3.17.0.5)
@@ -296,24 +296,24 @@ GEM
loofah (2.25.2)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.9.0)
mail (2.9.1)
logger
mini_mime (>= 0.1.1)
net-imap
net-pop
net-smtp
marcel (1.1.0)
marcel (1.2.1)
matrix (0.4.3)
method_source (1.1.0)
mini_mime (1.1.5)
minitest (6.0.6)
drb (~> 2.0)
prism (~> 1.5)
msgpack (1.8.0)
msgpack (1.8.4)
multi_json (1.19.1)
net-http (0.9.1)
uri (>= 0.11.1)
net-imap (0.6.4.1)
net-imap (0.6.6)
date
net-protocol
net-pop (0.1.2)
@@ -344,7 +344,7 @@ GEM
orm_adapter (0.5.0)
os (1.1.4)
package_json (0.2.0)
pagy (43.4.4)
pagy (43.6.1)
json
uri
yaml
@@ -357,7 +357,7 @@ GEM
pg (1.6.3-arm64-darwin)
pg (1.6.3-x86_64-linux)
pg (1.6.3-x86_64-linux-musl)
pp (0.6.3)
pp (0.6.4)
prettyprint
pretender (1.0.0)
actionpack (>= 7.2)
@@ -369,9 +369,6 @@ GEM
reline (>= 0.6.0)
pry-rails (0.3.11)
pry (>= 0.13.0)
psych (5.3.1)
date
stringio
public_suffix (7.0.5)
puma (8.0.2)
nio4r (~> 2.0)
@@ -386,20 +383,20 @@ GEM
rack (>= 1.3)
rackup (2.3.1)
rack (>= 3)
rails (8.1.3)
actioncable (= 8.1.3)
actionmailbox (= 8.1.3)
actionmailer (= 8.1.3)
actionpack (= 8.1.3)
actiontext (= 8.1.3)
actionview (= 8.1.3)
activejob (= 8.1.3)
activemodel (= 8.1.3)
activerecord (= 8.1.3)
activestorage (= 8.1.3)
activesupport (= 8.1.3)
rails (8.1.3.1)
actioncable (= 8.1.3.1)
actionmailbox (= 8.1.3.1)
actionmailer (= 8.1.3.1)
actionpack (= 8.1.3.1)
actiontext (= 8.1.3.1)
actionview (= 8.1.3.1)
activejob (= 8.1.3.1)
activemodel (= 8.1.3.1)
activerecord (= 8.1.3.1)
activestorage (= 8.1.3.1)
activesupport (= 8.1.3.1)
bundler (>= 1.15.0)
railties (= 8.1.3)
railties (= 8.1.3.1)
rails-dom-testing (2.3.0)
activesupport (>= 5.0.0)
minitest
@@ -410,9 +407,9 @@ GEM
rails-i18n (8.1.0)
i18n (>= 0.7, < 2)
railties (>= 8.0.0, < 9)
railties (8.1.3)
actionpack (= 8.1.3)
activesupport (= 8.1.3)
railties (8.1.3.1)
actionpack (= 8.1.3.1)
activesupport (= 8.1.3.1)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
@@ -421,9 +418,14 @@ GEM
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.4.2)
rdoc (7.2.0)
rbs (4.1.0)
logger
prism (>= 1.6.0)
tsort
rdoc (8.0.0)
erb
psych (>= 4.0.0)
prism (>= 1.6.0)
rbs (>= 4.0.0)
tsort
redis-client (0.29.0)
connection_pool
@@ -495,9 +497,6 @@ GEM
ruby-vips (2.3.0)
ffi (~> 1.12)
logger
rubyXL (3.4.35)
nokogiri (>= 1.10.8)
rubyzip (>= 3.2.2)
rubyzip (3.2.2)
securerandom (0.4.1)
semantic_range (3.1.1)
@@ -525,12 +524,11 @@ GEM
simplecov-html (0.13.2)
simplecov_json_formatter (0.1.4)
smart_properties (1.17.0)
sqlite3 (2.9.2-aarch64-linux-gnu)
sqlite3 (2.9.2-aarch64-linux-musl)
sqlite3 (2.9.2-arm64-darwin)
sqlite3 (2.9.2-x86_64-linux-gnu)
sqlite3 (2.9.2-x86_64-linux-musl)
stringio (3.2.0)
sqlite3 (2.9.5-aarch64-linux-gnu)
sqlite3 (2.9.5-aarch64-linux-musl)
sqlite3 (2.9.5-arm64-darwin)
sqlite3 (2.9.5-x86_64-linux-gnu)
sqlite3 (2.9.5-x86_64-linux-musl)
strip_attributes (2.0.1)
activemodel (>= 3.0, < 9.0)
strscan (3.1.8)
@@ -570,14 +568,17 @@ GEM
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.9.2)
websocket-driver (0.8.0)
websocket-driver (0.8.2)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xlsxtream (3.1.0)
zip_kit (>= 6.2, < 7)
xpath (3.2.0)
nokogiri (~> 1.8)
yaml (0.4.0)
zeitwerk (2.7.5)
zeitwerk (2.8.2)
zip_kit (6.3.4)
PLATFORMS
aarch64-linux
@@ -637,7 +638,7 @@ DEPENDENCIES
rubocop-rails
rubocop-rspec
ruby-vips
rubyXL
rubyzip
shakapacker
sidekiq
simplecov
@@ -649,6 +650,7 @@ DEPENDENCIES
tzinfo-data
web-console
webmock
xlsxtream
RUBY VERSION
ruby 4.0.5
@@ -37,7 +37,7 @@ class AccountConfigsController < ApplicationController
end
def destroy
raise InvalidKey unless allowed_keys.include?(@account_config.key)
raise InvalidKey unless allowed_destroy_keys.include?(@account_config.key)
@account_config.destroy!
@@ -50,6 +50,10 @@ class AccountConfigsController < ApplicationController
ALLOWED_KEYS
end
def allowed_destroy_keys
ALLOWED_KEYS
end
def load_account_config
raise InvalidKey unless allowed_keys.include?(account_config_params[:key])
+13 -9
View File
@@ -23,17 +23,20 @@ class AccountsController < ApplicationController
unless Docuseal.multitenant?
@encrypted_config = EncryptedConfig.find_or_initialize_by(account: current_account,
key: EncryptedConfig::APP_URL_KEY)
@encrypted_config.assign_attributes(app_url_params)
unless URI.parse(@encrypted_config.value.to_s).class.in?([URI::HTTP, URI::HTTPS])
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url'))
if can?(:manage, @encrypted_config)
@encrypted_config.assign_attributes(app_url_params)
return render :show, status: :unprocessable_content
unless URI.parse(@encrypted_config.value.to_s).class.in?([URI::HTTP, URI::HTTPS])
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url'))
return render :show, status: :unprocessable_content
end
@encrypted_config.save!
Docuseal.refresh_default_url_options!
end
@encrypted_config.save!
Docuseal.refresh_default_url_options!
end
with_locale do
@@ -47,7 +50,8 @@ class AccountsController < ApplicationController
authorize!(:manage, current_account)
true_user.skip_reconfirmation!
true_user.update!(locked_at: Time.current, email: true_user.email.sub('@', '+removed@'))
true_user.update!(locked_at: Time.current, archived_at: Time.current,
email: true_user.email.sub('@', '+removed@'))
true_user.account.update!(archived_at: Time.current)
# rubocop:disable Layout/LineLength
+6 -7
View File
@@ -17,24 +17,23 @@ module Api
def verify
file = Base64.decode64(params[:file])
pdf = HexaPDF::Document.new(io: StringIO.new(file))
trusted_certs = Accounts.load_trusted_certs(current_account)
is_checksum_found = CompletedDocument.exists?(sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(file)))
render json: {
checksum_status: is_checksum_found ? 'verified' : 'not_found',
signatures: pdf.signatures.map do |sig|
signatures: VerifyPdfSignature.call(StringIO.new(file), trusted_certs).map do |sig|
{
verification_result: sig.verify(trusted_certs:).messages,
signer_name: sig.signer_name,
signing_reason: sig.signing_reason,
verification_result: sig.messages.map { |m| { type: m.status || :info, content: m.text } },
signer_name: sig.common_name,
signing_reason: sig.reason,
signing_time: sig.signing_time,
signature_type: sig.signature_type
signature_type: sig.type
}
end
}
rescue HexaPDF::MalformedPDFError
rescue Pdfium::PdfiumError
render json: { error: 'Malformed PDF' }, status: :unprocessable_content
end
end
+1 -1
View File
@@ -30,7 +30,7 @@ class ApplicationController < ActionController::Base
rescue_from RateLimit::LimitApproached do |e|
Rollbar.error(e) if defined?(Rollbar)
redirect_to request.referer, alert: 'Too many requests', status: :too_many_requests
redirect_back_or_to root_path, alert: 'Too many requests'
end
if Rails.env.production? || Rails.env.test?
+4
View File
@@ -14,6 +14,8 @@ class MfaSetupController < ApplicationController
def edit; end
def create
RateLimit.call("mfa-setup-otp-#{current_user.id}", limit: 5, ttl: 5.minutes, enabled: true)
if current_user.validate_and_consume_otp!(params[:otp_attempt])
current_user.otp_required_for_login = true
current_user.save!
@@ -29,6 +31,8 @@ class MfaSetupController < ApplicationController
end
def destroy
RateLimit.call("mfa-setup-otp-#{current_user.id}", limit: 5, ttl: 5.minutes, enabled: true)
if current_user.validate_and_consume_otp!(params[:otp_attempt])
current_user.update!(otp_required_for_login: false, otp_secret: nil)
@@ -8,6 +8,7 @@ class NotificationsSettingsController < ApplicationController
before_action :build_account_config, only: :create
authorize_resource :account_config, only: :create
before_action :authorize_email_reminders!, only: :create
def index; end
@@ -21,6 +22,14 @@ class NotificationsSettingsController < ApplicationController
private
def authorize_email_reminders!
return unless Docuseal.multitenant?
return if @account_config.key != AccountConfig::SUBMITTER_REMINDERS
return if can?(:manage, :email_reminders)
redirect_back fallback_location: settings_notifications_path, alert: I18n.t('unlock_with_docuseal_pro')
end
def build_account_config
@account_config =
AccountConfig.find_or_initialize_by(account: current_account, key: email_config_params[:key])
@@ -5,6 +5,10 @@ class PreviewDocumentPageController < ActionController::API
FORMAT = Templates::ProcessDocument::FORMAT
TMPFILE_PREFIX = 'attachment-'
TMPFILE_TTL = 5.minutes
TMPFILE_MAX_TOTAL_SIZE = Docuseal.multitenant? ? 400.megabytes : 1.gigabyte
def show
result_data =
ApplicationRecord.signed_id_verifier.verified(params[:signed_key], purpose: :attachment)
@@ -30,34 +34,63 @@ class PreviewDocumentPageController < ActionController::API
allow_other_host: true
end
file_path =
if attachment.service.name == :disk
ActiveStorage::Blob.service.path_for(attachment.key)
else
find_or_create_document_tempfile_path(attachment)
end
preview_image =
Templates::ProcessDocument.generate_pdf_preview_from_file(attachment, file_path, params[:id].to_i)
open_attachment_io(attachment) do |io|
Templates::ProcessDocument.generate_pdf_preview_from_io(attachment, io, params[:id].to_i)
end
redirect_to preview_image.url(time: ActiveStorage::Attachment.service_url_time), allow_other_host: true
end
def find_or_create_document_tempfile_path(attachment)
file_path = "#{Dir.tmpdir}/attachment-#{Digest::SHA1.hexdigest("#{attachment.id}-#{attachment.uuid}")}"
def open_attachment_io(attachment, &)
return File.open(ActiveStorage::Blob.service.path_for(attachment.key), 'rb', &) if attachment.service.name == :disk
File.open(file_path, File::RDWR | File::CREAT, 0o644) do |f|
f.flock(File::LOCK_EX)
file_name = "#{TMPFILE_PREFIX}#{Digest::SHA1.hexdigest("#{attachment.id}-#{attachment.uuid}")}"
file_path = File.join(Dir.tmpdir, file_name)
File.open(file_path, File::RDWR | File::CREAT, 0o644) do |file|
file.flock(File::LOCK_EX)
# rubocop:disable Style/ZeroLengthPredicate
if f.size.zero?
f.binmode
if file.size.zero?
cleanup_stale_tempfiles
f.write(attachment.download)
file.binmode
file.write(attachment.download)
else
FileUtils.touch(file_path)
end
# rubocop:enable Style/ZeroLengthPredicate
end
file_path
file.flock(File::LOCK_UN)
yield file
end
end
def cleanup_stale_tempfiles
entries =
Dir.glob(File.join(Dir.tmpdir, "#{TMPFILE_PREFIX}*")).filter_map do |path|
stat = File.stat(path)
[path, stat.mtime, stat.size]
rescue Errno::ENOENT
nil
end
total_size = entries.sum(&:last)
stale_time = TMPFILE_TTL.ago
entries.sort_by(&:second).each do |path, mtime, size|
break if mtime > stale_time && total_size <= TMPFILE_MAX_TOTAL_SIZE
File.unlink(path)
total_size -= size
rescue Errno::ENOENT
nil
end
end
end
+7
View File
@@ -0,0 +1,7 @@
# frozen_string_literal: true
class SettingsController < ApplicationController
skip_authorization_check
def index; end
end
+48 -133
View File
@@ -8,13 +8,9 @@ class StartFormController < ApplicationController
around_action :with_browser_locale, only: %i[show update completed]
before_action :maybe_redirect_com, only: %i[show completed]
before_action :load_resubmit_submitter, only: :update
before_action :load_template
before_action :authorize_start!, only: :update
COOKIES_TTL = 12.hours
COOKIES_DEFAULTS = { httponly: true, secure: Rails.env.production? }.freeze
def show
if @template.preferences['require_phone_2fa'] || @template.preferences['require_email_2fa']
raise ActionController::RoutingError, I18n.t('not_found')
@@ -23,8 +19,7 @@ class StartFormController < ApplicationController
if @template.shared_link?
@submitter = @template.submissions.new(account_id: @template.account_id)
.submitters.new(account_id: @template.account_id,
uuid: (filter_undefined_submitters(@template).first ||
@template.submitters.first)['uuid'])
uuid: Submitters::StartForm.first_submitter_uuid(@template))
render :email_verification if params[:email_verification]
else
Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar)
@@ -41,24 +36,28 @@ class StartFormController < ApplicationController
if @submitter.completed_at?
redirect_to start_form_completed_path(@template.slug, submitter_params.compact_blank)
else
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record?
if Templates.filter_undefined_submitters(@template.submitters).size > 1 && @submitter.new_record?
@error_message = multiple_submitters_error_message
return render :show, status: :unprocessable_content
end
if (is_new_record = @submitter.new_record?)
assign_submission_attributes(@submitter, @template)
Submitters::StartForm.assign_submission_attributes(
@submitter, @template, ip: request.remote_ip, user_agent: request.user_agent
)
Submissions::AssignDefinedSubmitters.call(@submitter.submission)
else
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
end
if @template.preferences['shared_link_2fa'] == true
handle_require_2fa(@submitter, is_new_record:)
if require_link_2fa?(@template, @submitter)
handle_require_2fa(@template, @submitter)
elsif @submitter.errors.blank? && @submitter.save
enqueue_new_submitter_jobs(@submitter) if is_new_record
Submitters::StartForm.enqueue_new_submitter_jobs(@submitter) if is_new_record
Submitters::StartForm.assign_start_form_cookie(@submitter, request)
redirect_to submit_form_path(@submitter.slug)
else
@@ -88,41 +87,40 @@ class StartFormController < ApplicationController
private
def enqueue_new_submitter_jobs(submitter)
WebhookUrls.enqueue_events(submitter.submission, 'submission.created')
def find_or_initialize_submitter(template, submitter_params)
required_fields = template.preferences.fetch('link_form_fields', ['email'])
SearchEntries.enqueue_reindex(submitter)
blank_fields = required_fields.select { |key| submitter_params[key].blank? }
expire_at = submitter.submission.expire_at
return unless expire_at
ProcessSubmissionExpiredJob.perform_at(expire_at, 'submission_id' => submitter.submission_id,
'expire_at' => expire_at.to_i)
end
def load_resubmit_submitter
@resubmit_submitter =
if params[:resubmit].present? && !params[:resubmit].in?([true, 'true'])
submitter = Submitter.find_by(slug: params[:resubmit])
submitter if submitter && can_resubmit?(submitter)
submitter =
if blank_fields.present?
Submitter.new(submitter_params)
else
Submitters::StartForm.find_or_initialize_submitter(
template, submitter_params, exclude_completed: params[:resubmit].present?,
request:, current_user:
)
end
blank_fields.each { |key| submitter.errors.add(key.to_sym, :blank) }
submitter
end
def can_resubmit?(submitter)
submitter.completed_at? && submitter.completed_at > 14.days.ago &&
%w[api embed mcp].exclude?(submitter.submission.source) &&
submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false
def require_link_2fa?(template, submitter)
return true if template.preferences['shared_link_2fa'] == true
return false if cookies.encrypted[:start_form_slug] == submitter.slug
return false if current_user && submitter.email == current_user.email &&
current_user.account_id == submitter.account_id
!submitter.new_record?
end
def authorize_start!
is_archived = @template.archived_at? || @template.account.archived_at?
return redirect_to submit_form_path(@resubmit_submitter.slug) if @resubmit_submitter && is_archived
return redirect_to start_form_path(@template.slug) if is_archived
return if @resubmit_submitter
return if @template.shared_link? || (current_user && current_ability.can?(:read, @template))
Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar)
@@ -130,90 +128,14 @@ class StartFormController < ApplicationController
redirect_to start_form_path(@template.slug)
end
def find_or_initialize_submitter(template, submitter_params)
required_fields = template.preferences.fetch('link_form_fields', ['email'])
required_params = required_fields.index_with { |key| submitter_params[key] }
find_params = required_params.except('name')
submitter = Submitter.new if find_params.compact_blank.blank?
submitter ||=
Submitter
.where(submission: template.submissions.non_expired.active)
.order(id: :desc)
.where(declined_at: nil)
.where(external_id: nil)
.where(template.preferences['shared_link_2fa'] == true ? {} : { ip: [nil, request.remote_ip] })
.then { |rel| params[:resubmit].present? || params[:selfsign].present? ? rel.where(completed_at: nil) : rel }
.find_or_initialize_by(find_params)
submitter = Submitter.new(find_params) if submitter.submission&.completed_at? && submitter.viewer?
submitter.name = required_params['name'] if submitter.new_record?
unless @resubmit_submitter
required_params.each do |key, value|
submitter.errors.add(key.to_sym, :blank) if value.blank?
end
end
submitter
end
def assign_submission_attributes(submitter, template)
submitter.assign_attributes(
uuid: (filter_undefined_submitters(template).first || @template.submitters.first)['uuid'],
ip: request.remote_ip,
ua: request.user_agent,
values: @resubmit_submitter&.preferences&.fetch('default_values', nil) || {},
preferences: @resubmit_submitter&.preferences.presence || { 'send_email' => true },
metadata: @resubmit_submitter&.metadata.presence || {}
)
submitter.assign_attributes(@resubmit_submitter.slice(:name, :email, :phone)) if @resubmit_submitter
if submitter.values.present?
@resubmit_submitter.attachments.each do |attachment|
submitter.attachments << attachment.dup if submitter.values.value?(attachment.uuid)
end
end
submitter.submission ||= Submission.new(template:,
account_id: template.account_id,
template_submitters: template.submitters,
expire_at: Templates.build_default_expire_at(template),
submitters: [submitter],
source: :link)
Submissions::CreateFromSubmitters.maybe_set_dynamic_documents(submitter.submission)
submitter.account_id = submitter.submission.account_id
submitter
end
def filter_undefined_submitters(template)
Templates.filter_undefined_submitters(template.submitters)
end
def submitter_params
return { 'email' => current_user.email, 'name' => current_user.full_name } if params[:selfsign]
return @resubmit_submitter.slice(:name, :phone, :email) if @resubmit_submitter.present?
params.require(:submitter).permit(:email, :phone, :name).tap do |attrs|
attrs[:email] = Submissions.normalize_email(attrs[:email])
end
end
def load_template
@template =
if @resubmit_submitter
@resubmit_submitter.template
else
Template.find_by!(slug: params[:slug] || params[:start_form_slug])
end
@template = Template.find_by!(slug: params[:slug] || params[:start_form_slug])
end
def multiple_submitters_error_message
@@ -224,38 +146,31 @@ class StartFormController < ApplicationController
end
end
def handle_require_2fa(submitter, is_new_record:)
def handle_require_2fa(template, submitter)
return render :show, status: :unprocessable_content if submitter.errors.present?
is_otp_verified = Submitters.verify_link_otp!(params[:one_time_code], submitter)
if cookies.encrypted[:email_2fa_slug] == submitter.slug || is_otp_verified
if submitter.save
enqueue_new_submitter_jobs(submitter) if is_new_record
if is_otp_verified
SubmissionEvents.create_with_tracking_data(submitter, 'email_verified', request)
cookies.encrypted[:email_2fa_slug] =
{ value: submitter.slug, expires: COOKIES_TTL.from_now, **COOKIES_DEFAULTS }
end
redirect_to submit_form_path(submitter.slug)
else
render :show, status: :unprocessable_content
end
if Submitters::StartForm.verify_2fa_and_save_submitter(submitter, request, is_new_record: submitter.new_record?)
redirect_to submit_form_path(submitter.slug)
else
if defined?(Rollbar) && template.preferences['shared_link_2fa'] != true
Rollbar.info("2FA link requested: #{submitter.id}")
end
Submitters.send_shared_link_email_verification_code(submitter, request:)
render :email_verification
end
rescue Submitters::StartForm::NotSaved
render :show, status: :unprocessable_content
rescue Submitters::BouncedEmail => e
@error_message = e.message
render :error, status: :unprocessable_content
rescue Submitters::UnableToSendCode, Submitters::InvalidOtp => e
redirect_to start_form_path(submitter.submission.template.slug,
params: submitter_params.merge(email_verification: true)),
redirect_to start_form_path(template.slug, params: submitter_params.merge(email_verification: true)),
alert: e.message
rescue RateLimit::LimitApproached
redirect_to start_form_path(submitter.submission.template.slug,
params: submitter_params.merge(email_verification: true)),
redirect_to start_form_path(template.slug, params: submitter_params.merge(email_verification: true)),
alert: I18n.t(:too_many_attempts)
end
end
@@ -1,23 +1,34 @@
# frozen_string_literal: true
class StartFormEmail2faSendController < ApplicationController
layout 'form'
around_action :with_browser_locale
skip_before_action :authenticate_user!
skip_authorization_check
def create
@template = Template.find_by!(slug: params[:slug])
before_action :load_template
before_action :authorize_start!
def create
@submitter = @template.submissions.new(account_id: @template.account_id)
.submitters.new(**submitter_params, account_id: @template.account_id)
if @submitter.email.blank?
return redirect_to start_form_path(@template.slug), alert: I18n.t(:provide_your_email_to_start)
end
Submitters.send_shared_link_email_verification_code(@submitter, request:)
redir_params = { notice: I18n.t(:code_has_been_resent) } if params[:resend]
redirect_to start_form_path(@template.slug, params: submitter_params.merge(email_verification: true)),
**redir_params
rescue Submitters::BouncedEmail => e
@error_message = e.message
render 'start_form/error', status: :unprocessable_content
rescue Submitters::UnableToSendCode => e
redirect_to start_form_path(@template.slug, params: submitter_params.merge(email_verification: true)),
alert: e.message
@@ -25,7 +36,34 @@ class StartFormEmail2faSendController < ApplicationController
private
def load_template
@template = Template.find_by!(slug: params[:slug])
end
def authorize_start!
is_archived = @template.archived_at? || @template.account.archived_at?
return redirect_to start_form_path(@template.slug) if is_archived
return if (@template.shared_link? || (current_user && current_ability.can?(:read, @template))) &&
(@template.preferences['shared_link_2fa'] == true || submitter_exists?(@template, params))
Rollbar.warning("Not 2FA shared template: #{@template.id}") if defined?(Rollbar)
redirect_to start_form_path(@template.slug)
end
def submitter_exists?(template, params)
email = Submissions.normalize_email(params.dig(:submitter, :email))
return false if email.blank?
Submitters::StartForm.build_submitters_scope(template, exclude_completed: true).exists?(email:)
end
def submitter_params
params.require(:submitter).permit(:name, :email, :phone)
params.require(:submitter).permit(:name, :email, :phone).tap do |attrs|
attrs[:email] = Submissions.normalize_email(attrs[:email])
end
end
end
@@ -0,0 +1,103 @@
# frozen_string_literal: true
class StartFormResubmitController < ApplicationController
layout 'form'
skip_before_action :authenticate_user!
skip_authorization_check
around_action :with_browser_locale
before_action :load_resubmit_submitter
before_action :load_template
before_action :authorize_start!
def update
@submitter = find_or_initialize_submitter(@template, @resubmit_submitter)
if Templates.filter_undefined_submitters(@template.submitters).size > 1 && @submitter.new_record?
@error_message = multiple_submitters_error_message
return render 'start_form/show', status: :unprocessable_content
end
if (is_new_record = @submitter.new_record?)
Submitters::StartForm.assign_submission_attributes(
@submitter, @template,
ip: request.remote_ip, user_agent: request.user_agent, resubmit_submitter: @resubmit_submitter,
source: @resubmit_submitter.submission.source_self? ? :self : :link
)
Submissions::AssignDefinedSubmitters.call(@submitter.submission)
else
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
end
if @template.preferences['shared_link_2fa'] == true
handle_require_2fa(@submitter)
elsif @submitter.save
Submitters::StartForm.enqueue_new_submitter_jobs(@submitter) if is_new_record
Submitters::StartForm.assign_start_form_cookie(@submitter, request)
redirect_to submit_form_path(@submitter.slug)
else
render 'start_form/show', status: :unprocessable_content
end
end
private
def find_or_initialize_submitter(template, resubmit_submitter)
Submitters::StartForm.build_submitters_scope(template, exclude_completed: true)
.find_by(slug: cookies.encrypted[:start_form_slug],
email: resubmit_submitter.email) || Submitter.new
end
def load_resubmit_submitter
@resubmit_submitter = Submitter.find_by(slug: params[:resubmit])
raise ActiveRecord::RecordNotFound if @resubmit_submitter.blank? ||
!Submitters::StartForm.can_resubmit?(@resubmit_submitter)
end
def load_template
@template = @resubmit_submitter.template
end
def authorize_start!
redirect_to submit_form_path(@resubmit_submitter.slug) if @template.archived_at? || @template.account.archived_at?
end
def multiple_submitters_error_message
if current_user&.account_id == @template.account_id
helpers.t('this_submission_has_multiple_signers_which_prevents_the_use_of_a_sharing_link_html')
else
I18n.t('not_found')
end
end
def handle_require_2fa(submitter)
if Submitters::StartForm.verify_2fa_and_save_submitter(submitter, request,
is_new_record: submitter.new_record?)
redirect_to submit_form_path(submitter.slug)
else
Submitters.send_shared_link_email_verification_code(submitter, request:)
render 'start_form/email_verification'
end
rescue Submitters::StartForm::NotSaved
render 'start_form/show', status: :unprocessable_content
rescue Submitters::BouncedEmail => e
@error_message = e.message
render 'start_form/error', status: :unprocessable_content
rescue Submitters::UnableToSendCode, Submitters::InvalidOtp => e
flash.now[:alert] = e.message
render 'start_form/email_verification'
rescue RateLimit::LimitApproached
flash.now[:alert] = I18n.t(:too_many_attempts)
render 'start_form/email_verification'
end
end
@@ -0,0 +1,50 @@
# frozen_string_literal: true
class StartFormSelfController < ApplicationController
load_resource :template, parent: false
around_action :with_browser_locale
before_action :authorize_start!
def update
@submitter = Submitters::StartForm.build_submitters_scope(@template, exclude_completed: true, source: :self)
.find_or_initialize_by(email: current_user.email)
@submitter = Submitter.new(email: current_user.email) if @submitter.persisted? &&
outdated_template_fields?(@submitter, @template)
if (is_new_record = @submitter.new_record?)
@submitter.name = current_user.full_name
Submitters::StartForm.assign_submission_attributes(
@submitter, @template, ip: request.remote_ip, user_agent: request.user_agent, source: :self
)
Submissions::AssignDefinedSubmitters.call(@submitter.submission)
else
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
end
@submitter.save!
Submitters::StartForm.enqueue_new_submitter_jobs(@submitter) if is_new_record
Submitters::StartForm.assign_start_form_cookie(@submitter, request)
redirect_to submit_form_path(@submitter.slug)
end
private
def outdated_template_fields?(submitter, template)
template_fields = submitter.submission.template_fields
template_fields.present? && template_fields != template.fields
end
def authorize_start!
authorize!(:read, @template)
redirect_to template_path(@template) if @template.archived_at?
end
end
@@ -24,5 +24,7 @@ class SubmissionEventsController < ApplicationController
load_and_authorize_resource :submission
def index; end
def index
render :index, layout: 'plain'
end
end
+3 -1
View File
@@ -33,12 +33,14 @@ class SubmissionsController < ApplicationController
def new
authorize!(:new, Submission)
render :new, layout: 'plain'
end
def create
return redirect_to template_path(@template), alert: I18n.t('template_has_been_archived') if @template.archived_at?
save_template_message(@template, params) if params[:save_message] == '1'
save_template_message(@template, params) if params[:save_message] == '1' && can?(:update, @template)
[params.delete(:subject), params.delete(:body)] if params[:is_custom_message] != '1'
@@ -10,18 +10,19 @@ class SubmitFormCompletedDownloadController < ApplicationController
def index
@submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
signature_valid =
if @submitter&.slug == submitter_slug
true
else
@submitter = nil
end
signature_valid = @submitter&.slug == submitter_slug
@submitter ||= Submitter.find_by!(slug: submitter_slug)
@submitter = Submitter.find_by!(slug: submitter_slug) unless signature_valid
unless completed_submitter?(@submitter)
Rollbar.error("Not completed: #{@submitter.id}") if defined?(Rollbar)
return head :not_found
end
Submissions::EnsureResultGenerated.call(@submitter) if @submitter.completed_at?
last_submitter = @submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
last_submitter = @submitter.submission.submitters.completed.order(:completed_at).last
return head :not_found unless last_submitter
@@ -47,7 +48,7 @@ class SubmitFormCompletedDownloadController < ApplicationController
private
def submitter_slug
params[:submit_form_slug] || params[:submitter_slug] || params[:submitter_id]
params[:submit_form_slug] || params[:submitter_slug]
end
def respond_with_combined(submitter)
@@ -60,6 +61,10 @@ class SubmitFormCompletedDownloadController < ApplicationController
end
end
def completed_submitter?(submitter)
submitter.completed_at? || (submitter.viewer? && submitter.submission.completed_at?)
end
def current_user_submitter?(submitter)
current_user && current_ability.can?(:read, submitter)
end
@@ -107,6 +107,8 @@ class SubmitFormController < ApplicationController
submitter_version = SubmitterVersion.find_by!(slug: params[:slug] || params[:submit_form_slug])
@submitter = submitter_version.submitter
maybe_render_locked_page
end
private
@@ -19,14 +19,15 @@ class SubmitFormDeclineController < ApplicationController
request)
ApplicationRecord.transaction do
@submitter.update!(declined_at: Time.current)
@submitter.update!(declined_at: Time.current, values: @submitter.preferences['default_values'] || {})
SubmissionEvents.create_with_tracking_data(@submitter, 'decline_form', request, { reason: params[:reason] })
end
user = @submitter.submission.created_by_user || @submitter.template.author
if user.user_configs.find_by(key: UserConfig::RECEIVE_DECLINED_EMAIL)&.value != false
if Users.send_emails?(user) &&
user.user_configs.find_by(key: UserConfig::RECEIVE_DECLINED_EMAIL)&.value != false
SubmitterMailer.declined_email(@submitter, user).deliver_later!
end
@@ -32,7 +32,8 @@ class SubmitFormDelegateController < ApplicationController
SubmissionEvents.create_with_tracking_data(@submitter, 'delegate_form', request,
{ old_email: @submitter.email, email: })
@submitter.update!(email:, phone: nil, name: nil, slug: SecureRandom.base58(14))
@submitter.update!(email:, phone: nil, name: nil, slug: SecureRandom.base58(14),
values: Submitters.fetch_values_for_delegate(@submitter))
end
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => @submitter.id)
@@ -8,9 +8,6 @@ class SubmitFormEmail2fasController < ApplicationController
before_action :load_submitter
COOKIES_TTL = 12.hours
COOKIES_DEFAULTS = { httponly: true, secure: Rails.env.production? }.freeze
def create
RateLimit.call("verify-2fa-code-#{@submitter.id}", limit: 2, ttl: 45.seconds, enabled: true)
@@ -20,7 +17,8 @@ class SubmitFormEmail2fasController < ApplicationController
SubmissionEvents.create_with_tracking_data(@submitter, 'email_verified', request, { email: @submitter.email })
cookies.encrypted[:email_2fa_slug] =
{ value: @submitter.slug, expires: COOKIES_TTL.from_now, **COOKIES_DEFAULTS }
{ value: @submitter.slug, expires: Submitters::StartForm::COOKIES_TTL.from_now,
**Submitters::StartForm::COOKIES_DEFAULTS }
redirect_to submit_form_path(@submitter.slug)
else
@@ -37,6 +35,12 @@ class SubmitFormEmail2fasController < ApplicationController
RateLimit.call("send-email-code-#{@submitter.id}", limit: 2, ttl: 45.seconds, enabled: true)
if Docuseal.multitenant? && Submitters.email_bounced_recently?(@submitter.email)
Rollbar.warning("Bounced OTP email for submitter: #{@submitter.id}") if defined?(Rollbar)
return redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:verification_email_bounced)
end
SendSubmitterVerificationEmailJob.perform_async('submitter_id' => @submitter.id, 'locale' => I18n.locale.to_s)
redir_params = params[:resend] ? { alert: I18n.t(:code_has_been_resent) } : {}
@@ -23,7 +23,7 @@ class SubmitFormInviteController < ApplicationController
@submitter.submission.submitters.create!(uuid: attrs[:uuid], email:, account_id: @submitter.account_id)
SubmissionEvents.create_with_tracking_data(@submitter, 'invite_party', request, { uuid: @submitter.uuid })
SubmissionEvents.create_with_tracking_data(@submitter, 'invite_party', request, { uuid: attrs[:uuid] })
end
@submitter.submission.update!(submitters_order: :preserved)
@@ -36,7 +36,7 @@ class SubmittersAutocompleteController < ApplicationController
else
column = Submitter.arel_table[field.to_sym]
term = "#{params[:q].downcase}%"
term = "#{ActiveRecord::Base.sanitize_sql_like(params[:q].downcase)}%"
submitters.where(column.matches(term))
end
+2
View File
@@ -8,6 +8,8 @@ class SubmittersController < ApplicationController
if @submitter.preferences['email_message_uuid'].present?
@submitter.account.email_messages.find_by(uuid: @submitter.preferences['email_message_uuid'])
end
render :edit, layout: 'plain'
end
def update
+3 -1
View File
@@ -25,7 +25,9 @@ class TemplatesController < ApplicationController
redirect_to root_path
end
def new; end
def new
render :new, layout: 'plain'
end
def edit
@template_data = Templates.serialize_for_builder(@template)
@@ -13,9 +13,10 @@ class TemplatesDebugController < ApplicationController
data = attachment.download
unless attachment.image?
pdf = HexaPDF::Document.new(io: StringIO.new(data))
fields = Templates::FindAcroFields.call(pdf, attachment, data)
fields =
Pdfium::Document.open_io(StringIO.new(data)) do |doc|
Templates::FindPdfiumAcroFields.call(attachment, doc, data)
end
end
# fields, = Templates::DetectFields.call(StringIO.new(data), attachment:) if fields.blank?
@@ -10,8 +10,14 @@ class TemplatesDetectFieldsController < ApplicationController
sse = SSE.new(response.stream)
documents = @template.schema_documents.preload(:blob)
documents = documents.where(uuid: params[:attachment_uuid]) if params[:attachment_uuid].present?
documents =
if params[:attachment_uuid].present?
@template.documents.where(uuid: params[:attachment_uuid])
else
@template.schema_documents
end
documents = documents.preload(:blob)
page_number = params[:page].presence&.to_i
@@ -13,11 +13,13 @@ class TemplatesFormPreviewController < ApplicationController
@submitter.submission.submitters = @template.submitters.map { |item| Submitter.new(uuid: item['uuid']) }
Submissions::CreateFromSubmitters.maybe_set_dynamic_documents(@submitter.submission, preview: true)
Submissions.preload_with_pages(@submitter.submission)
@attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments)
.preload(:blob).index_by(&:uuid)
@form_configs = Submitters::FormConfigs.call(@submitter)
@form_configs = Submitters::FormConfigs.call(@submitter, SubmitFormController::CONFIG_KEYS)
end
end
@@ -14,7 +14,9 @@ class TemplatesPreferencesController < ApplicationController
completed_notification_email_body]
}.freeze
def show; end
def show
render :show, layout: 'plain'
end
def create
authorize!(:update, @template)
@@ -3,7 +3,9 @@
class TemplatesShareLinkController < ApplicationController
load_and_authorize_resource :template
def show; end
def show
render :show, layout: 'plain'
end
def create
authorize!(:update, @template)
@@ -6,7 +6,9 @@ class TemplatesUploadsController < ApplicationController
layout 'plain'
def show
redirect_to root_path if params[:url].blank?
url_uri = Addressable::URI.parse(params[:url].to_s)
redirect_to root_path if url_uri.normalized_scheme != 'https' || url_uri.host.blank?
end
def create
@@ -61,7 +63,7 @@ class TemplatesUploadsController < ApplicationController
tempfile.write(DownloadUtils.call(params[:url], validate: true).body)
tempfile.rewind
filename = URI.decode_www_form_component(params[:filename]) if params[:filename].present?
filename = URI.decode_www_form_component(params[:filename]).tr('/', '-') if params[:filename].present?
filename ||= File.basename(URI.decode_www_form_component(params[:url]))
file = ActionDispatch::Http::UploadedFile.new(
@@ -6,6 +6,7 @@ class TestingAccountsController < ApplicationController
def create
authorize!(:manage, current_account)
authorize!(:manage, current_user)
authorize!(:manage, EncryptedConfig)
impersonate_user(Accounts.find_or_create_testing_user(true_user.account))
+2
View File
@@ -75,6 +75,8 @@ class UsersController < ApplicationController
authorize!(:manage, account)
@user.account = account
authorize!(:create, @user)
end
if @user.update(attrs.except(*(current_user == @user ? %i[password otp_required_for_login role] : %i[password])))
@@ -4,16 +4,16 @@ class VerifyPdfSignatureController < ApplicationController
skip_authorization_check
def create
pdfs =
params[:files].map do |file|
HexaPDF::Document.new(io: file.open)
end
trusted_certs = Accounts.load_trusted_certs(current_account)
signatures =
params[:files].map do |file|
VerifyPdfSignature.call(file.open, trusted_certs)
end
render turbo_stream: turbo_stream.replace('result', partial: 'result',
locals: { pdfs:, files: params[:files], trusted_certs: })
rescue HexaPDF::MalformedPDFError
locals: { signatures:, files: params[:files] })
rescue Pdfium::PdfiumError
render turbo_stream: turbo_stream.replace('result', html: helpers.tag.div(I18n.t('invalid_pdf'), id: 'result'))
end
end
+9 -1
View File
@@ -3,11 +3,19 @@
class WebhookSecretController < ApplicationController
load_and_authorize_resource :webhook_url, parent: false
HEADER_NAME_REGEXP = /\A[\w-]+\z/
def show; end
def update
key = webhook_secret_params[:key]
if key.present? && !HEADER_NAME_REGEXP.match?(key)
return redirect_back(fallback_location: settings_webhook_path(@webhook_url), alert: I18n.t('unable_to_save'))
end
@webhook_url.update!(secret: {
webhook_secret_params[:key] => webhook_secret_params[:value]
key => webhook_secret_params[:value]
}.compact_blank)
redirect_back(fallback_location: settings_webhook_path(@webhook_url),
+26 -1
View File
@@ -56,6 +56,9 @@ import OpenModal from './elements/open_modal'
import BarChart from './elements/bar_chart'
import FieldCondition from './elements/field_condition'
import ConfirmUpload from './elements/confirm_upload'
import ScrollFade from './elements/scroll_fade'
import OpenModalMobile from './elements/open_modal_mobile'
import HistoryBack from './elements/history_back'
import * as TurboInstantClick from './lib/turbo_instant_click'
@@ -150,10 +153,14 @@ safeRegisterElement('open-modal', OpenModal)
safeRegisterElement('bar-chart', BarChart)
safeRegisterElement('field-condition', FieldCondition)
safeRegisterElement('confirm-upload', ConfirmUpload)
safeRegisterElement('scroll-fade', ScrollFade)
safeRegisterElement('open-modal-mobile', OpenModalMobile)
safeRegisterElement('history-back', HistoryBack)
safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () {
document.addEventListener('turbo:submit-end', this.onSubmit)
document.addEventListener('turbo:before-cache', this.onBeforeCache)
this.appElem = document.createElement('div')
@@ -181,7 +188,10 @@ safeRegisterElement('template-builder', class extends HTMLElement {
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
withCustomFields: true,
withPayment: this.dataset.withPayment === 'true',
isPaymentConnected: this.dataset.isPaymentConnected === 'true',
isStripeConnected: this.dataset.isPaymentConnected === 'true' || this.dataset.isStripeConnected === 'true',
withStripe: this.dataset.withStripe !== 'false',
withPaypal: this.dataset.withPaypal === 'true',
isPaypalConnected: this.dataset.isPaypalConnected === 'true',
withFormula: this.dataset.withFormula === 'true',
withSendButton: this.dataset.withSendButton !== 'false',
withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false',
@@ -234,8 +244,14 @@ safeRegisterElement('template-builder', class extends HTMLElement {
}
}
onBeforeCache = () => {
this.app?.unmount()
this.appElem?.remove()
}
disconnectedCallback () {
document.removeEventListener('turbo:submit-end', this.onSubmit)
document.removeEventListener('turbo:before-cache', this.onBeforeCache)
this.app?.unmount()
this.appElem?.remove()
@@ -244,6 +260,8 @@ safeRegisterElement('template-builder', class extends HTMLElement {
safeRegisterElement('import-list', class extends HTMLElement {
connectedCallback () {
document.addEventListener('turbo:before-cache', this.onBeforeCache)
this.appElem = document.createElement('div')
this.app = createApp(ImportList, {
@@ -258,7 +276,14 @@ safeRegisterElement('import-list', class extends HTMLElement {
this.appendChild(this.appElem)
}
onBeforeCache = () => {
this.app?.unmount()
this.appElem?.remove()
}
disconnectedCallback () {
document.removeEventListener('turbo:before-cache', this.onBeforeCache)
this.app?.unmount()
this.appElem?.remove()
}
+7
View File
@@ -114,6 +114,13 @@ button[disabled] .enabled, button.btn-disabled .enabled {
bottom: auto;
}
@media (hover: none), (pointer: coarse) {
.tooltip-no-touch:before,
.tooltip-no-touch:after {
display: none;
}
}
.autocomplete {
background: white;
z-index: 1000;
+15
View File
@@ -0,0 +1,15 @@
export default class extends HTMLElement {
connectedCallback () {
this.addEventListener('click', this.onClick)
}
disconnectedCallback () {
this.removeEventListener('click', this.onClick)
}
onClick = (e) => {
e.preventDefault()
window.history.back()
}
}
@@ -0,0 +1,17 @@
export default class extends HTMLElement {
connectedCallback () {
if (this.dataset.disabled === 'true') return
if (!this.isMobile()) return
if (window.innerWidth >= 768) return
this.querySelectorAll('a[data-turbo-frame]').forEach((link) => {
link.dataset.turboFrame = '_top'
})
}
isMobile () {
const isTouchWebkit = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit|android/i.test(navigator.userAgent)
return isTouchWebkit || /iPhone|iPad|iPod/i.test(navigator.userAgent)
}
}
+40
View File
@@ -0,0 +1,40 @@
export default class extends HTMLElement {
connectedCallback () {
this.addEventListener('scroll', this.updateFade, { passive: true })
window.addEventListener('resize', this.updateFade)
this.updateFade()
}
disconnectedCallback () {
window.removeEventListener('resize', this.updateFade)
}
updateFade = () => {
const maxScroll = this.scrollWidth - this.clientWidth
const fadeGradient = {
right: 'linear-gradient(to right, black calc(100% - 24px), transparent)',
left: 'linear-gradient(to right, transparent, black 24px)',
both: 'linear-gradient(to right, transparent, black 24px, black calc(100% - 24px), transparent)'
}
let state = 'none'
if (maxScroll > 4) {
if (this.scrollLeft <= 4) {
state = 'right'
} else if (this.scrollLeft >= maxScroll - 4) {
state = 'left'
} else {
state = 'both'
}
}
this.applyFade(fadeGradient[state] || '')
}
applyFade (gradient) {
this.style.maskImage = gradient
this.style.webkitMaskImage = gradient
}
}
+8 -2
View File
@@ -6,7 +6,7 @@ export default actionable(class extends HTMLElement {
document.addEventListener('keyup', this.onEscKey)
document.addEventListener('turbo:before-cache', this.close)
document.addEventListener('turbo:before-cache', this.onBeforeCache)
if (this.dataset.closeAfterSubmit !== 'false') {
document.addEventListener('turbo:submit-end', this.onSubmit)
@@ -18,7 +18,7 @@ export default actionable(class extends HTMLElement {
document.removeEventListener('keyup', this.onEscKey)
document.removeEventListener('turbo:submit-end', this.onSubmit)
document.removeEventListener('turbo:before-cache', this.close)
document.removeEventListener('turbo:before-cache', this.onBeforeCache)
}
onSubmit = (e) => {
@@ -33,6 +33,12 @@ export default actionable(class extends HTMLElement {
}
}
onBeforeCache = () => {
if (this.closest('turbo-frame')?.src) {
this.remove()
}
}
close = (e) => {
e?.preventDefault()
+1
View File
@@ -35,6 +35,7 @@ safeRegisterElement('submission-form', class extends HTMLElement {
scrollPadding: this.dataset.scrollPadding || '-80px',
signatureText: this.dataset.signatureText,
language: this.dataset.language,
paymentProvider: this.dataset.paymentProvider,
dryRun: this.dataset.dryRun === 'true',
expand: ['true', 'false'].includes(this.dataset.expand) ? this.dataset.expand === 'true' : null,
withSignatureId: this.dataset.withSignatureId === 'true',
@@ -69,8 +69,8 @@
:attachments-index="attachmentsIndex"
/>
<FieldArea
v-else-if="item.type === 'formula_area' && isMathLoaded"
:model-value="calculateFormula(item.field)"
v-else-if="item.type === 'formula_area' && (isMathLoaded || item.field.type === 'text')"
:model-value="item.field.type === 'text' ? evalTextFormula(item.field) : calculateFormula(item.field)"
:is-inline-size="isInlineSize"
:field="item.field"
:area="item.area"
@@ -289,6 +289,7 @@ export default {
methods: {
normalizeFormula: FormulaAreas.methods.normalizeFormula,
calculateFormula: FormulaAreas.methods.calculateFormula,
evalTextFormula: FormulaAreas.methods.evalTextFormula,
scrollInContainer: FieldAreas.methods.scrollInContainer,
scrollIntoArea: FieldAreas.methods.scrollIntoArea,
scrollIntoField: FieldAreas.methods.scrollIntoField,
+1 -1
View File
@@ -101,7 +101,7 @@
</template>
</div>
<div>
{{ new Date(signature.created_at).toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', timeZoneName: 'short' }) }}
{{ new Date(signature.created_at).toLocaleString(undefined, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric', timeZoneName: 'short' }) }}
</div>
</div>
</div>
+1 -1
View File
@@ -230,7 +230,7 @@ export default {
download () {
this.isDownloading = true
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`, {
fetch(this.baseUrl + `/s/${this.submitterSlug}/documents`, {
method: 'GET',
...this.fetchOptions
}).then(async (response) => {
+10 -1
View File
@@ -560,6 +560,8 @@
:fields="formulaFields"
:values="values"
:readonly-values="readonlyFieldValues"
:fetch-options="fetchOptions"
:provider="paymentProvider"
@attached="attachments.push($event)"
@focus="scrollIntoField(currentField)"
@submit="!isSubmitting && submitStep()"
@@ -572,6 +574,7 @@
:empty-value-required-step="emptyValueRequiredStep"
:field="currentField"
:submitter-slug="submitterSlug"
:fetch-options="fetchOptions"
:values="values"
@submit="!isSubmitting && submitStep()"
/>
@@ -584,6 +587,7 @@
:empty-value-required-step="emptyValueRequiredStep"
:field="currentField"
:submitter-slug="submitterSlug"
:fetch-options="fetchOptions"
:values="values"
@focus="scrollIntoField(currentField)"
@submit="!isSubmitting && submitStep()"
@@ -1000,6 +1004,11 @@ export default {
required: false,
default: ''
},
paymentProvider: {
type: String,
required: false,
default: ''
},
values: {
type: Object,
required: false,
@@ -1770,7 +1779,7 @@ export default {
window.location.href = sanitizeUrl(this.completedRedirectUrl)
} else {
this.$nextTick(() => {
const root = this.$root.$el.parentNode.getRootNode()
const root = this.$root.$el.parentNode?.getRootNode() || document
const completedEl = root.getElementById('form_completed')
if (completedEl) {
@@ -12,8 +12,8 @@
:to="findPageElementForArea(area)"
>
<FieldArea
v-if="isMathLoaded"
:model-value="calculateFormula(field)"
v-if="isMathLoaded || field.type === 'text'"
:model-value="field.type === 'text' ? evalTextFormula(field) : calculateFormula(field)"
:is-inline-size="isInlineSize"
:field="field"
:area="area"
@@ -95,6 +95,25 @@ export default {
})
return this.math.evaluate(transformedFormula.toLowerCase())
},
evalTextFormula (field, depth = 0) {
if (depth > 10) return ''
return field.preferences.formula.replace(/{{(.*?)}}/g, (match, uuid) => {
const formulaField = this.fieldsUuidIndex[uuid]
if (formulaField?.preferences?.formula) {
if (formulaField.type === 'text') {
return this.evalTextFormula(formulaField, depth + 1)
} else if (this.isMathLoaded) {
return this.calculateFormula(formulaField)
}
}
const value = this.readonlyValues[uuid] ?? this.values[uuid]
return Array.isArray(value) ? value.join(', ') : (value ?? '')
})
}
}
}
+14
View File
@@ -107,6 +107,7 @@ const en = {
email_has_been_sent: 'Email has been sent',
processing: 'Processing',
pay_with_stripe: 'Pay with Stripe',
pay_with_paypal: 'Pay with PayPal',
reupload: 'Reupload',
upload: 'Upload',
files: 'Files',
@@ -225,6 +226,7 @@ const es = {
email_has_been_sent: 'El correo electrónico ha sido enviado',
processing: 'Procesando',
pay_with_stripe: 'Pagar con Stripe',
pay_with_paypal: 'Pagar con PayPal',
reupload: 'Volver a subir',
upload: 'Subir',
files: 'Archivos',
@@ -343,6 +345,7 @@ const it = {
email_has_been_sent: "L'email è stata inviata",
processing: 'Elaborazione',
pay_with_stripe: 'Paga con Stripe',
pay_with_paypal: 'Paga con PayPal',
reupload: 'Ricarica',
upload: 'Carica',
files: 'File',
@@ -461,6 +464,7 @@ const de = {
email_has_been_sent: 'E-Mail wurde gesendet',
processing: 'Verarbeitung',
pay_with_stripe: 'Mit Stripe bezahlen',
pay_with_paypal: 'Mit PayPal bezahlen',
reupload: 'Erneut hochladen',
upload: 'Hochladen',
files: 'Dateien',
@@ -579,6 +583,7 @@ const fr = {
email_has_been_sent: "L'e-mail a été envoyé",
processing: 'Traitement en cours',
pay_with_stripe: 'Payer avec Stripe',
pay_with_paypal: 'Payer avec PayPal',
reupload: 'Retéléverser',
upload: 'Téléverser',
files: 'Fichiers',
@@ -697,6 +702,7 @@ const pl = {
email_has_been_sent: 'E-mail został wysłany',
processing: 'Przetwarzanie',
pay_with_stripe: 'Zapłać za pomocą Stripe',
pay_with_paypal: 'Zapłać za pomocą PayPal',
reupload: 'Ponowne przesłanie',
upload: 'Przesyłanie',
files: 'Pliki',
@@ -815,6 +821,7 @@ const uk = {
email_has_been_sent: 'Електронний лист був відправлений',
processing: 'Обробка',
pay_with_stripe: 'Сплатити за допомогою Stripe',
pay_with_paypal: 'Сплатити за допомогою PayPal',
reupload: 'Перезавантажити',
upload: 'Завантажити',
files: 'Файли',
@@ -933,6 +940,7 @@ const cs = {
email_has_been_sent: 'E-mail byl odeslán',
processing: 'Zpracování',
pay_with_stripe: 'Zaplatit přes Stripe',
pay_with_paypal: 'Zaplatit přes PayPal',
reupload: 'Znovu nahrát',
upload: 'Nahrát',
files: 'Soubory',
@@ -1051,6 +1059,7 @@ const pt = {
email_has_been_sent: 'Email enviado',
processing: 'Processamento',
pay_with_stripe: 'Pagar com Stripe',
pay_with_paypal: 'Pagar com PayPal',
reupload: 'Recarregar',
upload: 'Carregar',
files: 'Arquivos',
@@ -1169,6 +1178,7 @@ const he = {
email_has_been_sent: 'האימייל נשלח',
processing: 'מעבד',
pay_with_stripe: 'שלם עם סטרייפ',
pay_with_paypal: 'שלם עם פייפאל',
reupload: 'העלה שוב',
upload: 'העלאה',
files: 'קבצים',
@@ -1287,6 +1297,7 @@ const nl = {
email_has_been_sent: 'E-mail is verzonden',
processing: 'Verwerken',
pay_with_stripe: 'Betalen met Stripe',
pay_with_paypal: 'Betalen met PayPal',
reupload: 'Opnieuw uploaden',
upload: 'Uploaden',
files: 'Bestanden',
@@ -1405,6 +1416,7 @@ const ar = {
email_has_been_sent: 'تم إرسال البريد الإلكتروني',
processing: 'جارٍ المعالجة',
pay_with_stripe: 'الدفع بواسطة Stripe',
pay_with_paypal: 'الدفع بواسطة PayPal',
reupload: 'إعادة التحميل',
upload: 'تحميل',
files: 'الملفات',
@@ -1523,6 +1535,7 @@ const ko = {
email_has_been_sent: '이메일이 전송되었습니다',
processing: '처리 중',
pay_with_stripe: '스트라이프로 결제',
pay_with_paypal: 'PayPal로 결제',
reupload: '다시 업로드',
upload: '업로드',
files: '파일',
@@ -1641,6 +1654,7 @@ const ja = {
email_has_been_sent: 'メールが送信されました',
processing: '処理中',
pay_with_stripe: 'Stripeで支払う',
pay_with_paypal: 'PayPalで支払う',
reupload: '再アップロード',
upload: 'アップロード',
files: 'ファイル',
+10 -3
View File
@@ -312,6 +312,11 @@ export default {
values: {
type: Object,
required: true
},
fetchOptions: {
type: Object,
required: false,
default: () => ({})
}
},
emits: ['submit'],
@@ -494,8 +499,9 @@ export default {
const resp = await fetch(this.baseUrl + '/api/kba', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
body: JSON.stringify(payload),
...this.fetchOptions,
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
})
const data = await resp.json()
@@ -550,7 +556,8 @@ export default {
reference: this.reference,
submitter_slug: this.submitterSlug
}),
headers: { 'Content-Type': 'application/json' }
...this.fetchOptions,
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
})
const data = await resp.json()
+62 -15
View File
@@ -1,6 +1,6 @@
<template>
<label
v-if="!modelValue && !sessionId"
v-if="!modelValue && !isProcessing"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
>
<MarkdownContent
@@ -25,14 +25,14 @@
class="hidden"
>
<div
v-if="modelValue && !sessionId"
v-if="modelValue && !isProcessing"
class=" text-2xl mb-2"
>
{{ t('already_paid') }}
</div>
<div v-else>
<button
v-if="sessionId"
v-if="isProcessing"
type="button"
disabled
class="base-button w-full modal-save-button"
@@ -49,8 +49,8 @@
v-else
:id="field.uuid"
type="button"
class="btn bg-[#7B73FF] text-white hover:bg-[#0A2540] text-lg w-full"
:class="{ disabled: isCreatingCheckout }"
class="btn text-white text-lg w-full"
:class="[isPaypal ? 'bg-[#003087] hover:bg-[#012169]' : 'bg-[#7B73FF] hover:bg-[#0A2540]', { disabled: isCreatingCheckout }]"
:disabled="isCreatingCheckout"
@click.prevent="postCheckout"
>
@@ -59,12 +59,16 @@
width="22"
class="animate-spin"
/>
<IconBrandPaypal
v-else-if="isPaypal"
width="22"
/>
<IconBrandStripe
v-else
width="22"
/>
<span>
{{ t('pay_with_stripe') }}
{{ isPaypal ? t('pay_with_paypal') : t('pay_with_stripe') }}
</span>
</button>
</div>
@@ -72,13 +76,14 @@
</template>
<script>
import { IconBrandStripe, IconInnerShadowTop, IconLoader } from '@tabler/icons-vue'
import { IconBrandStripe, IconBrandPaypal, IconInnerShadowTop, IconLoader } from '@tabler/icons-vue'
import MarkdownContent from './markdown_content'
export default {
name: 'PaymentStep',
components: {
IconBrandStripe,
IconBrandPaypal,
MarkdownContent,
IconInnerShadowTop,
IconLoader
@@ -111,12 +116,23 @@ export default {
submitterSlug: {
type: String,
required: true
},
provider: {
type: String,
required: false,
default: ''
},
fetchOptions: {
type: Object,
required: false,
default: () => ({})
}
},
emits: ['focus', 'submit', 'update:model-value', 'attached'],
data () {
return {
isCreatingCheckout: false,
isProcessing: false,
isMathLoaded: false
}
},
@@ -131,8 +147,14 @@ export default {
queryParams () {
return new URLSearchParams(window.location.search)
},
isPaypal () {
return this.provider === 'paypal'
},
paymentEndpoint () {
return this.isPaypal ? '/api/paypal_payments' : '/api/stripe_payments'
},
sessionId () {
return this.queryParams.get('stripe_session_id')
return this.queryParams.get('stripe_session_id') || this.queryParams.get('paypal_order_id') || (this.isPaypal && this.queryParams.get('token'))
},
defaultName () {
if (this.field.preferences?.price_id || this.field.preferences?.payment_link_id) {
@@ -159,6 +181,8 @@ export default {
},
async mounted () {
if (this.sessionId) {
this.isProcessing = true
this.$emit('submit')
}
@@ -175,6 +199,21 @@ export default {
}
},
methods: {
normalizedPaymentUrl () {
const url = new URL(window.location.href)
const keys = ['stripe_session_id', 'paypal_order_id', 'field_uuid']
if (this.isPaypal) {
keys.push('token', 'PayerID')
}
keys.forEach((key) => url.searchParams.delete(key))
return url.toString()
},
normalizedPaymentParams () {
window.history.replaceState({}, document.title, this.normalizedPaymentUrl())
},
calculateFormula () {
const transformedFormula = this.normalizeFormula(this.field.preferences.formula).replace(/{{(.*?)}}/g, (match, uuid) => {
return this.readonlyValues[uuid] || this.values[uuid] || 0.0
@@ -195,24 +234,31 @@ export default {
},
async submit () {
if (this.sessionId) {
return fetch(this.baseUrl + '/api/stripe_payments/' + this.sessionId, {
return fetch(this.baseUrl + this.paymentEndpoint + '/' + this.sessionId, {
method: 'PUT',
body: JSON.stringify({
submitter_slug: this.submitterSlug
}),
headers: { 'Content-Type': 'application/json' }
...this.fetchOptions,
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
}).then(async (resp) => {
if (resp.status === 422 || resp.status === 500) {
this.isProcessing = false
const data = await resp.json()
alert(data.error || 'Unexpected error')
if (data.error) {
alert(data.error)
}
return Promise.reject(new Error(data.error))
}
const attachment = await resp.json()
window.history.replaceState({}, document.title, window.location.pathname)
this.isProcessing = false
this.normalizedPaymentParams()
this.$emit('update:model-value', attachment.uuid)
this.$emit('attached', attachment)
@@ -226,15 +272,16 @@ export default {
postCheckout ({ checkStatus } = {}) {
this.isCreatingCheckout = true
fetch(this.baseUrl + '/api/stripe_payments', {
fetch(this.baseUrl + this.paymentEndpoint, {
method: 'POST',
body: JSON.stringify({
submitter_slug: this.submitterSlug,
field_uuid: this.field.uuid,
check_status: checkStatus,
referer: document.location.href
referer: this.normalizedPaymentUrl()
}),
headers: { 'Content-Type': 'application/json' }
...this.fetchOptions,
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
}).then(async (resp) => {
if (resp.status === 422 || resp.status === 500) {
const data = await resp.json()
@@ -39,7 +39,7 @@
required
maxlength="6"
autofocus
inputmode="decimal"
inputmode="numeric"
@input="onInputCode"
>
<div class="flex justify-between mt-2 -mb-2 md:-mb-4">
@@ -261,7 +261,7 @@ export default {
}
},
resendCode () {
if (this.codeSentAt && Date.now() - this.codeSentAt < 15000) {
if (this.codeSentAt && Date.now() - this.codeSentAt < 30000) {
this.startResendCodeCountdown()
} else {
this.isResendLoading = true
@@ -274,7 +274,7 @@ export default {
}
},
startResendCodeCountdown () {
this.resendCodeCountdown = 15 - parseInt((Date.now() - this.codeSentAt) / 1000)
this.resendCodeCountdown = 30 - parseInt((Date.now() - this.codeSentAt) / 1000)
this.interval = setInterval(() => {
this.resendCodeCountdown--
@@ -91,6 +91,11 @@ export default {
submitterSlug: {
type: String,
required: true
},
fetchOptions: {
type: Object,
required: false,
default: () => ({})
}
},
emits: ['focus', 'submit', 'update:model-value', 'attached'],
@@ -163,7 +168,8 @@ export default {
submitter_slug: this.submitterSlug,
redirect_url: document.location.href
}),
headers: { 'Content-Type': 'application/json' }
...this.fetchOptions,
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
}).then(async (resp) => {
this.eidEasyData = await resp.json()
@@ -205,7 +211,8 @@ export default {
body: JSON.stringify({
submitter_slug: this.submitterSlug
}),
headers: { 'Content-Type': 'application/json' }
...this.fetchOptions,
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
})
if (resp.status === 404) {
+80 -18
View File
@@ -110,7 +110,7 @@
data-turbo="false"
class="inline"
method="post"
:action="`/d/${template.slug}`"
:action="`/start_form_self/${template.id}`"
@submit="maybeShowErrorTemplateAlert"
>
<input
@@ -125,12 +125,6 @@
:value="authenticityToken"
autocomplete="off"
>
<input
type="hidden"
name="selfsign"
value="true"
autocomplete="off"
>
<button
class="btn btn-primary btn-ghost text-base hidden md:flex"
type="submit"
@@ -164,7 +158,7 @@
v-if="withSendButton"
id="send_button"
:href="`/templates/${template.id}/submissions/new?with_link=true`"
data-turbo-frame="modal"
:data-turbo-frame="isMobile && isBreakpointLg ? '_top' : 'modal'"
class="white-button md:!px-6"
@click="maybeShowErrorTemplateAlert"
>
@@ -202,7 +196,7 @@
</button>
<div
class="dropdown dropdown-end"
:class="{ 'dropdown-open': isDownloading }"
:class="{ 'dropdown-open': isDownloading || isPreviewLoading }"
>
<label
tabindex="0"
@@ -221,15 +215,23 @@
:href="`/templates/${template.id}/form`"
data-turbo="false"
class="flex items-center justify-center space-x-2"
@click.exact="isPreviewLoading = true"
>
<IconEye class="w-6 h-6 flex-shrink-0" />
<IconInnerShadowTop
v-if="isPreviewLoading"
class="animate-spin w-6 h-6 flex-shrink-0"
/>
<IconEye
v-else
class="w-6 h-6 flex-shrink-0"
/>
<span class="whitespace-nowrap">{{ t('save_and_preview') }}</span>
</a>
</li>
<li>
<a
:href="`/templates/${template.id}/preferences`"
data-turbo-frame="modal"
:data-turbo-frame="isMobile && isBreakpointLg ? '_top' : 'modal'"
class="flex space-x-2"
@click="closeDropdown"
>
@@ -281,6 +283,10 @@
:href="`/templates/${template.id}`"
class="base-button"
>
<IconX
width="20"
class="md:hidden stroke-2"
/>
<span class="hidden md:inline">
{{ t('back') }}
</span>
@@ -779,7 +785,10 @@ export default {
withVerification: this.withVerification,
withKba: this.withKba,
withPayment: this.withPayment,
isPaymentConnected: this.isPaymentConnected,
isStripeConnected: this.isStripeConnected,
withStripe: this.withStripe,
withPaypal: this.withPaypal,
isPaypalConnected: this.isPaypalConnected,
withFormula: this.withFormula,
withConditions: this.withConditions,
withCustomFields: this.withCustomFields,
@@ -1049,7 +1058,22 @@ export default {
required: false,
default: false
},
isPaymentConnected: {
isStripeConnected: {
type: Boolean,
required: false,
default: false
},
withStripe: {
type: Boolean,
required: false,
default: true
},
withPaypal: {
type: Boolean,
required: false,
default: false
},
isPaypalConnected: {
type: Boolean,
required: false,
default: false
@@ -1100,6 +1124,7 @@ export default {
documentRefs: [],
isBreakpointLg: false,
isDownloading: false,
isPreviewLoading: false,
isLoadingBlankPage: false,
isSaving: false,
isDetectingPageFields: false,
@@ -1241,7 +1266,7 @@ export default {
return formulaFields.reduce((acc, f) => {
if (this.conditionalFieldIndex[f.uuid] !== false) {
acc[f.uuid] = this.calculateFormula(f)
acc[f.uuid] = f.type === 'text' ? this.evalTextFormula(f) : this.calculateFormula(f)
}
return acc
@@ -1363,6 +1388,10 @@ export default {
if (document.location.search?.includes('stripe_connect_success')) {
document.querySelector('form[action="/auth/stripe_connect"]')?.closest('.dropdown')?.querySelector('label')?.focus()
}
if (document.location.search?.includes('paypal_connect_success')) {
document.querySelector('a[href^="/auth/paypal_connect"]')?.closest('.dropdown')?.querySelector('label')?.focus()
}
})
this.template.schema.forEach((item) => {
@@ -1504,6 +1533,25 @@ export default {
return this.math.evaluate(transformedFormula.toLowerCase())
},
evalTextFormula (field, depth = 0) {
if (depth > 10) return ''
return field.preferences.formula.replace(/{{(.*?)}}/g, (match, uuid) => {
const formulaField = this.fieldsUuidIndex[uuid]
if (formulaField?.preferences?.formula) {
if (formulaField.type === 'text') {
return this.evalTextFormula(formulaField, depth + 1)
} else if (this.isMathLoaded) {
return this.calculateFormula(formulaField)
}
}
const value = formulaField?.default_value
return Array.isArray(value) ? value.join(', ') : (value ?? '')
})
},
hasFormulaDependencyValue (field) {
const normalized = this.normalizeFormula(field.preferences.formula)
@@ -2323,7 +2371,6 @@ export default {
const areaCopy = JSON.parse(JSON.stringify(area))
delete fieldCopy.areas
delete fieldCopy.submitter_uuid
areaCopy.relativeX = area.x - minX
areaCopy.relativeY = area.y - minY
@@ -2333,6 +2380,7 @@ export default {
const clipboardData = {
items,
submitters: this.template.submitters.map((submitter) => ({ uuid: submitter.uuid, name: submitter.name })),
templateId: this.template.id,
timestamp: Date.now(),
isGroup: true
@@ -2443,6 +2491,20 @@ export default {
const fieldUuidIndex = {}
const fieldOptionsMap = {}
const submitterUuidsMap = {}
const copiedSubmitterUuids = [...new Set(data.items.map((item) => item.field.submitter_uuid).filter(Boolean))]
if (copiedSubmitterUuids.length > 1) {
copiedSubmitterUuids.forEach((uuid) => {
const name = data.submitters?.find((submitter) => submitter.uuid === uuid)?.name
const submitter = this.template.submitters.find((s) => s.uuid === uuid) ||
(name && this.template.submitters.find((s) => s.name.toLowerCase() === name.toLowerCase()))
submitterUuidsMap[uuid] = (submitter || this.selectedSubmitter).uuid
})
}
data.items.forEach((item) => {
const field = JSON.parse(JSON.stringify(item.field))
@@ -2467,7 +2529,7 @@ export default {
const newField = fieldUuidIndex[field.uuid] || {
...field,
uuid: v4(),
submitter_uuid: this.selectedSubmitter.uuid,
submitter_uuid: submitterUuidsMap[field.submitter_uuid] || this.selectedSubmitter.uuid,
areas: []
}
@@ -3477,6 +3539,8 @@ export default {
this.onChange(this.template)
}
this.pushUndo()
if (!this.autosave && !force) {
return Promise.resolve({})
}
@@ -3487,8 +3551,6 @@ export default {
}
})
this.pushUndo()
return this.baseFetch(`/templates/${this.template.id}`, {
method: 'PUT',
body: JSON.stringify({
@@ -417,10 +417,8 @@ function buildOperators (tags) {
break
}
case 'end': {
const popped = stack.pop()
if (popped.operator) {
popped.operator.endTag = tag
if (stack.length > 1) {
stack.pop().operator.endTag = tag
}
break
@@ -434,7 +432,13 @@ function buildOperators (tags) {
return operators
}
function hasBlankKey (keyString) {
return !keyString || keyString.split('.').some((key) => !key.trim())
}
function assignNestedSchema (propertiesHash, parentProperties, keyString, value) {
if (hasBlankKey(keyString)) return
const keys = keyString.split('.')
const lastKey = keys.pop()
@@ -460,6 +464,8 @@ function assignNestedSchema (propertiesHash, parentProperties, keyString, value)
}
function assignNestedSchemaWithPriority (propertiesHash, parentProperties, keyString, newType) {
if (hasBlankKey(keyString)) return
const keys = keyString.split('.')
const lastKey = keys.pop()
@@ -523,6 +529,8 @@ function processOperators (operators, propertiesHash = {}, parentProperties = {}
processOperators(op.elseChildren, propertiesHash, parentProperties)
break
case 'for': {
if (hasBlankKey(op.variableName)) break
const parts = op.variableName.split('.')
const singularKey = singularize(parts[parts.length - 1])
@@ -494,6 +494,7 @@ import FieldType from './field_type.vue'
import FieldSettings from './field_settings.vue'
import ContextSubmenu from './field_context_submenu.vue'
import ContextModal from './field_context_modal.vue'
import { stripeConnectedRef } from './payment_settings'
export default {
name: 'FieldContextMenu',
@@ -610,7 +611,7 @@ export default {
return this.withCondition && !['stamp', 'heading'].includes(this.field.type)
},
showFormula () {
return this.field.type === 'number' || this.field.type === 'payment'
return this.field.type === 'number' || this.field.type === 'payment' || !!this.field.preferences?.formula
},
showRequired () {
return this.withRequired && !['phone', 'stamp', 'verification', 'strikethrough', 'heading'].includes(this.field.type)
@@ -716,12 +717,18 @@ export default {
currentCurrency () {
return this.field.preferences?.currency || 'USD'
},
isStripeConnected: () => stripeConnectedRef.value,
priceTypeOptions () {
return [
const options = [
{ value: 'one_off', label: this.t('fixed') },
{ value: 'formula', label: this.t('formula') },
{ value: 'payment_link', label: this.t('payment_link') }
{ value: 'formula', label: this.t('formula') }
]
if (this.isStripeConnected) {
options.push({ value: 'payment_link', label: this.t('payment_link') })
}
return options
},
currentPriceType () {
if (this.field.preferences?.formula) {
@@ -500,7 +500,7 @@
</label>
</li>
<li
v-if="field.type == 'number'"
v-if="field.type == 'number' || field.preferences?.formula"
class="field-settings-formula"
>
<label
+8 -5
View File
@@ -555,8 +555,10 @@ export default {
return this.template.schema.some((item) => item.dynamic)
},
numberOfPages () {
return this.template.documents.reduce((acc, doc) => {
return acc + doc.metadata?.pdf?.number_of_pages || doc.preview_images.length
return this.template.schema.reduce((acc, item) => {
const doc = this.template.documents.find((d) => d.uuid === item.attachment_uuid)
return acc + (doc?.metadata?.pdf?.number_of_pages || doc?.preview_images?.length || 0)
}, 0)
},
isShowFieldSearch () {
@@ -774,9 +776,10 @@ export default {
headers: {
'Content-Type': 'application/json'
},
...(this.withDetectExistingFields
? { body: JSON.stringify({ fields: this.buildExistingFields() }) }
: {})
body: JSON.stringify({
attachment_uuid: this.template.schema.map((item) => item.attachment_uuid),
...(this.withDetectExistingFields ? { fields: this.buildExistingFields() } : {})
})
}).then(async (response) => {
const reader = response.body.getReader()
const decoder = new TextDecoder('utf-8')
@@ -61,7 +61,10 @@
@input="resizeTextarea"
/>
</div>
<div class="mb-3 mt-1">
<div
v-if="field.type !== 'text'"
class="mb-3 mt-1"
>
<div
target="blank"
class="text-sm mb-2 inline space-x-2 font-mono"
@@ -161,7 +164,9 @@ export default {
computed: {
fields () {
return this.template.fields.reduce((acc, f) => {
if (f !== this.field && this.isNumberField(f) && (!f.preferences?.formula || !f.preferences.formula.includes(this.field.uuid))) {
const isAllowed = this.field.type === 'text' ? this.isTextField(f) : this.isNumberField(f)
if (f !== this.field && isAllowed && (!f.preferences?.formula || !f.preferences.formula.includes(this.field.uuid))) {
acc.push(f)
}
@@ -179,6 +184,9 @@ export default {
isNumberField (field) {
return field.type === 'number' || (['radio', 'select'].includes(field.type) && field.options?.every((o) => String(o.value).match(/^[\d.-]+$/)))
},
isTextField (field) {
return ['text', 'number', 'select', 'radio', 'cells', 'phone'].includes(field.type)
},
humanizeFormula (text) {
return text.replace(/{{(.*?)}}/g, (match, uuid) => {
const foundField = this.template.fields.find((f) => f.uuid === uuid)
@@ -366,6 +366,9 @@ export default {
}
}
},
beforeUnmount () {
document.getElementById('list_form_buttons')?.classList?.add('hidden')
},
methods: {
t (key) {
return this.i18n[key] || key
@@ -1,7 +1,7 @@
<template>
<span
class="dropdown dropdown-end field-settings-dropdown"
:class="{ 'dropdown-open': withForceOpen && ((!field.preferences?.price && !field.preferences?.formula && !field.preferences?.price_id && !field.preferences?.payment_link_id) || !isConnected) && !isLoading }"
:class="{ 'dropdown-open': withForceOpen && !field.preferences?.price && !field.preferences?.formula && !field.preferences?.price_id && !field.preferences?.payment_link_id && !isLoading }"
>
<label
tabindex="0"
@@ -21,7 +21,7 @@
@click="closeDropdown"
>
<div
v-if="!('price_id' in field.preferences) && !('payment_link_id' in field.preferences)"
v-if="isPaypal || (!('price_id' in field.preferences) && !('payment_link_id' in field.preferences))"
class="field-settings-currency py-1.5 px-1 relative"
@click.stop
>
@@ -52,14 +52,14 @@
@click.stop
>
<input
v-if="'payment_link_id' in field.preferences"
v-if="isStripe && 'payment_link_id' in field.preferences"
v-model="field.preferences.payment_link_id"
placeholder="plink_XXXXX"
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
@blur="save"
>
<input
v-else-if="'price_id' in field.preferences"
v-else-if="isStripe && 'price_id' in field.preferences"
v-model="field.preferences.price_id"
placeholder="Price ID: price_XXXXX"
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
@@ -89,7 +89,10 @@
>
{{ 'payment_link_id' in field.preferences ? t('payment_link') : t('price') }}
</label>
<div class="flex items-center justify-center">
<div
v-if="isStripe && stripeConnected"
class="flex items-center justify-center"
>
<a
href="#"
class="hover:underline"
@@ -120,21 +123,22 @@
</div>
</div>
<div
v-if="!isConnected || isOauthSuccess"
class="field-settings-stripe-connect py-1.5 px-1 relative"
v-if="!stripeConnected && !paypalConnected"
class="flex flex-col gap-1 py-1.5 px-1"
@click.stop
>
<div
v-if="isConnected && isOauthSuccess"
class="text-sm text-center"
<a
v-if="withPaypal"
:href="paypalConnectUrl"
target="_blank"
data-turbo="false"
class="btn bg-[#003087] hover:bg-[#012169] btn-sm text-white w-full"
>
<IconCircleCheck
class="inline text-green-600 w-4 h-4"
/>
Stripe Connected
</div>
<IconBrandPaypal class="w-4 h-4 inline" />
<span>Connect PayPal</span>
</a>
<form
v-if="!isConnected"
v-if="withStripe"
data-turbo="false"
action="/auth/stripe_connect"
accept-charset="UTF-8"
@@ -170,33 +174,25 @@
:disabled="isLoading"
class="btn bg-[#7B73FF] hover:bg-[#0A2540] btn-sm text-white w-full"
>
<span
<IconInnerShadowTop
v-if="isLoading"
class="flex items-center space-x-1"
>
<IconInnerShadowTop
class="w-4 h-4 animate-spin inline"
/>
<span>
Connect Stripe
</span>
</span>
<span
class="w-4 h-4 animate-spin inline"
/>
<IconBrandStripe
v-else
class="flex items-center space-x-1"
>
<IconBrandStripe
class="w-4 h-4 inline"
/>
<span>
Connect Stripe
</span>
</span>
class="w-4 h-4 inline"
/>
<span>Connect Stripe</span>
</button>
</form>
</div>
<div
v-if="isStripe && !stripeConnected"
class="px-1 pb-1.5"
@click.stop
>
<a
v-if="!isConnected"
class="block link text-center mt-1"
class="block link text-center text-xs"
href="https://www.docuseal.com/blog/accept-payments-and-request-signatures-with-ease"
target="_blank"
data-turbo="false"
@@ -272,24 +268,25 @@
</template>
<script>
import { IconMathFunction, IconSettings, IconCircleCheck, IconInfoCircle, IconBrandStripe, IconInnerShadowTop, IconRouteAltLeft, IconForms } from '@tabler/icons-vue'
import { IconMathFunction, IconSettings, IconInfoCircle, IconBrandStripe, IconBrandPaypal, IconInnerShadowTop, IconRouteAltLeft, IconForms } from '@tabler/icons-vue'
import { ref } from 'vue'
const isConnected = ref(false)
export const stripeConnectedRef = ref(false)
export const paypalConnectedRef = ref(false)
export default {
name: 'PaymentSettings',
components: {
IconSettings,
IconCircleCheck,
IconRouteAltLeft,
IconInfoCircle,
IconForms,
IconMathFunction,
IconInnerShadowTop,
IconBrandStripe
IconBrandStripe,
IconBrandPaypal
},
inject: ['backgroundColor', 'save', 'currencies', 't', 'isPaymentConnected'],
inject: ['backgroundColor', 'save', 'currencies', 't', 'isStripeConnected', 'isPaypalConnected', 'withStripe', 'withPaypal'],
props: {
field: {
type: Object,
@@ -318,13 +315,27 @@ export default {
}
},
computed: {
isConnected: () => isConnected.value,
isOauthSuccess () {
return document.location.search?.includes('stripe_connect_success')
stripeConnected: () => stripeConnectedRef.value,
paypalConnected: () => paypalConnectedRef.value,
provider () {
if (this.paypalConnected && !this.stripeConnected) return 'paypal'
if (this.stripeConnected && !this.paypalConnected) return 'stripe'
if (this.withPaypal && !this.withStripe) return 'paypal'
return 'stripe'
},
isStripe () {
return this.provider === 'stripe'
},
isPaypal () {
return this.provider === 'paypal'
},
redirectUri () {
return document.location.origin + '/auth/stripe_connect/callback'
},
paypalConnectUrl () {
return `/auth/paypal_connect?redir=${encodeURIComponent(document.location.href)}`
},
defaultCurrencies () {
return ['USD', 'EUR', 'GBP', 'CAD', 'AUD']
},
@@ -363,26 +374,53 @@ export default {
mounted () {
this.field.preferences.currency ||= this.defaultCurrency
isConnected.value ||= this.isPaymentConnected
stripeConnectedRef.value ||= this.isStripeConnected
paypalConnectedRef.value ||= this.isPaypalConnected
if (!this.isConnected) {
this.checkStatus()
if (this.withStripe && !this.stripeConnected && !this.paypalConnected) {
this.checkStripeStatus()
}
if (this.withPaypal && !this.paypalConnected && !this.stripeConnected) {
this.checkPaypalStatus()
window.addEventListener('focus', this.onWindowFocus)
}
},
beforeUnmount () {
window.removeEventListener('focus', this.onWindowFocus)
},
methods: {
checkStatus () {
onWindowFocus () {
if (this.withPaypal && !this.paypalConnected) {
this.checkPaypalStatus()
}
},
checkStripeStatus () {
this.isLoading = true
fetch('/api/stripe_connect').then(async (resp) => {
const { status } = await resp.json()
if (status === 'connected') {
isConnected.value = true
stripeConnectedRef.value = true
window.removeEventListener('focus', this.onWindowFocus)
}
}).finally(() => {
this.isLoading = false
})
},
checkPaypalStatus () {
fetch('/api/paypal_connect').then(async (resp) => {
const { status } = await resp.json()
if (status === 'connected') {
paypalConnectedRef.value = true
window.removeEventListener('focus', this.onWindowFocus)
}
})
},
closeDropdown () {
document.activeElement.blur()
}
-17
View File
@@ -1,17 +0,0 @@
# frozen_string_literal: true
class GeneratePreviewImagesJob
include Sidekiq::Job
sidekiq_options queue: :images
def perform(params = {})
attachment = ActiveStorage::Attachment.find(params['attachment_id'])
max_page = [attachment.metadata['pdf']['number_of_pages'].to_i - 1,
Templates::ProcessDocument::MAX_NUMBER_OF_PAGES_PROCESSED].min
Templates::ProcessDocument.generate_document_preview_images(attachment, attachment.download, 1..max_page,
concurrency: 1)
end
end
+28 -13
View File
@@ -3,6 +3,9 @@
class ProcessSubmitterCompletionJob
include Sidekiq::Job
BCC_LIMIT = 14
TooManyBcc = Class.new(StandardError)
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
submission = submitter.submission
@@ -120,37 +123,49 @@ class ProcessSubmitterCompletionJob
user = submission.created_by_user || template.author
copy_email_configs = AccountConfigs.find_or_initialize_for_key(submitter.account,
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
is_copy_email_enabled = documents_copy_email_enabled?(submitter, copy_email_configs)
if submitter.account.users.exists?(id: user.id) && submission.preferences['send_email'] != false &&
(!template || template.preferences['completed_notification_email_enabled'] != false)
user_submitter = submission.submitters.find { |s| s.email == user.email }
is_sent_to_user =
if user.role != 'integration' &&
(!user_submitter || user_submitter.preferences['send_email'] == false) &&
if user.role != 'integration' && Users.send_emails?(user) &&
(!user_submitter || user_submitter.preferences['send_email'] == false || !is_copy_email_enabled) &&
user.user_configs.find_by(key: UserConfig::RECEIVE_COMPLETED_EMAIL)&.value != false
SubmitterMailer.completed_email(submitter, user).deliver_later!
true
end
build_bcc_addresses(submission).each do |to|
next if is_sent_to_user && to == user.email
SubmitterMailer.completed_email(submitter, user, to:).deliver_later!
end
enqueue_bcc_completed_emails(submitter, user, is_sent_to_user)
end
maybe_enqueue_copy_emails(submitter)
enqueue_copy_emails(submitter, copy_email_configs) if is_copy_email_enabled
end
def maybe_enqueue_copy_emails(submitter)
return if submitter.template&.preferences&.dig('documents_copy_email_enabled') == false
def enqueue_bcc_completed_emails(submitter, user, is_sent_to_user)
bcc_addresses = build_bcc_addresses(submitter.submission).uniq
configs = AccountConfigs.find_or_initialize_for_key(submitter.account,
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
raise TooManyBcc, submitter.account_id if Docuseal.multitenant? && bcc_addresses.size > BCC_LIMIT
return if configs.value['enabled'] == false
bcc_addresses.each do |to|
next if is_sent_to_user && to == user.email
SubmitterMailer.completed_email(submitter, user, to:).deliver_later!
end
end
def documents_copy_email_enabled?(submitter, configs)
return false if submitter.template&.preferences&.dig('documents_copy_email_enabled') == false
configs.value['enabled'] != false
end
def enqueue_copy_emails(submitter, configs)
to = submitter.submission.submitters.reject { |e| e.preferences['send_email'] == false }
.sort_by { |e| e.completed_at || Time.current }.select(&:email?).map(&:friendly_name)
+2
View File
@@ -31,6 +31,8 @@ class EmailMessage < ApplicationRecord
attribute :uuid, :string, default: -> { SecureRandom.uuid }
normalizes :body, with: ->(value) { value.to_s }, apply_to_nil: true
before_validation :set_sha1, on: :create
def normalized_body
+2 -1
View File
@@ -112,7 +112,8 @@ class Submission < ApplicationRecord
api: 'api',
embed: 'embed',
mcp: 'mcp',
link: 'link'
link: 'link',
self: 'self'
}, scope: false, prefix: true
enum :submitters_order, {
+1 -1
View File
@@ -90,7 +90,7 @@ class Submitter < ApplicationRecord
def friendly_name
if name.present? && email.present? && email.exclude?(',')
%("#{name.delete('"')}" <#{email}>)
%("#{name.delete('"').squish}" <#{email}>)
else
email
end
+27 -27
View File
@@ -1,7 +1,7 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
<%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto">
<h1 class="text-4xl font-bold mb-4">
<h1 class="text-3xl md:text-4xl font-bold mb-4">
<%= t('account') %>
</h1>
<%= form_for '', url: settings_account_path, method: :patch, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
@@ -53,8 +53,8 @@
<div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1">
<span class="text-left"><%= t('force_2fa_with_authenticator_app') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('require_two_factor_authentication_2fa_with_an_authenticator_app_e_g_google_authenticator_authy_all_users_signing_documents_must_pass_the_second_factor_verification_using_a_secure_code_in_addition_to_their_password') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
<span class="tooltip tooltip-top hidden md:flex cursor-pointer" data-tip="<%= t('require_two_factor_authentication_2fa_with_an_authenticator_app_e_g_google_authenticator_authy_all_users_signing_documents_must_pass_the_second_factor_verification_using_a_secure_code_in_addition_to_their_password') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4 shrink-0') %>
</span>
</div>
<submit-form data-on="change" class="flex">
@@ -70,8 +70,8 @@
<div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1">
<span class="text-left"><%= t('add_signature_id_to_the_documents') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('add_a_unique_signature_id_and_timestamp_to_each_signature_for_audit_and_traceability_purposes_along_with_the_timestamp_part_of_docuseals_21_cfr_part_11_compliance_settings') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
<span class="tooltip tooltip-top hidden md:flex cursor-pointer" data-tip="<%= t('add_a_unique_signature_id_and_timestamp_to_each_signature_for_audit_and_traceability_purposes_along_with_the_timestamp_part_of_docuseals_21_cfr_part_11_compliance_settings') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4 shrink-0') %>
</span>
</div>
<submit-form data-on="change" class="flex">
@@ -87,8 +87,8 @@
<div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1">
<span class="text-left"><%= t('require_signing_reason') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('require_signer_to_provide_a_reason_for_signing_before_completing_their_signature_e_g_approvals_certifications_part_of_docuseals_21_cfr_part_11_compliance_settings') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
<span class="tooltip tooltip-top hidden md:flex cursor-pointer" data-tip="<%= t('require_signer_to_provide_a_reason_for_signing_before_completing_their_signature_e_g_approvals_certifications_part_of_docuseals_21_cfr_part_11_compliance_settings') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4 shrink-0') %>
</span>
</div>
<submit-form data-on="change" class="flex">
@@ -104,8 +104,8 @@
<div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1">
<span class="text-left"><%= t('allow_typed_text_signatures') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('allow_signers_to_create_signatures_by_typing_their_name_instead_of_drawing_or_uploading_one') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
<span class="tooltip tooltip-top hidden md:flex cursor-pointer" data-tip="<%= t('allow_signers_to_create_signatures_by_typing_their_name_instead_of_drawing_or_uploading_one') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4 shrink-0') %>
</span>
</div>
<submit-form data-on="change" class="flex">
@@ -121,8 +121,8 @@
<div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1">
<span class="text-left"><%= t('allow_to_resubmit_completed_forms') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('allow_signers_to_resubmit_forms_after_completion_useful_when_corrections_or_multiple_submissions_are_needed') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
<span class="tooltip tooltip-top hidden md:flex cursor-pointer" data-tip="<%= t('allow_signers_to_resubmit_forms_after_completion_useful_when_corrections_or_multiple_submissions_are_needed') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4 shrink-0') %>
</span>
</div>
<submit-form data-on="change" class="flex">
@@ -138,8 +138,8 @@
<div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1">
<span class="text-left"><%= t('allow_to_decline_documents') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('allow_recipients_to_decline_signing_a_document_the_decline_reason_notification_will_be_sent_to_the_signature_requester') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
<span class="tooltip tooltip-top hidden md:flex cursor-pointer" data-tip="<%= t('allow_recipients_to_decline_signing_a_document_the_decline_reason_notification_will_be_sent_to_the_signature_requester') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4 shrink-0') %>
</span>
</div>
<% if !Docuseal.multitenant? || can?(:manage, :disable_decline) %>
@@ -147,7 +147,7 @@
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false %>
</submit-form>
<% else %>
<a href="<%= console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") %>" data-turbo="false" data-tip="<%= I18n.t('unlock_with_docuseal_pro') %>" data-on="change" class="flex tooltip">
<a href="<%= console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") %>" data-turbo="false" data-tip="<%= I18n.t('unlock_with_docuseal_pro') %>" data-on="change" class="flex tooltip tooltip-no-touch">
<%= f.check_box :value, class: 'toggle pointer-events-none', checked: account_config.value != false, disabled: true %>
</a>
<% end %>
@@ -161,8 +161,8 @@
<div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1">
<span class="text-left"><%= t('allow_to_delegate_documents') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('allow_recipients_to_delegate_documents') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
<span class="tooltip tooltip-top hidden md:flex cursor-pointer" data-tip="<%= t('allow_recipients_to_delegate_documents') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4 shrink-0') %>
</span>
</div>
<% if !Docuseal.multitenant? || can?(:manage, :delegate_form) %>
@@ -170,7 +170,7 @@
<%= f.check_box :value, class: 'toggle', checked: account_config.value == true %>
</submit-form>
<% else %>
<a href="<%= console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") %>" data-turbo="false" data-tip="<%= I18n.t('unlock_with_docuseal_pro') %>" class="flex tooltip">
<a href="<%= console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") %>" data-turbo="false" data-tip="<%= I18n.t('unlock_with_docuseal_pro') %>" class="flex tooltip tooltip-no-touch">
<%= f.check_box :value, class: 'toggle pointer-events-none', checked: account_config.value == true, disabled: true %>
</a>
<% end %>
@@ -184,8 +184,8 @@
<div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1">
<span class="text-left"><%= t('remember_and_pre_fill_signatures') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('save_a_users_signature_and_automatically_pre_fill_it_in_future_signing_sessions') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
<span class="tooltip tooltip-top hidden md:flex cursor-pointer" data-tip="<%= t('save_a_users_signature_and_automatically_pre_fill_it_in_future_signing_sessions') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4 shrink-0') %>
</span>
</div>
<submit-form data-on="change" class="flex">
@@ -201,8 +201,8 @@
<div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1">
<span class="text-left"><%= t('expirable_file_download_links') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('make_document_download_links_expire_after_40_minutes_to_prevent_long_term_access_and_enhance_security') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
<span class="tooltip tooltip-top hidden md:flex cursor-pointer" data-tip="<%= t('make_document_download_links_expire_after_40_minutes_to_prevent_long_term_access_and_enhance_security') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4 shrink-0') %>
</span>
</div>
<submit-form data-on="change" class="flex">
@@ -218,8 +218,8 @@
<div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1">
<span class="text-left"><%= t('require_authentication_for_file_download_links') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('require_authentication_with_user_login_or_api_key_to_access_the_document_download_links') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
<span class="tooltip tooltip-top hidden md:flex cursor-pointer" data-tip="<%= t('require_authentication_with_user_login_or_api_key_to_access_the_document_download_links') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4 shrink-0') %>
</span>
</div>
<submit-form data-on="change" class="flex">
@@ -235,8 +235,8 @@
<div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1">
<span class="text-left"><%= t('combine_completed_documents_and_audit_log') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('combine_signed_documents_and_the_audit_log_into_a_single_pdf_file_for_easier_recordkeeping_and_compliance') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
<span class="tooltip tooltip-top hidden md:flex cursor-pointer" data-tip="<%= t('combine_signed_documents_and_the_audit_log_into_a_single_pdf_file_for_easier_recordkeeping_and_compliance') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4 shrink-0') %>
</span>
</div>
<submit-form data-on="change" class="flex">
@@ -253,8 +253,8 @@
<div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1">
<span class="text-left"><%= t('always_enforce_signing_order') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('make_the_recipients_signing_order_always_enforced_so_that_the_second_signer_can_start_signing_their_part_only_after_the_first_signer_has_completed_signing') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
<span class="tooltip tooltip-top hidden md:flex cursor-pointer" data-tip="<%= t('make_the_recipients_signing_order_always_enforced_so_that_the_second_signer_can_start_signing_their_part_only_after_the_first_signer_has_completed_signing') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4 shrink-0') %>
</span>
</div>
<submit-form data-on="change" class="flex">
+1 -1
View File
@@ -2,7 +2,7 @@
<%= render 'shared/settings_nav' %>
<div class="flex-grow">
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-end mb-4">
<h1 class="text-4xl font-bold">API</h1>
<h1 class="text-3xl md:text-4xl font-bold">API</h1>
<%= render 'shared/test_mode_toggle' %>
</div>
<div class="card bg-base-200">
+2 -2
View File
@@ -1,10 +1,10 @@
<form action="<%= root_path %>" method="get" id="templates_submissions_toggle" class="bg-base-200 px-1.5 rounded-xl py-1 whitespace-nowrap">
<toggle-cookies data-value="templates" data-key="dashboard_view" class="sm:tooltip tooltip-top" data-tip="<%= t('templates') %>">
<toggle-cookies data-value="templates" data-key="dashboard_view" class="tooltip tooltip-top tooltip-no-touch" data-tip="<%= t('templates') %>">
<button class="<%= local_assigns[:selected] == 'submissions' ? 'btn !border !rounded-lg btn-square !p-0 !btn-sm !h-8 !w-9' : 'btn btn-neutral !rounded-lg btn-square !p-0 hover:text-neutral-300 !btn-sm !h-8 !w-9 disabled:btn-neutral' %>">
<%= svg_icon('layout_grid', class: 'w-6 h-6 stroke-2') %>
</button>
</toggle-cookies>
<toggle-cookies data-value="submissions" data-key="dashboard_view" class="sm:tooltip tooltip-top" data-tip="<%= t('submissions') %>">
<toggle-cookies data-value="submissions" data-key="dashboard_view" class="tooltip tooltip-top tooltip-no-touch" data-tip="<%= t('submissions') %>">
<button class="<%= local_assigns[:selected] == 'submissions' ? 'btn btn-neutral !rounded-lg btn-square !p-0 hover:text-neutral-300 !btn-sm !h-8 !w-9' : 'btn !border !rounded-lg btn-square !p-0 !btn-sm !h-8 !w-9 disabled:btn-neutral' %>">
<%= svg_icon('layout_list', class: 'w-6 h-6 stroke-2') %>
</button>
+1 -1
View File
@@ -1,4 +1,4 @@
<div class="form-control">
<%= label_tag 'user[otp_attempt]', t('two_factor_code_from_authenticator_app'), class: 'label' %>
<%= text_field_tag 'user[otp_attempt]', nil, autofocus: true, autocomplete: 'off', placeholder: 'XXX-XXX', required: true, class: 'base-input' %>
<%= text_field_tag 'user[otp_attempt]', nil, autofocus: true, autocomplete: 'off', inputmode: 'numeric', placeholder: 'XXXXXX', required: true, class: 'base-input' %>
</div>
+3 -3
View File
@@ -1,7 +1,7 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
<%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto">
<h1 class="text-4xl font-bold mb-4">Email SMTP</h1>
<div class="flex-grow max-w-xl mx-auto min-w-0">
<h1 class="text-3xl md:text-4xl font-bold mb-4">Email SMTP</h1>
<% value = @encrypted_config.value || {} %>
<%= form_for @encrypted_config, url: settings_email_index_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<%= f.fields_for :value do |ff| %>
@@ -38,7 +38,7 @@
<% if !Docuseal.multitenant? || can?(:manage, :personalization_advanced) %>
<div class="form-control">
<%= ff.label :security_label, 'SMTP Security', class: 'label' %>
<div class="flex items-center space-x-6">
<div class="flex flex-wrap items-center gap-x-6">
<% [%w[STARTTLS none], %w[TLS tls], %w[SSL ssl], %w[Noverify noverify]].each do |(label, val)| %>
<%= ff.label :security, value: val, for: "#{val}_radio", class: 'label' do %>
<%= ff.radio_button :security, val, checked: (value['security'].blank? && val == 'none') || value['security'] == val, id: "#{val}_radio", class: 'base-radio mr-2' %>
@@ -1,15 +1,15 @@
<tr scope="row" class="group">
<tr class="group">
<td class="flex items-center space-x-1">
<%= svg_icon('discount_check_filled', class: 'w-6 h-6 text-green-500') %>
<%= svg_icon('discount_check_filled', class: 'w-6 h-6 shrink-0 text-green-500') %>
<span class="flex items-center">
<%= t('docuseal_trusted_signature') %>
<div class="tooltip ml-1" data-tip="<%= t('sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature') %>">
<%= svg_icon('circle_question', class: 'w-4 h-4 stroke-1') %>
<span class="whitespace-nowrap"><%= t('docuseal_trusted_signature') %></span>
<div class="tooltip tooltip-top ml-1 shrink-0" data-tip="<%= t('sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature') %>">
<%= svg_icon('circle_question', class: 'w-4 h-4 stroke-1 shrink-0') %>
</div>
</span>
</td>
<td>
<a href="<%= "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}" %>" class="btn btn-neutral btn-sm text-white">
<a href="<%= "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}" %>" class="btn btn-neutral btn-sm text-white whitespace-nowrap">
<%= t('unlock_with_docuseal_pro') %>
</a>
</td>
+5 -5
View File
@@ -2,7 +2,7 @@
<%= render 'shared/settings_nav' %>
<div class="md:flex-grow">
<div class="max-w-xl">
<h1 class="text-4xl font-bold mb-4">
<h1 class="text-3xl md:text-4xl font-bold mb-4">
<%= t('pdf_signature') %>
</h1>
<div id="result">
@@ -41,7 +41,7 @@
<% end %>
</div>
<div class="flex flex-col md:flex-row gap-2 md:justify-between md:items-end mb-4 mt-8">
<h2 class="text-3xl font-bold">
<h2 class="text-2xl md:text-3xl font-bold">
<%= t('signing_certificates') %>
</h2>
<% if can?(:create, @encrypted_config) %>
@@ -72,7 +72,7 @@
<tbody>
<% @pkcs_list.each do |item| %>
<% next if item['pkcs'].blank? %>
<tr scope="row" class="group">
<tr class="group">
<td>
<%= item['name'] %>
</td>
@@ -109,7 +109,7 @@
<% if !Docuseal.multitenant? && can?(:manage, encrypted_config) %>
<div class="flex-grow max-w-xl">
<div class="flex justify-between items-end mb-4 mt-8">
<h2 class="text-3xl font-bold">
<h2 class="text-2xl md:text-3xl font-bold">
<%= t('timestamp_server') %>
</h2>
</div>
@@ -137,7 +137,7 @@
<% if can?(:manage, account_config) %>
<div class="px-1 mt-8 max-w-xl">
<div class="flex justify-between items-end mb-4 mt-8">
<h2 class="text-3xl font-bold">
<h2 class="text-2xl md:text-3xl font-bold">
<%= t('preferences') %>
</h2>
</div>
+1 -1
View File
@@ -1 +1 @@
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-brand-google-drive"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M12 10l-6 10l-3 -5l6 -10z" /><path d="M9 15h12l-3 5h-12" /><path d="M15 15l-6 -10h6l6 10z" /></svg>
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M12 10l-6 10l-3 -5l6 -10z" /><path d="M9 15h12l-3 5h-12" /><path d="M15 15l-6 -10h6l6 10z" /></svg>

Before

Width:  |  Height:  |  Size: 458 B

After

Width:  |  Height:  |  Size: 381 B

+1 -2
View File
@@ -1,8 +1,7 @@
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 5a1 1 0 0 1 1 -1h16a1 1 0 0 1 1 1v10a1 1 0 0 1 -1 1h-16a1 1 0 0 1 -1 -1v-10z" />
<path d="M7 20h10" /><path d="M9 16v4" />
<path d="M15 16v4" /></svg>
<path d="M7 20h10" />
<path d="M9 16v4" />
<path d="M15 16v4" />
</svg>

Before

Width:  |  Height:  |  Size: 497 B

After

Width:  |  Height:  |  Size: 447 B

+4 -4
View File
@@ -2,7 +2,7 @@
<%= render 'shared/settings_nav' %>
<div class="md:flex-grow">
<div class="flex flex-col md:flex-row md:flex-wrap gap-2 md:justify-between md:items-end mb-4 min-h-12">
<h1 class="text-4xl font-bold">
<h1 class="text-3xl md:text-4xl font-bold">
<%= t('mcp_server') %>
</h1>
<div class="flex flex-col md:flex-row gap-y-2 gap-x-4 md:items-center">
@@ -67,7 +67,7 @@
</thead>
<tbody>
<% @mcp_tokens.each do |mcp_token| %>
<tr scope="row">
<tr>
<td>
<%= mcp_token.name %>
</td>
@@ -98,8 +98,8 @@
<div class="flex items-center gap-4 py-2.5">
<div class="flex items-center space-x-1">
<span class="text-left"><%= t('enable_mcp_server') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('all_existing_mcp_connections_will_be_stopped_immediately_when_this_setting_is_disabled') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
<span class="tooltip tooltip-top hidden md:flex cursor-pointer" data-tip="<%= t('all_existing_mcp_connections_will_be_stopped_immediately_when_this_setting_is_disabled') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4 shrink-0') %>
</span>
</div>
<submit-form data-on="change" class="flex">
+2 -2
View File
@@ -3,10 +3,10 @@
<%= t('use_an_authenticator_mobile_app_like_google_authenticator_or_1password_to_scan_the_qr_code_below') %>
</p>
<div>
<%== RQRCode::QRCode.new(@provision_url).as_svg(viewbox: true, svg_attributes: { class: 'md:w-80 md:h-80 my-4 mx-auto' }) %>
<%== RQRCode::QRCode.new(@provision_url).as_svg(viewbox: true, svg_attributes: { class: 'md:w-80 md:h-80 my-4 mx-auto' }).sub(/\A<\?xml[^>]*\?>/, '') %>
</div>
<div class="form-control my-6 space-y-2">
<%= f.text_field :otp_attempt, required: true, placeholder: 'XXX-XXX', class: 'base-input text-center' %>
<%= f.text_field :otp_attempt, required: true, inputmode: 'numeric', placeholder: 'XXXXXX', class: 'base-input text-center' %>
<span>
<%= @error_message %>
</span>
+1 -1
View File
@@ -1,7 +1,7 @@
<%= render 'shared/turbo_modal', title: t('remove_2fa') do %>
<%= form_for '', url: mfa_setup_path, method: :delete, data: { turbo_frame: :_top } do |f| %>
<div class="form-control my-6 space-y-2">
<%= f.text_field :otp_attempt, required: true, placeholder: 'XXX-XXX', class: 'base-input text-center' %>
<%= f.text_field :otp_attempt, required: true, inputmode: 'numeric', placeholder: 'XXXXXX', class: 'base-input text-center' %>
<span>
<%= @error_message %>
</span>
@@ -6,7 +6,7 @@
<span>
<%= t('completed_documents_notification_bcc_address') %>
</span>
<span class="tooltip" data-tip="<%= t('send_email_copy_with_completed_documents_to_a_specified_bcc_address') %>">
<span class="tooltip hidden md:inline-flex" data-tip="<%= t('send_email_copy_with_completed_documents_to_a_specified_bcc_address') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</span>
@@ -1,7 +1,7 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
<%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto">
<h1 class="text-4xl font-bold mb-4">
<h1 class="text-3xl md:text-4xl font-bold mb-4">
<%= t('email_notifications') %>
</h1>
<div class="mt-2 mb-1">
@@ -21,13 +21,15 @@
<% end %>
</div>
<%= render 'bcc_form', config: @bcc_config %>
<div class="flex justify-between items-end mb-4 mt-8">
<h2 class="text-3xl font-bold">
<%= t('sign_request_email_reminders') %>
</h2>
</div>
<%= render 'reminder_banner' %>
<%= render 'reminder_form', config: @reminder_config %>
<% unless can?(:manage, :personalization_restricted) %>
<div class="flex justify-between items-end mb-4 mt-8">
<h2 class="text-2xl md:text-3xl font-bold">
<%= t('sign_request_email_reminders') %>
</h2>
</div>
<%= render 'reminder_banner' %>
<%= render 'reminder_form', config: @reminder_config %>
<% end %>
</div>
<div class="w-0 md:w-52"></div>
</div>
@@ -6,11 +6,16 @@
</div>
</div>
<div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'flex flex-col gap-4' } do |f| %>
<%= f.hidden_field :key %>
<%= f.fields_for :value, Struct.new(:subject, :body, :reply_to, :attach_audit_log, :attach_documents, :bcc_recipients, :enabled).new(*f.object.value.values_at('subject', 'body', 'reply_to', 'attach_audit_log', 'attach_documents', 'bcc_recipients', 'enabled')) do |ff| %>
<div class="form-control">
<%= ff.label :subject, t('subject'), class: 'label' %>
<div class="flex items-center">
<%= ff.label :subject, t('subject'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('completed_documents_copy_email_sent_to_recipients_upon_completion_containing_their_documents') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<%= render 'personalization_settings/email_body_field', ff:, config: f.object %>
@@ -1,21 +1,21 @@
<div class="flex items-center px-2 py-2 border-b" style="height: 42px;">
<div class="flex items-center gap-1">
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('bold') %> (Ctrl+B)">
<div class="tooltip tooltip-top tooltip-no-touch before:text-xs" data-tip="<%= t('bold') %> (Ctrl+B)">
<button type="button" data-action="click:<%= editor_tag %>#bold" data-target="<%= editor_tag %>.boldButton" aria-label="<%= t('bold') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('bold', class: 'w-4 h-4') %>
</button>
</div>
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('italic') %> (Ctrl+I)">
<div class="tooltip tooltip-top tooltip-no-touch before:text-xs" data-tip="<%= t('italic') %> (Ctrl+I)">
<button type="button" data-action="click:<%= editor_tag %>#italic" data-target="<%= editor_tag %>.italicButton" aria-label="<%= t('italic') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('italic', class: 'w-4 h-4') %>
</button>
</div>
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('underline') %> (Ctrl+U)">
<div class="tooltip tooltip-top tooltip-no-touch before:text-xs" data-tip="<%= t('underline') %> (Ctrl+U)">
<button type="button" data-action="click:<%= editor_tag %>#underline" data-target="<%= editor_tag %>.underlineButton" aria-label="<%= t('underline') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('underline', class: 'w-4 h-4') %>
</button>
</div>
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('link') %> (Ctrl+K)">
<div class="tooltip tooltip-top tooltip-no-touch before:text-xs" data-tip="<%= t('link') %> (Ctrl+K)">
<button type="button" data-action="click:<%= editor_tag %>#linkSelection" data-target="<%= editor_tag %>.linkButton" aria-label="<%= t('link') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('link', class: 'w-4 h-4') %>
</button>
@@ -23,12 +23,12 @@
</div>
<div class="mx-2 h-5 border-l border-base-content/20"></div>
<div class="flex items-center gap-1">
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('undo') %> (Ctrl+Z)">
<div class="tooltip tooltip-top tooltip-no-touch before:text-xs" data-tip="<%= t('undo') %> (Ctrl+Z)">
<button type="button" data-action="click:<%= editor_tag %>#undo" data-target="<%= editor_tag %>.undoButton" aria-label="<%= t('undo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('arrow_back_up', class: 'w-4 h-4') %>
</button>
</div>
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('redo') %> (Ctrl+Shift+Z)">
<div class="tooltip tooltip-top tooltip-no-touch before:text-xs" data-tip="<%= t('redo') %> (Ctrl+Shift+Z)">
<button type="button" data-action="click:<%= editor_tag %>#redo" data-target="<%= editor_tag %>.redoButton" aria-label="<%= t('redo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('arrow_forward_up', class: 'w-4 h-4') %>
</button>
@@ -37,8 +37,9 @@
<% if local_assigns[:variables]&.any? %>
<% variable_labels = { 'account.name' => t('variables.account_name'), 'submitter.link' => t('variables.submitter_link'), 'template.name' => t('variables.template_name'), 'submission.submitters' => t('variables.submission_submitters'), 'submission.link' => t('variables.submission_link'), 'documents.link' => t('variables.documents_link') } %>
<div class="dropdown dropdown-end ml-auto">
<label tabindex="0" class="flex items-center gap-1 text-sm px-2 py-1 rounded hover:bg-base-200 cursor-pointer">
<%= t('add_variable') %>
<label tabindex="0" class="flex items-center gap-1 text-sm px-2 py-1 rounded hover:bg-base-200 cursor-pointer whitespace-nowrap">
<span class="hidden sm:inline"><%= t('add_variable') %></span>
<span class="sm:hidden"><%= t('variable') %></span>
<%= svg_icon('chevron_down', class: 'w-3.5 h-3.5') %>
</label>
<div tabindex="0" class="dropdown-content right-0 top-full mt-1 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg z-50">
@@ -6,7 +6,7 @@
</div>
</div>
<div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::FORM_COMPLETED_BUTTON_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::FORM_COMPLETED_BUTTON_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'flex flex-col gap-4' } do |f| %>
<%= f.hidden_field :key %>
<%= f.fields_for :value, Struct.new(:title, :url).new(*(f.object.value || {}).values_at('title', 'url')) do |ff| %>
<div class="form-control">
@@ -6,7 +6,7 @@
</div>
</div>
<div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::FORM_COMPLETED_MESSAGE_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::FORM_COMPLETED_MESSAGE_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'flex flex-col gap-4' } do |f| %>
<%= f.hidden_field :key %>
<%= f.fields_for :value, Struct.new(:title, :body).new(*(f.object.value || {}).values_at('title', 'body')) do |ff| %>
<div class="form-control">
@@ -7,7 +7,7 @@
</div>
</div>
<div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::POLICY_LINKS_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::POLICY_LINKS_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'flex flex-col gap-4' } do |f| %>
<%= f.hidden_field :key %>
<div class="form-control">
<autoresize-textarea>
@@ -6,11 +6,16 @@
</div>
</div>
<div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'flex flex-col gap-4' } do |f| %>
<%= f.hidden_field :key %>
<%= f.fields_for :value, Struct.new(:subject, :body, :reply_to).new(*f.object.value.values_at('subject', 'body', 'reply_to')) do |ff| %>
<div class="form-control">
<%= ff.label :subject, t('subject'), class: 'label' %>
<div class="flex items-center">
<%= ff.label :subject, t('subject'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('signature_request_email_sent_to_recipients_with_a_link_to_review_and_sign') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<%= render 'personalization_settings/email_body_field', ff:, config: f.object %>
@@ -6,13 +6,13 @@
</div>
</div>
<div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'flex flex-col gap-4' } do |f| %>
<%= f.hidden_field :key %>
<%= f.fields_for :value, Struct.new(:subject, :body, :attach_audit_log, :attach_documents).new(*f.object.value.values_at('subject', 'body', 'attach_audit_log', 'attach_documents')) do |ff| %>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :subject, t('subject'), class: 'label' %>
<span class="tooltip" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY].map { |v| "{#{v}}" }.join(', ') %>">
<span class="tooltip tooltip-right" data-tip="<%= t('notification_email_sent_to_the_sender_user_and_bcc_addresses_once_the_submission_is_completed_by_all_parties') %> <%= t('use_following_placeholders_text_') %> <%= AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY].map { |v| "{#{v}}" }.join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
@@ -1,7 +1,7 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
<%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto">
<p class="text-4xl font-bold mb-4">
<p class="text-3xl md:text-4xl font-bold mb-4">
<%= t('email_templates') %>
</p>
<div class="space-y-4">
@@ -9,11 +9,11 @@
<%= render 'documents_copy_email_form' %>
<%= render 'submitter_completed_email_form' %>
</div>
<p class="text-4xl font-bold mb-4 mt-8">
<p class="text-3xl md:text-4xl font-bold mb-4 mt-8">
<%= t('company_logo') %>
</p>
<%= render 'logo_form' %>
<p class="text-4xl font-bold mb-4 mt-8">
<p class="text-3xl md:text-4xl font-bold mb-4 mt-8">
<%= t('submission_form') %>
</p>
<div class="space-y-4">
+1 -1
View File
@@ -1,7 +1,7 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
<%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto">
<h1 class="text-4xl font-bold mb-4">
<h1 class="text-3xl md:text-4xl font-bold mb-4">
<%= t('profile') %>
</h1>
<%= form_for current_user, url: update_contact_settings_profile_index_path, method: :patch, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
+7
View File
@@ -0,0 +1,7 @@
<div class="mb-2 w-full">
<a href="<%= root_path %>" class="flex items-center space-x-0.5 font-medium">
<%= svg_icon('arrow_left', class: 'w-4 h-4 stroke-2') %>
<span><%= t('back') %></span>
</a>
</div>
<%= render 'shared/settings_nav', expanded: true %>
+1 -1
View File
@@ -1 +1 @@
<% 'stats stat stat-figure stat-title stat-value text-accent w-fit hover:bg-white dropdown-open border-base-content/30 before:border-base-content/30' %>
<% 'stats stat stat-figure stat-title stat-value text-accent w-fit hover:bg-white dropdown-open border-base-content/30 before:border-base-content/30 order-1 order-2 order-3 order-4 order-5 lg:inline lg:hidden' %>
+1 -1
View File
@@ -1,4 +1,4 @@
<a target="_blank" href="<%= Docuseal::GITHUB_URL %>" rel="noopener noreferrer nofollow" class="relative flex items-center rounded-full px-2 py-0.5 text-xs leading-4 mt-1 text-base-content border border-base-300 tooltip tooltip-bottom" data-tip="Give a star on GitHub">
<a target="_blank" href="<%= Docuseal::GITHUB_URL %>" rel="noopener noreferrer nofollow" class="relative flex items-center rounded-full px-2 py-0.5 text-xs leading-4 mt-1 text-base-content border border-base-300 tooltip tooltip-bottom tooltip-no-touch" data-tip="Give a star on GitHub">
<span class="flex items-center justify-between space-x-0.5 font-medium">
<%= svg_icon('start', class: 'h-3 w-3') %>
<span>18k</span>
+7 -1
View File
@@ -28,7 +28,13 @@
<span class="text-sm align-text-top"><%= current_user.initials %></span>
</label>
<ul tabindex="0" class="z-10 dropdown-content p-2 mt-2 shadow menu text-base bg-base-100 rounded-box min-w-[160px] text-right">
<li>
<li class="md:hidden">
<%= link_to settings_path, class: 'flex items-center' do %>
<%= svg_icon('adjustments', class: 'w-5 h-5 flex-shrink-0 stroke-2') %>
<span class="mr-1"><%= t('settings') %></span>
<% end %>
</li>
<li class="hidden md:list-item">
<%= link_to settings_profile_index_path, class: 'flex items-center' do %>
<%= svg_icon('adjustments', class: 'w-5 h-5 flex-shrink-0 stroke-2') %>
<span class="mr-1"><%= t('profile') %></span>
+12 -9
View File
@@ -1,14 +1,17 @@
<% if pagy.pages > 1 %>
<div class="flex my-6 justify-center md:justify-between">
<div class="hidden md:block text-sm">
<% if pagy.count.nil? %>
<%= t("pagination.#{local_assigns.fetch(:items_name, 'items')}.range_without_total", from: local_assigns.fetch(:from, pagy.from), to: local_assigns.fetch(:to, pagy.to)) %>
<% else %>
<%= t("pagination.#{local_assigns.fetch(:items_name, 'items')}.range_with_total", from: local_assigns.fetch(:from, pagy.from), to: local_assigns.fetch(:to, pagy.to), count: local_assigns.fetch(:count, pagy.count)) %>
<% end %>
<%= local_assigns[:left_additional_html] %>
<% left_additional_html = local_assigns[:left_additional_html] %>
<div class="flex flex-col md:flex-row md:items-center <%= left_additional_html.present? ? 'mt-3' : 'mt-6' %> mb-6 md:my-6 justify-center md:justify-between">
<div class="text-sm <%= 'mb-2 md:mb-0' if left_additional_html.present? %>">
<span class="hidden md:inline">
<% if pagy.count.nil? %>
<%= t("pagination.#{local_assigns.fetch(:items_name, 'items')}.range_without_total", from: local_assigns.fetch(:from, pagy.from), to: local_assigns.fetch(:to, pagy.to)) %>
<% else %>
<%= t("pagination.#{local_assigns.fetch(:items_name, 'items')}.range_with_total", from: local_assigns.fetch(:from, pagy.from), to: local_assigns.fetch(:to, pagy.to), count: local_assigns.fetch(:count, pagy.count)) %>
<% end %>
</span>
<%= left_additional_html %>
</div>
<div class="flex items-center space-x-1.5">
<div class="flex items-center space-x-1.5 justify-center md:justify-end">
<%= local_assigns[:right_additional_html] %>
<div class="join">
<% if pagy.previous %>
+1 -1
View File
@@ -15,7 +15,7 @@
</div>
<% end %>
<search-input class="group flex items-center max-md:focus-within:flex-1 <%= 'max-md:flex-1' if params[:q].present? %>">
<input id="search" name="q" value="<%= params[:q] %>" enterkeyhint="search" class="input text-lg pr-10 -mr-12 w-0 md:w-60 max-md:group-focus-within:w-full <%= 'pl-8 input-outlined max-md:w-full' if params[:q].present? %>" placeholder="<%= local_assigns[:placeholder] %>">
<input id="search" name="q" value="<%= params[:q] %>" enterkeyhint="search" class="input text-lg pr-10 -mr-12 w-0 md:w-60 max-md:group-focus-within:w-full <%= 'pl-8 input-outlined w-full' if params[:q].present? %>" placeholder="<%= local_assigns[:placeholder] %>">
<button type="submit" title="<%= t('search') %>" class="btn btn-ghost btn-circle">
<span class="enabled">
<%= svg_icon('search', class: 'w-6 h-6 stroke-2') %>
+153 -135
View File
@@ -1,150 +1,168 @@
<div class="block w-full md:w-52 flex-none">
<menu-active>
<ul id="account_settings_menu" class="menu px-0">
<li class="menu-title py-0 !bg-transparent mb-3 -mt-5"><a href="<%= '/' %>" class="!bg-transparent !text-neutral font-medium flex items-center space-x-0.5"><%= svg_icon('arrow_left', class: 'w-4 h-4 stroke-2') %><span><%= t('back') %></span></a></li>
<li class="menu-title py-0 !bg-transparent">
<span class="!bg-transparent"><%= t('settings') %></span>
</li>
<li></li>
<li>
<%= link_to t('profile'), settings_profile_index_path, class: 'text-base hover:bg-base-300' %>
</li>
<li>
<%= link_to t('account'), settings_account_path, class: 'text-base hover:bg-base-300' %>
</li>
<% if (!Docuseal.multitenant? || EncryptedConfig.exists?(key: EncryptedConfig::EMAIL_SMTP_KEY, account: current_account)) && can?(:read, EncryptedConfig.new(key: EncryptedConfig::EMAIL_SMTP_KEY, account: current_account)) && ENV['SMTP_ADDRESS'].blank? && true_user == current_user %>
<li>
<%= link_to t('email'), settings_email_index_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% unless Docuseal.multitenant? %>
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::FILES_STORAGE_KEY, account: current_account)) && true_user == current_user && ENV['S3_ATTACHMENTS_BUCKET'].blank? && ENV['GCS_BUCKET'].blank? && ENV['AZURE_CONTAINER'].blank? %>
<li>
<%= link_to t('storage'), settings_storage_index_path, class: 'text-base hover:bg-base-300' %>
<% expanded = local_assigns[:expanded] %>
<div class="<%= expanded ? 'block -mx-3 md:mx-0' : 'w-full md:w-52 flex-none' %>">
<% unless expanded %>
<div class="md:hidden w-full">
<a href="<%= settings_path %>" class="flex items-center space-x-0.5 font-medium">
<%= svg_icon('arrow_left', class: 'w-4 h-4 stroke-2') %>
<span><%= t('settings') %></span>
</a>
</div>
<% end %>
<div class="<%= 'hidden md:block' unless expanded %>">
<menu-active>
<ul id="account_settings_menu" class="menu px-0">
<% unless expanded %>
<li class="menu-title py-0 !bg-transparent mb-3 -mt-5">
<a href="<%= root_path %>" class="!bg-transparent !text-neutral font-medium flex items-center space-x-0.5">
<%= svg_icon('arrow_left', class: 'w-4 h-4 stroke-2') %>
<span><%= t('back') %></span>
</a>
</li>
<% end %>
<% if can?(:read, EncryptedConfig.new(key: 'submitter_invitation_sms', account: current_account)) && true_user == current_user %>
<li class="menu-title py-0 !bg-transparent">
<span class="!bg-transparent"><%= t('settings') %></span>
</li>
<li></li>
<li>
<%= link_to t('profile'), settings_profile_index_path, class: 'text-base hover:bg-base-300' %>
</li>
<li>
<%= link_to t('account'), settings_account_path, class: 'text-base hover:bg-base-300' %>
</li>
<% if (!Docuseal.multitenant? || EncryptedConfig.exists?(key: EncryptedConfig::EMAIL_SMTP_KEY, account: current_account)) && can?(:read, EncryptedConfig.new(key: EncryptedConfig::EMAIL_SMTP_KEY, account: current_account)) && ENV['SMTP_ADDRESS'].blank? && true_user == current_user %>
<li>
<%= link_to 'SMS', settings_sms_path, class: 'text-base hover:bg-base-300' %>
<%= link_to t('email'), settings_email_index_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% end %>
<% if can?(:read, AccountConfig) %>
<li>
<%= link_to t('notifications'), settings_notifications_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::ESIGN_CERTS_KEY, account: current_account)) %>
<li>
<%= link_to t('e_signature'), settings_esign_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% if can?(:read, AccountConfig) %>
<li>
<%= link_to t('personalization'), settings_personalization_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% if can?(:read, User) %>
<li>
<%= link_to t('users'), settings_users_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<%= render 'shared/settings_nav_extra' %>
<% if Docuseal.demo? || !Docuseal.multitenant? %>
<% if can?(:read, AccessToken) %>
<li>
<%= link_to 'API', settings_api_index_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% end %>
<% if Docuseal.demo? || !Docuseal.multitenant? || (current_user != true_user && !current_account.testing?) %>
<% if can?(:read, WebhookUrl) %>
<li>
<%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% end %>
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user != true_user || !current_account.linked_account_account) %>
<li>
<%= content_for(:pro_link) || link_to(Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}", class: 'text-base hover:bg-base-300', data: { turbo: false }) do %>
<%= t('plans') %>
<span class="badge badge-warning"><%= t('pro') %></span>
<% end %>
</li>
<% end %>
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user == true_user || current_account.testing?) %>
<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 %>
<% if Docuseal.multitenant? %> API <% else %> <%= t('console') %> <% end %>
<% end %>
</li>
<% if Docuseal.multitenant? %>
<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 %>
<%= t('embedding') %>
<% end %>
</li>
<% end %>
<% if (!Docuseal.multitenant? || can?(:manage, :saml_sso)) && can?(:read, EncryptedConfig.new(key: 'saml_configs', account: current_account)) && true_user == current_user %>
<li>
<%= link_to 'SSO', settings_sso_index_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% if !Docuseal.multitenant? && can?(:read, McpToken) && can?(:manage, :mcp) %>
<li>
<%= link_to 'MCP', settings_mcp_index_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<%= render 'shared/settings_nav_extra2' %>
<% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %>
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :post, html: { class: 'w-full' } do |f| %>
<% unless Docuseal.multitenant? %>
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::FILES_STORAGE_KEY, account: current_account)) && true_user == current_user && ENV['S3_ATTACHMENTS_BUCKET'].blank? && ENV['GCS_BUCKET'].blank? && ENV['AZURE_CONTAINER'].blank? %>
<li>
<label class="flex items-center text-base hover:bg-base-300 w-full justify-between" for="testing_toggle">
<span class="mr-2 w-full">
<%= t('test_mode') %>
</span>
<submit-form data-on="change" class="flex">
<%= f.check_box :testing_toggle, class: 'toggle toggle-sm', checked: current_account.testing? %>
</submit-form>
</label>
<%= link_to t('storage'), settings_storage_index_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% if can?(:read, EncryptedConfig.new(key: 'submitter_invitation_sms', account: current_account)) && true_user == current_user %>
<li>
<%= link_to 'SMS', settings_sms_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% end %>
<% end %>
</ul>
</menu-active>
<% if Docuseal.multitenant? || cannot?(:manage, :tenants) %>
<div id="support_channels" class="mx-4 border-t border-base-300 hidden md:block">
<div class="text-sm mt-3">
<%= t('need_help_ask_a_question_') %>
</div>
<div class="flex mt-3 space-x-3">
<div class="tooltip" data-tip="GitHub">
<a href="<%= Docuseal::GITHUB_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md">
<%= svg_icon('brand_github', class: 'w-8 h-8') %>
</a>
<% if can?(:read, AccountConfig) %>
<li>
<%= link_to t('notifications'), settings_notifications_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::ESIGN_CERTS_KEY, account: current_account)) %>
<li>
<%= link_to t('e_signature'), settings_esign_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% if can?(:read, AccountConfig) %>
<li>
<%= link_to t('personalization'), settings_personalization_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% if can?(:read, User) %>
<li>
<%= link_to t('users'), settings_users_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<%= render 'shared/settings_nav_extra' %>
<% if Docuseal.demo? || !Docuseal.multitenant? %>
<% if can?(:read, AccessToken) %>
<li>
<%= link_to 'API', settings_api_index_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% end %>
<% if Docuseal.demo? || !Docuseal.multitenant? || (current_user != true_user && !current_account.testing?) %>
<% if can?(:read, WebhookUrl) %>
<li>
<%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% end %>
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user != true_user || !current_account.linked_account_account) %>
<li>
<%= content_for(:pro_link) || link_to(Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}", class: 'text-base hover:bg-base-300', data: { turbo: false }) do %>
<%= t('plans') %>
<span class="badge badge-warning"><%= t('pro') %></span>
<% end %>
</li>
<% end %>
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user == true_user || current_account.testing?) %>
<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 %>
<% if Docuseal.multitenant? %> API <% else %> <%= t('console') %> <% end %>
<% end %>
</li>
<% if Docuseal.multitenant? %>
<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 %>
<%= t('embedding') %>
<% end %>
</li>
<% end %>
<% if (!Docuseal.multitenant? || can?(:manage, :saml_sso)) && can?(:read, EncryptedConfig.new(key: 'saml_configs', account: current_account)) && true_user == current_user %>
<li>
<%= link_to 'SSO', settings_sso_index_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% if !Docuseal.multitenant? && can?(:read, McpToken) && can?(:manage, :mcp) %>
<li>
<%= link_to 'MCP', settings_mcp_index_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<%= render 'shared/settings_nav_extra2' %>
<% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %>
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :post, html: { class: 'w-full' } do |f| %>
<li>
<label class="flex items-center text-base hover:bg-base-300 w-full justify-between" for="testing_toggle">
<span class="mr-2 w-full">
<%= t('test_mode') %>
</span>
<submit-form data-on="change" class="flex">
<%= f.check_box :testing_toggle, class: 'toggle toggle-sm', checked: current_account.testing? %>
</submit-form>
</label>
</li>
<% end %>
<% end %>
<% end %>
</ul>
</menu-active>
<% if Docuseal.multitenant? || cannot?(:manage, :tenants) %>
<div id="support_channels" class="mx-4 border-t border-base-300 <%= 'hidden md:block' unless expanded %>">
<div class="text-sm mt-3">
<%= t('need_help_ask_a_question_') %>
</div>
<div class="tooltip" data-tip="<%= t('discord_community') %>">
<a href="<%= Docuseal::DISCORD_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md">
<%= svg_icon('brand_discord', class: 'w-8 h-8') %>
</a>
</div>
<%= capture do %>
<div class="tooltip" data-tip="<%= t('ai_assistant') %>">
<a href="<%= Docuseal::CHATGPT_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md">
<%= svg_icon('message', class: 'w-8 h-8') %>
<div class="flex mt-3 space-x-3">
<div class="tooltip tooltip-no-touch" data-tip="GitHub">
<a href="<%= Docuseal::GITHUB_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md">
<%= svg_icon('brand_github', class: 'w-8 h-8') %>
</a>
</div>
<div class="tooltip tooltip-no-touch" data-tip="<%= t('discord_community') %>">
<a href="<%= Docuseal::DISCORD_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md">
<%= svg_icon('brand_discord', class: 'w-8 h-8') %>
</a>
</div>
<%= capture do %>
<div class="tooltip tooltip-no-touch" data-tip="<%= t('ai_assistant') %>">
<a href="<%= Docuseal::CHATGPT_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md">
<%= svg_icon('message', class: 'w-8 h-8') %>
</a>
</div>
<% end %>
</div>
<a href="mailto:<%= Docuseal::SUPPORT_EMAIL %>" target="_blank" class="w-full block mt-4 underline">
<%= Docuseal::SUPPORT_EMAIL %>
</a>
<% if Docuseal.version.present? && !Docuseal.multitenant? && can?(:manage, EncryptedConfig) %>
<a href="https://github.com/docusealco/docuseal/releases" target="_blank" class="badge badge-outline text-xs block mx-auto mt-4">
v<%= Docuseal.version %>
</a>
<% end %>
</div>
<a href="mailto:<%= Docuseal::SUPPORT_EMAIL %>" target="_blank" class="w-full block mt-4 underline text-center">
<%= Docuseal::SUPPORT_EMAIL %>
</a>
<% if Docuseal.version.present? && !Docuseal.multitenant? && can?(:manage, EncryptedConfig) %>
<a href="https://github.com/docusealco/docuseal/releases" target="_blank" class="badge badge-outline text-xs block mx-auto mt-4">
v<%= Docuseal.version %>
</a>
<% end %>
</div>
<% end %>
<% end %>
</div>
</div>
+3 -2
View File
@@ -1,7 +1,8 @@
<% offset = local_assigns[:title] ? '45px' : '0px' %>
<turbo-frame id="drawer">
<turbo-modal class="modal modal-open items-start !animate-none justify-end overflow-y-auto !justify-normal md:!justify-end" data-close-after-submit="<%= local_assigns.key?(:close_after_submit) ? local_assigns[:close_after_submit] : true %>">
<div class="absolute top-0 bottom-0 right-0 left-0" data-action="click:turbo-modal#close"></div>
<div class="bg-base-100 min-h-screen max-h-screen relative w-full relative">
<div class="bg-base-100 min-h-screen max-h-screen relative w-full min-w-0">
<% if local_assigns[:title] %>
<div class="flex justify-between bg-base-100 py-2 px-4 items-center border-b pb-2 font-medium">
<span>
@@ -14,7 +15,7 @@
&times;
</span>
<% end %>
<div class="w-screen md:w-[620px] overflow-y-auto" style="max-height: calc(100vh - <%= local_assigns[:title] ? '45px' : '0px' %>)">
<div class="md:w-[620px] overflow-y-auto" style="max-height: calc(100vh - <%= offset %>); max-height: calc(100dvh - <%= offset %>)">
<%= yield %>
</div>
</div>
+31 -15
View File
@@ -1,18 +1,34 @@
<turbo-frame id="modal">
<turbo-modal class="modal modal-open items-start !animate-none overflow-y-auto !justify-normal md:!justify-center" data-close-after-submit="<%= local_assigns.key?(:close_after_submit) ? local_assigns[:close_after_submit] : true %>">
<div class="absolute top-0 bottom-0 right-0 left-0" data-action="click:turbo-modal#close"></div>
<div class="bg-base-100 min-h-screen max-h-screen md:min-h-fit md:mt-3 md:rounded-2xl relative w-full relative overflow-y-auto">
<% if local_assigns[:title] %>
<div class="flex justify-between bg-base-100 py-2 px-5 items-center border-b pb-2 font-medium mt-0.5">
<span>
<%= local_assigns[:title] %>
</span>
<a href="#" class="text-xl" data-action="click:turbo-modal#close">&times;</a>
<% if turbo_frame_request? %>
<turbo-frame id="modal">
<turbo-modal class="modal modal-open items-start !animate-none overflow-y-auto !justify-normal md:!justify-center" data-close-after-submit="<%= local_assigns.key?(:close_after_submit) ? local_assigns[:close_after_submit] : true %>">
<div class="absolute top-0 bottom-0 right-0 left-0" data-action="click:turbo-modal#close"></div>
<div class="bg-base-100 min-h-screen max-h-screen md:min-h-fit md:mt-3 md:rounded-2xl relative w-full relative overflow-y-auto">
<% if local_assigns[:title] %>
<div class="flex justify-between bg-base-100 py-2 px-5 items-center border-b pb-2 font-medium mt-0.5">
<span>
<%= local_assigns[:title] %>
</span>
<a href="#" class="text-xl" data-action="click:turbo-modal#close">&times;</a>
</div>
<% end %>
<div class="w-full md:w-[590px] overflow-y-auto" style="max-height: calc(100vh - 14px - 45px); max-height: calc(100dvh - 14px - 45px)">
<%= yield %>
</div>
<% end %>
<div class="w-full md:w-[590px] overflow-y-auto" style="max-height: calc(100vh - 14px - 45px)">
<%= yield %>
</div>
</turbo-modal>
</turbo-frame>
<% else %>
<% if local_assigns[:title] %>
<div class="sticky top-0 z-10 flex justify-between bg-base-100 py-2 px-5 items-center border-b pb-2 font-medium">
<span>
<%= local_assigns[:title] %>
</span>
<history-back>
<a href="#" class="text-xl block py-2 px-5 -my-2 -mr-5">&times;</a>
</history-back>
</div>
</turbo-modal>
</turbo-frame>
<% end %>
<div class="w-full">
<%= yield %>
</div>
<% end %>
+1 -1
View File
@@ -1,7 +1,7 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
<%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto">
<h1 class="text-4xl font-bold mb-4">SMS</h1>
<h1 class="text-3xl md:text-4xl font-bold mb-4">SMS</h1>
<%= render 'placeholder' %>
</div>
<div class="w-0 md:w-52"></div>
Loaded 100 of 180 files, more files were not shown because too many files have changed in this diff. Show more