mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 14:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27a0d10525 | ||
|
|
d25f0812ca | ||
|
|
9e2aa6f8c8 | ||
|
|
11c76bf554 | ||
|
|
e5733d88c0 | ||
|
|
6d38c9f375 | ||
|
|
91ec29ad2c | ||
|
|
d374c7fa10 | ||
|
|
ca0daf6d0b | ||
|
|
076c3d805a | ||
|
|
1f149ac67a | ||
|
|
616f7b4fd8 | ||
|
|
6f4527038b | ||
|
|
8ed2883504 | ||
|
|
651b84a460 | ||
|
|
73c4f158ff | ||
|
|
6f1cc34433 | ||
|
|
a77029ffcb | ||
|
|
0480572ef5 | ||
|
|
807463c465 | ||
|
|
0f61053976 | ||
|
|
b8cf525034 | ||
|
|
fef203daed | ||
|
|
41b38cc594 | ||
|
|
4e4354af99 | ||
|
|
113b61588b | ||
|
|
c1aac6b763 | ||
|
|
f85fd3cff7 | ||
|
|
004a22c1c8 | ||
|
|
69343b1bdd | ||
|
|
81a306928c | ||
|
|
2bf3d42ed6 | ||
|
|
eec08266c9 | ||
|
|
cbbc75b485 | ||
|
|
3d93bbe5aa | ||
|
|
5308e3e15b | ||
|
|
22c146d564 | ||
|
|
c8d3af9841 | ||
|
|
ee1d45d461 | ||
|
|
27d075ed63 | ||
|
|
df63acf0c8 | ||
|
|
185827c951 | ||
|
|
e361cbabf4 | ||
|
|
e867bf5c48 | ||
|
|
726d0ea4f3 | ||
|
|
90be34b243 | ||
|
|
e87c06d811 | ||
|
|
54454d1dfc | ||
|
|
726174deff | ||
|
|
52eb8ac863 | ||
|
|
97d91d8f27 | ||
|
|
dd358a8401 | ||
|
|
8e254b7d3c | ||
|
|
b9018fa442 | ||
|
|
09310f3690 | ||
|
|
2dd38260ba | ||
|
|
ffb3247cdc | ||
|
|
c59c470321 | ||
|
|
f35510ffd6 | ||
|
|
eeec852b3b | ||
|
|
485b7f2c8f | ||
|
|
b6498ec049 | ||
|
|
dc1de72e18 | ||
|
|
7dddef4c94 | ||
|
|
a2d8b85549 | ||
|
|
4d68783b82 | ||
|
|
b05c3b2030 | ||
|
|
784ae9aa4b | ||
|
|
03b5956cbc | ||
|
|
2876c9a18e | ||
|
|
c4b7be3925 | ||
|
|
e2ffbdac1d | ||
|
|
0d197bb2eb | ||
|
|
55089bee7f | ||
|
|
b96fe74db1 | ||
|
|
2fc1ba3037 | ||
|
|
c67d37207e | ||
|
|
dba50cd1bf | ||
|
|
edf2c0d9ed | ||
|
|
35f7c36f28 | ||
|
|
3a24ecac63 | ||
|
|
50ec7682b5 | ||
|
|
3667993caf | ||
|
|
68e8cd1188 | ||
|
|
3eebf6d2cf | ||
|
|
34d0e00910 | ||
|
|
b8ab7190d2 | ||
|
|
59fe8a7917 | ||
|
|
d56070827b | ||
|
|
74dbd4bed3 | ||
|
|
807e82b21e | ||
|
|
66862e93c8 | ||
|
|
270461ad3b | ||
|
|
3edd1ab32d | ||
|
|
e77998e53c | ||
|
|
1421bc2bce | ||
|
|
cbbbfdda80 | ||
|
|
2d510081be | ||
|
|
704893ee1d | ||
|
|
88612913a6 | ||
|
|
ce6823ed38 | ||
|
|
9074d23b3c | ||
|
|
76d0e76fd5 | ||
|
|
5ec8e4ccc7 | ||
|
|
b8c29324b8 | ||
|
|
972f38242e | ||
|
|
2c6ddd7f90 |
No files matched your search
+19
-25
@@ -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
@@ -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
|
||||
|
||||
+79
-78
@@ -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
|
||||
@@ -525,12 +527,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 +571,14 @@ 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)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
yaml (0.4.0)
|
||||
zeitwerk (2.7.5)
|
||||
zeitwerk (2.8.2)
|
||||
|
||||
PLATFORMS
|
||||
aarch64-linux
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -20,6 +20,8 @@ class MfaSetupController < ApplicationController
|
||||
|
||||
redirect_to settings_profile_index_path, notice: I18n.t('2fa_has_been_configured')
|
||||
else
|
||||
RateLimit.call("mfa-setup-otp-#{current_user.id}", limit: 5, ttl: 5.minutes, enabled: true)
|
||||
|
||||
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name)
|
||||
|
||||
@error_message = I18n.t('code_is_invalid')
|
||||
@@ -34,6 +36,8 @@ class MfaSetupController < ApplicationController
|
||||
|
||||
redirect_to settings_profile_index_path, notice: I18n.t('2fa_has_been_removed')
|
||||
else
|
||||
RateLimit.call("mfa-setup-otp-#{current_user.id}", limit: 5, ttl: 5.minutes, enabled: true)
|
||||
|
||||
@error_message = I18n.t('code_is_invalid')
|
||||
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_content
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SettingsController < ApplicationController
|
||||
skip_authorization_check
|
||||
|
||||
def index; end
|
||||
end
|
||||
@@ -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,27 @@ 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::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
|
||||
@@ -6,12 +6,17 @@ class StartFormEmail2faSendController < ApplicationController
|
||||
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]
|
||||
@@ -25,7 +30,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,99 @@
|
||||
# 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::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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,7 +19,7 @@ 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ?? '')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: 'ファイル',
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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) &&
|
||||
(!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)
|
||||
|
||||
|
||||
@@ -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,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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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 @@
|
||||
<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,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 |
@@ -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">
|
||||
|
||||
@@ -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,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>
|
||||
@@ -10,7 +10,12 @@
|
||||
<%= 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">
|
||||
|
||||
@@ -10,7 +10,12 @@
|
||||
<%= 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 %>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<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,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| %>
|
||||
|
||||
@@ -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 @@
|
||||
<% '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,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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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') %>
|
||||
|
||||
@@ -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>
|
||||
@@ -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 @@
|
||||
×
|
||||
</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>
|
||||
|
||||
@@ -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">×</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">×</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">×</a>
|
||||
</history-back>
|
||||
</div>
|
||||
</turbo-modal>
|
||||
</turbo-frame>
|
||||
<% end %>
|
||||
<div class="w-full">
|
||||
<%= yield %>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -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>
|
||||
|
||||
@@ -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">SAML SSO</h1>
|
||||
<h1 class="text-3xl md:text-4xl font-bold mb-4">SAML SSO</h1>
|
||||
<%= render 'placeholder' %>
|
||||
</div>
|
||||
<div class="w-0 md:w-52"></div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="space-y-6 mx-auto">
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-center">
|
||||
<%= render 'banner' %>
|
||||
<%= render 'start_form/banner' %>
|
||||
</div>
|
||||
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
|
||||
<div class="flex items-center">
|
||||
@@ -26,7 +26,7 @@
|
||||
<%= button_to button_title(title: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { template_slug: @template.slug, email: params[:email] }, class: 'base-button w-full' %>
|
||||
</toggle-submit>
|
||||
<% end %>
|
||||
<% if Templates.filter_undefined_submitters(@template.submitters).size == 1 && %w[api embed mcp].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false && @template.shared_link? %>
|
||||
<% if @template.shared_link? && Templates.filter_undefined_submitters(@template.submitters).size == 1 && %w[api embed mcp].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false %>
|
||||
<toggle-submit class="block">
|
||||
<%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@template.slug), params: { submitter: params.permit(:name, :email, :phone).compact_blank, resubmit: true }, method: :put, class: 'white-button w-full' %>
|
||||
</toggle-submit>
|
||||
|
||||
@@ -23,13 +23,13 @@
|
||||
<div>
|
||||
<%= t('we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue') %>
|
||||
</div>
|
||||
<%= form_for '', url: start_form_path(@template.slug), method: :put, html: { class: 'space-y-4', id: 'code_form' } do |f| %>
|
||||
<%= form_for '', url: @resubmit_submitter ? resubmit_form_path : start_form_path(@template.slug), method: :put, html: { class: 'space-y-4', id: 'code_form' } do |f| %>
|
||||
<div dir="auto" class="form-control !mt-0">
|
||||
<%= f.hidden_field 'resubmit', value: params[:resubmit] %>
|
||||
<%= f.hidden_field 'submitter[name]', value: params[:name] || @submitter&.name %>
|
||||
<%= f.hidden_field 'submitter[email]', value: params[:email] || @submitter&.email %>
|
||||
<%= f.hidden_field 'submitter[phone]', value: params[:phone] || @submitter&.phone %>
|
||||
<%= f.text_field :one_time_code, required: true, class: 'base-input text-center', placeholder: 'XXX-XXX' %>
|
||||
<%= f.hidden_field 'submitter[name]', value: params[:name] || params.dig(:submitter, :name) %>
|
||||
<%= f.hidden_field 'submitter[email]', value: params[:email] || params.dig(:submitter, :email) %>
|
||||
<%= f.hidden_field 'submitter[phone]', value: params[:phone] || params.dig(:submitter, :phone) %>
|
||||
<%= f.text_field :one_time_code, required: true, inputmode: 'numeric', class: 'base-input text-center', placeholder: 'XXXXXX' %>
|
||||
<div class="flex justify-between items-center mt-1">
|
||||
<span>
|
||||
<% if flash[:alert] %>
|
||||
@@ -49,6 +49,10 @@
|
||||
<%= f.button button_title(title: t('submit')), class: 'base-button' %>
|
||||
</toggle-submit>
|
||||
<% end %>
|
||||
<%= button_to t(:re_send_email), start_form_email_2fa_send_index_path, params: { slug: @template.slug, resend: true, submitter: { name: params[:name] || @submitter&.name, email: params[:email] || @submitter&.email, phone: params[:phone] || @submitter&.phone } }.compact, method: :post, id: 'resend_code', class: 'hidden' %>
|
||||
<% if @resubmit_submitter %>
|
||||
<%= button_to t(:re_send_email), resubmit_form_path, params: { resubmit: @resubmit_submitter.slug }, method: :put, id: 'resend_code', class: 'hidden' %>
|
||||
<% else %>
|
||||
<%= button_to t(:re_send_email), start_form_email_2fa_send_index_path, params: { slug: @template.slug, resend: true, submitter: { name: params[:name] || params.dig(:submitter, :name), email: params[:email] || params.dig(:submitter, :email), phone: params[:phone] || params.dig(:submitter, :phone) } }.compact, method: :post, id: 'resend_code', class: 'hidden' %>
|
||||
<% end %>
|
||||
</div>
|
||||
</main>
|
||||
@@ -4,7 +4,7 @@
|
||||
<% end %>
|
||||
<main class="max-w-md space-y-6 mx-auto px-2 mt-12 mb-4">
|
||||
<div class="text-center w-full space-y-6">
|
||||
<%= render 'banner' %>
|
||||
<%= render 'start_form/banner' %>
|
||||
<p class="text-xl font-semibold text-center">
|
||||
<%= t('share_link_is_currently_disabled') %>
|
||||
</p>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="space-y-6 mx-auto">
|
||||
<div class="space-y-6">
|
||||
<div class="text-center w-full space-y-6">
|
||||
<%= render 'banner' %>
|
||||
<%= render 'start_form/banner' %>
|
||||
<% unless @template.archived_at? %>
|
||||
<p class="text-xl font-semibold text-center"><%= t('you_have_been_invited_to_submit_a_form') %></p>
|
||||
<% end %>
|
||||
@@ -28,7 +28,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<% if !@template.archived_at? && !@template.account.archived_at? %>
|
||||
<%= form_for @submitter, url: start_form_path(@template.slug), data: { turbo_frame: :_top }, method: :put, html: { class: 'space-y-4' } do |f| %>
|
||||
<%= form_for @submitter, url: @resubmit_submitter ? resubmit_form_path : start_form_path(@template.slug), data: { turbo_frame: :_top }, method: :put, html: { class: 'space-y-4' } do |f| %>
|
||||
<%= hidden_field_tag :resubmit, @resubmit_submitter.slug if @resubmit_submitter %>
|
||||
<% if @error_message %>
|
||||
<div class="alert items-start bg-base-100 border border-red-400 text-red-400">
|
||||
<%= svg_icon('info_circle', class: 'stroke-current shrink-0 h-6 w-6 mt-1') %>
|
||||
|
||||
@@ -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('storage') %>
|
||||
</h1>
|
||||
<% value = @encrypted_config.value || { 'service' => 'disk' } %>
|
||||
|
||||
@@ -45,8 +45,8 @@
|
||||
<% default_subject = template&.preferences&.dig('request_email_subject').presence || config.value['subject'] %>
|
||||
<% default_body = template&.preferences&.dig('request_email_body').presence || config_body %>
|
||||
<% is_edit_viewer = local_assigns[:submitter] && local_assigns[:viewer_submitter_uuids].include?(local_assigns[:submitter].uuid) %>
|
||||
<div id="<%= message_field_id %>" class="card card-compact bg-base-300/40 hidden">
|
||||
<div class="card-body">
|
||||
<div id="<%= message_field_id %>" class="relative flex flex-col rounded-2xl md:bg-base-300/40 hidden">
|
||||
<div class="flex flex-auto flex-col gap-2 md:text-sm md:p-4">
|
||||
<%= tag.input id: toggle_uuid = SecureRandom.uuid, value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: local_assigns[:message_per_submitter] != false && template_submitters.size < 11 && template&.preferences&.dig('submitters').to_a.size > 1 %>
|
||||
<% if local_assigns[:viewer_submitter_uuids].present? && local_assigns[:submitter].blank? %>
|
||||
<% (template_submitters.pluck('uuid') - local_assigns[:viewer_submitter_uuids].to_a).each do |signer_uuid| %>
|
||||
@@ -69,7 +69,7 @@
|
||||
<%= f.label :message, t('body'), class: 'label' %>
|
||||
<% body_variables = AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY] %>
|
||||
<%= render 'personalization_settings/email_body_editor', name: f.field_name(:body), value: local_assigns[:submitter_email_message]&.normalized_body.presence || (is_edit_viewer ? view_template_body : nil) || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_body').presence || (is_edit_viewer ? view_config.value['body'] : default_body), variables: body_variables %>
|
||||
<% if !local_assigns.fetch(:disable_save_as_default_template_option, false) && config.value['body_type'] != 'html' %>
|
||||
<% if !local_assigns.fetch(:disable_save_as_default_template_option, false) && config.value['body_type'] != 'html' && template && can?(:update, template) %>
|
||||
<label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer">
|
||||
<%= check_box_tag :save_message, id: uuid, class: 'base-checkbox', checked: false %>
|
||||
<span class="label"><%= t('save_as_default_template_message') %></span>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="mt-2 mb-1">
|
||||
<div class="tooltip w-full" data-tip="<%= t('unlock_with_docuseal_pro') %>">
|
||||
<div class="tooltip tooltip-no-touch w-full" data-tip="<%= t('unlock_with_docuseal_pro') %>">
|
||||
<%= link_to submitter.sent_at? ? t('re_send_sms') : t('send_sms'), Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}", class: 'btn btn-sm btn-primary text-gray-400 w-full' %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -25,10 +25,12 @@
|
||||
<span class="hidden md:inline"><%= t('audit_log') %></span>
|
||||
</a>
|
||||
<% elsif signed_in? %>
|
||||
<%= link_to submission_events_path(@submission), class: 'white-button', data: { turbo_frame: :modal } do %>
|
||||
<%= svg_icon('logs', class: 'w-6 h-6') %>
|
||||
<span class="hidden md:block"><%= t('event_log') %></span>
|
||||
<% end %>
|
||||
<open-modal-mobile class="flex">
|
||||
<%= link_to submission_events_path(@submission), class: 'white-button', data: { turbo_frame: :modal } do %>
|
||||
<%= svg_icon('logs', class: 'w-6 h-6') %>
|
||||
<span class="hidden md:block"><%= t('event_log') %></span>
|
||||
<% end %>
|
||||
</open-modal-mobile>
|
||||
<% end %>
|
||||
<% if signed_in? && !@submission.completed_at? && can?(:manage, :resend_all) && @submission.submitters.to_a.size > 3 && !@submission.archived_at? && !@submission.template&.archived_at? && !@submission.expired? && can?(:update, @submission) %>
|
||||
<% pending_submitters_count = @submission.submitters.to_a.count { |s| !s.completed_at? && s.email.present? && !s.declined_at? } %>
|
||||
@@ -180,10 +182,12 @@
|
||||
</span>
|
||||
</div>
|
||||
<% if signed_in? && can?(:update, @submission) && submitter && !submitter.completed_at? && !@submission.completed_at? && !submitter.declined_at? && !@submission.archived_at? && !@submission.template&.archived_at? && !@submission.expired? && !submitter.start_form_submission_events.any? %>
|
||||
<span class="tooltip tooltip-left" data-tip="<%= t('edit') %>">
|
||||
<%= link_to edit_submitter_path(submitter), class: 'shrink-0 inline md:hidden md:group-hover:inline', data: { turbo_frame: 'modal' } do %>
|
||||
<%= svg_icon('pencil', class: 'w-5 h-5') %>
|
||||
<% end %>
|
||||
<span class="tooltip tooltip-left tooltip-no-touch" data-tip="<%= t('edit') %>">
|
||||
<open-modal-mobile>
|
||||
<%= link_to edit_submitter_path(submitter), class: 'shrink-0 inline md:hidden md:group-hover:inline', data: { turbo_frame: 'modal' } do %>
|
||||
<%= svg_icon('pencil', class: 'w-5 h-5') %>
|
||||
<% end %>
|
||||
</open-modal-mobile>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-end md:flex-row gap-2">
|
||||
<%= render 'submissions_filters/applied_filters', filter_params: %>
|
||||
<div class="flex items-center md:items-end gap-2">
|
||||
<%= render 'submissions_filters/applied_filters', filter_params:, with_status: true %>
|
||||
<%= render 'submissions_filters/filter_button', filter_params: %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,10 +18,12 @@
|
||||
<span class="hidden sm:block">
|
||||
<%= render 'templates/upload_button' %>
|
||||
</span>
|
||||
<%= link_to new_template_path, class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %>
|
||||
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
|
||||
<span class="hidden md:block"><%= t('create') %></span>
|
||||
<% end %>
|
||||
<open-modal-mobile data-disabled="<%= !Docuseal.multitenant? %>" class="flex">
|
||||
<%= link_to new_template_path, class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %>
|
||||
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
|
||||
<span class="hidden md:block"><%= t('create') %></span>
|
||||
<% end %>
|
||||
</open-modal-mobile>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -34,7 +36,7 @@
|
||||
<% end %>
|
||||
<% if is_show_tabs %>
|
||||
<div class="flex items-center flex-col md:flex-row md:flex-wrap gap-2 mb-4">
|
||||
<div class="flex items-center md:items-end flex-col md:flex-row gap-2 w-full md:w-fit">
|
||||
<div class="hidden md:flex md:items-end gap-2 md:w-fit">
|
||||
<a href="<%= url_for(params: request.query_parameters.except('status')) %>" class="<%= params[:status].blank? ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
|
||||
<div class="flex items-center space-x-1">
|
||||
<%= svg_icon('list', class: 'w-5 h-5') %>
|
||||
@@ -54,8 +56,8 @@
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex items-end flex-col md:flex-row gap-2 w-full md:w-fit">
|
||||
<%= render 'submissions_filters/applied_filters', filter_params: %>
|
||||
<div class="flex items-center md:items-end gap-2 w-full md:w-fit">
|
||||
<%= render 'submissions_filters/applied_filters', filter_params:, with_status: true %>
|
||||
<%= render 'submissions_filters/filter_button', filter_params: %>
|
||||
<%= render 'submissions_filters/actions', filter_params: %>
|
||||
</div>
|
||||
|
||||
@@ -1,72 +1,136 @@
|
||||
<% query_params = params.permit(:q).merge(filter_params) %>
|
||||
<% if icon = { 'declined' => 'x_circle', 'expired' => 'clock_cancel', 'partially_completed' => 'clock_edit', 'sent' => 'send', 'opened' => 'mail_opened' }[params[:status]] %>
|
||||
<div class="flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-36 border-neutral-700">
|
||||
<%= link_to submissions_filter_path('status', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %>
|
||||
<%= svg_icon(icon, class: 'w-5 h-5 shrink-0') %>
|
||||
<span class="font-normal truncate"><%= t(params[:status]) %></span>
|
||||
<% end %>
|
||||
<%= link_to url_for(params: request.query_parameters.except('status')), class: 'rounded-lg ml-1 shrink-0 hover:bg-base-content hover:text-white' do %>
|
||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% filter_names = %w[status folder author completed_at created_at] %>
|
||||
<% ordered_filters = request.query_parameters.keys.filter_map { |key| filter_names.find { |name| key.start_with?(name) } }.uniq %>
|
||||
<% chip_order = ordered_filters.reverse.each_with_index.to_h { |name, index| [name, index + 1] } %>
|
||||
<% chip_order.default = ordered_filters.size + 1 %>
|
||||
<% status_icon = { 'declined' => 'x_circle', 'expired' => 'clock_cancel', 'partially_completed' => 'clock_edit', 'sent' => 'send', 'opened' => 'mail_opened' }[params[:status]] %>
|
||||
<% status_icons = { 'all' => 'list', 'pending' => 'clock', 'completed' => 'circle_check' } %>
|
||||
<% current_status = status_icons.key?(params[:status].to_s) ? params[:status].to_s : 'all' %>
|
||||
<% with_status_button = local_assigns[:with_status] && status_icon.blank? %>
|
||||
<% chips_html = capture do %>
|
||||
<% if status_icon %>
|
||||
<div class="order-none flex h-10 px-2 py-1 text-base md:text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl shrink-0 max-w-[60vw] md:w-36 border-neutral-700">
|
||||
<%= link_to submissions_filter_path('status', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %>
|
||||
<%= svg_icon(status_icon, class: 'w-5 h-5 shrink-0') %>
|
||||
<span class="font-normal truncate"><%= t(params[:status]) %></span>
|
||||
<% end %>
|
||||
<%= link_to url_for(params: request.query_parameters.except('status')), class: 'rounded-lg ml-1 shrink-0 hover:bg-base-content hover:text-white' do %>
|
||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if params[:folder].present? %>
|
||||
<div class="order-<%= chip_order['folder'] %> md:order-none md:tooltip tooltip-no-touch tooltip-bottom flex md:flex h-10 px-2 py-1 text-base md:text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl shrink-0 max-w-[60vw] md:w-36 border-neutral-700" data-tip="<%= t('folder') %>">
|
||||
<%= link_to submissions_filter_path('folder', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %>
|
||||
<%= svg_icon('folder', class: 'w-5 h-5 shrink-0') %>
|
||||
<span class="font-normal truncate"><%= params[:folder] %></span>
|
||||
<% end %>
|
||||
<%= link_to url_for(params: request.query_parameters.except('folder')), class: 'rounded-lg ml-1 shrink-0 hover:bg-base-content hover:text-white' do %>
|
||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if params[:author].present? %>
|
||||
<div class="order-<%= chip_order['author'] %> md:order-none md:tooltip tooltip-no-touch tooltip-bottom flex md:flex h-10 px-2 py-1 text-base md:text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl shrink-0 max-w-[60vw] md:w-36 border-neutral-700" data-tip="<%= t('author') %>">
|
||||
<%= link_to submissions_filter_path('author', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %>
|
||||
<%= svg_icon('user', class: 'w-5 h-5 shrink-0') %>
|
||||
<span class="font-normal truncate"><%= current_account.users.accessible_by(current_ability).where(account: current_account).find_by(email: params[:author])&.full_name || 'NA' %></span>
|
||||
<% end %>
|
||||
<%= link_to url_for(params: request.query_parameters.except('author')), class: 'rounded-lg ml-1 shrink-0 hover:bg-base-content hover:text-white' do %>
|
||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if query_params[:completed_at_from].present? || query_params[:completed_at_to].present? %>
|
||||
<div class="order-<%= chip_order['completed_at'] %> md:order-none md:tooltip tooltip-no-touch tooltip-bottom flex md:flex h-10 px-2 py-1 text-base md:text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl shrink-0 max-w-[60vw] md:w-36 border-neutral-700" data-tip="<%= t('completed_at') %>">
|
||||
<%= link_to submissions_filter_path('completed_at', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %>
|
||||
<%= svg_icon('calendar_check', class: 'w-5 h-5 shrink-0') %>
|
||||
<span class="flex flex-col font-normal text-left md:text-center text-xs">
|
||||
<% if query_params[:completed_at_from] == query_params[:completed_at_to] %>
|
||||
<span><%= l(Date.parse(query_params[:completed_at_from]), locale: current_account.locale) %></span>
|
||||
<% else %>
|
||||
<span><%= query_params[:completed_at_from].present? ? l(Date.parse(query_params[:completed_at_from]), locale: current_account.locale) : '∞' %></span>
|
||||
<span><%= query_params[:completed_at_to].present? ? l(Date.parse(query_params[:completed_at_to]), locale: current_account.locale) : t('today') %></span>
|
||||
<% end %>
|
||||
</span>
|
||||
<% end %>
|
||||
<%= link_to url_for(params: request.query_parameters.except('completed_at_from', 'completed_at_to')), class: 'rounded-lg ml-1 shrink-0 hover:bg-base-content hover:text-white' do %>
|
||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if query_params[:created_at_from].present? || query_params[:created_at_to].present? %>
|
||||
<div class="order-<%= chip_order['created_at'] %> md:order-none md:tooltip tooltip-no-touch tooltip-bottom flex md:flex h-10 px-2 py-1 text-base md:text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl shrink-0 max-w-[60vw] md:w-36 border-neutral-700" data-tip="<%= t('created_at') %>">
|
||||
<%= link_to submissions_filter_path('created_at', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %>
|
||||
<%= svg_icon('calendar', class: 'w-5 h-5 shrink-0') %>
|
||||
<span class="flex flex-col font-normal text-left md:text-center text-xs">
|
||||
<% if query_params[:created_at_from] == query_params[:created_at_to] %>
|
||||
<span><%= l(Date.parse(query_params[:created_at_from]), locale: current_account.locale) %></span>
|
||||
<% else %>
|
||||
<span><%= query_params[:created_at_from].present? ? l(Date.parse(query_params[:created_at_from]), locale: current_account.locale) : '∞' %></span>
|
||||
<span><%= query_params[:created_at_to].present? ? l(Date.parse(query_params[:created_at_to]), locale: current_account.locale) : t('today') %></span>
|
||||
<% end %>
|
||||
</span>
|
||||
<% end %>
|
||||
<%= link_to url_for(params: request.query_parameters.except('created_at_to', 'created_at_from')), class: 'rounded-lg ml-1 shrink-0 hover:bg-base-content hover:text-white' do %>
|
||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if params[:folder].present? %>
|
||||
<div class="tooltip tooltip-bottom flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-36 border-neutral-700" data-tip="<%= t('folder') %>">
|
||||
<%= link_to submissions_filter_path('folder', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %>
|
||||
<%= svg_icon('folder', class: 'w-5 h-5 shrink-0') %>
|
||||
<span class="font-normal truncate"><%= params[:folder] %></span>
|
||||
<% end %>
|
||||
<%= link_to url_for(params: request.query_parameters.except('folder')), class: 'rounded-lg ml-1 shrink-0 hover:bg-base-content hover:text-white' do %>
|
||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if params[:author].present? %>
|
||||
<div class="tooltip tooltip-bottom flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-36 border-neutral-700" data-tip="<%= t('author') %>">
|
||||
<%= link_to submissions_filter_path('author', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %>
|
||||
<%= svg_icon('user', class: 'w-5 h-5 shrink-0') %>
|
||||
<span class="font-normal truncate"><%= current_account.users.accessible_by(current_ability).where(account: current_account).find_by(email: params[:author])&.full_name || 'NA' %></span>
|
||||
<% end %>
|
||||
<%= link_to url_for(params: request.query_parameters.except('author')), class: 'rounded-lg ml-1 shrink-0 hover:bg-base-content hover:text-white' do %>
|
||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if query_params[:completed_at_from].present? || query_params[:completed_at_to].present? %>
|
||||
<div class="tooltip tooltip-bottom flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-36 border-neutral-700" data-tip="<%= t('completed_at') %>">
|
||||
<%= link_to submissions_filter_path('completed_at', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %>
|
||||
<%= svg_icon('calendar_check', class: 'w-5 h-5 shrink-0') %>
|
||||
<span class="flex flex-row md:flex-col font-normal text-left md:text-center md:text-xs">
|
||||
<% if query_params[:completed_at_from] == query_params[:completed_at_to] %>
|
||||
<span><%= l(Date.parse(query_params[:completed_at_from]), locale: current_account.locale) %></span>
|
||||
<% else %>
|
||||
<span><%= query_params[:completed_at_from].present? ? l(Date.parse(query_params[:completed_at_from]), locale: current_account.locale) : '∞' %></span>
|
||||
<span class="px-1 md:px-0 md:hidden">-</span>
|
||||
<span><%= query_params[:completed_at_to].present? ? l(Date.parse(query_params[:completed_at_to]), locale: current_account.locale) : t('today') %></span>
|
||||
<% if with_status_button && chips_html.blank? %>
|
||||
<div class="dropdown md:hidden shrink-0">
|
||||
<label tabindex="0" class="cursor-pointer flex h-10 px-2 py-1 space-x-1 text-base items-center border text-neutral rounded-xl <%= current_status == 'all' ? 'border-neutral-300' : 'border-neutral-700' %>">
|
||||
<%= svg_icon(status_icons[current_status], class: 'w-5 h-5 shrink-0') %>
|
||||
<span><%= t(current_status) %></span>
|
||||
<%= svg_icon('chevron_down', class: 'w-4 h-4 shrink-0') %>
|
||||
</label>
|
||||
<ul tabindex="0" class="z-10 dropdown-content p-2 mt-2 shadow menu text-base bg-base-100 rounded-box" style="min-width: 180px;">
|
||||
<li>
|
||||
<%= link_to url_for(params: request.query_parameters.except('status')), class: ('bg-base-200' if current_status == 'all') do %>
|
||||
<%= svg_icon('list', class: 'w-5 h-5 shrink-0') %>
|
||||
<span><%= t('all') %></span>
|
||||
<% if local_assigns[:all_count] %>
|
||||
<span class="badge badge-sm badge-ghost font-medium px-2 ml-auto">
|
||||
<%= local_assigns[:all_count] %>
|
||||
</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</span>
|
||||
<% end %>
|
||||
<%= link_to url_for(params: request.query_parameters.except('completed_at_from', 'completed_at_to')), class: 'rounded-lg ml-1 shrink-0 hover:bg-base-content hover:text-white' do %>
|
||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if query_params[:created_at_from].present? || query_params[:created_at_to].present? %>
|
||||
<div class="tooltip tooltip-bottom flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-36 border-neutral-700" data-tip="<%= t('created_at') %>">
|
||||
<%= link_to submissions_filter_path('created_at', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %>
|
||||
<%= svg_icon('calendar', class: 'w-5 h-5 shrink-0') %>
|
||||
<span class="flex flex-row md:flex-col font-normal text-left md:text-center md:text-xs">
|
||||
<% if query_params[:created_at_from] == query_params[:created_at_to] %>
|
||||
<span><%= l(Date.parse(query_params[:created_at_from]), locale: current_account.locale) %></span>
|
||||
<% else %>
|
||||
<span><%= query_params[:created_at_from].present? ? l(Date.parse(query_params[:created_at_from]), locale: current_account.locale) : '∞' %></span>
|
||||
<span class="px-1 md:px-0 md:hidden">-</span>
|
||||
<span><%= query_params[:created_at_to].present? ? l(Date.parse(query_params[:created_at_to]), locale: current_account.locale) : t('today') %></span>
|
||||
</li>
|
||||
<li>
|
||||
<%= link_to url_for(params: request.query_parameters.merge('status' => 'pending')), class: ('bg-base-200' if current_status == 'pending') do %>
|
||||
<%= svg_icon('clock', class: 'w-5 h-5 shrink-0') %>
|
||||
<span><%= t('pending') %></span>
|
||||
<% if local_assigns[:pending_count] %>
|
||||
<span class="badge badge-sm badge-ghost font-medium px-2 ml-auto">
|
||||
<%= local_assigns[:pending_count] %>
|
||||
</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</span>
|
||||
<% end %>
|
||||
<%= link_to url_for(params: request.query_parameters.except('created_at_to', 'created_at_from')), class: 'rounded-lg ml-1 shrink-0 hover:bg-base-content hover:text-white' do %>
|
||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||
<% end %>
|
||||
</li>
|
||||
<li>
|
||||
<%= link_to url_for(params: request.query_parameters.merge('status' => 'completed')), class: ('bg-base-200' if current_status == 'completed') do %>
|
||||
<%= svg_icon('circle_check', class: 'w-5 h-5 shrink-0') %>
|
||||
<span><%= t('completed') %></span>
|
||||
<% if local_assigns[:completed_count] %>
|
||||
<span class="badge badge-sm badge-ghost font-medium px-2 ml-auto">
|
||||
<%= local_assigns[:completed_count] %>
|
||||
</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if chips_html.present? %>
|
||||
<scroll-fade class="flex items-center gap-2 overflow-x-auto flex-1 min-w-0 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden [&::-webkit-scrollbar]:h-0 [&::-webkit-scrollbar]:w-0 md:contents">
|
||||
<% if with_status_button %>
|
||||
<a href="<%= submissions_filter_path('status', query_params.merge(path: url_for)) %>" data-turbo-frame="modal" class="order-none md:hidden flex h-10 px-2 py-1 space-x-1 text-base items-center border text-neutral rounded-xl shrink-0 <%= current_status == 'all' ? 'border-neutral-300' : 'border-neutral-700' %>">
|
||||
<%= svg_icon(status_icons[current_status], class: 'w-5 h-5 shrink-0') %>
|
||||
<span><%= t(current_status) %></span>
|
||||
<%= svg_icon('chevron_down', class: 'w-4 h-4 shrink-0') %>
|
||||
</a>
|
||||
<% end %>
|
||||
<%= chips_html %>
|
||||
</scroll-fade>
|
||||
<% end %>
|
||||
@@ -1,10 +1,13 @@
|
||||
<% query_params = params.permit(:q).merge(filter_params) %>
|
||||
<div class="dropdown dropdown-end">
|
||||
<label tabindex="0" class="cursor-pointer flex h-10 px-3 py-1 space-x-1 text-lg items-center justify-between border text-center text-neutral rounded-xl border-neutral-300 hover:border-neutral-700">
|
||||
<% advanced_filters = filter_params.then { |f| f[:status].in?(%w[pending completed]) ? f.except(:status) : f } %>
|
||||
<div class="dropdown dropdown-end shrink-0">
|
||||
<label tabindex="0" class="cursor-pointer flex h-10 py-1 text-lg items-center border text-center text-neutral rounded-xl hover:border-neutral-700 <%= advanced_filters.present? ? 'w-10 md:w-auto px-0 md:px-3 space-x-0 md:space-x-1 justify-center md:justify-between border-neutral-700 md:border-neutral-300' : 'px-3 space-x-1 justify-between border-neutral-300' %>">
|
||||
<%= svg_icon('filter', class: 'w-5 h-5 flex-shrink-0 stroke-2') %>
|
||||
<span class="<%= filter_params.then { |f| f[:status].in?(%w[pending completed]) ? f.except(:status) : f }.present? ? 'md:hidden' : '' %>">Filter</span>
|
||||
<% if advanced_filters.blank? %>
|
||||
<span>Filter</span>
|
||||
<% end %>
|
||||
</label>
|
||||
<ul tabindex="0" class="z-10 dropdown-content p-2 mt-2 shadow menu text-base bg-base-100 rounded-box min-w-[180px] text-right">
|
||||
<ul tabindex="0" class="z-10 dropdown-content p-2 mt-2 shadow menu text-base bg-base-100 rounded-box text-right" style="min-width: 180px;">
|
||||
<li class="flex">
|
||||
<%= link_to submissions_filter_path('completed_at', query_params.merge(path: url_for)), data: { turbo_frame: 'modal' } do %>
|
||||
<%= svg_icon('calendar_check', class: 'w-5 h-5 flex-shrink-0 stroke-2') %>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<% path_uri = Addressable::URI.parse(params[:path].to_s) %>
|
||||
<% path = url_for(**Rails.application.routes.recognize_path(path_uri.path, method: :get)) %>
|
||||
<%= render 'shared/turbo_modal', title: local_assigns[:title] do %>
|
||||
<%= form_for '', url: params[:path], method: :get, data: { turbo_frame: :_top }, html: { autocomplete: :off } do |f| %>
|
||||
<%= form_for '', url: path, method: :get, data: { turbo_frame: :_top }, html: { autocomplete: :off } do |f| %>
|
||||
<%= hidden_field_tag :q, params[:q] if params[:q].present? %>
|
||||
<% local_assigns[:default_params].each do |key, value| %>
|
||||
<%= hidden_field_tag(key, value) if value.present? %>
|
||||
@@ -10,7 +12,7 @@
|
||||
</div>
|
||||
<% if params[:with_remove] %>
|
||||
<div class="text-center w-full mt-4">
|
||||
<%= link_to t('remove_filter'), "#{params[:path]}?#{request.query_parameters.slice('q').merge(local_assigns[:default_params]).to_query}", class: 'link', data: { turbo_frame: :_top } %>
|
||||
<%= link_to t('remove_filter'), "#{path}?#{request.query_parameters.slice('q').merge(local_assigns[:default_params]).to_query}", class: 'link', data: { turbo_frame: :_top } %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
<div class="form-control w-full">
|
||||
<div id="status" class="radio-select grid grid-cols-2 gap-2 px-1">
|
||||
<% ['', 'pending', 'completed', 'partially_completed', 'sent', 'opened', 'declined', 'expired'].each do |status| %>
|
||||
<label class="radio-label cursor-pointer inline-flex items-center space-x-2">
|
||||
<%= radio_button_tag 'status', status, params[:status] == status || (status == '' && params[:status].blank?), class: 'base-radio' %>
|
||||
<span><%= t(status.presence || 'all') %></span>
|
||||
<label class="radio-label cursor-pointer inline-flex items-center space-x-2 min-w-0">
|
||||
<%= radio_button_tag 'status', status, params[:status] == status || (status == '' && params[:status].blank?), class: 'base-radio shrink-0' %>
|
||||
<span class="truncate"><%= t(status.presence || 'all') %></span>
|
||||
</label>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% undefined_submitters = Templates.filter_undefined_submitters(@submitter.submission.template_submitters) %>
|
||||
<% if undefined_submitters.size == 1 && undefined_submitters.first['uuid'] == @submitter.uuid && @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 && @submitter.template && !@submitter.template.archived_at? %>
|
||||
<% if undefined_submitters.size == 1 && undefined_submitters.first['uuid'] == @submitter.uuid && Submitters::StartForm.can_resubmit?(@submitter) && @submitter.template && !@submitter.template.archived_at? %>
|
||||
<div class="divider uppercase"><%= t('or') %></div>
|
||||
<toggle-submit class="block">
|
||||
<%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), resubmit_form_path, params: { resubmit: @submitter.slug }, method: :put, class: 'white-button w-full' %>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<%= form_for '', url: submit_form_email_2fa_path, method: :post, html: { class: 'space-y-4', id: 'code_form' } do |f| %>
|
||||
<div dir="auto" class="form-control !mt-0">
|
||||
<%= f.hidden_field :submitter_slug, value: @submitter.slug %>
|
||||
<%= f.text_field :one_time_code, required: true, class: 'base-input text-center', placeholder: 'XXX-XXX' %>
|
||||
<%= f.text_field :one_time_code, required: true, inputmode: 'numeric', class: 'base-input text-center', placeholder: 'XXXXXX' %>
|
||||
<div class="flex justify-between items-center mt-1">
|
||||
<span>
|
||||
<% if flash[:alert] %>
|
||||
|
||||
@@ -37,10 +37,12 @@
|
||||
<span class="hidden sm:block">
|
||||
<%= render 'templates/upload_button', folder_name: @template_folder.full_name %>
|
||||
</span>
|
||||
<%= link_to new_template_path(folder_name: @template_folder.full_name), class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %>
|
||||
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
|
||||
<span class="hidden md:block"><%= t('create') %></span>
|
||||
<% end %>
|
||||
<open-modal-mobile data-disabled="<%= !Docuseal.multitenant? %>" class="flex">
|
||||
<%= link_to new_template_path(folder_name: @template_folder.full_name), class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %>
|
||||
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
|
||||
<span class="hidden md:block"><%= t('create') %></span>
|
||||
<% end %>
|
||||
</open-modal-mobile>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loaded 100 of 144 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user