mirror of
https://github.com/docusealco/docuseal
synced 2026-11-05 06:42:32 -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 |
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'
|
||||||
@@ -28,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'
|
||||||
|
|||||||
@@ -114,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
|
||||||
@@ -123,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)
|
||||||
@@ -146,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)
|
||||||
@@ -423,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)
|
||||||
@@ -555,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
|
||||||
@@ -584,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,31 +5,36 @@ 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] || params[:email]).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,
|
||||||
mark_as_sent: 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,
|
||||||
mark_as_sent: 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')
|
||||||
|
|
||||||
Submitters.send_signature_requests(submitters, send_email: 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
|
||||||
|
|||||||
@@ -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,11 +2,11 @@
|
|||||||
|
|
||||||
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 = Templates::CreateAttachments.call(@template, params)
|
||||||
|
|
||||||
schema = documents.map do |doc|
|
schema = documents.map do |doc|
|
||||||
|
|||||||
@@ -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
|
||||||
+3
-1
@@ -1,7 +1,9 @@
|
|||||||
# 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
|
||||||
|
|
||||||
@@ -11,9 +11,12 @@ class EsignSettingsController < ApplicationController
|
|||||||
end
|
end
|
||||||
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
|
def show
|
||||||
cert_data = EncryptedConfig.find_by(account: current_account,
|
cert_data = @encrypted_config.value || {}
|
||||||
key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
|
|
||||||
|
|
||||||
default_pkcs = GenerateCertificate.load_pkcs(cert_data) if cert_data['cert'].present?
|
default_pkcs = GenerateCertificate.load_pkcs(cert_data) if cert_data['cert'].present?
|
||||||
|
|
||||||
@@ -42,10 +45,7 @@ class EsignSettingsController < ApplicationController
|
|||||||
def create
|
def create
|
||||||
@cert_record = CertFormRecord.new(**cert_params)
|
@cert_record = CertFormRecord.new(**cert_params)
|
||||||
|
|
||||||
cert_configs = EncryptedConfig.find_or_initialize_by(account: current_account,
|
if (@encrypted_config.value && @encrypted_config.value['custom']&.any? { |e| e['name'] == @cert_record.name }) ||
|
||||||
key: EncryptedConfig::ESIGN_CERTS_KEY)
|
|
||||||
|
|
||||||
if (cert_configs.value && cert_configs.value['custom']&.any? { |e| e['name'] == @cert_record.name }) ||
|
|
||||||
@cert_record.name == DEFAULT_CERT_NAME
|
@cert_record.name == DEFAULT_CERT_NAME
|
||||||
|
|
||||||
@cert_record.errors.add(:name, 'already exists')
|
@cert_record.errors.add(:name, 'already exists')
|
||||||
@@ -54,7 +54,7 @@ class EsignSettingsController < ApplicationController
|
|||||||
status: :unprocessable_entity
|
status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
save_new_cert!(cert_configs, @cert_record)
|
save_new_cert!(@encrypted_config, @cert_record)
|
||||||
|
|
||||||
redirect_to settings_esign_path, notice: 'Certificate has been successfully added!'
|
redirect_to settings_esign_path, notice: 'Certificate has been successfully added!'
|
||||||
rescue OpenSSL::PKCS12::PKCS12Error
|
rescue OpenSSL::PKCS12::PKCS12Error
|
||||||
@@ -64,29 +64,31 @@ class EsignSettingsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
cert_configs = EncryptedConfig.find_by(account: current_account, key: EncryptedConfig::ESIGN_CERTS_KEY)
|
@encrypted_config.value['custom'].each { |e| e['status'] = 'validate' }
|
||||||
|
|
||||||
cert_configs.value['custom'].each { |e| e['status'] = 'validate' }
|
custom_cert_data = @encrypted_config.value['custom'].find { |e| e['name'] == params[:name] }
|
||||||
custom_cert_data = cert_configs.value['custom'].find { |e| e['name'] == params[:name] }
|
|
||||||
custom_cert_data['status'] = 'default' if custom_cert_data
|
custom_cert_data['status'] = 'default' if custom_cert_data
|
||||||
|
|
||||||
cert_configs.save!
|
@encrypted_config.save!
|
||||||
|
|
||||||
redirect_to settings_esign_path, notice: 'Default certificate has been selected'
|
redirect_to settings_esign_path, notice: 'Default certificate has been selected'
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
cert_configs = EncryptedConfig.find_by(account: current_account, key: EncryptedConfig::ESIGN_CERTS_KEY)
|
@encrypted_config.value['custom'].reject! { |e| e['name'] == params[:name] }
|
||||||
|
|
||||||
cert_configs.value['custom'].reject! { |e| e['name'] == params[:name] }
|
@encrypted_config.save!
|
||||||
|
|
||||||
cert_configs.save!
|
|
||||||
|
|
||||||
redirect_to settings_esign_path, notice: 'Certificate has been removed'
|
redirect_to settings_esign_path, notice: 'Certificate has been removed'
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
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)
|
def save_new_cert!(cert_configs, cert_record)
|
||||||
pkcs = OpenSSL::PKCS12.new(cert_record.file.read, cert_record.password)
|
pkcs = OpenSSL::PKCS12.new(cert_record.file.read, cert_record.password)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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!
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class SmsSettingsController < ApplicationController
|
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
|
def index; end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_encrypted_config
|
||||||
|
@encrypted_config =
|
||||||
|
EncryptedConfig.find_or_initialize_by(account: current_account, key: 'sms_configs')
|
||||||
|
end
|
||||||
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,19 +2,26 @@
|
|||||||
|
|
||||||
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, template_schema_documents: [:blob, { 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,
|
||||||
@@ -26,25 +33,20 @@ class SubmissionsController < ApplicationController
|
|||||||
Submissions.create_from_submitters(template: @template,
|
Submissions.create_from_submitters(template: @template,
|
||||||
user: current_user,
|
user: current_user,
|
||||||
source: :invite,
|
source: :invite,
|
||||||
|
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
|
||||||
mark_as_sent: params[:send_email] == '1',
|
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)
|
||||||
|
|
||||||
Submitters.send_signature_requests(submitters, params)
|
redirect_to template_path(@template), notice: 'New recipients have been added'
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -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,6 +4,7 @@ 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 =
|
||||||
@@ -23,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
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class SubmittersSendEmailController < ApplicationController
|
class SubmittersSendEmailController < ApplicationController
|
||||||
|
load_and_authorize_resource :submitter, id_param: :submitter_slug, find_by: :slug
|
||||||
|
|
||||||
def create
|
def create
|
||||||
submitter = Submitter.joins(:template)
|
SubmitterMailer.invitation_email(@submitter).deliver_later!
|
||||||
.where(template: { account_id: current_account.id })
|
|
||||||
.find_by!(slug: params[:submitter_slug])
|
|
||||||
|
|
||||||
SubmitterMailer.invitation_email(submitter).deliver_later!
|
SubmissionEvent.create!(submitter: @submitter, event_type: 'send_email')
|
||||||
|
|
||||||
submitter.sent_at ||= Time.current
|
@submitter.sent_at ||= Time.current
|
||||||
submitter.save!
|
@submitter.save!
|
||||||
|
|
||||||
redirect_back(fallback_location: submission_path(submitter.submission), notice: 'Email has been sent')
|
redirect_back(fallback_location: submission_path(@submitter.submission), notice: 'Email has been sent')
|
||||||
end
|
end
|
||||||
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
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class VerifyPdfSignatureController < ApplicationController
|
class VerifyPdfSignatureController < ApplicationController
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
def create
|
def create
|
||||||
pdfs =
|
pdfs =
|
||||||
params[:files].map do |file|
|
params[:files].map do |file|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ 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',
|
backgroundColor: '#faf7f5',
|
||||||
|
withPhone: this.dataset.withPhone === 'true',
|
||||||
|
acceptFileTypes: this.dataset.acceptFileTypes,
|
||||||
isDirectUpload: this.dataset.isDirectUpload === 'true'
|
isDirectUpload: this.dataset.isDirectUpload === 'true'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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 () {
|
||||||
|
|||||||
@@ -140,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
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -265,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
|
||||||
@@ -295,7 +305,7 @@
|
|||||||
:is-demo="isDemo"
|
:is-demo="isDemo"
|
||||||
:attribution="attribution"
|
:attribution="attribution"
|
||||||
:with-confetti="withConfetti"
|
:with-confetti="withConfetti"
|
||||||
:can-send-email="canSendEmail && submitter.email"
|
:can-send-email="canSendEmail && !!submitter.email"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
/>
|
/>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
@@ -320,6 +330,7 @@ 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'
|
import { t } from './i18n'
|
||||||
@@ -334,6 +345,7 @@ export default {
|
|||||||
MultiSelectStep,
|
MultiSelectStep,
|
||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
IconArrowsDiagonal,
|
IconArrowsDiagonal,
|
||||||
|
PhoneStep,
|
||||||
IconArrowsDiagonalMinimize2,
|
IconArrowsDiagonalMinimize2,
|
||||||
FormCompleted
|
FormCompleted
|
||||||
},
|
},
|
||||||
@@ -409,7 +421,8 @@ export default {
|
|||||||
isCompleted: false,
|
isCompleted: false,
|
||||||
isFormVisible: true,
|
isFormVisible: true,
|
||||||
currentStep: 0,
|
currentStep: 0,
|
||||||
isSubmitting: false
|
isSubmitting: false,
|
||||||
|
recalculateButtonDisabledKey: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -423,9 +436,13 @@ export default {
|
|||||||
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]
|
||||||
@@ -473,13 +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,
|
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])
|
||||||
}
|
}
|
||||||
@@ -504,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
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,7 +13,15 @@ const en = {
|
|||||||
form_has_been_completed: 'Form has been completed!',
|
form_has_been_completed: 'Form has been completed!',
|
||||||
create_a_free_account: 'Create a Free Account',
|
create_a_free_account: 'Create a Free Account',
|
||||||
signed_with: 'Signed with',
|
signed_with: 'Signed with',
|
||||||
open_source_documents_software: 'open source documents software'
|
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 = {
|
const es = {
|
||||||
@@ -31,7 +39,15 @@ const es = {
|
|||||||
form_has_been_completed: '¡El formulario ha sido completado!',
|
form_has_been_completed: '¡El formulario ha sido completado!',
|
||||||
create_a_free_account: 'Crear una Cuenta Gratuita',
|
create_a_free_account: 'Crear una Cuenta Gratuita',
|
||||||
signed_with: 'Firmado con',
|
signed_with: 'Firmado con',
|
||||||
open_source_documents_software: 'software de documentos de código abierto'
|
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 = {
|
const it = {
|
||||||
@@ -49,7 +65,15 @@ const it = {
|
|||||||
form_has_been_completed: 'Il modulo è stato completato!',
|
form_has_been_completed: 'Il modulo è stato completato!',
|
||||||
create_a_free_account: 'Crea un Account Gratuito',
|
create_a_free_account: 'Crea un Account Gratuito',
|
||||||
signed_with: 'Firmato con',
|
signed_with: 'Firmato con',
|
||||||
open_source_documents_software: 'software di documenti open source'
|
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 = {
|
const de = {
|
||||||
@@ -67,7 +91,15 @@ const de = {
|
|||||||
form_has_been_completed: 'Formular wurde ausgefüllt!',
|
form_has_been_completed: 'Formular wurde ausgefüllt!',
|
||||||
create_a_free_account: 'Kostenloses Konto erstellen',
|
create_a_free_account: 'Kostenloses Konto erstellen',
|
||||||
signed_with: 'Unterschrieben mit',
|
signed_with: 'Unterschrieben mit',
|
||||||
open_source_documents_software: 'Open-Source-Dokumentensoftware'
|
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 = {
|
const fr = {
|
||||||
@@ -85,7 +117,15 @@ const fr = {
|
|||||||
form_has_been_completed: 'Le formulaire a été complété !',
|
form_has_been_completed: 'Le formulaire a été complété !',
|
||||||
create_a_free_account: 'Créer un Compte Gratuit',
|
create_a_free_account: 'Créer un Compte Gratuit',
|
||||||
signed_with: 'Signé avec',
|
signed_with: 'Signé avec',
|
||||||
open_source_documents_software: 'logiciel de documents open source'
|
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 = {
|
const pl = {
|
||||||
@@ -103,7 +143,15 @@ const pl = {
|
|||||||
form_has_been_completed: 'Formularz został wypełniony!',
|
form_has_been_completed: 'Formularz został wypełniony!',
|
||||||
create_a_free_account: 'Utwórz darmowe konto',
|
create_a_free_account: 'Utwórz darmowe konto',
|
||||||
signed_with: 'Podpisane za pomocą',
|
signed_with: 'Podpisane za pomocą',
|
||||||
open_source_documents_software: 'oprogramowanie do dokumentów open source'
|
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 = {
|
const uk = {
|
||||||
@@ -121,7 +169,15 @@ const uk = {
|
|||||||
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: 'Підтвердіть номер телефону',
|
||||||
|
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 = {
|
const cs = {
|
||||||
@@ -139,14 +195,48 @@ const cs = {
|
|||||||
form_has_been_completed: 'Formulář byl dokončen!',
|
form_has_been_completed: 'Formulář byl dokončen!',
|
||||||
create_a_free_account: 'Vytvořit bezplatný účet',
|
create_a_free_account: 'Vytvořit bezplatný účet',
|
||||||
signed_with: 'Podepsáno pomocí',
|
signed_with: 'Podepsáno pomocí',
|
||||||
open_source_documents_software: 'open source software pro dokumenty'
|
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 i18n = { en, es, it, de, fr, pl, uk, cs }
|
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 browserLanguage = (navigator.language || navigator.userLanguage || 'en').split('-')[0]
|
||||||
|
|
||||||
const t = (key) => i18n[browserLanguage][key] || i18n.en[key] || key
|
const t = (key) => i18n[browserLanguage]?.[key] || i18n.en[key] || key
|
||||||
|
|
||||||
export default i18n
|
export default i18n
|
||||||
export { t }
|
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)"
|
||||||
@@ -90,6 +91,7 @@
|
|||||||
>
|
>
|
||||||
<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"
|
||||||
@@ -104,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"
|
||||||
/>
|
/>
|
||||||
@@ -237,6 +240,7 @@ export default {
|
|||||||
save: this.save,
|
save: this.save,
|
||||||
baseFetch: this.baseFetch,
|
baseFetch: this.baseFetch,
|
||||||
backgroundColor: this.backgroundColor,
|
backgroundColor: this.backgroundColor,
|
||||||
|
withPhone: this.withPhone,
|
||||||
selectedAreaRef: computed(() => this.selectedAreaRef)
|
selectedAreaRef: computed(() => this.selectedAreaRef)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -255,6 +259,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
acceptFileTypes: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'image/*, application/pdf'
|
||||||
|
},
|
||||||
baseUrl: {
|
baseUrl: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -270,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,
|
||||||
@@ -336,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,49 +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"
|
|
||||||
:style="{ backgroundColor: backgroundColor }"
|
|
||||||
@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"
|
||||||
@@ -95,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', 'backgroundColor'],
|
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,16 @@ 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,
|
has_many :template_schema_documents,
|
||||||
->(e) { where(uuid: (e.template_schema.presence || e.template.schema).pluck('attachment_uuid')) },
|
->(e) { where(uuid: (e.template_schema.presence || e.template.schema).pluck('attachment_uuid')) },
|
||||||
@@ -50,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
|
||||||
@@ -44,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?
|
||||||
@@ -57,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>
|
||||||
|
|||||||
@@ -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,6 +1,6 @@
|
|||||||
<div class="max-w-xl mx-auto px-2">
|
<div class="max-w-lg mx-auto px-2">
|
||||||
<%= render 'devise/shared/select_server' if Docuseal.multitenant? %>
|
<%= render 'devise/shared/select_server' if Docuseal.multitenant? %>
|
||||||
<h1 class="text-3xl font-bold text-center my-8">Create Free Account</h1>
|
<h1 class="text-3xl font-bold text-center 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,6 +1,6 @@
|
|||||||
<div class="max-w-xl mx-auto px-2">
|
<div class="max-w-lg mx-auto px-2">
|
||||||
<%= render 'devise/shared/select_server' if Docuseal.multitenant? %>
|
<%= render 'devise/shared/select_server' if Docuseal.multitenant? %>
|
||||||
<h1 class="text-4xl font-bold text-center my-8">Log In</h1>
|
<h1 class="text-4xl font-bold text-center 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>
|
||||||
File renamed without changes.
@@ -14,7 +14,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.label :password, 'Password (optional)', class: 'label' %>
|
<%= f.label :password, 'Password (optional)', class: 'label' %>
|
||||||
<%= f.text_field :password, class: 'base-input' %>
|
<%= f.password_field :password, class: 'base-input' %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control pt-2">
|
<div class="form-control pt-2">
|
||||||
|
|||||||
@@ -40,9 +40,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between items-end mb-4 mt-8">
|
<div class="flex justify-between items-end mb-4 mt-8">
|
||||||
<h2 class="text-3xl font-bold">Signing Certificates</h2>
|
<h2 class="text-3xl font-bold">Signing Certificates</h2>
|
||||||
<%= link_to new_settings_esign_path, class: 'btn btn-primary btn-md', data: { turbo_frame: 'modal' } do %>
|
<% if can?(:create, @encrypted_config) %>
|
||||||
<%= svg_icon('plus', class: 'w-6 h-6') %>
|
<%= link_to new_settings_esign_path, class: 'btn btn-primary btn-md', data: { turbo_frame: 'modal' } do %>
|
||||||
<span>Upload Cert</span>
|
<%= svg_icon('plus', class: 'w-6 h-6') %>
|
||||||
|
<span>Upload Cert</span>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<%= render 'alert' %>
|
<%= render 'alert' %>
|
||||||
@@ -77,14 +79,14 @@
|
|||||||
<span class="badge badge-lg badge-info badge-outline">
|
<span class="badge badge-lg badge-info badge-outline">
|
||||||
<%= item['status'] %>
|
<%= item['status'] %>
|
||||||
</span>
|
</span>
|
||||||
<% else %>
|
<% 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 %>
|
<%= 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
|
Make Default
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<% if item['name'] != EsignSettingsController::DEFAULT_CERT_NAME && item['status'] != 'default' %>
|
<% 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 %>
|
<%= 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
|
Remove
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -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" 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' %>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<%= 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">
|
||||||
|
<% record = Struct.new(:first_duration, :second_duration, :third_duration).new(*(f.object.value || {}).values_at('first_duration', 'second_duration', 'third_duration')) %>
|
||||||
|
<div class="flex space-x-4">
|
||||||
|
<div class="w-full">
|
||||||
|
<%= f.fields_for :value, record do |ff| %>
|
||||||
|
<%= ff.label :first_duration, 'First reminder in', class: 'label' %>
|
||||||
|
<%= ff.select :first_duration, AccountConfigs::REMINDER_DURATIONS.invert, { include_blank: 'None' }, class: 'base-select' %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<%= f.fields_for :value, record do |ff| %>
|
||||||
|
<%= ff.label :second_duration, 'Second reminder in', class: 'label' %>
|
||||||
|
<%= ff.select :second_duration, AccountConfigs::REMINDER_DURATIONS.invert, { include_blank: 'None' }, class: 'base-select' %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<%= f.fields_for :value, record do |ff| %>
|
||||||
|
<%= ff.label :third_duration, 'Third reminder in', class: 'label' %>
|
||||||
|
<%= ff.select :third_duration, AccountConfigs::REMINDER_DURATIONS.invert, { include_blank: 'None' }, class: 'base-select' %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-control pt-4">
|
||||||
|
<%= f.button button_title(title: 'Save', disabled_with: 'Updating'), class: 'base-button' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<div class="alert my-4">
|
||||||
|
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
|
||||||
|
<div>
|
||||||
|
<p class="font-bold">Unlock with DocuSeal Pro</p>
|
||||||
|
<p>
|
||||||
|
Send automatic email reminders to your recipients.
|
||||||
|
<br>
|
||||||
|
<a class="link font-medium" target="_blank" href="<%= "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">Learn More</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
|
||||||
|
<%= render 'shared/settings_nav' %>
|
||||||
|
<div class="flex-grow max-w-xl mx-auto">
|
||||||
|
<h1 class="text-4xl font-bold mb-4">Email Notifications</h1>
|
||||||
|
<%= render 'email_stats' %>
|
||||||
|
<%= render 'bcc_form', config: @bcc_config %>
|
||||||
|
<div class="flex justify-between items-end mb-4 mt-8">
|
||||||
|
<h2 class="text-3xl font-bold">Signature Email Reminders</h2>
|
||||||
|
</div>
|
||||||
|
<%= render 'reminder_banner' %>
|
||||||
|
<%= render 'reminder_form', config: @reminder_config %>
|
||||||
|
</div>
|
||||||
|
<div class="w-0 md:w-52"></div>
|
||||||
|
</div>
|
||||||
@@ -35,6 +35,26 @@
|
|||||||
<%= f.button button_title(title: 'Update', disabled_with: 'Updating'), class: 'base-button' %>
|
<%= f.button button_title(title: 'Update', disabled_with: 'Updating'), class: 'base-button' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<p class="text-2xl font-bold mt-8 mb-4">Two-Factor Authentication</p>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<% if current_user.otp_required_for_login %>
|
||||||
|
<p class="flex items-center space-x-1">
|
||||||
|
<%= svg_icon('circle_check', class: 'stroke-success inline flex-none w-5 h-5') %>
|
||||||
|
<span>
|
||||||
|
2FA has been configured.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<a href="<%= edit_mfa_setup_path %>" data-turbo-frame="modal" class="white-button w-full !px-8">🔓 Remove 2FA</a>
|
||||||
|
<% else %>
|
||||||
|
<p class="flex items-center space-x-1">
|
||||||
|
<%= svg_icon('info_circle', class: 'stroke-warning inline flex-none w-5 h-5') %>
|
||||||
|
<span>
|
||||||
|
2FA is not configured.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<a href="<%= new_mfa_setup_path %>" data-turbo-frame="modal" class="base-button w-full !px-8">🔒 Set up 2FA</a>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-0 md:w-52"></div>
|
<div class="w-0 md:w-52"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
<div class="flex my-6 justify-center md:justify-between">
|
<div class="flex my-6 justify-center md:justify-between">
|
||||||
<div class="hidden md:block text-sm">
|
<div class="hidden md:block text-sm">
|
||||||
<%= @pagy.from %>-<%= @pagy.to %> of <%= @pagy.count %> <%= local_assigns[:items_name] || 'items' %>
|
<%= @pagy.from %>-<%= @pagy.to %> of <%= @pagy.count %> <%= local_assigns[:items_name] || 'items' %>
|
||||||
|
<%= local_assigns[:left_additional_html] %>
|
||||||
</div>
|
</div>
|
||||||
<div class="join">
|
<div class="join">
|
||||||
<% if @pagy.prev %>
|
<% if @pagy.prev %>
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<form action="<%= request.path %>" method="get" class="items-center hidden md:flex">
|
||||||
|
<input id="search" name="q" value="<%= params[:q] %>" class="input input-ghost text-lg pr-10 -mr-12">
|
||||||
|
<button type="submit" title="Search" class="btn btn-ghost btn-circle" onclick="window.search.value || document.activeElement === window.search ? null : [event.preventDefault(), window.search.focus()]">
|
||||||
|
<span class="enabled">
|
||||||
|
<%= svg_icon('search', class: 'w-6 h-6 stroke-2') %>
|
||||||
|
</span>
|
||||||
|
<span class="disabled">
|
||||||
|
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin') %>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
@@ -10,33 +10,54 @@
|
|||||||
<%= link_to 'Account', settings_account_path, class: 'text-base hover:bg-base-300' %>
|
<%= link_to 'Account', settings_account_path, class: 'text-base hover:bg-base-300' %>
|
||||||
</li>
|
</li>
|
||||||
<% unless Docuseal.multitenant? %>
|
<% unless Docuseal.multitenant? %>
|
||||||
|
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::EMAIL_SMTP_KEY, account: current_account)) %>
|
||||||
|
<li>
|
||||||
|
<%= link_to 'Email', settings_email_index_path, class: 'text-base hover:bg-base-300' %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::FILES_STORAGE_KEY, account: current_account)) %>
|
||||||
|
<li>
|
||||||
|
<%= link_to 'Storage', settings_storage_index_path, class: 'text-base hover:bg-base-300' %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
<% if can?(:read, EncryptedConfig.new(key: 'submitter_invitation_sms', account: current_account)) %>
|
||||||
|
<li>
|
||||||
|
<%= link_to 'SMS', settings_sms_path, class: 'text-base hover:bg-base-300' %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% if can?(:read, AccountConfig) %>
|
||||||
<li>
|
<li>
|
||||||
<%= link_to 'Email', settings_email_index_path, class: 'text-base hover:bg-base-300' %>
|
<%= link_to 'Notifications', settings_notifications_path, class: 'text-base hover:bg-base-300' %>
|
||||||
</li>
|
</li>
|
||||||
|
<% end %>
|
||||||
|
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::ESIGN_CERTS_KEY, account: current_account)) %>
|
||||||
<li>
|
<li>
|
||||||
<%= link_to 'Storage', settings_storage_index_path, class: 'text-base hover:bg-base-300' %>
|
<%= link_to 'E-Signature', settings_esign_path, class: 'text-base hover:bg-base-300' %>
|
||||||
</li>
|
</li>
|
||||||
|
<% end %>
|
||||||
|
<% if can?(:read, User) %>
|
||||||
<li>
|
<li>
|
||||||
<%= link_to 'SMS', settings_sms_path, class: 'text-base hover:bg-base-300' %>
|
<%= link_to 'Team', settings_users_path, class: 'text-base hover:bg-base-300' %>
|
||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
<li>
|
|
||||||
<%= link_to 'E-Signature', settings_esign_path, class: 'text-base hover:bg-base-300' %>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<%= link_to 'Team', settings_users_path, class: 'text-base hover:bg-base-300' %>
|
|
||||||
</li>
|
|
||||||
<% if Docuseal.demo? || !Docuseal.multitenant? %>
|
<% if Docuseal.demo? || !Docuseal.multitenant? %>
|
||||||
|
<% if can?(:read, AccessToken) %>
|
||||||
|
<li>
|
||||||
|
<%= link_to 'API', settings_api_index_path, class: 'text-base hover:bg-base-300' %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::WEBHOOK_URL_KEY, account: current_account)) %>
|
||||||
|
<li>
|
||||||
|
<%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% if can?(:read, AccountConfig) %>
|
||||||
<li>
|
<li>
|
||||||
<%= link_to 'API', settings_api_index_path, class: 'text-base hover:bg-base-300' %>
|
<%= link_to 'Personalization', settings_personalization_path, class: 'text-base hover:bg-base-300' %>
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %>
|
|
||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
<li>
|
|
||||||
<%= link_to 'Personalization', settings_personalization_path, class: 'text-base hover:bg-base-300' %>
|
|
||||||
</li>
|
|
||||||
<% unless Docuseal.demo? %>
|
<% unless Docuseal.demo? %>
|
||||||
<li>
|
<li>
|
||||||
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center text-2xl font-semibold">
|
<div class="text-center text-2xl font-semibold">
|
||||||
Form has been submitter already
|
Form has been submitted already
|
||||||
</div>
|
</div>
|
||||||
<% if Accounts.can_send_emails?(@submitter.submission.template.account) %>
|
<% if Accounts.can_send_emails?(@submitter.submission.template.account) %>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
||||||
<input type="text" name="submission[1][submitters][][name]" autocomplete="off" class="input input-sm input-bordered" placeholder="Name" required>
|
<input type="text" name="submission[1][submitters][][name]" autocomplete="off" class="input input-sm input-bordered" placeholder="Name" required>
|
||||||
<div class="grid <%= 'md:grid-cols-2 gap-1' if template.submitters.size == 1 %>">
|
<div class="grid <%= 'md:grid-cols-2 gap-1' if template.submitters.size == 1 %>">
|
||||||
<input type="email" name="submission[1][submitters][][email]" autocomplete="off" class="input input-sm input-bordered mt-1.5" placeholder="Email (optional)">
|
<input type="email" multiple name="submission[1][submitters][][email]" autocomplete="off" class="input input-sm input-bordered mt-1.5" placeholder="Email (optional)">
|
||||||
<input type="tel" pattern="^\+[0-9\s\-]+$" oninvalid="this.value ? this.setCustomValidity('Use internatioanl format: +1xxx...') : ''" oninput="this.setCustomValidity('')" name="submission[1][submitters][][phone]" autocomplete="off" class="input input-sm input-bordered mt-1.5" placeholder="Phone (optional)">
|
<input type="tel" pattern="^\+[0-9\s\-]+$" oninvalid="this.value ? this.setCustomValidity('Use internatioanl format: +1xxx...') : ''" oninput="this.setCustomValidity('')" name="submission[1][submitters][][phone]" autocomplete="off" class="input input-sm input-bordered mt-1.5" placeholder="Phone (optional)">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,10 +30,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem">
|
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem">
|
||||||
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
|
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
|
||||||
<span>Add Recipient</span>
|
<span>Add New</span>
|
||||||
</a>
|
</a>
|
||||||
</dynamic-list>
|
</dynamic-list>
|
||||||
<div>
|
<div>
|
||||||
|
<%= render('submitters_order', f:, template:) if Accounts.can_send_emails?(current_account) %>
|
||||||
<%= render 'send_email', f:, template: %>
|
<%= render 'send_email', f:, template: %>
|
||||||
<%= render 'send_sms', f: %>
|
<%= render 'send_sms', f: %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<span class="label-text"> <%= item['name'] %></span>
|
<span class="label-text"> <%= item['name'] %></span>
|
||||||
</label>
|
</label>
|
||||||
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
||||||
<input type="email" name="submission[1][submitters][][email]" autocomplete="off" class="input input-sm input-bordered" placeholder="Email" required>
|
<input type="email" multiple name="submission[1][submitters][][email]" autocomplete="off" class="input input-sm input-bordered" placeholder="Email" required>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
@@ -31,11 +31,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem">
|
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem">
|
||||||
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
|
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
|
||||||
<span>Add Recipient</span>
|
<span>Add New</span>
|
||||||
</a>
|
</a>
|
||||||
</dynamic-list>
|
</dynamic-list>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render 'send_email', f:, template: %>
|
<div>
|
||||||
|
<%= render 'send_email', f:, template: %>
|
||||||
|
<%= render('submitters_order', f:, template:) if Accounts.can_send_emails?(current_account) %>
|
||||||
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.button button_title(title: 'Add Recipients'), class: 'base-button' %>
|
<%= f.button button_title(title: 'Add Recipients'), class: 'base-button' %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loaded 100 of 150 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user