mirror of
https://github.com/docusealco/docuseal
synced 2026-11-05 06:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
15cbdfb20c | ||
|
|
a2cf8fc5b5 | ||
|
|
f94ef3afdc | ||
|
|
9e3a9f0eeb | ||
|
|
1280572e6f | ||
|
|
cba2e84338 | ||
|
|
17ca8f013a | ||
|
|
96ef7ffaf7 | ||
|
|
1bbcdb8073 | ||
|
|
da130c3f4f | ||
|
|
c15f3ecd83 | ||
|
|
df0e0e6a1b | ||
|
|
3d75443d6e | ||
|
|
b9af384cf2 | ||
|
|
c40a8e1a69 | ||
|
|
4c8a706906 | ||
|
|
4c8e38867a | ||
|
|
7ed1b71a6b | ||
|
|
6f3fe1dd7b | ||
|
|
f7ef99a624 | ||
|
|
03ad848d45 |
No files matched your search
@@ -27,3 +27,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:
|
||||||
|
|||||||
+4
-1
@@ -27,7 +27,7 @@ Lint/MissingSuper:
|
|||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Metrics/MethodLength:
|
Metrics/MethodLength:
|
||||||
Max: 20
|
Max: 25
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'db/migrate/**'
|
- 'db/migrate/**'
|
||||||
|
|
||||||
@@ -61,5 +61,8 @@ RSpec/ExampleLength:
|
|||||||
Rails/I18nLocaleTexts:
|
Rails/I18nLocaleTexts:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/SkipsModelValidations:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
Rails/ApplicationController:
|
Rails/ApplicationController:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
@@ -13,9 +13,12 @@ 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-rails_csrf_protection'
|
||||||
gem 'pagy'
|
gem 'pagy'
|
||||||
gem 'pg', require: false
|
gem 'pg', require: false
|
||||||
gem 'premailer-rails'
|
gem 'premailer-rails'
|
||||||
@@ -25,6 +28,7 @@ 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 'shakapacker'
|
gem 'shakapacker'
|
||||||
|
gem 'sidekiq', require: ENV.key?('REDIS_URL')
|
||||||
gem 'sqlite3', require: false
|
gem 'sqlite3', require: false
|
||||||
gem 'strip_attributes'
|
gem 'strip_attributes'
|
||||||
gem 'turbo-rails'
|
gem 'turbo-rails'
|
||||||
|
|||||||
@@ -232,6 +232,7 @@ 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)
|
||||||
|
hashie (5.0.0)
|
||||||
hexapdf (0.32.2)
|
hexapdf (0.32.2)
|
||||||
cmdparse (~> 3.0, >= 3.0.3)
|
cmdparse (~> 3.0, >= 3.0.3)
|
||||||
geom2d (~> 0.3)
|
geom2d (~> 0.3)
|
||||||
@@ -282,6 +283,7 @@ GEM
|
|||||||
minitest (5.18.1)
|
minitest (5.18.1)
|
||||||
msgpack (1.7.1)
|
msgpack (1.7.1)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.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)
|
||||||
@@ -298,7 +300,29 @@ GEM
|
|||||||
nio4r (2.5.9)
|
nio4r (2.5.9)
|
||||||
nokogiri (1.15.2-arm64-darwin)
|
nokogiri (1.15.2-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
|
oauth2 (2.0.9)
|
||||||
|
faraday (>= 0.17.3, < 3.0)
|
||||||
|
jwt (>= 1.0, < 3.0)
|
||||||
|
multi_xml (~> 0.5)
|
||||||
|
rack (>= 1.2, < 4)
|
||||||
|
snaky_hash (~> 2.0)
|
||||||
|
version_gem (~> 1.1)
|
||||||
oj (3.15.0)
|
oj (3.15.0)
|
||||||
|
omniauth (2.1.1)
|
||||||
|
hashie (>= 3.4.6)
|
||||||
|
rack (>= 2.2.3)
|
||||||
|
rack-protection
|
||||||
|
omniauth-google-oauth2 (1.1.1)
|
||||||
|
jwt (>= 2.0)
|
||||||
|
oauth2 (~> 2.0.6)
|
||||||
|
omniauth (~> 2.0)
|
||||||
|
omniauth-oauth2 (~> 1.8.0)
|
||||||
|
omniauth-oauth2 (1.8.0)
|
||||||
|
oauth2 (>= 1.4, < 3)
|
||||||
|
omniauth (~> 2.0)
|
||||||
|
omniauth-rails_csrf_protection (1.0.1)
|
||||||
|
actionpack (>= 4.2)
|
||||||
|
omniauth (~> 2.0)
|
||||||
openssl (3.1.0)
|
openssl (3.1.0)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
@@ -326,6 +350,8 @@ GEM
|
|||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
racc (1.7.1)
|
racc (1.7.1)
|
||||||
rack (2.2.7)
|
rack (2.2.7)
|
||||||
|
rack-protection (3.0.6)
|
||||||
|
rack
|
||||||
rack-proxy (0.7.6)
|
rack-proxy (0.7.6)
|
||||||
rack
|
rack
|
||||||
rack-test (2.1.0)
|
rack-test (2.1.0)
|
||||||
@@ -362,6 +388,8 @@ GEM
|
|||||||
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)
|
||||||
|
connection_pool
|
||||||
regexp_parser (2.8.1)
|
regexp_parser (2.8.1)
|
||||||
reline (0.3.5)
|
reline (0.3.5)
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
@@ -432,6 +460,11 @@ GEM
|
|||||||
rack-proxy (>= 0.6.1)
|
rack-proxy (>= 0.6.1)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
semantic_range (>= 2.3.0)
|
semantic_range (>= 2.3.0)
|
||||||
|
sidekiq (7.1.2)
|
||||||
|
concurrent-ruby (< 2)
|
||||||
|
connection_pool (>= 2.3.0)
|
||||||
|
rack (>= 2.2.4)
|
||||||
|
redis-client (>= 0.14.0)
|
||||||
signet (0.17.0)
|
signet (0.17.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
faraday (>= 0.17.5, < 3.a)
|
faraday (>= 0.17.5, < 3.a)
|
||||||
@@ -444,6 +477,9 @@ GEM
|
|||||||
simplecov-html (0.12.3)
|
simplecov-html (0.12.3)
|
||||||
simplecov_json_formatter (0.1.4)
|
simplecov_json_formatter (0.1.4)
|
||||||
smart_properties (1.17.0)
|
smart_properties (1.17.0)
|
||||||
|
snaky_hash (2.0.1)
|
||||||
|
hashie
|
||||||
|
version_gem (~> 1.1, >= 1.1.1)
|
||||||
sqlite3 (1.5.4)
|
sqlite3 (1.5.4)
|
||||||
mini_portile2 (~> 2.8.0)
|
mini_portile2 (~> 2.8.0)
|
||||||
strip_attributes (1.13.0)
|
strip_attributes (1.13.0)
|
||||||
@@ -462,6 +498,7 @@ GEM
|
|||||||
uber (0.1.0)
|
uber (0.1.0)
|
||||||
unicode-display_width (2.4.2)
|
unicode-display_width (2.4.2)
|
||||||
uniform_notifier (1.16.0)
|
uniform_notifier (1.16.0)
|
||||||
|
version_gem (1.1.3)
|
||||||
warden (1.2.9)
|
warden (1.2.9)
|
||||||
rack (>= 2.0.9)
|
rack (>= 2.0.9)
|
||||||
web-console (4.2.0)
|
web-console (4.2.0)
|
||||||
@@ -503,10 +540,13 @@ DEPENDENCIES
|
|||||||
google-cloud-storage
|
google-cloud-storage
|
||||||
hexapdf
|
hexapdf
|
||||||
image_processing
|
image_processing
|
||||||
|
jwt
|
||||||
letter_opener_web
|
letter_opener_web
|
||||||
lograge
|
lograge
|
||||||
mysql2
|
mysql2
|
||||||
oj
|
oj
|
||||||
|
omniauth-google-oauth2
|
||||||
|
omniauth-rails_csrf_protection
|
||||||
pagy
|
pagy
|
||||||
pg
|
pg
|
||||||
premailer-rails
|
premailer-rails
|
||||||
@@ -522,6 +562,7 @@ DEPENDENCIES
|
|||||||
rubocop-rspec
|
rubocop-rspec
|
||||||
ruby-vips
|
ruby-vips
|
||||||
shakapacker
|
shakapacker
|
||||||
|
sidekiq
|
||||||
simplecov
|
simplecov
|
||||||
sqlite3
|
sqlite3
|
||||||
strip_attributes
|
strip_attributes
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
</p>
|
</p>
|
||||||
<h2 align="center">
|
<h2 align="center">
|
||||||
<a href="https://demo.docuseal.co">✨ Live Demo</a>
|
<a href="https://demo.docuseal.co">✨ Live Demo</a>
|
||||||
|
<span>|</span>
|
||||||
|
<a href="https://docuseal.co/sign_up">☁️ Try in Cloud</a>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
[](https://demo.docuseal.co)
|
[](https://demo.docuseal.co)
|
||||||
@@ -40,13 +42,16 @@ 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
|
||||||
|
|
||||||
## Deploy
|
## Deploy
|
||||||
|
|
||||||
| Heroku | Railway | DigitalOcean |
|
|Heroku|Railway|
|
||||||
| ---------------------------------------------------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
|
|:--:|:---:|
|
||||||
| [<img alt="Deploy on Heroku" src="https://www.herokucdn.com/deploy/button.svg" height="40">](https://heroku.com/deploy?template=https://github.com/docusealco/docuseal-heroku) | [<img alt="Deploy on Railway" src="https://railway.app/button.svg" height="40">](https://railway.app/template/IGoDnc?referralCode=ruU7JR) | [<img alt="Deploy on DigitalOcean" src="https://www.deploytodo.com/do-btn-blue.svg" height="40">](https://cloud.digitalocean.com/apps/new?repo=https://github.com/docusealco/docuseal-digitalocean/tree/master&refcode=421d50f53990) |
|
| [<img alt="Deploy on Heroku" src="https://www.herokucdn.com/deploy/button.svg" height="40">](https://heroku.com/deploy?template=https://github.com/docusealco/docuseal-heroku) | [<img alt="Deploy on Railway" src="https://railway.app/button.svg" height="40">](https://railway.app/template/IGoDnc?referralCode=ruU7JR)|
|
||||||
|
|**DigitalOcean**|**Render**|
|
||||||
|
| [<img alt="Deploy on DigitalOcean" src="https://www.deploytodo.com/do-btn-blue.svg" height="40">](https://cloud.digitalocean.com/apps/new?repo=https://github.com/docusealco/docuseal-digitalocean/tree/master&refcode=421d50f53990) | [<img alt="Deploy to Render" src="https://render.com/images/deploy-to-render-button.svg" height="40">](https://render.com/deploy?repo=https://github.com/docusealco/docuseal-render)
|
||||||
|
|
||||||
|
|
||||||
#### Docker
|
#### Docker
|
||||||
@@ -69,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.
|
||||||
@@ -40,6 +40,8 @@ class AccountsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def app_url_params
|
def app_url_params
|
||||||
|
return {} if params[:encrypted_config].blank?
|
||||||
|
|
||||||
params.require(:encrypted_config).permit(:value)
|
params.require(:encrypted_config).permit(:value)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api
|
||||||
|
class SubmissionsController < ApiBaseController
|
||||||
|
def create
|
||||||
|
template = current_account.templates.find(params[:template_id])
|
||||||
|
|
||||||
|
submissions =
|
||||||
|
if params[:emails].present?
|
||||||
|
Submissions.create_from_emails(template:,
|
||||||
|
user: current_user,
|
||||||
|
source: :api,
|
||||||
|
send_email: params[:send_email] != 'false',
|
||||||
|
emails: params[:emails] || params[:email])
|
||||||
|
else
|
||||||
|
Submissions.create_from_submitters(template:,
|
||||||
|
user: current_user,
|
||||||
|
source: :api,
|
||||||
|
send_email: params[:send_email] != 'false',
|
||||||
|
submissions_attrs: submissions_params[:submission])
|
||||||
|
end
|
||||||
|
|
||||||
|
submitters = submissions.flat_map(&:submitters)
|
||||||
|
|
||||||
|
if params[:send_email] != 'false'
|
||||||
|
submitters.each do |submitter|
|
||||||
|
SubmitterMailer.invitation_email(submitter, message: params[:message]).deliver_later!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: submitters
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def submissions_params
|
||||||
|
params.permit(submission: [{ submitters: [%i[uuid name email]] }])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
module Api
|
module Api
|
||||||
class TemplatesController < ApiBaseController
|
class TemplatesController < ApiBaseController
|
||||||
|
def index
|
||||||
|
render json: current_account.templates
|
||||||
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@template = current_account.templates.find(params[:id])
|
@template = current_account.templates.find(params[:id])
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
module Api
|
module Api
|
||||||
class TemplatesDocumentsController < ApiBaseController
|
class TemplatesDocumentsController < ApiBaseController
|
||||||
def create
|
def create
|
||||||
|
return head :unprocessable_entity if params[:blobs].blank? && params[:files].blank?
|
||||||
|
|
||||||
@template = current_account.templates.find(params[:template_id])
|
@template = current_account.templates.find(params[:template_id])
|
||||||
|
|
||||||
documents =
|
documents =
|
||||||
|
|||||||
@@ -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,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ConsoleRedirectController < ApplicationController
|
||||||
|
def index
|
||||||
|
auth = JsonWebToken.encode(uuid: current_user.uuid,
|
||||||
|
scope: :console,
|
||||||
|
exp: 1.minute.from_now.to_i)
|
||||||
|
|
||||||
|
redirect_to("#{Docuseal::CONSOLE_URL}?#{{ auth: }.to_query}", allow_other_host: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -4,6 +4,7 @@ class DashboardController < ApplicationController
|
|||||||
skip_before_action :authenticate_user!, only: %i[index]
|
skip_before_action :authenticate_user!, only: %i[index]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
return redirect_to Docuseal::PRODUCT_URL, allow_other_host: true if Docuseal.multitenant? && !signed_in?
|
||||||
return render 'pages/landing' unless signed_in?
|
return render 'pages/landing' unless signed_in?
|
||||||
|
|
||||||
templates = current_account.templates.active.preload(:author).order(id: :desc)
|
templates = current_account.templates.active.preload(:author).order(id: :desc)
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||||
|
def google_oauth2
|
||||||
|
@user = Users.from_omniauth(request.env['omniauth.auth'])
|
||||||
|
|
||||||
|
if @user.persisted?
|
||||||
|
flash[:notice] = I18n.t('devise.omniauth_callbacks.success', kind: 'Google')
|
||||||
|
|
||||||
|
sign_in_and_redirect @user, event: :authentication
|
||||||
|
else
|
||||||
|
redirect_to new_registration_path(oauth_callback: true, user: @user.slice(:email, :first_name, :last_name)),
|
||||||
|
notice: 'Please complete registration with Google auth'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,24 +1,44 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class RegistrationsController < Devise::RegistrationsController
|
class RegistrationsController < Devise::RegistrationsController
|
||||||
|
prepend_before_action :require_no_authentication, only: [:show]
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
|
||||||
|
def create
|
||||||
|
super
|
||||||
|
|
||||||
|
Accounts.create_default_template(resource.account) if resource.account.persisted?
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def require_no_authentication
|
||||||
|
super
|
||||||
|
|
||||||
|
flash.clear
|
||||||
|
end
|
||||||
|
|
||||||
def build_resource(_hash = {})
|
def build_resource(_hash = {})
|
||||||
account = Account.new(account_params)
|
account = Account.new(account_params)
|
||||||
account.timezone = Accounts.normalize_timezone(account.timezone)
|
account.timezone = Accounts.normalize_timezone(account.timezone)
|
||||||
|
|
||||||
self.resource = account.users.new(user_params)
|
self.resource = account.users.new(user_params)
|
||||||
|
|
||||||
|
account.name ||= "#{resource.full_name}'s Company" if params[:action] == 'create'
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_params
|
def user_params
|
||||||
return {} if params[:user].blank?
|
return {} if params[:user].blank?
|
||||||
|
|
||||||
params.require(:user).permit(:first_name, :last_name, :email, :password)
|
params.require(:user).permit(:first_name, :last_name, :email, :password).compact_blank.tap do |attrs|
|
||||||
|
attrs[:password] ||= SecureRandom.hex if params[:action] == 'create'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def account_params
|
def account_params
|
||||||
return {} if params[:account].blank?
|
return {} if params[:account].blank?
|
||||||
|
|
||||||
params.require(:account).permit(:name, :timezone)
|
params.require(:account).permit(:name, :timezone).compact_blank
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SessionsController < Devise::SessionsController
|
||||||
|
def create
|
||||||
|
if Docuseal.multitenant? && !User.exists?(email: sign_in_params[:email])
|
||||||
|
return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)),
|
||||||
|
notice: 'Create a new account'
|
||||||
|
end
|
||||||
|
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def require_no_authentication
|
||||||
|
super
|
||||||
|
|
||||||
|
flash.clear
|
||||||
|
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)
|
||||||
|
|||||||
@@ -17,19 +17,29 @@ 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,
|
||||||
|
send_email: params[:send_email] == '1',
|
||||||
|
emails: params[:emails])
|
||||||
else
|
else
|
||||||
create_submissions_from_submitters
|
Submissions.create_from_submitters(template: @template,
|
||||||
|
user: current_user,
|
||||||
|
source: :invite,
|
||||||
|
send_email: params[:send_email] == '1',
|
||||||
|
submissions_attrs: submissions_params[:submission].to_h.values)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
submitters = submissions.flat_map(&:submitters)
|
||||||
|
|
||||||
if params[:send_email] == '1'
|
if params[:send_email] == '1'
|
||||||
submissions.flat_map(&:submitters).each do |submitter|
|
submitters.each do |submitter|
|
||||||
SubmitterMailer.invitation_email(submitter, message: params[:message]).deliver_later!
|
SubmitterMailer.invitation_email(submitter, message: params[:message]).deliver_later!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
redirect_to template_path(@template),
|
redirect_to template_path(@template),
|
||||||
notice: "#{submissions.size} #{'recipient'.pluralize(submissions.size)} added"
|
notice: "#{submitters.size} #{'recipient'.pluralize(submitters.size)} added"
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@@ -43,30 +53,6 @@ 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]] })
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -17,15 +17,20 @@ 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!
|
update_submitter!(submitter)
|
||||||
|
|
||||||
|
Submissions.update_template_fields!(submitter.submission) if submitter.submission.template_fields.blank?
|
||||||
|
|
||||||
|
submitter.submission.save!
|
||||||
|
|
||||||
if submitter.completed_at?
|
if submitter.completed_at?
|
||||||
GenerateSubmitterResultAttachmentsJob.perform_later(submitter)
|
GenerateSubmitterResultAttachmentsJob.perform_later(submitter)
|
||||||
|
|
||||||
|
if submitter.account.encrypted_configs.exists?(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
||||||
|
SendWebhookRequestJob.perform_later(submitter)
|
||||||
|
end
|
||||||
|
|
||||||
submitter.submission.template.account.users.active.each do |user|
|
submitter.submission.template.account.users.active.each do |user|
|
||||||
SubmitterMailer.completed_email(submitter, user).deliver_later!
|
SubmitterMailer.completed_email(submitter, user).deliver_later!
|
||||||
end
|
end
|
||||||
@@ -40,6 +45,16 @@ class SubmitFormController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def update_submitter!(submitter)
|
||||||
|
submitter.values.merge!(normalized_values)
|
||||||
|
submitter.completed_at = Time.current if params[:completed] == 'true'
|
||||||
|
submitter.opened_at ||= Time.current
|
||||||
|
|
||||||
|
submitter.save!
|
||||||
|
|
||||||
|
submitter
|
||||||
|
end
|
||||||
|
|
||||||
def normalized_values
|
def normalized_values
|
||||||
params.fetch(:values, {}).to_unsafe_h.transform_values do |v|
|
params.fetch(:values, {}).to_unsafe_h.transform_values do |v|
|
||||||
if params[:cast_boolean] == 'true'
|
if params[:cast_boolean] == 'true'
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default actionable(class extends HTMLElement {
|
|||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,14 +21,14 @@
|
|||||||
</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] }}
|
||||||
<template v-if="field.type === 'checkbox'">
|
<template v-if="field.type === 'checkbox' && !field.name">
|
||||||
{{ fieldIndex + 1 }}
|
{{ fieldIndex + 1 }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="!field.required">
|
<template v-else-if="!field.required && field.type !== 'checkbox'">
|
||||||
(optional)
|
(optional)
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -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="`#page-${area.attachment_uuid}-${area.page}`"
|
||||||
|
>
|
||||||
<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,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<div class="space-y-3 mt-5">
|
<div class="space-y-3 mt-5">
|
||||||
<button
|
<button
|
||||||
v-if="canSendEmail"
|
v-if="canSendEmail && !isDemo"
|
||||||
class="white-button flex items-center space-x-1 w-full"
|
class="white-button flex items-center space-x-1 w-full"
|
||||||
:disabled="isSendingCopy"
|
:disabled="isSendingCopy"
|
||||||
@click.prevent="sendCopyToEmail"
|
@click.prevent="sendCopyToEmail"
|
||||||
@@ -51,6 +51,16 @@
|
|||||||
Star on Github
|
Star on Github
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a
|
||||||
|
v-if="isDemo"
|
||||||
|
href="https://docuseal.co/sign_up"
|
||||||
|
class="white-button flex items-center space-x-1 w-full"
|
||||||
|
>
|
||||||
|
<IconLogin />
|
||||||
|
<span>
|
||||||
|
Create a Free Account
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center mt-4">
|
<div class="text-center mt-4">
|
||||||
Signed with
|
Signed with
|
||||||
@@ -64,7 +74,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop } from '@tabler/icons-vue'
|
import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop, IconLogin } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FormCompleted',
|
name: 'FormCompleted',
|
||||||
@@ -73,6 +83,7 @@ export default {
|
|||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
IconBrandGithub,
|
IconBrandGithub,
|
||||||
IconMail,
|
IconMail,
|
||||||
|
IconLogin,
|
||||||
IconDownload
|
IconDownload
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -4,262 +4,299 @@
|
|||||||
: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)]"
|
@focus-step="[saveStep(), goToStep($event, false, true), currentField.type !== 'checkbox' ? isFormVisible = true : '']"
|
||||||
/>
|
/>
|
||||||
<div>
|
<button
|
||||||
<form
|
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"
|
||||||
|
@click.prevent="isFormVisible = true"
|
||||||
|
>
|
||||||
|
Submit Form
|
||||||
|
<IconArrowsDiagonal
|
||||||
|
class="absolute right-0 mr-4"
|
||||||
|
:width="20"
|
||||||
|
:height="20"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-show="isFormVisible"
|
||||||
|
id="form_container"
|
||||||
|
class="shadow-md bg-base-100 absolute bottom-0 md:bottom-4 w-full border-base-200 border p-4 rounded"
|
||||||
|
>
|
||||||
|
<button
|
||||||
v-if="!isCompleted"
|
v-if="!isCompleted"
|
||||||
ref="form"
|
class="absolute right-0 mr-2 mt-2 top-0 hidden md:block"
|
||||||
:action="submitPath"
|
title="Minimize"
|
||||||
method="post"
|
@click.prevent="isFormVisible = false"
|
||||||
class="md:mx-16"
|
|
||||||
@submit.prevent="submitStep"
|
|
||||||
>
|
>
|
||||||
<input
|
<IconArrowsDiagonalMinimize2
|
||||||
type="hidden"
|
:width="20"
|
||||||
name="authenticity_token"
|
:height="20"
|
||||||
:value="authenticityToken"
|
/>
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<form
|
||||||
|
v-if="!isCompleted"
|
||||||
|
ref="form"
|
||||||
|
:action="submitPath"
|
||||||
|
method="post"
|
||||||
|
class="md:mx-16"
|
||||||
|
@submit.prevent="submitStep"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
value="put"
|
type="hidden"
|
||||||
name="_method"
|
name="authenticity_token"
|
||||||
type="hidden"
|
:value="authenticityToken"
|
||||||
>
|
>
|
||||||
<div class="md:mt-4">
|
<input
|
||||||
<div v-if="['cells', 'text'].includes(currentField.type)">
|
value="put"
|
||||||
<label
|
name="_method"
|
||||||
v-if="currentField.name"
|
type="hidden"
|
||||||
:for="currentField.uuid"
|
>
|
||||||
class="label text-2xl mb-2"
|
<div class="md:mt-4">
|
||||||
>{{ currentField.name }}
|
<div v-if="['cells', 'text'].includes(currentField.type)">
|
||||||
<template v-if="!currentField.required">(optional)</template>
|
<label
|
||||||
</label>
|
v-if="currentField.name"
|
||||||
<div
|
:for="currentField.uuid"
|
||||||
v-else
|
class="label text-2xl mb-2"
|
||||||
class="py-1"
|
>{{ currentField.name }}
|
||||||
/>
|
<template v-if="!currentField.required">(optional)</template>
|
||||||
<div>
|
</label>
|
||||||
<input
|
<div
|
||||||
|
v-else
|
||||||
|
class="py-1"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
:id="currentField.uuid"
|
||||||
|
v-model="values[currentField.uuid]"
|
||||||
|
class="base-input !text-2xl w-full"
|
||||||
|
:required="currentField.required"
|
||||||
|
:placeholder="`Type here...${currentField.required ? '' : ' (optional)'}`"
|
||||||
|
type="text"
|
||||||
|
:name="`values[${currentField.uuid}]`"
|
||||||
|
@focus="$refs.areas.scrollIntoField(currentField)"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="currentField.type === 'date'">
|
||||||
|
<label
|
||||||
|
v-if="currentField.name"
|
||||||
|
:for="currentField.uuid"
|
||||||
|
class="label text-2xl mb-2"
|
||||||
|
>{{ currentField.name }}
|
||||||
|
<template v-if="!currentField.required">(optional)</template>
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="py-1"
|
||||||
|
/>
|
||||||
|
<div class="text-center">
|
||||||
|
<input
|
||||||
|
:id="currentField.uuid"
|
||||||
|
v-model="values[currentField.uuid]"
|
||||||
|
class="base-input !text-2xl text-center w-full"
|
||||||
|
:required="currentField.required"
|
||||||
|
type="date"
|
||||||
|
:name="`values[${currentField.uuid}]`"
|
||||||
|
@focus="$refs.areas.scrollIntoField(currentField)"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="currentField.type === 'select'">
|
||||||
|
<label
|
||||||
|
v-if="currentField.name"
|
||||||
|
:for="currentField.uuid"
|
||||||
|
class="label text-2xl mb-2"
|
||||||
|
>{{ currentField.name }}
|
||||||
|
<template v-if="!currentField.required">(optional)</template>
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="py-1"
|
||||||
|
/>
|
||||||
|
<select
|
||||||
:id="currentField.uuid"
|
:id="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
|
||||||
class="base-input !text-2xl w-full"
|
|
||||||
:required="currentField.required"
|
:required="currentField.required"
|
||||||
:placeholder="`Type here...${currentField.required ? '' : ' (optional)'}`"
|
class="select base-input !text-2xl w-full text-center font-normal"
|
||||||
type="text"
|
|
||||||
:name="`values[${currentField.uuid}]`"
|
:name="`values[${currentField.uuid}]`"
|
||||||
|
@change="values[currentField.uuid] = $event.target.value"
|
||||||
@focus="$refs.areas.scrollIntoField(currentField)"
|
@focus="$refs.areas.scrollIntoField(currentField)"
|
||||||
>
|
>
|
||||||
</div>
|
<option
|
||||||
</div>
|
value=""
|
||||||
<div v-else-if="currentField.type === 'date'">
|
:selected="!values[currentField.uuid]"
|
||||||
<label
|
>
|
||||||
v-if="currentField.name"
|
Select your option
|
||||||
:for="currentField.uuid"
|
</option>
|
||||||
class="label text-2xl mb-2"
|
<option
|
||||||
>{{ currentField.name }}
|
|
||||||
<template v-if="!currentField.required">(optional)</template>
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="py-1"
|
|
||||||
/>
|
|
||||||
<div class="text-center">
|
|
||||||
<input
|
|
||||||
:id="currentField.uuid"
|
|
||||||
v-model="values[currentField.uuid]"
|
|
||||||
class="base-input !text-2xl text-center w-full"
|
|
||||||
:required="currentField.required"
|
|
||||||
type="date"
|
|
||||||
:name="`values[${currentField.uuid}]`"
|
|
||||||
@focus="$refs.areas.scrollIntoField(currentField)"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="currentField.type === 'select'">
|
|
||||||
<label
|
|
||||||
v-if="currentField.name"
|
|
||||||
:for="currentField.uuid"
|
|
||||||
class="label text-2xl mb-2"
|
|
||||||
>{{ currentField.name }}
|
|
||||||
<template v-if="!currentField.required">(optional)</template>
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="py-1"
|
|
||||||
/>
|
|
||||||
<select
|
|
||||||
:id="currentField.uuid"
|
|
||||||
:required="currentField.required"
|
|
||||||
class="select base-input !text-2xl w-full text-center font-normal"
|
|
||||||
:name="`values[${currentField.uuid}]`"
|
|
||||||
@change="values[currentField.uuid] = $event.target.value"
|
|
||||||
@focus="$refs.areas.scrollIntoField(currentField)"
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
value=""
|
|
||||||
:selected="!values[currentField.uuid]"
|
|
||||||
>
|
|
||||||
Select your option
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
v-for="(option, index) in currentField.options"
|
|
||||||
:key="index"
|
|
||||||
:selected="values[currentField.uuid] == option"
|
|
||||||
:value="option"
|
|
||||||
>
|
|
||||||
{{ option }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="currentField.type === 'radio'">
|
|
||||||
<label
|
|
||||||
v-if="currentField.name"
|
|
||||||
:for="currentField.uuid"
|
|
||||||
class="label text-2xl mb-2"
|
|
||||||
>{{ currentField.name }}
|
|
||||||
<template v-if="!currentField.required">(optional)</template>
|
|
||||||
</label>
|
|
||||||
<div class="flex w-full">
|
|
||||||
<div class="space-y-3.5 mx-auto">
|
|
||||||
<div
|
|
||||||
v-for="(option, index) in currentField.options"
|
v-for="(option, index) in currentField.options"
|
||||||
:key="index"
|
:key="index"
|
||||||
|
:selected="values[currentField.uuid] == option"
|
||||||
|
:value="option"
|
||||||
>
|
>
|
||||||
<label
|
{{ option }}
|
||||||
:for="currentField.uuid + option"
|
</option>
|
||||||
class="flex items-center space-x-3"
|
</select>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="currentField.type === 'radio'">
|
||||||
|
<label
|
||||||
|
v-if="currentField.name"
|
||||||
|
:for="currentField.uuid"
|
||||||
|
class="label text-2xl mb-2"
|
||||||
|
>{{ currentField.name }}
|
||||||
|
<template v-if="!currentField.required">(optional)</template>
|
||||||
|
</label>
|
||||||
|
<div class="flex w-full">
|
||||||
|
<div class="space-y-3.5 mx-auto">
|
||||||
|
<div
|
||||||
|
v-for="(option, index) in currentField.options"
|
||||||
|
:key="index"
|
||||||
>
|
>
|
||||||
<input
|
<label
|
||||||
:id="currentField.uuid + option"
|
:for="currentField.uuid + option"
|
||||||
v-model="values[currentField.uuid]"
|
class="flex items-center space-x-3"
|
||||||
type="radio"
|
|
||||||
class="base-radio !h-7 !w-7"
|
|
||||||
:name="`values[${currentField.uuid}]`"
|
|
||||||
:value="option"
|
|
||||||
:required="currentField.required"
|
|
||||||
>
|
>
|
||||||
<span class="text-xl">
|
<input
|
||||||
{{ option }}
|
:id="currentField.uuid + option"
|
||||||
</span>
|
v-model="values[currentField.uuid]"
|
||||||
</label>
|
type="radio"
|
||||||
|
class="base-radio !h-7 !w-7"
|
||||||
|
:name="`values[${currentField.uuid}]`"
|
||||||
|
:value="option"
|
||||||
|
:required="currentField.required"
|
||||||
|
>
|
||||||
|
<span class="text-xl">
|
||||||
|
{{ option }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<MultiSelectStep
|
||||||
<MultiSelectStep
|
v-else-if="currentField.type === 'multiple'"
|
||||||
v-else-if="currentField.type === 'multiple'"
|
v-model="values[currentField.uuid]"
|
||||||
v-model="values[currentField.uuid]"
|
:field="currentField"
|
||||||
:field="currentField"
|
/>
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-else-if="currentField.type === 'checkbox'"
|
|
||||||
class="flex w-full"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
name="cast_boolean"
|
|
||||||
value="true"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="space-y-3.5 mx-auto"
|
v-else-if="currentField.type === 'checkbox'"
|
||||||
|
class="flex w-full max-h-44 overflow-y-auto"
|
||||||
>
|
>
|
||||||
<div
|
<input
|
||||||
v-for="(field, index) in currentStepFields"
|
type="hidden"
|
||||||
:key="field.uuid"
|
name="cast_boolean"
|
||||||
|
value="true"
|
||||||
>
|
>
|
||||||
<label
|
<div
|
||||||
:for="field.uuid"
|
class="space-y-3.5 mx-auto"
|
||||||
class="flex items-center space-x-3"
|
>
|
||||||
>
|
<template v-if="isAnonymousChecboxes">
|
||||||
<input
|
Complete hightlighted checkboxes and click <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? 'submit' : 'next' }}</span>.
|
||||||
type="hidden"
|
</template>
|
||||||
:name="`values[${field.uuid}]`"
|
<template v-else>
|
||||||
:value="!!values[field.uuid]"
|
<div
|
||||||
|
v-for="(field, index) in currentStepFields"
|
||||||
|
:key="field.uuid"
|
||||||
>
|
>
|
||||||
<input
|
<label
|
||||||
:id="field.uuid"
|
:for="field.uuid"
|
||||||
type="checkbox"
|
class="flex items-center space-x-3"
|
||||||
class="base-checkbox !h-7 !w-7"
|
>
|
||||||
:checked="!!values[field.uuid]"
|
<input
|
||||||
@click="[$refs.areas.scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
|
type="hidden"
|
||||||
>
|
:name="`values[${field.uuid}]`"
|
||||||
<span class="text-xl">
|
:value="!!values[field.uuid]"
|
||||||
{{ currentField.name || currentField.type + ' ' + (index + 1) }}
|
>
|
||||||
</span>
|
<input
|
||||||
</label>
|
:id="field.uuid"
|
||||||
|
type="checkbox"
|
||||||
|
class="base-checkbox !h-7 !w-7"
|
||||||
|
:checked="!!values[field.uuid]"
|
||||||
|
@click="[$refs.areas.scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
|
||||||
|
>
|
||||||
|
<span class="text-xl">
|
||||||
|
{{ field.name || field.type + ' ' + (index + 1) }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ImageStep
|
||||||
|
v-else-if="currentField.type === 'image'"
|
||||||
|
v-model="values[currentField.uuid]"
|
||||||
|
:field="currentField"
|
||||||
|
:is-direct-upload="isDirectUpload"
|
||||||
|
:attachments-index="attachmentsIndex"
|
||||||
|
:submitter-slug="submitterSlug"
|
||||||
|
@attached="[attachments.push($event), $refs.areas.scrollIntoField(currentField)]"
|
||||||
|
/>
|
||||||
|
<SignatureStep
|
||||||
|
v-else-if="currentField.type === 'signature'"
|
||||||
|
ref="currentStep"
|
||||||
|
v-model="values[currentField.uuid]"
|
||||||
|
:field="currentField"
|
||||||
|
:is-direct-upload="isDirectUpload"
|
||||||
|
:attachments-index="attachmentsIndex"
|
||||||
|
:submitter-slug="submitterSlug"
|
||||||
|
@attached="attachments.push($event)"
|
||||||
|
@start="$refs.areas.scrollIntoField(currentField)"
|
||||||
|
@minimize="isFormVisible = false"
|
||||||
|
/>
|
||||||
|
<AttachmentStep
|
||||||
|
v-else-if="currentField.type === 'file'"
|
||||||
|
v-model="values[currentField.uuid]"
|
||||||
|
:is-direct-upload="isDirectUpload"
|
||||||
|
:field="currentField"
|
||||||
|
:attachments-index="attachmentsIndex"
|
||||||
|
:submitter-slug="submitterSlug"
|
||||||
|
@attached="[attachments.push($event), $refs.areas.scrollIntoField(currentField)]"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ImageStep
|
<div class="mt-6 md:mt-8">
|
||||||
v-else-if="currentField.type === 'image'"
|
<button
|
||||||
v-model="values[currentField.uuid]"
|
type="submit"
|
||||||
:field="currentField"
|
class="base-button w-full flex justify-center"
|
||||||
:is-direct-upload="isDirectUpload"
|
:disabled="isButtonDisabled"
|
||||||
:attachments-index="attachmentsIndex"
|
>
|
||||||
:submitter-slug="submitterSlug"
|
<span class="flex">
|
||||||
@attached="[attachments.push($event), $refs.areas.scrollIntoField(currentField)]"
|
<IconInnerShadowTop
|
||||||
/>
|
v-if="isSubmitting"
|
||||||
<SignatureStep
|
class="mr-1 animate-spin"
|
||||||
v-else-if="currentField.type === 'signature'"
|
/>
|
||||||
ref="currentStep"
|
<span v-if="stepFields.length === currentStep + 1">
|
||||||
v-model="values[currentField.uuid]"
|
Submit
|
||||||
:field="currentField"
|
</span>
|
||||||
:is-direct-upload="isDirectUpload"
|
<span v-else>
|
||||||
:attachments-index="attachmentsIndex"
|
Next
|
||||||
:submitter-slug="submitterSlug"
|
</span><span
|
||||||
@attached="attachments.push($event)"
|
v-if="isSubmitting"
|
||||||
/>
|
class="w-6 flex justify-start mr-1"
|
||||||
<AttachmentStep
|
><span>...</span></span>
|
||||||
v-else-if="currentField.type === 'file'"
|
|
||||||
v-model="values[currentField.uuid]"
|
|
||||||
:is-direct-upload="isDirectUpload"
|
|
||||||
:field="currentField"
|
|
||||||
:attachments-index="attachmentsIndex"
|
|
||||||
:submitter-slug="submitterSlug"
|
|
||||||
@attached="[attachments.push($event), $refs.areas.scrollIntoField(currentField)]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="mt-6 md:mt-8">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="base-button w-full flex justify-center"
|
|
||||||
:disabled="isButtonDisabled"
|
|
||||||
>
|
|
||||||
<span class="flex">
|
|
||||||
<IconInnerShadowTop
|
|
||||||
v-if="isSubmitting"
|
|
||||||
class="mr-1 animate-spin"
|
|
||||||
/>
|
|
||||||
<span v-if="stepFields.length === currentStep + 1">
|
|
||||||
Submit
|
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
</button>
|
||||||
Next
|
</div>
|
||||||
</span><span
|
</form>
|
||||||
v-if="isSubmitting"
|
<FormCompleted
|
||||||
class="w-6 flex justify-start mr-1"
|
v-else
|
||||||
><span>...</span></span>
|
:is-demo="isDemo"
|
||||||
</span>
|
:can-send-email="canSendEmail"
|
||||||
</button>
|
:submitter-slug="submitterSlug"
|
||||||
</div>
|
/>
|
||||||
</form>
|
<div class="flex justify-center">
|
||||||
<FormCompleted
|
<div class="flex items-center mt-5 mb-1">
|
||||||
v-else
|
<a
|
||||||
:is-demo="isDemo"
|
v-for="(step, index) in stepFields"
|
||||||
:can-send-email="canSendEmail"
|
:key="step[0].uuid"
|
||||||
:submitter-slug="submitterSlug"
|
href="#"
|
||||||
/>
|
class="inline border border-base-300 h-3 w-3 rounded-full mx-1"
|
||||||
<div class="flex justify-center">
|
:class="{ 'bg-base-300': index === currentStep, 'bg-base-content': index < currentStep || isCompleted, 'bg-white': index > currentStep }"
|
||||||
<div class="flex items-center mt-5 mb-1">
|
@click.prevent="isCompleted ? '' : [saveStep(), goToStep(step, true)]"
|
||||||
<a
|
/>
|
||||||
v-for="(step, index) in stepFields"
|
</div>
|
||||||
:key="step[0].uuid"
|
|
||||||
href="#"
|
|
||||||
class="inline border border-base-300 h-3 w-3 rounded-full mx-1"
|
|
||||||
:class="{ 'bg-base-300': index === currentStep, 'bg-base-content': index < currentStep || isCompleted, 'bg-white': index > currentStep }"
|
|
||||||
@click.prevent="isCompleted ? '' : [saveStep(), goToStep(step, true)]"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -272,7 +309,7 @@ import SignatureStep from './signature_step'
|
|||||||
import AttachmentStep from './attachment_step'
|
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 } from '@tabler/icons-vue'
|
import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SubmissionForm',
|
name: 'SubmissionForm',
|
||||||
@@ -283,6 +320,8 @@ export default {
|
|||||||
AttachmentStep,
|
AttachmentStep,
|
||||||
MultiSelectStep,
|
MultiSelectStep,
|
||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
|
IconArrowsDiagonal,
|
||||||
|
IconArrowsDiagonalMinimize2,
|
||||||
FormCompleted
|
FormCompleted
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
@@ -332,6 +371,7 @@ export default {
|
|||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isCompleted: false,
|
isCompleted: false,
|
||||||
|
isFormVisible: true,
|
||||||
currentStep: 0,
|
currentStep: 0,
|
||||||
isSubmitting: false
|
isSubmitting: false
|
||||||
}
|
}
|
||||||
@@ -340,6 +380,9 @@ export default {
|
|||||||
currentStepFields () {
|
currentStepFields () {
|
||||||
return this.stepFields[this.currentStep]
|
return this.stepFields[this.currentStep]
|
||||||
},
|
},
|
||||||
|
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) ||
|
||||||
@@ -396,7 +439,7 @@ export default {
|
|||||||
|
|
||||||
this.$refs.form.querySelector('input[type="date"], input[type="text"], select')?.focus()
|
this.$refs.form.querySelector('input[type="date"], input[type="text"], select')?.focus()
|
||||||
|
|
||||||
if (clickUpload && !this.values[this.currentField.uuid]) {
|
if (clickUpload && !this.values[this.currentField.uuid] && ['file', 'image'].includes(this.currentField.type)) {
|
||||||
this.$refs.form.querySelector('input[type="file"]')?.click()
|
this.$refs.form.querySelector('input[type="file"]')?.click()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,22 +4,62 @@
|
|||||||
<label
|
<label
|
||||||
class="label text-2xl"
|
class="label text-2xl"
|
||||||
>{{ field.name || 'Signature' }}</label>
|
>{{ field.name || 'Signature' }}</label>
|
||||||
<button
|
<div class="space-x-2 flex">
|
||||||
v-if="modelValue"
|
<span
|
||||||
class="btn btn-outline btn-sm"
|
class="tooltip"
|
||||||
@click.prevent="remove"
|
data-tip="Type text"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<button
|
||||||
Redraw
|
class="btn btn-sm btn-circle"
|
||||||
</button>
|
:class="{ 'btn-neutral': isTextSignature, 'btn-outline': !isTextSignature }"
|
||||||
<button
|
@click.prevent="toggleTextInput"
|
||||||
v-else
|
>
|
||||||
class="btn btn-outline btn-sm"
|
<IconTextSize :width="16" />
|
||||||
@click.prevent="clear"
|
</button>
|
||||||
>
|
</span>
|
||||||
<IconReload :width="16" />
|
<span
|
||||||
Clear
|
class="tooltip"
|
||||||
</button>
|
data-tip="Take photo"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="btn btn-outline btn-sm btn-circle"
|
||||||
|
>
|
||||||
|
<IconCamera :width="16" />
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
hidden
|
||||||
|
accept="image/*"
|
||||||
|
@change="drawImage"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
v-if="modelValue"
|
||||||
|
class="btn btn-outline btn-sm"
|
||||||
|
@click.prevent="remove"
|
||||||
|
>
|
||||||
|
<IconReload :width="16" />
|
||||||
|
Redraw
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
class="btn btn-outline btn-sm"
|
||||||
|
@click.prevent="clear"
|
||||||
|
>
|
||||||
|
<IconReload :width="16" />
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
title="Minimize"
|
||||||
|
class="py-1.5 inline md:hidden"
|
||||||
|
@click.prevent="$emit('minimize')"
|
||||||
|
>
|
||||||
|
<IconArrowsDiagonalMinimize2
|
||||||
|
:width="20"
|
||||||
|
:height="20"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
:value="modelValue"
|
:value="modelValue"
|
||||||
@@ -29,23 +69,35 @@
|
|||||||
<img
|
<img
|
||||||
v-if="modelValue"
|
v-if="modelValue"
|
||||||
:src="attachmentsIndex[modelValue].url"
|
:src="attachmentsIndex[modelValue].url"
|
||||||
class="w-full bg-white border border-base-300 rounded"
|
class="mx-auto bg-white border border-base-300 rounded max-h-72"
|
||||||
>
|
>
|
||||||
<canvas
|
<canvas
|
||||||
v-show="!modelValue"
|
v-show="!modelValue"
|
||||||
ref="canvas"
|
ref="canvas"
|
||||||
class="bg-white border border-base-300 rounded"
|
class="bg-white border border-base-300 rounded"
|
||||||
/>
|
/>
|
||||||
|
<input
|
||||||
|
v-if="isTextSignature"
|
||||||
|
ref="textInput"
|
||||||
|
class="base-input !text-2xl w-full mt-6"
|
||||||
|
:required="field.required"
|
||||||
|
:placeholder="`Type signature here...`"
|
||||||
|
type="text"
|
||||||
|
@input="updateWrittenSignature"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconReload } from '@tabler/icons-vue'
|
import { IconReload, IconCamera, IconTextSize, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SignatureStep',
|
name: 'SignatureStep',
|
||||||
components: {
|
components: {
|
||||||
IconReload
|
IconReload,
|
||||||
|
IconCamera,
|
||||||
|
IconTextSize,
|
||||||
|
IconArrowsDiagonalMinimize2
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
@@ -72,10 +124,11 @@ export default {
|
|||||||
default: ''
|
default: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['attached', 'update:model-value'],
|
emits: ['attached', 'update:model-value', 'start', 'minimize'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isSignatureStarted: false
|
isSignatureStarted: false,
|
||||||
|
isTextSignature: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted () {
|
async mounted () {
|
||||||
@@ -94,6 +147,8 @@ export default {
|
|||||||
|
|
||||||
this.pad.addEventListener('beginStroke', () => {
|
this.pad.addEventListener('beginStroke', () => {
|
||||||
this.isSignatureStarted = true
|
this.isSignatureStarted = true
|
||||||
|
|
||||||
|
this.$emit('start')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -104,6 +159,134 @@ export default {
|
|||||||
this.pad.clear()
|
this.pad.clear()
|
||||||
|
|
||||||
this.isSignatureStarted = false
|
this.isSignatureStarted = false
|
||||||
|
|
||||||
|
if (this.$refs.textInput) {
|
||||||
|
this.$refs.textInput.value = ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateWrittenSignature (e) {
|
||||||
|
this.isSignatureStarted = true
|
||||||
|
|
||||||
|
const canvas = this.$refs.canvas
|
||||||
|
const context = canvas.getContext('2d')
|
||||||
|
|
||||||
|
const fontFamily = 'Arial'
|
||||||
|
const fontSize = '44px'
|
||||||
|
const fontStyle = 'italic'
|
||||||
|
const fontWeight = ''
|
||||||
|
|
||||||
|
context.font = fontStyle + ' ' + fontWeight + ' ' + fontSize + ' ' + fontFamily
|
||||||
|
context.textAlign = 'center'
|
||||||
|
|
||||||
|
context.clearRect(0, 0, canvas.width, canvas.height)
|
||||||
|
context.fillText(e.target.value, canvas.width / 2, canvas.height / 2 + 11)
|
||||||
|
},
|
||||||
|
toggleTextInput () {
|
||||||
|
this.remove()
|
||||||
|
this.isTextSignature = !this.isTextSignature
|
||||||
|
|
||||||
|
if (this.isTextSignature) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.textInput.focus()
|
||||||
|
|
||||||
|
this.$emit('start')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
drawImage (event) {
|
||||||
|
this.remove()
|
||||||
|
this.isSignatureStarted = true
|
||||||
|
|
||||||
|
const file = event.target.files[0]
|
||||||
|
|
||||||
|
if (file && file.type.match('image.*')) {
|
||||||
|
const reader = new FileReader()
|
||||||
|
|
||||||
|
reader.onload = (event) => {
|
||||||
|
const img = new Image()
|
||||||
|
|
||||||
|
img.src = event.target.result
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
const canvas = this.$refs.canvas
|
||||||
|
const context = canvas.getContext('2d')
|
||||||
|
|
||||||
|
const aspectRatio = img.width / img.height
|
||||||
|
|
||||||
|
let targetWidth = canvas.width
|
||||||
|
let targetHeight = canvas.height
|
||||||
|
|
||||||
|
if (canvas.width / canvas.height > aspectRatio) {
|
||||||
|
targetWidth = canvas.height * aspectRatio
|
||||||
|
} else {
|
||||||
|
targetHeight = canvas.width / aspectRatio
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetHeight > targetWidth) {
|
||||||
|
const scale = targetHeight / targetWidth
|
||||||
|
targetWidth = targetWidth * scale
|
||||||
|
targetHeight = targetHeight * scale
|
||||||
|
}
|
||||||
|
|
||||||
|
const x = (canvas.width - targetWidth) / 2
|
||||||
|
const y = (canvas.height - targetHeight) / 2
|
||||||
|
|
||||||
|
context.clearRect(0, 0, canvas.width, canvas.height)
|
||||||
|
context.drawImage(img, x, y, targetWidth, targetHeight)
|
||||||
|
|
||||||
|
this.$emit('start')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cropCanvasAndExportToPNG (canvas) {
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
|
||||||
|
const width = canvas.width
|
||||||
|
const height = canvas.height
|
||||||
|
|
||||||
|
let topmost = height
|
||||||
|
let bottommost = 0
|
||||||
|
let leftmost = width
|
||||||
|
let rightmost = 0
|
||||||
|
|
||||||
|
const imageData = ctx.getImageData(0, 0, width, height)
|
||||||
|
const pixels = imageData.data
|
||||||
|
|
||||||
|
for (let y = 0; y < height; y++) {
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
const pixelIndex = (y * width + x) * 4
|
||||||
|
const alpha = pixels[pixelIndex + 3]
|
||||||
|
if (alpha !== 0) {
|
||||||
|
topmost = Math.min(topmost, y)
|
||||||
|
bottommost = Math.max(bottommost, y)
|
||||||
|
leftmost = Math.min(leftmost, x)
|
||||||
|
rightmost = Math.max(rightmost, x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const croppedWidth = rightmost - leftmost + 1
|
||||||
|
const croppedHeight = bottommost - topmost + 1
|
||||||
|
|
||||||
|
const croppedCanvas = document.createElement('canvas')
|
||||||
|
croppedCanvas.width = croppedWidth
|
||||||
|
croppedCanvas.height = croppedHeight
|
||||||
|
const croppedCtx = croppedCanvas.getContext('2d')
|
||||||
|
|
||||||
|
croppedCtx.drawImage(canvas, leftmost, topmost, croppedWidth, croppedHeight, 0, 0, croppedWidth, croppedHeight)
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
croppedCanvas.toBlob((blob) => {
|
||||||
|
if (blob) {
|
||||||
|
resolve(blob)
|
||||||
|
} else {
|
||||||
|
reject(new Error('Failed to create a PNG blob.'))
|
||||||
|
}
|
||||||
|
}, 'image/png')
|
||||||
|
})
|
||||||
},
|
},
|
||||||
async submit () {
|
async submit () {
|
||||||
if (this.modelValue) {
|
if (this.modelValue) {
|
||||||
@@ -111,7 +294,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.$refs.canvas.toBlob(async (blob) => {
|
this.cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => {
|
||||||
const file = new File([blob], 'signature.png', { type: 'image/png' })
|
const file = new File([blob], 'signature.png', { type: 'image/png' })
|
||||||
|
|
||||||
if (this.isDirectUpload) {
|
if (this.isDirectUpload) {
|
||||||
@@ -153,7 +336,7 @@ export default {
|
|||||||
return resolve(attachment)
|
return resolve(attachment)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, 'image/png')
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
@blur="onNameBlur"
|
@blur="onNameBlur"
|
||||||
>{{ field.name || defaultName }}</span>
|
>{{ field.name || defaultName }}</span>
|
||||||
<div
|
<div
|
||||||
v-if="isNameFocus"
|
v-if="isNameFocus && field.type !== 'checkbox'"
|
||||||
class="flex items-center ml-1.5"
|
class="flex items-center ml-1.5"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -64,8 +64,11 @@
|
|||||||
: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"
|
||||||
@@ -133,7 +136,7 @@
|
|||||||
<p>
|
<p>
|
||||||
<button
|
<button
|
||||||
class="base-button"
|
class="base-button"
|
||||||
@click="drawField = false"
|
@click="drawField = null"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
@@ -314,7 +317,7 @@ export default {
|
|||||||
const field = {
|
const field = {
|
||||||
name: '',
|
name: '',
|
||||||
uuid: v4(),
|
uuid: v4(),
|
||||||
required: true,
|
required: type !== 'checkbox',
|
||||||
type,
|
type,
|
||||||
submitter_uuid: this.selectedSubmitter.uuid,
|
submitter_uuid: this.selectedSubmitter.uuid,
|
||||||
areas: [area]
|
areas: [area]
|
||||||
@@ -333,7 +336,7 @@ export default {
|
|||||||
type: this.dragFieldType,
|
type: this.dragFieldType,
|
||||||
uuid: v4(),
|
uuid: v4(),
|
||||||
submitter_uuid: this.selectedSubmitter.uuid,
|
submitter_uuid: this.selectedSubmitter.uuid,
|
||||||
required: true
|
required: this.dragFieldType !== 'checkbox'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['select', 'multiple', 'radio'].includes(this.dragFieldType)) {
|
if (['select', 'multiple', 'radio'].includes(this.dragFieldType)) {
|
||||||
@@ -353,7 +356,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 +420,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) {
|
||||||
|
|||||||
@@ -93,12 +93,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.')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,19 +31,21 @@
|
|||||||
v-if="isNameFocus"
|
v-if="isNameFocus"
|
||||||
class="flex items-center relative"
|
class="flex items-center relative"
|
||||||
>
|
>
|
||||||
<input
|
<template v-if="field.type !== 'checkbox'">
|
||||||
:id="`required-checkbox-${field.uuid}`"
|
<input
|
||||||
v-model="field.required"
|
:id="`required-checkbox-${field.uuid}`"
|
||||||
type="checkbox"
|
v-model="field.required"
|
||||||
class="checkbox checkbox-xs no-animation rounded"
|
type="checkbox"
|
||||||
@mousedown.prevent
|
class="checkbox checkbox-xs no-animation rounded"
|
||||||
>
|
@mousedown.prevent
|
||||||
<label
|
>
|
||||||
:for="`required-checkbox-${field.uuid}`"
|
<label
|
||||||
class="label text-xs"
|
:for="`required-checkbox-${field.uuid}`"
|
||||||
@click.prevent="field.required = !field.required"
|
class="label text-xs"
|
||||||
@mousedown.prevent
|
@click.prevent="field.required = !field.required"
|
||||||
>Required</label>
|
@mousedown.prevent
|
||||||
|
>Required</label>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
|
|||||||
@@ -71,12 +71,12 @@
|
|||||||
v-if="fields.length < 4"
|
v-if="fields.length < 4"
|
||||||
class="text-xs p-2 border border-base-200 rounded"
|
class="text-xs p-2 border border-base-200 rounded"
|
||||||
>
|
>
|
||||||
<ui>
|
<ul class="list-disc list-outside ml-3">
|
||||||
<li>
|
<li>
|
||||||
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
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
<li>
|
<li>
|
||||||
Click on the field type above to add it to the form without drawing it on the document
|
Click on the field type above to add it to the form without drawing it on the document
|
||||||
</li>
|
</li>
|
||||||
</ui>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -171,7 +171,8 @@ export default {
|
|||||||
const field = {
|
const field = {
|
||||||
name: '',
|
name: '',
|
||||||
uuid: v4(),
|
uuid: v4(),
|
||||||
required: true,
|
required: type !== 'checkbox',
|
||||||
|
areas: [],
|
||||||
submitter_uuid: this.selectedSubmitter.uuid,
|
submitter_uuid: this.selectedSubmitter.uuid,
|
||||||
type
|
type
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,70 @@
|
|||||||
|
<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',
|
||||||
|
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>
|
||||||
@@ -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
|
||||||
@@ -3,12 +3,13 @@
|
|||||||
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: '')
|
||||||
@submitter = submitter
|
@submitter = submitter
|
||||||
@message = message
|
@message = message.presence || format(DEFAULT_MESSAGE, name: submitter.submission.template.name)
|
||||||
|
|
||||||
mail(to: @submitter.email,
|
mail(to: @submitter.email,
|
||||||
subject: 'You have been invited to submit a form')
|
subject: 'You have been invited to submit a form',
|
||||||
|
reply_to: submitter.submission.created_by_user&.friendly_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
def completed_email(submitter, user)
|
def completed_email(submitter, user)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -15,6 +15,8 @@ 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 :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'
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class EncryptedConfig < ApplicationRecord
|
|||||||
EMAIL_SMTP_KEY = 'action_mailer_smtp'
|
EMAIL_SMTP_KEY = 'action_mailer_smtp'
|
||||||
ESIGN_CERTS_KEY = 'esign_certs'
|
ESIGN_CERTS_KEY = 'esign_certs'
|
||||||
APP_URL_KEY = 'app_url'
|
APP_URL_KEY = 'app_url'
|
||||||
|
WEBHOOK_URL_KEY = 'webhook_url'
|
||||||
|
|
||||||
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,23 @@
|
|||||||
#
|
#
|
||||||
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'
|
||||||
|
|
||||||
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
|
||||||
@@ -30,9 +30,11 @@
|
|||||||
#
|
#
|
||||||
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(10) }
|
attribute :slug, :string, default: -> { SecureRandom.base58(14) }
|
||||||
|
|
||||||
serialize :values, JSON
|
serialize :values, JSON
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class Template < ApplicationRecord
|
|||||||
attribute :fields, :string, default: -> { [] }
|
attribute :fields, :string, default: -> { [] }
|
||||||
attribute :schema, :string, default: -> { [] }
|
attribute :schema, :string, default: -> { [] }
|
||||||
attribute :submitters, :string, default: -> { [{ name: DEFAULT_SUBMITTER_NAME, uuid: SecureRandom.uuid }] }
|
attribute :submitters, :string, default: -> { [{ name: DEFAULT_SUBMITTER_NAME, uuid: SecureRandom.uuid }] }
|
||||||
attribute :slug, :string, default: -> { SecureRandom.base58(10) }
|
attribute :slug, :string, default: -> { SecureRandom.base58(14) }
|
||||||
|
|
||||||
serialize :fields, JSON
|
serialize :fields, JSON
|
||||||
serialize :schema, JSON
|
serialize :schema, JSON
|
||||||
@@ -44,6 +44,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) }
|
||||||
|
|||||||
+14
-3
@@ -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,6 +33,7 @@
|
|||||||
# 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
|
||||||
#
|
#
|
||||||
@@ -40,22 +42,31 @@
|
|||||||
class User < ApplicationRecord
|
class User < ApplicationRecord
|
||||||
ROLES = %w[admin].freeze
|
ROLES = %w[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
|
||||||
|
|
||||||
devise :database_authenticatable, :recoverable, :rememberable, :validatable, :trackable
|
devise :database_authenticatable, :recoverable, :rememberable, :validatable, :trackable
|
||||||
devise :registerable if Docuseal.multitenant?
|
devise :registerable, :omniauthable, omniauth_providers: [:google_oauth2] if Docuseal.multitenant?
|
||||||
|
|
||||||
attribute :role, :string, default: 'admin'
|
attribute :role, :string, default: 'admin'
|
||||||
|
attribute :uuid, :string, default: -> { SecureRandom.uuid }
|
||||||
|
|
||||||
scope :active, -> { where(deleted_at: nil) }
|
scope :active, -> { where(deleted_at: nil) }
|
||||||
|
|
||||||
|
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].join.upcase
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<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
|
||||||
|
</div>
|
||||||
|
<div class="mt-1">
|
||||||
|
<div class="badge badge-warning badge-lg">POST</div>
|
||||||
|
<div class="badge badge-primary badge-lg"><%= api_submissions_path %></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="collapse-content" style="display: inherit">
|
||||||
|
<div class="mockup-code overflow-hidden">
|
||||||
|
<% text = capture do %>curl --location '<%= api_submissions_url %>' \
|
||||||
|
--header 'X-Auth-Token: <%= current_user.access_token.token %>' \
|
||||||
|
--data-raw '{
|
||||||
|
"template_id": <%= current_account.templates.last&.id || 1 %>,
|
||||||
|
"submission": [
|
||||||
|
{
|
||||||
|
"submitters": [
|
||||||
|
{ "name": "<%= current_account.templates.last ? current_account.templates.last.submitters.first['name'] : 'First Submitter' %>", "email": "<%= current_user.email.sub('@', '+test@') %>" },
|
||||||
|
{ "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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="max-w-xl mx-auto px-2">
|
<div class="max-w-xl mx-auto px-2">
|
||||||
<h1 class="text-4xl font-bold text-center my-8">Sign up</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: registration_path) do |f| %>
|
<%= form_for('', as: resource_name, html: { class: 'space-y-6' }, url: new_registration_path) do |f| %>
|
||||||
<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| %>
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= f.fields_for resource do |ff| %>
|
<%= f.fields_for resource do |ff| %>
|
||||||
<div class="form-control">
|
<div class="form-control <%= 'hidden' if (params[:oauth_callback] || params[:sign_up]) && resource.email? %>">
|
||||||
<%= ff.label :email, class: 'label' %>
|
<%= ff.label :email, class: 'label' %>
|
||||||
<%= ff.email_field :email, required: true, class: 'base-input' %>
|
<%= ff.email_field :email, required: true, class: 'base-input' %>
|
||||||
</div>
|
</div>
|
||||||
@@ -25,14 +25,14 @@
|
|||||||
<set-timezone data-input-id="_account_timezone"></set-timezone>
|
<set-timezone data-input-id="_account_timezone"></set-timezone>
|
||||||
<%= ff.hidden_field :timezone %>
|
<%= ff.hidden_field :timezone %>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= ff.label :name, 'Company name', class: 'label' %>
|
<%= ff.label :name, 'Company name (optional)', class: 'label' %>
|
||||||
<%= ff.text_field :name, required: true, class: 'base-input' %>
|
<%= ff.text_field :name, class: 'base-input' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= f.fields_for resource do |ff| %>
|
<%= f.fields_for resource do |ff| %>
|
||||||
<div class="form-control">
|
<div class="form-control <%= 'hidden' if params[:oauth_callback] %>">
|
||||||
<%= ff.label :password, class: 'label' %>
|
<%= ff.label :password, class: 'label' %>
|
||||||
<%= ff.password_field :password, required: true, class: 'base-input' %>
|
<%= ff.password_field :password, required: !params[:oauth_callback], class: 'base-input' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<div class="max-w-xl mx-auto px-2">
|
||||||
|
<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| %>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="form-control">
|
||||||
|
<%= f.label :email, class: 'label' %>
|
||||||
|
<%= f.email_field :email, autofocus: true, autocomplete: 'email', class: 'base-input' %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<%= f.button button_title(title: 'Sign up', disabled_with: 'Sign up'), name: 'sign_up', value: true, class: 'base-button' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% if devise_mapping.omniauthable? %>
|
||||||
|
<%= button_to button_title(title: 'Sign up with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), omniauth_authorize_path(resource_name, :google_oauth2), class: 'white-button w-full mt-4', data: { turbo: false }, method: :post %>
|
||||||
|
<% end %>
|
||||||
|
<%= render 'devise/shared/links' %>
|
||||||
|
</div>
|
||||||
@@ -10,18 +10,13 @@
|
|||||||
<%= 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' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% if devise_mapping.omniauthable? %>
|
||||||
|
<%= button_to button_title(title: 'Log in with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), omniauth_authorize_path(resource_name, :google_oauth2), class: 'white-button w-full mt-4', data: { turbo: false }, method: :post %>
|
||||||
|
<% end %>
|
||||||
<%= render 'devise/shared/links' %>
|
<%= render 'devise/shared/links' %>
|
||||||
</div>
|
</div>
|
||||||
@@ -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 'Sign up', new_registration_path, class: 'link link-hover' %>
|
<%= link_to 'Create free account', registration_path, 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' %>
|
||||||
@@ -14,9 +14,4 @@
|
|||||||
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
|
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
|
||||||
<%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name), class: 'link link-hover' %>
|
<%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name), class: 'link link-hover' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%- if devise_mapping.omniauthable? %>
|
|
||||||
<%- resource_class.omniauth_providers.each do |provider| %>
|
|
||||||
<%= button_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), class: 'link link-hover', data: { turbo: false } %>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<svg width="800" height="800" viewBox="-0.5 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>">
|
||||||
|
<g id="Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="Color-" transform="translate(-401.000000, -860.000000)">
|
||||||
|
<g id="Google" transform="translate(401.000000, 860.000000)">
|
||||||
|
<path d="M9.82727273,24 C9.82727273,22.4757333 10.0804318,21.0144 10.5322727,19.6437333 L2.62345455,13.6042667 C1.08206818,16.7338667 0.213636364,20.2602667 0.213636364,24 C0.213636364,27.7365333 1.081,31.2608 2.62025,34.3882667 L10.5247955,28.3370667 C10.0772273,26.9728 9.82727273,25.5168 9.82727273,24" id="Fill-1" fill="#FBBC05">
|
||||||
|
</path>
|
||||||
|
<path d="M23.7136364,10.1333333 C27.025,10.1333333 30.0159091,11.3066667 32.3659091,13.2266667 L39.2022727,6.4 C35.0363636,2.77333333 29.6954545,0.533333333 23.7136364,0.533333333 C14.4268636,0.533333333 6.44540909,5.84426667 2.62345455,13.6042667 L10.5322727,19.6437333 C12.3545909,14.112 17.5491591,10.1333333 23.7136364,10.1333333" id="Fill-2" fill="#EB4335">
|
||||||
|
</path>
|
||||||
|
<path d="M23.7136364,37.8666667 C17.5491591,37.8666667 12.3545909,33.888 10.5322727,28.3562667 L2.62345455,34.3946667 C6.44540909,42.1557333 14.4268636,47.4666667 23.7136364,47.4666667 C29.4455,47.4666667 34.9177955,45.4314667 39.0249545,41.6181333 L31.5177727,35.8144 C29.3995682,37.1488 26.7323182,37.8666667 23.7136364,37.8666667" id="Fill-3" fill="#34A853">
|
||||||
|
</path>
|
||||||
|
<path d="M46.1454545,24 C46.1454545,22.6133333 45.9318182,21.12 45.6113636,19.7333333 L23.7136364,19.7333333 L23.7136364,28.8 L36.3181818,28.8 C35.6879545,31.8912 33.9724545,34.2677333 31.5177727,35.8144 L39.0249545,41.6181333 C43.3393409,37.6138667 46.1454545,31.6490667 46.1454545,24" id="Fill-4" fill="#4285F4">
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -12,7 +12,7 @@
|
|||||||
---
|
---
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Sent using <a href="<%= Docuseal::PRODUCT_URL %>"><%= Docuseal::PRODUCT_NAME %></a> documents software.
|
Sent using <a href="<%= Docuseal::PRODUCT_URL %>"><%= Docuseal::PRODUCT_NAME %></a> free document signing.
|
||||||
</p>
|
</p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
DocuSeal
|
DocuSeal
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="mb-16 text-lg text-center text-gray-500">
|
<h2 class="mb-16 text-lg text-center text-gray-600">
|
||||||
A self-hosted and open-source web platform that provides secure and efficient digital document signing and processing.
|
A self-hosted and open-source web platform that provides secure and efficient digital document signing and processing.
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<h3 class="mb-4 text-2xl font-semibold">Easy to Start</h3>
|
<h3 class="mb-4 text-2xl font-semibold">Easy to Start</h3>
|
||||||
<p class="text-base text-gray-500">
|
<p class="text-base text-gray-500">
|
||||||
Run on your own host using <a href="https://hub.docker.com/r/docuseal/docuseal" class="link link-neutral font-bold" target="_blank">Docker</a> container, or deploy on your favorite managed PaaS with a single <a href="https:///www.docuseal.co/install" class="link link-neutral font-bold">click</a>.
|
Run on your own host using <a href="https://hub.docker.com/r/docuseal/docuseal" class="link link-neutral font-bold" target="_blank">Docker</a> container, or deploy on your favorite managed PaaS with a single <a href="https://www.docuseal.co/install" class="link link-neutral font-bold">click</a>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,3 +70,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<%= render 'shared/attribution' %>
|
||||||
@@ -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-6 h-6 animate-spin') %>
|
||||||
<span><%= disabled_with %>...</span>
|
<span><%= disabled_with %>...</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -2,13 +2,13 @@
|
|||||||
<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>
|
||||||
</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>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<div class="mx-auto mt-1.5">
|
<div class="mx-auto mt-1.5">
|
||||||
<div class="py-3 rounded-2xl flex items-center justify-between mx-4 md:mx-0">
|
<div class="py-3 rounded-2xl flex items-center justify-between mx-4 md:mx-0">
|
||||||
<div class="w-full text-center">
|
<div class="w-full text-center">
|
||||||
<span class="font-bold">DocuSeal Demo Environment</span>
|
<span class="font-bold">Demo Environment</span>
|
||||||
<br>
|
<br>
|
||||||
Feel free to
|
Feel free to
|
||||||
<a href="<%= new_template_path %>" data-turbo-frame="modal" class="inline underline font-medium">create a new template</a> document form or
|
<a href="<%= new_template_path %>" data-turbo-frame="modal" class="inline underline font-medium">create a new template</a> document form or
|
||||||
|
|||||||
@@ -29,8 +29,8 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<%= link_to destroy_user_session_path, data: { turbo_method: :delete } do %>
|
<%= button_to destroy_user_session_path, method: :delete, data: { turbo: false } do %>
|
||||||
<%= svg_icon('logout', class: 'w-5 h-5 stroke-2') %>
|
<%= svg_icon('logout', class: 'w-5 h-5 stroke-2 inline') %>
|
||||||
<span class="mr-1">Sign out</span>
|
<span class="mr-1">Sign out</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
@@ -38,11 +38,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= link_to new_user_session_path, class: 'font-medium text-lg' do %>
|
<div class="flex space-x-6">
|
||||||
<span class="flex items-center justify-center space-x-1">
|
<%= link_to new_user_session_path, class: 'font-medium text-lg' do %>
|
||||||
<%= svg_icon('login', class: 'w-6 h-6') %>
|
<span class="flex items-center justify-center space-x-1">
|
||||||
<span>Sign in</span>
|
<%= svg_icon('login', class: 'w-6 h-6') %>
|
||||||
</span>
|
<span>Sign in</span>
|
||||||
<% end %>
|
</span>
|
||||||
|
<% end %>
|
||||||
|
<% if Docuseal.multitenant? %>
|
||||||
|
<%= link_to registration_path, class: 'font-medium text-lg hidden md:inline' do %>
|
||||||
|
<span class="flex items-center justify-center space-x-1">
|
||||||
|
<%= svg_icon('user_plus', class: 'w-6 h-6') %>
|
||||||
|
<span>Try for Free</span>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
@@ -23,6 +23,22 @@
|
|||||||
<li>
|
<li>
|
||||||
<%= link_to 'Team', settings_users_path, class: 'text-base hover:bg-base-300' %>
|
<%= link_to 'Team', settings_users_path, class: 'text-base hover:bg-base-300' %>
|
||||||
</li>
|
</li>
|
||||||
|
<% if Docuseal.demo? || !Docuseal.multitenant? %>
|
||||||
|
<li>
|
||||||
|
<%= link_to 'API', settings_api_index_path, class: 'text-base hover:bg-base-300' %>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
<% unless Docuseal.demo? %>
|
||||||
|
<li>
|
||||||
|
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, class: 'text-base hover:bg-base-300' do %>
|
||||||
|
Console
|
||||||
|
<span class="badge badge-warning">New</span>
|
||||||
|
<% end %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
</menu-active>
|
</menu-active>
|
||||||
<div class="mx-4 border-t border-base-300 hidden md:block">
|
<div class="mx-4 border-t border-base-300 hidden md:block">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<turbo-frame id="modal">
|
<turbo-frame id="modal">
|
||||||
<turbo-modal class="modal modal-open items-start !animate-none">
|
<turbo-modal class="modal modal-open items-start !animate-none overflow-y-auto">
|
||||||
<div class="absolute top-0 bottom-0 right-0 left-0" data-action="click:turbo-modal#close"></div>
|
<div class="absolute top-0 bottom-0 right-0 left-0" data-action="click:turbo-modal#close"></div>
|
||||||
<div class="modal-box pt-4 pb-6 px-6 mt-20">
|
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none">
|
||||||
<% if local_assigns[:title] %>
|
<% if local_assigns[:title] %>
|
||||||
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
|
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center bg-base-300 rounded-xl p-4 mb-4">
|
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="mr-3">
|
<div class="mr-3">
|
||||||
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
|
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="space-y-6 mx-auto">
|
<div class="space-y-6 mx-auto">
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="text-center w-full space-y-6">
|
<div class="text-center w-full space-y-6">
|
||||||
<a href="/" class="flex justify-center">
|
<a href="/" class="flex justify-center items-center">
|
||||||
<span class="mr-3">
|
<span class="mr-3">
|
||||||
<%= render 'shared/logo', width: '50px', height: '50px' %>
|
<%= render 'shared/logo', width: '50px', height: '50px' %>
|
||||||
</span>
|
</span>
|
||||||
@@ -10,14 +10,14 @@
|
|||||||
</a>
|
</a>
|
||||||
<p class="text-xl font-semibold text-center">You have been invited to submit a form</p>
|
<p class="text-xl font-semibold text-center">You have been invited to submit a form</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center bg-base-300 rounded-xl p-4 mb-4">
|
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="mr-3">
|
<div class="mr-3">
|
||||||
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
|
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-lg font-bold mb-1"><%= @template.name %></p>
|
<p class="text-lg font-bold mb-1"><%= @template.name %></p>
|
||||||
<p class="text-sm">Invited by <%= @template.account.name %></p>
|
<p class="text-sm">Invited by <span class="font-semibold"><%= @template.account.name %></span></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex md:max-h-[calc(100vh-60px)]">
|
<div class="flex md:max-h-[calc(100vh-60px)]">
|
||||||
<div class="overflow-y-auto overflow-x-hidden hidden lg:block w-52 flex-none pr-3 mt-0.5 pt-0.5">
|
<div class="overflow-y-auto overflow-x-hidden hidden lg:block w-52 flex-none pr-3 mt-0.5 pt-0.5">
|
||||||
<% @submission.template.schema.each do |item| %>
|
<% (@submission.template_schema || @submission.template.schema).each do |item| %>
|
||||||
<% document = @submission.template.documents.find { |a| item['attachment_uuid'] == a.uuid } %>
|
<% document = @submission.template.documents.find { |a| item['attachment_uuid'] == a.uuid } %>
|
||||||
<a href="#<%= "page-#{document.uuid}-0" %>" onclick="[event.preventDefault(), window[event.target.closest('a').href.split('#')[1]].scrollIntoView({ behavior: 'smooth', block: 'start' })]" class="block cursor-pointer">
|
<a href="#<%= "page-#{document.uuid}-0" %>" onclick="[event.preventDefault(), window[event.target.closest('a').href.split('#')[1]].scrollIntoView({ behavior: 'smooth', block: 'start' })]" class="block cursor-pointer">
|
||||||
<img src="<%= document.preview_images.first.url %>" width="<%= document.preview_images.first.metadata['width'] %>" height="<%= document.preview_images.first.metadata['height'] %>" class="rounded border" loading="lazy">
|
<img src="<%= document.preview_images.first.url %>" width="<%= document.preview_images.first.metadata['width'] %>" height="<%= document.preview_images.first.metadata['height'] %>" class="rounded border" loading="lazy">
|
||||||
@@ -35,10 +35,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="w-full overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5">
|
<div class="w-full overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5">
|
||||||
<div class="pr-3.5 pl-0.5">
|
<div class="pr-3.5 pl-0.5">
|
||||||
<% fields_index = Templates.build_field_areas_index(@submission.template) %>
|
<% fields_index = Templates.build_field_areas_index(@submission.template_fields || @submission.template.fields) %>
|
||||||
<% values = @submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %>
|
<% values = @submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %>
|
||||||
<% attachments_index = ActiveStorage::Attachment.where(record: @submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %>
|
<% attachments_index = ActiveStorage::Attachment.where(record: @submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %>
|
||||||
<% @submission.template.schema.each do |item| %>
|
<% (@submission.template_schema || @submission.template.schema).each do |item| %>
|
||||||
<% document = @submission.template.documents.find { |e| e.uuid == item['attachment_uuid'] } %>
|
<% document = @submission.template.documents.find { |e| e.uuid == item['attachment_uuid'] } %>
|
||||||
<% document.preview_images.sort_by { |a| a.filename.base.to_i }.each_with_index do |page, index| %>
|
<% document.preview_images.sort_by { |a| a.filename.base.to_i }.each_with_index do |page, index| %>
|
||||||
<div id="<%= "page-#{document.uuid}-#{index}" %>" class="relative">
|
<div id="<%= "page-#{document.uuid}-#{index}" %>" class="relative">
|
||||||
@@ -57,15 +57,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="hidden md:block relative w-80 flex-none pt-0.5 pr-4 pl-0.5 overflow-auto space">
|
<div class="hidden md:block relative w-80 flex-none pt-0.5 pr-4 pl-0.5 overflow-auto space">
|
||||||
<% colors = %w[bg-red-500 bg-sky-500 bg-emerald-500 bg-yellow-300 bg-purple-600] %>
|
<% colors = %w[bg-red-500 bg-sky-500 bg-emerald-500 bg-yellow-300 bg-purple-600] %>
|
||||||
<% submitter_fields_index = @submission.template.fields.group_by { |f| f['submitter_uuid'] } %>
|
<% submitter_fields_index = (@submission.template_fields || @submission.template.fields).group_by { |f| f['submitter_uuid'] } %>
|
||||||
<% @submission.template.submitters.each_with_index do |item, index| %>
|
<% (@submission.template_submitters || @submission.template.submitters).each_with_index do |item, index| %>
|
||||||
<% submitter = @submission.submitters.find { |e| e.uuid == item['uuid'] } %>
|
<% submitter = @submission.submitters.find { |e| e.uuid == item['uuid'] } %>
|
||||||
<div class="sticky -top-1 bg-base-100 pt-1 -mt-1 z-10">
|
<div class="sticky -top-1 bg-base-100 pt-1 -mt-1 z-10">
|
||||||
<div class="border border-base-300 rounded-md px-2 py-1 mb-1">
|
<div class="border border-base-300 rounded-md px-2 py-1 mb-1">
|
||||||
<div class="flex items-center space-x-1">
|
<div class="flex items-center space-x-1">
|
||||||
<span class="mx-1 w-3 h-3 rounded-full <%= colors[index] %>"></span>
|
<span class="mx-1 w-3 h-3 rounded-full <%= colors[index] %>"></span>
|
||||||
<span class="text-lg">
|
<span class="text-lg">
|
||||||
<%= @submission.template.submitters.find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %>
|
<%= (@submission.template_submitters || @submission.template.submitters).find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-1 mt-1">
|
<div class="flex items-center space-x-1 mt-1">
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
|
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center bg-base-300 rounded-xl p-4 mb-4">
|
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="mr-3">
|
<div class="mr-3">
|
||||||
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
|
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<% if Accounts.can_send_emails?(@submitter.submission.template.account) %>
|
<% if Accounts.can_send_emails?(@submitter.submission.template.account) %>
|
||||||
<%= button_to button_title(title: 'Send copy to Email', disabled_with: 'Sending', icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'base-button w-full' %>
|
<%= button_to button_title(title: 'Send copy to Email', disabled_with: 'Sending', icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
|
||||||
<div class="divider">OR</div>
|
<div class="divider">OR</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<download-button data-src="<%= submitter_download_index_path(@submitter.slug) %>" class="base-button w-full">
|
<download-button data-src="<%= submitter_download_index_path(@submitter.slug) %>" class="base-button w-full">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<% fields_index = Templates.build_field_areas_index(@submitter.submission.template) %>
|
<% fields_index = Templates.build_field_areas_index(@submitter.submission.template_fields || @submitter.submission.template.fields) %>
|
||||||
<% values = @submitter.submission.submitters.where.not(id: @submitter.id).reduce({}) { |acc, sub| acc.merge(sub.values) } %>
|
<% values = @submitter.submission.submitters.where.not(id: @submitter.id).reduce({}) { |acc, sub| acc.merge(sub.values) } %>
|
||||||
<% attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %>
|
<% attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %>
|
||||||
<div style="max-height: -webkit-fill-available;">
|
<div style="max-height: -webkit-fill-available;">
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<span>DocuSeal</span>
|
<span>DocuSeal</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<% @submitter.submission.template.schema.each do |item| %>
|
<% (@submitter.submission.template_schema || @submitter.submission.template.schema).each do |item| %>
|
||||||
<% document = @submitter.submission.template.documents.find { |a| a.uuid == item['attachment_uuid'] } %>
|
<% document = @submitter.submission.template.documents.find { |a| a.uuid == item['attachment_uuid'] } %>
|
||||||
<% document.preview_images.sort_by { |a| a.filename.base.to_i }.each_with_index do |page, index| %>
|
<% document.preview_images.sort_by { |a| a.filename.base.to_i }.each_with_index do |page, index| %>
|
||||||
<div class="relative my-4 shadow-md">
|
<div class="relative my-4 shadow-md">
|
||||||
@@ -32,9 +32,7 @@
|
|||||||
<div class="fixed bottom-0 w-full h-0 z-20">
|
<div class="fixed bottom-0 w-full h-0 z-20">
|
||||||
<div class="mx-auto" style="max-width: 1000px">
|
<div class="mx-auto" style="max-width: 1000px">
|
||||||
<div class="relative md:mx-32">
|
<div class="relative md:mx-32">
|
||||||
<div id="form_container" class="shadow-md bg-base-100 absolute bottom-0 md:bottom-4 w-full border border-base-200 border p-4 rounded">
|
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter-uuid="<%= @submitter.uuid %>" data-submitter-slug="<%= @submitter.slug %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" data-attachments="<%= attachments_index.values.select { |e| e.record_id == @submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>" data-fields="<%= (@submitter.submission.template_fields || @submitter.submission.template.fields).select { |f| f['submitter_uuid'] == @submitter.uuid }.to_json %>" data-values="<%= @submitter.values.to_json %>" data-authenticity-token="<%= form_authenticity_token %>"></submission-form>
|
||||||
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter-uuid="<%= @submitter.uuid %>" data-submitter-slug="<%= @submitter.slug %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" data-attachments="<%= attachments_index.values.select { |e| e.record_id == @submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>" data-fields="<%= @submitter.submission.template.fields.select { |f| f['submitter_uuid'] == @submitter.uuid }.to_json %>" data-values="<%= @submitter.values.to_json %>" data-authenticity-token="<%= form_authenticity_token %>"></submission-form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1 +1 @@
|
|||||||
<template-builder data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-template="<%= @template.to_json(include: { documents: { include: { preview_images: { methods: %i[url metadata filename] } } } }) %>"></template-builder>
|
<template-builder data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-template="<%= @template.as_json.merge(documents: @template.schema_documents.as_json(include: { preview_images: { methods: %i[url metadata filename] } })).to_json %>"></template-builder>
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<% @submissions.each do |submission| %>
|
<% @submissions.each do |submission| %>
|
||||||
<a href="<%= submission_path(submission) %>" class="bg-base-200 w-full flex flex-col md:flex-row space-y-4 md:space-y-0 md:justify-between rounded-2xl px-6 py-5 md:items-center">
|
<a href="<%= submission_path(submission) %>" class="bg-base-200 w-full flex flex-col md:flex-row space-y-4 md:space-y-0 md:justify-between rounded-2xl px-6 py-5 md:items-center">
|
||||||
<% submitters = submission.template.submitters.filter_map { |item| submission.submitters.find { |e| e.uuid == item['uuid'] } } %>
|
<% submitters = (submission.template_submitters || submission.template.submitters).filter_map { |item| submission.submitters.find { |e| e.uuid == item['uuid'] } } %>
|
||||||
<% is_submission_complated = submitters.all?(&:completed_at?) %>
|
<% is_submission_complated = submitters.all?(&:completed_at?) %>
|
||||||
<% if submitters.size == 1 %>
|
<% if submitters.size == 1 %>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
|
||||||
|
<%= render 'shared/settings_nav' %>
|
||||||
|
<div class="flex-grow">
|
||||||
|
<h1 class="text-4xl font-bold mb-4">Webhooks</h1>
|
||||||
|
<div class="card bg-base-200">
|
||||||
|
<div class="card-body p-6">
|
||||||
|
<%= form_for @encrypted_config, url: settings_webhooks_path, method: :post, html: { autocomplete: 'off' } do |f| %>
|
||||||
|
<%= f.label :value, 'Webhook URL', class: 'text-sm font-semibold' %>
|
||||||
|
<div class="flex flex-row space-x-4 mt-2">
|
||||||
|
<%= f.text_field :value, required: true, class: 'input font-mono input-bordered w-full', placeholder: 'https://example.com/hook' %>
|
||||||
|
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button w-32' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last %>
|
||||||
|
<% if submitter %>
|
||||||
|
<div class="space-y-4 mt-4">
|
||||||
|
<div class="collapse collapse-open bg-base-200 px-1">
|
||||||
|
<div class="p-4 text-xl font-medium">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>
|
||||||
|
Submission example payload
|
||||||
|
</span>
|
||||||
|
<% if @encrypted_config.value.present? %>
|
||||||
|
<%= button_to button_title(title: 'Test Webhook', disabled_with: 'Sending', icon_disabled: svg_icon('loader', class: 'w-4 h-4 animate-spin')), settings_webhooks_path, class: 'btn btn-neutral btn-outline btn-sm', method: :put %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="collapse-content" style="display: inherit">
|
||||||
|
<div class="mockup-code overflow-hidden relative">
|
||||||
|
<span class="top-0 right-0 absolute">
|
||||||
|
<%= render 'shared/clipboard_copy', icon: 'copy', text: code = JSON.pretty_generate({ event_type: 'form.submitted', timestamp: Time.current.iso8601, data: Submitters::SerializeForWebhook.call(submitter) }).gsub(/^/, ' ').sub(/^\s+/, ''), class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
|
||||||
|
</span>
|
||||||
|
<pre><code class="overflow-hidden w-full"><%= code %></code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -9,6 +9,8 @@ require 'action_view/railtie'
|
|||||||
require 'action_mailer/railtie'
|
require 'action_mailer/railtie'
|
||||||
require 'active_job/railtie'
|
require 'active_job/railtie'
|
||||||
|
|
||||||
|
require_relative '../lib/api_path_consider_json_middleware'
|
||||||
|
|
||||||
Bundler.require(*Rails.groups)
|
Bundler.require(*Rails.groups)
|
||||||
|
|
||||||
module DocuSeal
|
module DocuSeal
|
||||||
@@ -26,5 +28,6 @@ module DocuSeal
|
|||||||
config.action_view.frozen_string_literal = true
|
config.action_view.frozen_string_literal = true
|
||||||
|
|
||||||
config.middleware.insert_before ActionDispatch::Static, Rack::Deflater
|
config.middleware.insert_before ActionDispatch::Static, Rack::Deflater
|
||||||
|
config.middleware.insert_before ActionDispatch::Static, ApiPathConsiderJsonMiddleware
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -45,6 +45,8 @@ Rails.application.configure do
|
|||||||
config.cache_store = :null_store
|
config.cache_store = :null_store
|
||||||
end
|
end
|
||||||
|
|
||||||
|
config.active_job.queue_adapter = :sidekiq if defined?(Sidekiq)
|
||||||
|
|
||||||
# Store uploaded files on the local file system (see config/storage.yml for options).
|
# Store uploaded files on the local file system (see config/storage.yml for options).
|
||||||
config.active_storage.service = :disk
|
config.active_storage.service = :disk
|
||||||
config.active_storage.resolve_model_to_route = :rails_storage_proxy
|
config.active_storage.resolve_model_to_route = :rails_storage_proxy
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ Rails.application.configure do
|
|||||||
config.action_controller.perform_caching = true
|
config.action_controller.perform_caching = true
|
||||||
config.active_record.sqlite3_production_warning = false
|
config.active_record.sqlite3_production_warning = false
|
||||||
|
|
||||||
|
config.active_job.queue_adapter = :sidekiq if defined?(Sidekiq)
|
||||||
|
|
||||||
# Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
|
# Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
|
||||||
# or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
|
# or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
|
||||||
# config.require_master_key = true
|
# config.require_master_key = true
|
||||||
@@ -74,7 +76,7 @@ Rails.application.configure do
|
|||||||
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
|
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
|
||||||
config.action_mailer.raise_delivery_errors = false
|
config.action_mailer.raise_delivery_errors = false
|
||||||
|
|
||||||
if ENV['SMTP_USERNAME']
|
if ENV['SMTP_ADDRESS']
|
||||||
config.action_mailer.delivery_method = :smtp
|
config.action_mailer.delivery_method = :smtp
|
||||||
config.action_mailer.smtp_settings = {
|
config.action_mailer.smtp_settings = {
|
||||||
address: ENV.fetch('SMTP_ADDRESS', nil),
|
address: ENV.fetch('SMTP_ADDRESS', nil),
|
||||||
@@ -84,7 +86,7 @@ Rails.application.configure do
|
|||||||
password: ENV.fetch('SMTP_PASSWORD', nil),
|
password: ENV.fetch('SMTP_PASSWORD', nil),
|
||||||
authentication: ENV.fetch('SMTP_AUTHENTICATION', 'plain'),
|
authentication: ENV.fetch('SMTP_AUTHENTICATION', 'plain'),
|
||||||
enable_starttls_auto: ENV['SMTP_ENABLE_STARTTLS_AUTO'] == 'true'
|
enable_starttls_auto: ENV['SMTP_ENABLE_STARTTLS_AUTO'] == 'true'
|
||||||
}
|
}.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
|
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative '../../lib/auth_with_token_strategy'
|
||||||
|
|
||||||
|
Warden::Strategies.add(:auth_token, AuthWithTokenStrategy)
|
||||||
|
|
||||||
# Assuming you have not yet modified this file, each configuration option below
|
# Assuming you have not yet modified this file, each configuration option below
|
||||||
# is set to its default value. Note that some are commented out while others
|
# is set to its default value. Note that some are commented out while others
|
||||||
# are not: uncommented lines are intended to protect your configuration from
|
# are not: uncommented lines are intended to protect your configuration from
|
||||||
@@ -120,7 +124,7 @@ Devise.setup do |config|
|
|||||||
# a value less than 10 in other environments. Note that, for bcrypt (the default
|
# a value less than 10 in other environments. Note that, for bcrypt (the default
|
||||||
# algorithm), the cost increases exponentially with the number of stretches (e.g.
|
# algorithm), the cost increases exponentially with the number of stretches (e.g.
|
||||||
# a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation).
|
# a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation).
|
||||||
config.stretches = Rails.env.test? ? 1 : 12
|
config.stretches = Rails.env.test? ? 1 : 10
|
||||||
|
|
||||||
# Set up a pepper to generate the hashed password.
|
# Set up a pepper to generate the hashed password.
|
||||||
# Send a notification to the original email when the user's email is changed.
|
# Send a notification to the original email when the user's email is changed.
|
||||||
@@ -159,7 +163,7 @@ Devise.setup do |config|
|
|||||||
|
|
||||||
# ==> Configuration for :rememberable
|
# ==> Configuration for :rememberable
|
||||||
# The time the user will be remembered without asking for credentials again.
|
# The time the user will be remembered without asking for credentials again.
|
||||||
# config.remember_for = 2.weeks
|
config.remember_for = 2.years
|
||||||
|
|
||||||
# Invalidates all the remember me tokens when the user signs out.
|
# Invalidates all the remember me tokens when the user signs out.
|
||||||
config.expire_all_remember_me_on_sign_out = true
|
config.expire_all_remember_me_on_sign_out = true
|
||||||
@@ -266,16 +270,16 @@ Devise.setup do |config|
|
|||||||
# ==> OmniAuth
|
# ==> OmniAuth
|
||||||
# Add a new OmniAuth provider. Check the wiki for more information on setting
|
# Add a new OmniAuth provider. Check the wiki for more information on setting
|
||||||
# up on your models and hooks.
|
# up on your models and hooks.
|
||||||
# config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo'
|
config.omniauth :google_oauth2, ENV.fetch('GOOGLE_CLIENT_ID', nil), ENV.fetch('GOOGLE_CLIENT_SECRET', nil), {}
|
||||||
|
|
||||||
# ==> Warden configuration
|
# ==> Warden configuration
|
||||||
# If you want to use other strategies, that are not supported by Devise, or
|
# If you want to use other strategies, that are not supported by Devise, or
|
||||||
# change the failure app, you can configure them inside the config.warden block.
|
# change the failure app, you can configure them inside the config.warden block.
|
||||||
#
|
#
|
||||||
# config.warden do |manager|
|
config.warden do |manager|
|
||||||
# manager.intercept_401 = false
|
# manager.intercept_401 = false
|
||||||
# manager.default_strategies(scope: :user).unshift :some_external_strategy
|
manager.default_strategies(scope: :user).unshift(:auth_token)
|
||||||
# end
|
end
|
||||||
|
|
||||||
# ==> Mountable engine configurations
|
# ==> Mountable engine configurations
|
||||||
# When using Devise inside an engine, let's call it `MyEngine`, and this engine
|
# When using Devise inside an engine, let's call it `MyEngine`, and this engine
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
ActiveRecord::Tasks::DatabaseTasks.migrate if ENV['RAILS_ENV'] == 'production'
|
Rails.configuration.to_prepare do
|
||||||
|
ActiveRecord::Tasks::DatabaseTasks.migrate if ENV['RAILS_ENV'] == 'production'
|
||||||
|
end
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
if defined?(Sidekiq)
|
||||||
|
require 'sidekiq/web'
|
||||||
|
|
||||||
|
Sidekiq::Web.use(Rack::Auth::Basic) do |_, password|
|
||||||
|
next true if Rails.env.development?
|
||||||
|
|
||||||
|
ActiveSupport::SecurityUtils.secure_compare(
|
||||||
|
Digest::SHA256.hexdigest(password),
|
||||||
|
Digest::SHA256.hexdigest(ENV.fetch('SIDEKIQ_BASIC_AUTH_PASSWORD'))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
Sidekiq.strict_args!
|
||||||
|
end
|
||||||
+19
-4
@@ -2,14 +2,22 @@
|
|||||||
|
|
||||||
Rails.application.routes.draw do
|
Rails.application.routes.draw do
|
||||||
mount LetterOpenerWeb::Engine, at: '/letter_opener' if Rails.env.development?
|
mount LetterOpenerWeb::Engine, at: '/letter_opener' if Rails.env.development?
|
||||||
|
mount Sidekiq::Web => '/sidekiq' if defined?(Sidekiq)
|
||||||
|
|
||||||
root 'dashboard#index'
|
root 'dashboard#index'
|
||||||
|
|
||||||
devise_for :users, path: '/', only: %i[sessions passwords]
|
devise_for :users,
|
||||||
|
path: '/', only: %i[sessions passwords omniauth_callbacks],
|
||||||
|
controllers: begin
|
||||||
|
options = { sessions: 'sessions' }
|
||||||
|
options[:omniauth_callbacks] = 'omniauth_callbacks' if Docuseal.multitenant?
|
||||||
|
options
|
||||||
|
end
|
||||||
|
|
||||||
devise_scope :user do
|
devise_scope :user do
|
||||||
if Docuseal.multitenant?
|
if Docuseal.multitenant?
|
||||||
resource :registration, only: %i[create], path: 'sign_up' do
|
resource :registration, only: %i[show], path: 'sign_up'
|
||||||
|
resource :registration, only: %i[create], path: 'new' do
|
||||||
get '' => :new, as: :new
|
get '' => :new, as: :new
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -19,9 +27,11 @@ Rails.application.routes.draw do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
namespace :api do
|
namespace :api, defaults: { format: :json } do
|
||||||
resources :attachments, only: %i[create]
|
resources :attachments, only: %i[create]
|
||||||
resources :templates, only: %i[update] do
|
resources :submissions, only: %i[create]
|
||||||
|
resources :templates, only: %i[update index] do
|
||||||
|
resources :submissions, only: %i[create]
|
||||||
resources :documents, only: %i[create destroy], controller: 'templates_documents'
|
resources :documents, only: %i[create destroy], controller: 'templates_documents'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -31,6 +41,7 @@ Rails.application.routes.draw do
|
|||||||
resource :newsletter, only: %i[show update]
|
resource :newsletter, only: %i[show update]
|
||||||
resources :users, only: %i[new create edit update destroy]
|
resources :users, only: %i[new create edit update destroy]
|
||||||
resources :submissions, only: %i[show destroy]
|
resources :submissions, only: %i[show destroy]
|
||||||
|
resources :console_redirect, only: %i[index]
|
||||||
resources :templates, only: %i[new create edit show destroy] do
|
resources :templates, only: %i[new create edit show destroy] do
|
||||||
resources :submissions, only: %i[new create]
|
resources :submissions, only: %i[new create]
|
||||||
end
|
end
|
||||||
@@ -59,6 +70,10 @@ Rails.application.routes.draw do
|
|||||||
end
|
end
|
||||||
resources :esign, only: %i[index create], controller: 'esign_settings'
|
resources :esign, only: %i[index create], controller: 'esign_settings'
|
||||||
resources :users, only: %i[index]
|
resources :users, only: %i[index]
|
||||||
|
if !Docuseal.multitenant? || Docuseal.demo?
|
||||||
|
resources :api, only: %i[index], controller: 'api_settings'
|
||||||
|
resource :webhooks, only: %i[show create update], controller: 'webhook_settings'
|
||||||
|
end
|
||||||
resource :account, only: %i[show update]
|
resource :account, only: %i[show update]
|
||||||
resources :profile, only: %i[index] do
|
resources :profile, only: %i[index] do
|
||||||
collection do
|
collection do
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
queues:
|
||||||
|
- [default, 1]
|
||||||
|
- [mailers, 1]
|
||||||
|
|
||||||
|
production:
|
||||||
|
:concurrency: 15
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddTemplateFieldsToSubmission < ActiveRecord::Migration[7.0]
|
||||||
|
class MigrationTemplate < ApplicationRecord
|
||||||
|
self.table_name = 'templates'
|
||||||
|
end
|
||||||
|
|
||||||
|
class MigrationSubmission < ApplicationRecord
|
||||||
|
self.table_name = 'submissions'
|
||||||
|
end
|
||||||
|
|
||||||
|
def up
|
||||||
|
add_column :submissions, :template_fields, :text
|
||||||
|
add_column :submissions, :template_schema, :text
|
||||||
|
add_column :submissions, :template_submitters, :text
|
||||||
|
|
||||||
|
MigrationTemplate.all.each do |template|
|
||||||
|
MigrationSubmission.where(template_id: template.id).each do |submission|
|
||||||
|
submission.update_columns(template_fields: template.fields,
|
||||||
|
template_schema: template.schema,
|
||||||
|
template_submitters: template.submitters)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_column :submissions, :template_fields
|
||||||
|
remove_column :submissions, :template_schema
|
||||||
|
remove_column :submissions, :template_submitters
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddUuidToUsers < ActiveRecord::Migration[7.0]
|
||||||
|
class MigrationUser < ApplicationRecord
|
||||||
|
self.table_name = 'users'
|
||||||
|
end
|
||||||
|
|
||||||
|
def up
|
||||||
|
add_column :users, :uuid, :text
|
||||||
|
add_index :users, :uuid, unique: true
|
||||||
|
|
||||||
|
MigrationUser.all.each do |user|
|
||||||
|
user.update_columns(uuid: SecureRandom.uuid)
|
||||||
|
end
|
||||||
|
|
||||||
|
change_column_null :users, :uuid, false
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
drop_column :users, :uuid
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddSourceToSubmissions < ActiveRecord::Migration[7.0]
|
||||||
|
class MigrationSubmission < ApplicationRecord
|
||||||
|
self.table_name = 'submissions'
|
||||||
|
end
|
||||||
|
|
||||||
|
def up
|
||||||
|
add_column :submissions, :source, :text
|
||||||
|
|
||||||
|
MigrationSubmission.where(source: nil).update_all(source: :invite)
|
||||||
|
|
||||||
|
change_column_null :submissions, :source, false
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
drop_column :submissions, :source
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateAccessTokens < ActiveRecord::Migration[7.0]
|
||||||
|
def change
|
||||||
|
create_table :access_tokens do |t|
|
||||||
|
t.references :user, null: false, foreign_key: true, index: true
|
||||||
|
t.text :token, null: false
|
||||||
|
t.text :sha256, null: false, index: { unique: true }
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
+18
-1
@@ -10,10 +10,20 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.0].define(version: 2023_07_01_075115) do
|
ActiveRecord::Schema[7.0].define(version: 2023_08_06_140534) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
|
||||||
|
create_table "access_tokens", force: :cascade do |t|
|
||||||
|
t.bigint "user_id", null: false
|
||||||
|
t.text "token", null: false
|
||||||
|
t.text "sha256", null: false
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["sha256"], name: "index_access_tokens_on_sha256", unique: true
|
||||||
|
t.index ["user_id"], name: "index_access_tokens_on_user_id"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "accounts", force: :cascade do |t|
|
create_table "accounts", force: :cascade do |t|
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
t.string "timezone", null: false
|
t.string "timezone", null: false
|
||||||
@@ -77,6 +87,10 @@ ActiveRecord::Schema[7.0].define(version: 2023_07_01_075115) do
|
|||||||
t.datetime "deleted_at"
|
t.datetime "deleted_at"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
|
t.text "template_fields"
|
||||||
|
t.text "template_schema"
|
||||||
|
t.text "template_submitters"
|
||||||
|
t.text "source", null: false
|
||||||
t.index ["created_by_user_id"], name: "index_submissions_on_created_by_user_id"
|
t.index ["created_by_user_id"], name: "index_submissions_on_created_by_user_id"
|
||||||
t.index ["template_id"], name: "index_submissions_on_template_id"
|
t.index ["template_id"], name: "index_submissions_on_template_id"
|
||||||
end
|
end
|
||||||
@@ -136,12 +150,15 @@ ActiveRecord::Schema[7.0].define(version: 2023_07_01_075115) do
|
|||||||
t.datetime "deleted_at"
|
t.datetime "deleted_at"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
|
t.text "uuid", null: false
|
||||||
t.index ["account_id"], name: "index_users_on_account_id"
|
t.index ["account_id"], name: "index_users_on_account_id"
|
||||||
t.index ["email"], name: "index_users_on_email", unique: true
|
t.index ["email"], name: "index_users_on_email", unique: true
|
||||||
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
||||||
t.index ["unlock_token"], name: "index_users_on_unlock_token", unique: true
|
t.index ["unlock_token"], name: "index_users_on_unlock_token", unique: true
|
||||||
|
t.index ["uuid"], name: "index_users_on_uuid", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
add_foreign_key "access_tokens", "users"
|
||||||
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
||||||
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
|
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
|
||||||
add_foreign_key "document_generation_events", "submitters"
|
add_foreign_key "document_generation_events", "submitters"
|
||||||
|
|||||||
+15
-1
@@ -16,7 +16,7 @@ module Accounts
|
|||||||
new_template = template.dup
|
new_template = template.dup
|
||||||
|
|
||||||
new_template.account = new_account
|
new_template.account = new_account
|
||||||
new_template.slug = SecureRandom.base58(10)
|
new_template.slug = SecureRandom.base58(14)
|
||||||
|
|
||||||
new_template.save!
|
new_template.save!
|
||||||
|
|
||||||
@@ -28,6 +28,20 @@ module Accounts
|
|||||||
new_account
|
new_account
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def create_default_template(account)
|
||||||
|
template = Template.find(1)
|
||||||
|
|
||||||
|
new_template = Template.find(1).dup
|
||||||
|
new_template.account_id = account.id
|
||||||
|
new_template.slug = SecureRandom.base58(14)
|
||||||
|
|
||||||
|
new_template.save!
|
||||||
|
|
||||||
|
Templates::CloneAttachments.call(template: new_template, original_template: template)
|
||||||
|
|
||||||
|
new_template
|
||||||
|
end
|
||||||
|
|
||||||
def load_signing_certs(account)
|
def load_signing_certs(account)
|
||||||
certs =
|
certs =
|
||||||
if Docuseal.multitenant?
|
if Docuseal.multitenant?
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ module ActionMailerConfigsInterceptor
|
|||||||
module_function
|
module_function
|
||||||
|
|
||||||
def delivering_email(message)
|
def delivering_email(message)
|
||||||
|
if Docuseal.demo?
|
||||||
|
message.delivery_method(:test)
|
||||||
|
|
||||||
|
return message
|
||||||
|
end
|
||||||
|
|
||||||
if Rails.env.production? && Rails.application.config.action_mailer.delivery_method
|
if Rails.env.production? && Rails.application.config.action_mailer.delivery_method
|
||||||
message.from = ENV.fetch('SMTP_FROM')
|
message.from = ENV.fetch('SMTP_FROM')
|
||||||
|
|
||||||
@@ -19,7 +25,7 @@ module ActionMailerConfigsInterceptor
|
|||||||
port: email_configs.value['port'],
|
port: email_configs.value['port'],
|
||||||
tls: email_configs.value['port'].to_s == '465')
|
tls: email_configs.value['port'].to_s == '465')
|
||||||
|
|
||||||
message.from = email_configs.value['from_email']
|
message.from = "#{email_configs.account.name} <#{email_configs.value['from_email']}>"
|
||||||
else
|
else
|
||||||
message.delivery_method(:test)
|
message.delivery_method(:test)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ApiPathConsiderJsonMiddleware
|
||||||
|
def initialize(app)
|
||||||
|
@app = app
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(env)
|
||||||
|
if env['PATH_INFO'].starts_with?('/api') &&
|
||||||
|
!env['PATH_INFO'].ends_with?('/documents') &&
|
||||||
|
!env['PATH_INFO'].ends_with?('/attachments')
|
||||||
|
env['CONTENT_TYPE'] = 'application/json'
|
||||||
|
end
|
||||||
|
|
||||||
|
@app.call(env)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AuthWithTokenStrategy < Devise::Strategies::Base
|
||||||
|
def valid?
|
||||||
|
request.headers['X-Auth-Token'].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def authenticate!
|
||||||
|
sha256 = Digest::SHA256.hexdigest(request.headers['X-Auth-Token'])
|
||||||
|
|
||||||
|
user = User.joins(:access_token).find_by(access_token: { sha256: })
|
||||||
|
|
||||||
|
if user
|
||||||
|
success!(user)
|
||||||
|
else
|
||||||
|
fail!('Invalid token')
|
||||||
|
end
|
||||||
|
rescue JWT::VerificationError
|
||||||
|
fail!('Invalid token')
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -10,6 +10,11 @@ module Docuseal
|
|||||||
TWITTER_URL = 'https://twitter.com/docusealco'
|
TWITTER_URL = 'https://twitter.com/docusealco'
|
||||||
TWITTER_HANDLE = '@docusealco'
|
TWITTER_HANDLE = '@docusealco'
|
||||||
SUPPORT_EMAIL = 'support@docuseal.co'
|
SUPPORT_EMAIL = 'support@docuseal.co'
|
||||||
|
CONSOLE_URL = if Rails.env.development?
|
||||||
|
'http://console.localhost.io:3001'
|
||||||
|
else
|
||||||
|
'https://console.docuseal.co'
|
||||||
|
end
|
||||||
|
|
||||||
CERTS = JSON.parse(ENV.fetch('CERTS', '{}'))
|
CERTS = JSON.parse(ENV.fetch('CERTS', '{}'))
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module JsonWebToken
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def encode(payload)
|
||||||
|
JWT.encode(payload, Rails.application.secrets.secret_key_base)
|
||||||
|
end
|
||||||
|
|
||||||
|
def decode(token)
|
||||||
|
JWT.decode(token, Rails.application.secrets.secret_key_base)[0]
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,4 +1,47 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Submissions
|
module Submissions
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def update_template_fields!(submission)
|
||||||
|
submission.template_fields = submission.template.fields
|
||||||
|
submission.template_schema = submission.template.schema
|
||||||
|
submission.template_submitters = submission.template.submitters
|
||||||
|
|
||||||
|
submission.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_from_emails(template:, user:, emails:, source:, send_email: false)
|
||||||
|
emails = emails.to_s.scan(User::EMAIL_REGEXP)
|
||||||
|
|
||||||
|
emails.map do |email|
|
||||||
|
submission = template.submissions.new(created_by_user: user, source:)
|
||||||
|
submission.submitters.new(email:,
|
||||||
|
uuid: template.submitters.first['uuid'],
|
||||||
|
sent_at: send_email ? Time.current : nil)
|
||||||
|
|
||||||
|
submission.tap(&:save!)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_from_submitters(template:, user:, submissions_attrs:, source:, send_email: false)
|
||||||
|
submissions_attrs.map do |attrs|
|
||||||
|
submission = template.submissions.new(created_by_user: user, source:)
|
||||||
|
|
||||||
|
attrs[:submitters].each_with_index do |submitter_attrs, index|
|
||||||
|
uuid =
|
||||||
|
submitter_attrs[:uuid].presence ||
|
||||||
|
template.submitters.find { |e| e['name'] == submitter_attrs[:name] }&.dig('uuid') ||
|
||||||
|
template.submitters[index]&.dig('uuid')
|
||||||
|
|
||||||
|
next if uuid.blank?
|
||||||
|
|
||||||
|
submission.submitters.new(email: submitter_attrs[:email],
|
||||||
|
sent_at: send_email ? Time.current : nil,
|
||||||
|
uuid:)
|
||||||
|
end
|
||||||
|
|
||||||
|
submission.tap(&:save!)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -27,7 +27,7 @@ module Submissions
|
|||||||
|
|
||||||
pdfs_index = build_pdfs_index(submitter)
|
pdfs_index = build_pdfs_index(submitter)
|
||||||
|
|
||||||
template.fields.each do |field|
|
submitter.submission.template_fields.each do |field|
|
||||||
next if field['submitter_uuid'] != submitter.uuid
|
next if field['submitter_uuid'] != submitter.uuid
|
||||||
|
|
||||||
field.fetch('areas', []).each do |area|
|
field.fetch('areas', []).each do |area|
|
||||||
@@ -159,7 +159,7 @@ module Submissions
|
|||||||
original_documents = template.documents.preload(:blob)
|
original_documents = template.documents.preload(:blob)
|
||||||
|
|
||||||
results =
|
results =
|
||||||
template.schema.map do |item|
|
submitter.submission.template_schema.map do |item|
|
||||||
pdf = pdfs_index[item['attachment_uuid']]
|
pdf = pdfs_index[item['attachment_uuid']]
|
||||||
|
|
||||||
attachment = save_signed_pdf(pdf:, submitter:, certs:, uuid: item['attachment_uuid'], name: item['name'])
|
attachment = save_signed_pdf(pdf:, submitter:, certs:, uuid: item['attachment_uuid'], name: item['name'])
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Submitters
|
||||||
|
module SerializeForWebhook
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def call(submitter)
|
||||||
|
values = build_values_array(submitter)
|
||||||
|
documents = build_documents_array(submitter)
|
||||||
|
|
||||||
|
submitter_name = submitter.submission.template_submitters.find { |e| e['uuid'] == submitter.uuid }['name']
|
||||||
|
|
||||||
|
submitter.as_json(include: [template: { only: %i[id name created_at updated_at] }])
|
||||||
|
.except('uuid', 'values', 'slug')
|
||||||
|
.merge('values' => values,
|
||||||
|
'documents' => documents,
|
||||||
|
'submitter_name' => submitter_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_values_array(submitter)
|
||||||
|
fields_index = submitter.submission.template_fields.index_by { |e| e['uuid'] }
|
||||||
|
attachments_index = submitter.attachments.preload(:blob).index_by(&:uuid)
|
||||||
|
submitter_field_counters = Hash.new { 0 }
|
||||||
|
|
||||||
|
submitter.values.map do |uuid, value|
|
||||||
|
field = fields_index[uuid]
|
||||||
|
submitter_field_counters[field['type']] += 1
|
||||||
|
|
||||||
|
field_name =
|
||||||
|
field['name'].presence || "#{field['type'].titleize} Field #{submitter_field_counters[field['type']]}"
|
||||||
|
|
||||||
|
value = fetch_field_value(field, value, attachments_index)
|
||||||
|
|
||||||
|
{ field: field_name, value: }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_documents_array(submitter)
|
||||||
|
submitter.documents.preload(:blob).map do |attachment|
|
||||||
|
{ name: attachment.filename.base, url: attachment.url }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_field_value(field, value, attachments_index)
|
||||||
|
if field['type'].in?(%w[image signature])
|
||||||
|
attachments_index[value]&.url
|
||||||
|
elsif field['type'] == 'file'
|
||||||
|
Array.wrap(value).compact_blank.filter_map { |e| attachments_index[e]&.url }
|
||||||
|
else
|
||||||
|
value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
+2
-2
@@ -3,10 +3,10 @@
|
|||||||
module Templates
|
module Templates
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def build_field_areas_index(template)
|
def build_field_areas_index(fields)
|
||||||
hash = {}
|
hash = {}
|
||||||
|
|
||||||
template.fields.each do |field|
|
fields.each do |field|
|
||||||
(field['areas'] || []).each do |area|
|
(field['areas'] || []).each do |area|
|
||||||
hash[area['attachment_uuid']] ||= {}
|
hash[area['attachment_uuid']] ||= {}
|
||||||
acc = (hash[area['attachment_uuid']][area['page']] ||= [])
|
acc = (hash[area['attachment_uuid']][area['page']] ||= [])
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Users
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def from_omniauth(oauth)
|
||||||
|
user = User.find_by(email: oauth.info.email)
|
||||||
|
|
||||||
|
return user if user
|
||||||
|
|
||||||
|
User.new(email: oauth.info.email,
|
||||||
|
first_name: oauth.extra.id_info.given_name,
|
||||||
|
last_name: oauth.extra.id_info.family_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
+1
-1
@@ -19,7 +19,7 @@
|
|||||||
"compression-webpack-plugin": "10.0.0",
|
"compression-webpack-plugin": "10.0.0",
|
||||||
"css-loader": "^6.7.3",
|
"css-loader": "^6.7.3",
|
||||||
"css-minimizer-webpack-plugin": "^5.0.0",
|
"css-minimizer-webpack-plugin": "^5.0.0",
|
||||||
"daisyui": "^3.1.5",
|
"daisyui": "^3.2.1",
|
||||||
"mini-css-extract-plugin": "^2.7.5",
|
"mini-css-extract-plugin": "^2.7.5",
|
||||||
"postcss": "^8.4.23",
|
"postcss": "^8.4.23",
|
||||||
"postcss-import": "^15.1.0",
|
"postcss-import": "^15.1.0",
|
||||||
|
|||||||
@@ -2300,13 +2300,14 @@ csstype@^3.1.1:
|
|||||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
|
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
|
||||||
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
|
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
|
||||||
|
|
||||||
daisyui@^3.1.5:
|
daisyui@^3.2.1:
|
||||||
version "3.1.5"
|
version "3.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/daisyui/-/daisyui-3.1.5.tgz#49cb961d03504beebd238d58f0169dbe81103c95"
|
resolved "https://registry.yarnpkg.com/daisyui/-/daisyui-3.2.1.tgz#7fac542b03023adbde2afc869518840dbef509ee"
|
||||||
integrity sha512-3nk5L//IU31ISzsonKEFG8ris8hS/MRAc+PrOTOyRbssjv1fYmNKSdIrOhG6op2ED3RyJL4p6I81JUwd7RWqTw==
|
integrity sha512-gIqE6wiqoJt9G8+n3R/SwLeUnpNCE2eDhT73rP0yZYVaM7o6zVcakBH3aEW5RGpx3UkonPiLuvcgxRcb2lE8TA==
|
||||||
dependencies:
|
dependencies:
|
||||||
colord "^2.9"
|
colord "^2.9"
|
||||||
css-selector-tokenizer "^0.8"
|
css-selector-tokenizer "^0.8"
|
||||||
|
postcss "^8"
|
||||||
postcss-js "^4"
|
postcss-js "^4"
|
||||||
tailwindcss "^3"
|
tailwindcss "^3"
|
||||||
|
|
||||||
@@ -4448,6 +4449,15 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^
|
|||||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
|
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
|
||||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||||
|
|
||||||
|
postcss@^8:
|
||||||
|
version "8.4.26"
|
||||||
|
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.26.tgz#1bc62ab19f8e1e5463d98cf74af39702a00a9e94"
|
||||||
|
integrity sha512-jrXHFF8iTloAenySjM/ob3gSj7pCu0Ji49hnjqzsgSRa50hkWCKD0HQ+gMNJkW38jBI68MpAAg7ZWwHwX8NMMw==
|
||||||
|
dependencies:
|
||||||
|
nanoid "^3.3.6"
|
||||||
|
picocolors "^1.0.0"
|
||||||
|
source-map-js "^1.0.2"
|
||||||
|
|
||||||
postcss@^8.1.10, postcss@^8.4.21, postcss@^8.4.23:
|
postcss@^8.1.10, postcss@^8.4.21, postcss@^8.4.23:
|
||||||
version "8.4.23"
|
version "8.4.23"
|
||||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.23.tgz#df0aee9ac7c5e53e1075c24a3613496f9e6552ab"
|
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.23.tgz#df0aee9ac7c5e53e1075c24a3613496f9e6552ab"
|
||||||
|
|||||||
Reference in new issue
Block a user