mirror of
https://github.com/docusealco/docuseal.git
synced 2026-08-07 15:25:16 +00:00
Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d875477ee6 | |||
| 1daee8214c | |||
| 9cc85595e3 | |||
| f6879d7f94 | |||
| f6247e68b5 | |||
| 454acb63db | |||
| 6d78d5430c | |||
| e75e7b18b4 | |||
| 644b142798 | |||
| e4bb5466f5 | |||
| 8929adbe83 | |||
| 896b149b25 | |||
| 7697b94f73 | |||
| 7912c9a1ee | |||
| 63e1246a11 | |||
| cbca34a7d7 | |||
| 9471a510c9 | |||
| e754c5d414 | |||
| 182f593d86 | |||
| 7f4548e5c7 | |||
| 5e4f207844 | |||
| 1f29e624e7 | |||
| 658e8a7bd6 | |||
| 08c3b04511 | |||
| 7779eb880d | |||
| 21b9ca8ea5 | |||
| db7e3794d9 | |||
| df1d78897a | |||
| e1eb2da6f9 | |||
| a92d1d82c1 | |||
| 2dcfc41365 | |||
| 15cbdfb20c | |||
| a2cf8fc5b5 | |||
| f94ef3afdc | |||
| 9e3a9f0eeb | |||
| 1280572e6f | |||
| cba2e84338 | |||
| 17ca8f013a | |||
| 96ef7ffaf7 | |||
| 1bbcdb8073 | |||
| da130c3f4f | |||
| c15f3ecd83 | |||
| df0e0e6a1b | |||
| 3d75443d6e | |||
| b9af384cf2 | |||
| c40a8e1a69 | |||
| 4c8a706906 | |||
| 4c8e38867a | |||
| 7ed1b71a6b | |||
| 6f3fe1dd7b | |||
| f7ef99a624 | |||
| 03ad848d45 | |||
| 4ffd198505 | |||
| d595e7d52b | |||
| aabbf2dc7f | |||
| 95e4bf945c | |||
| 15523d8377 | |||
| e2cda2bd5c | |||
| 12b98fdcd4 | |||
| be208a1080 | |||
| 9268c60f9e | |||
| 52b310e1a8 | |||
| 16757acbe0 |
@@ -1,18 +0,0 @@
|
||||
spec:
|
||||
name: docuseal
|
||||
services:
|
||||
- name: docuseal
|
||||
dockerfile_path: Dockerfile
|
||||
git:
|
||||
repo_clone_url: https://github.com/docusealco/docuseal.git
|
||||
branch: master
|
||||
envs:
|
||||
- key: SECRET_KEY_BASE
|
||||
value: "- 64 characters string 64 characters string 64 characters string -"
|
||||
type: SECRET
|
||||
scope: RUN_TIME
|
||||
- key: DATABASE_URL
|
||||
scope: RUN_TIME
|
||||
value: ${docuseal-db.DATABASE_URL}
|
||||
databases:
|
||||
- name: docuseal-db
|
||||
@@ -27,3 +27,4 @@
|
||||
/attachments
|
||||
/docuseal
|
||||
.DS_Store
|
||||
.env
|
||||
|
||||
@@ -30,3 +30,5 @@ linters:
|
||||
Enabled: false
|
||||
Style/ZeroLengthPredicate:
|
||||
Enabled: false
|
||||
Style/RedundantConstantBase:
|
||||
Enabled: false
|
||||
|
||||
@@ -127,6 +127,7 @@ jobs:
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 4
|
||||
yarn install
|
||||
sudo apt-get update
|
||||
sudo apt-get install libvips
|
||||
- name: Run
|
||||
env:
|
||||
|
||||
@@ -35,3 +35,4 @@ yarn-debug.log*
|
||||
/coverage
|
||||
/attachments
|
||||
/docuseal
|
||||
/ee
|
||||
|
||||
+4
-1
@@ -27,7 +27,7 @@ Lint/MissingSuper:
|
||||
Enabled: false
|
||||
|
||||
Metrics/MethodLength:
|
||||
Max: 20
|
||||
Max: 25
|
||||
Exclude:
|
||||
- 'db/migrate/**'
|
||||
|
||||
@@ -61,5 +61,8 @@ RSpec/ExampleLength:
|
||||
Rails/I18nLocaleTexts:
|
||||
Enabled: false
|
||||
|
||||
Rails/SkipsModelValidations:
|
||||
Enabled: false
|
||||
|
||||
Rails/ApplicationController:
|
||||
Enabled: false
|
||||
|
||||
@@ -13,9 +13,12 @@ gem 'faraday'
|
||||
gem 'google-cloud-storage', require: false
|
||||
gem 'hexapdf'
|
||||
gem 'image_processing'
|
||||
gem 'jwt'
|
||||
gem 'lograge'
|
||||
gem 'mysql2', require: false
|
||||
gem 'oj'
|
||||
gem 'omniauth-google-oauth2'
|
||||
gem 'omniauth-rails_csrf_protection'
|
||||
gem 'pagy'
|
||||
gem 'pg', require: false
|
||||
gem 'premailer-rails'
|
||||
@@ -25,6 +28,7 @@ gem 'rails-i18n'
|
||||
gem 'rollbar', require: ENV.key?('ROLLBAR_ACCESS_TOKEN')
|
||||
gem 'ruby-vips'
|
||||
gem 'shakapacker'
|
||||
gem 'sidekiq', require: ENV.key?('REDIS_URL')
|
||||
gem 'sqlite3', require: false
|
||||
gem 'strip_attributes'
|
||||
gem 'turbo-rails'
|
||||
|
||||
@@ -232,6 +232,7 @@ GEM
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
hashdiff (1.0.1)
|
||||
hashie (5.0.0)
|
||||
hexapdf (0.32.2)
|
||||
cmdparse (~> 3.0, >= 3.0.3)
|
||||
geom2d (~> 0.3)
|
||||
@@ -282,6 +283,7 @@ GEM
|
||||
minitest (5.18.1)
|
||||
msgpack (1.7.1)
|
||||
multi_json (1.15.0)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.3.0)
|
||||
mysql2 (0.5.5)
|
||||
net-http-persistent (4.0.2)
|
||||
@@ -298,7 +300,29 @@ GEM
|
||||
nio4r (2.5.9)
|
||||
nokogiri (1.15.2-arm64-darwin)
|
||||
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)
|
||||
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)
|
||||
orm_adapter (0.5.0)
|
||||
os (1.1.4)
|
||||
@@ -326,6 +350,8 @@ GEM
|
||||
nio4r (~> 2.0)
|
||||
racc (1.7.1)
|
||||
rack (2.2.7)
|
||||
rack-protection (3.0.6)
|
||||
rack
|
||||
rack-proxy (0.7.6)
|
||||
rack
|
||||
rack-test (2.1.0)
|
||||
@@ -362,6 +388,8 @@ GEM
|
||||
zeitwerk (~> 2.5)
|
||||
rainbow (3.1.1)
|
||||
rake (13.0.6)
|
||||
redis-client (0.14.1)
|
||||
connection_pool
|
||||
regexp_parser (2.8.1)
|
||||
reline (0.3.5)
|
||||
io-console (~> 0.5)
|
||||
@@ -432,6 +460,11 @@ GEM
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 5.2)
|
||||
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)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
@@ -444,6 +477,9 @@ GEM
|
||||
simplecov-html (0.12.3)
|
||||
simplecov_json_formatter (0.1.4)
|
||||
smart_properties (1.17.0)
|
||||
snaky_hash (2.0.1)
|
||||
hashie
|
||||
version_gem (~> 1.1, >= 1.1.1)
|
||||
sqlite3 (1.5.4)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
strip_attributes (1.13.0)
|
||||
@@ -462,6 +498,7 @@ GEM
|
||||
uber (0.1.0)
|
||||
unicode-display_width (2.4.2)
|
||||
uniform_notifier (1.16.0)
|
||||
version_gem (1.1.3)
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
web-console (4.2.0)
|
||||
@@ -503,10 +540,13 @@ DEPENDENCIES
|
||||
google-cloud-storage
|
||||
hexapdf
|
||||
image_processing
|
||||
jwt
|
||||
letter_opener_web
|
||||
lograge
|
||||
mysql2
|
||||
oj
|
||||
omniauth-google-oauth2
|
||||
omniauth-rails_csrf_protection
|
||||
pagy
|
||||
pg
|
||||
premailer-rails
|
||||
@@ -522,6 +562,7 @@ DEPENDENCIES
|
||||
rubocop-rspec
|
||||
ruby-vips
|
||||
shakapacker
|
||||
sidekiq
|
||||
simplecov
|
||||
sqlite3
|
||||
strip_attributes
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<a href="https://hub.docker.com/r/docuseal/docuseal">
|
||||
<img alt="Docker releases" src="https://img.shields.io/docker/v/docuseal/docuseal">
|
||||
</a>
|
||||
<a href="https://discord.com/invite/B5wg5wKk">
|
||||
<a href="https://discord.gg/qygYCDGck9">
|
||||
<img src="https://img.shields.io/discord/1125112641170448454?logo=discord"/>
|
||||
</a>
|
||||
<a href="https://twitter.com/intent/follow?screen_name=docusealco">
|
||||
@@ -26,6 +26,8 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
||||
</p>
|
||||
<h2 align="center">
|
||||
<a href="https://demo.docuseal.co">✨ Live Demo</a>
|
||||
<span>|</span>
|
||||
<a href="https://docuseal.co/sign_up">☁️ Try in Cloud</a>
|
||||
</h2>
|
||||
|
||||
[](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] Users management
|
||||
- [x] Mobile-optimized
|
||||
- [x] API and Webhooks for integrations
|
||||
- [x] Easy to deploy in minutes
|
||||
|
||||
## Deploy
|
||||
|
||||
| Heroku | Railway | DigitalOcean |
|
||||
| ---------------------------------------------------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
|
||||
| [<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/tree/master&refcode=421d50f53990) |
|
||||
|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)|
|
||||
|**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
|
||||
@@ -64,11 +69,19 @@ Download docker-compose.yml into your private server:
|
||||
curl https://raw.githubusercontent.com/docusealco/docuseal/master/docker-compose.yml > docker-compose.yml
|
||||
```
|
||||
|
||||
Run docker compose under a custom domain over https (make sure your DNS points to the server to automatically issue ssl certs with Caddy):
|
||||
Run the app under a custom domain over https using docker compose (make sure your DNS points to the server to automatically issue ssl certs with Caddy):
|
||||
```sh
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
def app_url_params
|
||||
return {} if params[:encrypted_config].blank?
|
||||
|
||||
params.require(:encrypted_config).permit(:value)
|
||||
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
|
||||
class TemplatesController < ApiBaseController
|
||||
def index
|
||||
render json: current_account.templates
|
||||
end
|
||||
|
||||
def update
|
||||
@template = current_account.templates.find(params[:id])
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
module Api
|
||||
class TemplatesDocumentsController < ApiBaseController
|
||||
def create
|
||||
return head :unprocessable_entity if params[:blobs].blank? && params[:files].blank?
|
||||
|
||||
@template = current_account.templates.find(params[:template_id])
|
||||
|
||||
documents =
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ApiSettingsController < ApplicationController
|
||||
def index; end
|
||||
end
|
||||
@@ -23,7 +23,7 @@ class ApplicationController < ActionController::Base
|
||||
private
|
||||
|
||||
def sign_in_for_demo
|
||||
sign_in(User.first) unless signed_in?
|
||||
sign_in(User.order('random()').take) unless signed_in?
|
||||
end
|
||||
|
||||
def current_account
|
||||
@@ -34,8 +34,8 @@ class ApplicationController < ActionController::Base
|
||||
redirect_to setup_index_path unless User.exists?
|
||||
end
|
||||
|
||||
def button_title(title: 'Submit', disabled_with: 'Submitting', icon: nil)
|
||||
render_to_string(partial: 'shared/button_title', locals: { title:, disabled_with:, icon: })
|
||||
def button_title(title: 'Submit', disabled_with: 'Submitting', icon: nil, icon_disabled: nil)
|
||||
render_to_string(partial: 'shared/button_title', locals: { title:, disabled_with:, icon:, icon_disabled: })
|
||||
end
|
||||
|
||||
def svg_icon(icon_name, class: '')
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ConsoleRedirectController < ApplicationController
|
||||
def index
|
||||
auth = JsonWebToken.encode(uuid: current_user.uuid,
|
||||
scope: :console,
|
||||
exp: 1.minute.from_now.to_i)
|
||||
|
||||
redirect_to("#{Docuseal::CONSOLE_URL}?#{{ auth: }.to_query}", allow_other_host: true)
|
||||
end
|
||||
end
|
||||
@@ -4,6 +4,7 @@ class DashboardController < ApplicationController
|
||||
skip_before_action :authenticate_user!, only: %i[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?
|
||||
|
||||
templates = current_account.templates.active.preload(:author).order(id: :desc)
|
||||
|
||||
@@ -7,10 +7,16 @@ class EmailSettingsController < ApplicationController
|
||||
|
||||
def create
|
||||
if @encrypted_config.update(storage_configs)
|
||||
SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email']).deliver_now!
|
||||
|
||||
redirect_to settings_email_index_path, notice: 'Changes have been saved'
|
||||
else
|
||||
render :index, status: :unprocessable_entity
|
||||
end
|
||||
rescue Net::SMTPError, OpenSSL::SSL::SSLError, Net::ReadTimeout => e
|
||||
flash[:alert] = e.message
|
||||
|
||||
render :index, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -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
|
||||
|
||||
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
|
||||
|
||||
def require_no_authentication
|
||||
super
|
||||
|
||||
flash.clear
|
||||
end
|
||||
|
||||
def build_resource(_hash = {})
|
||||
account = Account.new(account_params)
|
||||
account.timezone = Accounts.normalize_timezone(account.timezone)
|
||||
|
||||
self.resource = account.users.new(user_params)
|
||||
|
||||
account.name ||= "#{resource.full_name}'s Company" if params[:action] == 'create'
|
||||
end
|
||||
|
||||
def user_params
|
||||
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
|
||||
|
||||
def account_params
|
||||
return {} if params[:account].blank?
|
||||
|
||||
params.require(:account).permit(:name, :timezone)
|
||||
params.require(:account).permit(:name, :timezone).compact_blank
|
||||
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
|
||||
)
|
||||
|
||||
@submitter.submission ||= Submission.new(template: @template)
|
||||
@submitter.submission ||= Submission.new(template: @template, source: :link)
|
||||
|
||||
if @submitter.save
|
||||
redirect_to submit_form_path(@submitter.slug)
|
||||
|
||||
@@ -17,19 +17,29 @@ class SubmissionsController < ApplicationController
|
||||
def create
|
||||
submissions =
|
||||
if params[:emails].present?
|
||||
create_submissions_from_emails
|
||||
Submissions.create_from_emails(template: @template,
|
||||
user: current_user,
|
||||
source: :invite,
|
||||
send_email: params[:send_email] == '1',
|
||||
emails: params[:emails])
|
||||
else
|
||||
create_submissions_from_submitters
|
||||
Submissions.create_from_submitters(template: @template,
|
||||
user: current_user,
|
||||
source: :invite,
|
||||
send_email: params[:send_email] == '1',
|
||||
submissions_attrs: submissions_params[:submission].to_h.values)
|
||||
end
|
||||
|
||||
submitters = submissions.flat_map(&:submitters)
|
||||
|
||||
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!
|
||||
end
|
||||
end
|
||||
|
||||
redirect_to template_path(@template),
|
||||
notice: "#{submissions.size} #{'recipient'.pluralize(submissions.size)} added"
|
||||
notice: "#{submitters.size} #{'recipient'.pluralize(submitters.size)} added"
|
||||
end
|
||||
|
||||
def destroy
|
||||
@@ -43,30 +53,6 @@ class SubmissionsController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def create_submissions_from_emails
|
||||
emails = params[:emails].to_s.scan(User::EMAIL_REGEXP)
|
||||
|
||||
emails.map do |email|
|
||||
submission = @template.submissions.new(created_by_user: current_user)
|
||||
submission.submitters.new(email:, uuid: @template.submitters.first['uuid'],
|
||||
sent_at: params[:send_email] == '1' ? Time.current : nil)
|
||||
|
||||
submission.tap(&:save!)
|
||||
end
|
||||
end
|
||||
|
||||
def create_submissions_from_submitters
|
||||
submissions_params[:submission].to_h.map do |_, attrs|
|
||||
submission = @template.submissions.new(created_by_user: current_user)
|
||||
|
||||
attrs[:submitters].each do |submitter_attrs|
|
||||
submission.submitters.new(**submitter_attrs, sent_at: params[:send_email] == '1' ? Time.current : nil)
|
||||
end
|
||||
|
||||
submission.tap(&:save!)
|
||||
end
|
||||
end
|
||||
|
||||
def submissions_params
|
||||
params.permit(submission: { submitters: [%i[uuid email]] })
|
||||
end
|
||||
|
||||
@@ -17,15 +17,20 @@ class SubmitFormController < ApplicationController
|
||||
|
||||
def update
|
||||
submitter = Submitter.find_by!(slug: params[:slug])
|
||||
submitter.values.merge!(normalized_values)
|
||||
submitter.completed_at = Time.current if params[:completed] == 'true'
|
||||
submitter.opened_at ||= Time.current
|
||||
|
||||
submitter.save!
|
||||
update_submitter!(submitter)
|
||||
|
||||
Submissions.update_template_fields!(submitter.submission) if submitter.submission.template_fields.blank?
|
||||
|
||||
submitter.submission.save!
|
||||
|
||||
if submitter.completed_at?
|
||||
GenerateSubmitterResultAttachmentsJob.perform_later(submitter)
|
||||
|
||||
if submitter.account.encrypted_configs.exists?(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
||||
SendWebhookRequestJob.perform_later(submitter)
|
||||
end
|
||||
|
||||
submitter.submission.template.account.users.active.each do |user|
|
||||
SubmitterMailer.completed_email(submitter, user).deliver_later!
|
||||
end
|
||||
@@ -40,6 +45,16 @@ class SubmitFormController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def update_submitter!(submitter)
|
||||
submitter.values.merge!(normalized_values)
|
||||
submitter.completed_at = Time.current if params[:completed] == 'true'
|
||||
submitter.opened_at ||= Time.current
|
||||
|
||||
submitter.save!
|
||||
|
||||
submitter
|
||||
end
|
||||
|
||||
def normalized_values
|
||||
params.fetch(:values, {}).to_unsafe_h.transform_values do |v|
|
||||
if params[:cast_boolean] == 'true'
|
||||
|
||||
@@ -24,24 +24,30 @@ class UsersController < ApplicationController
|
||||
if @user.save
|
||||
UserMailer.invitation_email(@user).deliver_later!
|
||||
|
||||
redirect_to settings_users_path, notice: 'User has been invited.'
|
||||
redirect_to settings_users_path, notice: 'User has been invited'
|
||||
else
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
return redirect_to settings_users_path, notice: 'Unable to update user.' if Docuseal.demo?
|
||||
|
||||
if @user.update(user_params.compact_blank)
|
||||
redirect_to settings_users_path, notice: 'User has been updated.'
|
||||
redirect_to settings_users_path, notice: 'User has been updated'
|
||||
else
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if Docuseal.demo? || @user.id == current_user.id
|
||||
return redirect_to settings_users_path, notice: 'Unable to remove user'
|
||||
end
|
||||
|
||||
@user.update!(deleted_at: Time.current)
|
||||
|
||||
redirect_to settings_users_path, notice: 'User has been removed.'
|
||||
redirect_to settings_users_path, notice: 'User has been removed'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -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
|
||||
@@ -70,3 +70,7 @@ button[disabled] .enabled {
|
||||
.base-radio {
|
||||
@apply radio bg-white radio-sm no-animation;
|
||||
}
|
||||
|
||||
.base-select {
|
||||
@apply select base-input w-full font-normal;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.querySelectorAll('a').forEach((link) => {
|
||||
if (document.location.pathname.startsWith(link.pathname)) {
|
||||
if (document.location.pathname.startsWith(link.pathname) && !link.getAttribute('href').startsWith('http')) {
|
||||
link.classList.add('bg-base-300')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -13,7 +13,7 @@ export default actionable(class extends HTMLElement {
|
||||
document.body.classList.remove('overflow-hidden')
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ window.customElements.define('submission-form', class extends HTMLElement {
|
||||
authenticityToken: this.dataset.authenticityToken,
|
||||
canSendEmail: this.dataset.canSendEmail === 'true',
|
||||
isDirectUpload: this.dataset.isDirectUpload === 'true',
|
||||
isDemo: this.dataset.isDemo === 'true',
|
||||
values: reactive(JSON.parse(this.dataset.values)),
|
||||
attachments: reactive(JSON.parse(this.dataset.attachments)),
|
||||
fields: JSON.parse(this.dataset.fields)
|
||||
|
||||
@@ -21,14 +21,14 @@
|
||||
</span>
|
||||
</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"
|
||||
>
|
||||
{{ field.name || fieldNames[field.type] }}
|
||||
<template v-if="field.type === 'checkbox'">
|
||||
<template v-if="field.type === 'checkbox' && !field.name">
|
||||
{{ fieldIndex + 1 }}
|
||||
</template>
|
||||
<template v-else-if="!field.required">
|
||||
<template v-else-if="!field.required && field.type !== 'checkbox'">
|
||||
(optional)
|
||||
</template>
|
||||
</div>
|
||||
@@ -146,6 +146,11 @@ export default {
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
withLabel: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
fieldIndex: {
|
||||
type: Number,
|
||||
required: false,
|
||||
@@ -207,7 +212,7 @@ export default {
|
||||
},
|
||||
formattedDate () {
|
||||
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 {
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
v-for="(area, areaIndex) in field.areas"
|
||||
:key="areaIndex"
|
||||
>
|
||||
<Teleport :to="`#page-${area.attachment_uuid}-${area.page}`">
|
||||
<Teleport
|
||||
:to="`#page-${area.attachment_uuid}-${area.page}`"
|
||||
>
|
||||
<FieldArea
|
||||
:ref="setAreaRef"
|
||||
v-model="values[field.uuid]"
|
||||
@@ -20,6 +22,7 @@
|
||||
:submittable="true"
|
||||
:field-index="fieldIndex"
|
||||
:is-active="currentStep === step"
|
||||
:with-label="withLabel"
|
||||
:is-value-set="step.some((f) => f.uuid in values)"
|
||||
:attachments-index="attachmentsIndex"
|
||||
@click="$emit('focus-step', step)"
|
||||
@@ -54,6 +57,11 @@ export default {
|
||||
required: false,
|
||||
default: () => ({})
|
||||
},
|
||||
withLabel: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
currentStep: {
|
||||
type: Array,
|
||||
required: false,
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</p>
|
||||
<div class="space-y-3 mt-5">
|
||||
<button
|
||||
v-if="canSendEmail"
|
||||
v-if="canSendEmail && !isDemo"
|
||||
class="white-button flex items-center space-x-1 w-full"
|
||||
:disabled="isSendingCopy"
|
||||
@click.prevent="sendCopyToEmail"
|
||||
@@ -40,6 +40,27 @@
|
||||
Download
|
||||
</span>
|
||||
</button>
|
||||
<a
|
||||
v-if="isDemo"
|
||||
target="_blank"
|
||||
href="https://github.com/docusealco/docuseal"
|
||||
class="white-button flex items-center space-x-1 w-full"
|
||||
>
|
||||
<IconBrandGithub />
|
||||
<span>
|
||||
Star on Github
|
||||
</span>
|
||||
</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 class="text-center mt-4">
|
||||
Signed with
|
||||
@@ -53,14 +74,16 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IconCircleCheck, IconMail, IconDownload, IconInnerShadowTop } from '@tabler/icons-vue'
|
||||
import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop, IconLogin } from '@tabler/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'FormCompleted',
|
||||
components: {
|
||||
IconCircleCheck,
|
||||
IconInnerShadowTop,
|
||||
IconBrandGithub,
|
||||
IconMail,
|
||||
IconLogin,
|
||||
IconDownload
|
||||
},
|
||||
props: {
|
||||
@@ -68,6 +91,11 @@ export default {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
isDemo: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
canSendEmail: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
||||
@@ -4,261 +4,306 @@
|
||||
:steps="stepFields"
|
||||
:values="values"
|
||||
:attachments-index="attachmentsIndex"
|
||||
:with-label="!isAnonymousChecboxes"
|
||||
:current-step="currentStepFields"
|
||||
@focus-step="[saveStep(), goToStep($event, false, true)]"
|
||||
@focus-step="[saveStep(), goToStep($event, false, true), currentField.type !== 'checkbox' ? isFormVisible = true : '']"
|
||||
/>
|
||||
<div>
|
||||
<form
|
||||
<button
|
||||
v-if="!isFormVisible"
|
||||
class="btn btn-neutral text-white absolute rounded-none border-x-0 md:border md:rounded-full bottom-0 w-full md:mb-4 text-base"
|
||||
@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"
|
||||
ref="form"
|
||||
:action="submitPath"
|
||||
method="post"
|
||||
class="md:mx-16"
|
||||
@submit.prevent="submitStep"
|
||||
class="absolute right-0 mr-2 mt-2 top-0 hidden md:block"
|
||||
title="Minimize"
|
||||
@click.prevent="isFormVisible = false"
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="authenticity_token"
|
||||
:value="authenticityToken"
|
||||
<IconArrowsDiagonalMinimize2
|
||||
:width="20"
|
||||
:height="20"
|
||||
/>
|
||||
</button>
|
||||
<div>
|
||||
<form
|
||||
v-if="!isCompleted"
|
||||
ref="form"
|
||||
:action="submitPath"
|
||||
method="post"
|
||||
class="md:mx-16"
|
||||
@submit.prevent="submitStep"
|
||||
>
|
||||
<input
|
||||
value="put"
|
||||
name="_method"
|
||||
type="hidden"
|
||||
>
|
||||
<div class="md:mt-4">
|
||||
<div v-if="['cells', 'text'].includes(currentField.type)">
|
||||
<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>
|
||||
<input
|
||||
<input
|
||||
type="hidden"
|
||||
name="authenticity_token"
|
||||
:value="authenticityToken"
|
||||
>
|
||||
<input
|
||||
value="put"
|
||||
name="_method"
|
||||
type="hidden"
|
||||
>
|
||||
<div class="md:mt-4">
|
||||
<div v-if="['cells', 'text'].includes(currentField.type)">
|
||||
<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>
|
||||
<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"
|
||||
v-model="values[currentField.uuid]"
|
||||
class="base-input !text-2xl w-full"
|
||||
:required="currentField.required"
|
||||
:placeholder="`Type here...${currentField.required ? '' : ' (optional)'}`"
|
||||
type="text"
|
||||
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)"
|
||||
>
|
||||
</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"
|
||||
: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
|
||||
<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"
|
||||
>
|
||||
<label
|
||||
:for="currentField.uuid + option"
|
||||
class="flex items-center space-x-3"
|
||||
{{ 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"
|
||||
:key="index"
|
||||
>
|
||||
<input
|
||||
:id="currentField.uuid + option"
|
||||
v-model="values[currentField.uuid]"
|
||||
type="radio"
|
||||
class="base-radio !h-7 !w-7"
|
||||
:name="`values[${currentField.uuid}]`"
|
||||
:value="option"
|
||||
:required="currentField.required"
|
||||
<label
|
||||
:for="currentField.uuid + option"
|
||||
class="flex items-center space-x-3"
|
||||
>
|
||||
<span class="text-xl">
|
||||
{{ option }}
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
:id="currentField.uuid + option"
|
||||
v-model="values[currentField.uuid]"
|
||||
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>
|
||||
<MultiSelectStep
|
||||
v-else-if="currentField.type === 'multiple'"
|
||||
v-model="values[currentField.uuid]"
|
||||
:field="currentField"
|
||||
/>
|
||||
<div
|
||||
v-else-if="currentField.type === 'checkbox'"
|
||||
class="flex w-full"
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="cast_boolean"
|
||||
value="true"
|
||||
>
|
||||
<MultiSelectStep
|
||||
v-else-if="currentField.type === 'multiple'"
|
||||
v-model="values[currentField.uuid]"
|
||||
:field="currentField"
|
||||
/>
|
||||
<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
|
||||
v-for="(field, index) in currentStepFields"
|
||||
:key="field.uuid"
|
||||
<input
|
||||
type="hidden"
|
||||
name="cast_boolean"
|
||||
value="true"
|
||||
>
|
||||
<label
|
||||
:for="field.uuid"
|
||||
class="flex items-center space-x-3"
|
||||
>
|
||||
<div
|
||||
class="space-y-3.5 mx-auto"
|
||||
>
|
||||
<template v-if="isAnonymousChecboxes">
|
||||
Complete hightlighted checkboxes and click <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? 'submit' : 'next' }}</span>.
|
||||
<input
|
||||
v-for="field in currentStepFields"
|
||||
:key="field.uuid"
|
||||
type="hidden"
|
||||
:name="`values[${field.uuid}]`"
|
||||
:value="!!values[field.uuid]"
|
||||
>
|
||||
<input
|
||||
: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]]"
|
||||
</template>
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="(field, index) in currentStepFields"
|
||||
:key="field.uuid"
|
||||
>
|
||||
<span class="text-xl">
|
||||
{{ currentField.name || currentField.type + ' ' + (index + 1) }}
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
:for="field.uuid"
|
||||
class="flex items-center space-x-3"
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
:name="`values[${field.uuid}]`"
|
||||
:value="!!values[field.uuid]"
|
||||
>
|
||||
<input
|
||||
: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>
|
||||
<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>
|
||||
<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)"
|
||||
/>
|
||||
<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 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
|
||||
<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 v-else>
|
||||
Next
|
||||
</span><span
|
||||
v-if="isSubmitting"
|
||||
class="w-6 flex justify-start mr-1"
|
||||
><span>...</span></span>
|
||||
</span>
|
||||
<span v-else>
|
||||
Next
|
||||
</span><span
|
||||
v-if="isSubmitting"
|
||||
class="w-6 flex justify-start mr-1"
|
||||
><span>...</span></span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<FormCompleted
|
||||
v-else
|
||||
:can-send-email="canSendEmail"
|
||||
:submitter-slug="submitterSlug"
|
||||
/>
|
||||
<div class="flex justify-center">
|
||||
<div class="flex items-center mt-5 mb-1">
|
||||
<a
|
||||
v-for="(step, index) in stepFields"
|
||||
: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)]"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<FormCompleted
|
||||
v-else
|
||||
:is-demo="isDemo"
|
||||
:can-send-email="canSendEmail"
|
||||
:submitter-slug="submitterSlug"
|
||||
/>
|
||||
<div class="flex justify-center">
|
||||
<div class="flex items-center mt-5 mb-1">
|
||||
<a
|
||||
v-for="(step, index) in stepFields"
|
||||
: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>
|
||||
@@ -271,7 +316,7 @@ import SignatureStep from './signature_step'
|
||||
import AttachmentStep from './attachment_step'
|
||||
import MultiSelectStep from './multi_select_step'
|
||||
import FormCompleted from './completed'
|
||||
import { IconInnerShadowTop } from '@tabler/icons-vue'
|
||||
import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'SubmissionForm',
|
||||
@@ -282,6 +327,8 @@ export default {
|
||||
AttachmentStep,
|
||||
MultiSelectStep,
|
||||
IconInnerShadowTop,
|
||||
IconArrowsDiagonal,
|
||||
IconArrowsDiagonalMinimize2,
|
||||
FormCompleted
|
||||
},
|
||||
props: {
|
||||
@@ -317,6 +364,11 @@ export default {
|
||||
required: true,
|
||||
default: false
|
||||
},
|
||||
isDemo: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false
|
||||
},
|
||||
values: {
|
||||
type: Object,
|
||||
required: false,
|
||||
@@ -326,6 +378,7 @@ export default {
|
||||
data () {
|
||||
return {
|
||||
isCompleted: false,
|
||||
isFormVisible: true,
|
||||
currentStep: 0,
|
||||
isSubmitting: false
|
||||
}
|
||||
@@ -334,6 +387,9 @@ export default {
|
||||
currentStepFields () {
|
||||
return this.stepFields[this.currentStep]
|
||||
},
|
||||
isAnonymousChecboxes () {
|
||||
return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name) && this.currentStepFields.length > 4
|
||||
},
|
||||
isButtonDisabled () {
|
||||
return this.isSubmitting ||
|
||||
(this.currentField.required && ['image', 'file'].includes(this.currentField.type) && !this.values[this.currentField.uuid]?.length) ||
|
||||
@@ -390,7 +446,7 @@ export default {
|
||||
|
||||
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()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -4,22 +4,62 @@
|
||||
<label
|
||||
class="label text-2xl"
|
||||
>{{ field.name || 'Signature' }}</label>
|
||||
<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>
|
||||
<div class="space-x-2 flex">
|
||||
<span
|
||||
class="tooltip"
|
||||
data-tip="Type text"
|
||||
>
|
||||
<button
|
||||
class="btn btn-sm btn-circle"
|
||||
:class="{ 'btn-neutral': isTextSignature, 'btn-outline': !isTextSignature }"
|
||||
@click.prevent="toggleTextInput"
|
||||
>
|
||||
<IconTextSize :width="16" />
|
||||
</button>
|
||||
</span>
|
||||
<span
|
||||
class="tooltip"
|
||||
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>
|
||||
<input
|
||||
:value="modelValue"
|
||||
@@ -29,23 +69,35 @@
|
||||
<img
|
||||
v-if="modelValue"
|
||||
: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
|
||||
v-show="!modelValue"
|
||||
ref="canvas"
|
||||
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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IconReload } from '@tabler/icons-vue'
|
||||
import { IconReload, IconCamera, IconTextSize, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'SignatureStep',
|
||||
components: {
|
||||
IconReload
|
||||
IconReload,
|
||||
IconCamera,
|
||||
IconTextSize,
|
||||
IconArrowsDiagonalMinimize2
|
||||
},
|
||||
props: {
|
||||
field: {
|
||||
@@ -72,10 +124,11 @@ export default {
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
emits: ['attached', 'update:model-value'],
|
||||
emits: ['attached', 'update:model-value', 'start', 'minimize'],
|
||||
data () {
|
||||
return {
|
||||
isSignatureStarted: false
|
||||
isSignatureStarted: false,
|
||||
isTextSignature: false
|
||||
}
|
||||
},
|
||||
async mounted () {
|
||||
@@ -94,6 +147,8 @@ export default {
|
||||
|
||||
this.pad.addEventListener('beginStroke', () => {
|
||||
this.isSignatureStarted = true
|
||||
|
||||
this.$emit('start')
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
@@ -104,6 +159,134 @@ export default {
|
||||
this.pad.clear()
|
||||
|
||||
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 () {
|
||||
if (this.modelValue) {
|
||||
@@ -111,7 +294,7 @@ export default {
|
||||
}
|
||||
|
||||
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' })
|
||||
|
||||
if (this.isDirectUpload) {
|
||||
@@ -153,7 +336,7 @@ export default {
|
||||
return resolve(attachment)
|
||||
})
|
||||
}
|
||||
}, 'image/png')
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
@blur="onNameBlur"
|
||||
>{{ field.name || defaultName }}</span>
|
||||
<div
|
||||
v-if="isNameFocus"
|
||||
v-if="isNameFocus && field.type !== 'checkbox'"
|
||||
class="flex items-center ml-1.5"
|
||||
>
|
||||
<input
|
||||
|
||||
@@ -64,8 +64,11 @@
|
||||
:with-arrows="template.schema.length > 1"
|
||||
:item="item"
|
||||
:document="sortedDocuments[index]"
|
||||
:template="template"
|
||||
:is-direct-upload="isDirectUpload"
|
||||
@scroll-to="scrollIntoDocument(item)"
|
||||
@remove="onDocumentRemove"
|
||||
@replace="onDocumentReplace"
|
||||
@up="moveDocument(item, -1)"
|
||||
@down="moveDocument(item, 1)"
|
||||
@change="save"
|
||||
@@ -133,7 +136,7 @@
|
||||
<p>
|
||||
<button
|
||||
class="base-button"
|
||||
@click="drawField = false"
|
||||
@click="drawField = null"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -314,7 +317,7 @@ export default {
|
||||
const field = {
|
||||
name: '',
|
||||
uuid: v4(),
|
||||
required: true,
|
||||
required: type !== 'checkbox',
|
||||
type,
|
||||
submitter_uuid: this.selectedSubmitter.uuid,
|
||||
areas: [area]
|
||||
@@ -333,7 +336,7 @@ export default {
|
||||
type: this.dragFieldType,
|
||||
uuid: v4(),
|
||||
submitter_uuid: this.selectedSubmitter.uuid,
|
||||
required: true
|
||||
required: this.dragFieldType !== 'checkbox'
|
||||
}
|
||||
|
||||
if (['select', 'multiple', 'radio'].includes(this.dragFieldType)) {
|
||||
@@ -353,7 +356,7 @@ export default {
|
||||
|
||||
if (this.selectedField?.type === this.dragFieldType) {
|
||||
baseArea = this.selectedAreaRef.value
|
||||
} else if (previousField?.areas) {
|
||||
} else if (previousField?.areas?.length) {
|
||||
baseArea = previousField.areas[previousField.areas.length - 1]
|
||||
} else {
|
||||
if (['checkbox'].includes(this.dragFieldType)) {
|
||||
@@ -417,6 +420,27 @@ export default {
|
||||
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()
|
||||
},
|
||||
moveDocument (item, direction) {
|
||||
|
||||
@@ -93,12 +93,21 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.isDirectUpload) {
|
||||
import('@rails/activestorage')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
upload: Upload.methods.upload,
|
||||
onDropFiles (e) {
|
||||
this.$refs.input.files = e.dataTransfer.files
|
||||
if ([...e.dataTransfer.files].every((f) => f.type.match(/(?:image\/)|(?:application\/pdf)/))) {
|
||||
this.$refs.input.files = e.dataTransfer.files
|
||||
|
||||
this.upload()
|
||||
this.upload()
|
||||
} else {
|
||||
alert('Only PDF and images are supported.')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,19 +31,21 @@
|
||||
v-if="isNameFocus"
|
||||
class="flex items-center relative"
|
||||
>
|
||||
<input
|
||||
:id="`required-checkbox-${field.uuid}`"
|
||||
v-model="field.required"
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-xs no-animation rounded"
|
||||
@mousedown.prevent
|
||||
>
|
||||
<label
|
||||
:for="`required-checkbox-${field.uuid}`"
|
||||
class="label text-xs"
|
||||
@click.prevent="field.required = !field.required"
|
||||
@mousedown.prevent
|
||||
>Required</label>
|
||||
<template v-if="field.type !== 'checkbox'">
|
||||
<input
|
||||
:id="`required-checkbox-${field.uuid}`"
|
||||
v-model="field.required"
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-xs no-animation rounded"
|
||||
@mousedown.prevent
|
||||
>
|
||||
<label
|
||||
:for="`required-checkbox-${field.uuid}`"
|
||||
class="label text-xs"
|
||||
@click.prevent="field.required = !field.required"
|
||||
@mousedown.prevent
|
||||
>Required</label>
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
|
||||
@@ -71,12 +71,12 @@
|
||||
v-if="fields.length < 4"
|
||||
class="text-xs p-2 border border-base-200 rounded"
|
||||
>
|
||||
<ui>
|
||||
<ul class="list-disc list-outside ml-3">
|
||||
<li>
|
||||
Draw a text field on the page with a mouse
|
||||
</li>
|
||||
<li>
|
||||
Single click of the page to add a checkbox
|
||||
Single click on the page to add a checkbox
|
||||
</li>
|
||||
<li>
|
||||
Drag & drop any other field type on the page
|
||||
@@ -84,7 +84,7 @@
|
||||
<li>
|
||||
Click on the field type above to add it to the form without drawing it on the document
|
||||
</li>
|
||||
</ui>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -171,7 +171,8 @@ export default {
|
||||
const field = {
|
||||
name: '',
|
||||
uuid: v4(),
|
||||
required: true,
|
||||
required: type !== 'checkbox',
|
||||
areas: [],
|
||||
submitter_uuid: this.selectedSubmitter.uuid,
|
||||
type
|
||||
}
|
||||
|
||||
@@ -9,39 +9,51 @@
|
||||
loading="lazy"
|
||||
>
|
||||
<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)"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col justify-between opacity-0 group-hover:opacity-100"
|
||||
>
|
||||
<div class="rounded-bl rounded-tr bg-white border">
|
||||
<button
|
||||
class="rounded-bl rounded-tr hover:text-base-100 hover:bg-base-content w-full transition-colors"
|
||||
style="width: 24px"
|
||||
@click.stop="$emit('remove', item)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<div class="flex justify-between w-full">
|
||||
<div style="width: 26px" />
|
||||
<div class="">
|
||||
<ReplaceButton
|
||||
:is-direct-upload="isDirectUpload"
|
||||
:template-id="template.id"
|
||||
class="opacity-0 group-hover:opacity-100"
|
||||
@click.stop
|
||||
@success="$emit('replace', { replaceSchemaItem: item, ...$event })"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="withArrows"
|
||||
class="flex flex-col border rounded-br rounded-tl bg-white divide-y"
|
||||
class="flex flex-col justify-between opacity-0 group-hover:opacity-100"
|
||||
>
|
||||
<button
|
||||
class="rounded-tl hover:text-base-100 hover:bg-base-content w-full transition-colors"
|
||||
style="width: 24px"
|
||||
@click.stop="$emit('up', item)"
|
||||
<div>
|
||||
<button
|
||||
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
|
||||
style="width: 24px; height: 24px"
|
||||
@click.stop="$emit('remove', item)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="withArrows"
|
||||
class="flex flex-col space-y-1"
|
||||
>
|
||||
↑
|
||||
</button>
|
||||
<button
|
||||
class="rounded-br hover:text-base-100 hover:bg-base-content w-full transition-colors"
|
||||
style="width: 24px"
|
||||
@click.stop="$emit('down', item)"
|
||||
>
|
||||
↓
|
||||
</button>
|
||||
<button
|
||||
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
|
||||
style="width: 24px; height: 24px"
|
||||
@click.stop="$emit('up', item)"
|
||||
>
|
||||
↑
|
||||
</button>
|
||||
<button
|
||||
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
|
||||
style="width: 24px; height: 24px"
|
||||
@click.stop="$emit('down', item)"
|
||||
>
|
||||
↓
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -60,34 +72,52 @@
|
||||
|
||||
<script>
|
||||
import Contenteditable from './contenteditable'
|
||||
import Upload from './upload'
|
||||
import ReplaceButton from './replace'
|
||||
|
||||
export default {
|
||||
name: 'DocumentPreview',
|
||||
components: {
|
||||
Contenteditable
|
||||
Contenteditable,
|
||||
ReplaceButton
|
||||
},
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
template: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
document: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isDirectUpload: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false
|
||||
},
|
||||
withArrows: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
emits: ['scroll-to', 'change', 'remove', 'up', 'down'],
|
||||
emits: ['scroll-to', 'change', 'remove', 'up', 'down', 'replace'],
|
||||
computed: {
|
||||
previewImage () {
|
||||
return this.document.preview_images[0]
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.isDirectUpload) {
|
||||
import('@rails/activestorage')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
upload: Upload.methods.upload,
|
||||
onUpdateName (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
|
||||
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SettingsMailer < ApplicationMailer
|
||||
def smtp_successful_setup(email)
|
||||
mail(to: email, subject: 'SMTP has been configured')
|
||||
end
|
||||
end
|
||||
@@ -3,12 +3,13 @@
|
||||
class SubmitterMailer < ApplicationMailer
|
||||
DEFAULT_MESSAGE = %(You have been invited to submit the "%<name>s" form:)
|
||||
|
||||
def invitation_email(submitter, message: format(DEFAULT_MESSAGE, name: submitter.submission.template.name))
|
||||
def invitation_email(submitter, message: '')
|
||||
@submitter = submitter
|
||||
@message = message
|
||||
@message = message.presence || format(DEFAULT_MESSAGE, name: submitter.submission.template.name)
|
||||
|
||||
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
|
||||
|
||||
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 :encrypted_configs, dependent: :destroy
|
||||
has_many :templates, dependent: :destroy
|
||||
has_many :submissions, through: :templates
|
||||
has_many :submitters, through: :submissions
|
||||
has_many :active_users, -> { active }, dependent: :destroy,
|
||||
inverse_of: :account, class_name: 'User'
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ class EncryptedConfig < ApplicationRecord
|
||||
EMAIL_SMTP_KEY = 'action_mailer_smtp'
|
||||
ESIGN_CERTS_KEY = 'esign_certs'
|
||||
APP_URL_KEY = 'app_url'
|
||||
WEBHOOK_URL_KEY = 'webhook_url'
|
||||
|
||||
belongs_to :account
|
||||
|
||||
|
||||
@@ -4,12 +4,16 @@
|
||||
#
|
||||
# Table name: submissions
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# deleted_at :datetime
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# created_by_user_id :bigint
|
||||
# template_id :bigint not null
|
||||
# id :bigint not null, primary key
|
||||
# deleted_at :datetime
|
||||
# source :text not null
|
||||
# template_fields :text
|
||||
# template_schema :text
|
||||
# template_submitters :text
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# created_by_user_id :bigint
|
||||
# template_id :bigint not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
@@ -23,9 +27,23 @@
|
||||
#
|
||||
class Submission < ApplicationRecord
|
||||
belongs_to :template
|
||||
has_one :account, through: :template
|
||||
belongs_to :created_by_user, class_name: 'User', optional: true
|
||||
|
||||
has_many :submitters, dependent: :destroy
|
||||
|
||||
serialize :template_fields, JSON
|
||||
serialize :template_schema, JSON
|
||||
serialize :template_submitters, JSON
|
||||
|
||||
attribute :source, :string, default: 'link'
|
||||
|
||||
scope :active, -> { where(deleted_at: nil) }
|
||||
|
||||
enum :source, {
|
||||
invite: 'invite',
|
||||
api: 'api',
|
||||
embed: 'embed',
|
||||
link: 'link'
|
||||
}, scope: false, prefix: true
|
||||
end
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
# slug :string not null
|
||||
# ua :string
|
||||
# uuid :string not null
|
||||
# values :string not null
|
||||
# values :text not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# submission_id :bigint not null
|
||||
@@ -30,9 +30,11 @@
|
||||
#
|
||||
class Submitter < ApplicationRecord
|
||||
belongs_to :submission
|
||||
has_one :template, through: :submission
|
||||
has_one :account, through: :template
|
||||
|
||||
attribute :values, :string, default: -> { {} }
|
||||
attribute :slug, :string, default: -> { SecureRandom.base58(10) }
|
||||
attribute :slug, :string, default: -> { SecureRandom.base58(14) }
|
||||
|
||||
serialize :values, JSON
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# deleted_at :datetime
|
||||
# fields :string not null
|
||||
# fields :text not null
|
||||
# name :string not null
|
||||
# schema :string not null
|
||||
# schema :text not null
|
||||
# slug :string not null
|
||||
# submitters :string not null
|
||||
# submitters :text not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint not null
|
||||
@@ -36,7 +36,7 @@ class Template < ApplicationRecord
|
||||
attribute :fields, :string, default: -> { [] }
|
||||
attribute :schema, :string, default: -> { [] }
|
||||
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 :schema, JSON
|
||||
@@ -44,6 +44,9 @@ class Template < ApplicationRecord
|
||||
|
||||
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
|
||||
|
||||
scope :active, -> { where(deleted_at: nil) }
|
||||
|
||||
+14
-3
@@ -22,6 +22,7 @@
|
||||
# role :string not null
|
||||
# sign_in_count :integer default(0), not null
|
||||
# unlock_token :string
|
||||
# uuid :text not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint not null
|
||||
@@ -32,6 +33,7 @@
|
||||
# index_users_on_email (email) UNIQUE
|
||||
# index_users_on_reset_password_token (reset_password_token) UNIQUE
|
||||
# index_users_on_unlock_token (unlock_token) UNIQUE
|
||||
# index_users_on_uuid (uuid) UNIQUE
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
@@ -40,22 +42,31 @@
|
||||
class User < ApplicationRecord
|
||||
ROLES = %w[admin].freeze
|
||||
|
||||
EMAIL_REGEXP =
|
||||
/[a-z0-9][.']?(?:(?:[a-z0-9_-]++[.'])*[a-z0-9_-]++)*@(?:[a-z0-9]++[.-])*[a-z0-9]++\.[a-z]{2,}/i
|
||||
EMAIL_REGEXP = /[^@,\s]+@[^@,\s]+/
|
||||
|
||||
belongs_to :account
|
||||
has_one :access_token, dependent: :destroy
|
||||
|
||||
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 :uuid, :string, default: -> { SecureRandom.uuid }
|
||||
|
||||
scope :active, -> { where(deleted_at: nil) }
|
||||
|
||||
def access_token
|
||||
super || build_access_token.tap(&:save!)
|
||||
end
|
||||
|
||||
def active_for_authentication?
|
||||
!deleted_at?
|
||||
end
|
||||
|
||||
def remember_me
|
||||
true
|
||||
end
|
||||
|
||||
def initials
|
||||
[first_name.first, last_name.first].join.upcase
|
||||
end
|
||||
|
||||
@@ -11,13 +11,13 @@
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<%= ff.label :timezone, class: 'label' %>
|
||||
<%= ff.select :timezone, nil, {}, class: 'select base-input w-full font-normal' do %>
|
||||
<%= ff.select :timezone, nil, {}, class: 'base-select' do %>
|
||||
<%= time_zone_options_for_select(current_account.timezone) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= ff.label :locale, 'Time format', class: 'label' %>
|
||||
<%= ff.select :locale, options_for_select(controller.class::LOCALE_OPTIONS.invert, current_account.locale), {}, class: 'select base-input w-full font-normal' %>
|
||||
<%= ff.select :locale, options_for_select(controller.class::LOCALE_OPTIONS.invert, current_account.locale), {}, class: 'base-select' %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -0,0 +1,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,3 +1,4 @@
|
||||
<% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %>
|
||||
<% if @templates.any? %>
|
||||
<div class="flex justify-between mb-4 items-center">
|
||||
<h1 class="text-4xl font-bold">Templates</h1>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="max-w-xl mx-auto px-2">
|
||||
<h1 class="text-4xl font-bold text-center my-8">Sign up</h1>
|
||||
<%= form_for('', as: resource_name, html: { class: 'space-y-6' }, url: registration_path) do |f| %>
|
||||
<h1 class="text-3xl font-bold text-center my-8">Profile Details</h1>
|
||||
<%= form_for('', as: resource_name, html: { class: 'space-y-6' }, url: new_registration_path) do |f| %>
|
||||
<div class="space-y-2">
|
||||
<%= render 'devise/shared/error_messages', resource: %>
|
||||
<%= f.fields_for resource do |ff| %>
|
||||
@@ -16,7 +16,7 @@
|
||||
</div>
|
||||
<% end %>
|
||||
<%= 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.email_field :email, required: true, class: 'base-input' %>
|
||||
</div>
|
||||
@@ -25,14 +25,14 @@
|
||||
<set-timezone data-input-id="_account_timezone"></set-timezone>
|
||||
<%= ff.hidden_field :timezone %>
|
||||
<div class="form-control">
|
||||
<%= ff.label :name, 'Company name', class: 'label' %>
|
||||
<%= ff.text_field :name, required: true, class: 'base-input' %>
|
||||
<%= ff.label :name, 'Company name (optional)', class: 'label' %>
|
||||
<%= ff.text_field :name, class: 'base-input' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= 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.password_field :password, required: true, class: 'base-input' %>
|
||||
<%= ff.password_field :password, required: !params[:oauth_callback], class: 'base-input' %>
|
||||
</div>
|
||||
<% end %>
|
||||
</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.password_field :password, autocomplete: 'current-password', class: 'base-input' %>
|
||||
</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 class="form-control">
|
||||
<%= f.button button_title(title: 'Log In', disabled_with: 'Logging In'), class: 'base-button' %>
|
||||
</div>
|
||||
<% 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' %>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<%= link_to 'Log in', new_session_path(resource_name), class: 'link link-hover' %>
|
||||
<% end %>
|
||||
<%- 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 %>
|
||||
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
|
||||
<%= 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' %>
|
||||
<%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name), class: 'link link-hover' %>
|
||||
<% 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>
|
||||
|
||||
@@ -25,6 +25,27 @@
|
||||
<%= ff.password_field :password, value: value['password'], required: true, class: 'base-input' %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<%= ff.label :domain, 'Domain (optional)', class: 'label' %>
|
||||
<%= ff.text_field :domain, value: value['domain'], class: 'base-input' %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= ff.label :authentication, class: 'label' %>
|
||||
<%= ff.select :authentication, options_for_select([%w[Plain plain], %w[Login login], %w[CRAM-MD5 cram_md5]], value.fetch('authentication', 'plain')), { prompt: true }, required: true, class: 'base-select' %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= ff.label :security_label, 'SMTP Security', class: 'label' %>
|
||||
<div class="flex items-center space-x-6">
|
||||
<% [%w[None none], %w[SSL ssl], %w[TLS tls]].each do |(label, val)| %>
|
||||
<%= ff.label :security, value: val, for: "#{val}_radio", class: 'label' do %>
|
||||
<%= ff.radio_button :security, val, checked: (value['security'].blank? && val == 'none') || value['security'] == val, id: "#{val}_radio", class: 'base-radio mr-2' %>
|
||||
<%= label %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= ff.label :from_email, 'Send from Email', class: 'label' %>
|
||||
<%= ff.email_field :from_email, value: value['from_email'], required: true, class: 'base-input' %>
|
||||
|
||||
@@ -0,0 +1,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 |
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html data-theme="docuseal">
|
||||
<html data-theme="docuseal" lang="en">
|
||||
<head>
|
||||
<title>
|
||||
DocuSeal | Open Source Document Filling and Signing
|
||||
@@ -15,7 +15,6 @@
|
||||
<body>
|
||||
<turbo-frame id="modal"></turbo-frame>
|
||||
<%= render 'shared/navbar' %>
|
||||
<% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %>
|
||||
<% if flash.present? %><%= render 'shared/flash' %><% end %>
|
||||
<div class="max-w-6xl mx-auto px-4 md:px-2 mb-8">
|
||||
<%= yield %>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html data-theme="docuseal">
|
||||
<html data-theme="docuseal" lang="en">
|
||||
<head>
|
||||
<title>
|
||||
DocuSeal | Open Source Document Filling and Signing
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
---
|
||||
</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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html data-theme="docuseal">
|
||||
<html data-theme="docuseal" lang="en">
|
||||
<head>
|
||||
<title>
|
||||
DocuSeal | Open Source Document Filling and Signing
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
DocuSeal
|
||||
</h1>
|
||||
</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.
|
||||
</h2>
|
||||
</div>
|
||||
@@ -22,7 +22,7 @@
|
||||
</div>
|
||||
<h3 class="mb-4 text-2xl font-semibold">Easy to Start</h3>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -70,3 +70,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<%= render 'shared/attribution' %>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<p>SMTP has been successfully configured!</p>
|
||||
@@ -8,7 +8,7 @@
|
||||
</span>
|
||||
<span class="disabled">
|
||||
<span class="flex items-center justify-center space-x-2">
|
||||
<%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
|
||||
<%= local_assigns[:icon_disabled] || svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
|
||||
<span><%= disabled_with %>...</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
<label class="<%= local_assigns[:class] %>">
|
||||
<input type="radio" class="peer hidden">
|
||||
<span class="peer-checked:hidden flex items-center space-x-2">
|
||||
<%= svg_icon('link', class: local_assigns[:icon_class] || 'w-6 h-6 text-white') %>
|
||||
<%= svg_icon(local_assigns[:icon] || 'link', class: local_assigns[:icon_class] || 'w-6 h-6 text-white') %>
|
||||
<span class="hidden md:inline">
|
||||
<%= local_assigns[:copy_title] || 'Copy' %>
|
||||
</span>
|
||||
</span>
|
||||
<span class="hidden peer-checked:flex items-center space-x-2">
|
||||
<%= svg_icon('clipboard_copy', class: local_assigns[:icon_class] || 'w-6 h-6 text-white') %>
|
||||
<%= svg_icon(local_assigns[:copied_icon] || 'clipboard_copy', class: local_assigns[:icon_class] || 'w-6 h-6 text-white') %>
|
||||
<span class="hidden md:inline">
|
||||
<%= local_assigns[:copied_title] || 'Copied' %>
|
||||
</span>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<div class="absolute top-0 w-full h-0">
|
||||
<div class="max-w-xl mx-auto mt-1.5">
|
||||
<div class="px-4 py-3 rounded-2xl flex items-center justify-between mx-4 md:mx-0">
|
||||
<a target="_blank" href="<%= start_form_url(slug: ::Template.first&.slug) %>" class="font-medium w-full text-center underline">
|
||||
Demo Environment
|
||||
</a>
|
||||
<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="w-full text-center">
|
||||
<span class="font-bold">Demo Environment</span>
|
||||
<br>
|
||||
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="<%= start_form_url(slug: ::Template.first&.slug) %>" target="_blank" class="inline underline font-medium">submit the existing one</a> 😊
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<a href="https://github.com/docusealco/docuseal" class="btn btn-neutral btn-sm btn-outline inline-flex items-center justify-center" target="_blank" alt="Star on GitHub" style="height: 37px">
|
||||
<span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="flex">
|
||||
<span class="hidden lg:block">Star on </span>GitHub
|
||||
</span>
|
||||
</a>
|
||||
@@ -4,13 +4,19 @@
|
||||
<%= render 'shared/logo' %>
|
||||
<span>DocuSeal</span>
|
||||
</a>
|
||||
<a href="<%= Docuseal::GITHUB_URL %>" target="_blank" class="inline">
|
||||
<img alt="GitHub Repo stars" src="https://www.docuseal.co/github-badge.svg" style="height: 22px">
|
||||
</a>
|
||||
<% unless Docuseal.demo? %>
|
||||
<a href="<%= Docuseal::GITHUB_URL %>" target="_blank" class="inline">
|
||||
<img alt="GitHub Repo stars" src="https://www.docuseal.co/github-badge.svg" style="height: 22px">
|
||||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if signed_in? %>
|
||||
<div class="space-x-6">
|
||||
<%= link_to 'Settings', settings_profile_index_path, class: 'hidden md:inline font-medium text-lg' %>
|
||||
<div class="space-x-4 flex items-center">
|
||||
<% if Docuseal.demo? %>
|
||||
<%= render 'shared/github_button' %>
|
||||
<% else %>
|
||||
<%= link_to 'Settings', settings_profile_index_path, class: 'hidden md:inline font-medium text-lg mr-2' %>
|
||||
<% end %>
|
||||
<div class="dropdown dropdown-end">
|
||||
<label tabindex="0" class="cursor-pointer bg-base-content text-purple-300 rounded-full p-2 w-9 justify-center flex">
|
||||
<span class="text-sm align-text-top"><%= current_user.initials %></span>
|
||||
@@ -23,8 +29,8 @@
|
||||
<% end %>
|
||||
</li>
|
||||
<li>
|
||||
<%= link_to destroy_user_session_path, data: { turbo_method: :delete } do %>
|
||||
<%= svg_icon('logout', class: 'w-5 h-5 stroke-2') %>
|
||||
<%= button_to destroy_user_session_path, method: :delete, data: { turbo: false } do %>
|
||||
<%= svg_icon('logout', class: 'w-5 h-5 stroke-2 inline') %>
|
||||
<span class="mr-1">Sign out</span>
|
||||
<% end %>
|
||||
</li>
|
||||
@@ -32,11 +38,21 @@
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= link_to new_user_session_path, class: 'font-medium text-lg' do %>
|
||||
<span class="flex items-center justify-center space-x-1">
|
||||
<%= svg_icon('login', class: 'w-6 h-6') %>
|
||||
<span>Sign in</span>
|
||||
</span>
|
||||
<% end %>
|
||||
<div class="flex space-x-6">
|
||||
<%= link_to new_user_session_path, class: 'font-medium text-lg' do %>
|
||||
<span class="flex items-center justify-center space-x-1">
|
||||
<%= svg_icon('login', class: 'w-6 h-6') %>
|
||||
<span>Sign in</span>
|
||||
</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 %>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,22 @@
|
||||
<li>
|
||||
<%= link_to 'Team', settings_users_path, class: 'text-base hover:bg-base-300' %>
|
||||
</li>
|
||||
<% if Docuseal.demo? || !Docuseal.multitenant? %>
|
||||
<li>
|
||||
<%= link_to 'API', settings_api_index_path, class: 'text-base hover:bg-base-300' %>
|
||||
</li>
|
||||
<li>
|
||||
<%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %>
|
||||
</li>
|
||||
<% end %>
|
||||
<% 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>
|
||||
</menu-active>
|
||||
<div class="mx-4 border-t border-base-300 hidden md:block">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<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="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] %>
|
||||
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||
<span>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
|
||||
</a>
|
||||
</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="mr-3">
|
||||
<%= 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">
|
||||
<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">
|
||||
<%= render 'shared/logo', width: '50px', height: '50px' %>
|
||||
</span>
|
||||
@@ -10,14 +10,14 @@
|
||||
</a>
|
||||
<p class="text-xl font-semibold text-center">You have been invited to submit a form</p>
|
||||
</div>
|
||||
<div class="flex items-center bg-base-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="mr-3">
|
||||
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</div>
|
||||
<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">
|
||||
<% @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 } %>
|
||||
<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">
|
||||
@@ -35,10 +35,10 @@
|
||||
</div>
|
||||
<div class="w-full overflow-y-auto overflow-x-hidden mt-0.5 pt-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) } %>
|
||||
<% 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.preview_images.sort_by { |a| a.filename.base.to_i }.each_with_index do |page, index| %>
|
||||
<div id="<%= "page-#{document.uuid}-#{index}" %>" class="relative">
|
||||
@@ -57,15 +57,15 @@
|
||||
</div>
|
||||
<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] %>
|
||||
<% submitter_fields_index = @submission.template.fields.group_by { |f| f['submitter_uuid'] } %>
|
||||
<% @submission.template.submitters.each_with_index do |item, index| %>
|
||||
<% submitter_fields_index = (@submission.template_fields || @submission.template.fields).group_by { |f| f['submitter_uuid'] } %>
|
||||
<% (@submission.template_submitters || @submission.template.submitters).each_with_index do |item, index| %>
|
||||
<% 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="border border-base-300 rounded-md px-2 py-1 mb-1">
|
||||
<div class="flex items-center space-x-1">
|
||||
<span class="mx-1 w-3 h-3 rounded-full <%= colors[index] %>"></span>
|
||||
<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>
|
||||
</div>
|
||||
<div class="flex items-center space-x-1 mt-1">
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
|
||||
</a>
|
||||
</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="mr-3">
|
||||
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
|
||||
@@ -23,7 +23,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<% 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>
|
||||
<% end %>
|
||||
<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) } %>
|
||||
<% attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %>
|
||||
<div style="max-height: -webkit-fill-available;">
|
||||
@@ -10,7 +10,7 @@
|
||||
<span>DocuSeal</span>
|
||||
</a>
|
||||
</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.preview_images.sort_by { |a| a.filename.base.to_i }.each_with_index do |page, index| %>
|
||||
<div class="relative my-4 shadow-md">
|
||||
@@ -32,9 +32,7 @@
|
||||
<div class="fixed bottom-0 w-full h-0 z-20">
|
||||
<div class="mx-auto" style="max-width: 1000px">
|
||||
<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-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>
|
||||
<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>
|
||||
</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">
|
||||
<% @submissions.each do |submission| %>
|
||||
<a href="<%= submission_path(submission) %>" class="bg-base-200 w-full flex flex-col md:flex-row space-y-4 md:space-y-0 md:justify-between rounded-2xl px-6 py-5 md:items-center">
|
||||
<% submitters = submission.template.submitters.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?) %>
|
||||
<% if submitters.size == 1 %>
|
||||
<div>
|
||||
@@ -157,13 +157,19 @@
|
||||
<% else %>
|
||||
<div class="card bg-base-200">
|
||||
<div class="card-body text-center py-16">
|
||||
<div class="max-w-md mx-auto">
|
||||
<div class="max-w-lg mx-auto">
|
||||
<p class="text-3xl font-bold text-base-content mb-4">There are no Submissions</p>
|
||||
<p class="text-gray-600">Send an invitation to fill and complete the form</p>
|
||||
<%= link_to new_template_submission_path(@template), class: 'base-button mt-6', data: { turbo_frame: 'modal' } do %>
|
||||
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
|
||||
<span class="mr-1">Add Recipients</span>
|
||||
<% end %>
|
||||
<div class="space-x-2">
|
||||
<%= link_to new_template_submission_path(@template), class: 'base-button mt-6', data: { turbo_frame: 'modal' } do %>
|
||||
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
|
||||
<span class="mr-1">Add Recipients</span>
|
||||
<% end %>
|
||||
<%= link_to start_form_url(slug: @template.slug), class: 'white-button mt-6', target: '_blank', rel: 'noopener' do %>
|
||||
<%= svg_icon('writing', class: 'w-6 h-6') %>
|
||||
<span class="mr-1">Submit it Yourself</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</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 'active_job/railtie'
|
||||
|
||||
require_relative '../lib/api_path_consider_json_middleware'
|
||||
|
||||
Bundler.require(*Rails.groups)
|
||||
|
||||
module DocuSeal
|
||||
@@ -26,5 +28,6 @@ module DocuSeal
|
||||
config.action_view.frozen_string_literal = true
|
||||
|
||||
config.middleware.insert_before ActionDispatch::Static, Rack::Deflater
|
||||
config.middleware.insert_before ActionDispatch::Static, ApiPathConsiderJsonMiddleware
|
||||
end
|
||||
end
|
||||
|
||||
@@ -45,6 +45,8 @@ Rails.application.configure do
|
||||
config.cache_store = :null_store
|
||||
end
|
||||
|
||||
config.active_job.queue_adapter = :sidekiq if defined?(Sidekiq)
|
||||
|
||||
# Store uploaded files on the local file system (see config/storage.yml for options).
|
||||
config.active_storage.service = :disk
|
||||
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.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"]
|
||||
# or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
|
||||
# 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.
|
||||
config.action_mailer.raise_delivery_errors = false
|
||||
|
||||
if ENV['SMTP_USERNAME']
|
||||
if ENV['SMTP_ADDRESS']
|
||||
config.action_mailer.delivery_method = :smtp
|
||||
config.action_mailer.smtp_settings = {
|
||||
address: ENV.fetch('SMTP_ADDRESS', nil),
|
||||
@@ -84,7 +86,7 @@ Rails.application.configure do
|
||||
password: ENV.fetch('SMTP_PASSWORD', nil),
|
||||
authentication: ENV.fetch('SMTP_AUTHENTICATION', 'plain'),
|
||||
enable_starttls_auto: ENV['SMTP_ENABLE_STARTTLS_AUTO'] == 'true'
|
||||
}
|
||||
}.compact
|
||||
end
|
||||
|
||||
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
|
||||
@@ -110,7 +112,7 @@ Rails.application.configure do
|
||||
config.active_record.encryption = {
|
||||
primary_key: encryption_secret.first(32),
|
||||
deterministic_key: encryption_secret.last(32),
|
||||
key_derivation_salt: encryption_secret
|
||||
key_derivation_salt: Digest::SHA256.hexdigest(encryption_secret)
|
||||
}
|
||||
|
||||
# Do not dump schema after migrations.
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../../lib/auth_with_token_strategy'
|
||||
|
||||
Warden::Strategies.add(:auth_token, AuthWithTokenStrategy)
|
||||
|
||||
# Assuming you have not yet modified this file, each configuration option below
|
||||
# is set to its default value. Note that some are commented out while others
|
||||
# are not: uncommented lines are intended to protect your configuration from
|
||||
@@ -120,7 +124,7 @@ Devise.setup do |config|
|
||||
# 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.
|
||||
# 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.
|
||||
# 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
|
||||
# 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.
|
||||
config.expire_all_remember_me_on_sign_out = true
|
||||
@@ -266,16 +270,16 @@ Devise.setup do |config|
|
||||
# ==> OmniAuth
|
||||
# Add a new OmniAuth provider. Check the wiki for more information on setting
|
||||
# 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
|
||||
# If you want to use other strategies, that are not supported by Devise, or
|
||||
# change the failure app, you can configure them inside the config.warden block.
|
||||
#
|
||||
# config.warden do |manager|
|
||||
# manager.intercept_401 = false
|
||||
# manager.default_strategies(scope: :user).unshift :some_external_strategy
|
||||
# end
|
||||
config.warden do |manager|
|
||||
# manager.intercept_401 = false
|
||||
manager.default_strategies(scope: :user).unshift(:auth_token)
|
||||
end
|
||||
|
||||
# ==> Mountable engine configurations
|
||||
# When using Devise inside an engine, let's call it `MyEngine`, and this engine
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# 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
|
||||
mount LetterOpenerWeb::Engine, at: '/letter_opener' if Rails.env.development?
|
||||
mount Sidekiq::Web => '/sidekiq' if defined?(Sidekiq)
|
||||
|
||||
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
|
||||
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
|
||||
end
|
||||
end
|
||||
@@ -19,9 +27,11 @@ Rails.application.routes.draw do
|
||||
end
|
||||
end
|
||||
|
||||
namespace :api do
|
||||
namespace :api, defaults: { format: :json } do
|
||||
resources :attachments, only: %i[create]
|
||||
resources :templates, only: %i[update] do
|
||||
resources :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'
|
||||
end
|
||||
end
|
||||
@@ -31,6 +41,7 @@ Rails.application.routes.draw do
|
||||
resource :newsletter, only: %i[show update]
|
||||
resources :users, only: %i[new create edit update destroy]
|
||||
resources :submissions, only: %i[show destroy]
|
||||
resources :console_redirect, only: %i[index]
|
||||
resources :templates, only: %i[new create edit show destroy] do
|
||||
resources :submissions, only: %i[new create]
|
||||
end
|
||||
@@ -59,6 +70,10 @@ Rails.application.routes.draw do
|
||||
end
|
||||
resources :esign, only: %i[index create], controller: 'esign_settings'
|
||||
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]
|
||||
resources :profile, only: %i[index] 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
|
||||
+22
-5
@@ -10,10 +10,20 @@
|
||||
#
|
||||
# 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
|
||||
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|
|
||||
t.string "name", 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 "created_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 ["template_id"], name: "index_submissions_on_template_id"
|
||||
end
|
||||
@@ -86,7 +100,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_07_01_075115) do
|
||||
t.string "uuid", null: false
|
||||
t.string "email", null: false
|
||||
t.string "slug", null: false
|
||||
t.string "values", null: false
|
||||
t.text "values", null: false
|
||||
t.string "ua"
|
||||
t.string "ip"
|
||||
t.datetime "sent_at"
|
||||
@@ -102,9 +116,9 @@ ActiveRecord::Schema[7.0].define(version: 2023_07_01_075115) do
|
||||
create_table "templates", force: :cascade do |t|
|
||||
t.string "slug", null: false
|
||||
t.string "name", null: false
|
||||
t.string "schema", null: false
|
||||
t.string "fields", null: false
|
||||
t.string "submitters", null: false
|
||||
t.text "schema", null: false
|
||||
t.text "fields", null: false
|
||||
t.text "submitters", null: false
|
||||
t.bigint "author_id", null: false
|
||||
t.bigint "account_id", null: false
|
||||
t.datetime "deleted_at"
|
||||
@@ -136,12 +150,15 @@ ActiveRecord::Schema[7.0].define(version: 2023_07_01_075115) do
|
||||
t.datetime "deleted_at"
|
||||
t.datetime "created_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 ["email"], name: "index_users_on_email", 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 ["uuid"], name: "index_users_on_uuid", unique: true
|
||||
end
|
||||
|
||||
add_foreign_key "access_tokens", "users"
|
||||
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 "document_generation_events", "submitters"
|
||||
|
||||
@@ -3,6 +3,45 @@
|
||||
module Accounts
|
||||
module_function
|
||||
|
||||
def create_duplicate(account)
|
||||
new_account = account.dup
|
||||
|
||||
new_user = account.users.first.dup
|
||||
|
||||
new_user.account = new_account
|
||||
new_user.encrypted_password = SecureRandom.hex
|
||||
new_user.email = "#{SecureRandom.hex}@docuseal.co"
|
||||
|
||||
account.templates.each do |template|
|
||||
new_template = template.dup
|
||||
|
||||
new_template.account = new_account
|
||||
new_template.slug = SecureRandom.base58(14)
|
||||
|
||||
new_template.save!
|
||||
|
||||
Templates::CloneAttachments.call(template: new_template, original_template: template)
|
||||
end
|
||||
|
||||
new_user.save!(validate: false)
|
||||
|
||||
new_account
|
||||
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)
|
||||
certs =
|
||||
if Docuseal.multitenant?
|
||||
|
||||
@@ -4,6 +4,12 @@ module ActionMailerConfigsInterceptor
|
||||
module_function
|
||||
|
||||
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
|
||||
message.from = ENV.fetch('SMTP_FROM')
|
||||
|
||||
@@ -13,17 +19,29 @@ module ActionMailerConfigsInterceptor
|
||||
email_configs = EncryptedConfig.find_by(key: EncryptedConfig::EMAIL_SMTP_KEY)
|
||||
|
||||
if email_configs
|
||||
message.delivery_method(:smtp, user_name: email_configs.value['username'],
|
||||
password: email_configs.value['password'],
|
||||
address: email_configs.value['host'],
|
||||
port: email_configs.value['port'],
|
||||
tls: email_configs.value['port'].to_s == '465')
|
||||
message.delivery_method(:smtp, build_smtp_configs_hash(email_configs))
|
||||
|
||||
message.from = email_configs.value['from_email']
|
||||
message.from = "#{email_configs.account.name} <#{email_configs.value['from_email']}>"
|
||||
else
|
||||
message.delivery_method(:test)
|
||||
end
|
||||
|
||||
message
|
||||
end
|
||||
|
||||
def build_smtp_configs_hash(email_configs)
|
||||
value = email_configs.value
|
||||
|
||||
{
|
||||
user_name: value['username'],
|
||||
password: value['password'],
|
||||
address: value['host'],
|
||||
port: value['port'],
|
||||
domain: value['domain'],
|
||||
authentication: value.fetch('authentication', 'plain'),
|
||||
enable_starttls_auto: true,
|
||||
ssl: value['security'] == 'ssl',
|
||||
tls: value['security'] == 'tls' || (value['security'].blank? && value['port'].to_s == '465')
|
||||
}.compact_blank
|
||||
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
|
||||
+6
-1
@@ -6,10 +6,15 @@ module Docuseal
|
||||
PRODUCT_NAME = 'DocuSeal'
|
||||
DEFAULT_APP_URL = 'http://localhost:3000'
|
||||
GITHUB_URL = 'https://github.com/docusealco/docuseal'
|
||||
DISCORD_URL = 'https://discord.com/invite/B5wg5wKk'
|
||||
DISCORD_URL = 'https://discord.gg/qygYCDGck9'
|
||||
TWITTER_URL = 'https://twitter.com/docusealco'
|
||||
TWITTER_HANDLE = '@docusealco'
|
||||
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', '{}'))
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user