Compare commits

..
52 Commits
Author SHA1 Message Date
Pete Matsyburka 5f32e2a5af adjust create event 2024-04-21 17:01:44 +03:00
Pete Matsyburka 70012fbd7c fix form 2024-04-21 16:40:28 +03:00
Pete Matsyburka 668f6ca975 fix cells draw 2024-04-21 00:32:40 +03:00
Pete Matsyburka e78dd301ef fix download ttl 2024-04-20 23:58:43 +03:00
Pete Matsyburka f8ec3daf10 add minimize prop 2024-04-20 18:53:16 +03:00
Pete Matsyburka 75c929639d fix condition last field error 2024-04-20 17:23:28 +03:00
Pete Matsyburka e9c1242b8d verify field only once 2024-04-20 17:07:31 +03:00
Pete Matsyburka e7eb1d4f12 add noindex 2024-04-20 15:47:48 +03:00
Pete Matsyburka abbd3e809a remove sign now from archived 2024-04-20 12:54:14 +03:00
Pete Matsyburka f430b4f5a6 change default email 2024-04-20 11:25:04 +03:00
Pete Matsyburka e65234b4c8 add archived users view 2024-04-19 16:19:02 +03:00
Pete Matsyburka fe04d22c94 fix email normalize 2024-04-19 12:30:10 +03:00
Pete Matsyburka 44a7a5ebdc fix go to last 2024-04-19 10:33:31 +03:00
Pete Matsyburka 119ef0d0c7 add force sso key 2024-04-19 02:04:35 +03:00
Pete Matsyburka cdb8eb7931 fix pdf number of pages 2024-04-18 22:25:07 +03:00
Pete Matsyburka aef176c0cc downgrade net smtp 2024-04-18 18:43:47 +03:00
Pete Matsyburka 0c21b55865 adjust fixed email log 2024-04-18 17:02:53 +03:00
Pete Matsyburka 964b1444a5 adjust completed action 2024-04-18 16:45:09 +03:00
Pete Matsyburka 48de343948 add fix email log 2024-04-18 10:44:45 +03:00
Pete Matsyburka 2640934c15 skip .om tld typo fix 2024-04-18 10:42:54 +03:00
Pete Matsyburka 442dd4d6db add submitters template_id filter 2024-04-18 01:48:14 +03:00
Pete Matsyburka 3168feb3d6 clone template preferences 2024-04-17 21:09:35 +03:00
Pete Matsyburka fbae4933f6 fix multiple bcc addresses 2024-04-17 20:51:34 +03:00
Pete Matsyburka 78bb66c344 adjust wording 2024-04-17 20:24:31 +03:00
Pete Matsyburka 5af65ef74f adjust drawer 2024-04-17 17:27:43 +03:00
Pete Matsyburka 4d354ae9ff do not load storage configs in dev 2024-04-17 16:51:42 +03:00
Pete Matsyburka 1e57c1ace7 add template preferences 2024-04-17 16:09:09 +03:00
Pete Matsyburka fefd138dfd add devise log 2024-04-17 00:12:05 +03:00
Pete Matsyburka 16a77d3536 add log 2024-04-16 14:07:38 +03:00
Pete Matsyburka e03857bdb7 mark required field on mobile 2024-04-16 10:42:11 +03:00
Pete Matsyburka fc679bcd72 add require file links auth toggle 2024-04-15 20:34:00 +03:00
Pete Matsyburka 652b0da021 add fields param 2024-04-15 19:36:16 +03:00
Pete Matsyburka 2d54b0db2f update deps 2024-04-15 18:33:55 +03:00
Pete Matsyburka 7a29894926 add required fields prop 2024-04-15 16:34:44 +03:00
Pete Matsyburka f9ae79dd5b add go to last 2024-04-13 17:41:49 +03:00
Pete Matsyburka cadb78ca35 check on email events 2024-04-13 12:37:48 +03:00
Pete Matsyburka 36a6b7ff1d fix typo 2024-04-13 11:35:04 +03:00
Pete Matsyburka efee46f113 add bulk source 2024-04-12 17:20:00 +03:00
Pete Matsyburka b6639738c3 adjust dockerfile 2024-04-11 22:52:12 +03:00
Alex Turchyn aab07c5f49 add icons 2024-04-11 22:52:11 +03:00
Pete Matsyburka 33c0784bc6 add folder name to submission resp 2024-04-11 16:36:53 +03:00
Pete Matsyburka 6f5ebfc8a6 fix preview completed 2024-04-11 02:45:38 +03:00
Pete Matsyburka 811091ff14 adjust text signature size 2024-04-10 22:24:19 +03:00
Pete Matsyburka 6666f39928 add redacted param 2024-04-10 19:56:35 +03:00
Pete Matsyburka f67db9fe5f add field uuid param 2024-04-10 13:30:27 +03:00
Pete Matsyburka 45f002803e adjust padding 2024-04-10 10:55:51 +03:00
Pete Matsyburka 8c19f90e88 remove debug 2024-04-10 00:44:36 +03:00
Pete Matsyburka a8f5d5cea7 fix typo 2024-04-09 19:08:05 +03:00
Pete Matsyburka 6788071eec add webhook event preferences 2024-04-09 18:11:58 +03:00
Pete Matsyburka 0c690b3033 custom submit button 2024-04-09 01:54:39 +03:00
Pete Matsyburka dcb963b623 add autoscrollFields prop 2024-04-08 18:37:10 +03:00
Pete Matsyburka 04e30dbdd6 add submitter update params 2024-04-08 10:51:18 +03:00
96 changed files with 1050 additions and 289 deletions

No files matched your search

+4 -4
View File
@@ -23,10 +23,10 @@ RUN yarn install --network-timeout 1000000
COPY ./bin/shakapacker ./bin/shakapacker COPY ./bin/shakapacker ./bin/shakapacker
COPY ./config/webpack ./config/webpack COPY ./config/webpack ./config/webpack
COPY ./config/shakapacker.yml ./config/shakapacker.yml COPY ./config/shakapacker.yml ./config/shakapacker.yml
COPY ./postcss.config.js ./postcss.config.js ./ COPY ./postcss.config.js ./postcss.config.js
COPY ./tailwind.config.js ./tailwind.config.js ./ COPY ./tailwind.config.js ./tailwind.config.js
COPY ./tailwind.form.config.js ./tailwind.form.config.js ./ COPY ./tailwind.form.config.js ./tailwind.form.config.js
COPY ./tailwind.application.config.js ./tailwind.application.config.js ./ COPY ./tailwind.application.config.js ./tailwind.application.config.js
COPY ./app/javascript ./app/javascript COPY ./app/javascript ./app/javascript
COPY ./app/views ./app/views COPY ./app/views ./app/views
+175 -164
View File
@@ -2,35 +2,35 @@ GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
Ascii85 (1.1.0) Ascii85 (1.1.0)
actioncable (7.1.2) actioncable (7.1.3.2)
actionpack (= 7.1.2) actionpack (= 7.1.3.2)
activesupport (= 7.1.2) activesupport (= 7.1.3.2)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
actionmailbox (7.1.2) actionmailbox (7.1.3.2)
actionpack (= 7.1.2) actionpack (= 7.1.3.2)
activejob (= 7.1.2) activejob (= 7.1.3.2)
activerecord (= 7.1.2) activerecord (= 7.1.3.2)
activestorage (= 7.1.2) activestorage (= 7.1.3.2)
activesupport (= 7.1.2) activesupport (= 7.1.3.2)
mail (>= 2.7.1) mail (>= 2.7.1)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
actionmailer (7.1.2) actionmailer (7.1.3.2)
actionpack (= 7.1.2) actionpack (= 7.1.3.2)
actionview (= 7.1.2) actionview (= 7.1.3.2)
activejob (= 7.1.2) activejob (= 7.1.3.2)
activesupport (= 7.1.2) activesupport (= 7.1.3.2)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
actionpack (7.1.2) actionpack (7.1.3.2)
actionview (= 7.1.2) actionview (= 7.1.3.2)
activesupport (= 7.1.2) activesupport (= 7.1.3.2)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
racc racc
rack (>= 2.2.4) rack (>= 2.2.4)
@@ -38,35 +38,35 @@ GEM
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
actiontext (7.1.2) actiontext (7.1.3.2)
actionpack (= 7.1.2) actionpack (= 7.1.3.2)
activerecord (= 7.1.2) activerecord (= 7.1.3.2)
activestorage (= 7.1.2) activestorage (= 7.1.3.2)
activesupport (= 7.1.2) activesupport (= 7.1.3.2)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (7.1.2) actionview (7.1.3.2)
activesupport (= 7.1.2) activesupport (= 7.1.3.2)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.11) erubi (~> 1.11)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
activejob (7.1.2) activejob (7.1.3.2)
activesupport (= 7.1.2) activesupport (= 7.1.3.2)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (7.1.2) activemodel (7.1.3.2)
activesupport (= 7.1.2) activesupport (= 7.1.3.2)
activerecord (7.1.2) activerecord (7.1.3.2)
activemodel (= 7.1.2) activemodel (= 7.1.3.2)
activesupport (= 7.1.2) activesupport (= 7.1.3.2)
timeout (>= 0.4.0) timeout (>= 0.4.0)
activestorage (7.1.2) activestorage (7.1.3.2)
actionpack (= 7.1.2) actionpack (= 7.1.3.2)
activejob (= 7.1.2) activejob (= 7.1.3.2)
activerecord (= 7.1.2) activerecord (= 7.1.3.2)
activesupport (= 7.1.2) activesupport (= 7.1.3.2)
marcel (~> 1.0) marcel (~> 1.0)
activesupport (7.1.2) activesupport (7.1.3.2)
base64 base64
bigdecimal bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
@@ -85,17 +85,17 @@ GEM
arabic-letter-connector (0.1.1) arabic-letter-connector (0.1.1)
ast (2.4.2) ast (2.4.2)
aws-eventstream (1.3.0) aws-eventstream (1.3.0)
aws-partitions (1.874.0) aws-partitions (1.913.0)
aws-sdk-core (3.190.1) aws-sdk-core (3.191.6)
aws-eventstream (~> 1, >= 1.3.0) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0) aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8) aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.75.0) aws-sdk-kms (1.79.0)
aws-sdk-core (~> 3, >= 3.188.0) aws-sdk-core (~> 3, >= 3.191.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.142.0) aws-sdk-s3 (1.146.1)
aws-sdk-core (~> 3, >= 3.189.0) aws-sdk-core (~> 3, >= 3.191.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8) aws-sigv4 (~> 1.8)
aws-sigv4 (1.8.0) aws-sigv4 (1.8.0)
@@ -110,52 +110,54 @@ GEM
nokogiri (~> 1, >= 1.10.8) nokogiri (~> 1, >= 1.10.8)
base64 (0.2.0) base64 (0.2.0)
bcrypt (3.1.20) bcrypt (3.1.20)
better_html (2.0.2) better_html (2.1.1)
actionview (>= 6.0) actionview (>= 6.0)
activesupport (>= 6.0) activesupport (>= 6.0)
ast (~> 2.0) ast (~> 2.0)
erubi (~> 1.4) erubi (~> 1.4)
parser (>= 2.4) parser (>= 2.4)
smart_properties smart_properties
bigdecimal (3.1.5) bigdecimal (3.1.7)
bindex (0.8.1) bindex (0.8.1)
bootsnap (1.17.0) bootsnap (1.18.3)
msgpack (~> 1.2) msgpack (~> 1.2)
builder (3.2.4) builder (3.2.4)
bullet (7.1.4) bullet (7.1.6)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.11) uniform_notifier (~> 1.11)
camertron-eprun (1.1.1) camertron-eprun (1.1.1)
cancancan (3.5.0) cancancan (3.5.0)
capybara (3.39.2) capybara (3.40.0)
addressable addressable
matrix matrix
mini_mime (>= 0.1.3) mini_mime (>= 0.1.3)
nokogiri (~> 1.8) nokogiri (~> 1.11)
rack (>= 1.6.0) rack (>= 1.6.0)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0) regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2) xpath (~> 3.2)
childprocess (5.0.0)
chunky_png (1.4.0) chunky_png (1.4.0)
cldr-plurals-runtime-rb (1.1.0) cldr-plurals-runtime-rb (1.1.0)
cmdparse (3.0.7) cmdparse (3.0.7)
coderay (1.1.3) coderay (1.1.3)
concurrent-ruby (1.2.2) concurrent-ruby (1.2.3)
connection_pool (2.4.1) connection_pool (2.4.1)
crack (0.4.5) crack (1.0.0)
bigdecimal
rexml rexml
crass (1.0.6) crass (1.0.6)
css_parser (1.16.0) css_parser (1.17.1)
addressable addressable
cuprite (0.15) cuprite (0.15)
capybara (~> 3.0) capybara (~> 3.0)
ferrum (~> 0.14.0) ferrum (~> 0.14.0)
date (3.3.4) date (3.3.4)
debug (1.9.1) debug (1.9.2)
irb (~> 1.10) irb (~> 1.10)
reline (>= 0.3.8) reline (>= 0.3.8)
declarative (0.0.20) declarative (0.0.20)
devise (4.9.3) devise (4.9.4)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
railties (>= 4.1.0) railties (>= 4.1.0)
@@ -166,13 +168,12 @@ GEM
devise (~> 4.0) devise (~> 4.0)
railties (~> 7.0) railties (~> 7.0)
rotp (~> 6.0) rotp (~> 6.0)
diff-lcs (1.5.0) diff-lcs (1.5.1)
digest-crc (0.6.5) digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0) rake (>= 12.0.0, < 14.0.0)
docile (1.4.0) docile (1.4.0)
dotenv (2.8.1) dotenv (3.1.0)
drb (2.2.0) drb (2.2.1)
ruby2_keywords
email_typo (0.2.3) email_typo (0.2.3)
erb_lint (0.5.0) erb_lint (0.5.0)
activesupport activesupport
@@ -182,12 +183,12 @@ GEM
rubocop rubocop
smart_properties smart_properties
erubi (1.12.0) erubi (1.12.0)
factory_bot (6.4.4) factory_bot (6.4.6)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
factory_bot_rails (6.4.2) factory_bot_rails (6.4.3)
factory_bot (~> 6.4) factory_bot (~> 6.4)
railties (>= 5.0.0) railties (>= 5.0.0)
faker (3.2.2) faker (3.3.1)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
faraday (1.10.3) faraday (1.10.3)
faraday-em_http (~> 1.0) faraday-em_http (~> 1.0)
@@ -225,34 +226,34 @@ GEM
geom2d (0.4.1) geom2d (0.4.1)
globalid (1.2.1) globalid (1.2.1)
activesupport (>= 6.1) activesupport (>= 6.1)
google-apis-core (0.11.2) google-apis-core (0.14.1)
addressable (~> 2.5, >= 2.5.1) addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a) googleauth (~> 1.9)
httpclient (>= 2.8.1, < 3.a) httpclient (>= 2.8.1, < 3.a)
mini_mime (~> 1.0) mini_mime (~> 1.0)
representable (~> 3.0) representable (~> 3.0)
retriable (>= 2.0, < 4.a) retriable (>= 2.0, < 4.a)
rexml rexml
webrick google-apis-iamcredentials_v1 (0.20.0)
google-apis-iamcredentials_v1 (0.17.0) google-apis-core (>= 0.14.0, < 2.a)
google-apis-core (>= 0.11.0, < 2.a) google-apis-storage_v1 (0.37.0)
google-apis-storage_v1 (0.29.0) google-apis-core (>= 0.14.0, < 2.a)
google-apis-core (>= 0.11.0, < 2.a) google-cloud-core (1.7.0)
google-cloud-core (1.6.1)
google-cloud-env (>= 1.0, < 3.a) google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0) google-cloud-errors (~> 1.0)
google-cloud-env (2.1.0) google-cloud-env (2.1.1)
faraday (>= 1.0, < 3.a) faraday (>= 1.0, < 3.a)
google-cloud-errors (1.3.1) google-cloud-errors (1.4.0)
google-cloud-storage (1.45.0) google-cloud-storage (1.49.0)
addressable (~> 2.8) addressable (~> 2.8)
digest-crc (~> 0.4) digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1) google-apis-core (~> 0.13)
google-apis-storage_v1 (~> 0.29.0) google-apis-iamcredentials_v1 (~> 0.18)
google-apis-storage_v1 (~> 0.33)
google-cloud-core (~> 1.6) google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a) googleauth (~> 1.9)
mini_mime (~> 1.0) mini_mime (~> 1.0)
googleauth (1.9.1) googleauth (1.11.0)
faraday (>= 1.0, < 3.a) faraday (>= 1.0, < 3.a)
google-cloud-env (~> 2.1) google-cloud-env (~> 2.1)
jwt (>= 1.4, < 3.0) jwt (>= 1.4, < 3.0)
@@ -261,29 +262,31 @@ GEM
signet (>= 0.16, < 2.a) signet (>= 0.16, < 2.a)
hashdiff (1.1.0) hashdiff (1.1.0)
hashery (2.1.2) hashery (2.1.2)
hexapdf (0.37.1) hexapdf (0.40.0)
cmdparse (~> 3.0, >= 3.0.3) cmdparse (~> 3.0, >= 3.0.3)
geom2d (~> 0.4, >= 0.4.1) geom2d (~> 0.4, >= 0.4.1)
openssl (>= 2.2.1) openssl (>= 2.2.1)
htmlentities (4.3.4) htmlentities (4.3.4)
httpclient (2.8.3) httpclient (2.8.3)
i18n (1.14.1) i18n (1.14.4)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
image_processing (1.12.2) image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5) mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3) ruby-vips (>= 2.0.17, < 3)
io-console (0.7.1) io-console (0.7.2)
irb (1.11.0) irb (1.12.0)
rdoc rdoc
reline (>= 0.3.8) reline (>= 0.4.2)
jmespath (1.6.2) jmespath (1.6.2)
json (2.7.1) json (2.7.2)
jwt (2.7.1) jwt (2.8.1)
base64
language_server-protocol (3.17.0.3) language_server-protocol (3.17.0.3)
launchy (2.5.2) launchy (3.0.0)
addressable (~> 2.8) addressable (~> 2.8)
letter_opener (1.8.1) childprocess (~> 5.0)
launchy (>= 2.2, < 3) letter_opener (1.10.0)
launchy (>= 2.2, < 4)
letter_opener_web (2.0.0) letter_opener_web (2.0.0)
actionmailer (>= 5.2) actionmailer (>= 5.2)
letter_opener (~> 1.7) letter_opener (~> 1.7)
@@ -302,21 +305,21 @@ GEM
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
marcel (1.0.2) marcel (1.0.4)
matrix (0.4.2) matrix (0.4.2)
method_source (1.0.0) method_source (1.1.0)
mini_magick (4.12.0) mini_magick (4.12.0)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.5) mini_portile2 (2.8.6)
minitest (5.20.0) minitest (5.22.3)
msgpack (1.7.2) msgpack (1.7.2)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.3.0) multipart-post (2.4.0)
mutex_m (0.2.0) mutex_m (0.2.0)
mysql2 (0.5.5) mysql2 (0.5.6)
net-http-persistent (4.0.2) net-http-persistent (4.0.2)
connection_pool (~> 2.2) connection_pool (~> 2.2)
net-imap (0.4.9) net-imap (0.4.10)
date date
net-protocol net-protocol
net-pop (0.1.2) net-pop (0.1.2)
@@ -325,18 +328,21 @@ GEM
timeout timeout
net-smtp (0.4.0) net-smtp (0.4.0)
net-protocol net-protocol
nio4r (2.7.0) nio4r (2.7.1)
nokogiri (1.15.5) nokogiri (1.16.4)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.4-arm64-darwin)
racc (~> 1.4)
oj (3.16.3) oj (3.16.3)
bigdecimal (>= 3.0) bigdecimal (>= 3.0)
openssl (3.2.0) openssl (3.2.0)
orm_adapter (0.5.0) orm_adapter (0.5.0)
os (1.1.4) os (1.1.4)
pagy (6.2.0) package_json (0.1.0)
pagy (8.1.2)
parallel (1.24.0) parallel (1.24.0)
parser (3.2.2.4) parser (3.3.0.5)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
pdf-reader (2.12.0) pdf-reader (2.12.0)
@@ -345,8 +351,8 @@ GEM
hashery (~> 2.0) hashery (~> 2.0)
ruby-rc4 ruby-rc4
ttfunk ttfunk
pg (1.5.4) pg (1.5.6)
premailer (1.21.0) premailer (1.23.0)
addressable addressable
css_parser (>= 1.12.0) css_parser (>= 1.12.0)
htmlentities (>= 4.0.0) htmlentities (>= 4.0.0)
@@ -363,11 +369,11 @@ GEM
pry (>= 0.10.4) pry (>= 0.10.4)
psych (5.1.2) psych (5.1.2)
stringio stringio
public_suffix (5.0.4) public_suffix (5.0.5)
puma (6.4.0) puma (6.4.2)
nio4r (~> 2.0) nio4r (~> 2.0)
racc (1.7.3) racc (1.7.3)
rack (2.2.8) rack (2.2.9)
rack-proxy (0.7.7) rack-proxy (0.7.7)
rack rack
rack-session (1.0.2) rack-session (1.0.2)
@@ -377,20 +383,20 @@ GEM
rackup (1.0.0) rackup (1.0.0)
rack (< 3) rack (< 3)
webrick webrick
rails (7.1.2) rails (7.1.3.2)
actioncable (= 7.1.2) actioncable (= 7.1.3.2)
actionmailbox (= 7.1.2) actionmailbox (= 7.1.3.2)
actionmailer (= 7.1.2) actionmailer (= 7.1.3.2)
actionpack (= 7.1.2) actionpack (= 7.1.3.2)
actiontext (= 7.1.2) actiontext (= 7.1.3.2)
actionview (= 7.1.2) actionview (= 7.1.3.2)
activejob (= 7.1.2) activejob (= 7.1.3.2)
activemodel (= 7.1.2) activemodel (= 7.1.3.2)
activerecord (= 7.1.2) activerecord (= 7.1.3.2)
activestorage (= 7.1.2) activestorage (= 7.1.3.2)
activesupport (= 7.1.2) activesupport (= 7.1.3.2)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 7.1.2) railties (= 7.1.3.2)
rails-dom-testing (2.2.0) rails-dom-testing (2.2.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
minitest minitest
@@ -398,35 +404,35 @@ GEM
rails-html-sanitizer (1.6.0) rails-html-sanitizer (1.6.0)
loofah (~> 2.21) loofah (~> 2.21)
nokogiri (~> 1.14) nokogiri (~> 1.14)
rails-i18n (7.0.8) rails-i18n (7.0.9)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8) railties (>= 6.0.0, < 8)
rails_autolink (1.1.8) rails_autolink (1.1.8)
actionview (> 3.1) actionview (> 3.1)
activesupport (> 3.1) activesupport (> 3.1)
railties (> 3.1) railties (> 3.1)
railties (7.1.2) railties (7.1.3.2)
actionpack (= 7.1.2) actionpack (= 7.1.3.2)
activesupport (= 7.1.2) activesupport (= 7.1.3.2)
irb irb
rackup (>= 1.0.0) rackup (>= 1.0.0)
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0, >= 1.2.2) thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.1.0) rake (13.2.1)
rdoc (6.6.2) rdoc (6.6.3.1)
psych (>= 4.0.0) psych (>= 4.0.0)
redis-client (0.19.1) redis-client (0.22.0)
connection_pool connection_pool
regexp_parser (2.8.3) regexp_parser (2.9.0)
reline (0.4.1) reline (0.5.1)
io-console (~> 0.5) io-console (~> 0.5)
representable (3.2.0) representable (3.2.0)
declarative (< 0.1.0) declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0) trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0) uber (< 0.2.0)
request_store (1.5.1) request_store (1.6.0)
rack (>= 1.4) rack (>= 1.4)
responders (3.1.1) responders (3.1.1)
actionpack (>= 5.2) actionpack (>= 5.2)
@@ -438,73 +444,77 @@ GEM
chunky_png (~> 1.0) chunky_png (~> 1.0)
rqrcode_core (~> 1.0) rqrcode_core (~> 1.0)
rqrcode_core (1.2.0) rqrcode_core (1.2.0)
rspec-core (3.12.2) rspec-core (3.13.0)
rspec-support (~> 3.12.0) rspec-support (~> 3.13.0)
rspec-expectations (3.12.3) rspec-expectations (3.13.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0) rspec-support (~> 3.13.0)
rspec-mocks (3.12.6) rspec-mocks (3.13.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0) rspec-support (~> 3.13.0)
rspec-rails (6.1.0) rspec-rails (6.1.2)
actionpack (>= 6.1) actionpack (>= 6.1)
activesupport (>= 6.1) activesupport (>= 6.1)
railties (>= 6.1) railties (>= 6.1)
rspec-core (~> 3.12) rspec-core (~> 3.13)
rspec-expectations (~> 3.12) rspec-expectations (~> 3.13)
rspec-mocks (~> 3.12) rspec-mocks (~> 3.13)
rspec-support (~> 3.12) rspec-support (~> 3.13)
rspec-support (3.12.1) rspec-support (3.13.1)
rubocop (1.59.0) rubocop (1.63.1)
json (~> 2.3) json (~> 2.3)
language_server-protocol (>= 3.17.0) language_server-protocol (>= 3.17.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.2.2.4) parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0) regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0) rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.30.0, < 2.0) rubocop-ast (>= 1.31.1, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0) unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.30.0) rubocop-ast (1.31.2)
parser (>= 3.2.1.0) parser (>= 3.3.0.4)
rubocop-capybara (2.19.0) rubocop-capybara (2.20.0)
rubocop (~> 1.41) rubocop (~> 1.41)
rubocop-factory_bot (2.24.0) rubocop-factory_bot (2.25.1)
rubocop (~> 1.33) rubocop (~> 1.41)
rubocop-performance (1.20.1) rubocop-performance (1.21.0)
rubocop (>= 1.48.1, < 2.0) rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.30.0, < 2.0) rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rails (2.23.1) rubocop-rails (2.24.1)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0) rubocop (>= 1.33.0, < 2.0)
rubocop-ast (>= 1.30.0, < 2.0) rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rspec (2.25.0) rubocop-rspec (2.29.1)
rubocop (~> 1.40) rubocop (~> 1.40)
rubocop-capybara (~> 2.17) rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22) rubocop-factory_bot (~> 2.22)
rubocop-rspec_rails (~> 2.28)
rubocop-rspec_rails (2.28.3)
rubocop (~> 1.40)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
ruby-rc4 (0.1.5) ruby-rc4 (0.1.5)
ruby-vips (2.2.0) ruby-vips (2.2.1)
ffi (~> 1.12) ffi (~> 1.12)
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rubyXL (3.4.25) rubyXL (3.4.27)
nokogiri (>= 1.10.8) nokogiri (>= 1.10.8)
rubyzip (>= 1.3.0) rubyzip (>= 1.3.0)
rubyzip (2.3.2) rubyzip (2.3.2)
semantic_range (3.0.0) semantic_range (3.0.0)
shakapacker (7.1.0) shakapacker (7.2.3)
activesupport (>= 5.2) activesupport (>= 5.2)
package_json
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 5.2) railties (>= 5.2)
semantic_range (>= 2.3.0) semantic_range (>= 2.3.0)
sidekiq (7.2.0) sidekiq (7.2.2)
concurrent-ruby (< 2) concurrent-ruby (< 2)
connection_pool (>= 2.3.0) connection_pool (>= 2.3.0)
rack (>= 2.2.4) rack (>= 2.2.4)
redis-client (>= 0.14.0) redis-client (>= 0.19.0)
signet (0.18.0) signet (0.19.0)
addressable (~> 2.8) addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a) faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0) jwt (>= 1.5, < 3.0)
@@ -516,17 +526,18 @@ GEM
simplecov-html (0.12.3) simplecov-html (0.12.3)
simplecov_json_formatter (0.1.4) simplecov_json_formatter (0.1.4)
smart_properties (1.17.0) smart_properties (1.17.0)
sqlite3 (1.7.0) sqlite3 (1.7.3)
mini_portile2 (~> 2.8.0) mini_portile2 (~> 2.8.0)
sqlite3 (1.7.0-arm64-darwin) sqlite3 (1.7.3-arm64-darwin)
stringio (3.1.0) stringio (3.1.0)
strip_attributes (1.13.0) strip_attributes (1.13.0)
activemodel (>= 3.0, < 8.0) activemodel (>= 3.0, < 8.0)
thor (1.3.0) thor (1.3.1)
timeout (0.4.1) timeout (0.4.1)
trailblazer-option (0.1.2) trailblazer-option (0.1.2)
ttfunk (1.7.0) ttfunk (1.8.0)
turbo-rails (1.5.0) bigdecimal (~> 3.1)
turbo-rails (2.0.5)
actionpack (>= 6.0.0) actionpack (>= 6.0.0)
activejob (>= 6.0.0) activejob (>= 6.0.0)
railties (>= 6.0.0) railties (>= 6.0.0)
@@ -536,7 +547,7 @@ GEM
tzinfo tzinfo
tzinfo (2.0.6) tzinfo (2.0.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
tzinfo-data (1.2023.4) tzinfo-data (1.2024.1)
tzinfo (>= 1.0.0) tzinfo (>= 1.0.0)
uber (0.1.0) uber (0.1.0)
unicode-display_width (2.5.0) unicode-display_width (2.5.0)
@@ -548,7 +559,7 @@ GEM
activemodel (>= 6.0.0) activemodel (>= 6.0.0)
bindex (>= 0.4.0) bindex (>= 0.4.0)
railties (>= 6.0.0) railties (>= 6.0.0)
webmock (3.19.1) webmock (3.23.0)
addressable (>= 2.8.0) addressable (>= 2.8.0)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0) hashdiff (>= 0.4.0, < 2.0.0)
@@ -558,7 +569,7 @@ GEM
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
zeitwerk (2.6.12) zeitwerk (2.6.13)
PLATFORMS PLATFORMS
arm64-darwin-22 arm64-darwin-22
@@ -9,7 +9,9 @@ class AccountConfigsController < ApplicationController
AccountConfig::FORCE_MFA, AccountConfig::FORCE_MFA,
AccountConfig::ALLOW_TO_RESUBMIT, AccountConfig::ALLOW_TO_RESUBMIT,
AccountConfig::ESIGNING_PREFERENCE_KEY, AccountConfig::ESIGNING_PREFERENCE_KEY,
AccountConfig::FORM_WITH_CONFETTI_KEY AccountConfig::FORM_WITH_CONFETTI_KEY,
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
AccountConfig::FORCE_SSO_AUTH_KEY
].freeze ].freeze
InvalidKey = Class.new(StandardError) InvalidKey = Class.new(StandardError)
@@ -8,6 +8,7 @@ module Api
skip_authorization_check skip_authorization_check
before_action :set_cors_headers before_action :set_cors_headers
before_action :set_noindex_headers
def show def show
blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid]) blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
@@ -20,6 +21,8 @@ module Api
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid) blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
authorization_check!(blob) if exp.blank?
if request.headers['Range'].present? if request.headers['Range'].present?
send_blob_byte_range_data blob, request.headers['Range'] send_blob_byte_range_data blob, request.headers['Range']
else else
@@ -31,5 +34,22 @@ module Api
end end
end end
end end
private
def authorization_check!(blob)
is_authorized =
blob.attachments.all? do |a|
a.name.in?(%w[logo preview_images]) ||
(current_user && a.record.account.id == current_user.account_id) ||
!a.record.account.account_configs.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value
end
return if is_authorized
Rollbar.error('Blob aunauthorized') if defined?(Rollbar)
raise CanCan::AccessDenied
end
end end
end end
@@ -7,6 +7,9 @@ module Api
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
skip_authorization_check skip_authorization_check
before_action :set_cors_headers
before_action :set_noindex_headers
# rubocop:disable Metrics # rubocop:disable Metrics
def show def show
Rollbar.info('Blob legacy') if defined?(Rollbar) Rollbar.info('Blob legacy') if defined?(Rollbar)
+13 -7
View File
@@ -50,20 +50,26 @@ module Api
end end
def authenticate_user! def authenticate_user!
@current_user ||=
if request.headers['X-Auth-Token'].present?
sha256 = Digest::SHA256.hexdigest(request.headers['X-Auth-Token'])
User.joins(:access_token).active.find_by(access_token: { sha256: })
end
render json: { error: 'Not authenticated' }, status: :unauthorized unless current_user render json: { error: 'Not authenticated' }, status: :unauthorized unless current_user
end end
def current_user
super || @current_user ||=
if request.headers['X-Auth-Token'].present?
sha256 = Digest::SHA256.hexdigest(request.headers['X-Auth-Token'])
User.joins(:access_token).active.find_by(access_token: { sha256: })
end
end
def current_account def current_account
current_user&.account current_user&.account
end end
def set_noindex_headers
headers['X-Robots-Tag'] = 'noindex'
end
def set_cors_headers def set_cors_headers
headers['Access-Control-Allow-Origin'] = '*' headers['Access-Control-Allow-Origin'] = '*'
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS' headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
+10 -6
View File
@@ -17,7 +17,8 @@ module Api
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] }) submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] })
end end
submissions = paginate(submissions.preload(:created_by_user, :template, :submitters, submissions = paginate(submissions.preload(:created_by_user, :submitters,
template: :folder,
audit_trail_attachment: :blob)) audit_trail_attachment: :blob))
render json: { render json: {
@@ -102,6 +103,8 @@ module Api
def destroy def destroy
@submission.update!(archived_at: Time.current) @submission.update!(archived_at: Time.current)
SendSubmissionArchivedWebhookRequestJob.perform_later(@submission)
render json: @submission.as_json(only: %i[id], methods: %i[archived_at]) render json: @submission.as_json(only: %i[id], methods: %i[archived_at])
end end
@@ -153,7 +156,8 @@ module Api
completed_at opened_at sent_at completed_at opened_at sent_at
created_at updated_at external_id metadata], created_at updated_at external_id metadata],
methods: %i[status application_key] }, methods: %i[status application_key] },
template: { only: %i[id name created_at updated_at] }, template: { only: %i[id name external_id created_at updated_at],
methods: %i[folder_name] },
created_by_user: { only: %i[id email first_name last_name] } created_by_user: { only: %i[id email first_name last_name] }
} }
} }
@@ -161,14 +165,14 @@ module Api
def submissions_params def submissions_params
permitted_attrs = [ permitted_attrs = [
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
{ {
message: %i[subject body], message: %i[subject body],
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role, submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :external_id, :reply_to, :completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body], { metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
fields: [:name, :default_value, :title, :description, fields: [:name, :uuid, :default_value, :title, :description,
:readonly, :validation_pattern, :invalid_message, :readonly, :redacted, :validation_pattern, :invalid_message,
{ default_value: [] }] }]] { default_value: [] }] }]]
} }
] ]
+16 -3
View File
@@ -10,6 +10,11 @@ module Api
submitters = submitters.where(external_id: params[:application_key]) if params[:application_key].present? submitters = submitters.where(external_id: params[:application_key]) if params[:application_key].present?
submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present? submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present?
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present? submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
if params[:template_id].present?
submitters = submitters.joins(:submission).where(submission: { template_id: params[:template_id] })
end
submitters = maybe_filder_by_completed_at(submitters, params) submitters = maybe_filder_by_completed_at(submitters, params)
submitters = paginate( submitters = paginate(
@@ -73,10 +78,10 @@ module Api
submitter_params = params.key?(:submitter) ? params.require(:submitter) : params submitter_params = params.key?(:submitter) ? params.require(:submitter) : params
submitter_params.permit( submitter_params.permit(
:send_email, :send_sms, :uuid, :name, :email, :role, :send_email, :send_sms, :reply_to, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :external_id, :completed, :phone, :application_key, :external_id, :go_to_last,
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body], { metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
fields: [[:name, :default_value, :readonly, :validation_pattern, :invalid_message, fields: [[:name, :uuid, :default_value, :readonly, :redacted, :validation_pattern, :invalid_message,
{ default_value: [] }]] } { default_value: [] }]] }
) )
end end
@@ -125,6 +130,14 @@ module Api
end end
submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms') submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms')
submitter.preferences['reply_to'] = submitter_preferences['reply_to'] if submitter_preferences.key?('reply_to')
if submitter_preferences.key?('go_to_last')
submitter.preferences['go_to_last'] = submitter_preferences['go_to_last']
end
if submitter_preferences.key?('completed_redirect_url')
submitter.preferences['completed_redirect_url'] = submitter_preferences['completed_redirect_url']
end
return unless submitter_preferences.key?('email_message_uuid') return unless submitter_preferences.key?('email_message_uuid')
+17 -2
View File
@@ -91,10 +91,25 @@ module Api
end end
def template_params def template_params
permitted_params = [
:name,
{
submitters: [%i[name uuid]],
fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value,
:title, :description,
{ preferences: {},
conditions: [%i[field_uuid value action]],
options: [%i[value uuid]],
validation: %i[message pattern],
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
}
]
if params.key?(:template) if params.key?(:template)
params.require(:template).permit(:name, submitters: [%i[name uuid]]) params.require(:template).permit(permitted_params)
else else
params.permit(:name, submitters: [%i[name uuid]]) params.permit(permitted_params)
end end
end end
end end
@@ -35,12 +35,12 @@ class NotificationsSettingsController < ApplicationController
def load_reminder_config def load_reminder_config
@reminder_config = @reminder_config =
AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::SUBMITTER_REMAILERS) AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::SUBMITTER_REMINDERS)
end end
def email_config_params def email_config_params
params.require(:account_config).permit!.tap do |attrs| params.require(:account_config).permit!.tap do |attrs|
attrs[:key] = nil unless attrs[:key].in?([AccountConfig::BCC_EMAILS, AccountConfig::SUBMITTER_REMAILERS]) attrs[:key] = nil unless attrs[:key].in?([AccountConfig::BCC_EMAILS, AccountConfig::SUBMITTER_REMINDERS])
end end
end end
end end
+2
View File
@@ -7,6 +7,8 @@ class SessionsController < Devise::SessionsController
email = sign_in_params[:email].to_s.downcase email = sign_in_params[:email].to_s.downcase
if Docuseal.multitenant? && !User.exists?(email:) if Docuseal.multitenant? && !User.exists?(email:)
Rollbar.warning('Sign in new user') if defined?(Rollbar)
return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)), return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)),
notice: 'Create a new account' notice: 'Create a new account'
end end
+11
View File
@@ -19,6 +19,8 @@ class SubmissionsController < ApplicationController
def create def create
authorize!(:create, Submission) authorize!(:create, Submission)
save_template_message(@template, params) if params[:save_message] == '1'
if params[:is_custom_message] != '1' if params[:is_custom_message] != '1'
params.delete(:subject) params.delete(:subject)
params.delete(:body) params.delete(:body)
@@ -50,11 +52,20 @@ class SubmissionsController < ApplicationController
def destroy def destroy
@submission.update!(archived_at: Time.current) @submission.update!(archived_at: Time.current)
SendSubmissionArchivedWebhookRequestJob.perform_later(@submission)
redirect_back(fallback_location: template_path(@submission.template), notice: 'Submission has been archived') redirect_back(fallback_location: template_path(@submission.template), notice: 'Submission has been archived')
end end
private private
def save_template_message(template, params)
template.preferences['request_email_subject'] = params[:subject] if params[:subject].present?
template.preferences['request_email_body'] = params[:body] if params[:body].present?
template.save!
end
def submissions_params def submissions_params
params.permit(submission: { submitters: [%i[uuid email phone name]] }) params.permit(submission: { submitters: [%i[uuid email phone name]] })
end end
@@ -44,7 +44,7 @@ class SubmissionsDownloadController < ApplicationController
def build_urls(submitter) def build_urls(submitter)
Submitters.select_attachments_for_download(submitter).map do |attachment| Submitters.select_attachments_for_download(submitter).map do |attachment|
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.minutes.from_now.to_i) ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
end end
end end
end end
+1 -1
View File
@@ -55,7 +55,7 @@ class SubmitFormController < ApplicationController
end end
def completed def completed
@submitter = Submitter.find_by!(slug: params[:submit_form_slug]) @submitter = Submitter.completed.find_by!(slug: params[:submit_form_slug])
end end
def success; end def success; end
@@ -0,0 +1,22 @@
# frozen_string_literal: true
class TemplatesPreferencesController < ApplicationController
load_and_authorize_resource :template
def show; end
def create
authorize!(:update, @template)
@template.preferences = @template.preferences.merge(template_params[:preferences])
@template.save!
head :ok
end
private
def template_params
params.require(:template).permit(preferences: %i[bcc_completed request_email_subject request_email_body])
end
end
+22 -6
View File
@@ -7,7 +7,14 @@ class UsersController < ApplicationController
authorize_resource :user, only: :create authorize_resource :user, only: :create
def index def index
@pagy, @users = pagy(@users.active.order(id: :desc)) @users =
if params[:status] == 'archived'
@users.archived
else
@users.active
end
@pagy, @users = pagy(@users.order(id: :desc))
end end
def new; end def new; end
@@ -15,10 +22,19 @@ class UsersController < ApplicationController
def edit; end def edit; end
def create def create
existing_user = User.accessible_by(current_ability).find_by(email: @user.email)
if existing_user
existing_user.archived_at = nil
existing_user.assign_attributes(user_params)
@user = existing_user
end
if @user.save if @user.save
UserMailer.invitation_email(@user).deliver_later! UserMailer.invitation_email(@user).deliver_later!
redirect_to settings_users_path, notice: 'User has been invited' redirect_back fallback_location: settings_users_path, notice: 'User has been invited'
else else
render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity
end end
@@ -27,11 +43,11 @@ class UsersController < ApplicationController
def update def update
return redirect_to settings_users_path, notice: 'Unable to update user.' if Docuseal.demo? return redirect_to settings_users_path, notice: 'Unable to update user.' if Docuseal.demo?
attrs = user_params.compact_blank attrs = user_params.compact_blank.merge(user_params.slice(:archived_at))
attrs.delete(:role) if !role_valid?(attrs[:role]) || current_user == @user attrs.delete(:role) if !role_valid?(attrs[:role]) || current_user == @user
if @user.update(attrs) if @user.update(attrs)
redirect_to settings_users_path, notice: 'User has been updated' redirect_back fallback_location: settings_users_path, notice: 'User has been updated'
else else
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity
end end
@@ -44,7 +60,7 @@ class UsersController < ApplicationController
@user.update!(archived_at: Time.current) @user.update!(archived_at: Time.current)
redirect_to settings_users_path, notice: 'User has been removed' redirect_back fallback_location: settings_users_path, notice: 'User has been removed'
end end
private private
@@ -65,6 +81,6 @@ class UsersController < ApplicationController
end end
def user_params def user_params
params.require(:user).permit(:email, :first_name, :last_name, :password, :role) params.require(:user).permit(:email, :first_name, :last_name, :password, :role, :archived_at)
end end
end end
@@ -0,0 +1,33 @@
# frozen_string_literal: true
class WebhookPreferencesController < ApplicationController
EVENTS = %w[
form.viewed
form.started
form.completed
submission.archived
].freeze
before_action :load_account_config
authorize_resource :account_config, parent: false
def create
@account_config.value[account_config_params[:event]] = account_config_params[:value] == '1'
@account_config.save!
head :ok
end
private
def load_account_config
@account_config =
current_account.account_configs.find_or_initialize_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)
@account_config.value ||= {}
end
def account_config_params
params.permit(:event, :value)
end
end
+2
View File
@@ -23,6 +23,7 @@ import SignatureForm from './elements/signature_form'
import SubmitForm from './elements/submit_form' import SubmitForm from './elements/submit_form'
import PromptPassword from './elements/prompt_password' import PromptPassword from './elements/prompt_password'
import EmailsTextarea from './elements/emails_textarea' import EmailsTextarea from './elements/emails_textarea'
import ToggleOnSubmit from './elements/toggle_on_submit'
import * as TurboInstantClick from './lib/turbo_instant_click' import * as TurboInstantClick from './lib/turbo_instant_click'
@@ -58,6 +59,7 @@ window.customElements.define('submit-form', SubmitForm)
window.customElements.define('prompt-password', PromptPassword) window.customElements.define('prompt-password', PromptPassword)
window.customElements.define('emails-textarea', EmailsTextarea) window.customElements.define('emails-textarea', EmailsTextarea)
window.customElements.define('toggle-cookies', ToggleCookies) window.customElements.define('toggle-cookies', ToggleCookies)
window.customElements.define('toggle-on-submit', ToggleOnSubmit)
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody) document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
document.addEventListener('turbo:submit-end', async (event) => { document.addEventListener('turbo:submit-end', async (event) => {
@@ -7,7 +7,7 @@ export default class extends HTMLElement {
resize () { resize () {
if (this.textarea.clientHeight < this.textarea.scrollHeight) { if (this.textarea.clientHeight < this.textarea.scrollHeight) {
this.textarea.style.height = `${this.textarea.scrollHeight}px` this.textarea.style.height = `${this.textarea.scrollHeight + 5}px`
} }
} }
@@ -0,0 +1,35 @@
export default class extends HTMLElement {
connectedCallback () {
document.addEventListener('turbo:submit-end', this.onSubmitEnd)
this.form.addEventListener('submit', this.onSubmit)
}
disconnectedCallback () {
document.removeEventListener('turbo:submit-end', this.onSubmitEnd)
this.form.removeEventListener('submit', this.onSubmit)
}
onSubmit = () => {
this.element.classList.add('invisible')
}
onSubmitEnd = (event) => {
if (event.target === this.form) {
const resp = event.detail?.formSubmission?.result?.fetchResponse?.response
if (resp?.status / 100 === 2) {
this.element.classList.remove('invisible')
}
}
}
get element () {
return document.getElementById(this.dataset.elementId)
}
get form () {
return this.closest('form')
}
}
+1 -1
View File
@@ -19,7 +19,7 @@
</label> </label>
<button <button
class="btn btn-outline btn-sm !normal-case font-normal" class="btn btn-outline btn-sm !normal-case font-normal"
@click.prevent="setCurrentDate" @click.prevent="[setCurrentDate(), $emit('focus')]"
> >
<IconCalendarCheck :width="16" /> <IconCalendarCheck :width="16" />
{{ t('set_today') }} {{ t('set_today') }}
+63 -6
View File
@@ -16,13 +16,17 @@
<button <button
v-if="!isFormVisible" v-if="!isFormVisible"
id="expand_form_button" id="expand_form_button"
class="btn btn-neutral flex text-white absolute rounded-none border-x-0 md:border md:rounded-full bottom-0 w-full md:mb-4 text-base" class="btn btn-neutral flex text-white absolute bottom-0 w-full mb-3"
style="width: 96%; margin-left: 2%"
@click.prevent="[isFormVisible = true, scrollIntoField(currentField)]" @click.prevent="[isFormVisible = true, scrollIntoField(currentField)]"
> >
<template v-if="['initials', 'signature'].includes(currentField.type)"> <template v-if="['initials', 'signature'].includes(currentField.type)">
<IconWritingSign stroke-width="1.5" /> <IconWritingSign stroke-width="1.5" />
{{ t('sign_now') }} {{ t('sign_now') }}
</template> </template>
<template v-else-if="alwaysMinimize">
{{ t('next') }}
</template>
<template v-else> <template v-else>
{{ t('submit_form') }} {{ t('submit_form') }}
</template> </template>
@@ -357,6 +361,7 @@
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:field="currentField" :field="currentField"
:show-field-names="showFieldNames" :show-field-names="showFieldNames"
:verified-value="phoneVerifiedValues[currentField.uuid]"
:default-value="submitter.phone" :default-value="submitter.phone"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@focus="scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
@@ -390,7 +395,7 @@
v-if="isSubmitting" v-if="isSubmitting"
class="mr-1 animate-spin" class="mr-1 animate-spin"
/> />
<span v-if="stepFields.length === currentStep + 1"> <span v-if="alwaysMinimize || stepFields.length === currentStep + 1">
{{ t('submit') }} {{ t('submit') }}
</span> </span>
<span v-else> <span v-else>
@@ -539,11 +544,21 @@ export default {
required: false, required: false,
default: null default: null
}, },
minimize: {
type: Boolean,
required: false,
default: false
},
withConfetti: { withConfetti: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false
}, },
autoscrollFields: {
type: Boolean,
required: false,
default: true
},
showFieldNames: { showFieldNames: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -646,12 +661,20 @@ export default {
isFormVisible: this.expand !== false, isFormVisible: this.expand !== false,
showFillAllRequiredFields: false, showFillAllRequiredFields: false,
currentStep: 0, currentStep: 0,
phoneVerifiedValues: {},
orientation: screen?.orientation?.type,
isSubmitting: false, isSubmitting: false,
submittedValues: {}, submittedValues: {},
recalculateButtonDisabledKey: '' recalculateButtonDisabledKey: ''
} }
}, },
computed: { computed: {
isMobile () {
return /android|iphone|ipad/i.test(navigator.userAgent)
},
alwaysMinimize () {
return this.minimize || (this.orientation?.includes('landscape') && this.isMobile && parseInt(window.innerHeight) < 550)
},
currentStepFields () { currentStepFields () {
return this.stepFields[this.currentStep] return this.stepFields[this.currentStep]
}, },
@@ -724,11 +747,21 @@ export default {
watch: { watch: {
expand (value) { expand (value) {
this.isFormVisible = value this.isFormVisible = value
},
currentStepFields (value) {
if (!value) {
this.currentStep -= 1
}
} }
}, },
beforeUnmount () {
screen?.orientation?.removeEventListener('change', this.onOrientationChange)
},
mounted () { mounted () {
this.submittedValues = JSON.parse(JSON.stringify(this.values)) this.submittedValues = JSON.parse(JSON.stringify(this.values))
screen?.orientation.addEventListener('change', this.onOrientationChange)
this.fields.forEach((field) => { this.fields.forEach((field) => {
if (field.default_value && !field.readonly) { if (field.default_value && !field.readonly) {
this.values[field.uuid] ||= field.default_value this.values[field.uuid] ||= field.default_value
@@ -762,6 +795,10 @@ export default {
this.isFormVisible = false this.isFormVisible = false
} }
if (this.alwaysMinimize) {
this.isFormVisible = false
}
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) { if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
this.$nextTick(() => { this.$nextTick(() => {
const root = this.$root.$el.parentNode.getRootNode() const root = this.$root.$el.parentNode.getRootNode()
@@ -791,6 +828,9 @@ export default {
t (key) { t (key) {
return this.i18n[key] || i18n[this.language?.toLowerCase()]?.[key] || i18n[this.browserLanguage]?.[key] || i18n.en[key] || key return this.i18n[key] || i18n[this.language?.toLowerCase()]?.[key] || i18n[this.browserLanguage]?.[key] || i18n.en[key] || key
}, },
onOrientationChange (event) {
this.orientation = event.target.type
},
checkFieldConditions (field) { checkFieldConditions (field) {
if (field.conditions?.length) { if (field.conditions?.length) {
return field.conditions.reduce((acc, c) => { return field.conditions.reduce((acc, c) => {
@@ -900,9 +940,9 @@ export default {
if (scrollToArea) { if (scrollToArea) {
this.scrollIntoField(step[0]) this.scrollIntoField(step[0])
}
this.$refs.form.querySelector('input[type="date"], input[type="number"], input[type="text"], select')?.focus() this.$refs.form.querySelector('input[type="date"], input[type="number"], input[type="text"], select')?.focus()
}
if (clickUpload && !this.values[this.currentField.uuid] && ['file', 'image'].includes(this.currentField.type)) { if (clickUpload && !this.values[this.currentField.uuid] && ['file', 'image'].includes(this.currentField.type)) {
this.$refs.form.querySelector('input[type="file"]')?.click() this.$refs.form.querySelector('input[type="file"]')?.click()
@@ -911,6 +951,7 @@ export default {
}, },
saveStep (formData) { saveStep (formData) {
const currentFieldUuids = this.currentStepFields.map((f) => f.uuid) const currentFieldUuids = this.currentStepFields.map((f) => f.uuid)
const currentFieldType = this.currentField.type
if (this.isCompleted) { if (this.isCompleted) {
return Promise.resolve({}) return Promise.resolve({})
@@ -922,6 +963,10 @@ export default {
if (response.status === 200) { if (response.status === 200) {
currentFieldUuids.forEach((fieldUuid) => { currentFieldUuids.forEach((fieldUuid) => {
this.submittedValues[fieldUuid] = this.values[fieldUuid] this.submittedValues[fieldUuid] = this.values[fieldUuid]
if (currentFieldType === 'phone') {
this.phoneVerifiedValues[fieldUuid] = this.values[fieldUuid]
}
}) })
} }
@@ -960,7 +1005,15 @@ export default {
formData.append('completed', 'true') formData.append('completed', 'true')
} }
await this.saveStep(formData).then(async (response) => { let saveStepRequest
if (!isLastStep && this.phoneVerifiedValues[this.currentField.uuid] && this.phoneVerifiedValues[this.currentField.uuid] === this.values[this.currentField.uuid]) {
saveStepRequest = Promise.resolve({})
} else {
saveStepRequest = this.saveStep(formData)
}
await saveStepRequest.then(async (response) => {
if (response.status === 422 || response.status === 500) { if (response.status === 422 || response.status === 500) {
const data = await response.json() const data = await response.json()
@@ -976,7 +1029,11 @@ export default {
const nextStep = (isLastStep && emptyRequiredField) || this.stepFields[this.currentStep + 1] const nextStep = (isLastStep && emptyRequiredField) || this.stepFields[this.currentStep + 1]
if (nextStep) { if (nextStep) {
this.goToStep(nextStep, true) if (this.alwaysMinimize) {
this.isFormVisible = false
}
this.goToStep(nextStep, this.autoscrollFields)
if (emptyRequiredField === nextStep) { if (emptyRequiredField === nextStep) {
this.showFillAllRequiredFields = true this.showFillAllRequiredFields = true
+2 -2
View File
@@ -24,7 +24,7 @@ const en = {
appears_on: 'Appears on', appears_on: 'Appears on',
page: 'Page', page: 'Page',
select_your_option: 'Select your option', select_your_option: 'Select your option',
complete_hightlighted_checkboxes_and_click: 'Complete hightlighted checkboxes and click', complete_hightlighted_checkboxes_and_click: 'Complete highlighted checkboxes and click',
submit: 'submit', submit: 'submit',
next: 'next', next: 'next',
click_to_upload: 'Click to upload', click_to_upload: 'Click to upload',
@@ -42,7 +42,7 @@ const en = {
please_check_the_box_to_continue: 'Please check the box to continue', please_check_the_box_to_continue: 'Please check the box to continue',
open_source_documents_software: 'open source documents software', open_source_documents_software: 'open source documents software',
verified_phone_number: 'Verify Phone Number', verified_phone_number: 'Verify Phone Number',
use_international_format: 'Use internatioanl format: +1xxx', use_international_format: 'Use international format: +1xxx',
six_digits_code: '6-digit code', six_digits_code: '6-digit code',
change_phone_number: 'Change phone number', change_phone_number: 'Change phone number',
sending: 'Sending...', sending: 'Sending...',
@@ -193,8 +193,6 @@ export default {
this.$refs.canvas.getContext('2d').scale(scale, scale) this.$refs.canvas.getContext('2d').scale(scale, scale)
} }
this.$refs.textInput?.focus()
}) })
if (this.$refs.canvas) { if (this.$refs.canvas) {
@@ -108,6 +108,11 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
verifiedValue: {
type: String,
required: false,
default: ''
},
submitterSlug: { submitterSlug: {
type: String, type: String,
required: true required: true
@@ -164,6 +169,10 @@ export default {
}) })
}, },
async submit () { async submit () {
if (this.verifiedValue && this.verifiedValue === this.modelValue) {
return Promise.resolve({})
}
if (!this.$refs.phone.value.toString().startsWith('+')) { if (!this.$refs.phone.value.toString().startsWith('+')) {
alert(this.t('use_international_format')) alert(this.t('use_international_format'))
+1 -1
View File
@@ -68,7 +68,7 @@
@keydown.enter.prevent="onNameEnter" @keydown.enter.prevent="onNameEnter"
@focus="onNameFocus" @focus="onNameFocus"
@blur="onNameBlur" @blur="onNameBlur"
>{{ optionIndexText }} {{ field.name || defaultName }}</span> >{{ optionIndexText }} {{ (defaultField ? (field.title || field.name) : field.name) || defaultName }}</span>
<div <div
v-if="isNameFocus && !['checkbox', 'phone'].includes(field.type)" v-if="isNameFocus && !['checkbox', 'phone'].includes(field.type)"
class="flex items-center ml-1.5" class="flex items-center ml-1.5"
+39 -6
View File
@@ -37,7 +37,7 @@
class="btn btn-primary btn-ghost text-base hidden md:flex" class="btn btn-primary btn-ghost text-base hidden md:flex"
:target="template.submitters.length > 1 ? '' : '_blank'" :target="template.submitters.length > 1 ? '' : '_blank'"
:data-turbo-frame="template.submitters.length > 1 ? 'modal' : ''" :data-turbo-frame="template.submitters.length > 1 ? 'modal' : ''"
@click="maybeShowEmptyTemplateAlert" @click="maybeShowErrorTemplateAlert"
> >
<IconWritingSign <IconWritingSign
width="22" width="22"
@@ -51,7 +51,7 @@
:href="`/templates/${template.id}/submissions/new?with_link=true`" :href="`/templates/${template.id}/submissions/new?with_link=true`"
data-turbo-frame="modal" data-turbo-frame="modal"
class="white-button md:!px-6" class="white-button md:!px-6"
@click="maybeShowEmptyTemplateAlert" @click="maybeShowErrorTemplateAlert"
> >
<IconUsersPlus <IconUsersPlus
width="20" width="20"
@@ -159,7 +159,7 @@
:selected-submitter="selectedSubmitter" :selected-submitter="selectedSubmitter"
:document="document" :document="document"
:is-drag="!!dragField" :is-drag="!!dragField"
:default-fields="defaultFields" :default-fields="[...defaultRequiredFields, ...defaultFields]"
:allow-draw="!onlyDefinedFields" :allow-draw="!onlyDefinedFields"
:default-submitters="defaultSubmitters" :default-submitters="defaultSubmitters"
:draw-field="drawField" :draw-field="drawField"
@@ -242,7 +242,8 @@
:with-help="withHelp" :with-help="withHelp"
:default-submitters="defaultSubmitters" :default-submitters="defaultSubmitters"
:draw-field-type="drawFieldType" :draw-field-type="drawFieldType"
:default-fields="defaultFields" :default-fields="[...defaultRequiredFields, ...defaultFields]"
:default-required-fields="defaultRequiredFields"
:field-types="fieldTypes" :field-types="fieldTypes"
:with-sticky-submitters="withStickySubmitters" :with-sticky-submitters="withStickySubmitters"
:only-defined-fields="onlyDefinedFields" :only-defined-fields="onlyDefinedFields"
@@ -273,7 +274,8 @@
<MobileFields <MobileFields
v-if="sortedDocuments.length && !drawField && editable" v-if="sortedDocuments.length && !drawField && editable"
:fields="template.fields" :fields="template.fields"
:default-fields="defaultFields" :default-fields="[...defaultRequiredFields, ...defaultFields]"
:default-required-fields="defaultRequiredFields"
:field-types="fieldTypes" :field-types="fieldTypes"
:selected-submitter="selectedSubmitter" :selected-submitter="selectedSubmitter"
@select="startFieldDraw($event)" @select="startFieldDraw($event)"
@@ -371,6 +373,11 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
defaultRequiredFields: {
type: Array,
required: false,
default: () => []
},
withSelectedFieldType: { withSelectedFieldType: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -425,6 +432,13 @@ export default {
return () => {} return () => {}
} }
}, },
onChange: {
type: Function,
required: false,
default () {
return () => {}
}
},
withStickySubmitters: { withStickySubmitters: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -522,6 +536,11 @@ export default {
return areas return areas
}, },
isAllRequiredFieldsAdded () {
return !this.defaultRequiredFields?.some((f) => {
return !this.template.fields?.some((field) => field.name === f.name)
})
},
selectedField () { selectedField () {
return this.template.fields.find((f) => f.areas?.includes(this.selectedAreaRef.value)) return this.template.fields.find((f) => f.areas?.includes(this.selectedAreaRef.value))
}, },
@@ -1093,7 +1112,13 @@ export default {
this.save() this.save()
}, },
maybeShowEmptyTemplateAlert (e) { maybeShowErrorTemplateAlert (e) {
if (!this.isAllRequiredFieldsAdded) {
e.preventDefault()
return alert(this.t('add_all_required_fields_to_continue'))
}
if (!this.template.fields.length) { if (!this.template.fields.length) {
e.preventDefault() e.preventDefault()
@@ -1101,6 +1126,10 @@ export default {
} }
}, },
onSaveClick () { onSaveClick () {
if (!this.isAllRequiredFieldsAdded) {
return alert(this.t('add_all_required_fields_to_continue'))
}
if (this.template.fields.length) { if (this.template.fields.length) {
this.isSaving = true this.isSaving = true
@@ -1131,6 +1160,10 @@ export default {
}) })
}, },
save ({ force } = { force: false }) { save ({ force } = { force: false }) {
if (this.onChange) {
this.onChange(this.template)
}
if (!this.autosave && !force) { if (!this.autosave && !force) {
return Promise.resolve({}) return Promise.resolve({})
} }
@@ -34,6 +34,7 @@
v-model="description" v-model="description"
dir="auto" dir="auto"
class="base-textarea !text-base w-full" class="base-textarea !text-base w-full"
:readonly="!editable"
@input="resizeTextarea" @input="resizeTextarea"
/> />
</div> </div>
@@ -49,6 +50,7 @@
id="title_field" id="title_field"
v-model="title" v-model="title"
dir="auto" dir="auto"
:readonly="!editable"
class="base-input !text-base w-full" class="base-input !text-base w-full"
> >
</div> </div>
@@ -73,6 +75,11 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
editable: {
type: Boolean,
required: false,
default: true
},
buildDefaultName: { buildDefaultName: {
type: Function, type: Function,
required: true required: true
+4 -1
View File
@@ -21,7 +21,7 @@
/> />
<Contenteditable <Contenteditable
ref="name" ref="name"
:model-value="field.name || defaultName" :model-value="(defaultField ? (field.title || field.name) : field.name) || defaultName"
:editable="editable && !defaultField" :editable="editable && !defaultField"
:icon-inline="true" :icon-inline="true"
:icon-width="18" :icon-width="18"
@@ -235,6 +235,7 @@
<input <input
v-model="field.required" v-model="field.required"
type="checkbox" type="checkbox"
:disabled="!editable || defaultField"
class="toggle toggle-xs" class="toggle toggle-xs"
@update:model-value="save" @update:model-value="save"
> >
@@ -481,6 +482,7 @@
> >
<FormulaModal <FormulaModal
:field="field" :field="field"
:editable="editable && !defaultField"
:build-default-name="buildDefaultName" :build-default-name="buildDefaultName"
@close="isShowFormulaModal = false" @close="isShowFormulaModal = false"
/> />
@@ -501,6 +503,7 @@
> >
<DescriptionModal <DescriptionModal
:field="field" :field="field"
:editable="editable && !defaultField"
:build-default-name="buildDefaultName" :build-default-name="buildDefaultName"
@close="isShowDescriptionModal = false" @close="isShowDescriptionModal = false"
/> />
+13 -1
View File
@@ -86,9 +86,16 @@
:button-width="20" :button-width="20"
/> />
<span class="block pl-0.5"> <span class="block pl-0.5">
{{ field.name }} {{ field.title || field.name }}
</span> </span>
</div> </div>
<span
v-if="defaultRequiredFields.includes(field)"
:data-tip="t('required')"
class="text-red-400 text-3xl pr-1.5 tooltip tooltip-left h-8"
>
*
</span>
</div> </div>
</div> </div>
</template> </template>
@@ -210,6 +217,11 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
defaultRequiredFields: {
type: Array,
required: false,
default: () => []
},
onlyDefinedFields: { onlyDefinedFields: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -49,6 +49,7 @@
ref="textarea" ref="textarea"
v-model="formula" v-model="formula"
class="base-textarea !rounded-xl !text-base font-mono w-full !outline-0 !ring-0 !px-3" class="base-textarea !rounded-xl !text-base font-mono w-full !outline-0 !ring-0 !px-3"
:readonly="!editable"
required="true" required="true"
@input="resizeTextarea" @input="resizeTextarea"
/> />
@@ -134,6 +135,11 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
editable: {
type: Boolean,
required: false,
default: true
},
buildDefaultName: { buildDefaultName: {
type: Function, type: Function,
required: true required: true
+1
View File
@@ -3,6 +3,7 @@ const en = {
field_not_found: 'Field not found', field_not_found: 'Field not found',
clear: 'Clear', clear: 'Clear',
align: 'Align', align: 'Align',
add_all_required_fields_to_continue: 'Add all required fields to continue',
left: 'Left', left: 'Left',
right: 'Right', right: 'Right',
center: 'Center', center: 'Center',
@@ -38,7 +38,14 @@
:stroke-width="1.6" :stroke-width="1.6"
:width="20" :width="20"
/> />
{{ field.name }} {{ field.title || field.name }}
<span
v-if="defaultRequiredFields.includes(field)"
:data-tip="t('required')"
class="text-red-400 text-2xl tooltip tooltip-left h-6"
>
*
</span>
</a> </a>
</li> </li>
</template> </template>
@@ -99,6 +106,11 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
defaultRequiredFields: {
type: Array,
required: false,
default: () => []
},
defaultFields: { defaultFields: {
type: Array, type: Array,
required: false, required: false,
+1 -1
View File
@@ -219,7 +219,7 @@ export default {
this.newArea.y = e.offsetY / this.$refs.mask.clientHeight this.newArea.y = e.offsetY / this.$refs.mask.clientHeight
} }
if (this.drawField?.type === 'cells') { if ((this.drawField?.type || this.drawFieldType) === 'cells') {
this.newArea.cell_w = this.newArea.h * (this.$refs.mask.clientHeight / this.$refs.mask.clientWidth) this.newArea.cell_w = this.newArea.h * (this.$refs.mask.clientHeight / this.$refs.mask.clientWidth)
} }
+2 -1
View File
@@ -16,7 +16,7 @@ class ProcessSubmitterCompletionJob < ApplicationJob
enqueue_completed_emails(submitter) enqueue_completed_emails(submitter)
end end
return if Accounts.load_webhook_configs(submitter.account).blank? return if Accounts.load_webhook_url(submitter.account).blank?
SendFormCompletedWebhookRequestJob.perform_later(submitter) SendFormCompletedWebhookRequestJob.perform_later(submitter)
end end
@@ -34,6 +34,7 @@ class ProcessSubmitterCompletionJob < ApplicationJob
end end
bcc = submission.preferences['bcc_completed'].presence || bcc = submission.preferences['bcc_completed'].presence ||
submission.template.preferences['bcc_completed'].presence ||
submission.account.account_configs submission.account.account_configs
.find_by(key: AccountConfig::BCC_EMAILS)&.value .find_by(key: AccountConfig::BCC_EMAILS)&.value
@@ -7,16 +7,20 @@ class SendFormCompletedWebhookRequestJob < ApplicationJob
def perform(submitter, params = {}) def perform(submitter, params = {})
attempt = params[:attempt].to_i attempt = params[:attempt].to_i
config = Accounts.load_webhook_configs(submitter.submission.account) url = Accounts.load_webhook_url(submitter.submission.account)
return if config.blank? || config.value.blank? return if url.blank?
preferences = Accounts.load_webhook_preferences(submitter.submission.account)
return if preferences['form.completed'] == false
Submissions::EnsureResultGenerated.call(submitter) Submissions::EnsureResultGenerated.call(submitter)
ActiveStorage::Current.url_options = Docuseal.default_url_options ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin resp = begin
Faraday.post(config.value, Faraday.post(url,
{ {
event_type: 'form.completed', event_type: 'form.completed',
timestamp: Time.current, timestamp: Time.current,
@@ -7,14 +7,18 @@ class SendFormStartedWebhookRequestJob < ApplicationJob
def perform(submitter, params = {}) def perform(submitter, params = {})
attempt = params[:attempt].to_i attempt = params[:attempt].to_i
config = Accounts.load_webhook_configs(submitter.submission.account) url = Accounts.load_webhook_url(submitter.submission.account)
return if config.blank? || config.value.blank? return if url.blank?
preferences = Accounts.load_webhook_preferences(submitter.submission.account)
return if preferences['form.started'] == false
ActiveStorage::Current.url_options = Docuseal.default_url_options ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin resp = begin
Faraday.post(config.value, Faraday.post(url,
{ {
event_type: 'form.started', event_type: 'form.started',
timestamp: Time.current, timestamp: Time.current,
@@ -7,14 +7,18 @@ class SendFormViewedWebhookRequestJob < ApplicationJob
def perform(submitter, params = {}) def perform(submitter, params = {})
attempt = params[:attempt].to_i attempt = params[:attempt].to_i
config = Accounts.load_webhook_configs(submitter.submission.account) url = Accounts.load_webhook_url(submitter.submission.account)
return if config.blank? || config.value.blank? return if url.blank?
preferences = Accounts.load_webhook_preferences(submitter.submission.account)
return if preferences['form.viewed'] == false
ActiveStorage::Current.url_options = Docuseal.default_url_options ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin resp = begin
Faraday.post(config.value, Faraday.post(url,
{ {
event_type: 'form.viewed', event_type: 'form.viewed',
timestamp: Time.current, timestamp: Time.current,
@@ -0,0 +1,40 @@
# frozen_string_literal: true
class SendSubmissionArchivedWebhookRequestJob < ApplicationJob
USER_AGENT = 'DocuSeal.co Webhook'
MAX_ATTEMPTS = 10
def perform(submission, params = {})
attempt = params[:attempt].to_i
url = Accounts.load_webhook_url(submission.account)
return if url.blank?
preferences = Accounts.load_webhook_preferences(submission.account)
return if preferences['submission.archived'].blank?
resp = begin
Faraday.post(url,
{
event_type: 'submission.archived',
timestamp: Time.current,
data: submission.as_json(only: %i[id archived_at])
}.to_json,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
nil
end
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
SendSubmissionArchivedWebhookRequestJob.set(wait: (2**attempt).minutes)
.perform_later(submitter, {
attempt: attempt + 1,
last_status: resp&.status.to_i
})
end
end
end
@@ -4,7 +4,7 @@ class SendSubmitterInvitationEmailJob < ApplicationJob
def perform(params = {}) def perform(params = {})
submitter = Submitter.find(params['submitter_id']) submitter = Submitter.find(params['submitter_id'])
if submitter.submission.source == 'invite' && !Accounts.can_send_emails?(submitter.account) if submitter.submission.source == 'invite' && !Accounts.can_send_emails?(submitter.account, on_events: true)
Rollbar.error("Skip email: #{submitter.id}") if defined?(Rollbar) Rollbar.error("Skip email: #{submitter.id}") if defined?(Rollbar)
return return
+1 -1
View File
@@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class ApplicationMailer < ActionMailer::Base class ApplicationMailer < ActionMailer::Base
default from: 'DocuSeal <hi@docuseal.co>' default from: 'DocuSeal <info@docuseal.co>'
layout 'mailer' layout 'mailer'
register_interceptor ActionMailerConfigsInterceptor register_interceptor ActionMailerConfigsInterceptor
+2 -2
View File
@@ -14,8 +14,8 @@ class SubmitterMailer < ApplicationMailer
@email_message = submitter.account.email_messages.find_by(uuid: submitter.preferences['email_message_uuid']) @email_message = submitter.account.email_messages.find_by(uuid: submitter.preferences['email_message_uuid'])
end end
@body = @email_message&.body.presence @body = @email_message&.body.presence || @submitter.template.preferences['request_email_body'].presence
@subject = @email_message&.subject.presence @subject = @email_message&.subject.presence || @submitter.template.preferences['request_email_subject'].presence
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) @email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY)
+4 -1
View File
@@ -28,11 +28,14 @@ class AccountConfig < ApplicationRecord
FORCE_MFA = 'force_mfa' FORCE_MFA = 'force_mfa'
ALLOW_TYPED_SIGNATURE = 'allow_typed_signature' ALLOW_TYPED_SIGNATURE = 'allow_typed_signature'
ALLOW_TO_RESUBMIT = 'allow_to_resubmit' ALLOW_TO_RESUBMIT = 'allow_to_resubmit'
SUBMITTER_REMAILERS = 'submitter_reminders' SUBMITTER_REMINDERS = 'submitter_reminders'
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button' FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
FORM_COMPLETED_MESSAGE_KEY = 'form_completed_message' FORM_COMPLETED_MESSAGE_KEY = 'form_completed_message'
FORM_WITH_CONFETTI_KEY = 'form_with_confetti' FORM_WITH_CONFETTI_KEY = 'form_with_confetti'
ESIGNING_PREFERENCE_KEY = 'esigning_preference' ESIGNING_PREFERENCE_KEY = 'esigning_preference'
WEBHOOK_PREFERENCES_KEY = 'webhook_preferences'
DOWNLOAD_LINKS_AUTH_KEY = 'download_links_auth'
FORCE_SSO_AUTH_KEY = 'force_sso_auth'
DEFAULT_VALUES = { DEFAULT_VALUES = {
SUBMITTER_INVITATION_EMAIL_KEY => { SUBMITTER_INVITATION_EMAIL_KEY => {
+1
View File
@@ -63,6 +63,7 @@ class Submission < ApplicationRecord
enum :source, { enum :source, {
invite: 'invite', invite: 'invite',
bulk: 'bulk',
api: 'api', api: 'api',
embed: 'embed', embed: 'embed',
link: 'link' link: 'link'
+2
View File
@@ -53,6 +53,8 @@ class Submitter < ApplicationRecord
has_many :document_generation_events, dependent: :destroy has_many :document_generation_events, dependent: :destroy
has_many :submission_events, dependent: :destroy has_many :submission_events, dependent: :destroy
scope :completed, -> { where.not(completed_at: nil) }
def status def status
if completed_at? if completed_at?
'completed' 'completed'
+5
View File
@@ -8,6 +8,7 @@
# archived_at :datetime # archived_at :datetime
# fields :text not null # fields :text not null
# name :string not null # name :string not null
# preferences :text not null
# schema :text not null # schema :text not null
# slug :string not null # slug :string not null
# source :text not null # source :text not null
@@ -41,12 +42,14 @@ class Template < ApplicationRecord
before_validation :maybe_set_default_folder, on: :create before_validation :maybe_set_default_folder, on: :create
attribute :preferences, :string, default: -> { {} }
attribute :fields, :string, default: -> { [] } attribute :fields, :string, default: -> { [] }
attribute :schema, :string, default: -> { [] } attribute :schema, :string, default: -> { [] }
attribute :submitters, :string, default: -> { [{ name: DEFAULT_SUBMITTER_NAME, uuid: SecureRandom.uuid }] } attribute :submitters, :string, default: -> { [{ name: DEFAULT_SUBMITTER_NAME, uuid: SecureRandom.uuid }] }
attribute :slug, :string, default: -> { SecureRandom.base58(14) } attribute :slug, :string, default: -> { SecureRandom.base58(14) }
attribute :source, :string, default: 'native' attribute :source, :string, default: 'native'
serialize :preferences, coder: JSON
serialize :fields, coder: JSON serialize :fields, coder: JSON
serialize :schema, coder: JSON serialize :schema, coder: JSON
serialize :submitters, coder: JSON serialize :submitters, coder: JSON
@@ -62,6 +65,8 @@ class Template < ApplicationRecord
scope :active, -> { where(archived_at: nil) } scope :active, -> { where(archived_at: nil) }
scope :archived, -> { where.not(archived_at: nil) } scope :archived, -> { where.not(archived_at: nil) }
delegate :name, to: :folder, prefix: true
def application_key def application_key
external_id external_id
end end
+1
View File
@@ -65,6 +65,7 @@ class User < ApplicationRecord
attribute :uuid, :string, default: -> { SecureRandom.uuid } attribute :uuid, :string, default: -> { SecureRandom.uuid }
scope :active, -> { where(archived_at: nil) } scope :active, -> { where(archived_at: nil) }
scope :archived, -> { where.not(archived_at: nil) }
scope :admins, -> { where(role: ADMIN_ROLE) } scope :admins, -> { where(role: ADMIN_ROLE) }
def access_token def access_token
+12
View File
@@ -75,6 +75,18 @@
</div> </div>
<% end %> <% end %>
<% end %> <% end %>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY) %>
<% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %>
<div class="flex items-center justify-between py-2.5">
<span>
Require authentication for file download links
</span>
<%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()' %>
</div>
<% end %>
<% end %>
</div> </div>
<% end %> <% end %>
<% if can?(:manage, current_account) && Docuseal.multitenant? && true_user == current_user %> <% if can?(:manage, current_account) && Docuseal.multitenant? && true_user == current_user %>
@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M14 6m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
<path d="M4 6l8 0" />
<path d="M16 6l4 0" />
<path d="M8 12m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
<path d="M4 12l2 0" />
<path d="M10 12l10 0" />
<path d="M17 18m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
<path d="M4 18l11 0" />
<path d="M19 18l1 0" />
</svg>

After

Width:  |  Height:  |  Size: 602 B

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 3a9 9 0 1 0 0 18a9 9 0 0 0 0 -18" />
<path d="M16 12l-4 -4" />
<path d="M16 12h-8" />
<path d="M12 16l4 -4" />
</svg>

After

Width:  |  Height:  |  Size: 415 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" aria-hidden="true" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
</svg>

After

Width:  |  Height:  |  Size: 338 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M6.06004 6.00016C6.21678 5.55461 6.52614 5.1789 6.93334 4.93958C7.34055 4.70027 7.8193 4.61279 8.28483 4.69264C8.75035 4.77249 9.17259 5.01451 9.47676 5.37585C9.78093 5.73718 9.94741 6.19451 9.94671 6.66683C9.94671 8.00016 7.94671 8.66683 7.94671 8.66683M8.00004 11.3335H8.00671M14.6667 8.00016C14.6667 11.6821 11.6819 14.6668 8.00004 14.6668C4.31814 14.6668 1.33337 11.6821 1.33337 8.00016C1.33337 4.31826 4.31814 1.3335 8.00004 1.3335C11.6819 1.3335 14.6667 4.31826 14.6667 8.00016Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 730 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M6 6v6a3 3 0 0 0 3 3h10l-4 -4m0 8l4 -4" />
</svg>

After

Width:  |  Height:  |  Size: 337 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z" />
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" />
</svg>

After

Width:  |  Height:  |  Size: 872 B

+1
View File
@@ -12,6 +12,7 @@
</head> </head>
<body> <body>
<turbo-frame id="modal"></turbo-frame> <turbo-frame id="modal"></turbo-frame>
<turbo-frame id="drawer"></turbo-frame>
<%= render 'shared/navbar' %> <%= render 'shared/navbar' %>
<% if flash.present? %><%= render 'shared/flash' %><% end %> <% if flash.present? %><%= render 'shared/flash' %><% end %>
<div class="max-w-6xl mx-auto px-4 md:px-2 mb-8"> <div class="max-w-6xl mx-auto px-4 md:px-2 mb-8">
@@ -11,7 +11,7 @@
</span> </span>
</span> </span>
<% end %> <% end %>
<%= f.email_field :value, autocomplete: 'off', class: 'base-input' %> <%= tag.input type: 'email', multiple: true, name: 'account_config[value]', autocomplete: 'off', class: 'base-input', value: f.object.value %>
</div> </div>
<div class="form-control pt-2"> <div class="form-control pt-2">
<%= f.button button_title(title: 'Save', disabled_with: 'Updating'), class: 'base-button' %> <%= f.button button_title(title: 'Save', disabled_with: 'Updating'), class: 'base-button' %>
+3 -3
View File
@@ -1,4 +1,4 @@
<% link = pagy_link_proc(@pagy) %> <% link = pagy_anchor(@pagy) %>
<% if @pagy.pages > 1 %> <% if @pagy.pages > 1 %>
<div class="flex my-6 justify-center md:justify-between"> <div class="flex my-6 justify-center md:justify-between">
<div class="hidden md:block text-sm"> <div class="hidden md:block text-sm">
@@ -7,7 +7,7 @@
</div> </div>
<div class="join"> <div class="join">
<% if @pagy.prev %> <% if @pagy.prev %>
<%== link.call(@pagy.prev, '«', 'class="join-item btn min-h-full h-10"') %> <%== link.call(@pagy.prev, '«', classes: 'join-item btn min-h-full h-10') %>
<% else %> <% else %>
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">«</span> <span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">«</span>
<% end %> <% end %>
@@ -15,7 +15,7 @@
Page <%= @pagy.page %> Page <%= @pagy.page %>
</span> </span>
<% if @pagy.next %> <% if @pagy.next %>
<%== link.call(@pagy.next, '»', 'class="join-item btn min-h-full h-10"') %> <%== link.call(@pagy.next, '»', classes: 'join-item btn min-h-full h-10') %>
<% else %> <% else %>
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">»</span> <span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">»</span>
<% end %> <% end %>
+22
View File
@@ -0,0 +1,22 @@
<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 overflow-y-auto">
<% 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>
<%= local_assigns[:title] %>
</span>
<a href="#" class="text-xl" data-action="click:turbo-modal#close">&times;</a>
</div>
<% else %>
<span data-action="click:turbo-modal#close" class="absolute btn border border-base-100 bg-base-100 hover:bg-base-300 hover:border-base-300 btn-primary !text-base btn-sm btn-circle" style="left: -40px; top: 6px">
&times;
</span>
<% end %>
<div class="w-full md:w-[620px] overflow-y-auto" style="max-height: calc(100vh - <%= local_assigns[:title] ? '45px' : '0px' %>)">
<%= yield %>
</div>
</div>
</turbo-modal>
</turbo-frame>
@@ -0,0 +1,18 @@
<turbo-frame id="modal">
<turbo-modal class="modal modal-open items-start !animate-none overflow-y-auto !justify-normal md:!justify-center" data-close-after-submit="<%= local_assigns.key?(:close_after_submit) ? local_assigns[:close_after_submit] : true %>">
<div class="absolute top-0 bottom-0 right-0 left-0" data-action="click:turbo-modal#close"></div>
<div class="bg-base-100 min-h-screen max-h-screen md:min-h-fit md:mt-3 md:rounded-2xl relative w-full relative overflow-y-auto">
<% if local_assigns[:title] %>
<div class="flex justify-between bg-base-100 py-2 px-5 items-center border-b pb-2 font-medium mt-0.5">
<span>
<%= local_assigns[:title] %>
</span>
<a href="#" class="text-xl" data-action="click:turbo-modal#close">&times;</a>
</div>
<% end %>
<div class="w-full md:w-[590px] overflow-y-auto" style="max-height: calc(100vh - 14px - 45px)">
<%= yield %>
</div>
</div>
</turbo-modal>
</turbo-frame>
+3 -1
View File
@@ -48,6 +48,8 @@
<%= render('submitters_order', f:, template:) if Accounts.can_send_emails?(current_account) %> <%= render('submitters_order', f:, template:) if Accounts.can_send_emails?(current_account) %>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= f.button button_title(title: 'Add Recipients'), class: 'base-button' %> <%= content_for(:submit_button) || capture do %>
<%= f.button button_title(title: 'Add Recipients'), class: 'base-button' %>
<% end %>
</div> </div>
<% end %> <% end %>
+6 -2
View File
@@ -35,7 +35,7 @@
<div class="form-control space-y-2"> <div class="form-control space-y-2">
<div class="form-control"> <div class="form-control">
<%= f.label :subject, class: 'label' %> <%= f.label :subject, class: 'label' %>
<%= f.text_field :subject, value: config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %> <%= f.text_field :subject, value: @template.preferences['request_email_subject'].presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<div class="flex items-center"> <div class="flex items-center">
@@ -45,8 +45,12 @@
</span> </span>
</div> </div>
<autoresize-textarea> <autoresize-textarea>
<%= f.text_area :body, value: config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %> <%= f.text_area :body, value: @template.preferences['request_email_body'].presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
</autoresize-textarea> </autoresize-textarea>
<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">Save as default template message</span>
</label>
</div> </div>
<%= render 'message_fields' %> <%= render 'message_fields' %>
</div> </div>
@@ -13,8 +13,8 @@
</div> </div>
<div> <div>
<p dir="auto" class="text-lg font-bold mb-1"><%= @submission.template.name %></p> <p dir="auto" class="text-lg font-bold mb-1"><%= @submission.template.name %></p>
<% if submitter = @submission.submitters.map(&:completed_at).max %> <% if completed_at = @submission.submitters.map(&:completed_at).compact.max %>
<p dir="auto" class="text-sm"><%= t('signed_on_time', time: l(submitter.to_date, format: :long)) %></p> <p dir="auto" class="text-sm"><%= t('signed_on_time', time: l(completed_at.to_date, format: :long)) %></p>
<% end %> <% end %>
</div> </div>
</div> </div>
@@ -1,4 +1,4 @@
<% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %> <% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>
<% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %> <% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %>
<% configs = Submitters::FormConfigs.call(submitter) %> <% configs = Submitters::FormConfigs.call(submitter) %>
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-with-confetti="<%= configs[:with_confetti] %>" data-completed-redirect-url="<%= submitter.preferences['completed_redirect_url'] %>" data-completed-message="<%= configs[:completed_message].to_json %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.opened_at? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(submitter.submission.account) %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>"></submission-form> <submission-form data-is-demo="<%= Docuseal.demo? %>" data-with-confetti="<%= configs[:with_confetti] %>" data-completed-redirect-url="<%= submitter.preferences['completed_redirect_url'] %>" data-completed-message="<%= configs[:completed_message].to_json %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.preferences.key?('go_to_last') ? submitter.preferences['go_to_last'] : submitter.opened_at? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(submitter.submission.account) %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>"></submission-form>
+1
View File
@@ -24,6 +24,7 @@
<% value = values[field['uuid']].presence || (field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submitter.submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %> <% value = values[field['uuid']].presence || (field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submitter.submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
<% next if value.blank? %> <% next if value.blank? %>
<% next if !field['readonly'] && field['submitter_uuid'] == @submitter.uuid %> <% next if !field['readonly'] && field['submitter_uuid'] == @submitter.uuid %>
<% next if field['redacted'] && field['submitter_uuid'] != @submitter.uuid %>
<% next if value == '{{date}}' && field['submitter_uuid'] != @submitter.uuid %> <% next if value == '{{date}}' && field['submitter_uuid'] != @submitter.uuid %>
<% next if field.dig('preferences', 'formula') && field['submitter_uuid'] == @submitter.uuid %> <% next if field.dig('preferences', 'formula') && field['submitter_uuid'] == @submitter.uuid %>
<%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submitter.account.locale, timezone: @submitter.account.timezone %> <%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submitter.account.locale, timezone: @submitter.account.timezone %>
+76 -1
View File
@@ -1,4 +1,4 @@
<toggle-visible data-element-ids="[&quot;js_1&quot;,&quot;react_1&quot;,&quot;vue_1&quot;]" class="block relative" data-catalyst=""> <toggle-visible data-element-ids="[&quot;js_1&quot;,&quot;react_1&quot;,&quot;vue_1&quot;,&quot;angular_1&quot;]" class="block relative">
<ul class="items-center w-full text-sm font-medium text-gray-900 my-4 space-y-2 sm:space-y-0 sm:flex sm:space-x-2"> <ul class="items-center w-full text-sm font-medium text-gray-900 my-4 space-y-2 sm:space-y-0 sm:flex sm:space-x-2">
<li class="w-full h-10 text-sm font-medium flex items-center relative group py-3.5"> <li class="w-full h-10 text-sm font-medium flex items-center relative group py-3.5">
<input type="radio" name="option_1" id="js_radio_1" value="js_1" data-action="change:toggle-visible#trigger" class="relative peer z-10 hidden" checked="checked"> <input type="radio" name="option_1" id="js_radio_1" value="js_1" data-action="change:toggle-visible#trigger" class="relative peer z-10 hidden" checked="checked">
@@ -33,6 +33,23 @@
<span>Vue</span> <span>Vue</span>
</label> </label>
</li> </li>
<li class="w-full h-10 text-sm font-medium flex items-center relative group py-3.5">
<input type="radio" name="option_1" id="angular_radio_1" value="angular_1" data-action="change:toggle-visible#trigger" class="relative peer z-10 hidden">
<label for="angular_radio_1" class="absolute border-neutral-focus space-x-2 border rounded-xl left-0 right-0 top-0 bottom-0 flex items-center justify-center group-hover:bg-neutral group-hover:text-white peer-checked:btn-neutral">
<span><svg class="h-5 w-5" width="800px" height="800px" viewBox="-8 0 272 272" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g>
<path d="M0.0996108949,45.522179 L125.908171,0.697276265 L255.103502,44.7252918 L234.185214,211.175097 L125.908171,271.140856 L19.3245136,211.971984 L0.0996108949,45.522179 Z" fill="#E23237">
</path>
<path d="M255.103502,44.7252918 L125.908171,0.697276265 L125.908171,271.140856 L234.185214,211.274708 L255.103502,44.7252918 L255.103502,44.7252918 Z" fill="#B52E31">
</path>
<path d="M126.107393,32.27393 L126.107393,32.27393 L47.7136187,206.692607 L76.9992218,206.194553 L92.7377432,166.848249 L126.207004,166.848249 L126.306615,166.848249 L163.063035,166.848249 L180.29572,206.692607 L208.286381,207.190661 L126.107393,32.27393 L126.107393,32.27393 Z M126.306615,88.155642 L152.803113,143.5393 L127.402335,143.5393 L126.107393,143.5393 L102.997665,143.5393 L126.306615,88.155642 L126.306615,88.155642 Z" fill="#FFFFFF">
</path>
</g>
</svg>
</span>
<span>Angular</span>
</label>
</li>
</ul> </ul>
</toggle-visible> </toggle-visible>
<div id="js_1" class="block my-4"> <div id="js_1" class="block my-4">
@@ -173,3 +190,61 @@ export default {
</code></pre> </code></pre>
</div> </div>
</div> </div>
<div id="angular_1" class="block my-4 hidden">
<div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex">
<%= link_to 'Learn More', console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<clipboard-copy data-text="import { Component } from '@angular/core';
import { DocusealFormComponent } from '@docuseal/angular';
@Component({
selector: 'app-root',
standalone: true,
imports: [DocusealFormComponent],
template: `
<div class=&quot;app&quot;>
<docuseal-form
[src]=&quot;'<%= start_form_url(slug: template.slug) %>'&quot;>
</docuseal-form>
</div>
`
})
export class AppComponent {}
">
<label class="btn btn-ghost text-white">
<input type="radio" class="peer hidden">
<span class="peer-checked:hidden flex items-center space-x-2">
<%= svg_icon('copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copy
</span>
</span>
<span class="hidden peer-checked:flex items-center space-x-2">
<%= svg_icon('clipboard_copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copied
</span>
</span>
</label>
</clipboard-copy>
</span>
<pre class="before:!m-0 pl-6 pb-4 overflow-auto"><code class="overflow-hidden w-full"><span style="color: #aa759f">import</span> <span style="color: #d0d0d0">{</span> <span style="color: #d0d0d0;background-color: #151515">Component</span> <span style="color: #d0d0d0">}</span> <span style="color: #aa759f">from</span> <span style="color: #90a959">'</span><span style="color: #90a959">@angular/core</span><span style="color: #90a959">'</span><span style="color: #d0d0d0">;</span>
<span style="color: #aa759f">import</span> <span style="color: #d0d0d0">{</span> <span style="color: #d0d0d0;background-color: #151515">DocusealFormComponent</span> <span style="color: #d0d0d0">}</span> <span style="color: #aa759f">from</span> <span style="color: #90a959">'</span><span style="color: #90a959">@docuseal/angular</span><span style="color: #90a959">'</span><span style="color: #d0d0d0">;</span>
<span style="color: #d0d0d0">@</span><span style="color: #d0d0d0;background-color: #151515">Component</span><span style="color: #d0d0d0">({</span>
<span style="color: #6a9fb5">selector</span><span style="color: #d0d0d0">:</span> <span style="color: #90a959">'</span><span style="color: #90a959">app-root</span><span style="color: #90a959">'</span><span style="color: #d0d0d0">,</span>
<span style="color: #6a9fb5">standalone</span><span style="color: #d0d0d0">:</span> <span style="color: #d28445">true</span><span style="color: #d0d0d0">,</span>
<span style="color: #6a9fb5">imports</span><span style="color: #d0d0d0">:</span> <span style="color: #d0d0d0">[</span><span style="color: #d0d0d0;background-color: #151515">DocusealFormComponent</span><span style="color: #d0d0d0">],</span>
<span style="color: #6a9fb5">template</span><span style="color: #d0d0d0">:</span> <span style="color: #90a959">`
&lt;div class="app"&gt;
&lt;docuseal-form
[src]="'<%= start_form_url(slug: template.slug) %>'"&gt;
&lt;/docuseal-form&gt;
&lt;/div&gt;
`</span>
<span style="color: #d0d0d0">})</span>
<span style="color: #aa759f">export</span> <span style="color: #d28445">class</span> <span style="color: #f4bf75">AppComponent</span> <span style="color: #d0d0d0">{}</span>
</code></pre>
</div>
</div>
+4 -4
View File
@@ -61,7 +61,7 @@
</download-button> </download-button>
</button> </button>
</form> </form>
<% else %> <% elsif !submission.archived_at? && !template.archived_at? %>
<% if current_user.email == submitter.email %> <% if current_user.email == submitter.email %>
<form data-turbo="false" method="get" action="<%= submit_form_url(slug: submitter.slug) %>" class="flex-1 md:flex-none"> <form data-turbo="false" method="get" action="<%= submit_form_url(slug: submitter.slug) %>" class="flex-1 md:flex-none">
<button onclick="event.stopPropagation()" class="btn btn-sm btn-neutral btn-outline bg-white w-full md:w-36 flex"> <button onclick="event.stopPropagation()" class="btn btn-sm btn-neutral btn-outline bg-white w-full md:w-36 flex">
@@ -80,7 +80,7 @@
<div class="flex-1 md:flex-none"> <div class="flex-1 md:flex-none">
<span class="btn btn-outline btn-sm w-full md:w-24">View</span> <span class="btn btn-outline btn-sm w-full md:w-24">View</span>
</div> </div>
<% if !submission.archived_at? && can?(:destroy, submission) %> <% if !submission.archived_at? && !template.archived_at? && can?(:destroy, submission) %>
<span data-tip="Archive" class="sm:tooltip tooltip-top"> <span data-tip="Archive" class="sm:tooltip tooltip-top">
<%= button_to button_title(title: nil, disabled_with: 'Arch', icon: svg_icon('archive', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: 'Archive', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %> <%= button_to button_title(title: nil, disabled_with: 'Arch', icon: svg_icon('archive', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: 'Archive', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %>
</span> </span>
@@ -127,7 +127,7 @@
</download-button> </download-button>
</button> </button>
</form> </form>
<% elsif !is_submission_completed %> <% elsif !template.archived_at? && !submission.archived_at? && !is_submission_completed %>
<div class="relative flex items-center space-x-3"> <div class="relative flex items-center space-x-3">
<% if current_user.email == submitter.email %> <% if current_user.email == submitter.email %>
<form data-turbo="false" method="get" action="<%= submit_form_url(slug: submitter.slug) %>"> <form data-turbo="false" method="get" action="<%= submit_form_url(slug: submitter.slug) %>">
@@ -171,7 +171,7 @@
<div class="flex-1 md:flex-none"> <div class="flex-1 md:flex-none">
<span class="btn btn-outline btn-sm w-full md:w-24">View</span> <span class="btn btn-outline btn-sm w-full md:w-24">View</span>
</div> </div>
<% unless submission.archived_at? %> <% if !submission.archived_at? && !template.archived_at? %>
<span data-tip="Archive" class="sm:tooltip tooltip-top"> <span data-tip="Archive" class="sm:tooltip tooltip-top">
<%= button_to button_title(title: nil, disabled_with: 'Arch', icon: svg_icon('archive', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: 'Archive', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %> <%= button_to button_title(title: nil, disabled_with: 'Arch', icon: svg_icon('archive', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: 'Archive', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %>
</span> </span>
+4 -4
View File
@@ -4,7 +4,7 @@
<h1 class="text-4xl font-semibold" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;"> <h1 class="text-4xl font-semibold" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;">
<% template.name.split(/(_)/).each do |item| %><%= item %><wbr><% end %> <% template.name.split(/(_)/).each do |item| %><%= item %><wbr><% end %>
<% if template.archived_at? %> <% if template.archived_at? %>
<span class="badge badge-outline badge-lg align-middle">Archived</span> <span class="ml-1 badge badge-outline badge-lg align-middle">Archived</span>
<% end %> <% end %>
</h1> </h1>
<% if @template.account_id == current_account.id %> <% if @template.account_id == current_account.id %>
@@ -38,10 +38,10 @@
<% if !template.archived_at? %> <% if !template.archived_at? %>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<% if can?(:update, template) && (Docuseal.multitenant? || current_account.testing? || current_account.id == 1) %> <% if can?(:update, template) && (Docuseal.multitenant? || current_account.testing? || current_account.id == 1) %>
<div class="tooltip" data-tip="API and Embedding"> <div class="tooltip" data-tip="Preferences">
<%= link_to template_code_modal_path(template), class: 'btn btn-ghost btn-sm flex-1 hidden md:flex', data: { turbo_frame: :modal } do %> <%= link_to template_preferences_path(template), class: 'btn border border-base-200 bg-base-200 hover:bg-base-300 hover:border-base-300 btn-sm flex-1 hidden md:flex', data: { turbo_frame: :modal } do %>
<span class="flex items-center justify-center space-x-2"> <span class="flex items-center justify-center space-x-2">
<%= svg_icon('code', class: 'w-6 h-6') %> <%= svg_icon('adjustments_horizontal', class: 'w-6 h-6') %>
</span> </span>
<% end %> <% end %>
</div> </div>
+1 -1
View File
@@ -55,7 +55,7 @@
<% end %> <% end %>
<% if @submissions.present? %> <% if @submissions.present? %>
<div class="space-y-4"> <div class="space-y-4">
<%= render partial: 'submission', collection: @submissions %> <%= render partial: 'submission', collection: @submissions, locals: { template: @template } %>
</div> </div>
<% view_archived_html = capture do %> <% view_archived_html = capture do %>
<% if @template.submissions.where.not(archived_at: nil).exists? && !@template.archived_at? %> <% if @template.submissions.where.not(archived_at: nil).exists? && !@template.archived_at? %>
@@ -19,7 +19,7 @@
</div> </div>
<% if @pagy.count > 0 %> <% if @pagy.count > 0 %>
<div class="space-y-4"> <div class="space-y-4">
<%= render partial: 'templates/submission', collection: @submissions %> <%= render partial: 'templates/submission', collection: @submissions, locals: { template: @template } %>
</div> </div>
<% elsif params[:q].present? %> <% elsif params[:q].present? %>
<div class="text-center"> <div class="text-center">
@@ -0,0 +1,100 @@
<%= render 'shared/turbo_modal_large', title: 'Preferences', close_after_submit: false do %>
<% options = [['General', 'general'], ['API and Embedding', 'api']] %>
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center mt-3 block">
<div class="join">
<% options.each_with_index do |(label, value), index| %>
<span>
<%= radio_button_tag 'option', value, value == 'general', class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %>
<label for="option_<%= value %>" class="<%= '!rounded-s-full' if index.zero? %> btn btn-focus btn-sm join-item md:w-48 peer-checked:btn-active normal-case <%= 'px-8 md:px-0' if value == 'general' %>">
<%= label %>
</label>
</span>
<% end %>
</div>
</toggle-visible>
<div id="general" class="px-5 mb-4">
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' } do |f| %>
<toggle-on-submit data-element-id="email_saved_alert"></toggle-on-submit>
<%= f.fields_for :preferences, Struct.new(:request_email_subject, :request_email_body).new(*(@template.preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence || AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY).value.values_at('subject', 'body'))) do |ff| %>
<div class="form-control">
<%= ff.label :request_email_subject, 'Email subject', class: 'label' %>
<%= ff.text_field :request_email_subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :request_email_body, 'Email body', class: 'label' %>
<span class="tooltip tooltip-right" data-tip="Use following placeholders text: <%= AccountConfig::DEFAULT_VALUES.dig(AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY, 'body').scan(/{{.*?}}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :request_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<% end %>
<div class="form-control pt-2">
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button' %>
<div class="flex justify-center">
<span id="email_saved_alert" class="text-sm invisible font-normal mt-0.5">Changes have been saved</span>
</div>
</div>
<% end %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-2' } do |f| %>
<toggle-on-submit data-element-id="bcc_saved_alert"></toggle-on-submit>
<%= f.fields_for :preferences, Struct.new(:bcc_completed).new(@template.preferences['bcc_completed']) do |ff| %>
<div class="form-control">
<%= ff.label :bcc_completed, class: 'label' do %>
<span class="flex items-center space-x-1 justify-between w-full">
<span>
Completed documents BCC address
</span>
</span>
<% end %>
<%= tag.input type: 'email', multiple: true, name: 'template[preferences][bcc_completed]', autocomplete: 'off', class: 'base-input', value: ff.object.bcc_completed %>
</div>
<% end %>
<div class="form-control pt-3">
<%= f.button button_title(title: 'Save', disabled_with: 'Updating'), class: 'base-button' %>
<div class="flex justify-center">
<span id="bcc_saved_alert" class="text-sm invisible font-normal mt-0.5">Changes have been saved</span>
</div>
</div>
<% end %>
<%= render 'templates_code_modal/preferences' %>
</div>
<div id="api" class="hidden mt-2 mb-4 px-5">
<div>
<label class="text-sm font-semibold" for="template_id">
Template ID
</label>
<div class="flex gap-2 mb-4 mt-2">
<input id="template_id" type="text" value="<%= @template.id %>" class="base-input w-full" autocomplete="off" readonly>
<%= render 'shared/clipboard_copy', icon: 'copy', text: @template.id, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
</div>
</div>
<div>
<label class="text-sm font-semibold" for="embedding_url">
Embedding URL
</label>
<div class="flex gap-2 mb-4 mt-2">
<input id="embedding_url" type="text" value="<%= start_form_url(slug: @template.slug) %>" class="base-input w-full" autocomplete="off" readonly>
<%= render 'shared/clipboard_copy', icon: 'copy', text: start_form_url(slug: @template.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
</div>
</div>
<%= render 'templates_code_modal/placeholder' %>
<%= render 'templates/embedding', template: @template %>
<% if can?(:manage, TemplateSharing.new(template: @template)) %>
<%= form_for '', url: template_sharings_testing_index_path, method: :post, html: { class: 'mt-1' } do |f| %>
<%= f.hidden_field :template_id, value: @template.id %>
<div class="flex items-center justify-between">
<span>
Share template with Test Environment
</span>
<%= f.check_box :value, class: 'toggle', checked: @template.template_sharings.exists?(account_id: current_account.testing_accounts), onchange: 'this.form.requestSubmit()' %>
</div>
<% end %>
<div class="mb-4">
</div>
<% end %>
</div>
<% end %>
+25 -3
View File
@@ -2,7 +2,7 @@
<%= render 'shared/settings_nav' %> <%= render 'shared/settings_nav' %>
<div class="md:flex-grow"> <div class="md:flex-grow">
<div class="flex justify-between mb-4"> <div class="flex justify-between mb-4">
<h1 class="text-4xl font-bold">Users</h1> <h1 class="text-4xl font-bold"><%= params[:status].to_s.titleize %> Users</h1>
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<% if can?(:create, User.new(account: current_account)) %> <% if can?(:create, User.new(account: current_account)) %>
<%= link_to new_user_path, class: 'btn btn-primary btn-md gap-2', data: { turbo_frame: 'modal' } do %> <%= link_to new_user_path, class: 'btn btn-primary btn-md gap-2', data: { turbo_frame: 'modal' } do %>
@@ -55,17 +55,39 @@
Edit Edit
<% end %> <% end %>
<% end %> <% end %>
<% if can?(:destroy, user) && user != current_user %> <% if params[:status].blank? && can?(:destroy, user) && user != current_user %>
<%= button_to user_path(user), method: :delete, class: 'btn btn-outline btn-error btn-xs', title: 'Delete', data: { turbo_confirm: 'Are you sure?' } do %> <%= button_to user_path(user), method: :delete, class: 'btn btn-outline btn-error btn-xs', title: 'Delete', data: { turbo_confirm: 'Are you sure?' } do %>
Remove Remove
<% end %> <% end %>
<% end %> <% end %>
<% if params[:status].present? && can?(:manage, user) && user != current_user %>
<%= button_to user_path(user), method: :put, params: { user: { archived_at: nil } }, class: 'btn btn-outline btn-secondary btn-xs', title: 'Unarchive', data: { turbo_confirm: 'Are you sure?' } do %>
Unarchive
<% end %>
<% end %>
</td> </td>
</tr> </tr>
<% end %> <% end %>
</tbody> </tbody>
</table> </table>
</div> </div>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'users' %> <% view_archived_html = capture do %>
<% if current_account.users.archived.exists? %>
<div>
<% if params[:status] == 'archived' %>
<a href="<%= settings_users_path %>" class="link text-sm">View Active</a>
<% else %>
<a href="<%= settings_archived_users_path %>" class="link text-sm">View Archived</a>
<% end %>
</div>
<% end %>
<% end %>
<% if @pagy.pages > 1 %>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'users', left_additional_html: view_archived_html %>
<% else %>
<div class="mt-2">
<%= view_archived_html %>
</div>
<% end %>
</div> </div>
</div> </div>
+17
View File
@@ -14,6 +14,23 @@
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button w-full md:w-32' %> <%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button w-full md:w-32' %>
</div> </div>
<% end %> <% end %>
<% preference = current_account.account_configs.find_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)&.value || {} %>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 mt-2 gap-y-2">
<% WebhookPreferencesController::EVENTS.each do |event| %>
<%= form_for '', url: webhook_preferences_path, method: :post do |f| %>
<%= f.hidden_field :event, value: event %>
<% uuid = SecureRandom.uuid %>
<div class="flex">
<label for="<%= uuid %>" class="flex items-center space-x-2">
<%= f.check_box :value, class: 'base-checkbox', checked: preference[event] || (!preference.key?(event) && event.starts_with?('form.')), onchange: 'this.form.requestSubmit()', id: uuid %>
<span>
<%= event %>
</span>
</label>
</div>
<% end %>
<% end %>
</div>
</div> </div>
</div> </div>
<% submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last %> <% submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last %>
+1 -1
View File
@@ -30,7 +30,7 @@ Rails.application.configure do
config.cache_store = :null_store config.cache_store = :null_store
# Raise exceptions instead of rendering exception templates. # Raise exceptions instead of rendering exception templates.
config.action_dispatch.show_exceptions = false config.action_dispatch.show_exceptions = :none
# Disable request forgery protection in test environment. # Disable request forgery protection in test environment.
config.action_controller.allow_forgery_protection = false config.action_controller.allow_forgery_protection = false
+9
View File
@@ -2,6 +2,14 @@
Devise.otp_allowed_drift = 60.seconds Devise.otp_allowed_drift = 60.seconds
class FailureApp < Devise::FailureApp
def respond
Rollbar.warning('Invalid password') if defined?(Rollbar) && warden_message == :invalid
super
end
end
# Assuming you have not yet modified this file, each configuration option below # Assuming you have not yet modified this file, each configuration option below
# is set to its default value. Note that some are commented out while others # is set to its default value. Note that some are commented out while others
# are not: uncommented lines are intended to protect your configuration from # are not: uncommented lines are intended to protect your configuration from
@@ -277,6 +285,7 @@ Devise.setup do |config|
config.warden do |manager| config.warden do |manager|
# manager.intercept_401 = false # manager.intercept_401 = false
# manager.default_strategies(scope: :user).unshift(:auth_token) # manager.default_strategies(scope: :user).unshift(:auth_token)
manager.failure_app = FailureApp
end end
# ==> Mountable engine configurations # ==> Mountable engine configurations
+6
View File
@@ -59,6 +59,7 @@ Rails.application.routes.draw do
resources :testing_api_settings, only: %i[index] resources :testing_api_settings, only: %i[index]
resources :submitters_autocomplete, only: %i[index] resources :submitters_autocomplete, only: %i[index]
resources :template_folders_autocomplete, only: %i[index] resources :template_folders_autocomplete, only: %i[index]
resources :webhook_preferences, only: %i[create]
resource :templates_upload, only: %i[create] resource :templates_upload, only: %i[create]
authenticated do authenticated do
resource :templates_upload, only: %i[show], path: 'new' resource :templates_upload, only: %i[show], path: 'new'
@@ -75,9 +76,12 @@ Rails.application.routes.draw do
resource :folder, only: %i[edit update], controller: 'templates_folders' resource :folder, only: %i[edit update], controller: 'templates_folders'
resource :preview, only: %i[show], controller: 'templates_preview' resource :preview, only: %i[show], controller: 'templates_preview'
resource :code_modal, only: %i[show], controller: 'templates_code_modal' resource :code_modal, only: %i[show], controller: 'templates_code_modal'
resource :preferences, only: %i[show create], controller: 'templates_preferences'
resources :submissions_export, only: %i[index new] resources :submissions_export, only: %i[index new]
end end
resources :preview_document_page, only: %i[show], path: '/preview/:signed_uuid' resources :preview_document_page, only: %i[show], path: '/preview/:signed_uuid'
resource :blobs_proxy, only: %i[show], path: '/file/:signed_uuid/*filename',
controller: 'api/active_storage_blobs_proxy'
resource :blobs_proxy, only: %i[show], path: '/blobs_proxy/:signed_uuid/*filename', resource :blobs_proxy, only: %i[show], path: '/blobs_proxy/:signed_uuid/*filename',
controller: 'api/active_storage_blobs_proxy' controller: 'api/active_storage_blobs_proxy'
@@ -127,6 +131,8 @@ Rails.application.routes.draw do
resources :notifications, only: %i[index create], controller: 'notifications_settings' resources :notifications, only: %i[index create], controller: 'notifications_settings'
resource :esign, only: %i[show create new update destroy], controller: 'esign_settings' resource :esign, only: %i[show create new update destroy], controller: 'esign_settings'
resources :users, only: %i[index] resources :users, only: %i[index]
resources :archived_users, only: %i[index], path: 'users/:status', controller: 'users',
defaults: { status: :archived }
resource :personalization, only: %i[show create], controller: 'personalization_settings' resource :personalization, only: %i[show create], controller: 'personalization_settings'
resources :api, only: %i[index create], controller: 'api_settings' resources :api, only: %i[index create], controller: 'api_settings'
resource :webhooks, only: %i[show create update], controller: 'webhook_settings' resource :webhooks, only: %i[show create update], controller: 'webhook_settings'
+1 -1
View File
@@ -19,7 +19,7 @@ development:
compiler_strategy: mtime compiler_strategy: mtime
dev_server: dev_server:
https: false server: 'http'
host: localhost host: localhost
port: 3035 port: 3035
hmr: false hmr: false
@@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddPreferencesToTemplates < ActiveRecord::Migration[7.1]
class MigrationTemplate < ApplicationRecord
self.table_name = 'templates'
end
def change
add_column :templates, :preferences, :text
MigrationTemplate.where(preferences: nil).update_all(preferences: '{}')
change_column_null :templates, :preferences, false
end
end
+2 -1
View File
@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.1].define(version: 2024_04_05_165329) do ActiveRecord::Schema[7.1].define(version: 2024_04_16_170023) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@@ -222,6 +222,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_04_05_165329) do
t.text "source", null: false t.text "source", null: false
t.bigint "folder_id", null: false t.bigint "folder_id", null: false
t.string "external_id" t.string "external_id"
t.text "preferences", null: false
t.index ["account_id"], name: "index_templates_on_account_id" t.index ["account_id"], name: "index_templates_on_account_id"
t.index ["author_id"], name: "index_templates_on_author_id" t.index ["author_id"], name: "index_templates_on_author_id"
t.index ["folder_id"], name: "index_templates_on_folder_id" t.index ["folder_id"], name: "index_templates_on_folder_id"
+13 -3
View File
@@ -67,14 +67,24 @@ module Accounts
new_template new_template
end end
def load_webhook_configs(account) def load_webhook_url(account)
configs = account.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY) configs = account.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
unless Docuseal.multitenant? unless Docuseal.multitenant?
configs ||= Account.order(:id).first.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY) configs ||= Account.order(:id).first.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
end end
configs configs&.value.presence
end
def load_webhook_preferences(account)
configs = account.account_configs.find_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)
unless Docuseal.multitenant?
configs ||= Account.order(:id).first.account_configs.find_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)
end
configs&.value.presence || {}
end end
def load_signing_pkcs(account) def load_signing_pkcs(account)
@@ -108,7 +118,7 @@ module Accounts
end.presence end.presence
end end
def can_send_emails?(_account) def can_send_emails?(_account, **_params)
return true if Docuseal.multitenant? return true if Docuseal.multitenant?
return true if ENV['SMTP_ADDRESS'].present? return true if ENV['SMTP_ADDRESS'].present?
+1
View File
@@ -19,6 +19,7 @@ module LoadActiveStorageConfigs
return if Docuseal.multitenant? return if Docuseal.multitenant?
return if IS_ENV_CONFIGURED return if IS_ENV_CONFIGURED
return if Rails.env.test? return if Rails.env.test?
return if Rails.env.development?
encrypted_config = EncryptedConfig.find_by(key: EncryptedConfig::FILES_STORAGE_KEY) encrypted_config = EncryptedConfig.find_by(key: EncryptedConfig::FILES_STORAGE_KEY)
+1 -1
View File
@@ -87,7 +87,7 @@ module Params
type(submitter_params, :fields, Array) type(submitter_params, :fields, Array)
in_path_each(submitter_params, :fields) do |field_params| in_path_each(submitter_params, :fields) do |field_params|
required(field_params, :name) required(field_params, %i[name uuid])
type(field_params, :name, String) type(field_params, :name, String)
type(field_params, :validation_pattern, String) type(field_params, :validation_pattern, String)
+3 -2
View File
@@ -26,12 +26,13 @@ module SubmissionEvents
).first(TRACKING_PARAM_LENGTH) ).first(TRACKING_PARAM_LENGTH)
end end
def create_with_tracking_data(submitter, event_type, request) def create_with_tracking_data(submitter, event_type, request, data = {})
SubmissionEvent.create!(submitter:, event_type:, data: { SubmissionEvent.create!(submitter:, event_type:, data: {
ip: request.remote_ip, ip: request.remote_ip,
ua: request.user_agent, ua: request.user_agent,
sid: request.session.id.to_s, sid: request.session.id.to_s,
uid: request.env['warden'].user(:user)&.id uid: request.env['warden'].user(:user)&.id,
**data
}.compact_blank) }.compact_blank)
end end
end end
+9 -1
View File
@@ -106,7 +106,15 @@ module Submissions
return if email.blank? return if email.blank?
return email.downcase if email.to_s.include?(',') return email.downcase if email.to_s.include?(',')
return email.downcase if email.to_s.include?('.gob') return email.downcase if email.to_s.include?('.gob')
return email.downcase if email.to_s.include?('.om')
return email.downcase unless email.to_s.include?('.')
EmailTypo.call(email.delete_prefix('<')) fixed_email = EmailTypo.call(email.delete_prefix('<'))
if defined?(Rollbar) && fixed_email != email.downcase.delete_prefix('<').strip
Rollbar.warning("Fixed email #{email}")
end
fixed_email
end end
end end
+6 -1
View File
@@ -102,7 +102,11 @@ module Submissions
next if f['submitter_uuid'] != submitter_uuid next if f['submitter_uuid'] != submitter_uuid
field_configs = fields.find do |e| field_configs = fields.find do |e|
e['name'].to_s.casecmp(f['name'].to_s).zero? || e['name'] == f['name'].to_s.parameterize.underscore if e['name'].present?
e['name'].to_s.casecmp(f['name'].to_s).zero? || e['name'] == f['name'].to_s.parameterize.underscore
else
e['uuid'] == f['uuid']
end
end end
next if field_configs.blank? next if field_configs.blank?
@@ -115,6 +119,7 @@ module Submissions
field['title'] = attrs['title'] if attrs['title'].present? field['title'] = attrs['title'] if attrs['title'].present?
field['description'] = attrs['description'] if attrs['description'].present? field['description'] = attrs['description'] if attrs['description'].present?
field['readonly'] = attrs['readonly'] if attrs.key?('readonly') field['readonly'] = attrs['readonly'] if attrs.key?('readonly')
field['redacted'] = attrs['redacted'] if attrs.key?('redacted')
field['required'] = attrs['required'] if attrs.key?('required') field['required'] = attrs['required'] if attrs.key?('required')
if attrs.key?('default_value') && !field['type'].in?(%w[signature image initials file]) if attrs.key?('default_value') && !field['type'].in?(%w[signature image initials file])
+1 -1
View File
@@ -279,7 +279,7 @@ module Submissions
font: [FONT_BOLD_NAME, { variant: :bold }] }, font: [FONT_BOLD_NAME, { variant: :bold }] },
event.event_type.include?('send_') ? ' to ' : ' by ', event.event_type.include?('send_') ? ' to ' : ' by ',
if event.event_type.include?('sms') || event.event_type.include?('phone') if event.event_type.include?('sms') || event.event_type.include?('phone')
submitter.phone event.data['phone'] || submitter.phone
else else
submitter.name || submitter.email || submitter.phone submitter.name || submitter.email || submitter.phone
end end
+1 -1
View File
@@ -22,7 +22,7 @@ module Submissions
default_values = submitter_params[:values] || {} default_values = submitter_params[:values] || {}
submitter_params[:fields]&.each do |f| submitter_params[:fields]&.each do |f|
default_values[f[:name]] = f[:default_value] if f.key?(:default_value) default_values[f[:name].presence || f[:uuid]] = f[:default_value] if f.key?(:default_value)
end end
return submitter_params if default_values.blank? return submitter_params if default_values.blank?
+1
View File
@@ -62,6 +62,7 @@ module Submitters
preferences['send_sms'] = params['send_sms'].in?(TRUE_VALUES) if params.key?('send_sms') preferences['send_sms'] = params['send_sms'].in?(TRUE_VALUES) if params.key?('send_sms')
preferences['bcc_completed'] = params['bcc_completed'] if params.key?('bcc_completed') preferences['bcc_completed'] = params['bcc_completed'] if params.key?('bcc_completed')
preferences['reply_to'] = params['reply_to'] if params.key?('reply_to') preferences['reply_to'] = params['reply_to'] if params.key?('reply_to')
preferences['go_to_last'] = params['go_to_last'] if params.key?('go_to_last')
preferences['completed_redirect_url'] = params['completed_redirect_url'] if params.key?('completed_redirect_url') preferences['completed_redirect_url'] = params['completed_redirect_url'] if params.key?('completed_redirect_url')
preferences preferences
+2 -2
View File
@@ -118,12 +118,12 @@ module Submitters
blob = blob =
if value.match?(%r{\Ahttps?://}) if value.match?(%r{\Ahttps?://})
find_or_create_blob_from_url(account, value) find_or_create_blob_from_url(account, value)
elsif type.in?(%w[signature initials]) && value.length < 50 elsif type.in?(%w[signature initials]) && value.length < 60
find_or_create_blob_from_text(account, value, type) find_or_create_blob_from_text(account, value, type)
elsif (data = Base64.decode64(value)) && Marcel::MimeType.for(data).include?('image') elsif (data = Base64.decode64(value)) && Marcel::MimeType.for(data).include?('image')
find_or_create_blob_from_base64(account, data, type) find_or_create_blob_from_base64(account, data, type)
else else
raise InvalidDefaultValue, "Invalid value, url, base64 or text < 50 chars is expected: #{value.first(50)}..." raise InvalidDefaultValue, "Invalid value, url, base64 or text < 60 chars is expected: #{value.first(200)}..."
end end
attachment = for_submitter.attachments.find_by(blob_id: blob.id) if for_submitter attachment = for_submitter.attachments.find_by(blob_id: blob.id) if for_submitter
+1
View File
@@ -11,6 +11,7 @@ module Templates
template.external_id = external_id template.external_id = external_id
template.author = author template.author = author
template.preferences = original_template.preferences.deep_dup
template.name = name || "#{original_template.name} (Clone)" template.name = name || "#{original_template.name} (Clone)"
template.assign_attributes(original_template.slice(:folder_id, :schema)) template.assign_attributes(original_template.slice(:folder_id, :schema))
+1 -3
View File
@@ -10,15 +10,13 @@ module Templates
module_function module_function
def call(template, params) def call(template, params)
res = Array.wrap(params[:files].presence || params[:file]).map do |file| Array.wrap(params[:files].presence || params[:file]).map do |file|
if file.content_type.exclude?('image') && file.content_type != PDF_CONTENT_TYPE if file.content_type.exclude?('image') && file.content_type != PDF_CONTENT_TYPE
next handle_file_types(template, file, params) next handle_file_types(template, file, params)
end end
handle_pdf_or_image(template, file, file.read, params) handle_pdf_or_image(template, file, file.read, params)
end end
Rails.logger.debug res
res
end end
def handle_pdf_or_image(template, file, document_data = nil, params = {}) def handle_pdf_or_image(template, file, document_data = nil, params = {})
+2 -1
View File
@@ -45,7 +45,8 @@ module Templates
ActiveStorage::Attachment.where(name: ATTACHMENT_NAME, record: attachment).destroy_all ActiveStorage::Attachment.where(name: ATTACHMENT_NAME, record: attachment).destroy_all
number_of_pages = HexaPDF::Document.new(io: StringIO.new(data)).pages.size number_of_pages = HexaPDF::Document.new(io: StringIO.new(data)).pages.size
(attachment.metadata['pdf'] ||= {})[:number_of_pages] = number_of_pages attachment.metadata['pdf'] ||= {}
attachment.metadata['pdf']['number_of_pages'] = number_of_pages
attachment.save! attachment.save!
+1 -3
View File
@@ -6,12 +6,10 @@ module Templates
def call(template, schema_documents = template.schema_documents, preview_image_attachments = []) def call(template, schema_documents = template.schema_documents, preview_image_attachments = [])
json = template.as_json( json = template.as_json(
methods: %i[application_key], methods: %i[application_key folder_name],
include: { author: { only: %i[id email first_name last_name] } } include: { author: { only: %i[id email first_name last_name] } }
) )
json[:folder_name] = template.folder.name
json[:documents] = template.schema.map do |item| json[:documents] = template.schema.map do |item|
attachment = schema_documents.find { |e| e.uuid == item['attachment_uuid'] } attachment = schema_documents.find { |e| e.uuid == item['attachment_uuid'] }
+1 -1
View File
@@ -27,7 +27,7 @@
"postcss-loader": "^7.3.0", "postcss-loader": "^7.3.0",
"sass": "^1.62.1", "sass": "^1.62.1",
"sass-loader": "^13.2.2", "sass-loader": "^13.2.2",
"shakapacker": "7.1.0", "shakapacker": "7.2.3",
"signature_pad": "^4.1.5", "signature_pad": "^4.1.5",
"snarkdown": "^2.0.0", "snarkdown": "^2.0.0",
"tailwindcss": "^3.3.2", "tailwindcss": "^3.3.2",
+1 -1
View File
@@ -34,7 +34,7 @@ RSpec.describe 'Dashboard Page' do
end end
expect(page).to have_content('Templates') expect(page).to have_content('Templates')
expect(page).not_to have_content(other_template.name) expect(page).to have_no_content(other_template.name)
expect(page).to have_link('Create', href: new_template_path) expect(page).to have_link('Create', href: new_template_path)
end end
+2 -2
View File
@@ -23,7 +23,7 @@ RSpec.describe 'Team Settings' do
users.each do |user| users.each do |user|
expect(page).to have_content(user.full_name) expect(page).to have_content(user.full_name)
expect(page).to have_content(user.email) expect(page).to have_content(user.email)
expect(page).not_to have_content(other_user.email) expect(page).to have_no_content(other_user.email)
end end
end end
end end
@@ -86,7 +86,7 @@ RSpec.describe 'Team Settings' do
end end
it 'does not allow to remove the current user' do it 'does not allow to remove the current user' do
expect(page).not_to have_content('User has been removed') expect(page).to have_no_content('User has been removed')
end end
end end
end end
+4 -4
View File
@@ -4882,10 +4882,10 @@ setprototypeof@1.2.0:
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
shakapacker@7.1.0: shakapacker@7.2.3:
version "7.1.0" version "7.2.3"
resolved "https://registry.yarnpkg.com/shakapacker/-/shakapacker-7.1.0.tgz#95bd37db60702ffa21f9ea29a88f438de3db25b2" resolved "https://registry.yarnpkg.com/shakapacker/-/shakapacker-7.2.3.tgz#12613e8ca3139e47d42003f8d01846dea28f6c40"
integrity sha512-xKfF4LKrFQdMLYeIi/uBV6pfkPTO4lgCAIMx3W5+MHW61ENOXu4WeQ50qVR9u5hV6XXzi7AiS7C6dWO2GFnYAg== integrity sha512-+qF74Y6+hEARqtsmJmBtdc2L0q6eXO21PlPsvtw6aqoxtqhM2VpCCDt51/5Cqnj/5IyEOEICZqStRCSO/q38hg==
dependencies: dependencies:
glob "^7.2.0" glob "^7.2.0"
js-yaml "^4.1.0" js-yaml "^4.1.0"