Compare commits

...
54 Commits
Author SHA1 Message Date
Pete Matsyburka 1b2d978449 download multiple files on safari ios 2024-01-01 20:44:52 +02:00
Pete Matsyburka 30582b2d75 fix phone/payment fields visibility 2024-01-01 16:32:35 +02:00
Pete Matsyburka 38a8037a4d fix draw area message 2024-01-01 13:18:09 +02:00
Pete Matsyburka 67de45f787 optimize fields reorder 2024-01-01 13:07:25 +02:00
Pete Matsyburka f358f24abb optimize form render with many fields 2023-12-31 17:23:27 +02:00
Pete Matsyburka fd4e15003a adjust create button style 2023-12-31 00:17:18 +02:00
Pete Matsyburka 81df249d5c refactor user roles validate 2023-12-30 22:51:26 +02:00
Pete Matsyburka ba60997992 fix signature update 2023-12-30 15:12:00 +02:00
Pete Matsyburka 8961294a29 do not use puma pidfile 2023-12-30 13:34:22 +02:00
Pete Matsyburka bc2e0a000a adjust phone tooltip position 2023-12-30 11:46:04 +02:00
Pete Matsyburka 1a693fba2d remove payment field type from selfhosted 2023-12-30 11:42:46 +02:00
Pete Matsyburka 05b7acf6aa fix AR encryption 2023-12-30 01:44:05 +02:00
Pete Matsyburka de13d8d397 refactor pdf info creator 2023-12-30 01:19:46 +02:00
Pete Matsyburka 6d7d8ae87c rename deleted_at to archived_at 2023-12-30 00:37:03 +02:00
Pete Matsyburka cfb1bdcd52 back to ruby 3.2.2 2023-12-29 21:52:23 +02:00
Pete Matsyburka 48d41833f1 use Rails.application.secret_key_base 2023-12-29 21:08:10 +02:00
Alex Turchyn 01535f1afc add health-check endpoint 2023-12-29 20:51:19 +02:00
Alex Turchyn fe845bfa36 rails v7.1.2 and ruby to v3.3.0 2023-12-29 20:50:36 +02:00
Pete Matsyburka b94dbc75b7 preserve default values on resubmit 2023-12-29 19:09:43 +02:00
Pete Matsyburka bdf40640d0 add payment learn more button 2023-12-29 01:55:51 +02:00
Pete Matsyburka 496f6c706b render cell only if width specified 2023-12-29 00:05:55 +02:00
Pete Matsyburka 79f97d5555 fix update submitter with API completion 2023-12-28 21:18:23 +02:00
Pete Matsyburka 82f679c84c add error message when template not found 2023-12-28 17:39:09 +02:00
Pete Matsyburka 9b789889fa add stamp translations 2023-12-28 17:38:21 +02:00
Pete Matsyburka dd8dd118da add button ids 2023-12-28 16:59:50 +02:00
Pete Matsyburka 8222a4b6a4 fix template show page 2023-12-28 01:13:47 +02:00
Pete Matsyburka 31f8057bfa fix api pagination 2023-12-27 17:17:35 +02:00
Pete Matsyburka 1d8e0417bd adjust default field area size 2023-12-27 16:44:20 +02:00
Pete Matsyburka 8100ca46c2 show builder default fields on mobile dropdown 2023-12-27 16:36:00 +02:00
Pete Matsyburka 94d745cf1c rescue json parse error 2023-12-27 11:28:41 +02:00
Pete Matsyburka a92bd0ad0f error message on undefined submitter role 2023-12-27 11:05:07 +02:00
Pete Matsyburka 94a13ffca2 do not use devise strategy for api auth 2023-12-26 01:56:00 +02:00
Pete Matsyburka 64c8707957 fix multiple signatures config 2023-12-25 13:16:59 +02:00
Pete Matsyburka d421a73988 fix default submitters order 2023-12-25 13:03:41 +02:00
Pete Matsyburka f7eef1f8d6 clone only active template schema docs 2023-12-24 20:44:01 +02:00
Pete Matsyburka 08c780d309 add status and documents to submission get api 2023-12-23 22:24:57 +02:00
Alex Turchyn baaf1f510f generate unique uuids for cloned templates 2023-12-23 21:30:46 +02:00
Pete Matsyburka 8898dcc054 fix archive submission api 2023-12-23 17:47:58 +02:00
Pete Matsyburka 33521c8aeb draw default area size on click 2023-12-23 12:08:46 +02:00
Pete Matsyburka 9db03bd7b9 fix sso page header 2023-12-23 12:08:46 +02:00
Pete Matsyburka bd600c1fba adjust builder header 2023-12-23 12:08:46 +02:00
Pete Matsyburka 82b18b08f1 set template name from first document 2023-12-23 12:08:46 +02:00
Pete Matsyburka 0aefe616b3 add archived add to submissions 2023-12-23 12:08:45 +02:00
Pete Matsyburka 59a66bbd16 draw stamp when added 2023-12-22 16:39:43 +02:00
Pete Matsyburka 155c708e4f update errors controller 2023-12-22 16:36:15 +02:00
Pete Matsyburka 5502c2ab1b fix phone tooltip placement 2023-12-22 16:32:48 +02:00
Pete Matsyburka e89b42de40 add stamp field type 2023-12-22 13:53:51 +02:00
Pete Matsyburka 2562c4f432 add option to toggle single digital sign 2023-12-21 23:29:06 +02:00
Pete Matsyburka 17323074ca fix images document sign 2023-12-21 23:13:32 +02:00
Pete Matsyburka 8a4c5c5e09 remove users utils module 2023-12-21 17:54:39 +02:00
Pete Matsyburka 3033faff0d fix time format 2023-12-21 10:02:57 +02:00
Pete Matsyburka 0e6aab0e8d add builder section props 2023-12-20 22:13:19 +02:00
Pete Matsyburka 75e3dd839a fix document name editable prop 2023-12-20 21:58:47 +02:00
Pete Matsyburka 830e6eb88d hardcode signature size when using tsa 2023-12-20 21:25:27 +02:00
90 changed files with 1271 additions and 518 deletions

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
FROM ruby:3.2.2-alpine as webpack FROM ruby:3.2.2-alpine3.18 as webpack
ENV RAILS_ENV=production ENV RAILS_ENV=production
ENV NODE_ENV=production ENV NODE_ENV=production
+1
View File
@@ -26,6 +26,7 @@ gem 'pdf-reader'
gem 'pg', require: false gem 'pg', require: false
gem 'premailer-rails' gem 'premailer-rails'
gem 'puma' gem 'puma'
gem 'rack'
gem 'rails' gem 'rails'
gem 'rails_autolink' gem 'rails_autolink'
gem 'rails-i18n' gem 'rails-i18n'
+186 -160
View File
@@ -2,93 +2,102 @@ GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
Ascii85 (1.1.0) Ascii85 (1.1.0)
actioncable (7.0.7) actioncable (7.1.2)
actionpack (= 7.0.7) actionpack (= 7.1.2)
activesupport (= 7.0.7) activesupport (= 7.1.2)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
actionmailbox (7.0.7) zeitwerk (~> 2.6)
actionpack (= 7.0.7) actionmailbox (7.1.2)
activejob (= 7.0.7) actionpack (= 7.1.2)
activerecord (= 7.0.7) activejob (= 7.1.2)
activestorage (= 7.0.7) activerecord (= 7.1.2)
activesupport (= 7.0.7) activestorage (= 7.1.2)
activesupport (= 7.1.2)
mail (>= 2.7.1) mail (>= 2.7.1)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
actionmailer (7.0.7) actionmailer (7.1.2)
actionpack (= 7.0.7) actionpack (= 7.1.2)
actionview (= 7.0.7) actionview (= 7.1.2)
activejob (= 7.0.7) activejob (= 7.1.2)
activesupport (= 7.0.7) activesupport (= 7.1.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.0) rails-dom-testing (~> 2.2)
actionpack (7.0.7) actionpack (7.1.2)
actionview (= 7.0.7) actionview (= 7.1.2)
activesupport (= 7.0.7) activesupport (= 7.1.2)
rack (~> 2.0, >= 2.2.4) nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4)
rack-session (>= 1.0.1)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.0, >= 1.2.0) rails-html-sanitizer (~> 1.6)
actiontext (7.0.7) actiontext (7.1.2)
actionpack (= 7.0.7) actionpack (= 7.1.2)
activerecord (= 7.0.7) activerecord (= 7.1.2)
activestorage (= 7.0.7) activestorage (= 7.1.2)
activesupport (= 7.0.7) activesupport (= 7.1.2)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (7.0.7) actionview (7.1.2)
activesupport (= 7.0.7) activesupport (= 7.1.2)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.11)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.1, >= 1.2.0) rails-html-sanitizer (~> 1.6)
activejob (7.0.7) activejob (7.1.2)
activesupport (= 7.0.7) activesupport (= 7.1.2)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (7.0.7) activemodel (7.1.2)
activesupport (= 7.0.7) activesupport (= 7.1.2)
activerecord (7.0.7) activerecord (7.1.2)
activemodel (= 7.0.7) activemodel (= 7.1.2)
activesupport (= 7.0.7) activesupport (= 7.1.2)
activestorage (7.0.7) timeout (>= 0.4.0)
actionpack (= 7.0.7) activestorage (7.1.2)
activejob (= 7.0.7) actionpack (= 7.1.2)
activerecord (= 7.0.7) activejob (= 7.1.2)
activesupport (= 7.0.7) activerecord (= 7.1.2)
activesupport (= 7.1.2)
marcel (~> 1.0) marcel (~> 1.0)
mini_mime (>= 1.1.0) activesupport (7.1.2)
activesupport (7.0.7) base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
minitest (>= 5.1) minitest (>= 5.1)
mutex_m
tzinfo (~> 2.0) tzinfo (~> 2.0)
addressable (2.8.5) addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0) public_suffix (>= 2.0.2, < 6.0)
afm (0.2.2) afm (0.2.2)
annotate (3.2.0) annotate (3.2.0)
activerecord (>= 3.2, < 8.0) activerecord (>= 3.2, < 8.0)
rake (>= 10.4, < 14.0) rake (>= 10.4, < 14.0)
ast (2.4.2) ast (2.4.2)
aws-eventstream (1.2.0) aws-eventstream (1.3.0)
aws-partitions (1.807.0) aws-partitions (1.874.0)
aws-sdk-core (3.180.3) aws-sdk-core (3.190.1)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0) aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.71.0) aws-sdk-kms (1.75.0)
aws-sdk-core (~> 3, >= 3.177.0) aws-sdk-core (~> 3, >= 3.188.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.132.1) aws-sdk-s3 (1.142.0)
aws-sdk-core (~> 3, >= 3.179.0) aws-sdk-core (~> 3, >= 3.189.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.6) aws-sigv4 (~> 1.8)
aws-sigv4 (1.6.0) aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
azure-storage-blob (2.0.3) azure-storage-blob (2.0.3)
azure-storage-common (~> 2.0) azure-storage-common (~> 2.0)
@@ -98,7 +107,8 @@ GEM
faraday_middleware (~> 1.0, >= 1.0.0.rc1) faraday_middleware (~> 1.0, >= 1.0.0.rc1)
net-http-persistent (~> 4.0) net-http-persistent (~> 4.0)
nokogiri (~> 1, >= 1.10.8) nokogiri (~> 1, >= 1.10.8)
bcrypt (3.1.19) base64 (0.2.0)
bcrypt (3.1.20)
better_html (2.0.2) better_html (2.0.2)
actionview (>= 6.0) actionview (>= 6.0)
activesupport (>= 6.0) activesupport (>= 6.0)
@@ -106,11 +116,12 @@ GEM
erubi (~> 1.4) erubi (~> 1.4)
parser (>= 2.4) parser (>= 2.4)
smart_properties smart_properties
bigdecimal (3.1.5)
bindex (0.8.1) bindex (0.8.1)
bootsnap (1.16.0) bootsnap (1.17.0)
msgpack (~> 1.2) msgpack (~> 1.2)
builder (3.2.4) builder (3.2.4)
bullet (7.0.7) bullet (7.1.4)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.11) uniform_notifier (~> 1.11)
cancancan (3.5.0) cancancan (3.5.0)
@@ -131,17 +142,17 @@ GEM
crack (0.4.5) crack (0.4.5)
rexml rexml
crass (1.0.6) crass (1.0.6)
css_parser (1.14.0) css_parser (1.16.0)
addressable addressable
cuprite (0.14.3) cuprite (0.15)
capybara (~> 3.0) capybara (~> 3.0)
ferrum (~> 0.13.0) ferrum (~> 0.14.0)
date (3.3.3) date (3.3.4)
debug (1.8.0) debug (1.9.1)
irb (>= 1.5.0) irb (~> 1.10)
reline (>= 0.3.1) reline (>= 0.3.8)
declarative (0.0.20) declarative (0.0.20)
devise (4.9.2) devise (4.9.3)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
railties (>= 4.1.0) railties (>= 4.1.0)
@@ -157,8 +168,10 @@ GEM
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 (2.8.1)
drb (2.2.0)
ruby2_keywords
email_typo (0.2.3) email_typo (0.2.3)
erb_lint (0.4.0) erb_lint (0.5.0)
activesupport activesupport
better_html (>= 2.0.1) better_html (>= 2.0.1)
parser (>= 2.7.1.4) parser (>= 2.7.1.4)
@@ -166,12 +179,12 @@ GEM
rubocop rubocop
smart_properties smart_properties
erubi (1.12.0) erubi (1.12.0)
factory_bot (6.2.1) factory_bot (6.4.4)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
factory_bot_rails (6.2.0) factory_bot_rails (6.4.2)
factory_bot (~> 6.2.0) factory_bot (~> 6.4)
railties (>= 5.0.0) railties (>= 5.0.0)
faker (3.2.1) faker (3.2.2)
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)
@@ -200,16 +213,16 @@ GEM
faraday-retry (1.0.3) faraday-retry (1.0.3)
faraday_middleware (1.2.0) faraday_middleware (1.2.0)
faraday (~> 1.0) faraday (~> 1.0)
ferrum (0.13) ferrum (0.14)
addressable (~> 2.5) addressable (~> 2.5)
concurrent-ruby (~> 1.1) concurrent-ruby (~> 1.1)
webrick (~> 1.7) webrick (~> 1.7)
websocket-driver (>= 0.6, < 0.8) websocket-driver (>= 0.6, < 0.8)
ffi (1.15.5) ffi (1.16.3)
geom2d (0.4.1) geom2d (0.4.1)
globalid (1.1.0) globalid (1.2.1)
activesupport (>= 5.0) activesupport (>= 6.1)
google-apis-core (0.11.1) google-apis-core (0.11.2)
addressable (~> 2.5, >= 2.5.1) addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a) googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a) httpclient (>= 2.8.1, < 3.a)
@@ -220,30 +233,30 @@ GEM
webrick webrick
google-apis-iamcredentials_v1 (0.17.0) google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.11.0, < 2.a) google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.19.0) google-apis-storage_v1 (0.29.0)
google-apis-core (>= 0.9.0, < 2.a) google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.6.0) google-cloud-core (1.6.1)
google-cloud-env (~> 1.0) google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0) google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0) google-cloud-env (2.1.0)
faraday (>= 0.17.3, < 3.0) faraday (>= 1.0, < 3.a)
google-cloud-errors (1.3.1) google-cloud-errors (1.3.1)
google-cloud-storage (1.44.0) google-cloud-storage (1.45.0)
addressable (~> 2.8) addressable (~> 2.8)
digest-crc (~> 0.4) digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1) google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.19.0) google-apis-storage_v1 (~> 0.29.0)
google-cloud-core (~> 1.6) google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a) googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0) mini_mime (~> 1.0)
googleauth (1.7.0) googleauth (1.9.1)
faraday (>= 0.17.3, < 3.a) faraday (>= 1.0, < 3.a)
google-cloud-env (~> 2.1)
jwt (>= 1.4, < 3.0) jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11) multi_json (~> 1.11)
os (>= 0.9, < 2.0) os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a) signet (>= 0.16, < 2.a)
hashdiff (1.0.1) hashdiff (1.1.0)
hashery (2.1.2) hashery (2.1.2)
hexapdf (0.34.1) hexapdf (0.34.1)
cmdparse (~> 3.0, >= 3.0.3) cmdparse (~> 3.0, >= 3.0.3)
@@ -256,11 +269,12 @@ GEM
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.6.0) io-console (0.7.1)
irb (1.7.4) irb (1.11.0)
reline (>= 0.3.6) rdoc
reline (>= 0.3.8)
jmespath (1.6.2) jmespath (1.6.2)
json (2.6.3) json (2.7.1)
jwt (2.7.1) jwt (2.7.1)
language_server-protocol (3.17.0.3) language_server-protocol (3.17.0.3)
launchy (2.5.2) launchy (2.5.2)
@@ -272,12 +286,12 @@ GEM
letter_opener (~> 1.7) letter_opener (~> 1.7)
railties (>= 5.2) railties (>= 5.2)
rexml rexml
lograge (0.13.0) lograge (0.14.0)
actionpack (>= 4) actionpack (>= 4)
activesupport (>= 4) activesupport (>= 4)
railties (>= 4) railties (>= 4)
request_store (~> 1.0) request_store (~> 1.0)
loofah (2.21.3) loofah (2.22.0)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
mail (2.8.1) mail (2.8.1)
@@ -287,49 +301,48 @@ GEM
net-smtp net-smtp
marcel (1.0.2) marcel (1.0.2)
matrix (0.4.2) matrix (0.4.2)
memoist (0.16.2)
method_source (1.0.0) method_source (1.0.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.4) mini_portile2 (2.8.5)
minitest (5.20.0) minitest (5.20.0)
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.3.0)
mutex_m (0.2.0)
mysql2 (0.5.5) mysql2 (0.5.5)
net-http-persistent (4.0.2) net-http-persistent (4.0.2)
connection_pool (~> 2.2) connection_pool (~> 2.2)
net-imap (0.3.7) net-imap (0.4.9)
date date
net-protocol net-protocol
net-pop (0.1.2) net-pop (0.1.2)
net-protocol net-protocol
net-protocol (0.2.1) net-protocol (0.2.2)
timeout timeout
net-smtp (0.3.3) net-smtp (0.4.0)
net-protocol net-protocol
nio4r (2.5.9) nio4r (2.7.0)
nokogiri (1.15.4) nokogiri (1.15.5)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.15.4-arm64-darwin) oj (3.16.3)
racc (~> 1.4) bigdecimal (>= 3.0)
oj (3.16.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.0.4) pagy (6.2.0)
parallel (1.23.0) parallel (1.24.0)
parser (3.2.2.4) parser (3.2.2.4)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
pdf-reader (2.11.0) pdf-reader (2.12.0)
Ascii85 (~> 1.0) Ascii85 (~> 1.0)
afm (~> 0.2.1) afm (~> 0.2.1)
hashery (~> 2.0) hashery (~> 2.0)
ruby-rc4 ruby-rc4
ttfunk ttfunk
pg (1.5.3) pg (1.5.4)
premailer (1.21.0) premailer (1.21.0)
addressable addressable
css_parser (>= 1.12.0) css_parser (>= 1.12.0)
@@ -343,29 +356,36 @@ GEM
method_source (~> 1.0) method_source (~> 1.0)
pry-rails (0.3.9) pry-rails (0.3.9)
pry (>= 0.10.4) pry (>= 0.10.4)
public_suffix (5.0.3) psych (5.1.2)
puma (6.3.1) stringio
public_suffix (5.0.4)
puma (6.4.0)
nio4r (~> 2.0) nio4r (~> 2.0)
racc (1.7.1) racc (1.7.3)
rack (2.2.8) rack (2.2.8)
rack-proxy (0.7.6) rack-proxy (0.7.7)
rack rack
rack-session (1.0.2)
rack (< 3)
rack-test (2.1.0) rack-test (2.1.0)
rack (>= 1.3) rack (>= 1.3)
rails (7.0.7) rackup (1.0.0)
actioncable (= 7.0.7) rack (< 3)
actionmailbox (= 7.0.7) webrick
actionmailer (= 7.0.7) rails (7.1.2)
actionpack (= 7.0.7) actioncable (= 7.1.2)
actiontext (= 7.0.7) actionmailbox (= 7.1.2)
actionview (= 7.0.7) actionmailer (= 7.1.2)
activejob (= 7.0.7) actionpack (= 7.1.2)
activemodel (= 7.0.7) actiontext (= 7.1.2)
activerecord (= 7.0.7) actionview (= 7.1.2)
activestorage (= 7.0.7) activejob (= 7.1.2)
activesupport (= 7.0.7) activemodel (= 7.1.2)
activerecord (= 7.1.2)
activestorage (= 7.1.2)
activesupport (= 7.1.2)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 7.0.7) railties (= 7.1.2)
rails-dom-testing (2.2.0) rails-dom-testing (2.2.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
minitest minitest
@@ -380,19 +400,22 @@ GEM
actionview (> 3.1) actionview (> 3.1)
activesupport (> 3.1) activesupport (> 3.1)
railties (> 3.1) railties (> 3.1)
railties (7.0.7) railties (7.1.2)
actionpack (= 7.0.7) actionpack (= 7.1.2)
activesupport (= 7.0.7) activesupport (= 7.1.2)
method_source irb
rackup (>= 1.0.0)
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0) thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.5) zeitwerk (~> 2.6)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.0.6) rake (13.1.0)
redis-client (0.16.0) rdoc (6.6.2)
psych (>= 4.0.0)
redis-client (0.19.1)
connection_pool connection_pool
regexp_parser (2.8.2) regexp_parser (2.8.3)
reline (0.3.8) reline (0.4.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)
@@ -400,12 +423,12 @@ GEM
uber (< 0.2.0) uber (< 0.2.0)
request_store (1.5.1) request_store (1.5.1)
rack (>= 1.4) rack (>= 1.4)
responders (3.1.0) responders (3.1.1)
actionpack (>= 5.2) actionpack (>= 5.2)
railties (>= 5.2) railties (>= 5.2)
retriable (3.1.2) retriable (3.1.2)
rexml (3.2.6) rexml (3.2.6)
rollbar (3.4.0) rollbar (3.4.2)
rotp (6.3.0) rotp (6.3.0)
rqrcode (2.2.0) rqrcode (2.2.0)
chunky_png (~> 1.0) chunky_png (~> 1.0)
@@ -419,7 +442,7 @@ GEM
rspec-mocks (3.12.6) rspec-mocks (3.12.6)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0) rspec-support (~> 3.12.0)
rspec-rails (6.0.3) rspec-rails (6.1.0)
actionpack (>= 6.1) actionpack (>= 6.1)
activesupport (>= 6.1) activesupport (>= 6.1)
railties (>= 6.1) railties (>= 6.1)
@@ -428,7 +451,7 @@ GEM
rspec-mocks (~> 3.12) rspec-mocks (~> 3.12)
rspec-support (~> 3.12) rspec-support (~> 3.12)
rspec-support (3.12.1) rspec-support (3.12.1)
rubocop (1.57.2) rubocop (1.59.0)
json (~> 2.3) json (~> 2.3)
language_server-protocol (>= 3.17.0) language_server-protocol (>= 3.17.0)
parallel (~> 1.10) parallel (~> 1.10)
@@ -436,7 +459,7 @@ GEM
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.28.1, < 2.0) rubocop-ast (>= 1.30.0, < 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.30.0)
@@ -445,20 +468,21 @@ GEM
rubocop (~> 1.41) rubocop (~> 1.41)
rubocop-factory_bot (2.24.0) rubocop-factory_bot (2.24.0)
rubocop (~> 1.33) rubocop (~> 1.33)
rubocop-performance (1.19.1) rubocop-performance (1.20.1)
rubocop (>= 1.7.0, < 2.0) rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 0.4.0) rubocop-ast (>= 1.30.0, < 2.0)
rubocop-rails (2.22.1) rubocop-rails (2.23.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-rspec (2.25.0) rubocop-rspec (2.25.0)
rubocop (~> 1.40) rubocop (~> 1.40)
rubocop-capybara (~> 2.17) rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22) rubocop-factory_bot (~> 2.22)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
ruby-rc4 (0.1.5) ruby-rc4 (0.1.5)
ruby-vips (2.1.4) ruby-vips (2.2.0)
ffi (~> 1.12) ffi (~> 1.12)
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rubyXL (3.4.25) rubyXL (3.4.25)
@@ -466,17 +490,17 @@ GEM
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.0.3) shakapacker (7.1.0)
activesupport (>= 5.2) activesupport (>= 5.2)
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.1.2) sidekiq (7.2.0)
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.14.0)
signet (0.17.0) signet (0.18.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)
@@ -488,34 +512,35 @@ 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.6.3) sqlite3 (1.7.0)
mini_portile2 (~> 2.8.0) mini_portile2 (~> 2.8.0)
sqlite3 (1.6.3-arm64-darwin) sqlite3 (1.7.0-arm64-darwin)
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.2.2) thor (1.3.0)
timeout (0.4.0) timeout (0.4.1)
trailblazer-option (0.1.2) trailblazer-option (0.1.2)
ttfunk (1.7.0) ttfunk (1.7.0)
turbo-rails (1.4.0) turbo-rails (1.5.0)
actionpack (>= 6.0.0) actionpack (>= 6.0.0)
activejob (>= 6.0.0) activejob (>= 6.0.0)
railties (>= 6.0.0) railties (>= 6.0.0)
tzinfo (2.0.6) tzinfo (2.0.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
tzinfo-data (1.2023.3) tzinfo-data (1.2023.4)
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)
uniform_notifier (1.16.0) uniform_notifier (1.16.0)
warden (1.2.9) warden (1.2.9)
rack (>= 2.0.9) rack (>= 2.0.9)
web-console (4.2.0) web-console (4.2.1)
actionview (>= 6.0.0) actionview (>= 6.0.0)
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.18.1) webmock (3.19.1)
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)
@@ -525,7 +550,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.11) zeitwerk (2.6.12)
PLATFORMS PLATFORMS
arm64-darwin-22 arm64-darwin-22
@@ -565,6 +590,7 @@ DEPENDENCIES
premailer-rails premailer-rails
pry-rails pry-rails
puma puma
rack
rails rails
rails-i18n rails-i18n
rails_autolink rails_autolink
@@ -592,4 +618,4 @@ RUBY VERSION
ruby 3.2.2p53 ruby 3.2.2p53
BUNDLED WITH BUNDLED WITH
2.4.10 2.5.3
@@ -6,7 +6,8 @@ class AccountConfigsController < ApplicationController
ALLOWED_KEYS = [ ALLOWED_KEYS = [
AccountConfig::ALLOW_TYPED_SIGNATURE, AccountConfig::ALLOW_TYPED_SIGNATURE,
AccountConfig::FORCE_MFA AccountConfig::FORCE_MFA,
AccountConfig::ESIGNING_PREFERENCE_KEY
].freeze ].freeze
def create def create
+19 -2
View File
@@ -19,6 +19,12 @@ module Api
render json: { error: e.message }, status: :forbidden render json: { error: e.message }, status: :forbidden
end end
rescue_from JSON::ParserError do |e|
Rollbar.error(e) if defined?(Rollbar)
render json: { error: "JSON parse error: #{e.message}" }, status: :unprocessable_entity
end
end end
private private
@@ -27,12 +33,23 @@ module Api
result = relation.order(id: :desc) result = relation.order(id: :desc)
.limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min) .limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min)
result = result.where('id < ?', params[:after]) if params[:after].present? result = result.where(relation.arel_table[:id].lt(params[:after])) if params[:after].present?
result = result.where('id > ?', params[:before]) if params[:before].present? result = result.where(relation.arel_table[:id].gt(params[:before])) if params[:before].present?
result result
end end
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).find_by(access_token: { sha256: })
end
render json: { error: 'Not authenticated' }, status: :unauthorized unless current_user
end
def current_account def current_account
current_user&.account current_user&.account
end end
+25 -14
View File
@@ -31,29 +31,40 @@ module Api
end end
def show def show
serialized_subbmitters = submitters = @submission.submitters.preload(documents_attachments: :blob, attachments_attachments: :blob)
@submission.submitters.preload(documents_attachments: :blob, attachments_attachments: :blob).map do |submitter|
Submissions::EnsureResultGenerated.call(submitter) if submitter.completed_at?
Submitters::SerializeForApi.call(submitter) serialized_submitters = submitters.map do |submitter|
end Submissions::EnsureResultGenerated.call(submitter) if submitter.completed_at?
Submitters::SerializeForApi.call(submitter)
end
json = @submission.as_json( json = @submission.as_json(
serialize_params.deep_merge( serialize_params.deep_merge(
include: { include: { submission_events: { only: %i[id submitter_id event_type event_timestamp] } }
submission_events: {
only: %i[id submitter_id event_type event_timestamp]
}
}
) )
) )
json[:submitters] = serialized_subbmitters if submitters.all?(&:completed_at?)
last_submitter = submitters.max_by(&:completed_at)
json[:documents] = serialized_submitters.find { |e| e['id'] == last_submitter.id }['documents']
json[:status] = 'completed'
json[:completed_at] = last_submitter.completed_at
else
json[:documents] = []
json[:status] = 'pending'
json[:completed_at] = nil
end
json[:submitters] = serialized_submitters
render json: render json:
end end
def create def create
return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil?
params[:send_email] = true unless params.key?(:send_email) params[:send_email] = true unless params.key?(:send_email)
params[:send_sms] = false unless params.key?(:send_sms) params[:send_sms] = false unless params.key?(:send_sms)
@@ -67,9 +78,9 @@ module Api
end end
def destroy def destroy
@submission.update!(deleted_at: Time.current) @submission.update!(archived_at: Time.current)
render json: @submission.as_json(only: %i[id deleted_at]) render json: @submission.as_json(only: %i[id], methods: %i[archived_at])
end end
private private
@@ -107,7 +118,7 @@ module Api
def serialize_params def serialize_params
{ {
only: %i[id source submitters_order created_at updated_at], only: %i[id source submitters_order created_at updated_at archived_at],
methods: %i[audit_log_url], methods: %i[audit_log_url],
include: { include: {
submitters: { only: %i[id slug uuid name email phone submitters: { only: %i[id slug uuid name email phone
+9 -1
View File
@@ -80,18 +80,26 @@ module Api
def assign_submitter_attrs(submitter, attrs) def assign_submitter_attrs(submitter, attrs)
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email) submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '') if attrs.key?(:phone) submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '') if attrs.key?(:phone)
submitter.values = submitter.values.merge(attrs[:values].to_unsafe_h) if attrs[:values] submitter.values = submitter.values.merge(attrs[:values].to_unsafe_h) if attrs[:values].present?
submitter.completed_at = attrs[:completed] ? Time.current : submitter.completed_at submitter.completed_at = attrs[:completed] ? Time.current : submitter.completed_at
submitter.application_key = attrs[:application_key] if attrs.key?(:application_key) submitter.application_key = attrs[:application_key] if attrs.key?(:application_key)
assign_submission_fields(submitter.submission)
assign_preferences(submitter, attrs) assign_preferences(submitter, attrs)
submitter submitter
end end
def assign_submission_fields(submission)
submission.template_fields ||= submission.template.fields
submission.template_schema ||= submission.template.schema
end
def assign_preferences(submitter, attrs) def assign_preferences(submitter, attrs)
submitter_preferences = Submitters.normalize_preferences(submitter.account, current_user, attrs) submitter_preferences = Submitters.normalize_preferences(submitter.account, current_user, attrs)
submitter.preferences['default_values'] = attrs[:values].to_unsafe_h if attrs[:values].present?
if submitter_preferences.key?('send_email') if submitter_preferences.key?('send_email')
submitter.preferences['send_email'] = submitter_preferences['send_email'] submitter.preferences['send_email'] = submitter_preferences['send_email']
end end
@@ -7,10 +7,10 @@ module Api
LIMIT = 100 LIMIT = 100
def index def index
template_folders = @template_folders.joins(:templates).where(templates: { deleted_at: nil }).distinct template_folders = @template_folders.joins(:templates).where(templates: { archived_at: nil }).distinct
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT) template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
render json: template_folders.as_json(only: %i[name deleted_at]) render json: template_folders.as_json(only: %i[name archived_at])
end end
end end
end end
@@ -7,23 +7,18 @@ module Api
def create def create
authorize!(:manage, @template) authorize!(:manage, @template)
template = current_account.templates.new(source: :api) cloned_template = Templates::Clone.call(@template,
author: current_user,
name: params[:name],
application_key: params[:application_key],
folder_name: params[:folder_name])
template.application_key = params[:application_key] cloned_template.source = :api
template.name = params[:name] || "#{@template.name} (Clone)" cloned_template.save!
template.account = @template.account
template.author = current_user
template.assign_attributes(@template.slice(:folder_id, :fields, :schema, :submitters))
if params[:folder_name].present? Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
end
template.save! render json: cloned_template.as_json(serialize_params)
Templates::CloneAttachments.call(template:, original_template: @template)
render json: template.as_json(serialize_params)
end end
private private
+2 -2
View File
@@ -38,9 +38,9 @@ module Api
end end
def destroy def destroy
@template.update!(deleted_at: Time.current) @template.update!(archived_at: Time.current)
render json: @template.as_json(only: %i[id deleted_at]) render json: @template.as_json(only: %i[id archived_at])
end end
private private
+5 -1
View File
@@ -8,7 +8,11 @@ class ErrorsController < ActionController::Base
'/templates/html', '/templates/html',
'/api/templates/html', '/api/templates/html',
'/templates/pdf', '/templates/pdf',
'/api/templates/pdf' '/api/templates/pdf',
'/templates/doc',
'/api/templates/doc',
'/templates/docx',
'/api/templates/docx'
].freeze ].freeze
def show def show
+14 -2
View File
@@ -13,7 +13,7 @@ class StartFormController < ApplicationController
end end
def update def update
@submitter = Submitter.where(submission: @template.submissions.where(deleted_at: nil)) @submitter = Submitter.where(submission: @template.submissions.where(archived_at: nil))
.order(id: :desc) .order(id: :desc)
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel } .then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
.find_or_initialize_by(**submitter_params.compact_blank) .find_or_initialize_by(**submitter_params.compact_blank)
@@ -46,13 +46,25 @@ class StartFormController < ApplicationController
private private
def assign_submission_attributes(submitter, template) def assign_submission_attributes(submitter, template)
resubmit_submitter =
if params[:resubmit].present?
Submitter.where(submission: @template.submissions).find_by(slug: params[:resubmit])
end
submitter.assign_attributes( submitter.assign_attributes(
uuid: template.submitters.first['uuid'], uuid: template.submitters.first['uuid'],
ip: request.remote_ip, ip: request.remote_ip,
ua: request.user_agent, ua: request.user_agent,
preferences: { 'send_email' => true } values: resubmit_submitter&.preferences&.fetch('default_values', nil) || {},
preferences: resubmit_submitter&.preferences.presence || { 'send_email' => true }
) )
if submitter.values.present?
resubmit_submitter.attachments.each do |attachment|
submitter.attachments << attachment.dup if submitter.values.value?(attachment.uuid)
end
end
submitter.submission ||= Submission.new(template:, submitter.submission ||= Submission.new(template:,
template_submitters: template.submitters, template_submitters: template.submitters,
source: :link) source: :link)
+1 -1
View File
@@ -63,7 +63,7 @@ class SubmissionsController < ApplicationController
end end
def destroy def destroy
@submission.update!(deleted_at: Time.current) @submission.update!(archived_at: Time.current)
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
+2 -2
View File
@@ -32,7 +32,7 @@ class SubmitFormController < ApplicationController
cookies[:submitter_sid] = @submitter.signed_id cookies[:submitter_sid] = @submitter.signed_id
render @submitter.submission.template.deleted_at? ? :archived : :show render @submitter.submission.template.archived_at? ? :archived : :show
end end
def update def update
@@ -42,7 +42,7 @@ class SubmitFormController < ApplicationController
return render json: { error: 'Form has been completed already.' }, status: :unprocessable_entity return render json: { error: 'Form has been completed already.' }, status: :unprocessable_entity
end end
if submitter.template.deleted_at? || submitter.submission.deleted_at? if submitter.template.archived_at? || submitter.submission.archived_at?
Rollbar.info("Archived template: #{submitter.template.id}") if defined?(Rollbar) Rollbar.info("Archived template: #{submitter.template.id}") if defined?(Rollbar)
return render json: { error: 'Form has been archived.' }, status: :unprocessable_entity return render json: { error: 'Form has been archived.' }, status: :unprocessable_entity
@@ -4,7 +4,7 @@ class TemplatesArchivedController < ApplicationController
load_and_authorize_resource :template, parent: false load_and_authorize_resource :template, parent: false
def index def index
@templates = @templates.where.not(deleted_at: nil).preload(:author, :folder).order(id: :desc) @templates = @templates.where.not(archived_at: nil).preload(:author, :folder).order(id: :desc)
@templates = Templates.search(@templates, params[:q]) @templates = Templates.search(@templates, params[:q])
@pagy, @templates = pagy(@templates, items: 12) @pagy, @templates = pagy(@templates, items: 12)
@@ -5,7 +5,7 @@ class TemplatesArchivedSubmissionsController < ApplicationController
load_and_authorize_resource :submission, through: :template, parent: false load_and_authorize_resource :submission, through: :template, parent: false
def index def index
@submissions = @submissions.where.not(deleted_at: nil) @submissions = @submissions.where.not(archived_at: nil)
@submissions = Submissions.search(@submissions, params[:q]) @submissions = Submissions.search(@submissions, params[:q])
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc)) @pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
+11 -6
View File
@@ -7,7 +7,7 @@ class TemplatesController < ApplicationController
def show def show
submissions = @template.submissions submissions = @template.submissions
submissions = submissions.active if @template.deleted_at.blank? submissions = submissions.active if @template.archived_at.blank?
submissions = Submissions.search(submissions, params[:q]) submissions = Submissions.search(submissions, params[:q])
@base_submissions = submissions @base_submissions = submissions
@@ -42,10 +42,15 @@ class TemplatesController < ApplicationController
end end
def create def create
@template.account = current_account if @base_template
@template.author = current_user @template = Templates::Clone.call(@base_template, author: current_user,
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name]) name: params.dig(:template, :name),
@template.assign_attributes(@base_template.slice(:fields, :schema, :submitters)) if @base_template folder_name: params[:folder_name])
else
@template.account = current_account
@template.author = current_user
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
end
if @template.save if @template.save
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
@@ -65,7 +70,7 @@ class TemplatesController < ApplicationController
'Template has been removed.' 'Template has been removed.'
else else
@template.update!(deleted_at: Time.current) @template.update!(archived_at: Time.current)
'Template has been archived.' 'Template has been archived.'
end end
@@ -4,7 +4,7 @@ class TemplatesRestoreController < ApplicationController
load_and_authorize_resource :template load_and_authorize_resource :template
def create def create
@template.update!(deleted_at: nil) @template.update!(archived_at: nil)
redirect_to template_path(@template), notice: 'Template has been unarchived' redirect_to template_path(@template), notice: 'Template has been unarchived'
end end
@@ -30,6 +30,7 @@ class TimestampServerController < ApplicationController
reason: 'Test', reason: 'Test',
certificate: pkcs.certificate, certificate: pkcs.certificate,
key: pkcs.key, key: pkcs.key,
signature_size: 10_000,
certificate_chain: pkcs.ca_certs || [], certificate_chain: pkcs.ca_certs || [],
timestamp_handler: Submissions::TimestampHandler.new(tsa_url: url)) timestamp_handler: Submissions::TimestampHandler.new(tsa_url: url))
end end
+8 -4
View File
@@ -28,9 +28,9 @@ class UsersController < ApplicationController
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
attrs.delete(:role) if User::ROLES.exclude?(attrs[:role]) attrs.delete(:role) if !role_valid?(attrs[:role]) || current_user == @user
if @user.update(attrs.except(current_user == @user ? :role : nil)) if @user.update(attrs)
redirect_to settings_users_path, notice: 'User has been updated' redirect_to 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
@@ -42,17 +42,21 @@ class UsersController < ApplicationController
return redirect_to settings_users_path, notice: 'Unable to remove user' return redirect_to settings_users_path, notice: 'Unable to remove user'
end end
@user.update!(deleted_at: Time.current) @user.update!(archived_at: Time.current)
redirect_to settings_users_path, notice: 'User has been removed' redirect_to settings_users_path, notice: 'User has been removed'
end end
private private
def role_valid?(role)
User::ROLES.include?(role)
end
def build_user def build_user
@user = current_account.users.find_by(email: user_params[:email])&.tap do |user| @user = current_account.users.find_by(email: user_params[:email])&.tap do |user|
user.assign_attributes(user_params) user.assign_attributes(user_params)
user.deleted_at = nil user.archived_at = nil
end end
@user ||= current_account.users.new(user_params) @user ||= current_account.users.new(user_params)
+1 -1
View File
@@ -86,7 +86,7 @@ window.customElements.define('template-builder', class extends HTMLElement {
backgroundColor: '#faf7f5', backgroundColor: '#faf7f5',
withPhone: this.dataset.withPhone === 'true', withPhone: this.dataset.withPhone === 'true',
withLogo: this.dataset.withLogo !== 'false', withLogo: this.dataset.withLogo !== 'false',
withPayment: this.dataset.withPayment !== 'false', withPayment: this.dataset.withPayment === 'true',
acceptFileTypes: this.dataset.acceptFileTypes, acceptFileTypes: this.dataset.acceptFileTypes,
isDirectUpload: this.dataset.isDirectUpload === 'true' isDirectUpload: this.dataset.isDirectUpload === 'true'
}) })
+4 -4
View File
@@ -75,16 +75,16 @@ button[disabled] .enabled {
@apply select base-input w-full font-normal; @apply select base-input w-full font-normal;
} }
.tooltip-bottom-end:before { .tooltip-bottom-start:before {
transform: translateX(-95%); transform: translateX(-30%);
top: var(--tooltip-offset); top: var(--tooltip-offset);
left: 100%; left: 100%;
right: auto; right: auto;
bottom: auto; bottom: auto;
} }
.tooltip-bottom-end:after { .tooltip-bottom-start:after {
transform: translateX(-25%); transform: translateX(-75%);
border-color: transparent transparent var(--tooltip-color) transparent; border-color: transparent transparent var(--tooltip-color) transparent;
top: var(--tooltip-tail-offset); top: var(--tooltip-tail-offset);
left: 50%; left: 50%;
+52 -15
View File
@@ -18,27 +18,64 @@ export default targetable(class extends HTMLElement {
this.toggleState() this.toggleState()
fetch(this.dataset.src).then((response) => response.json()).then((urls) => { fetch(this.dataset.src).then((response) => response.json()).then((urls) => {
const fileRequests = urls.map((url) => { const isSafariIos = /iPhone|iPad|iPod/i.test(navigator.userAgent)
return () => {
return fetch(url).then(async (resp) => {
const blobUrl = URL.createObjectURL(await resp.blob())
const link = document.createElement('a')
link.href = blobUrl if (isSafariIos && urls.length > 1) {
link.setAttribute('download', decodeURI(url.split('/').pop())) this.downloadSafariIos(urls)
} else {
this.downloadUrls(urls)
}
})
}
link.click() downloadUrls (urls) {
const fileRequests = urls.map((url) => {
return () => {
return fetch(url).then(async (resp) => {
const blobUrl = URL.createObjectURL(await resp.blob())
const link = document.createElement('a')
URL.revokeObjectURL(url) link.href = blobUrl
}) link.setAttribute('download', decodeURI(url.split('/').pop()))
}
link.click()
URL.revokeObjectURL(blobUrl)
})
}
})
fileRequests.reduce(
(prevPromise, request) => prevPromise.then(() => request()),
Promise.resolve()
)
this.toggleState()
}
downloadSafariIos (urls) {
const fileRequests = urls.map((url) => {
return fetch(url).then(async (resp) => {
const blob = await resp.blob()
const blobUrl = URL.createObjectURL(blob.slice(0, blob.size, 'application/octet-stream'))
const link = document.createElement('a')
link.href = blobUrl
link.setAttribute('download', decodeURI(url.split('/').pop()))
return link
}) })
})
fileRequests.reduce( Promise.all(fileRequests).then((links) => {
(prevPromise, request) => prevPromise.then(() => request()), links.forEach((link, index) => {
Promise.resolve() setTimeout(() => {
) link.click()
URL.revokeObjectURL(link.href)
}, index * 50)
})
}).finally(() => {
this.toggleState() this.toggleState()
}) })
} }
+15 -1
View File
@@ -42,6 +42,11 @@
class="object-contain mx-auto" class="object-contain mx-auto"
:src="image.url" :src="image.url"
> >
<img
v-else-if="field.type === 'stamp' && stamp"
class="object-contain mx-auto"
:src="stamp.url"
>
<img <img
v-else-if="field.type === 'signature' && signature" v-else-if="field.type === 'signature' && signature"
class="object-contain mx-auto" class="object-contain mx-auto"
@@ -158,7 +163,7 @@
</template> </template>
<script> <script>
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard } from '@tabler/icons-vue' import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp } from '@tabler/icons-vue'
export default { export default {
name: 'FieldArea', name: 'FieldArea',
@@ -225,6 +230,7 @@ export default {
signature: 'Signature', signature: 'Signature',
date: 'Date', date: 'Date',
image: 'Image', image: 'Image',
stamp: 'Stamp',
initials: 'Initials', initials: 'Initials',
file: 'File', file: 'File',
select: 'Select', select: 'Select',
@@ -246,6 +252,7 @@ export default {
select: IconSelect, select: IconSelect,
checkbox: IconCheckbox, checkbox: IconCheckbox,
radio: IconCircleDot, radio: IconCircleDot,
stamp: IconRubberStamp,
cells: IconColumns3, cells: IconColumns3,
multiple: IconChecks, multiple: IconChecks,
phone: IconPhoneCheck, phone: IconPhoneCheck,
@@ -259,6 +266,13 @@ export default {
return null return null
} }
}, },
stamp () {
if (this.field.type === 'stamp') {
return this.attachmentsIndex[this.modelValue]
} else {
return null
}
},
signature () { signature () {
if (this.field.type === 'signature') { if (this.field.type === 'signature') {
return this.attachmentsIndex[this.modelValue] return this.attachmentsIndex[this.modelValue]
+50 -15
View File
@@ -179,27 +179,62 @@ export default {
this.isDownloading = true this.isDownloading = true
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`).then((response) => response.json()).then((urls) => { fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`).then((response) => response.json()).then((urls) => {
const fileRequests = urls.map((url) => { const isSafariIos = /iPhone|iPad|iPod/i.test(navigator.userAgent)
return () => {
return fetch(url).then(async (resp) => {
const blobUrl = URL.createObjectURL(await resp.blob())
const link = document.createElement('a')
link.href = blobUrl if (isSafariIos && urls.length > 1) {
link.setAttribute('download', decodeURI(url.split('/').pop())) this.downloadSafariIos(urls)
} else {
this.downloadUrls(urls)
}
})
},
downloadUrls (urls) {
const fileRequests = urls.map((url) => {
return () => {
return fetch(url).then(async (resp) => {
const blobUrl = URL.createObjectURL(await resp.blob())
const link = document.createElement('a')
link.click() link.href = blobUrl
link.setAttribute('download', decodeURI(url.split('/').pop()))
URL.revokeObjectURL(url) link.click()
})
} URL.revokeObjectURL(blobUrl)
})
}
})
fileRequests.reduce(
(prevPromise, request) => prevPromise.then(() => request()),
Promise.resolve()
)
this.isDownloading = false
},
downloadSafariIos (urls) {
const fileRequests = urls.map((url) => {
return fetch(url).then(async (resp) => {
const blob = await resp.blob()
const blobUrl = URL.createObjectURL(blob.slice(0, blob.size, 'application/octet-stream'))
const link = document.createElement('a')
link.href = blobUrl
link.setAttribute('download', decodeURI(url.split('/').pop()))
return link
}) })
})
fileRequests.reduce( Promise.all(fileRequests).then((links) => {
(prevPromise, request) => prevPromise.then(() => request()), links.forEach((link, index) => {
Promise.resolve() setTimeout(() => {
) link.click()
URL.revokeObjectURL(link.href)
}, index * 50)
})
}).finally(() => {
this.isDownloading = false this.isDownloading = false
}) })
} }
+14 -4
View File
@@ -10,6 +10,7 @@
/> />
<button <button
v-if="!isFormVisible" v-if="!isFormVisible"
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 rounded-none border-x-0 md:border md:rounded-full bottom-0 w-full md:mb-4 text-base"
@click.prevent="isFormVisible = true" @click.prevent="isFormVisible = true"
> >
@@ -28,6 +29,7 @@
> >
<button <button
v-if="!isCompleted" v-if="!isCompleted"
id="minimize_form_button"
class="absolute right-0 mr-2 mt-2 top-0 hidden md:block" class="absolute right-0 mr-2 mt-2 top-0 hidden md:block"
title="Minimize" title="Minimize"
@click.prevent="isFormVisible = false" @click.prevent="isFormVisible = false"
@@ -206,7 +208,7 @@
</div> </div>
</div> </div>
<ImageStep <ImageStep
v-else-if="currentField.type === 'image'" v-else-if="currentField.type === 'image' || currentField.type === 'stamp'"
:key="currentField.uuid" :key="currentField.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:field="currentField" :field="currentField"
@@ -283,6 +285,7 @@
class="mt-6 md:mt-8" class="mt-6 md:mt-8"
> >
<button <button
id="submit_form_button"
ref="submitButton" ref="submitButton"
type="submit" type="submit"
class="base-button w-full flex justify-center" class="base-button w-full flex justify-center"
@@ -323,18 +326,25 @@
:can-send-email="canSendEmail && !!submitter.email" :can-send-email="canSendEmail && !!submitter.email"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
/> />
<div class="flex justify-center"> <div
<div class="flex items-center mt-5 mb-1"> v-if="stepFields.length < 80"
class="flex justify-center"
>
<div class="flex items-center mt-4 mb-1 flex-wrap">
<a <a
v-for="(step, index) in stepFields" v-for="(step, index) in stepFields"
:key="step[0].uuid" :key="step[0].uuid"
href="#" href="#"
class="inline border border-base-300 h-3 w-3 rounded-full mx-1" class="inline border border-base-300 h-3 w-3 rounded-full mx-1 mt-1"
:class="{ 'bg-base-300': index === currentStep, 'bg-base-content': (index < currentStep && stepFields[index].every((f) => !f.required || ![null, undefined, ''].includes(values[f.uuid]))) || isCompleted, 'bg-white': index > currentStep }" :class="{ 'bg-base-300': index === currentStep, 'bg-base-content': (index < currentStep && stepFields[index].every((f) => !f.required || ![null, undefined, ''].includes(values[f.uuid]))) || isCompleted, 'bg-white': index > currentStep }"
@click.prevent="isCompleted ? '' : [saveStep(), goToStep(step, true)]" @click.prevent="isCompleted ? '' : [saveStep(), goToStep(step, true)]"
/> />
</div> </div>
</div> </div>
<div
v-else
class="mt-5"
/>
</div> </div>
</div> </div>
</template> </template>
+112 -53
View File
@@ -5,7 +5,8 @@
> >
<div <div
v-if="$slots.buttons || withTitle" v-if="$slots.buttons || withTitle"
class="flex justify-between py-1.5 items-center pr-4 sticky top-0 z-10" class="flex justify-between py-1.5 items-center pr-4 top-0 z-10"
:class="{ sticky: withStickySubmitters || isBreakpointLg }"
:style="{ backgroundColor }" :style="{ backgroundColor }"
> >
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
@@ -86,6 +87,7 @@
:class="$slots.buttons || withTitle ? 'md:max-h-[calc(100%_-_60px)]' : 'md:max-h-[100%]'" :class="$slots.buttons || withTitle ? 'md:max-h-[calc(100%_-_60px)]' : 'md:max-h-[100%]'"
> >
<div <div
v-if="withDocumentsList"
ref="previews" ref="previews"
:style="{ 'display': isBreakpointLg ? 'none' : 'initial' }" :style="{ 'display': isBreakpointLg ? 'none' : 'initial' }"
class="overflow-y-auto overflow-x-hidden w-52 flex-none pr-3 mt-0.5 pt-0.5 hidden lg:block" class="overflow-y-auto overflow-x-hidden w-52 flex-none pr-3 mt-0.5 pt-0.5 hidden lg:block"
@@ -192,36 +194,22 @@
@cancel="[drawField = null, drawOption = null]" @cancel="[drawField = null, drawOption = null]"
@change-submitter="[selectedSubmitter = $event, drawField.submitter_uuid = $event.uuid]" @change-submitter="[selectedSubmitter = $event, drawField.submitter_uuid = $event.uuid]"
/> />
<FieldType <MobileFields
v-if="sortedDocuments.length && !drawField && editable" v-if="sortedDocuments.length && !drawField && editable"
class="dropdown-top dropdown-end fixed bottom-4 right-4 z-10 md:hidden" :fields="template.fields"
:model-value="''" :default-fields="defaultFields"
@update:model-value="startFieldDraw($event)" :selected-submitter="selectedSubmitter"
> @select="startFieldDraw($event)"
<label />
class="btn btn-neutral text-white btn-circle btn-lg group"
tabindex="0"
>
<IconPlus
class="group-focus:hidden"
width="28"
height="28"
/>
<IconX
class="hidden group-focus:inline"
width="28"
height="28"
/>
</label>
</FieldType>
</div> </div>
<div <div
v-if="withFieldsList"
class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block" class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block"
:class="drawField ? 'overflow-hidden' : 'overflow-auto'" :class="drawField ? 'overflow-hidden' : 'overflow-y-auto overflow-x-hidden'"
> >
<div <div
v-if="drawField" v-if="drawField"
class="sticky inset-0 h-full" class="sticky inset-0 h-full z-20"
:style="{ backgroundColor }" :style="{ backgroundColor }"
> >
<div class="bg-base-300 rounded-lg p-5 text-center space-y-4"> <div class="bg-base-300 rounded-lg p-5 text-center space-y-4">
@@ -270,8 +258,8 @@ import Logo from './logo'
import Contenteditable from './contenteditable' import Contenteditable from './contenteditable'
import DocumentPreview from './preview' import DocumentPreview from './preview'
import DocumentControls from './controls' import DocumentControls from './controls'
import FieldType from './field_type' import MobileFields from './mobile_fields'
import { IconUsersPlus, IconDeviceFloppy, IconWritingSign, IconInnerShadowTop, IconPlus, IconX } from '@tabler/icons-vue' import { IconUsersPlus, IconDeviceFloppy, IconWritingSign, IconInnerShadowTop } from '@tabler/icons-vue'
import { v4 } from 'uuid' import { v4 } from 'uuid'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
@@ -282,10 +270,8 @@ export default {
Document, Document,
Fields, Fields,
MobileDrawField, MobileDrawField,
IconPlus,
FieldType,
IconX,
IconWritingSign, IconWritingSign,
MobileFields,
Logo, Logo,
Dropzone, Dropzone,
DocumentPreview, DocumentPreview,
@@ -373,6 +359,16 @@ export default {
required: false, required: false,
default: true default: true
}, },
withFieldsList: {
type: Boolean,
required: false,
default: true
},
withDocumentsList: {
type: Boolean,
required: false,
default: true
},
withPhone: { withPhone: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -401,8 +397,6 @@ export default {
} }
}, },
computed: { computed: {
fieldIcons: FieldType.computed.fieldIcons,
fieldNames: FieldType.computed.fieldNames,
selectedAreaRef: () => ref(), selectedAreaRef: () => ref(),
fieldAreasIndex () { fieldAreasIndex () {
const areas = {} const areas = {}
@@ -429,11 +423,20 @@ export default {
} }
}, },
created () { created () {
const existingSubmittersUuids = this.defaultSubmitters.map((name) => {
return this.template.submitters.find(e => e.name === name)?.uuid
})
this.defaultSubmitters.forEach((name, index) => { this.defaultSubmitters.forEach((name, index) => {
const submitter = (this.template.submitters[index] ||= {}) const submitter = (this.template.submitters[index] ||= {})
submitter.name = name submitter.name = name
submitter.uuid ||= v4()
if (existingSubmittersUuids.filter(Boolean).length) {
submitter.uuid = existingSubmittersUuids[index] || v4()
} else {
submitter.uuid ||= v4()
}
}) })
this.selectedSubmitter = this.template.submitters[0] this.selectedSubmitter = this.template.submitters[0]
@@ -467,27 +470,38 @@ export default {
this.documentRefs = [] this.documentRefs = []
}, },
methods: { methods: {
startFieldDraw (type) { startFieldDraw ({ name, type }) {
const field = { const existingField = this.template.fields?.find((f) => f.submitter_uuid === this.selectedSubmitter.uuid && name && name === f.name)
name: '',
uuid: v4(),
required: type !== 'checkbox',
areas: [],
submitter_uuid: this.selectedSubmitter.uuid,
type
}
if (['select', 'multiple', 'radio'].includes(type)) { if (existingField) {
field.options = [{ value: '', uuid: v4() }] this.drawField = existingField
} } else {
const field = {
if (type === 'date') { name: name || '',
field.preferences = { uuid: v4(),
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY' required: type !== 'checkbox',
areas: [],
submitter_uuid: this.selectedSubmitter.uuid,
type
} }
if (['select', 'multiple', 'radio'].includes(type)) {
field.options = [{ value: '', uuid: v4() }]
}
if (type === 'stamp') {
field.readonly = true
}
if (type === 'date') {
field.preferences = {
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
}
}
this.drawField = field
} }
this.drawField = field
this.drawOption = null this.drawOption = null
}, },
undo () { undo () {
@@ -595,6 +609,41 @@ export default {
area.option_uuid = this.drawOption.uuid area.option_uuid = this.drawOption.uuid
} }
if (area.w === 0 || area.h === 0) {
const previousArea = this.drawField.areas?.[this.drawField.areas.length - 1]
if (this.selectedField?.type === this.drawField.type) {
area.w = this.selectedAreaRef.value.w
area.h = this.selectedAreaRef.value.h
} else if (previousArea) {
area.w = previousArea.w
area.h = previousArea.h
} else {
const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid)
const pageMask = documentRef.pageRefs[area.page].$refs.mask
if (this.drawField.type === 'checkbox' || this.drawOption) {
area.w = pageMask.clientWidth / 30 / pageMask.clientWidth
area.h = (pageMask.clientWidth / 30 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight)
} else if (this.drawField.type === 'image') {
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight)
} else if (this.drawField.type === 'signature' || this.drawField.type === 'stamp') {
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight) / 2
} else if (this.drawField.type === 'initials') {
area.w = pageMask.clientWidth / 10 / pageMask.clientWidth
area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth)
} else {
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth)
}
}
area.x -= area.w / 2
area.y -= area.h / 2
}
this.drawField.areas ||= [] this.drawField.areas ||= []
this.drawField.areas.push(area) this.drawField.areas.push(area)
@@ -663,6 +712,10 @@ export default {
field.options = [{ value: '', uuid: v4() }] field.options = [{ value: '', uuid: v4() }]
} }
if (field.type === 'stamp') {
field.readonly = true
}
if (field.type === 'date') { if (field.type === 'date') {
field.preferences = { field.preferences = {
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY' format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
@@ -695,7 +748,7 @@ export default {
w: area.maskW / 5 / area.maskW, w: area.maskW / 5 / area.maskW,
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH)
} }
} else if (field.type === 'signature') { } else if (field.type === 'signature' || field.type === 'stamp') {
baseArea = { baseArea = {
w: area.maskW / 5 / area.maskW, w: area.maskW / 5 / area.maskW,
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2 h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2
@@ -739,6 +792,10 @@ export default {
this.scrollIntoDocument(schema[0]) this.scrollIntoDocument(schema[0])
}) })
if (this.template.name === 'New Document') {
this.template.name = this.template.schema[0].name
}
if (this.onUpload) { if (this.onUpload) {
this.onUpload(this.template) this.onUpload(this.template)
} }
@@ -831,9 +888,11 @@ export default {
}) })
}, },
save () { save () {
if (this.$el.closest('template-builder')) { this.$nextTick(() => {
this.$el.closest('template-builder').dataset.template = JSON.stringify(this.template) if (this.$el.closest('template-builder')) {
} this.$el.closest('template-builder').dataset.template = JSON.stringify(this.template)
}
})
this.pushUndo() this.pushUndo()
+10 -4
View File
@@ -70,8 +70,10 @@
:field="field" :field="field"
/> />
<span <span
v-else-if="!defaultField" v-else
class="dropdown dropdown-end" class="dropdown dropdown-end"
@mouseenter="renderDropdown = true"
@touchstart="renderDropdown = true"
> >
<label <label
tabindex="0" tabindex="0"
@@ -84,6 +86,7 @@
/> />
</label> </label>
<ul <ul
v-if="renderDropdown"
tabindex="0" tabindex="0"
class="mt-1.5 dropdown-content menu menu-xs p-2 shadow bg-base-100 rounded-box w-52 z-10" class="mt-1.5 dropdown-content menu menu-xs p-2 shadow bg-base-100 rounded-box w-52 z-10"
draggable="true" draggable="true"
@@ -91,7 +94,7 @@
@click="closeDropdown" @click="closeDropdown"
> >
<div <div
v-if="field.type === 'text'" v-if="field.type === 'text' && !defaultField"
class="py-1.5 px-1 relative" class="py-1.5 px-1 relative"
@click.stop @click.stop
> >
@@ -153,7 +156,7 @@
</label> </label>
</li> </li>
<li <li
v-if="field.type === 'text'" v-if="field.type === 'text' && !defaultField"
@click.stop @click.stop
> >
<label class="cursor-pointer py-1.5"> <label class="cursor-pointer py-1.5">
@@ -247,6 +250,7 @@
class="w-full input input-primary input-xs text-sm bg-transparent !pr-7 -mr-6" class="w-full input input-primary input-xs text-sm bg-transparent !pr-7 -mr-6"
type="text" type="text"
required required
:placeholder="`Option ${index + 1}`"
@blur="save" @blur="save"
> >
<button <button
@@ -264,6 +268,7 @@
v-else v-else
v-model="option.value" v-model="option.value"
class="w-full input input-primary input-xs text-sm bg-transparent" class="w-full input input-primary input-xs text-sm bg-transparent"
:placeholder="`Option ${index + 1}`"
type="text" type="text"
required required
@focus="maybeFocusOnOptionArea(option)" @focus="maybeFocusOnOptionArea(option)"
@@ -329,7 +334,8 @@ export default {
data () { data () {
return { return {
isNameFocus: false, isNameFocus: false,
showPaymentModal: false showPaymentModal: false,
renderDropdown: false
} }
}, },
computed: { computed: {
@@ -1,5 +1,9 @@
<template> <template>
<div v-if="mobileView"> <div
v-if="mobileView"
@mouseenter="renderDropdown = true"
@touchstart="renderDropdown = true"
>
<div class="flex space-x-2 items-end"> <div class="flex space-x-2 items-end">
<div class="group/contenteditable-container bg-base-100 rounded-md p-2 border border-base-300 w-full flex justify-between items-end"> <div class="group/contenteditable-container bg-base-100 rounded-md p-2 border border-base-300 w-full flex justify-between items-end">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
@@ -29,7 +33,7 @@
/> />
</label> </label>
<ul <ul
v-if="editable" v-if="editable && renderDropdown"
tabindex="0" tabindex="0"
class="rounded-md min-w-max mb-2" class="rounded-md min-w-max mb-2"
:class="menuClasses" :class="menuClasses"
@@ -85,6 +89,8 @@
<div <div
v-else v-else
class="dropdown" class="dropdown"
@mouseenter="renderDropdown = true"
@touchstart="renderDropdown = true"
> >
<label <label
v-if="compact" v-if="compact"
@@ -125,7 +131,7 @@
</span> </span>
</label> </label>
<ul <ul
v-if="editable || !compact" v-if="(editable || !compact) && renderDropdown"
tabindex="0" tabindex="0"
:class="menuClasses" :class="menuClasses"
@click="closeDropdown" @click="closeDropdown"
@@ -222,6 +228,11 @@ export default {
} }
}, },
emits: ['update:model-value', 'remove', 'new-submitter', 'name-change'], emits: ['update:model-value', 'remove', 'new-submitter', 'name-change'],
data () {
return {
renderDropdown: false
}
},
computed: { computed: {
colors () { colors () {
return [ return [
+20 -9
View File
@@ -1,5 +1,9 @@
<template> <template>
<span class="dropdown"> <span
class="dropdown"
@mouseenter="renderDropdown = true"
@touchstart="renderDropdown = true"
>
<slot> <slot>
<label <label
tabindex="0" tabindex="0"
@@ -15,7 +19,7 @@
</label> </label>
</slot> </slot>
<ul <ul
v-if="editable" v-if="editable && renderDropdown"
tabindex="0" tabindex="0"
class="dropdown-content menu menu-xs p-2 shadow rounded-box w-52 z-10 mb-3" class="dropdown-content menu menu-xs p-2 shadow rounded-box w-52 z-10 mb-3"
:class="menuClasses" :class="menuClasses"
@@ -25,7 +29,7 @@
v-for="(icon, type) in fieldIcons" v-for="(icon, type) in fieldIcons"
:key="type" :key="type"
> >
<li v-if="withPhone || withPayment || !['phone', 'payment'].includes(type)"> <li v-if="(withPhone || type != 'phone') && (withPayment || type != 'payment')">
<a <a
href="#" href="#"
class="text-sm py-1 px-2" class="text-sm py-1 px-2"
@@ -46,7 +50,7 @@
</template> </template>
<script> <script>
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard } from '@tabler/icons-vue' import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp } from '@tabler/icons-vue'
export default { export default {
name: 'FiledTypeDropdown', name: 'FiledTypeDropdown',
@@ -78,6 +82,11 @@ export default {
} }
}, },
emits: ['update:model-value'], emits: ['update:model-value'],
data () {
return {
renderDropdown: false
}
},
computed: { computed: {
fieldNames () { fieldNames () {
return { return {
@@ -92,8 +101,9 @@ export default {
multiple: 'Multiple', multiple: 'Multiple',
radio: 'Radio', radio: 'Radio',
cells: 'Cells', cells: 'Cells',
phone: 'Phone', stamp: 'Stamp',
payment: 'Payment' payment: 'Payment',
phone: 'Phone'
} }
}, },
fieldIcons () { fieldIcons () {
@@ -104,13 +114,14 @@ export default {
date: IconCalendarEvent, date: IconCalendarEvent,
image: IconPhoto, image: IconPhoto,
file: IconPaperclip, file: IconPaperclip,
select: IconSelect,
checkbox: IconCheckbox, checkbox: IconCheckbox,
radio: IconCircleDot, radio: IconCircleDot,
select: IconSelect,
multiple: IconChecks, multiple: IconChecks,
cells: IconColumns3, cells: IconColumns3,
phone: IconPhoneCheck, stamp: IconRubberStamp,
payment: IconCreditCard payment: IconCreditCard,
phone: IconPhoneCheck
} }
} }
}, },
+30 -14
View File
@@ -13,9 +13,10 @@
/> />
</div> </div>
<div <div
ref="fields"
class="mb-1 mt-2" class="mb-1 mt-2"
@dragover.prevent="onFieldDragover" @dragover.prevent="onFieldDragover"
@drop="save" @drop="reorderFields"
> >
<Field <Field
v-for="field in submitterFields" v-for="field in submitterFields"
@@ -23,7 +24,7 @@
:data-uuid="field.uuid" :data-uuid="field.uuid"
:field="field" :field="field"
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)" :type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
:editable="editable && !dragField" :editable="editable && (!dragField || dragField !== field)"
:default-field="defaultFields.find((f) => f.name === field.name)" :default-field="defaultFields.find((f) => f.name === field.name)"
:draggable="editable" :draggable="editable"
@dragstart="dragField = field" @dragstart="dragField = field"
@@ -91,7 +92,8 @@
</button> </button>
<div <div
v-else-if="type == 'phone'" v-else-if="type == 'phone'"
class="tooltip tooltip-bottom-end flex" class="tooltip tooltip-bottom flex"
:class="{'tooltip-bottom-start': !withPayment, 'tooltip-bottom': withPayment }"
data-tip="Unlock SMS-verified phone number field with paid plan. Use text field for phone numbers without verification." data-tip="Unlock SMS-verified phone number field with paid plan. Use text field for phone numbers without verification."
> >
<a <a
@@ -210,23 +212,33 @@ export default {
this.$emit('set-drag', field) this.$emit('set-drag', field)
}, },
onFieldDragover (e) { onFieldDragover (e) {
const targetFieldUuid = e.target.closest('[data-uuid]')?.dataset?.uuid const targetField = e.target.closest('[data-uuid]')
const dragField = this.$refs.fields.querySelector(`[data-uuid="${this.dragField.uuid}"]`)
if (this.dragField && targetFieldUuid && this.dragField.uuid !== targetFieldUuid) { if (dragField && targetField && targetField !== dragField) {
const field = this.fields.find((f) => f.uuid === targetFieldUuid) const fields = Array.from(this.$refs.fields.children)
const currentIndex = fields.indexOf(dragField)
const currentIndex = this.fields.indexOf(this.dragField) const targetIndex = fields.indexOf(targetField)
const targetIndex = this.fields.indexOf(field)
if (currentIndex < targetIndex) { if (currentIndex < targetIndex) {
this.fields.splice(targetIndex + 1, 0, this.dragField) targetField.after(dragField)
this.fields.splice(currentIndex, 1)
} else { } else {
this.fields.splice(targetIndex, 0, this.dragField) targetField.before(dragField)
this.fields.splice(currentIndex + 1, 1)
} }
} }
}, },
reorderFields () {
Array.from(this.$refs.fields.children).forEach((el, index) => {
if (el.dataset.uuid !== this.fields[index].uuid) {
const field = this.fields.find((f) => f.uuid === el.dataset.uuid)
this.fields.splice(this.fields.indexOf(field), 1)
this.fields.splice(index, 0, field)
}
})
this.save()
},
removeSubmitter (submitter) { removeSubmitter (submitter) {
[...this.fields].forEach((field) => { [...this.fields].forEach((field) => {
if (field.submitter_uuid === submitter.uuid) { if (field.submitter_uuid === submitter.uuid) {
@@ -261,6 +273,10 @@ export default {
field.options = [{ value: '', uuid: v4() }] field.options = [{ value: '', uuid: v4() }]
} }
if (type === 'stamp') {
field.readonly = true
}
if (type === 'date') { if (type === 'date') {
field.preferences = { field.preferences = {
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY' format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
@@ -269,7 +285,7 @@ export default {
this.fields.push(field) this.fields.push(field)
if (['signature', 'initials', 'cells'].includes(type)) { if (['signature', 'initials', 'cells', 'stamp'].includes(type)) {
this.$emit('set-draw', { field }) this.$emit('set-draw', { field })
} }
@@ -0,0 +1,121 @@
<template>
<span
class="dropdown dropdown-top dropdown-end fixed bottom-4 right-4 z-10 md:hidden"
>
<label
class="btn btn-neutral text-white btn-circle btn-lg group"
tabindex="0"
>
<IconPlus
class="group-focus:hidden"
width="28"
height="28"
/>
<IconX
class="hidden group-focus:inline"
width="28"
height="28"
/>
</label>
<ul
tabindex="0"
class="dropdown-content menu menu-xs p-2 shadow rounded-box w-52 z-10 mb-3 mt-1.5 bg-base-100"
@click="closeDropdown"
>
<template v-if="submitterDefaultFields.length">
<template
v-for="field in submitterDefaultFields"
:key="field.name"
>
<li>
<a
href="#"
class="text-sm py-1 px-2"
@click.prevent="$emit('select', { name: field.name || '', type: field.type || 'text' })"
>
<component
:is="fieldIcons[field.type || 'text']"
:stroke-width="1.6"
:width="20"
/>
{{ field.name }}
</a>
</li>
</template>
</template>
<template v-else>
<template
v-for="(icon, type) in fieldIcons"
:key="type"
>
<li v-if="(withPhone || type != 'phone') && (withPayment || type != 'payment')">
<a
href="#"
class="text-sm py-1 px-2"
:class="{ 'active': type === modelValue }"
@click.prevent="$emit('select', { type })"
>
<component
:is="icon"
:stroke-width="1.6"
:width="20"
/>
{{ fieldNames[type] }}
</a>
</li>
</template>
</template>
</ul>
</span>
</template>
<script>
import { IconPlus, IconX } from '@tabler/icons-vue'
import FieldType from './field_type'
export default {
name: 'MobileFields',
components: {
IconPlus,
IconX
},
inject: ['withPhone', 'withPayment', 'backgroundColor'],
props: {
modelValue: {
type: String,
required: false,
default: ''
},
fields: {
type: Array,
required: false,
default: () => []
},
selectedSubmitter: {
type: Object,
required: true
},
defaultFields: {
type: Array,
required: false,
default: () => []
}
},
emits: ['select'],
computed: {
...FieldType.computed,
submitterFields () {
return this.fields.filter((f) => f.submitter_uuid === this.selectedSubmitter.uuid)
},
submitterDefaultFields () {
return this.defaultFields.filter((f) => {
return (!f.role || f.role === this.selectedSubmitter.name)
})
}
},
methods: {
closeDropdown () {
document.activeElement.blur()
}
}
}
</script>
@@ -141,6 +141,13 @@
</span> </span>
</button> </button>
</form> </form>
<a
v-if="!isConnected"
class="block link text-center mt-1"
href="https://www.docuseal.co/blog/accept-payments-and-request-signatures-with-ease"
target="_blank"
data-turbo="false"
>Learn more</a>
</div> </div>
</ul> </ul>
</span> </span>
@@ -67,6 +67,7 @@
<Contenteditable <Contenteditable
:model-value="item.name" :model-value="item.name"
:icon-width="16" :icon-width="16"
:editable="editable"
style="max-width: 95%" style="max-width: 95%"
class="mx-auto" class="mx-auto"
@update:model-value="onUpdateName" @update:model-value="onUpdateName"
+2 -1
View File
@@ -29,6 +29,7 @@ class AccountConfig < ApplicationRecord
ALLOW_TYPED_SIGNATURE = 'allow_typed_signature' ALLOW_TYPED_SIGNATURE = 'allow_typed_signature'
SUBMITTER_REMAILERS = 'submitter_reminders' SUBMITTER_REMAILERS = 'submitter_reminders'
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button' FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
ESIGNING_PREFERENCE_KEY = 'esigning_preference'
DEFAULT_VALUES = { DEFAULT_VALUES = {
SUBMITTER_INVITATION_EMAIL_KEY => { SUBMITTER_INVITATION_EMAIL_KEY => {
@@ -59,5 +60,5 @@ class AccountConfig < ApplicationRecord
belongs_to :account belongs_to :account
serialize :value, JSON serialize :value, coder: JSON
end end
+1 -1
View File
@@ -34,5 +34,5 @@ class EncryptedConfig < ApplicationRecord
encrypts :value encrypts :value
serialize :value, JSON serialize :value, coder: JSON
end end
+1 -1
View File
@@ -25,5 +25,5 @@ class EncryptedUserConfig < ApplicationRecord
encrypts :value encrypts :value
serialize :value, JSON serialize :value, coder: JSON
end end
+6 -6
View File
@@ -5,7 +5,7 @@
# Table name: submissions # Table name: submissions
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# deleted_at :datetime # archived_at :datetime
# preferences :text not null # preferences :text not null
# slug :string not null # slug :string not null
# source :text not null # source :text not null
@@ -39,10 +39,10 @@ class Submission < ApplicationRecord
attribute :preferences, :string, default: -> { {} } attribute :preferences, :string, default: -> { {} }
serialize :template_fields, JSON serialize :template_fields, coder: JSON
serialize :template_schema, JSON serialize :template_schema, coder: JSON
serialize :template_submitters, JSON serialize :template_submitters, coder: JSON
serialize :preferences, JSON serialize :preferences, coder: JSON
attribute :source, :string, default: 'link' attribute :source, :string, default: 'link'
attribute :submitters_order, :string, default: 'random' attribute :submitters_order, :string, default: 'random'
@@ -55,7 +55,7 @@ class Submission < ApplicationRecord
->(e) { where(uuid: (e.template_schema.presence || e.template.schema).pluck('attachment_uuid')) }, ->(e) { where(uuid: (e.template_schema.presence || e.template.schema).pluck('attachment_uuid')) },
through: :template, source: :documents_attachments through: :template, source: :documents_attachments
scope :active, -> { where(deleted_at: nil) } scope :active, -> { where(archived_at: nil) }
scope :pending, -> { joins(:submitters).where(submitters: { completed_at: nil }).distinct } scope :pending, -> { joins(:submitters).where(submitters: { completed_at: nil }).distinct }
scope :completed, -> { where.not(id: pending.select(:submission_id)) } scope :completed, -> { where.not(id: pending.select(:submission_id)) }
+1 -1
View File
@@ -31,7 +31,7 @@ class SubmissionEvent < ApplicationRecord
attribute :data, :string, default: -> { {} } attribute :data, :string, default: -> { {} }
attribute :event_timestamp, :datetime, default: -> { Time.current } attribute :event_timestamp, :datetime, default: -> { Time.current }
serialize :data, JSON serialize :data, coder: JSON
before_validation :set_submission_id, on: :create before_validation :set_submission_id, on: :create
+2 -2
View File
@@ -41,8 +41,8 @@ class Submitter < ApplicationRecord
attribute :preferences, :string, default: -> { {} } attribute :preferences, :string, default: -> { {} }
attribute :slug, :string, default: -> { SecureRandom.base58(14) } attribute :slug, :string, default: -> { SecureRandom.base58(14) }
serialize :values, JSON serialize :values, coder: JSON
serialize :preferences, JSON serialize :preferences, coder: JSON
has_many_attached :documents has_many_attached :documents
has_many_attached :attachments has_many_attached :attachments
+6 -6
View File
@@ -6,7 +6,7 @@
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# application_key :string # application_key :string
# deleted_at :datetime # archived_at :datetime
# fields :text not null # fields :text not null
# name :string not null # name :string not null
# schema :text not null # schema :text not null
@@ -47,9 +47,9 @@ class Template < ApplicationRecord
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 :fields, JSON serialize :fields, coder: JSON
serialize :schema, JSON serialize :schema, coder: JSON
serialize :submitters, JSON serialize :submitters, coder: JSON
has_many_attached :documents has_many_attached :documents
@@ -58,8 +58,8 @@ class Template < ApplicationRecord
has_many :submissions, dependent: :destroy has_many :submissions, dependent: :destroy
scope :active, -> { where(deleted_at: nil) } scope :active, -> { where(archived_at: nil) }
scope :archived, -> { where.not(deleted_at: nil) } scope :archived, -> { where.not(archived_at: nil) }
private private
+9 -9
View File
@@ -4,13 +4,13 @@
# #
# Table name: template_folders # Table name: template_folders
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# deleted_at :datetime # archived_at :datetime
# name :string not null # name :string not null
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# account_id :bigint not null # account_id :bigint not null
# author_id :bigint not null # author_id :bigint not null
# #
# Indexes # Indexes
# #
@@ -29,10 +29,10 @@ class TemplateFolder < ApplicationRecord
belongs_to :account belongs_to :account
has_many :templates, dependent: :destroy, foreign_key: :folder_id, inverse_of: :folder has_many :templates, dependent: :destroy, foreign_key: :folder_id, inverse_of: :folder
has_many :active_templates, -> { where(deleted_at: nil) }, has_many :active_templates, -> { where(archived_at: nil) },
class_name: 'Template', dependent: :destroy, foreign_key: :folder_id, inverse_of: :folder class_name: 'Template', dependent: :destroy, foreign_key: :folder_id, inverse_of: :folder
scope :active, -> { where(deleted_at: nil) } scope :active, -> { where(archived_at: nil) }
def default? def default?
name == DEFAULT_NAME name == DEFAULT_NAME
+5 -3
View File
@@ -5,10 +5,10 @@
# Table name: users # Table name: users
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# archived_at :datetime
# consumed_timestep :integer # consumed_timestep :integer
# current_sign_in_at :datetime # current_sign_in_at :datetime
# current_sign_in_ip :string # current_sign_in_ip :string
# deleted_at :datetime
# email :string not null # email :string not null
# encrypted_password :string not null # encrypted_password :string not null
# failed_attempts :integer default(0), not null # failed_attempts :integer default(0), not null
@@ -49,6 +49,8 @@ class User < ApplicationRecord
EMAIL_REGEXP = /[^@;,<>\s]+@[^@;,<>\s]+/ EMAIL_REGEXP = /[^@;,<>\s]+@[^@;,<>\s]+/
has_one_attached :signature
belongs_to :account belongs_to :account
has_one :access_token, dependent: :destroy has_one :access_token, dependent: :destroy
has_many :templates, dependent: :destroy, foreign_key: :author_id, inverse_of: :author has_many :templates, dependent: :destroy, foreign_key: :author_id, inverse_of: :author
@@ -62,7 +64,7 @@ class User < ApplicationRecord
attribute :role, :string, default: ADMIN_ROLE attribute :role, :string, default: ADMIN_ROLE
attribute :uuid, :string, default: -> { SecureRandom.uuid } attribute :uuid, :string, default: -> { SecureRandom.uuid }
scope :active, -> { where(deleted_at: nil) } scope :active, -> { where(archived_at: nil) }
scope :admins, -> { where(role: ADMIN_ROLE) } scope :admins, -> { where(role: ADMIN_ROLE) }
def access_token def access_token
@@ -70,7 +72,7 @@ class User < ApplicationRecord
end end
def active_for_authentication? def active_for_authentication?
!deleted_at? !archived_at?
end end
def remember_me def remember_me
+1 -1
View File
@@ -25,5 +25,5 @@ class UserConfig < ApplicationRecord
belongs_to :user belongs_to :user
serialize :value, JSON serialize :value, coder: JSON
end end
+1 -1
View File
@@ -16,7 +16,7 @@
</div> </div>
</div> </div>
<% view_archived_html = capture do %> <% view_archived_html = capture do %>
<% if current_account.templates.where.not(deleted_at: nil).exists? %> <% if current_account.templates.where.not(archived_at: nil).exists? %>
<div> <div>
<a href="<%= templates_archived_index_path %>" class="link text-sm">View Archived</a> <a href="<%= templates_archived_index_path %>" class="link text-sm">View Archived</a>
</div> </div>
+22 -1
View File
@@ -121,7 +121,28 @@
<%= f.button button_title(title: 'Save', disabled_with: 'Updating'), class: 'base-button' %> <%= f.button button_title(title: 'Save', disabled_with: 'Updating'), class: 'base-button' %>
</div> </div>
<% end %> <% end %>
</div>
<% end %>
<% if !Docuseal.multitenant? %>
<% account_config = AccountConfig.where(account: current_account, key: AccountConfig::ESIGNING_PREFERENCE_KEY).first_or_initialize(value: 'multiple') %>
<% if can?(:manage, account_config) %>
<div class="px-1 mt-8 max-w-xl">
<div class="flex justify-between items-end mb-4 mt-8">
<h2 class="text-3xl font-bold">Preferences</h2>
</div>
<% 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>
Apply multiple PDF digital signatures in the document per each signer
</span>
<%= f.check_box :value, { class: 'toggle', checked: account_config.value == 'multiple', onchange: 'this.form.requestSubmit()' }, 'multiple', 'single' %>
</div>
<% end %>
<% end %>
</div>
<% end %> <% end %>
</div> <% end %>
</div> </div>
</div> </div>
+1 -1
View File
@@ -1,7 +1,7 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0"> <div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
<%= render 'shared/settings_nav' %> <%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto"> <div class="flex-grow max-w-xl mx-auto">
<h1 class="text-4xl font-bold mb-4">SMS</h1> <h1 class="text-4xl font-bold mb-4">SAML SSO</h1>
<%= render 'placeholder' %> <%= render 'placeholder' %>
</div> </div>
<div class="w-0 md:w-52"></div> <div class="w-0 md:w-52"></div>
+1 -1
View File
@@ -26,7 +26,7 @@
<% end %> <% end %>
<% if @template.submitters.to_a.size == 1 %> <% if @template.submitters.to_a.size == 1 %>
<div> <div>
<%= button_to button_title(title: 'Resubmit', disabled_with: 'Resubmit', icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@template.slug), params: { submitter: { email: params[:email] }, resubmit: true }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %> <%= button_to button_title(title: 'Resubmit', disabled_with: 'Resubmit', icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@template.slug), params: { submitter: { email: params[:email] }, resubmit: @submitter.slug }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
</div> </div>
<% end %> <% end %>
</div> </div>
+3 -3
View File
@@ -3,7 +3,7 @@
<div class="space-y-6"> <div class="space-y-6">
<div class="text-center w-full space-y-6"> <div class="text-center w-full space-y-6">
<%= render 'banner' %> <%= render 'banner' %>
<% unless @template.deleted_at? %> <% unless @template.archived_at? %>
<p class="text-xl font-semibold text-center">You have been invited to submit a form</p> <p class="text-xl font-semibold text-center">You have been invited to submit a form</p>
<% end %> <% end %>
</div> </div>
@@ -14,7 +14,7 @@
</div> </div>
<div> <div>
<p class="text-lg font-bold mb-1"><%= @template.name %></p> <p class="text-lg font-bold mb-1"><%= @template.name %></p>
<% if @template.deleted_at? %> <% if @template.archived_at? %>
<p class="text-sm">Form has been deleted by <span class="font-semibold"><%= @template.account.name %></span>.</p> <p class="text-sm">Form has been deleted by <span class="font-semibold"><%= @template.account.name %></span>.</p>
<% else %> <% else %>
<p class="text-sm">Invited by <span class="font-semibold"><%= @template.account.name %></span></p> <p class="text-sm">Invited by <span class="font-semibold"><%= @template.account.name %></span></p>
@@ -23,7 +23,7 @@
</div> </div>
</div> </div>
</div> </div>
<% unless @template.deleted_at? %> <% unless @template.archived_at? %>
<%= form_for @submitter, url: start_form_path(@template.slug), data: { turbo_frame: :_top }, method: :put, html: { class: 'space-y-4', onsubmit: 'event.submitter.disabled = true' } do |f| %> <%= form_for @submitter, url: start_form_path(@template.slug), data: { turbo_frame: :_top }, method: :put, html: { class: 'space-y-4', onsubmit: 'event.submitter.disabled = true' } do |f| %>
<div class="form-control !mt-0"> <div class="form-control !mt-0">
<%= f.label :email, class: 'label' %> <%= f.label :email, class: 'label' %>
+2 -2
View File
@@ -1,5 +1,5 @@
<field-value class="flex absolute text-[1.5vw] lg:text-base" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%"> <field-value class="flex absolute text-[1.5vw] lg:text-base" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%">
<% if field['type'].in?(['signature', 'image', 'initials']) %> <% if field['type'].in?(['signature', 'image', 'initials', 'stamp']) %>
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy"> <img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy">
<% elsif field['type'].in?(['file', 'payment']) %> <% elsif field['type'].in?(['file', 'payment']) %>
<autosize-field></autosize-field> <autosize-field></autosize-field>
@@ -22,7 +22,7 @@
<%= svg_icon('check', class: "aspect-square #{area['w'] > area['h'] ? '!w-auto !h-full' : '!w-full !h-auto'}") %> <%= svg_icon('check', class: "aspect-square #{area['w'] > area['h'] ? '!w-auto !h-full' : '!w-full !h-auto'}") %>
</div> </div>
<% end %> <% end %>
<% elsif field['type'] == 'cells' %> <% elsif field['type'] == 'cells' && area['cell_w'] %>
<% cell_width = area['cell_w'] / area['w'] * 100 %> <% cell_width = area['cell_w'] / area['w'] * 100 %>
<div class="w-full flex items-center"> <div class="w-full flex items-center">
<% (0..(area['w'] / area['cell_w']).ceil).each do |index| %> <% (0..(area['w'] / area['cell_w']).ceil).each do |index| %>
+1 -1
View File
@@ -146,7 +146,7 @@
<div class="w-full bg-base-300 py-1"> <div class="w-full bg-base-300 py-1">
<img class="object-contain mx-auto" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy"> <img class="object-contain mx-auto" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
</div> </div>
<% elsif field['type'] == 'image' %> <% elsif field['type'].in?(['image', 'stamp']) %>
<img class="object-contain mx-auto max-h-28" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy"> <img class="object-contain mx-auto max-h-28" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
<% elsif field['type'] == 'file' || field['type'] == 'payment' %> <% elsif field['type'] == 'file' || field['type'] == 'payment' %>
<div class="flex flex-col justify-center"> <div class="flex flex-col justify-center">
+1 -1
View File
@@ -39,7 +39,7 @@
<% if @submitter.submission.template.submitters.size == 1 %> <% if @submitter.submission.template.submitters.size == 1 %>
<div class="divider">OR</div> <div class="divider">OR</div>
<div> <div>
<%= button_to button_title(title: 'Resubmit', disabled_with: 'Resubmit', icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@submitter.submission.template.slug), params: { submitter: { email: @submitter.email, phone: @submitter.phone, name: @submitter.name }, resubmit: true }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %> <%= button_to button_title(title: 'Resubmit', disabled_with: 'Resubmit', icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@submitter.submission.template.slug), params: { submitter: { email: @submitter.email, phone: @submitter.phone, name: @submitter.name }, resubmit: @submitter.slug }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
</div> </div>
<% end %> <% end %>
</div> </div>
+1 -1
View File
@@ -24,7 +24,7 @@
<% end %> <% end %>
<% if can?(:create, ::Template) %> <% if can?(:create, ::Template) %>
<%= render 'templates/upload_button', folder_name: @template_folder.name %> <%= render 'templates/upload_button', folder_name: @template_folder.name %>
<%= link_to new_template_path(folder_name: @template_folder.name), class: 'btn btn-primary text-base btn-md gap-2', data: { turbo_frame: :modal } do %> <%= link_to new_template_path(folder_name: @template_folder.name), class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %> <%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span class="hidden md:block">Create</span> <span class="hidden md:block">Create</span>
<% end %> <% end %>
+3 -3
View File
@@ -1,7 +1,7 @@
<% status_badges = { 'awaiting' => 'badge-info', 'sent' => 'badge-info', 'completed' => 'badge-success', 'opened' => 'badge-warning' } %> <% status_badges = { 'awaiting' => 'badge-info', 'sent' => 'badge-info', 'completed' => 'badge-success', 'opened' => 'badge-warning' } %>
<a href="<%= submission_path(submission) %>" class="bg-base-200 w-full flex flex-col md:flex-row space-y-4 md:space-y-0 md:justify-between rounded-2xl px-6 py-5 md:items-center"> <a href="<%= submission_path(submission) %>" class="bg-base-200 w-full flex flex-col md:flex-row space-y-4 md:space-y-0 md:justify-between rounded-2xl px-6 py-5 md:items-center">
<% submitters = (submission.template_submitters || submission.template.submitters).filter_map { |item| submission.submitters.find { |e| e.uuid == item['uuid'] } } %> <% submitters = (submission.template_submitters || submission.template.submitters).filter_map { |item| submission.submitters.find { |e| e.uuid == item['uuid'] } } %>
<% is_submission_completed = submitters.all?(&:completed_at?) %> <% is_submission_completed = submitters.all?(&:completed_at?) && submitters.size.positive? %>
<% if submitters.size == 1 %> <% if submitters.size == 1 %>
<div> <div>
<% submitter = submitters.first %> <% submitter = submitters.first %>
@@ -49,7 +49,7 @@
<% end %> <% end %>
<% end %> <% end %>
<span class="btn btn-outline btn-sm w-20 md:w-24">View</span> <span class="btn btn-outline btn-sm w-20 md:w-24">View</span>
<% if !submission.deleted_at? && can?(:destroy, submission) %> <% if !submission.archived_at? && can?(:destroy, submission) %>
<%= button_to button_title(title: nil, disabled_with: 'Remov', icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm', form: { class: 'flex' }, title: 'Delete', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %> <%= button_to button_title(title: nil, disabled_with: 'Remov', icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm', form: { class: 'flex' }, title: 'Delete', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %>
<% end %> <% end %>
</div> </div>
@@ -134,7 +134,7 @@
</form> </form>
<% end %> <% end %>
<span class="btn btn-outline btn-sm w-20 md:w-24">View</span> <span class="btn btn-outline btn-sm w-20 md:w-24">View</span>
<% unless submission.deleted_at? %> <% unless submission.archived_at? %>
<%= button_to button_title(title: nil, disabled_with: 'Remov', icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm', form: { class: 'flex' }, title: 'Delete', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %> <%= button_to button_title(title: nil, disabled_with: 'Remov', icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm', form: { class: 'flex' }, title: 'Delete', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %>
<% end %> <% end %>
</div> </div>
+6 -6
View File
@@ -13,7 +13,7 @@
<%= svg_icon('calendar', class: 'w-4 h-4') %> <%= svg_icon('calendar', class: 'w-4 h-4') %>
<span><%= l(template.created_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %></span> <span><%= l(template.created_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %></span>
</span> </span>
<% if template.deleted_at? %> <% if template.archived_at? %>
<span class="flex items-center space-x-1 w-1/2"> <span class="flex items-center space-x-1 w-1/2">
<%= svg_icon('folder', class: 'w-4 h-4 flex-shrink-0') %> <%= svg_icon('folder', class: 'w-4 h-4 flex-shrink-0') %>
<span class="truncate"><%= template.folder.name %></span> <span class="truncate"><%= template.folder.name %></span>
@@ -24,14 +24,14 @@
</a> </a>
<div class="absolute top-0 bottom-0 w-0 flex items-center hidden md:group-hover:flex" style="right: 37px"> <div class="absolute top-0 bottom-0 w-0 flex items-center hidden md:group-hover:flex" style="right: 37px">
<div class="space-y-1"> <div class="space-y-1">
<% if can?(:update, template) && !template.deleted_at? %> <% if can?(:update, template) && !template.archived_at? %>
<span class="tooltip tooltip-left" data-tip="Move"> <span class="tooltip tooltip-left" data-tip="Move">
<a href="<%= edit_template_folder_path(template.id) %>" data-turbo-frame="modal" class="btn btn-xs hover:btn-outline bg-base-200 btn-circle"> <a href="<%= edit_template_folder_path(template.id) %>" data-turbo-frame="modal" class="btn btn-xs hover:btn-outline bg-base-200 btn-circle">
<%= svg_icon('folder_share', class: 'w-4 h-4') %> <%= svg_icon('folder_share', class: 'w-4 h-4') %>
</a> </a>
</span> </span>
<% end %> <% end %>
<% if template.deleted_at? && can?(:update, template) %> <% if template.archived_at? && can?(:update, template) %>
<span class="tooltip tooltip-left" data-tip="Restore"> <span class="tooltip tooltip-left" data-tip="Restore">
<%= button_to template_restore_index_path(template), class: 'btn btn-xs hover:btn-outline bg-base-200 btn-circle' do %> <%= button_to template_restore_index_path(template), class: 'btn btn-xs hover:btn-outline bg-base-200 btn-circle' do %>
<%= svg_icon('rotate', class: 'w-4 h-4 enabled') %> <%= svg_icon('rotate', class: 'w-4 h-4 enabled') %>
@@ -53,9 +53,9 @@
</span> </span>
<% end %> <% end %>
<% if can?(:destroy, template) %> <% if can?(:destroy, template) %>
<span class="tooltip tooltip-left" data-tip="<%= template.deleted_at? ? 'Delete' : 'Archive' %>"> <span class="tooltip tooltip-left" data-tip="<%= template.archived_at? ? 'Delete' : 'Archive' %>">
<%= button_to template_path(template), data: { turbo_confirm: template.deleted_at? ? 'Template deletion is irreversible and will permanently remove all associated signed documents with it. Are you sure?' : 'Are you sure?' }, params: { permanently: template.deleted_at? }.compact_blank, method: :delete, class: 'btn btn-xs hover:btn-outline bg-base-200 btn-circle', aria_label: 'Restore' do %> <%= button_to template_path(template), data: { turbo_confirm: template.archived_at? ? 'Template deletion is irreversible and will permanently remove all associated signed documents with it. Are you sure?' : 'Are you sure?' }, params: { permanently: template.archived_at? }.compact_blank, method: :delete, class: 'btn btn-xs hover:btn-outline bg-base-200 btn-circle', aria_label: 'Restore' do %>
<%= svg_icon(template.deleted_at? ? 'trash' : 'archive', class: 'w-4 h-4 enabled') %> <%= svg_icon(template.archived_at? ? 'trash' : 'archive', class: 'w-4 h-4 enabled') %>
<%= svg_icon('loader', class: 'w-4 h-4 animate-spin disabled') %> <%= svg_icon('loader', class: 'w-4 h-4 animate-spin disabled') %>
<% end %> <% end %>
</span> </span>
+5 -5
View File
@@ -2,7 +2,7 @@
<div> <div>
<h1 class="text-4xl font-semibold mr-4" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;"> <h1 class="text-4xl font-semibold mr-4" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;">
<%= template.name %> <%= template.name %>
<% if template.deleted_at? %> <% if template.archived_at? %>
<span class="badge badge-outline badge-lg align-middle">Archived</span> <span class="badge badge-outline badge-lg align-middle">Archived</span>
<% end %> <% end %>
</h1> </h1>
@@ -23,10 +23,10 @@
</div> </div>
</div> </div>
<div class="flex md:justify-between space-x-2 flex-none pt-1"> <div class="flex md:justify-between space-x-2 flex-none pt-1">
<% if !template.deleted_at? %> <% if !template.archived_at? %>
<%= render 'shared/clipboard_copy', text: start_form_url(slug: @template.slug), class: 'btn btn-sm btn-neutral text-white', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Link', copied_title: 'Copied', copy_title_md: 'Link', copied_title_md: 'Copied' %> <%= render 'shared/clipboard_copy', text: start_form_url(slug: @template.slug), class: 'btn btn-sm btn-neutral text-white', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Link', copied_title: 'Copied', copy_title_md: 'Link', copied_title_md: 'Copied' %>
<% end %> <% end %>
<% if !template.deleted_at? && can?(:destroy, template) %> <% if !template.archived_at? && can?(:destroy, template) %>
<%= button_to button_title(title: 'Archive', disabled_with: 'Archiving', title_class: 'hidden md:inline', icon: svg_icon('archive', class: 'w-6 h-6')), template_path(template), class: 'btn btn-outline btn-sm', method: :delete, data: { turbo_confirm: 'Are you sure?' } %> <%= button_to button_title(title: 'Archive', disabled_with: 'Archiving', title_class: 'hidden md:inline', icon: svg_icon('archive', class: 'w-6 h-6')), template_path(template), class: 'btn btn-outline btn-sm', method: :delete, data: { turbo_confirm: 'Are you sure?' } %>
<% end %> <% end %>
<% if can?(:create, template) %> <% if can?(:create, template) %>
@@ -35,7 +35,7 @@
<span class="hidden md:inline">Clone</span> <span class="hidden md:inline">Clone</span>
<% end %> <% end %>
<% end %> <% end %>
<% if !template.deleted_at? && can?(:update, template) %> <% if !template.archived_at? && can?(:update, template) %>
<%= link_to edit_template_path(template), class: 'btn btn-outline btn-sm' do %> <%= link_to edit_template_path(template), class: 'btn btn-outline btn-sm' do %>
<span class="flex items-center justify-center space-x-2"> <span class="flex items-center justify-center space-x-2">
<%= svg_icon('pencil', class: 'w-6 h-6') %> <%= svg_icon('pencil', class: 'w-6 h-6') %>
@@ -43,7 +43,7 @@
</span> </span>
<% end %> <% end %>
<% end %> <% end %>
<% if template.deleted_at? && can?(:create, template) %> <% if template.archived_at? && can?(:create, template) %>
<%= button_to button_title(title: 'Restore', disabled_with: 'Restoring', icon: svg_icon('rotate', class: 'w-6 h-6')), template_restore_index_path(template), class: 'btn btn-outline btn-sm' %> <%= button_to button_title(title: 'Restore', disabled_with: 'Restoring', icon: svg_icon('rotate', class: 'w-6 h-6')), template_restore_index_path(template), class: 'btn btn-outline btn-sm' %>
<% end %> <% end %>
</div> </div>
+3 -3
View File
@@ -11,7 +11,7 @@
<%= svg_icon('download', class: 'w-6 h-6 stroke-2') %> <%= svg_icon('download', class: 'w-6 h-6 stroke-2') %>
<span>Export</span> <span>Export</span>
<% end %> <% end %>
<% if !@template.deleted_at? && can?(:create, Submission) %> <% if !@template.archived_at? && can?(:create, Submission) %>
<%= link_to new_template_submission_path(@template), class: 'white-button !border', data: { turbo_frame: 'modal' } do %> <%= link_to new_template_submission_path(@template), class: 'white-button !border', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %> <%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span>Add <span class="hidden md:inline">Recipients</span></span> <span>Add <span class="hidden md:inline">Recipients</span></span>
@@ -56,7 +56,7 @@
<%= render partial: 'submission', collection: @submissions %> <%= render partial: 'submission', collection: @submissions %>
</div> </div>
<% view_archived_html = capture do %> <% view_archived_html = capture do %>
<% if @template.submissions.where.not(deleted_at: nil).exists? && !@template.deleted_at? %> <% if @template.submissions.where.not(archived_at: nil).exists? && !@template.archived_at? %>
<div> <div>
<a href="<%= template_archived_index_path(@template) %>" class="link text-sm">View Archived</a> <a href="<%= template_archived_index_path(@template) %>" class="link text-sm">View Archived</a>
</div> </div>
@@ -74,7 +74,7 @@
<div class="card-body text-center py-16"> <div class="card-body text-center py-16">
<div class="max-w-lg mx-auto"> <div class="max-w-lg mx-auto">
<p class="text-3xl font-bold text-base-content mb-4">There are no Submissions</p> <p class="text-3xl font-bold text-base-content mb-4">There are no Submissions</p>
<% if @template.deleted_at.blank? && params[:q].blank? %> <% if @template.archived_at.blank? && params[:q].blank? %>
<p>Send an invitation to fill and complete the form</p> <p>Send an invitation to fill and complete the form</p>
<div class="space-y-2 flex flex-col"> <div class="space-y-2 flex flex-col">
<% if can?(:create, Submission) %> <% if can?(:create, Submission) %>
+1 -1
View File
@@ -5,7 +5,7 @@ require "fileutils"
APP_ROOT = File.expand_path("..", __dir__) APP_ROOT = File.expand_path("..", __dir__)
def system!(*args) def system!(*args)
system(*args) || abort("\n== Command #{args} failed ==") system(*args, exception: true)
end end
FileUtils.chdir APP_ROOT do FileUtils.chdir APP_ROOT do
+8 -4
View File
@@ -8,6 +8,7 @@ require 'action_controller/railtie'
require 'action_view/railtie' require 'action_view/railtie'
require 'action_mailer/railtie' require 'action_mailer/railtie'
require 'active_job/railtie' require 'active_job/railtie'
require 'rails/health_controller'
require_relative '../lib/api_path_consider_json_middleware' require_relative '../lib/api_path_consider_json_middleware'
@@ -15,14 +16,13 @@ Bundler.require(*Rails.groups)
module DocuSeal module DocuSeal
class Application < Rails::Application class Application < Rails::Application
config.load_defaults 7.0 config.load_defaults 7.1
config.autoload_paths << Rails.root.join('lib') config.autoload_lib(ignore: %w[assets tasks])
config.eager_load_paths << Rails.root.join('lib')
config.active_storage.routes_prefix = '' config.active_storage.routes_prefix = ''
config.i18n.available_locales = %i[en en-US en-GB es-ES fr-FR pt-PT de-DE] config.i18n.available_locales = %i[en en-US en-GB es-ES fr-FR pt-PT de-DE pt]
config.i18n.fallbacks = [:en] config.i18n.fallbacks = [:en]
config.exceptions_app = ->(env) { ErrorsController.action(:show).call(env) } config.exceptions_app = ->(env) { ErrorsController.action(:show).call(env) }
@@ -32,6 +32,10 @@ module DocuSeal
config.middleware.insert_before ActionDispatch::Static, Rack::Deflater config.middleware.insert_before ActionDispatch::Static, Rack::Deflater
config.middleware.insert_before ActionDispatch::Static, ApiPathConsiderJsonMiddleware config.middleware.insert_before ActionDispatch::Static, ApiPathConsiderJsonMiddleware
config.generators.system_tests = nil
autoloaders.once.do_not_eager_load("#{Turbo::Engine.root}/app/channels") # https://github.com/hotwired/turbo-rails/issues/512
ActiveSupport.run_load_hooks(:application_config, self) ActiveSupport.run_load_hooks(:application_config, self)
end end
end end
+3 -1
View File
@@ -17,7 +17,7 @@ Rails.application.configure do
# In the development environment your application's code is reloaded any time # In the development environment your application's code is reloaded any time
# it changes. This slows down response time but is perfect for development # it changes. This slows down response time but is perfect for development
# since you don't have to restart the web server when you make code changes. # since you don't have to restart the web server when you make code changes.
config.cache_classes = false config.enable_reloading = true
# Do not eager load code on boot. # Do not eager load code on boot.
config.eager_load = false config.eager_load = false
@@ -79,6 +79,8 @@ Rails.application.configure do
key_derivation_salt: 'dev key derivation salt' key_derivation_salt: 'dev key derivation salt'
} }
ActiveRecord::Encryption.configure(**config.active_record.encryption)
# Raises error for missing translations. # Raises error for missing translations.
# config.i18n.raise_on_missing_translations = true # config.i18n.raise_on_missing_translations = true
+3 -1
View File
@@ -7,7 +7,7 @@ Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb. # Settings specified here will take precedence over those in config/application.rb.
# Code is not reloaded between requests. # Code is not reloaded between requests.
config.cache_classes = true config.enable_reloading = false
config.public_file_server.headers = { config.public_file_server.headers = {
'Cache-Control' => 'public, s-maxage=31536000, max-age=15552000', 'Cache-Control' => 'public, s-maxage=31536000, max-age=15552000',
@@ -115,6 +115,8 @@ Rails.application.configure do
key_derivation_salt: Digest::SHA256.hexdigest(encryption_secret) key_derivation_salt: Digest::SHA256.hexdigest(encryption_secret)
} }
ActiveRecord::Encryption.configure(**config.active_record.encryption)
# Do not dump schema after migrations. # Do not dump schema after migrations.
config.active_record.dump_schema_after_migration = false config.active_record.dump_schema_after_migration = false
+3 -1
View File
@@ -11,7 +11,7 @@ Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb. # Settings specified here will take precedence over those in config/application.rb.
# Turn false under Spring and add config.action_view.cache_template_loading = true. # Turn false under Spring and add config.action_view.cache_template_loading = true.
config.cache_classes = true config.enable_reloading = false
# Eager loading loads your whole application. When running a single test locally, # Eager loading loads your whole application. When running a single test locally,
# this probably isn't necessary. It's a good idea to do in a continuous integration # this probably isn't necessary. It's a good idea to do in a continuous integration
@@ -67,6 +67,8 @@ Rails.application.configure do
key_derivation_salt: 'test key derivation salt' key_derivation_salt: 'test key derivation salt'
} }
ActiveRecord::Encryption.configure(**config.active_record.encryption)
config.middleware.use(Class.new do config.middleware.use(Class.new do
def initialize(app) def initialize(app)
@app = app @app = app
+2 -5
View File
@@ -1,9 +1,5 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative '../../lib/auth_with_token_strategy'
Warden::Strategies.add(:auth_token, AuthWithTokenStrategy)
Devise.otp_allowed_drift = 60.seconds Devise.otp_allowed_drift = 60.seconds
# Assuming you have not yet modified this file, each configuration option below # Assuming you have not yet modified this file, each configuration option below
@@ -24,6 +20,7 @@ Devise.setup do |config|
# confirmation, reset password and unlock tokens in the database. # confirmation, reset password and unlock tokens in the database.
# Devise will use the `secret_key_base` as its `secret_key` # Devise will use the `secret_key_base` as its `secret_key`
# by default. You can change it below and use your own secret key. # by default. You can change it below and use your own secret key.
config.secret_key = Rails.application.secret_key_base
# ==> Controller configuration # ==> Controller configuration
# Configure the parent class to the devise controllers. # Configure the parent class to the devise controllers.
@@ -279,7 +276,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)
end end
# ==> Mountable engine configurations # ==> Mountable engine configurations
@@ -1,3 +1,4 @@
# frozen_string_literal: true # frozen_string_literal: true
Rails.application.config.filter_parameters += %i[password token otp_attempt] Rails.application.config.filter_parameters += %i[password token otp_attempt passw secret token _key crypt salt
certificate otp ssn]
+20
View File
@@ -1,9 +1,29 @@
en-US: en-US:
digitally_signed_by: Digitally signed by
role: Role
date: date:
formats: formats:
default: "%m/%d/%Y" default: "%m/%d/%Y"
en-GB: en-GB:
digitally_signed_by: Digitally signed by
role: Role
date: date:
formats: formats:
default: "%d/%m/%Y" default: "%d/%m/%Y"
es-ES:
digitally_signed_by: Firmado digitalmente por
role: Rol
fr-FR:
digitally_signed_by: Signé numériquement par
role: Rôle
pt-PT:
digitally_signed_by: Assinado digitalmente por
role: Função
de-DE:
digitally_signed_by: Digital signiert von
role: Rolle
+1 -1
View File
@@ -24,7 +24,7 @@ port ENV.fetch('PORT', 3000)
environment ENV.fetch('RAILS_ENV', 'development') environment ENV.fetch('RAILS_ENV', 'development')
# Specifies the `pidfile` that Puma will use. # Specifies the `pidfile` that Puma will use.
pidfile ENV.fetch('PIDFILE', "#{Dir.tmpdir}/server.pid") # pidfile ENV.fetch('PIDFILE', "#{Dir.tmpdir}/server.pid")
# Specifies the number of `workers` to boot in clustered mode. # Specifies the number of `workers` to boot in clustered mode.
# Workers are forked web server processes. If using threads and workers together # Workers are forked web server processes. If using threads and workers together
+2
View File
@@ -6,6 +6,8 @@ Rails.application.routes.draw do
root 'dashboard#index' root 'dashboard#index'
get 'up' => 'rails/health#show'
devise_for :users, devise_for :users,
path: '/', only: %i[sessions passwords omniauth_callbacks], path: '/', only: %i[sessions passwords omniauth_callbacks],
controllers: begin controllers: begin
@@ -0,0 +1,10 @@
# frozen_string_literal: true
class RenameDeletedAtToArchivedAt < ActiveRecord::Migration[7.1]
def change
rename_column :templates, :deleted_at, :archived_at
rename_column :submissions, :deleted_at, :archived_at
rename_column :users, :deleted_at, :archived_at
rename_column :template_folders, :deleted_at, :archived_at
end
end
+5 -5
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.0].define(version: 2023_11_22_212612) do ActiveRecord::Schema[7.1].define(version: 2023_12_29_220819) 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"
@@ -130,7 +130,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_11_22_212612) do
create_table "submissions", force: :cascade do |t| create_table "submissions", force: :cascade do |t|
t.bigint "template_id", null: false t.bigint "template_id", null: false
t.bigint "created_by_user_id" t.bigint "created_by_user_id"
t.datetime "deleted_at" t.datetime "archived_at"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.text "template_fields" t.text "template_fields"
@@ -171,7 +171,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_11_22_212612) do
t.string "name", null: false t.string "name", null: false
t.bigint "author_id", null: false t.bigint "author_id", null: false
t.bigint "account_id", null: false t.bigint "account_id", null: false
t.datetime "deleted_at" t.datetime "archived_at"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_template_folders_on_account_id" t.index ["account_id"], name: "index_template_folders_on_account_id"
@@ -186,7 +186,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_11_22_212612) do
t.text "submitters", null: false t.text "submitters", null: false
t.bigint "author_id", null: false t.bigint "author_id", null: false
t.bigint "account_id", null: false t.bigint "account_id", null: false
t.datetime "deleted_at" t.datetime "archived_at"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.text "source", null: false t.text "source", null: false
@@ -226,7 +226,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_11_22_212612) do
t.integer "failed_attempts", default: 0, null: false t.integer "failed_attempts", default: 0, null: false
t.string "unlock_token" t.string "unlock_token"
t.datetime "locked_at" t.datetime "locked_at"
t.datetime "deleted_at" t.datetime "archived_at"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.text "uuid", null: false t.text "uuid", null: false
+1 -1
View File
@@ -19,7 +19,7 @@ module Accounts
new_template.account = new_account new_template.account = new_account
new_template.slug = SecureRandom.base58(14) new_template.slug = SecureRandom.base58(14)
new_template.deleted_at = nil new_template.archived_at = nil
new_template.save! new_template.save!
Templates::CloneAttachments.call(template: new_template, original_template: template) Templates::CloneAttachments.call(template: new_template, original_template: template)
-19
View File
@@ -1,19 +0,0 @@
# frozen_string_literal: true
class AuthWithTokenStrategy < Devise::Strategies::Base
def valid?
request.headers['X-Auth-Token'].present?
end
def authenticate!
sha256 = Digest::SHA256.hexdigest(request.headers['X-Auth-Token'])
user = User.joins(:access_token).find_by(access_token: { sha256: })
if user
success!(user)
else
fail!('Invalid token')
end
end
end
+2 -2
View File
@@ -4,10 +4,10 @@ module JsonWebToken
module_function module_function
def encode(payload) def encode(payload)
JWT.encode(payload, Rails.application.secrets.secret_key_base) JWT.encode(payload, Rails.application.secret_key_base)
end end
def decode(token) def decode(token)
JWT.decode(token, Rails.application.secrets.secret_key_base)[0] JWT.decode(token, Rails.application.secret_key_base)[0]
end end
end end
+15 -3
View File
@@ -9,14 +9,26 @@ module PdfIcons
module_function module_function
def check_io def check_io
@check_io ||= StringIO.new(PATH.join('check.png').read) StringIO.new(check_data)
end end
def paperclip_io def paperclip_io
@paperclip_io ||= StringIO.new(PATH.join('paperclip.png').read) StringIO.new(paperclip_data)
end end
def logo_io def logo_io
@logo_io ||= StringIO.new(PATH.join('logo.png').read) StringIO.new(logo_data)
end
def check_data
@check_data ||= PATH.join('check.png').read
end
def paperclip_data
@paperclip_data ||= PATH.join('paperclip.png').read
end
def logo_data
@logo_data ||= PATH.join('logo.png').read
end end
end end
+1 -1
View File
@@ -21,7 +21,7 @@ module SubmissionEvents
def build_tracking_param(submitter, event_type = 'click_email') def build_tracking_param(submitter, event_type = 'click_email')
Base64.urlsafe_encode64( Base64.urlsafe_encode64(
[submitter.slug, event_type, Rails.application.secrets.secret_key_base].join(':') [submitter.slug, event_type, Rails.application.secret_key_base].join(':')
).first(TRACKING_PARAM_LENGTH) ).first(TRACKING_PARAM_LENGTH)
end end
+20 -1
View File
@@ -46,6 +46,7 @@ module Submissions
submitter_uuid ||= find_submitter_uuid(submission.template, submitter_attrs, index) submitter_uuid ||= find_submitter_uuid(submission.template, submitter_attrs, index)
process_readonly_fields_param(submitter_attrs[:readonly_fields], template_fields, submitter_uuid) process_readonly_fields_param(submitter_attrs[:readonly_fields], template_fields, submitter_uuid)
process_field_values_param(submitter_attrs[:values], template_fields, submitter_uuid)
process_fields_param(submitter_attrs[:fields], template_fields, submitter_uuid) process_fields_param(submitter_attrs[:fields], template_fields, submitter_uuid)
end end
@@ -70,6 +71,21 @@ module Submissions
end end
end end
def process_field_values_param(values, template_fields, submitter_uuid)
return if values.blank?
template_fields.each do |f|
next if f['type'].in?(%w[signature image initials file])
next if f['submitter_uuid'] != submitter_uuid
value = values[f['uuid']]
next if value.blank?
f['default_value'] = value
end
end
def process_fields_param(fields, template_fields, submitter_uuid) def process_fields_param(fields, template_fields, submitter_uuid)
return if fields.blank? return if fields.blank?
@@ -83,6 +99,8 @@ module Submissions
next if field_configs.blank? next if field_configs.blank?
f['readonly'] = field_configs['readonly'] if field_configs['readonly'].present? f['readonly'] = field_configs['readonly'] if field_configs['readonly'].present?
f['default_value'] = field_configs['default_value'] if field_configs['default_value'].present? &&
!f['type'].in?(%w[signature image initials file])
next if field_configs['validation_pattern'].blank? next if field_configs['validation_pattern'].blank?
@@ -111,7 +129,8 @@ module Submissions
completed_at: attrs[:completed] ? Time.current : nil, completed_at: attrs[:completed] ? Time.current : nil,
sent_at: mark_as_sent && email.present? && is_order_sent ? Time.current : nil, sent_at: mark_as_sent && email.present? && is_order_sent ? Time.current : nil,
values: attrs[:values] || {}, values: attrs[:values] || {},
preferences: preferences.merge(submitter_preferences), preferences: preferences.merge(submitter_preferences)
.merge({ default_values: attrs[:values] }.compact_blank),
uuid: uuid:
) )
end end
+7 -5
View File
@@ -17,7 +17,6 @@ module Submissions
'Helvetica' 'Helvetica'
end end
INFO_CREATOR = "#{Docuseal.product_name} (#{Docuseal::PRODUCT_URL})".freeze
SIGN_REASON = 'Signed with DocuSeal.co' SIGN_REASON = 'Signed with DocuSeal.co'
VERIFIED_TEXT = 'Verified' VERIFIED_TEXT = 'Verified'
UNVERIFIED_TEXT = 'Unverified' UNVERIFIED_TEXT = 'Unverified'
@@ -192,7 +191,7 @@ module Submissions
} }
].compact_blank, line_spacing: 1.8, padding: [0, 0, 5, 0] ].compact_blank, line_spacing: 1.8, padding: [0, 0, 5, 0]
), ),
if field['type'].in?(%w[image signature initials]) if field['type'].in?(%w[image signature initials stamp])
attachment = submitter.attachments.find { |a| a.uuid == value } attachment = submitter.attachments.find { |a| a.uuid == value }
image = Vips::Image.new_from_buffer(attachment.download, '').autorot image = Vips::Image.new_from_buffer(attachment.download, '').autorot
@@ -257,7 +256,7 @@ module Submissions
if event.event_type.include?('sms') || event.event_type.include?('phone') if event.event_type.include?('sms') || event.event_type.include?('phone')
submitter.phone submitter.phone
else else
(submitter.name || submitter.email || submitter.phone) submitter.name || submitter.email || submitter.phone
end end
] ]
) )
@@ -268,7 +267,7 @@ module Submissions
io = StringIO.new io = StringIO.new
composer.document.trailer.info[:Creator] = INFO_CREATOR composer.document.trailer.info[:Creator] = "#{Docuseal.product_name} (#{Docuseal::PRODUCT_URL})"
sign_params = { sign_params = {
reason: SIGN_REASON, reason: SIGN_REASON,
@@ -277,7 +276,10 @@ module Submissions
certificate_chain: pkcs.ca_certs || [] certificate_chain: pkcs.ca_certs || []
} }
sign_params[:timestamp_handler] = Submissions::TimestampHandler.new(tsa_url:) if tsa_url if tsa_url
sign_params[:timestamp_handler] = Submissions::TimestampHandler.new(tsa_url:)
sign_params[:signature_size] = 10_000
end
composer.document.sign(io, **sign_params) composer.document.sign(io, **sign_params)
+56 -22
View File
@@ -10,8 +10,8 @@ module Submissions
'Helvetica' 'Helvetica'
end end
INFO_CREATOR = "#{Docuseal.product_name} (#{Docuseal::PRODUCT_URL})".freeze SIGN_REASON = 'Signed by %<name>s with DocuSeal.co'
SIGN_REASON = 'Signed by %<email>s with DocuSeal.co' SIGN_SIGNLE_REASON = 'Digitally signed with DocuSeal.co'
TEXT_LEFT_MARGIN = 1 TEXT_LEFT_MARGIN = 1
TEXT_TOP_MARGIN = 1 TEXT_TOP_MARGIN = 1
@@ -66,7 +66,7 @@ module Submissions
canvas.font(FONT_NAME, size: font_size) canvas.font(FONT_NAME, size: font_size)
case field['type'] case field['type']
when 'image', 'signature', 'initials' when 'image', 'signature', 'initials', 'stamp'
attachment = submitter.attachments.find { |a| a.uuid == value } attachment = submitter.attachments.find { |a| a.uuid == value }
image = Vips::Image.new_from_buffer(attachment.download, '').autorot image = Vips::Image.new_from_buffer(attachment.download, '').autorot
@@ -200,9 +200,9 @@ module Submissions
submitter.submission.template_schema.map do |item| submitter.submission.template_schema.map do |item|
pdf = pdfs_index[item['attachment_uuid']] pdf = pdfs_index[item['attachment_uuid']]
attachment = save_signed_pdf(pdf:, submitter:, pkcs:, tsa_url:, attachment = save_pdf(pdf:, submitter:, pkcs:, tsa_url:,
uuid: item['attachment_uuid'], uuid: item['attachment_uuid'],
name: item['name']) name: item['name'])
image_pdfs << pdf if original_documents.find { |a| a.uuid == item['attachment_uuid'] }.image? image_pdfs << pdf if original_documents.find { |a| a.uuid == item['attachment_uuid'] }.image?
@@ -217,9 +217,10 @@ module Submissions
end end
images_pdf_result = images_pdf_result =
save_signed_pdf( save_pdf(
pdf: images_pdf, pdf: images_pdf,
submitter:, submitter:,
tsa_url:,
pkcs:, pkcs:,
uuid: images_pdf_uuid(original_documents.select(&:image?)), uuid: images_pdf_uuid(original_documents.select(&:image?)),
name: template.name name: template.name
@@ -229,27 +230,34 @@ module Submissions
end end
# rubocop:enable Metrics # rubocop:enable Metrics
def save_signed_pdf(pdf:, submitter:, pkcs:, tsa_url:, uuid:, name:) def save_pdf(pdf:, submitter:, pkcs:, tsa_url:, uuid:, name:)
io = StringIO.new io = StringIO.new
pdf.trailer.info[:Creator] = info_creator pdf.trailer.info[:Creator] = info_creator
sign_params = { sign_reason = fetch_sign_reason(submitter)
reason: sign_reason(submitter.email),
certificate: pkcs.certificate,
key: pkcs.key,
certificate_chain: pkcs.ca_certs || []
}
sign_params[:timestamp_handler] = Submissions::TimestampHandler.new(tsa_url:) if tsa_url if sign_reason
sign_params = {
reason: sign_reason,
certificate: pkcs.certificate,
key: pkcs.key,
certificate_chain: pkcs.ca_certs || []
}
pdf.sign(io, **sign_params) if tsa_url
sign_params[:timestamp_handler] = Submissions::TimestampHandler.new(tsa_url:)
sign_params[:signature_size] = 10_000
end
pdf.sign(io, **sign_params)
else
pdf.write(io, incremental: true)
end
ActiveStorage::Attachment.create!( ActiveStorage::Attachment.create!(
uuid:, uuid:,
blob: ActiveStorage::Blob.create_and_upload!( blob: ActiveStorage::Blob.create_and_upload!(io: StringIO.new(io.string), filename: "#{name}.pdf"),
io: StringIO.new(io.string), filename: "#{name}.pdf"
),
metadata: { sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(io.string)) }, metadata: { sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(io.string)) },
name: 'documents', name: 'documents',
record: submitter record: submitter
@@ -304,12 +312,38 @@ module Submissions
pdf pdf
end end
def sign_reason(email) def sign_reason(name)
format(SIGN_REASON, email:) format(SIGN_REASON, name:)
end
def single_sign_reason
SIGN_SIGNLE_REASON
end
def fetch_sign_reason(submitter)
reason_name = submitter.email || submitter.name || submitter.phone
return sign_reason(reason_name) if Docuseal.multitenant?
config =
if Docuseal.multitenant?
AccountConfig.where(account: submitter.account, key: AccountConfig::ESIGNING_PREFERENCE_KEY)
.first_or_initialize(value: 'multiple')
else
AccountConfig.where(key: AccountConfig::ESIGNING_PREFERENCE_KEY)
.first_or_initialize(value: 'multiple')
end
return sign_reason(reason_name) if config.value == 'multiple'
return single_sign_reason if !submitter.submission.submitters.exists?(completed_at: nil) &&
submitter.completed_at == submitter.submission.submitters.maximum(:completed_at)
nil
end end
def info_creator def info_creator
INFO_CREATOR "#{Docuseal.product_name} (#{Docuseal::PRODUCT_URL})"
end end
def h def h
+1 -1
View File
@@ -46,7 +46,7 @@ module Submissions
attachments_index = attachments.index_by(&:uuid) attachments_index = attachments.index_by(&:uuid)
submitters.each do |submitter| submitters.each do |submitter|
submitter.values.to_a.each do |_, value| submitter.values.each_value do |value|
attachment = attachments_index[value] attachment = attachments_index[value]
next unless attachment next unless attachment
+91
View File
@@ -0,0 +1,91 @@
# frozen_string_literal: true
module Submitters
module CreateStampAttachment
WIDTH = 400
HEIGHT = 200
TRANSPARENT_PIXEL = "\x89PNG\r\n\u001A\n\u0000\u0000\u0000\rIHDR\u0000" \
"\u0000\u0000\u0001\u0000\u0000\u0000\u0001\b\u0004" \
"\u0000\u0000\u0000\xB5\u001C\f\u0002\u0000\u0000\u0000" \
"\vIDATx\xDAc\xFC_\u000F\u0000\u0002\x83\u0001\x804\xC3ڨ" \
"\u0000\u0000\u0000\u0000IEND\xAEB`\x82"
module_function
def call(submitter)
image = generate_stamp_image(submitter)
image_data = image.write_to_buffer('.png')
checksum = Digest::MD5.base64digest(image_data)
attachment = submitter.attachments.joins(:blob).find_by(blob: { checksum: })
attachment || ActiveStorage::Attachment.create!(
blob: ActiveStorage::Blob.create_and_upload!(io: StringIO.new(image_data), filename: 'stamp.png'),
metadata: { analyzed: true, identified: true, width: image.width, height: image.height },
name: 'attachments',
record: submitter
)
end
# rubocop:disable Metrics
def generate_stamp_image(submitter)
logo = Vips::Image.new_from_buffer(load_logo(submitter).read, '')
logo = logo.resize([WIDTH / logo.width.to_f, HEIGHT / logo.height.to_f].min)
base_layer = Vips::Image.black(WIDTH, HEIGHT).new_from_image([255, 255, 255]).copy(interpretation: :srgb)
opacity_layer = Vips::Image.new_from_buffer(TRANSPARENT_PIXEL, '').resize(WIDTH)
text = build_text_image(submitter)
text_layer = text.new_from_image([0, 0, 0]).copy(interpretation: :srgb)
text_layer = text_layer.bandjoin(text)
base_layer = base_layer.composite(logo, 'over',
x: (WIDTH - logo.width) / 2,
y: (HEIGHT - logo.height) / 2)
base_layer = base_layer.composite(opacity_layer, 'over')
base_layer.composite(text_layer, 'over',
x: (WIDTH - text_layer.width) / 2,
y: (HEIGHT - text_layer.height) / 2)
end
def build_text_image(submitter)
time = I18n.l(submitter.completed_at.in_time_zone(submitter.account.timezone), format: :long,
locale: submitter.account.locale)
timezone = TimeUtils.timezone_abbr(submitter.account.timezone, submitter.completed_at)
name = if submitter.name.present? && submitter.email.present?
"#{submitter.name} #{submitter.email}"
else
submitter.name || submitter.email || submitter.phone
end
role = if submitter.submission.template_submitters.size > 1
item = submitter.submission.template_submitters.find { |e| e['uuid'] == submitter.uuid }
"#{I18n.t(:role, locale: submitter.account.locale)}: #{item['name']}\n"
else
''
end
digitally_signed_by = I18n.t(:digitally_signed_by, locale: submitter.account.locale)
text = %(<span size="90">#{digitally_signed_by}: <b>#{name}</b>\n#{role}#{time} #{timezone}</span>)
Vips::Image.text(text, width: WIDTH, height: HEIGHT)
end
# rubocop:enable Metrics
def load_logo(_submitter)
PdfIcons.logo_io
end
end
end
+8 -2
View File
@@ -44,12 +44,18 @@ module Submitters
if submitter_name if submitter_name
submitter = submitter =
template.submitters.find { |e| e['name'] == submitter_name } || template.submitters.find { |e| e['name'] == submitter_name } ||
raise(UnknownSubmitterName, "Unknown submitter: #{submitter_name}") raise(UnknownSubmitterName, "Unknown submitter role: #{submitter_name}")
end end
fields = for_submitter&.submission&.template_fields || template.fields fields = for_submitter&.submission&.template_fields || template.fields
fields.select { |e| e['submitter_uuid'] == (for_submitter&.uuid || submitter['uuid']) } fields.select do |e|
submitter_uuid =
for_submitter&.uuid || submitter&.dig('uuid') ||
raise(UnknownSubmitterName, "Unknown submitter role: template defines #{template.submitters.pluck('name')}")
e['submitter_uuid'] == submitter_uuid
end
end end
def build_fields_index(fields) def build_fields_index(fields)
+2 -1
View File
@@ -19,7 +19,8 @@ module Submitters
serialize_params = { serialize_params = {
include: {}, include: {},
only: %i[id slug uuid name email phone completed_at application_key only: %i[id slug uuid name email phone completed_at application_key
opened_at sent_at created_at updated_at] opened_at sent_at created_at updated_at],
methods: %i[status]
} }
serialize_params[:include][:template] = { only: %i[id name created_at updated_at] } if with_template serialize_params[:include][:template] = { only: %i[id name created_at updated_at] } if with_template
+6
View File
@@ -74,6 +74,12 @@ module Submitters
default_values = submitter.submission.template_fields.each_with_object({}) do |field, acc| default_values = submitter.submission.template_fields.each_with_object({}) do |field, acc|
next if field['submitter_uuid'] != submitter.uuid next if field['submitter_uuid'] != submitter.uuid
if field['type'] == 'stamp'
acc[field['uuid']] ||= Submitters::CreateStampAttachment.call(submitter).uuid
next
end
value = field['default_value'] value = field['default_value']
next if value.blank? next if value.blank?
+42
View File
@@ -0,0 +1,42 @@
# frozen_string_literal: true
module Templates
module Clone
module_function
def call(original_template, author:, application_key: nil, name: nil, folder_name: nil)
original_template_account = original_template.account
template = original_template_account.templates.new
template.application_key = application_key
template.author = author
template.name = name || "#{original_template.name} (Clone)"
template.assign_attributes(original_template.slice(:folder_id, :schema))
template.folder = TemplateFolders.find_or_create_by_name(author, folder_name) if folder_name.present?
submitter_uuids_replacements = {}
cloned_submitters = original_template['submitters'].deep_dup
cloned_fields = original_template['fields'].deep_dup
cloned_submitters.each do |submitter|
new_submitter_uuid = SecureRandom.uuid
submitter_uuids_replacements[submitter['uuid']] = new_submitter_uuid
submitter['uuid'] = new_submitter_uuid
end
cloned_fields.each do |field|
field['uuid'] = SecureRandom.uuid
field['submitter_uuid'] = submitter_uuids_replacements[field['submitter_uuid']]
end
template.assign_attributes(fields: cloned_fields, submitters: cloned_submitters)
template
end
end
end
+36 -11
View File
@@ -5,23 +5,48 @@ module Templates
module_function module_function
def call(template:, original_template:) def call(template:, original_template:)
original_template.documents.preload(:preview_images_attachments).each do |document| schema_uuids_replacements = {}
cloned_schema = original_template['schema'].deep_dup
cloned_fields = template['fields'].deep_dup
cloned_schema.each do |schema_item|
new_schema_item_uuid = SecureRandom.uuid
schema_uuids_replacements[schema_item['attachment_uuid']] = new_schema_item_uuid
schema_item['attachment_uuid'] = new_schema_item_uuid
end
cloned_fields.each do |field|
next if field['areas'].blank?
field['areas'].each do |area|
area['attachment_uuid'] = schema_uuids_replacements[area['attachment_uuid']]
end
end
template.update!(schema: cloned_schema, fields: cloned_fields)
original_template.schema_documents.preload(:preview_images_attachments).each do |document|
new_document = ActiveStorage::Attachment.create!( new_document = ActiveStorage::Attachment.create!(
uuid: document.uuid, uuid: schema_uuids_replacements[document.uuid],
blob_id: document.blob_id, blob_id: document.blob_id,
name: 'documents', name: 'documents',
record: template record: template
) )
ApplicationRecord.no_touching do clone_document_preview_images_attachments(document:, new_document:)
document.preview_images_attachments.each do |preview_image| end
ActiveStorage::Attachment.create!( end
uuid: preview_image.uuid,
blob_id: preview_image.blob_id, def clone_document_preview_images_attachments(document:, new_document:)
name: 'preview_images', ApplicationRecord.no_touching do
record: new_document document.preview_images_attachments.each do |preview_image|
) ActiveStorage::Attachment.create!(
end blob_id: preview_image.blob_id,
name: 'preview_images',
record: new_document
)
end end
end end
end end
+1 -1
View File
@@ -32,7 +32,7 @@ module TimeUtils
end end
def format_date_string(string, format, locale) def format_date_string(string, format, locale)
date = Date.parse(string) date = Date.parse(string.to_s)
format ||= locale.to_s.ends_with?('US') ? DEFAULT_DATE_FORMAT_US : DEFAULT_DATE_FORMAT format ||= locale.to_s.ends_with?('US') ? DEFAULT_DATE_FORMAT_US : DEFAULT_DATE_FORMAT
-24
View File
@@ -1,24 +0,0 @@
# frozen_string_literal: true
module Users
module_function
def from_omniauth(oauth)
user = User.find_by(email: oauth.info.email.to_s.downcase)
return user if user
case oauth['provider'].to_s
when 'google_oauth2'
User.new(email: oauth.info.email,
first_name: oauth.extra.id_info.given_name,
last_name: oauth.extra.id_info.family_name)
when 'microsoft_office365'
User.new(email: oauth.info.email,
first_name: oauth.info.first_name,
last_name: oauth.info.last_name)
when 'github'
User.new(email: oauth.info.email, first_name: oauth.info.name)
end
end
end
+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.0.3", "shakapacker": "7.1.0",
"signature_pad": "^4.1.5", "signature_pad": "^4.1.5",
"tailwindcss": "^3.3.2", "tailwindcss": "^3.3.2",
"terser-webpack-plugin": "5.3.8", "terser-webpack-plugin": "5.3.8",
+16
View File
@@ -0,0 +1,16 @@
# frozen_string_literal: true
FactoryBot.define do
factory :template_folder do
account
author factory: %i[user]
name { Faker::Book.title }
trait :with_templates do
after(:create) do |template_folder|
create_list(:template, 2, folder: template_folder, account: template_folder.account)
end
end
end
end
+65
View File
@@ -74,5 +74,70 @@ RSpec.describe 'Template' do
expect(page).to have_current_path(edit_template_path(template), ignore_query: true) expect(page).to have_current_path(edit_template_path(template), ignore_query: true)
end end
end end
it 'clone a template and move it to a new folder' do
click_link 'Clone'
within '#modal' do
fill_in 'template[name]', with: 'New Template Name'
click_link 'Change Folder'
fill_in 'folder_name', with: 'New Folder Name'
expect do
click_button 'Submit'
end.to change { Template.active.count }.by(1).and change { TemplateFolder.active.count }.by(1)
template = Template.last
expect(template.name).to eq('New Template Name')
expect(template.folder.name).to eq('New Folder Name')
expect(page).to have_current_path(edit_template_path(template), ignore_query: true)
end
end
it 'clones a template and moves it to an existing folder' do
template_folder = create(:template_folder, :with_templates, account:, author: user)
click_link 'Clone'
within '#modal' do
template_folder.reload
fill_in 'template[name]', with: 'New Template Name'
click_link 'Change Folder'
end
within '.autocomplete' do
find('div', text: template_folder.name).click
end
within '#modal' do
expect do
click_button 'Submit'
end.not_to(change { TemplateFolder.active.count })
end
template = Template.last
expect(template.name).to eq('New Template Name')
expect(template.folder.name).to eq(template_folder.name)
expect(page).to have_current_path(edit_template_path(template), ignore_query: true)
end
it 'moves a template' do
find('[data-tip="Move"]', visible: false).hover
find('[data-tip="Move"]').click
within '#modal' do
fill_in 'name', with: 'New Folder Name'
expect do
click_button 'Move'
end.to change { TemplateFolder.active.count }.by(1)
template_folder = TemplateFolder.last
expect(template_folder.name).to eq('New Folder Name')
expect(page).to have_current_path(template_path(template), ignore_query: true)
end
end
end end
end end
+4 -4
View File
@@ -4832,10 +4832,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.0.3: shakapacker@7.1.0:
version "7.0.3" version "7.1.0"
resolved "https://registry.yarnpkg.com/shakapacker/-/shakapacker-7.0.3.tgz#e67fca4e74c3ef380e1acf53a1d51ab4784ca01c" resolved "https://registry.yarnpkg.com/shakapacker/-/shakapacker-7.1.0.tgz#95bd37db60702ffa21f9ea29a88f438de3db25b2"
integrity sha512-2kwNP8kadkmGOqb7Bp/iNzF3bV31zni8b35Uzst2DZHjnFcnDbW96MERXHc7lS3p3AmKJVbUBmHAFCPsLT9tcw== integrity sha512-xKfF4LKrFQdMLYeIi/uBV6pfkPTO4lgCAIMx3W5+MHW61ENOXu4WeQ50qVR9u5hV6XXzi7AiS7C6dWO2GFnYAg==
dependencies: dependencies:
glob "^7.2.0" glob "^7.2.0"
js-yaml "^4.1.0" js-yaml "^4.1.0"