mirror of
https://github.com/docusealco/docuseal
synced 2026-11-05 14:42:33 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d757f06ff4 | ||
|
|
132999176c | ||
|
|
2f74caf16a | ||
|
|
41d3eaf582 | ||
|
|
907aaa9c30 | ||
|
|
7bb241815f | ||
|
|
84e879bad0 | ||
|
|
a1a0205d33 | ||
|
|
6f8e88cdbd | ||
|
|
2f1843151d | ||
|
|
f89c50d096 | ||
|
|
ef618f8f49 | ||
|
|
b3662ddea8 | ||
|
|
53fa35fd01 | ||
|
|
7f6c0af0e8 | ||
|
|
3faec9d174 | ||
|
|
9d9e134bb4 | ||
|
|
ec4f399fcc | ||
|
|
effa7b0fc7 | ||
|
|
016d9de87a | ||
|
|
71ff064356 | ||
|
|
9100d48cc3 | ||
|
|
21b207cb1a | ||
|
|
3aa703032f | ||
|
|
fcfd2e13bd | ||
|
|
cf6cacdc92 | ||
|
|
8cabe31827 | ||
|
|
62cea05084 | ||
|
|
07e49f9f07 | ||
|
|
baad784818 | ||
|
|
6eecd3afa4 | ||
|
|
8e90100871 | ||
|
|
12c5138364 | ||
|
|
80ad166fd9 | ||
|
|
2fab6984ea | ||
|
|
422639114b | ||
|
|
fca441878a | ||
|
|
76578077e8 | ||
|
|
bbd3618f0e | ||
|
|
47ed8db955 | ||
|
|
fec6e2e35d |
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 =
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -493,10 +493,49 @@ export default {
|
|||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.recalculateButtonDisabledKey = Math.random()
|
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)
|
||||||
|
|
||||||
@@ -531,14 +570,14 @@ export default {
|
|||||||
? this.$refs.currentStep.submit
|
? this.$refs.currentStep.submit
|
||||||
: () => Promise.resolve({})
|
: () => Promise.resolve({})
|
||||||
|
|
||||||
stepPromise().then(() => {
|
stepPromise().then(async () => {
|
||||||
const formData = new FormData(this.$refs.form)
|
const formData = new FormData(this.$refs.form)
|
||||||
|
|
||||||
if (this.currentStep === this.stepFields.length - 1) {
|
if (this.currentStep === this.stepFields.length - 1) {
|
||||||
formData.append('completed', 'true')
|
formData.append('completed', 'true')
|
||||||
}
|
}
|
||||||
|
|
||||||
this.saveStep(formData).then(async (response) => {
|
await this.saveStep(formData).then(async (response) => {
|
||||||
if (response.status === 422) {
|
if (response.status === 422) {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ const en = {
|
|||||||
change_phone_number: 'Change phone number',
|
change_phone_number: 'Change phone number',
|
||||||
sending: 'Sending...',
|
sending: 'Sending...',
|
||||||
resend_code: 'Re-send code',
|
resend_code: 'Re-send code',
|
||||||
verification_code_has_been_resent: 'Verification code has been re-sent via SMS'
|
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 = {
|
||||||
@@ -45,7 +46,8 @@ const es = {
|
|||||||
change_phone_number: 'Cambiar número de teléfono',
|
change_phone_number: 'Cambiar número de teléfono',
|
||||||
sending: 'Enviando...',
|
sending: 'Enviando...',
|
||||||
resend_code: 'Reenviar código',
|
resend_code: 'Reenviar código',
|
||||||
verification_code_has_been_resent: 'El código de verificación ha sido reenviado por SMS'
|
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 = {
|
||||||
@@ -70,7 +72,8 @@ const it = {
|
|||||||
change_phone_number: 'Cambia numero di telefono',
|
change_phone_number: 'Cambia numero di telefono',
|
||||||
sending: 'Invio in corso...',
|
sending: 'Invio in corso...',
|
||||||
resend_code: 'Rinvia codice',
|
resend_code: 'Rinvia codice',
|
||||||
verification_code_has_been_resent: 'Il codice di verifica è stato rinviato tramite SMS'
|
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 = {
|
||||||
@@ -95,7 +98,8 @@ const de = {
|
|||||||
change_phone_number: 'Telefonnummer ändern',
|
change_phone_number: 'Telefonnummer ändern',
|
||||||
sending: 'Senden...',
|
sending: 'Senden...',
|
||||||
resend_code: 'Code erneut senden',
|
resend_code: 'Code erneut senden',
|
||||||
verification_code_has_been_resent: 'Die Verifizierungscode wurde erneut per SMS gesendet'
|
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 = {
|
||||||
@@ -120,7 +124,8 @@ const fr = {
|
|||||||
change_phone_number: 'Changer le numéro de téléphone',
|
change_phone_number: 'Changer le numéro de téléphone',
|
||||||
sending: 'Envoi en cours...',
|
sending: 'Envoi en cours...',
|
||||||
resend_code: 'Renvoyer le code',
|
resend_code: 'Renvoyer le code',
|
||||||
verification_code_has_been_resent: 'Le code de vérification a été renvoyé par SMS'
|
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 = {
|
||||||
@@ -145,7 +150,8 @@ const pl = {
|
|||||||
change_phone_number: 'Zmień numer telefonu',
|
change_phone_number: 'Zmień numer telefonu',
|
||||||
sending: 'Wysyłanie...',
|
sending: 'Wysyłanie...',
|
||||||
resend_code: 'Ponownie wyślij kod',
|
resend_code: 'Ponownie wyślij kod',
|
||||||
verification_code_has_been_resent: 'Kod weryfikacyjny został ponownie wysłany'
|
verification_code_has_been_resent: 'Kod weryfikacyjny został ponownie wysłany',
|
||||||
|
email_has_been_sent: 'E-mail został wysłany'
|
||||||
}
|
}
|
||||||
|
|
||||||
const uk = {
|
const uk = {
|
||||||
@@ -170,7 +176,8 @@ const uk = {
|
|||||||
change_phone_number: 'Змінити номер телефону',
|
change_phone_number: 'Змінити номер телефону',
|
||||||
sending: 'Надсилаю...',
|
sending: 'Надсилаю...',
|
||||||
resend_code: 'Повторно відправити код',
|
resend_code: 'Повторно відправити код',
|
||||||
verification_code_has_been_resent: 'Код підтвердження був повторно надісланий'
|
verification_code_has_been_resent: 'Код підтвердження був повторно надісланий',
|
||||||
|
email_has_been_sent: 'Електронний лист був відправлений'
|
||||||
}
|
}
|
||||||
|
|
||||||
const cs = {
|
const cs = {
|
||||||
@@ -195,10 +202,37 @@ const cs = {
|
|||||||
change_phone_number: 'Změnit telefonní číslo',
|
change_phone_number: 'Změnit telefonní číslo',
|
||||||
sending: 'Odesílání...',
|
sending: 'Odesílání...',
|
||||||
resend_code: 'Znovu odeslat kód',
|
resend_code: 'Znovu odeslat kód',
|
||||||
verification_code_has_been_resent: 'Ověřovací kód byl znovu odeslán'
|
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]
|
||||||
|
|
||||||
|
|||||||
@@ -350,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) {
|
||||||
|
|||||||
@@ -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.name}} has completed the "{{template.name}}" form',
|
'subject' => '{{template.name}} has been completed by {{submission.submitters}}',
|
||||||
'body' => "Hi,\n\n" \
|
'body' => "Hi,\n\n" \
|
||||||
"{{submitter.name}} 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>
|
||||||
|
|||||||
Whitespace-only changes.
@@ -34,10 +34,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>
|
||||||
<%= render 'send_sms', f: %>
|
<%= render 'send_sms', f: %>
|
||||||
|
<%= render('submitters_order', f:, template:) if Accounts.can_send_emails?(current_account) %>
|
||||||
<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>
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<% is_smtp_configured = Accounts.can_send_emails?(current_account) %>
|
<% is_smtp_configured = Accounts.can_send_emails?(current_account) %>
|
||||||
<%= f.label :send_email, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %>
|
<div class="flex justify-between items-center">
|
||||||
<%= f.check_box :send_email, id: uuid, class: 'base-checkbox', disabled: !is_smtp_configured, onchange: "window.message_field && message_field.classList.toggle('hidden', !event.currentTarget.checked)" %>
|
<%= f.label :send_email, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %>
|
||||||
<span class="label">Send Email</span>
|
<%= f.check_box :send_email, id: uuid, class: 'base-checkbox', disabled: !is_smtp_configured, checked: is_smtp_configured %>
|
||||||
<% end %>
|
<span class="label">Send emails</span>
|
||||||
|
<% end %>
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
<%= f.check_box :is_custom_message, onchange: "[this.form.querySelector('#message_field').classList.toggle('hidden', !event.currentTarget.checked)]", checked: false, class: 'hidden peer' %>
|
||||||
|
<span class="link peer-checked:hidden">Edit message</span>
|
||||||
|
</label>
|
||||||
|
<%= render 'email_stats' %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<% unless is_smtp_configured %>
|
<% unless is_smtp_configured %>
|
||||||
<div class="alert my-4">
|
<div class="alert my-4">
|
||||||
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
|
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
|
||||||
@@ -18,20 +27,20 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% unless AccountConfig.exists?(key: AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) %>
|
<% config = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) %>
|
||||||
<div id="message_field" class="card card-compact bg-base-200 hidden">
|
<div id="message_field" class="card card-compact bg-base-200 hidden">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="form-control space-y-2">
|
<div class="form-control space-y-2">
|
||||||
<span class="label-text">Hi there,</span>
|
<div class="form-control">
|
||||||
|
<%= f.label :subject, class: 'label' %>
|
||||||
|
<%= f.text_field :subject, value: config.value['subject'], required: true, class: '!text-sm base-input w-full' %>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<%= f.label :message, 'Body', class: 'label' %>
|
||||||
<autoresize-textarea>
|
<autoresize-textarea>
|
||||||
<%= f.text_area :message, value: format(SubmitterMailer::DEFAULT_MESSAGE, name: template.name), required: true, class: 'base-textarea !rounded-lg w-full' %>
|
<%= f.text_area :body, value: config.value['body'], required: true, class: 'base-textarea w-full', rows: 10 %>
|
||||||
</autoresize-textarea>
|
</autoresize-textarea>
|
||||||
<span class="label-text">
|
|
||||||
Thanks,
|
|
||||||
<br>
|
|
||||||
<%= current_account.name %>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
</div>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<% if template.submitters.size > 1 %>
|
||||||
|
<div class="form-control">
|
||||||
|
<%= f.label :preserve_order, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %>
|
||||||
|
<%= f.check_box :preserve_order, id: uuid, class: 'base-checkbox', checked: template.submissions.last&.submitters_order.in?(['preserved', nil]) %>
|
||||||
|
<span class="label">Preserve submitters order</span>
|
||||||
|
<span class="tooltip" data-tip="When checked, notifications will be sent to the second submitter once the form is completed by the previous submitter. Uncheck this option to send notifications to all submitters simultaneously right away.">
|
||||||
|
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
@@ -5,6 +5,12 @@
|
|||||||
<span class="text-3xl font-semibold focus:text-clip"><%= @submission.template.name %></span>
|
<span class="text-3xl font-semibold focus:text-clip"><%= @submission.template.name %></span>
|
||||||
</a>
|
</a>
|
||||||
<div class="space-x-3 flex items-center">
|
<div class="space-x-3 flex items-center">
|
||||||
|
<% if @submission.audit_trail.present? %>
|
||||||
|
<a href="<%= rails_blob_path(@submission.audit_trail) %>" class="white-button" target="_blank">
|
||||||
|
<%= svg_icon('external_link', class: 'w-6 h-6') %>
|
||||||
|
Audit Log
|
||||||
|
</a>
|
||||||
|
<% end %>
|
||||||
<% if last_submitter = @submission.submitters.to_a.select(&:completed_at?).max_by(&:completed_at) %>
|
<% if last_submitter = @submission.submitters.to_a.select(&:completed_at?).max_by(&:completed_at) %>
|
||||||
<download-button data-src="<%= submitter_download_index_path(last_submitter.slug) %>" class="base-button">
|
<download-button data-src="<%= submitter_download_index_path(last_submitter.slug) %>" class="base-button">
|
||||||
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
||||||
@@ -102,15 +108,15 @@
|
|||||||
<%= submitter&.completed_at? ? l(submitter.completed_at.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) : 'Not completed yet' %>
|
<%= submitter&.completed_at? ? l(submitter.completed_at.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) : 'Not completed yet' %>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<% if submitter && submitter.email && !submitter.completed_at %>
|
<% if submitter && submitter.email && !submitter.completed_at && can?(:update, submitter) %>
|
||||||
<div class="mt-2 mb-1">
|
<div class="mt-2 mb-1">
|
||||||
<%= button_to button_title(title: submitter.sent_at? ? 'Re-send Email' : 'Send Email', disabled_with: 'Sending'), submitter_send_email_index_path(submitter_slug: submitter.slug), class: 'btn btn-sm btn-primary w-full' %>
|
<%= button_to button_title(title: submitter.sent_at? ? 'Re-send Email' : 'Send Email', disabled_with: 'Sending'), submitter_send_email_index_path(submitter_slug: submitter.slug), class: 'btn btn-sm btn-primary w-full' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if submitter && submitter.phone && !submitter.completed_at %>
|
<% if submitter && submitter.phone && !submitter.completed_at && can?(:update, submitter) %>
|
||||||
<%= render 'send_sms_button', submitter: %>
|
<%= render 'send_sms_button', submitter: %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if submitter && !submitter.completed_at? %>
|
<% if submitter && !submitter.completed_at? && can?(:create, submitter) %>
|
||||||
<div class="mt-2 mb-1">
|
<div class="mt-2 mb-1">
|
||||||
<a class="btn btn-sm btn-primary w-full" target="_blank" href="<%= submit_form_path(slug: submitter.slug) %>">
|
<a class="btn btn-sm btn-primary w-full" target="_blank" href="<%= submit_form_path(slug: submitter.slug) %>">
|
||||||
Submit Form
|
Submit Form
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<% if @email_config %>
|
<% if @email_config %>
|
||||||
<%= auto_link(simple_format(h(ReplaceEmailVariables.call(@email_config.value['body'], submitter: @submitter)))) %>
|
<%= auto_link(simple_format(h(ReplaceEmailVariables.call(@email_config.value['body'], submitter: @submitter)))) %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<p>Hi <%= @user.first_name %>,</p>
|
<p>Hi there,</p>
|
||||||
<p><%= @submitter.email %> has completed the "<%= @submitter.submission.template.name %>" form.</p>
|
<p>"<%= @submitter.submission.template.name %>" form has been completed by <%= @submitter.submission.submitters.order(:completed_at).map { |e| e.name || e.email || e.phone }.join(', ') %>.</p>
|
||||||
<p><%= link_to 'View Submission', submission_url(@submitter.submission) %></p>
|
<p><%= link_to 'View Submission', submission_url(@submitter.submission) %></p>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
<% if @email_config %>
|
<% if @email_config || @body.present? %>
|
||||||
<%= auto_link(simple_format(h(ReplaceEmailVariables.call(@email_config.value['body'], submitter: @submitter)))) %>
|
<%= auto_link(simple_format(h(ReplaceEmailVariables.call(@body.presence || @email_config.value['body'], submitter: @submitter)))) %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<p>Hi there,</p>
|
<p>Hi there,</p>
|
||||||
<%= simple_format(@message) %>
|
<p>You have been invited to submit the "<%= @submitter.submission.template.name %>" form</p>
|
||||||
<p><%= link_to 'Submit Form', submit_form_url(slug: @submitter.slug) %></p>
|
<p><%= link_to 'Submit Form', submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email')) %></p>
|
||||||
<p>Please contact us by replying to this email if you didn't request this.</p>
|
<p>Please contact us by replying to this email if you didn't request this.</p>
|
||||||
<p>
|
<p>
|
||||||
Thanks,<br><%= @current_account.name %>
|
Thanks,<br><%= @current_account.name %>
|
||||||
|
|||||||
@@ -38,7 +38,9 @@
|
|||||||
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-sm btn-neutral text-white md:w-36 flex', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Link', copy_title_md: 'Copy', copied_title_md: 'Copied' %>
|
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-sm btn-neutral text-white md:w-36 flex', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Link', copy_title_md: 'Copy', copied_title_md: 'Copied' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<span class="btn btn-outline btn-sm w-20 md:w-24">View</span>
|
<span class="btn btn-outline btn-sm w-20 md:w-24">View</span>
|
||||||
<%= button_to button_title(title: nil, disabled_with: 'Remov', icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm', title: 'Delete', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %>
|
<% if !submission.deleted_at? && can?(:destroy, submission) %>
|
||||||
|
<%= button_to button_title(title: nil, disabled_with: 'Remov', icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm', title: 'Delete', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="space-y-1 w-full md:mr-2">
|
<div class="space-y-1 w-full md:mr-2">
|
||||||
@@ -110,7 +112,9 @@
|
|||||||
</form>
|
</form>
|
||||||
<% end %>
|
<% end %>
|
||||||
<span class="btn btn-outline btn-sm w-20 md:w-24">View</span>
|
<span class="btn btn-outline btn-sm w-20 md:w-24">View</span>
|
||||||
<%= button_to button_title(title: nil, disabled_with: 'Remov', icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm', title: 'Delete', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %>
|
<% unless submission.deleted_at? %>
|
||||||
|
<%= button_to button_title(title: nil, disabled_with: 'Remov', icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm', title: 'Delete', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</a>
|
</a>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<div class="h-36 relative group">
|
||||||
|
<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 class="pb-4 text-xl font-semibold" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;">
|
||||||
|
<%= template.name %>
|
||||||
|
</div>
|
||||||
|
<div class="pb-6 pt-1 space-y-1">
|
||||||
|
<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">
|
||||||
|
<% if template.deleted_at? && can?(:update, template) %>
|
||||||
|
<%= button_to template_restore_index_path(template), class: 'btn btn-xs hover:btn-outline bg-base-200 btn-circle' do %>
|
||||||
|
<%= svg_icon('rotate', class: 'w-4 h-4 enabled') %>
|
||||||
|
<%= svg_icon('loader', class: 'w-4 h-4 animate-spin disabled') %>
|
||||||
|
<% end %>
|
||||||
|
<% elsif can?(:update, template) %>
|
||||||
|
<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>
|
||||||
|
<% end %>
|
||||||
|
<% if can?(:create, template) %>
|
||||||
|
<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>
|
||||||
|
<% end %>
|
||||||
|
<% if !template.deleted_at? && can?(:destroy, template) %>
|
||||||
|
<%= 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', aria_label: 'Restore' do %>
|
||||||
|
<%= svg_icon('trash', class: 'w-4 h-4 enabled') %>
|
||||||
|
<%= svg_icon('loader', class: 'w-4 h-4 animate-spin disabled') %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<div class="flex flex-col items-start md:flex-row space-y-2 md:space-y-0 md:justify-between md:items-center mb-8">
|
||||||
|
<h1 class="text-4xl font-semibold mr-4" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;">
|
||||||
|
<%= template.name %>
|
||||||
|
<% if template.deleted_at? %>
|
||||||
|
<span class="badge badge-outline badge-lg align-middle">Archived</span>
|
||||||
|
<% end %>
|
||||||
|
</h1>
|
||||||
|
<div class="flex md:justify-between space-x-2 flex-none">
|
||||||
|
<% if !template.deleted_at? && can?(:destroy, template) %>
|
||||||
|
<%= button_to button_title(title: 'Remove', disabled_with: 'Removing', icon: svg_icon('trash', class: 'w-6 h-6')), template_path(template), class: 'btn btn-outline btn-sm', method: :delete, data: { turbo_confirm: 'Are you sure?' } %>
|
||||||
|
<% end %>
|
||||||
|
<% if can?(:create, template) %>
|
||||||
|
<%= link_to new_template_path(base_template_id: template.id), class: 'btn btn-outline btn-sm', data: { turbo_frame: :modal } do %>
|
||||||
|
<%= svg_icon('copy', class: 'w-6 h-6') %>
|
||||||
|
<span>Clone</span>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% if !template.deleted_at? && can?(:update, template) %>
|
||||||
|
<%= link_to edit_template_path(template), class: 'btn btn-outline btn-sm' do %>
|
||||||
|
<span class="flex items-center justify-center space-x-2">
|
||||||
|
<%= svg_icon('pencil', class: 'w-6 h-6') %>
|
||||||
|
<span>Edit</span>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% if template.deleted_at? && can?(:create, template) %>
|
||||||
|
<%= button_to button_title(title: 'Restore', disabled_with: 'Restoring', icon: svg_icon('rotate', class: 'w-6 h-6')), template_restore_index_path(template), class: 'btn btn-outline btn-sm' %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Loaded 100 of 139 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user