mirror of
https://github.com/docusealco/docuseal
synced 2026-11-05 14:42:33 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d757f06ff4 | ||
|
|
132999176c | ||
|
|
2f74caf16a | ||
|
|
41d3eaf582 | ||
|
|
907aaa9c30 | ||
|
|
7bb241815f | ||
|
|
84e879bad0 | ||
|
|
a1a0205d33 | ||
|
|
6f8e88cdbd | ||
|
|
2f1843151d | ||
|
|
f89c50d096 | ||
|
|
ef618f8f49 | ||
|
|
b3662ddea8 | ||
|
|
53fa35fd01 | ||
|
|
7f6c0af0e8 | ||
|
|
3faec9d174 | ||
|
|
9d9e134bb4 | ||
|
|
ec4f399fcc | ||
|
|
effa7b0fc7 | ||
|
|
016d9de87a | ||
|
|
71ff064356 | ||
|
|
9100d48cc3 | ||
|
|
21b207cb1a | ||
|
|
3aa703032f | ||
|
|
fcfd2e13bd | ||
|
|
cf6cacdc92 | ||
|
|
8cabe31827 | ||
|
|
62cea05084 | ||
|
|
07e49f9f07 | ||
|
|
baad784818 | ||
|
|
6eecd3afa4 | ||
|
|
8e90100871 | ||
|
|
12c5138364 | ||
|
|
80ad166fd9 | ||
|
|
2fab6984ea | ||
|
|
422639114b | ||
|
|
fca441878a | ||
|
|
76578077e8 | ||
|
|
bbd3618f0e | ||
|
|
47ed8db955 | ||
|
|
fec6e2e35d | ||
|
|
7c5c8779ec | ||
|
|
37ed110ad4 | ||
|
|
2ad5f8a316 | ||
|
|
e7594d81b6 | ||
|
|
e7c5f14860 | ||
|
|
c38830dd1c | ||
|
|
2cbf429293 | ||
|
|
517c292c17 | ||
|
|
14bd039a88 | ||
|
|
8c8c34cb75 | ||
|
|
0cc53066eb | ||
|
|
3d13d28e05 | ||
|
|
a80e20234f | ||
|
|
b1b46cf998 | ||
|
|
52ef0ae929 | ||
|
|
c4daf8ed49 | ||
|
|
c098e61947 | ||
|
|
ceb052b158 | ||
|
|
61b79d5b24 | ||
|
|
5a9cffc6fc | ||
|
|
08447ec4cf | ||
|
|
d3a5f36555 |
No files matched your search
@@ -26,6 +26,9 @@ Style/Documentation:
|
|||||||
Lint/MissingSuper:
|
Lint/MissingSuper:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Metrics/ParameterLists:
|
||||||
|
Max: 10
|
||||||
|
|
||||||
Metrics/MethodLength:
|
Metrics/MethodLength:
|
||||||
Max: 25
|
Max: 25
|
||||||
Exclude:
|
Exclude:
|
||||||
|
|||||||
+1
-1
@@ -31,7 +31,7 @@ ENV BUNDLE_WITHOUT="development:test"
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler vips-heif libc6-compat ttf-freefont ttf-liberation && cp /usr/share/fonts/liberation/LiberationSans-Regular.ttf /LiberationSans-Regular.ttf && apk del ttf-liberation
|
RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler vips-heif libc6-compat ttf-freefont ttf-liberation && cp /usr/share/fonts/liberation/LiberationSans-Regular.ttf /usr/share/fonts/liberation/LiberationSans-Bold.ttf / && apk del ttf-liberation
|
||||||
|
|
||||||
COPY ./Gemfile ./Gemfile.lock ./
|
COPY ./Gemfile ./Gemfile.lock ./
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,11 @@ ruby '3.2.2'
|
|||||||
gem 'aws-sdk-s3', require: false
|
gem 'aws-sdk-s3', require: false
|
||||||
gem 'azure-storage-blob', require: false
|
gem 'azure-storage-blob', require: false
|
||||||
gem 'bootsnap', require: false
|
gem 'bootsnap', require: false
|
||||||
|
gem 'cancancan'
|
||||||
gem 'devise'
|
gem 'devise'
|
||||||
|
gem 'devise-two-factor'
|
||||||
gem 'dotenv', require: false
|
gem 'dotenv', require: false
|
||||||
|
gem 'email_typo'
|
||||||
gem 'faraday'
|
gem 'faraday'
|
||||||
gem 'google-cloud-storage', require: false
|
gem 'google-cloud-storage', require: false
|
||||||
gem 'hexapdf'
|
gem 'hexapdf'
|
||||||
@@ -20,6 +23,7 @@ gem 'oj'
|
|||||||
gem 'omniauth-google-oauth2'
|
gem 'omniauth-google-oauth2'
|
||||||
gem 'omniauth-rails_csrf_protection'
|
gem 'omniauth-rails_csrf_protection'
|
||||||
gem 'pagy'
|
gem 'pagy'
|
||||||
|
gem 'pdf-reader'
|
||||||
gem 'pg', require: false
|
gem 'pg', require: false
|
||||||
gem 'premailer-rails'
|
gem 'premailer-rails'
|
||||||
gem 'puma'
|
gem 'puma'
|
||||||
@@ -27,6 +31,8 @@ gem 'rails'
|
|||||||
gem 'rails_autolink'
|
gem 'rails_autolink'
|
||||||
gem 'rails-i18n'
|
gem 'rails-i18n'
|
||||||
gem 'rollbar', require: ENV.key?('ROLLBAR_ACCESS_TOKEN')
|
gem 'rollbar', require: ENV.key?('ROLLBAR_ACCESS_TOKEN')
|
||||||
|
gem 'rotp'
|
||||||
|
gem 'rqrcode'
|
||||||
gem 'ruby-vips'
|
gem 'ruby-vips'
|
||||||
gem 'rubyXL'
|
gem 'rubyXL'
|
||||||
gem 'shakapacker'
|
gem 'shakapacker'
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
|
Ascii85 (1.1.0)
|
||||||
actioncable (7.0.7)
|
actioncable (7.0.7)
|
||||||
actionpack (= 7.0.7)
|
actionpack (= 7.0.7)
|
||||||
activesupport (= 7.0.7)
|
activesupport (= 7.0.7)
|
||||||
@@ -68,6 +69,7 @@ GEM
|
|||||||
tzinfo (~> 2.0)
|
tzinfo (~> 2.0)
|
||||||
addressable (2.8.5)
|
addressable (2.8.5)
|
||||||
public_suffix (>= 2.0.2, < 6.0)
|
public_suffix (>= 2.0.2, < 6.0)
|
||||||
|
afm (0.2.2)
|
||||||
annotate (3.2.0)
|
annotate (3.2.0)
|
||||||
activerecord (>= 3.2, < 8.0)
|
activerecord (>= 3.2, < 8.0)
|
||||||
rake (>= 10.4, < 14.0)
|
rake (>= 10.4, < 14.0)
|
||||||
@@ -112,6 +114,7 @@ GEM
|
|||||||
bullet (7.0.7)
|
bullet (7.0.7)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
uniform_notifier (~> 1.11)
|
uniform_notifier (~> 1.11)
|
||||||
|
cancancan (3.5.0)
|
||||||
capybara (3.39.2)
|
capybara (3.39.2)
|
||||||
addressable
|
addressable
|
||||||
matrix
|
matrix
|
||||||
@@ -121,6 +124,7 @@ GEM
|
|||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
regexp_parser (>= 1.5, < 3.0)
|
regexp_parser (>= 1.5, < 3.0)
|
||||||
xpath (~> 3.2)
|
xpath (~> 3.2)
|
||||||
|
chunky_png (1.4.0)
|
||||||
cmdparse (3.0.7)
|
cmdparse (3.0.7)
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
concurrent-ruby (1.2.2)
|
concurrent-ruby (1.2.2)
|
||||||
@@ -144,11 +148,17 @@ GEM
|
|||||||
railties (>= 4.1.0)
|
railties (>= 4.1.0)
|
||||||
responders
|
responders
|
||||||
warden (~> 1.2.3)
|
warden (~> 1.2.3)
|
||||||
|
devise-two-factor (5.0.0)
|
||||||
|
activesupport (~> 7.0)
|
||||||
|
devise (~> 4.0)
|
||||||
|
railties (~> 7.0)
|
||||||
|
rotp (~> 6.0)
|
||||||
diff-lcs (1.5.0)
|
diff-lcs (1.5.0)
|
||||||
digest-crc (0.6.5)
|
digest-crc (0.6.5)
|
||||||
rake (>= 12.0.0, < 14.0.0)
|
rake (>= 12.0.0, < 14.0.0)
|
||||||
docile (1.4.0)
|
docile (1.4.0)
|
||||||
dotenv (2.8.1)
|
dotenv (2.8.1)
|
||||||
|
email_typo (0.2.3)
|
||||||
erb_lint (0.4.0)
|
erb_lint (0.4.0)
|
||||||
activesupport
|
activesupport
|
||||||
better_html (>= 2.0.1)
|
better_html (>= 2.0.1)
|
||||||
@@ -233,6 +243,7 @@ GEM
|
|||||||
os (>= 0.9, < 2.0)
|
os (>= 0.9, < 2.0)
|
||||||
signet (>= 0.16, < 2.a)
|
signet (>= 0.16, < 2.a)
|
||||||
hashdiff (1.0.1)
|
hashdiff (1.0.1)
|
||||||
|
hashery (2.1.2)
|
||||||
hashie (5.0.0)
|
hashie (5.0.0)
|
||||||
hexapdf (0.33.0)
|
hexapdf (0.33.0)
|
||||||
cmdparse (~> 3.0, >= 3.0.3)
|
cmdparse (~> 3.0, >= 3.0.3)
|
||||||
@@ -335,6 +346,12 @@ GEM
|
|||||||
parser (3.2.2.3)
|
parser (3.2.2.3)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
|
pdf-reader (2.11.0)
|
||||||
|
Ascii85 (~> 1.0)
|
||||||
|
afm (~> 0.2.1)
|
||||||
|
hashery (~> 2.0)
|
||||||
|
ruby-rc4
|
||||||
|
ttfunk
|
||||||
pg (1.5.3)
|
pg (1.5.3)
|
||||||
premailer (1.21.0)
|
premailer (1.21.0)
|
||||||
addressable
|
addressable
|
||||||
@@ -414,6 +431,11 @@ GEM
|
|||||||
retriable (3.1.2)
|
retriable (3.1.2)
|
||||||
rexml (3.2.6)
|
rexml (3.2.6)
|
||||||
rollbar (3.4.0)
|
rollbar (3.4.0)
|
||||||
|
rotp (6.3.0)
|
||||||
|
rqrcode (2.2.0)
|
||||||
|
chunky_png (~> 1.0)
|
||||||
|
rqrcode_core (~> 1.0)
|
||||||
|
rqrcode_core (1.2.0)
|
||||||
rspec-core (3.12.2)
|
rspec-core (3.12.2)
|
||||||
rspec-support (~> 3.12.0)
|
rspec-support (~> 3.12.0)
|
||||||
rspec-expectations (3.12.3)
|
rspec-expectations (3.12.3)
|
||||||
@@ -461,6 +483,7 @@ GEM
|
|||||||
rubocop-capybara (~> 2.17)
|
rubocop-capybara (~> 2.17)
|
||||||
rubocop-factory_bot (~> 2.22)
|
rubocop-factory_bot (~> 2.22)
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
|
ruby-rc4 (0.1.5)
|
||||||
ruby-vips (2.1.4)
|
ruby-vips (2.1.4)
|
||||||
ffi (~> 1.12)
|
ffi (~> 1.12)
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
@@ -502,6 +525,7 @@ GEM
|
|||||||
thor (1.2.2)
|
thor (1.2.2)
|
||||||
timeout (0.4.0)
|
timeout (0.4.0)
|
||||||
trailblazer-option (0.1.2)
|
trailblazer-option (0.1.2)
|
||||||
|
ttfunk (1.7.0)
|
||||||
turbo-rails (1.4.0)
|
turbo-rails (1.4.0)
|
||||||
actionpack (>= 6.0.0)
|
actionpack (>= 6.0.0)
|
||||||
activejob (>= 6.0.0)
|
activejob (>= 6.0.0)
|
||||||
@@ -544,11 +568,14 @@ DEPENDENCIES
|
|||||||
better_html
|
better_html
|
||||||
bootsnap
|
bootsnap
|
||||||
bullet
|
bullet
|
||||||
|
cancancan
|
||||||
capybara
|
capybara
|
||||||
cuprite
|
cuprite
|
||||||
debug
|
debug
|
||||||
devise
|
devise
|
||||||
|
devise-two-factor
|
||||||
dotenv
|
dotenv
|
||||||
|
email_typo
|
||||||
erb_lint
|
erb_lint
|
||||||
factory_bot_rails
|
factory_bot_rails
|
||||||
faker
|
faker
|
||||||
@@ -564,6 +591,7 @@ DEPENDENCIES
|
|||||||
omniauth-google-oauth2
|
omniauth-google-oauth2
|
||||||
omniauth-rails_csrf_protection
|
omniauth-rails_csrf_protection
|
||||||
pagy
|
pagy
|
||||||
|
pdf-reader
|
||||||
pg
|
pg
|
||||||
premailer-rails
|
premailer-rails
|
||||||
pry-rails
|
pry-rails
|
||||||
@@ -572,6 +600,8 @@ DEPENDENCIES
|
|||||||
rails-i18n
|
rails-i18n
|
||||||
rails_autolink
|
rails_autolink
|
||||||
rollbar
|
rollbar
|
||||||
|
rotp
|
||||||
|
rqrcode
|
||||||
rspec-rails
|
rspec-rails
|
||||||
rubocop
|
rubocop
|
||||||
rubocop-performance
|
rubocop-performance
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
| [<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 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**|
|
|**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)
|
| [<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)
|
||||||
|
|**Koyeb**| |
|
||||||
|
| [<img alt="Deploy on Koyeb" src="https://www.koyeb.com/static/images/deploy/button.svg" height="40">](https://app.koyeb.com/deploy?name=docuseal&type=docker&image=docker.io/docuseal/docuseal&env[PORT]=8000&env[DATABASE_URL]=CHANGE_ME&env[SECRET_KEY_BASE]=CHANGE_ME&ports=8000;http;/) | |
|
||||||
|
|
||||||
|
|
||||||
#### Docker
|
#### Docker
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ class AccountsController < ApplicationController
|
|||||||
'de-DE' => 'German (Germany)'
|
'de-DE' => 'German (Germany)'
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
|
before_action :load_account
|
||||||
|
authorize_resource :account
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@@ -35,6 +38,10 @@ class AccountsController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def load_account
|
||||||
|
@account = current_account
|
||||||
|
end
|
||||||
|
|
||||||
def account_params
|
def account_params
|
||||||
params.require(:account).permit(:name, :timezone, :locale)
|
params.require(:account).permit(:name, :timezone, :locale)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,6 +5,15 @@ module Api
|
|||||||
include ActiveStorage::SetCurrent
|
include ActiveStorage::SetCurrent
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
|
check_authorization
|
||||||
|
|
||||||
|
if Rails.env.production?
|
||||||
|
rescue_from CanCan::AccessDenied do |e|
|
||||||
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
|
render json: { error: e.message }, status: :forbidden
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
module Api
|
module Api
|
||||||
class AttachmentsController < ApiBaseController
|
class AttachmentsController < ApiBaseController
|
||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
def create
|
def create
|
||||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|||||||
@@ -5,45 +5,44 @@ module Api
|
|||||||
UnknownFieldName = Class.new(StandardError)
|
UnknownFieldName = Class.new(StandardError)
|
||||||
UnknownSubmitterName = Class.new(StandardError)
|
UnknownSubmitterName = Class.new(StandardError)
|
||||||
|
|
||||||
def create
|
load_and_authorize_resource :template
|
||||||
template = current_account.templates.find(params[:template_id])
|
|
||||||
|
|
||||||
|
before_action do
|
||||||
|
authorize!(:create, Submission)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
submissions =
|
submissions =
|
||||||
if params[:emails].present?
|
if (emails = (params[:emails] || params[:email]).presence)
|
||||||
Submissions.create_from_emails(template:,
|
Submissions.create_from_emails(template: @template,
|
||||||
user: current_user,
|
user: current_user,
|
||||||
source: :api,
|
source: :api,
|
||||||
send_email: params[:send_email] != 'false',
|
mark_as_sent: params[:send_email] != 'false',
|
||||||
emails: params[:emails] || params[:email])
|
emails:)
|
||||||
else
|
else
|
||||||
submissions_attrs = normalize_submissions_params!(submissions_params[:submission], template)
|
submissions_attrs = normalize_submissions_params!(submissions_params[:submission], @template)
|
||||||
|
|
||||||
Submissions.create_from_submitters(template:,
|
Submissions.create_from_submitters(
|
||||||
user: current_user,
|
template: @template,
|
||||||
source: :api,
|
user: current_user,
|
||||||
send_email: params[:send_email] != 'false',
|
source: :api,
|
||||||
submissions_attrs:)
|
mark_as_sent: params[:send_email] != 'false',
|
||||||
|
submitters_order: params[:submitters_order] || 'preserved',
|
||||||
|
submissions_attrs:
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
submitters = submissions.flat_map(&:submitters)
|
Submissions.send_signature_requests(submissions, send_email: params[:send_email] != 'false')
|
||||||
|
|
||||||
send_invitation_emails(submitters) if params[:send_email] != 'false'
|
render json: submissions.flat_map(&:submitters)
|
||||||
|
|
||||||
render json: submitters
|
|
||||||
rescue UnknownFieldName, UnknownSubmitterName => e
|
rescue UnknownFieldName, UnknownSubmitterName => e
|
||||||
render json: { error: e.message }, status: :unprocessable_entity
|
render json: { error: e.message }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def send_invitation_emails(submitters)
|
|
||||||
submitters.each do |submitter|
|
|
||||||
SubmitterMailer.invitation_email(submitter, message: params[:message]).deliver_later!
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def submissions_params
|
def submissions_params
|
||||||
params.permit(submission: [{ submitters: [[:uuid, :name, :email, { values: {} }]] }])
|
params.permit(submission: [{ submitters: [[:uuid, :name, :email, :role, :phone, { values: {} }]] }])
|
||||||
end
|
end
|
||||||
|
|
||||||
def normalize_submissions_params!(submissions_params, template)
|
def normalize_submissions_params!(submissions_params, template)
|
||||||
@@ -53,7 +52,8 @@ module Api
|
|||||||
|
|
||||||
submitter[:values] =
|
submitter[:values] =
|
||||||
normalize_submitter_values(template,
|
normalize_submitter_values(template,
|
||||||
submitter[:values], submitter[:name] || template.submitters[index]['name'])
|
submitter[:values],
|
||||||
|
submitter[:role] || template.submitters[index]['name'])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api
|
||||||
|
class SubmitterEmailClicksController < ApiBaseController
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
def create
|
||||||
|
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
|
if params[:t] == SubmissionEvents.build_tracking_param(submitter, 'click_email')
|
||||||
|
SubmissionEvents.create_with_tracking_data(submitter, 'click_email', request)
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: {}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api
|
||||||
|
class SubmitterFormViewsController < ApiBaseController
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
def create
|
||||||
|
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
|
SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request)
|
||||||
|
|
||||||
|
SendFormViewedWebhookRequestJob.perform_later(submitter)
|
||||||
|
|
||||||
|
render json: {}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
module Api
|
module Api
|
||||||
class TemplatesController < ApiBaseController
|
class TemplatesController < ApiBaseController
|
||||||
before_action :load_template, only: %i[show update]
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
def index
|
def index
|
||||||
render json: current_account.templates
|
render json: @templates
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@@ -28,9 +28,5 @@ module Api
|
|||||||
fields: [[:uuid, :submitter_uuid, :name, :type, :required,
|
fields: [[:uuid, :submitter_uuid, :name, :type, :required,
|
||||||
{ options: [], areas: [%i[x y w h cell_w attachment_uuid page]] }]])
|
{ options: [], areas: [%i[x y w h cell_w attachment_uuid page]] }]])
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_template
|
|
||||||
@template = current_account.templates.find(params[:id])
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -2,17 +2,12 @@
|
|||||||
|
|
||||||
module Api
|
module Api
|
||||||
class TemplatesDocumentsController < ApiBaseController
|
class TemplatesDocumentsController < ApiBaseController
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
def create
|
def create
|
||||||
return head :unprocessable_entity if params[:blobs].blank? && params[:files].blank?
|
return head :unprocessable_entity if params[:blobs].blank? && params[:files].blank?
|
||||||
|
|
||||||
@template = current_account.templates.find(params[:template_id])
|
documents = Templates::CreateAttachments.call(@template, params)
|
||||||
|
|
||||||
documents =
|
|
||||||
find_or_create_blobs.map do |blob|
|
|
||||||
document = @template.documents.create!(blob:)
|
|
||||||
|
|
||||||
Templates::ProcessDocument.call(document)
|
|
||||||
end
|
|
||||||
|
|
||||||
schema = documents.map do |doc|
|
schema = documents.map do |doc|
|
||||||
{ attachment_uuid: doc.uuid, name: doc.filename.base }
|
{ attachment_uuid: doc.uuid, name: doc.filename.base }
|
||||||
@@ -27,19 +22,5 @@ module Api
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def find_or_create_blobs
|
|
||||||
blobs = params[:blobs]&.map do |attrs|
|
|
||||||
ActiveStorage::Blob.find_signed(attrs[:signed_id])
|
|
||||||
end
|
|
||||||
|
|
||||||
blobs || params[:files].map do |file|
|
|
||||||
ActiveStorage::Blob.create_and_upload!(io: file.open,
|
|
||||||
filename: file.original_filename,
|
|
||||||
content_type: file.content_type)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ApiSettingsController < ApplicationController
|
class ApiSettingsController < ApplicationController
|
||||||
def index; end
|
def index
|
||||||
|
authorize!(:read, current_user.access_token)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -4,6 +4,8 @@ class ApplicationController < ActionController::Base
|
|||||||
include ActiveStorage::SetCurrent
|
include ActiveStorage::SetCurrent
|
||||||
include Pagy::Backend
|
include Pagy::Backend
|
||||||
|
|
||||||
|
check_authorization unless: :devise_controller?
|
||||||
|
|
||||||
before_action :sign_in_for_demo, if: -> { Docuseal.demo? }
|
before_action :sign_in_for_demo, if: -> { Docuseal.demo? }
|
||||||
before_action :maybe_redirect_to_setup, unless: :signed_in?
|
before_action :maybe_redirect_to_setup, unless: :signed_in?
|
||||||
before_action :authenticate_user!, unless: :devise_controller?
|
before_action :authenticate_user!, unless: :devise_controller?
|
||||||
@@ -16,6 +18,14 @@ class ApplicationController < ActionController::Base
|
|||||||
redirect_to request.path
|
redirect_to request.path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if Rails.env.production?
|
||||||
|
rescue_from CanCan::AccessDenied do |e|
|
||||||
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
|
redirect_back(fallback_location: root_path, alert: e.message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def default_url_options
|
def default_url_options
|
||||||
Docuseal.default_url_options
|
Docuseal.default_url_options
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
class ConsoleRedirectController < ApplicationController
|
class ConsoleRedirectController < ApplicationController
|
||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
def index
|
def index
|
||||||
return redirect_to(new_user_session_path({ redir: params[:redir] }.compact)) if current_user.blank?
|
return redirect_to(new_user_session_path({ redir: params[:redir] }.compact)) if current_user.blank?
|
||||||
|
|||||||
@@ -3,12 +3,29 @@
|
|||||||
class DashboardController < ApplicationController
|
class DashboardController < ApplicationController
|
||||||
skip_before_action :authenticate_user!, only: %i[index]
|
skip_before_action :authenticate_user!, only: %i[index]
|
||||||
|
|
||||||
|
before_action :maybe_redirect_product_url
|
||||||
|
before_action :maybe_render_landing
|
||||||
|
|
||||||
|
load_and_authorize_resource :template, parent: false
|
||||||
|
|
||||||
def index
|
def index
|
||||||
return redirect_to Docuseal::PRODUCT_URL, allow_other_host: true if Docuseal.multitenant? && !signed_in?
|
@templates = @templates.active.preload(:author).order(id: :desc)
|
||||||
return render 'pages/landing' unless signed_in?
|
@templates = Templates.search(@templates, params[:q])
|
||||||
|
|
||||||
templates = current_account.templates.active.preload(:author).order(id: :desc)
|
@pagy, @templates = pagy(@templates, items: 12)
|
||||||
|
end
|
||||||
|
|
||||||
@pagy, @templates = pagy(templates, items: 12)
|
private
|
||||||
|
|
||||||
|
def maybe_redirect_product_url
|
||||||
|
return if !Docuseal.multitenant? || signed_in?
|
||||||
|
|
||||||
|
redirect_to Docuseal::PRODUCT_URL, allow_other_host: true
|
||||||
|
end
|
||||||
|
|
||||||
|
def maybe_render_landing
|
||||||
|
return if signed_in?
|
||||||
|
|
||||||
|
render 'pages/landing'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
+5
-3
@@ -1,12 +1,14 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class EmailSettingsController < ApplicationController
|
class EmailSmtpSettingsController < ApplicationController
|
||||||
before_action :load_encrypted_config
|
before_action :load_encrypted_config
|
||||||
|
authorize_resource :encrypted_config, only: :index
|
||||||
|
authorize_resource :encrypted_config, parent: false, only: :create
|
||||||
|
|
||||||
def index; end
|
def index; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
if @encrypted_config.update(storage_configs)
|
if @encrypted_config.update(email_configs)
|
||||||
SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email']).deliver_now!
|
SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email']).deliver_now!
|
||||||
|
|
||||||
redirect_to settings_email_index_path, notice: 'Changes have been saved'
|
redirect_to settings_email_index_path, notice: 'Changes have been saved'
|
||||||
@@ -26,7 +28,7 @@ class EmailSettingsController < ApplicationController
|
|||||||
EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::EMAIL_SMTP_KEY)
|
EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::EMAIL_SMTP_KEY)
|
||||||
end
|
end
|
||||||
|
|
||||||
def storage_configs
|
def email_configs
|
||||||
params.require(:encrypted_config).permit(value: {}).tap do |e|
|
params.require(:encrypted_config).permit(value: {}).tap do |e|
|
||||||
e[:value].compact_blank!
|
e[:value].compact_blank!
|
||||||
end
|
end
|
||||||
@@ -1,19 +1,113 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class EsignSettingsController < ApplicationController
|
class EsignSettingsController < ApplicationController
|
||||||
|
DEFAULT_CERT_NAME = 'DocuSeal Self-Host Autogenerated'
|
||||||
|
|
||||||
|
CertFormRecord = Struct.new(:name, :file, :password, keyword_init: true) do
|
||||||
|
include ActiveModel::Validations
|
||||||
|
|
||||||
|
def to_key
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
before_action :load_encrypted_config
|
||||||
|
authorize_resource :encrypted_config, parent: false, only: %i[new create]
|
||||||
|
authorize_resource :encrypted_config, only: %i[update destroy show]
|
||||||
|
|
||||||
|
def show
|
||||||
|
cert_data = @encrypted_config.value || {}
|
||||||
|
|
||||||
|
default_pkcs = GenerateCertificate.load_pkcs(cert_data) if cert_data['cert'].present?
|
||||||
|
|
||||||
|
custom_pkcs_list = (cert_data['custom'] || []).map do |e|
|
||||||
|
{ 'pkcs' => OpenSSL::PKCS12.new(Base64.urlsafe_decode64(e['data']), e['password'].to_s),
|
||||||
|
'name' => e['name'],
|
||||||
|
'status' => e['status'] }
|
||||||
|
end
|
||||||
|
|
||||||
|
@pkcs_list = [
|
||||||
|
if default_pkcs
|
||||||
|
{
|
||||||
|
'pkcs' => default_pkcs,
|
||||||
|
'name' => DEFAULT_CERT_NAME,
|
||||||
|
'status' => custom_pkcs_list.any? { |e| e['status'] == 'default' } ? 'validate' : 'default'
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
*custom_pkcs_list
|
||||||
|
].compact.reverse
|
||||||
|
end
|
||||||
|
|
||||||
|
def new
|
||||||
|
@cert_record = CertFormRecord.new
|
||||||
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
pdfs =
|
@cert_record = CertFormRecord.new(**cert_params)
|
||||||
params[:files].map do |file|
|
|
||||||
HexaPDF::Document.new(io: file.open)
|
|
||||||
end
|
|
||||||
|
|
||||||
certs = Accounts.load_signing_certs(current_account)
|
if (@encrypted_config.value && @encrypted_config.value['custom']&.any? { |e| e['name'] == @cert_record.name }) ||
|
||||||
|
@cert_record.name == DEFAULT_CERT_NAME
|
||||||
|
|
||||||
trusted_certs = [certs[:cert], certs[:sub_ca], certs[:root_ca]]
|
@cert_record.errors.add(:name, 'already exists')
|
||||||
|
|
||||||
render turbo_stream: turbo_stream.replace('result', partial: 'result',
|
return render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'),
|
||||||
locals: { pdfs:, files: params[:files], trusted_certs: })
|
status: :unprocessable_entity
|
||||||
rescue HexaPDF::MalformedPDFError
|
end
|
||||||
render turbo_stream: turbo_stream.replace('result', html: helpers.tag.div('Invalid PDF', id: 'result'))
|
|
||||||
|
save_new_cert!(@encrypted_config, @cert_record)
|
||||||
|
|
||||||
|
redirect_to settings_esign_path, notice: 'Certificate has been successfully added!'
|
||||||
|
rescue OpenSSL::PKCS12::PKCS12Error
|
||||||
|
@cert_record.errors.add(:password, "is invalid. Make sure you're uploading a valid .p12 file")
|
||||||
|
|
||||||
|
render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@encrypted_config.value['custom'].each { |e| e['status'] = 'validate' }
|
||||||
|
|
||||||
|
custom_cert_data = @encrypted_config.value['custom'].find { |e| e['name'] == params[:name] }
|
||||||
|
custom_cert_data['status'] = 'default' if custom_cert_data
|
||||||
|
|
||||||
|
@encrypted_config.save!
|
||||||
|
|
||||||
|
redirect_to settings_esign_path, notice: 'Default certificate has been selected'
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@encrypted_config.value['custom'].reject! { |e| e['name'] == params[:name] }
|
||||||
|
|
||||||
|
@encrypted_config.save!
|
||||||
|
|
||||||
|
redirect_to settings_esign_path, notice: 'Certificate has been removed'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_encrypted_config
|
||||||
|
@encrypted_config = EncryptedConfig.find_or_initialize_by(account: current_account,
|
||||||
|
key: EncryptedConfig::ESIGN_CERTS_KEY)
|
||||||
|
end
|
||||||
|
|
||||||
|
def save_new_cert!(cert_configs, cert_record)
|
||||||
|
pkcs = OpenSSL::PKCS12.new(cert_record.file.read, cert_record.password)
|
||||||
|
|
||||||
|
cert_configs.value ||= {}
|
||||||
|
cert_configs.value['custom'] ||= []
|
||||||
|
cert_configs.value['custom'].each { |e| e['status'] = 'validate' }
|
||||||
|
cert_configs.value['custom'] << {
|
||||||
|
data: Base64.urlsafe_encode64(pkcs.to_der),
|
||||||
|
password: cert_record.password,
|
||||||
|
name: cert_record.name,
|
||||||
|
status: 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
cert_configs.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
def cert_params
|
||||||
|
return {} if params[:esign_settings_controller_cert_form_record].blank?
|
||||||
|
|
||||||
|
params.require(:esign_settings_controller_cert_form_record).permit(:name, :file, :password)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class MfaSetupController < ApplicationController
|
||||||
|
before_action do
|
||||||
|
authorize!(:update, current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
def new
|
||||||
|
current_user.otp_secret ||= User.generate_otp_secret
|
||||||
|
|
||||||
|
current_user.save!
|
||||||
|
|
||||||
|
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal::PRODUCT_NAME)
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit; end
|
||||||
|
|
||||||
|
def create
|
||||||
|
if current_user.validate_and_consume_otp!(params[:otp_attempt])
|
||||||
|
current_user.otp_required_for_login = true
|
||||||
|
current_user.save!
|
||||||
|
|
||||||
|
redirect_to settings_profile_index_path, notice: '2FA has been configured'
|
||||||
|
else
|
||||||
|
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal::PRODUCT_NAME)
|
||||||
|
|
||||||
|
@error_message = 'Code is invalid'
|
||||||
|
|
||||||
|
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/new'), status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
if current_user.validate_and_consume_otp!(params[:otp_attempt])
|
||||||
|
current_user.update!(otp_required_for_login: false, otp_secret: nil)
|
||||||
|
|
||||||
|
redirect_to settings_profile_index_path, notice: '2FA has been removed'
|
||||||
|
else
|
||||||
|
@error_message = 'Code is invalid'
|
||||||
|
|
||||||
|
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class NewslettersController < ApplicationController
|
class NewslettersController < ApplicationController
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class NotificationsSettingsController < ApplicationController
|
||||||
|
before_action :load_bcc_config, only: :index
|
||||||
|
before_action :load_reminder_config, only: :index
|
||||||
|
authorize_resource :bcc_config, only: :index
|
||||||
|
authorize_resource :reminder_config, only: :index
|
||||||
|
|
||||||
|
before_action :build_account_config, only: :create
|
||||||
|
authorize_resource :account_config, only: :create
|
||||||
|
|
||||||
|
def index; end
|
||||||
|
|
||||||
|
def create
|
||||||
|
if @account_config.save
|
||||||
|
redirect_back fallback_location: settings_notifications_path, notice: 'Changes have been saved'
|
||||||
|
else
|
||||||
|
redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def build_account_config
|
||||||
|
@account_config =
|
||||||
|
AccountConfig.find_or_initialize_by(account: current_account, key: email_config_params[:key])
|
||||||
|
|
||||||
|
@account_config.assign_attributes(email_config_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_bcc_config
|
||||||
|
@bcc_config =
|
||||||
|
AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::BCC_EMAILS)
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_reminder_config
|
||||||
|
@reminder_config =
|
||||||
|
AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::SUBMITTER_REMAILERS)
|
||||||
|
end
|
||||||
|
|
||||||
|
def email_config_params
|
||||||
|
params.require(:account_config).permit!.tap do |attrs|
|
||||||
|
attrs[:key] = nil unless attrs[:key].in?([AccountConfig::BCC_EMAILS, AccountConfig::SUBMITTER_REMAILERS])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class PersonalizationSettingsController < ApplicationController
|
class PersonalizationSettingsController < ApplicationController
|
||||||
def show; end
|
def show
|
||||||
|
authorize!(:read, AccountConfig)
|
||||||
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
account_config =
|
account_config =
|
||||||
current_account.account_configs.find_or_initialize_by(key: encrypted_config_params[:key])
|
current_account.account_configs.find_or_initialize_by(key: encrypted_config_params[:key])
|
||||||
|
|
||||||
|
authorize!(:create, account_config)
|
||||||
|
|
||||||
account_config.update!(encrypted_config_params)
|
account_config.update!(encrypted_config_params)
|
||||||
|
|
||||||
redirect_back(fallback_location: settings_personalization_path, notice: 'Settings have been saved.')
|
redirect_back(fallback_location: settings_personalization_path, notice: 'Settings have been saved.')
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ProfileController < ApplicationController
|
class ProfileController < ApplicationController
|
||||||
|
before_action do
|
||||||
|
authorize!(:manage, current_user)
|
||||||
|
end
|
||||||
|
|
||||||
def index; end
|
def index; end
|
||||||
|
|
||||||
def update_contact
|
def update_contact
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class RegistrationsController < Devise::RegistrationsController
|
|||||||
return unless signed_in?
|
return unless signed_in?
|
||||||
return if params[:redir].blank?
|
return if params[:redir].blank?
|
||||||
|
|
||||||
redirect_to after_sign_up_path_for(current_user)
|
redirect_to after_sign_up_path_for(current_user), allow_other_host: true
|
||||||
end
|
end
|
||||||
|
|
||||||
def require_no_authentication
|
def require_no_authentication
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ class SendSubmissionEmailController < ApplicationController
|
|||||||
|
|
||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_before_action :verify_authenticity_token
|
skip_before_action :verify_authenticity_token
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
def success; end
|
def success; end
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class SessionsController < Devise::SessionsController
|
class SessionsController < Devise::SessionsController
|
||||||
|
before_action :configure_permitted_parameters
|
||||||
|
|
||||||
def create
|
def create
|
||||||
if Docuseal.multitenant? && !User.exists?(email: sign_in_params[:email])
|
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)),
|
return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)),
|
||||||
notice: 'Create a new account'
|
notice: 'Create a new account'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if User.exists?(email: sign_in_params[:email], otp_required_for_login: true) && sign_in_params[:otp_attempt].blank?
|
||||||
|
return render :otp, locals: { resource: User.new(sign_in_params) }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -22,6 +28,10 @@ class SessionsController < Devise::SessionsController
|
|||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def configure_permitted_parameters
|
||||||
|
devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt])
|
||||||
|
end
|
||||||
|
|
||||||
def require_no_authentication
|
def require_no_authentication
|
||||||
super
|
super
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
class SetupController < ApplicationController
|
class SetupController < ApplicationController
|
||||||
skip_before_action :maybe_redirect_to_setup
|
skip_before_action :maybe_redirect_to_setup
|
||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
before_action :redirect_to_root_if_signed, if: :signed_in?
|
before_action :redirect_to_root_if_signed, if: :signed_in?
|
||||||
before_action :ensure_first_user_not_created!
|
before_action :ensure_first_user_not_created!
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SmsSettingsController < ApplicationController
|
||||||
|
before_action :load_encrypted_config
|
||||||
|
authorize_resource :encrypted_config, only: :index
|
||||||
|
authorize_resource :encrypted_config, parent: false, except: :index
|
||||||
|
|
||||||
|
def index; end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_encrypted_config
|
||||||
|
@encrypted_config =
|
||||||
|
EncryptedConfig.find_or_initialize_by(account: current_account, key: 'sms_configs')
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -4,6 +4,7 @@ class StartFormController < ApplicationController
|
|||||||
layout 'form'
|
layout 'form'
|
||||||
|
|
||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
before_action :load_template
|
before_action :load_template
|
||||||
|
|
||||||
@@ -25,7 +26,9 @@ class StartFormController < ApplicationController
|
|||||||
ua: request.user_agent
|
ua: request.user_agent
|
||||||
)
|
)
|
||||||
|
|
||||||
@submitter.submission ||= Submission.new(template: @template, source: :link)
|
@submitter.submission ||= Submission.new(template: @template,
|
||||||
|
template_submitters: @template.submitters,
|
||||||
|
source: :link)
|
||||||
|
|
||||||
if @submitter.save
|
if @submitter.save
|
||||||
redirect_to submit_form_path(@submitter.slug)
|
redirect_to submit_form_path(@submitter.slug)
|
||||||
@@ -44,7 +47,9 @@ class StartFormController < ApplicationController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def submitter_params
|
def submitter_params
|
||||||
params.require(:submitter).permit(:email)
|
params.require(:submitter).permit(:email).tap do |attrs|
|
||||||
|
attrs[:email] = Submissions.normalize_email(attrs[:email])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_template
|
def load_template
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
class StorageSettingsController < ApplicationController
|
class StorageSettingsController < ApplicationController
|
||||||
before_action :load_encrypted_config
|
before_action :load_encrypted_config
|
||||||
|
authorize_resource :encrypted_config, only: :index
|
||||||
|
authorize_resource :encrypted_config, parent: false, only: :create
|
||||||
|
|
||||||
def index; end
|
def index; end
|
||||||
|
|
||||||
|
|||||||
@@ -2,59 +2,57 @@
|
|||||||
|
|
||||||
class SubmissionsController < ApplicationController
|
class SubmissionsController < ApplicationController
|
||||||
before_action :load_template, only: %i[new create]
|
before_action :load_template, only: %i[new create]
|
||||||
|
authorize_resource :template, only: %i[new create]
|
||||||
|
|
||||||
|
load_and_authorize_resource :submission, only: %i[show destroy]
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@submission =
|
ActiveRecord::Associations::Preloader.new(
|
||||||
Submission.joins(:template).where(template: { account_id: current_account.id })
|
records: [@submission],
|
||||||
.preload(template: { documents_attachments: { preview_images_attachments: :blob } })
|
associations: [:template, { template_schema_documents: [:blob, { preview_images_attachments: :blob }] }]
|
||||||
.find(params[:id])
|
).call
|
||||||
|
|
||||||
render :show, layout: 'plain'
|
render :show, layout: 'plain'
|
||||||
end
|
end
|
||||||
|
|
||||||
def new; end
|
def new
|
||||||
|
authorize!(:new, Submission)
|
||||||
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
authorize!(:create, Submission)
|
||||||
|
|
||||||
submissions =
|
submissions =
|
||||||
if params[:emails].present?
|
if params[:emails].present?
|
||||||
Submissions.create_from_emails(template: @template,
|
Submissions.create_from_emails(template: @template,
|
||||||
user: current_user,
|
user: current_user,
|
||||||
source: :invite,
|
source: :invite,
|
||||||
send_email: params[:send_email] == '1',
|
mark_as_sent: params[:send_email] == '1',
|
||||||
emails: params[:emails])
|
emails: params[:emails])
|
||||||
else
|
else
|
||||||
Submissions.create_from_submitters(template: @template,
|
Submissions.create_from_submitters(template: @template,
|
||||||
user: current_user,
|
user: current_user,
|
||||||
source: :invite,
|
source: :invite,
|
||||||
send_email: params[:send_email] == '1',
|
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
|
||||||
|
mark_as_sent: params[:send_email] == '1',
|
||||||
submissions_attrs: submissions_params[:submission].to_h.values)
|
submissions_attrs: submissions_params[:submission].to_h.values)
|
||||||
end
|
end
|
||||||
|
|
||||||
submitters = submissions.flat_map(&:submitters)
|
Submissions.send_signature_requests(submissions, params)
|
||||||
|
|
||||||
if params[:send_email] == '1'
|
redirect_to template_path(@template), notice: 'New recipients have been added'
|
||||||
submitters.each do |submitter|
|
|
||||||
SubmitterMailer.invitation_email(submitter, message: params[:message]).deliver_later!
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
redirect_to template_path(@template),
|
|
||||||
notice: "#{submitters.size} #{'recipient'.pluralize(submitters.size)} added"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
submission = Submission.joins(:template).where(template: { account_id: current_account.id })
|
@submission.update!(deleted_at: Time.current)
|
||||||
.find(params[:id])
|
|
||||||
|
|
||||||
submission.update!(deleted_at: Time.current)
|
redirect_back(fallback_location: template_path(@submission.template), notice: 'Submission has been archived')
|
||||||
|
|
||||||
redirect_back(fallback_location: template_path(submission.template), notice: 'Submission has been archived')
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def submissions_params
|
def submissions_params
|
||||||
params.permit(submission: { submitters: [%i[uuid email]] })
|
params.permit(submission: { submitters: [%i[uuid email phone name]] })
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_template
|
def load_template
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ class SubmissionsDebugController < ApplicationController
|
|||||||
layout 'plain'
|
layout 'plain'
|
||||||
|
|
||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@submitter = Submitter.preload({ attachments_attachments: :blob },
|
@submitter = Submitter.preload({ attachments_attachments: :blob },
|
||||||
@@ -15,9 +16,13 @@ class SubmissionsDebugController < ApplicationController
|
|||||||
render 'submit_form/show'
|
render 'submit_form/show'
|
||||||
end
|
end
|
||||||
f.pdf do
|
f.pdf do
|
||||||
Submissions::GenerateResultAttachments.call(@submitter)
|
if params[:audit]
|
||||||
|
Submissions::GenerateAuditTrail.call(@submitter.submission)
|
||||||
|
else
|
||||||
|
Submissions::GenerateResultAttachments.call(@submitter)
|
||||||
|
end
|
||||||
|
|
||||||
send_data ActiveStorage::Attachment.where(name: :documents).last.download,
|
send_data ActiveStorage::Attachment.where(name: params[:audit] ? :audit_trail : :documents).last.download,
|
||||||
filename: 'debug.pdf',
|
filename: 'debug.pdf',
|
||||||
disposition: 'inline',
|
disposition: 'inline',
|
||||||
type: 'application/pdf'
|
type: 'application/pdf'
|
||||||
|
|||||||
@@ -2,14 +2,19 @@
|
|||||||
|
|
||||||
class SubmissionsDownloadController < ApplicationController
|
class SubmissionsDownloadController < ApplicationController
|
||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
def index
|
def index
|
||||||
submitter = Submitter.find_by(slug: params[:submitter_slug])
|
submitter = Submitter.find_by(slug: params[:submitter_slug])
|
||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(submitter)
|
Submissions::EnsureResultGenerated.call(submitter)
|
||||||
|
|
||||||
|
last_submitter = submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
|
||||||
|
|
||||||
|
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||||
|
|
||||||
urls =
|
urls =
|
||||||
Submitters.select_attachments_for_download(submitter).map do |attachment|
|
Submitters.select_attachments_for_download(last_submitter).map do |attachment|
|
||||||
helpers.rails_blob_url(attachment)
|
helpers.rails_blob_url(attachment)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class SubmissionsExportController < ApplicationController
|
class SubmissionsExportController < ApplicationController
|
||||||
before_action :load_template
|
load_and_authorize_resource :template
|
||||||
|
load_and_authorize_resource :submission, through: :template, parent: false, only: :index
|
||||||
|
|
||||||
def index
|
def index
|
||||||
submissions = @template.submissions.active
|
submissions = @submissions.active
|
||||||
.preload(submitters: { documents_attachments: :blob,
|
.preload(submitters: { documents_attachments: :blob,
|
||||||
attachments_attachments: :blob })
|
attachments_attachments: :blob })
|
||||||
.order(id: :asc)
|
.order(id: :asc)
|
||||||
|
|
||||||
if params[:format] == 'csv'
|
if params[:format] == 'csv'
|
||||||
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format]),
|
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format]),
|
||||||
@@ -19,10 +20,4 @@ class SubmissionsExportController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def new; end
|
def new; end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def load_template
|
|
||||||
@template = current_account.templates.find(params[:template_id])
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
@@ -4,10 +4,13 @@ class SubmitFormController < ApplicationController
|
|||||||
layout 'form'
|
layout 'form'
|
||||||
|
|
||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@submitter =
|
@submitter =
|
||||||
Submitter.preload(submission: { template: { documents_attachments: { preview_images_attachments: :blob } } })
|
Submitter.preload(submission: [
|
||||||
|
:template, { template_schema_documents: [:blob, { preview_images_attachments: :blob }] }
|
||||||
|
])
|
||||||
.find_by!(slug: params[:slug])
|
.find_by!(slug: params[:slug])
|
||||||
|
|
||||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||||
@@ -21,6 +24,8 @@ class SubmitFormController < ApplicationController
|
|||||||
Submitters::SubmitValues.call(submitter, params, request)
|
Submitters::SubmitValues.call(submitter, params, request)
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
|
rescue Submitters::SubmitValues::ValidationError => e
|
||||||
|
render json: { error: e.message }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
def completed
|
def completed
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmittersSendEmailController < ApplicationController
|
||||||
|
load_and_authorize_resource :submitter, id_param: :submitter_slug, find_by: :slug
|
||||||
|
|
||||||
|
def create
|
||||||
|
SubmitterMailer.invitation_email(@submitter).deliver_later!
|
||||||
|
|
||||||
|
SubmissionEvent.create!(submitter: @submitter, event_type: 'send_email')
|
||||||
|
|
||||||
|
@submitter.sent_at ||= Time.current
|
||||||
|
@submitter.save!
|
||||||
|
|
||||||
|
redirect_back(fallback_location: submission_path(@submitter.submission), notice: 'Email has been sent')
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesArchivedController < ApplicationController
|
||||||
|
load_and_authorize_resource :template, parent: false
|
||||||
|
|
||||||
|
def index
|
||||||
|
@templates = @templates.where.not(deleted_at: nil).preload(:author).order(id: :desc)
|
||||||
|
@templates = Templates.search(@templates, params[:q])
|
||||||
|
|
||||||
|
@pagy, @templates = pagy(@templates, items: 12)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesArchivedSubmissionsController < ApplicationController
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
load_and_authorize_resource :submission, through: :template, parent: false
|
||||||
|
|
||||||
|
def index
|
||||||
|
@submissions = @submissions.where.not(deleted_at: nil)
|
||||||
|
@submissions = Submissions.search(@submissions, params[:q])
|
||||||
|
|
||||||
|
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
redirect_to root_path
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,30 +1,35 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class TemplatesController < ApplicationController
|
class TemplatesController < ApplicationController
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
before_action :load_base_template, only: %i[new create]
|
before_action :load_base_template, only: %i[new create]
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@template = current_account.templates.active.find(params[:id])
|
submissions = @template.submissions
|
||||||
|
submissions = submissions.active if @template.deleted_at.blank?
|
||||||
|
submissions = Submissions.search(submissions, params[:q])
|
||||||
|
|
||||||
@pagy, @submissions = pagy(@template.submissions.active.preload(:submitters).order(id: :desc))
|
@pagy, @submissions = pagy(submissions.preload(:submitters).order(id: :desc))
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
redirect_to root_path
|
redirect_to root_path
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
@template = current_account.templates.new
|
|
||||||
@template.name = "#{@base_template.name} (Clone)" if @base_template
|
@template.name = "#{@base_template.name} (Clone)" if @base_template
|
||||||
end
|
end
|
||||||
|
|
||||||
def edit
|
def edit
|
||||||
@template = current_account.templates.preload(schema_documents: { preview_images_attachments: :blob })
|
ActiveRecord::Associations::Preloader.new(
|
||||||
.find(params[:id])
|
records: [@template],
|
||||||
|
associations: [schema_documents: { preview_images_attachments: :blob }]
|
||||||
|
).call
|
||||||
|
|
||||||
render :edit, layout: 'plain'
|
render :edit, layout: 'plain'
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@template = current_account.templates.new(template_params)
|
@template.account = current_account
|
||||||
@template.author = current_user
|
@template.author = current_user
|
||||||
@template.assign_attributes(@base_template.slice(:fields, :schema, :submitters)) if @base_template
|
@template.assign_attributes(@base_template.slice(:fields, :schema, :submitters)) if @base_template
|
||||||
|
|
||||||
@@ -38,7 +43,6 @@ class TemplatesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@template = current_account.templates.find(params[:id])
|
|
||||||
@template.update!(deleted_at: Time.current)
|
@template.update!(deleted_at: Time.current)
|
||||||
|
|
||||||
redirect_back(fallback_location: root_path, notice: 'Template has been archived.')
|
redirect_back(fallback_location: root_path, notice: 'Template has been archived.')
|
||||||
@@ -53,8 +57,6 @@ class TemplatesController < ApplicationController
|
|||||||
def load_base_template
|
def load_base_template
|
||||||
return if params[:base_template_id].blank?
|
return if params[:base_template_id].blank?
|
||||||
|
|
||||||
@base_template = current_account.templates
|
@base_template = current_account.templates.find_by(id: params[:base_template_id])
|
||||||
.preload(documents_attachments: :preview_images_attachments)
|
|
||||||
.find_by(id: params[:base_template_id])
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesRestoreController < ApplicationController
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def create
|
||||||
|
@template.update!(deleted_at: nil)
|
||||||
|
|
||||||
|
redirect_to template_path(@template), notice: 'Template has been unarchived'
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesUploadsController < ApplicationController
|
||||||
|
load_and_authorize_resource :template, parent: false
|
||||||
|
|
||||||
|
def create
|
||||||
|
@template.author = current_user
|
||||||
|
@template.name = File.basename(params[:files].first.original_filename, '.*')
|
||||||
|
|
||||||
|
@template.save!
|
||||||
|
|
||||||
|
documents = Templates::CreateAttachments.call(@template, params)
|
||||||
|
|
||||||
|
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
|
||||||
|
|
||||||
|
@template.update!(schema:)
|
||||||
|
|
||||||
|
redirect_to edit_template_path(@template)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,26 +1,20 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class UsersController < ApplicationController
|
class UsersController < ApplicationController
|
||||||
before_action :load_user, only: %i[edit update destroy]
|
load_and_authorize_resource :user, only: %i[index edit new update destroy]
|
||||||
|
|
||||||
|
before_action :build_user, only: :create
|
||||||
|
authorize_resource :user, only: :create
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@pagy, @users = pagy(current_account.users.active.order(id: :desc))
|
@pagy, @users = pagy(@users.active.order(id: :desc))
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new; end
|
||||||
@user = current_account.users.new
|
|
||||||
end
|
|
||||||
|
|
||||||
def edit; end
|
def edit; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@user = current_account.users.find_by(email: user_params[:email])&.tap do |user|
|
|
||||||
user.assign_attributes(user_params)
|
|
||||||
user.deleted_at = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
@user ||= current_account.users.new(user_params)
|
|
||||||
|
|
||||||
if @user.save
|
if @user.save
|
||||||
UserMailer.invitation_email(@user).deliver_later!
|
UserMailer.invitation_email(@user).deliver_later!
|
||||||
|
|
||||||
@@ -33,7 +27,7 @@ class UsersController < ApplicationController
|
|||||||
def update
|
def update
|
||||||
return redirect_to settings_users_path, notice: 'Unable to update user.' if Docuseal.demo?
|
return redirect_to settings_users_path, notice: 'Unable to update user.' if Docuseal.demo?
|
||||||
|
|
||||||
if @user.update(user_params.compact_blank)
|
if @user.update(user_params.compact_blank.except(current_user == @user ? :role : nil))
|
||||||
redirect_to settings_users_path, notice: 'User has been updated'
|
redirect_to settings_users_path, notice: 'User has been updated'
|
||||||
else
|
else
|
||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity
|
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity
|
||||||
@@ -52,11 +46,18 @@ class UsersController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def load_user
|
def build_user
|
||||||
@user = current_account.users.find(params[:id])
|
@user = current_account.users.find_by(email: user_params[:email])&.tap do |user|
|
||||||
|
user.assign_attributes(user_params)
|
||||||
|
user.deleted_at = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
@user ||= current_account.users.new(user_params)
|
||||||
|
|
||||||
|
@user
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_params
|
def user_params
|
||||||
params.require(:user).permit(:email, :first_name, :last_name, :password)
|
params.require(:user).permit(:email, :first_name, :last_name, :password, :role)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class VerifyPdfSignatureController < ApplicationController
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
def create
|
||||||
|
pdfs =
|
||||||
|
params[:files].map do |file|
|
||||||
|
HexaPDF::Document.new(io: file.open)
|
||||||
|
end
|
||||||
|
|
||||||
|
cert_data = if Docuseal.multitenant?
|
||||||
|
Docuseal::CERTS
|
||||||
|
else
|
||||||
|
EncryptedConfig.find_by(account: current_account, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
|
||||||
|
end
|
||||||
|
|
||||||
|
default_pkcs = GenerateCertificate.load_pkcs(cert_data)
|
||||||
|
|
||||||
|
custom_certs = cert_data.fetch('custom', []).map do |e|
|
||||||
|
OpenSSL::PKCS12.new(Base64.urlsafe_decode64(e['data']), e['password'].to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
trusted_certs = [default_pkcs.certificate,
|
||||||
|
*default_pkcs.ca_certs,
|
||||||
|
*custom_certs.map(&:certificate),
|
||||||
|
*custom_certs.flat_map(&:ca_certs).compact]
|
||||||
|
|
||||||
|
render turbo_stream: turbo_stream.replace('result', partial: 'result',
|
||||||
|
locals: { pdfs:, files: params[:files], trusted_certs: })
|
||||||
|
rescue HexaPDF::MalformedPDFError
|
||||||
|
render turbo_stream: turbo_stream.replace('result', html: helpers.tag.div('Invalid PDF', id: 'result'))
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,15 +1,12 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class WebhookSettingsController < ApplicationController
|
class WebhookSettingsController < ApplicationController
|
||||||
def show
|
before_action :load_encrypted_config
|
||||||
@encrypted_config =
|
authorize_resource :encrypted_config, parent: false
|
||||||
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
|
||||||
end
|
def show; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@encrypted_config =
|
|
||||||
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
|
||||||
|
|
||||||
@encrypted_config.update!(encrypted_config_params)
|
@encrypted_config.update!(encrypted_config_params)
|
||||||
|
|
||||||
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook URL has been saved.')
|
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook URL has been saved.')
|
||||||
@@ -18,13 +15,18 @@ class WebhookSettingsController < ApplicationController
|
|||||||
def update
|
def update
|
||||||
submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last
|
submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last
|
||||||
|
|
||||||
SendWebhookRequestJob.perform_later(submitter)
|
SendFormCompletedWebhookRequestJob.perform_later(submitter)
|
||||||
|
|
||||||
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook request has been sent.')
|
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook request has been sent.')
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def load_encrypted_config
|
||||||
|
@encrypted_config =
|
||||||
|
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
||||||
|
end
|
||||||
|
|
||||||
def encrypted_config_params
|
def encrypted_config_params
|
||||||
params.require(:encrypted_config).permit(:value)
|
params.require(:encrypted_config).permit(:value)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -74,6 +74,9 @@ window.customElements.define('template-builder', class extends HTMLElement {
|
|||||||
|
|
||||||
this.app = createApp(TemplateBuilder, {
|
this.app = createApp(TemplateBuilder, {
|
||||||
template: reactive(JSON.parse(this.dataset.template)),
|
template: reactive(JSON.parse(this.dataset.template)),
|
||||||
|
backgroundColor: '#faf7f5',
|
||||||
|
withPhone: this.dataset.withPhone === 'true',
|
||||||
|
acceptFileTypes: this.dataset.acceptFileTypes,
|
||||||
isDirectUpload: this.dataset.isDirectUpload === 'true'
|
isDirectUpload: this.dataset.isDirectUpload === 'true'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,11 @@ export default actionable(targetable(class extends HTMLElement {
|
|||||||
this.uploadFiles(this.input.files)
|
this.uploadFiles(this.input.files)
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleLoading = () => {
|
toggleLoading = (e) => {
|
||||||
|
if (e && e.target && !e.target.contains(this)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.loading.classList.toggle('hidden')
|
this.loading.classList.toggle('hidden')
|
||||||
this.icon.classList.toggle('hidden')
|
this.icon.classList.toggle('hidden')
|
||||||
this.classList.toggle('opacity-50')
|
this.classList.toggle('opacity-50')
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ window.customElements.define('submission-form', class extends HTMLElement {
|
|||||||
this.appElem = document.createElement('div')
|
this.appElem = document.createElement('div')
|
||||||
|
|
||||||
this.app = createApp(Form, {
|
this.app = createApp(Form, {
|
||||||
submitterSlug: this.dataset.submitterSlug,
|
submitter: JSON.parse(this.dataset.submitter),
|
||||||
submitterUuid: this.dataset.submitterUuid,
|
|
||||||
authenticityToken: this.dataset.authenticityToken,
|
authenticityToken: this.dataset.authenticityToken,
|
||||||
canSendEmail: this.dataset.canSendEmail === 'true',
|
canSendEmail: this.dataset.canSendEmail === 'true',
|
||||||
isDirectUpload: this.dataset.isDirectUpload === 'true',
|
isDirectUpload: this.dataset.isDirectUpload === 'true',
|
||||||
isDemo: this.dataset.isDemo === 'true',
|
isDemo: this.dataset.isDemo === 'true',
|
||||||
|
attribution: this.dataset.attribution !== 'false',
|
||||||
withConfetti: true,
|
withConfetti: true,
|
||||||
values: reactive(JSON.parse(this.dataset.values)),
|
values: reactive(JSON.parse(this.dataset.values)),
|
||||||
attachments: reactive(JSON.parse(this.dataset.attachments)),
|
attachments: reactive(JSON.parse(this.dataset.attachments)),
|
||||||
|
|||||||
@@ -113,7 +113,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3 } from '@tabler/icons-vue'
|
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FieldArea',
|
name: 'FieldArea',
|
||||||
@@ -179,7 +179,8 @@ export default {
|
|||||||
select: 'Select',
|
select: 'Select',
|
||||||
checkbox: 'Checkbox',
|
checkbox: 'Checkbox',
|
||||||
radio: 'Radio',
|
radio: 'Radio',
|
||||||
multiple: 'Multiple Select'
|
multiple: 'Multiple Select',
|
||||||
|
phone: 'Phone'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fieldIcons () {
|
fieldIcons () {
|
||||||
@@ -193,7 +194,8 @@ export default {
|
|||||||
checkbox: IconCheckbox,
|
checkbox: IconCheckbox,
|
||||||
radio: IconCircleDot,
|
radio: IconCircleDot,
|
||||||
cells: IconColumns3,
|
cells: IconColumns3,
|
||||||
multiple: IconChecks
|
multiple: IconChecks,
|
||||||
|
phone: IconPhoneCheck
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
image () {
|
image () {
|
||||||
|
|||||||
@@ -7,13 +7,13 @@
|
|||||||
:height="30"
|
:height="30"
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
Form has been completed!
|
{{ t('form_has_been_completed') }}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<div class="space-y-3 mt-5">
|
<div class="space-y-3 mt-5">
|
||||||
<button
|
<button
|
||||||
v-if="canSendEmail && !isDemo"
|
v-if="canSendEmail && !isDemo"
|
||||||
class="white-button flex items-center space-x-1 w-full"
|
class="white-button !h-auto flex items-center space-x-1 w-full"
|
||||||
:disabled="isSendingCopy"
|
:disabled="isSendingCopy"
|
||||||
@click.prevent="sendCopyToEmail"
|
@click.prevent="sendCopyToEmail"
|
||||||
>
|
>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
/>
|
/>
|
||||||
<IconMail v-else />
|
<IconMail v-else />
|
||||||
<span>
|
<span>
|
||||||
Send copy via email
|
{{ t('send_copy_via_email') }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
/>
|
/>
|
||||||
<IconDownload v-else />
|
<IconDownload v-else />
|
||||||
<span>
|
<span>
|
||||||
Download
|
{{ t('download') }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<a
|
<a
|
||||||
@@ -58,17 +58,20 @@
|
|||||||
>
|
>
|
||||||
<IconLogin />
|
<IconLogin />
|
||||||
<span>
|
<span>
|
||||||
Create a Free Account
|
{{ t('create_a_free_account') }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center mt-4">
|
<div
|
||||||
Signed with
|
v-if="attribution"
|
||||||
|
class="text-center mt-4"
|
||||||
|
>
|
||||||
|
{{ t('signed_with') }}
|
||||||
<a
|
<a
|
||||||
href="https://www.docuseal.co"
|
href="https://www.docuseal.co"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="underline"
|
class="underline"
|
||||||
>DocuSeal</a> - open source documents software
|
>DocuSeal</a> - {{ t('open_source_documents_software') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -86,7 +89,7 @@ export default {
|
|||||||
IconLogin,
|
IconLogin,
|
||||||
IconDownload
|
IconDownload
|
||||||
},
|
},
|
||||||
inject: ['baseUrl'],
|
inject: ['baseUrl', 't'],
|
||||||
props: {
|
props: {
|
||||||
submitterSlug: {
|
submitterSlug: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -97,6 +100,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
attribution: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
withConfetti: {
|
withConfetti: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -132,7 +140,7 @@ export default {
|
|||||||
fetch(this.baseUrl + `/send_submission_email.json?submitter_slug=${this.submitterSlug}`, {
|
fetch(this.baseUrl + `/send_submission_email.json?submitter_slug=${this.submitterSlug}`, {
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
alert('Email has been sent')
|
alert(this.t('email_has_been_sent'))
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.isSendingCopy = false
|
this.isSendingCopy = false
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
{{ message }}
|
{{ message }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs">
|
<div class="text-xs">
|
||||||
<span class="font-medium">Click to upload</span> or drag and drop files
|
<span class="font-medium">{{ t('click_to_upload') }}</span> {{ t('or_drag_and_drop_files') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -55,7 +55,7 @@ export default {
|
|||||||
IconCloudUpload,
|
IconCloudUpload,
|
||||||
IconInnerShadowTop
|
IconInnerShadowTop
|
||||||
},
|
},
|
||||||
inject: ['baseUrl'],
|
inject: ['baseUrl', 't'],
|
||||||
props: {
|
props: {
|
||||||
message: {
|
message: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
class="btn btn-neutral flex text-white absolute rounded-none border-x-0 md:border md:rounded-full bottom-0 w-full md:mb-4 text-base"
|
class="btn btn-neutral flex text-white absolute rounded-none border-x-0 md:border md:rounded-full bottom-0 w-full md:mb-4 text-base"
|
||||||
@click.prevent="isFormVisible = true"
|
@click.prevent="isFormVisible = true"
|
||||||
>
|
>
|
||||||
Submit Form
|
{{ t('submit_form') }}
|
||||||
<IconArrowsDiagonal
|
<IconArrowsDiagonal
|
||||||
class="absolute right-0 mr-4"
|
class="absolute right-0 mr-4"
|
||||||
:width="20"
|
:width="20"
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
v-show="isFormVisible"
|
v-show="isFormVisible"
|
||||||
id="form_container"
|
id="form_container"
|
||||||
class="shadow-md bg-base-100 absolute bottom-0 md:bottom-4 w-full border-base-200 border p-4 rounded"
|
class="shadow-md bg-base-100 absolute bottom-0 md:bottom-4 w-full border-base-200 border p-4 rounded"
|
||||||
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-if="!isCompleted"
|
v-if="!isCompleted"
|
||||||
@@ -62,7 +63,7 @@
|
|||||||
:for="currentField.uuid"
|
:for="currentField.uuid"
|
||||||
class="label text-2xl mb-2"
|
class="label text-2xl mb-2"
|
||||||
>{{ currentField.name }}
|
>{{ currentField.name }}
|
||||||
<template v-if="!currentField.required">(optional)</template>
|
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
@@ -74,7 +75,7 @@
|
|||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
class="base-input !text-2xl w-full"
|
class="base-input !text-2xl w-full"
|
||||||
:required="currentField.required"
|
:required="currentField.required"
|
||||||
:placeholder="`Type here...${currentField.required ? '' : ' (optional)'}`"
|
:placeholder="`${t('type_here')}...${currentField.required ? '' : ` (${t('optional')})`}`"
|
||||||
type="text"
|
type="text"
|
||||||
:name="`values[${currentField.uuid}]`"
|
:name="`values[${currentField.uuid}]`"
|
||||||
@focus="$refs.areas.scrollIntoField(currentField)"
|
@focus="$refs.areas.scrollIntoField(currentField)"
|
||||||
@@ -87,7 +88,7 @@
|
|||||||
:for="currentField.uuid"
|
:for="currentField.uuid"
|
||||||
class="label text-2xl mb-2"
|
class="label text-2xl mb-2"
|
||||||
>{{ currentField.name }}
|
>{{ currentField.name }}
|
||||||
<template v-if="!currentField.required">(optional)</template>
|
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
@@ -111,7 +112,7 @@
|
|||||||
:for="currentField.uuid"
|
:for="currentField.uuid"
|
||||||
class="label text-2xl mb-2"
|
class="label text-2xl mb-2"
|
||||||
>{{ currentField.name }}
|
>{{ currentField.name }}
|
||||||
<template v-if="!currentField.required">(optional)</template>
|
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
@@ -129,7 +130,7 @@
|
|||||||
value=""
|
value=""
|
||||||
:selected="!values[currentField.uuid]"
|
:selected="!values[currentField.uuid]"
|
||||||
>
|
>
|
||||||
Select your option
|
{{ t('select_your_option') }}
|
||||||
</option>
|
</option>
|
||||||
<option
|
<option
|
||||||
v-for="(option, index) in currentField.options"
|
v-for="(option, index) in currentField.options"
|
||||||
@@ -147,7 +148,7 @@
|
|||||||
:for="currentField.uuid"
|
:for="currentField.uuid"
|
||||||
class="label text-2xl mb-2"
|
class="label text-2xl mb-2"
|
||||||
>{{ currentField.name }}
|
>{{ currentField.name }}
|
||||||
<template v-if="!currentField.required">(optional)</template>
|
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
||||||
</label>
|
</label>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="space-y-3.5 mx-auto">
|
<div class="space-y-3.5 mx-auto">
|
||||||
@@ -195,7 +196,7 @@
|
|||||||
>
|
>
|
||||||
<template v-if="isAnonymousChecboxes">
|
<template v-if="isAnonymousChecboxes">
|
||||||
<span class="text-xl">
|
<span class="text-xl">
|
||||||
Complete hightlighted checkboxes and click <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? 'submit' : 'next' }}</span>.
|
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? t('submit') : t('next') }}</span>.
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
v-for="field in currentStepFields"
|
v-for="field in currentStepFields"
|
||||||
@@ -264,6 +265,16 @@
|
|||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
@attached="[attachments.push($event), $refs.areas.scrollIntoField(currentField)]"
|
@attached="[attachments.push($event), $refs.areas.scrollIntoField(currentField)]"
|
||||||
/>
|
/>
|
||||||
|
<PhoneStep
|
||||||
|
v-else-if="currentField.type === 'phone'"
|
||||||
|
ref="currentStep"
|
||||||
|
v-model="values[currentField.uuid]"
|
||||||
|
:field="currentField"
|
||||||
|
:default-value="submitter.phone"
|
||||||
|
:submitter-slug="submitterSlug"
|
||||||
|
@focus="$refs.areas.scrollIntoField(currentField)"
|
||||||
|
@submit="submitStep"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-6 md:mt-8">
|
<div class="mt-6 md:mt-8">
|
||||||
<button
|
<button
|
||||||
@@ -277,10 +288,10 @@
|
|||||||
class="mr-1 animate-spin"
|
class="mr-1 animate-spin"
|
||||||
/>
|
/>
|
||||||
<span v-if="stepFields.length === currentStep + 1">
|
<span v-if="stepFields.length === currentStep + 1">
|
||||||
Submit
|
{{ t('submit') }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
Next
|
{{ t('next') }}
|
||||||
</span><span
|
</span><span
|
||||||
v-if="isSubmitting"
|
v-if="isSubmitting"
|
||||||
class="w-6 flex justify-start mr-1"
|
class="w-6 flex justify-start mr-1"
|
||||||
@@ -292,8 +303,9 @@
|
|||||||
<FormCompleted
|
<FormCompleted
|
||||||
v-else
|
v-else
|
||||||
:is-demo="isDemo"
|
:is-demo="isDemo"
|
||||||
|
:attribution="attribution"
|
||||||
:with-confetti="withConfetti"
|
:with-confetti="withConfetti"
|
||||||
:can-send-email="canSendEmail"
|
:can-send-email="canSendEmail && !!submitter.email"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
/>
|
/>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
@@ -318,8 +330,10 @@ import ImageStep from './image_step'
|
|||||||
import SignatureStep from './signature_step'
|
import SignatureStep from './signature_step'
|
||||||
import AttachmentStep from './attachment_step'
|
import AttachmentStep from './attachment_step'
|
||||||
import MultiSelectStep from './multi_select_step'
|
import MultiSelectStep from './multi_select_step'
|
||||||
|
import PhoneStep from './phone_step'
|
||||||
import FormCompleted from './completed'
|
import FormCompleted from './completed'
|
||||||
import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||||
|
import { t } from './i18n'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SubmissionForm',
|
name: 'SubmissionForm',
|
||||||
@@ -331,17 +345,19 @@ export default {
|
|||||||
MultiSelectStep,
|
MultiSelectStep,
|
||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
IconArrowsDiagonal,
|
IconArrowsDiagonal,
|
||||||
|
PhoneStep,
|
||||||
IconArrowsDiagonalMinimize2,
|
IconArrowsDiagonalMinimize2,
|
||||||
FormCompleted
|
FormCompleted
|
||||||
},
|
},
|
||||||
provide () {
|
provide () {
|
||||||
return {
|
return {
|
||||||
baseUrl: this.baseUrl
|
baseUrl: this.baseUrl,
|
||||||
|
t: this.t
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
submitterSlug: {
|
submitter: {
|
||||||
type: String,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
canSendEmail: {
|
canSendEmail: {
|
||||||
@@ -349,10 +365,6 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
submitterUuid: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
attachments: {
|
attachments: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -378,6 +390,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
backgroundColor: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
isDirectUpload: {
|
isDirectUpload: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -388,6 +405,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
attribution: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
values: {
|
values: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -399,20 +421,28 @@ export default {
|
|||||||
isCompleted: false,
|
isCompleted: false,
|
||||||
isFormVisible: true,
|
isFormVisible: true,
|
||||||
currentStep: 0,
|
currentStep: 0,
|
||||||
isSubmitting: false
|
isSubmitting: false,
|
||||||
|
recalculateButtonDisabledKey: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
currentStepFields () {
|
currentStepFields () {
|
||||||
return this.stepFields[this.currentStep]
|
return this.stepFields[this.currentStep]
|
||||||
},
|
},
|
||||||
|
submitterSlug () {
|
||||||
|
return this.submitter.slug
|
||||||
|
},
|
||||||
isAnonymousChecboxes () {
|
isAnonymousChecboxes () {
|
||||||
return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name) && this.currentStepFields.length > 4
|
return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name) && this.currentStepFields.length > 4
|
||||||
},
|
},
|
||||||
isButtonDisabled () {
|
isButtonDisabled () {
|
||||||
return this.isSubmitting ||
|
if (this.recalculateButtonDisabledKey) {
|
||||||
|
return this.isSubmitting ||
|
||||||
(this.currentField.required && ['image', 'file'].includes(this.currentField.type) && !this.values[this.currentField.uuid]?.length) ||
|
(this.currentField.required && ['image', 'file'].includes(this.currentField.type) && !this.values[this.currentField.uuid]?.length) ||
|
||||||
(this.currentField.required && this.currentField.type === 'signature' && !this.values[this.currentField.uuid]?.length && this.$refs.currentStep && !this.$refs.currentStep.isSignatureStarted)
|
(this.currentField.required && this.currentField.type === 'signature' && !this.values[this.currentField.uuid]?.length && this.$refs.currentStep && !this.$refs.currentStep.isSignatureStarted)
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
currentField () {
|
currentField () {
|
||||||
return this.currentStepFields[0]
|
return this.currentStepFields[0]
|
||||||
@@ -460,12 +490,58 @@ export default {
|
|||||||
scrollbox.parentNode.style.maxHeight = '-webkit-fill-available'
|
scrollbox.parentNode.style.maxHeight = '-webkit-fill-available'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.recalculateButtonDisabledKey = Math.random()
|
||||||
|
|
||||||
|
this.maybeTrackEmailClick().finally(() => {
|
||||||
|
this.trackViewForm()
|
||||||
|
})
|
||||||
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
t,
|
||||||
|
maybeTrackEmailClick () {
|
||||||
|
const queryParams = new URLSearchParams(window.location.search)
|
||||||
|
|
||||||
|
if (queryParams.has('t')) {
|
||||||
|
const t = queryParams.get('t')
|
||||||
|
|
||||||
|
queryParams.delete('t')
|
||||||
|
const newUrl = [window.location.pathname, queryParams.toString()].filter(Boolean).join('?')
|
||||||
|
window.history.replaceState({}, document.title, newUrl)
|
||||||
|
|
||||||
|
return fetch(this.baseUrl + '/api/submitter_email_clicks', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
t,
|
||||||
|
submitter_slug: this.submitterSlug
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return Promise.resolve({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trackViewForm () {
|
||||||
|
fetch(this.baseUrl + '/api/submitter_form_views', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
submitter_slug: this.submitterSlug
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
goToStep (step, scrollToArea = false, clickUpload = false) {
|
goToStep (step, scrollToArea = false, clickUpload = false) {
|
||||||
this.currentStep = this.stepFields.indexOf(step)
|
this.currentStep = this.stepFields.indexOf(step)
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
this.recalculateButtonDisabledKey = Math.random()
|
||||||
|
|
||||||
if (scrollToArea) {
|
if (scrollToArea) {
|
||||||
this.$refs.areas.scrollIntoField(step[0])
|
this.$refs.areas.scrollIntoField(step[0])
|
||||||
}
|
}
|
||||||
@@ -490,28 +566,40 @@ export default {
|
|||||||
async submitStep () {
|
async submitStep () {
|
||||||
this.isSubmitting = true
|
this.isSubmitting = true
|
||||||
|
|
||||||
const stepPromise = this.currentField.type === 'signature'
|
const stepPromise = ['signature', 'phone'].includes(this.currentField.type)
|
||||||
? this.$refs.currentStep.submit
|
? this.$refs.currentStep.submit
|
||||||
: () => Promise.resolve({})
|
: () => Promise.resolve({})
|
||||||
|
|
||||||
await stepPromise()
|
stepPromise().then(async () => {
|
||||||
|
const formData = new FormData(this.$refs.form)
|
||||||
|
|
||||||
const formData = new FormData(this.$refs.form)
|
if (this.currentStep === this.stepFields.length - 1) {
|
||||||
|
formData.append('completed', 'true')
|
||||||
if (this.currentStep === this.stepFields.length - 1) {
|
|
||||||
formData.append('completed', 'true')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.saveStep(formData).then(response => {
|
|
||||||
const nextStep = this.stepFields[this.currentStep + 1]
|
|
||||||
|
|
||||||
if (nextStep) {
|
|
||||||
this.goToStep(this.stepFields[this.currentStep + 1], true)
|
|
||||||
} else {
|
|
||||||
this.isCompleted = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.saveStep(formData).then(async (response) => {
|
||||||
|
if (response.status === 422) {
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
alert(data.error || 'Value is invalid')
|
||||||
|
|
||||||
|
return Promise.reject(new Error(data.error))
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextStep = this.stepFields[this.currentStep + 1]
|
||||||
|
|
||||||
|
if (nextStep) {
|
||||||
|
this.goToStep(this.stepFields[this.currentStep + 1], true)
|
||||||
|
} else {
|
||||||
|
this.isCompleted = true
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Error submitting form:', error)
|
||||||
|
}).finally(() => {
|
||||||
|
this.isSubmitting = false
|
||||||
|
})
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error('Error submitting form:', error)
|
console.log(error)
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.isSubmitting = false
|
this.isSubmitting = false
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,242 @@
|
|||||||
|
const en = {
|
||||||
|
submit_form: 'Submit Form',
|
||||||
|
type_here: 'Type here',
|
||||||
|
optional: 'optional',
|
||||||
|
select_your_option: 'Select your option',
|
||||||
|
complete_hightlighted_checkboxes_and_click: 'Complete hightlighted checkboxes and click',
|
||||||
|
submit: 'submit',
|
||||||
|
next: 'next',
|
||||||
|
click_to_upload: 'Click to upload',
|
||||||
|
or_drag_and_drop_files: 'or drag and drop files',
|
||||||
|
send_copy_via_email: 'Send copy via email',
|
||||||
|
download: 'Download',
|
||||||
|
form_has_been_completed: 'Form has been completed!',
|
||||||
|
create_a_free_account: 'Create a Free Account',
|
||||||
|
signed_with: 'Signed with',
|
||||||
|
open_source_documents_software: 'open source documents software',
|
||||||
|
verified_phone_number: 'Verify Phone Number',
|
||||||
|
use_international_format: 'Use internatioanl format: +1xxx',
|
||||||
|
six_digits_code: '6-digit code',
|
||||||
|
change_phone_number: 'Change phone number',
|
||||||
|
sending: 'Sending...',
|
||||||
|
resend_code: 'Re-send code',
|
||||||
|
verification_code_has_been_resent: 'Verification code has been re-sent via SMS',
|
||||||
|
email_has_been_sent: 'Email has been sent'
|
||||||
|
}
|
||||||
|
|
||||||
|
const es = {
|
||||||
|
submit_form: 'Enviar Formulario',
|
||||||
|
type_here: 'Escribe aquí',
|
||||||
|
optional: 'opcional',
|
||||||
|
select_your_option: 'Selecciona tu opción',
|
||||||
|
complete_hightlighted_checkboxes_and_click: 'Completa las casillas resaltadas y haz clic',
|
||||||
|
submit: 'enviar',
|
||||||
|
next: 'siguiente',
|
||||||
|
click_to_upload: 'Haz clic para cargar',
|
||||||
|
or_drag_and_drop_files: 'o arrastra y suelta archivos',
|
||||||
|
send_copy_via_email: 'Enviar copia por correo electrónico',
|
||||||
|
download: 'Descargar',
|
||||||
|
form_has_been_completed: '¡El formulario ha sido completado!',
|
||||||
|
create_a_free_account: 'Crear una Cuenta Gratuita',
|
||||||
|
signed_with: 'Firmado con',
|
||||||
|
open_source_documents_software: 'software de documentos de código abierto',
|
||||||
|
verified_phone_number: 'Verificar número de teléfono',
|
||||||
|
use_international_format: 'Usar formato internacional: +1xxx',
|
||||||
|
six_digits_code: 'Código de 6 dígitos',
|
||||||
|
change_phone_number: 'Cambiar número de teléfono',
|
||||||
|
sending: 'Enviando...',
|
||||||
|
resend_code: 'Reenviar código',
|
||||||
|
verification_code_has_been_resent: 'El código de verificación ha sido reenviado por SMS',
|
||||||
|
email_has_been_sent: 'El correo electrónico ha sido enviado'
|
||||||
|
}
|
||||||
|
|
||||||
|
const it = {
|
||||||
|
submit_form: 'Invia Modulo',
|
||||||
|
type_here: 'Digita qui',
|
||||||
|
optional: 'opzionale',
|
||||||
|
select_your_option: 'Seleziona la tua opzione',
|
||||||
|
complete_hightlighted_checkboxes_and_click: 'Completa le caselle evidenziate e fai clic',
|
||||||
|
submit: 'invia',
|
||||||
|
next: 'avanti',
|
||||||
|
click_to_upload: 'Clicca per caricare',
|
||||||
|
or_drag_and_drop_files: 'oppure trascina e rilascia i file',
|
||||||
|
send_copy_via_email: 'Invia copia via email',
|
||||||
|
download: 'Scarica',
|
||||||
|
form_has_been_completed: 'Il modulo è stato completato!',
|
||||||
|
create_a_free_account: 'Crea un Account Gratuito',
|
||||||
|
signed_with: 'Firmato con',
|
||||||
|
open_source_documents_software: 'software di documenti open source',
|
||||||
|
verified_phone_number: 'Verifica numero di telefono',
|
||||||
|
use_international_format: 'Usa formato internazionale: +1xxx',
|
||||||
|
six_digits_code: 'Codice a 6 cifre',
|
||||||
|
change_phone_number: 'Cambia numero di telefono',
|
||||||
|
sending: 'Invio in corso...',
|
||||||
|
resend_code: 'Rinvia codice',
|
||||||
|
verification_code_has_been_resent: 'Il codice di verifica è stato rinviato tramite SMS',
|
||||||
|
email_has_been_sent: "L'email è stata inviata"
|
||||||
|
}
|
||||||
|
|
||||||
|
const de = {
|
||||||
|
submit_form: 'Formular absenden',
|
||||||
|
type_here: 'Hier eingeben',
|
||||||
|
optional: 'optional',
|
||||||
|
select_your_option: 'Wähle deine Option',
|
||||||
|
complete_hightlighted_checkboxes_and_click: 'Markierte Kontrollkästchen ausfüllen und klicken',
|
||||||
|
submit: 'absenden',
|
||||||
|
next: 'weiter',
|
||||||
|
click_to_upload: 'Klicken zum Hochladen',
|
||||||
|
or_drag_and_drop_files: 'oder Dateien hierher ziehen und ablegen',
|
||||||
|
send_copy_via_email: 'Kopie per E-Mail senden',
|
||||||
|
download: 'Herunterladen',
|
||||||
|
form_has_been_completed: 'Formular wurde ausgefüllt!',
|
||||||
|
create_a_free_account: 'Kostenloses Konto erstellen',
|
||||||
|
signed_with: 'Unterschrieben mit',
|
||||||
|
open_source_documents_software: 'Open-Source-Dokumentensoftware',
|
||||||
|
verified_phone_number: 'Telefonnummer überprüfen',
|
||||||
|
use_international_format: 'Internationales Format verwenden: +1xxx',
|
||||||
|
six_digits_code: '6-stelliger Code',
|
||||||
|
change_phone_number: 'Telefonnummer ändern',
|
||||||
|
sending: 'Senden...',
|
||||||
|
resend_code: 'Code erneut senden',
|
||||||
|
verification_code_has_been_resent: 'Die Verifizierungscode wurde erneut per SMS gesendet',
|
||||||
|
email_has_been_sent: 'Die E-Mail wurde gesendet'
|
||||||
|
}
|
||||||
|
|
||||||
|
const fr = {
|
||||||
|
submit_form: 'Envoyer le Formulaire',
|
||||||
|
type_here: 'Tapez ici',
|
||||||
|
optional: 'facultatif',
|
||||||
|
select_your_option: 'Sélectionnez votre option',
|
||||||
|
complete_hightlighted_checkboxes_and_click: 'Complétez les cases à cocher en surbrillance et cliquez',
|
||||||
|
submit: 'envoyer',
|
||||||
|
next: 'suivant',
|
||||||
|
click_to_upload: 'Cliquez pour télécharger',
|
||||||
|
or_drag_and_drop_files: 'ou faites glisser-déposer les fichiers',
|
||||||
|
send_copy_via_email: 'Envoyer une copie par e-mail',
|
||||||
|
download: 'Télécharger',
|
||||||
|
form_has_been_completed: 'Le formulaire a été complété !',
|
||||||
|
create_a_free_account: 'Créer un Compte Gratuit',
|
||||||
|
signed_with: 'Signé avec',
|
||||||
|
open_source_documents_software: 'logiciel de documents open source',
|
||||||
|
verified_phone_number: 'Vérifier le numéro de téléphone',
|
||||||
|
use_international_format: 'Utiliser le format international : +1xxx',
|
||||||
|
six_digits_code: 'Code à 6 chiffres',
|
||||||
|
change_phone_number: 'Changer le numéro de téléphone',
|
||||||
|
sending: 'Envoi en cours...',
|
||||||
|
resend_code: 'Renvoyer le code',
|
||||||
|
verification_code_has_been_resent: 'Le code de vérification a été renvoyé par SMS',
|
||||||
|
email_has_been_sent: "L'email a été envoyé"
|
||||||
|
}
|
||||||
|
|
||||||
|
const pl = {
|
||||||
|
submit_form: 'Wyślij Formularz',
|
||||||
|
type_here: 'Wpisz tutaj',
|
||||||
|
optional: 'opcjonalny',
|
||||||
|
select_your_option: 'Wybierz swoją opcję',
|
||||||
|
complete_hightlighted_checkboxes_and_click: 'Wypełnij zaznaczone pola wyboru i kliknij',
|
||||||
|
submit: 'wyślij',
|
||||||
|
next: 'dalej',
|
||||||
|
click_to_upload: 'Kliknij, aby przesłać',
|
||||||
|
or_drag_and_drop_files: 'lub przeciągnij i upuść pliki',
|
||||||
|
send_copy_via_email: 'Wyślij kopię drogą mailową',
|
||||||
|
download: 'Pobierz',
|
||||||
|
form_has_been_completed: 'Formularz został wypełniony!',
|
||||||
|
create_a_free_account: 'Utwórz darmowe konto',
|
||||||
|
signed_with: 'Podpisane za pomocą',
|
||||||
|
open_source_documents_software: 'oprogramowanie do dokumentów open source',
|
||||||
|
verified_phone_number: 'Zweryfikuj numer telefonu',
|
||||||
|
use_international_format: 'Użyj międzynarodowego formatu: +1xxx',
|
||||||
|
six_digits_code: '6-cyfrowy kod',
|
||||||
|
change_phone_number: 'Zmień numer telefonu',
|
||||||
|
sending: 'Wysyłanie...',
|
||||||
|
resend_code: 'Ponownie wyślij kod',
|
||||||
|
verification_code_has_been_resent: 'Kod weryfikacyjny został ponownie wysłany',
|
||||||
|
email_has_been_sent: 'E-mail został wysłany'
|
||||||
|
}
|
||||||
|
|
||||||
|
const uk = {
|
||||||
|
submit_form: 'Надіслати Форму',
|
||||||
|
type_here: 'Введіть тут',
|
||||||
|
optional: 'необов’язково',
|
||||||
|
select_your_option: 'Виберіть варіант',
|
||||||
|
complete_hightlighted_checkboxes_and_click: 'Заповніть позначені прапорці та натисніть',
|
||||||
|
submit: 'надіслати',
|
||||||
|
next: 'далі',
|
||||||
|
click_to_upload: 'Клацніть, щоб завантажити',
|
||||||
|
or_drag_and_drop_files: 'або перетягніть файли сюди',
|
||||||
|
send_copy_via_email: 'Надіслати копію електронною поштою',
|
||||||
|
download: 'Завантажити',
|
||||||
|
form_has_been_completed: 'Форму заповнено!',
|
||||||
|
create_a_free_account: 'Створити безкоштовний обліковий запис',
|
||||||
|
signed_with: 'Підписано за допомогою',
|
||||||
|
open_source_documents_software: 'відкритий програмний засіб для документів',
|
||||||
|
verified_phone_number: 'Підтвердіть номер телефону',
|
||||||
|
use_international_format: 'Використовуйте міжнародний формат: +1xxx',
|
||||||
|
six_digits_code: '6-значний код',
|
||||||
|
change_phone_number: 'Змінити номер телефону',
|
||||||
|
sending: 'Надсилаю...',
|
||||||
|
resend_code: 'Повторно відправити код',
|
||||||
|
verification_code_has_been_resent: 'Код підтвердження був повторно надісланий',
|
||||||
|
email_has_been_sent: 'Електронний лист був відправлений'
|
||||||
|
}
|
||||||
|
|
||||||
|
const cs = {
|
||||||
|
submit_form: 'Odeslat formulář',
|
||||||
|
type_here: 'Zadejte zde',
|
||||||
|
optional: 'volitelné',
|
||||||
|
select_your_option: 'Vyberte svou volbu',
|
||||||
|
complete_hightlighted_checkboxes_and_click: 'Označte zvýrazněné zaškrtávací políčka a klikněte na',
|
||||||
|
submit: 'odeslat',
|
||||||
|
next: 'další',
|
||||||
|
click_to_upload: 'Klikněte pro nahrání',
|
||||||
|
or_drag_and_drop_files: 'nebo přetáhněte soubory sem',
|
||||||
|
send_copy_via_email: 'Odeslat kopii e-mailem',
|
||||||
|
download: 'Stáhnout',
|
||||||
|
form_has_been_completed: 'Formulář byl dokončen!',
|
||||||
|
create_a_free_account: 'Vytvořit bezplatný účet',
|
||||||
|
signed_with: 'Podepsáno pomocí',
|
||||||
|
open_source_documents_software: 'open source software pro dokumenty',
|
||||||
|
verified_phone_number: 'Ověřte telefonní číslo',
|
||||||
|
use_international_format: 'Použijte mezinárodní formát: +1xxx',
|
||||||
|
six_digits_code: '6-místný kód',
|
||||||
|
change_phone_number: 'Změnit telefonní číslo',
|
||||||
|
sending: 'Odesílání...',
|
||||||
|
resend_code: 'Znovu odeslat kód',
|
||||||
|
verification_code_has_been_resent: 'Ověřovací kód byl znovu odeslán',
|
||||||
|
email_has_been_sent: 'E-mail byl odeslán'
|
||||||
|
}
|
||||||
|
|
||||||
|
const pt = {
|
||||||
|
submit_form: 'Enviar Formulário',
|
||||||
|
type_here: 'Digite aqui',
|
||||||
|
optional: 'opcional',
|
||||||
|
select_your_option: 'Selecione sua opção',
|
||||||
|
complete_hightlighted_checkboxes_and_click: 'Complete as caixas de seleção destacadas e clique',
|
||||||
|
submit: 'enviar',
|
||||||
|
next: 'próximo',
|
||||||
|
click_to_upload: 'Clique para fazer o upload',
|
||||||
|
or_drag_and_drop_files: 'ou arraste e solte arquivos',
|
||||||
|
send_copy_via_email: 'Enviar cópia por e-mail',
|
||||||
|
download: 'Baixar',
|
||||||
|
form_has_been_completed: 'O formulário foi concluído!',
|
||||||
|
create_a_free_account: 'Criar uma Conta Gratuita',
|
||||||
|
signed_with: 'Assinado com',
|
||||||
|
open_source_documents_software: 'software de documentos de código aberto',
|
||||||
|
verified_phone_number: 'Verificar Número de Telefone',
|
||||||
|
use_international_format: 'Use formato internacional: +1xxx',
|
||||||
|
six_digits_code: 'Código de 6 dígitos',
|
||||||
|
change_phone_number: 'Alterar número de telefone',
|
||||||
|
sending: 'Enviando...',
|
||||||
|
resend_code: 'Reenviar código',
|
||||||
|
verification_code_has_been_resent: 'O código de verificação foi reenviado via SMS',
|
||||||
|
email_has_been_sent: 'Email enviado'
|
||||||
|
}
|
||||||
|
|
||||||
|
const i18n = { en, es, it, de, fr, pl, uk, cs, pt }
|
||||||
|
|
||||||
|
const browserLanguage = (navigator.language || navigator.userLanguage || 'en').split('-')[0]
|
||||||
|
|
||||||
|
const t = (key) => i18n[browserLanguage]?.[key] || i18n.en[key] || key
|
||||||
|
|
||||||
|
export default i18n
|
||||||
|
export { t }
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
:for="isCodeSent ? 'one_time_code' : field.uuid"
|
||||||
|
class="label text-2xl mb-2"
|
||||||
|
>{{ field.name || t('verified_phone_number') }}
|
||||||
|
<template v-if="!field.required">({{ t('optional') }})</template>
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="normalize_phone"
|
||||||
|
value="true"
|
||||||
|
>
|
||||||
|
<div v-if="isCodeSent">
|
||||||
|
<input
|
||||||
|
id="one_time_code"
|
||||||
|
class="base-input !text-2xl w-full text-center"
|
||||||
|
name="one_time_code"
|
||||||
|
type="text"
|
||||||
|
autocomplete="one-time-code"
|
||||||
|
:placeholder="t('six_digits_code')"
|
||||||
|
required
|
||||||
|
maxlength="6"
|
||||||
|
autofocus
|
||||||
|
inputmode="decimal"
|
||||||
|
@input="onInputCode"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between mt-2 -mb-2 md:-mb-4">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="link"
|
||||||
|
@click.prevent="isCodeSent = false"
|
||||||
|
>
|
||||||
|
{{ t('change_phone_number') }}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="link"
|
||||||
|
@click.prevent="resendCode"
|
||||||
|
>
|
||||||
|
{{ isResendLoading ? t('sending') : t('resend_code') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-show="!isCodeSent"
|
||||||
|
:id="field.uuid"
|
||||||
|
ref="phone"
|
||||||
|
:value="modelValue || defaultValue"
|
||||||
|
class="base-input !text-2xl w-full"
|
||||||
|
autocomplete="tel"
|
||||||
|
pattern="^\+[0-9\s\-]+$"
|
||||||
|
:oninvalid="`this.value ? this.setCustomValidity('${t('use_international_format')}...') : ''`"
|
||||||
|
oninput="this.setCustomValidity('')"
|
||||||
|
type="tel"
|
||||||
|
inputmode="tel"
|
||||||
|
:required="field.required"
|
||||||
|
placeholder="+1 234 567-8900"
|
||||||
|
:name="`values[${field.uuid}]`"
|
||||||
|
@input="$emit('update:model-value', $event.target.value)"
|
||||||
|
@focus="$emit('focus')"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function throttle (func, delay) {
|
||||||
|
let lastCallTime = 0
|
||||||
|
|
||||||
|
return function (...args) {
|
||||||
|
const now = Date.now()
|
||||||
|
|
||||||
|
if (now - lastCallTime >= delay) {
|
||||||
|
func.apply(this, args)
|
||||||
|
lastCallTime = now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PhoneStep',
|
||||||
|
inject: ['t', 'baseUrl'],
|
||||||
|
props: {
|
||||||
|
field: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
submitterSlug: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
defaultValue: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['update:model-value', 'focus', 'submit'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isCodeSent: false,
|
||||||
|
isResendLoading: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
emitSubmit: throttle(function (e) {
|
||||||
|
this.$emit('submit')
|
||||||
|
}, 1000),
|
||||||
|
onInputCode (e) {
|
||||||
|
if (e.target.value.length === 6) {
|
||||||
|
this.emitSubmit()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resendCode () {
|
||||||
|
this.isResendLoading = true
|
||||||
|
|
||||||
|
this.sendVerificationCode().finally(() => {
|
||||||
|
alert(this.t('verification_code_has_been_resent'))
|
||||||
|
|
||||||
|
this.isResendLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
sendVerificationCode () {
|
||||||
|
return fetch(this.baseUrl + '/api/send_phone_verification_code', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
submitter_slug: this.submitterSlug,
|
||||||
|
phone: this.$refs.phone.value
|
||||||
|
}),
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async submit () {
|
||||||
|
if (!this.isCodeSent) {
|
||||||
|
this.sendVerificationCode()
|
||||||
|
|
||||||
|
this.$emit('update:model-value', this.$refs.phone.value)
|
||||||
|
|
||||||
|
this.isCodeSent = true
|
||||||
|
|
||||||
|
return Promise.reject(new Error('verify with code'))
|
||||||
|
} else {
|
||||||
|
return Promise.resolve({})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -75,6 +75,7 @@
|
|||||||
:with-arrows="template.schema.length > 1"
|
:with-arrows="template.schema.length > 1"
|
||||||
:item="item"
|
:item="item"
|
||||||
:document="sortedDocuments[index]"
|
:document="sortedDocuments[index]"
|
||||||
|
:accept-file-types="acceptFileTypes"
|
||||||
:template="template"
|
:template="template"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
@scroll-to="scrollIntoDocument(item)"
|
@scroll-to="scrollIntoDocument(item)"
|
||||||
@@ -84,9 +85,13 @@
|
|||||||
@down="moveDocument(item, 1)"
|
@down="moveDocument(item, 1)"
|
||||||
@change="save"
|
@change="save"
|
||||||
/>
|
/>
|
||||||
<div class="sticky bottom-0 bg-base-100 py-2">
|
<div
|
||||||
|
class="sticky bottom-0 py-2"
|
||||||
|
:class="{ 'bg-base-100': withStickySubmitters }"
|
||||||
|
>
|
||||||
<Upload
|
<Upload
|
||||||
v-if="sortedDocuments.length"
|
v-if="sortedDocuments.length"
|
||||||
|
:accept-file-types="acceptFileTypes"
|
||||||
:template-id="template.id"
|
:template-id="template.id"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
@success="updateFromUpload"
|
@success="updateFromUpload"
|
||||||
@@ -101,6 +106,7 @@
|
|||||||
<Dropzone
|
<Dropzone
|
||||||
v-if="!sortedDocuments.length"
|
v-if="!sortedDocuments.length"
|
||||||
:template-id="template.id"
|
:template-id="template.id"
|
||||||
|
:accept-file-types="acceptFileTypes"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
@success="updateFromUpload"
|
@success="updateFromUpload"
|
||||||
/>
|
/>
|
||||||
@@ -233,6 +239,8 @@ export default {
|
|||||||
template: this.template,
|
template: this.template,
|
||||||
save: this.save,
|
save: this.save,
|
||||||
baseFetch: this.baseFetch,
|
baseFetch: this.baseFetch,
|
||||||
|
backgroundColor: this.backgroundColor,
|
||||||
|
withPhone: this.withPhone,
|
||||||
selectedAreaRef: computed(() => this.selectedAreaRef)
|
selectedAreaRef: computed(() => this.selectedAreaRef)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -246,6 +254,16 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
backgroundColor: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
acceptFileTypes: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'image/*, application/pdf'
|
||||||
|
},
|
||||||
baseUrl: {
|
baseUrl: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -261,6 +279,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
withPhone: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -327,7 +350,7 @@ export default {
|
|||||||
onWindowResize (e) {
|
onWindowResize (e) {
|
||||||
const breakpointLg = 1024
|
const breakpointLg = 1024
|
||||||
|
|
||||||
this.isBreakpointLg = this.$el.getRootNode().querySelector('div').offsetWidth < breakpointLg
|
this.isBreakpointLg = this.$el.getRootNode().querySelector('div[data-v-app]').offsetWidth < breakpointLg
|
||||||
},
|
},
|
||||||
setDocumentRefs (el) {
|
setDocumentRefs (el) {
|
||||||
if (el) {
|
if (el) {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
ref="input"
|
ref="input"
|
||||||
type="file"
|
type="file"
|
||||||
name="files[]"
|
name="files[]"
|
||||||
accept="image/*, application/pdf"
|
:accept="acceptFileTypes"
|
||||||
multiple
|
multiple
|
||||||
@change="upload"
|
@change="upload"
|
||||||
>
|
>
|
||||||
@@ -67,6 +67,11 @@ export default {
|
|||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
acceptFileTypes: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'image/*, application/pdf'
|
||||||
|
},
|
||||||
isDirectUpload: {
|
isDirectUpload: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
@@ -89,8 +94,10 @@ export default {
|
|||||||
return 'Uploading...'
|
return 'Uploading...'
|
||||||
} else if (this.isProcessing) {
|
} else if (this.isProcessing) {
|
||||||
return 'Processing...'
|
return 'Processing...'
|
||||||
} else {
|
} else if (this.acceptFileTypes === 'image/*, application/pdf') {
|
||||||
return 'Add PDF documents or images'
|
return 'Add PDF documents or images'
|
||||||
|
} else {
|
||||||
|
return 'Add documents or images'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -102,7 +109,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
upload: Upload.methods.upload,
|
upload: Upload.methods.upload,
|
||||||
onDropFiles (e) {
|
onDropFiles (e) {
|
||||||
if ([...e.dataTransfer.files].every((f) => f.type.match(/(?:image\/)|(?:application\/pdf)/))) {
|
if (this.acceptFileTypes !== 'image/*, application/pdf' || [...e.dataTransfer.files].every((f) => f.type.match(/(?:image\/)|(?:application\/pdf)/))) {
|
||||||
this.$refs.input.files = e.dataTransfer.files
|
this.$refs.input.files = e.dataTransfer.files
|
||||||
|
|
||||||
this.upload()
|
this.upload()
|
||||||
|
|||||||
@@ -29,8 +29,11 @@
|
|||||||
@update:model-value="$emit('name-change', selectedSubmitter)"
|
@update:model-value="$emit('name-change', selectedSubmitter)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span>
|
<span class="flex items-center">
|
||||||
▼
|
<IconPlus
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<ul
|
<ul
|
||||||
@@ -86,7 +89,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconUserPlus, IconTrashX } from '@tabler/icons-vue'
|
import { IconUserPlus, IconTrashX, IconPlus } from '@tabler/icons-vue'
|
||||||
import Contenteditable from './contenteditable'
|
import Contenteditable from './contenteditable'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
|
|
||||||
@@ -95,6 +98,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
IconUserPlus,
|
IconUserPlus,
|
||||||
Contenteditable,
|
Contenteditable,
|
||||||
|
IconPlus,
|
||||||
IconTrashX
|
IconTrashX
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -18,33 +18,36 @@
|
|||||||
:class="menuClasses"
|
:class="menuClasses"
|
||||||
@click="closeDropdown"
|
@click="closeDropdown"
|
||||||
>
|
>
|
||||||
<li
|
<template
|
||||||
v-for="(icon, type) in fieldIcons"
|
v-for="(icon, type) in fieldIcons"
|
||||||
:key="type"
|
:key="type"
|
||||||
>
|
>
|
||||||
<a
|
<li v-if="withPhone || type !== 'phone'">
|
||||||
href="#"
|
<a
|
||||||
class="text-sm py-1 px-2"
|
href="#"
|
||||||
:class="{ 'active': type === modelValue }"
|
class="text-sm py-1 px-2"
|
||||||
@click.prevent="$emit('update:model-value', type)"
|
:class="{ 'active': type === modelValue }"
|
||||||
>
|
@click.prevent="$emit('update:model-value', type)"
|
||||||
<component
|
>
|
||||||
:is="icon"
|
<component
|
||||||
:stroke-width="1.6"
|
:is="icon"
|
||||||
:width="20"
|
:stroke-width="1.6"
|
||||||
/>
|
:width="20"
|
||||||
{{ fieldNames[type] }}
|
/>
|
||||||
</a>
|
{{ fieldNames[type] }}
|
||||||
</li>
|
</a>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3 } from '@tabler/icons-vue'
|
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FiledTypeDropdown',
|
name: 'FiledTypeDropdown',
|
||||||
|
inject: ['withPhone'],
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -79,7 +82,8 @@ export default {
|
|||||||
checkbox: 'Checkbox',
|
checkbox: 'Checkbox',
|
||||||
multiple: 'Multiple',
|
multiple: 'Multiple',
|
||||||
radio: 'Radio',
|
radio: 'Radio',
|
||||||
cells: 'Cells'
|
cells: 'Cells',
|
||||||
|
phone: 'Phone'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fieldIcons () {
|
fieldIcons () {
|
||||||
@@ -93,7 +97,8 @@ export default {
|
|||||||
checkbox: IconCheckbox,
|
checkbox: IconCheckbox,
|
||||||
cells: IconColumns3,
|
cells: IconColumns3,
|
||||||
multiple: IconChecks,
|
multiple: IconChecks,
|
||||||
radio: IconCircleDot
|
radio: IconCircleDot,
|
||||||
|
phone: IconPhoneCheck
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -25,48 +25,79 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-3 gap-1 pb-2">
|
<div class="grid grid-cols-3 gap-1 pb-2">
|
||||||
<button
|
<template
|
||||||
v-for="(icon, type) in fieldIcons"
|
v-for="(icon, type) in fieldIcons"
|
||||||
:key="type"
|
:key="type"
|
||||||
draggable="true"
|
|
||||||
class="flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
|
|
||||||
@dragstart="onDragstart(type)"
|
|
||||||
@dragend="$emit('drag-end')"
|
|
||||||
@click="addField(type)"
|
|
||||||
>
|
>
|
||||||
<div class="w-0 absolute left-0">
|
<button
|
||||||
<svg
|
v-if="withPhone || type != 'phone'"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
draggable="true"
|
||||||
class="cursor-grab"
|
class="flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
|
||||||
width="18"
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
height="18"
|
@dragstart="onDragstart(type)"
|
||||||
viewBox="0 0 24 24"
|
@dragend="$emit('drag-end')"
|
||||||
stroke-width="1.5"
|
@click="addField(type)"
|
||||||
stroke="currentColor"
|
>
|
||||||
fill="none"
|
<div class="w-0 absolute left-0">
|
||||||
stroke-linecap="round"
|
<svg
|
||||||
stroke-linejoin="round"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
class="cursor-grab"
|
||||||
<path
|
width="18"
|
||||||
stroke="none"
|
height="18"
|
||||||
d="M0 0h24v24H0z"
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
fill="none"
|
fill="none"
|
||||||
/>
|
stroke-linecap="round"
|
||||||
<path d="M9 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
stroke-linejoin="round"
|
||||||
<path d="M9 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
>
|
||||||
<path d="M9 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
<path
|
||||||
<path d="M15 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
stroke="none"
|
||||||
<path d="M15 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
d="M0 0h24v24H0z"
|
||||||
<path d="M15 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
fill="none"
|
||||||
</svg>
|
/>
|
||||||
|
<path d="M9 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||||
|
<path d="M9 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||||
|
<path d="M9 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||||
|
<path d="M15 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||||
|
<path d="M15 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||||
|
<path d="M15 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center flex-col px-2 py-2">
|
||||||
|
<component :is="icon" />
|
||||||
|
<span class="text-xs mt-1">
|
||||||
|
{{ fieldNames[type] }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="tooltip flex"
|
||||||
|
data-tip="Unlock SMS-verified phone number field with paid plan. Use text field for phone numbers without verification."
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="https://www.docuseal.co/pricing"
|
||||||
|
target="_blank"
|
||||||
|
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
|
||||||
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
|
>
|
||||||
|
<div class="w-0 absolute left-0">
|
||||||
|
<IconLock
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
stroke-width="1.5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center flex-col px-2 py-2">
|
||||||
|
<component :is="icon" />
|
||||||
|
<span class="text-xs mt-1">
|
||||||
|
{{ fieldNames[type] }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center flex-col px-2 py-2">
|
</template>
|
||||||
<component :is="icon" />
|
|
||||||
<span class="text-xs mt-1">
|
|
||||||
{{ fieldNames[type] }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="fields.length < 4"
|
v-if="fields.length < 4"
|
||||||
@@ -94,14 +125,16 @@ import Field from './field'
|
|||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
import FieldType from './field_type'
|
import FieldType from './field_type'
|
||||||
import FieldSubmitter from './field_submitter'
|
import FieldSubmitter from './field_submitter'
|
||||||
|
import { IconLock } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TemplateFields',
|
name: 'TemplateFields',
|
||||||
components: {
|
components: {
|
||||||
Field,
|
Field,
|
||||||
FieldSubmitter
|
FieldSubmitter,
|
||||||
|
IconLock
|
||||||
},
|
},
|
||||||
inject: ['save'],
|
inject: ['save', 'backgroundColor', 'withPhone'],
|
||||||
props: {
|
props: {
|
||||||
fields: {
|
fields: {
|
||||||
type: Array,
|
type: Array,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
<ReplaceButton
|
<ReplaceButton
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
:template-id="template.id"
|
:template-id="template.id"
|
||||||
|
:accept-file-types="acceptFileTypes"
|
||||||
class="opacity-0 group-hover:opacity-100"
|
class="opacity-0 group-hover:opacity-100"
|
||||||
@click.stop
|
@click.stop
|
||||||
@success="$emit('replace', { replaceSchemaItem: item, ...$event })"
|
@success="$emit('replace', { replaceSchemaItem: item, ...$event })"
|
||||||
@@ -94,6 +95,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
acceptFileTypes: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'image/*, application/pdf'
|
||||||
|
},
|
||||||
isDirectUpload: {
|
isDirectUpload: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
ref="input"
|
ref="input"
|
||||||
name="files[]"
|
name="files[]"
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*, application/pdf"
|
:accept="acceptFileTypes"
|
||||||
@change="upload"
|
@change="upload"
|
||||||
>
|
>
|
||||||
</form>
|
</form>
|
||||||
@@ -32,6 +32,11 @@ export default {
|
|||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
acceptFileTypes: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'image/*, application/pdf'
|
||||||
|
},
|
||||||
isDirectUpload: {
|
isDirectUpload: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
ref="input"
|
ref="input"
|
||||||
name="files[]"
|
name="files[]"
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*, application/pdf"
|
:accept="acceptFileTypes"
|
||||||
multiple
|
multiple
|
||||||
@change="upload"
|
@change="upload"
|
||||||
>
|
>
|
||||||
@@ -56,6 +56,11 @@ export default {
|
|||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
acceptFileTypes: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'image/*, application/pdf'
|
||||||
|
},
|
||||||
isDirectUpload: {
|
isDirectUpload: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class GenerateSubmitterResultAttachmentsJob < ApplicationJob
|
|
||||||
def perform(submitter)
|
|
||||||
Submissions::EnsureResultGenerated.call(submitter)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ProcessSubmitterCompletionJob < ApplicationJob
|
||||||
|
def perform(submitter)
|
||||||
|
is_all_completed = !submitter.submission.submitters.exists?(completed_at: nil)
|
||||||
|
|
||||||
|
if !is_all_completed && submitter.submission.submitters_order_preserved?
|
||||||
|
enqueue_next_submitter_request_notification(submitter)
|
||||||
|
end
|
||||||
|
|
||||||
|
Submissions::EnsureResultGenerated.call(submitter)
|
||||||
|
|
||||||
|
if submitter.account.encrypted_configs.exists?(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
||||||
|
SendFormCompletedWebhookRequestJob.perform_later(submitter)
|
||||||
|
end
|
||||||
|
|
||||||
|
return unless is_all_completed
|
||||||
|
return if submitter.completed_at != submitter.submission.submitters.maximum(:completed_at)
|
||||||
|
|
||||||
|
Submissions::GenerateAuditTrail.call(submitter.submission)
|
||||||
|
|
||||||
|
enqueue_emails(submitter)
|
||||||
|
end
|
||||||
|
|
||||||
|
def enqueue_emails(submitter)
|
||||||
|
user = submitter.submission.created_by_user || submitter.template.author
|
||||||
|
|
||||||
|
if submitter.template.account.users.exists?(id: user.id)
|
||||||
|
bcc = submitter.submission.template.account.account_configs
|
||||||
|
.find_by(key: AccountConfig::BCC_EMAILS)&.value
|
||||||
|
|
||||||
|
SubmitterMailer.completed_email(submitter, user, bcc:).deliver_later!
|
||||||
|
end
|
||||||
|
|
||||||
|
to = submitter.submission.submitters.sort_by(&:completed_at).select(&:email?).map(&:friendly_name).join(', ')
|
||||||
|
|
||||||
|
SubmitterMailer.documents_copy_email(submitter, to:).deliver_later! if to.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def enqueue_next_submitter_request_notification(submitter)
|
||||||
|
next_submitter_item =
|
||||||
|
submitter.submission.template_submitters.find do |e|
|
||||||
|
sub = submitter.submission.submitters.find { |s| s.uuid == e['uuid'] }
|
||||||
|
|
||||||
|
sub.completed_at.blank? && sub.sent_at.blank?
|
||||||
|
end
|
||||||
|
|
||||||
|
return unless next_submitter_item
|
||||||
|
|
||||||
|
next_submitter = submitter.submission.submitters.find { |s| s.uuid == next_submitter_item['uuid'] }
|
||||||
|
|
||||||
|
Submitters.send_signature_requests([next_submitter], send_email: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class SendWebhookRequestJob < ApplicationJob
|
class SendFormCompletedWebhookRequestJob < ApplicationJob
|
||||||
USER_AGENT = 'DocuSeal.co Webhook'
|
USER_AGENT = 'DocuSeal.co Webhook'
|
||||||
|
|
||||||
def perform(submitter)
|
def perform(submitter)
|
||||||
@@ -14,7 +14,7 @@ class SendWebhookRequestJob < ApplicationJob
|
|||||||
|
|
||||||
Faraday.post(config.value,
|
Faraday.post(config.value,
|
||||||
{
|
{
|
||||||
event_type: 'form.submitted',
|
event_type: 'form.completed',
|
||||||
timestamp: Time.current.iso8601,
|
timestamp: Time.current.iso8601,
|
||||||
data: Submitters::SerializeForWebhook.call(submitter)
|
data: Submitters::SerializeForWebhook.call(submitter)
|
||||||
}.to_json,
|
}.to_json,
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SendFormStartedWebhookRequestJob < 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?
|
||||||
|
|
||||||
|
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
||||||
|
|
||||||
|
Faraday.post(config.value,
|
||||||
|
{
|
||||||
|
event_type: 'form.started',
|
||||||
|
timestamp: Time.current.iso8601,
|
||||||
|
data: Submitters::SerializeForWebhook.call(submitter)
|
||||||
|
}.to_json,
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'User-Agent' => USER_AGENT)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SendFormViewedWebhookRequestJob < 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?
|
||||||
|
|
||||||
|
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
||||||
|
|
||||||
|
Faraday.post(config.value,
|
||||||
|
{
|
||||||
|
event_type: 'form.viewed',
|
||||||
|
timestamp: Time.current.iso8601,
|
||||||
|
data: Submitters::SerializeForWebhook.call(submitter)
|
||||||
|
}.to_json,
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'User-Agent' => USER_AGENT)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SendSubmitterInvitationEmailJob < ApplicationJob
|
||||||
|
def perform(params = {})
|
||||||
|
submitter = Submitter.find(params['submitter_id'])
|
||||||
|
|
||||||
|
SubmitterMailer.invitation_email(submitter, subject: params['subject'], body: params['body']).deliver_now!
|
||||||
|
|
||||||
|
SubmissionEvent.create!(submitter:, event_type: 'send_email')
|
||||||
|
|
||||||
|
submitter.sent_at ||= Time.current
|
||||||
|
submitter.save
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,58 +1,60 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class SubmitterMailer < ApplicationMailer
|
class SubmitterMailer < ApplicationMailer
|
||||||
DEFAULT_MESSAGE = %(You have been invited to submit the "%<name>s" form:)
|
def invitation_email(submitter, body: nil, subject: nil)
|
||||||
|
|
||||||
def invitation_email(submitter, message: '')
|
|
||||||
@current_account = submitter.submission.template.account
|
@current_account = submitter.submission.template.account
|
||||||
@submitter = submitter
|
@submitter = submitter
|
||||||
@message = message.presence || format(DEFAULT_MESSAGE, name: submitter.submission.template.name)
|
@body = body.presence
|
||||||
|
|
||||||
@email_config = @current_account.account_configs.find_by(key: AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY)
|
@email_config = @current_account.account_configs.find_by(key: AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY)
|
||||||
|
|
||||||
subject =
|
subject =
|
||||||
if @email_config
|
if @email_config || subject.present?
|
||||||
ReplaceEmailVariables.call(@email_config.value['subject'], submitter:)
|
ReplaceEmailVariables.call(subject.presence || @email_config.value['subject'], submitter:)
|
||||||
else
|
else
|
||||||
'You have been invited to submit a form'
|
'You have been invited to submit a form'
|
||||||
end
|
end
|
||||||
|
|
||||||
mail(to: @submitter.email,
|
mail(to: @submitter.friendly_name,
|
||||||
from: from_address_for_submitter(submitter),
|
from: from_address_for_submitter(submitter),
|
||||||
subject:,
|
subject:,
|
||||||
reply_to: submitter.submission.created_by_user&.friendly_name)
|
reply_to: (submitter.submission.created_by_user || submitter.template.author)&.friendly_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
def completed_email(submitter, user)
|
def completed_email(submitter, user, bcc: nil)
|
||||||
@current_account = submitter.submission.template.account
|
@current_account = submitter.submission.template.account
|
||||||
@submitter = submitter
|
@submitter = submitter
|
||||||
|
@submission = submitter.submission
|
||||||
@user = user
|
@user = user
|
||||||
|
|
||||||
|
Submissions::EnsureResultGenerated.call(submitter)
|
||||||
|
|
||||||
@email_config = @current_account.account_configs.find_by(key: AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY)
|
@email_config = @current_account.account_configs.find_by(key: AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY)
|
||||||
|
|
||||||
|
add_completed_email_attachments!(submitter)
|
||||||
|
|
||||||
subject =
|
subject =
|
||||||
if @email_config
|
if @email_config
|
||||||
ReplaceEmailVariables.call(@email_config.value['subject'], submitter:)
|
ReplaceEmailVariables.call(@email_config.value['subject'], submitter:)
|
||||||
else
|
else
|
||||||
%(#{submitter.email} has completed the "#{submitter.submission.template.name}" form)
|
submitters = submitter.submission.submitters.order(:completed_at)
|
||||||
|
.map { |e| e.name || e.email || e.phone }.join(', ')
|
||||||
|
%(#{submitter.submission.template.name} has been completed by #{submitters})
|
||||||
end
|
end
|
||||||
|
|
||||||
mail(from: from_address_for_submitter(submitter),
|
mail(from: from_address_for_submitter(submitter),
|
||||||
to: user.email,
|
to: user.friendly_name,
|
||||||
|
bcc:,
|
||||||
subject:)
|
subject:)
|
||||||
end
|
end
|
||||||
|
|
||||||
def documents_copy_email(submitter)
|
def documents_copy_email(submitter, to: nil)
|
||||||
@current_account = submitter.submission.template.account
|
@current_account = submitter.submission.template.account
|
||||||
@submitter = submitter
|
@submitter = submitter
|
||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(@submitter)
|
Submissions::EnsureResultGenerated.call(@submitter)
|
||||||
|
|
||||||
@documents = Submitters.select_attachments_for_download(submitter)
|
@documents = add_completed_email_attachments!(submitter)
|
||||||
|
|
||||||
@documents.each do |attachment|
|
|
||||||
attachments[attachment.filename.to_s] = attachment.download
|
|
||||||
end
|
|
||||||
|
|
||||||
@email_config = @current_account.account_configs.find_by(key: AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
|
@email_config = @current_account.account_configs.find_by(key: AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
|
||||||
|
|
||||||
@@ -64,12 +66,26 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
end
|
end
|
||||||
|
|
||||||
mail(from: from_address_for_submitter(submitter),
|
mail(from: from_address_for_submitter(submitter),
|
||||||
to: submitter.email,
|
to: to || @submitter.friendly_name,
|
||||||
subject:)
|
subject:)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def add_completed_email_attachments!(submitter)
|
||||||
|
documents = Submitters.select_attachments_for_download(submitter)
|
||||||
|
|
||||||
|
documents.each do |attachment|
|
||||||
|
attachments[attachment.filename.to_s] = attachment.download
|
||||||
|
end
|
||||||
|
|
||||||
|
if submitter.submission.audit_trail.present?
|
||||||
|
attachments[submitter.submission.audit_trail.filename.to_s] = submitter.submission.audit_trail.download
|
||||||
|
end
|
||||||
|
|
||||||
|
documents
|
||||||
|
end
|
||||||
|
|
||||||
def from_address_for_submitter(submitter)
|
def from_address_for_submitter(submitter)
|
||||||
submitter.submission.created_by_user&.friendly_name || submitter.submission.template.author.friendly_name
|
submitter.submission.created_by_user&.friendly_name || submitter.submission.template.author.friendly_name
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ class AccountConfig < ApplicationRecord
|
|||||||
SUBMITTER_INVITATION_EMAIL_KEY = 'submitter_invitation_email'
|
SUBMITTER_INVITATION_EMAIL_KEY = 'submitter_invitation_email'
|
||||||
SUBMITTER_COMPLETED_EMAIL_KEY = 'submitter_completed_email'
|
SUBMITTER_COMPLETED_EMAIL_KEY = 'submitter_completed_email'
|
||||||
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY = 'submitter_documents_copy_email'
|
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY = 'submitter_documents_copy_email'
|
||||||
|
BCC_EMAILS = 'bcc_emails'
|
||||||
|
SUBMITTER_REMAILERS = 'submitter_reminders'
|
||||||
|
|
||||||
DEFAULT_VALUES = {
|
DEFAULT_VALUES = {
|
||||||
SUBMITTER_INVITATION_EMAIL_KEY => {
|
SUBMITTER_INVITATION_EMAIL_KEY => {
|
||||||
@@ -36,12 +38,10 @@ class AccountConfig < ApplicationRecord
|
|||||||
'{{account.name}}'
|
'{{account.name}}'
|
||||||
},
|
},
|
||||||
SUBMITTER_COMPLETED_EMAIL_KEY => {
|
SUBMITTER_COMPLETED_EMAIL_KEY => {
|
||||||
'subject' => '{{submitter.email}} has completed the "{{template.name}}" form',
|
'subject' => '{{template.name}} has been completed by {{submission.submitters}}',
|
||||||
'body' => "Hi,\n\n" \
|
'body' => "Hi,\n\n" \
|
||||||
"{{submitter.email}} has completed the \"{{template.name}}\" form:\n\n" \
|
"\"{{template.name}}\" form has been completed by {{submission.submitters}}\n\n" \
|
||||||
"{{submission.link}}\n\n" \
|
'{{submission.link}}'
|
||||||
"Thanks,\n" \
|
|
||||||
'{{account.name}}'
|
|
||||||
},
|
},
|
||||||
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => {
|
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => {
|
||||||
'subject' => 'Your copy of documents',
|
'subject' => 'Your copy of documents',
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
# deleted_at :datetime
|
# deleted_at :datetime
|
||||||
# source :text not null
|
# source :text not null
|
||||||
|
# submitters_order :string not null
|
||||||
# template_fields :text
|
# template_fields :text
|
||||||
# template_schema :text
|
# template_schema :text
|
||||||
# template_submitters :text
|
# template_submitters :text
|
||||||
@@ -31,12 +32,20 @@ class Submission < ApplicationRecord
|
|||||||
belongs_to :created_by_user, class_name: 'User', optional: true
|
belongs_to :created_by_user, class_name: 'User', optional: true
|
||||||
|
|
||||||
has_many :submitters, dependent: :destroy
|
has_many :submitters, dependent: :destroy
|
||||||
|
has_many :submission_events, dependent: :destroy
|
||||||
|
|
||||||
serialize :template_fields, JSON
|
serialize :template_fields, JSON
|
||||||
serialize :template_schema, JSON
|
serialize :template_schema, JSON
|
||||||
serialize :template_submitters, JSON
|
serialize :template_submitters, JSON
|
||||||
|
|
||||||
attribute :source, :string, default: 'link'
|
attribute :source, :string, default: 'link'
|
||||||
|
attribute :submitters_order, :string, default: 'random'
|
||||||
|
|
||||||
|
has_one_attached :audit_trail
|
||||||
|
|
||||||
|
has_many :template_schema_documents,
|
||||||
|
->(e) { where(uuid: (e.template_schema.presence || e.template.schema).pluck('attachment_uuid')) },
|
||||||
|
through: :template, source: :documents_attachments
|
||||||
|
|
||||||
scope :active, -> { where(deleted_at: nil) }
|
scope :active, -> { where(deleted_at: nil) }
|
||||||
|
|
||||||
@@ -46,4 +55,9 @@ class Submission < ApplicationRecord
|
|||||||
embed: 'embed',
|
embed: 'embed',
|
||||||
link: 'link'
|
link: 'link'
|
||||||
}, scope: false, prefix: true
|
}, scope: false, prefix: true
|
||||||
|
|
||||||
|
enum :submitters_order, {
|
||||||
|
random: 'random',
|
||||||
|
preserved: 'preserved'
|
||||||
|
}, scope: false, prefix: true
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: submission_events
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# data :text not null
|
||||||
|
# event_timestamp :datetime not null
|
||||||
|
# event_type :string not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# submission_id :bigint not null
|
||||||
|
# submitter_id :bigint
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_submission_events_on_submission_id (submission_id)
|
||||||
|
# index_submission_events_on_submitter_id (submitter_id)
|
||||||
|
#
|
||||||
|
# Foreign Keys
|
||||||
|
#
|
||||||
|
# fk_rails_... (submission_id => submissions.id)
|
||||||
|
# fk_rails_... (submitter_id => submitters.id)
|
||||||
|
#
|
||||||
|
class SubmissionEvent < ApplicationRecord
|
||||||
|
belongs_to :submission
|
||||||
|
has_one :account, through: :submission
|
||||||
|
belongs_to :submitter, optional: true
|
||||||
|
|
||||||
|
attribute :data, :string, default: -> { {} }
|
||||||
|
attribute :event_timestamp, :datetime, default: -> { Time.current }
|
||||||
|
|
||||||
|
serialize :data, JSON
|
||||||
|
|
||||||
|
before_validation :set_submission_id, on: :create
|
||||||
|
|
||||||
|
enum :event_type, {
|
||||||
|
send_email: 'send_email',
|
||||||
|
send_reminder_email: 'send_reminder_email',
|
||||||
|
send_sms: 'send_sms',
|
||||||
|
open_email: 'open_email',
|
||||||
|
click_email: 'click_email',
|
||||||
|
click_sms: 'click_sms',
|
||||||
|
start_form: 'start_form',
|
||||||
|
view_form: 'view_form',
|
||||||
|
complete_form: 'complete_form'
|
||||||
|
}, scope: false
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_submission_id
|
||||||
|
self.submission_id = submitter&.submission_id
|
||||||
|
end
|
||||||
|
end
|
||||||
+12
-1
@@ -6,9 +6,11 @@
|
|||||||
#
|
#
|
||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
# completed_at :datetime
|
# completed_at :datetime
|
||||||
# email :string not null
|
# email :string
|
||||||
# ip :string
|
# ip :string
|
||||||
|
# name :string
|
||||||
# opened_at :datetime
|
# opened_at :datetime
|
||||||
|
# phone :string
|
||||||
# sent_at :datetime
|
# sent_at :datetime
|
||||||
# slug :string not null
|
# slug :string not null
|
||||||
# ua :string
|
# ua :string
|
||||||
@@ -42,6 +44,7 @@ class Submitter < ApplicationRecord
|
|||||||
has_many_attached :attachments
|
has_many_attached :attachments
|
||||||
|
|
||||||
has_many :document_generation_events, dependent: :destroy
|
has_many :document_generation_events, dependent: :destroy
|
||||||
|
has_many :submission_events, dependent: :destroy
|
||||||
|
|
||||||
def status
|
def status
|
||||||
if completed_at?
|
if completed_at?
|
||||||
@@ -55,6 +58,14 @@ class Submitter < ApplicationRecord
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def friendly_name
|
||||||
|
if name.present? && email.present? && email.exclude?(',')
|
||||||
|
"#{name} <#{email}>"
|
||||||
|
else
|
||||||
|
email
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def status_event_at
|
def status_event_at
|
||||||
completed_at || opened_at || sent_at || created_at
|
completed_at || opened_at || sent_at || created_at
|
||||||
end
|
end
|
||||||
|
|||||||
+5
-2
@@ -5,6 +5,7 @@
|
|||||||
# Table name: users
|
# Table name: users
|
||||||
#
|
#
|
||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
|
# consumed_timestep :integer
|
||||||
# current_sign_in_at :datetime
|
# current_sign_in_at :datetime
|
||||||
# current_sign_in_ip :string
|
# current_sign_in_ip :string
|
||||||
# deleted_at :datetime
|
# deleted_at :datetime
|
||||||
@@ -16,6 +17,8 @@
|
|||||||
# last_sign_in_at :datetime
|
# last_sign_in_at :datetime
|
||||||
# last_sign_in_ip :string
|
# last_sign_in_ip :string
|
||||||
# locked_at :datetime
|
# locked_at :datetime
|
||||||
|
# otp_required_for_login :boolean default(FALSE), not null
|
||||||
|
# otp_secret :string
|
||||||
# remember_created_at :datetime
|
# remember_created_at :datetime
|
||||||
# reset_password_sent_at :datetime
|
# reset_password_sent_at :datetime
|
||||||
# reset_password_token :string
|
# reset_password_token :string
|
||||||
@@ -44,13 +47,13 @@ class User < ApplicationRecord
|
|||||||
ADMIN_ROLE = 'admin'
|
ADMIN_ROLE = 'admin'
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
EMAIL_REGEXP = /[^@,\s]+@[^@,\s]+/
|
EMAIL_REGEXP = /[^@;,<>\s]+@[^@;,<>\s]+/
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
has_one :access_token, dependent: :destroy
|
has_one :access_token, dependent: :destroy
|
||||||
has_many :templates, dependent: :destroy, foreign_key: :author_id, inverse_of: :author
|
has_many :templates, dependent: :destroy, foreign_key: :author_id, inverse_of: :author
|
||||||
|
|
||||||
devise :database_authenticatable, :recoverable, :rememberable, :validatable, :trackable
|
devise :two_factor_authenticatable, :recoverable, :rememberable, :validatable, :trackable
|
||||||
devise :registerable, :omniauthable, omniauth_providers: [:google_oauth2] if Docuseal.multitenant?
|
devise :registerable, :omniauthable, omniauth_providers: [:google_oauth2] if Docuseal.multitenant?
|
||||||
|
|
||||||
attribute :role, :string, default: ADMIN_ROLE
|
attribute :role, :string, default: ADMIN_ROLE
|
||||||
|
|||||||
@@ -29,9 +29,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="form-control pt-2">
|
<% if can?(:update, current_account) %>
|
||||||
<%= f.button button_title(title: 'Update', disabled_with: 'Updating'), class: 'base-button' %>
|
<div class="form-control pt-2">
|
||||||
</div>
|
<%= f.button button_title(title: 'Update', disabled_with: 'Updating'), class: 'base-button' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-0 md:w-52"></div>
|
<div class="w-0 md:w-52"></div>
|
||||||
|
|||||||
@@ -59,7 +59,8 @@
|
|||||||
{
|
{
|
||||||
"submitters": [
|
"submitters": [
|
||||||
{
|
{
|
||||||
"name": "<%= current_account.templates.last ? current_account.templates.last.submitters.first['name'] : 'First Submitter' %>",
|
"name": "John Doe",
|
||||||
|
"role": "<%= current_account.templates.last ? current_account.templates.last.submitters.first['name'] : 'First Submitter' %>",
|
||||||
"email": "<%= current_user.email.sub('@', '+test@') %>",
|
"email": "<%= current_user.email.sub('@', '+test@') %>",
|
||||||
"values": {
|
"values": {
|
||||||
"Form Text Field Name": "Default Value"
|
"Form Text Field Name": "Default Value"
|
||||||
|
|||||||
@@ -1,46 +1,45 @@
|
|||||||
<% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %>
|
<% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %>
|
||||||
<% if @templates.any? %>
|
<% if @pagy.count > 0 || params[:q].present? %>
|
||||||
<div class="flex justify-between mb-4 items-center">
|
<div class="flex justify-between mb-4 items-center">
|
||||||
<h1 class="text-4xl font-bold">Templates</h1>
|
<h1 class="text-4xl font-bold"><span class="hidden md:inline">Document</span> Templates</h1>
|
||||||
<%= link_to new_template_path, class: 'btn btn-primary text-base btn-md gap-2', data: { turbo_frame: :modal } do %>
|
<div class="flex space-x-2">
|
||||||
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
|
<% if params[:q].present? || @pagy.pages > 1 %>
|
||||||
<span class="hidden md:block">Create</span>
|
<%= render 'shared/search_input' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% if can?(:create, ::Template) %>
|
||||||
|
<%= render 'templates/upload_button' %>
|
||||||
|
<%= link_to new_template_path, class: 'btn btn-primary text-base btn-md gap-2', data: { turbo_frame: :modal } do %>
|
||||||
|
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
|
||||||
|
<span class="hidden md:block">Create</span>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid gap-4 md:grid-cols-3">
|
<% if @pagy.count > 0 %>
|
||||||
<% @templates.each do |template| %>
|
<div class="grid gap-4 md:grid-cols-3">
|
||||||
<div class="h-36 relative group">
|
<%= render partial: 'templates/template', collection: @templates %>
|
||||||
<a href="<%= template_path(template) %>" class="flex h-full flex-col justify-between rounded-2xl pt-6 px-7 w-full bg-base-200 peer">
|
</div>
|
||||||
<div class="pb-4 text-xl font-semibold" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;">
|
<% view_archived_html = capture do %>
|
||||||
<%= template.name %>
|
<% if current_account.templates.where.not(deleted_at: nil).exists? %>
|
||||||
</div>
|
<div>
|
||||||
<div class="pb-6 pt-1 space-y-1">
|
<a href="<%= templates_archived_index_path %>" class="link text-sm">View Archived</a>
|
||||||
<p class="flex items-center space-x-1 text-xs text-base-content/60">
|
|
||||||
<%= svg_icon('user', class: 'w-4 h-4') %>
|
|
||||||
<span><%= template.author.full_name.presence || template.author.email %></span>
|
|
||||||
</p>
|
|
||||||
<p class="flex items-center space-x-1 text-xs text-base-content/60">
|
|
||||||
<%= svg_icon('calendar', class: 'w-4 h-4') %>
|
|
||||||
<span><%= l(template.created_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %></span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<div class="absolute top-0 bottom-0 w-0 pt-7 space-y-1.5 hidden md:group-hover:block" style="right: 40px">
|
|
||||||
<a href="<%= edit_template_path(template) %>" class="btn btn-xs hover:btn-outline bg-base-200 btn-circle">
|
|
||||||
<%= svg_icon('pencil', class: 'w-4 h-4') %>
|
|
||||||
</a>
|
|
||||||
<a href="<%= new_template_path(base_template_id: template.id) %>" data-turbo-frame="modal" class="btn btn-xs hover:btn-outline bg-base-200 btn-circle">
|
|
||||||
<%= svg_icon('copy', class: 'w-4 h-4') %>
|
|
||||||
</a>
|
|
||||||
<%= button_to template_path(template), data: { turbo_confirm: 'Are you sure?' }, method: :delete, class: 'btn btn-xs hover:btn-outline bg-base-200 btn-circle' do %>
|
|
||||||
<%= svg_icon('trash', class: 'w-4 h-4 enabled') %>
|
|
||||||
<%= svg_icon('loader', class: 'w-4 h-4 animate-spin disabled') %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% if @pagy.pages > 1 %>
|
||||||
|
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', left_additional_html: view_archived_html %>
|
||||||
|
<% else %>
|
||||||
|
<div class="mt-2">
|
||||||
|
<%= view_archived_html %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
<% elsif params[:q].present? %>
|
||||||
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates' %>
|
<div class="text-center">
|
||||||
|
<div class="mt-16 text-3xl font-semibold">
|
||||||
|
Templates not Found
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="card bg-base-200 h-96 mb-2">
|
<div class="card bg-base-200 h-96 mb-2">
|
||||||
<div class="card-body text-center">
|
<div class="card-body text-center">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<div class="max-w-xl mx-auto px-2">
|
<div class="max-w-lg mx-auto px-2">
|
||||||
<h1 class="text-4xl font-bold text-center my-8">Change your password</h1>
|
<h1 class="text-4xl font-bold text-center mt-8">Change your password</h1>
|
||||||
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: 'space-y-6' }) do |f| %>
|
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: 'space-y-6' }) do |f| %>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<%= render 'devise/shared/error_messages', resource: %>
|
<%= render 'devise/shared/error_messages', resource: %>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<div class="max-w-xl mx-auto px-2">
|
<div class="max-w-lg mx-auto px-2">
|
||||||
<h1 class="text-4xl font-bold text-center my-8">Forgot your password?</h1>
|
<h1 class="text-4xl font-bold text-center mt-8">Forgot your password?</h1>
|
||||||
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post, class: 'space-y-6' }) do |f| %>
|
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post, class: 'space-y-6' }) do |f| %>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<%= render 'devise/shared/error_messages', resource: %>
|
<%= render 'devise/shared/error_messages', resource: %>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<div class="max-w-xl mx-auto px-2">
|
<div class="max-w-lg mx-auto px-2">
|
||||||
<h1 class="text-3xl font-bold text-center my-8">Profile Details</h1>
|
<h1 class="text-3xl font-bold text-center mt-8">Profile Details</h1>
|
||||||
<%= form_for('', as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: new_registration_path) do |f| %>
|
<%= form_for('', as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: new_registration_path) do |f| %>
|
||||||
<% if params[:redir].present? %>
|
<% if params[:redir].present? %>
|
||||||
<%= hidden_field_tag :redir, params[:redir] %>
|
<%= hidden_field_tag :redir, params[:redir] %>
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<%= f.fields_for resource do |ff| %>
|
<%= f.fields_for resource do |ff| %>
|
||||||
<div class="form-control <%= 'hidden' if (params[:oauth_callback] || params[:sign_up]) && resource.email? %>">
|
<div class="form-control <%= 'hidden' if (params[:oauth_callback] || params[:sign_up]) && resource.email? %>">
|
||||||
<%= ff.label :email, class: 'label' %>
|
<%= ff.label :email, class: 'label' %>
|
||||||
<%= ff.email_field :email, required: true, class: 'base-input' %>
|
<%= ff.email_field :email, value: EmailTypo.call(resource.email), required: true, class: 'base-input' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= f.fields_for resource.account do |ff| %>
|
<%= f.fields_for resource.account do |ff| %>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<div class="max-w-xl mx-auto px-2">
|
<div class="max-w-lg mx-auto px-2">
|
||||||
<h1 class="text-3xl font-bold text-center my-8">Create Free Account</h1>
|
<%= render 'devise/shared/select_server' if Docuseal.multitenant? %>
|
||||||
|
<h1 class="text-3xl font-bold text-center mt-8 mb-6">Create Free Account</h1>
|
||||||
<%= form_for(User.new, html: { class: 'space-y-6' }, url: new_registration_path, data: { turbo: params[:redir].blank? }, method: :get) do |f| %>
|
<%= form_for(User.new, html: { class: 'space-y-6' }, url: new_registration_path, data: { turbo: params[:redir].blank? }, method: :get) do |f| %>
|
||||||
<% if params[:redir].present? %>
|
<% if params[:redir].present? %>
|
||||||
<%= hidden_field_tag :redir, params[:redir] %>
|
<%= hidden_field_tag :redir, params[:redir] %>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<div class="max-w-xl mx-auto px-2">
|
<div class="max-w-lg mx-auto px-2">
|
||||||
<h1 class="text-4xl font-bold text-center my-8">Log In</h1>
|
<%= render 'devise/shared/select_server' if Docuseal.multitenant? %>
|
||||||
|
<h1 class="text-4xl font-bold text-center mt-8">Log In</h1>
|
||||||
<%= form_for(resource, as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: session_path(resource_name)) do |f| %>
|
<%= form_for(resource, as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: session_path(resource_name)) do |f| %>
|
||||||
<% if params[:redir].present? %>
|
<% if params[:redir].present? %>
|
||||||
<%= hidden_field_tag :redir, params[:redir] %>
|
<%= hidden_field_tag :redir, params[:redir] %>
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<div class="max-w-lg mx-auto px-2">
|
||||||
|
<h1 class="text-4xl font-bold text-center mt-8">Log In</h1>
|
||||||
|
<%= form_for(resource, as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: session_path(resource_name)) do |f| %>
|
||||||
|
<%= f.hidden_field :email %>
|
||||||
|
<%= f.hidden_field :password %>
|
||||||
|
<% if params[:redir].present? %>
|
||||||
|
<%= hidden_field_tag :redir, params[:redir] %>
|
||||||
|
<% end %>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="form-control">
|
||||||
|
<%= f.label :otp_attempt, 'Two-Factor Code', class: 'label' %>
|
||||||
|
<%= f.text_field :otp_attempt, autofocus: true, placeholder: 'XXX-XXX', required: true, class: 'base-input' %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<%= f.button button_title(title: 'Log In', disabled_with: 'Logging In'), class: 'base-button' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<div class="text-center">
|
||||||
|
<div class="join">
|
||||||
|
<a href="https://docuseal.co<%= request.fullpath.gsub('docuseal.eu', 'docuseal.co') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.co' %>">
|
||||||
|
🌎 Global
|
||||||
|
</a>
|
||||||
|
<a href="https://docuseal.eu<%= request.fullpath.gsub('docuseal.co', 'docuseal.eu') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.eu' %>">
|
||||||
|
🇪🇺 Europe
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
File renamed without changes.
Whitespace-only changes.
@@ -1,41 +0,0 @@
|
|||||||
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
|
|
||||||
<%= render 'shared/settings_nav' %>
|
|
||||||
<div class="flex-grow max-w-xl mx-auto">
|
|
||||||
<h1 class="text-4xl font-bold mb-4">PDF Signature</h1>
|
|
||||||
<div id="result">
|
|
||||||
<p class="mb-2">
|
|
||||||
Upload signed PDF file to validate its signature:
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<%= form_for '', url: settings_esign_index_path, method: :post, html: { enctype: 'multipart/form-data' } do |f| %>
|
|
||||||
<%= f.button type: 'submit', class: 'flex' do %>
|
|
||||||
<div class="disabled mb-3">
|
|
||||||
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin inline') %>
|
|
||||||
Analyzing...
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<file-dropzone data-is-direct-upload="false" data-name="verify_attachments" data-submit-on-upload="true" class="w-full">
|
|
||||||
<label for="file" class="w-full block h-32 relative bg-base-200 hover:bg-base-200/70 rounded-md border border-base-content border-dashed">
|
|
||||||
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
|
|
||||||
<div class="flex flex-col items-center">
|
|
||||||
<span data-target="file-dropzone.icon">
|
|
||||||
<%= svg_icon('cloud_upload', class: 'w-10 h-10') %>
|
|
||||||
</span>
|
|
||||||
<span data-target="file-dropzone.loading" class="hidden">
|
|
||||||
<%= svg_icon('loader', class: 'w-10 h-10 animate-spin') %>
|
|
||||||
</span>
|
|
||||||
<div class="font-medium mb-1">
|
|
||||||
Verify Signed PDF
|
|
||||||
</div>
|
|
||||||
<div class="text-xs">
|
|
||||||
<span class="font-medium">Click to upload</span> or drag and drop files
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<input id="file" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="application/pdf" multiple>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</file-dropzone>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<div class="w-0 md:w-52"></div>
|
|
||||||
</div>
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<%= render 'shared/turbo_modal', title: 'Upload Certificate' do %>
|
||||||
|
<%= form_for @cert_record, url: settings_esign_path, html: { class: 'space-y-4', enctype: 'multipart/form-data' }, data: { turbo_frame: :_top } do |f| %>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="form-control">
|
||||||
|
<%= f.label :name, class: 'label' %>
|
||||||
|
<%= f.text_field :name, required: true, class: 'base-input' %>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<%= f.label :file, class: 'label' %>
|
||||||
|
<%= f.file_field :file, required: true %>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text-alt">Use a valid .der, .p12 or .pfx file.</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<%= f.label :password, 'Password (optional)', class: 'label' %>
|
||||||
|
<%= f.password_field :password, class: 'base-input' %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-control pt-2">
|
||||||
|
<%= f.button button_title, class: 'base-button' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
|
||||||
|
<%= render 'shared/settings_nav' %>
|
||||||
|
<div class="flex-grow">
|
||||||
|
<div class="max-w-xl">
|
||||||
|
<h1 class="text-4xl font-bold mb-4">PDF Signature</h1>
|
||||||
|
<div id="result">
|
||||||
|
<p class="mb-2">
|
||||||
|
Upload signed PDF file to validate its signature:
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<%= form_for '', url: verify_pdf_signature_index_path, method: :post, html: { enctype: 'multipart/form-data' } do |f| %>
|
||||||
|
<%= f.button type: 'submit', class: 'flex' do %>
|
||||||
|
<div class="disabled mb-3">
|
||||||
|
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin inline') %>
|
||||||
|
Analyzing...
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<file-dropzone data-is-direct-upload="false" data-name="verify_attachments" data-submit-on-upload="true" class="w-full">
|
||||||
|
<label for="file" class="w-full block h-32 relative bg-base-200 hover:bg-base-200/70 rounded-md border border-base-content border-dashed">
|
||||||
|
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<span data-target="file-dropzone.icon">
|
||||||
|
<%= svg_icon('cloud_upload', class: 'w-10 h-10') %>
|
||||||
|
</span>
|
||||||
|
<span data-target="file-dropzone.loading" class="hidden">
|
||||||
|
<%= svg_icon('loader', class: 'w-10 h-10 animate-spin') %>
|
||||||
|
</span>
|
||||||
|
<div class="font-medium mb-1">
|
||||||
|
Verify Signed PDF
|
||||||
|
</div>
|
||||||
|
<div class="text-xs">
|
||||||
|
<span class="font-medium">Click to upload</span> or drag and drop files
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input id="file" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="application/pdf" multiple>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</file-dropzone>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-end mb-4 mt-8">
|
||||||
|
<h2 class="text-3xl font-bold">Signing Certificates</h2>
|
||||||
|
<% if can?(:create, @encrypted_config) %>
|
||||||
|
<%= link_to new_settings_esign_path, class: 'btn btn-primary btn-md', data: { turbo_frame: 'modal' } do %>
|
||||||
|
<%= svg_icon('plus', class: 'w-6 h-6') %>
|
||||||
|
<span>Upload Cert</span>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<%= render 'alert' %>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="table w-full table-lg rounded-b-none overflow-hidden">
|
||||||
|
<thead class="bg-base-200">
|
||||||
|
<tr class="text-neutral uppercase">
|
||||||
|
<th>
|
||||||
|
Name
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Valid To
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Status
|
||||||
|
</th>
|
||||||
|
<th class="text-right" width="1px">
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% @pkcs_list.each do |item| %>
|
||||||
|
<tr scope="row" class="group">
|
||||||
|
<td>
|
||||||
|
<%= item['name'] %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<%= l(item['pkcs'].certificate.not_after.to_date, format: :long, locale: current_account.locale) %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<% if item['status'] == 'default' %>
|
||||||
|
<span class="badge badge-lg badge-info badge-outline">
|
||||||
|
<%= item['status'] %>
|
||||||
|
</span>
|
||||||
|
<% elsif can?(:update, @encrypted_config) %>
|
||||||
|
<%= button_to settings_esign_path, method: :put, params: { name: item['name'] }, class: 'btn btn-outline btn-neutral btn-xs whitespace-nowrap', title: 'Delete', data: { turbo_confirm: 'Are you sure?' } do %>
|
||||||
|
Make Default
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<% if item['name'] != EsignSettingsController::DEFAULT_CERT_NAME && item['status'] != 'default' && can?(:destroy, @encrypted_config) %>
|
||||||
|
<%= button_to settings_esign_path, params: { name: item['name'] }, method: :delete, class: 'btn btn-outline btn-error btn-xs', title: 'Delete', data: { turbo_confirm: 'Are you sure?' } do %>
|
||||||
|
Remove
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path>
|
||||||
|
<path d="M12 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path>
|
||||||
|
<path d="M12 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 470 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M12 6h-6a2 2 0 0 0 -2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-6"></path>
|
||||||
|
<path d="M11 13l9 -9"></path>
|
||||||
|
<path d="M15 4h5v5"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 432 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M5 4h4l2 5l-2.5 1.5a11 11 0 0 0 5 5l1.5 -2.5l5 2v4a2 2 0 0 1 -2 2a16 16 0 0 1 -15 -15a2 2 0 0 1 2 -2"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 409 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M19.95 11a8 8 0 1 0 -.5 4m.5 5v-5h-5"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 345 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0"></path>
|
||||||
|
<path d="M21 21l-6 -6"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 384 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2"></path>
|
||||||
|
<path d="M7 9l5 -5l5 5"></path>
|
||||||
|
<path d="M12 4l0 12"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 416 B |
@@ -0,0 +1,13 @@
|
|||||||
|
<%= render 'shared/turbo_modal', title: 'Remove 2FA' do %>
|
||||||
|
<%= form_for '', url: mfa_setup_path, method: :delete, data: { turbo_frame: :_top } do |f| %>
|
||||||
|
<div class="form-control my-6 space-y-2">
|
||||||
|
<%= f.text_field :otp_attempt, required: true, placeholder: 'XXX-XXX', class: 'base-input text-center' %>
|
||||||
|
<span>
|
||||||
|
<%= @error_message %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-control mt-4">
|
||||||
|
<%= f.button button_title(title: 'Remove 2FA'), class: 'base-button' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<%= render 'shared/turbo_modal', title: 'Setup 2FA' do %>
|
||||||
|
<%= form_for '', url: mfa_setup_path, data: { turbo_frame: :_top } do |f| %>
|
||||||
|
<p class="text-center">
|
||||||
|
Use an authenticator mobile app like Google Authenticator or 1Password to scan the QR code below.
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<%== RQRCode::QRCode.new(@provision_url).as_svg(viewbox: true, svg_attributes: { class: 'w-80 h-80 my-4 mx-auto' }) %>
|
||||||
|
</div>
|
||||||
|
<div class="form-control my-6 space-y-2">
|
||||||
|
<%= f.text_field :otp_attempt, required: true, placeholder: 'XXX-XXX', class: 'base-input text-center' %>
|
||||||
|
<span>
|
||||||
|
<%= @error_message %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-control mt-4">
|
||||||
|
<%= f.button button_title(title: 'Save'), class: 'base-button' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<%= form_for config, url: settings_notifications_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
|
||||||
|
<%= f.hidden_field :key %>
|
||||||
|
<div class="form-control">
|
||||||
|
<%= f.label :value, class: 'label' do %>
|
||||||
|
<span class="flex items-center space-x-1">
|
||||||
|
<span>
|
||||||
|
Completed documents BCC address
|
||||||
|
</span>
|
||||||
|
<span class="tooltip" data-tip="Send email copy with completed documents to a specified BCC address.">
|
||||||
|
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
<%= f.email_field :value, autocomplete: 'off', class: 'base-input' %>
|
||||||
|
</div>
|
||||||
|
<div class="form-control pt-2">
|
||||||
|
<%= f.button button_title(title: 'Save', disabled_with: 'Updating'), class: 'base-button' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
Whitespace-only changes.
@@ -0,0 +1 @@
|
|||||||
|
<%= render 'reminder_placeholder' %>
|
||||||
Loaded 100 of 185 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user