Compare commits

...
41 Commits
Author SHA1 Message Date
Alex Turchyn d757f06ff4 ensure template account is set 2023-09-19 00:01:07 +03:00
Alex Turchyn 132999176c add ability to customize message before sending 2023-09-18 23:23:51 +03:00
Alex Turchyn 2f74caf16a add notifications settings 2023-09-18 11:42:54 +03:00
Alex Turchyn 41d3eaf582 skip pdf reader errors 2023-09-17 17:00:21 +03:00
Alex Turchyn 907aaa9c30 use pg 15 in docker compose 2023-09-17 11:54:47 +03:00
Alex Turchyn 7bb241815f adjust submissions page 2023-09-17 10:38:29 +03:00
Alex Turchyn 84e879bad0 fix email typos 2023-09-17 10:28:08 +03:00
Alex Turchyn a1a0205d33 narrow devise forms 2023-09-17 10:28:07 +03:00
Alex Turchyn 6f8e88cdbd store template_submitters on form start 2023-09-17 09:09:15 +03:00
Alex Turchyn 2f1843151d add cancan for authorization 2023-09-17 00:45:57 +03:00
Alex Turchyn f89c50d096 allow to add multiple emails per submitter 2023-09-16 13:49:05 +03:00
Alex Turchyn ef618f8f49 add 2FA 2023-09-16 02:27:36 +03:00
Alex Turchyn b3662ddea8 fix webhook serializer 2023-09-15 12:40:32 +03:00
Alex Turchyn 53fa35fd01 add templates and submissions search 2023-09-15 02:11:48 +03:00
Alex Turchyn 7f6c0af0e8 add archived template and submission pages 2023-09-15 00:47:35 +03:00
Alex Turchyn 3faec9d174 add started and viewed webhook events 2023-09-15 00:47:25 +03:00
Alex Turchyn 9d9e134bb4 use default date format in pdf result 2023-09-14 10:35:43 +03:00
Alex Turchyn ec4f399fcc add template upload button 2023-09-14 10:35:43 +03:00
Alex Turchyn effa7b0fc7 add audit log button 2023-09-14 10:35:43 +03:00
Oleksandr Turchyn 016d9de87a Merge pull request #105 from imchairmanm/add_koyeb_target
add Koyeb to README as deployment target
2023-09-14 01:25:46 +03:00
Justin Ellingwood 71ff064356 add Koyeb to README as deployment target 2023-09-13 13:52:31 +01:00
Alex Turchyn 9100d48cc3 fix audit trail generation 2023-09-12 22:36:04 +03:00
Alex Turchyn 21b207cb1a fix submissions api 2023-09-11 09:49:59 +03:00
Alex Turchyn 3aa703032f adjust audit trail 2023-09-11 01:24:43 +03:00
Alex Turchyn fcfd2e13bd track form open after link click 2023-09-10 20:46:45 +03:00
Alex Turchyn cf6cacdc92 add audit trail 2023-09-10 20:46:44 +03:00
Alex Turchyn 8cabe31827 add submission tracking events 2023-09-10 17:49:10 +03:00
Alex Turchyn 62cea05084 add preserve submitters order option 2023-09-10 11:24:28 +03:00
Alex Turchyn 07e49f9f07 adjust default email 2023-09-09 23:17:41 +03:00
Alex Turchyn baad784818 send docs copy only if email present 2023-09-09 14:00:14 +03:00
Alex Turchyn 6eecd3afa4 fix email template submitters list 2023-09-09 11:44:04 +03:00
Alex Turchyn 8e90100871 fix submission download 2023-09-09 02:00:11 +03:00
Alex Turchyn 12c5138364 adjust add recipients view 2023-09-09 01:25:44 +03:00
Alex Turchyn 80ad166fd9 automatically send emails when all parties completed the form 2023-09-09 00:43:32 +03:00
Alex Turchyn 2fab6984ea fix form button loading indicator 2023-09-06 22:21:15 +03:00
Alex Turchyn 422639114b add bcc emails config 2023-09-06 21:41:23 +03:00
Alex Turchyn fca441878a send completed notification emails to a single user 2023-09-06 20:10:55 +03:00
Alex Turchyn 76578077e8 fix pdf rendering with rotate prop 2023-09-06 17:27:54 +03:00
Alex Turchyn bbd3618f0e add js alert translation 2023-09-05 22:28:50 +03:00
Alex Turchyn 47ed8db955 fix typo 2023-09-05 21:53:17 +03:00
Alex Turchyn fec6e2e35d add portuguese form translations 2023-09-05 21:49:12 +03:00
139 changed files with 1841 additions and 361 deletions

No files matched your search

+3
View File
@@ -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
View File
@@ -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 ./
+5
View File
@@ -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'
+18
View File
@@ -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
+2
View File
@@ -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
+7
View File
@@ -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])
+20 -15
View File
@@ -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 -6
View File
@@ -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|
+3 -1
View File
@@ -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
+10
View File
@@ -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?
+21 -4
View File
@@ -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
@@ -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
+17 -15
View File
@@ -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)
+44
View File
@@ -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.')
+4
View File
@@ -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
+10
View File
@@ -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
+1
View File
@@ -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
+7 -2
View File
@@ -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
+17 -15
View File
@@ -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
+12 -10
View File
@@ -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
+17 -16
View File
@@ -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|
+10 -8
View 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
+1 -1
View File
@@ -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
}) })
+41 -2
View File
@@ -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()
+43 -9
View File
@@ -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]
+1 -1
View File
@@ -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
@@ -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
+34 -18
View File
@@ -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
+5 -5
View File
@@ -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',
+10
View File
@@ -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
+55
View File
@@ -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
+9
View File
@@ -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
View File
@@ -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
+5 -3
View File
@@ -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>
+36 -37
View File
@@ -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">
+2 -2
View File
@@ -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: %>
+2 -2
View File
@@ -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: %>
+3 -3
View File
@@ -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| %>
+2 -2
View File
@@ -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] %>
+2 -2
View File
@@ -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] %>
+19
View File
@@ -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>
+1 -1
View File
@@ -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">
+7 -5
View File
@@ -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 %>
+6
View File
@@ -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

+4
View File
@@ -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

+5
View File
@@ -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

+6
View File
@@ -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

+13
View File
@@ -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 %>
+19
View File
@@ -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>
+20
View File
@@ -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>
+1
View File
@@ -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 %>
+11
View File
@@ -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>
+37 -16
View File
@@ -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 %>
+1 -1
View File
@@ -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>
+6 -3
View File
@@ -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.
+2 -1
View File
@@ -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>
+25 -16
View File
@@ -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 %>
+9 -3
View File
@@ -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 %>
+6 -2
View File
@@ -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>
+40
View File
@@ -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>
+30
View File
@@ -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