mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 14:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14bd039a88 | ||
|
|
8c8c34cb75 | ||
|
|
0cc53066eb | ||
|
|
3d13d28e05 | ||
|
|
a80e20234f | ||
|
|
b1b46cf998 | ||
|
|
52ef0ae929 | ||
|
|
c4daf8ed49 | ||
|
|
c098e61947 | ||
|
|
ceb052b158 | ||
|
|
61b79d5b24 | ||
|
|
5a9cffc6fc | ||
|
|
08447ec4cf | ||
|
|
d3a5f36555 | ||
|
|
c9f39e7b1a | ||
|
|
1060aa84ba | ||
|
|
b6f68ddde3 | ||
|
|
12dd96e34c | ||
|
|
52ba415429 | ||
|
|
601c984f3f | ||
|
|
9d90304d9a | ||
|
|
788493c0a6 | ||
|
|
e1342860c4 | ||
|
|
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 | ||
|
|
182f593d86 | ||
|
|
7f4548e5c7 | ||
|
|
5e4f207844 | ||
|
|
1f29e624e7 | ||
|
|
658e8a7bd6 | ||
|
|
08c3b04511 | ||
|
|
7779eb880d | ||
|
|
21b9ca8ea5 | ||
|
|
db7e3794d9 | ||
|
|
df1d78897a | ||
|
|
e1eb2da6f9 | ||
|
|
a92d1d82c1 | ||
|
|
2dcfc41365 |
No files matched your search
@@ -1,5 +1,6 @@
|
|||||||
/db/*.sqlite3
|
/db/*.sqlite3
|
||||||
|
|
||||||
|
**/node_modules
|
||||||
/node_modules
|
/node_modules
|
||||||
/coverage
|
/coverage
|
||||||
/doc
|
/doc
|
||||||
@@ -27,3 +28,4 @@
|
|||||||
/attachments
|
/attachments
|
||||||
/docuseal
|
/docuseal
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.env
|
||||||
@@ -127,6 +127,7 @@ jobs:
|
|||||||
bundle config path vendor/bundle
|
bundle config path vendor/bundle
|
||||||
bundle install --jobs 4 --retry 4
|
bundle install --jobs 4 --retry 4
|
||||||
yarn install
|
yarn install
|
||||||
|
sudo apt-get update
|
||||||
sudo apt-get install libvips
|
sudo apt-get install libvips
|
||||||
- name: Run
|
- name: Run
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -35,3 +35,4 @@ yarn-debug.log*
|
|||||||
/coverage
|
/coverage
|
||||||
/attachments
|
/attachments
|
||||||
/docuseal
|
/docuseal
|
||||||
|
/ee
|
||||||
+9
-3
@@ -27,7 +27,7 @@ Lint/MissingSuper:
|
|||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Metrics/MethodLength:
|
Metrics/MethodLength:
|
||||||
Max: 20
|
Max: 25
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'db/migrate/**'
|
- 'db/migrate/**'
|
||||||
|
|
||||||
@@ -53,13 +53,19 @@ RSpec/NestedGroups:
|
|||||||
Max: 6
|
Max: 6
|
||||||
|
|
||||||
RSpec/MultipleExpectations:
|
RSpec/MultipleExpectations:
|
||||||
Max: 7
|
Max: 20
|
||||||
|
|
||||||
RSpec/ExampleLength:
|
RSpec/ExampleLength:
|
||||||
Max: 15
|
Max: 40
|
||||||
|
|
||||||
|
RSpec/MultipleMemoizedHelpers:
|
||||||
|
Max: 6
|
||||||
|
|
||||||
Rails/I18nLocaleTexts:
|
Rails/I18nLocaleTexts:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/SkipsModelValidations:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
Rails/ApplicationController:
|
Rails/ApplicationController:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
+10
-2
@@ -31,13 +31,21 @@ ENV BUNDLE_WITHOUT="development:test"
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler vips-heif libc6-compat ttf-freefont
|
RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler vips-heif libc6-compat ttf-freefont ttf-liberation && cp /usr/share/fonts/liberation/LiberationSans-Regular.ttf /LiberationSans-Regular.ttf && apk del ttf-liberation
|
||||||
|
|
||||||
COPY ./Gemfile ./Gemfile.lock ./
|
COPY ./Gemfile ./Gemfile.lock ./
|
||||||
|
|
||||||
RUN bundle update --bundler && bundle install && rm -rf ~/.bundle
|
RUN bundle update --bundler && bundle install && rm -rf ~/.bundle
|
||||||
|
|
||||||
COPY . ./
|
COPY ./bin ./bin
|
||||||
|
COPY ./app ./app
|
||||||
|
COPY ./config ./config
|
||||||
|
COPY ./db ./db
|
||||||
|
COPY ./log ./log
|
||||||
|
COPY ./lib ./lib
|
||||||
|
COPY ./public ./public
|
||||||
|
COPY ./tmp ./tmp
|
||||||
|
COPY LICENSE README.md Rakefile config.ru ./
|
||||||
|
|
||||||
COPY --from=webpack /app/public/packs ./public/packs
|
COPY --from=webpack /app/public/packs ./public/packs
|
||||||
|
|
||||||
|
|||||||
@@ -13,19 +13,23 @@ gem 'faraday'
|
|||||||
gem 'google-cloud-storage', require: false
|
gem 'google-cloud-storage', require: false
|
||||||
gem 'hexapdf'
|
gem 'hexapdf'
|
||||||
gem 'image_processing'
|
gem 'image_processing'
|
||||||
|
gem 'jwt'
|
||||||
gem 'lograge'
|
gem 'lograge'
|
||||||
gem 'mysql2', require: false
|
gem 'mysql2', require: false
|
||||||
gem 'oj'
|
gem 'oj'
|
||||||
gem 'omniauth-google-oauth2'
|
gem 'omniauth-google-oauth2'
|
||||||
gem 'omniauth-rails_csrf_protection'
|
gem 'omniauth-rails_csrf_protection'
|
||||||
gem 'pagy'
|
gem 'pagy'
|
||||||
|
gem 'pdf-reader'
|
||||||
gem 'pg', require: false
|
gem 'pg', require: false
|
||||||
gem 'premailer-rails'
|
gem 'premailer-rails'
|
||||||
gem 'puma'
|
gem 'puma'
|
||||||
gem 'rails'
|
gem 'rails'
|
||||||
|
gem 'rails_autolink'
|
||||||
gem 'rails-i18n'
|
gem 'rails-i18n'
|
||||||
gem 'rollbar', require: ENV.key?('ROLLBAR_ACCESS_TOKEN')
|
gem 'rollbar', require: ENV.key?('ROLLBAR_ACCESS_TOKEN')
|
||||||
gem 'ruby-vips'
|
gem 'ruby-vips'
|
||||||
|
gem 'rubyXL'
|
||||||
gem 'shakapacker'
|
gem 'shakapacker'
|
||||||
gem 'sidekiq', require: ENV.key?('REDIS_URL')
|
gem 'sidekiq', require: ENV.key?('REDIS_URL')
|
||||||
gem 'sqlite3', require: false
|
gem 'sqlite3', require: false
|
||||||
|
|||||||
+133
-102
@@ -1,92 +1,94 @@
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (7.0.5)
|
Ascii85 (1.1.0)
|
||||||
actionpack (= 7.0.5)
|
actioncable (7.0.7)
|
||||||
activesupport (= 7.0.5)
|
actionpack (= 7.0.7)
|
||||||
|
activesupport (= 7.0.7)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailbox (7.0.5)
|
actionmailbox (7.0.7)
|
||||||
actionpack (= 7.0.5)
|
actionpack (= 7.0.7)
|
||||||
activejob (= 7.0.5)
|
activejob (= 7.0.7)
|
||||||
activerecord (= 7.0.5)
|
activerecord (= 7.0.7)
|
||||||
activestorage (= 7.0.5)
|
activestorage (= 7.0.7)
|
||||||
activesupport (= 7.0.5)
|
activesupport (= 7.0.7)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
actionmailer (7.0.5)
|
actionmailer (7.0.7)
|
||||||
actionpack (= 7.0.5)
|
actionpack (= 7.0.7)
|
||||||
actionview (= 7.0.5)
|
actionview (= 7.0.7)
|
||||||
activejob (= 7.0.5)
|
activejob (= 7.0.7)
|
||||||
activesupport (= 7.0.5)
|
activesupport (= 7.0.7)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
actionpack (7.0.5)
|
actionpack (7.0.7)
|
||||||
actionview (= 7.0.5)
|
actionview (= 7.0.7)
|
||||||
activesupport (= 7.0.5)
|
activesupport (= 7.0.7)
|
||||||
rack (~> 2.0, >= 2.2.4)
|
rack (~> 2.0, >= 2.2.4)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||||
actiontext (7.0.5)
|
actiontext (7.0.7)
|
||||||
actionpack (= 7.0.5)
|
actionpack (= 7.0.7)
|
||||||
activerecord (= 7.0.5)
|
activerecord (= 7.0.7)
|
||||||
activestorage (= 7.0.5)
|
activestorage (= 7.0.7)
|
||||||
activesupport (= 7.0.5)
|
activesupport (= 7.0.7)
|
||||||
globalid (>= 0.6.0)
|
globalid (>= 0.6.0)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (7.0.5)
|
actionview (7.0.7)
|
||||||
activesupport (= 7.0.5)
|
activesupport (= 7.0.7)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||||
activejob (7.0.5)
|
activejob (7.0.7)
|
||||||
activesupport (= 7.0.5)
|
activesupport (= 7.0.7)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (7.0.5)
|
activemodel (7.0.7)
|
||||||
activesupport (= 7.0.5)
|
activesupport (= 7.0.7)
|
||||||
activerecord (7.0.5)
|
activerecord (7.0.7)
|
||||||
activemodel (= 7.0.5)
|
activemodel (= 7.0.7)
|
||||||
activesupport (= 7.0.5)
|
activesupport (= 7.0.7)
|
||||||
activestorage (7.0.5)
|
activestorage (7.0.7)
|
||||||
actionpack (= 7.0.5)
|
actionpack (= 7.0.7)
|
||||||
activejob (= 7.0.5)
|
activejob (= 7.0.7)
|
||||||
activerecord (= 7.0.5)
|
activerecord (= 7.0.7)
|
||||||
activesupport (= 7.0.5)
|
activesupport (= 7.0.7)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
mini_mime (>= 1.1.0)
|
mini_mime (>= 1.1.0)
|
||||||
activesupport (7.0.5)
|
activesupport (7.0.7)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
tzinfo (~> 2.0)
|
tzinfo (~> 2.0)
|
||||||
addressable (2.8.4)
|
addressable (2.8.5)
|
||||||
public_suffix (>= 2.0.2, < 6.0)
|
public_suffix (>= 2.0.2, < 6.0)
|
||||||
|
afm (0.2.2)
|
||||||
annotate (3.2.0)
|
annotate (3.2.0)
|
||||||
activerecord (>= 3.2, < 8.0)
|
activerecord (>= 3.2, < 8.0)
|
||||||
rake (>= 10.4, < 14.0)
|
rake (>= 10.4, < 14.0)
|
||||||
ast (2.4.2)
|
ast (2.4.2)
|
||||||
aws-eventstream (1.2.0)
|
aws-eventstream (1.2.0)
|
||||||
aws-partitions (1.781.0)
|
aws-partitions (1.807.0)
|
||||||
aws-sdk-core (3.175.0)
|
aws-sdk-core (3.180.3)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
aws-partitions (~> 1, >= 1.651.0)
|
aws-partitions (~> 1, >= 1.651.0)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
aws-sdk-kms (1.67.0)
|
aws-sdk-kms (1.71.0)
|
||||||
aws-sdk-core (~> 3, >= 3.174.0)
|
aws-sdk-core (~> 3, >= 3.177.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.126.0)
|
aws-sdk-s3 (1.132.1)
|
||||||
aws-sdk-core (~> 3, >= 3.174.0)
|
aws-sdk-core (~> 3, >= 3.179.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.4)
|
aws-sigv4 (~> 1.6)
|
||||||
aws-sigv4 (1.5.2)
|
aws-sigv4 (1.6.0)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
azure-storage-blob (2.0.3)
|
azure-storage-blob (2.0.3)
|
||||||
azure-storage-common (~> 2.0)
|
azure-storage-common (~> 2.0)
|
||||||
@@ -96,8 +98,9 @@ GEM
|
|||||||
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
|
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
|
||||||
net-http-persistent (~> 4.0)
|
net-http-persistent (~> 4.0)
|
||||||
nokogiri (~> 1, >= 1.10.8)
|
nokogiri (~> 1, >= 1.10.8)
|
||||||
|
base64 (0.1.1)
|
||||||
bcrypt (3.1.19)
|
bcrypt (3.1.19)
|
||||||
better_html (2.0.1)
|
better_html (2.0.2)
|
||||||
actionview (>= 6.0)
|
actionview (>= 6.0)
|
||||||
activesupport (>= 6.0)
|
activesupport (>= 6.0)
|
||||||
ast (~> 2.0)
|
ast (~> 2.0)
|
||||||
@@ -105,7 +108,7 @@ GEM
|
|||||||
parser (>= 2.4)
|
parser (>= 2.4)
|
||||||
smart_properties
|
smart_properties
|
||||||
bindex (0.8.1)
|
bindex (0.8.1)
|
||||||
bootsnap (1.15.0)
|
bootsnap (1.16.0)
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
builder (3.2.4)
|
builder (3.2.4)
|
||||||
bullet (7.0.7)
|
bullet (7.0.7)
|
||||||
@@ -144,7 +147,7 @@ GEM
|
|||||||
responders
|
responders
|
||||||
warden (~> 1.2.3)
|
warden (~> 1.2.3)
|
||||||
diff-lcs (1.5.0)
|
diff-lcs (1.5.0)
|
||||||
digest-crc (0.6.4)
|
digest-crc (0.6.5)
|
||||||
rake (>= 12.0.0, < 14.0.0)
|
rake (>= 12.0.0, < 14.0.0)
|
||||||
docile (1.4.0)
|
docile (1.4.0)
|
||||||
dotenv (2.8.1)
|
dotenv (2.8.1)
|
||||||
@@ -161,7 +164,7 @@ GEM
|
|||||||
factory_bot_rails (6.2.0)
|
factory_bot_rails (6.2.0)
|
||||||
factory_bot (~> 6.2.0)
|
factory_bot (~> 6.2.0)
|
||||||
railties (>= 5.0.0)
|
railties (>= 5.0.0)
|
||||||
faker (3.2.0)
|
faker (3.2.1)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
faraday (1.10.3)
|
faraday (1.10.3)
|
||||||
faraday-em_http (~> 1.0)
|
faraday-em_http (~> 1.0)
|
||||||
@@ -194,10 +197,10 @@ GEM
|
|||||||
webrick (~> 1.7)
|
webrick (~> 1.7)
|
||||||
websocket-driver (>= 0.6, < 0.8)
|
websocket-driver (>= 0.6, < 0.8)
|
||||||
ffi (1.15.5)
|
ffi (1.15.5)
|
||||||
geom2d (0.3.1)
|
geom2d (0.4.1)
|
||||||
globalid (1.1.0)
|
globalid (1.1.0)
|
||||||
activesupport (>= 5.0)
|
activesupport (>= 5.0)
|
||||||
google-apis-core (0.11.0)
|
google-apis-core (0.11.1)
|
||||||
addressable (~> 2.5, >= 2.5.1)
|
addressable (~> 2.5, >= 2.5.1)
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
httpclient (>= 2.8.1, < 3.a)
|
httpclient (>= 2.8.1, < 3.a)
|
||||||
@@ -224,7 +227,7 @@ GEM
|
|||||||
google-cloud-core (~> 1.6)
|
google-cloud-core (~> 1.6)
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
mini_mime (~> 1.0)
|
mini_mime (~> 1.0)
|
||||||
googleauth (1.6.0)
|
googleauth (1.7.0)
|
||||||
faraday (>= 0.17.3, < 3.a)
|
faraday (>= 0.17.3, < 3.a)
|
||||||
jwt (>= 1.4, < 3.0)
|
jwt (>= 1.4, < 3.0)
|
||||||
memoist (~> 0.16)
|
memoist (~> 0.16)
|
||||||
@@ -232,10 +235,11 @@ GEM
|
|||||||
os (>= 0.9, < 2.0)
|
os (>= 0.9, < 2.0)
|
||||||
signet (>= 0.16, < 2.a)
|
signet (>= 0.16, < 2.a)
|
||||||
hashdiff (1.0.1)
|
hashdiff (1.0.1)
|
||||||
|
hashery (2.1.2)
|
||||||
hashie (5.0.0)
|
hashie (5.0.0)
|
||||||
hexapdf (0.32.2)
|
hexapdf (0.33.0)
|
||||||
cmdparse (~> 3.0, >= 3.0.3)
|
cmdparse (~> 3.0, >= 3.0.3)
|
||||||
geom2d (~> 0.3)
|
geom2d (~> 0.4, >= 0.4.1)
|
||||||
openssl (>= 2.2.1)
|
openssl (>= 2.2.1)
|
||||||
htmlentities (4.3.4)
|
htmlentities (4.3.4)
|
||||||
httpclient (2.8.3)
|
httpclient (2.8.3)
|
||||||
@@ -245,8 +249,8 @@ GEM
|
|||||||
mini_magick (>= 4.9.5, < 5)
|
mini_magick (>= 4.9.5, < 5)
|
||||||
ruby-vips (>= 2.0.17, < 3)
|
ruby-vips (>= 2.0.17, < 3)
|
||||||
io-console (0.6.0)
|
io-console (0.6.0)
|
||||||
irb (1.7.0)
|
irb (1.7.4)
|
||||||
reline (>= 0.3.0)
|
reline (>= 0.3.6)
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.6.3)
|
json (2.6.3)
|
||||||
jwt (2.7.1)
|
jwt (2.7.1)
|
||||||
@@ -260,7 +264,7 @@ GEM
|
|||||||
letter_opener (~> 1.7)
|
letter_opener (~> 1.7)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
rexml
|
rexml
|
||||||
lograge (0.12.0)
|
lograge (0.13.0)
|
||||||
actionpack (>= 4)
|
actionpack (>= 4)
|
||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
railties (>= 4)
|
railties (>= 4)
|
||||||
@@ -278,17 +282,17 @@ GEM
|
|||||||
memoist (0.16.2)
|
memoist (0.16.2)
|
||||||
method_source (1.0.0)
|
method_source (1.0.0)
|
||||||
mini_magick (4.12.0)
|
mini_magick (4.12.0)
|
||||||
mini_mime (1.1.2)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.2)
|
mini_portile2 (2.8.4)
|
||||||
minitest (5.18.1)
|
minitest (5.19.0)
|
||||||
msgpack (1.7.1)
|
msgpack (1.7.2)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multi_xml (0.6.0)
|
multi_xml (0.6.0)
|
||||||
multipart-post (2.3.0)
|
multipart-post (2.3.0)
|
||||||
mysql2 (0.5.5)
|
mysql2 (0.5.5)
|
||||||
net-http-persistent (4.0.2)
|
net-http-persistent (4.0.2)
|
||||||
connection_pool (~> 2.2)
|
connection_pool (~> 2.2)
|
||||||
net-imap (0.3.6)
|
net-imap (0.3.7)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
net-pop (0.1.2)
|
net-pop (0.1.2)
|
||||||
@@ -298,7 +302,10 @@ GEM
|
|||||||
net-smtp (0.3.3)
|
net-smtp (0.3.3)
|
||||||
net-protocol
|
net-protocol
|
||||||
nio4r (2.5.9)
|
nio4r (2.5.9)
|
||||||
nokogiri (1.15.2-arm64-darwin)
|
nokogiri (1.15.4)
|
||||||
|
mini_portile2 (~> 2.8.2)
|
||||||
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.15.4-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
oauth2 (2.0.9)
|
oauth2 (2.0.9)
|
||||||
faraday (>= 0.17.3, < 3.0)
|
faraday (>= 0.17.3, < 3.0)
|
||||||
@@ -307,7 +314,7 @@ GEM
|
|||||||
rack (>= 1.2, < 4)
|
rack (>= 1.2, < 4)
|
||||||
snaky_hash (~> 2.0)
|
snaky_hash (~> 2.0)
|
||||||
version_gem (~> 1.1)
|
version_gem (~> 1.1)
|
||||||
oj (3.15.0)
|
oj (3.16.0)
|
||||||
omniauth (2.1.1)
|
omniauth (2.1.1)
|
||||||
hashie (>= 3.4.6)
|
hashie (>= 3.4.6)
|
||||||
rack (>= 2.2.3)
|
rack (>= 2.2.3)
|
||||||
@@ -331,6 +338,12 @@ GEM
|
|||||||
parser (3.2.2.3)
|
parser (3.2.2.3)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
|
pdf-reader (2.11.0)
|
||||||
|
Ascii85 (~> 1.0)
|
||||||
|
afm (~> 0.2.1)
|
||||||
|
hashery (~> 2.0)
|
||||||
|
ruby-rc4
|
||||||
|
ttfunk
|
||||||
pg (1.5.3)
|
pg (1.5.3)
|
||||||
premailer (1.21.0)
|
premailer (1.21.0)
|
||||||
addressable
|
addressable
|
||||||
@@ -345,53 +358,58 @@ GEM
|
|||||||
method_source (~> 1.0)
|
method_source (~> 1.0)
|
||||||
pry-rails (0.3.9)
|
pry-rails (0.3.9)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
public_suffix (5.0.1)
|
public_suffix (5.0.3)
|
||||||
puma (6.3.0)
|
puma (6.3.1)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
racc (1.7.1)
|
racc (1.7.1)
|
||||||
rack (2.2.7)
|
rack (2.2.8)
|
||||||
rack-protection (3.0.6)
|
rack-protection (3.1.0)
|
||||||
rack
|
rack (~> 2.2, >= 2.2.4)
|
||||||
rack-proxy (0.7.6)
|
rack-proxy (0.7.6)
|
||||||
rack
|
rack
|
||||||
rack-test (2.1.0)
|
rack-test (2.1.0)
|
||||||
rack (>= 1.3)
|
rack (>= 1.3)
|
||||||
rails (7.0.5)
|
rails (7.0.7)
|
||||||
actioncable (= 7.0.5)
|
actioncable (= 7.0.7)
|
||||||
actionmailbox (= 7.0.5)
|
actionmailbox (= 7.0.7)
|
||||||
actionmailer (= 7.0.5)
|
actionmailer (= 7.0.7)
|
||||||
actionpack (= 7.0.5)
|
actionpack (= 7.0.7)
|
||||||
actiontext (= 7.0.5)
|
actiontext (= 7.0.7)
|
||||||
actionview (= 7.0.5)
|
actionview (= 7.0.7)
|
||||||
activejob (= 7.0.5)
|
activejob (= 7.0.7)
|
||||||
activemodel (= 7.0.5)
|
activemodel (= 7.0.7)
|
||||||
activerecord (= 7.0.5)
|
activerecord (= 7.0.7)
|
||||||
activestorage (= 7.0.5)
|
activestorage (= 7.0.7)
|
||||||
activesupport (= 7.0.5)
|
activesupport (= 7.0.7)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 7.0.5)
|
railties (= 7.0.7)
|
||||||
rails-dom-testing (2.0.3)
|
rails-dom-testing (2.2.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 5.0.0)
|
||||||
|
minitest
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
rails-html-sanitizer (1.6.0)
|
rails-html-sanitizer (1.6.0)
|
||||||
loofah (~> 2.21)
|
loofah (~> 2.21)
|
||||||
nokogiri (~> 1.14)
|
nokogiri (~> 1.14)
|
||||||
rails-i18n (7.0.7)
|
rails-i18n (7.0.8)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
railties (>= 6.0.0, < 8)
|
railties (>= 6.0.0, < 8)
|
||||||
railties (7.0.5)
|
rails_autolink (1.1.8)
|
||||||
actionpack (= 7.0.5)
|
actionview (> 3.1)
|
||||||
activesupport (= 7.0.5)
|
activesupport (> 3.1)
|
||||||
|
railties (> 3.1)
|
||||||
|
railties (7.0.7)
|
||||||
|
actionpack (= 7.0.7)
|
||||||
|
activesupport (= 7.0.7)
|
||||||
method_source
|
method_source
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
zeitwerk (~> 2.5)
|
zeitwerk (~> 2.5)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.0.6)
|
rake (13.0.6)
|
||||||
redis-client (0.14.1)
|
redis-client (0.16.0)
|
||||||
connection_pool
|
connection_pool
|
||||||
regexp_parser (2.8.1)
|
regexp_parser (2.8.1)
|
||||||
reline (0.3.5)
|
reline (0.3.8)
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
representable (3.2.0)
|
representable (3.2.0)
|
||||||
declarative (< 0.1.0)
|
declarative (< 0.1.0)
|
||||||
@@ -403,14 +421,14 @@ GEM
|
|||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
retriable (3.1.2)
|
retriable (3.1.2)
|
||||||
rexml (3.2.5)
|
rexml (3.2.6)
|
||||||
rollbar (3.4.0)
|
rollbar (3.4.0)
|
||||||
rspec-core (3.12.2)
|
rspec-core (3.12.2)
|
||||||
rspec-support (~> 3.12.0)
|
rspec-support (~> 3.12.0)
|
||||||
rspec-expectations (3.12.3)
|
rspec-expectations (3.12.3)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.12.0)
|
rspec-support (~> 3.12.0)
|
||||||
rspec-mocks (3.12.5)
|
rspec-mocks (3.12.6)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.12.0)
|
rspec-support (~> 3.12.0)
|
||||||
rspec-rails (6.0.3)
|
rspec-rails (6.0.3)
|
||||||
@@ -421,8 +439,9 @@ GEM
|
|||||||
rspec-expectations (~> 3.12)
|
rspec-expectations (~> 3.12)
|
||||||
rspec-mocks (~> 3.12)
|
rspec-mocks (~> 3.12)
|
||||||
rspec-support (~> 3.12)
|
rspec-support (~> 3.12)
|
||||||
rspec-support (3.12.0)
|
rspec-support (3.12.1)
|
||||||
rubocop (1.53.0)
|
rubocop (1.56.1)
|
||||||
|
base64 (~> 0.1.1)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (>= 3.17.0)
|
language_server-protocol (>= 3.17.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
@@ -430,7 +449,7 @@ GEM
|
|||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 1.8, < 3.0)
|
regexp_parser (>= 1.8, < 3.0)
|
||||||
rexml (>= 3.2.5, < 4.0)
|
rexml (>= 3.2.5, < 4.0)
|
||||||
rubocop-ast (>= 1.28.0, < 2.0)
|
rubocop-ast (>= 1.28.1, < 2.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 2.4.0, < 3.0)
|
unicode-display_width (>= 2.4.0, < 3.0)
|
||||||
rubocop-ast (1.29.0)
|
rubocop-ast (1.29.0)
|
||||||
@@ -439,23 +458,28 @@ GEM
|
|||||||
rubocop (~> 1.41)
|
rubocop (~> 1.41)
|
||||||
rubocop-factory_bot (2.23.1)
|
rubocop-factory_bot (2.23.1)
|
||||||
rubocop (~> 1.33)
|
rubocop (~> 1.33)
|
||||||
rubocop-performance (1.18.0)
|
rubocop-performance (1.19.0)
|
||||||
rubocop (>= 1.7.0, < 2.0)
|
rubocop (>= 1.7.0, < 2.0)
|
||||||
rubocop-ast (>= 0.4.0)
|
rubocop-ast (>= 0.4.0)
|
||||||
rubocop-rails (2.20.2)
|
rubocop-rails (2.20.2)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.33.0, < 2.0)
|
rubocop (>= 1.33.0, < 2.0)
|
||||||
rubocop-rspec (2.22.0)
|
rubocop-rspec (2.23.2)
|
||||||
rubocop (~> 1.33)
|
rubocop (~> 1.33)
|
||||||
rubocop-capybara (~> 2.17)
|
rubocop-capybara (~> 2.17)
|
||||||
rubocop-factory_bot (~> 2.22)
|
rubocop-factory_bot (~> 2.22)
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
|
ruby-rc4 (0.1.5)
|
||||||
ruby-vips (2.1.4)
|
ruby-vips (2.1.4)
|
||||||
ffi (~> 1.12)
|
ffi (~> 1.12)
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
|
rubyXL (3.4.25)
|
||||||
|
nokogiri (>= 1.10.8)
|
||||||
|
rubyzip (>= 1.3.0)
|
||||||
|
rubyzip (2.3.2)
|
||||||
semantic_range (3.0.0)
|
semantic_range (3.0.0)
|
||||||
shakapacker (7.0.0)
|
shakapacker (7.0.3)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
rack-proxy (>= 0.6.1)
|
rack-proxy (>= 0.6.1)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
@@ -480,13 +504,15 @@ GEM
|
|||||||
snaky_hash (2.0.1)
|
snaky_hash (2.0.1)
|
||||||
hashie
|
hashie
|
||||||
version_gem (~> 1.1, >= 1.1.1)
|
version_gem (~> 1.1, >= 1.1.1)
|
||||||
sqlite3 (1.5.4)
|
sqlite3 (1.6.3)
|
||||||
mini_portile2 (~> 2.8.0)
|
mini_portile2 (~> 2.8.0)
|
||||||
|
sqlite3 (1.6.3-arm64-darwin)
|
||||||
strip_attributes (1.13.0)
|
strip_attributes (1.13.0)
|
||||||
activemodel (>= 3.0, < 8.0)
|
activemodel (>= 3.0, < 8.0)
|
||||||
thor (1.2.2)
|
thor (1.2.2)
|
||||||
timeout (0.4.0)
|
timeout (0.4.0)
|
||||||
trailblazer-option (0.1.2)
|
trailblazer-option (0.1.2)
|
||||||
|
ttfunk (1.7.0)
|
||||||
turbo-rails (1.4.0)
|
turbo-rails (1.4.0)
|
||||||
actionpack (>= 6.0.0)
|
actionpack (>= 6.0.0)
|
||||||
activejob (>= 6.0.0)
|
activejob (>= 6.0.0)
|
||||||
@@ -511,15 +537,16 @@ GEM
|
|||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff (>= 0.4.0, < 2.0.0)
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
webrick (1.8.1)
|
webrick (1.8.1)
|
||||||
websocket-driver (0.7.5)
|
websocket-driver (0.7.6)
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
xpath (3.2.0)
|
xpath (3.2.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
zeitwerk (2.6.8)
|
zeitwerk (2.6.11)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
arm64-darwin-22
|
arm64-darwin-22
|
||||||
|
ruby
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
annotate
|
annotate
|
||||||
@@ -540,6 +567,7 @@ DEPENDENCIES
|
|||||||
google-cloud-storage
|
google-cloud-storage
|
||||||
hexapdf
|
hexapdf
|
||||||
image_processing
|
image_processing
|
||||||
|
jwt
|
||||||
letter_opener_web
|
letter_opener_web
|
||||||
lograge
|
lograge
|
||||||
mysql2
|
mysql2
|
||||||
@@ -547,12 +575,14 @@ DEPENDENCIES
|
|||||||
omniauth-google-oauth2
|
omniauth-google-oauth2
|
||||||
omniauth-rails_csrf_protection
|
omniauth-rails_csrf_protection
|
||||||
pagy
|
pagy
|
||||||
|
pdf-reader
|
||||||
pg
|
pg
|
||||||
premailer-rails
|
premailer-rails
|
||||||
pry-rails
|
pry-rails
|
||||||
puma
|
puma
|
||||||
rails
|
rails
|
||||||
rails-i18n
|
rails-i18n
|
||||||
|
rails_autolink
|
||||||
rollbar
|
rollbar
|
||||||
rspec-rails
|
rspec-rails
|
||||||
rubocop
|
rubocop
|
||||||
@@ -560,6 +590,7 @@ DEPENDENCIES
|
|||||||
rubocop-rails
|
rubocop-rails
|
||||||
rubocop-rspec
|
rubocop-rspec
|
||||||
ruby-vips
|
ruby-vips
|
||||||
|
rubyXL
|
||||||
shakapacker
|
shakapacker
|
||||||
sidekiq
|
sidekiq
|
||||||
simplecov
|
simplecov
|
||||||
|
|||||||
@@ -42,15 +42,9 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
- [x] PDF signature verification
|
- [x] PDF signature verification
|
||||||
- [x] Users management
|
- [x] Users management
|
||||||
- [x] Mobile-optimized
|
- [x] Mobile-optimized
|
||||||
|
- [x] API and Webhooks for integrations
|
||||||
- [x] Easy to deploy in minutes
|
- [x] Easy to deploy in minutes
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
[](https://cal.com/docuseal)
|
|
||||||
|
|
||||||
## Deploy
|
## Deploy
|
||||||
|
|
||||||
|Heroku|Railway|
|
|Heroku|Railway|
|
||||||
@@ -80,6 +74,14 @@ Run the app under a custom domain over https using docker compose (make sure you
|
|||||||
HOST=your-domain-name.com docker-compose up
|
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, 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
|
## 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
|
def create
|
||||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
blob =
|
attachment = Submitters.create_attachment!(submitter, params)
|
||||||
if (file = params[:file])
|
|
||||||
ActiveStorage::Blob.create_and_upload!(io: file.open,
|
|
||||||
filename: file.original_filename,
|
|
||||||
content_type: file.content_type)
|
|
||||||
else
|
|
||||||
ActiveStorage::Blob.find_signed(params[:blob_signed_id])
|
|
||||||
end
|
|
||||||
|
|
||||||
attachment = ActiveStorage::Attachment.create!(
|
|
||||||
blob:,
|
|
||||||
name: params[:name],
|
|
||||||
record: submitter
|
|
||||||
)
|
|
||||||
|
|
||||||
render json: attachment.as_json(only: %i[uuid], methods: %i[url filename content_type])
|
render json: attachment.as_json(only: %i[uuid], methods: %i[url filename content_type])
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# 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] || params[:email]).present?
|
||||||
|
Submissions.create_from_emails(template:,
|
||||||
|
user: current_user,
|
||||||
|
source: :api,
|
||||||
|
mark_as_sent: 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,
|
||||||
|
mark_as_sent: params[:send_email] != 'false',
|
||||||
|
submissions_attrs:)
|
||||||
|
end
|
||||||
|
|
||||||
|
submitters = submissions.flat_map(&:submitters)
|
||||||
|
|
||||||
|
Submitters.send_signature_requests(submitters, send_email: params[:send_email] != 'false')
|
||||||
|
|
||||||
|
render json: submitters
|
||||||
|
rescue UnknownFieldName, UnknownSubmitterName => e
|
||||||
|
render json: { error: e.message }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def submissions_params
|
||||||
|
params.permit(submission: [{ submitters: [[:uuid, :name, :email, :role, :phone, { 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[:role] || 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
|
module Api
|
||||||
class TemplatesController < ApiBaseController
|
class TemplatesController < ApiBaseController
|
||||||
def update
|
before_action :load_template, only: %i[show update]
|
||||||
@template = current_account.templates.find(params[:id])
|
|
||||||
|
|
||||||
|
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)
|
@template.update!(template_params)
|
||||||
|
|
||||||
render :ok
|
render :ok
|
||||||
@@ -19,5 +28,9 @@ module Api
|
|||||||
fields: [[:uuid, :submitter_uuid, :name, :type, :required,
|
fields: [[:uuid, :submitter_uuid, :name, :type, :required,
|
||||||
{ options: [], areas: [%i[x y w h cell_w attachment_uuid page]] }]])
|
{ options: [], areas: [%i[x y w h cell_w attachment_uuid page]] }]])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def load_template
|
||||||
|
@template = current_account.templates.find(params[:id])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -7,12 +7,7 @@ module Api
|
|||||||
|
|
||||||
@template = current_account.templates.find(params[:template_id])
|
@template = current_account.templates.find(params[:template_id])
|
||||||
|
|
||||||
documents =
|
documents = Templates::CreateAttachments.call(@template, params)
|
||||||
find_or_create_blobs.map do |blob|
|
|
||||||
document = @template.documents.create!(blob:)
|
|
||||||
|
|
||||||
Templates::ProcessDocument.call(document)
|
|
||||||
end
|
|
||||||
|
|
||||||
schema = documents.map do |doc|
|
schema = documents.map do |doc|
|
||||||
{ attachment_uuid: doc.uuid, name: doc.filename.base }
|
{ attachment_uuid: doc.uuid, name: doc.filename.base }
|
||||||
@@ -27,19 +22,5 @@ module Api
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def find_or_create_blobs
|
|
||||||
blobs = params[:blobs]&.map do |attrs|
|
|
||||||
ActiveStorage::Blob.find_signed(attrs[:signed_id])
|
|
||||||
end
|
|
||||||
|
|
||||||
blobs || params[:files].map do |file|
|
|
||||||
ActiveStorage::Blob.create_and_upload!(io: file.open,
|
|
||||||
filename: file.original_filename,
|
|
||||||
content_type: file.content_type)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
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?
|
redirect_to setup_index_path unless User.exists?
|
||||||
end
|
end
|
||||||
|
|
||||||
def button_title(title: 'Submit', disabled_with: 'Submitting', icon: nil)
|
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: })
|
render_to_string(partial: 'shared/button_title', locals: { title:, disabled_with:, icon:, icon_disabled: })
|
||||||
end
|
end
|
||||||
|
|
||||||
def svg_icon(icon_name, class: '')
|
def svg_icon(icon_name, class: '')
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ConsoleRedirectController < ApplicationController
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
|
||||||
|
def index
|
||||||
|
return redirect_to(new_user_session_path({ redir: params[:redir] }.compact)) if current_user.blank?
|
||||||
|
|
||||||
|
auth = JsonWebToken.encode(uuid: current_user.uuid,
|
||||||
|
scope: :console,
|
||||||
|
exp: 1.minute.from_now.to_i)
|
||||||
|
|
||||||
|
path = Addressable::URI.parse(params[:redir]).path if params[:redir].to_s.starts_with?(Docuseal::CONSOLE_URL)
|
||||||
|
|
||||||
|
redirect_to("#{Docuseal::CONSOLE_URL}#{path}?#{{ auth: }.to_query}", allow_other_host: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -6,11 +6,17 @@ class EmailSettingsController < ApplicationController
|
|||||||
def index; end
|
def index; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
if @encrypted_config.update(storage_configs)
|
if @encrypted_config.update(email_configs)
|
||||||
|
SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email']).deliver_now!
|
||||||
|
|
||||||
redirect_to settings_email_index_path, notice: 'Changes have been saved'
|
redirect_to settings_email_index_path, notice: 'Changes have been saved'
|
||||||
else
|
else
|
||||||
render :index, status: :unprocessable_entity
|
render :index, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
rescue StandardError => e
|
||||||
|
flash[:alert] = e.message
|
||||||
|
|
||||||
|
render :index, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -20,7 +26,7 @@ class EmailSettingsController < ApplicationController
|
|||||||
EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::EMAIL_SMTP_KEY)
|
EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::EMAIL_SMTP_KEY)
|
||||||
end
|
end
|
||||||
|
|
||||||
def storage_configs
|
def email_configs
|
||||||
params.require(:encrypted_config).permit(value: {}).tap do |e|
|
params.require(:encrypted_config).permit(value: {}).tap do |e|
|
||||||
e[:value].compact_blank!
|
e[:value].compact_blank!
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,19 +1,111 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class EsignSettingsController < ApplicationController
|
class EsignSettingsController < ApplicationController
|
||||||
|
DEFAULT_CERT_NAME = 'DocuSeal Self-Host Autogenerated'
|
||||||
|
|
||||||
|
CertFormRecord = Struct.new(:name, :file, :password, keyword_init: true) do
|
||||||
|
include ActiveModel::Validations
|
||||||
|
|
||||||
|
def to_key
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
cert_data = EncryptedConfig.find_by(account: current_account,
|
||||||
|
key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
|
||||||
|
|
||||||
|
default_pkcs = GenerateCertificate.load_pkcs(cert_data) if cert_data['cert'].present?
|
||||||
|
|
||||||
|
custom_pkcs_list = (cert_data['custom'] || []).map do |e|
|
||||||
|
{ 'pkcs' => OpenSSL::PKCS12.new(Base64.urlsafe_decode64(e['data']), e['password'].to_s),
|
||||||
|
'name' => e['name'],
|
||||||
|
'status' => e['status'] }
|
||||||
|
end
|
||||||
|
|
||||||
|
@pkcs_list = [
|
||||||
|
if default_pkcs
|
||||||
|
{
|
||||||
|
'pkcs' => default_pkcs,
|
||||||
|
'name' => DEFAULT_CERT_NAME,
|
||||||
|
'status' => custom_pkcs_list.any? { |e| e['status'] == 'default' } ? 'validate' : 'default'
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
*custom_pkcs_list
|
||||||
|
].compact.reverse
|
||||||
|
end
|
||||||
|
|
||||||
|
def new
|
||||||
|
@cert_record = CertFormRecord.new
|
||||||
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
pdfs =
|
@cert_record = CertFormRecord.new(**cert_params)
|
||||||
params[:files].map do |file|
|
|
||||||
HexaPDF::Document.new(io: file.open)
|
|
||||||
end
|
|
||||||
|
|
||||||
certs = Accounts.load_signing_certs(current_account)
|
cert_configs = EncryptedConfig.find_or_initialize_by(account: current_account,
|
||||||
|
key: EncryptedConfig::ESIGN_CERTS_KEY)
|
||||||
|
|
||||||
trusted_certs = [certs[:cert], certs[:sub_ca], certs[:root_ca]]
|
if (cert_configs.value && cert_configs.value['custom']&.any? { |e| e['name'] == @cert_record.name }) ||
|
||||||
|
@cert_record.name == DEFAULT_CERT_NAME
|
||||||
|
|
||||||
render turbo_stream: turbo_stream.replace('result', partial: 'result',
|
@cert_record.errors.add(:name, 'already exists')
|
||||||
locals: { pdfs:, files: params[:files], trusted_certs: })
|
|
||||||
rescue HexaPDF::MalformedPDFError
|
return render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'),
|
||||||
render turbo_stream: turbo_stream.replace('result', html: helpers.tag.div('Invalid PDF', id: 'result'))
|
status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
|
save_new_cert!(cert_configs, @cert_record)
|
||||||
|
|
||||||
|
redirect_to settings_esign_path, notice: 'Certificate has been successfully added!'
|
||||||
|
rescue OpenSSL::PKCS12::PKCS12Error
|
||||||
|
@cert_record.errors.add(:password, "is invalid. Make sure you're uploading a valid .p12 file")
|
||||||
|
|
||||||
|
render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
cert_configs = EncryptedConfig.find_by(account: current_account, key: EncryptedConfig::ESIGN_CERTS_KEY)
|
||||||
|
|
||||||
|
cert_configs.value['custom'].each { |e| e['status'] = 'validate' }
|
||||||
|
custom_cert_data = cert_configs.value['custom'].find { |e| e['name'] == params[:name] }
|
||||||
|
custom_cert_data['status'] = 'default' if custom_cert_data
|
||||||
|
|
||||||
|
cert_configs.save!
|
||||||
|
|
||||||
|
redirect_to settings_esign_path, notice: 'Default certificate has been selected'
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
cert_configs = EncryptedConfig.find_by(account: current_account, key: EncryptedConfig::ESIGN_CERTS_KEY)
|
||||||
|
|
||||||
|
cert_configs.value['custom'].reject! { |e| e['name'] == params[:name] }
|
||||||
|
|
||||||
|
cert_configs.save!
|
||||||
|
|
||||||
|
redirect_to settings_esign_path, notice: 'Certificate has been removed'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def save_new_cert!(cert_configs, cert_record)
|
||||||
|
pkcs = OpenSSL::PKCS12.new(cert_record.file.read, cert_record.password)
|
||||||
|
|
||||||
|
cert_configs.value ||= {}
|
||||||
|
cert_configs.value['custom'] ||= []
|
||||||
|
cert_configs.value['custom'].each { |e| e['status'] = 'validate' }
|
||||||
|
cert_configs.value['custom'] << {
|
||||||
|
data: Base64.urlsafe_encode64(pkcs.to_der),
|
||||||
|
password: cert_record.password,
|
||||||
|
name: cert_record.name,
|
||||||
|
status: 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
cert_configs.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
def cert_params
|
||||||
|
return {} if params[:esign_settings_controller_cert_form_record].blank?
|
||||||
|
|
||||||
|
params.require(:esign_settings_controller_cert_form_record).permit(:name, :file, :password)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class PersonalizationSettingsController < ApplicationController
|
||||||
|
def show; end
|
||||||
|
|
||||||
|
def create
|
||||||
|
account_config =
|
||||||
|
current_account.account_configs.find_or_initialize_by(key: encrypted_config_params[:key])
|
||||||
|
|
||||||
|
account_config.update!(encrypted_config_params)
|
||||||
|
|
||||||
|
redirect_back(fallback_location: settings_personalization_path, notice: 'Settings have been saved.')
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def encrypted_config_params
|
||||||
|
params.require(:account_config).permit!
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
class RegistrationsController < Devise::RegistrationsController
|
class RegistrationsController < Devise::RegistrationsController
|
||||||
prepend_before_action :require_no_authentication, only: [:show]
|
prepend_before_action :require_no_authentication, only: [:show]
|
||||||
|
prepend_before_action :maybe_redirect_if_signed_in, only: [:show]
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
@@ -13,6 +14,23 @@ class RegistrationsController < Devise::RegistrationsController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def after_sign_up_path_for(...)
|
||||||
|
if params[:redir].present?
|
||||||
|
return console_redirect_index_path(redir: params[:redir]) if params[:redir].starts_with?(Docuseal::CONSOLE_URL)
|
||||||
|
|
||||||
|
return params[:redir]
|
||||||
|
end
|
||||||
|
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
def maybe_redirect_if_signed_in
|
||||||
|
return unless signed_in?
|
||||||
|
return if params[:redir].blank?
|
||||||
|
|
||||||
|
redirect_to after_sign_up_path_for(current_user), allow_other_host: true
|
||||||
|
end
|
||||||
|
|
||||||
def require_no_authentication
|
def require_no_authentication
|
||||||
super
|
super
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,16 @@ class SessionsController < Devise::SessionsController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def after_sign_in_path_for(...)
|
||||||
|
if params[:redir].present?
|
||||||
|
return console_redirect_index_path(redir: params[:redir]) if params[:redir].starts_with?(Docuseal::CONSOLE_URL)
|
||||||
|
|
||||||
|
return params[:redir]
|
||||||
|
end
|
||||||
|
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
def require_no_authentication
|
def require_no_authentication
|
||||||
super
|
super
|
||||||
|
|
||||||
|
|||||||
@@ -16,16 +16,17 @@ class SetupController < ApplicationController
|
|||||||
def create
|
def create
|
||||||
@account = Account.new(account_params)
|
@account = Account.new(account_params)
|
||||||
@account.timezone = Accounts.normalize_timezone(@account.timezone)
|
@account.timezone = Accounts.normalize_timezone(@account.timezone)
|
||||||
|
|
||||||
@user = @account.users.new(user_params)
|
@user = @account.users.new(user_params)
|
||||||
|
@encrypted_config = EncryptedConfig.new(encrypted_config_params)
|
||||||
|
|
||||||
unless URI.parse(encrypted_config_params[:value].to_s).class.in?([URI::HTTP, URI::HTTPS])
|
unless URI.parse(encrypted_config_params[:value].to_s).class.in?([URI::HTTP, URI::HTTPS])
|
||||||
@encrypted_config = EncryptedConfig.new(encrypted_config_params)
|
|
||||||
@encrypted_config.errors.add(:value, 'should be a valid URL')
|
@encrypted_config.errors.add(:value, 'should be a valid URL')
|
||||||
|
|
||||||
return render :index, status: :unprocessable_entity
|
return render :index, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
|
return render :index, status: :unprocessable_entity unless @account.valid?
|
||||||
|
|
||||||
if @user.save
|
if @user.save
|
||||||
encrypted_configs = [
|
encrypted_configs = [
|
||||||
{ key: EncryptedConfig::APP_URL_KEY, value: encrypted_config_params[:value] },
|
{ key: EncryptedConfig::APP_URL_KEY, value: encrypted_config_params[:value] },
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SmsSettingsController < ApplicationController
|
||||||
|
def index; end
|
||||||
|
end
|
||||||
@@ -25,7 +25,7 @@ class StartFormController < ApplicationController
|
|||||||
ua: request.user_agent
|
ua: request.user_agent
|
||||||
)
|
)
|
||||||
|
|
||||||
@submitter.submission ||= Submission.new(template: @template)
|
@submitter.submission ||= Submission.new(template: @template, source: :link)
|
||||||
|
|
||||||
if @submitter.save
|
if @submitter.save
|
||||||
redirect_to submit_form_path(@submitter.slug)
|
redirect_to submit_form_path(@submitter.slug)
|
||||||
@@ -36,7 +36,9 @@ class StartFormController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def completed
|
def completed
|
||||||
@submitter = Submitter.where(submission: @template.submissions).find_by!(email: params[:email])
|
@submitter = Submitter.where(submission: @template.submissions)
|
||||||
|
.where.not(completed_at: nil)
|
||||||
|
.find_by!(email: params[:email])
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class SubmissionsController < ApplicationController
|
|||||||
def show
|
def show
|
||||||
@submission =
|
@submission =
|
||||||
Submission.joins(:template).where(template: { account_id: current_account.id })
|
Submission.joins(:template).where(template: { account_id: current_account.id })
|
||||||
.preload(template: { documents_attachments: { preview_images_attachments: :blob } })
|
.preload(:template, template_schema_documents: [:blob, { preview_images_attachments: :blob }])
|
||||||
.find(params[:id])
|
.find(params[:id])
|
||||||
|
|
||||||
render :show, layout: 'plain'
|
render :show, layout: 'plain'
|
||||||
@@ -17,18 +17,22 @@ class SubmissionsController < ApplicationController
|
|||||||
def create
|
def create
|
||||||
submissions =
|
submissions =
|
||||||
if params[:emails].present?
|
if params[:emails].present?
|
||||||
create_submissions_from_emails
|
Submissions.create_from_emails(template: @template,
|
||||||
|
user: current_user,
|
||||||
|
source: :invite,
|
||||||
|
mark_as_sent: params[:send_email] == '1',
|
||||||
|
emails: params[:emails])
|
||||||
else
|
else
|
||||||
create_submissions_from_submitters
|
Submissions.create_from_submitters(template: @template,
|
||||||
|
user: current_user,
|
||||||
|
source: :invite,
|
||||||
|
mark_as_sent: params[:send_email] == '1',
|
||||||
|
submissions_attrs: submissions_params[:submission].to_h.values)
|
||||||
end
|
end
|
||||||
|
|
||||||
submitters = submissions.flat_map(&:submitters)
|
submitters = submissions.flat_map(&:submitters)
|
||||||
|
|
||||||
if params[:send_email] == '1'
|
Submitters.send_signature_requests(submitters, params)
|
||||||
submitters.each do |submitter|
|
|
||||||
SubmitterMailer.invitation_email(submitter, message: params[:message]).deliver_later!
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
redirect_to template_path(@template),
|
redirect_to template_path(@template),
|
||||||
notice: "#{submitters.size} #{'recipient'.pluralize(submitters.size)} added"
|
notice: "#{submitters.size} #{'recipient'.pluralize(submitters.size)} added"
|
||||||
@@ -45,32 +49,8 @@ class SubmissionsController < ApplicationController
|
|||||||
|
|
||||||
private
|
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
|
def submissions_params
|
||||||
params.permit(submission: { submitters: [%i[uuid email]] })
|
params.permit(submission: { submitters: [%i[uuid email phone name]] })
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_template
|
def load_template
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -7,7 +7,9 @@ class SubmitFormController < ApplicationController
|
|||||||
|
|
||||||
def show
|
def show
|
||||||
@submitter =
|
@submitter =
|
||||||
Submitter.preload(submission: { template: { documents_attachments: { preview_images_attachments: :blob } } })
|
Submitter.preload(submission: [
|
||||||
|
:template, { template_schema_documents: [:blob, { preview_images_attachments: :blob }] }
|
||||||
|
])
|
||||||
.find_by!(slug: params[:slug])
|
.find_by!(slug: params[:slug])
|
||||||
|
|
||||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||||
@@ -17,19 +19,8 @@ class SubmitFormController < ApplicationController
|
|||||||
|
|
||||||
def update
|
def update
|
||||||
submitter = Submitter.find_by!(slug: params[:slug])
|
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!
|
Submitters::SubmitValues.call(submitter, params, request)
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
@@ -37,16 +28,4 @@ class SubmitFormController < ApplicationController
|
|||||||
def completed
|
def completed
|
||||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def normalized_values
|
|
||||||
params.fetch(:values, {}).to_unsafe_h.transform_values do |v|
|
|
||||||
if params[:cast_boolean] == 'true'
|
|
||||||
v == 'true'
|
|
||||||
else
|
|
||||||
v.is_a?(Array) ? v.compact_blank : v
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmittersSendEmailController < ApplicationController
|
||||||
|
def create
|
||||||
|
submitter = Submitter.joins(:template)
|
||||||
|
.where(template: { account_id: current_account.id })
|
||||||
|
.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
|
SubmitterMailer.invitation_email(submitter).deliver_later!
|
||||||
|
|
||||||
|
submitter.sent_at ||= Time.current
|
||||||
|
submitter.save!
|
||||||
|
|
||||||
|
redirect_back(fallback_location: submission_path(submitter.submission), notice: 'Email has been sent')
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -17,7 +17,7 @@ class TemplatesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def edit
|
def edit
|
||||||
@template = current_account.templates.preload(documents_attachments: { preview_images_attachments: :blob })
|
@template = current_account.templates.preload(schema_documents: { preview_images_attachments: :blob })
|
||||||
.find(params[:id])
|
.find(params[:id])
|
||||||
|
|
||||||
render :edit, layout: 'plain'
|
render :edit, layout: 'plain'
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class VerifyPdfSignatureController < ApplicationController
|
||||||
|
def create
|
||||||
|
pdfs =
|
||||||
|
params[:files].map do |file|
|
||||||
|
HexaPDF::Document.new(io: file.open)
|
||||||
|
end
|
||||||
|
|
||||||
|
cert_data = if Docuseal.multitenant?
|
||||||
|
Docuseal::CERTS
|
||||||
|
else
|
||||||
|
EncryptedConfig.find_by(account: current_account, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
|
||||||
|
end
|
||||||
|
|
||||||
|
default_pkcs = GenerateCertificate.load_pkcs(cert_data)
|
||||||
|
|
||||||
|
custom_certs = cert_data.fetch('custom', []).map do |e|
|
||||||
|
OpenSSL::PKCS12.new(Base64.urlsafe_decode64(e['data']), e['password'].to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
trusted_certs = [default_pkcs.certificate,
|
||||||
|
*default_pkcs.ca_certs,
|
||||||
|
*custom_certs.map(&:certificate),
|
||||||
|
*custom_certs.flat_map(&:ca_certs).compact]
|
||||||
|
|
||||||
|
render turbo_stream: turbo_stream.replace('result', partial: 'result',
|
||||||
|
locals: { pdfs:, files: params[:files], trusted_certs: })
|
||||||
|
rescue HexaPDF::MalformedPDFError
|
||||||
|
render turbo_stream: turbo_stream.replace('result', html: helpers.tag.div('Invalid PDF', id: 'result'))
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -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,13 +45,36 @@ window.customElements.define('set-timezone', SetTimezone)
|
|||||||
window.customElements.define('autoresize-textarea', AutoresizeTextarea)
|
window.customElements.define('autoresize-textarea', AutoresizeTextarea)
|
||||||
|
|
||||||
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
|
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
|
||||||
|
document.addEventListener('turbo:submit-end', async (event) => {
|
||||||
|
const resp = event.detail?.formSubmission?.result?.fetchResponse?.response
|
||||||
|
|
||||||
|
if (!resp?.headers?.get('content-disposition')?.includes('attachment')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(await resp.blob())
|
||||||
|
const link = document.createElement('a')
|
||||||
|
|
||||||
|
link.href = url
|
||||||
|
link.setAttribute('download', decodeURIComponent(resp.headers.get('content-disposition').split('"')[1]))
|
||||||
|
|
||||||
|
document.body.appendChild(link)
|
||||||
|
|
||||||
|
link.click()
|
||||||
|
|
||||||
|
document.body.removeChild(link)
|
||||||
|
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
})
|
||||||
|
|
||||||
window.customElements.define('template-builder', class extends HTMLElement {
|
window.customElements.define('template-builder', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.appElem = document.createElement('div')
|
this.appElem = document.createElement('div')
|
||||||
|
this.appElem.classList.add('max-h-screen')
|
||||||
|
|
||||||
this.app = createApp(TemplateBuilder, {
|
this.app = createApp(TemplateBuilder, {
|
||||||
template: reactive(JSON.parse(this.dataset.template)),
|
template: reactive(JSON.parse(this.dataset.template)),
|
||||||
|
backgroundColor: '#faf7f5',
|
||||||
isDirectUpload: this.dataset.isDirectUpload === 'true'
|
isDirectUpload: this.dataset.isDirectUpload === 'true'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -70,3 +70,7 @@ button[disabled] .enabled {
|
|||||||
.base-radio {
|
.base-radio {
|
||||||
@apply radio bg-white radio-sm no-animation;
|
@apply radio bg-white radio-sm no-animation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.base-select {
|
||||||
|
@apply select base-input w-full font-normal;
|
||||||
|
}
|
||||||
@@ -38,7 +38,11 @@ export default actionable(targetable(class extends HTMLElement {
|
|||||||
this.uploadFiles(this.input.files)
|
this.uploadFiles(this.input.files)
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleLoading = () => {
|
toggleLoading = (e) => {
|
||||||
|
if (e && e.target && !e.target.contains(this)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.loading.classList.toggle('hidden')
|
this.loading.classList.toggle('hidden')
|
||||||
this.icon.classList.toggle('hidden')
|
this.icon.classList.toggle('hidden')
|
||||||
this.classList.toggle('opacity-50')
|
this.classList.toggle('opacity-50')
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export default class extends HTMLElement {
|
export default class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.querySelectorAll('a').forEach((link) => {
|
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')
|
link.classList.add('bg-base-300')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,15 +5,19 @@ export default actionable(class extends HTMLElement {
|
|||||||
document.body.classList.add('overflow-hidden')
|
document.body.classList.add('overflow-hidden')
|
||||||
|
|
||||||
document.addEventListener('keyup', this.onEscKey)
|
document.addEventListener('keyup', this.onEscKey)
|
||||||
document.addEventListener('turbo:submit-end', this.onSubmit)
|
|
||||||
document.addEventListener('turbo:before-cache', this.close)
|
document.addEventListener('turbo:before-cache', this.close)
|
||||||
|
|
||||||
|
if (this.dataset.closeAfterSubmit !== 'false') {
|
||||||
|
document.addEventListener('turbo:submit-end', this.onSubmit)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback () {
|
disconnectedCallback () {
|
||||||
document.body.classList.remove('overflow-hidden')
|
document.body.classList.remove('overflow-hidden')
|
||||||
|
|
||||||
document.removeEventListener('keyup', this.onEscKey)
|
document.removeEventListener('keyup', this.onEscKey)
|
||||||
document.removeEventListener('turbo:submit-end', this.handleSubmit)
|
document.removeEventListener('turbo:submit-end', this.onSubmit)
|
||||||
document.removeEventListener('turbo:before-cache', this.close)
|
document.removeEventListener('turbo:before-cache', this.close)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,13 @@ window.customElements.define('submission-form', class extends HTMLElement {
|
|||||||
this.appElem = document.createElement('div')
|
this.appElem = document.createElement('div')
|
||||||
|
|
||||||
this.app = createApp(Form, {
|
this.app = createApp(Form, {
|
||||||
submitterSlug: this.dataset.submitterSlug,
|
submitter: JSON.parse(this.dataset.submitter),
|
||||||
submitterUuid: this.dataset.submitterUuid,
|
|
||||||
authenticityToken: this.dataset.authenticityToken,
|
authenticityToken: this.dataset.authenticityToken,
|
||||||
canSendEmail: this.dataset.canSendEmail === 'true',
|
canSendEmail: this.dataset.canSendEmail === 'true',
|
||||||
isDirectUpload: this.dataset.isDirectUpload === 'true',
|
isDirectUpload: this.dataset.isDirectUpload === 'true',
|
||||||
isDemo: this.dataset.isDemo === 'true',
|
isDemo: this.dataset.isDemo === 'true',
|
||||||
|
attribution: this.dataset.attribution !== 'false',
|
||||||
|
withConfetti: true,
|
||||||
values: reactive(JSON.parse(this.dataset.values)),
|
values: reactive(JSON.parse(this.dataset.values)),
|
||||||
attachments: reactive(JSON.parse(this.dataset.attachments)),
|
attachments: reactive(JSON.parse(this.dataset.attachments)),
|
||||||
fields: JSON.parse(this.dataset.fields)
|
fields: JSON.parse(this.dataset.fields)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="isActive"
|
v-if="isActive && withLabel"
|
||||||
class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap"
|
class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap"
|
||||||
>
|
>
|
||||||
{{ field.name || fieldNames[field.type] }}
|
{{ field.name || fieldNames[field.type] }}
|
||||||
@@ -146,6 +146,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
withLabel: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
fieldIndex: {
|
fieldIndex: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -207,7 +212,7 @@ export default {
|
|||||||
},
|
},
|
||||||
formattedDate () {
|
formattedDate () {
|
||||||
if (this.field.type === 'date' && this.modelValue) {
|
if (this.field.type === 'date' && this.modelValue) {
|
||||||
return new Intl.DateTimeFormat([], { year: 'numeric', month: 'long', day: 'numeric' }).format(new Date(this.modelValue))
|
return new Intl.DateTimeFormat([], { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' }).format(new Date(this.modelValue))
|
||||||
} else {
|
} else {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,9 @@
|
|||||||
v-for="(area, areaIndex) in field.areas"
|
v-for="(area, areaIndex) in field.areas"
|
||||||
:key="areaIndex"
|
:key="areaIndex"
|
||||||
>
|
>
|
||||||
<Teleport :to="`#page-${area.attachment_uuid}-${area.page}`">
|
<Teleport
|
||||||
|
:to="findPageElementForArea(area)"
|
||||||
|
>
|
||||||
<FieldArea
|
<FieldArea
|
||||||
:ref="setAreaRef"
|
:ref="setAreaRef"
|
||||||
v-model="values[field.uuid]"
|
v-model="values[field.uuid]"
|
||||||
@@ -20,6 +22,7 @@
|
|||||||
:submittable="true"
|
:submittable="true"
|
||||||
:field-index="fieldIndex"
|
:field-index="fieldIndex"
|
||||||
:is-active="currentStep === step"
|
:is-active="currentStep === step"
|
||||||
|
:with-label="withLabel"
|
||||||
:is-value-set="step.some((f) => f.uuid in values)"
|
:is-value-set="step.some((f) => f.uuid in values)"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
@click="$emit('focus-step', step)"
|
@click="$emit('focus-step', step)"
|
||||||
@@ -54,6 +57,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
},
|
},
|
||||||
|
withLabel: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
currentStep: {
|
currentStep: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -70,10 +78,16 @@ export default {
|
|||||||
this.areaRefs = []
|
this.areaRefs = []
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
findPageElementForArea (area) {
|
||||||
|
return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${area.attachment_uuid}-${area.page}`)
|
||||||
|
},
|
||||||
scrollIntoField (field) {
|
scrollIntoField (field) {
|
||||||
this.areaRefs.find((area) => {
|
this.areaRefs.find((area) => {
|
||||||
if (area.field === field) {
|
if (area.field === field) {
|
||||||
if (document.body.style.overflow === 'hidden') {
|
const root = this.$root.$el.parentNode.getRootNode()
|
||||||
|
const container = root.body || root.querySelector('div')
|
||||||
|
|
||||||
|
if (container.style.overflow === 'hidden') {
|
||||||
this.scrollInContainer(area.$el)
|
this.scrollInContainer(area.$el)
|
||||||
} else {
|
} else {
|
||||||
area.$refs.scrollToElem.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
area.$refs.scrollToElem.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||||
@@ -86,13 +100,19 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
scrollInContainer (target) {
|
scrollInContainer (target) {
|
||||||
|
const root = this.$root.$el.parentNode.getRootNode()
|
||||||
|
|
||||||
|
const scrollbox = root.getElementById('scrollbox')
|
||||||
|
const formContainer = root.getElementById('form_container')
|
||||||
|
const container = root.body || root.querySelector('div')
|
||||||
|
|
||||||
const padding = 64
|
const padding = 64
|
||||||
const boxRect = window.scrollbox.children[0].getBoundingClientRect()
|
const boxRect = scrollbox.children[0].getBoundingClientRect()
|
||||||
const targetRect = target.getBoundingClientRect()
|
const targetRect = target.getBoundingClientRect()
|
||||||
|
|
||||||
const targetTopRelativeToBox = targetRect.top - boxRect.top
|
const targetTopRelativeToBox = targetRect.top - boxRect.top
|
||||||
|
|
||||||
window.scrollbox.scrollTop = targetTopRelativeToBox - document.body.offsetHeight + window.form_container.offsetHeight + target.offsetHeight + padding
|
scrollbox.scrollTop = targetTopRelativeToBox - container.offsetHeight + formContainer.offsetHeight + target.offsetHeight + padding
|
||||||
},
|
},
|
||||||
setAreaRef (el) {
|
setAreaRef (el) {
|
||||||
if (el) {
|
if (el) {
|
||||||
|
|||||||
@@ -7,13 +7,13 @@
|
|||||||
:height="30"
|
:height="30"
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
Form has been completed!
|
{{ t('form_has_been_completed') }}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<div class="space-y-3 mt-5">
|
<div class="space-y-3 mt-5">
|
||||||
<button
|
<button
|
||||||
v-if="canSendEmail && !isDemo"
|
v-if="canSendEmail && !isDemo"
|
||||||
class="white-button flex items-center space-x-1 w-full"
|
class="white-button !h-auto flex items-center space-x-1 w-full"
|
||||||
:disabled="isSendingCopy"
|
:disabled="isSendingCopy"
|
||||||
@click.prevent="sendCopyToEmail"
|
@click.prevent="sendCopyToEmail"
|
||||||
>
|
>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
/>
|
/>
|
||||||
<IconMail v-else />
|
<IconMail v-else />
|
||||||
<span>
|
<span>
|
||||||
Send copy via email
|
{{ t('send_copy_via_email') }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
/>
|
/>
|
||||||
<IconDownload v-else />
|
<IconDownload v-else />
|
||||||
<span>
|
<span>
|
||||||
Download
|
{{ t('download') }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<a
|
<a
|
||||||
@@ -58,17 +58,20 @@
|
|||||||
>
|
>
|
||||||
<IconLogin />
|
<IconLogin />
|
||||||
<span>
|
<span>
|
||||||
Create a Free Account
|
{{ t('create_a_free_account') }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center mt-4">
|
<div
|
||||||
Signed with
|
v-if="attribution"
|
||||||
|
class="text-center mt-4"
|
||||||
|
>
|
||||||
|
{{ t('signed_with') }}
|
||||||
<a
|
<a
|
||||||
href="https://www.docuseal.co"
|
href="https://www.docuseal.co"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="underline"
|
class="underline"
|
||||||
>DocuSeal</a> - open source documents software
|
>DocuSeal</a> - {{ t('open_source_documents_software') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -86,6 +89,7 @@ export default {
|
|||||||
IconLogin,
|
IconLogin,
|
||||||
IconDownload
|
IconDownload
|
||||||
},
|
},
|
||||||
|
inject: ['baseUrl', 't'],
|
||||||
props: {
|
props: {
|
||||||
submitterSlug: {
|
submitterSlug: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -96,6 +100,16 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
attribution: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
withConfetti: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
canSendEmail: {
|
canSendEmail: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -109,19 +123,21 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted () {
|
async mounted () {
|
||||||
const { default: confetti } = await import('canvas-confetti')
|
if (this.withConfetti) {
|
||||||
|
const { default: confetti } = await import('canvas-confetti')
|
||||||
|
|
||||||
confetti({
|
confetti({
|
||||||
particleCount: 50,
|
particleCount: 50,
|
||||||
startVelocity: 30,
|
startVelocity: 30,
|
||||||
spread: 140
|
spread: 140
|
||||||
})
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
sendCopyToEmail () {
|
sendCopyToEmail () {
|
||||||
this.isSendingCopy = true
|
this.isSendingCopy = true
|
||||||
|
|
||||||
fetch(`/send_submission_email.json?submitter_slug=${this.submitterSlug}`, {
|
fetch(this.baseUrl + `/send_submission_email.json?submitter_slug=${this.submitterSlug}`, {
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
alert('Email has been sent')
|
alert('Email has been sent')
|
||||||
@@ -132,7 +148,7 @@ export default {
|
|||||||
download () {
|
download () {
|
||||||
this.isDownloading = true
|
this.isDownloading = true
|
||||||
|
|
||||||
fetch(`/submitters/${this.submitterSlug}/download`).then((response) => response.json()).then((urls) => {
|
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`).then((response) => response.json()).then((urls) => {
|
||||||
const fileRequests = urls.map((url) => {
|
const fileRequests = urls.map((url) => {
|
||||||
return () => {
|
return () => {
|
||||||
return fetch(url).then(async (resp) => {
|
return fetch(url).then(async (resp) => {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
{{ message }}
|
{{ message }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs">
|
<div class="text-xs">
|
||||||
<span class="font-medium">Click to upload</span> or drag and drop files
|
<span class="font-medium">{{ t('click_to_upload') }}</span> {{ t('or_drag_and_drop_files') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -55,6 +55,7 @@ export default {
|
|||||||
IconCloudUpload,
|
IconCloudUpload,
|
||||||
IconInnerShadowTop
|
IconInnerShadowTop
|
||||||
},
|
},
|
||||||
|
inject: ['baseUrl', 't'],
|
||||||
props: {
|
props: {
|
||||||
message: {
|
message: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -141,7 +142,7 @@ export default {
|
|||||||
|
|
||||||
return await Promise.all(
|
return await Promise.all(
|
||||||
blobs.map((blob) => {
|
blobs.map((blob) => {
|
||||||
return fetch('/api/attachments', {
|
return fetch(this.baseUrl + '/api/attachments', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: 'attachments',
|
name: 'attachments',
|
||||||
@@ -166,7 +167,7 @@ export default {
|
|||||||
formData.append('submitter_slug', this.submitterSlug)
|
formData.append('submitter_slug', this.submitterSlug)
|
||||||
formData.append('name', 'attachments')
|
formData.append('name', 'attachments')
|
||||||
|
|
||||||
return fetch('/api/attachments', {
|
return fetch(this.baseUrl + '/api/attachments', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
}).then(resp => resp.json()).then((data) => {
|
}).then(resp => resp.json()).then((data) => {
|
||||||
|
|||||||
@@ -4,15 +4,16 @@
|
|||||||
:steps="stepFields"
|
:steps="stepFields"
|
||||||
:values="values"
|
:values="values"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
|
:with-label="!isAnonymousChecboxes"
|
||||||
:current-step="currentStepFields"
|
:current-step="currentStepFields"
|
||||||
@focus-step="[saveStep(), goToStep($event, false, true), currentField.type !== 'checkbox' ? isFormVisible = true : '']"
|
@focus-step="[saveStep(), goToStep($event, false, true), currentField.type !== 'checkbox' ? isFormVisible = true : '']"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="!isFormVisible"
|
v-if="!isFormVisible"
|
||||||
class="btn btn-neutral text-white absolute rounded-none border-x-0 md:border md:rounded-full bottom-0 w-full md:mb-4 text-base"
|
class="btn btn-neutral flex text-white absolute rounded-none border-x-0 md:border md:rounded-full bottom-0 w-full md:mb-4 text-base"
|
||||||
@click.prevent="isFormVisible = true"
|
@click.prevent="isFormVisible = true"
|
||||||
>
|
>
|
||||||
Submit Form
|
{{ t('submit_form') }}
|
||||||
<IconArrowsDiagonal
|
<IconArrowsDiagonal
|
||||||
class="absolute right-0 mr-4"
|
class="absolute right-0 mr-4"
|
||||||
:width="20"
|
:width="20"
|
||||||
@@ -23,6 +24,7 @@
|
|||||||
v-show="isFormVisible"
|
v-show="isFormVisible"
|
||||||
id="form_container"
|
id="form_container"
|
||||||
class="shadow-md bg-base-100 absolute bottom-0 md:bottom-4 w-full border-base-200 border p-4 rounded"
|
class="shadow-md bg-base-100 absolute bottom-0 md:bottom-4 w-full border-base-200 border p-4 rounded"
|
||||||
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-if="!isCompleted"
|
v-if="!isCompleted"
|
||||||
@@ -61,7 +63,7 @@
|
|||||||
:for="currentField.uuid"
|
:for="currentField.uuid"
|
||||||
class="label text-2xl mb-2"
|
class="label text-2xl mb-2"
|
||||||
>{{ currentField.name }}
|
>{{ currentField.name }}
|
||||||
<template v-if="!currentField.required">(optional)</template>
|
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
@@ -73,7 +75,7 @@
|
|||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
class="base-input !text-2xl w-full"
|
class="base-input !text-2xl w-full"
|
||||||
:required="currentField.required"
|
:required="currentField.required"
|
||||||
:placeholder="`Type here...${currentField.required ? '' : ' (optional)'}`"
|
:placeholder="`${t('type_here')}...${currentField.required ? '' : ` (${t('optional')})`}`"
|
||||||
type="text"
|
type="text"
|
||||||
:name="`values[${currentField.uuid}]`"
|
:name="`values[${currentField.uuid}]`"
|
||||||
@focus="$refs.areas.scrollIntoField(currentField)"
|
@focus="$refs.areas.scrollIntoField(currentField)"
|
||||||
@@ -86,7 +88,7 @@
|
|||||||
:for="currentField.uuid"
|
:for="currentField.uuid"
|
||||||
class="label text-2xl mb-2"
|
class="label text-2xl mb-2"
|
||||||
>{{ currentField.name }}
|
>{{ currentField.name }}
|
||||||
<template v-if="!currentField.required">(optional)</template>
|
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
@@ -110,7 +112,7 @@
|
|||||||
:for="currentField.uuid"
|
:for="currentField.uuid"
|
||||||
class="label text-2xl mb-2"
|
class="label text-2xl mb-2"
|
||||||
>{{ currentField.name }}
|
>{{ currentField.name }}
|
||||||
<template v-if="!currentField.required">(optional)</template>
|
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
@@ -128,7 +130,7 @@
|
|||||||
value=""
|
value=""
|
||||||
:selected="!values[currentField.uuid]"
|
:selected="!values[currentField.uuid]"
|
||||||
>
|
>
|
||||||
Select your option
|
{{ t('select_your_option') }}
|
||||||
</option>
|
</option>
|
||||||
<option
|
<option
|
||||||
v-for="(option, index) in currentField.options"
|
v-for="(option, index) in currentField.options"
|
||||||
@@ -146,7 +148,7 @@
|
|||||||
:for="currentField.uuid"
|
:for="currentField.uuid"
|
||||||
class="label text-2xl mb-2"
|
class="label text-2xl mb-2"
|
||||||
>{{ currentField.name }}
|
>{{ currentField.name }}
|
||||||
<template v-if="!currentField.required">(optional)</template>
|
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
||||||
</label>
|
</label>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="space-y-3.5 mx-auto">
|
<div class="space-y-3.5 mx-auto">
|
||||||
@@ -182,7 +184,7 @@
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-else-if="currentField.type === 'checkbox'"
|
v-else-if="currentField.type === 'checkbox'"
|
||||||
class="flex w-full"
|
class="flex w-full max-h-44 overflow-y-auto"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
@@ -192,31 +194,45 @@
|
|||||||
<div
|
<div
|
||||||
class="space-y-3.5 mx-auto"
|
class="space-y-3.5 mx-auto"
|
||||||
>
|
>
|
||||||
<div
|
<template v-if="isAnonymousChecboxes">
|
||||||
v-for="(field, index) in currentStepFields"
|
<span class="text-xl">
|
||||||
:key="field.uuid"
|
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? t('submit') : t('next') }}</span>.
|
||||||
>
|
</span>
|
||||||
<label
|
<input
|
||||||
:for="field.uuid"
|
v-for="field in currentStepFields"
|
||||||
class="flex items-center space-x-3"
|
:key="field.uuid"
|
||||||
|
type="hidden"
|
||||||
|
:name="`values[${field.uuid}]`"
|
||||||
|
:value="!!values[field.uuid]"
|
||||||
>
|
>
|
||||||
<input
|
</template>
|
||||||
type="hidden"
|
<template v-else>
|
||||||
:name="`values[${field.uuid}]`"
|
<div
|
||||||
:value="!!values[field.uuid]"
|
v-for="(field, index) in currentStepFields"
|
||||||
|
:key="field.uuid"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
:for="field.uuid"
|
||||||
|
class="flex items-center space-x-3"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
:id="field.uuid"
|
type="hidden"
|
||||||
type="checkbox"
|
:name="`values[${field.uuid}]`"
|
||||||
class="base-checkbox !h-7 !w-7"
|
:value="!!values[field.uuid]"
|
||||||
:checked="!!values[field.uuid]"
|
>
|
||||||
@click="[$refs.areas.scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
|
<input
|
||||||
>
|
:id="field.uuid"
|
||||||
<span class="text-xl">
|
type="checkbox"
|
||||||
{{ field.name || field.type + ' ' + (index + 1) }}
|
class="base-checkbox !h-7 !w-7"
|
||||||
</span>
|
:checked="!!values[field.uuid]"
|
||||||
</label>
|
@click="[$refs.areas.scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
|
||||||
</div>
|
>
|
||||||
|
<span class="text-xl">
|
||||||
|
{{ field.name || field.type + ' ' + (index + 1) }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ImageStep
|
<ImageStep
|
||||||
@@ -262,10 +278,10 @@
|
|||||||
class="mr-1 animate-spin"
|
class="mr-1 animate-spin"
|
||||||
/>
|
/>
|
||||||
<span v-if="stepFields.length === currentStep + 1">
|
<span v-if="stepFields.length === currentStep + 1">
|
||||||
Submit
|
{{ t('submit') }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
Next
|
{{ t('next') }}
|
||||||
</span><span
|
</span><span
|
||||||
v-if="isSubmitting"
|
v-if="isSubmitting"
|
||||||
class="w-6 flex justify-start mr-1"
|
class="w-6 flex justify-start mr-1"
|
||||||
@@ -277,7 +293,9 @@
|
|||||||
<FormCompleted
|
<FormCompleted
|
||||||
v-else
|
v-else
|
||||||
:is-demo="isDemo"
|
:is-demo="isDemo"
|
||||||
:can-send-email="canSendEmail"
|
:attribution="attribution"
|
||||||
|
:with-confetti="withConfetti"
|
||||||
|
:can-send-email="canSendEmail && submitter.email"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
/>
|
/>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
@@ -304,6 +322,7 @@ import AttachmentStep from './attachment_step'
|
|||||||
import MultiSelectStep from './multi_select_step'
|
import MultiSelectStep from './multi_select_step'
|
||||||
import FormCompleted from './completed'
|
import FormCompleted from './completed'
|
||||||
import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||||
|
import { t } from './i18n'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SubmissionForm',
|
name: 'SubmissionForm',
|
||||||
@@ -318,9 +337,15 @@ export default {
|
|||||||
IconArrowsDiagonalMinimize2,
|
IconArrowsDiagonalMinimize2,
|
||||||
FormCompleted
|
FormCompleted
|
||||||
},
|
},
|
||||||
|
provide () {
|
||||||
|
return {
|
||||||
|
baseUrl: this.baseUrl,
|
||||||
|
t: this.t
|
||||||
|
}
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
submitterSlug: {
|
submitter: {
|
||||||
type: String,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
canSendEmail: {
|
canSendEmail: {
|
||||||
@@ -328,15 +353,21 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
submitterUuid: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
attachments: {
|
attachments: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
withConfetti: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
baseUrl: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
fields: {
|
fields: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -344,18 +375,29 @@ export default {
|
|||||||
},
|
},
|
||||||
authenticityToken: {
|
authenticityToken: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
backgroundColor: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
},
|
},
|
||||||
isDirectUpload: {
|
isDirectUpload: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
isDemo: {
|
isDemo: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
attribution: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
values: {
|
values: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -374,6 +416,12 @@ export default {
|
|||||||
currentStepFields () {
|
currentStepFields () {
|
||||||
return this.stepFields[this.currentStep]
|
return this.stepFields[this.currentStep]
|
||||||
},
|
},
|
||||||
|
submitterSlug () {
|
||||||
|
return this.submitter.slug
|
||||||
|
},
|
||||||
|
isAnonymousChecboxes () {
|
||||||
|
return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name) && this.currentStepFields.length > 4
|
||||||
|
},
|
||||||
isButtonDisabled () {
|
isButtonDisabled () {
|
||||||
return this.isSubmitting ||
|
return this.isSubmitting ||
|
||||||
(this.currentField.required && ['image', 'file'].includes(this.currentField.type) && !this.values[this.currentField.uuid]?.length) ||
|
(this.currentField.required && ['image', 'file'].includes(this.currentField.type) && !this.values[this.currentField.uuid]?.length) ||
|
||||||
@@ -413,13 +461,21 @@ export default {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
|
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
|
||||||
document.body.style.overflow = 'hidden'
|
this.$nextTick(() => {
|
||||||
|
const root = this.$root.$el.parentNode.getRootNode()
|
||||||
|
const scrollbox = root.getElementById('scrollbox')
|
||||||
|
const parent = root.body || root.querySelector('div')
|
||||||
|
|
||||||
window.scrollbox.classList.add('h-full', 'overflow-y-auto')
|
parent.style.overflow = 'hidden'
|
||||||
window.scrollbox.parentNode.classList.add('h-screen', 'overflow-y-auto')
|
|
||||||
|
scrollbox.classList.add('h-full', 'overflow-y-auto')
|
||||||
|
scrollbox.parentNode.classList.add('h-screen', 'overflow-y-auto')
|
||||||
|
scrollbox.parentNode.style.maxHeight = '-webkit-fill-available'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
t,
|
||||||
goToStep (step, scrollToArea = false, clickUpload = false) {
|
goToStep (step, scrollToArea = false, clickUpload = false) {
|
||||||
this.currentStep = this.stepFields.indexOf(step)
|
this.currentStep = this.stepFields.indexOf(step)
|
||||||
|
|
||||||
@@ -439,7 +495,7 @@ export default {
|
|||||||
if (this.isCompleted) {
|
if (this.isCompleted) {
|
||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
} else {
|
} else {
|
||||||
return fetch(this.submitPath, {
|
return fetch(this.baseUrl + this.submitPath, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData || new FormData(this.$refs.form)
|
body: formData || new FormData(this.$refs.form)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
const en = {
|
||||||
|
submit_form: 'Submit Form',
|
||||||
|
type_here: 'Type here',
|
||||||
|
optional: 'optional',
|
||||||
|
select_your_option: 'Select your option',
|
||||||
|
complete_hightlighted_checkboxes_and_click: 'Complete hightlighted checkboxes and click',
|
||||||
|
submit: 'submit',
|
||||||
|
next: 'next',
|
||||||
|
click_to_upload: 'Click to upload',
|
||||||
|
or_drag_and_drop_files: 'or drag and drop files',
|
||||||
|
send_copy_via_email: 'Send copy via email',
|
||||||
|
download: 'Download',
|
||||||
|
form_has_been_completed: 'Form has been completed!',
|
||||||
|
create_a_free_account: 'Create a Free Account',
|
||||||
|
signed_with: 'Signed with',
|
||||||
|
open_source_documents_software: 'open source documents software'
|
||||||
|
}
|
||||||
|
|
||||||
|
const es = {
|
||||||
|
submit_form: 'Enviar Formulario',
|
||||||
|
type_here: 'Escribe aquí',
|
||||||
|
optional: 'opcional',
|
||||||
|
select_your_option: 'Selecciona tu opción',
|
||||||
|
complete_hightlighted_checkboxes_and_click: 'Completa las casillas resaltadas y haz clic',
|
||||||
|
submit: 'enviar',
|
||||||
|
next: 'siguiente',
|
||||||
|
click_to_upload: 'Haz clic para cargar',
|
||||||
|
or_drag_and_drop_files: 'o arrastra y suelta archivos',
|
||||||
|
send_copy_via_email: 'Enviar copia por correo electrónico',
|
||||||
|
download: 'Descargar',
|
||||||
|
form_has_been_completed: '¡El formulario ha sido completado!',
|
||||||
|
create_a_free_account: 'Crear una Cuenta Gratuita',
|
||||||
|
signed_with: 'Firmado con',
|
||||||
|
open_source_documents_software: 'software de documentos de código abierto'
|
||||||
|
}
|
||||||
|
|
||||||
|
const it = {
|
||||||
|
submit_form: 'Invia Modulo',
|
||||||
|
type_here: 'Digita qui',
|
||||||
|
optional: 'opzionale',
|
||||||
|
select_your_option: 'Seleziona la tua opzione',
|
||||||
|
complete_hightlighted_checkboxes_and_click: 'Completa le caselle evidenziate e fai clic',
|
||||||
|
submit: 'invia',
|
||||||
|
next: 'avanti',
|
||||||
|
click_to_upload: 'Clicca per caricare',
|
||||||
|
or_drag_and_drop_files: 'oppure trascina e rilascia i file',
|
||||||
|
send_copy_via_email: 'Invia copia via email',
|
||||||
|
download: 'Scarica',
|
||||||
|
form_has_been_completed: 'Il modulo è stato completato!',
|
||||||
|
create_a_free_account: 'Crea un Account Gratuito',
|
||||||
|
signed_with: 'Firmato con',
|
||||||
|
open_source_documents_software: 'software di documenti open source'
|
||||||
|
}
|
||||||
|
|
||||||
|
const de = {
|
||||||
|
submit_form: 'Formular absenden',
|
||||||
|
type_here: 'Hier eingeben',
|
||||||
|
optional: 'optional',
|
||||||
|
select_your_option: 'Wähle deine Option',
|
||||||
|
complete_hightlighted_checkboxes_and_click: 'Markierte Kontrollkästchen ausfüllen und klicken',
|
||||||
|
submit: 'absenden',
|
||||||
|
next: 'weiter',
|
||||||
|
click_to_upload: 'Klicken zum Hochladen',
|
||||||
|
or_drag_and_drop_files: 'oder Dateien hierher ziehen und ablegen',
|
||||||
|
send_copy_via_email: 'Kopie per E-Mail senden',
|
||||||
|
download: 'Herunterladen',
|
||||||
|
form_has_been_completed: 'Formular wurde ausgefüllt!',
|
||||||
|
create_a_free_account: 'Kostenloses Konto erstellen',
|
||||||
|
signed_with: 'Unterschrieben mit',
|
||||||
|
open_source_documents_software: 'Open-Source-Dokumentensoftware'
|
||||||
|
}
|
||||||
|
|
||||||
|
const fr = {
|
||||||
|
submit_form: 'Envoyer le Formulaire',
|
||||||
|
type_here: 'Tapez ici',
|
||||||
|
optional: 'facultatif',
|
||||||
|
select_your_option: 'Sélectionnez votre option',
|
||||||
|
complete_hightlighted_checkboxes_and_click: 'Complétez les cases à cocher en surbrillance et cliquez',
|
||||||
|
submit: 'envoyer',
|
||||||
|
next: 'suivant',
|
||||||
|
click_to_upload: 'Cliquez pour télécharger',
|
||||||
|
or_drag_and_drop_files: 'ou faites glisser-déposer les fichiers',
|
||||||
|
send_copy_via_email: 'Envoyer une copie par e-mail',
|
||||||
|
download: 'Télécharger',
|
||||||
|
form_has_been_completed: 'Le formulaire a été complété !',
|
||||||
|
create_a_free_account: 'Créer un Compte Gratuit',
|
||||||
|
signed_with: 'Signé avec',
|
||||||
|
open_source_documents_software: 'logiciel de documents open source'
|
||||||
|
}
|
||||||
|
|
||||||
|
const pl = {
|
||||||
|
submit_form: 'Wyślij Formularz',
|
||||||
|
type_here: 'Wpisz tutaj',
|
||||||
|
optional: 'opcjonalny',
|
||||||
|
select_your_option: 'Wybierz swoją opcję',
|
||||||
|
complete_hightlighted_checkboxes_and_click: 'Wypełnij zaznaczone pola wyboru i kliknij',
|
||||||
|
submit: 'wyślij',
|
||||||
|
next: 'dalej',
|
||||||
|
click_to_upload: 'Kliknij, aby przesłać',
|
||||||
|
or_drag_and_drop_files: 'lub przeciągnij i upuść pliki',
|
||||||
|
send_copy_via_email: 'Wyślij kopię drogą mailową',
|
||||||
|
download: 'Pobierz',
|
||||||
|
form_has_been_completed: 'Formularz został wypełniony!',
|
||||||
|
create_a_free_account: 'Utwórz darmowe konto',
|
||||||
|
signed_with: 'Podpisane za pomocą',
|
||||||
|
open_source_documents_software: 'oprogramowanie do dokumentów open source'
|
||||||
|
}
|
||||||
|
|
||||||
|
const uk = {
|
||||||
|
submit_form: 'Надіслати Форму',
|
||||||
|
type_here: 'Введіть тут',
|
||||||
|
optional: 'необов’язково',
|
||||||
|
select_your_option: 'Виберіть варіант',
|
||||||
|
complete_hightlighted_checkboxes_and_click: 'Заповніть позначені прапорці та натисніть',
|
||||||
|
submit: 'надіслати',
|
||||||
|
next: 'далі',
|
||||||
|
click_to_upload: 'Клацніть, щоб завантажити',
|
||||||
|
or_drag_and_drop_files: 'або перетягніть файли сюди',
|
||||||
|
send_copy_via_email: 'Надіслати копію електронною поштою',
|
||||||
|
download: 'Завантажити',
|
||||||
|
form_has_been_completed: 'Форму заповнено!',
|
||||||
|
create_a_free_account: 'Створити безкоштовний обліковий запис',
|
||||||
|
signed_with: 'Підписано за допомогою',
|
||||||
|
open_source_documents_software: 'відкритий програмний засіб для документів'
|
||||||
|
}
|
||||||
|
|
||||||
|
const cs = {
|
||||||
|
submit_form: 'Odeslat formulář',
|
||||||
|
type_here: 'Zadejte zde',
|
||||||
|
optional: 'volitelné',
|
||||||
|
select_your_option: 'Vyberte svou volbu',
|
||||||
|
complete_hightlighted_checkboxes_and_click: 'Označte zvýrazněné zaškrtávací políčka a klikněte na',
|
||||||
|
submit: 'odeslat',
|
||||||
|
next: 'další',
|
||||||
|
click_to_upload: 'Klikněte pro nahrání',
|
||||||
|
or_drag_and_drop_files: 'nebo přetáhněte soubory sem',
|
||||||
|
send_copy_via_email: 'Odeslat kopii e-mailem',
|
||||||
|
download: 'Stáhnout',
|
||||||
|
form_has_been_completed: 'Formulář byl dokončen!',
|
||||||
|
create_a_free_account: 'Vytvořit bezplatný účet',
|
||||||
|
signed_with: 'Podepsáno pomocí',
|
||||||
|
open_source_documents_software: 'open source software pro dokumenty'
|
||||||
|
}
|
||||||
|
|
||||||
|
const i18n = { en, es, it, de, fr, pl, uk, cs }
|
||||||
|
|
||||||
|
const browserLanguage = (navigator.language || navigator.userLanguage || 'en').split('-')[0]
|
||||||
|
|
||||||
|
const t = (key) => i18n[browserLanguage][key] || i18n.en[key] || key
|
||||||
|
|
||||||
|
export default i18n
|
||||||
|
export { t }
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
data-tip="Type text"
|
data-tip="Type text"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
id="type_text_button"
|
||||||
class="btn btn-sm btn-circle"
|
class="btn btn-sm btn-circle"
|
||||||
:class="{ 'btn-neutral': isTextSignature, 'btn-outline': !isTextSignature }"
|
:class="{ 'btn-neutral': isTextSignature, 'btn-outline': !isTextSignature }"
|
||||||
@click.prevent="toggleTextInput"
|
@click.prevent="toggleTextInput"
|
||||||
@@ -78,6 +79,7 @@
|
|||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
v-if="isTextSignature"
|
v-if="isTextSignature"
|
||||||
|
id="signature_text_input"
|
||||||
ref="textInput"
|
ref="textInput"
|
||||||
class="base-input !text-2xl w-full mt-6"
|
class="base-input !text-2xl w-full mt-6"
|
||||||
:required="field.required"
|
:required="field.required"
|
||||||
@@ -99,6 +101,7 @@ export default {
|
|||||||
IconTextSize,
|
IconTextSize,
|
||||||
IconArrowsDiagonalMinimize2
|
IconArrowsDiagonalMinimize2
|
||||||
},
|
},
|
||||||
|
inject: ['baseUrl'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -304,7 +307,7 @@ export default {
|
|||||||
file,
|
file,
|
||||||
'/direct_uploads'
|
'/direct_uploads'
|
||||||
).create((_error, data) => {
|
).create((_error, data) => {
|
||||||
fetch('/api/attachments', {
|
fetch(this.baseUrl + '/api/attachments', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
submitter_slug: this.submitterSlug,
|
submitter_slug: this.submitterSlug,
|
||||||
@@ -326,12 +329,12 @@ export default {
|
|||||||
formData.append('submitter_slug', this.submitterSlug)
|
formData.append('submitter_slug', this.submitterSlug)
|
||||||
formData.append('name', 'attachments')
|
formData.append('name', 'attachments')
|
||||||
|
|
||||||
return fetch('/api/attachments', {
|
return fetch(this.baseUrl + '/api/attachments', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
}).then((resp) => resp.json()).then((attachment) => {
|
}).then((resp) => resp.json()).then((attachment) => {
|
||||||
this.$emit('update:model-value', attachment.uuid)
|
|
||||||
this.$emit('attached', attachment)
|
this.$emit('attached', attachment)
|
||||||
|
this.$emit('update:model-value', attachment.uuid)
|
||||||
|
|
||||||
return resolve(attachment)
|
return resolve(attachment)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -226,14 +226,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
startResizeCell (e) {
|
startResizeCell (e) {
|
||||||
document.addEventListener('mousemove', this.onResizeCell)
|
this.$el.getRootNode().addEventListener('mousemove', this.onResizeCell)
|
||||||
document.addEventListener('mouseup', this.stopResizeCell)
|
this.$el.getRootNode().addEventListener('mouseup', this.stopResizeCell)
|
||||||
|
|
||||||
this.$emit('start-resize', 'ew')
|
this.$emit('start-resize', 'ew')
|
||||||
},
|
},
|
||||||
stopResizeCell (e) {
|
stopResizeCell (e) {
|
||||||
document.removeEventListener('mousemove', this.onResizeCell)
|
this.$el.getRootNode().removeEventListener('mousemove', this.onResizeCell)
|
||||||
document.removeEventListener('mouseup', this.stopResizeCell)
|
this.$el.getRootNode().removeEventListener('mouseup', this.stopResizeCell)
|
||||||
|
|
||||||
this.$emit('stop-resize')
|
this.$emit('stop-resize')
|
||||||
|
|
||||||
@@ -266,11 +266,13 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
onNameBlur (e) {
|
onNameBlur (e) {
|
||||||
|
const text = this.$refs.name.innerText.trim()
|
||||||
|
|
||||||
this.isNameFocus = false
|
this.isNameFocus = false
|
||||||
this.$refs.name.style.minWidth = ''
|
this.$refs.name.style.minWidth = ''
|
||||||
|
|
||||||
if (e.target.innerText.trim()) {
|
if (text) {
|
||||||
this.field.name = e.target.innerText.trim()
|
this.field.name = text
|
||||||
} else {
|
} else {
|
||||||
this.field.name = ''
|
this.field.name = ''
|
||||||
this.$refs.name.innerText = this.defaultName
|
this.$refs.name.innerText = this.defaultName
|
||||||
@@ -302,14 +304,14 @@ export default {
|
|||||||
|
|
||||||
this.dragFrom = { x: e.clientX - rect.left, y: e.clientY - rect.top }
|
this.dragFrom = { x: e.clientX - rect.left, y: e.clientY - rect.top }
|
||||||
|
|
||||||
document.addEventListener('mousemove', this.drag)
|
this.$el.getRootNode().addEventListener('mousemove', this.drag)
|
||||||
document.addEventListener('mouseup', this.stopDrag)
|
this.$el.getRootNode().addEventListener('mouseup', this.stopDrag)
|
||||||
|
|
||||||
this.$emit('start-drag')
|
this.$emit('start-drag')
|
||||||
},
|
},
|
||||||
stopDrag () {
|
stopDrag () {
|
||||||
document.removeEventListener('mousemove', this.drag)
|
this.$el.getRootNode().removeEventListener('mousemove', this.drag)
|
||||||
document.removeEventListener('mouseup', this.stopDrag)
|
this.$el.getRootNode().removeEventListener('mouseup', this.stopDrag)
|
||||||
|
|
||||||
if (this.isDragged) {
|
if (this.isDragged) {
|
||||||
this.save()
|
this.save()
|
||||||
@@ -322,14 +324,14 @@ export default {
|
|||||||
startResize () {
|
startResize () {
|
||||||
this.selectedAreaRef.value = this.area
|
this.selectedAreaRef.value = this.area
|
||||||
|
|
||||||
document.addEventListener('mousemove', this.resize)
|
this.$el.getRootNode().addEventListener('mousemove', this.resize)
|
||||||
document.addEventListener('mouseup', this.stopResize)
|
this.$el.getRootNode().addEventListener('mouseup', this.stopResize)
|
||||||
|
|
||||||
this.$emit('start-resize', 'nwse')
|
this.$emit('start-resize', 'nwse')
|
||||||
},
|
},
|
||||||
stopResize () {
|
stopResize () {
|
||||||
document.removeEventListener('mousemove', this.resize)
|
this.$el.getRootNode().removeEventListener('mousemove', this.resize)
|
||||||
document.removeEventListener('mouseup', this.stopResize)
|
this.$el.getRootNode().removeEventListener('mouseup', this.stopResize)
|
||||||
|
|
||||||
this.$emit('stop-resize')
|
this.$emit('stop-resize')
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
style="max-width: 1600px"
|
style="max-width: 1600px"
|
||||||
class="mx-auto pl-4"
|
class="mx-auto pl-4 h-full"
|
||||||
>
|
>
|
||||||
<div class="flex justify-between py-1.5 items-center pr-4">
|
<div class="flex justify-between py-1.5 items-center pr-4">
|
||||||
<div class="flex space-x-3">
|
<div class="flex space-x-3">
|
||||||
<a href="/">
|
<a
|
||||||
|
v-if="withLogoLink"
|
||||||
|
href="/"
|
||||||
|
>
|
||||||
<Logo />
|
<Logo />
|
||||||
</a>
|
</a>
|
||||||
|
<Logo v-else />
|
||||||
<Contenteditable
|
<Contenteditable
|
||||||
:model-value="template.name"
|
:model-value="template.name"
|
||||||
class="text-3xl font-semibold focus:text-clip"
|
class="text-3xl font-semibold focus:text-clip"
|
||||||
@@ -16,46 +20,53 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-x-3 flex items-center">
|
<div class="space-x-3 flex items-center">
|
||||||
<a
|
<slot
|
||||||
:href="`/templates/${template.id}/submissions/new`"
|
v-if="$slots.buttons"
|
||||||
data-turbo-frame="modal"
|
name="buttons"
|
||||||
class="btn btn-primary"
|
/>
|
||||||
>
|
<template v-else>
|
||||||
<IconUsersPlus
|
<a
|
||||||
width="20"
|
:href="`/templates/${template.id}/submissions/new`"
|
||||||
class="inline"
|
data-turbo-frame="modal"
|
||||||
/>
|
class="btn btn-primary"
|
||||||
<span class="hidden md:inline">
|
>
|
||||||
Recipients
|
<IconUsersPlus
|
||||||
</span>
|
width="20"
|
||||||
</a>
|
class="inline"
|
||||||
<button
|
/>
|
||||||
class="base-button"
|
<span class="hidden md:inline">
|
||||||
:class="{ disabled: isSaving }"
|
Recipients
|
||||||
v-bind="isSaving ? { disabled: true } : {}"
|
</span>
|
||||||
@click.prevent="onSaveClick"
|
</a>
|
||||||
>
|
<button
|
||||||
<IconInnerShadowTop
|
class="base-button"
|
||||||
v-if="isSaving"
|
:class="{ disabled: isSaving }"
|
||||||
width="20"
|
v-bind="isSaving ? { disabled: true } : {}"
|
||||||
class="animate-spin"
|
@click.prevent="onSaveClick"
|
||||||
/>
|
>
|
||||||
<IconDeviceFloppy
|
<IconInnerShadowTop
|
||||||
v-else
|
v-if="isSaving"
|
||||||
width="20"
|
width="20"
|
||||||
/>
|
class="animate-spin"
|
||||||
<span class="hidden md:inline">
|
/>
|
||||||
Save
|
<IconDeviceFloppy
|
||||||
</span>
|
v-else
|
||||||
</button>
|
width="20"
|
||||||
|
/>
|
||||||
|
<span class="hidden md:inline">
|
||||||
|
Save
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex"
|
class="flex"
|
||||||
style="max-height: calc(100vh - 60px)"
|
style="max-height: calc(100% - 60px)"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref="previews"
|
ref="previews"
|
||||||
|
:style="{ 'display': isBreakpointLg ? 'none' : 'initial' }"
|
||||||
class="overflow-y-auto overflow-x-hidden w-52 flex-none pr-3 mt-0.5 pt-0.5 hidden lg:block"
|
class="overflow-y-auto overflow-x-hidden w-52 flex-none pr-3 mt-0.5 pt-0.5 hidden lg:block"
|
||||||
>
|
>
|
||||||
<DocumentPreview
|
<DocumentPreview
|
||||||
@@ -64,13 +75,19 @@
|
|||||||
:with-arrows="template.schema.length > 1"
|
:with-arrows="template.schema.length > 1"
|
||||||
:item="item"
|
:item="item"
|
||||||
:document="sortedDocuments[index]"
|
:document="sortedDocuments[index]"
|
||||||
|
:template="template"
|
||||||
|
:is-direct-upload="isDirectUpload"
|
||||||
@scroll-to="scrollIntoDocument(item)"
|
@scroll-to="scrollIntoDocument(item)"
|
||||||
@remove="onDocumentRemove"
|
@remove="onDocumentRemove"
|
||||||
|
@replace="onDocumentReplace"
|
||||||
@up="moveDocument(item, -1)"
|
@up="moveDocument(item, -1)"
|
||||||
@down="moveDocument(item, 1)"
|
@down="moveDocument(item, 1)"
|
||||||
@change="save"
|
@change="save"
|
||||||
/>
|
/>
|
||||||
<div class="sticky bottom-0 bg-base-100 py-2">
|
<div
|
||||||
|
class="sticky bottom-0 py-2"
|
||||||
|
:class="{ 'bg-base-100': withStickySubmitters }"
|
||||||
|
>
|
||||||
<Upload
|
<Upload
|
||||||
v-if="sortedDocuments.length"
|
v-if="sortedDocuments.length"
|
||||||
:template-id="template.id"
|
:template-id="template.id"
|
||||||
@@ -91,19 +108,46 @@
|
|||||||
@success="updateFromUpload"
|
@success="updateFromUpload"
|
||||||
/>
|
/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<Document
|
<template
|
||||||
v-for="document in sortedDocuments"
|
v-for="document in sortedDocuments"
|
||||||
:key="document.uuid"
|
:key="document.uuid"
|
||||||
:ref="setDocumentRefs"
|
>
|
||||||
:areas-index="fieldAreasIndex[document.uuid]"
|
<Document
|
||||||
:selected-submitter="selectedSubmitter"
|
:ref="setDocumentRefs"
|
||||||
:document="document"
|
:areas-index="fieldAreasIndex[document.uuid]"
|
||||||
:is-drag="!!dragFieldType"
|
:selected-submitter="selectedSubmitter"
|
||||||
:draw-field="drawField"
|
:document="document"
|
||||||
@draw="onDraw"
|
:is-drag="!!dragFieldType"
|
||||||
@drop-field="onDropfield"
|
:draw-field="drawField"
|
||||||
@remove-area="removeArea"
|
@draw="onDraw"
|
||||||
/>
|
@drop-field="onDropfield"
|
||||||
|
@remove-area="removeArea"
|
||||||
|
/>
|
||||||
|
<DocumentControls
|
||||||
|
v-if="isBreakpointLg"
|
||||||
|
:with-arrows="template.schema.length > 1"
|
||||||
|
:item="template.schema.find((item) => item.attachment_uuid === document.uuid)"
|
||||||
|
:document="document"
|
||||||
|
:template="template"
|
||||||
|
:is-direct-upload="isDirectUpload"
|
||||||
|
class="pb-2 mb-2 border-b border-base-300 border-dashed"
|
||||||
|
@remove="onDocumentRemove"
|
||||||
|
@replace="onDocumentReplace"
|
||||||
|
@up="moveDocument(template.schema.find((item) => item.attachment_uuid === document.uuid), -1)"
|
||||||
|
@down="moveDocument(template.schema.find((item) => item.attachment_uuid === document.uuid), 1)"
|
||||||
|
@change="save"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
v-if="sortedDocuments.length && isBreakpointLg"
|
||||||
|
class="pb-4"
|
||||||
|
>
|
||||||
|
<Upload
|
||||||
|
:template-id="template.id"
|
||||||
|
:is-direct-upload="isDirectUpload"
|
||||||
|
@success="updateFromUpload"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -119,7 +163,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="relative w-80 flex-none pt-0.5 pr-4 pl-0.5 hidden md:block"
|
class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block"
|
||||||
:class="drawField ? 'overflow-hidden' : 'overflow-auto'"
|
:class="drawField ? 'overflow-hidden' : 'overflow-auto'"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -133,7 +177,7 @@
|
|||||||
<p>
|
<p>
|
||||||
<button
|
<button
|
||||||
class="base-button"
|
class="base-button"
|
||||||
@click="drawField = false"
|
@click="drawField = null"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
@@ -146,6 +190,7 @@
|
|||||||
:fields="template.fields"
|
:fields="template.fields"
|
||||||
:submitters="template.submitters"
|
:submitters="template.submitters"
|
||||||
:selected-submitter="selectedSubmitter"
|
:selected-submitter="selectedSubmitter"
|
||||||
|
:with-sticky-submitters="withStickySubmitters"
|
||||||
@set-draw="drawField = $event"
|
@set-draw="drawField = $event"
|
||||||
@set-drag="dragFieldType = $event"
|
@set-drag="dragFieldType = $event"
|
||||||
@change-submitter="selectedSubmitter = $event"
|
@change-submitter="selectedSubmitter = $event"
|
||||||
@@ -166,6 +211,7 @@ import Document from './document'
|
|||||||
import Logo from './logo'
|
import Logo from './logo'
|
||||||
import Contenteditable from './contenteditable'
|
import Contenteditable from './contenteditable'
|
||||||
import DocumentPreview from './preview'
|
import DocumentPreview from './preview'
|
||||||
|
import DocumentControls from './controls'
|
||||||
import { IconUsersPlus, IconDeviceFloppy, IconInnerShadowTop } from '@tabler/icons-vue'
|
import { IconUsersPlus, IconDeviceFloppy, IconInnerShadowTop } from '@tabler/icons-vue'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
@@ -179,6 +225,7 @@ export default {
|
|||||||
Logo,
|
Logo,
|
||||||
Dropzone,
|
Dropzone,
|
||||||
DocumentPreview,
|
DocumentPreview,
|
||||||
|
DocumentControls,
|
||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
Contenteditable,
|
Contenteditable,
|
||||||
IconUsersPlus,
|
IconUsersPlus,
|
||||||
@@ -188,6 +235,8 @@ export default {
|
|||||||
return {
|
return {
|
||||||
template: this.template,
|
template: this.template,
|
||||||
save: this.save,
|
save: this.save,
|
||||||
|
baseFetch: this.baseFetch,
|
||||||
|
backgroundColor: this.backgroundColor,
|
||||||
selectedAreaRef: computed(() => this.selectedAreaRef)
|
selectedAreaRef: computed(() => this.selectedAreaRef)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -198,13 +247,39 @@ export default {
|
|||||||
},
|
},
|
||||||
isDirectUpload: {
|
isDirectUpload: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
backgroundColor: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
baseUrl: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
withLogoLink: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
withStickySubmitters: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
fetchOptions: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({ headers: {} })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
documentRefs: [],
|
documentRefs: [],
|
||||||
|
isBreakpointLg: false,
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
selectedSubmitter: null,
|
selectedSubmitter: null,
|
||||||
drawField: null,
|
drawField: null,
|
||||||
@@ -241,15 +316,28 @@ export default {
|
|||||||
this.selectedSubmitter = this.template.submitters[0]
|
this.selectedSubmitter = this.template.submitters[0]
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.onWindowResize()
|
||||||
|
})
|
||||||
|
|
||||||
document.addEventListener('keyup', this.onKeyUp)
|
document.addEventListener('keyup', this.onKeyUp)
|
||||||
|
|
||||||
|
window.addEventListener('resize', this.onWindowResize)
|
||||||
},
|
},
|
||||||
unmounted () {
|
unmounted () {
|
||||||
document.removeEventListener('keyup', this.onKeyUp)
|
document.removeEventListener('keyup', this.onKeyUp)
|
||||||
|
|
||||||
|
window.removeEventListener('resize', this.onWindowResize)
|
||||||
},
|
},
|
||||||
beforeUpdate () {
|
beforeUpdate () {
|
||||||
this.documentRefs = []
|
this.documentRefs = []
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
onWindowResize (e) {
|
||||||
|
const breakpointLg = 1024
|
||||||
|
|
||||||
|
this.isBreakpointLg = this.$el.getRootNode().querySelector('div').offsetWidth < breakpointLg
|
||||||
|
},
|
||||||
setDocumentRefs (el) {
|
setDocumentRefs (el) {
|
||||||
if (el) {
|
if (el) {
|
||||||
this.documentRefs.push(el)
|
this.documentRefs.push(el)
|
||||||
@@ -353,7 +441,7 @@ export default {
|
|||||||
|
|
||||||
if (this.selectedField?.type === this.dragFieldType) {
|
if (this.selectedField?.type === this.dragFieldType) {
|
||||||
baseArea = this.selectedAreaRef.value
|
baseArea = this.selectedAreaRef.value
|
||||||
} else if (previousField?.areas) {
|
} else if (previousField?.areas?.length) {
|
||||||
baseArea = previousField.areas[previousField.areas.length - 1]
|
baseArea = previousField.areas[previousField.areas.length - 1]
|
||||||
} else {
|
} else {
|
||||||
if (['checkbox'].includes(this.dragFieldType)) {
|
if (['checkbox'].includes(this.dragFieldType)) {
|
||||||
@@ -417,6 +505,27 @@ export default {
|
|||||||
this.template.schema.splice(this.template.schema.indexOf(item), 1)
|
this.template.schema.splice(this.template.schema.indexOf(item), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.template.fields.forEach((field) => {
|
||||||
|
[...(field.areas || [])].forEach((area) => {
|
||||||
|
if (area.attachment_uuid === item.attachment_uuid) {
|
||||||
|
field.areas.splice(field.areas.indexOf(area), 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
|
onDocumentReplace ({ replaceSchemaItem, schema, documents }) {
|
||||||
|
this.template.schema.splice(this.template.schema.indexOf(replaceSchemaItem), 1, schema[0])
|
||||||
|
this.template.documents.push(...documents)
|
||||||
|
this.template.fields.forEach((field) => {
|
||||||
|
(field.areas || []).forEach((area) => {
|
||||||
|
if (area.attachment_uuid === replaceSchemaItem.attachment_uuid) {
|
||||||
|
area.attachment_uuid = schema[0].attachment_uuid
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
moveDocument (item, direction) {
|
moveDocument (item, direction) {
|
||||||
@@ -450,10 +559,18 @@ export default {
|
|||||||
|
|
||||||
this.selectedAreaRef.value = area
|
this.selectedAreaRef.value = area
|
||||||
},
|
},
|
||||||
|
baseFetch (path, options = {}) {
|
||||||
|
return fetch(this.baseUrl + path, {
|
||||||
|
...options,
|
||||||
|
headers: { ...this.fetchOptions.headers, ...options.headers }
|
||||||
|
})
|
||||||
|
},
|
||||||
save () {
|
save () {
|
||||||
this.$el.closest('template-builder').dataset.template = JSON.stringify(this.template)
|
if (this.$el.closest('template-builder')) {
|
||||||
|
this.$el.closest('template-builder').dataset.template = JSON.stringify(this.template)
|
||||||
|
}
|
||||||
|
|
||||||
return fetch(`/api/templates/${this.template.id}`, {
|
return this.baseFetch(`/api/templates/${this.template.id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
template: {
|
template: {
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<Contenteditable
|
||||||
|
class="w-full block mr-6"
|
||||||
|
:model-value="item.name"
|
||||||
|
:icon-width="16"
|
||||||
|
@update:model-value="onUpdateName"
|
||||||
|
/>
|
||||||
|
<ReplaceButton
|
||||||
|
:is-direct-upload="isDirectUpload"
|
||||||
|
:template-id="template.id"
|
||||||
|
@click.stop
|
||||||
|
@success="$emit('replace', { replaceSchemaItem: item, ...$event })"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="withArrows"
|
||||||
|
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
|
||||||
|
style="width: 24px; height: 24px"
|
||||||
|
@click.stop="$emit('up', item)"
|
||||||
|
>
|
||||||
|
↑
|
||||||
|
</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,
|
IconCloudUpload,
|
||||||
IconInnerShadowTop
|
IconInnerShadowTop
|
||||||
},
|
},
|
||||||
|
inject: ['baseFetch'],
|
||||||
props: {
|
props: {
|
||||||
templateId: {
|
templateId: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
@@ -93,12 +94,21 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mounted () {
|
||||||
|
if (this.isDirectUpload) {
|
||||||
|
import('@rails/activestorage')
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
upload: Upload.methods.upload,
|
upload: Upload.methods.upload,
|
||||||
onDropFiles (e) {
|
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
|
<label
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
title="Areas"
|
title="Areas"
|
||||||
class="cursor-pointer text-base-100 group-hover:text-base-content"
|
class="cursor-pointer text-transparent group-hover:text-base-content"
|
||||||
>
|
>
|
||||||
<IconShape
|
<IconShape
|
||||||
:width="18"
|
:width="18"
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
<button
|
<button
|
||||||
v-else
|
v-else
|
||||||
title="Areas"
|
title="Areas"
|
||||||
class="relative cursor-pointer text-base-100 group-hover:text-base-content"
|
class="relative cursor-pointer text-transparent group-hover:text-base-content"
|
||||||
@click="$emit('set-draw', field)"
|
@click="$emit('set-draw', field)"
|
||||||
>
|
>
|
||||||
<IconShape
|
<IconShape
|
||||||
@@ -113,7 +113,7 @@
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="relative text-base-100 group-hover:text-base-content"
|
class="relative text-transparent group-hover:text-base-content"
|
||||||
title="Remove"
|
title="Remove"
|
||||||
@click="$emit('remove', field)"
|
@click="$emit('remove', field)"
|
||||||
>
|
>
|
||||||
@@ -122,7 +122,7 @@
|
|||||||
:stroke-width="1.6"
|
:stroke-width="1.6"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<div class="flex flex-col pr-1 text-base-100 group-hover:text-base-content">
|
<div class="flex flex-col pr-1 text-transparent group-hover:text-base-content">
|
||||||
<button
|
<button
|
||||||
title="Up"
|
title="Up"
|
||||||
class="relative"
|
class="relative"
|
||||||
@@ -156,7 +156,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
v-model="field.options[index]"
|
v-model="field.options[index]"
|
||||||
class="w-full input input-primary input-xs text-sm"
|
class="w-full input input-primary input-xs text-sm bg-transparent"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
@blur="save"
|
@blur="save"
|
||||||
@@ -254,8 +254,10 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
onNameBlur (e) {
|
onNameBlur (e) {
|
||||||
if (e.target.innerText.trim()) {
|
const text = this.$refs.name.$refs.contenteditable.innerText.trim()
|
||||||
this.field.name = e.target.innerText.trim()
|
|
||||||
|
if (text) {
|
||||||
|
this.field.name = text
|
||||||
} else {
|
} else {
|
||||||
this.field.name = ''
|
this.field.name = ''
|
||||||
this.$refs.name.$refs.contenteditable.innerText = this.defaultName
|
this.$refs.name.$refs.contenteditable.innerText = this.defaultName
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="sticky -top-1 bg-base-100 pt-1 -mt-1 z-10">
|
<div :class="withStickySubmitters ? 'sticky top-0 z-10' : ''">
|
||||||
<FieldSubmitter
|
<FieldSubmitter
|
||||||
:model-value="selectedSubmitter.uuid"
|
:model-value="selectedSubmitter.uuid"
|
||||||
class="w-full bg-base-100"
|
class="w-full rounded-lg"
|
||||||
|
:class="{ 'bg-base-100': withStickySubmitters }"
|
||||||
:submitters="submitters"
|
:submitters="submitters"
|
||||||
@new-submitter="save"
|
@new-submitter="save"
|
||||||
@remove="removeSubmitter"
|
@remove="removeSubmitter"
|
||||||
@@ -28,7 +29,8 @@
|
|||||||
v-for="(icon, type) in fieldIcons"
|
v-for="(icon, type) in fieldIcons"
|
||||||
:key="type"
|
:key="type"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
class="flex items-center justify-center border border-dashed border-base-300 bg-base-100 w-full rounded relative"
|
class="flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
|
||||||
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
@dragstart="onDragstart(type)"
|
@dragstart="onDragstart(type)"
|
||||||
@dragend="$emit('drag-end')"
|
@dragend="$emit('drag-end')"
|
||||||
@click="addField(type)"
|
@click="addField(type)"
|
||||||
@@ -76,7 +78,7 @@
|
|||||||
Draw a text field on the page with a mouse
|
Draw a text field on the page with a mouse
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
Single click of the page to add a checkbox
|
Single click on the page to add a checkbox
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
Drag & drop any other field type on the page
|
Drag & drop any other field type on the page
|
||||||
@@ -100,12 +102,17 @@ export default {
|
|||||||
Field,
|
Field,
|
||||||
FieldSubmitter
|
FieldSubmitter
|
||||||
},
|
},
|
||||||
inject: ['save'],
|
inject: ['save', 'backgroundColor'],
|
||||||
props: {
|
props: {
|
||||||
fields: {
|
fields: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
withStickySubmitters: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
submitters: {
|
submitters: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
@@ -172,6 +179,7 @@ export default {
|
|||||||
name: '',
|
name: '',
|
||||||
uuid: v4(),
|
uuid: v4(),
|
||||||
required: type !== 'checkbox',
|
required: type !== 'checkbox',
|
||||||
|
areas: [],
|
||||||
submitter_uuid: this.selectedSubmitter.uuid,
|
submitter_uuid: this.selectedSubmitter.uuid,
|
||||||
type
|
type
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
ref="image"
|
ref="image"
|
||||||
:src="image.url"
|
:src="image.url"
|
||||||
:width="width"
|
:width="width"
|
||||||
class="shadow-md mb-4"
|
class="border rounded mb-4"
|
||||||
:height="height"
|
:height="height"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -9,39 +9,51 @@
|
|||||||
loading="lazy"
|
loading="lazy"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="group flex justify-end cursor-pointer top-0 bottom-0 left-0 right-0 absolute"
|
class="group flex justify-end cursor-pointer top-0 bottom-0 left-0 right-0 absolute p-1"
|
||||||
@click="$emit('scroll-to', item)"
|
@click="$emit('scroll-to', item)"
|
||||||
>
|
>
|
||||||
<div
|
<div class="flex justify-between w-full">
|
||||||
class="flex flex-col justify-between opacity-0 group-hover:opacity-100"
|
<div style="width: 26px" />
|
||||||
>
|
<div class="">
|
||||||
<div class="rounded-bl rounded-tr bg-white border">
|
<ReplaceButton
|
||||||
<button
|
:is-direct-upload="isDirectUpload"
|
||||||
class="rounded-bl rounded-tr hover:text-base-100 hover:bg-base-content w-full transition-colors"
|
:template-id="template.id"
|
||||||
style="width: 24px"
|
class="opacity-0 group-hover:opacity-100"
|
||||||
@click.stop="$emit('remove', item)"
|
@click.stop
|
||||||
>
|
@success="$emit('replace', { replaceSchemaItem: item, ...$event })"
|
||||||
×
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="withArrows"
|
class="flex flex-col justify-between opacity-0 group-hover:opacity-100"
|
||||||
class="flex flex-col border rounded-br rounded-tl bg-white divide-y"
|
|
||||||
>
|
>
|
||||||
<button
|
<div>
|
||||||
class="rounded-tl hover:text-base-100 hover:bg-base-content w-full transition-colors"
|
<button
|
||||||
style="width: 24px"
|
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"
|
||||||
@click.stop="$emit('up', item)"
|
style="width: 24px; height: 24px"
|
||||||
|
@click.stop="$emit('remove', item)"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="withArrows"
|
||||||
|
class="flex flex-col space-y-1"
|
||||||
>
|
>
|
||||||
↑
|
<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"
|
||||||
<button
|
style="width: 24px; height: 24px"
|
||||||
class="rounded-br hover:text-base-100 hover:bg-base-content w-full transition-colors"
|
@click.stop="$emit('up', item)"
|
||||||
style="width: 24px"
|
>
|
||||||
@click.stop="$emit('down', item)"
|
↑
|
||||||
>
|
</button>
|
||||||
↓
|
<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('down', item)"
|
||||||
|
>
|
||||||
|
↓
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,34 +72,52 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Contenteditable from './contenteditable'
|
import Contenteditable from './contenteditable'
|
||||||
|
import Upload from './upload'
|
||||||
|
import ReplaceButton from './replace'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DocumentPreview',
|
name: 'DocumentPreview',
|
||||||
components: {
|
components: {
|
||||||
Contenteditable
|
Contenteditable,
|
||||||
|
ReplaceButton
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
item: {
|
item: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
template: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
document: {
|
document: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
isDirectUpload: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
withArrows: {
|
withArrows: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['scroll-to', 'change', 'remove', 'up', 'down'],
|
emits: ['scroll-to', 'change', 'remove', 'up', 'down', 'replace'],
|
||||||
computed: {
|
computed: {
|
||||||
previewImage () {
|
previewImage () {
|
||||||
return this.document.preview_images[0]
|
return this.document.preview_images[0]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mounted () {
|
||||||
|
if (this.isDirectUpload) {
|
||||||
|
import('@rails/activestorage')
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
upload: Upload.methods.upload,
|
||||||
onUpdateName (value) {
|
onUpdateName (value) {
|
||||||
this.item.name = value
|
this.item.name = value
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<label
|
||||||
|
:for="inputId"
|
||||||
|
class="btn btn-neutral btn-xs text-white transition-none"
|
||||||
|
:class="{ 'opacity-100': isLoading || isProcessing }"
|
||||||
|
>
|
||||||
|
{{ message }}
|
||||||
|
<form
|
||||||
|
ref="form"
|
||||||
|
class="hidden"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:id="inputId"
|
||||||
|
ref="input"
|
||||||
|
name="files[]"
|
||||||
|
type="file"
|
||||||
|
accept="image/*, application/pdf"
|
||||||
|
@change="upload"
|
||||||
|
>
|
||||||
|
</form>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Upload from './upload'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ReplaceDocument',
|
||||||
|
inject: ['baseFetch'],
|
||||||
|
props: {
|
||||||
|
templateId: {
|
||||||
|
type: [Number, String],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isDirectUpload: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['success'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
isProcessing: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
inputId () {
|
||||||
|
return 'el' + Math.random().toString(32).split('.')[1]
|
||||||
|
},
|
||||||
|
message () {
|
||||||
|
if (this.isLoading) {
|
||||||
|
return 'Uploading...'
|
||||||
|
} else if (this.isProcessing) {
|
||||||
|
return 'Processing...'
|
||||||
|
} else {
|
||||||
|
return 'Replace'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
if (this.isDirectUpload) {
|
||||||
|
import('@rails/activestorage')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
upload: Upload.methods.upload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -50,6 +50,7 @@ export default {
|
|||||||
IconUpload,
|
IconUpload,
|
||||||
IconInnerShadowTop
|
IconInnerShadowTop
|
||||||
},
|
},
|
||||||
|
inject: ['baseFetch'],
|
||||||
props: {
|
props: {
|
||||||
templateId: {
|
templateId: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
@@ -113,7 +114,7 @@ export default {
|
|||||||
|
|
||||||
this.isProcessing = true
|
this.isProcessing = true
|
||||||
|
|
||||||
fetch(`/api/templates/${this.templateId}/documents`, {
|
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ blobs }),
|
body: JSON.stringify({ blobs }),
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
@@ -124,7 +125,7 @@ export default {
|
|||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
fetch(`/api/templates/${this.templateId}/documents`, {
|
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: new FormData(this.$refs.form)
|
body: new FormData(this.$refs.form)
|
||||||
}).then(resp => resp.json()).then((data) => {
|
}).then(resp => resp.json()).then((data) => {
|
||||||
|
|||||||
@@ -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
|
register_interceptor ActionMailerConfigsInterceptor
|
||||||
|
|
||||||
|
before_action do
|
||||||
|
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
||||||
|
end
|
||||||
|
|
||||||
def default_url_options
|
def default_url_options
|
||||||
Docuseal.default_url_options
|
Docuseal.default_url_options
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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
|
class SubmitterMailer < ApplicationMailer
|
||||||
DEFAULT_MESSAGE = %(You have been invited to submit the "%<name>s" form:)
|
DEFAULT_MESSAGE = %(You have been invited to submit the "%<name>s" form:)
|
||||||
|
|
||||||
def invitation_email(submitter, message: format(DEFAULT_MESSAGE, name: submitter.submission.template.name))
|
def invitation_email(submitter, message: '')
|
||||||
|
@current_account = submitter.submission.template.account
|
||||||
@submitter = submitter
|
@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,
|
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
|
end
|
||||||
|
|
||||||
def completed_email(submitter, user)
|
def completed_email(submitter, user)
|
||||||
|
@current_account = submitter.submission.template.account
|
||||||
@submitter = submitter
|
@submitter = submitter
|
||||||
@user = user
|
@user = user
|
||||||
|
|
||||||
mail(to: user.email,
|
@email_config = @current_account.account_configs.find_by(key: AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY)
|
||||||
subject: %(#{submitter.email} has completed the "#{submitter.submission.template.name}" form))
|
|
||||||
|
subject =
|
||||||
|
if @email_config
|
||||||
|
ReplaceEmailVariables.call(@email_config.value['subject'], submitter:)
|
||||||
|
else
|
||||||
|
%(#{submitter.email} has completed the "#{submitter.submission.template.name}" form)
|
||||||
|
end
|
||||||
|
|
||||||
|
mail(from: from_address_for_submitter(submitter),
|
||||||
|
to: user.email,
|
||||||
|
subject:)
|
||||||
end
|
end
|
||||||
|
|
||||||
def documents_copy_email(submitter)
|
def documents_copy_email(submitter)
|
||||||
|
@current_account = submitter.submission.template.account
|
||||||
@submitter = submitter
|
@submitter = submitter
|
||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(@submitter)
|
Submissions::EnsureResultGenerated.call(@submitter)
|
||||||
@@ -30,6 +54,23 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
attachments[attachment.filename.to_s] = attachment.download
|
attachments[attachment.filename.to_s] = attachment.download
|
||||||
end
|
end
|
||||||
|
|
||||||
mail(to: submitter.email, subject: 'Your copy of documents')
|
@email_config = @current_account.account_configs.find_by(key: AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
|
||||||
|
|
||||||
|
subject =
|
||||||
|
if @email_config
|
||||||
|
ReplaceEmailVariables.call(@email_config.value['subject'], submitter:)
|
||||||
|
else
|
||||||
|
'Your copy of documents'
|
||||||
|
end
|
||||||
|
|
||||||
|
mail(from: from_address_for_submitter(submitter),
|
||||||
|
to: submitter.email,
|
||||||
|
subject:)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def from_address_for_submitter(submitter)
|
||||||
|
submitter.submission.created_by_user&.friendly_name || submitter.submission.template.author.friendly_name
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
class UserMailer < ApplicationMailer
|
class UserMailer < ApplicationMailer
|
||||||
def invitation_email(user)
|
def invitation_email(user)
|
||||||
|
@current_account = user.account
|
||||||
@user = user
|
@user = user
|
||||||
@token = @user.send(:set_reset_password_token)
|
@token = @user.send(:set_reset_password_token)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
class Account < ApplicationRecord
|
||||||
has_many :users, dependent: :destroy
|
has_many :users, dependent: :destroy
|
||||||
has_many :encrypted_configs, dependent: :destroy
|
has_many :encrypted_configs, dependent: :destroy
|
||||||
|
has_many :account_configs, dependent: :destroy
|
||||||
has_many :templates, dependent: :destroy
|
has_many :templates, dependent: :destroy
|
||||||
|
has_many :submissions, through: :templates
|
||||||
|
has_many :submitters, through: :submissions
|
||||||
has_many :active_users, -> { active }, dependent: :destroy,
|
has_many :active_users, -> { active }, dependent: :destroy,
|
||||||
inverse_of: :account, class_name: 'User'
|
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,10 +21,13 @@
|
|||||||
# fk_rails_... (account_id => accounts.id)
|
# fk_rails_... (account_id => accounts.id)
|
||||||
#
|
#
|
||||||
class EncryptedConfig < ApplicationRecord
|
class EncryptedConfig < ApplicationRecord
|
||||||
FILES_STORAGE_KEY = 'active_storage'
|
CONFIG_KEYS = [
|
||||||
EMAIL_SMTP_KEY = 'action_mailer_smtp'
|
FILES_STORAGE_KEY = 'active_storage',
|
||||||
ESIGN_CERTS_KEY = 'esign_certs'
|
EMAIL_SMTP_KEY = 'action_mailer_smtp',
|
||||||
APP_URL_KEY = 'app_url'
|
ESIGN_CERTS_KEY = 'esign_certs',
|
||||||
|
APP_URL_KEY = 'app_url',
|
||||||
|
WEBHOOK_URL_KEY = 'webhook_url'
|
||||||
|
].freeze
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,16 @@
|
|||||||
#
|
#
|
||||||
# Table name: submissions
|
# Table name: submissions
|
||||||
#
|
#
|
||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
# deleted_at :datetime
|
# deleted_at :datetime
|
||||||
# created_at :datetime not null
|
# source :text not null
|
||||||
# updated_at :datetime not null
|
# template_fields :text
|
||||||
# created_by_user_id :bigint
|
# template_schema :text
|
||||||
# template_id :bigint not null
|
# template_submitters :text
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# created_by_user_id :bigint
|
||||||
|
# template_id :bigint not null
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
@@ -23,9 +27,27 @@
|
|||||||
#
|
#
|
||||||
class Submission < ApplicationRecord
|
class Submission < ApplicationRecord
|
||||||
belongs_to :template
|
belongs_to :template
|
||||||
|
has_one :account, through: :template
|
||||||
belongs_to :created_by_user, class_name: 'User', optional: true
|
belongs_to :created_by_user, class_name: 'User', optional: true
|
||||||
|
|
||||||
has_many :submitters, dependent: :destroy
|
has_many :submitters, dependent: :destroy
|
||||||
|
|
||||||
|
serialize :template_fields, JSON
|
||||||
|
serialize :template_schema, JSON
|
||||||
|
serialize :template_submitters, JSON
|
||||||
|
|
||||||
|
attribute :source, :string, default: 'link'
|
||||||
|
|
||||||
|
has_many :template_schema_documents,
|
||||||
|
->(e) { where(uuid: (e.template_schema.presence || e.template.schema).pluck('attachment_uuid')) },
|
||||||
|
through: :template, source: :documents_attachments
|
||||||
|
|
||||||
scope :active, -> { where(deleted_at: nil) }
|
scope :active, -> { where(deleted_at: nil) }
|
||||||
|
|
||||||
|
enum :source, {
|
||||||
|
invite: 'invite',
|
||||||
|
api: 'api',
|
||||||
|
embed: 'embed',
|
||||||
|
link: 'link'
|
||||||
|
}, scope: false, prefix: true
|
||||||
end
|
end
|
||||||
@@ -6,9 +6,11 @@
|
|||||||
#
|
#
|
||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
# completed_at :datetime
|
# completed_at :datetime
|
||||||
# email :string not null
|
# email :string
|
||||||
# ip :string
|
# ip :string
|
||||||
|
# name :string
|
||||||
# opened_at :datetime
|
# opened_at :datetime
|
||||||
|
# phone :string
|
||||||
# sent_at :datetime
|
# sent_at :datetime
|
||||||
# slug :string not null
|
# slug :string not null
|
||||||
# ua :string
|
# ua :string
|
||||||
@@ -30,6 +32,8 @@
|
|||||||
#
|
#
|
||||||
class Submitter < ApplicationRecord
|
class Submitter < ApplicationRecord
|
||||||
belongs_to :submission
|
belongs_to :submission
|
||||||
|
has_one :template, through: :submission
|
||||||
|
has_one :account, through: :template
|
||||||
|
|
||||||
attribute :values, :string, default: -> { {} }
|
attribute :values, :string, default: -> { {} }
|
||||||
attribute :slug, :string, default: -> { SecureRandom.base58(14) }
|
attribute :slug, :string, default: -> { SecureRandom.base58(14) }
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
# name :string not null
|
# name :string not null
|
||||||
# schema :text not null
|
# schema :text not null
|
||||||
# slug :string not null
|
# slug :string not null
|
||||||
|
# source :text not null
|
||||||
# submitters :text not null
|
# submitters :text not null
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
@@ -37,6 +38,7 @@ class Template < ApplicationRecord
|
|||||||
attribute :schema, :string, default: -> { [] }
|
attribute :schema, :string, default: -> { [] }
|
||||||
attribute :submitters, :string, default: -> { [{ name: DEFAULT_SUBMITTER_NAME, uuid: SecureRandom.uuid }] }
|
attribute :submitters, :string, default: -> { [{ name: DEFAULT_SUBMITTER_NAME, uuid: SecureRandom.uuid }] }
|
||||||
attribute :slug, :string, default: -> { SecureRandom.base58(14) }
|
attribute :slug, :string, default: -> { SecureRandom.base58(14) }
|
||||||
|
attribute :source, :string, default: 'native'
|
||||||
|
|
||||||
serialize :fields, JSON
|
serialize :fields, JSON
|
||||||
serialize :schema, JSON
|
serialize :schema, JSON
|
||||||
@@ -44,6 +46,9 @@ class Template < ApplicationRecord
|
|||||||
|
|
||||||
has_many_attached :documents
|
has_many_attached :documents
|
||||||
|
|
||||||
|
has_many :schema_documents, ->(e) { where(uuid: e.schema.pluck('attachment_uuid')) },
|
||||||
|
class_name: 'ActiveStorage::Attachment', dependent: :destroy, as: :record, inverse_of: :record
|
||||||
|
|
||||||
has_many :submissions, dependent: :destroy
|
has_many :submissions, dependent: :destroy
|
||||||
|
|
||||||
scope :active, -> { where(deleted_at: nil) }
|
scope :active, -> { where(deleted_at: nil) }
|
||||||
|
|||||||
+28
-9
@@ -11,8 +11,8 @@
|
|||||||
# email :string not null
|
# email :string not null
|
||||||
# encrypted_password :string not null
|
# encrypted_password :string not null
|
||||||
# failed_attempts :integer default(0), not null
|
# failed_attempts :integer default(0), not null
|
||||||
# first_name :string not null
|
# first_name :string
|
||||||
# last_name :string not null
|
# last_name :string
|
||||||
# last_sign_in_at :datetime
|
# last_sign_in_at :datetime
|
||||||
# last_sign_in_ip :string
|
# last_sign_in_ip :string
|
||||||
# locked_at :datetime
|
# locked_at :datetime
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
# role :string not null
|
# role :string not null
|
||||||
# sign_in_count :integer default(0), not null
|
# sign_in_count :integer default(0), not null
|
||||||
# unlock_token :string
|
# unlock_token :string
|
||||||
|
# uuid :text not null
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
# account_id :bigint not null
|
# account_id :bigint not null
|
||||||
@@ -32,39 +33,57 @@
|
|||||||
# index_users_on_email (email) UNIQUE
|
# index_users_on_email (email) UNIQUE
|
||||||
# index_users_on_reset_password_token (reset_password_token) UNIQUE
|
# index_users_on_reset_password_token (reset_password_token) UNIQUE
|
||||||
# index_users_on_unlock_token (unlock_token) UNIQUE
|
# index_users_on_unlock_token (unlock_token) UNIQUE
|
||||||
|
# index_users_on_uuid (uuid) UNIQUE
|
||||||
#
|
#
|
||||||
# Foreign Keys
|
# Foreign Keys
|
||||||
#
|
#
|
||||||
# fk_rails_... (account_id => accounts.id)
|
# fk_rails_... (account_id => accounts.id)
|
||||||
#
|
#
|
||||||
class User < ApplicationRecord
|
class User < ApplicationRecord
|
||||||
ROLES = %w[admin].freeze
|
ROLES = [
|
||||||
|
ADMIN_ROLE = 'admin'
|
||||||
|
].freeze
|
||||||
|
|
||||||
EMAIL_REGEXP =
|
EMAIL_REGEXP = /[^@,\s]+@[^@,\s]+/
|
||||||
/[a-z0-9][.']?(?:(?:[a-z0-9_-]++[.'])*[a-z0-9_-]++)*@(?:[a-z0-9]++[.-])*[a-z0-9]++\.[a-z]{2,}/i
|
|
||||||
|
|
||||||
belongs_to :account
|
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 :database_authenticatable, :recoverable, :rememberable, :validatable, :trackable
|
||||||
devise :registerable, :omniauthable, omniauth_providers: [:google_oauth2] if Docuseal.multitenant?
|
devise :registerable, :omniauthable, omniauth_providers: [:google_oauth2] if Docuseal.multitenant?
|
||||||
|
|
||||||
attribute :role, :string, default: 'admin'
|
attribute :role, :string, default: ADMIN_ROLE
|
||||||
|
attribute :uuid, :string, default: -> { SecureRandom.uuid }
|
||||||
|
|
||||||
scope :active, -> { where(deleted_at: nil) }
|
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?
|
def active_for_authentication?
|
||||||
!deleted_at?
|
!deleted_at?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def remember_me
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
def initials
|
def initials
|
||||||
[first_name.first, last_name.first].join.upcase
|
[first_name&.first, last_name&.first].compact_blank.join.upcase
|
||||||
end
|
end
|
||||||
|
|
||||||
def full_name
|
def full_name
|
||||||
[first_name, last_name].join(' ')
|
[first_name, last_name].compact_blank.join(' ')
|
||||||
end
|
end
|
||||||
|
|
||||||
def friendly_name
|
def friendly_name
|
||||||
"#{full_name} <#{email}>"
|
if full_name.present?
|
||||||
|
"#{full_name} <#{email}>"
|
||||||
|
else
|
||||||
|
email
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -11,13 +11,13 @@
|
|||||||
<div class="grid md:grid-cols-2 gap-4">
|
<div class="grid md:grid-cols-2 gap-4">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= ff.label :timezone, class: 'label' %>
|
<%= 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) %>
|
<%= time_zone_options_for_select(current_account.timezone) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= ff.label :locale, 'Time format', class: 'label' %>
|
<%= 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>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<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": "John Doe",
|
||||||
|
"role": "<%= 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">
|
<div class="pb-6 pt-1 space-y-1">
|
||||||
<p class="flex items-center space-x-1 text-xs text-base-content/60">
|
<p class="flex items-center space-x-1 text-xs text-base-content/60">
|
||||||
<%= svg_icon('user', class: 'w-4 h-4') %>
|
<%= svg_icon('user', class: 'w-4 h-4') %>
|
||||||
<span><%= template.author.full_name %></span>
|
<span><%= template.author.full_name.presence || template.author.email %></span>
|
||||||
</p>
|
</p>
|
||||||
<p class="flex items-center space-x-1 text-xs text-base-content/60">
|
<p class="flex items-center space-x-1 text-xs text-base-content/60">
|
||||||
<%= svg_icon('calendar', class: 'w-4 h-4') %>
|
<%= svg_icon('calendar', class: 'w-4 h-4') %>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<div class="max-w-xl mx-auto px-2">
|
<div class="max-w-xl mx-auto px-2">
|
||||||
<h1 class="text-3xl font-bold text-center my-8">Profile Details</h1>
|
<h1 class="text-3xl font-bold text-center my-8">Profile Details</h1>
|
||||||
<%= form_for('', as: resource_name, html: { class: 'space-y-6' }, url: new_registration_path) do |f| %>
|
<%= form_for('', as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: new_registration_path) do |f| %>
|
||||||
|
<% if params[:redir].present? %>
|
||||||
|
<%= hidden_field_tag :redir, params[:redir] %>
|
||||||
|
<% end %>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<%= render 'devise/shared/error_messages', resource: %>
|
<%= render 'devise/shared/error_messages', resource: %>
|
||||||
<%= f.fields_for resource do |ff| %>
|
<%= f.fields_for resource do |ff| %>
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
<div class="max-w-xl mx-auto px-2">
|
<div class="max-w-xl mx-auto px-2">
|
||||||
|
<%= render 'devise/shared/select_server' if Docuseal.multitenant? %>
|
||||||
<h1 class="text-3xl font-bold text-center my-8">Create Free Account</h1>
|
<h1 class="text-3xl font-bold text-center my-8">Create Free Account</h1>
|
||||||
<%= form_for(User.new, html: { class: 'space-y-6' }, url: new_registration_path, method: :get) do |f| %>
|
<%= form_for(User.new, html: { class: 'space-y-6' }, url: new_registration_path, data: { turbo: params[:redir].blank? }, method: :get) do |f| %>
|
||||||
|
<% if params[:redir].present? %>
|
||||||
|
<%= hidden_field_tag :redir, params[:redir] %>
|
||||||
|
<% end %>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.label :email, class: 'label' %>
|
<%= f.label :email, class: 'label' %>
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
<div class="max-w-xl mx-auto px-2">
|
<div class="max-w-xl mx-auto px-2">
|
||||||
|
<%= render 'devise/shared/select_server' if Docuseal.multitenant? %>
|
||||||
<h1 class="text-4xl font-bold text-center my-8">Log In</h1>
|
<h1 class="text-4xl font-bold text-center my-8">Log In</h1>
|
||||||
<%= form_for(resource, as: resource_name, html: { class: 'space-y-6' }, url: session_path(resource_name)) do |f| %>
|
<%= form_for(resource, as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: session_path(resource_name)) do |f| %>
|
||||||
|
<% if params[:redir].present? %>
|
||||||
|
<%= hidden_field_tag :redir, params[:redir] %>
|
||||||
|
<% end %>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.label :email, class: 'label' %>
|
<%= f.label :email, class: 'label' %>
|
||||||
@@ -10,14 +14,6 @@
|
|||||||
<%= f.label :password, class: 'label' %>
|
<%= f.label :password, class: 'label' %>
|
||||||
<%= f.password_field :password, autocomplete: 'current-password', class: 'base-input' %>
|
<%= f.password_field :password, autocomplete: 'current-password', class: 'base-input' %>
|
||||||
</div>
|
</div>
|
||||||
<% if devise_mapping.rememberable? %>
|
|
||||||
<div class="form-control">
|
|
||||||
<%= f.label :remember_me, class: 'flex items-center cursor-pointer' do %>
|
|
||||||
<%= f.check_box :remember_me, checked: true, class: 'base-checkbox' %>
|
|
||||||
<span class="label">Remember me</span>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.button button_title(title: 'Log In', disabled_with: 'Logging In'), class: 'base-button' %>
|
<%= f.button button_title(title: 'Log In', disabled_with: 'Logging In'), class: 'base-button' %>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<%= link_to 'Log in', new_session_path(resource_name), class: 'link link-hover' %>
|
<%= link_to 'Log in', new_session_path(resource_name), class: 'link link-hover' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%- if devise_mapping.registerable? && controller_name != 'registrations' %>
|
<%- if devise_mapping.registerable? && controller_name != 'registrations' %>
|
||||||
<%= link_to 'Create free account', registration_path, class: 'link link-hover' %>
|
<%= link_to 'Create free account', registration_path({ redir: params[:redir] }.compact_blank), class: 'link link-hover' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
|
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
|
||||||
<%= link_to 'Forgot your password?', new_password_path(resource_name), class: 'link link-hover' %>
|
<%= link_to 'Forgot your password?', new_password_path(resource_name), class: 'link link-hover' %>
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<div class="text-center">
|
||||||
|
<div class="join">
|
||||||
|
<a href="https://docuseal.co<%= request.fullpath.gsub('docuseal.eu', 'docuseal.co') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.co' %>">
|
||||||
|
🌎 Global
|
||||||
|
</a>
|
||||||
|
<a href="https://docuseal.eu<%= request.fullpath.gsub('docuseal.co', 'docuseal.eu') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.eu' %>">
|
||||||
|
🇪🇺 Europe
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -25,6 +25,27 @@
|
|||||||
<%= ff.password_field :password, value: value['password'], required: true, class: 'base-input' %>
|
<%= ff.password_field :password, value: value['password'], required: true, class: 'base-input' %>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="form-control">
|
||||||
<%= ff.label :from_email, 'Send from Email', class: 'label' %>
|
<%= ff.label :from_email, 'Send from Email', class: 'label' %>
|
||||||
<%= ff.email_field :from_email, value: value['from_email'], required: true, class: 'base-input' %>
|
<%= ff.email_field :from_email, value: value['from_email'], required: true, class: 'base-input' %>
|
||||||
|
|||||||
Whitespace-only changes.
@@ -1,41 +0,0 @@
|
|||||||
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
|
|
||||||
<%= render 'shared/settings_nav' %>
|
|
||||||
<div class="flex-grow max-w-xl mx-auto">
|
|
||||||
<h1 class="text-4xl font-bold mb-4">PDF Signature</h1>
|
|
||||||
<div id="result">
|
|
||||||
<p class="mb-2">
|
|
||||||
Upload signed PDF file to validate its signature:
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<%= form_for '', url: settings_esign_index_path, method: :post, html: { enctype: 'multipart/form-data' } do |f| %>
|
|
||||||
<%= f.button type: 'submit', class: 'flex' do %>
|
|
||||||
<div class="disabled mb-3">
|
|
||||||
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin inline') %>
|
|
||||||
Analyzing...
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<file-dropzone data-is-direct-upload="false" data-name="verify_attachments" data-submit-on-upload="true" class="w-full">
|
|
||||||
<label for="file" class="w-full block h-32 relative bg-base-200 hover:bg-base-200/70 rounded-md border border-base-content border-dashed">
|
|
||||||
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
|
|
||||||
<div class="flex flex-col items-center">
|
|
||||||
<span data-target="file-dropzone.icon">
|
|
||||||
<%= svg_icon('cloud_upload', class: 'w-10 h-10') %>
|
|
||||||
</span>
|
|
||||||
<span data-target="file-dropzone.loading" class="hidden">
|
|
||||||
<%= svg_icon('loader', class: 'w-10 h-10 animate-spin') %>
|
|
||||||
</span>
|
|
||||||
<div class="font-medium mb-1">
|
|
||||||
Verify Signed PDF
|
|
||||||
</div>
|
|
||||||
<div class="text-xs">
|
|
||||||
<span class="font-medium">Click to upload</span> or drag and drop files
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<input id="file" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="application/pdf" multiple>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</file-dropzone>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<div class="w-0 md:w-52"></div>
|
|
||||||
</div>
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<%= render 'shared/turbo_modal', title: 'Upload Certificate' do %>
|
||||||
|
<%= form_for @cert_record, url: settings_esign_path, html: { class: 'space-y-4', enctype: 'multipart/form-data' }, data: { turbo_frame: :_top } do |f| %>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="form-control">
|
||||||
|
<%= f.label :name, class: 'label' %>
|
||||||
|
<%= f.text_field :name, required: true, class: 'base-input' %>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<%= f.label :file, class: 'label' %>
|
||||||
|
<%= f.file_field :file, required: true %>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text-alt">Use a valid .der, .p12 or .pfx file.</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<%= f.label :password, 'Password (optional)', class: 'label' %>
|
||||||
|
<%= f.text_field :password, class: 'base-input' %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-control pt-2">
|
||||||
|
<%= f.button button_title, class: 'base-button' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<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">
|
||||||
|
<div class="max-w-xl">
|
||||||
|
<h1 class="text-4xl font-bold mb-4">PDF Signature</h1>
|
||||||
|
<div id="result">
|
||||||
|
<p class="mb-2">
|
||||||
|
Upload signed PDF file to validate its signature:
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<%= form_for '', url: verify_pdf_signature_index_path, method: :post, html: { enctype: 'multipart/form-data' } do |f| %>
|
||||||
|
<%= f.button type: 'submit', class: 'flex' do %>
|
||||||
|
<div class="disabled mb-3">
|
||||||
|
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin inline') %>
|
||||||
|
Analyzing...
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<file-dropzone data-is-direct-upload="false" data-name="verify_attachments" data-submit-on-upload="true" class="w-full">
|
||||||
|
<label for="file" class="w-full block h-32 relative bg-base-200 hover:bg-base-200/70 rounded-md border border-base-content border-dashed">
|
||||||
|
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<span data-target="file-dropzone.icon">
|
||||||
|
<%= svg_icon('cloud_upload', class: 'w-10 h-10') %>
|
||||||
|
</span>
|
||||||
|
<span data-target="file-dropzone.loading" class="hidden">
|
||||||
|
<%= svg_icon('loader', class: 'w-10 h-10 animate-spin') %>
|
||||||
|
</span>
|
||||||
|
<div class="font-medium mb-1">
|
||||||
|
Verify Signed PDF
|
||||||
|
</div>
|
||||||
|
<div class="text-xs">
|
||||||
|
<span class="font-medium">Click to upload</span> or drag and drop files
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input id="file" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="application/pdf" multiple>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</file-dropzone>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-end mb-4 mt-8">
|
||||||
|
<h2 class="text-3xl font-bold">Signing Certificates</h2>
|
||||||
|
<%= link_to new_settings_esign_path, class: 'btn btn-primary btn-md', data: { turbo_frame: 'modal' } do %>
|
||||||
|
<%= svg_icon('plus', class: 'w-6 h-6') %>
|
||||||
|
<span>Upload Cert</span>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<%= render 'alert' %>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="table w-full table-lg rounded-b-none overflow-hidden">
|
||||||
|
<thead class="bg-base-200">
|
||||||
|
<tr class="text-neutral uppercase">
|
||||||
|
<th>
|
||||||
|
Name
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Valid To
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Status
|
||||||
|
</th>
|
||||||
|
<th class="text-right" width="1px">
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% @pkcs_list.each do |item| %>
|
||||||
|
<tr scope="row" class="group">
|
||||||
|
<td>
|
||||||
|
<%= item['name'] %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<%= l(item['pkcs'].certificate.not_after.to_date, format: :long, locale: current_account.locale) %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<% if item['status'] == 'default' %>
|
||||||
|
<span class="badge badge-lg badge-info badge-outline">
|
||||||
|
<%= item['status'] %>
|
||||||
|
</span>
|
||||||
|
<% else %>
|
||||||
|
<%= button_to settings_esign_path, method: :put, params: { name: item['name'] }, class: 'btn btn-outline btn-neutral btn-xs whitespace-nowrap', title: 'Delete', data: { turbo_confirm: 'Are you sure?' } do %>
|
||||||
|
Make Default
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<% if item['name'] != EsignSettingsController::DEFAULT_CERT_NAME && item['status'] != 'default' %>
|
||||||
|
<%= button_to settings_esign_path, params: { name: item['name'] }, method: :delete, class: 'btn btn-outline btn-error btn-xs', title: 'Delete', data: { turbo_confirm: 'Are you sure?' } do %>
|
||||||
|
Remove
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -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="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path>
|
||||||
|
<path d="M12 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path>
|
||||||
|
<path d="M12 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 470 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M5 4h4l2 5l-2.5 1.5a11 11 0 0 0 5 5l1.5 -2.5l5 2v4a2 2 0 0 1 -2 2a16 16 0 0 1 -15 -15a2 2 0 0 1 2 -2"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 409 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="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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%= yield %>
|
<%= yield %>
|
||||||
<p>
|
<%= render partial: 'shared/mailer_attribution' %>
|
||||||
---
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Sent using <a href="<%= Docuseal::PRODUCT_URL %>"><%= Docuseal::PRODUCT_NAME %></a> documents software.
|
|
||||||
</p>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<div class="collapse collapse-plus bg-base-200">
|
||||||
|
<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>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<%= render 'logo_placeholder' %>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<div class="alert my-4">
|
||||||
|
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
|
||||||
|
<div>
|
||||||
|
<p class="font-bold">Unlock with DocuSeal Pro</p>
|
||||||
|
<p>
|
||||||
|
Display your company name and logo when signing documents.
|
||||||
|
<br>
|
||||||
|
<a class="link font-medium" target="_blank" href="<%= "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">Learn More</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<div class="collapse collapse-plus bg-base-200">
|
||||||
|
<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>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<div class="collapse collapse-plus bg-base-200">
|
||||||
|
<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>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<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="space-y-4">
|
||||||
|
<%= render 'signature_request_email_form' %>
|
||||||
|
<%= render 'submitter_completed_email_form' %>
|
||||||
|
<%= render 'documents_copy_email_form' %>
|
||||||
|
</div>
|
||||||
|
<p class="text-4xl font-bold mb-4 mt-8">Company Logo</p>
|
||||||
|
<%= render 'logo_form' %>
|
||||||
|
</div>
|
||||||
|
<div class="w-0 md:w-52"></div>
|
||||||
|
</div>
|
||||||
@@ -2,12 +2,7 @@
|
|||||||
<div class="space-y-6 mx-auto">
|
<div class="space-y-6 mx-auto">
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<a href="/" class="flex items-center">
|
<%= render 'start_form/docuseal_logo' %>
|
||||||
<div class="mr-3">
|
|
||||||
<%= render 'shared/logo', width: '50px', height: '50px' %>
|
|
||||||
</div>
|
|
||||||
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center text-4xl font-bold">
|
<div class="text-center text-4xl font-bold">
|
||||||
Email has been sent
|
Email has been sent
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<p>SMTP has been successfully configured!</p>
|
||||||
@@ -1,4 +1 @@
|
|||||||
<div class="text-center px-2">
|
<%= render 'shared/powered_by' %>
|
||||||
Powered by
|
|
||||||
<a href="<%= Docuseal::PRODUCT_URL %>" class="underline"><%= Docuseal::PRODUCT_NAME %></a> - open source documents software
|
|
||||||
</div>
|
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class="disabled">
|
<span class="disabled">
|
||||||
<span class="flex items-center justify-center space-x-2">
|
<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-5 h-5 animate-spin') %>
|
||||||
<span><%= disabled_with %>...</span>
|
<span><%= disabled_with %>...</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -2,16 +2,26 @@
|
|||||||
<label class="<%= local_assigns[:class] %>">
|
<label class="<%= local_assigns[:class] %>">
|
||||||
<input type="radio" class="peer hidden">
|
<input type="radio" class="peer hidden">
|
||||||
<span class="peer-checked:hidden flex items-center space-x-2">
|
<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">
|
<span class="hidden md:inline">
|
||||||
<%= local_assigns[:copy_title] || 'Copy' %>
|
<%= local_assigns[:copy_title] || 'Copy' %>
|
||||||
</span>
|
</span>
|
||||||
|
<% if local_assigns[:copy_title_md] %>
|
||||||
|
<span class="inline md:hidden">
|
||||||
|
<%= local_assigns[:copy_title_md] %>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
</span>
|
</span>
|
||||||
<span class="hidden peer-checked:flex items-center space-x-2">
|
<span class="hidden peer-checked:flex items-center space-x-2">
|
||||||
<%= svg_icon('clipboard_copy', class: local_assigns[:icon_class] || 'w-6 h-6 text-white') %>
|
<%= svg_icon(local_assigns[:copied_icon] || 'clipboard_copy', class: local_assigns[:icon_class] || 'w-6 h-6 text-white') %>
|
||||||
<span class="hidden md:inline">
|
<span class="hidden md:inline">
|
||||||
<%= local_assigns[:copied_title] || 'Copied' %>
|
<%= local_assigns[:copied_title] || 'Copied' %>
|
||||||
</span>
|
</span>
|
||||||
|
<% if local_assigns[:copied_title_md] %>
|
||||||
|
<span class="inline md:hidden">
|
||||||
|
<%= local_assigns[:copied_title_md] %>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</clipboard-copy>
|
</clipboard-copy>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<p>
|
||||||
|
---
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Sent using <a href="<%= Docuseal::PRODUCT_URL %>"><%= Docuseal::PRODUCT_NAME %></a> free document signing.
|
||||||
|
</p>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<div id="flash" class="absolute top-0 w-full h-0">
|
<div id="flash" class="absolute top-0 w-full h-0 z-20">
|
||||||
<div class="max-w-xl mx-auto mt-1.5">
|
<div class="max-w-xl mx-auto mt-1.5">
|
||||||
<div class="px-4 py-3 rounded-2xl bg-base-200 flex items-center justify-between mx-4 md:mx-0">
|
<div class="px-4 py-3 rounded-2xl bg-base-200 flex items-center justify-between mx-4 md:mx-0">
|
||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center space-x-3">
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<%= render 'shared/email_attribution' %>
|
||||||
@@ -15,7 +15,14 @@
|
|||||||
<% if Docuseal.demo? %>
|
<% if Docuseal.demo? %>
|
||||||
<%= render 'shared/github_button' %>
|
<%= render 'shared/github_button' %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= link_to 'Settings', settings_profile_index_path, class: 'hidden md:inline font-medium text-lg mr-2' %>
|
<div class="flex items-center justify-center space-x-4 mr-1">
|
||||||
|
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, class: 'hidden md:inline-flex items-center font-medium text-lg', data: { prefetch: false } do %>
|
||||||
|
Console
|
||||||
|
<span class="badge badge-warning ml-1">New</span>
|
||||||
|
<% end %>
|
||||||
|
<span class="hidden md:inline-flex h-3 border-r border-base-content"></span>
|
||||||
|
<%= link_to 'Settings', settings_profile_index_path, class: 'hidden md:inline-flex font-medium text-lg' %>
|
||||||
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="dropdown dropdown-end">
|
<div class="dropdown dropdown-end">
|
||||||
<label tabindex="0" class="cursor-pointer bg-base-content text-purple-300 rounded-full p-2 w-9 justify-center flex">
|
<label tabindex="0" class="cursor-pointer bg-base-content text-purple-300 rounded-full p-2 w-9 justify-center flex">
|
||||||
@@ -23,14 +30,28 @@
|
|||||||
</label>
|
</label>
|
||||||
<ul tabindex="0" class="z-10 dropdown-content p-2 mt-2 shadow menu text-base bg-base-100 rounded-box whitespace-nowrap text-right">
|
<ul tabindex="0" class="z-10 dropdown-content p-2 mt-2 shadow menu text-base bg-base-100 rounded-box whitespace-nowrap text-right">
|
||||||
<li>
|
<li>
|
||||||
<%= link_to settings_profile_index_path do %>
|
<%= link_to settings_profile_index_path, class: 'flex items-center' do %>
|
||||||
<%= svg_icon('adjustments', class: 'w-5 h-5 stroke-2') %>
|
<%= svg_icon('adjustments', class: 'w-5 h-5 stroke-2') %>
|
||||||
<span class="mr-1">Profile</span>
|
<span class="mr-1">Profile</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
|
<% unless Docuseal.demo? %>
|
||||||
|
<li>
|
||||||
|
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, data: { prefetch: false }, class: 'flex items-center' do %>
|
||||||
|
<%= svg_icon('terminal', class: 'w-5 h-5 stroke-2') %>
|
||||||
|
Console
|
||||||
|
<% end %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
<li>
|
<li>
|
||||||
<%= button_to destroy_user_session_path, method: :delete, data: { turbo: false } do %>
|
<%= link_to settings_esign_path, class: 'flex items-center' do %>
|
||||||
<%= svg_icon('logout', class: 'w-5 h-5 stroke-2 inline') %>
|
<%= svg_icon('zoom_check', class: 'w-5 h-5 stroke-2') %>
|
||||||
|
<span class="mr-1">Verify PDF</span>
|
||||||
|
<% end %>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<%= button_to destroy_user_session_path, method: :delete, data: { turbo: false }, class: 'flex items-center' do %>
|
||||||
|
<%= svg_icon('logout', class: 'w-5 h-5 stroke-2 mr-2 inline') %>
|
||||||
<span class="mr-1">Sign out</span>
|
<span class="mr-1">Sign out</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<div class="text-center px-2">
|
||||||
|
Powered by
|
||||||
|
<a href="<%= Docuseal::PRODUCT_URL %>" class="underline"><%= Docuseal::PRODUCT_NAME %></a> - open source documents software
|
||||||
|
</div>
|
||||||
Loaded 100 of 197 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user