Compare commits

...
24 Commits
Author SHA1 Message Date
Alex Turchyn c9f39e7b1a fix sign up redirect 2023-08-27 23:34:00 +03:00
Alex Turchyn 1060aa84ba adjust placeholder 2023-08-27 23:33:55 +03:00
Alex Turchyn b6f68ddde3 remove redundant validation 2023-08-27 16:08:25 +03:00
Alex Turchyn 12dd96e34c bring back sign up path 2023-08-26 01:24:14 +03:00
Alex Turchyn 52ba415429 use active job test queue adapter in rspec 2023-08-26 01:20:43 +03:00
Alex Turchyn 601c984f3f do not display registration routes for signed up users 2023-08-25 23:54:55 +03:00
Alex Turchyn 9d90304d9a fix builder width handler 2023-08-25 23:44:14 +03:00
Alex Turchyn 788493c0a6 support UTF-8 text via ttf 2023-08-25 00:28:02 +03:00
Alex Turchyn e1342860c4 add redirect handler 2023-08-24 00:50:57 +03:00
Alex Turchyn 88968897e9 update gems 2023-08-21 20:09:56 +03:00
Alex Turchyn 966b786a30 store template submitters on submission creation 2023-08-21 16:32:39 +03:00
Alex Turchyn bc3b1b477e add export button 2023-08-20 15:18:15 +03:00
Alex Turchyn cbc6f4497a add base feature specs 2023-08-20 15:15:48 +03:00
Alex Turchyn 6b4e7c2264 allow to edit email templates 2023-08-20 00:11:11 +03:00
Alex Turchyn 8e6639d891 add a link to the console in navbar 2023-08-20 00:05:00 +03:00
Alex Turchyn 8e39f0078d add source to templates 2023-08-19 22:08:17 +03:00
Alex Turchyn 46f350217a refactor and fixes 2023-08-19 21:30:36 +03:00
Alex Turchyn 077c55f7e8 fix rubocop 2023-08-17 21:30:50 +03:00
Alex Turchyn e5c5620b20 collect ua and ip on form completion 2023-08-17 21:30:49 +03:00
Alex Turchyn 187b354605 add personalization settings 2023-08-16 23:51:55 +03:00
Alex Turchyn 545b18086a rescue all email smtp errors 2023-08-15 21:51:46 +03:00
Alex Turchyn 130c91178f fix templates api page when new installation 2023-08-15 21:39:25 +03:00
Alex Turchyn 47c57158f4 refactor and fixes 2023-08-14 00:48:42 +03:00
Alex Turchyn 3b745a650d optimize dockerfile 2023-08-12 15:48:50 +03:00
122 changed files with 2509 additions and 401 deletions

No files matched your search

+1
View File
@@ -1,5 +1,6 @@
/db/*.sqlite3 /db/*.sqlite3
**/node_modules
/node_modules /node_modules
/coverage /coverage
/doc /doc
+5 -2
View File
@@ -53,10 +53,13 @@ RSpec/NestedGroups:
Max: 6 Max: 6
RSpec/MultipleExpectations: RSpec/MultipleExpectations:
Max: 7 Max: 20
RSpec/ExampleLength: RSpec/ExampleLength:
Max: 15 Max: 40
RSpec/MultipleMemoizedHelpers:
Max: 6
Rails/I18nLocaleTexts: Rails/I18nLocaleTexts:
Enabled: false Enabled: false
+10 -2
View File
@@ -31,13 +31,21 @@ ENV BUNDLE_WITHOUT="development:test"
WORKDIR /app WORKDIR /app
RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler vips-heif libc6-compat ttf-freefont RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler vips-heif libc6-compat ttf-freefont ttf-liberation && cp /usr/share/fonts/liberation/LiberationSans-Regular.ttf /LiberationSans-Regular.ttf && apk del ttf-liberation
COPY ./Gemfile ./Gemfile.lock ./ COPY ./Gemfile ./Gemfile.lock ./
RUN bundle update --bundler && bundle install && rm -rf ~/.bundle RUN bundle update --bundler && bundle install && rm -rf ~/.bundle
COPY . ./ COPY ./bin ./bin
COPY ./app ./app
COPY ./config ./config
COPY ./db ./db
COPY ./log ./log
COPY ./lib ./lib
COPY ./public ./public
COPY ./tmp ./tmp
COPY LICENSE README.md Rakefile config.ru ./
COPY --from=webpack /app/public/packs ./public/packs COPY --from=webpack /app/public/packs ./public/packs
+2
View File
@@ -24,9 +24,11 @@ gem 'pg', require: false
gem 'premailer-rails' gem 'premailer-rails'
gem 'puma' gem 'puma'
gem 'rails' gem 'rails'
gem 'rails_autolink'
gem 'rails-i18n' gem 'rails-i18n'
gem 'rollbar', require: ENV.key?('ROLLBAR_ACCESS_TOKEN') gem 'rollbar', require: ENV.key?('ROLLBAR_ACCESS_TOKEN')
gem 'ruby-vips' gem 'ruby-vips'
gem 'rubyXL'
gem 'shakapacker' gem 'shakapacker'
gem 'sidekiq', require: ENV.key?('REDIS_URL') gem 'sidekiq', require: ENV.key?('REDIS_URL')
gem 'sqlite3', require: false gem 'sqlite3', require: false
+120 -102
View File
@@ -1,92 +1,92 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (7.0.5) actioncable (7.0.7)
actionpack (= 7.0.5) actionpack (= 7.0.7)
activesupport (= 7.0.5) activesupport (= 7.0.7)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
actionmailbox (7.0.5) actionmailbox (7.0.7)
actionpack (= 7.0.5) actionpack (= 7.0.7)
activejob (= 7.0.5) activejob (= 7.0.7)
activerecord (= 7.0.5) activerecord (= 7.0.7)
activestorage (= 7.0.5) activestorage (= 7.0.7)
activesupport (= 7.0.5) activesupport (= 7.0.7)
mail (>= 2.7.1) mail (>= 2.7.1)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
actionmailer (7.0.5) actionmailer (7.0.7)
actionpack (= 7.0.5) actionpack (= 7.0.7)
actionview (= 7.0.5) actionview (= 7.0.7)
activejob (= 7.0.5) activejob (= 7.0.7)
activesupport (= 7.0.5) activesupport (= 7.0.7)
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.0)
actionpack (7.0.5) actionpack (7.0.7)
actionview (= 7.0.5) actionview (= 7.0.7)
activesupport (= 7.0.5) activesupport (= 7.0.7)
rack (~> 2.0, >= 2.2.4) rack (~> 2.0, >= 2.2.4)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (7.0.5) actiontext (7.0.7)
actionpack (= 7.0.5) actionpack (= 7.0.7)
activerecord (= 7.0.5) activerecord (= 7.0.7)
activestorage (= 7.0.5) activestorage (= 7.0.7)
activesupport (= 7.0.5) activesupport (= 7.0.7)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (7.0.5) actionview (7.0.7)
activesupport (= 7.0.5) activesupport (= 7.0.7)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (7.0.5) activejob (7.0.7)
activesupport (= 7.0.5) activesupport (= 7.0.7)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (7.0.5) activemodel (7.0.7)
activesupport (= 7.0.5) activesupport (= 7.0.7)
activerecord (7.0.5) activerecord (7.0.7)
activemodel (= 7.0.5) activemodel (= 7.0.7)
activesupport (= 7.0.5) activesupport (= 7.0.7)
activestorage (7.0.5) activestorage (7.0.7)
actionpack (= 7.0.5) actionpack (= 7.0.7)
activejob (= 7.0.5) activejob (= 7.0.7)
activerecord (= 7.0.5) activerecord (= 7.0.7)
activesupport (= 7.0.5) activesupport (= 7.0.7)
marcel (~> 1.0) marcel (~> 1.0)
mini_mime (>= 1.1.0) mini_mime (>= 1.1.0)
activesupport (7.0.5) activesupport (7.0.7)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
minitest (>= 5.1) minitest (>= 5.1)
tzinfo (~> 2.0) tzinfo (~> 2.0)
addressable (2.8.4) addressable (2.8.5)
public_suffix (>= 2.0.2, < 6.0) public_suffix (>= 2.0.2, < 6.0)
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.2.0)
aws-partitions (1.781.0) aws-partitions (1.807.0)
aws-sdk-core (3.175.0) aws-sdk-core (3.180.3)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0) aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.67.0) aws-sdk-kms (1.71.0)
aws-sdk-core (~> 3, >= 3.174.0) aws-sdk-core (~> 3, >= 3.177.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.126.0) aws-sdk-s3 (1.132.1)
aws-sdk-core (~> 3, >= 3.174.0) aws-sdk-core (~> 3, >= 3.179.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4) aws-sigv4 (~> 1.6)
aws-sigv4 (1.5.2) aws-sigv4 (1.6.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)
@@ -96,8 +96,9 @@ 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)
base64 (0.1.1)
bcrypt (3.1.19) bcrypt (3.1.19)
better_html (2.0.1) better_html (2.0.2)
actionview (>= 6.0) actionview (>= 6.0)
activesupport (>= 6.0) activesupport (>= 6.0)
ast (~> 2.0) ast (~> 2.0)
@@ -105,7 +106,7 @@ GEM
parser (>= 2.4) parser (>= 2.4)
smart_properties smart_properties
bindex (0.8.1) bindex (0.8.1)
bootsnap (1.15.0) bootsnap (1.16.0)
msgpack (~> 1.2) msgpack (~> 1.2)
builder (3.2.4) builder (3.2.4)
bullet (7.0.7) bullet (7.0.7)
@@ -144,7 +145,7 @@ GEM
responders responders
warden (~> 1.2.3) warden (~> 1.2.3)
diff-lcs (1.5.0) diff-lcs (1.5.0)
digest-crc (0.6.4) digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0) rake (>= 12.0.0, < 14.0.0)
docile (1.4.0) docile (1.4.0)
dotenv (2.8.1) dotenv (2.8.1)
@@ -161,7 +162,7 @@ GEM
factory_bot_rails (6.2.0) factory_bot_rails (6.2.0)
factory_bot (~> 6.2.0) factory_bot (~> 6.2.0)
railties (>= 5.0.0) railties (>= 5.0.0)
faker (3.2.0) faker (3.2.1)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
faraday (1.10.3) faraday (1.10.3)
faraday-em_http (~> 1.0) faraday-em_http (~> 1.0)
@@ -194,10 +195,10 @@ GEM
webrick (~> 1.7) webrick (~> 1.7)
websocket-driver (>= 0.6, < 0.8) websocket-driver (>= 0.6, < 0.8)
ffi (1.15.5) ffi (1.15.5)
geom2d (0.3.1) geom2d (0.4.1)
globalid (1.1.0) globalid (1.1.0)
activesupport (>= 5.0) activesupport (>= 5.0)
google-apis-core (0.11.0) google-apis-core (0.11.1)
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)
@@ -224,7 +225,7 @@ GEM
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.6.0) googleauth (1.7.0)
faraday (>= 0.17.3, < 3.a) faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0) jwt (>= 1.4, < 3.0)
memoist (~> 0.16) memoist (~> 0.16)
@@ -233,9 +234,9 @@ GEM
signet (>= 0.16, < 2.a) signet (>= 0.16, < 2.a)
hashdiff (1.0.1) hashdiff (1.0.1)
hashie (5.0.0) hashie (5.0.0)
hexapdf (0.32.2) hexapdf (0.33.0)
cmdparse (~> 3.0, >= 3.0.3) cmdparse (~> 3.0, >= 3.0.3)
geom2d (~> 0.3) geom2d (~> 0.4, >= 0.4.1)
openssl (>= 2.2.1) openssl (>= 2.2.1)
htmlentities (4.3.4) htmlentities (4.3.4)
httpclient (2.8.3) httpclient (2.8.3)
@@ -245,8 +246,8 @@ GEM
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.6.0)
irb (1.7.0) irb (1.7.4)
reline (>= 0.3.0) reline (>= 0.3.6)
jmespath (1.6.2) jmespath (1.6.2)
json (2.6.3) json (2.6.3)
jwt (2.7.1) jwt (2.7.1)
@@ -260,7 +261,7 @@ GEM
letter_opener (~> 1.7) letter_opener (~> 1.7)
railties (>= 5.2) railties (>= 5.2)
rexml rexml
lograge (0.12.0) lograge (0.13.0)
actionpack (>= 4) actionpack (>= 4)
activesupport (>= 4) activesupport (>= 4)
railties (>= 4) railties (>= 4)
@@ -278,17 +279,17 @@ GEM
memoist (0.16.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.2) mini_mime (1.1.5)
mini_portile2 (2.8.2) mini_portile2 (2.8.4)
minitest (5.18.1) minitest (5.19.0)
msgpack (1.7.1) msgpack (1.7.2)
multi_json (1.15.0) multi_json (1.15.0)
multi_xml (0.6.0) multi_xml (0.6.0)
multipart-post (2.3.0) multipart-post (2.3.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.6) net-imap (0.3.7)
date date
net-protocol net-protocol
net-pop (0.1.2) net-pop (0.1.2)
@@ -298,7 +299,10 @@ GEM
net-smtp (0.3.3) net-smtp (0.3.3)
net-protocol net-protocol
nio4r (2.5.9) nio4r (2.5.9)
nokogiri (1.15.2-arm64-darwin) nokogiri (1.15.4)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.15.4-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
oauth2 (2.0.9) oauth2 (2.0.9)
faraday (>= 0.17.3, < 3.0) faraday (>= 0.17.3, < 3.0)
@@ -307,7 +311,7 @@ GEM
rack (>= 1.2, < 4) rack (>= 1.2, < 4)
snaky_hash (~> 2.0) snaky_hash (~> 2.0)
version_gem (~> 1.1) version_gem (~> 1.1)
oj (3.15.0) oj (3.16.0)
omniauth (2.1.1) omniauth (2.1.1)
hashie (>= 3.4.6) hashie (>= 3.4.6)
rack (>= 2.2.3) rack (>= 2.2.3)
@@ -345,53 +349,58 @@ 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.1) public_suffix (5.0.3)
puma (6.3.0) puma (6.3.1)
nio4r (~> 2.0) nio4r (~> 2.0)
racc (1.7.1) racc (1.7.1)
rack (2.2.7) rack (2.2.8)
rack-protection (3.0.6) rack-protection (3.1.0)
rack rack (~> 2.2, >= 2.2.4)
rack-proxy (0.7.6) rack-proxy (0.7.6)
rack rack
rack-test (2.1.0) rack-test (2.1.0)
rack (>= 1.3) rack (>= 1.3)
rails (7.0.5) rails (7.0.7)
actioncable (= 7.0.5) actioncable (= 7.0.7)
actionmailbox (= 7.0.5) actionmailbox (= 7.0.7)
actionmailer (= 7.0.5) actionmailer (= 7.0.7)
actionpack (= 7.0.5) actionpack (= 7.0.7)
actiontext (= 7.0.5) actiontext (= 7.0.7)
actionview (= 7.0.5) actionview (= 7.0.7)
activejob (= 7.0.5) activejob (= 7.0.7)
activemodel (= 7.0.5) activemodel (= 7.0.7)
activerecord (= 7.0.5) activerecord (= 7.0.7)
activestorage (= 7.0.5) activestorage (= 7.0.7)
activesupport (= 7.0.5) activesupport (= 7.0.7)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 7.0.5) railties (= 7.0.7)
rails-dom-testing (2.0.3) rails-dom-testing (2.2.0)
activesupport (>= 4.2.0) activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.6.0) rails-html-sanitizer (1.6.0)
loofah (~> 2.21) loofah (~> 2.21)
nokogiri (~> 1.14) nokogiri (~> 1.14)
rails-i18n (7.0.7) rails-i18n (7.0.8)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8) railties (>= 6.0.0, < 8)
railties (7.0.5) rails_autolink (1.1.8)
actionpack (= 7.0.5) actionview (> 3.1)
activesupport (= 7.0.5) activesupport (> 3.1)
railties (> 3.1)
railties (7.0.7)
actionpack (= 7.0.7)
activesupport (= 7.0.7)
method_source method_source
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0) thor (~> 1.0)
zeitwerk (~> 2.5) zeitwerk (~> 2.5)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.0.6) rake (13.0.6)
redis-client (0.14.1) redis-client (0.16.0)
connection_pool connection_pool
regexp_parser (2.8.1) regexp_parser (2.8.1)
reline (0.3.5) reline (0.3.8)
io-console (~> 0.5) io-console (~> 0.5)
representable (3.2.0) representable (3.2.0)
declarative (< 0.1.0) declarative (< 0.1.0)
@@ -403,14 +412,14 @@ GEM
actionpack (>= 5.2) actionpack (>= 5.2)
railties (>= 5.2) railties (>= 5.2)
retriable (3.1.2) retriable (3.1.2)
rexml (3.2.5) rexml (3.2.6)
rollbar (3.4.0) rollbar (3.4.0)
rspec-core (3.12.2) rspec-core (3.12.2)
rspec-support (~> 3.12.0) rspec-support (~> 3.12.0)
rspec-expectations (3.12.3) rspec-expectations (3.12.3)
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-mocks (3.12.5) 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.0.3)
@@ -421,8 +430,9 @@ GEM
rspec-expectations (~> 3.12) rspec-expectations (~> 3.12)
rspec-mocks (~> 3.12) rspec-mocks (~> 3.12)
rspec-support (~> 3.12) rspec-support (~> 3.12)
rspec-support (3.12.0) rspec-support (3.12.1)
rubocop (1.53.0) rubocop (1.56.1)
base64 (~> 0.1.1)
json (~> 2.3) json (~> 2.3)
language_server-protocol (>= 3.17.0) language_server-protocol (>= 3.17.0)
parallel (~> 1.10) parallel (~> 1.10)
@@ -430,7 +440,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.0, < 2.0) rubocop-ast (>= 1.28.1, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0) unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.29.0) rubocop-ast (1.29.0)
@@ -439,14 +449,14 @@ GEM
rubocop (~> 1.41) rubocop (~> 1.41)
rubocop-factory_bot (2.23.1) rubocop-factory_bot (2.23.1)
rubocop (~> 1.33) rubocop (~> 1.33)
rubocop-performance (1.18.0) rubocop-performance (1.19.0)
rubocop (>= 1.7.0, < 2.0) rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0) rubocop-ast (>= 0.4.0)
rubocop-rails (2.20.2) rubocop-rails (2.20.2)
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-rspec (2.22.0) rubocop-rspec (2.23.2)
rubocop (~> 1.33) rubocop (~> 1.33)
rubocop-capybara (~> 2.17) rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22) rubocop-factory_bot (~> 2.22)
@@ -454,8 +464,12 @@ GEM
ruby-vips (2.1.4) ruby-vips (2.1.4)
ffi (~> 1.12) ffi (~> 1.12)
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rubyXL (3.4.25)
nokogiri (>= 1.10.8)
rubyzip (>= 1.3.0)
rubyzip (2.3.2)
semantic_range (3.0.0) semantic_range (3.0.0)
shakapacker (7.0.0) shakapacker (7.0.3)
activesupport (>= 5.2) activesupport (>= 5.2)
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 5.2) railties (>= 5.2)
@@ -480,8 +494,9 @@ GEM
snaky_hash (2.0.1) snaky_hash (2.0.1)
hashie hashie
version_gem (~> 1.1, >= 1.1.1) version_gem (~> 1.1, >= 1.1.1)
sqlite3 (1.5.4) sqlite3 (1.6.3)
mini_portile2 (~> 2.8.0) mini_portile2 (~> 2.8.0)
sqlite3 (1.6.3-arm64-darwin)
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.2.2)
@@ -511,15 +526,16 @@ GEM
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0) hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.8.1) webrick (1.8.1)
websocket-driver (0.7.5) websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
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.8) zeitwerk (2.6.11)
PLATFORMS PLATFORMS
arm64-darwin-22 arm64-darwin-22
ruby
DEPENDENCIES DEPENDENCIES
annotate annotate
@@ -554,6 +570,7 @@ DEPENDENCIES
puma puma
rails rails
rails-i18n rails-i18n
rails_autolink
rollbar rollbar
rspec-rails rspec-rails
rubocop rubocop
@@ -561,6 +578,7 @@ DEPENDENCIES
rubocop-rails rubocop-rails
rubocop-rspec rubocop-rspec
ruby-vips ruby-vips
rubyXL
shakapacker shakapacker
sidekiq sidekiq
simplecov simplecov
+1 -14
View File
@@ -7,20 +7,7 @@ module Api
def create def create
submitter = Submitter.find_by!(slug: params[:submitter_slug]) submitter = Submitter.find_by!(slug: params[:submitter_slug])
blob = attachment = Submitters.create_attachment!(submitter, params)
if (file = params[:file])
ActiveStorage::Blob.create_and_upload!(io: file.open,
filename: file.original_filename,
content_type: file.content_type)
else
ActiveStorage::Blob.find_signed(params[:blob_signed_id])
end
attachment = ActiveStorage::Attachment.create!(
blob:,
name: params[:name],
record: submitter
)
render json: attachment.as_json(only: %i[uuid], methods: %i[url filename content_type]) render json: attachment.as_json(only: %i[uuid], methods: %i[url filename content_type])
end end
+2 -1
View File
@@ -9,7 +9,8 @@ module Api
end end
def show def show
render json: @template.as_json(include: { author: { only: %i[id email first_name last_name] } }) render json: @template.as_json(include: { author: { only: %i[id email first_name last_name] },
documents: { only: %i[id uuid], methods: %i[url filename] } })
end end
def update def update
@@ -1,11 +1,17 @@
# frozen_string_literal: true # frozen_string_literal: true
class ConsoleRedirectController < ApplicationController class ConsoleRedirectController < ApplicationController
skip_before_action :authenticate_user!
def index def index
return redirect_to(new_user_session_path({ redir: params[:redir] }.compact)) if current_user.blank?
auth = JsonWebToken.encode(uuid: current_user.uuid, auth = JsonWebToken.encode(uuid: current_user.uuid,
scope: :console, scope: :console,
exp: 1.minute.from_now.to_i) exp: 1.minute.from_now.to_i)
redirect_to("#{Docuseal::CONSOLE_URL}?#{{ auth: }.to_query}", allow_other_host: true) path = Addressable::URI.parse(params[:redir]).path if params[:redir].to_s.starts_with?(Docuseal::CONSOLE_URL)
redirect_to("#{Docuseal::CONSOLE_URL}#{path}?#{{ auth: }.to_query}", allow_other_host: true)
end end
end end
+1 -1
View File
@@ -13,7 +13,7 @@ class EmailSettingsController < ApplicationController
else else
render :index, status: :unprocessable_entity render :index, status: :unprocessable_entity
end end
rescue Net::SMTPError, OpenSSL::SSL::SSLError, Net::ReadTimeout => e rescue StandardError => e
flash[:alert] = e.message flash[:alert] = e.message
render :index, status: :unprocessable_entity render :index, status: :unprocessable_entity
@@ -0,0 +1,20 @@
# frozen_string_literal: true
class PersonalizationSettingsController < ApplicationController
def show; end
def create
account_config =
current_account.account_configs.find_or_initialize_by(key: encrypted_config_params[:key])
account_config.update!(encrypted_config_params)
redirect_back(fallback_location: settings_personalization_path, notice: 'Settings have been saved.')
end
private
def encrypted_config_params
params.require(:account_config).permit!
end
end
@@ -2,6 +2,7 @@
class RegistrationsController < Devise::RegistrationsController class RegistrationsController < Devise::RegistrationsController
prepend_before_action :require_no_authentication, only: [:show] prepend_before_action :require_no_authentication, only: [:show]
prepend_before_action :maybe_redirect_if_signed_in, only: [:show]
def show; end def show; end
@@ -13,6 +14,23 @@ class RegistrationsController < Devise::RegistrationsController
private private
def after_sign_up_path_for(...)
if params[:redir].present?
return console_redirect_index_path(redir: params[:redir]) if params[:redir].starts_with?(Docuseal::CONSOLE_URL)
return params[:redir]
end
super
end
def maybe_redirect_if_signed_in
return unless signed_in?
return if params[:redir].blank?
redirect_to after_sign_up_path_for(current_user)
end
def require_no_authentication def require_no_authentication
super super
+10
View File
@@ -12,6 +12,16 @@ class SessionsController < Devise::SessionsController
private private
def after_sign_in_path_for(...)
if params[:redir].present?
return console_redirect_index_path(redir: params[:redir]) if params[:redir].starts_with?(Docuseal::CONSOLE_URL)
return params[:redir]
end
super
end
def require_no_authentication def require_no_authentication
super super
+3 -2
View File
@@ -16,16 +16,17 @@ class SetupController < ApplicationController
def create def create
@account = Account.new(account_params) @account = Account.new(account_params)
@account.timezone = Accounts.normalize_timezone(@account.timezone) @account.timezone = Accounts.normalize_timezone(@account.timezone)
@user = @account.users.new(user_params) @user = @account.users.new(user_params)
@encrypted_config = EncryptedConfig.new(encrypted_config_params)
unless URI.parse(encrypted_config_params[:value].to_s).class.in?([URI::HTTP, URI::HTTPS]) unless URI.parse(encrypted_config_params[:value].to_s).class.in?([URI::HTTP, URI::HTTPS])
@encrypted_config = EncryptedConfig.new(encrypted_config_params)
@encrypted_config.errors.add(:value, 'should be a valid URL') @encrypted_config.errors.add(:value, 'should be a valid URL')
return render :index, status: :unprocessable_entity return render :index, status: :unprocessable_entity
end end
return render :index, status: :unprocessable_entity unless @account.valid?
if @user.save if @user.save
encrypted_configs = [ encrypted_configs = [
{ key: EncryptedConfig::APP_URL_KEY, value: encrypted_config_params[:value] }, { key: EncryptedConfig::APP_URL_KEY, value: encrypted_config_params[:value] },
+3 -1
View File
@@ -36,7 +36,9 @@ class StartFormController < ApplicationController
end end
def completed def completed
@submitter = Submitter.where(submission: @template.submissions).find_by!(email: params[:email]) @submitter = Submitter.where(submission: @template.submissions)
.where.not(completed_at: nil)
.find_by!(email: params[:email])
end end
private private
@@ -0,0 +1,28 @@
# frozen_string_literal: true
class SubmissionsExportController < ApplicationController
before_action :load_template
def index
submissions = @template.submissions.active
.preload(submitters: { documents_attachments: :blob,
attachments_attachments: :blob })
.order(id: :asc)
if params[:format] == 'csv'
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format]),
filename: "#{@template.name}.csv"
elsif params[:format] == 'xlsx'
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format]),
filename: "#{@template.name}.xlsx"
end
end
def new; end
private
def load_template
@template = current_account.templates.find(params[:template_id])
end
end
+1 -39
View File
@@ -18,23 +18,7 @@ class SubmitFormController < ApplicationController
def update def update
submitter = Submitter.find_by!(slug: params[:slug]) submitter = Submitter.find_by!(slug: params[:slug])
update_submitter!(submitter) Submitters::SubmitValues.call(submitter, params, request)
Submissions.update_template_fields!(submitter.submission) if submitter.submission.template_fields.blank?
submitter.submission.save!
if submitter.completed_at?
GenerateSubmitterResultAttachmentsJob.perform_later(submitter)
if submitter.account.encrypted_configs.exists?(key: EncryptedConfig::WEBHOOK_URL_KEY)
SendWebhookRequestJob.perform_later(submitter)
end
submitter.submission.template.account.users.active.each do |user|
SubmitterMailer.completed_email(submitter, user).deliver_later!
end
end
head :ok head :ok
end end
@@ -42,26 +26,4 @@ class SubmitFormController < ApplicationController
def completed def completed
@submitter = Submitter.find_by!(slug: params[:submit_form_slug]) @submitter = Submitter.find_by!(slug: params[:submit_form_slug])
end end
private
def update_submitter!(submitter)
submitter.values.merge!(normalized_values)
submitter.completed_at = Time.current if params[:completed] == 'true'
submitter.opened_at ||= Time.current
submitter.save!
submitter
end
def normalized_values
params.fetch(:values, {}).to_unsafe_h.transform_values do |v|
if params[:cast_boolean] == 'true'
v == 'true'
else
v.is_a?(Array) ? v.compact_blank : v
end
end
end
end end
+1 -1
View File
@@ -17,7 +17,7 @@ class TemplatesController < ApplicationController
end end
def edit def edit
@template = current_account.templates.preload(documents_attachments: { preview_images_attachments: :blob }) @template = current_account.templates.preload(schema_documents: { preview_images_attachments: :blob })
.find(params[:id]) .find(params[:id])
render :edit, layout: 'plain' render :edit, layout: 'plain'
+22
View File
@@ -45,10 +45,32 @@ window.customElements.define('set-timezone', SetTimezone)
window.customElements.define('autoresize-textarea', AutoresizeTextarea) window.customElements.define('autoresize-textarea', AutoresizeTextarea)
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody) document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
document.addEventListener('turbo:submit-end', async (event) => {
const resp = event.detail?.formSubmission?.result?.fetchResponse?.response
if (!resp?.headers?.get('content-disposition')?.includes('attachment')) {
return
}
const url = URL.createObjectURL(await resp.blob())
const link = document.createElement('a')
link.href = url
link.setAttribute('download', decodeURIComponent(resp.headers.get('content-disposition').split('"')[1]))
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
})
window.customElements.define('template-builder', class extends HTMLElement { window.customElements.define('template-builder', class extends HTMLElement {
connectedCallback () { connectedCallback () {
this.appElem = document.createElement('div') this.appElem = document.createElement('div')
this.appElem.classList.add('max-h-screen')
this.app = createApp(TemplateBuilder, { this.app = createApp(TemplateBuilder, {
template: reactive(JSON.parse(this.dataset.template)), template: reactive(JSON.parse(this.dataset.template)),
+5 -1
View File
@@ -5,8 +5,12 @@ export default actionable(class extends HTMLElement {
document.body.classList.add('overflow-hidden') document.body.classList.add('overflow-hidden')
document.addEventListener('keyup', this.onEscKey) document.addEventListener('keyup', this.onEscKey)
document.addEventListener('turbo:submit-end', this.onSubmit)
document.addEventListener('turbo:before-cache', this.close) document.addEventListener('turbo:before-cache', this.close)
if (this.dataset.closeAfterSubmit !== 'false') {
document.addEventListener('turbo:submit-end', this.onSubmit)
}
} }
disconnectedCallback () { disconnectedCallback () {
+1
View File
@@ -15,6 +15,7 @@ window.customElements.define('submission-form', class extends HTMLElement {
canSendEmail: this.dataset.canSendEmail === 'true', canSendEmail: this.dataset.canSendEmail === 'true',
isDirectUpload: this.dataset.isDirectUpload === 'true', isDirectUpload: this.dataset.isDirectUpload === 'true',
isDemo: this.dataset.isDemo === 'true', isDemo: this.dataset.isDemo === 'true',
withConfetti: true,
values: reactive(JSON.parse(this.dataset.values)), values: reactive(JSON.parse(this.dataset.values)),
attachments: reactive(JSON.parse(this.dataset.attachments)), attachments: reactive(JSON.parse(this.dataset.attachments)),
fields: JSON.parse(this.dataset.fields) fields: JSON.parse(this.dataset.fields)
+16 -4
View File
@@ -12,7 +12,7 @@
:key="areaIndex" :key="areaIndex"
> >
<Teleport <Teleport
:to="`#page-${area.attachment_uuid}-${area.page}`" :to="findPageElementForArea(area)"
> >
<FieldArea <FieldArea
:ref="setAreaRef" :ref="setAreaRef"
@@ -78,10 +78,16 @@ export default {
this.areaRefs = [] this.areaRefs = []
}, },
methods: { methods: {
findPageElementForArea (area) {
return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${area.attachment_uuid}-${area.page}`)
},
scrollIntoField (field) { scrollIntoField (field) {
this.areaRefs.find((area) => { this.areaRefs.find((area) => {
if (area.field === field) { if (area.field === field) {
if (document.body.style.overflow === 'hidden') { const root = this.$root.$el.parentNode.getRootNode()
const container = root.body || root.querySelector('div')
if (container.style.overflow === 'hidden') {
this.scrollInContainer(area.$el) this.scrollInContainer(area.$el)
} else { } else {
area.$refs.scrollToElem.scrollIntoView({ behavior: 'smooth', block: 'start' }) area.$refs.scrollToElem.scrollIntoView({ behavior: 'smooth', block: 'start' })
@@ -94,13 +100,19 @@ export default {
}) })
}, },
scrollInContainer (target) { scrollInContainer (target) {
const root = this.$root.$el.parentNode.getRootNode()
const scrollbox = root.getElementById('scrollbox')
const formContainer = root.getElementById('form_container')
const container = root.body || root.querySelector('div')
const padding = 64 const padding = 64
const boxRect = window.scrollbox.children[0].getBoundingClientRect() const boxRect = scrollbox.children[0].getBoundingClientRect()
const targetRect = target.getBoundingClientRect() const targetRect = target.getBoundingClientRect()
const targetTopRelativeToBox = targetRect.top - boxRect.top const targetTopRelativeToBox = targetRect.top - boxRect.top
window.scrollbox.scrollTop = targetTopRelativeToBox - document.body.offsetHeight + window.form_container.offsetHeight + target.offsetHeight + padding scrollbox.scrollTop = targetTopRelativeToBox - container.offsetHeight + formContainer.offsetHeight + target.offsetHeight + padding
}, },
setAreaRef (el) { setAreaRef (el) {
if (el) { if (el) {
+16 -8
View File
@@ -86,6 +86,7 @@ export default {
IconLogin, IconLogin,
IconDownload IconDownload
}, },
inject: ['baseUrl'],
props: { props: {
submitterSlug: { submitterSlug: {
type: String, type: String,
@@ -96,6 +97,11 @@ export default {
required: false, required: false,
default: false default: false
}, },
withConfetti: {
type: Boolean,
required: false,
default: false
},
canSendEmail: { canSendEmail: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -109,19 +115,21 @@ export default {
} }
}, },
async mounted () { async mounted () {
const { default: confetti } = await import('canvas-confetti') if (this.withConfetti) {
const { default: confetti } = await import('canvas-confetti')
confetti({ confetti({
particleCount: 50, particleCount: 50,
startVelocity: 30, startVelocity: 30,
spread: 140 spread: 140
}) })
}
}, },
methods: { methods: {
sendCopyToEmail () { sendCopyToEmail () {
this.isSendingCopy = true this.isSendingCopy = true
fetch(`/send_submission_email.json?submitter_slug=${this.submitterSlug}`, { fetch(this.baseUrl + `/send_submission_email.json?submitter_slug=${this.submitterSlug}`, {
method: 'POST' method: 'POST'
}).then(() => { }).then(() => {
alert('Email has been sent') alert('Email has been sent')
@@ -132,7 +140,7 @@ export default {
download () { download () {
this.isDownloading = true this.isDownloading = true
fetch(`/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 fileRequests = urls.map((url) => {
return () => { return () => {
return fetch(url).then(async (resp) => { return fetch(url).then(async (resp) => {
+3 -2
View File
@@ -55,6 +55,7 @@ export default {
IconCloudUpload, IconCloudUpload,
IconInnerShadowTop IconInnerShadowTop
}, },
inject: ['baseUrl'],
props: { props: {
message: { message: {
type: String, type: String,
@@ -141,7 +142,7 @@ export default {
return await Promise.all( return await Promise.all(
blobs.map((blob) => { blobs.map((blob) => {
return fetch('/api/attachments', { return fetch(this.baseUrl + '/api/attachments', {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
name: 'attachments', name: 'attachments',
@@ -166,7 +167,7 @@ export default {
formData.append('submitter_slug', this.submitterSlug) formData.append('submitter_slug', this.submitterSlug)
formData.append('name', 'attachments') formData.append('name', 'attachments')
return fetch('/api/attachments', { return fetch(this.baseUrl + '/api/attachments', {
method: 'POST', method: 'POST',
body: formData body: formData
}).then(resp => resp.json()).then((data) => { }).then(resp => resp.json()).then((data) => {
+35 -9
View File
@@ -10,7 +10,7 @@
/> />
<button <button
v-if="!isFormVisible" v-if="!isFormVisible"
class="btn btn-neutral 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"
> >
Submit Form Submit Form
@@ -194,7 +194,9 @@
class="space-y-3.5 mx-auto" class="space-y-3.5 mx-auto"
> >
<template v-if="isAnonymousChecboxes"> <template v-if="isAnonymousChecboxes">
Complete hightlighted checkboxes and click <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? 'submit' : 'next' }}</span>. <span class="text-xl">
Complete hightlighted checkboxes and click <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? 'submit' : 'next' }}</span>.
</span>
<input <input
v-for="field in currentStepFields" v-for="field in currentStepFields"
:key="field.uuid" :key="field.uuid"
@@ -290,6 +292,7 @@
<FormCompleted <FormCompleted
v-else v-else
:is-demo="isDemo" :is-demo="isDemo"
:with-confetti="withConfetti"
:can-send-email="canSendEmail" :can-send-email="canSendEmail"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
/> />
@@ -331,6 +334,11 @@ export default {
IconArrowsDiagonalMinimize2, IconArrowsDiagonalMinimize2,
FormCompleted FormCompleted
}, },
provide () {
return {
baseUrl: this.baseUrl
}
},
props: { props: {
submitterSlug: { submitterSlug: {
type: String, type: String,
@@ -350,6 +358,16 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
withConfetti: {
type: Boolean,
required: false,
default: false
},
baseUrl: {
type: String,
required: false,
default: ''
},
fields: { fields: {
type: Array, type: Array,
required: false, required: false,
@@ -357,16 +375,17 @@ export default {
}, },
authenticityToken: { authenticityToken: {
type: String, type: String,
required: true required: false,
default: ''
}, },
isDirectUpload: { isDirectUpload: {
type: Boolean, type: Boolean,
required: true, required: false,
default: false default: false
}, },
isDemo: { isDemo: {
type: Boolean, type: Boolean,
required: true, required: false,
default: false default: false
}, },
values: { values: {
@@ -429,10 +448,17 @@ export default {
) )
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) { if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
document.body.style.overflow = 'hidden' this.$nextTick(() => {
const root = this.$root.$el.parentNode.getRootNode()
const scrollbox = root.getElementById('scrollbox')
const parent = root.body || root.querySelector('div')
window.scrollbox.classList.add('h-full', 'overflow-y-auto') parent.style.overflow = 'hidden'
window.scrollbox.parentNode.classList.add('h-screen', 'overflow-y-auto')
scrollbox.classList.add('h-full', 'overflow-y-auto')
scrollbox.parentNode.classList.add('h-screen', 'overflow-y-auto')
scrollbox.parentNode.style.maxHeight = '-webkit-fill-available'
})
} }
}, },
methods: { methods: {
@@ -455,7 +481,7 @@ export default {
if (this.isCompleted) { if (this.isCompleted) {
return Promise.resolve({}) return Promise.resolve({})
} else { } else {
return fetch(this.submitPath, { return fetch(this.baseUrl + this.submitPath, {
method: 'POST', method: 'POST',
body: formData || new FormData(this.$refs.form) body: formData || new FormData(this.$refs.form)
}) })
@@ -10,6 +10,7 @@
data-tip="Type text" data-tip="Type text"
> >
<button <button
id="type_text_button"
class="btn btn-sm btn-circle" class="btn btn-sm btn-circle"
:class="{ 'btn-neutral': isTextSignature, 'btn-outline': !isTextSignature }" :class="{ 'btn-neutral': isTextSignature, 'btn-outline': !isTextSignature }"
@click.prevent="toggleTextInput" @click.prevent="toggleTextInput"
@@ -78,6 +79,7 @@
/> />
<input <input
v-if="isTextSignature" v-if="isTextSignature"
id="signature_text_input"
ref="textInput" ref="textInput"
class="base-input !text-2xl w-full mt-6" class="base-input !text-2xl w-full mt-6"
:required="field.required" :required="field.required"
@@ -99,6 +101,7 @@ export default {
IconTextSize, IconTextSize,
IconArrowsDiagonalMinimize2 IconArrowsDiagonalMinimize2
}, },
inject: ['baseUrl'],
props: { props: {
field: { field: {
type: Object, type: Object,
@@ -304,7 +307,7 @@ export default {
file, file,
'/direct_uploads' '/direct_uploads'
).create((_error, data) => { ).create((_error, data) => {
fetch('/api/attachments', { fetch(this.baseUrl + '/api/attachments', {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
submitter_slug: this.submitterSlug, submitter_slug: this.submitterSlug,
@@ -326,12 +329,12 @@ export default {
formData.append('submitter_slug', this.submitterSlug) formData.append('submitter_slug', this.submitterSlug)
formData.append('name', 'attachments') formData.append('name', 'attachments')
return fetch('/api/attachments', { return fetch(this.baseUrl + '/api/attachments', {
method: 'POST', method: 'POST',
body: formData body: formData
}).then((resp) => resp.json()).then((attachment) => { }).then((resp) => resp.json()).then((attachment) => {
this.$emit('update:model-value', attachment.uuid)
this.$emit('attached', attachment) this.$emit('attached', attachment)
this.$emit('update:model-value', attachment.uuid)
return resolve(attachment) return resolve(attachment)
}) })
+16 -14
View File
@@ -226,14 +226,14 @@ export default {
} }
}, },
startResizeCell (e) { startResizeCell (e) {
document.addEventListener('mousemove', this.onResizeCell) this.$el.getRootNode().addEventListener('mousemove', this.onResizeCell)
document.addEventListener('mouseup', this.stopResizeCell) this.$el.getRootNode().addEventListener('mouseup', this.stopResizeCell)
this.$emit('start-resize', 'ew') this.$emit('start-resize', 'ew')
}, },
stopResizeCell (e) { stopResizeCell (e) {
document.removeEventListener('mousemove', this.onResizeCell) this.$el.getRootNode().removeEventListener('mousemove', this.onResizeCell)
document.removeEventListener('mouseup', this.stopResizeCell) this.$el.getRootNode().removeEventListener('mouseup', this.stopResizeCell)
this.$emit('stop-resize') this.$emit('stop-resize')
@@ -266,11 +266,13 @@ export default {
}) })
}, },
onNameBlur (e) { onNameBlur (e) {
const text = this.$refs.name.innerText.trim()
this.isNameFocus = false this.isNameFocus = false
this.$refs.name.style.minWidth = '' this.$refs.name.style.minWidth = ''
if (e.target.innerText.trim()) { if (text) {
this.field.name = e.target.innerText.trim() this.field.name = text
} else { } else {
this.field.name = '' this.field.name = ''
this.$refs.name.innerText = this.defaultName this.$refs.name.innerText = this.defaultName
@@ -302,14 +304,14 @@ export default {
this.dragFrom = { x: e.clientX - rect.left, y: e.clientY - rect.top } this.dragFrom = { x: e.clientX - rect.left, y: e.clientY - rect.top }
document.addEventListener('mousemove', this.drag) this.$el.getRootNode().addEventListener('mousemove', this.drag)
document.addEventListener('mouseup', this.stopDrag) this.$el.getRootNode().addEventListener('mouseup', this.stopDrag)
this.$emit('start-drag') this.$emit('start-drag')
}, },
stopDrag () { stopDrag () {
document.removeEventListener('mousemove', this.drag) this.$el.getRootNode().removeEventListener('mousemove', this.drag)
document.removeEventListener('mouseup', this.stopDrag) this.$el.getRootNode().removeEventListener('mouseup', this.stopDrag)
if (this.isDragged) { if (this.isDragged) {
this.save() this.save()
@@ -322,14 +324,14 @@ export default {
startResize () { startResize () {
this.selectedAreaRef.value = this.area this.selectedAreaRef.value = this.area
document.addEventListener('mousemove', this.resize) this.$el.getRootNode().addEventListener('mousemove', this.resize)
document.addEventListener('mouseup', this.stopResize) this.$el.getRootNode().addEventListener('mouseup', this.stopResize)
this.$emit('start-resize', 'nwse') this.$emit('start-resize', 'nwse')
}, },
stopResize () { stopResize () {
document.removeEventListener('mousemove', this.resize) this.$el.getRootNode().removeEventListener('mousemove', this.resize)
document.removeEventListener('mouseup', this.stopResize) this.$el.getRootNode().removeEventListener('mouseup', this.stopResize)
this.$emit('stop-resize') this.$emit('stop-resize')
+134 -50
View File
@@ -1,13 +1,17 @@
<template> <template>
<div <div
style="max-width: 1600px" style="max-width: 1600px"
class="mx-auto pl-4" class="mx-auto pl-4 h-full"
> >
<div class="flex justify-between py-1.5 items-center pr-4"> <div class="flex justify-between py-1.5 items-center pr-4">
<div class="flex space-x-3"> <div class="flex space-x-3">
<a href="/"> <a
v-if="withLogoLink"
href="/"
>
<Logo /> <Logo />
</a> </a>
<Logo v-else />
<Contenteditable <Contenteditable
:model-value="template.name" :model-value="template.name"
class="text-3xl font-semibold focus:text-clip" class="text-3xl font-semibold focus:text-clip"
@@ -16,46 +20,53 @@
/> />
</div> </div>
<div class="space-x-3 flex items-center"> <div class="space-x-3 flex items-center">
<a <slot
:href="`/templates/${template.id}/submissions/new`" v-if="$slots.buttons"
data-turbo-frame="modal" name="buttons"
class="btn btn-primary" />
> <template v-else>
<IconUsersPlus <a
width="20" :href="`/templates/${template.id}/submissions/new`"
class="inline" data-turbo-frame="modal"
/> class="btn btn-primary"
<span class="hidden md:inline"> >
Recipients <IconUsersPlus
</span> width="20"
</a> class="inline"
<button />
class="base-button" <span class="hidden md:inline">
:class="{ disabled: isSaving }" Recipients
v-bind="isSaving ? { disabled: true } : {}" </span>
@click.prevent="onSaveClick" </a>
> <button
<IconInnerShadowTop class="base-button"
v-if="isSaving" :class="{ disabled: isSaving }"
width="20" v-bind="isSaving ? { disabled: true } : {}"
class="animate-spin" @click.prevent="onSaveClick"
/> >
<IconDeviceFloppy <IconInnerShadowTop
v-else v-if="isSaving"
width="20" width="20"
/> class="animate-spin"
<span class="hidden md:inline"> />
Save <IconDeviceFloppy
</span> v-else
</button> width="20"
/>
<span class="hidden md:inline">
Save
</span>
</button>
</template>
</div> </div>
</div> </div>
<div <div
class="flex" class="flex"
style="max-height: calc(100vh - 60px)" style="max-height: calc(100% - 60px)"
> >
<div <div
ref="previews" ref="previews"
: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"
> >
<DocumentPreview <DocumentPreview
@@ -94,19 +105,46 @@
@success="updateFromUpload" @success="updateFromUpload"
/> />
<template v-else> <template v-else>
<Document <template
v-for="document in sortedDocuments" v-for="document in sortedDocuments"
:key="document.uuid" :key="document.uuid"
:ref="setDocumentRefs" >
:areas-index="fieldAreasIndex[document.uuid]" <Document
:selected-submitter="selectedSubmitter" :ref="setDocumentRefs"
:document="document" :areas-index="fieldAreasIndex[document.uuid]"
:is-drag="!!dragFieldType" :selected-submitter="selectedSubmitter"
:draw-field="drawField" :document="document"
@draw="onDraw" :is-drag="!!dragFieldType"
@drop-field="onDropfield" :draw-field="drawField"
@remove-area="removeArea" @draw="onDraw"
/> @drop-field="onDropfield"
@remove-area="removeArea"
/>
<DocumentControls
v-if="isBreakpointLg"
:with-arrows="template.schema.length > 1"
:item="template.schema.find((item) => item.attachment_uuid === document.uuid)"
:document="document"
:template="template"
:is-direct-upload="isDirectUpload"
class="pb-2 mb-2 border-b border-base-300 border-dashed"
@remove="onDocumentRemove"
@replace="onDocumentReplace"
@up="moveDocument(template.schema.find((item) => item.attachment_uuid === document.uuid), -1)"
@down="moveDocument(template.schema.find((item) => item.attachment_uuid === document.uuid), 1)"
@change="save"
/>
</template>
<div
v-if="sortedDocuments.length && isBreakpointLg"
class="pb-4"
>
<Upload
:template-id="template.id"
:is-direct-upload="isDirectUpload"
@success="updateFromUpload"
/>
</div>
</template> </template>
</div> </div>
<div <div
@@ -122,7 +160,7 @@
</div> </div>
</div> </div>
<div <div
class="relative w-80 flex-none pt-0.5 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-auto'"
> >
<div <div
@@ -149,6 +187,7 @@
:fields="template.fields" :fields="template.fields"
:submitters="template.submitters" :submitters="template.submitters"
:selected-submitter="selectedSubmitter" :selected-submitter="selectedSubmitter"
:with-sticky-submitters="withStickySubmitters"
@set-draw="drawField = $event" @set-draw="drawField = $event"
@set-drag="dragFieldType = $event" @set-drag="dragFieldType = $event"
@change-submitter="selectedSubmitter = $event" @change-submitter="selectedSubmitter = $event"
@@ -169,6 +208,7 @@ import Document from './document'
import Logo from './logo' 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 { IconUsersPlus, IconDeviceFloppy, IconInnerShadowTop } from '@tabler/icons-vue' import { IconUsersPlus, IconDeviceFloppy, IconInnerShadowTop } from '@tabler/icons-vue'
import { v4 } from 'uuid' import { v4 } from 'uuid'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
@@ -182,6 +222,7 @@ export default {
Logo, Logo,
Dropzone, Dropzone,
DocumentPreview, DocumentPreview,
DocumentControls,
IconInnerShadowTop, IconInnerShadowTop,
Contenteditable, Contenteditable,
IconUsersPlus, IconUsersPlus,
@@ -191,6 +232,7 @@ export default {
return { return {
template: this.template, template: this.template,
save: this.save, save: this.save,
baseFetch: this.baseFetch,
selectedAreaRef: computed(() => this.selectedAreaRef) selectedAreaRef: computed(() => this.selectedAreaRef)
} }
}, },
@@ -201,13 +243,34 @@ export default {
}, },
isDirectUpload: { isDirectUpload: {
type: Boolean, type: Boolean,
required: true, required: false,
default: false default: false
},
baseUrl: {
type: String,
required: false,
default: ''
},
withLogoLink: {
type: Boolean,
required: false,
default: true
},
withStickySubmitters: {
type: Boolean,
required: false,
default: true
},
fetchOptions: {
type: Object,
required: false,
default: () => ({ headers: {} })
} }
}, },
data () { data () {
return { return {
documentRefs: [], documentRefs: [],
isBreakpointLg: false,
isSaving: false, isSaving: false,
selectedSubmitter: null, selectedSubmitter: null,
drawField: null, drawField: null,
@@ -244,15 +307,28 @@ export default {
this.selectedSubmitter = this.template.submitters[0] this.selectedSubmitter = this.template.submitters[0]
}, },
mounted () { mounted () {
this.$nextTick(() => {
this.onWindowResize()
})
document.addEventListener('keyup', this.onKeyUp) document.addEventListener('keyup', this.onKeyUp)
window.addEventListener('resize', this.onWindowResize)
}, },
unmounted () { unmounted () {
document.removeEventListener('keyup', this.onKeyUp) document.removeEventListener('keyup', this.onKeyUp)
window.removeEventListener('resize', this.onWindowResize)
}, },
beforeUpdate () { beforeUpdate () {
this.documentRefs = [] this.documentRefs = []
}, },
methods: { methods: {
onWindowResize (e) {
const breakpointLg = 1024
this.isBreakpointLg = this.$el.getRootNode().querySelector('div').offsetWidth < breakpointLg
},
setDocumentRefs (el) { setDocumentRefs (el) {
if (el) { if (el) {
this.documentRefs.push(el) this.documentRefs.push(el)
@@ -474,10 +550,18 @@ export default {
this.selectedAreaRef.value = area this.selectedAreaRef.value = area
}, },
baseFetch (path, options = {}) {
return fetch(this.baseUrl + path, {
...options,
headers: { ...this.fetchOptions.headers, ...options.headers }
})
},
save () { save () {
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)
}
return fetch(`/api/templates/${this.template.id}`, { return this.baseFetch(`/api/templates/${this.template.id}`, {
method: 'PUT', method: 'PUT',
body: JSON.stringify({ body: JSON.stringify({
template: { template: {
@@ -0,0 +1,91 @@
<template>
<div class="flex space-x-2">
<Contenteditable
class="w-full block mr-6"
:model-value="item.name"
:icon-width="16"
@update:model-value="onUpdateName"
/>
<ReplaceButton
:is-direct-upload="isDirectUpload"
:template-id="template.id"
@click.stop
@success="$emit('replace', { replaceSchemaItem: item, ...$event })"
/>
<button
v-if="withArrows"
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
style="width: 24px; height: 24px"
@click.stop="$emit('up', item)"
>
&uarr;
</button>
<button
v-if="withArrows"
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
style="width: 24px; height: 24px"
@click.stop="$emit('down', item)"
>
&darr;
</button>
<button
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
style="width: 24px; height: 24px"
@click.stop="$emit('remove', item)"
>
&times;
</button>
</div>
</template>
<script>
import Contenteditable from './contenteditable'
import Upload from './upload'
import ReplaceButton from './replace'
export default {
name: 'DocumentControls',
components: {
Contenteditable,
ReplaceButton
},
props: {
item: {
type: Object,
required: true
},
template: {
type: Object,
required: true
},
document: {
type: Object,
required: true
},
isDirectUpload: {
type: Boolean,
required: true,
default: false
},
withArrows: {
type: Boolean,
required: false,
default: true
}
},
emits: ['change', 'remove', 'up', 'down', 'replace'],
mounted () {
if (this.isDirectUpload) {
import('@rails/activestorage')
}
},
methods: {
upload: Upload.methods.upload,
onUpdateName (value) {
this.item.name = value
this.$emit('change')
}
}
}
</script>
@@ -61,6 +61,7 @@ export default {
IconCloudUpload, IconCloudUpload,
IconInnerShadowTop IconInnerShadowTop
}, },
inject: ['baseFetch'],
props: { props: {
templateId: { templateId: {
type: [Number, String], type: [Number, String],
+9 -7
View File
@@ -58,7 +58,7 @@
<label <label
tabindex="0" tabindex="0"
title="Areas" title="Areas"
class="cursor-pointer text-base-100 group-hover:text-base-content" class="cursor-pointer text-transparent group-hover:text-base-content"
> >
<IconShape <IconShape
:width="18" :width="18"
@@ -104,7 +104,7 @@
<button <button
v-else v-else
title="Areas" title="Areas"
class="relative cursor-pointer text-base-100 group-hover:text-base-content" class="relative cursor-pointer text-transparent group-hover:text-base-content"
@click="$emit('set-draw', field)" @click="$emit('set-draw', field)"
> >
<IconShape <IconShape
@@ -113,7 +113,7 @@
/> />
</button> </button>
<button <button
class="relative text-base-100 group-hover:text-base-content" class="relative text-transparent group-hover:text-base-content"
title="Remove" title="Remove"
@click="$emit('remove', field)" @click="$emit('remove', field)"
> >
@@ -122,7 +122,7 @@
:stroke-width="1.6" :stroke-width="1.6"
/> />
</button> </button>
<div class="flex flex-col pr-1 text-base-100 group-hover:text-base-content"> <div class="flex flex-col pr-1 text-transparent group-hover:text-base-content">
<button <button
title="Up" title="Up"
class="relative" class="relative"
@@ -156,7 +156,7 @@
</span> </span>
<input <input
v-model="field.options[index]" v-model="field.options[index]"
class="w-full input input-primary input-xs text-sm" class="w-full input input-primary input-xs text-sm bg-transparent"
type="text" type="text"
required required
@blur="save" @blur="save"
@@ -254,8 +254,10 @@ export default {
}) })
}, },
onNameBlur (e) { onNameBlur (e) {
if (e.target.innerText.trim()) { const text = this.$refs.name.$refs.contenteditable.innerText.trim()
this.field.name = e.target.innerText.trim()
if (text) {
this.field.name = text
} else { } else {
this.field.name = '' this.field.name = ''
this.$refs.name.$refs.contenteditable.innerText = this.defaultName this.$refs.name.$refs.contenteditable.innerText = this.defaultName
+9 -3
View File
@@ -1,8 +1,9 @@
<template> <template>
<div class="sticky -top-1 bg-base-100 pt-1 -mt-1 z-10"> <div :class="withStickySubmitters ? 'sticky top-0 z-10' : ''">
<FieldSubmitter <FieldSubmitter
:model-value="selectedSubmitter.uuid" :model-value="selectedSubmitter.uuid"
class="w-full bg-base-100" class="w-full rounded-lg"
:class="{ 'bg-base-100': withStickySubmitters }"
:submitters="submitters" :submitters="submitters"
@new-submitter="save" @new-submitter="save"
@remove="removeSubmitter" @remove="removeSubmitter"
@@ -28,7 +29,7 @@
v-for="(icon, type) in fieldIcons" v-for="(icon, type) in fieldIcons"
:key="type" :key="type"
draggable="true" draggable="true"
class="flex items-center justify-center border border-dashed border-base-300 bg-base-100 w-full rounded relative" class="flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
@dragstart="onDragstart(type)" @dragstart="onDragstart(type)"
@dragend="$emit('drag-end')" @dragend="$emit('drag-end')"
@click="addField(type)" @click="addField(type)"
@@ -106,6 +107,11 @@ export default {
type: Array, type: Array,
required: true required: true
}, },
withStickySubmitters: {
type: Boolean,
required: false,
default: true
},
submitters: { submitters: {
type: Array, type: Array,
required: true required: true
+1 -1
View File
@@ -4,7 +4,7 @@
ref="image" ref="image"
:src="image.url" :src="image.url"
:width="width" :width="width"
class="shadow-md mb-4" class="border rounded mb-4"
:height="height" :height="height"
loading="lazy" loading="lazy"
> >
@@ -26,6 +26,7 @@ import Upload from './upload'
export default { export default {
name: 'ReplaceDocument', name: 'ReplaceDocument',
inject: ['baseFetch'],
props: { props: {
templateId: { templateId: {
type: [Number, String], type: [Number, String],
+3 -2
View File
@@ -50,6 +50,7 @@ export default {
IconUpload, IconUpload,
IconInnerShadowTop IconInnerShadowTop
}, },
inject: ['baseFetch'],
props: { props: {
templateId: { templateId: {
type: [Number, String], type: [Number, String],
@@ -113,7 +114,7 @@ export default {
this.isProcessing = true this.isProcessing = true
fetch(`/api/templates/${this.templateId}/documents`, { this.baseFetch(`/api/templates/${this.templateId}/documents`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ blobs }), body: JSON.stringify({ blobs }),
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
@@ -124,7 +125,7 @@ export default {
this.isProcessing = false this.isProcessing = false
}) })
} else { } else {
fetch(`/api/templates/${this.templateId}/documents`, { this.baseFetch(`/api/templates/${this.templateId}/documents`, {
method: 'POST', method: 'POST',
body: new FormData(this.$refs.form) body: new FormData(this.$refs.form)
}).then(resp => resp.json()).then((data) => { }).then(resp => resp.json()).then((data) => {
+4
View File
@@ -6,6 +6,10 @@ class ApplicationMailer < ActionMailer::Base
register_interceptor ActionMailerConfigsInterceptor register_interceptor ActionMailerConfigsInterceptor
before_action do
ActiveStorage::Current.url_options = Docuseal.default_url_options
end
def default_url_options def default_url_options
Docuseal.default_url_options Docuseal.default_url_options
end end
+44 -4
View File
@@ -4,23 +4,46 @@ class SubmitterMailer < ApplicationMailer
DEFAULT_MESSAGE = %(You have been invited to submit the "%<name>s" form:) DEFAULT_MESSAGE = %(You have been invited to submit the "%<name>s" form:)
def invitation_email(submitter, message: '') def invitation_email(submitter, message: '')
@current_account = submitter.submission.template.account
@submitter = submitter @submitter = submitter
@message = message.presence || format(DEFAULT_MESSAGE, name: submitter.submission.template.name) @message = message.presence || format(DEFAULT_MESSAGE, name: submitter.submission.template.name)
@email_config = @current_account.account_configs.find_by(key: AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY)
subject =
if @email_config
ReplaceEmailVariables.call(@email_config.value['subject'], submitter:)
else
'You have been invited to submit a form'
end
mail(to: @submitter.email, mail(to: @submitter.email,
subject: 'You have been invited to submit a form', from: from_address_for_submitter(submitter),
subject:,
reply_to: submitter.submission.created_by_user&.friendly_name) reply_to: submitter.submission.created_by_user&.friendly_name)
end end
def completed_email(submitter, user) def completed_email(submitter, user)
@current_account = submitter.submission.template.account
@submitter = submitter @submitter = submitter
@user = user @user = user
mail(to: user.email, @email_config = @current_account.account_configs.find_by(key: AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY)
subject: %(#{submitter.email} has completed the "#{submitter.submission.template.name}" form))
subject =
if @email_config
ReplaceEmailVariables.call(@email_config.value['subject'], submitter:)
else
%(#{submitter.email} has completed the "#{submitter.submission.template.name}" form)
end
mail(from: from_address_for_submitter(submitter),
to: user.email,
subject:)
end end
def documents_copy_email(submitter) def documents_copy_email(submitter)
@current_account = submitter.submission.template.account
@submitter = submitter @submitter = submitter
Submissions::EnsureResultGenerated.call(@submitter) Submissions::EnsureResultGenerated.call(@submitter)
@@ -31,6 +54,23 @@ class SubmitterMailer < ApplicationMailer
attachments[attachment.filename.to_s] = attachment.download attachments[attachment.filename.to_s] = attachment.download
end end
mail(to: submitter.email, subject: 'Your copy of documents') @email_config = @current_account.account_configs.find_by(key: AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
subject =
if @email_config
ReplaceEmailVariables.call(@email_config.value['subject'], submitter:)
else
'Your copy of documents'
end
mail(from: from_address_for_submitter(submitter),
to: submitter.email,
subject:)
end
private
def from_address_for_submitter(submitter)
submitter.submission.created_by_user&.friendly_name || submitter.submission.template.author.friendly_name
end end
end end
+1
View File
@@ -2,6 +2,7 @@
class UserMailer < ApplicationMailer class UserMailer < ApplicationMailer
def invitation_email(user) def invitation_email(user)
@current_account = user.account
@user = user @user = user
@token = @user.send(:set_reset_password_token) @token = @user.send(:set_reset_password_token)
+1
View File
@@ -14,6 +14,7 @@
class Account < ApplicationRecord class Account < ApplicationRecord
has_many :users, dependent: :destroy has_many :users, dependent: :destroy
has_many :encrypted_configs, dependent: :destroy has_many :encrypted_configs, dependent: :destroy
has_many :account_configs, dependent: :destroy
has_many :templates, dependent: :destroy has_many :templates, dependent: :destroy
has_many :submissions, through: :templates has_many :submissions, through: :templates
has_many :submitters, through: :submissions has_many :submitters, through: :submissions
+60
View File
@@ -0,0 +1,60 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: account_configs
#
# id :bigint not null, primary key
# key :string not null
# value :text not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
#
# Indexes
#
# index_account_configs_on_account_id (account_id)
# index_account_configs_on_account_id_and_key (account_id,key) UNIQUE
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id)
#
class AccountConfig < ApplicationRecord
SUBMITTER_INVITATION_EMAIL_KEY = 'submitter_invitation_email'
SUBMITTER_COMPLETED_EMAIL_KEY = 'submitter_completed_email'
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY = 'submitter_documents_copy_email'
DEFAULT_VALUES = {
SUBMITTER_INVITATION_EMAIL_KEY => {
'subject' => 'You have been invited to submit a form',
'body' => "Hi there,\n\n" \
"You have been invited to submit the \"{{template.name}}\" form:\n\n" \
"{{submitter.link}}\n\n" \
"Please contact us by replying to this email if you didn't request this.\n\n" \
"Thanks,\n" \
'{{account.name}}'
},
SUBMITTER_COMPLETED_EMAIL_KEY => {
'subject' => '{{submitter.email}} has completed the "{{template.name}}" form',
'body' => "Hi,\n\n" \
"{{submitter.email}} has completed the \"{{template.name}}\" form:\n\n" \
"{{submission.link}}\n\n" \
"Thanks,\n" \
'{{account.name}}'
},
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => {
'subject' => 'Your copy of documents',
'body' => "Hi there,\n\n" \
"Please check the copy of your \"{{template.name}}\" submission in the email attachments.\n" \
"Alternatively, you can download the copy using:\n\n" \
"{{documents.links}}\n\n" \
"Thanks,\n" \
'{{account.name}}'
}
}.freeze
belongs_to :account
serialize :value, JSON
end
+7 -5
View File
@@ -21,11 +21,13 @@
# fk_rails_... (account_id => accounts.id) # fk_rails_... (account_id => accounts.id)
# #
class EncryptedConfig < ApplicationRecord class EncryptedConfig < ApplicationRecord
FILES_STORAGE_KEY = 'active_storage' CONFIG_KEYS = [
EMAIL_SMTP_KEY = 'action_mailer_smtp' FILES_STORAGE_KEY = 'active_storage',
ESIGN_CERTS_KEY = 'esign_certs' EMAIL_SMTP_KEY = 'action_mailer_smtp',
APP_URL_KEY = 'app_url' ESIGN_CERTS_KEY = 'esign_certs',
WEBHOOK_URL_KEY = 'webhook_url' APP_URL_KEY = 'app_url',
WEBHOOK_URL_KEY = 'webhook_url'
].freeze
belongs_to :account belongs_to :account
+2
View File
@@ -10,6 +10,7 @@
# name :string not null # name :string not null
# schema :text not null # schema :text not null
# slug :string not null # slug :string not null
# source :text not null
# submitters :text not null # submitters :text not null
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
@@ -37,6 +38,7 @@ class Template < ApplicationRecord
attribute :schema, :string, default: -> { [] } attribute :schema, :string, default: -> { [] }
attribute :submitters, :string, default: -> { [{ name: DEFAULT_SUBMITTER_NAME, uuid: SecureRandom.uuid }] } attribute :submitters, :string, default: -> { [{ name: DEFAULT_SUBMITTER_NAME, uuid: SecureRandom.uuid }] }
attribute :slug, :string, default: -> { SecureRandom.base58(14) } attribute :slug, :string, default: -> { SecureRandom.base58(14) }
attribute :source, :string, default: 'native'
serialize :fields, JSON serialize :fields, JSON
serialize :schema, JSON serialize :schema, JSON
+15 -7
View File
@@ -11,8 +11,8 @@
# 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
# first_name :string not null # first_name :string
# last_name :string not null # last_name :string
# last_sign_in_at :datetime # last_sign_in_at :datetime
# last_sign_in_ip :string # last_sign_in_ip :string
# locked_at :datetime # locked_at :datetime
@@ -40,20 +40,24 @@
# fk_rails_... (account_id => accounts.id) # fk_rails_... (account_id => accounts.id)
# #
class User < ApplicationRecord class User < ApplicationRecord
ROLES = %w[admin].freeze ROLES = [
ADMIN_ROLE = 'admin'
].freeze
EMAIL_REGEXP = /[^@,\s]+@[^@,\s]+/ EMAIL_REGEXP = /[^@,\s]+@[^@,\s]+/
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
devise :database_authenticatable, :recoverable, :rememberable, :validatable, :trackable devise :database_authenticatable, :recoverable, :rememberable, :validatable, :trackable
devise :registerable, :omniauthable, omniauth_providers: [:google_oauth2] if Docuseal.multitenant? devise :registerable, :omniauthable, omniauth_providers: [:google_oauth2] if Docuseal.multitenant?
attribute :role, :string, default: 'admin' 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(deleted_at: nil) }
scope :admins, -> { where(role: ADMIN_ROLE) }
def access_token def access_token
super || build_access_token.tap(&:save!) super || build_access_token.tap(&:save!)
@@ -68,14 +72,18 @@ class User < ApplicationRecord
end end
def initials def initials
[first_name.first, last_name.first].join.upcase [first_name&.first, last_name&.first].compact_blank.join.upcase
end end
def full_name def full_name
[first_name, last_name].join(' ') [first_name, last_name].compact_blank.join(' ')
end end
def friendly_name def friendly_name
"#{full_name} <#{email}>" if full_name.present?
"#{full_name} <#{email}>"
else
email
end
end end
end end
+1 -1
View File
@@ -90,7 +90,7 @@
</div> </div>
<div class="collapse-content" style="display: inherit"> <div class="collapse-content" style="display: inherit">
<div class="mockup-code overflow-hidden"> <div class="mockup-code overflow-hidden">
<% text = capture do %>curl '<%= api_template_url(current_account.templates.last) %>' \ <% text = capture do %>curl '<%= api_template_url(current_account.templates&.last || 1) %>' \
--header 'X-Auth-Token: <%= current_user.access_token.token %>'<% end.to_str %> --header 'X-Auth-Token: <%= current_user.access_token.token %>'<% end.to_str %>
<span class="top-0 right-0 absolute"> <span class="top-0 right-0 absolute">
<%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %> <%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
+1 -1
View File
@@ -17,7 +17,7 @@
<div class="pb-6 pt-1 space-y-1"> <div class="pb-6 pt-1 space-y-1">
<p class="flex items-center space-x-1 text-xs text-base-content/60"> <p class="flex items-center space-x-1 text-xs text-base-content/60">
<%= svg_icon('user', class: 'w-4 h-4') %> <%= svg_icon('user', class: 'w-4 h-4') %>
<span><%= template.author.full_name %></span> <span><%= template.author.full_name.presence || template.author.email %></span>
</p> </p>
<p class="flex items-center space-x-1 text-xs text-base-content/60"> <p class="flex items-center space-x-1 text-xs text-base-content/60">
<%= svg_icon('calendar', class: 'w-4 h-4') %> <%= svg_icon('calendar', class: 'w-4 h-4') %>
+4 -1
View File
@@ -1,6 +1,9 @@
<div class="max-w-xl mx-auto px-2"> <div class="max-w-xl mx-auto px-2">
<h1 class="text-3xl font-bold text-center my-8">Profile Details</h1> <h1 class="text-3xl font-bold text-center my-8">Profile Details</h1>
<%= form_for('', as: resource_name, html: { class: 'space-y-6' }, url: new_registration_path) do |f| %> <%= form_for('', as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: new_registration_path) do |f| %>
<% if params[:redir].present? %>
<%= hidden_field_tag :redir, params[:redir] %>
<% end %>
<div class="space-y-2"> <div class="space-y-2">
<%= render 'devise/shared/error_messages', resource: %> <%= render 'devise/shared/error_messages', resource: %>
<%= f.fields_for resource do |ff| %> <%= f.fields_for resource do |ff| %>
+4 -1
View File
@@ -1,6 +1,9 @@
<div class="max-w-xl mx-auto px-2"> <div class="max-w-xl mx-auto px-2">
<h1 class="text-3xl font-bold text-center my-8">Create Free Account</h1> <h1 class="text-3xl font-bold text-center my-8">Create Free Account</h1>
<%= form_for(User.new, html: { class: 'space-y-6' }, url: new_registration_path, method: :get) do |f| %> <%= form_for(User.new, html: { class: 'space-y-6' }, url: new_registration_path, data: { turbo: params[:redir].blank? }, method: :get) do |f| %>
<% if params[:redir].present? %>
<%= hidden_field_tag :redir, params[:redir] %>
<% end %>
<div class="space-y-2"> <div class="space-y-2">
<div class="form-control"> <div class="form-control">
<%= f.label :email, class: 'label' %> <%= f.label :email, class: 'label' %>
+4 -1
View File
@@ -1,6 +1,9 @@
<div class="max-w-xl mx-auto px-2"> <div class="max-w-xl mx-auto px-2">
<h1 class="text-4xl font-bold text-center my-8">Log In</h1> <h1 class="text-4xl font-bold text-center my-8">Log In</h1>
<%= form_for(resource, as: resource_name, html: { class: 'space-y-6' }, url: session_path(resource_name)) do |f| %> <%= form_for(resource, as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: session_path(resource_name)) do |f| %>
<% if params[:redir].present? %>
<%= hidden_field_tag :redir, params[:redir] %>
<% end %>
<div class="space-y-2"> <div class="space-y-2">
<div class="form-control"> <div class="form-control">
<%= f.label :email, class: 'label' %> <%= f.label :email, class: 'label' %>
+1 -1
View File
@@ -3,7 +3,7 @@
<%= link_to 'Log in', new_session_path(resource_name), class: 'link link-hover' %> <%= link_to 'Log in', new_session_path(resource_name), class: 'link link-hover' %>
<% end %> <% end %>
<%- if devise_mapping.registerable? && controller_name != 'registrations' %> <%- if devise_mapping.registerable? && controller_name != 'registrations' %>
<%= link_to 'Create free account', registration_path, class: 'link link-hover' %> <%= link_to 'Create free account', registration_path({ redir: params[:redir] }.compact_blank), class: 'link link-hover' %>
<% end %> <% end %>
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> <%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
<%= link_to 'Forgot your password?', new_password_path(resource_name), class: 'link link-hover' %> <%= link_to 'Forgot your password?', new_password_path(resource_name), class: 'link link-hover' %>
+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#000000" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M8 9l3 3l-3 3" />
<path d="M13 15l3 0" />
<path d="M3 4m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" />
</svg>

After

Width:  |  Height:  |  Size: 428 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#000000" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" />
<path d="M21 21l-6 -6" />
<path d="M7 10l2 2l4 -4" />
</svg>

After

Width:  |  Height:  |  Size: 394 B

+1 -6
View File
@@ -8,11 +8,6 @@
</head> </head>
<body> <body>
<%= yield %> <%= yield %>
<p> <%= render partial: 'shared/mailer_attribution' %>
---
</p>
<p>
Sent using <a href="<%= Docuseal::PRODUCT_URL %>"><%= Docuseal::PRODUCT_NAME %></a> free document signing.
</p>
</body> </body>
</html> </html>
@@ -0,0 +1 @@
<%= render 'logo_placeholder' %>
@@ -0,0 +1,11 @@
<div class="alert my-4">
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
<div>
<p class="font-bold">Unlock with DocuSeal Pro</p>
<p>
Display your company name and logo when signing documents.
<br>
<a class="link font-medium" target="_blank" href="<%= "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">Learn More</a>
</p>
</div>
</div>
@@ -0,0 +1,93 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
<%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto">
<p class="text-4xl font-bold mb-4">Email Templates</p>
<div class="collapse collapse-plus bg-base-200 px-1">
<input type="checkbox">
<div class="collapse-title text-xl font-medium">
<div>
Signature Request Email
</div>
</div>
<div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<%= f.hidden_field :key %>
<%= f.fields_for :value, Struct.new(:subject, :body).new(*f.object.value.values_at('subject', 'body')) do |ff| %>
<div class="form-control">
<%= ff.label :subject, class: 'label' %>
<%= ff.text_field :subject, required: true, class: 'base-input' %>
</div>
<div class="form-control">
<%= ff.label :body, class: 'label' %>
<autoresize-textarea>
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2' %>
</autoresize-textarea>
</div>
<% end %>
<div class="form-control pt-2">
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button' %>
</div>
<% end %>
</div>
</div>
<div class="collapse collapse-plus bg-base-200 px-1 mt-4">
<input type="checkbox">
<div class="collapse-title text-xl font-medium">
<div>
Form Completed Email
</div>
</div>
<div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<%= f.hidden_field :key %>
<%= f.fields_for :value, Struct.new(:subject, :body).new(*f.object.value.values_at('subject', 'body')) do |ff| %>
<div class="form-control">
<%= ff.label :subject, class: 'label' %>
<%= ff.text_field :subject, required: true, class: 'base-input' %>
</div>
<div class="form-control">
<%= ff.label :body, class: 'label' %>
<autoresize-textarea>
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2' %>
</autoresize-textarea>
</div>
<% end %>
<div class="form-control pt-2">
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button' %>
</div>
<% end %>
</div>
</div>
<div class="collapse collapse-plus bg-base-200 px-1 mt-4">
<input type="checkbox">
<div class="collapse-title text-xl font-medium">
<div>
Documents Copy Email
</div>
</div>
<div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<%= f.hidden_field :key %>
<%= f.fields_for :value, Struct.new(:subject, :body).new(*f.object.value.values_at('subject', 'body')) do |ff| %>
<div class="form-control">
<%= ff.label :subject, class: 'label' %>
<%= ff.text_field :subject, required: true, class: 'base-input' %>
</div>
<div class="form-control">
<%= ff.label :body, class: 'label' %>
<autoresize-textarea>
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2' %>
</autoresize-textarea>
</div>
<% end %>
<div class="form-control pt-2">
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button' %>
</div>
<% end %>
</div>
</div>
<p class="text-4xl font-bold mb-4 mt-8">Company Logo</p>
<%= render 'logo_form' %>
</div>
<div class="w-0 md:w-52"></div>
</div>
@@ -2,12 +2,7 @@
<div class="space-y-6 mx-auto"> <div class="space-y-6 mx-auto">
<div class="space-y-6"> <div class="space-y-6">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<a href="/" class="flex items-center"> <%= render 'start_form/docuseal_logo' %>
<div class="mr-3">
<%= render 'shared/logo', width: '50px', height: '50px' %>
</div>
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
</a>
</div> </div>
<div class="text-center text-4xl font-bold"> <div class="text-center text-4xl font-bold">
Email has been sent Email has been sent
+10
View File
@@ -6,12 +6,22 @@
<span class="hidden md:inline"> <span class="hidden md:inline">
<%= local_assigns[:copy_title] || 'Copy' %> <%= local_assigns[:copy_title] || 'Copy' %>
</span> </span>
<% if local_assigns[:copy_title_md] %>
<span class="inline md:hidden">
<%= local_assigns[:copy_title_md] %>
</span>
<% end %>
</span> </span>
<span class="hidden peer-checked:flex items-center space-x-2"> <span class="hidden peer-checked:flex items-center space-x-2">
<%= svg_icon(local_assigns[:copied_icon] || 'clipboard_copy', class: local_assigns[:icon_class] || 'w-6 h-6 text-white') %> <%= svg_icon(local_assigns[:copied_icon] || 'clipboard_copy', class: local_assigns[:icon_class] || 'w-6 h-6 text-white') %>
<span class="hidden md:inline"> <span class="hidden md:inline">
<%= local_assigns[:copied_title] || 'Copied' %> <%= local_assigns[:copied_title] || 'Copied' %>
</span> </span>
<% if local_assigns[:copied_title_md] %>
<span class="inline md:hidden">
<%= local_assigns[:copied_title_md] %>
</span>
<% end %>
</span> </span>
</label> </label>
</clipboard-copy> </clipboard-copy>
@@ -0,0 +1,6 @@
<p>
---
</p>
<p>
Sent using <a href="<%= Docuseal::PRODUCT_URL %>"><%= Docuseal::PRODUCT_NAME %></a> free document signing.
</p>
@@ -0,0 +1 @@
<%= render 'shared/email_attribution' %>
+25 -4
View File
@@ -15,7 +15,14 @@
<% if Docuseal.demo? %> <% if Docuseal.demo? %>
<%= render 'shared/github_button' %> <%= render 'shared/github_button' %>
<% else %> <% else %>
<%= link_to 'Settings', settings_profile_index_path, class: 'hidden md:inline font-medium text-lg mr-2' %> <div class="flex items-center justify-center space-x-4 mr-1">
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, class: 'hidden md:inline-flex items-center font-medium text-lg', data: { prefetch: false } do %>
Console
<span class="badge badge-warning ml-1">New</span>
<% end %>
<span class="hidden md:inline-flex h-3 border-r border-base-content"></span>
<%= link_to 'Settings', settings_profile_index_path, class: 'hidden md:inline-flex font-medium text-lg' %>
</div>
<% end %> <% end %>
<div class="dropdown dropdown-end"> <div class="dropdown dropdown-end">
<label tabindex="0" class="cursor-pointer bg-base-content text-purple-300 rounded-full p-2 w-9 justify-center flex"> <label tabindex="0" class="cursor-pointer bg-base-content text-purple-300 rounded-full p-2 w-9 justify-center flex">
@@ -23,14 +30,28 @@
</label> </label>
<ul tabindex="0" class="z-10 dropdown-content p-2 mt-2 shadow menu text-base bg-base-100 rounded-box whitespace-nowrap text-right"> <ul tabindex="0" class="z-10 dropdown-content p-2 mt-2 shadow menu text-base bg-base-100 rounded-box whitespace-nowrap text-right">
<li> <li>
<%= link_to settings_profile_index_path do %> <%= link_to settings_profile_index_path, class: 'flex items-center' do %>
<%= svg_icon('adjustments', class: 'w-5 h-5 stroke-2') %> <%= svg_icon('adjustments', class: 'w-5 h-5 stroke-2') %>
<span class="mr-1">Profile</span> <span class="mr-1">Profile</span>
<% end %> <% end %>
</li> </li>
<% unless Docuseal.demo? %>
<li>
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, data: { prefetch: false }, class: 'flex items-center' do %>
<%= svg_icon('terminal', class: 'w-5 h-5 stroke-2') %>
Console
<% end %>
</li>
<% end %>
<li> <li>
<%= button_to destroy_user_session_path, method: :delete, data: { turbo: false } do %> <%= link_to settings_esign_index_path, class: 'flex items-center' do %>
<%= svg_icon('logout', class: 'w-5 h-5 stroke-2 inline') %> <%= svg_icon('zoom_check', class: 'w-5 h-5 stroke-2') %>
<span class="mr-1">Verify PDF</span>
<% end %>
</li>
<li>
<%= button_to destroy_user_session_path, method: :delete, data: { turbo: false }, class: 'flex items-center' do %>
<%= svg_icon('logout', class: 'w-5 h-5 stroke-2 mr-2 inline') %>
<span class="mr-1">Sign out</span> <span class="mr-1">Sign out</span>
<% end %> <% end %>
</li> </li>
+4 -1
View File
@@ -31,9 +31,12 @@
<%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %> <%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
<% end %> <% end %>
<li>
<%= link_to 'Personalization', settings_personalization_path, class: 'text-base hover:bg-base-300' %>
</li>
<% unless Docuseal.demo? %> <% unless Docuseal.demo? %>
<li> <li>
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, class: 'text-base hover:bg-base-300' do %> <%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
Console Console
<span class="badge badge-warning">New</span> <span class="badge badge-warning">New</span>
<% end %> <% end %>
+1 -1
View File
@@ -1,5 +1,5 @@
<turbo-frame id="modal"> <turbo-frame id="modal">
<turbo-modal class="modal modal-open items-start !animate-none overflow-y-auto"> <turbo-modal class="modal modal-open items-start !animate-none overflow-y-auto" data-close-after-submit="<%= local_assigns.key?(:close_after_submit) ? local_assigns[:close_after_submit] : true %>">
<div class="absolute top-0 bottom-0 right-0 left-0" data-action="click:turbo-modal#close"></div> <div class="absolute top-0 bottom-0 right-0 left-0" data-action="click:turbo-modal#close"></div>
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none"> <div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none">
<% if local_assigns[:title] %> <% if local_assigns[:title] %>
+1
View File
@@ -0,0 +1 @@
<%= render 'docuseal_logo' %>
@@ -0,0 +1,6 @@
<a href="/" class="flex justify-center items-center">
<span class="mr-3">
<%= render 'shared/logo', width: '50px', height: '50px' %>
</span>
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
</a>
+1 -6
View File
@@ -2,12 +2,7 @@
<div class="space-y-6 mx-auto"> <div class="space-y-6 mx-auto">
<div class="space-y-6"> <div class="space-y-6">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<a href="/" class="flex items-center"> <%= render 'banner' %>
<div class="mr-3">
<%= render 'shared/logo', width: '50px', height: '50px' %>
</div>
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
</a>
</div> </div>
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4"> <div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
<div class="flex items-center"> <div class="flex items-center">
+1 -6
View File
@@ -2,12 +2,7 @@
<div class="space-y-6 mx-auto"> <div class="space-y-6 mx-auto">
<div class="space-y-6"> <div class="space-y-6">
<div class="text-center w-full space-y-6"> <div class="text-center w-full space-y-6">
<a href="/" class="flex justify-center items-center"> <%= render 'banner' %>
<span class="mr-3">
<%= render 'shared/logo', width: '50px', height: '50px' %>
</span>
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
</a>
<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>
</div> </div>
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4"> <div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
+16 -14
View File
@@ -40,7 +40,7 @@
<div class="form-control"> <div class="form-control">
<% is_smtp_configured = Accounts.can_send_emails?(current_account) %> <% is_smtp_configured = Accounts.can_send_emails?(current_account) %>
<%= f.label :send_email, class: 'flex items-center cursor-pointer' do %> <%= f.label :send_email, class: 'flex items-center cursor-pointer' do %>
<%= f.check_box :send_email, class: 'base-checkbox', disabled: !is_smtp_configured, onchange: "message_field.classList.toggle('hidden', !event.currentTarget.checked)" %> <%= f.check_box :send_email, class: 'base-checkbox', disabled: !is_smtp_configured, onchange: "window.message_field && message_field.classList.toggle('hidden', !event.currentTarget.checked)" %>
<span class="label">Send Email</span> <span class="label">Send Email</span>
<% end %> <% end %>
<% unless is_smtp_configured %> <% unless is_smtp_configured %>
@@ -57,21 +57,23 @@
</div> </div>
<% end %> <% end %>
</div> </div>
<div id="message_field" class="card card-compact bg-base-200 hidden"> <% unless AccountConfig.exists?(key: AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) %>
<div class="card-body"> <div id="message_field" class="card card-compact bg-base-200 hidden">
<div class="form-control space-y-2"> <div class="card-body">
<span class="label-text">Hi there,</span> <div class="form-control space-y-2">
<autoresize-textarea> <span class="label-text">Hi there,</span>
<%= f.text_area :message, value: format(SubmitterMailer::DEFAULT_MESSAGE, name: @template.name), required: true, class: 'base-textarea !rounded-lg w-full' %> <autoresize-textarea>
</autoresize-textarea> <%= f.text_area :message, value: format(SubmitterMailer::DEFAULT_MESSAGE, name: @template.name), required: true, class: 'base-textarea !rounded-lg w-full' %>
<span class="label-text"> </autoresize-textarea>
Thanks, <span class="label-text">
<br> Thanks,
<%= current_account.name %> <br>
</span> <%= current_account.name %>
</span>
</div>
</div> </div>
</div> </div>
</div> <% end %>
<div class="form-control"> <div class="form-control">
<%= f.button button_title(title: 'Add Recipients'), class: 'base-button' %> <%= f.button button_title(title: 'Add Recipients'), class: 'base-button' %>
</div> </div>
+32
View File
@@ -0,0 +1,32 @@
<%= render 'shared/turbo_modal', title: 'Export', close_after_submit: false do %>
<div class="space-y-2">
<%= button_to template_submissions_export_index_path(@template), params: { format: :xlsx }, method: :get, data: { turbo_frame: :_top } do %>
<div class="flex items-center p-4 text-left rounded-2xl border border-neutral-300 hover:cursor-pointer hover:bg-neutral hover:text-gray-300">
<div class="enabled">
<%= svg_icon('download', class: 'w-12 h-12 stroke-2 mr-2') %>
</div>
<div class="disabled">
<%= svg_icon('loader', class: 'w-12 h-12 stroke-2 mr-2 animate-spin') %>
</div>
<div class="flex flex-col">
<span class="mb-1 text-lg font-semibold">XLSX</span>
<p class="text-sm"> Primarily opened with Microsoft Excel. Other options include Google Sheets, LibreOffice Calc, and OpenOffice Calc.</p>
</div>
</div>
<% end %>
<%= button_to template_submissions_export_index_path(@template), params: { format: :csv }, method: :get, data: { turbo_frame: :_top } do %>
<div class="flex items-center text-left p-4 rounded-2xl border border-neutral-300 hover:cursor-pointer hover:bg-neutral hover:text-gray-300">
<div class="enabled">
<%= svg_icon('download', class: 'w-12 h-12 stroke-2 mr-2') %>
</div>
<div class="disabled">
<%= svg_icon('loader', class: 'w-12 h-12 stroke-2 mr-2 animate-spin') %>
</div>
<div class="flex flex-col">
<span class="mb-1 text-lg font-semibold">CSV</span>
<p class="text-sm">Can be opened with Microsoft Excel, Google Sheets, or any text editor like Notepad.</p>
</div>
</div>
<% end %>
</div>
<% end %>
+1
View File
@@ -0,0 +1 @@
<%= render 'docuseal_logo' %>
@@ -0,0 +1,4 @@
<a href="<%= root_path %>" class="mx-auto text-2xl md:text-3xl font-bold items-center flex space-x-3">
<%= render 'shared/logo', class: 'w-9 h-9 md:w-12 md:h-12' %>
<span><%= Docuseal::PRODUCT_NAME %></span>
</a>
+1 -6
View File
@@ -2,12 +2,7 @@
<div class="space-y-6 mx-auto"> <div class="space-y-6 mx-auto">
<div class="space-y-6"> <div class="space-y-6">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<a href="/" class="flex items-center"> <%= render 'start_form/banner' %>
<div class="mr-3">
<%= render 'shared/logo', width: '50px', height: '50px' %>
</div>
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
</a>
</div> </div>
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4"> <div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
<div class="flex items-center"> <div class="flex items-center">
+1 -4
View File
@@ -5,10 +5,7 @@
<div id="scrollbox"> <div id="scrollbox">
<div class="mx-auto block pb-72" style="max-width: 1000px"> <div class="mx-auto block pb-72" style="max-width: 1000px">
<div class="mt-4 flex"> <div class="mt-4 flex">
<a href="<%= root_path %>" class="mx-auto text-2xl md:text-3xl font-bold items-center flex space-x-3"> <%= render 'banner' %>
<%= render 'shared/logo', class: 'w-9 h-9 md:w-12 md:h-12' %>
<span>DocuSeal</span>
</a>
</div> </div>
<% (@submitter.submission.template_schema || @submitter.submission.template.schema).each do |item| %> <% (@submitter.submission.template_schema || @submitter.submission.template.schema).each do |item| %>
<% document = @submitter.submission.template.documents.find { |a| a.uuid == item['attachment_uuid'] } %> <% document = @submitter.submission.template.documents.find { |a| a.uuid == item['attachment_uuid'] } %>
@@ -1,3 +1,7 @@
<p>Hi <%= @user.first_name %>,</p> <% if @email_config %>
<p><%= @submitter.email %> has completed the "<%= @submitter.submission.template.name %>" form.</p> <%= auto_link(simple_format(h(ReplaceEmailVariables.call(@email_config.value['body'], submitter: @submitter)))) %>
<p><%= link_to 'View Submission', submission_url(@submitter.submission) %></p> <% else %>
<p>Hi <%= @user.first_name %>,</p>
<p><%= @submitter.email %> has completed the "<%= @submitter.submission.template.name %>" form.</p>
<p><%= link_to 'View Submission', submission_url(@submitter.submission) %></p>
<% end %>
@@ -1,13 +1,17 @@
<p>Hi there,</p> <% if @email_config %>
<p>Please check the copy of your "<%= @submitter.submission.template.name %>" submission in the email attachments.</p> <%= auto_link(simple_format(h(ReplaceEmailVariables.call(@email_config.value['body'], submitter: @submitter)))) %>
<p>Alternatively, you can download the copy using:</p> <% else %>
<% @documents.each do |document| %> <p>Hi there,</p>
<ul> <p>Please check the copy of your "<%= @submitter.submission.template.name %>" submission in the email attachments.</p>
<li> <p>Alternatively, you can download the copy using:</p>
<%= link_to document.filename.to_s, rails_blob_url(document) %> <% @documents.each do |document| %>
</li> <ul>
</ul> <li>
<%= link_to document.filename.to_s, rails_blob_url(document) %>
</li>
</ul>
<% end %>
<p>
Thanks,<br><%= @current_account.name %>
</p>
<% end %> <% end %>
<p>
Thanks,<br><%= @submitter.submission.template.account.name %>
</p>
@@ -1,7 +1,11 @@
<p>Hi there,</p> <% if @email_config %>
<%= simple_format(@message) %> <%= auto_link(simple_format(h(ReplaceEmailVariables.call(@email_config.value['body'], submitter: @submitter)))) %>
<p><%= link_to 'Submit Form', submit_form_url(slug: @submitter.slug) %></p> <% else %>
<p>Please contact us by replying to this email if you didn't request this.</p> <p>Hi there,</p>
<p> <%= simple_format(@message) %>
Thanks,<br><%= @submitter.submission.template.account.name %> <p><%= link_to 'Submit Form', submit_form_url(slug: @submitter.slug) %></p>
</p> <p>Please contact us by replying to this email if you didn't request this.</p>
<p>
Thanks,<br><%= @current_account.name %>
</p>
<% end %>
+1 -1
View File
@@ -1 +1 @@
<template-builder data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-template="<%= @template.as_json.merge(documents: @template.schema_documents.as_json(include: { preview_images: { methods: %i[url metadata filename] } })).to_json %>"></template-builder> <template-builder class="grid" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-template="<%= @template.as_json.merge(documents: @template.schema_documents.as_json(include: { preview_images: { methods: %i[url metadata filename] } })).to_json %>"></template-builder>
+18 -12
View File
@@ -1,4 +1,4 @@
<div class="flex flex-col md:flex-row space-y-2 md:space-y-0 md:justify-between items-start mb-8"> <div class="flex flex-col items-start md:flex-row space-y-2 md:space-y-0 md:justify-between md:items-center mb-8">
<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 %>
</h1> </h1>
@@ -17,16 +17,22 @@
</div> </div>
</div> </div>
<% if !@pagy.count.zero? || @template.submitters.to_a.size == 1 %> <% if !@pagy.count.zero? || @template.submitters.to_a.size == 1 %>
<div class="flex justify-between mb-6 items-end"> <div class="flex justify-between mb-6 md:items-end flex-col md:flex-row">
<p class="text-3xl font-bold">Submissions</p> <p class="text-3xl font-bold">Submissions</p>
<div class="flex space-x-2"> <div class="flex space-x-2 mt-3 md:mt-0">
<%= link_to new_template_submissions_export_path(@template), class: 'order-3 md:order-1 btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('download', class: 'w-6 h-6 stroke-2') %>
<span>Export</span>
<% end %>
<% if @template.submitters.to_a.size == 1 %> <% if @template.submitters.to_a.size == 1 %>
<%= render 'shared/clipboard_copy', text: start_form_url(slug: @template.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Share Link', copied_title: 'Copied to Clipboard' %> <span class="order-1">
<%= render 'shared/clipboard_copy', text: start_form_url(slug: @template.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Share Link', copied_title: 'Copied to Clipboard', copy_title_md: 'Copy', copied_title_md: 'Copied' %>
</span>
<% end %> <% end %>
<% unless @pagy.count.zero? %> <% unless @pagy.count.zero? %>
<%= link_to new_template_submission_path(@template), class: 'btn btn-primary text-base', data: { turbo_frame: 'modal' } do %> <%= link_to new_template_submission_path(@template), class: 'order-1 btn btn-primary text-base', 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">Add Recipients</span> <span>Add <span class="hidden md:inline">Recipients</span></span>
<% end %> <% end %>
<% end %> <% end %>
</div> </div>
@@ -38,7 +44,7 @@
<% @submissions.each do |submission| %> <% @submissions.each do |submission| %>
<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_complated = submitters.all?(&:completed_at?) %> <% is_submission_completed = submitters.all?(&:completed_at?) %>
<% if submitters.size == 1 %> <% if submitters.size == 1 %>
<div> <div>
<% submitter = submitters.first %> <% submitter = submitters.first %>
@@ -80,7 +86,7 @@
<% else %> <% else %>
<div class="space-y-1 w-full md:mr-2"> <div class="space-y-1 w-full md:mr-2">
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<% if is_submission_complated %> <% if is_submission_completed %>
<% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %> <% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %>
<div class="tooltip flex" data-tip="<%= l(latest_submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>"> <div class="tooltip flex" data-tip="<%= l(latest_submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<span class="badge <%= status_badges[latest_submitter.status] %> md:w-32 bg-opacity-50 badge-lg uppercase text-sm font-semibold"> <span class="badge <%= status_badges[latest_submitter.status] %> md:w-32 bg-opacity-50 badge-lg uppercase text-sm font-semibold">
@@ -92,7 +98,7 @@
<% submitters.each_with_index do |submitter, index| %> <% submitters.each_with_index do |submitter, index| %>
<div class="flex justify-between items-center space-x-3"> <div class="flex justify-between items-center space-x-3">
<span class="flex items-center space-x-3"> <span class="flex items-center space-x-3">
<% unless is_submission_complated %> <% unless is_submission_completed %>
<div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>"> <div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<span class="badge md:w-24 <%= status_badges[submitter.status] %> bg-opacity-50 uppercase text-xs font-semibold"> <span class="badge md:w-24 <%= status_badges[submitter.status] %> bg-opacity-50 uppercase text-xs font-semibold">
<%= submitter.status %> <%= submitter.status %>
@@ -103,7 +109,7 @@
<%= submitter.email %> <%= submitter.email %>
</span> </span>
</span> </span>
<% if submitter.completed_at? && !is_submission_complated %> <% if submitter.completed_at? && !is_submission_completed %>
<form onsubmit="event.preventDefault()"> <form onsubmit="event.preventDefault()">
<button onclick="event.stopPropagation()"> <button onclick="event.stopPropagation()">
<download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="btn btn-xs btn-neutral text-white md:w-36"> <download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="btn btn-xs btn-neutral text-white md:w-36">
@@ -118,7 +124,7 @@
</download-button> </download-button>
</button> </button>
</form> </form>
<% elsif !is_submission_complated %> <% elsif !is_submission_completed %>
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-xs text-xs btn-neutral text-white md:w-36 flex', icon_class: 'w-4 h-4 text-white', copy_title: 'Copy Link' %> <%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-xs text-xs btn-neutral text-white md:w-36 flex', icon_class: 'w-4 h-4 text-white', copy_title: 'Copy Link' %>
</div> </div>
@@ -129,7 +135,7 @@
</div> </div>
</div> </div>
<div class="flex space-x-2 items-center"> <div class="flex space-x-2 items-center">
<% if is_submission_complated %> <% if is_submission_completed %>
<% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %> <% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %>
<form onsubmit="event.preventDefault()"> <form onsubmit="event.preventDefault()">
<button onclick="event.stopPropagation()"> <button onclick="event.stopPropagation()">
@@ -3,5 +3,5 @@
<p><%= link_to 'Sign up', invitation_url(reset_password_token: @token) %></p> <p><%= link_to 'Sign up', invitation_url(reset_password_token: @token) %></p>
<p>Please contact us by replying to this email if you didn't request this.</p> <p>Please contact us by replying to this email if you didn't request this.</p>
<p> <p>
Thanks,<br><%= @user.account.name %> Thanks,<br><%= @current_account.name %>
</p> </p>
+2
View File
@@ -29,5 +29,7 @@ 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
ActiveSupport.run_load_hooks(:application_config, self)
end end
end end
+7
View File
@@ -44,6 +44,7 @@ Rails.application.configure do
# The :test delivery method accumulates sent emails in the # The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array. # ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test config.action_mailer.delivery_method = :test
config.active_job.queue_adapter = :test
# Print deprecation notices to the stderr. # Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr config.active_support.deprecation = :stderr
@@ -59,4 +60,10 @@ Rails.application.configure do
# Annotate rendered view with file names. # Annotate rendered view with file names.
# config.action_view.annotate_rendered_view_with_filenames = true # config.action_view.annotate_rendered_view_with_filenames = true
config.active_record.encryption = {
primary_key: 'test master key',
deterministic_key: 'test deterministic key',
key_derivation_salt: 'test key derivation salt'
}
end end
+14
View File
@@ -13,6 +13,20 @@ Rails.configuration.to_prepare do
response.set_header('Cache-Control', 'public, max-age=31536000') if action_name == 'show' response.set_header('Cache-Control', 'public, max-age=31536000') if action_name == 'show'
end end
ActiveStorage::Blobs::ProxyController.before_action do
response.set_header('Access-Control-Allow-Origin', '*')
response.set_header('Access-Control-Allow-Methods', 'GET')
response.set_header('Access-Control-Allow-Headers', '*')
response.set_header('Access-Control-Max-Age', '1728000')
end
ActiveStorage::Blobs::RedirectController.before_action do
response.set_header('Access-Control-Allow-Origin', '*')
response.set_header('Access-Control-Allow-Methods', 'GET')
response.set_header('Access-Control-Allow-Headers', '*')
response.set_header('Access-Control-Max-Age', '1728000')
end
ActiveStorage::DirectUploadsController.before_action do ActiveStorage::DirectUploadsController.before_action do
next if current_user next if current_user
next if Submitter.find_signed(cookies[:submitter_sid]) next if Submitter.find_signed(cookies[:submitter_sid])
+3
View File
@@ -0,0 +1,3 @@
# frozen_string_literal: true
require 'csv'
+9 -3
View File
@@ -17,8 +17,10 @@ Rails.application.routes.draw do
devise_scope :user do devise_scope :user do
if Docuseal.multitenant? if Docuseal.multitenant?
resource :registration, only: %i[show], path: 'sign_up' resource :registration, only: %i[show], path: 'sign_up'
resource :registration, only: %i[create], path: 'new' do unauthenticated do
get '' => :new, as: :new resource :registration, only: %i[create], path: 'new' do
get '' => :new, as: :new
end
end end
end end
@@ -32,7 +34,7 @@ Rails.application.routes.draw do
resources :submissions, only: %i[create] resources :submissions, only: %i[create]
resources :templates, only: %i[update show index] do resources :templates, only: %i[update show index] do
resources :submissions, only: %i[create] resources :submissions, only: %i[create]
resources :documents, only: %i[create destroy], controller: 'templates_documents' resources :documents, only: %i[create], controller: 'templates_documents'
end end
end end
@@ -44,6 +46,7 @@ Rails.application.routes.draw do
resources :console_redirect, only: %i[index] resources :console_redirect, only: %i[index]
resources :templates, only: %i[new create edit show destroy] do resources :templates, only: %i[new create edit show destroy] do
resources :submissions, only: %i[new create] resources :submissions, only: %i[new create]
resources :submissions_export, only: %i[index new]
end end
resources :start_form, only: %i[show update], path: 'd', param: 'slug' do resources :start_form, only: %i[show update], path: 'd', param: 'slug' do
@@ -70,6 +73,7 @@ Rails.application.routes.draw do
end end
resources :esign, only: %i[index create], controller: 'esign_settings' resources :esign, only: %i[index create], controller: 'esign_settings'
resources :users, only: %i[index] resources :users, only: %i[index]
resource :personalization, only: %i[show create], controller: 'personalization_settings'
if !Docuseal.multitenant? || Docuseal.demo? if !Docuseal.multitenant? || Docuseal.demo?
resources :api, only: %i[index], controller: 'api_settings' resources :api, only: %i[index], controller: 'api_settings'
resource :webhooks, only: %i[show create update], controller: 'webhook_settings' resource :webhooks, only: %i[show create update], controller: 'webhook_settings'
@@ -83,4 +87,6 @@ Rails.application.routes.draw do
end end
end end
end end
ActiveSupport.run_load_hooks(:routes, self)
end end
-1
View File
@@ -12,7 +12,6 @@ default: &default
compiler_strategy: digest compiler_strategy: digest
cache_manifest: false cache_manifest: false
ensure_consistent_versioning: false ensure_consistent_versioning: false
useContentHash: false
development: development:
<<: *default <<: *default
+5
View File
@@ -27,3 +27,8 @@ azure:
storage_access_key: <%= ENV['AZURE_STORAGE_ACCESS_KEY'] %> storage_access_key: <%= ENV['AZURE_STORAGE_ACCESS_KEY'] %>
container: <%= ENV['AZURE_CONTAINER'] %> container: <%= ENV['AZURE_CONTAINER'] %>
public: <%= ENV['ACTIVE_STORAGE_PUBLIC'] == 'true' %> public: <%= ENV['ACTIVE_STORAGE_PUBLIC'] == 'true' %>
test:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
public: true
@@ -0,0 +1,15 @@
# frozen_string_literal: true
class CreateAccountConfigs < ActiveRecord::Migration[7.0]
def change
create_table :account_configs do |t|
t.references :account, null: false, foreign_key: true, index: true
t.string :key, null: false
t.text :value, null: false
t.index %i[account_id key], unique: true
t.timestamps
end
end
end
@@ -0,0 +1,8 @@
# frozen_string_literal: true
class RemoveUserFirstLastNameNotNull < ActiveRecord::Migration[7.0]
def change
change_column_null :users, :first_name, true
change_column_null :users, :last_name, true
end
end
@@ -0,0 +1,19 @@
# frozen_string_literal: true
class AddSourceToTemplates < ActiveRecord::Migration[7.0]
class MigrationTemplate < ApplicationRecord
self.table_name = 'templates'
end
def up
add_column :templates, :source, :text
MigrationTemplate.where(source: nil).update_all(source: :native)
change_column_null :templates, :source, false
end
def down
drop_column :templates, :source
end
end
+15 -3
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_08_06_140534) do ActiveRecord::Schema[7.0].define(version: 2023_08_19_190316) 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"
@@ -24,6 +24,16 @@ ActiveRecord::Schema[7.0].define(version: 2023_08_06_140534) do
t.index ["user_id"], name: "index_access_tokens_on_user_id" t.index ["user_id"], name: "index_access_tokens_on_user_id"
end end
create_table "account_configs", force: :cascade do |t|
t.bigint "account_id", null: false
t.string "key", null: false
t.text "value", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id", "key"], name: "index_account_configs_on_account_id_and_key", unique: true
t.index ["account_id"], name: "index_account_configs_on_account_id"
end
create_table "accounts", force: :cascade do |t| create_table "accounts", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
t.string "timezone", null: false t.string "timezone", null: false
@@ -124,14 +134,15 @@ ActiveRecord::Schema[7.0].define(version: 2023_08_06_140534) do
t.datetime "deleted_at" t.datetime "deleted_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.index ["account_id"], name: "index_templates_on_account_id" t.index ["account_id"], name: "index_templates_on_account_id"
t.index ["author_id"], name: "index_templates_on_author_id" t.index ["author_id"], name: "index_templates_on_author_id"
t.index ["slug"], name: "index_templates_on_slug", unique: true t.index ["slug"], name: "index_templates_on_slug", unique: true
end end
create_table "users", force: :cascade do |t| create_table "users", force: :cascade do |t|
t.string "first_name", null: false t.string "first_name"
t.string "last_name", null: false t.string "last_name"
t.string "email", null: false t.string "email", null: false
t.string "role", null: false t.string "role", null: false
t.string "encrypted_password", null: false t.string "encrypted_password", null: false
@@ -159,6 +170,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_08_06_140534) do
end end
add_foreign_key "access_tokens", "users" add_foreign_key "access_tokens", "users"
add_foreign_key "account_configs", "accounts"
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
add_foreign_key "document_generation_events", "submitters" add_foreign_key "document_generation_events", "submitters"
+10
View File
@@ -0,0 +1,10 @@
# frozen_string_literal: true
module AccountConfigs
module_function
def find_or_initialize_for_key(account, key)
account.account_configs.find_by(key:) ||
account.account_configs.new(key:, value: AccountConfig::DEFAULT_VALUES[key])
end
end
+2
View File
@@ -4,6 +4,8 @@ module ActionMailerConfigsInterceptor
module_function module_function
def delivering_email(message) def delivering_email(message)
return message unless Rails.env.production?
if Docuseal.demo? if Docuseal.demo?
message.delivery_method(:test) message.delivery_method(:test)
+5
View File
@@ -15,6 +15,11 @@ module Docuseal
else else
'https://console.docuseal.co' 'https://console.docuseal.co'
end end
CDN_URL = if Rails.env.development?
'http://localhost:3000'
else
'https://cdn.docuseal.co'
end
CERTS = JSON.parse(ENV.fetch('CERTS', '{}')) CERTS = JSON.parse(ENV.fetch('CERTS', '{}'))
+1
View File
@@ -18,6 +18,7 @@ module LoadActiveStorageConfigs
def reload def reload
return if Docuseal.multitenant? return if Docuseal.multitenant?
return if IS_ENV_CONFIGURED return if IS_ENV_CONFIGURED
return if Rails.env.test?
encrypted_config = EncryptedConfig.find_by(key: EncryptedConfig::FILES_STORAGE_KEY) encrypted_config = EncryptedConfig.find_by(key: EncryptedConfig::FILES_STORAGE_KEY)
+43
View File
@@ -0,0 +1,43 @@
# frozen_string_literal: true
module ReplaceEmailVariables
TEMAPLTE_NAME = '{{template.name}}'
SUBMITTER_LINK = '{{submitter.link}}'
ACCOUNT_NAME = '{{account.name}}'
SUBMITTER_EMAIL = '{{submitter.email}}'
SUBMISSION_LINK = '{{submission.link}}'
DOCUMENTS_LINKS = '{{documents.links}}'
module_function
def call(text, submitter:)
submitter_link =
Rails.application.routes.url_helpers.submit_form_url(
slug: submitter.slug, **Docuseal.default_url_options
)
submission_link =
Rails.application.routes.url_helpers.submission_url(
submitter.submission, **Docuseal.default_url_options
)
text = text.gsub(TEMAPLTE_NAME, submitter.template.name)
text = text.gsub(SUBMITTER_EMAIL, submitter.email)
text = text.gsub(SUBMITTER_LINK, submitter_link)
text = text.gsub(SUBMISSION_LINK, submission_link)
text = text.gsub(DOCUMENTS_LINKS, build_documents_links_text(submitter))
text.gsub(ACCOUNT_NAME, submitter.template.account.name)
end
def build_documents_links_text(submitter)
submitter.documents.map do |document|
link =
Rails.application.routes.url_helpers.rails_blob_url(
document, **Docuseal.default_url_options
)
"#{link}\n"
end.join
end
end
+4 -4
View File
@@ -6,16 +6,16 @@ module Submissions
def update_template_fields!(submission) def update_template_fields!(submission)
submission.template_fields = submission.template.fields submission.template_fields = submission.template.fields
submission.template_schema = submission.template.schema submission.template_schema = submission.template.schema
submission.template_submitters = submission.template.submitters submission.template_submitters = submission.template.submitters if submission.template_submitters.blank?
submission.save! submission.save!
end end
def create_from_emails(template:, user:, emails:, source:, send_email: false) def create_from_emails(template:, user:, emails:, source:, send_email: false)
emails = emails.to_s.scan(User::EMAIL_REGEXP) emails = emails.to_s.scan(User::EMAIL_REGEXP) unless emails.is_a?(Array)
emails.map do |email| emails.map do |email|
submission = template.submissions.new(created_by_user: user, source:) submission = template.submissions.new(created_by_user: user, source:, template_submitters: template.submitters)
submission.submitters.new(email:, submission.submitters.new(email:,
uuid: template.submitters.first['uuid'], uuid: template.submitters.first['uuid'],
sent_at: send_email ? Time.current : nil) sent_at: send_email ? Time.current : nil)
@@ -26,7 +26,7 @@ module Submissions
def create_from_submitters(template:, user:, submissions_attrs:, source:, send_email: false) def create_from_submitters(template:, user:, submissions_attrs:, source:, send_email: false)
submissions_attrs.map do |attrs| submissions_attrs.map do |attrs|
submission = template.submissions.new(created_by_user: user, source:) submission = template.submissions.new(created_by_user: user, source:, template_submitters: template.submitters)
attrs[:submitters].each_with_index do |submitter_attrs, index| attrs[:submitters].each_with_index do |submitter_attrs, index|
uuid = uuid =
+137
View File
@@ -0,0 +1,137 @@
# frozen_string_literal: true
module Submissions
module GenerateExportFiles
UnknownFormat = Class.new(StandardError)
module_function
def call(submissions, format: :csv)
rows = build_table_rows(submissions)
if format.to_sym == :csv
rows_to_csv(rows)
elsif format.to_sym == :xlsx
rows_to_xlsx(rows)
else
raise UnknownFormat
end
end
def rows_to_xlsx(rows)
workbook = RubyXL::Workbook.new
worksheet = workbook[0]
worksheet.sheet_name = I18n.l(Time.current.to_date)
headers = build_headers(rows)
headers.each_with_index do |column_name, column_index|
worksheet.add_cell(0, column_index, column_name)
end
rows.each.with_index(1) do |row, row_index|
extract_columns(row, headers).each_with_index do |value, column_index|
worksheet.add_cell(row_index, column_index, value)
end
end
workbook.stream.string
end
def rows_to_csv(rows)
headers = build_headers(rows)
CSV.generate do |csv|
csv << headers
rows.each do |row|
csv << extract_columns(row, headers)
end
end
end
def build_headers(rows)
rows.reduce(Set.new) { |acc, row| acc + row.pluck(:name) }
end
def extract_columns(row, headers)
headers.map { |key| row.find { |e| e[:name] == key }&.dig(:value) }
end
def build_table_rows(submissions)
submissions.map do |submission|
submission_data = []
submitters_count = submission.submitters.size
submission.submitters.each do |submitter|
template_submitters = submission.template_submitters || submission.template.submitters
submitter_name = template_submitters.find { |s| s['uuid'] == submitter.uuid }['name']
submission_data += build_submission_data(submitter, submitter_name, submitters_count)
submission_data += submitter_formatted_fields(submitter).map do |field|
{
name: column_name(field[:name], submitter_name, submitters_count),
value: field[:value]
}
end
next if submitter != submission.submitters.select(&:completed_at?).max_by(&:completed_at)
submission_data += submitter.documents.map.with_index(1) do |attachment, index|
{
name: "Document #{index}",
value: attachment.url
}
end
end
submission_data
end
end
def build_submission_data(submitter, submitter_name, submitters_count)
[
{
name: column_name('Email', submitter_name, submitters_count),
value: submitter.email
},
{
name: column_name('Completed At', submitter_name, submitters_count),
value: submitter.completed_at
}
]
end
def column_name(name, submitter_name, submitters_count = 1)
submitters_count > 1 ? "#{submitter_name} - #{name}" : name
end
def submitter_formatted_fields(submitter)
fields = submitter.submission.template_fields || submitter.submission.template.fields
template_fields = fields.select { |f| f['submitter_uuid'] == submitter.uuid }
attachments_index = submitter.attachments.index_by(&:uuid)
template_field_counters = Hash.new { 0 }
template_fields.map do |template_field|
submitter_value = submitter.values.fetch(template_field['uuid'], nil)
template_field_type = template_field['type']
template_field_counters[template_field_type] += 1
template_field_name = template_field['name'].presence
template_field_name ||= "#{template_field_type.titleize} Field #{template_field_counters[template_field_type]}"
value =
if template_field_type.in?(%w[image signature])
attachments_index[submitter_value]&.url
elsif template_field_type == 'file'
Array.wrap(submitter_value).compact_blank.filter_map { |e| attachments_index[e]&.url }
else
submitter_value
end
{ name: template_field_name, uuid: template_field['uuid'], value: }
end
end
end
end
+11 -3
View File
@@ -3,7 +3,12 @@
module Submissions module Submissions
module GenerateResultAttachments module GenerateResultAttachments
FONT_SIZE = 11 FONT_SIZE = 11
FONT_NAME = 'Helvetica' FONT_PATH = '/LiberationSans-Regular.ttf'
FONT_NAME = if File.exist?(FONT_PATH)
FONT_PATH
else
'Helvetica'
end
INFO_CREATOR = "#{Docuseal::PRODUCT_NAME} (#{Docuseal::PRODUCT_URL})".freeze INFO_CREATOR = "#{Docuseal::PRODUCT_NAME} (#{Docuseal::PRODUCT_URL})".freeze
SIGN_REASON = 'Signed by %<email>s with DocuSeal.co' SIGN_REASON = 'Signed by %<email>s with DocuSeal.co'
@@ -32,9 +37,13 @@ module Submissions
field.fetch('areas', []).each do |area| field.fetch('areas', []).each do |area|
pdf = pdfs_index[area['attachment_uuid']] pdf = pdfs_index[area['attachment_uuid']]
pdf.fonts.add(FONT_NAME)
page = pdf.pages[area['page']] page = pdf.pages[area['page']]
page[:Annots] ||= []
page[:Annots] = page[:Annots].reject { |e| e[:A] && e[:A][:URI].to_s.starts_with?('file:///docuseal_field') }
width = page.box.width width = page.box.width
height = page.box.height height = page.box.height
font_size = ((page.box.width / A4_SIZE[0].to_f) * FONT_SIZE).to_i font_size = ((page.box.width / A4_SIZE[0].to_f) * FONT_SIZE).to_i
@@ -44,6 +53,7 @@ module Submissions
next if Array.wrap(value).compact_blank.blank? next if Array.wrap(value).compact_blank.blank?
canvas = page.canvas(type: :overlay) canvas = page.canvas(type: :overlay)
canvas.font(FONT_NAME, size: font_size)
case field['type'] case field['type']
when 'image', 'signature' when 'image', 'signature'
@@ -66,8 +76,6 @@ module Submissions
height: image.height * scale height: image.height * scale
) )
when 'file' when 'file'
page[:Annots] ||= []
items = Array.wrap(value).each_with_object([]) do |uuid, acc| items = Array.wrap(value).each_with_object([]) do |uuid, acc|
attachment = submitter.attachments.find { |a| a.uuid == uuid } attachment = submitter.attachments.find { |a| a.uuid == uuid }
+17
View File
@@ -11,4 +11,21 @@ module Submitters
is_more_than_two_images && original_documents.find { |a| a.uuid == attachment.uuid }&.image? is_more_than_two_images && original_documents.find { |a| a.uuid == attachment.uuid }&.image?
end end
end end
def create_attachment!(submitter, params)
blob =
if (file = params[:file])
ActiveStorage::Blob.create_and_upload!(io: file.open,
filename: file.original_filename,
content_type: file.content_type)
else
ActiveStorage::Blob.find_signed(params[:blob_signed_id])
end
ActiveStorage::Attachment.create!(
blob:,
name: params[:name],
record: submitter
)
end
end end
+54
View File
@@ -0,0 +1,54 @@
# frozen_string_literal: true
module Submitters
module SubmitValues
module_function
def call(submitter, params, request)
update_submitter!(submitter, params, request)
Submissions.update_template_fields!(submitter.submission) if submitter.submission.template_fields.blank?
submitter.submission.save!
return unless submitter.completed_at?
GenerateSubmitterResultAttachmentsJob.perform_later(submitter)
if submitter.account.encrypted_configs.exists?(key: EncryptedConfig::WEBHOOK_URL_KEY)
SendWebhookRequestJob.perform_later(submitter)
end
submitter.submission.template.account.users.active.admins.each do |user|
SubmitterMailer.completed_email(submitter, user).deliver_later!
end
submitter
end
def update_submitter!(submitter, params, request)
submitter.values.merge!(normalized_values(params))
submitter.opened_at ||= Time.current
if params[:completed] == 'true'
submitter.completed_at = Time.current
submitter.ip = request.remote_ip
submitter.ua = request.user_agent
end
submitter.save!
submitter
end
def normalized_values(params)
params.fetch(:values, {}).to_unsafe_h.transform_values do |v|
if params[:cast_boolean] == 'true'
v == 'true'
else
v.is_a?(Array) ? v.compact_blank : v
end
end
end
end
end
+1 -1
View File
@@ -26,7 +26,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.0", "shakapacker": "7.0.3",
"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",
+9
View File
@@ -0,0 +1,9 @@
# frozen_string_literal: true
FactoryBot.define do
factory :account do
name { Faker::Company.name }
locale { 'en-US' }
timezone { 'UTC' }
end
end
Loaded 100 of 122 files, more files were not shown because too many files have changed in this diff. Show more