mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 14:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88968897e9 | ||
|
|
966b786a30 | ||
|
|
bc3b1b477e | ||
|
|
cbc6f4497a | ||
|
|
6b4e7c2264 | ||
|
|
8e6639d891 | ||
|
|
8e39f0078d | ||
|
|
46f350217a | ||
|
|
077c55f7e8 | ||
|
|
e5c5620b20 | ||
|
|
187b354605 | ||
|
|
545b18086a | ||
|
|
130c91178f | ||
|
|
47c57158f4 | ||
|
|
3b745a650d | ||
|
|
6871a3f840 | ||
|
|
c57148eada | ||
|
|
d875477ee6 | ||
|
|
1daee8214c | ||
|
|
9cc85595e3 | ||
|
|
f6879d7f94 | ||
|
|
f6247e68b5 | ||
|
|
454acb63db | ||
|
|
6d78d5430c | ||
|
|
e75e7b18b4 | ||
|
|
644b142798 | ||
|
|
e4bb5466f5 | ||
|
|
8929adbe83 | ||
|
|
896b149b25 | ||
|
|
7697b94f73 | ||
|
|
7912c9a1ee | ||
|
|
63e1246a11 | ||
|
|
cbca34a7d7 | ||
|
|
9471a510c9 | ||
|
|
e754c5d414 |
No files matched your search
@@ -1,5 +1,6 @@
|
||||
/db/*.sqlite3
|
||||
|
||||
**/node_modules
|
||||
/node_modules
|
||||
/coverage
|
||||
/doc
|
||||
@@ -27,3 +28,4 @@
|
||||
/attachments
|
||||
/docuseal
|
||||
.DS_Store
|
||||
.env
|
||||
@@ -127,6 +127,7 @@ jobs:
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 4
|
||||
yarn install
|
||||
sudo apt-get update
|
||||
sudo apt-get install libvips
|
||||
- name: Run
|
||||
env:
|
||||
|
||||
@@ -35,3 +35,4 @@ yarn-debug.log*
|
||||
/coverage
|
||||
/attachments
|
||||
/docuseal
|
||||
/ee
|
||||
+6
-3
@@ -27,7 +27,7 @@ Lint/MissingSuper:
|
||||
Enabled: false
|
||||
|
||||
Metrics/MethodLength:
|
||||
Max: 20
|
||||
Max: 25
|
||||
Exclude:
|
||||
- 'db/migrate/**'
|
||||
|
||||
@@ -53,10 +53,13 @@ RSpec/NestedGroups:
|
||||
Max: 6
|
||||
|
||||
RSpec/MultipleExpectations:
|
||||
Max: 7
|
||||
Max: 20
|
||||
|
||||
RSpec/ExampleLength:
|
||||
Max: 15
|
||||
Max: 40
|
||||
|
||||
RSpec/MultipleMemoizedHelpers:
|
||||
Max: 6
|
||||
|
||||
Rails/I18nLocaleTexts:
|
||||
Enabled: false
|
||||
|
||||
+9
-1
@@ -37,7 +37,15 @@ COPY ./Gemfile ./Gemfile.lock ./
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ gem 'faraday'
|
||||
gem 'google-cloud-storage', require: false
|
||||
gem 'hexapdf'
|
||||
gem 'image_processing'
|
||||
gem 'jwt'
|
||||
gem 'lograge'
|
||||
gem 'mysql2', require: false
|
||||
gem 'oj'
|
||||
@@ -23,9 +24,11 @@ gem 'pg', require: false
|
||||
gem 'premailer-rails'
|
||||
gem 'puma'
|
||||
gem 'rails'
|
||||
gem 'rails_autolink'
|
||||
gem 'rails-i18n'
|
||||
gem 'rollbar', require: ENV.key?('ROLLBAR_ACCESS_TOKEN')
|
||||
gem 'ruby-vips'
|
||||
gem 'rubyXL'
|
||||
gem 'shakapacker'
|
||||
gem 'sidekiq', require: ENV.key?('REDIS_URL')
|
||||
gem 'sqlite3', require: false
|
||||
|
||||
+121
-102
@@ -1,92 +1,92 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (7.0.5)
|
||||
actionpack (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
actioncable (7.0.7)
|
||||
actionpack (= 7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (7.0.5)
|
||||
actionpack (= 7.0.5)
|
||||
activejob (= 7.0.5)
|
||||
activerecord (= 7.0.5)
|
||||
activestorage (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
actionmailbox (7.0.7)
|
||||
actionpack (= 7.0.7)
|
||||
activejob (= 7.0.7)
|
||||
activerecord (= 7.0.7)
|
||||
activestorage (= 7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
mail (>= 2.7.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
actionmailer (7.0.5)
|
||||
actionpack (= 7.0.5)
|
||||
actionview (= 7.0.5)
|
||||
activejob (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
actionmailer (7.0.7)
|
||||
actionpack (= 7.0.7)
|
||||
actionview (= 7.0.7)
|
||||
activejob (= 7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (7.0.5)
|
||||
actionview (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
actionpack (7.0.7)
|
||||
actionview (= 7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
rack (~> 2.0, >= 2.2.4)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (7.0.5)
|
||||
actionpack (= 7.0.5)
|
||||
activerecord (= 7.0.5)
|
||||
activestorage (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
actiontext (7.0.7)
|
||||
actionpack (= 7.0.7)
|
||||
activerecord (= 7.0.7)
|
||||
activestorage (= 7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
actionview (7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
activejob (7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
activejob (7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
activerecord (7.0.5)
|
||||
activemodel (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
activestorage (7.0.5)
|
||||
actionpack (= 7.0.5)
|
||||
activejob (= 7.0.5)
|
||||
activerecord (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
activemodel (7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
activerecord (7.0.7)
|
||||
activemodel (= 7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
activestorage (7.0.7)
|
||||
actionpack (= 7.0.7)
|
||||
activejob (= 7.0.7)
|
||||
activerecord (= 7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
marcel (~> 1.0)
|
||||
mini_mime (>= 1.1.0)
|
||||
activesupport (7.0.5)
|
||||
activesupport (7.0.7)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
tzinfo (~> 2.0)
|
||||
addressable (2.8.4)
|
||||
addressable (2.8.5)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
annotate (3.2.0)
|
||||
activerecord (>= 3.2, < 8.0)
|
||||
rake (>= 10.4, < 14.0)
|
||||
ast (2.4.2)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.781.0)
|
||||
aws-sdk-core (3.175.0)
|
||||
aws-partitions (1.807.0)
|
||||
aws-sdk-core (3.180.3)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.67.0)
|
||||
aws-sdk-core (~> 3, >= 3.174.0)
|
||||
aws-sdk-kms (1.71.0)
|
||||
aws-sdk-core (~> 3, >= 3.177.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.126.0)
|
||||
aws-sdk-core (~> 3, >= 3.174.0)
|
||||
aws-sdk-s3 (1.132.1)
|
||||
aws-sdk-core (~> 3, >= 3.179.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.4)
|
||||
aws-sigv4 (1.5.2)
|
||||
aws-sigv4 (~> 1.6)
|
||||
aws-sigv4 (1.6.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
azure-storage-blob (2.0.3)
|
||||
azure-storage-common (~> 2.0)
|
||||
@@ -96,8 +96,9 @@ GEM
|
||||
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
|
||||
net-http-persistent (~> 4.0)
|
||||
nokogiri (~> 1, >= 1.10.8)
|
||||
base64 (0.1.1)
|
||||
bcrypt (3.1.19)
|
||||
better_html (2.0.1)
|
||||
better_html (2.0.2)
|
||||
actionview (>= 6.0)
|
||||
activesupport (>= 6.0)
|
||||
ast (~> 2.0)
|
||||
@@ -105,7 +106,7 @@ GEM
|
||||
parser (>= 2.4)
|
||||
smart_properties
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.15.0)
|
||||
bootsnap (1.16.0)
|
||||
msgpack (~> 1.2)
|
||||
builder (3.2.4)
|
||||
bullet (7.0.7)
|
||||
@@ -144,7 +145,7 @@ GEM
|
||||
responders
|
||||
warden (~> 1.2.3)
|
||||
diff-lcs (1.5.0)
|
||||
digest-crc (0.6.4)
|
||||
digest-crc (0.6.5)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
docile (1.4.0)
|
||||
dotenv (2.8.1)
|
||||
@@ -161,7 +162,7 @@ GEM
|
||||
factory_bot_rails (6.2.0)
|
||||
factory_bot (~> 6.2.0)
|
||||
railties (>= 5.0.0)
|
||||
faker (3.2.0)
|
||||
faker (3.2.1)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (1.10.3)
|
||||
faraday-em_http (~> 1.0)
|
||||
@@ -194,10 +195,10 @@ GEM
|
||||
webrick (~> 1.7)
|
||||
websocket-driver (>= 0.6, < 0.8)
|
||||
ffi (1.15.5)
|
||||
geom2d (0.3.1)
|
||||
geom2d (0.4.1)
|
||||
globalid (1.1.0)
|
||||
activesupport (>= 5.0)
|
||||
google-apis-core (0.11.0)
|
||||
google-apis-core (0.11.1)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
@@ -224,7 +225,7 @@ GEM
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (1.6.0)
|
||||
googleauth (1.7.0)
|
||||
faraday (>= 0.17.3, < 3.a)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
@@ -233,9 +234,9 @@ GEM
|
||||
signet (>= 0.16, < 2.a)
|
||||
hashdiff (1.0.1)
|
||||
hashie (5.0.0)
|
||||
hexapdf (0.32.2)
|
||||
hexapdf (0.33.0)
|
||||
cmdparse (~> 3.0, >= 3.0.3)
|
||||
geom2d (~> 0.3)
|
||||
geom2d (~> 0.4, >= 0.4.1)
|
||||
openssl (>= 2.2.1)
|
||||
htmlentities (4.3.4)
|
||||
httpclient (2.8.3)
|
||||
@@ -245,8 +246,8 @@ GEM
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
io-console (0.6.0)
|
||||
irb (1.7.0)
|
||||
reline (>= 0.3.0)
|
||||
irb (1.7.4)
|
||||
reline (>= 0.3.6)
|
||||
jmespath (1.6.2)
|
||||
json (2.6.3)
|
||||
jwt (2.7.1)
|
||||
@@ -260,7 +261,7 @@ GEM
|
||||
letter_opener (~> 1.7)
|
||||
railties (>= 5.2)
|
||||
rexml
|
||||
lograge (0.12.0)
|
||||
lograge (0.13.0)
|
||||
actionpack (>= 4)
|
||||
activesupport (>= 4)
|
||||
railties (>= 4)
|
||||
@@ -278,17 +279,17 @@ GEM
|
||||
memoist (0.16.2)
|
||||
method_source (1.0.0)
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.2)
|
||||
mini_portile2 (2.8.2)
|
||||
minitest (5.18.1)
|
||||
msgpack (1.7.1)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.4)
|
||||
minitest (5.19.0)
|
||||
msgpack (1.7.2)
|
||||
multi_json (1.15.0)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.3.0)
|
||||
mysql2 (0.5.5)
|
||||
net-http-persistent (4.0.2)
|
||||
connection_pool (~> 2.2)
|
||||
net-imap (0.3.6)
|
||||
net-imap (0.3.7)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
@@ -298,7 +299,10 @@ GEM
|
||||
net-smtp (0.3.3)
|
||||
net-protocol
|
||||
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)
|
||||
oauth2 (2.0.9)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
@@ -307,7 +311,7 @@ GEM
|
||||
rack (>= 1.2, < 4)
|
||||
snaky_hash (~> 2.0)
|
||||
version_gem (~> 1.1)
|
||||
oj (3.15.0)
|
||||
oj (3.16.0)
|
||||
omniauth (2.1.1)
|
||||
hashie (>= 3.4.6)
|
||||
rack (>= 2.2.3)
|
||||
@@ -345,53 +349,58 @@ GEM
|
||||
method_source (~> 1.0)
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (5.0.1)
|
||||
puma (6.3.0)
|
||||
public_suffix (5.0.3)
|
||||
puma (6.3.1)
|
||||
nio4r (~> 2.0)
|
||||
racc (1.7.1)
|
||||
rack (2.2.7)
|
||||
rack-protection (3.0.6)
|
||||
rack
|
||||
rack (2.2.8)
|
||||
rack-protection (3.1.0)
|
||||
rack (~> 2.2, >= 2.2.4)
|
||||
rack-proxy (0.7.6)
|
||||
rack
|
||||
rack-test (2.1.0)
|
||||
rack (>= 1.3)
|
||||
rails (7.0.5)
|
||||
actioncable (= 7.0.5)
|
||||
actionmailbox (= 7.0.5)
|
||||
actionmailer (= 7.0.5)
|
||||
actionpack (= 7.0.5)
|
||||
actiontext (= 7.0.5)
|
||||
actionview (= 7.0.5)
|
||||
activejob (= 7.0.5)
|
||||
activemodel (= 7.0.5)
|
||||
activerecord (= 7.0.5)
|
||||
activestorage (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
rails (7.0.7)
|
||||
actioncable (= 7.0.7)
|
||||
actionmailbox (= 7.0.7)
|
||||
actionmailer (= 7.0.7)
|
||||
actionpack (= 7.0.7)
|
||||
actiontext (= 7.0.7)
|
||||
actionview (= 7.0.7)
|
||||
activejob (= 7.0.7)
|
||||
activemodel (= 7.0.7)
|
||||
activerecord (= 7.0.7)
|
||||
activestorage (= 7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 7.0.5)
|
||||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
railties (= 7.0.7)
|
||||
rails-dom-testing (2.2.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.6.0)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (~> 1.14)
|
||||
rails-i18n (7.0.7)
|
||||
rails-i18n (7.0.8)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 6.0.0, < 8)
|
||||
railties (7.0.5)
|
||||
actionpack (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
rails_autolink (1.1.8)
|
||||
actionview (> 3.1)
|
||||
activesupport (> 3.1)
|
||||
railties (> 3.1)
|
||||
railties (7.0.7)
|
||||
actionpack (= 7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
method_source
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0)
|
||||
zeitwerk (~> 2.5)
|
||||
rainbow (3.1.1)
|
||||
rake (13.0.6)
|
||||
redis-client (0.14.1)
|
||||
redis-client (0.16.0)
|
||||
connection_pool
|
||||
regexp_parser (2.8.1)
|
||||
reline (0.3.5)
|
||||
reline (0.3.8)
|
||||
io-console (~> 0.5)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
@@ -403,14 +412,14 @@ GEM
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
retriable (3.1.2)
|
||||
rexml (3.2.5)
|
||||
rexml (3.2.6)
|
||||
rollbar (3.4.0)
|
||||
rspec-core (3.12.2)
|
||||
rspec-support (~> 3.12.0)
|
||||
rspec-expectations (3.12.3)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.12.0)
|
||||
rspec-mocks (3.12.5)
|
||||
rspec-mocks (3.12.6)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.12.0)
|
||||
rspec-rails (6.0.3)
|
||||
@@ -421,8 +430,9 @@ GEM
|
||||
rspec-expectations (~> 3.12)
|
||||
rspec-mocks (~> 3.12)
|
||||
rspec-support (~> 3.12)
|
||||
rspec-support (3.12.0)
|
||||
rubocop (1.53.0)
|
||||
rspec-support (3.12.1)
|
||||
rubocop (1.56.1)
|
||||
base64 (~> 0.1.1)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
parallel (~> 1.10)
|
||||
@@ -430,7 +440,7 @@ GEM
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.8, < 3.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)
|
||||
unicode-display_width (>= 2.4.0, < 3.0)
|
||||
rubocop-ast (1.29.0)
|
||||
@@ -439,14 +449,14 @@ GEM
|
||||
rubocop (~> 1.41)
|
||||
rubocop-factory_bot (2.23.1)
|
||||
rubocop (~> 1.33)
|
||||
rubocop-performance (1.18.0)
|
||||
rubocop-performance (1.19.0)
|
||||
rubocop (>= 1.7.0, < 2.0)
|
||||
rubocop-ast (>= 0.4.0)
|
||||
rubocop-rails (2.20.2)
|
||||
activesupport (>= 4.2.0)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.33.0, < 2.0)
|
||||
rubocop-rspec (2.22.0)
|
||||
rubocop-rspec (2.23.2)
|
||||
rubocop (~> 1.33)
|
||||
rubocop-capybara (~> 2.17)
|
||||
rubocop-factory_bot (~> 2.22)
|
||||
@@ -454,8 +464,12 @@ GEM
|
||||
ruby-vips (2.1.4)
|
||||
ffi (~> 1.12)
|
||||
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)
|
||||
shakapacker (7.0.0)
|
||||
shakapacker (7.0.3)
|
||||
activesupport (>= 5.2)
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 5.2)
|
||||
@@ -480,8 +494,9 @@ GEM
|
||||
snaky_hash (2.0.1)
|
||||
hashie
|
||||
version_gem (~> 1.1, >= 1.1.1)
|
||||
sqlite3 (1.5.4)
|
||||
sqlite3 (1.6.3)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
sqlite3 (1.6.3-arm64-darwin)
|
||||
strip_attributes (1.13.0)
|
||||
activemodel (>= 3.0, < 8.0)
|
||||
thor (1.2.2)
|
||||
@@ -511,15 +526,16 @@ GEM
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webrick (1.8.1)
|
||||
websocket-driver (0.7.5)
|
||||
websocket-driver (0.7.6)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
zeitwerk (2.6.8)
|
||||
zeitwerk (2.6.11)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-22
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
annotate
|
||||
@@ -540,6 +556,7 @@ DEPENDENCIES
|
||||
google-cloud-storage
|
||||
hexapdf
|
||||
image_processing
|
||||
jwt
|
||||
letter_opener_web
|
||||
lograge
|
||||
mysql2
|
||||
@@ -553,6 +570,7 @@ DEPENDENCIES
|
||||
puma
|
||||
rails
|
||||
rails-i18n
|
||||
rails_autolink
|
||||
rollbar
|
||||
rspec-rails
|
||||
rubocop
|
||||
@@ -560,6 +578,7 @@ DEPENDENCIES
|
||||
rubocop-rails
|
||||
rubocop-rspec
|
||||
ruby-vips
|
||||
rubyXL
|
||||
shakapacker
|
||||
sidekiq
|
||||
simplecov
|
||||
|
||||
@@ -42,6 +42,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
||||
- [x] PDF signature verification
|
||||
- [x] Users management
|
||||
- [x] Mobile-optimized
|
||||
- [x] API and Webhooks for integrations
|
||||
- [x] Easy to deploy in minutes
|
||||
|
||||
## Deploy
|
||||
@@ -76,10 +77,11 @@ HOST=your-domain-name.com docker-compose up
|
||||
## For Companies
|
||||
### Integrate seamless document signing into your web or mobile apps with DocuSeal!
|
||||
|
||||
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
|
||||
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
|
||||
|
||||
[](https://cal.com/docuseal)
|
||||
|
||||
## License
|
||||
|
||||
DocuSeal is released under the GNU Affero General Public License v3.0.
|
||||
Distributed under the AGPLv3 License. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) for more information.
|
||||
Unless otherwise noted, all files © 2023 Oleksandr Turchyn.
|
||||
@@ -7,20 +7,7 @@ module Api
|
||||
def create
|
||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||
|
||||
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
|
||||
|
||||
attachment = ActiveStorage::Attachment.create!(
|
||||
blob:,
|
||||
name: params[:name],
|
||||
record: submitter
|
||||
)
|
||||
attachment = Submitters.create_attachment!(submitter, params)
|
||||
|
||||
render json: attachment.as_json(only: %i[uuid], methods: %i[url filename content_type])
|
||||
end
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
class SubmissionsController < ApiBaseController
|
||||
UnknownFieldName = Class.new(StandardError)
|
||||
UnknownSubmitterName = Class.new(StandardError)
|
||||
|
||||
def create
|
||||
template = current_account.templates.find(params[:template_id])
|
||||
|
||||
submissions =
|
||||
if params[:emails].present?
|
||||
Submissions.create_from_emails(template:,
|
||||
user: current_user,
|
||||
source: :api,
|
||||
send_email: params[:send_email] != 'false',
|
||||
emails: params[:emails] || params[:email])
|
||||
else
|
||||
submissions_attrs = normalize_submissions_params!(submissions_params[:submission], template)
|
||||
|
||||
Submissions.create_from_submitters(template:,
|
||||
user: current_user,
|
||||
source: :api,
|
||||
send_email: params[:send_email] != 'false',
|
||||
submissions_attrs:)
|
||||
end
|
||||
|
||||
submitters = submissions.flat_map(&:submitters)
|
||||
|
||||
send_invitation_emails(submitters) if params[:send_email] != 'false'
|
||||
|
||||
render json: submitters
|
||||
rescue UnknownFieldName, UnknownSubmitterName => e
|
||||
render json: { error: e.message }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def send_invitation_emails(submitters)
|
||||
submitters.each do |submitter|
|
||||
SubmitterMailer.invitation_email(submitter, message: params[:message]).deliver_later!
|
||||
end
|
||||
end
|
||||
|
||||
def submissions_params
|
||||
params.permit(submission: [{ submitters: [[:uuid, :name, :email, { values: {} }]] }])
|
||||
end
|
||||
|
||||
def normalize_submissions_params!(submissions_params, template)
|
||||
submissions_params.each do |submission|
|
||||
submission[:submitters].each_with_index do |submitter, index|
|
||||
next if submitter[:values].blank?
|
||||
|
||||
submitter[:values] =
|
||||
normalize_submitter_values(template,
|
||||
submitter[:values], submitter[:name] || template.submitters[index]['name'])
|
||||
end
|
||||
end
|
||||
|
||||
submissions_params
|
||||
end
|
||||
|
||||
def normalize_submitter_values(template, values, submitter_name)
|
||||
submitter =
|
||||
template.submitters.find { |e| e['name'] == submitter_name } ||
|
||||
raise(UnknownSubmitterName, "Unknown submitter: #{submitter_name}")
|
||||
|
||||
fields = template.fields.select { |e| e['submitter_uuid'] == submitter['uuid'] }
|
||||
|
||||
fields_uuid_index = fields.index_by { |e| e['uuid'] }
|
||||
fields_name_index = fields.index_by { |e| e['name'] }
|
||||
|
||||
values.transform_keys do |key|
|
||||
next key if fields_uuid_index[key].present?
|
||||
|
||||
fields_name_index[key]&.dig('uuid') || raise(UnknownFieldName, "Unknown field: #{key}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2,9 +2,18 @@
|
||||
|
||||
module Api
|
||||
class TemplatesController < ApiBaseController
|
||||
def update
|
||||
@template = current_account.templates.find(params[:id])
|
||||
before_action :load_template, only: %i[show update]
|
||||
|
||||
def index
|
||||
render json: current_account.templates
|
||||
end
|
||||
|
||||
def show
|
||||
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
|
||||
|
||||
def update
|
||||
@template.update!(template_params)
|
||||
|
||||
render :ok
|
||||
@@ -19,5 +28,9 @@ module Api
|
||||
fields: [[:uuid, :submitter_uuid, :name, :type, :required,
|
||||
{ options: [], areas: [%i[x y w h cell_w attachment_uuid page]] }]])
|
||||
end
|
||||
|
||||
def load_template
|
||||
@template = current_account.templates.find(params[:id])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ApiSettingsController < ApplicationController
|
||||
def index; end
|
||||
end
|
||||
@@ -34,8 +34,8 @@ class ApplicationController < ActionController::Base
|
||||
redirect_to setup_index_path unless User.exists?
|
||||
end
|
||||
|
||||
def button_title(title: 'Submit', disabled_with: 'Submitting', icon: nil)
|
||||
render_to_string(partial: 'shared/button_title', locals: { title:, disabled_with:, icon: })
|
||||
def button_title(title: 'Submit', disabled_with: 'Submitting', icon: nil, icon_disabled: nil)
|
||||
render_to_string(partial: 'shared/button_title', locals: { title:, disabled_with:, icon:, icon_disabled: })
|
||||
end
|
||||
|
||||
def svg_icon(icon_name, class: '')
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ConsoleRedirectController < ApplicationController
|
||||
def index
|
||||
auth = JsonWebToken.encode(uuid: current_user.uuid,
|
||||
scope: :console,
|
||||
exp: 1.minute.from_now.to_i)
|
||||
|
||||
redirect_to("#{Docuseal::CONSOLE_URL}?#{{ auth: }.to_query}", allow_other_host: true)
|
||||
end
|
||||
end
|
||||
@@ -7,10 +7,16 @@ class EmailSettingsController < ApplicationController
|
||||
|
||||
def create
|
||||
if @encrypted_config.update(storage_configs)
|
||||
SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email']).deliver_now!
|
||||
|
||||
redirect_to settings_email_index_path, notice: 'Changes have been saved'
|
||||
else
|
||||
render :index, status: :unprocessable_entity
|
||||
end
|
||||
rescue StandardError => e
|
||||
flash[:alert] = e.message
|
||||
|
||||
render :index, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -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
|
||||
@@ -16,16 +16,17 @@ class SetupController < ApplicationController
|
||||
def create
|
||||
@account = Account.new(account_params)
|
||||
@account.timezone = Accounts.normalize_timezone(@account.timezone)
|
||||
|
||||
@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])
|
||||
@encrypted_config = EncryptedConfig.new(encrypted_config_params)
|
||||
@encrypted_config.errors.add(:value, 'should be a valid URL')
|
||||
|
||||
return render :index, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
return render :index, status: :unprocessable_entity unless @account.valid?
|
||||
|
||||
if @user.save
|
||||
encrypted_configs = [
|
||||
{ key: EncryptedConfig::APP_URL_KEY, value: encrypted_config_params[:value] },
|
||||
|
||||
@@ -25,7 +25,7 @@ class StartFormController < ApplicationController
|
||||
ua: request.user_agent
|
||||
)
|
||||
|
||||
@submitter.submission ||= Submission.new(template: @template)
|
||||
@submitter.submission ||= Submission.new(template: @template, source: :link)
|
||||
|
||||
if @submitter.save
|
||||
redirect_to submit_form_path(@submitter.slug)
|
||||
@@ -36,7 +36,9 @@ class StartFormController < ApplicationController
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
private
|
||||
|
||||
@@ -17,9 +17,17 @@ class SubmissionsController < ApplicationController
|
||||
def create
|
||||
submissions =
|
||||
if params[:emails].present?
|
||||
create_submissions_from_emails
|
||||
Submissions.create_from_emails(template: @template,
|
||||
user: current_user,
|
||||
source: :invite,
|
||||
send_email: params[:send_email] == '1',
|
||||
emails: params[:emails])
|
||||
else
|
||||
create_submissions_from_submitters
|
||||
Submissions.create_from_submitters(template: @template,
|
||||
user: current_user,
|
||||
source: :invite,
|
||||
send_email: params[:send_email] == '1',
|
||||
submissions_attrs: submissions_params[:submission].to_h.values)
|
||||
end
|
||||
|
||||
submitters = submissions.flat_map(&:submitters)
|
||||
@@ -45,30 +53,6 @@ class SubmissionsController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def create_submissions_from_emails
|
||||
emails = params[:emails].to_s.scan(User::EMAIL_REGEXP)
|
||||
|
||||
emails.map do |email|
|
||||
submission = @template.submissions.new(created_by_user: current_user)
|
||||
submission.submitters.new(email:, uuid: @template.submitters.first['uuid'],
|
||||
sent_at: params[:send_email] == '1' ? Time.current : nil)
|
||||
|
||||
submission.tap(&:save!)
|
||||
end
|
||||
end
|
||||
|
||||
def create_submissions_from_submitters
|
||||
submissions_params[:submission].to_h.map do |_, attrs|
|
||||
submission = @template.submissions.new(created_by_user: current_user)
|
||||
|
||||
attrs[:submitters].each do |submitter_attrs|
|
||||
submission.submitters.new(**submitter_attrs, sent_at: params[:send_email] == '1' ? Time.current : nil)
|
||||
end
|
||||
|
||||
submission.tap(&:save!)
|
||||
end
|
||||
end
|
||||
|
||||
def submissions_params
|
||||
params.permit(submission: { submitters: [%i[uuid email]] })
|
||||
end
|
||||
|
||||
@@ -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
|
||||
@@ -17,23 +17,8 @@ class SubmitFormController < ApplicationController
|
||||
|
||||
def update
|
||||
submitter = Submitter.find_by!(slug: params[:slug])
|
||||
submitter.values.merge!(normalized_values)
|
||||
submitter.completed_at = Time.current if params[:completed] == 'true'
|
||||
submitter.opened_at ||= Time.current
|
||||
|
||||
submitter.save!
|
||||
|
||||
Submissions.update_template_fields!(submitter.submission) if submitter.submission.template_fields.blank?
|
||||
|
||||
submitter.submission.save!
|
||||
|
||||
if submitter.completed_at?
|
||||
GenerateSubmitterResultAttachmentsJob.perform_later(submitter)
|
||||
|
||||
submitter.submission.template.account.users.active.each do |user|
|
||||
SubmitterMailer.completed_email(submitter, user).deliver_later!
|
||||
end
|
||||
end
|
||||
Submitters::SubmitValues.call(submitter, params, request)
|
||||
|
||||
head :ok
|
||||
end
|
||||
@@ -41,16 +26,4 @@ class SubmitFormController < ApplicationController
|
||||
def completed
|
||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
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
|
||||
@@ -17,7 +17,7 @@ class TemplatesController < ApplicationController
|
||||
end
|
||||
|
||||
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])
|
||||
|
||||
render :edit, layout: 'plain'
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class WebhookSettingsController < ApplicationController
|
||||
def show
|
||||
@encrypted_config =
|
||||
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
||||
end
|
||||
|
||||
def create
|
||||
@encrypted_config =
|
||||
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
||||
|
||||
@encrypted_config.update!(encrypted_config_params)
|
||||
|
||||
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook URL has been saved.')
|
||||
end
|
||||
|
||||
def update
|
||||
submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last
|
||||
|
||||
SendWebhookRequestJob.perform_later(submitter)
|
||||
|
||||
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook request has been sent.')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def encrypted_config_params
|
||||
params.require(:encrypted_config).permit(:value)
|
||||
end
|
||||
end
|
||||
@@ -45,10 +45,32 @@ window.customElements.define('set-timezone', SetTimezone)
|
||||
window.customElements.define('autoresize-textarea', AutoresizeTextarea)
|
||||
|
||||
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 {
|
||||
connectedCallback () {
|
||||
this.appElem = document.createElement('div')
|
||||
this.appElem.classList.add('max-h-screen')
|
||||
|
||||
this.app = createApp(TemplateBuilder, {
|
||||
template: reactive(JSON.parse(this.dataset.template)),
|
||||
|
||||
@@ -70,3 +70,7 @@ button[disabled] .enabled {
|
||||
.base-radio {
|
||||
@apply radio bg-white radio-sm no-animation;
|
||||
}
|
||||
|
||||
.base-select {
|
||||
@apply select base-input w-full font-normal;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.querySelectorAll('a').forEach((link) => {
|
||||
if (document.location.pathname.startsWith(link.pathname)) {
|
||||
if (document.location.pathname.startsWith(link.pathname) && !link.getAttribute('href').startsWith('http')) {
|
||||
link.classList.add('bg-base-300')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -5,8 +5,12 @@ export default actionable(class extends HTMLElement {
|
||||
document.body.classList.add('overflow-hidden')
|
||||
|
||||
document.addEventListener('keyup', this.onEscKey)
|
||||
document.addEventListener('turbo:submit-end', this.onSubmit)
|
||||
|
||||
document.addEventListener('turbo:before-cache', this.close)
|
||||
|
||||
if (this.dataset.closeAfterSubmit !== 'false') {
|
||||
document.addEventListener('turbo:submit-end', this.onSubmit)
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback () {
|
||||
|
||||
@@ -15,6 +15,7 @@ window.customElements.define('submission-form', class extends HTMLElement {
|
||||
canSendEmail: this.dataset.canSendEmail === 'true',
|
||||
isDirectUpload: this.dataset.isDirectUpload === 'true',
|
||||
isDemo: this.dataset.isDemo === 'true',
|
||||
withConfetti: true,
|
||||
values: reactive(JSON.parse(this.dataset.values)),
|
||||
attachments: reactive(JSON.parse(this.dataset.attachments)),
|
||||
fields: JSON.parse(this.dataset.fields)
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
:key="areaIndex"
|
||||
>
|
||||
<Teleport
|
||||
:to="`#page-${area.attachment_uuid}-${area.page}`"
|
||||
:to="findPageElementForArea(area)"
|
||||
>
|
||||
<FieldArea
|
||||
:ref="setAreaRef"
|
||||
@@ -78,10 +78,16 @@ export default {
|
||||
this.areaRefs = []
|
||||
},
|
||||
methods: {
|
||||
findPageElementForArea (area) {
|
||||
return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${area.attachment_uuid}-${area.page}`)
|
||||
},
|
||||
scrollIntoField (field) {
|
||||
this.areaRefs.find((area) => {
|
||||
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)
|
||||
} else {
|
||||
area.$refs.scrollToElem.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
@@ -94,13 +100,19 @@ export default {
|
||||
})
|
||||
},
|
||||
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 boxRect = window.scrollbox.children[0].getBoundingClientRect()
|
||||
const boxRect = scrollbox.children[0].getBoundingClientRect()
|
||||
const targetRect = target.getBoundingClientRect()
|
||||
|
||||
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) {
|
||||
if (el) {
|
||||
|
||||
@@ -86,6 +86,7 @@ export default {
|
||||
IconLogin,
|
||||
IconDownload
|
||||
},
|
||||
inject: ['baseUrl'],
|
||||
props: {
|
||||
submitterSlug: {
|
||||
type: String,
|
||||
@@ -96,6 +97,11 @@ export default {
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
withConfetti: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
canSendEmail: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -109,19 +115,21 @@ export default {
|
||||
}
|
||||
},
|
||||
async mounted () {
|
||||
const { default: confetti } = await import('canvas-confetti')
|
||||
if (this.withConfetti) {
|
||||
const { default: confetti } = await import('canvas-confetti')
|
||||
|
||||
confetti({
|
||||
particleCount: 50,
|
||||
startVelocity: 30,
|
||||
spread: 140
|
||||
})
|
||||
confetti({
|
||||
particleCount: 50,
|
||||
startVelocity: 30,
|
||||
spread: 140
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
sendCopyToEmail () {
|
||||
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'
|
||||
}).then(() => {
|
||||
alert('Email has been sent')
|
||||
@@ -132,7 +140,7 @@ export default {
|
||||
download () {
|
||||
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) => {
|
||||
return () => {
|
||||
return fetch(url).then(async (resp) => {
|
||||
|
||||
@@ -55,6 +55,7 @@ export default {
|
||||
IconCloudUpload,
|
||||
IconInnerShadowTop
|
||||
},
|
||||
inject: ['baseUrl'],
|
||||
props: {
|
||||
message: {
|
||||
type: String,
|
||||
@@ -141,7 +142,7 @@ export default {
|
||||
|
||||
return await Promise.all(
|
||||
blobs.map((blob) => {
|
||||
return fetch('/api/attachments', {
|
||||
return fetch(this.baseUrl + '/api/attachments', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: 'attachments',
|
||||
@@ -166,7 +167,7 @@ export default {
|
||||
formData.append('submitter_slug', this.submitterSlug)
|
||||
formData.append('name', 'attachments')
|
||||
|
||||
return fetch('/api/attachments', {
|
||||
return fetch(this.baseUrl + '/api/attachments', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}).then(resp => resp.json()).then((data) => {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
/>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
Submit Form
|
||||
@@ -194,7 +194,16 @@
|
||||
class="space-y-3.5 mx-auto"
|
||||
>
|
||||
<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
|
||||
v-for="field in currentStepFields"
|
||||
:key="field.uuid"
|
||||
type="hidden"
|
||||
:name="`values[${field.uuid}]`"
|
||||
:value="!!values[field.uuid]"
|
||||
>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div
|
||||
@@ -283,6 +292,7 @@
|
||||
<FormCompleted
|
||||
v-else
|
||||
:is-demo="isDemo"
|
||||
:with-confetti="withConfetti"
|
||||
:can-send-email="canSendEmail"
|
||||
:submitter-slug="submitterSlug"
|
||||
/>
|
||||
@@ -324,6 +334,11 @@ export default {
|
||||
IconArrowsDiagonalMinimize2,
|
||||
FormCompleted
|
||||
},
|
||||
provide () {
|
||||
return {
|
||||
baseUrl: this.baseUrl
|
||||
}
|
||||
},
|
||||
props: {
|
||||
submitterSlug: {
|
||||
type: String,
|
||||
@@ -343,6 +358,16 @@ export default {
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
withConfetti: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
baseUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
fields: {
|
||||
type: Array,
|
||||
required: false,
|
||||
@@ -350,16 +375,17 @@ export default {
|
||||
},
|
||||
authenticityToken: {
|
||||
type: String,
|
||||
required: true
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
isDirectUpload: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
isDemo: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
values: {
|
||||
@@ -422,10 +448,17 @@ export default {
|
||||
)
|
||||
|
||||
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')
|
||||
window.scrollbox.parentNode.classList.add('h-screen', 'overflow-y-auto')
|
||||
parent.style.overflow = 'hidden'
|
||||
|
||||
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: {
|
||||
@@ -448,7 +481,7 @@ export default {
|
||||
if (this.isCompleted) {
|
||||
return Promise.resolve({})
|
||||
} else {
|
||||
return fetch(this.submitPath, {
|
||||
return fetch(this.baseUrl + this.submitPath, {
|
||||
method: 'POST',
|
||||
body: formData || new FormData(this.$refs.form)
|
||||
})
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
data-tip="Type text"
|
||||
>
|
||||
<button
|
||||
id="type_text_button"
|
||||
class="btn btn-sm btn-circle"
|
||||
:class="{ 'btn-neutral': isTextSignature, 'btn-outline': !isTextSignature }"
|
||||
@click.prevent="toggleTextInput"
|
||||
@@ -78,6 +79,7 @@
|
||||
/>
|
||||
<input
|
||||
v-if="isTextSignature"
|
||||
id="signature_text_input"
|
||||
ref="textInput"
|
||||
class="base-input !text-2xl w-full mt-6"
|
||||
:required="field.required"
|
||||
@@ -99,6 +101,7 @@ export default {
|
||||
IconTextSize,
|
||||
IconArrowsDiagonalMinimize2
|
||||
},
|
||||
inject: ['baseUrl'],
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
@@ -304,7 +307,7 @@ export default {
|
||||
file,
|
||||
'/direct_uploads'
|
||||
).create((_error, data) => {
|
||||
fetch('/api/attachments', {
|
||||
fetch(this.baseUrl + '/api/attachments', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
submitter_slug: this.submitterSlug,
|
||||
@@ -326,12 +329,12 @@ export default {
|
||||
formData.append('submitter_slug', this.submitterSlug)
|
||||
formData.append('name', 'attachments')
|
||||
|
||||
return fetch('/api/attachments', {
|
||||
return fetch(this.baseUrl + '/api/attachments', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}).then((resp) => resp.json()).then((attachment) => {
|
||||
this.$emit('update:model-value', attachment.uuid)
|
||||
this.$emit('attached', attachment)
|
||||
this.$emit('update:model-value', attachment.uuid)
|
||||
|
||||
return resolve(attachment)
|
||||
})
|
||||
|
||||
@@ -226,14 +226,14 @@ export default {
|
||||
}
|
||||
},
|
||||
startResizeCell (e) {
|
||||
document.addEventListener('mousemove', this.onResizeCell)
|
||||
document.addEventListener('mouseup', this.stopResizeCell)
|
||||
this.$el.getRootNode().addEventListener('mousemove', this.onResizeCell)
|
||||
this.$el.getRootNode().addEventListener('mouseup', this.stopResizeCell)
|
||||
|
||||
this.$emit('start-resize', 'ew')
|
||||
},
|
||||
stopResizeCell (e) {
|
||||
document.removeEventListener('mousemove', this.onResizeCell)
|
||||
document.removeEventListener('mouseup', this.stopResizeCell)
|
||||
this.$el.getRootNode().removeEventListener('mousemove', this.onResizeCell)
|
||||
this.$el.getRootNode().removeEventListener('mouseup', this.stopResizeCell)
|
||||
|
||||
this.$emit('stop-resize')
|
||||
|
||||
@@ -266,11 +266,13 @@ export default {
|
||||
})
|
||||
},
|
||||
onNameBlur (e) {
|
||||
const text = this.$refs.name.innerText.trim()
|
||||
|
||||
this.isNameFocus = false
|
||||
this.$refs.name.style.minWidth = ''
|
||||
|
||||
if (e.target.innerText.trim()) {
|
||||
this.field.name = e.target.innerText.trim()
|
||||
if (text) {
|
||||
this.field.name = text
|
||||
} else {
|
||||
this.field.name = ''
|
||||
this.$refs.name.innerText = this.defaultName
|
||||
@@ -302,14 +304,14 @@ export default {
|
||||
|
||||
this.dragFrom = { x: e.clientX - rect.left, y: e.clientY - rect.top }
|
||||
|
||||
document.addEventListener('mousemove', this.drag)
|
||||
document.addEventListener('mouseup', this.stopDrag)
|
||||
this.$el.getRootNode().addEventListener('mousemove', this.drag)
|
||||
this.$el.getRootNode().addEventListener('mouseup', this.stopDrag)
|
||||
|
||||
this.$emit('start-drag')
|
||||
},
|
||||
stopDrag () {
|
||||
document.removeEventListener('mousemove', this.drag)
|
||||
document.removeEventListener('mouseup', this.stopDrag)
|
||||
this.$el.getRootNode().removeEventListener('mousemove', this.drag)
|
||||
this.$el.getRootNode().removeEventListener('mouseup', this.stopDrag)
|
||||
|
||||
if (this.isDragged) {
|
||||
this.save()
|
||||
@@ -322,14 +324,14 @@ export default {
|
||||
startResize () {
|
||||
this.selectedAreaRef.value = this.area
|
||||
|
||||
document.addEventListener('mousemove', this.resize)
|
||||
document.addEventListener('mouseup', this.stopResize)
|
||||
this.$el.getRootNode().addEventListener('mousemove', this.resize)
|
||||
this.$el.getRootNode().addEventListener('mouseup', this.stopResize)
|
||||
|
||||
this.$emit('start-resize', 'nwse')
|
||||
},
|
||||
stopResize () {
|
||||
document.removeEventListener('mousemove', this.resize)
|
||||
document.removeEventListener('mouseup', this.stopResize)
|
||||
this.$el.getRootNode().removeEventListener('mousemove', this.resize)
|
||||
this.$el.getRootNode().removeEventListener('mouseup', this.stopResize)
|
||||
|
||||
this.$emit('stop-resize')
|
||||
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
<template>
|
||||
<div
|
||||
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 space-x-3">
|
||||
<a href="/">
|
||||
<a
|
||||
v-if="withLogoLink"
|
||||
href="/"
|
||||
>
|
||||
<Logo />
|
||||
</a>
|
||||
<Logo v-else />
|
||||
<Contenteditable
|
||||
:model-value="template.name"
|
||||
class="text-3xl font-semibold focus:text-clip"
|
||||
@@ -16,46 +20,53 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="space-x-3 flex items-center">
|
||||
<a
|
||||
:href="`/templates/${template.id}/submissions/new`"
|
||||
data-turbo-frame="modal"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
<IconUsersPlus
|
||||
width="20"
|
||||
class="inline"
|
||||
/>
|
||||
<span class="hidden md:inline">
|
||||
Recipients
|
||||
</span>
|
||||
</a>
|
||||
<button
|
||||
class="base-button"
|
||||
:class="{ disabled: isSaving }"
|
||||
v-bind="isSaving ? { disabled: true } : {}"
|
||||
@click.prevent="onSaveClick"
|
||||
>
|
||||
<IconInnerShadowTop
|
||||
v-if="isSaving"
|
||||
width="20"
|
||||
class="animate-spin"
|
||||
/>
|
||||
<IconDeviceFloppy
|
||||
v-else
|
||||
width="20"
|
||||
/>
|
||||
<span class="hidden md:inline">
|
||||
Save
|
||||
</span>
|
||||
</button>
|
||||
<slot
|
||||
v-if="$slots.buttons"
|
||||
name="buttons"
|
||||
/>
|
||||
<template v-else>
|
||||
<a
|
||||
:href="`/templates/${template.id}/submissions/new`"
|
||||
data-turbo-frame="modal"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
<IconUsersPlus
|
||||
width="20"
|
||||
class="inline"
|
||||
/>
|
||||
<span class="hidden md:inline">
|
||||
Recipients
|
||||
</span>
|
||||
</a>
|
||||
<button
|
||||
class="base-button"
|
||||
:class="{ disabled: isSaving }"
|
||||
v-bind="isSaving ? { disabled: true } : {}"
|
||||
@click.prevent="onSaveClick"
|
||||
>
|
||||
<IconInnerShadowTop
|
||||
v-if="isSaving"
|
||||
width="20"
|
||||
class="animate-spin"
|
||||
/>
|
||||
<IconDeviceFloppy
|
||||
v-else
|
||||
width="20"
|
||||
/>
|
||||
<span class="hidden md:inline">
|
||||
Save
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex"
|
||||
style="max-height: calc(100vh - 60px)"
|
||||
style="max-height: calc(100% - 60px)"
|
||||
>
|
||||
<div
|
||||
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"
|
||||
>
|
||||
<DocumentPreview
|
||||
@@ -94,19 +105,46 @@
|
||||
@success="updateFromUpload"
|
||||
/>
|
||||
<template v-else>
|
||||
<Document
|
||||
<template
|
||||
v-for="document in sortedDocuments"
|
||||
:key="document.uuid"
|
||||
:ref="setDocumentRefs"
|
||||
:areas-index="fieldAreasIndex[document.uuid]"
|
||||
:selected-submitter="selectedSubmitter"
|
||||
:document="document"
|
||||
:is-drag="!!dragFieldType"
|
||||
:draw-field="drawField"
|
||||
@draw="onDraw"
|
||||
@drop-field="onDropfield"
|
||||
@remove-area="removeArea"
|
||||
/>
|
||||
>
|
||||
<Document
|
||||
:ref="setDocumentRefs"
|
||||
:areas-index="fieldAreasIndex[document.uuid]"
|
||||
:selected-submitter="selectedSubmitter"
|
||||
:document="document"
|
||||
:is-drag="!!dragFieldType"
|
||||
:draw-field="drawField"
|
||||
@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>
|
||||
</div>
|
||||
<div
|
||||
@@ -122,7 +160,7 @@
|
||||
</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'"
|
||||
>
|
||||
<div
|
||||
@@ -149,6 +187,7 @@
|
||||
:fields="template.fields"
|
||||
:submitters="template.submitters"
|
||||
:selected-submitter="selectedSubmitter"
|
||||
:with-sticky-submitters="withStickySubmitters"
|
||||
@set-draw="drawField = $event"
|
||||
@set-drag="dragFieldType = $event"
|
||||
@change-submitter="selectedSubmitter = $event"
|
||||
@@ -169,6 +208,7 @@ import Document from './document'
|
||||
import Logo from './logo'
|
||||
import Contenteditable from './contenteditable'
|
||||
import DocumentPreview from './preview'
|
||||
import DocumentControls from './controls'
|
||||
import { IconUsersPlus, IconDeviceFloppy, IconInnerShadowTop } from '@tabler/icons-vue'
|
||||
import { v4 } from 'uuid'
|
||||
import { ref, computed } from 'vue'
|
||||
@@ -182,6 +222,7 @@ export default {
|
||||
Logo,
|
||||
Dropzone,
|
||||
DocumentPreview,
|
||||
DocumentControls,
|
||||
IconInnerShadowTop,
|
||||
Contenteditable,
|
||||
IconUsersPlus,
|
||||
@@ -191,6 +232,7 @@ export default {
|
||||
return {
|
||||
template: this.template,
|
||||
save: this.save,
|
||||
baseFetch: this.baseFetch,
|
||||
selectedAreaRef: computed(() => this.selectedAreaRef)
|
||||
}
|
||||
},
|
||||
@@ -201,13 +243,34 @@ export default {
|
||||
},
|
||||
isDirectUpload: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
required: 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 () {
|
||||
return {
|
||||
documentRefs: [],
|
||||
isBreakpointLg: false,
|
||||
isSaving: false,
|
||||
selectedSubmitter: null,
|
||||
drawField: null,
|
||||
@@ -244,15 +307,28 @@ export default {
|
||||
this.selectedSubmitter = this.template.submitters[0]
|
||||
},
|
||||
mounted () {
|
||||
this.$nextTick(() => {
|
||||
this.onWindowResize()
|
||||
})
|
||||
|
||||
document.addEventListener('keyup', this.onKeyUp)
|
||||
|
||||
window.addEventListener('resize', this.onWindowResize)
|
||||
},
|
||||
unmounted () {
|
||||
document.removeEventListener('keyup', this.onKeyUp)
|
||||
|
||||
window.removeEventListener('resize', this.onWindowResize)
|
||||
},
|
||||
beforeUpdate () {
|
||||
this.documentRefs = []
|
||||
},
|
||||
methods: {
|
||||
onWindowResize (e) {
|
||||
const breakpointLg = 1024
|
||||
|
||||
this.isBreakpointLg = this.$el.getRootNode().children[0].offsetWidth < breakpointLg
|
||||
},
|
||||
setDocumentRefs (el) {
|
||||
if (el) {
|
||||
this.documentRefs.push(el)
|
||||
@@ -474,10 +550,18 @@ export default {
|
||||
|
||||
this.selectedAreaRef.value = area
|
||||
},
|
||||
baseFetch (path, options = {}) {
|
||||
return fetch(this.baseUrl + path, {
|
||||
...options,
|
||||
headers: { ...this.fetchOptions.headers, ...options.headers }
|
||||
})
|
||||
},
|
||||
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',
|
||||
body: JSON.stringify({
|
||||
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)"
|
||||
>
|
||||
↑
|
||||
</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)"
|
||||
>
|
||||
↓
|
||||
</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)"
|
||||
>
|
||||
×
|
||||
</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,
|
||||
IconInnerShadowTop
|
||||
},
|
||||
inject: ['baseFetch'],
|
||||
props: {
|
||||
templateId: {
|
||||
type: [Number, String],
|
||||
@@ -101,9 +102,13 @@ export default {
|
||||
methods: {
|
||||
upload: Upload.methods.upload,
|
||||
onDropFiles (e) {
|
||||
this.$refs.input.files = e.dataTransfer.files
|
||||
if ([...e.dataTransfer.files].every((f) => f.type.match(/(?:image\/)|(?:application\/pdf)/))) {
|
||||
this.$refs.input.files = e.dataTransfer.files
|
||||
|
||||
this.upload()
|
||||
this.upload()
|
||||
} else {
|
||||
alert('Only PDF and images are supported.')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
<label
|
||||
tabindex="0"
|
||||
title="Areas"
|
||||
class="cursor-pointer text-base-100 group-hover:text-base-content"
|
||||
class="cursor-pointer text-transparent group-hover:text-base-content"
|
||||
>
|
||||
<IconShape
|
||||
:width="18"
|
||||
@@ -104,7 +104,7 @@
|
||||
<button
|
||||
v-else
|
||||
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)"
|
||||
>
|
||||
<IconShape
|
||||
@@ -113,7 +113,7 @@
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="relative text-base-100 group-hover:text-base-content"
|
||||
class="relative text-transparent group-hover:text-base-content"
|
||||
title="Remove"
|
||||
@click="$emit('remove', field)"
|
||||
>
|
||||
@@ -122,7 +122,7 @@
|
||||
:stroke-width="1.6"
|
||||
/>
|
||||
</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
|
||||
title="Up"
|
||||
class="relative"
|
||||
@@ -156,7 +156,7 @@
|
||||
</span>
|
||||
<input
|
||||
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"
|
||||
required
|
||||
@blur="save"
|
||||
@@ -254,8 +254,10 @@ export default {
|
||||
})
|
||||
},
|
||||
onNameBlur (e) {
|
||||
if (e.target.innerText.trim()) {
|
||||
this.field.name = e.target.innerText.trim()
|
||||
const text = this.$refs.name.$refs.contenteditable.innerText.trim()
|
||||
|
||||
if (text) {
|
||||
this.field.name = text
|
||||
} else {
|
||||
this.field.name = ''
|
||||
this.$refs.name.$refs.contenteditable.innerText = this.defaultName
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<template>
|
||||
<div class="sticky -top-1 bg-base-100 pt-1 -mt-1 z-10">
|
||||
<div :class="withStickySubmitters ? 'sticky top-0 z-10' : ''">
|
||||
<FieldSubmitter
|
||||
:model-value="selectedSubmitter.uuid"
|
||||
class="w-full bg-base-100"
|
||||
class="w-full rounded-lg"
|
||||
:class="{ 'bg-base-100': withStickySubmitters }"
|
||||
:submitters="submitters"
|
||||
@new-submitter="save"
|
||||
@remove="removeSubmitter"
|
||||
@@ -28,7 +29,7 @@
|
||||
v-for="(icon, type) in fieldIcons"
|
||||
:key="type"
|
||||
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)"
|
||||
@dragend="$emit('drag-end')"
|
||||
@click="addField(type)"
|
||||
@@ -76,7 +77,7 @@
|
||||
Draw a text field on the page with a mouse
|
||||
</li>
|
||||
<li>
|
||||
Single click of the page to add a checkbox
|
||||
Single click on the page to add a checkbox
|
||||
</li>
|
||||
<li>
|
||||
Drag & drop any other field type on the page
|
||||
@@ -106,6 +107,11 @@ export default {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
withStickySubmitters: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
submitters: {
|
||||
type: Array,
|
||||
required: true
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
ref="image"
|
||||
:src="image.url"
|
||||
:width="width"
|
||||
class="shadow-md mb-4"
|
||||
class="border rounded mb-4"
|
||||
:height="height"
|
||||
loading="lazy"
|
||||
>
|
||||
|
||||
@@ -26,6 +26,7 @@ import Upload from './upload'
|
||||
|
||||
export default {
|
||||
name: 'ReplaceDocument',
|
||||
inject: ['baseFetch'],
|
||||
props: {
|
||||
templateId: {
|
||||
type: [Number, String],
|
||||
|
||||
@@ -50,6 +50,7 @@ export default {
|
||||
IconUpload,
|
||||
IconInnerShadowTop
|
||||
},
|
||||
inject: ['baseFetch'],
|
||||
props: {
|
||||
templateId: {
|
||||
type: [Number, String],
|
||||
@@ -113,7 +114,7 @@ export default {
|
||||
|
||||
this.isProcessing = true
|
||||
|
||||
fetch(`/api/templates/${this.templateId}/documents`, {
|
||||
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ blobs }),
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
@@ -124,7 +125,7 @@ export default {
|
||||
this.isProcessing = false
|
||||
})
|
||||
} else {
|
||||
fetch(`/api/templates/${this.templateId}/documents`, {
|
||||
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
|
||||
method: 'POST',
|
||||
body: new FormData(this.$refs.form)
|
||||
}).then(resp => resp.json()).then((data) => {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SendWebhookRequestJob < ApplicationJob
|
||||
USER_AGENT = 'DocuSeal.co Webhook'
|
||||
|
||||
def perform(submitter)
|
||||
config = submitter.submission.account.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
||||
|
||||
return if config.blank? || config.value.blank?
|
||||
|
||||
Submissions::EnsureResultGenerated.call(submitter)
|
||||
|
||||
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
||||
|
||||
Faraday.post(config.value,
|
||||
{
|
||||
event_type: 'form.submitted',
|
||||
timestamp: Time.current.iso8601,
|
||||
data: Submitters::SerializeForWebhook.call(submitter)
|
||||
}.to_json,
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => USER_AGENT)
|
||||
end
|
||||
end
|
||||
@@ -6,6 +6,10 @@ class ApplicationMailer < ActionMailer::Base
|
||||
|
||||
register_interceptor ActionMailerConfigsInterceptor
|
||||
|
||||
before_action do
|
||||
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
||||
end
|
||||
|
||||
def default_url_options
|
||||
Docuseal.default_url_options
|
||||
end
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SettingsMailer < ApplicationMailer
|
||||
def smtp_successful_setup(email)
|
||||
mail(to: email, subject: 'SMTP has been configured')
|
||||
end
|
||||
end
|
||||
@@ -3,23 +3,47 @@
|
||||
class SubmitterMailer < ApplicationMailer
|
||||
DEFAULT_MESSAGE = %(You have been invited to submit the "%<name>s" form:)
|
||||
|
||||
def invitation_email(submitter, message: format(DEFAULT_MESSAGE, name: submitter.submission.template.name))
|
||||
def invitation_email(submitter, message: '')
|
||||
@current_account = submitter.submission.template.account
|
||||
@submitter = submitter
|
||||
@message = message
|
||||
@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,
|
||||
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)
|
||||
end
|
||||
|
||||
def completed_email(submitter, user)
|
||||
@current_account = submitter.submission.template.account
|
||||
@submitter = submitter
|
||||
@user = user
|
||||
|
||||
mail(to: user.email,
|
||||
subject: %(#{submitter.email} has completed the "#{submitter.submission.template.name}" form))
|
||||
@email_config = @current_account.account_configs.find_by(key: AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY)
|
||||
|
||||
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
|
||||
|
||||
def documents_copy_email(submitter)
|
||||
@current_account = submitter.submission.template.account
|
||||
@submitter = submitter
|
||||
|
||||
Submissions::EnsureResultGenerated.call(@submitter)
|
||||
@@ -30,6 +54,23 @@ class SubmitterMailer < ApplicationMailer
|
||||
attachments[attachment.filename.to_s] = attachment.download
|
||||
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
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
class UserMailer < ApplicationMailer
|
||||
def invitation_email(user)
|
||||
@current_account = user.account
|
||||
@user = user
|
||||
@token = @user.send(:set_reset_password_token)
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: access_tokens
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# sha256 :text not null
|
||||
# token :text not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# user_id :bigint not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_access_tokens_on_sha256 (sha256) UNIQUE
|
||||
# index_access_tokens_on_user_id (user_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# fk_rails_... (user_id => users.id)
|
||||
#
|
||||
class AccessToken < ApplicationRecord
|
||||
TOKEN_LENGTH = 22
|
||||
|
||||
belongs_to :user
|
||||
|
||||
before_validation :set_sha256, on: :create
|
||||
|
||||
attribute :token, :string, default: -> { SecureRandom.base58(TOKEN_LENGTH) }
|
||||
|
||||
encrypts :token
|
||||
|
||||
private
|
||||
|
||||
def set_sha256
|
||||
self.sha256 = Digest::SHA256.hexdigest(token)
|
||||
end
|
||||
end
|
||||
@@ -14,7 +14,10 @@
|
||||
class Account < ApplicationRecord
|
||||
has_many :users, dependent: :destroy
|
||||
has_many :encrypted_configs, dependent: :destroy
|
||||
has_many :account_configs, dependent: :destroy
|
||||
has_many :templates, dependent: :destroy
|
||||
has_many :submissions, through: :templates
|
||||
has_many :submitters, through: :submissions
|
||||
has_many :active_users, -> { active }, dependent: :destroy,
|
||||
inverse_of: :account, class_name: 'User'
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -21,13 +21,18 @@
|
||||
# fk_rails_... (account_id => accounts.id)
|
||||
#
|
||||
class EncryptedConfig < ApplicationRecord
|
||||
FILES_STORAGE_KEY = 'active_storage'
|
||||
EMAIL_SMTP_KEY = 'action_mailer_smtp'
|
||||
ESIGN_CERTS_KEY = 'esign_certs'
|
||||
APP_URL_KEY = 'app_url'
|
||||
CONFIG_KEYS = [
|
||||
FILES_STORAGE_KEY = 'active_storage',
|
||||
EMAIL_SMTP_KEY = 'action_mailer_smtp',
|
||||
ESIGN_CERTS_KEY = 'esign_certs',
|
||||
APP_URL_KEY = 'app_url',
|
||||
WEBHOOK_URL_KEY = 'webhook_url'
|
||||
].freeze
|
||||
|
||||
belongs_to :account
|
||||
|
||||
validates :key, inclusion: { in: CONFIG_KEYS }
|
||||
|
||||
encrypts :value
|
||||
|
||||
serialize :value, JSON
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# deleted_at :datetime
|
||||
# source :text not null
|
||||
# template_fields :text
|
||||
# template_schema :text
|
||||
# template_submitters :text
|
||||
@@ -26,6 +27,7 @@
|
||||
#
|
||||
class Submission < ApplicationRecord
|
||||
belongs_to :template
|
||||
has_one :account, through: :template
|
||||
belongs_to :created_by_user, class_name: 'User', optional: true
|
||||
|
||||
has_many :submitters, dependent: :destroy
|
||||
@@ -34,5 +36,14 @@ class Submission < ApplicationRecord
|
||||
serialize :template_schema, JSON
|
||||
serialize :template_submitters, JSON
|
||||
|
||||
attribute :source, :string, default: 'link'
|
||||
|
||||
scope :active, -> { where(deleted_at: nil) }
|
||||
|
||||
enum :source, {
|
||||
invite: 'invite',
|
||||
api: 'api',
|
||||
embed: 'embed',
|
||||
link: 'link'
|
||||
}, scope: false, prefix: true
|
||||
end
|
||||
@@ -30,6 +30,8 @@
|
||||
#
|
||||
class Submitter < ApplicationRecord
|
||||
belongs_to :submission
|
||||
has_one :template, through: :submission
|
||||
has_one :account, through: :template
|
||||
|
||||
attribute :values, :string, default: -> { {} }
|
||||
attribute :slug, :string, default: -> { SecureRandom.base58(14) }
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
# name :string not null
|
||||
# schema :text not null
|
||||
# slug :string not null
|
||||
# source :text not null
|
||||
# submitters :text not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
@@ -37,6 +38,7 @@ class Template < ApplicationRecord
|
||||
attribute :schema, :string, default: -> { [] }
|
||||
attribute :submitters, :string, default: -> { [{ name: DEFAULT_SUBMITTER_NAME, uuid: SecureRandom.uuid }] }
|
||||
attribute :slug, :string, default: -> { SecureRandom.base58(14) }
|
||||
attribute :source, :string, default: 'native'
|
||||
|
||||
serialize :fields, JSON
|
||||
serialize :schema, JSON
|
||||
|
||||
+24
-9
@@ -11,8 +11,8 @@
|
||||
# email :string not null
|
||||
# encrypted_password :string not null
|
||||
# failed_attempts :integer default(0), not null
|
||||
# first_name :string not null
|
||||
# last_name :string not null
|
||||
# first_name :string
|
||||
# last_name :string
|
||||
# last_sign_in_at :datetime
|
||||
# last_sign_in_ip :string
|
||||
# locked_at :datetime
|
||||
@@ -22,6 +22,7 @@
|
||||
# role :string not null
|
||||
# sign_in_count :integer default(0), not null
|
||||
# unlock_token :string
|
||||
# uuid :text not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint not null
|
||||
@@ -32,25 +33,35 @@
|
||||
# index_users_on_email (email) UNIQUE
|
||||
# index_users_on_reset_password_token (reset_password_token) UNIQUE
|
||||
# index_users_on_unlock_token (unlock_token) UNIQUE
|
||||
# index_users_on_uuid (uuid) UNIQUE
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# fk_rails_... (account_id => accounts.id)
|
||||
#
|
||||
class User < ApplicationRecord
|
||||
ROLES = %w[admin].freeze
|
||||
ROLES = [
|
||||
ADMIN_ROLE = 'admin'
|
||||
].freeze
|
||||
|
||||
EMAIL_REGEXP =
|
||||
/[a-z0-9][.']?(?:(?:[a-z0-9_-]++[.'])*[a-z0-9_-]++)*@(?:[a-z0-9]++[.-])*[a-z0-9]++\.[a-z]{2,}/i
|
||||
EMAIL_REGEXP = /[^@,\s]+@[^@,\s]+/
|
||||
|
||||
belongs_to :account
|
||||
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 :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 }
|
||||
|
||||
scope :active, -> { where(deleted_at: nil) }
|
||||
scope :admins, -> { where(role: ADMIN_ROLE) }
|
||||
|
||||
def access_token
|
||||
super || build_access_token.tap(&:save!)
|
||||
end
|
||||
|
||||
def active_for_authentication?
|
||||
!deleted_at?
|
||||
@@ -61,14 +72,18 @@ class User < ApplicationRecord
|
||||
end
|
||||
|
||||
def initials
|
||||
[first_name.first, last_name.first].join.upcase
|
||||
[first_name&.first, last_name&.first].compact_blank.join.upcase
|
||||
end
|
||||
|
||||
def full_name
|
||||
[first_name, last_name].join(' ')
|
||||
[first_name, last_name].compact_blank.join(' ')
|
||||
end
|
||||
|
||||
def friendly_name
|
||||
"#{full_name} <#{email}>"
|
||||
if full_name.present?
|
||||
"#{full_name} <#{email}>"
|
||||
else
|
||||
email
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -11,13 +11,13 @@
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<%= ff.label :timezone, class: 'label' %>
|
||||
<%= ff.select :timezone, nil, {}, class: 'select base-input w-full font-normal' do %>
|
||||
<%= ff.select :timezone, nil, {}, class: 'base-select' do %>
|
||||
<%= time_zone_options_for_select(current_account.timezone) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= ff.label :locale, 'Time format', class: 'label' %>
|
||||
<%= ff.select :locale, options_for_select(controller.class::LOCALE_OPTIONS.invert, current_account.locale), {}, class: 'select base-input w-full font-normal' %>
|
||||
<%= ff.select :locale, options_for_select(controller.class::LOCALE_OPTIONS.invert, current_account.locale), {}, class: 'base-select' %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
|
||||
<%= render 'shared/settings_nav' %>
|
||||
<div class="flex-grow">
|
||||
<h1 class="text-4xl font-bold mb-4">API</h1>
|
||||
<div class="card bg-base-200">
|
||||
<div class="card-body p-6">
|
||||
<label for="api_key" class="text-sm font-semibold">X-Auth-Token</label>
|
||||
<div class="flex flex-row space-x-4">
|
||||
<input id="api_key" type="text" value="<%= current_user.access_token.token %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
|
||||
<%= render 'shared/clipboard_copy', icon: 'copy', text: current_user.access_token.token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-4 mt-4">
|
||||
<div class="collapse collapse-plus bg-base-200 px-1">
|
||||
<input type="checkbox">
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
<div>
|
||||
Request signature, single submitter
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<div class="badge badge-warning badge-lg">POST</div>
|
||||
<div class="badge badge-primary badge-lg"><%= api_submissions_path %></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse-content" style="display: inherit">
|
||||
<div class="mockup-code overflow-hidden">
|
||||
<% text = capture do %>curl --location '<%= api_submissions_url %>' \
|
||||
--header 'X-Auth-Token: <%= current_user.access_token.token %>' \
|
||||
--data-raw '{
|
||||
"template_id": <%= current_account.templates.last&.id || 1 %>,
|
||||
"emails": "<%= current_user.email.sub('@', '+test@') %>, <%= current_user.email.sub('@', '+test2@') %>"
|
||||
}'<% end.to_str %>
|
||||
<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' %>
|
||||
</span>
|
||||
<pre data-prefix="$"><code class="overflow-hidden w-full"><%= text %></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse collapse-plus bg-base-200 px-1">
|
||||
<input type="checkbox">
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
<div>
|
||||
Request signature, multiple submitters with default values
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<div class="badge badge-warning badge-lg">POST</div>
|
||||
<div class="badge badge-primary badge-lg"><%= api_submissions_path %></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse-content" style="display: inherit">
|
||||
<div class="mockup-code overflow-hidden">
|
||||
<% text = capture do %>curl --location '<%= api_submissions_url %>' \
|
||||
--header 'X-Auth-Token: <%= current_user.access_token.token %>' \
|
||||
--data-raw '{
|
||||
"template_id": <%= current_account.templates.last&.id || 1 %>,
|
||||
"submission": [
|
||||
{
|
||||
"submitters": [
|
||||
{
|
||||
"name": "<%= current_account.templates.last ? current_account.templates.last.submitters.first['name'] : 'First Submitter' %>",
|
||||
"email": "<%= current_user.email.sub('@', '+test@') %>",
|
||||
"values": {
|
||||
"Form Text Field Name": "Default Value"
|
||||
}
|
||||
},
|
||||
{ "name": "Second Submitter", "email": "<%= current_user.email.sub('@', '+test2@') %>" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}'<% end.to_str %>
|
||||
<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' %>
|
||||
</span>
|
||||
<pre data-prefix="$"><code class="overflow-hidden w-full"><%= text %></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse collapse-plus bg-base-200 px-1">
|
||||
<input type="checkbox">
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
<div>
|
||||
Template details
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<div class="badge badge-info badge-lg">GET</div>
|
||||
<div class="badge badge-primary badge-lg"><%= api_template_path(':id') %></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse-content" style="display: inherit">
|
||||
<div class="mockup-code overflow-hidden">
|
||||
<% text = capture do %>curl '<%= api_template_url(current_account.templates&.last || 1) %>' \
|
||||
--header 'X-Auth-Token: <%= current_user.access_token.token %>'<% end.to_str %>
|
||||
<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' %>
|
||||
</span>
|
||||
<pre data-prefix="$"><code class="overflow-hidden w-full"><%= text %></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="pb-6 pt-1 space-y-1">
|
||||
<p class="flex items-center space-x-1 text-xs text-base-content/60">
|
||||
<%= 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 class="flex items-center space-x-1 text-xs text-base-content/60">
|
||||
<%= svg_icon('calendar', class: 'w-4 h-4') %>
|
||||
|
||||
@@ -25,6 +25,27 @@
|
||||
<%= ff.password_field :password, value: value['password'], required: true, class: 'base-input' %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<%= ff.label :domain, 'Domain (optional)', class: 'label' %>
|
||||
<%= ff.text_field :domain, value: value['domain'], class: 'base-input' %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= ff.label :authentication, class: 'label' %>
|
||||
<%= ff.select :authentication, options_for_select([%w[Plain plain], %w[Login login], %w[CRAM-MD5 cram_md5]], value.fetch('authentication', 'plain')), { prompt: true }, required: true, class: 'base-select' %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= ff.label :security_label, 'SMTP Security', class: 'label' %>
|
||||
<div class="flex items-center space-x-6">
|
||||
<% [%w[None none], %w[SSL ssl], %w[TLS tls]].each do |(label, val)| %>
|
||||
<%= ff.label :security, value: val, for: "#{val}_radio", class: 'label' do %>
|
||||
<%= ff.radio_button :security, val, checked: (value['security'].blank? && val == 'none') || value['security'] == val, id: "#{val}_radio", class: 'base-radio mr-2' %>
|
||||
<%= label %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= ff.label :from_email, 'Send from Email', class: 'label' %>
|
||||
<%= ff.email_field :from_email, value: value['from_email'], required: true, class: 'base-input' %>
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -8,11 +8,6 @@
|
||||
</head>
|
||||
<body>
|
||||
<%= yield %>
|
||||
<p>
|
||||
---
|
||||
</p>
|
||||
<p>
|
||||
Sent using <a href="<%= Docuseal::PRODUCT_URL %>"><%= Docuseal::PRODUCT_NAME %></a> documents software.
|
||||
</p>
|
||||
<%= render partial: 'shared/mailer_attribution' %>
|
||||
</body>
|
||||
</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 Enterprise</p>
|
||||
<p>
|
||||
Display your company name and logo when signing documents.
|
||||
<br>
|
||||
<a class="link font-medium" target="_blank" href="<%= "#{Docuseal::PRODUCT_URL}/pricing" %>">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">
|
||||
<div class="flex items-center justify-center">
|
||||
<a href="/" class="flex items-center">
|
||||
<div class="mr-3">
|
||||
<%= render 'shared/logo', width: '50px', height: '50px' %>
|
||||
</div>
|
||||
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
|
||||
</a>
|
||||
<%= render 'start_form/docuseal_logo' %>
|
||||
</div>
|
||||
<div class="text-center text-4xl font-bold">
|
||||
Email has been sent
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<p>SMTP has been successfully configured!</p>
|
||||
@@ -8,7 +8,7 @@
|
||||
</span>
|
||||
<span class="disabled">
|
||||
<span class="flex items-center justify-center space-x-2">
|
||||
<%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
|
||||
<%= local_assigns[:icon_disabled] || svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
|
||||
<span><%= disabled_with %>...</span>
|
||||
</span>
|
||||
</span>
|
||||
@@ -2,16 +2,26 @@
|
||||
<label class="<%= local_assigns[:class] %>">
|
||||
<input type="radio" class="peer hidden">
|
||||
<span class="peer-checked:hidden flex items-center space-x-2">
|
||||
<%= svg_icon('link', class: local_assigns[:icon_class] || 'w-6 h-6 text-white') %>
|
||||
<%= svg_icon(local_assigns[:icon] || 'link', class: local_assigns[:icon_class] || 'w-6 h-6 text-white') %>
|
||||
<span class="hidden md:inline">
|
||||
<%= local_assigns[:copy_title] || 'Copy' %>
|
||||
</span>
|
||||
<% if local_assigns[:copy_title_md] %>
|
||||
<span class="inline md:hidden">
|
||||
<%= local_assigns[:copy_title_md] %>
|
||||
</span>
|
||||
<% end %>
|
||||
</span>
|
||||
<span class="hidden peer-checked:flex items-center space-x-2">
|
||||
<%= svg_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">
|
||||
<%= local_assigns[:copied_title] || 'Copied' %>
|
||||
</span>
|
||||
<% if local_assigns[:copied_title_md] %>
|
||||
<span class="inline md:hidden">
|
||||
<%= local_assigns[:copied_title_md] %>
|
||||
</span>
|
||||
<% end %>
|
||||
</span>
|
||||
</label>
|
||||
</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' %>
|
||||
@@ -15,7 +15,14 @@
|
||||
<% if Docuseal.demo? %>
|
||||
<%= render 'shared/github_button' %>
|
||||
<% 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 %>
|
||||
<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">
|
||||
@@ -23,14 +30,28 @@
|
||||
</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">
|
||||
<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') %>
|
||||
<span class="mr-1">Profile</span>
|
||||
<% end %>
|
||||
</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>
|
||||
<%= button_to destroy_user_session_path, method: :delete, data: { turbo: false } do %>
|
||||
<%= svg_icon('logout', class: 'w-5 h-5 stroke-2 inline') %>
|
||||
<%= link_to settings_esign_index_path, class: 'flex items-center' do %>
|
||||
<%= 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>
|
||||
<% end %>
|
||||
</li>
|
||||
|
||||
@@ -23,6 +23,25 @@
|
||||
<li>
|
||||
<%= link_to 'Team', settings_users_path, class: 'text-base hover:bg-base-300' %>
|
||||
</li>
|
||||
<% if Docuseal.demo? || !Docuseal.multitenant? %>
|
||||
<li>
|
||||
<%= link_to 'API', settings_api_index_path, class: 'text-base hover:bg-base-300' %>
|
||||
</li>
|
||||
<li>
|
||||
<%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %>
|
||||
</li>
|
||||
<% end %>
|
||||
<li>
|
||||
<%= link_to 'Personalization', settings_personalization_path, class: 'text-base hover:bg-base-300' %>
|
||||
</li>
|
||||
<% unless Docuseal.demo? %>
|
||||
<li>
|
||||
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
||||
Console
|
||||
<span class="badge badge-warning">New</span>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</menu-active>
|
||||
<div class="mx-4 border-t border-base-300 hidden md:block">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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="modal-box pt-4 pb-6 px-6 mt-20 max-h-none">
|
||||
<% if local_assigns[:title] %>
|
||||
|
||||
@@ -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>
|
||||
@@ -2,12 +2,7 @@
|
||||
<div class="space-y-6 mx-auto">
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-center">
|
||||
<a href="/" class="flex items-center">
|
||||
<div class="mr-3">
|
||||
<%= render 'shared/logo', width: '50px', height: '50px' %>
|
||||
</div>
|
||||
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
|
||||
</a>
|
||||
<%= render 'banner' %>
|
||||
</div>
|
||||
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
|
||||
<div class="flex items-center">
|
||||
|
||||
@@ -2,12 +2,7 @@
|
||||
<div class="space-y-6 mx-auto">
|
||||
<div class="space-y-6">
|
||||
<div class="text-center w-full space-y-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>
|
||||
<%= render 'banner' %>
|
||||
<p class="text-xl font-semibold text-center">You have been invited to submit a form</p>
|
||||
</div>
|
||||
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<div class="form-control">
|
||||
<% is_smtp_configured = Accounts.can_send_emails?(current_account) %>
|
||||
<%= 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>
|
||||
<% end %>
|
||||
<% unless is_smtp_configured %>
|
||||
@@ -57,21 +57,23 @@
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div id="message_field" class="card card-compact bg-base-200 hidden">
|
||||
<div class="card-body">
|
||||
<div class="form-control space-y-2">
|
||||
<span class="label-text">Hi there,</span>
|
||||
<autoresize-textarea>
|
||||
<%= f.text_area :message, value: format(SubmitterMailer::DEFAULT_MESSAGE, name: @template.name), required: true, class: 'base-textarea !rounded-lg w-full' %>
|
||||
</autoresize-textarea>
|
||||
<span class="label-text">
|
||||
Thanks,
|
||||
<br>
|
||||
<%= current_account.name %>
|
||||
</span>
|
||||
<% unless AccountConfig.exists?(key: AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) %>
|
||||
<div id="message_field" class="card card-compact bg-base-200 hidden">
|
||||
<div class="card-body">
|
||||
<div class="form-control space-y-2">
|
||||
<span class="label-text">Hi there,</span>
|
||||
<autoresize-textarea>
|
||||
<%= f.text_area :message, value: format(SubmitterMailer::DEFAULT_MESSAGE, name: @template.name), required: true, class: 'base-textarea !rounded-lg w-full' %>
|
||||
</autoresize-textarea>
|
||||
<span class="label-text">
|
||||
Thanks,
|
||||
<br>
|
||||
<%= current_account.name %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="form-control">
|
||||
<%= f.button button_title(title: 'Add Recipients'), class: 'base-button' %>
|
||||
</div>
|
||||
|
||||
@@ -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 %>
|
||||
@@ -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>
|
||||
@@ -2,12 +2,7 @@
|
||||
<div class="space-y-6 mx-auto">
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-center">
|
||||
<a href="/" class="flex items-center">
|
||||
<div class="mr-3">
|
||||
<%= render 'shared/logo', width: '50px', height: '50px' %>
|
||||
</div>
|
||||
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
|
||||
</a>
|
||||
<%= render 'start_form/banner' %>
|
||||
</div>
|
||||
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
|
||||
<div class="flex items-center">
|
||||
|
||||
@@ -5,10 +5,7 @@
|
||||
<div id="scrollbox">
|
||||
<div class="mx-auto block pb-72" style="max-width: 1000px">
|
||||
<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 'shared/logo', class: 'w-9 h-9 md:w-12 md:h-12' %>
|
||||
<span>DocuSeal</span>
|
||||
</a>
|
||||
<%= render 'banner' %>
|
||||
</div>
|
||||
<% (@submitter.submission.template_schema || @submitter.submission.template.schema).each do |item| %>
|
||||
<% document = @submitter.submission.template.documents.find { |a| a.uuid == item['attachment_uuid'] } %>
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
<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>
|
||||
<% if @email_config %>
|
||||
<%= auto_link(simple_format(h(ReplaceEmailVariables.call(@email_config.value['body'], submitter: @submitter)))) %>
|
||||
<% 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>
|
||||
<p>Please check the copy of your "<%= @submitter.submission.template.name %>" submission in the email attachments.</p>
|
||||
<p>Alternatively, you can download the copy using:</p>
|
||||
<% @documents.each do |document| %>
|
||||
<ul>
|
||||
<li>
|
||||
<%= link_to document.filename.to_s, rails_blob_url(document) %>
|
||||
</li>
|
||||
</ul>
|
||||
<% if @email_config %>
|
||||
<%= auto_link(simple_format(h(ReplaceEmailVariables.call(@email_config.value['body'], submitter: @submitter)))) %>
|
||||
<% else %>
|
||||
<p>Hi there,</p>
|
||||
<p>Please check the copy of your "<%= @submitter.submission.template.name %>" submission in the email attachments.</p>
|
||||
<p>Alternatively, you can download the copy using:</p>
|
||||
<% @documents.each do |document| %>
|
||||
<ul>
|
||||
<li>
|
||||
<%= link_to document.filename.to_s, rails_blob_url(document) %>
|
||||
</li>
|
||||
</ul>
|
||||
<% end %>
|
||||
<p>
|
||||
Thanks,<br><%= @current_account.name %>
|
||||
</p>
|
||||
<% end %>
|
||||
<p>
|
||||
Thanks,<br><%= @submitter.submission.template.account.name %>
|
||||
</p>
|
||||
@@ -1,7 +1,11 @@
|
||||
<p>Hi there,</p>
|
||||
<%= simple_format(@message) %>
|
||||
<p><%= link_to 'Submit Form', submit_form_url(slug: @submitter.slug) %></p>
|
||||
<p>Please contact us by replying to this email if you didn't request this.</p>
|
||||
<p>
|
||||
Thanks,<br><%= @submitter.submission.template.account.name %>
|
||||
</p>
|
||||
<% if @email_config %>
|
||||
<%= auto_link(simple_format(h(ReplaceEmailVariables.call(@email_config.value['body'], submitter: @submitter)))) %>
|
||||
<% else %>
|
||||
<p>Hi there,</p>
|
||||
<%= simple_format(@message) %>
|
||||
<p><%= link_to 'Submit Form', submit_form_url(slug: @submitter.slug) %></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 @@
|
||||
<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>
|
||||
@@ -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;">
|
||||
<%= @template.name %>
|
||||
</h1>
|
||||
@@ -17,16 +17,22 @@
|
||||
</div>
|
||||
</div>
|
||||
<% 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>
|
||||
<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 %>
|
||||
<%= 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 %>
|
||||
<% 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') %>
|
||||
<span class="hidden md:block">Add Recipients</span>
|
||||
<span>Add <span class="hidden md:inline">Recipients</span></span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -38,7 +44,7 @@
|
||||
<% @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">
|
||||
<% 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 %>
|
||||
<div>
|
||||
<% submitter = submitters.first %>
|
||||
@@ -80,7 +86,7 @@
|
||||
<% else %>
|
||||
<div class="space-y-1 w-full md:mr-2">
|
||||
<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) %>
|
||||
<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">
|
||||
@@ -92,7 +98,7 @@
|
||||
<% submitters.each_with_index do |submitter, index| %>
|
||||
<div class="flex justify-between 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) %>">
|
||||
<span class="badge md:w-24 <%= status_badges[submitter.status] %> bg-opacity-50 uppercase text-xs font-semibold">
|
||||
<%= submitter.status %>
|
||||
@@ -103,7 +109,7 @@
|
||||
<%= submitter.email %>
|
||||
</span>
|
||||
</span>
|
||||
<% if submitter.completed_at? && !is_submission_complated %>
|
||||
<% if submitter.completed_at? && !is_submission_completed %>
|
||||
<form onsubmit="event.preventDefault()">
|
||||
<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">
|
||||
@@ -118,7 +124,7 @@
|
||||
</download-button>
|
||||
</button>
|
||||
</form>
|
||||
<% elsif !is_submission_complated %>
|
||||
<% elsif !is_submission_completed %>
|
||||
<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' %>
|
||||
</div>
|
||||
@@ -129,7 +135,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<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) %>
|
||||
<form onsubmit="event.preventDefault()">
|
||||
<button onclick="event.stopPropagation()">
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
<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>
|
||||
Thanks,<br><%= @user.account.name %>
|
||||
Thanks,<br><%= @current_account.name %>
|
||||
</p>
|
||||
@@ -0,0 +1,42 @@
|
||||
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
|
||||
<%= render 'shared/settings_nav' %>
|
||||
<div class="flex-grow">
|
||||
<h1 class="text-4xl font-bold mb-4">Webhooks</h1>
|
||||
<div class="card bg-base-200">
|
||||
<div class="card-body p-6">
|
||||
<%= form_for @encrypted_config, url: settings_webhooks_path, method: :post, html: { autocomplete: 'off' } do |f| %>
|
||||
<%= f.label :value, 'Webhook URL', class: 'text-sm font-semibold' %>
|
||||
<div class="flex flex-row space-x-4 mt-2">
|
||||
<%= f.text_field :value, required: true, class: 'input font-mono input-bordered w-full', placeholder: 'https://example.com/hook' %>
|
||||
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button w-32' %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last %>
|
||||
<% if submitter %>
|
||||
<div class="space-y-4 mt-4">
|
||||
<div class="collapse collapse-open bg-base-200 px-1">
|
||||
<div class="p-4 text-xl font-medium">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>
|
||||
Submission example payload
|
||||
</span>
|
||||
<% if @encrypted_config.value.present? %>
|
||||
<%= button_to button_title(title: 'Test Webhook', disabled_with: 'Sending', icon_disabled: svg_icon('loader', class: 'w-4 h-4 animate-spin')), settings_webhooks_path, class: 'btn btn-neutral btn-outline btn-sm', method: :put %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse-content" style="display: inherit">
|
||||
<div class="mockup-code overflow-hidden relative">
|
||||
<span class="top-0 right-0 absolute">
|
||||
<%= render 'shared/clipboard_copy', icon: 'copy', text: code = JSON.pretty_generate({ event_type: 'form.submitted', timestamp: Time.current.iso8601, data: Submitters::SerializeForWebhook.call(submitter) }).gsub(/^/, ' ').sub(/^\s+/, ''), class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
|
||||
</span>
|
||||
<pre><code class="overflow-hidden w-full"><%= code %></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -9,6 +9,8 @@ require 'action_view/railtie'
|
||||
require 'action_mailer/railtie'
|
||||
require 'active_job/railtie'
|
||||
|
||||
require_relative '../lib/api_path_consider_json_middleware'
|
||||
|
||||
Bundler.require(*Rails.groups)
|
||||
|
||||
module DocuSeal
|
||||
@@ -26,5 +28,8 @@ module DocuSeal
|
||||
config.action_view.frozen_string_literal = true
|
||||
|
||||
config.middleware.insert_before ActionDispatch::Static, Rack::Deflater
|
||||
config.middleware.insert_before ActionDispatch::Static, ApiPathConsiderJsonMiddleware
|
||||
|
||||
ActiveSupport.run_load_hooks(:application_config, self)
|
||||
end
|
||||
end
|
||||
@@ -59,4 +59,10 @@ Rails.application.configure do
|
||||
|
||||
# Annotate rendered view with file names.
|
||||
# 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
|
||||
@@ -13,6 +13,20 @@ Rails.configuration.to_prepare do
|
||||
response.set_header('Cache-Control', 'public, max-age=31536000') if action_name == 'show'
|
||||
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
|
||||
next if current_user
|
||||
next if Submitter.find_signed(cookies[:submitter_sid])
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'csv'
|
||||
@@ -1,5 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../../lib/auth_with_token_strategy'
|
||||
|
||||
Warden::Strategies.add(:auth_token, AuthWithTokenStrategy)
|
||||
|
||||
# Assuming you have not yet modified this file, each configuration option below
|
||||
# is set to its default value. Note that some are commented out while others
|
||||
# are not: uncommented lines are intended to protect your configuration from
|
||||
@@ -272,10 +276,10 @@ Devise.setup do |config|
|
||||
# If you want to use other strategies, that are not supported by Devise, or
|
||||
# change the failure app, you can configure them inside the config.warden block.
|
||||
#
|
||||
# config.warden do |manager|
|
||||
# manager.intercept_401 = false
|
||||
# manager.default_strategies(scope: :user).unshift :some_external_strategy
|
||||
# end
|
||||
config.warden do |manager|
|
||||
# manager.intercept_401 = false
|
||||
manager.default_strategies(scope: :user).unshift(:auth_token)
|
||||
end
|
||||
|
||||
# ==> Mountable engine configurations
|
||||
# When using Devise inside an engine, let's call it `MyEngine`, and this engine
|
||||
|
||||
+14
-3
@@ -27,10 +27,12 @@ Rails.application.routes.draw do
|
||||
end
|
||||
end
|
||||
|
||||
namespace :api do
|
||||
namespace :api, defaults: { format: :json } do
|
||||
resources :attachments, only: %i[create]
|
||||
resources :templates, only: %i[update] do
|
||||
resources :documents, only: %i[create destroy], controller: 'templates_documents'
|
||||
resources :submissions, only: %i[create]
|
||||
resources :templates, only: %i[update show index] do
|
||||
resources :submissions, only: %i[create]
|
||||
resources :documents, only: %i[create], controller: 'templates_documents'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -39,8 +41,10 @@ Rails.application.routes.draw do
|
||||
resource :newsletter, only: %i[show update]
|
||||
resources :users, only: %i[new create edit update destroy]
|
||||
resources :submissions, only: %i[show destroy]
|
||||
resources :console_redirect, only: %i[index]
|
||||
resources :templates, only: %i[new create edit show destroy] do
|
||||
resources :submissions, only: %i[new create]
|
||||
resources :submissions_export, only: %i[index new]
|
||||
end
|
||||
|
||||
resources :start_form, only: %i[show update], path: 'd', param: 'slug' do
|
||||
@@ -67,6 +71,11 @@ Rails.application.routes.draw do
|
||||
end
|
||||
resources :esign, only: %i[index create], controller: 'esign_settings'
|
||||
resources :users, only: %i[index]
|
||||
resource :personalization, only: %i[show create], controller: 'personalization_settings'
|
||||
if !Docuseal.multitenant? || Docuseal.demo?
|
||||
resources :api, only: %i[index], controller: 'api_settings'
|
||||
resource :webhooks, only: %i[show create update], controller: 'webhook_settings'
|
||||
end
|
||||
resource :account, only: %i[show update]
|
||||
resources :profile, only: %i[index] do
|
||||
collection do
|
||||
@@ -76,4 +85,6 @@ Rails.application.routes.draw do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
ActiveSupport.run_load_hooks(:routes, self)
|
||||
end
|
||||
@@ -12,7 +12,6 @@ default: &default
|
||||
compiler_strategy: digest
|
||||
cache_manifest: false
|
||||
ensure_consistent_versioning: false
|
||||
useContentHash: false
|
||||
|
||||
development:
|
||||
<<: *default
|
||||
|
||||
@@ -27,3 +27,8 @@ azure:
|
||||
storage_access_key: <%= ENV['AZURE_STORAGE_ACCESS_KEY'] %>
|
||||
container: <%= ENV['AZURE_CONTAINER'] %>
|
||||
public: <%= ENV['ACTIVE_STORAGE_PUBLIC'] == 'true' %>
|
||||
|
||||
test:
|
||||
service: Disk
|
||||
root: <%= Rails.root.join("tmp/storage") %>
|
||||
public: true
|
||||
@@ -0,0 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddUuidToUsers < ActiveRecord::Migration[7.0]
|
||||
class MigrationUser < ApplicationRecord
|
||||
self.table_name = 'users'
|
||||
end
|
||||
|
||||
def up
|
||||
add_column :users, :uuid, :text
|
||||
add_index :users, :uuid, unique: true
|
||||
|
||||
MigrationUser.all.each do |user|
|
||||
user.update_columns(uuid: SecureRandom.uuid)
|
||||
end
|
||||
|
||||
change_column_null :users, :uuid, false
|
||||
end
|
||||
|
||||
def down
|
||||
drop_column :users, :uuid
|
||||
end
|
||||
end
|
||||
Loaded 100 of 146 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user