mirror of
https://github.com/docusealco/docuseal
synced 2026-11-05 06:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05d578d88f | ||
|
|
8773b30b10 | ||
|
|
1b636c5878 | ||
|
|
46e948ab99 | ||
|
|
889103aa5e | ||
|
|
558a14a5f2 | ||
|
|
72bbdb38bf | ||
|
|
3975a03cb4 | ||
|
|
e398327fb7 | ||
|
|
9266694d65 | ||
|
|
350c4b1c2a | ||
|
|
c3a933d87d | ||
|
|
970e53a5a3 | ||
|
|
520bac2ec7 | ||
|
|
e1eddd31c4 | ||
|
|
3c616a824c | ||
|
|
90d86b58c2 | ||
|
|
4332b4ebb8 | ||
|
|
e2f930f6f7 | ||
|
|
c303159c63 | ||
|
|
c1ce2bec00 | ||
|
|
3d2046f227 | ||
|
|
babaf1aa48 | ||
|
|
3f03d343ca | ||
|
|
110cdb5123 | ||
|
|
440d1114be | ||
|
|
9032517d4e | ||
|
|
7c71fefc47 | ||
|
|
e8bd8ff76f | ||
|
|
fed420cae1 | ||
|
|
910490528d | ||
|
|
ab810be250 | ||
|
|
178ee47093 | ||
|
|
6f90147a11 | ||
|
|
3ebaacbb7a | ||
|
|
64b39b53a2 | ||
|
|
2948910c25 | ||
|
|
1d20c79cf4 | ||
|
|
48f3b13f3e | ||
|
|
1fc1e9af16 | ||
|
|
ce70016603 | ||
|
|
e0e79e343d | ||
|
|
6568f81ee9 | ||
|
|
386f130d7a | ||
|
|
3ee41975fd | ||
|
|
d9f314898b | ||
|
|
4fc7412cac | ||
|
|
9adbfc52c2 | ||
|
|
1ad8d9196a | ||
|
|
e9172392b5 | ||
|
|
d6d94de328 | ||
|
|
77261f457c | ||
|
|
c258915031 | ||
|
|
c1db8bdd3b | ||
|
|
c700484ce8 | ||
|
|
b62bd200ed | ||
|
|
a164fe8dd8 | ||
|
|
bb40e872b3 | ||
|
|
bab80e9488 | ||
|
|
d090d79ebf | ||
|
|
2299ba4e29 | ||
|
|
d6474f0e6e | ||
|
|
e0a297cf81 | ||
|
|
40f5fa4217 |
No files matched your search
+1
-1
@@ -66,7 +66,7 @@ RSpec/ExampleLength:
|
|||||||
Max: 40
|
Max: 40
|
||||||
|
|
||||||
RSpec/MultipleMemoizedHelpers:
|
RSpec/MultipleMemoizedHelpers:
|
||||||
Max: 6
|
Max: 9
|
||||||
|
|
||||||
Metrics/BlockNesting:
|
Metrics/BlockNesting:
|
||||||
Max: 4
|
Max: 4
|
||||||
|
|||||||
+2
-2
@@ -41,7 +41,7 @@ ENV OPENSSL_CONF=/app/openssl_legacy.cnf
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils redis vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils redis libheif@edge vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
||||||
|
|
||||||
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
|
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
|
||||||
\n\
|
\n\
|
||||||
@@ -57,7 +57,7 @@ activate = 1' >> /app/openssl_legacy.cnf
|
|||||||
|
|
||||||
COPY ./Gemfile ./Gemfile.lock ./
|
COPY ./Gemfile ./Gemfile.lock ./
|
||||||
|
|
||||||
RUN apk add --no-cache build-base && bundle install && apk del build-base && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf
|
RUN apk add --no-cache build-base && bundle install && apk del --no-cache build-base && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf
|
||||||
|
|
||||||
COPY ./bin ./bin
|
COPY ./bin ./bin
|
||||||
COPY ./app ./app
|
COPY ./app ./app
|
||||||
|
|||||||
+56
-56
@@ -1,29 +1,29 @@
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (7.2.1)
|
actioncable (7.2.1.1)
|
||||||
actionpack (= 7.2.1)
|
actionpack (= 7.2.1.1)
|
||||||
activesupport (= 7.2.1)
|
activesupport (= 7.2.1.1)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
actionmailbox (7.2.1)
|
actionmailbox (7.2.1.1)
|
||||||
actionpack (= 7.2.1)
|
actionpack (= 7.2.1.1)
|
||||||
activejob (= 7.2.1)
|
activejob (= 7.2.1.1)
|
||||||
activerecord (= 7.2.1)
|
activerecord (= 7.2.1.1)
|
||||||
activestorage (= 7.2.1)
|
activestorage (= 7.2.1.1)
|
||||||
activesupport (= 7.2.1)
|
activesupport (= 7.2.1.1)
|
||||||
mail (>= 2.8.0)
|
mail (>= 2.8.0)
|
||||||
actionmailer (7.2.1)
|
actionmailer (7.2.1.1)
|
||||||
actionpack (= 7.2.1)
|
actionpack (= 7.2.1.1)
|
||||||
actionview (= 7.2.1)
|
actionview (= 7.2.1.1)
|
||||||
activejob (= 7.2.1)
|
activejob (= 7.2.1.1)
|
||||||
activesupport (= 7.2.1)
|
activesupport (= 7.2.1.1)
|
||||||
mail (>= 2.8.0)
|
mail (>= 2.8.0)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
actionpack (7.2.1)
|
actionpack (7.2.1.1)
|
||||||
actionview (= 7.2.1)
|
actionview (= 7.2.1.1)
|
||||||
activesupport (= 7.2.1)
|
activesupport (= 7.2.1.1)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
racc
|
racc
|
||||||
rack (>= 2.2.4, < 3.2)
|
rack (>= 2.2.4, < 3.2)
|
||||||
@@ -32,35 +32,35 @@ GEM
|
|||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.6)
|
rails-html-sanitizer (~> 1.6)
|
||||||
useragent (~> 0.16)
|
useragent (~> 0.16)
|
||||||
actiontext (7.2.1)
|
actiontext (7.2.1.1)
|
||||||
actionpack (= 7.2.1)
|
actionpack (= 7.2.1.1)
|
||||||
activerecord (= 7.2.1)
|
activerecord (= 7.2.1.1)
|
||||||
activestorage (= 7.2.1)
|
activestorage (= 7.2.1.1)
|
||||||
activesupport (= 7.2.1)
|
activesupport (= 7.2.1.1)
|
||||||
globalid (>= 0.6.0)
|
globalid (>= 0.6.0)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (7.2.1)
|
actionview (7.2.1.1)
|
||||||
activesupport (= 7.2.1)
|
activesupport (= 7.2.1.1)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.11)
|
erubi (~> 1.11)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.6)
|
rails-html-sanitizer (~> 1.6)
|
||||||
activejob (7.2.1)
|
activejob (7.2.1.1)
|
||||||
activesupport (= 7.2.1)
|
activesupport (= 7.2.1.1)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (7.2.1)
|
activemodel (7.2.1.1)
|
||||||
activesupport (= 7.2.1)
|
activesupport (= 7.2.1.1)
|
||||||
activerecord (7.2.1)
|
activerecord (7.2.1.1)
|
||||||
activemodel (= 7.2.1)
|
activemodel (= 7.2.1.1)
|
||||||
activesupport (= 7.2.1)
|
activesupport (= 7.2.1.1)
|
||||||
timeout (>= 0.4.0)
|
timeout (>= 0.4.0)
|
||||||
activestorage (7.2.1)
|
activestorage (7.2.1.1)
|
||||||
actionpack (= 7.2.1)
|
actionpack (= 7.2.1.1)
|
||||||
activejob (= 7.2.1)
|
activejob (= 7.2.1.1)
|
||||||
activerecord (= 7.2.1)
|
activerecord (= 7.2.1.1)
|
||||||
activesupport (= 7.2.1)
|
activesupport (= 7.2.1.1)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
activesupport (7.2.1)
|
activesupport (7.2.1.1)
|
||||||
base64
|
base64
|
||||||
bigdecimal
|
bigdecimal
|
||||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||||
@@ -320,7 +320,7 @@ GEM
|
|||||||
mysql2 (0.5.6)
|
mysql2 (0.5.6)
|
||||||
net-http-persistent (4.0.4)
|
net-http-persistent (4.0.4)
|
||||||
connection_pool (~> 2.2)
|
connection_pool (~> 2.2)
|
||||||
net-imap (0.4.16)
|
net-imap (0.4.17)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
net-pop (0.1.2)
|
net-pop (0.1.2)
|
||||||
@@ -372,7 +372,7 @@ GEM
|
|||||||
puma (6.4.3)
|
puma (6.4.3)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rack (3.1.7)
|
rack (3.1.8)
|
||||||
rack-proxy (0.7.7)
|
rack-proxy (0.7.7)
|
||||||
rack
|
rack
|
||||||
rack-session (2.0.0)
|
rack-session (2.0.0)
|
||||||
@@ -382,20 +382,20 @@ GEM
|
|||||||
rackup (2.1.0)
|
rackup (2.1.0)
|
||||||
rack (>= 3)
|
rack (>= 3)
|
||||||
webrick (~> 1.8)
|
webrick (~> 1.8)
|
||||||
rails (7.2.1)
|
rails (7.2.1.1)
|
||||||
actioncable (= 7.2.1)
|
actioncable (= 7.2.1.1)
|
||||||
actionmailbox (= 7.2.1)
|
actionmailbox (= 7.2.1.1)
|
||||||
actionmailer (= 7.2.1)
|
actionmailer (= 7.2.1.1)
|
||||||
actionpack (= 7.2.1)
|
actionpack (= 7.2.1.1)
|
||||||
actiontext (= 7.2.1)
|
actiontext (= 7.2.1.1)
|
||||||
actionview (= 7.2.1)
|
actionview (= 7.2.1.1)
|
||||||
activejob (= 7.2.1)
|
activejob (= 7.2.1.1)
|
||||||
activemodel (= 7.2.1)
|
activemodel (= 7.2.1.1)
|
||||||
activerecord (= 7.2.1)
|
activerecord (= 7.2.1.1)
|
||||||
activestorage (= 7.2.1)
|
activestorage (= 7.2.1.1)
|
||||||
activesupport (= 7.2.1)
|
activesupport (= 7.2.1.1)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 7.2.1)
|
railties (= 7.2.1.1)
|
||||||
rails-dom-testing (2.2.0)
|
rails-dom-testing (2.2.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
minitest
|
minitest
|
||||||
@@ -410,9 +410,9 @@ GEM
|
|||||||
actionview (> 3.1)
|
actionview (> 3.1)
|
||||||
activesupport (> 3.1)
|
activesupport (> 3.1)
|
||||||
railties (> 3.1)
|
railties (> 3.1)
|
||||||
railties (7.2.1)
|
railties (7.2.1.1)
|
||||||
actionpack (= 7.2.1)
|
actionpack (= 7.2.1.1)
|
||||||
activesupport (= 7.2.1)
|
activesupport (= 7.2.1.1)
|
||||||
irb (~> 1.13)
|
irb (~> 1.13)
|
||||||
rackup (>= 1.0.0)
|
rackup (>= 1.0.0)
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
@@ -559,7 +559,7 @@ GEM
|
|||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
xpath (3.2.0)
|
xpath (3.2.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
zeitwerk (2.6.18)
|
zeitwerk (2.7.0)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
aarch64-linux
|
aarch64-linux
|
||||||
|
|||||||
@@ -25,12 +25,12 @@
|
|||||||
DocuSeal is an open source platform that provides secure and efficient digital document signing and processing. Create PDF forms to have them filled and signed online on any device with an easy-to-use, mobile-optimized web tool.
|
DocuSeal is an open source platform that provides secure and efficient digital document signing and processing. Create PDF forms to have them filled and signed online on any device with an easy-to-use, mobile-optimized web tool.
|
||||||
</p>
|
</p>
|
||||||
<h2 align="center">
|
<h2 align="center">
|
||||||
<a href="https://demo.docuseal.co">✨ Live Demo</a>
|
<a href="https://demo.docuseal.tech">✨ Live Demo</a>
|
||||||
<span>|</span>
|
<span>|</span>
|
||||||
<a href="https://docuseal.co/sign_up">☁️ Try in Cloud</a>
|
<a href="https://docuseal.com/sign_up">☁️ Try in Cloud</a>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
[](https://demo.docuseal.co)
|
[](https://demo.docuseal.tech)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- PDF form fields builder (WYSIWYG)
|
- PDF form fields builder (WYSIWYG)
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# Reporting a Vulnerability
|
# Reporting a Vulnerability
|
||||||
|
|
||||||
If you come across any security concern or vulnarability, please report the information via email to security@docuseal.co instead of opening a GitHub issue. We will promptly respond and will collaborate with you to validate the issue, and resolve it ASAP.
|
If you come across any security concern or vulnarability, please report the information via email to security@docuseal.com instead of opening a GitHub issue. We will promptly respond and will collaborate with you to validate the issue, and resolve it ASAP.
|
||||||
|
|
||||||
**We have a bug bounty program to reward security researchers.**
|
**We have a bug bounty program to reward security researchers.**
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ class AccountConfigsController < ApplicationController
|
|||||||
AccountConfig::ALLOW_TYPED_SIGNATURE,
|
AccountConfig::ALLOW_TYPED_SIGNATURE,
|
||||||
AccountConfig::FORCE_MFA,
|
AccountConfig::FORCE_MFA,
|
||||||
AccountConfig::ALLOW_TO_RESUBMIT,
|
AccountConfig::ALLOW_TO_RESUBMIT,
|
||||||
|
AccountConfig::ALLOW_TO_DECLINE_KEY,
|
||||||
AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
|
AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
|
||||||
AccountConfig::ESIGNING_PREFERENCE_KEY,
|
AccountConfig::ESIGNING_PREFERENCE_KEY,
|
||||||
AccountConfig::FORM_WITH_CONFETTI_KEY,
|
AccountConfig::FORM_WITH_CONFETTI_KEY,
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ module Api
|
|||||||
render json: { error: 'Too many requests' }, status: :too_many_requests
|
render json: { error: 'Too many requests' }, status: :too_many_requests
|
||||||
end
|
end
|
||||||
|
|
||||||
if Rails.env.production?
|
unless Rails.env.development?
|
||||||
rescue_from CanCan::AccessDenied do |e|
|
rescue_from CanCan::AccessDenied do |e|
|
||||||
render json: { error: e.message }, status: :forbidden
|
render json: { error: access_denied_error_message(e) }, status: :forbidden
|
||||||
end
|
end
|
||||||
|
|
||||||
rescue_from JSON::ParserError do |e|
|
rescue_from JSON::ParserError do |e|
|
||||||
@@ -39,6 +39,33 @@ module Api
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def access_denied_error_message(error)
|
||||||
|
return 'Not authorized' if request.headers['X-Auth-Token'].blank?
|
||||||
|
return 'Not authorized' unless error.subject.is_a?(ActiveRecord::Base)
|
||||||
|
return 'Not authorized' unless error.subject.respond_to?(:account_id)
|
||||||
|
|
||||||
|
linked_account_record_exists =
|
||||||
|
if current_user.account.testing?
|
||||||
|
current_user.account.linked_account_accounts.where(account_type: 'testing')
|
||||||
|
.exists?(account_id: error.subject.account_id)
|
||||||
|
else
|
||||||
|
current_user.account.testing_accounts.exists?(id: error.subject.account_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
return 'Not authorized' unless linked_account_record_exists
|
||||||
|
|
||||||
|
object_name = error.subject.model_name.human
|
||||||
|
id = error.subject.id
|
||||||
|
|
||||||
|
if current_user.account.testing?
|
||||||
|
"#{object_name} #{id} not found using testing API key; Use production API key to " \
|
||||||
|
"access production #{object_name.downcase.pluralize}."
|
||||||
|
else
|
||||||
|
"#{object_name} #{id} not found using production API key; Use testing API key to " \
|
||||||
|
"access testing #{object_name.downcase.pluralize}."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def paginate(relation, field: :id)
|
def paginate(relation, field: :id)
|
||||||
result = relation.order(field => :desc)
|
result = relation.order(field => :desc)
|
||||||
.limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min)
|
.limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min)
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api
|
||||||
|
class SubmissionDocumentsController < ApiBaseController
|
||||||
|
load_and_authorize_resource :submission
|
||||||
|
|
||||||
|
def index
|
||||||
|
documents =
|
||||||
|
if @submission.submitters.all?(&:completed_at?)
|
||||||
|
last_submitter = @submission.submitters.max_by(&:completed_at)
|
||||||
|
|
||||||
|
if last_submitter.documents_attachments.blank?
|
||||||
|
last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(submitter)
|
||||||
|
end
|
||||||
|
|
||||||
|
last_submitter.documents_attachments
|
||||||
|
else
|
||||||
|
values_hash = Submissions::GeneratePreviewAttachments.build_values_hash(@submission)
|
||||||
|
|
||||||
|
if @submission.preview_documents.present? &&
|
||||||
|
@submission.preview_documents.all? { |s| s.metadata['values_hash'] == values_hash }
|
||||||
|
@submission.preview_documents
|
||||||
|
else
|
||||||
|
ApplicationRecord.no_touching do
|
||||||
|
@submission.preview_documents.each(&:destroy)
|
||||||
|
end
|
||||||
|
|
||||||
|
Submissions::GeneratePreviewAttachments.call(@submission, values_hash:)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ActiveRecord::Associations::Preloader.new(
|
||||||
|
records: documents,
|
||||||
|
associations: [:blob]
|
||||||
|
).call
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
id: @submission.id,
|
||||||
|
documents: documents.map do |attachment|
|
||||||
|
{ name: attachment.filename.base, url: ActiveStorage::Blob.proxy_url(attachment.blob) }
|
||||||
|
end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -74,13 +74,14 @@ module Api
|
|||||||
Submissions.send_signature_requests(submissions)
|
Submissions.send_signature_requests(submissions)
|
||||||
|
|
||||||
submissions.each do |submission|
|
submissions.each do |submission|
|
||||||
if submission.submitters.all?(&:completed_at?) && submission.submitters.last
|
submission.submitters.each do |submitter|
|
||||||
ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => submission.submitters.last.id })
|
ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => submitter.id }) if submitter.completed_at?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: build_create_json(submissions)
|
render json: build_create_json(submissions)
|
||||||
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
|
rescue Submitters::NormalizeValues::BaseError, Submissions::CreateFromSubmitters::BaseError,
|
||||||
|
DownloadUtils::UnableToDownload => e
|
||||||
Rollbar.warning(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
render json: { error: e.message }, status: :unprocessable_entity
|
render json: { error: e.message }, status: :unprocessable_entity
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ module Api
|
|||||||
end&.dig('uuid')
|
end&.dig('uuid')
|
||||||
|
|
||||||
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
|
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
|
||||||
|
submitter.name = attrs[:name] if attrs.key?(:name)
|
||||||
|
|
||||||
if attrs.key?(:phone)
|
if attrs.key?(:phone)
|
||||||
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '')
|
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '')
|
||||||
|
|||||||
@@ -20,10 +20,7 @@ module Api
|
|||||||
pdf = HexaPDF::Document.new(io: StringIO.new(file))
|
pdf = HexaPDF::Document.new(io: StringIO.new(file))
|
||||||
|
|
||||||
trusted_certs = Accounts.load_trusted_certs(current_account)
|
trusted_certs = Accounts.load_trusted_certs(current_account)
|
||||||
|
is_checksum_found = CompletedDocument.exists?(sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(file)))
|
||||||
is_checksum_found = ActiveStorage::Attachment.joins(:blob)
|
|
||||||
.where(name: 'documents', record_type: 'Submitter')
|
|
||||||
.exists?(blob: { checksum: Digest::MD5.base64digest(file) })
|
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
checksum_status: is_checksum_found ? 'verified' : 'not_found',
|
checksum_status: is_checksum_found ? 'verified' : 'not_found',
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ class ApplicationController < ActionController::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def default_url_options
|
def default_url_options
|
||||||
|
if request.domain == 'docuseal.com'
|
||||||
|
return { host: 'docuseal.com', protocol: ENV['FORCE_SSL'].present? ? 'https' : 'http' }
|
||||||
|
end
|
||||||
|
|
||||||
Docuseal.default_url_options
|
Docuseal.default_url_options
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class EmailSmtpSettingsController < ApplicationController
|
|||||||
|
|
||||||
def create
|
def create
|
||||||
if @encrypted_config.update(email_configs)
|
if @encrypted_config.update(email_configs)
|
||||||
SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email']).deliver_now!
|
SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email'] || current_user.email).deliver_now!
|
||||||
|
|
||||||
redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved')
|
redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved')
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ class PreviewDocumentPageController < ActionController::API
|
|||||||
|
|
||||||
return head :not_found unless attachment
|
return head :not_found unless attachment
|
||||||
|
|
||||||
preview_image = attachment.preview_images.joins(:blob).find_by(blob: { filename: "#{params[:id]}#{FORMAT}" })
|
preview_image = attachment.preview_images.joins(:blob)
|
||||||
|
.find_by(blob: { filename: ["#{params[:id]}.png", "#{params[:id]}.jpg"] })
|
||||||
|
|
||||||
return redirect_to preview_image.url, allow_other_host: true if preview_image
|
return redirect_to preview_image.url, allow_other_host: true if preview_image
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,13 @@ class SubmissionsController < ApplicationController
|
|||||||
def show
|
def show
|
||||||
@submission = Submissions.preload_with_pages(@submission)
|
@submission = Submissions.preload_with_pages(@submission)
|
||||||
|
|
||||||
|
unless @submission.submitters.all?(&:completed_at?)
|
||||||
|
ActiveRecord::Associations::Preloader.new(
|
||||||
|
records: [@submission],
|
||||||
|
associations: [submitters: :start_form_submission_events]
|
||||||
|
).call
|
||||||
|
end
|
||||||
|
|
||||||
render :show, layout: 'plain'
|
render :show, layout: 'plain'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,6 @@ class SubmissionsDashboardController < ApplicationController
|
|||||||
@submissions = @submissions.pending if params[:status] == 'pending'
|
@submissions = @submissions.pending if params[:status] == 'pending'
|
||||||
@submissions = @submissions.completed if params[:status] == 'completed'
|
@submissions = @submissions.completed if params[:status] == 'completed'
|
||||||
|
|
||||||
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
|
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events).order(id: :desc))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmittersController < ApplicationController
|
||||||
|
load_and_authorize_resource :submitter, only: %i[edit update]
|
||||||
|
|
||||||
|
def edit
|
||||||
|
@submitter_email_message =
|
||||||
|
if @submitter.preferences['email_message_uuid'].present?
|
||||||
|
@submitter.account.email_messages.find_by(uuid: @submitter.preferences['email_message_uuid'])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
submission = @submitter.submission
|
||||||
|
|
||||||
|
if @submitter.submission_events.exists?(event_type: 'start_form') || submission.archived_at? || submission.expired?
|
||||||
|
return redirect_back fallback_location: submission_path(submission), alert: I18n.t('submitter_cannot_be_updated')
|
||||||
|
end
|
||||||
|
|
||||||
|
if submitter_params.values.all?(&:blank?)
|
||||||
|
return redirect_back fallback_location: submission_path(submission),
|
||||||
|
alert: I18n.t('at_least_one_field_must_be_filled')
|
||||||
|
end
|
||||||
|
|
||||||
|
if params[:is_custom_message] != '1'
|
||||||
|
params.delete(:subject)
|
||||||
|
params.delete(:body)
|
||||||
|
end
|
||||||
|
|
||||||
|
submitter_preferences = Submitters.normalize_preferences(@submitter.account, current_user, params)
|
||||||
|
|
||||||
|
if submitter_preferences.key?('email_message_uuid')
|
||||||
|
@submitter.preferences['email_message_uuid'] = submitter_preferences['email_message_uuid']
|
||||||
|
end
|
||||||
|
|
||||||
|
assign_submitter_attrs(@submitter, submitter_params)
|
||||||
|
|
||||||
|
if @submitter.save
|
||||||
|
maybe_resend_email_sms(@submitter, params)
|
||||||
|
|
||||||
|
redirect_back fallback_location: submission_path(submission), notice: I18n.t('changes_have_been_saved')
|
||||||
|
else
|
||||||
|
redirect_back fallback_location: submission_path(submission), alert: I18n.t('unable_to_save')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def maybe_resend_email_sms(submitter, params)
|
||||||
|
if params[:send_email] == '1' && submitter.email.present?
|
||||||
|
is_sent_recently = Docuseal.multitenant? &&
|
||||||
|
EmailEvent.exists?(email: submitter.email,
|
||||||
|
tag: 'submitter_invitation',
|
||||||
|
emailable: submitter,
|
||||||
|
event_type: 'send',
|
||||||
|
created_at: 4.hours.ago..Time.current)
|
||||||
|
|
||||||
|
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => submitter.id) unless is_sent_recently
|
||||||
|
end
|
||||||
|
|
||||||
|
return if submitter.phone.blank?
|
||||||
|
return unless params[:send_sms] == '1'
|
||||||
|
|
||||||
|
SendSubmitterInvitationSmsJob.perform_async('submitter_id' => submitter.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def assign_submitter_attrs(submitter, attrs)
|
||||||
|
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '') if attrs.key?(:phone)
|
||||||
|
|
||||||
|
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
|
||||||
|
|
||||||
|
submitter.name = attrs[:name] if attrs.key?(:name)
|
||||||
|
|
||||||
|
submitter
|
||||||
|
end
|
||||||
|
|
||||||
|
def submitter_params
|
||||||
|
params.require(:submitter).permit(:email, :name, :phone).transform_values(&:strip)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -15,7 +15,7 @@ class TemplatesController < ApplicationController
|
|||||||
submissions = submissions.pending if params[:status] == 'pending'
|
submissions = submissions.pending if params[:status] == 'pending'
|
||||||
submissions = submissions.completed if params[:status] == 'completed'
|
submissions = submissions.completed if params[:status] == 'completed'
|
||||||
|
|
||||||
@pagy, @submissions = pagy(submissions.preload(:submitters).order(id: :desc))
|
@pagy, @submissions = pagy(submissions.preload(submitters: :start_form_submission_events).order(id: :desc))
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
redirect_to root_path
|
redirect_to root_path
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ class UsersController < ApplicationController
|
|||||||
def index
|
def index
|
||||||
@users =
|
@users =
|
||||||
if params[:status] == 'archived'
|
if params[:status] == 'archived'
|
||||||
@users.archived
|
@users.archived.where.not(role: 'integration')
|
||||||
|
elsif params[:status] == 'integration'
|
||||||
|
@users.active.where(role: 'integration')
|
||||||
else
|
else
|
||||||
@users.active
|
@users.active.where.not(role: 'integration')
|
||||||
end
|
end
|
||||||
|
|
||||||
@pagy, @users = pagy(@users.where(account: current_account).order(id: :desc))
|
@pagy, @users = pagy(@users.where(account: current_account).order(id: :desc))
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import SearchInput from './elements/search_input'
|
|||||||
import ToggleAttribute from './elements/toggle_attribute'
|
import ToggleAttribute from './elements/toggle_attribute'
|
||||||
import LinkedInput from './elements/linked_input'
|
import LinkedInput from './elements/linked_input'
|
||||||
import CheckboxGroup from './elements/checkbox_group'
|
import CheckboxGroup from './elements/checkbox_group'
|
||||||
|
import MaskedInput from './elements/masked_input'
|
||||||
|
|
||||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||||
|
|
||||||
@@ -95,6 +96,7 @@ safeRegisterElement('search-input', SearchInput)
|
|||||||
safeRegisterElement('toggle-attribute', ToggleAttribute)
|
safeRegisterElement('toggle-attribute', ToggleAttribute)
|
||||||
safeRegisterElement('linked-input', LinkedInput)
|
safeRegisterElement('linked-input', LinkedInput)
|
||||||
safeRegisterElement('checkbox-group', CheckboxGroup)
|
safeRegisterElement('checkbox-group', CheckboxGroup)
|
||||||
|
safeRegisterElement('masked-input', MaskedInput)
|
||||||
|
|
||||||
safeRegisterElement('template-builder', class extends HTMLElement {
|
safeRegisterElement('template-builder', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
|
|||||||
@@ -130,7 +130,3 @@ button[disabled] .enabled {
|
|||||||
outline-offset: 3px;
|
outline-offset: 3px;
|
||||||
outline-color: hsl(var(--bc) / 0.2);
|
outline-color: hsl(var(--bc) / 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
select:required:invalid {
|
|
||||||
color: gray !important;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
const maskedToken = this.input.value
|
||||||
|
|
||||||
|
this.input.addEventListener('focus', () => {
|
||||||
|
this.input.value = this.dataset.token
|
||||||
|
this.input.select()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.input.addEventListener('focusout', () => {
|
||||||
|
this.input.value = maskedToken
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get input () {
|
||||||
|
return this.querySelector('input')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ safeRegisterElement('submission-form', class extends HTMLElement {
|
|||||||
requireSigningReason: this.dataset.requireSigningReason === 'true',
|
requireSigningReason: this.dataset.requireSigningReason === 'true',
|
||||||
withConfetti: this.dataset.withConfetti !== 'false',
|
withConfetti: this.dataset.withConfetti !== 'false',
|
||||||
withDisclosure: this.dataset.withDisclosure === 'true',
|
withDisclosure: this.dataset.withDisclosure === 'true',
|
||||||
|
reuseSignature: this.dataset.reuseSignature !== 'false',
|
||||||
withTypedSignature: this.dataset.withTypedSignature !== 'false',
|
withTypedSignature: this.dataset.withTypedSignature !== 'false',
|
||||||
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
||||||
rememberSignature: this.dataset.rememberSignature === 'true',
|
rememberSignature: this.dataset.rememberSignature === 'true',
|
||||||
|
|||||||
@@ -31,10 +31,6 @@ button[disabled] .enabled {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
select:required:invalid {
|
|
||||||
@apply text-gray-300;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-bordered {
|
.input-bordered {
|
||||||
@apply border-base-content/20;
|
@apply border-base-content/20;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -405,7 +405,8 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.field.preferences?.font_size) {
|
if (this.field.preferences?.font_size) {
|
||||||
style.fontSize = this.field.preferences.font_size + 'pt'
|
style.fontSize = `clamp(4pt, 1.6vw, ${this.field.preferences.font_size}pt)`
|
||||||
|
style.lineHeight = `clamp(6pt, 2.0vw, ${parseInt(this.field.preferences.font_size) + 3}pt)`
|
||||||
}
|
}
|
||||||
|
|
||||||
return style
|
return style
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
v-if="isDemo"
|
v-if="isDemo"
|
||||||
href="https://docuseal.co/sign_up"
|
href="https://docuseal.com/sign_up"
|
||||||
class="white-button flex items-center space-x-1 w-full"
|
class="white-button flex items-center space-x-1 w-full"
|
||||||
>
|
>
|
||||||
<IconLogin />
|
<IconLogin />
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
:name="`values[${field.uuid}]`"
|
:name="`values[${field.uuid}]`"
|
||||||
@keydown.enter="onEnter"
|
@keydown.enter="onEnter"
|
||||||
@focus="$emit('focus')"
|
@focus="$emit('focus')"
|
||||||
|
@paste="onPaste"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -98,6 +99,25 @@ export default {
|
|||||||
this.$emit('submit')
|
this.$emit('submit')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onPaste (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
let pasteData = e.clipboardData.getData('text').trim()
|
||||||
|
|
||||||
|
if (pasteData.match(/^\d{2}\.\d{2}\.\d{4}$/)) {
|
||||||
|
pasteData = pasteData.split('.').reverse().join('-')
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedDate = new Date(pasteData)
|
||||||
|
|
||||||
|
if (!isNaN(parsedDate)) {
|
||||||
|
const inputEl = this.$refs.input
|
||||||
|
|
||||||
|
inputEl.valueAsDate = new Date(parsedDate.getTime() - parsedDate.getTimezoneOffset() * 60000)
|
||||||
|
|
||||||
|
inputEl.dispatchEvent(new Event('input', { bubbles: true }))
|
||||||
|
}
|
||||||
|
},
|
||||||
setCurrentDate () {
|
setCurrentDate () {
|
||||||
const inputEl = this.$refs.input
|
const inputEl = this.$refs.input
|
||||||
|
|
||||||
|
|||||||
@@ -115,7 +115,7 @@
|
|||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:show-field-names="showFieldNames"
|
:show-field-names="showFieldNames"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
@submit="submitStep"
|
@submit="!isSubmitting && submitStep()"
|
||||||
@focus="scrollIntoField(currentField)"
|
@focus="scrollIntoField(currentField)"
|
||||||
/>
|
/>
|
||||||
<div v-else-if="currentField.type === 'select'">
|
<div v-else-if="currentField.type === 'select'">
|
||||||
@@ -152,6 +152,7 @@
|
|||||||
dir="auto"
|
dir="auto"
|
||||||
:required="currentField.required"
|
:required="currentField.required"
|
||||||
class="select base-input !text-2xl w-full text-center font-normal"
|
class="select base-input !text-2xl w-full text-center font-normal"
|
||||||
|
:class="{ 'text-gray-300': !values[currentField.uuid] }"
|
||||||
:name="`values[${currentField.uuid}]`"
|
:name="`values[${currentField.uuid}]`"
|
||||||
@change="values[currentField.uuid] = $event.target.value"
|
@change="values[currentField.uuid] = $event.target.value"
|
||||||
@focus="scrollIntoField(currentField)"
|
@focus="scrollIntoField(currentField)"
|
||||||
@@ -159,6 +160,7 @@
|
|||||||
<option
|
<option
|
||||||
value=""
|
value=""
|
||||||
:selected="!values[currentField.uuid]"
|
:selected="!values[currentField.uuid]"
|
||||||
|
class="text-gray-300"
|
||||||
>
|
>
|
||||||
{{ t('select_your_option') }}
|
{{ t('select_your_option') }}
|
||||||
</option>
|
</option>
|
||||||
@@ -167,6 +169,7 @@
|
|||||||
:key="option.uuid"
|
:key="option.uuid"
|
||||||
:selected="values[currentField.uuid] == option.value"
|
:selected="values[currentField.uuid] == option.value"
|
||||||
:value="option.value"
|
:value="option.value"
|
||||||
|
class="text-base-content"
|
||||||
>
|
>
|
||||||
{{ option.value }}
|
{{ option.value }}
|
||||||
</option>
|
</option>
|
||||||
@@ -392,7 +395,7 @@
|
|||||||
:default-value="submitter.phone"
|
:default-value="submitter.phone"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
@focus="scrollIntoField(currentField)"
|
@focus="scrollIntoField(currentField)"
|
||||||
@submit="submitStep"
|
@submit="!isSubmitting && submitStep()"
|
||||||
/>
|
/>
|
||||||
<PaymentStep
|
<PaymentStep
|
||||||
v-else-if="currentField.type === 'payment'"
|
v-else-if="currentField.type === 'payment'"
|
||||||
@@ -404,7 +407,7 @@
|
|||||||
:values="values"
|
:values="values"
|
||||||
@attached="attachments.push($event)"
|
@attached="attachments.push($event)"
|
||||||
@focus="scrollIntoField(currentField)"
|
@focus="scrollIntoField(currentField)"
|
||||||
@submit="submitStep"
|
@submit="!isSubmitting && submitStep()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -1104,6 +1107,10 @@ export default {
|
|||||||
const currentFieldUuids = this.currentStepFields.map((f) => f.uuid)
|
const currentFieldUuids = this.currentStepFields.map((f) => f.uuid)
|
||||||
const currentFieldType = this.currentField.type
|
const currentFieldType = this.currentField.type
|
||||||
|
|
||||||
|
if (!formData && !this.$refs.form.checkValidity()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (this.dryRun) {
|
if (this.dryRun) {
|
||||||
currentFieldUuids.forEach((fieldUuid) => {
|
currentFieldUuids.forEach((fieldUuid) => {
|
||||||
this.submittedValues[fieldUuid] = this.values[fieldUuid]
|
this.submittedValues[fieldUuid] = this.values[fieldUuid]
|
||||||
@@ -1142,13 +1149,15 @@ export default {
|
|||||||
async submitStep () {
|
async submitStep () {
|
||||||
this.isSubmitting = true
|
this.isSubmitting = true
|
||||||
|
|
||||||
|
const submitStep = this.currentStep
|
||||||
|
|
||||||
const stepPromise = ['signature', 'phone', 'initials', 'payment'].includes(this.currentField.type)
|
const stepPromise = ['signature', 'phone', 'initials', 'payment'].includes(this.currentField.type)
|
||||||
? this.$refs.currentStep.submit
|
? this.$refs.currentStep.submit
|
||||||
: () => Promise.resolve({})
|
: () => Promise.resolve({})
|
||||||
|
|
||||||
stepPromise().then(async () => {
|
stepPromise().then(async () => {
|
||||||
const emptyRequiredField = this.stepFields.find((fields, index) => {
|
const emptyRequiredField = this.stepFields.find((fields, index) => {
|
||||||
if (index >= this.currentStep) {
|
if (index >= submitStep) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1158,7 +1167,7 @@ export default {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const formData = new FormData(this.$refs.form)
|
const formData = new FormData(this.$refs.form)
|
||||||
const isLastStep = this.currentStep === this.stepFields.length - 1
|
const isLastStep = submitStep === this.stepFields.length - 1
|
||||||
|
|
||||||
if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length) {
|
if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length) {
|
||||||
formData.append('completed', 'true')
|
formData.append('completed', 'true')
|
||||||
@@ -1183,7 +1192,7 @@ export default {
|
|||||||
return Promise.reject(new Error(data.error))
|
return Promise.reject(new Error(data.error))
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextStep = (isLastStep && emptyRequiredField) || this.stepFields[this.currentStep + 1]
|
const nextStep = (isLastStep && emptyRequiredField) || this.stepFields[submitStep + 1]
|
||||||
|
|
||||||
if (nextStep) {
|
if (nextStep) {
|
||||||
if (this.alwaysMinimize) {
|
if (this.alwaysMinimize) {
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -61,30 +61,55 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<div
|
||||||
v-show="!isCodeSent"
|
v-show="!isCodeSent"
|
||||||
:id="field.uuid"
|
class="flex w-full rounded-full outline-neutral-content outline-2 outline-offset-2 focus-within:outline"
|
||||||
ref="phone"
|
|
||||||
:value="modelValue || defaultValue"
|
|
||||||
:readonly="!!defaultValue"
|
|
||||||
class="base-input !text-2xl w-full"
|
|
||||||
autocomplete="tel"
|
|
||||||
pattern="^\+[0-9\s\-]+$"
|
|
||||||
type="tel"
|
|
||||||
inputmode="tel"
|
|
||||||
:required="field.required"
|
|
||||||
placeholder="+1 234 567-8900"
|
|
||||||
:name="`values[${field.uuid}]`"
|
|
||||||
@invalid="$event.target.value ? $event.target.setCustomValidity(`${t('use_international_format')}...`) : ''"
|
|
||||||
@input="[$event.target.setCustomValidity(''), $emit('update:model-value', $event.target.value)]"
|
|
||||||
@focus="$emit('focus')"
|
|
||||||
>
|
>
|
||||||
|
<div class="relative inline-block">
|
||||||
|
<div class="btn bg-base-200 border border-neutral-300 text-2xl whitespace-nowrap font-normal rounded-l-full">
|
||||||
|
{{ selectedCountry.flag }} +{{ selectedCountry.dial }}
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
class="absolute top-0 bottom-0 right-0 left-0 opacity-0 w-full h-full cursor-pointer"
|
||||||
|
:disabled="!!defaultValue"
|
||||||
|
@change="onCountrySelect(countries.find((country) => country.flag === $event.target.value))"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="(country, index) in countries"
|
||||||
|
:key="index"
|
||||||
|
:value="country.flag"
|
||||||
|
>
|
||||||
|
{{ country.flag }} {{ country.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
:name="`values[${field.uuid}]`"
|
||||||
|
:value="fullInternationalPhoneValue"
|
||||||
|
hidden
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:id="field.uuid"
|
||||||
|
ref="phone"
|
||||||
|
:value="phoneValue"
|
||||||
|
:readonly="!!defaultValue"
|
||||||
|
class="base-input !text-2xl !rounded-l-none !border-l-0 !outline-none w-full"
|
||||||
|
autocomplete="tel"
|
||||||
|
type="tel"
|
||||||
|
inputmode="tel"
|
||||||
|
:required="field.required"
|
||||||
|
placeholder="234 567-8900"
|
||||||
|
@input="onPhoneInput"
|
||||||
|
@focus="$emit('focus')"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import MarkdownContent from './markdown_content'
|
import MarkdownContent from './markdown_content'
|
||||||
|
import phoneData from './phone_data'
|
||||||
|
|
||||||
function throttle (func, delay) {
|
function throttle (func, delay) {
|
||||||
let lastCallTime = 0
|
let lastCallTime = 0
|
||||||
@@ -144,13 +169,73 @@ export default {
|
|||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isCodeSent: false,
|
isCodeSent: false,
|
||||||
isResendLoading: false
|
isResendLoading: false,
|
||||||
|
phoneValue: this.modelValue || this.defaultValue || '',
|
||||||
|
selectedCountry: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
countries () {
|
||||||
|
return phoneData.map(([name, dial, flag, tz]) => {
|
||||||
|
return { name, dial, flag, tz }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
countriesDialIndex () {
|
||||||
|
return this.countries.reduce((acc, item) => {
|
||||||
|
acc[item.dial] ||= item
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
dialCodesRegexp () {
|
||||||
|
const dialCodes = this.countries.map((country) => country.dial).sort((a, b) => b.length - a.length)
|
||||||
|
|
||||||
|
return new RegExp(`^\\+(${dialCodes.join('|')})`)
|
||||||
|
},
|
||||||
|
detectedPhoneValueDialCode () {
|
||||||
|
return (this.phoneValue || '').replace(/[^\d+]/g, '').match(this.dialCodesRegexp)?.[1]
|
||||||
|
},
|
||||||
|
fullInternationalPhoneValue () {
|
||||||
|
if (this.detectedPhoneValueDialCode) {
|
||||||
|
return this.phoneValue
|
||||||
|
} else if (this.phoneValue) {
|
||||||
|
return ['+', this.selectedCountry.dial, this.phoneValue].filter(Boolean).join('')
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
const browserTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||||
|
|
||||||
|
if (this.detectedPhoneValueDialCode) {
|
||||||
|
this.selectedCountry = this.countriesDialIndex[this.detectedPhoneValueDialCode]
|
||||||
|
} else if (browserTimeZone) {
|
||||||
|
const tz = browserTimeZone.split('/')[1]
|
||||||
|
|
||||||
|
this.selectedCountry = this.countries.find((country) => country.tz.includes(tz)) || this.countries[0]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
emitSubmit: throttle(function (e) {
|
emitSubmit: throttle(function (e) {
|
||||||
this.$emit('submit')
|
this.$emit('submit')
|
||||||
}, 1000),
|
}, 1000),
|
||||||
|
onCountrySelect (country) {
|
||||||
|
if (this.selectedCountry.flag !== country.flag) {
|
||||||
|
this.phoneValue = this.phoneValue.replace(`+${this.selectedCountry.dial}`, `+${country.dial}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectedCountry = country
|
||||||
|
|
||||||
|
this.$refs.phone.focus()
|
||||||
|
},
|
||||||
|
onPhoneInput (e) {
|
||||||
|
this.phoneValue = e.target.value
|
||||||
|
|
||||||
|
if (this.detectedPhoneValueDialCode) {
|
||||||
|
this.selectedCountry = this.countriesDialIndex[this.detectedPhoneValueDialCode]
|
||||||
|
}
|
||||||
|
},
|
||||||
onInputCode (e) {
|
onInputCode (e) {
|
||||||
if (e.target.value.length === 6) {
|
if (e.target.value.length === 6) {
|
||||||
this.emitSubmit()
|
this.emitSubmit()
|
||||||
@@ -171,14 +256,14 @@ export default {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
submitter_slug: this.submitterSlug,
|
submitter_slug: this.submitterSlug,
|
||||||
locale: this.locale,
|
locale: this.locale,
|
||||||
phone: this.$refs.phone.value
|
phone: this.fullInternationalPhoneValue
|
||||||
}),
|
}),
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
}).then(async (resp) => {
|
}).then(async (resp) => {
|
||||||
if (resp.status === 422) {
|
if (resp.status === 422) {
|
||||||
const data = await resp.json()
|
const data = await resp.json()
|
||||||
|
|
||||||
alert(this.t('number_phone_is_invalid').replace('{number}', this.$refs.phone.value))
|
alert(this.t('number_phone_is_invalid').replace('{number}', this.fullInternationalPhoneValue))
|
||||||
|
|
||||||
return Promise.reject(new Error(data.error))
|
return Promise.reject(new Error(data.error))
|
||||||
}
|
}
|
||||||
@@ -189,13 +274,13 @@ export default {
|
|||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.$refs.phone.value.toString().startsWith('+')) {
|
if (!this.fullInternationalPhoneValue.toString().startsWith('+')) {
|
||||||
alert(this.t('use_international_format'))
|
alert(this.t('use_international_format'))
|
||||||
|
|
||||||
return Promise.reject(new Error('phone invalid'))
|
return Promise.reject(new Error('phone invalid'))
|
||||||
} else if (!this.isCodeSent) {
|
} else if (!this.isCodeSent) {
|
||||||
return this.sendVerificationCode().then(() => {
|
return this.sendVerificationCode().then(() => {
|
||||||
this.$emit('update:model-value', this.$refs.phone.value)
|
this.$emit('update:model-value', this.fullInternationalPhoneValue)
|
||||||
|
|
||||||
this.isCodeSent = true
|
this.isCodeSent = true
|
||||||
|
|
||||||
|
|||||||
@@ -197,6 +197,7 @@
|
|||||||
<select
|
<select
|
||||||
v-if="requireSigningReason && !isOtherReason"
|
v-if="requireSigningReason && !isOtherReason"
|
||||||
class="select base-input !text-2xl w-full mt-6 text-center"
|
class="select base-input !text-2xl w-full mt-6 text-center"
|
||||||
|
:class="{ 'text-gray-300': !reason }"
|
||||||
required
|
required
|
||||||
:name="`values[${field.preferences.reason_field_uuid}]`"
|
:name="`values[${field.preferences.reason_field_uuid}]`"
|
||||||
@change="$event.target.value === 'other' ? [reason = '', isOtherReason = true] : $emit('update:reason', $event.target.value)"
|
@change="$event.target.value === 'other' ? [reason = '', isOtherReason = true] : $emit('update:reason', $event.target.value)"
|
||||||
@@ -205,6 +206,7 @@
|
|||||||
value=""
|
value=""
|
||||||
disabled
|
disabled
|
||||||
:selected="!reason"
|
:selected="!reason"
|
||||||
|
class="text-gray-300"
|
||||||
>
|
>
|
||||||
{{ t('select_a_reason') }}
|
{{ t('select_a_reason') }}
|
||||||
</option>
|
</option>
|
||||||
@@ -213,10 +215,14 @@
|
|||||||
:key="option"
|
:key="option"
|
||||||
:value="option"
|
:value="option"
|
||||||
:selected="reason === option"
|
:selected="reason === option"
|
||||||
|
class="text-base-content"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</option>
|
</option>
|
||||||
<option value="other">
|
<option
|
||||||
|
value="other"
|
||||||
|
class="text-base-content"
|
||||||
|
>
|
||||||
{{ t('other') }}
|
{{ t('other') }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -693,6 +693,7 @@ export default {
|
|||||||
'de-DE': 'DD.MM.YYYY',
|
'de-DE': 'DD.MM.YYYY',
|
||||||
'fr-FR': 'DD/MM/YYYY',
|
'fr-FR': 'DD/MM/YYYY',
|
||||||
'it-IT': 'DD/MM/YYYY',
|
'it-IT': 'DD/MM/YYYY',
|
||||||
|
'en-GB': 'DD/MM/YYYY',
|
||||||
'es-ES': 'DD/MM/YYYY'
|
'es-ES': 'DD/MM/YYYY'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<select
|
<select
|
||||||
class="select select-bordered select-sm w-full bg-white h-11 pl-4 text-base font-normal"
|
class="select select-bordered select-sm w-full bg-white h-11 pl-4 text-base font-normal"
|
||||||
|
:class="{ 'text-gray-300': !condition.field_uuid }"
|
||||||
required
|
required
|
||||||
@change="[
|
@change="[
|
||||||
condition.field_uuid = $event.target.value,
|
condition.field_uuid = $event.target.value,
|
||||||
@@ -68,6 +69,7 @@
|
|||||||
v-for="f in fields"
|
v-for="f in fields"
|
||||||
:key="f.uuid"
|
:key="f.uuid"
|
||||||
:value="f.uuid"
|
:value="f.uuid"
|
||||||
|
class="text-base-content"
|
||||||
:selected="condition.field_uuid === f.uuid"
|
:selected="condition.field_uuid === f.uuid"
|
||||||
>
|
>
|
||||||
{{ f.name || buildDefaultName(f, template.fields) }}
|
{{ f.name || buildDefaultName(f, template.fields) }}
|
||||||
@@ -88,8 +90,9 @@
|
|||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<select
|
<select
|
||||||
v-if="conditionField(condition)?.options?.length"
|
v-if="['radio', 'select', 'multiple'].includes(conditionField(condition)?.type) && conditionField(condition)?.options"
|
||||||
class="select select-bordered select-sm w-full bg-white h-11 pl-4 text-base font-normal"
|
class="select select-bordered select-sm w-full bg-white h-11 pl-4 text-base font-normal"
|
||||||
|
:class="{ 'text-gray-300': !condition.value }"
|
||||||
required
|
required
|
||||||
@change="condition.value = $event.target.value"
|
@change="condition.value = $event.target.value"
|
||||||
>
|
>
|
||||||
@@ -105,6 +108,7 @@
|
|||||||
:key="option.uuid"
|
:key="option.uuid"
|
||||||
:value="option.uuid"
|
:value="option.uuid"
|
||||||
:selected="condition.value === option.uuid"
|
:selected="condition.value === option.uuid"
|
||||||
|
class="text-base-content"
|
||||||
>
|
>
|
||||||
{{ option.value || `${t('option')} ${index + 1}` }}
|
{{ option.value || `${t('option')} ${index + 1}` }}
|
||||||
</option>
|
</option>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
const en = {
|
const en = {
|
||||||
editable: 'Editable',
|
editable: 'Editable',
|
||||||
|
recurrent: 'Recurrent',
|
||||||
|
one_off: 'One-off',
|
||||||
search_field: 'Search field',
|
search_field: 'Search field',
|
||||||
field_not_found: 'Field not found',
|
field_not_found: 'Field not found',
|
||||||
clear: 'Clear',
|
clear: 'Clear',
|
||||||
@@ -151,6 +153,8 @@ const en = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
|
recurrent: 'Recurrente',
|
||||||
|
one_off: 'Único',
|
||||||
editable: 'Editable',
|
editable: 'Editable',
|
||||||
search_field: 'Campo de búsqueda',
|
search_field: 'Campo de búsqueda',
|
||||||
field_not_found: 'Campo no encontrado',
|
field_not_found: 'Campo no encontrado',
|
||||||
@@ -303,6 +307,8 @@ const es = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
|
ricorrente: 'Ricorrente',
|
||||||
|
una_volta: 'Una volta',
|
||||||
editable: 'Modificabile',
|
editable: 'Modificabile',
|
||||||
search_field: 'Campo di ricerca',
|
search_field: 'Campo di ricerca',
|
||||||
field_not_found: 'Campo non trovato',
|
field_not_found: 'Campo non trovato',
|
||||||
@@ -455,6 +461,8 @@ const it = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
|
recurrent: 'Recurrente',
|
||||||
|
one_off: 'Único',
|
||||||
editable: 'Editável',
|
editable: 'Editável',
|
||||||
search_field: 'Campo de busca',
|
search_field: 'Campo de busca',
|
||||||
field_not_found: 'Campo não encontrado',
|
field_not_found: 'Campo não encontrado',
|
||||||
@@ -607,6 +615,8 @@ const pt = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
|
recurrent: 'Récurrent',
|
||||||
|
one_off: 'Ponctuel',
|
||||||
editable: 'Éditable',
|
editable: 'Éditable',
|
||||||
search_field: 'Champ de recherche',
|
search_field: 'Champ de recherche',
|
||||||
field_not_found: 'Champ non trouvé',
|
field_not_found: 'Champ non trouvé',
|
||||||
@@ -759,6 +769,8 @@ const fr = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
|
wiederkehrend: 'Wiederkehrend',
|
||||||
|
einmalig: 'Einmalig',
|
||||||
editable: 'Bearbeitbar',
|
editable: 'Bearbeitbar',
|
||||||
search_field: 'Suchfeld',
|
search_field: 'Suchfeld',
|
||||||
field_not_found: 'Feld nicht gefunden',
|
field_not_found: 'Feld nicht gefunden',
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
<div class="flex">
|
<div class="flex">
|
||||||
<select
|
<select
|
||||||
class="base-select !select-sm !h-10"
|
class="base-select !select-sm !h-10"
|
||||||
|
:class="{ '!text-gray-300': !mapping.field_name }"
|
||||||
required
|
required
|
||||||
@change="mapping.field_name = $event.target.value"
|
@change="mapping.field_name = $event.target.value"
|
||||||
>
|
>
|
||||||
@@ -65,6 +66,7 @@
|
|||||||
:key="index"
|
:key="index"
|
||||||
:value="field.name"
|
:value="field.name"
|
||||||
:selected="mapping.field_name === field.name"
|
:selected="mapping.field_name === field.name"
|
||||||
|
class="text-base-content"
|
||||||
>
|
>
|
||||||
{{ field.name }}
|
{{ field.name }}
|
||||||
</option>
|
</option>
|
||||||
@@ -75,6 +77,7 @@
|
|||||||
<div class="w-full relative">
|
<div class="w-full relative">
|
||||||
<select
|
<select
|
||||||
class="base-select !select-sm !h-10"
|
class="base-select !select-sm !h-10"
|
||||||
|
:class="{ '!text-gray-300': !mapping.column_index }"
|
||||||
required
|
required
|
||||||
@change="mapping.column_index = parseInt($event.target.value)"
|
@change="mapping.column_index = parseInt($event.target.value)"
|
||||||
>
|
>
|
||||||
@@ -93,6 +96,7 @@
|
|||||||
v-if="column"
|
v-if="column"
|
||||||
:value="index"
|
:value="index"
|
||||||
:selected="index === mapping.column_index"
|
:selected="index === mapping.column_index"
|
||||||
|
class="text-base-content"
|
||||||
>
|
>
|
||||||
{{ column }}
|
{{ column }}
|
||||||
</option>
|
</option>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
@click="closeDropdown"
|
@click="closeDropdown"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
v-if="!('price_id' in field.preferences)"
|
||||||
class="py-1.5 px-1 relative"
|
class="py-1.5 px-1 relative"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
@@ -58,6 +59,13 @@
|
|||||||
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
|
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
|
||||||
@blur="save"
|
@blur="save"
|
||||||
>
|
>
|
||||||
|
<input
|
||||||
|
v-else-if="'price_id' in field.preferences"
|
||||||
|
v-model="field.preferences.price_id"
|
||||||
|
placeholder="Price ID: price_XXXXX"
|
||||||
|
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
|
||||||
|
@blur="save"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
v-else
|
v-else
|
||||||
v-model="field.preferences.price"
|
v-model="field.preferences.price"
|
||||||
@@ -74,6 +82,23 @@
|
|||||||
>
|
>
|
||||||
{{ t('price') }}
|
{{ t('price') }}
|
||||||
</label>
|
</label>
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="hover:underline"
|
||||||
|
style="font-size: 11px"
|
||||||
|
:class="{'underline': !('price_id' in field.preferences)}"
|
||||||
|
@click="delete field.preferences.price_id"
|
||||||
|
>{{ t('one_off') }}</a>
|
||||||
|
<span class="h-2.5 border-l border-base-content mx-1" />
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="hover:underline"
|
||||||
|
style="font-size: 11px"
|
||||||
|
:class="{'underline': ('price_id' in field.preferences)}"
|
||||||
|
@click="field.preferences.price_id ??= ''"
|
||||||
|
>{{ t('recurrent') }}</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="!isConnected || isOauthSuccess"
|
v-if="!isConnected || isOauthSuccess"
|
||||||
@@ -158,7 +183,10 @@
|
|||||||
data-turbo="false"
|
data-turbo="false"
|
||||||
>{{ t('learn_more') }}</a>
|
>{{ t('learn_more') }}</a>
|
||||||
</div>
|
</div>
|
||||||
<li class="mb-1">
|
<li
|
||||||
|
v-if="!('price_id' in field.preferences)"
|
||||||
|
class="mb-1"
|
||||||
|
>
|
||||||
<label
|
<label
|
||||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||||
@click="$emit('click-formula')"
|
@click="$emit('click-formula')"
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class GeneratePreviewImagesJob
|
||||||
|
include Sidekiq::Job
|
||||||
|
|
||||||
|
sidekiq_options queue: :images
|
||||||
|
|
||||||
|
def perform(params = {})
|
||||||
|
attachment = ActiveStorage::Attachment.find(params['attachment_id'])
|
||||||
|
|
||||||
|
max_page = [attachment.metadata['pdf']['number_of_pages'].to_i - 1,
|
||||||
|
Templates::ProcessDocument::MAX_NUMBER_OF_PAGES_PROCESSED].min
|
||||||
|
|
||||||
|
Templates::ProcessDocument.generate_document_preview_images(attachment, attachment.download, (1..max_page),
|
||||||
|
concurrency: 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -6,6 +6,8 @@ class ProcessSubmitterCompletionJob
|
|||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submitter = Submitter.find(params['submitter_id'])
|
submitter = Submitter.find(params['submitter_id'])
|
||||||
|
|
||||||
|
create_completed_submitter!(submitter)
|
||||||
|
|
||||||
is_all_completed = !submitter.submission.submitters.exists?(completed_at: nil)
|
is_all_completed = !submitter.submission.submitters.exists?(completed_at: nil)
|
||||||
|
|
||||||
if !is_all_completed && submitter.submission.submitters_order_preserved?
|
if !is_all_completed && submitter.submission.submitters_order_preserved?
|
||||||
@@ -24,9 +26,42 @@ class ProcessSubmitterCompletionJob
|
|||||||
enqueue_completed_emails(submitter)
|
enqueue_completed_emails(submitter)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_completed_documents!(submitter)
|
||||||
|
|
||||||
enqueue_completed_webhooks(submitter, is_all_completed:)
|
enqueue_completed_webhooks(submitter, is_all_completed:)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def create_completed_submitter!(submitter)
|
||||||
|
completed_submitter = CompletedSubmitter.find_or_initialize_by(submitter_id: submitter.id)
|
||||||
|
|
||||||
|
return completed_submitter if completed_submitter.persisted?
|
||||||
|
|
||||||
|
submission = submitter.submission
|
||||||
|
|
||||||
|
completed_submitter.assign_attributes(
|
||||||
|
submission_id: submitter.submission_id,
|
||||||
|
account_id: submission.account_id,
|
||||||
|
template_id: submission.template_id,
|
||||||
|
source: submission.source,
|
||||||
|
sms_count: submitter.submission_events.where(event_type: %w[send_sms send_2fa_sms]).count,
|
||||||
|
completed_at: submitter.completed_at
|
||||||
|
)
|
||||||
|
|
||||||
|
completed_submitter.save!
|
||||||
|
|
||||||
|
completed_submitter
|
||||||
|
rescue ActiveRecord::RecordNotUnique
|
||||||
|
retry
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_completed_documents!(submitter)
|
||||||
|
submitter.documents.filter_map do |attachment|
|
||||||
|
next if attachment.metadata['sha256'].blank?
|
||||||
|
|
||||||
|
CompletedDocument.find_or_create_by!(sha256: attachment.metadata['sha256'], submitter_id: submitter.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def enqueue_completed_webhooks(submitter, is_all_completed: false)
|
def enqueue_completed_webhooks(submitter, is_all_completed: false)
|
||||||
webhook_config = Accounts.load_webhook_config(submitter.account)
|
webhook_config = Accounts.load_webhook_config(submitter.account)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ApplicationMailer < ActionMailer::Base
|
class ApplicationMailer < ActionMailer::Base
|
||||||
default from: 'DocuSeal <info@docuseal.co>'
|
default from: 'DocuSeal <info@docuseal.com>'
|
||||||
layout 'mailer'
|
layout 'mailer'
|
||||||
|
|
||||||
register_interceptor ActionMailerConfigsInterceptor
|
register_interceptor ActionMailerConfigsInterceptor
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
|
|
||||||
class SettingsMailer < ApplicationMailer
|
class SettingsMailer < ApplicationMailer
|
||||||
def smtp_successful_setup(email)
|
def smtp_successful_setup(email)
|
||||||
mail(to: email, subject: 'SMTP has been configured')
|
mail(to: email, from: email, subject: 'SMTP has been configured')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -28,6 +28,7 @@ class AccountConfig < ApplicationRecord
|
|||||||
FORCE_MFA = 'force_mfa'
|
FORCE_MFA = 'force_mfa'
|
||||||
ALLOW_TYPED_SIGNATURE = 'allow_typed_signature'
|
ALLOW_TYPED_SIGNATURE = 'allow_typed_signature'
|
||||||
ALLOW_TO_RESUBMIT = 'allow_to_resubmit'
|
ALLOW_TO_RESUBMIT = 'allow_to_resubmit'
|
||||||
|
ALLOW_TO_DECLINE_KEY = 'allow_to_decline'
|
||||||
SUBMITTER_REMINDERS = 'submitter_reminders'
|
SUBMITTER_REMINDERS = 'submitter_reminders'
|
||||||
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
|
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
|
||||||
FORM_COMPLETED_MESSAGE_KEY = 'form_completed_message'
|
FORM_COMPLETED_MESSAGE_KEY = 'form_completed_message'
|
||||||
@@ -41,6 +42,7 @@ class AccountConfig < ApplicationRecord
|
|||||||
WITH_SIGNATURE_ID = 'with_signature_id'
|
WITH_SIGNATURE_ID = 'with_signature_id'
|
||||||
WITH_AUDIT_VALUES_KEY = 'with_audit_values'
|
WITH_AUDIT_VALUES_KEY = 'with_audit_values'
|
||||||
REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason'
|
REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason'
|
||||||
|
REUSE_SIGNATURE_KEY = 'reuse_signature'
|
||||||
COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key'
|
COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key'
|
||||||
|
|
||||||
DEFAULT_VALUES = {
|
DEFAULT_VALUES = {
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: completed_documents
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# sha256 :string not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# submitter_id :bigint not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_completed_documents_on_sha256 (sha256)
|
||||||
|
# index_completed_documents_on_submitter_id (submitter_id)
|
||||||
|
#
|
||||||
|
class CompletedDocument < ApplicationRecord
|
||||||
|
belongs_to :submitter, optional: true
|
||||||
|
|
||||||
|
has_one :completed_submitter, primary_key: :submitter_id, inverse_of: :completed_documents, dependent: :destroy
|
||||||
|
end
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: completed_submitters
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# completed_at :datetime not null
|
||||||
|
# sms_count :integer not null
|
||||||
|
# source :string not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# account_id :bigint not null
|
||||||
|
# submission_id :bigint not null
|
||||||
|
# submitter_id :bigint not null
|
||||||
|
# template_id :bigint not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_completed_submitters_on_account_id (account_id)
|
||||||
|
# index_completed_submitters_on_submitter_id (submitter_id) UNIQUE
|
||||||
|
#
|
||||||
|
class CompletedSubmitter < ApplicationRecord
|
||||||
|
belongs_to :submitter
|
||||||
|
belongs_to :submission
|
||||||
|
belongs_to :account
|
||||||
|
belongs_to :template
|
||||||
|
|
||||||
|
has_many :completed_documents, dependent: :destroy,
|
||||||
|
primary_key: :submitter_id,
|
||||||
|
foreign_key: :submitter_id,
|
||||||
|
inverse_of: :submitter
|
||||||
|
end
|
||||||
@@ -55,6 +55,8 @@ class Submission < ApplicationRecord
|
|||||||
has_one_attached :audit_trail
|
has_one_attached :audit_trail
|
||||||
has_one_attached :combined_document
|
has_one_attached :combined_document
|
||||||
|
|
||||||
|
has_many_attached :preview_documents
|
||||||
|
|
||||||
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')) },
|
||||||
through: :template, source: :documents_attachments
|
through: :template, source: :documents_attachments
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ class Submitter < ApplicationRecord
|
|||||||
|
|
||||||
has_many :document_generation_events, dependent: :destroy
|
has_many :document_generation_events, dependent: :destroy
|
||||||
has_many :submission_events, dependent: :destroy
|
has_many :submission_events, dependent: :destroy
|
||||||
|
has_many :start_form_submission_events, -> { where(event_type: :start_form) },
|
||||||
|
class_name: 'SubmissionEvent', dependent: :destroy, inverse_of: :submitter
|
||||||
|
|
||||||
scope :completed, -> { where.not(completed_at: nil) }
|
scope :completed, -> { where.not(completed_at: nil) }
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ class User < ApplicationRecord
|
|||||||
|
|
||||||
EMAIL_REGEXP = /[^@;,<>\s]+@[^@;,<>\s]+/
|
EMAIL_REGEXP = /[^@;,<>\s]+@[^@;,<>\s]+/
|
||||||
|
|
||||||
|
FULL_EMAIL_REGEXP =
|
||||||
|
/\A[a-z0-9][\.']?(?:(?:[a-z0-9_-]+[\.\+'])*[a-z0-9_-]+)*@(?:[a-z0-9]+[\.-])*[a-z0-9]+\.[a-z]{2,}\z/i
|
||||||
|
|
||||||
has_one_attached :signature
|
has_one_attached :signature
|
||||||
has_one_attached :initials
|
has_one_attached :initials
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::ALLOW_TO_DECLINE_KEY) %>
|
||||||
|
<% if can?(:manage, account_config) %>
|
||||||
|
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
|
||||||
|
<%= f.hidden_field :key %>
|
||||||
|
<div class="flex items-center justify-between py-2.5">
|
||||||
|
<span>
|
||||||
|
<%= t('allow_to_decline_documents') %>
|
||||||
|
</span>
|
||||||
|
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false, onchange: 'this.form.requestSubmit()' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::FORM_PREFILL_SIGNATURE_KEY) %>
|
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::FORM_PREFILL_SIGNATURE_KEY) %>
|
||||||
<% if can?(:manage, account_config) %>
|
<% if can?(:manage, account_config) %>
|
||||||
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
|
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
|
||||||
|
|||||||
@@ -10,8 +10,11 @@
|
|||||||
<label for="api_key" class="text-sm font-semibold">X-Auth-Token</label>
|
<label for="api_key" class="text-sm font-semibold">X-Auth-Token</label>
|
||||||
<div class="flex flex-col md:flex-row gap-4">
|
<div class="flex flex-col md:flex-row gap-4">
|
||||||
<div class="flex w-full space-x-4">
|
<div class="flex w-full space-x-4">
|
||||||
<input id="api_key" type="text" value="<%= current_user.access_token.token %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
|
<% token = current_user.access_token.token %>
|
||||||
<%= render 'shared/clipboard_copy', icon: 'copy', text: current_user.access_token.token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
<masked-input class="block w-full" data-token="<%= token %>">
|
||||||
|
<input id="api_key" type="text" value="<%= token.sub(token[5..], '*' * token[5..].size) %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
|
||||||
|
</masked-input>
|
||||||
|
<%= render 'shared/clipboard_copy', icon: 'copy', text: token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
||||||
</div>
|
</div>
|
||||||
<%= button_to button_title(title: t('rotate'), disabled_with: t('rotate'), icon: svg_icon('reload', class: 'w-6 h-6')), settings_api_index_path, class: 'white-button w-full', data: { turbo_confirm: t('remove_existing_api_token_and_generated_a_new_one_are_you_sure_') } %>
|
<%= button_to button_title(title: t('rotate'), disabled_with: t('rotate'), icon: svg_icon('reload', class: 'w-6 h-6')), settings_api_index_path, class: 'white-button w-full', data: { turbo_confirm: t('remove_existing_api_token_and_generated_a_new_one_are_you_sure_') } %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="join">
|
<div class="join">
|
||||||
<a href="https://docuseal.co<%= request.fullpath.gsub('docuseal.eu', 'docuseal.co') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.co' %>">
|
<a href="https://docuseal.com<%= request.fullpath.gsub('docuseal.eu', 'docuseal.com') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.com' %>">
|
||||||
<%= svg_icon 'world', class: 'w-5 h-5' %>
|
<%= svg_icon 'world', class: 'w-5 h-5' %>
|
||||||
Global
|
Global
|
||||||
</a>
|
</a>
|
||||||
<a href="https://docuseal.eu<%= request.fullpath.gsub('docuseal.co', 'docuseal.eu') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.eu' %>">
|
<a href="https://docuseal.eu<%= request.fullpath.gsub('docuseal.com', 'docuseal.eu') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.eu' %>">
|
||||||
<%= svg_icon 'eu_flag', class: 'w-5 h-5' %>
|
<%= svg_icon 'eu_flag', class: 'w-5 h-5' %>
|
||||||
Europe
|
Europe
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= ff.label :from_email, t('send_from_email'), class: 'label' %>
|
<%= ff.label :from_email, t('send_from_email'), class: 'label' %>
|
||||||
<%= ff.email_field :from_email, value: value['from_email'], required: true, class: 'base-input' %>
|
<%= ff.email_field :from_email, value: value['from_email'], required: !Docuseal.multitenant?, class: 'base-input' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="form-control pt-2">
|
<div class="form-control pt-2">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<% if signed_in? %>
|
<% if signed_in? %>
|
||||||
<div class="space-x-4 flex items-center">
|
<div class="space-x-4 flex items-center">
|
||||||
<% if Docuseal.demo? %>
|
<% if Docuseal.demo? %>
|
||||||
<a href="https://docuseal.co/sign_up" class="btn btn-neutral btn-sm btn-outline inline-flex items-center justify-center" style="height: 37px">
|
<a href="https://docuseal.com/sign_up" class="btn btn-neutral btn-sm btn-outline inline-flex items-center justify-center" style="height: 37px">
|
||||||
<%= t('sign_up') %>
|
<%= t('sign_up') %>
|
||||||
</a>
|
</a>
|
||||||
<span class="hidden sm:inline">
|
<span class="hidden sm:inline">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="text-center px-2">
|
<div class="text-center px-2">
|
||||||
<% if local_assigns[:with_counter] %>
|
<% if local_assigns[:with_counter] %>
|
||||||
<% count = Submitter.where.not(completed_at: nil).distinct.count(:submission_id) %>
|
<% count = CompletedSubmitter.distinct.count(:submission_id) %>
|
||||||
<% if count > 1 %>
|
<% if count > 1 %>
|
||||||
<%= t('count_documents_signed_with_html', count:) %>
|
<%= t('count_documents_signed_with_html', count:) %>
|
||||||
<% else %>
|
<% else %>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
</submitters-autocomplete>
|
</submitters-autocomplete>
|
||||||
<submitters-autocomplete data-field="phone">
|
<submitters-autocomplete data-field="phone">
|
||||||
<linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
<linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
||||||
<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="base-input !h-10 mt-1.5 w-full" placeholder="<%= "#{t('phone')} (#{t('optional')})" %>" id="detailed_phone_<%= item['uuid'] %>">
|
<input type="tel" pattern="^\+[0-9\s\-]+$" oninvalid="this.value ? this.setCustomValidity('<%= t('use_international_format_1xxx_') %>') : ''" oninput="this.setCustomValidity('')" name="submission[1][submitters][][phone]" autocomplete="off" class="base-input !h-10 mt-1.5 w-full" placeholder="<%= "#{t('phone')} (#{t('optional')})" %>" id="detailed_phone_<%= item['uuid'] %>">
|
||||||
</linked-input>
|
</linked-input>
|
||||||
</submitters-autocomplete>
|
</submitters-autocomplete>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
||||||
<submitters-autocomplete data-field="phone">
|
<submitters-autocomplete data-field="phone">
|
||||||
<linked-input data-target-id="<%= "phone_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
<linked-input data-target-id="<%= "phone_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
||||||
<%= tag.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: 'base-input !h-10 w-full', placeholder: t('phone'), required: index.zero?, id: "phone_phone_#{item['uuid']}" %>
|
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('#{t('use_international_format_1xxx_')}') : ''", oninput: "this.setCustomValidity('')", name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: t('phone'), required: index.zero?, id: "phone_phone_#{item['uuid']}" %>
|
||||||
</linked-input>
|
</linked-input>
|
||||||
</submitters-autocomplete>
|
</submitters-autocomplete>
|
||||||
<% if submitters.size > 1 %>
|
<% if submitters.size > 1 %>
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
<% can_send_emails = Accounts.can_send_emails?(current_account) %>
|
<% can_send_emails = Accounts.can_send_emails?(current_account) %>
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<%= f.label :send_email, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %>
|
<%= f.label :send_email, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %>
|
||||||
<%= f.check_box :send_email, id: uuid, class: 'base-checkbox', disabled: !can_send_emails, checked: can_send_emails %>
|
<%= f.check_box :send_email, id: uuid, class: 'base-checkbox', disabled: !can_send_emails, checked: can_send_emails && !local_assigns.key?(:resend_email) %>
|
||||||
<span class="label"><%= t('send_emails') %></span>
|
<span class="label"><%= local_assigns[:resend_email] ? t('re_send_email') : t('send_email') %></span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div>
|
<div>
|
||||||
<% if can_send_emails %>
|
<% if can_send_emails %>
|
||||||
<%= render 'email_stats' %>
|
<%= render 'submissions/email_stats' %>
|
||||||
<%= content_for(:edit_button) || capture do %>
|
<%= content_for(:edit_button) || capture do %>
|
||||||
<label>
|
<label>
|
||||||
<%= f.check_box :is_custom_message, onchange: "[this.form.querySelector('#message_field').classList.toggle('hidden', !event.currentTarget.checked)]", checked: false, class: 'hidden peer' %>
|
<%= f.check_box :is_custom_message, onchange: "[this.form.querySelector('#message_field').classList.toggle('hidden', !event.currentTarget.checked)]", checked: false, class: 'hidden peer' %>
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
<div class="form-control space-y-2">
|
<div class="form-control space-y-2">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.label :subject, t('subject'), class: 'label' %>
|
<%= f.label :subject, t('subject'), class: 'label' %>
|
||||||
<%= f.text_field :subject, value: @template.preferences['request_email_subject'].presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
|
<%= f.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || template.preferences['request_email_subject'].presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
@@ -49,14 +49,16 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<autoresize-textarea>
|
<autoresize-textarea>
|
||||||
<%= f.text_area :body, value: @template.preferences['request_email_body'].presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
|
<%= f.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || template.preferences['request_email_body'].presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
|
||||||
</autoresize-textarea>
|
</autoresize-textarea>
|
||||||
<label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer">
|
<% unless local_assigns.fetch(:disable_save_as_default_template_option, false) %>
|
||||||
<%= check_box_tag :save_message, id: uuid, class: 'base-checkbox', checked: false %>
|
<label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer">
|
||||||
<span class="label"><%= t('save_as_default_template_message') %></span>
|
<%= check_box_tag :save_message, id: uuid, class: 'base-checkbox', checked: false %>
|
||||||
</label>
|
<span class="label"><%= t('save_as_default_template_message') %></span>
|
||||||
|
</label>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<%= render 'message_fields' %>
|
<%= render 'submissions/message_fields' %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<% align = field.dig('preferences', 'align') %>
|
<% align = field.dig('preferences', 'align') %>
|
||||||
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-serif' if field.dig('preferences', 'font') == 'Times' %> <%= align == 'right' ? 'justify-end' : (align == 'center' ? 'justify-center' : '') %>" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: #{field['preferences']['font_size']}pt" if field.dig('preferences', 'font_size') %>">
|
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-serif' if field.dig('preferences', 'font') == 'Times' %> <%= align == 'right' ? 'justify-end' : (align == 'center' ? 'justify-center' : '') %>" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size']}pt); line-height: `clamp(6pt, 2.0vw, #{field['preferences']['font_size'].to_i + 3}pt)`" if field.dig('preferences', 'font_size') %>">
|
||||||
<% if field['type'] == 'signature' %>
|
<% if field['type'] == 'signature' %>
|
||||||
<div class="flex flex-col justify-between h-full overflow-hidden">
|
<div class="flex flex-col justify-between h-full overflow-hidden">
|
||||||
<div class="flex-grow flex overflow-hidden" style="min-height: 50%">
|
<div class="flex-grow flex overflow-hidden" style="min-height: 50%">
|
||||||
|
|||||||
@@ -17,41 +17,43 @@
|
|||||||
<% end %>
|
<% 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) %>
|
||||||
<% is_all_completed = @submission.submitters.to_a.all?(&:completed_at?) %>
|
<% is_all_completed = @submission.submitters.to_a.all?(&:completed_at?) %>
|
||||||
<div class="join relative">
|
<% if is_all_completed || !is_combined_enabled %>
|
||||||
<download-button data-src="<%= submitter_download_index_path(last_submitter.slug, { sig: params[:sig], combined: is_combined_enabled }.compact) %>" class="base-button <%= '!rounded-r-none !pr-2' if is_all_completed && !is_combined_enabled %>">
|
<div class="join relative">
|
||||||
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
<download-button data-src="<%= submitter_download_index_path(last_submitter.slug, { sig: params[:sig], combined: is_combined_enabled }.compact) %>" class="base-button <%= '!rounded-r-none !pr-2' if is_all_completed && !is_combined_enabled %>">
|
||||||
<%= svg_icon('download', class: 'w-6 h-6') %>
|
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
||||||
<span class="hidden md:inline"><%= t('download') %></span>
|
<%= svg_icon('download', class: 'w-6 h-6') %>
|
||||||
</span>
|
<span class="hidden md:inline"><%= t('download') %></span>
|
||||||
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
|
</span>
|
||||||
<%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
|
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
|
||||||
<span class="hidden md:inline"><%= t('downloading') %></span>
|
<%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
|
||||||
</span>
|
<span class="hidden md:inline"><%= t('downloading') %></span>
|
||||||
</download-button>
|
</span>
|
||||||
<% if is_all_completed && !is_combined_enabled %>
|
</download-button>
|
||||||
<div class="dropdown dropdown-end">
|
<% if is_all_completed && !is_combined_enabled %>
|
||||||
<label tabindex="0" class="base-button !rounded-l-none !pl-1 !pr-2 !border-l-neutral-500">
|
<div class="dropdown dropdown-end">
|
||||||
<span class="text-sm align-text-top">
|
<label tabindex="0" class="base-button !rounded-l-none !pl-1 !pr-2 !border-l-neutral-500">
|
||||||
<%= svg_icon('chevron_down', class: 'w-6 h-6 flex-shrink-0 stroke-2') %>
|
<span class="text-sm align-text-top">
|
||||||
</span>
|
<%= svg_icon('chevron_down', class: 'w-6 h-6 flex-shrink-0 stroke-2') %>
|
||||||
</label>
|
</span>
|
||||||
<ul tabindex="0" class="z-10 dropdown-content p-2 mt-2 shadow menu text-base bg-base-100 rounded-box text-right">
|
</label>
|
||||||
<li>
|
<ul tabindex="0" class="z-10 dropdown-content p-2 mt-2 shadow menu text-base bg-base-100 rounded-box text-right">
|
||||||
<download-button data-src="<%= submitter_download_index_path(last_submitter.slug, { sig: params[:sig], combined: true }.compact) %>" class="flex items-center">
|
<li>
|
||||||
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
<download-button data-src="<%= submitter_download_index_path(last_submitter.slug, { sig: params[:sig], combined: true }.compact) %>" class="flex items-center">
|
||||||
<%= svg_icon('download', class: 'w-6 h-6 flex-shrink-0') %>
|
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
||||||
<span class="whitespace-nowrap"><%= t('download_combined_pdf') %></span>
|
<%= svg_icon('download', class: 'w-6 h-6 flex-shrink-0') %>
|
||||||
</span>
|
<span class="whitespace-nowrap"><%= t('download_combined_pdf') %></span>
|
||||||
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
|
</span>
|
||||||
<%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
|
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
|
||||||
<span><%= t('downloading') %></span>
|
<%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
|
||||||
</span>
|
<span><%= t('downloading') %></span>
|
||||||
</download-button>
|
</span>
|
||||||
</li>
|
</download-button>
|
||||||
</ul>
|
</li>
|
||||||
</div>
|
</ul>
|
||||||
<% end %>
|
</div>
|
||||||
</div>
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
<% elsif @submission.submitters.to_a.size == 1 && !@submission.expired? && !@submission.submitters.to_a.first.declined_at? %>
|
<% elsif @submission.submitters.to_a.size == 1 && !@submission.expired? && !@submission.submitters.to_a.first.declined_at? %>
|
||||||
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: @submission.submitters.to_a.first.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy_share_link'), copied_title: t('copied_to_clipboard') %>
|
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: @submission.submitters.to_a.first.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy_share_link'), copied_title: t('copied_to_clipboard') %>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -107,12 +109,21 @@
|
|||||||
<% (@submission.template_submitters || @submission.template.submitters).each_with_index do |item, index| %>
|
<% (@submission.template_submitters || @submission.template.submitters).each_with_index do |item, index| %>
|
||||||
<% submitter = @submission.submitters.find { |e| e.uuid == item['uuid'] } %>
|
<% submitter = @submission.submitters.find { |e| e.uuid == item['uuid'] } %>
|
||||||
<div class="sticky -top-1 bg-base-100 pt-1 -mt-1">
|
<div class="sticky -top-1 bg-base-100 pt-1 -mt-1">
|
||||||
<div class="border border-base-300 rounded-md px-2 py-1 mb-1">
|
<div class="group border border-base-300 rounded-md px-2 py-1 mb-1">
|
||||||
<div class="flex items-center space-x-1">
|
<div class="flex items-center justify-between">
|
||||||
<span class="mx-1 w-3 h-3 rounded-full <%= colors[index] %>"></span>
|
<div class="flex items-center space-x-1">
|
||||||
<span class="text-lg" dir="auto">
|
<span class="mx-1 w-3 h-3 shrink-0 rounded-full <%= colors[index] %>"></span>
|
||||||
<%= (@submission.template_submitters || @submission.template.submitters).find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %>
|
<span class="text-lg" dir="auto">
|
||||||
</span>
|
<%= (@submission.template_submitters || @submission.template.submitters).find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<% if signed_in? && can?(:update, submitter) && !submitter.completed_at? && !submitter.declined_at? && !@submission.archived_at? && !@submission.expired? && !submitter.start_form_submission_events.any? %>
|
||||||
|
<span class="tooltip tooltip-left" data-tip="<%= t('edit') %>">
|
||||||
|
<%= link_to edit_submitter_path(submitter), class: 'shrink-0 inline md:hidden md:group-hover:inline', data: { turbo_frame: 'modal' } do %>
|
||||||
|
<%= svg_icon('pencil', class: 'w-5 h-5') %>
|
||||||
|
<% end %>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% if submitter&.name.present? %>
|
<% if submitter&.name.present? %>
|
||||||
<div class="flex items-center space-x-1 mt-1">
|
<div class="flex items-center space-x-1 mt-1">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid created_at], methods: %i[url filename content_type]) %>
|
<% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid created_at], methods: %i[url filename content_type]) %>
|
||||||
<% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %>
|
<% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %>
|
||||||
<% invite_submitters = (submitter.submission.template_submitters || submitter.submission.template.submitters).select { |s| s['invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } }.to_json %>
|
<% invite_submitters = (submitter.submission.template_submitters || submitter.submission.template.submitters).select { |s| s['invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } }.to_json %>
|
||||||
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-require-signing-reason="<%= configs[:require_signing_reason] %>" data-with-signature-id="<%= configs[:with_signature_id] %>" data-with-confetti="<%= configs[:with_confetti] %>" data-completed-redirect-url="<%= submitter.preferences['completed_redirect_url'].presence || submitter.submission.template.preferences['completed_redirect_url'] %>" data-completed-message="<%= (configs[:completed_message]&.compact_blank.presence || submitter.submission.template.preferences['completed_message'] || {}).to_json %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.preferences.key?('go_to_last') ? submitter.preferences['go_to_last'] : submitter.opened_at? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(submitter.submission.account) %>" data-invite-submitters="<%= invite_submitters %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>" data-previous-signature-value="<%= local_assigns[:signature_attachment]&.uuid %>" data-remember-signature="<%= configs[:prefill_signature] %>" data-dry-run="<%= local_assigns[:dry_run] %>" data-expand="<%= local_assigns[:expand] %>" data-scroll-padding="<%= local_assigns[:scroll_padding] %>" data-language="<%= I18n.locale.to_s.split('-').first %>"></submission-form>
|
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-reuse-signature="<%= configs[:reuse_signature] %>" data-require-signing-reason="<%= configs[:require_signing_reason] %>" data-with-signature-id="<%= configs[:with_signature_id] %>" data-with-confetti="<%= configs[:with_confetti] %>" data-completed-redirect-url="<%= submitter.preferences['completed_redirect_url'].presence || submitter.submission.template.preferences['completed_redirect_url'] %>" data-completed-message="<%= (configs[:completed_message]&.compact_blank.presence || submitter.submission.template.preferences['completed_message'] || {}).to_json %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.preferences.key?('go_to_last') ? submitter.preferences['go_to_last'] : submitter.opened_at? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(submitter.submission.account) %>" data-invite-submitters="<%= invite_submitters %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>" data-previous-signature-value="<%= local_assigns[:signature_attachment]&.uuid %>" data-remember-signature="<%= configs[:prefill_signature] %>" data-dry-run="<%= local_assigns[:dry_run] %>" data-expand="<%= local_assigns[:expand] %>" data-scroll-padding="<%= local_assigns[:scroll_padding] %>" data-language="<%= I18n.locale.to_s.split('-').first %>"></submission-form>
|
||||||
@@ -15,18 +15,21 @@
|
|||||||
<%= @submitter.submission.template.name %>
|
<%= @submitter.submission.template.name %>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-2" style="margin-left: 20px; flex-shrink: 0">
|
<div class="flex items-center space-x-2" style="margin-left: 20px; flex-shrink: 0">
|
||||||
<div>
|
<% if @form_configs[:with_decline] %>
|
||||||
<%= render 'shared/html_modal', title: t(:decline), btn_text: t(:decline), btn_class: 'btn btn-sm !px-5', button_id: 'decline_button' do %>
|
<div>
|
||||||
<%= render 'submit_form/decline_form', submitter: @submitter %>
|
<%= render 'shared/html_modal', title: t(:decline), btn_text: t(:decline), btn_class: 'btn btn-sm !px-5', button_id: 'decline_button' do %>
|
||||||
<% end %>
|
<%= render 'submit_form/decline_form', submitter: @submitter %>
|
||||||
</div>
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
<download-button data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm !px-4">
|
<download-button data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm !px-4">
|
||||||
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
<span class="flex items-center justify-center" data-target="download-button.defaultButton">
|
||||||
<span><%= t('download') %></span>
|
<%= svg_icon('download', class: 'w-6 h-6 inline md:hidden') %>
|
||||||
|
<span class="hidden md:inline"><%= t('download') %></span>
|
||||||
</span>
|
</span>
|
||||||
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
|
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
|
||||||
<%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
|
<%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
|
||||||
<span><%= t('downloading') %></span>
|
<span class="hidden md:inline"><%= t('downloading') %></span>
|
||||||
</span>
|
</span>
|
||||||
</download-button>
|
</download-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -59,7 +62,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render 'shared/attribution', link_path: '/start', account: @submitter.account %>
|
<%= render 'shared/attribution', link_path: '/start', account: @submitter.account, with_style: false %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="w-full bg-white rounded-2xl border relative" style="height: 300px">
|
<div class="w-full bg-white rounded-2xl border relative" style="height: 300px">
|
||||||
<canvas class="w-full"></canvas>
|
<canvas class="w-full"></canvas>
|
||||||
<button aria-label="<%= t('clear') %>" class="btn btn-ghost btn-sm font-medium top-0 right-0 absolute mt-1 mr-1">
|
<button aria-label="Clear" class="btn btn-ghost btn-sm font-medium top-0 right-0 absolute mt-1 mr-1">
|
||||||
<%= svg_icon('reload', class: 'w-5 h-5') %>
|
<%= svg_icon('reload', class: 'w-5 h-5') %>
|
||||||
<span class="inline"><%= t('clear') %></span>
|
<span class="inline"><%= t('clear') %></span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<%= render 'shared/turbo_modal_large', title: t('edit_recipient') do %>
|
||||||
|
<div class="px-5 mb-5 mt-4">
|
||||||
|
<%= form_for '', url: submitter_path(@submitter), method: :patch, html: { autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
|
||||||
|
<div class="mb-2">
|
||||||
|
<submitter-item class="form-control">
|
||||||
|
<submitters-autocomplete data-field="name">
|
||||||
|
<%= text_field_tag 'submitter[name]', @submitter.name, autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: "#{t('name')} (#{t('optional')})", dir: 'auto' %>
|
||||||
|
</submitters-autocomplete>
|
||||||
|
<div class="grid md:grid-cols-2 gap-1.5 mt-1.5">
|
||||||
|
<submitters-autocomplete data-field="email">
|
||||||
|
<%= email_field_tag 'submitter[email]', @submitter.email, autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: "#{t('email')} (#{t('optional')})" %>
|
||||||
|
</submitters-autocomplete>
|
||||||
|
<submitters-autocomplete data-field="phone">
|
||||||
|
<%= telephone_field_tag 'submitter[phone]', @submitter.phone, autocomplete: 'off', pattern: '^\+[0-9\s\-]+$', class: 'base-input !h-10 mt-1.5 w-full', placeholder: "#{t('phone')} (#{t('optional')})", oninvalid: "this.value ? this.setCustomValidity('#{t('use_international_format_1xxx_')}') : ''", oninput: "this.setCustomValidity('')" %>
|
||||||
|
</submitters-autocomplete>
|
||||||
|
</div>
|
||||||
|
</submitter-item>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<%= render 'submissions/send_email', f:, template: @submitter.template, submitter: @submitter, resend_email: @submitter.sent_at?, submitter_email_message: @submitter_email_message, disable_save_as_default_template_option: true %>
|
||||||
|
<%= render 'submissions/send_sms', f:, resend_sms: @submitter.sent_at? %>
|
||||||
|
</div>
|
||||||
|
<div class="form-control mt-4">
|
||||||
|
<%= f.button button_title(title: t('update_recipient'), disabled_with: t('updating')), class: 'base-button' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<%= content_for(:modal_extra) %>
|
||||||
|
<% end %>
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<% end %>
|
<% end %>
|
||||||
<a href="<%= submission_path(submission) %>" class="w-full flex flex-col md:flex-row space-y-4 md:space-y-0 md:justify-between px-5 md:px-6 pb-5 md:items-center pt-5">
|
<div class="w-full flex flex-col md:flex-row space-y-4 md:space-y-0 md:justify-between px-5 md:px-6 pb-5 md:items-center pt-5 relative cursor-pointer">
|
||||||
<% submitters = (submission.template_submitters || submission.template.submitters).filter_map { |item| submission.submitters.find { |e| e.uuid == item['uuid'] } } %>
|
<% submitters = (submission.template_submitters || submission.template.submitters).filter_map { |item| submission.submitters.find { |e| e.uuid == item['uuid'] } } %>
|
||||||
<% is_submission_completed = submitters.all?(&:completed_at?) && submitters.size.positive? %>
|
<% is_submission_completed = submitters.all?(&:completed_at?) && submitters.size.positive? %>
|
||||||
<% if submitters.size == 1 %>
|
<% if submitters.size == 1 %>
|
||||||
@@ -41,22 +41,31 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
|
<a href="<%= submission_path(submission) %>" class="flex z-[1]">
|
||||||
<span class="badge <%= status_badges[submitter.status] %> md:w-32 badge-lg bg-opacity-50 uppercase text-sm font-semibold">
|
<span class="badge <%= status_badges[submitter.status] %> md:w-32 badge-lg bg-opacity-50 uppercase text-sm font-semibold tooltip" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
|
||||||
<%= t(submitter.status) %>
|
<%= t(submitter.status) %>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</a>
|
||||||
<% end %>
|
<% end %>
|
||||||
<span class="text-lg break-all flex items-center">
|
<span class="flex items-center z-[1]">
|
||||||
<%= submitter.name || submitter.email || submitter.phone %>
|
<a href="<%= submission_path(submission) %>" class="text-lg break-all peer">
|
||||||
|
<%= submitter.name || submitter.email || submitter.phone %>
|
||||||
|
</a>
|
||||||
|
<% if can?(:update, submitter) && !submitter.start_form_submission_events.any? && !submission.archived_at? && !submission.expired? %>
|
||||||
|
<span class="pl-0.5 tooltip tooltip-top md:opacity-0 md:hover:opacity-100 md:peer-hover:opacity-100" data-tip="<%= t('edit') %>">
|
||||||
|
<%= link_to edit_submitter_path(submitter), class: 'shrink-0', data: { turbo_frame: 'modal' } do %>
|
||||||
|
<%= svg_icon('pencil', class: 'w-5 h-5') %>
|
||||||
|
<% end %>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex space-x-2 items-center">
|
<div class="flex space-x-2 items-center z-[1]">
|
||||||
<% if submitter.completed_at? %>
|
<% if submitter.completed_at? %>
|
||||||
<form onsubmit="event.preventDefault()" class="flex-1 md:flex-none">
|
<div class="flex-1 md:flex-none">
|
||||||
<button onclick="event.stopPropagation()" class="w-full md:w-fit">
|
<div class="w-full md:w-fit">
|
||||||
<download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="btn btn-sm btn-neutral text-white w-full md:w-36">
|
<download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="btn btn-sm btn-neutral text-white w-full md:w-36">
|
||||||
<span class="flex items-center justify-center space-x-1 <%= 'md:space-x-2' if t('download').length < 11 %>" data-target="download-button.defaultButton">
|
<span class="flex items-center justify-center space-x-1 <%= 'md:space-x-2' if t('download').length < 11 %>" data-target="download-button.defaultButton">
|
||||||
<%= svg_icon('download', class: 'w-5 h-5 stroke-2') %>
|
<%= svg_icon('download', class: 'w-5 h-5 stroke-2') %>
|
||||||
@@ -67,37 +76,39 @@
|
|||||||
<span class="inline"><%= t('download')[..-2] %>...</span>
|
<span class="inline"><%= t('download')[..-2] %>...</span>
|
||||||
</span>
|
</span>
|
||||||
</download-button>
|
</download-button>
|
||||||
</button>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
<% elsif !submission.archived_at? && !template.archived_at? && !submission.expired? && !submitter.declined_at? %>
|
<% elsif !submission.archived_at? && !template.archived_at? && !submission.expired? && !submitter.declined_at? %>
|
||||||
<% if current_user.email == submitter.email %>
|
<% if current_user.email == submitter.email %>
|
||||||
<form data-turbo="false" method="get" action="<%= submit_form_url(slug: submitter.slug) %>" class="flex-1 md:flex-none">
|
<div class="flex-1 md:flex-none md:w-36 flex">
|
||||||
<button onclick="event.stopPropagation()" class="btn btn-sm btn-neutral btn-outline bg-white w-full md:w-36 flex">
|
<a href="<%= submit_form_url(slug: submitter.slug) %>" data-turbo="false" target="_blank" class="btn btn-sm btn-neutral btn-outline bg-white w-full md:w-36 flex z-[1]">
|
||||||
<span class="flex items-center justify-center space-x-1 md:space-x-2">
|
<span class="flex items-center justify-center space-x-1 md:space-x-2">
|
||||||
<% if t('sign_now').length < 12 %>
|
<% if t('sign_now').length < 12 %>
|
||||||
<%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %>
|
<%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<span class="inline shrink-0"><%= t('sign_now') %></span>
|
<span class="inline shrink-0"><%= t('sign_now') %></span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</a>
|
||||||
</form>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="flex-1 md:flex-none">
|
<div class="flex-1 md:flex-none">
|
||||||
<%= 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: t('copy_link').length < 10 ? t('copy_link') : t('copy'), copy_title_md: t('copy'), copied_title_md: t('copied') %>
|
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-sm btn-neutral text-white md:w-36 flex z-[1]', icon_class: 'w-6 h-6 text-white', copy_title: t('copy_link').length < 10 ? t('copy_link') : t('copy'), copy_title_md: t('copy'), copied_title_md: t('copied') %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="flex-1 md:flex-none">
|
<div class="flex-1 md:flex-none z-[1]">
|
||||||
<span class="btn btn-outline btn-sm w-full md:w-24"><%= t('view') %></span>
|
<a href="<%= submission_path(submission) %>" class="btn btn-outline btn-sm w-full md:w-24">
|
||||||
|
<%= t('view') %>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<% if !submission.archived_at? && !template.archived_at? && can?(:destroy, submission) %>
|
<% if !submission.archived_at? && !template.archived_at? && can?(:destroy, submission) %>
|
||||||
<span data-tip="<%= t('archive') %>" class="sm:tooltip tooltip-top">
|
<span data-tip="<%= t('archive') %>" class="sm:tooltip tooltip-top">
|
||||||
<%= button_to button_title(title: nil, disabled_with: t(:archive).first(4), icon: svg_icon('archive', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('archive'), method: :delete, onclick: 'event.stopPropagation()' %>
|
<%= button_to button_title(title: nil, disabled_with: t(:archive).first(4), icon: svg_icon('archive', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('archive'), method: :delete %>
|
||||||
</span>
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if local_assigns[:archived] && can?(:destroy, submission) %>
|
<% if local_assigns[:archived] && can?(:destroy, submission) %>
|
||||||
<span data-tip="<%= t('remove') %>" class="sm:tooltip tooltip-top">
|
<span data-tip="<%= t('remove') %>" class="sm:tooltip tooltip-top">
|
||||||
<%= button_to button_title(title: nil, disabled_with: t(:remove).first(3), icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission, permanently: true), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('remove'), method: :delete, data: { turbo_confirm: t('submission_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_') }, onclick: 'event.stopPropagation()' %>
|
<%= button_to button_title(title: nil, disabled_with: t(:remove).first(3), icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission, permanently: true), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('remove'), method: :delete, data: { turbo_confirm: t('submission_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_') } %>
|
||||||
</span>
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
@@ -106,11 +117,11 @@
|
|||||||
<div class="flex flex-col md:flex-row md:items-center gap-3">
|
<div class="flex flex-col md:flex-row md:items-center gap-3">
|
||||||
<% if is_submission_completed %>
|
<% if is_submission_completed %>
|
||||||
<% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %>
|
<% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %>
|
||||||
<div class="tooltip flex" data-tip="<%= l(latest_submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
|
<a href="<%= submission_path(submission) %>" class="z-[1] tooltip flex" data-tip="<%= l(latest_submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
|
||||||
<span class="badge <%= status_badges[latest_submitter.status] %> md:w-32 bg-opacity-50 badge-lg uppercase text-sm font-semibold">
|
<span class="badge <%= status_badges[latest_submitter.status] %> md:w-32 bg-opacity-50 badge-lg uppercase text-sm font-semibold">
|
||||||
<%= t(latest_submitter.status) %>
|
<%= t(latest_submitter.status) %>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</a>
|
||||||
<% elsif submission.expired? %>
|
<% elsif submission.expired? %>
|
||||||
<div class="tooltip flex" data-tip="<%= l(submission.expire_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
|
<div class="tooltip flex" data-tip="<%= l(submission.expire_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
|
||||||
<span class="badge badge-error md:w-32 bg-opacity-50 badge-lg uppercase text-sm font-semibold">
|
<span class="badge badge-error md:w-32 bg-opacity-50 badge-lg uppercase text-sm font-semibold">
|
||||||
@@ -123,46 +134,49 @@
|
|||||||
<div class="relative flex justify-between items-start md:items-center space-x-3">
|
<div class="relative flex justify-between items-start md:items-center space-x-3">
|
||||||
<span class="flex flex-col md:flex-row md:items-center gap-2">
|
<span class="flex flex-col md:flex-row md:items-center gap-2">
|
||||||
<% if !is_submission_completed && !submission.expired? %>
|
<% if !is_submission_completed && !submission.expired? %>
|
||||||
<div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
|
<a href="<%= submission_path(submission) %>" class="z-[1] tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
|
||||||
<span class="badge md:w-24 <%= status_badges[submitter.status] %> bg-opacity-50 uppercase text-xs font-semibold">
|
<span class="badge md:w-24 <%= status_badges[submitter.status] %> bg-opacity-50 uppercase text-xs font-semibold">
|
||||||
<%= t(submitter.status) %>
|
<%= t(submitter.status) %>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</a>
|
||||||
<% end %>
|
<% end %>
|
||||||
<span class="text-lg break-all">
|
<span class="flex items-center z-[1]">
|
||||||
<%= submitter.name || submitter.email || submitter.phone %>
|
<a href="<%= submission_path(submission) %>" class="text-lg break-all peer">
|
||||||
|
<%= submitter.name || submitter.email || submitter.phone %>
|
||||||
|
</a>
|
||||||
|
<% if can?(:update, submitter) && !submitter.start_form_submission_events.any? && !submission.archived_at? && !submission.expired? %>
|
||||||
|
<span class="pl-0.5 tooltip tooltip-top md:opacity-0 md:hover:opacity-100 md:peer-hover:opacity-100" data-tip="<%= t('edit') %>">
|
||||||
|
<%= link_to edit_submitter_path(submitter), class: 'shrink-0', data: { turbo_frame: 'modal' } do %>
|
||||||
|
<%= svg_icon('pencil', class: 'w-5 h-5') %>
|
||||||
|
<% end %>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<% if submitter.completed_at? && !is_submission_completed %>
|
<% if submitter.completed_at? && !is_submission_completed %>
|
||||||
<form onsubmit="event.preventDefault()">
|
<download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="absolute md:relative top-0 right-0 btn btn-xs btn-neutral text-white md:w-36 z-[1]">
|
||||||
<button onclick="event.stopPropagation()">
|
<span class="flex items-center justify-center space-x-1 md:space-x-2" data-target="download-button.defaultButton">
|
||||||
<download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="absolute md:relative top-0 right-0 btn btn-xs btn-neutral text-white md:w-36">
|
<%= svg_icon('download', class: 'w-4 h-4 stroke-2') %>
|
||||||
<span class="flex items-center justify-center space-x-1 md:space-x-2" data-target="download-button.defaultButton">
|
<span class="inline"><%= t('download') %></span>
|
||||||
<%= svg_icon('download', class: 'w-4 h-4 stroke-2') %>
|
</span>
|
||||||
<span class="inline"><%= t('download') %></span>
|
<span class="flex items-center justify-center space-x-1 md:space-x-2 hidden" data-target="download-button.loadingButton">
|
||||||
</span>
|
<%= svg_icon('loader', class: 'w-4 h-4 animate-spin') %>
|
||||||
<span class="flex items-center justify-center space-x-1 md:space-x-2 hidden" data-target="download-button.loadingButton">
|
<span class="inline"><%= t('download')[..-2] %>...</span>
|
||||||
<%= svg_icon('loader', class: 'w-4 h-4 animate-spin') %>
|
</span>
|
||||||
<span class="inline"><%= t('download')[..-2] %>...</span>
|
</download-button>
|
||||||
</span>
|
|
||||||
</download-button>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<% elsif !template.archived_at? && !submission.archived_at? && !is_submission_completed && !submission.expired? && !submitter.declined_at? %>
|
<% elsif !template.archived_at? && !submission.archived_at? && !is_submission_completed && !submission.expired? && !submitter.declined_at? %>
|
||||||
<div class="relative flex items-center space-x-3">
|
<div class="relative flex items-center space-x-3">
|
||||||
<% if current_user.email == submitter.email %>
|
<% if current_user.email == submitter.email %>
|
||||||
<form data-turbo="false" method="get" action="<%= submit_form_url(slug: submitter.slug) %>">
|
<a href="<%= submit_form_url(slug: submitter.slug) %>" data-turbo="false" target="_blank" class="absolute md:relative top-0 right-0 btn btn-xs btn-outline btn-neutral bg-white w-28 md:w-36 z-[1]">
|
||||||
<button onclick="event.stopPropagation()" class="absolute md:relative top-0 right-0 btn btn-xs btn-outline btn-neutral bg-white w-28 md:w-36">
|
<span class="flex items-center justify-center space-x-1 md:space-x-2">
|
||||||
<span class="flex items-center justify-center space-x-1 md:space-x-2">
|
<% if t('sign_now').length < 12 %>
|
||||||
<% if t('sign_now').length < 12 %>
|
<%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %>
|
||||||
<%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %>
|
<% end %>
|
||||||
<% end %>
|
<span class="inline shrink-0"><%= t('sign_now') %></span>
|
||||||
<span class="inline shrink-0"><%= t('sign_now') %></span>
|
</span>
|
||||||
</span>
|
</a>
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'absolute md:relative top-0 right-0 btn btn-xs text-xs btn-neutral text-white w-28 md:w-36 flex', icon_class: 'w-4 h-4 text-white', copy_title: t('copy_link'), copy_title_md: t('copy_link').length < 10 ? t('copy_link') : t('copy'), copied_title_md: t('copied') %>
|
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'absolute md:relative top-0 right-0 btn btn-xs text-xs btn-neutral text-white w-28 md:w-36 flex z-[1]', icon_class: 'w-4 h-4 text-white', copy_title: t('copy_link'), copy_title_md: t('copy_link').length < 10 ? t('copy_link') : t('copy'), copied_title_md: t('copied') %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -171,40 +185,41 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex space-x-1 md:space-x-2 items-center">
|
<div class="flex space-x-1 md:space-x-2 items-center z-[1]">
|
||||||
<% if is_submission_completed %>
|
<% if is_submission_completed %>
|
||||||
<% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %>
|
<% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %>
|
||||||
<div class="flex-1 md:flex-none">
|
<div class="flex-1 md:flex-none">
|
||||||
<form onsubmit="event.preventDefault()" class="w-full md:w-fit">
|
<div class="w-full md:w-fit">
|
||||||
<button onclick="event.stopPropagation()" class="w-full">
|
<download-button data-src="<%= submitter_download_index_path(latest_submitter.slug) %>" class="btn btn-sm btn-neutral text-white w-full md:w-36 z-[1]">
|
||||||
<download-button data-src="<%= submitter_download_index_path(latest_submitter.slug) %>" class="btn btn-sm btn-neutral text-white w-full md:w-36">
|
<span class="flex items-center justify-center space-x-1 md:space-x-2" data-target="download-button.defaultButton">
|
||||||
<span class="flex items-center justify-center space-x-1 md:space-x-2" data-target="download-button.defaultButton">
|
<%= svg_icon('download', class: 'w-5 h-5 stroke-2') %>
|
||||||
<%= svg_icon('download', class: 'w-5 h-5 stroke-2') %>
|
<span class="inline"><%= t('download') %></span>
|
||||||
<span class="inline"><%= t('download') %></span>
|
</span>
|
||||||
</span>
|
<span class="flex items-center justify-center space-x-1 md:space-x-2 hidden" data-target="download-button.loadingButton">
|
||||||
<span class="flex items-center justify-center space-x-1 md:space-x-2 hidden" data-target="download-button.loadingButton">
|
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin') %>
|
||||||
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin') %>
|
<span class="inline"><%= t('download')[..-2] %>...</span>
|
||||||
<span class="inline"><%= t('download')[..-2] %>...</span>
|
</span>
|
||||||
</span>
|
</download-button>
|
||||||
</download-button>
|
</div>
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="flex-1 md:flex-none">
|
<div class="flex-1 md:flex-none z-[1]">
|
||||||
<span class="btn btn-outline btn-sm w-full md:w-24"><%= t('view') %></span>
|
<a href="<%= submission_path(submission) %>" class="btn btn-outline btn-sm w-full md:w-24">
|
||||||
|
<%= t('view') %>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<% if !submission.archived_at? && !template.archived_at? %>
|
<% if !submission.archived_at? && !template.archived_at? %>
|
||||||
<span data-tip="<%= t('archive') %>" class="sm:tooltip tooltip-top">
|
<span data-tip="<%= t('archive') %>" class="sm:tooltip tooltip-top">
|
||||||
<%= button_to button_title(title: nil, disabled_with: t(:archive).first(4), icon: svg_icon('archive', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('archive'), method: :delete, onclick: 'event.stopPropagation()' %>
|
<%= button_to button_title(title: nil, disabled_with: t(:archive).first(4), icon: svg_icon('archive', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('archive'), method: :delete %>
|
||||||
</span>
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if local_assigns[:archived] && can?(:destroy, submission) %>
|
<% if local_assigns[:archived] && can?(:destroy, submission) %>
|
||||||
<span data-tip="<%= t('remove') %>" class="sm:tooltip tooltip-top">
|
<span data-tip="<%= t('remove') %>" class="sm:tooltip tooltip-top">
|
||||||
<%= button_to button_title(title: nil, disabled_with: t(:remove).first(3), icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission, permanently: true), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('remove'), method: :delete, data: { turbo_confirm: t('submission_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_') }, onclick: 'event.stopPropagation()' %>
|
<%= button_to button_title(title: nil, disabled_with: t(:remove).first(3), icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission, permanently: true), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('remove'), method: :delete, data: { turbo_confirm: t('submission_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_') } %>
|
||||||
</span>
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</a>
|
<a href="<%= submission_path(submission) %>" class="top-0 bottom-0 left-0 right-0 absolute"></a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<div class="flex items-center space-x-1">
|
||||||
|
<% if %w[archived integration].include?(params[:status]) %>
|
||||||
|
<%= link_to t('view_active'), settings_users_path, class: 'link text-sm' %>
|
||||||
|
<% else %>
|
||||||
|
<% archived_exists = current_account.users.archived.where.not(role: 'integration').exists? %>
|
||||||
|
<% if current_account.users.active.exists?(role: 'integration') %>
|
||||||
|
<%= link_to t('view_embedding_users'), settings_integration_users_path, class: 'link text-sm' %>
|
||||||
|
<% if archived_exists %>
|
||||||
|
<span class="text-neutral-700">|</span>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% if archived_exists %>
|
||||||
|
<%= link_to t('view_archived'), settings_archived_users_path, class: 'link text-sm' %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
@@ -1,12 +1,18 @@
|
|||||||
<div class="flex-wrap space-y-4 md:flex md:flex-nowrap md:space-y-0 md:space-x-10">
|
<div class="flex-wrap space-y-4 md:flex md:flex-nowrap md:space-y-0 md:space-x-10">
|
||||||
<%= render 'shared/settings_nav' %>
|
<%= render 'shared/settings_nav' %>
|
||||||
<div class="md:flex-grow">
|
<div class="md:flex-grow">
|
||||||
<div class="flex flex-col md:flex-row gap-2 md:justify-between md:items-end mb-4">
|
<div class="flex flex-col md:flex-row gap-2 md:justify-between md:items-end mb-4 min-h-12">
|
||||||
<h1 class="text-4xl font-bold">
|
<h1 class="text-4xl font-bold">
|
||||||
<%= t('status_users', status: params[:status].to_s.titleize) %>
|
<% if params[:status] == 'archived' %>
|
||||||
|
<%= t('archived_users') %>
|
||||||
|
<% elsif params[:status] == 'integration' %>
|
||||||
|
<%= t('embedding_users') %>
|
||||||
|
<% else %>
|
||||||
|
<%= t('users') %>
|
||||||
|
<% end %>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="flex flex-col md:flex-row gap-y-2 gap-x-4 md:items-center">
|
<div class="flex flex-col md:flex-row gap-y-2 gap-x-4 md:items-center">
|
||||||
<% if can?(:create, User.new(account: current_account)) %>
|
<% if params[:status].blank? && can?(:create, User.new(account: current_account)) %>
|
||||||
<%= render 'users/extra_buttons' %>
|
<%= render 'users/extra_buttons' %>
|
||||||
<% if content_for(:add_user_button) %>
|
<% if content_for(:add_user_button) %>
|
||||||
<%= content_for(:add_user_button) %>
|
<%= content_for(:add_user_button) %>
|
||||||
@@ -57,17 +63,17 @@
|
|||||||
<%= user.last_sign_in_at ? l(user.last_sign_in_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) : '-' %>
|
<%= user.last_sign_in_at ? l(user.last_sign_in_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) : '-' %>
|
||||||
</td>
|
</td>
|
||||||
<td class="flex items-center space-x-2 justify-end">
|
<td class="flex items-center space-x-2 justify-end">
|
||||||
<% if can?(:update, user) && user.archived_at.blank? %>
|
<% if params[:status].blank? && can?(:update, user) && user.archived_at.blank? %>
|
||||||
<%= link_to edit_user_path(user), class: 'btn btn-outline btn-xs', title: t('edit'), data: { turbo_frame: 'modal' } do %>
|
<%= link_to edit_user_path(user), class: 'btn btn-outline btn-xs', title: t('edit'), data: { turbo_frame: 'modal' } do %>
|
||||||
<%= t('edit') %>
|
<%= t('edit') %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if params[:status].blank? && can?(:destroy, user) && user != current_user %>
|
<% if params[:status] != 'archived' && can?(:destroy, user) && user != current_user %>
|
||||||
<%= button_to user_path(user), method: :delete, class: 'btn btn-outline btn-error btn-xs', title: t('remove'), data: { turbo_confirm: t('are_you_sure_') } do %>
|
<%= button_to user_path(user), method: :delete, class: 'btn btn-outline btn-error btn-xs', title: t('remove'), data: { turbo_confirm: t('are_you_sure_') } do %>
|
||||||
<%= t('remove') %>
|
<%= t('remove') %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if params[:status].present? && can?(:manage, user) && user != current_user %>
|
<% if params[:status] == 'archived' && can?(:manage, user) && user != current_user && user.archived_at? %>
|
||||||
<%= button_to user_path(user), method: :put, params: { user: { archived_at: nil } }, class: 'btn btn-outline btn-xs', title: t('unarchive'), data: { turbo_confirm: t('are_you_sure_') } do %>
|
<%= button_to user_path(user), method: :put, params: { user: { archived_at: nil } }, class: 'btn btn-outline btn-xs', title: t('unarchive'), data: { turbo_confirm: t('are_you_sure_') } do %>
|
||||||
<%= t('unarchive') %>
|
<%= t('unarchive') %>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -78,22 +84,11 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<% view_archived_html = capture do %>
|
|
||||||
<% if current_account.users.archived.exists? %>
|
|
||||||
<div>
|
|
||||||
<% if params[:status] == 'archived' %>
|
|
||||||
<a href="<%= settings_users_path %>" class="link text-sm"><%= t('view_active') %></a>
|
|
||||||
<% else %>
|
|
||||||
<a href="<%= settings_archived_users_path %>" class="link text-sm"><%= t('view_archived') %></a>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
<% if @pagy.pages > 1 %>
|
<% if @pagy.pages > 1 %>
|
||||||
<%= render 'shared/pagination', pagy: @pagy, items_name: 'users', left_additional_html: view_archived_html %>
|
<%= render 'shared/pagination', pagy: @pagy, items_name: 'users', left_additional_html: render('bottom_links') %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<%= view_archived_html %>
|
<%= render 'bottom_links' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
Rails.configuration.to_prepare do
|
Rails.configuration.to_prepare do
|
||||||
ActiveRecord::Tasks::DatabaseTasks.migrate if ENV['RAILS_ENV'] == 'production'
|
ActiveRecord::Tasks::DatabaseTasks.migrate if ENV['RAILS_ENV'] == 'production' && ENV['RUN_MIGRATIONS'] != 'false'
|
||||||
end
|
end
|
||||||
+55
-6
@@ -134,6 +134,7 @@ en: &en
|
|||||||
require_signing_reason: Require signing reason
|
require_signing_reason: Require signing reason
|
||||||
allow_typed_text_signatures: Allow typed text signatures
|
allow_typed_text_signatures: Allow typed text signatures
|
||||||
allow_to_resubmit_completed_forms: Allow to resubmit completed forms
|
allow_to_resubmit_completed_forms: Allow to resubmit completed forms
|
||||||
|
allow_to_decline_documents: Allow to decline documents
|
||||||
remember_and_pre_fill_signatures: Remember and pre-fill signatures
|
remember_and_pre_fill_signatures: Remember and pre-fill signatures
|
||||||
require_authentication_for_file_download_links: Require authentication for file download links
|
require_authentication_for_file_download_links: Require authentication for file download links
|
||||||
combine_completed_documents_and_audit_log: Combine completed documents and Audit Log
|
combine_completed_documents_and_audit_log: Combine completed documents and Audit Log
|
||||||
@@ -473,7 +474,6 @@ en: &en
|
|||||||
back: Back
|
back: Back
|
||||||
add_secret: Add Secret
|
add_secret: Add Secret
|
||||||
submission_example_payload: Submission example payload
|
submission_example_payload: Submission example payload
|
||||||
status_users: '%{status} Users'
|
|
||||||
there_are_no_signatures: There are no signatures
|
there_are_no_signatures: There are no signatures
|
||||||
signed_with_trusted_certificate: Signed with trusted certificate
|
signed_with_trusted_certificate: Signed with trusted certificate
|
||||||
signed_with_external_certificate: Signed with external certificate
|
signed_with_external_certificate: Signed with external certificate
|
||||||
@@ -613,6 +613,15 @@ en: &en
|
|||||||
unverified: Unverified
|
unverified: Unverified
|
||||||
document: Document
|
document: Document
|
||||||
completed_at: Completed At
|
completed_at: Completed At
|
||||||
|
edit_recipient: Edit Recipient
|
||||||
|
update_recipient: Update Recipient
|
||||||
|
use_international_format_1xxx_: 'Use internatioanl format: +1xxx...'
|
||||||
|
submitter_cannot_be_updated: Submitter cannot be updated.
|
||||||
|
at_least_one_field_must_be_filled: At least one field must be filled.
|
||||||
|
archived_users: Archived Users
|
||||||
|
embedding_users: Embedding Users
|
||||||
|
view_embedding_users: View Embedding Users
|
||||||
|
view_users: View Users
|
||||||
submission_event_names:
|
submission_event_names:
|
||||||
send_email_to_html: '<b>Email sent</b> to %{submitter_name}'
|
send_email_to_html: '<b>Email sent</b> to %{submitter_name}'
|
||||||
send_reminder_email_to_html: '<b>Reminder email sent</b> to %{submitter_name}'
|
send_reminder_email_to_html: '<b>Reminder email sent</b> to %{submitter_name}'
|
||||||
@@ -765,6 +774,7 @@ es: &es
|
|||||||
require_signing_reason: Requerir motivo de firma
|
require_signing_reason: Requerir motivo de firma
|
||||||
allow_typed_text_signatures: Permitir firmas de texto mecanografiadas
|
allow_typed_text_signatures: Permitir firmas de texto mecanografiadas
|
||||||
allow_to_resubmit_completed_forms: Permitir reenviar formularios completados
|
allow_to_resubmit_completed_forms: Permitir reenviar formularios completados
|
||||||
|
allow_to_decline_documents: Permitir rechazar documentos
|
||||||
remember_and_pre_fill_signatures: Recordar y prellenar firmas
|
remember_and_pre_fill_signatures: Recordar y prellenar firmas
|
||||||
require_authentication_for_file_download_links: Requerir autenticación para enlaces de descarga de archivos
|
require_authentication_for_file_download_links: Requerir autenticación para enlaces de descarga de archivos
|
||||||
combine_completed_documents_and_audit_log: Combinar documentos completados y Registro de Auditoría
|
combine_completed_documents_and_audit_log: Combinar documentos completados y Registro de Auditoría
|
||||||
@@ -1104,7 +1114,6 @@ es: &es
|
|||||||
back: Atrás
|
back: Atrás
|
||||||
add_secret: Agregar secreto
|
add_secret: Agregar secreto
|
||||||
submission_example_payload: Ejemplo de payload de envío
|
submission_example_payload: Ejemplo de payload de envío
|
||||||
status_users: '%{status} Usuarios'
|
|
||||||
there_are_no_signatures: No hay firmas
|
there_are_no_signatures: No hay firmas
|
||||||
signed_with_trusted_certificate: Firmado con certificado de confianza
|
signed_with_trusted_certificate: Firmado con certificado de confianza
|
||||||
signed_with_external_certificate: Firmado con certificado externo
|
signed_with_external_certificate: Firmado con certificado externo
|
||||||
@@ -1244,6 +1253,14 @@ es: &es
|
|||||||
unverified: No verificado
|
unverified: No verificado
|
||||||
document: Documento
|
document: Documento
|
||||||
completed_at: Completado el
|
completed_at: Completado el
|
||||||
|
edit_recipient: Editar Destinatario
|
||||||
|
update_recipient: Actualizar Destinatario
|
||||||
|
use_international_format_1xxx_: 'Usa el formato internacional: +1xxx...'
|
||||||
|
submitter_cannot_be_updated: El remitente no puede ser actualizado.
|
||||||
|
at_least_one_field_must_be_filled: Al menos un campo debe estar completo.
|
||||||
|
archived_users: Usuarios Archivados
|
||||||
|
embedding_users: Usuarios Integrados
|
||||||
|
view_embedding_users: Ver Usuarios Integrado
|
||||||
submission_event_names:
|
submission_event_names:
|
||||||
send_email_to_html: '<b>Correo electrónico enviado</b> a %{submitter_name}'
|
send_email_to_html: '<b>Correo electrónico enviado</b> a %{submitter_name}'
|
||||||
send_reminder_email_to_html: '<b>Correo de recordatorio enviado</b> a %{submitter_name}'
|
send_reminder_email_to_html: '<b>Correo de recordatorio enviado</b> a %{submitter_name}'
|
||||||
@@ -1396,6 +1413,7 @@ it: &it
|
|||||||
require_signing_reason: Richiedere il motivo della firma
|
require_signing_reason: Richiedere il motivo della firma
|
||||||
allow_typed_text_signatures: Permettere firme con testo digitato
|
allow_typed_text_signatures: Permettere firme con testo digitato
|
||||||
allow_to_resubmit_completed_forms: Permettere di reinviare i moduli completati
|
allow_to_resubmit_completed_forms: Permettere di reinviare i moduli completati
|
||||||
|
allow_to_decline_documents: Permettere di rifiutare i documenti
|
||||||
remember_and_pre_fill_signatures: Ricordare e precompilare le firme
|
remember_and_pre_fill_signatures: Ricordare e precompilare le firme
|
||||||
require_authentication_for_file_download_links: "Richiedere l'autenticazione per i link di download dei file"
|
require_authentication_for_file_download_links: "Richiedere l'autenticazione per i link di download dei file"
|
||||||
combine_completed_documents_and_audit_log: Combinare i documenti completati e il Registro di Audit
|
combine_completed_documents_and_audit_log: Combinare i documenti completati e il Registro di Audit
|
||||||
@@ -1735,7 +1753,6 @@ it: &it
|
|||||||
back: Indietro
|
back: Indietro
|
||||||
add_secret: Aggiungi segreto
|
add_secret: Aggiungi segreto
|
||||||
submission_example_payload: Esempio di payload di invio
|
submission_example_payload: Esempio di payload di invio
|
||||||
status_users: '%{status} Utenti'
|
|
||||||
there_are_no_signatures: Non ci sono firme
|
there_are_no_signatures: Non ci sono firme
|
||||||
signed_with_trusted_certificate: Firmato con certificato affidabile
|
signed_with_trusted_certificate: Firmato con certificato affidabile
|
||||||
signed_with_external_certificate: Firmato con certificato esterno
|
signed_with_external_certificate: Firmato con certificato esterno
|
||||||
@@ -1875,6 +1892,14 @@ it: &it
|
|||||||
unverified: Non verificato
|
unverified: Non verificato
|
||||||
document: Documento
|
document: Documento
|
||||||
completed_at: Completato il
|
completed_at: Completato il
|
||||||
|
edit_recipient: Modifica Destinatario
|
||||||
|
update_recipient: Aggiorna Destinatario
|
||||||
|
use_international_format_1xxx_: 'Utilizza il formato internazionale: +1xxx...'
|
||||||
|
submitter_cannot_be_updated: Il mittente non può essere aggiornato.
|
||||||
|
at_least_one_field_must_be_filled: Almeno un campo deve essere compilato.
|
||||||
|
archived_users: Utenti Archiviati
|
||||||
|
embedding_users: Utenti Incorporati
|
||||||
|
view_embedding_users: Visualizza Utenti Incorporati
|
||||||
submission_event_names:
|
submission_event_names:
|
||||||
send_email_to_html: '<b>E-mail inviato</b> a %{submitter_name}'
|
send_email_to_html: '<b>E-mail inviato</b> a %{submitter_name}'
|
||||||
send_reminder_email_to_html: '<b>E-mail di promemoria inviato</b> a %{submitter_name}'
|
send_reminder_email_to_html: '<b>E-mail di promemoria inviato</b> a %{submitter_name}'
|
||||||
@@ -2028,6 +2053,7 @@ fr: &fr
|
|||||||
require_signing_reason: Demander une raison pour la signature
|
require_signing_reason: Demander une raison pour la signature
|
||||||
allow_typed_text_signatures: Autoriser les signatures en texte tapé
|
allow_typed_text_signatures: Autoriser les signatures en texte tapé
|
||||||
allow_to_resubmit_completed_forms: Autoriser la resoumission des formulaires complétés
|
allow_to_resubmit_completed_forms: Autoriser la resoumission des formulaires complétés
|
||||||
|
allow_to_decline_documents: Autoriser le refus des documents
|
||||||
remember_and_pre_fill_signatures: Se souvenir et préremplir les signatures
|
remember_and_pre_fill_signatures: Se souvenir et préremplir les signatures
|
||||||
require_authentication_for_file_download_links: Exiger une authentification pour les liens de téléchargement des fichiers
|
require_authentication_for_file_download_links: Exiger une authentification pour les liens de téléchargement des fichiers
|
||||||
combine_completed_documents_and_audit_log: "Combiner les documents complétés et le journal d'audit"
|
combine_completed_documents_and_audit_log: "Combiner les documents complétés et le journal d'audit"
|
||||||
@@ -2367,7 +2393,6 @@ fr: &fr
|
|||||||
back: Retour
|
back: Retour
|
||||||
add_secret: Ajouter un secret
|
add_secret: Ajouter un secret
|
||||||
submission_example_payload: Exemple de payload de soumission
|
submission_example_payload: Exemple de payload de soumission
|
||||||
status_users: '%{status} Utilisateurs'
|
|
||||||
there_are_no_signatures: "Il n'y a pas de signatures"
|
there_are_no_signatures: "Il n'y a pas de signatures"
|
||||||
signed_with_trusted_certificate: Signé avec un certificat de confiance
|
signed_with_trusted_certificate: Signé avec un certificat de confiance
|
||||||
signed_with_external_certificate: Signé avec un certificat externe
|
signed_with_external_certificate: Signé avec un certificat externe
|
||||||
@@ -2507,6 +2532,14 @@ fr: &fr
|
|||||||
unverified: Non vérifié
|
unverified: Non vérifié
|
||||||
document: Document
|
document: Document
|
||||||
completed_at: Terminé le
|
completed_at: Terminé le
|
||||||
|
edit_recipient: Modifica Destinatario
|
||||||
|
update_recipient: Aggiorna Destinatario
|
||||||
|
use_international_format_1xxx_: 'Utilizza il formato internazionale: +1xxx...'
|
||||||
|
submitter_cannot_be_updated: Il mittente non può essere aggiornato.
|
||||||
|
at_least_one_field_must_be_filled: Almeno un campo deve essere compilato.
|
||||||
|
archived_users: Utilisateurs Archivés
|
||||||
|
embedding_users: Utilisateurs Intégrés
|
||||||
|
view_embedding_users: Voir les Utilisateurs Intégrés
|
||||||
submission_event_names:
|
submission_event_names:
|
||||||
send_email_to_html: '<b>E-mail envoyé</b> à %{submitter_name}'
|
send_email_to_html: '<b>E-mail envoyé</b> à %{submitter_name}'
|
||||||
send_reminder_email_to_html: '<b>E-mail de rappel envoyé</b> à %{submitter_name}'
|
send_reminder_email_to_html: '<b>E-mail de rappel envoyé</b> à %{submitter_name}'
|
||||||
@@ -2659,6 +2692,7 @@ pt: &pt
|
|||||||
require_signing_reason: Requerer motivo para assinatura
|
require_signing_reason: Requerer motivo para assinatura
|
||||||
allow_typed_text_signatures: Permitir assinaturas digitadas
|
allow_typed_text_signatures: Permitir assinaturas digitadas
|
||||||
allow_to_resubmit_completed_forms: Permitir reenviar formulários concluídos
|
allow_to_resubmit_completed_forms: Permitir reenviar formulários concluídos
|
||||||
|
allow_to_decline_documents: Permitir recusar documentos
|
||||||
remember_and_pre_fill_signatures: Lembrar e preencher assinaturas automaticamente
|
remember_and_pre_fill_signatures: Lembrar e preencher assinaturas automaticamente
|
||||||
require_authentication_for_file_download_links: Requerer autenticação para links de download de arquivos
|
require_authentication_for_file_download_links: Requerer autenticação para links de download de arquivos
|
||||||
combine_completed_documents_and_audit_log: Combinar documentos concluídos e log de auditoria
|
combine_completed_documents_and_audit_log: Combinar documentos concluídos e log de auditoria
|
||||||
@@ -2998,7 +3032,6 @@ pt: &pt
|
|||||||
back: Voltar
|
back: Voltar
|
||||||
add_secret: Adicionar segredo
|
add_secret: Adicionar segredo
|
||||||
submission_example_payload: Exemplo de payload de submissão
|
submission_example_payload: Exemplo de payload de submissão
|
||||||
status_users: '%{status} Usuários'
|
|
||||||
there_are_no_signatures: Não há assinaturas
|
there_are_no_signatures: Não há assinaturas
|
||||||
signed_with_trusted_certificate: Assinado com certificado confiável
|
signed_with_trusted_certificate: Assinado com certificado confiável
|
||||||
signed_with_external_certificate: Assinado com certificado externo
|
signed_with_external_certificate: Assinado com certificado externo
|
||||||
@@ -3138,6 +3171,14 @@ pt: &pt
|
|||||||
unverified: Não verificado
|
unverified: Não verificado
|
||||||
document: Documento
|
document: Documento
|
||||||
completed_at: Concluído em
|
completed_at: Concluído em
|
||||||
|
edit_recipient: Editar Destinatário
|
||||||
|
update_recipient: Atualizar Destinatário
|
||||||
|
use_international_format_1xxx_: 'Use o formato internacional: +1xxx...'
|
||||||
|
submitter_cannot_be_updated: O remetente não pode ser atualizado.
|
||||||
|
at_least_one_field_must_be_filled: Pelo menos um campo deve ser preenchido.
|
||||||
|
archived_users: Usuários Arquivados
|
||||||
|
embedding_users: Usuários Incorporados
|
||||||
|
view_embedding_users: Ver Usuários Incorporados
|
||||||
submission_event_names:
|
submission_event_names:
|
||||||
send_email_to_html: '<b>E-mail enviado</b> para %{submitter_name}'
|
send_email_to_html: '<b>E-mail enviado</b> para %{submitter_name}'
|
||||||
send_reminder_email_to_html: '<b>E-mail de lembrete enviado</b> para %{submitter_name}'
|
send_reminder_email_to_html: '<b>E-mail de lembrete enviado</b> para %{submitter_name}'
|
||||||
@@ -3290,6 +3331,7 @@ de: &de
|
|||||||
require_signing_reason: Grund für die Signatur erforderlich
|
require_signing_reason: Grund für die Signatur erforderlich
|
||||||
allow_typed_text_signatures: Unterschriften mit getipptem Text zulassen
|
allow_typed_text_signatures: Unterschriften mit getipptem Text zulassen
|
||||||
allow_to_resubmit_completed_forms: Erneutes Einreichen abgeschlossener Formulare zulassen
|
allow_to_resubmit_completed_forms: Erneutes Einreichen abgeschlossener Formulare zulassen
|
||||||
|
allow_to_decline_documents: Erlauben, Dokumente abzulehnen
|
||||||
remember_and_pre_fill_signatures: Signaturen merken und vorab ausfüllen
|
remember_and_pre_fill_signatures: Signaturen merken und vorab ausfüllen
|
||||||
require_authentication_for_file_download_links: Authentifizierung für Dateidownload-Links erforderlich
|
require_authentication_for_file_download_links: Authentifizierung für Dateidownload-Links erforderlich
|
||||||
combine_completed_documents_and_audit_log: Abgeschlossene Dokumente und Prüfprotokoll kombinieren
|
combine_completed_documents_and_audit_log: Abgeschlossene Dokumente und Prüfprotokoll kombinieren
|
||||||
@@ -3629,7 +3671,6 @@ de: &de
|
|||||||
back: Zurück
|
back: Zurück
|
||||||
add_secret: Geheimnis hinzufügen
|
add_secret: Geheimnis hinzufügen
|
||||||
submission_example_payload: Beispiel-Payload für Einreichung
|
submission_example_payload: Beispiel-Payload für Einreichung
|
||||||
status_users: '%{status} Benutzer'
|
|
||||||
there_are_no_signatures: Es gibt keine Unterschriften
|
there_are_no_signatures: Es gibt keine Unterschriften
|
||||||
signed_with_trusted_certificate: Signiert mit vertrauenswürdigem Zertifikat
|
signed_with_trusted_certificate: Signiert mit vertrauenswürdigem Zertifikat
|
||||||
signed_with_external_certificate: Signiert mit externem Zertifikat
|
signed_with_external_certificate: Signiert mit externem Zertifikat
|
||||||
@@ -3769,6 +3810,14 @@ de: &de
|
|||||||
unverified: Nicht verifiziert
|
unverified: Nicht verifiziert
|
||||||
document: Dokument
|
document: Dokument
|
||||||
completed_at: Abgeschlossen am
|
completed_at: Abgeschlossen am
|
||||||
|
edit_recipient: Empfänger bearbeiten
|
||||||
|
update_recipient: Empfänger aktualisieren
|
||||||
|
use_international_format_1xxx_: 'Verwenden Sie das internationale Format: +1xxx...'
|
||||||
|
submitter_cannot_be_updated: Der Absender kann nicht aktualisiert werden.
|
||||||
|
at_least_one_field_must_be_filled: Mindestens ein Feld muss ausgefüllt werden.
|
||||||
|
archived_users: Archivierte Benutzer
|
||||||
|
embedding_users: Einbettende Benutzer
|
||||||
|
view_embedding_users: Einbettende Benutzer anzeigen
|
||||||
submission_event_names:
|
submission_event_names:
|
||||||
send_email_to_html: '<b>E-Mail gesendet</b> an %{submitter_name}'
|
send_email_to_html: '<b>E-Mail gesendet</b> an %{submitter_name}'
|
||||||
send_reminder_email_to_html: '<b>Erinnerungs-E-Mail gesendet</b> an %{submitter_name}'
|
send_reminder_email_to_html: '<b>Erinnerungs-E-Mail gesendet</b> an %{submitter_name}'
|
||||||
|
|||||||
+5
-1
@@ -34,6 +34,7 @@ Rails.application.routes.draw do
|
|||||||
resources :submitter_form_views, only: %i[create]
|
resources :submitter_form_views, only: %i[create]
|
||||||
resources :submitters, only: %i[index show update]
|
resources :submitters, only: %i[index show update]
|
||||||
resources :submissions, only: %i[index show create destroy] do
|
resources :submissions, only: %i[index show create destroy] do
|
||||||
|
resources :documents, only: %i[index], controller: 'submission_documents'
|
||||||
collection do
|
collection do
|
||||||
resources :init, only: %i[create], controller: 'submissions'
|
resources :init, only: %i[create], controller: 'submissions'
|
||||||
resources :emails, only: %i[create], controller: 'submissions', as: :submissions_emails
|
resources :emails, only: %i[create], controller: 'submissions', as: :submissions_emails
|
||||||
@@ -69,6 +70,7 @@ Rails.application.routes.draw do
|
|||||||
resources :submissions_archived, only: %i[index], path: 'submissions/archived'
|
resources :submissions_archived, only: %i[index], path: 'submissions/archived'
|
||||||
resources :submissions, only: %i[index], controller: 'submissions_dashboard'
|
resources :submissions, only: %i[index], controller: 'submissions_dashboard'
|
||||||
resources :submissions, only: %i[show destroy]
|
resources :submissions, only: %i[show destroy]
|
||||||
|
resources :submitters, only: %i[edit update]
|
||||||
resources :console_redirect, only: %i[index]
|
resources :console_redirect, only: %i[index]
|
||||||
resources :upgrade, only: %i[index], controller: 'console_redirect'
|
resources :upgrade, only: %i[index], controller: 'console_redirect'
|
||||||
resources :manage, only: %i[index], controller: 'console_redirect'
|
resources :manage, only: %i[index], controller: 'console_redirect'
|
||||||
@@ -151,15 +153,17 @@ Rails.application.routes.draw do
|
|||||||
scope '/settings', as: :settings do
|
scope '/settings', as: :settings do
|
||||||
unless Docuseal.multitenant?
|
unless Docuseal.multitenant?
|
||||||
resources :storage, only: %i[index create], controller: 'storage_settings'
|
resources :storage, only: %i[index create], controller: 'storage_settings'
|
||||||
resources :email, only: %i[index create], controller: 'email_smtp_settings'
|
|
||||||
resources :sms, only: %i[index], controller: 'sms_settings'
|
resources :sms, only: %i[index], controller: 'sms_settings'
|
||||||
end
|
end
|
||||||
|
resources :email, only: %i[index create], controller: 'email_smtp_settings'
|
||||||
resources :sso, only: %i[index], controller: 'sso_settings'
|
resources :sso, only: %i[index], controller: 'sso_settings'
|
||||||
resources :notifications, only: %i[index create], controller: 'notifications_settings'
|
resources :notifications, only: %i[index create], controller: 'notifications_settings'
|
||||||
resource :esign, only: %i[show create new update destroy], controller: 'esign_settings'
|
resource :esign, only: %i[show create new update destroy], controller: 'esign_settings'
|
||||||
resources :users, only: %i[index]
|
resources :users, only: %i[index]
|
||||||
resources :archived_users, only: %i[index], path: 'users/:status', controller: 'users',
|
resources :archived_users, only: %i[index], path: 'users/:status', controller: 'users',
|
||||||
defaults: { status: :archived }
|
defaults: { status: :archived }
|
||||||
|
resources :integration_users, only: %i[index], path: 'users/:status', controller: 'users',
|
||||||
|
defaults: { status: :integration }
|
||||||
resource :personalization, only: %i[show create], controller: 'personalization_settings'
|
resource :personalization, only: %i[show create], controller: 'personalization_settings'
|
||||||
resources :api, only: %i[index create], controller: 'api_settings'
|
resources :api, only: %i[index create], controller: 'api_settings'
|
||||||
resource :webhooks, only: %i[show create update], controller: 'webhook_settings'
|
resource :webhooks, only: %i[show create update], controller: 'webhook_settings'
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
queues:
|
queues:
|
||||||
- [default, 1]
|
- [default, 1]
|
||||||
- [webhooks, 1]
|
- [webhooks, 1]
|
||||||
|
- [images, 1]
|
||||||
- [mailers, 1]
|
- [mailers, 1]
|
||||||
- [recurrent, 1]
|
- [recurrent, 1]
|
||||||
- [rollbar, 1]
|
- [rollbar, 1]
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const configs = generateWebpackConfig({
|
|||||||
},
|
},
|
||||||
optimization: {
|
optimization: {
|
||||||
runtimeChunk: false,
|
runtimeChunk: false,
|
||||||
|
concatenateModules: !process.env.BUNDLE_ANALYZE,
|
||||||
splitChunks: {
|
splitChunks: {
|
||||||
chunks (chunk) {
|
chunks (chunk) {
|
||||||
return chunk.name !== 'rollbar'
|
return chunk.name !== 'rollbar'
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateCompletedSubmittersAndDocuments < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
create_table :completed_submitters do |t|
|
||||||
|
t.bigint :submitter_id, null: false, index: true
|
||||||
|
t.bigint :submission_id, null: false
|
||||||
|
t.bigint :account_id, null: false, index: true
|
||||||
|
t.bigint :template_id, null: false
|
||||||
|
t.string :source, null: false
|
||||||
|
t.integer :sms_count, null: false
|
||||||
|
t.datetime :completed_at, null: false
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table :completed_documents do |t|
|
||||||
|
t.bigint :submitter_id, null: false, index: true
|
||||||
|
t.string :sha256, null: false, index: true
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class PopulateCompletedSubmittersAndDocuments < ActiveRecord::Migration[7.2]
|
||||||
|
disable_ddl_transaction
|
||||||
|
|
||||||
|
class MigrationSubmitter < ApplicationRecord
|
||||||
|
self.table_name = 'submitters'
|
||||||
|
|
||||||
|
belongs_to :submission, class_name: 'MigrationSubmission'
|
||||||
|
has_many :submission_sms_events, -> { where(event_type: %w[send_sms send_2fa_sms]) },
|
||||||
|
class_name: 'MigrationSubmissionEvent', foreign_key: :submitter_id
|
||||||
|
end
|
||||||
|
|
||||||
|
class MigrationSubmission < ApplicationRecord
|
||||||
|
self.table_name = 'submissions'
|
||||||
|
end
|
||||||
|
|
||||||
|
class MigrationSubmissionEvent < ApplicationRecord
|
||||||
|
self.table_name = 'submission_events'
|
||||||
|
end
|
||||||
|
|
||||||
|
class MigrationCompletedSubmitter < ApplicationRecord
|
||||||
|
self.table_name = 'completed_submitters'
|
||||||
|
end
|
||||||
|
|
||||||
|
class MigrationCompletedDocument < ApplicationRecord
|
||||||
|
self.table_name = 'completed_documents'
|
||||||
|
end
|
||||||
|
|
||||||
|
def up
|
||||||
|
submitters = MigrationSubmitter.where.not(completed_at: nil)
|
||||||
|
.preload(:submission, :submission_sms_events)
|
||||||
|
|
||||||
|
count = submitters.count
|
||||||
|
|
||||||
|
puts "Updating the database - it might take ~#{(count / 1000 * 3) + 1} seconds" if count > 2000
|
||||||
|
|
||||||
|
submitters.find_each do |submitter|
|
||||||
|
completed_submitter = MigrationCompletedSubmitter.find_or_initialize_by(submitter_id: submitter.id)
|
||||||
|
|
||||||
|
next if completed_submitter.persisted?
|
||||||
|
|
||||||
|
submission = submitter.submission
|
||||||
|
|
||||||
|
completed_submitter.assign_attributes(
|
||||||
|
submission_id: submitter.submission_id,
|
||||||
|
account_id: submission.account_id,
|
||||||
|
template_id: submission.template_id,
|
||||||
|
source: submission.source,
|
||||||
|
sms_count: submitter.submission_sms_events.size,
|
||||||
|
completed_at: submitter.completed_at,
|
||||||
|
created_at: submitter.completed_at,
|
||||||
|
updated_at: submitter.completed_at
|
||||||
|
)
|
||||||
|
|
||||||
|
completed_submitter.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
attachments = ActiveStorage::Attachment.where(record_type: 'Submitter', name: 'documents').preload(:blob)
|
||||||
|
|
||||||
|
attachments.find_each do |attachment|
|
||||||
|
sha256 = attachment.metadata['sha256']
|
||||||
|
|
||||||
|
next if sha256.blank?
|
||||||
|
|
||||||
|
completed_document = MigrationCompletedDocument.find_or_initialize_by(submitter_id: attachment.record_id, sha256:)
|
||||||
|
|
||||||
|
next if completed_document.persisted?
|
||||||
|
|
||||||
|
completed_document.assign_attributes(created_at: attachment.created_at, updated_at: attachment.created_at)
|
||||||
|
|
||||||
|
completed_document.save!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddUniqueIndexOnCompletedSubmitters < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
remove_index :completed_submitters, :submitter_id
|
||||||
|
add_index :completed_submitters, :submitter_id, unique: true
|
||||||
|
end
|
||||||
|
end
|
||||||
+24
-1
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.1].define(version: 2024_08_20_180922) do
|
ActiveRecord::Schema[7.2].define(version: 2024_10_26_161207) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
|
||||||
@@ -89,6 +89,29 @@ ActiveRecord::Schema[7.1].define(version: 2024_08_20_180922) do
|
|||||||
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
|
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "completed_documents", force: :cascade do |t|
|
||||||
|
t.bigint "submitter_id", null: false
|
||||||
|
t.string "sha256", null: false
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["sha256"], name: "index_completed_documents_on_sha256"
|
||||||
|
t.index ["submitter_id"], name: "index_completed_documents_on_submitter_id"
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table "completed_submitters", force: :cascade do |t|
|
||||||
|
t.bigint "submitter_id", null: false
|
||||||
|
t.bigint "submission_id", null: false
|
||||||
|
t.bigint "account_id", null: false
|
||||||
|
t.bigint "template_id", null: false
|
||||||
|
t.string "source", null: false
|
||||||
|
t.integer "sms_count", null: false
|
||||||
|
t.datetime "completed_at", null: false
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["account_id"], name: "index_completed_submitters_on_account_id"
|
||||||
|
t.index ["submitter_id"], name: "index_completed_submitters_on_submitter_id", unique: true
|
||||||
|
end
|
||||||
|
|
||||||
create_table "document_generation_events", force: :cascade do |t|
|
create_table "document_generation_events", force: :cascade do |t|
|
||||||
t.bigint "submitter_id", null: false
|
t.bigint "submitter_id", null: false
|
||||||
t.string "event_name", null: false
|
t.string "event_name", null: false
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@ module Accounts
|
|||||||
new_user.uuid = SecureRandom.uuid
|
new_user.uuid = SecureRandom.uuid
|
||||||
new_user.account = new_account
|
new_user.account = new_account
|
||||||
new_user.encrypted_password = SecureRandom.hex
|
new_user.encrypted_password = SecureRandom.hex
|
||||||
new_user.email = "#{SecureRandom.hex}@docuseal.co"
|
new_user.email = "#{SecureRandom.hex}@docuseal.com"
|
||||||
|
|
||||||
account.templates.each do |template|
|
account.templates.each do |template|
|
||||||
new_template = template.dup
|
new_template = template.dup
|
||||||
|
|||||||
@@ -16,19 +16,27 @@ module ActionMailerConfigsInterceptor
|
|||||||
end
|
end
|
||||||
|
|
||||||
if Rails.env.production? && Rails.application.config.action_mailer.delivery_method
|
if Rails.env.production? && Rails.application.config.action_mailer.delivery_method
|
||||||
message.from = ENV.fetch('SMTP_FROM')
|
from = ENV.fetch('SMTP_FROM').to_s.split(',').sample
|
||||||
|
|
||||||
|
if from.match?(User::FULL_EMAIL_REGEXP)
|
||||||
|
message[:from] = message[:from].to_s.sub(User::EMAIL_REGEXP, from)
|
||||||
|
else
|
||||||
|
message.from = from
|
||||||
|
end
|
||||||
|
|
||||||
return message
|
return message
|
||||||
end
|
end
|
||||||
|
|
||||||
email_configs = EncryptedConfig.find_by(key: EncryptedConfig::EMAIL_SMTP_KEY)
|
unless Docuseal.multitenant?
|
||||||
|
email_configs = EncryptedConfig.find_by(key: EncryptedConfig::EMAIL_SMTP_KEY)
|
||||||
|
|
||||||
if email_configs
|
if email_configs
|
||||||
message.delivery_method(:smtp, build_smtp_configs_hash(email_configs))
|
message.delivery_method(:smtp, build_smtp_configs_hash(email_configs))
|
||||||
|
|
||||||
message.from = %("#{email_configs.account.name.to_s.delete('"')}" <#{email_configs.value['from_email']}>)
|
message.from = %("#{email_configs.account.name.to_s.delete('"')}" <#{email_configs.value['from_email']}>)
|
||||||
else
|
else
|
||||||
message.delivery_method(:test)
|
message.delivery_method(:test)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
message
|
message
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ module ActionMailerEventsObserver
|
|||||||
emailable_type:,
|
emailable_type:,
|
||||||
event_type: :send,
|
event_type: :send,
|
||||||
email:,
|
email:,
|
||||||
data: { method: mail.delivery_method.class.name.underscore },
|
data: { from: mail.from, method: mail.delivery_method.class.name.underscore },
|
||||||
event_datetime: Time.current
|
event_datetime: Time.current
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|||||||
+4
-4
@@ -12,7 +12,7 @@ module Docuseal
|
|||||||
TWITTER_URL = 'https://twitter.com/docusealco'
|
TWITTER_URL = 'https://twitter.com/docusealco'
|
||||||
TWITTER_HANDLE = '@docusealco'
|
TWITTER_HANDLE = '@docusealco'
|
||||||
CHATGPT_URL = 'https://chatgpt.com/g/g-9hg8AAw0r-docuseal'
|
CHATGPT_URL = 'https://chatgpt.com/g/g-9hg8AAw0r-docuseal'
|
||||||
SUPPORT_EMAIL = 'support@docuseal.co'
|
SUPPORT_EMAIL = 'support@docuseal.com'
|
||||||
HOST = ENV.fetch('HOST', 'localhost')
|
HOST = ENV.fetch('HOST', 'localhost')
|
||||||
AATL_CERT_NAME = 'docuseal_aatl'
|
AATL_CERT_NAME = 'docuseal_aatl'
|
||||||
CONSOLE_URL = if Rails.env.development?
|
CONSOLE_URL = if Rails.env.development?
|
||||||
@@ -20,19 +20,19 @@ module Docuseal
|
|||||||
elsif ENV['MULTITENANT'] == 'true'
|
elsif ENV['MULTITENANT'] == 'true'
|
||||||
"https://console.#{HOST}"
|
"https://console.#{HOST}"
|
||||||
else
|
else
|
||||||
'https://console.docuseal.co'
|
'https://console.docuseal.com'
|
||||||
end
|
end
|
||||||
CLOUD_URL = if Rails.env.development?
|
CLOUD_URL = if Rails.env.development?
|
||||||
'http://localhost:3000'
|
'http://localhost:3000'
|
||||||
else
|
else
|
||||||
'https://docuseal.co'
|
'https://docuseal.com'
|
||||||
end
|
end
|
||||||
CDN_URL = if Rails.env.development?
|
CDN_URL = if Rails.env.development?
|
||||||
'http://localhost:3000'
|
'http://localhost:3000'
|
||||||
elsif ENV['MULTITENANT'] == 'true'
|
elsif ENV['MULTITENANT'] == 'true'
|
||||||
"https://cdn.#{HOST}"
|
"https://cdn.#{HOST}"
|
||||||
else
|
else
|
||||||
'https://cdn.docuseal.co'
|
'https://cdn.docuseal.com'
|
||||||
end
|
end
|
||||||
|
|
||||||
CERTS = JSON.parse(ENV.fetch('CERTS', '{}'))
|
CERTS = JSON.parse(ENV.fetch('CERTS', '{}'))
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
module Params
|
module Params
|
||||||
class BaseValidator
|
class BaseValidator
|
||||||
|
EMAIL_REGEXP = User::FULL_EMAIL_REGEXP
|
||||||
|
|
||||||
InvalidParameterError = Class.new(StandardError)
|
InvalidParameterError = Class.new(StandardError)
|
||||||
|
|
||||||
def self.call(...)
|
def self.call(...)
|
||||||
@@ -65,6 +67,28 @@ module Params
|
|||||||
raise_error(message || "#{key} must follow the #{regexp.source} format")
|
raise_error(message || "#{key} must follow the #{regexp.source} format")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def email_format(params, key, message: nil)
|
||||||
|
return if params.blank?
|
||||||
|
return if params[key].blank?
|
||||||
|
return if params[key].to_s.include?('<')
|
||||||
|
|
||||||
|
if params[key].to_s.strip.split(/\s*[;,]\s*/).compact_blank
|
||||||
|
.all? { |email| EmailTypo::DotCom.call(email).match?(EMAIL_REGEXP) }
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
raise_error(message || "#{key} must follow the email format: '#{params[key]}'")
|
||||||
|
end
|
||||||
|
|
||||||
|
def unique_value(params, key, message: nil)
|
||||||
|
return unless params.is_a?(Array)
|
||||||
|
return if params.none?
|
||||||
|
return if params.all? { |p| p[key].blank? }
|
||||||
|
return if params.pluck(key).compact_blank.uniq.size == params.pluck(key).compact_blank.size
|
||||||
|
|
||||||
|
raise_error(message || "#{key} must be unique")
|
||||||
|
end
|
||||||
|
|
||||||
def in_path(params, path = [])
|
def in_path(params, path = [])
|
||||||
old_path = @current_path
|
old_path = @current_path
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ module Params
|
|||||||
required(params, %i[emails email])
|
required(params, %i[emails email])
|
||||||
|
|
||||||
type(params, :emails, String)
|
type(params, :emails, String)
|
||||||
|
email_format(params, :emails, message: 'emails are invalid')
|
||||||
boolean(params, :send_email)
|
boolean(params, :send_email)
|
||||||
type(params, :message, Hash)
|
type(params, :message, Hash)
|
||||||
|
|
||||||
@@ -43,8 +44,8 @@ module Params
|
|||||||
type(params, :completed_redirect_url, String)
|
type(params, :completed_redirect_url, String)
|
||||||
type(params, :bcc_completed, String)
|
type(params, :bcc_completed, String)
|
||||||
type(params, :reply_to, String)
|
type(params, :reply_to, String)
|
||||||
format(params, :bcc_completed, /@/, message: 'bcc_completed email is invalid')
|
email_format(params, :bcc_completed, message: 'bcc_completed email is invalid')
|
||||||
format(params, :reply_to, /@/, message: 'reply_to email is invalid')
|
email_format(params, :reply_to, message: 'reply_to email is invalid')
|
||||||
type(params, :message, Hash)
|
type(params, :message, Hash)
|
||||||
type(params, :submitters, Array)
|
type(params, :submitters, Array)
|
||||||
|
|
||||||
@@ -60,6 +61,7 @@ module Params
|
|||||||
if params[:submitters].present?
|
if params[:submitters].present?
|
||||||
in_path(params, :submitters) do |submitters_params|
|
in_path(params, :submitters) do |submitters_params|
|
||||||
type(submitters_params, 0, Hash)
|
type(submitters_params, 0, Hash)
|
||||||
|
unique_value(submitters_params, :role)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -74,8 +76,8 @@ module Params
|
|||||||
type(submitter_params, :name, String)
|
type(submitter_params, :name, String)
|
||||||
type(submitter_params, :reply_to, String)
|
type(submitter_params, :reply_to, String)
|
||||||
type(submitter_params, :email, String)
|
type(submitter_params, :email, String)
|
||||||
format(submitter_params, :email, /@/, message: 'email is invalid')
|
email_format(submitter_params, :email, message: 'email is invalid')
|
||||||
format(submitter_params, :reply_to, /@/, message: 'reply_to email is invalid')
|
email_format(submitter_params, :reply_to, message: 'reply_to email is invalid')
|
||||||
type(submitter_params, :phone, String)
|
type(submitter_params, :phone, String)
|
||||||
format(submitter_params, :phone, /\A\+\d+\z/,
|
format(submitter_params, :phone, /\A\+\d+\z/,
|
||||||
message: 'phone should start with +<country code> and contain only digits')
|
message: 'phone should start with +<country code> and contain only digits')
|
||||||
@@ -105,7 +107,7 @@ module Params
|
|||||||
type(params, :order, String)
|
type(params, :order, String)
|
||||||
type(params, :completed_redirect_url, String)
|
type(params, :completed_redirect_url, String)
|
||||||
type(params, :bcc_completed, String)
|
type(params, :bcc_completed, String)
|
||||||
format(params, :bcc_completed, /@/, message: 'bcc_completed email is invalid')
|
email_format(params, :bcc_completed, message: 'bcc_completed email is invalid')
|
||||||
type(params, :message, Hash)
|
type(params, :message, Hash)
|
||||||
|
|
||||||
in_path(params, :message) do |message_params|
|
in_path(params, :message) do |message_params|
|
||||||
|
|||||||
@@ -2,8 +2,11 @@
|
|||||||
|
|
||||||
module Submissions
|
module Submissions
|
||||||
module CreateFromSubmitters
|
module CreateFromSubmitters
|
||||||
|
BaseError = Class.new(StandardError)
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
|
# rubocop:disable Metrics/BlockLength
|
||||||
def call(template:, user:, submissions_attrs:, source:, submitters_order:, params: {})
|
def call(template:, user:, submissions_attrs:, source:, submitters_order:, params: {})
|
||||||
preferences = Submitters.normalize_preferences(user.account, user, params)
|
preferences = Submitters.normalize_preferences(user.account, user, params)
|
||||||
|
|
||||||
@@ -37,6 +40,10 @@ module Submissions
|
|||||||
preferences: preferences.merge(submission_preferences))
|
preferences: preferences.merge(submission_preferences))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if submission.submitters.size > template.submitters.size
|
||||||
|
raise BaseError, 'Defined more signing parties than in template'
|
||||||
|
end
|
||||||
|
|
||||||
next if submission.submitters.blank?
|
next if submission.submitters.blank?
|
||||||
|
|
||||||
maybe_add_invite_submitters(submission, template)
|
maybe_add_invite_submitters(submission, template)
|
||||||
@@ -44,6 +51,7 @@ module Submissions
|
|||||||
submission.tap(&:save!)
|
submission.tap(&:save!)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
# rubocop:enable Metrics/BlockLength
|
||||||
|
|
||||||
def maybe_add_invite_submitters(submission, template)
|
def maybe_add_invite_submitters(submission, template)
|
||||||
template.submitters.each do |item|
|
template.submitters.each do |item|
|
||||||
|
|||||||
@@ -393,7 +393,7 @@ module Submissions
|
|||||||
end
|
end
|
||||||
|
|
||||||
def sign_reason
|
def sign_reason
|
||||||
'Signed with DocuSeal.co'
|
'Signed with DocuSeal.com'
|
||||||
end
|
end
|
||||||
|
|
||||||
def maybe_add_background(_canvas, _submission, _page_size); end
|
def maybe_add_background(_canvas, _submission, _page_size); end
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Submissions
|
||||||
|
module GeneratePreviewAttachments
|
||||||
|
module_function
|
||||||
|
|
||||||
|
# rubocop:disable Metrics
|
||||||
|
def call(submission, values_hash: nil)
|
||||||
|
values_hash ||= build_values_hash(submission)
|
||||||
|
|
||||||
|
configs = submission.account.account_configs.where(key: [AccountConfig::FLATTEN_RESULT_PDF_KEY,
|
||||||
|
AccountConfig::WITH_SIGNATURE_ID])
|
||||||
|
|
||||||
|
with_signature_id = configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true
|
||||||
|
is_flatten = configs.find { |c| c.key == AccountConfig::FLATTEN_RESULT_PDF_KEY }&.value != false
|
||||||
|
|
||||||
|
pdfs_index = GenerateResultAttachments.build_pdfs_index(submission, flatten: is_flatten)
|
||||||
|
|
||||||
|
submission.submitters.where(completed_at: nil).preload(attachments_attachments: :blob).each do |submitter|
|
||||||
|
GenerateResultAttachments.fill_submitter_fields(submitter, submission.account, pdfs_index,
|
||||||
|
with_signature_id:, is_flatten:)
|
||||||
|
end
|
||||||
|
|
||||||
|
template = submission.template
|
||||||
|
|
||||||
|
image_pdfs = []
|
||||||
|
original_documents = template.documents.preload(:blob)
|
||||||
|
|
||||||
|
result_attachments =
|
||||||
|
(submission.template_schema || template.schema).map do |item|
|
||||||
|
pdf = pdfs_index[item['attachment_uuid']]
|
||||||
|
|
||||||
|
if original_documents.find { |a| a.uuid == item['attachment_uuid'] }.image?
|
||||||
|
pdf = GenerateResultAttachments.normalize_image_pdf(pdf)
|
||||||
|
|
||||||
|
image_pdfs << pdf
|
||||||
|
end
|
||||||
|
|
||||||
|
build_pdf_attachment(pdf:, submission:,
|
||||||
|
uuid: item['attachment_uuid'],
|
||||||
|
values_hash:,
|
||||||
|
name: item['name'])
|
||||||
|
end
|
||||||
|
|
||||||
|
return ApplicationRecord.no_touching { result_attachments.map { |e| e.tap(&:save!) } } if image_pdfs.size < 2
|
||||||
|
|
||||||
|
images_pdf =
|
||||||
|
image_pdfs.each_with_object(HexaPDF::Document.new) do |pdf, doc|
|
||||||
|
pdf.pages.each { |page| doc.pages << doc.import(page) }
|
||||||
|
end
|
||||||
|
|
||||||
|
images_pdf = GenerateResultAttachments.normalize_image_pdf(images_pdf)
|
||||||
|
|
||||||
|
images_pdf_attachment =
|
||||||
|
build_pdf_attachment(
|
||||||
|
pdf: images_pdf,
|
||||||
|
submission:,
|
||||||
|
uuid: GenerateResultAttachments.images_pdf_uuid(original_documents.select(&:image?)),
|
||||||
|
values_hash:,
|
||||||
|
name: template.name
|
||||||
|
)
|
||||||
|
|
||||||
|
ApplicationRecord.no_touching do
|
||||||
|
(result_attachments + [images_pdf_attachment]).map { |e| e.tap(&:save!) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_values_hash(submission)
|
||||||
|
submission.submitters.reduce({}) { |acc, s| acc.merge(s.values) }.hash
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_pdf_attachment(pdf:, submission:, uuid:, name:, values_hash:)
|
||||||
|
io = StringIO.new
|
||||||
|
|
||||||
|
begin
|
||||||
|
pdf.write(io, incremental: true, validate: false)
|
||||||
|
rescue HexaPDF::MalformedPDFError => e
|
||||||
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
|
pdf.write(io, incremental: false, validate: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
ActiveStorage::Attachment.new(
|
||||||
|
blob: ActiveStorage::Blob.create_and_upload!(io: io.tap(&:rewind), filename: "#{name}.pdf"),
|
||||||
|
metadata: { original_uuid: uuid,
|
||||||
|
values_hash:,
|
||||||
|
analyzed: true,
|
||||||
|
sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(io.string)) },
|
||||||
|
name: 'preview_documents',
|
||||||
|
record: submission
|
||||||
|
)
|
||||||
|
end
|
||||||
|
# rubocop:enable Metrics
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -92,19 +92,13 @@ module Submissions
|
|||||||
end
|
end
|
||||||
|
|
||||||
def generate_pdfs(submitter)
|
def generate_pdfs(submitter)
|
||||||
cell_layouter = HexaPDF::Layout::TextLayouter.new(text_valign: :center, text_align: :center)
|
configs = submitter.account.account_configs.where(key: [AccountConfig::FLATTEN_RESULT_PDF_KEY,
|
||||||
|
AccountConfig::WITH_SIGNATURE_ID])
|
||||||
|
|
||||||
with_signature_id = submitter.account.account_configs
|
with_signature_id = configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true
|
||||||
.exists?(key: AccountConfig::WITH_SIGNATURE_ID, value: true)
|
is_flatten = configs.find { |c| c.key == AccountConfig::FLATTEN_RESULT_PDF_KEY }&.value != false
|
||||||
|
|
||||||
is_flatten =
|
pdfs_index = build_pdfs_index(submitter.submission, submitter:, flatten: is_flatten)
|
||||||
submitter.account.account_configs
|
|
||||||
.find_or_initialize_by(key: AccountConfig::FLATTEN_RESULT_PDF_KEY).value != false
|
|
||||||
|
|
||||||
account = submitter.account
|
|
||||||
attachments_data_cache = {}
|
|
||||||
|
|
||||||
pdfs_index = build_pdfs_index(submitter, flatten: is_flatten)
|
|
||||||
|
|
||||||
if with_signature_id || submitter.account.testing?
|
if with_signature_id || submitter.account.testing?
|
||||||
pdfs_index.each_value do |pdf|
|
pdfs_index.each_value do |pdf|
|
||||||
@@ -145,6 +139,16 @@ module Submissions
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
fill_submitter_fields(submitter, submitter.account, pdfs_index, with_signature_id:, is_flatten:)
|
||||||
|
end
|
||||||
|
|
||||||
|
def fill_submitter_fields(submitter, account, pdfs_index, with_signature_id:, is_flatten:)
|
||||||
|
cell_layouter = HexaPDF::Layout::TextLayouter.new(text_valign: :center, text_align: :center)
|
||||||
|
|
||||||
|
attachments_data_cache = {}
|
||||||
|
|
||||||
|
return pdfs_index if submitter.submission.template_fields.blank?
|
||||||
|
|
||||||
submitter.submission.template_fields.each do |field|
|
submitter.submission.template_fields.each do |field|
|
||||||
next if field['submitter_uuid'] != submitter.uuid
|
next if field['submitter_uuid'] != submitter.uuid
|
||||||
|
|
||||||
@@ -459,6 +463,8 @@ module Submissions
|
|||||||
**build_signing_params(submitter, pkcs, tsa_url)
|
**build_signing_params(submitter, pkcs, tsa_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pdf.pages.first[:Annots] = [] unless pdf.pages.first[:Annots].respond_to?(:<<)
|
||||||
|
|
||||||
begin
|
begin
|
||||||
pdf.sign(io, write_options: { validate: false }, **sign_params)
|
pdf.sign(io, write_options: { validate: false }, **sign_params)
|
||||||
rescue HexaPDF::MalformedPDFError => e
|
rescue HexaPDF::MalformedPDFError => e
|
||||||
@@ -512,13 +518,13 @@ module Submissions
|
|||||||
Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, attachments.map(&:uuid).sort.join(':'))
|
Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, attachments.map(&:uuid).sort.join(':'))
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_pdfs_index(submitter, flatten: true)
|
def build_pdfs_index(submission, submitter: nil, flatten: true)
|
||||||
latest_submitter = find_last_submitter(submitter)
|
latest_submitter = find_last_submitter(submission, submitter:)
|
||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(latest_submitter) if latest_submitter
|
Submissions::EnsureResultGenerated.call(latest_submitter) if latest_submitter
|
||||||
|
|
||||||
documents = latest_submitter&.documents&.preload(:blob).to_a.presence
|
documents = latest_submitter&.documents&.preload(:blob).to_a.presence
|
||||||
documents ||= submitter.submission.template_schema_documents.preload(:blob)
|
documents ||= submission.template_schema_documents.preload(:blob)
|
||||||
|
|
||||||
documents.to_h do |attachment|
|
documents.to_h do |attachment|
|
||||||
pdf =
|
pdf =
|
||||||
@@ -580,11 +586,11 @@ module Submissions
|
|||||||
font_wrapper.custom_glyph(replace_with, character)
|
font_wrapper.custom_glyph(replace_with, character)
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_last_submitter(submitter)
|
def find_last_submitter(submission, submitter: nil)
|
||||||
submitter.submission.submitters
|
submission.submitters
|
||||||
.select(&:completed_at?)
|
.select(&:completed_at?)
|
||||||
.select { |e| e.id != submitter.id && e.completed_at <= submitter.completed_at }
|
.select { |e| submitter.nil? ? true : e.id != submitter.id && e.completed_at <= submitter.completed_at }
|
||||||
.max_by(&:completed_at)
|
.max_by(&:completed_at)
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_pdf_from_image(attachment)
|
def build_pdf_from_image(attachment)
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ module Submitters
|
|||||||
AccountConfig::FORM_WITH_CONFETTI_KEY,
|
AccountConfig::FORM_WITH_CONFETTI_KEY,
|
||||||
AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
|
AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
|
||||||
AccountConfig::WITH_SIGNATURE_ID,
|
AccountConfig::WITH_SIGNATURE_ID,
|
||||||
|
AccountConfig::ALLOW_TO_DECLINE_KEY,
|
||||||
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
|
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
|
||||||
|
AccountConfig::REUSE_SIGNATURE_KEY,
|
||||||
AccountConfig::ALLOW_TYPED_SIGNATURE].freeze
|
AccountConfig::ALLOW_TYPED_SIGNATURE].freeze
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
@@ -20,16 +22,22 @@ module Submitters
|
|||||||
with_typed_signature = find_safe_value(configs, AccountConfig::ALLOW_TYPED_SIGNATURE) != false
|
with_typed_signature = find_safe_value(configs, AccountConfig::ALLOW_TYPED_SIGNATURE) != false
|
||||||
with_confetti = find_safe_value(configs, AccountConfig::FORM_WITH_CONFETTI_KEY) != false
|
with_confetti = find_safe_value(configs, AccountConfig::FORM_WITH_CONFETTI_KEY) != false
|
||||||
prefill_signature = find_safe_value(configs, AccountConfig::FORM_PREFILL_SIGNATURE_KEY) != false
|
prefill_signature = find_safe_value(configs, AccountConfig::FORM_PREFILL_SIGNATURE_KEY) != false
|
||||||
|
reuse_signature = find_safe_value(configs, AccountConfig::REUSE_SIGNATURE_KEY) != false
|
||||||
|
with_decline = find_safe_value(configs, AccountConfig::ALLOW_TO_DECLINE_KEY) != false
|
||||||
with_signature_id = find_safe_value(configs, AccountConfig::WITH_SIGNATURE_ID) == true
|
with_signature_id = find_safe_value(configs, AccountConfig::WITH_SIGNATURE_ID) == true
|
||||||
require_signing_reason = find_safe_value(configs, AccountConfig::REQUIRE_SIGNING_REASON_KEY) == true
|
require_signing_reason = find_safe_value(configs, AccountConfig::REQUIRE_SIGNING_REASON_KEY) == true
|
||||||
|
|
||||||
attrs = { completed_button:,
|
attrs = {
|
||||||
with_typed_signature:,
|
completed_button:,
|
||||||
with_confetti:,
|
with_typed_signature:,
|
||||||
completed_message:,
|
with_confetti:,
|
||||||
require_signing_reason:,
|
reuse_signature:,
|
||||||
prefill_signature:,
|
with_decline:,
|
||||||
with_signature_id: }
|
completed_message:,
|
||||||
|
require_signing_reason:,
|
||||||
|
prefill_signature:,
|
||||||
|
with_signature_id:
|
||||||
|
}
|
||||||
|
|
||||||
keys.each do |key|
|
keys.each do |key|
|
||||||
attrs[key.to_sym] = configs.find { |e| e.key == key.to_s }&.value
|
attrs[key.to_sym] = configs.find { |e| e.key == key.to_s }&.value
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ module Submitters
|
|||||||
end
|
end
|
||||||
when 'date'
|
when 'date'
|
||||||
if with_time
|
if with_time
|
||||||
I18n.l(Time.current.in_time_zone(submission.account.timezone).to_date)
|
Time.current.in_time_zone(submission.account.timezone).to_date.to_s
|
||||||
else
|
else
|
||||||
e
|
e
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ module Templates
|
|||||||
media_box_start = [media_box[0], media_box[1]]
|
media_box_start = [media_box[0], media_box[1]]
|
||||||
crop_shift = [crop_box[0] - media_box[0], crop_box[1] - media_box[1]]
|
crop_shift = [crop_box[0] - media_box[0], crop_box[1] - media_box[1]]
|
||||||
|
|
||||||
|
next unless child_field[:Rect]
|
||||||
|
|
||||||
x0, y0, x1, y1 = child_field[:Rect]
|
x0, y0, x1, y1 = child_field[:Rect]
|
||||||
|
|
||||||
x0, y0 = correct_coordinates(x0, y0, crop_shift, media_box_start)
|
x0, y0 = correct_coordinates(x0, y0, crop_shift, media_box_start)
|
||||||
@@ -202,6 +204,7 @@ module Templates
|
|||||||
field = annot[:Parent]
|
field = annot[:Parent]
|
||||||
field = field[:Parent] while field[:Parent]
|
field = field[:Parent] while field[:Parent]
|
||||||
|
|
||||||
|
annots_index[field.hash] ||= page
|
||||||
fields_index[field.hash] ||= HexaPDF::Type::AcroForm::Field.wrap(pdf, field)
|
fields_index[field.hash] ||= HexaPDF::Type::AcroForm::Field.wrap(pdf, field)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ module Templates
|
|||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def call(attachment, data, extract_fields: false)
|
def call(attachment, data, extract_fields: false, max_pages: MAX_NUMBER_OF_PAGES_PROCESSED)
|
||||||
if attachment.content_type == PDF_CONTENT_TYPE
|
if attachment.content_type == PDF_CONTENT_TYPE
|
||||||
if extract_fields && data.size < MAX_FLATTEN_FILE_SIZE
|
if extract_fields && data.size < MAX_FLATTEN_FILE_SIZE
|
||||||
pdf = HexaPDF::Document.new(io: StringIO.new(data))
|
pdf = HexaPDF::Document.new(io: StringIO.new(data))
|
||||||
@@ -25,7 +25,7 @@ module Templates
|
|||||||
fields = Templates::FindAcroFields.call(pdf, attachment)
|
fields = Templates::FindAcroFields.call(pdf, attachment)
|
||||||
end
|
end
|
||||||
|
|
||||||
generate_pdf_preview_images(attachment, data, pdf)
|
generate_pdf_preview_images(attachment, data, pdf, max_pages:)
|
||||||
|
|
||||||
attachment.metadata['pdf']['fields'] = fields if fields
|
attachment.metadata['pdf']['fields'] = fields if fields
|
||||||
elsif attachment.image?
|
elsif attachment.image?
|
||||||
@@ -56,7 +56,7 @@ module Templates
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_pdf_preview_images(attachment, data, pdf = nil)
|
def generate_pdf_preview_images(attachment, data, pdf = nil, max_pages: MAX_NUMBER_OF_PAGES_PROCESSED)
|
||||||
ActiveStorage::Attachment.where(name: ATTACHMENT_NAME, record: attachment).destroy_all
|
ActiveStorage::Attachment.where(name: ATTACHMENT_NAME, record: attachment).destroy_all
|
||||||
|
|
||||||
pdf ||= HexaPDF::Document.new(io: StringIO.new(data))
|
pdf ||= HexaPDF::Document.new(io: StringIO.new(data))
|
||||||
@@ -71,12 +71,16 @@ module Templates
|
|||||||
attachment.save!
|
attachment.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
max_pages_to_process = data.size < GENERATE_PREVIEW_SIZE_LIMIT ? MAX_NUMBER_OF_PAGES_PROCESSED : 1
|
max_pages_to_process = data.size < GENERATE_PREVIEW_SIZE_LIMIT ? max_pages : 1
|
||||||
|
|
||||||
pool = Concurrent::FixedThreadPool.new(CONCURRENCY)
|
generate_document_preview_images(attachment, data, (0..[number_of_pages - 1, max_pages_to_process].min))
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_document_preview_images(attachment, data, range, concurrency: CONCURRENCY)
|
||||||
|
pool = Concurrent::FixedThreadPool.new(concurrency)
|
||||||
|
|
||||||
promises =
|
promises =
|
||||||
(0..[number_of_pages - 1, max_pages_to_process].min).map do |page_number|
|
range.map do |page_number|
|
||||||
Concurrent::Promise.execute(executor: pool) { build_and_upload_blob(data, page_number) }
|
Concurrent::Promise.execute(executor: pool) { build_and_upload_blob(data, page_number) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -5,5 +5,14 @@ FactoryBot.define do
|
|||||||
name { Faker::Company.name }
|
name { Faker::Company.name }
|
||||||
locale { 'en-US' }
|
locale { 'en-US' }
|
||||||
timezone { 'UTC' }
|
timezone { 'UTC' }
|
||||||
|
|
||||||
|
trait :with_testing_account do
|
||||||
|
after(:create) do |account|
|
||||||
|
testing_account = account.dup.tap { |a| a.name = "Testing - #{account.name}" }
|
||||||
|
testing_account.uuid = SecureRandom.uuid
|
||||||
|
account.testing_accounts << testing_account
|
||||||
|
account.save!
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :completed_document do
|
||||||
|
submitter
|
||||||
|
sha256 { SecureRandom.hex(32) }
|
||||||
|
end
|
||||||
|
end
|
||||||
+43
-32
@@ -7,7 +7,11 @@ FactoryBot.define do
|
|||||||
author factory: %i[user]
|
author factory: %i[user]
|
||||||
name { Faker::Book.title }
|
name { Faker::Book.title }
|
||||||
|
|
||||||
after(:create) do |template|
|
transient do
|
||||||
|
submitter_count { 1 }
|
||||||
|
end
|
||||||
|
|
||||||
|
after(:create) do |template, evaluator|
|
||||||
blob = ActiveStorage::Blob.create_and_upload!(
|
blob = ActiveStorage::Blob.create_and_upload!(
|
||||||
io: Rails.root.join('spec/fixtures/sample-document.pdf').open,
|
io: Rails.root.join('spec/fixtures/sample-document.pdf').open,
|
||||||
filename: 'sample-document.pdf',
|
filename: 'sample-document.pdf',
|
||||||
@@ -22,39 +26,46 @@ FactoryBot.define do
|
|||||||
Templates::ProcessDocument.call(attachment, attachment.download)
|
Templates::ProcessDocument.call(attachment, attachment.download)
|
||||||
|
|
||||||
template.schema = [{ attachment_uuid: attachment.uuid, name: 'sample-document' }]
|
template.schema = [{ attachment_uuid: attachment.uuid, name: 'sample-document' }]
|
||||||
template.submitters = [
|
number_words = %w[first second third fourth fifth sixth seventh eighth ninth tenth]
|
||||||
|
|
||||||
|
template.submitters = Array.new(evaluator.submitter_count) do |i|
|
||||||
{
|
{
|
||||||
'name' => 'First Party',
|
'name' => "#{number_words[i]&.capitalize} Party",
|
||||||
'uuid' => '513848eb-1096-4abc-a743-68596b5aaa4c'
|
'uuid' => SecureRandom.uuid
|
||||||
}
|
}
|
||||||
]
|
end
|
||||||
template.fields = [
|
|
||||||
{
|
template.fields = template.submitters.reduce([]) do |fields, submitter|
|
||||||
'uuid' => '21637fc9-0655-45df-8952-04ec64949e85',
|
fields += [
|
||||||
'submitter_uuid' => '513848eb-1096-4abc-a743-68596b5aaa4c',
|
{
|
||||||
'name' => 'First Name',
|
'uuid' => SecureRandom.uuid,
|
||||||
'type' => 'text',
|
'submitter_uuid' => submitter['uuid'],
|
||||||
'required' => true,
|
'name' => 'First Name',
|
||||||
'areas' => [
|
'type' => 'text',
|
||||||
{
|
'required' => true,
|
||||||
'x' => 0.09027777777777778,
|
'areas' => [
|
||||||
'y' => 0.1197252208047105,
|
{
|
||||||
'w' => 0.3069444444444444,
|
'x' => 0.09027777777777778,
|
||||||
'h' => 0.03336604514229637,
|
'y' => 0.1197252208047105,
|
||||||
'attachment_uuid' => attachment.uuid,
|
'w' => 0.3069444444444444,
|
||||||
'page' => 0
|
'h' => 0.03336604514229637,
|
||||||
}
|
'attachment_uuid' => attachment.uuid,
|
||||||
]
|
'page' => 0
|
||||||
},
|
}
|
||||||
{
|
]
|
||||||
'uuid' => '1f97f8e3-dc82-4586-aeea-6ebed6204e46',
|
},
|
||||||
'submitter_uuid' => '513848eb-1096-4abc-a743-68596b5aaa4c',
|
{
|
||||||
'name' => '',
|
'uuid' => SecureRandom.uuid,
|
||||||
'type' => 'signature',
|
'submitter_uuid' => submitter['uuid'],
|
||||||
'required' => true,
|
'name' => '',
|
||||||
'areas' => []
|
'type' => 'signature',
|
||||||
}
|
'required' => true,
|
||||||
]
|
'areas' => []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
fields
|
||||||
|
end
|
||||||
|
|
||||||
template.save!
|
template.save!
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe ProcessSubmitterCompletionJob do
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
let(:user) { create(:user, account:) }
|
||||||
|
let(:template) { create(:template, account:, author: user) }
|
||||||
|
let(:submission) { create(:submission, template:, created_by_user: user) }
|
||||||
|
let(:submitter) { create(:submitter, submission:, uuid: SecureRandom.uuid, completed_at: Time.current) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY,
|
||||||
|
value: GenerateCertificate.call.transform_values(&:to_pem))
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
it 'creates a completed submitter' do
|
||||||
|
expect do
|
||||||
|
described_class.new.perform('submitter_id' => submitter.id)
|
||||||
|
end.to change(CompletedSubmitter, :count).by(1)
|
||||||
|
|
||||||
|
completed_submitter = CompletedSubmitter.last
|
||||||
|
submitter.reload
|
||||||
|
|
||||||
|
expect(completed_submitter.submitter_id).to eq(submitter.id)
|
||||||
|
expect(completed_submitter.submission_id).to eq(submitter.submission_id)
|
||||||
|
expect(completed_submitter.account_id).to eq(submitter.submission.account_id)
|
||||||
|
expect(completed_submitter.template_id).to eq(submitter.submission.template_id)
|
||||||
|
expect(completed_submitter.source).to eq(submitter.submission.source)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a completed document' do
|
||||||
|
expect do
|
||||||
|
described_class.new.perform('submitter_id' => submitter.id)
|
||||||
|
end.to change(CompletedDocument, :count).by(1)
|
||||||
|
|
||||||
|
completed_document = CompletedDocument.last
|
||||||
|
|
||||||
|
expect(completed_document.submitter_id).to eq(submitter.id)
|
||||||
|
expect(completed_document.sha256).to be_present
|
||||||
|
expect(completed_document.sha256).to eq(submitter.documents.first.metadata['sha256'])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises an error if the submitter is not found' do
|
||||||
|
expect do
|
||||||
|
described_class.new.perform('submitter_id' => 'invalid_id')
|
||||||
|
end.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Params::BaseValidator do
|
||||||
|
let(:validator) { described_class.new({}) }
|
||||||
|
|
||||||
|
describe '#email_format' do
|
||||||
|
it 'when email is valid' do
|
||||||
|
emails = [
|
||||||
|
' john.doe@example.com ',
|
||||||
|
'john.doe@example.com',
|
||||||
|
'Test <john.doe@example.com>',
|
||||||
|
'jane+newsletter@domain.org',
|
||||||
|
'mike_smith@company.net',
|
||||||
|
'lisa-wong@sub.example.co.uk',
|
||||||
|
'peter@webmail.com',
|
||||||
|
'anna.jones123@my-domain.com',
|
||||||
|
'contact@company.email',
|
||||||
|
'info@my-company123.org',
|
||||||
|
'hello.world@business.info',
|
||||||
|
'feedback@new-domain.com',
|
||||||
|
'alerts+user@localdomain.net',
|
||||||
|
'webmaster@industry.biz',
|
||||||
|
'services@agency.example',
|
||||||
|
'george123@consultant.pro',
|
||||||
|
'sales-team@company.io'
|
||||||
|
]
|
||||||
|
|
||||||
|
emails.each do |email|
|
||||||
|
expect { validator.email_format({ email: }, :email) }.not_to raise_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'when signle email is invalid' do
|
||||||
|
emails = [
|
||||||
|
'jone.doe@',
|
||||||
|
'this...is@strange.but.valid.com',
|
||||||
|
'user@-weird-domain-.com',
|
||||||
|
'user.name@[IPv6:2001:db8::1]',
|
||||||
|
'tricky.email@sub.example-.com'
|
||||||
|
]
|
||||||
|
|
||||||
|
emails.each do |email|
|
||||||
|
expect do
|
||||||
|
validator.email_format({ email: }, :email)
|
||||||
|
end.to raise_error(described_class::InvalidParameterError, "email must follow the email format: '#{email}'")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'when multiple emails are valid' do
|
||||||
|
emails = [
|
||||||
|
'john.doe@example.com, jane.doe+newsletter@domain.org',
|
||||||
|
'joshua@automobile.car ; chloe+fashion@food.delivery',
|
||||||
|
'mike-smith@company.net;lisa.wong-sales@sub.example.co.uk',
|
||||||
|
'peter.parker+info@webmail.com,laura.martin-office@company.co',
|
||||||
|
'anna.jones123@my-domain.com, jack.brown+work@college.edu',
|
||||||
|
'susan.green@business-info.org; dave.clark+personal@nonprofit.org',
|
||||||
|
'chris.lee+team@new-domain.com;jenny.king.marketing@localdomain.net',
|
||||||
|
'george.morris@consultant.pro; nancy.davis-office@company.io',
|
||||||
|
'joshua-jones@automobile.car; chloe.taylor+fashion@food.delivery',
|
||||||
|
'ryan.moore+alerts@music-band.com,isabella.walker.design@fashion.design',
|
||||||
|
'support-team@company.com, contact.us@domain.org',
|
||||||
|
'admin.office@industry.biz, hr.department@service.pro',
|
||||||
|
'feedback@agency-example.org; hello.world@creative-studio.net',
|
||||||
|
'sales-team@e-commerce.shop, support.department@technology.co',
|
||||||
|
'media.contact@financial.servicesl; events-coordinator@food.delivery',
|
||||||
|
'order@music-band.com; info.support@creative.example',
|
||||||
|
'design.team@webmail.com , admin-office@company.co',
|
||||||
|
'contact.sales@sub-example.co.uk, support+info@legal.gov',
|
||||||
|
'support@media.group;subscribe-updates@concert.events'
|
||||||
|
]
|
||||||
|
|
||||||
|
emails.each do |email|
|
||||||
|
expect { validator.email_format({ email: }, :email) }.not_to raise_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'when multiple emails are invalid' do
|
||||||
|
emails = [
|
||||||
|
'john.doe@example.com dave@nonprofit.org',
|
||||||
|
'amy.baker@ example.com, george.morris@ example.com',
|
||||||
|
'jenny.king@example.com . diana.robinson@example.com',
|
||||||
|
'nancy.davis@.com, henry.ford@.com',
|
||||||
|
'jack.brown@example.com, laura.martin@example .com',
|
||||||
|
'anna.jones@example,com lisa.wong@example.com',
|
||||||
|
'dave.clark@example.com kevin.white@example;com',
|
||||||
|
'susan.green@ example.com; john.doe@example.com',
|
||||||
|
'amy.baker@sub_domain.com george.morris@-example.com',
|
||||||
|
'nancy.davis@example..com john.doe@example.c',
|
||||||
|
'peter.parker@example.com, .henry.ford@example.com',
|
||||||
|
'diana.robinson@.example..com, mike.smith@.',
|
||||||
|
'oliver.scott@example.com; laura.martin@ example.com, jane.doe@@example.com'
|
||||||
|
]
|
||||||
|
|
||||||
|
emails.each do |email|
|
||||||
|
expect do
|
||||||
|
validator.email_format({ email: }, :email)
|
||||||
|
end.to raise_error(described_class::InvalidParameterError, "email must follow the email format: '#{email}'")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'when email is invalid with custom message' do
|
||||||
|
expect do
|
||||||
|
validator.email_format({ email: 'jone.doe@' }, :email, message: 'email is invalid')
|
||||||
|
end.to raise_error(described_class::InvalidParameterError, 'email is invalid')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -3,10 +3,17 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
describe 'Submission API', type: :request do
|
describe 'Submission API', type: :request do
|
||||||
let!(:account) { create(:account) }
|
let(:account) { create(:account, :with_testing_account) }
|
||||||
let!(:author) { create(:user, account:) }
|
let(:testing_account) { account.testing_accounts.first }
|
||||||
let!(:folder) { create(:template_folder, account:) }
|
let(:author) { create(:user, account:) }
|
||||||
let!(:templates) { create_list(:template, 2, account:, author:, folder:) }
|
let(:testing_author) { create(:user, account: testing_account) }
|
||||||
|
let(:folder) { create(:template_folder, account:) }
|
||||||
|
let(:testing_folder) { create(:template_folder, account: testing_account) }
|
||||||
|
let(:templates) { create_list(:template, 2, account:, author:, folder:) }
|
||||||
|
let(:multiple_submitters_template) { create(:template, submitter_count: 3, account:, author:, folder:) }
|
||||||
|
let(:testing_templates) do
|
||||||
|
create_list(:template, 2, account: testing_account, author: testing_author, folder: testing_folder)
|
||||||
|
end
|
||||||
|
|
||||||
describe 'GET /api/submissions' do
|
describe 'GET /api/submissions' do
|
||||||
it 'returns a list of submissions' do
|
it 'returns a list of submissions' do
|
||||||
@@ -40,6 +47,31 @@ describe 'Submission API', type: :request do
|
|||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
expect(response.parsed_body).to eq(JSON.parse(show_submission_body(submission).to_json))
|
expect(response.parsed_body).to eq(JSON.parse(show_submission_body(submission).to_json))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'returns an authorization error if test account API token is used with a production submission' do
|
||||||
|
submission = create(:submission, :with_submitters, :with_events, template: templates[0], created_by_user: author)
|
||||||
|
|
||||||
|
get "/api/submissions/#{submission.id}", headers: { 'x-auth-token': testing_author.access_token.token }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
expect(response.parsed_body).to eq(
|
||||||
|
JSON.parse({ error: "Submission #{submission.id} not found using testing API key; " \
|
||||||
|
'Use production API key to access production submissions.' }.to_json)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an authorization error if production account API token is used with a test submission' do
|
||||||
|
submission = create(:submission, :with_submitters, :with_events, template: testing_templates[0],
|
||||||
|
created_by_user: testing_author)
|
||||||
|
|
||||||
|
get "/api/submissions/#{submission.id}", headers: { 'x-auth-token': author.access_token.token }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
expect(response.parsed_body).to eq(
|
||||||
|
JSON.parse({ error: "Submission #{submission.id} not found using production API key; " \
|
||||||
|
'Use testing API key to access testing submissions.' }.to_json)
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'POST /api/submissions' do
|
describe 'POST /api/submissions' do
|
||||||
@@ -47,7 +79,7 @@ describe 'Submission API', type: :request do
|
|||||||
post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: {
|
post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: {
|
||||||
template_id: templates[0].id,
|
template_id: templates[0].id,
|
||||||
send_email: true,
|
send_email: true,
|
||||||
submitters: [{ role: 'First Role', email: 'john.doe@example.com' }]
|
submitters: [{ role: 'First Party', email: 'john.doe@example.com' }]
|
||||||
}.to_json
|
}.to_json
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
@@ -56,13 +88,16 @@ describe 'Submission API', type: :request do
|
|||||||
|
|
||||||
expect(response.parsed_body).to eq(JSON.parse(create_submission_body(submission).to_json))
|
expect(response.parsed_body).to eq(JSON.parse(create_submission_body(submission).to_json))
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
describe 'POST /api/submissions/emails' do
|
it 'creates a submission when some submitter roles are not provided' do
|
||||||
it 'creates a submission using email' do
|
|
||||||
post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: {
|
post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: {
|
||||||
template_id: templates[0].id,
|
template_id: multiple_submitters_template.id,
|
||||||
emails: 'john.doe@example.com'
|
send_email: true,
|
||||||
|
submitters: [
|
||||||
|
{ role: 'First Party', email: 'john.doe@example.com' },
|
||||||
|
{ email: 'jane.doe@example.com' },
|
||||||
|
{ email: 'mike.doe@example.com' }
|
||||||
|
]
|
||||||
}.to_json
|
}.to_json
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
@@ -70,6 +105,95 @@ describe 'Submission API', type: :request do
|
|||||||
submission = Submission.last
|
submission = Submission.last
|
||||||
|
|
||||||
expect(response.parsed_body).to eq(JSON.parse(create_submission_body(submission).to_json))
|
expect(response.parsed_body).to eq(JSON.parse(create_submission_body(submission).to_json))
|
||||||
|
expect(response.parsed_body).to eq(JSON.parse(create_submission_body(submission).to_json))
|
||||||
|
expect(response.parsed_body[0]['role']).to eq('First Party')
|
||||||
|
expect(response.parsed_body[0]['email']).to eq('john.doe@example.com')
|
||||||
|
expect(response.parsed_body[1]['role']).to eq('Second Party')
|
||||||
|
expect(response.parsed_body[1]['email']).to eq('jane.doe@example.com')
|
||||||
|
expect(response.parsed_body[2]['role']).to eq('Third Party')
|
||||||
|
expect(response.parsed_body[2]['email']).to eq('mike.doe@example.com')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an error if the submitter email is invalid' do
|
||||||
|
post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: {
|
||||||
|
template_id: templates[0].id,
|
||||||
|
send_email: true,
|
||||||
|
submitters: [
|
||||||
|
{ role: 'First Party', email: 'john@example' }
|
||||||
|
]
|
||||||
|
}.to_json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
|
||||||
|
expect(response.parsed_body).to eq({ 'error' => 'email is invalid in `submitters[0]`.' })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an error if the template fields are missing' do
|
||||||
|
templates[0].update(fields: [])
|
||||||
|
|
||||||
|
post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: {
|
||||||
|
template_id: templates[0].id,
|
||||||
|
send_email: true,
|
||||||
|
submitters: [{ role: 'First Party', email: 'john.doe@example.com' }]
|
||||||
|
}.to_json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
expect(response.parsed_body).to eq({ 'error' => 'Template does not contain fields' })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an error if submitter roles are not unique' do
|
||||||
|
post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: {
|
||||||
|
template_id: multiple_submitters_template.id,
|
||||||
|
send_email: true,
|
||||||
|
submitters: [
|
||||||
|
{ role: 'First Party', email: 'john.doe@example.com' },
|
||||||
|
{ role: 'First Party', email: 'jane.doe@example.com' }
|
||||||
|
]
|
||||||
|
}.to_json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
expect(response.parsed_body).to eq({ 'error' => 'role must be unique in `submitters`.' })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an error if number of submitters more than in the template' do
|
||||||
|
post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: {
|
||||||
|
template_id: templates[0].id,
|
||||||
|
send_email: true,
|
||||||
|
submitters: [
|
||||||
|
{ email: 'jane.doe@example.com' },
|
||||||
|
{ role: 'First Party', email: 'john.doe@example.com' }
|
||||||
|
]
|
||||||
|
}.to_json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
expect(response.parsed_body).to eq({ 'error' => 'Defined more signing parties than in template' })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST /api/submissions/emails' do
|
||||||
|
it 'creates a submission using email' do
|
||||||
|
post '/api/submissions/emails', headers: { 'x-auth-token': author.access_token.token }, params: {
|
||||||
|
template_id: templates[0].id,
|
||||||
|
emails: 'john.doe@example.com,jane.doe@example.com'
|
||||||
|
}.to_json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
|
submissions = Submission.last(2)
|
||||||
|
submissions_body = submissions.reduce([]) { |acc, submission| acc + create_submission_body(submission) }
|
||||||
|
|
||||||
|
expect(response.parsed_body).to eq(JSON.parse(submissions_body.to_json))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an error if emails are invalid' do
|
||||||
|
post '/api/submissions/emails', headers: { 'x-auth-token': author.access_token.token }, params: {
|
||||||
|
template_id: templates[0].id,
|
||||||
|
emails: 'amy.baker@example.com, george.morris@example.com@gmail.com'
|
||||||
|
}.to_json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
|
||||||
|
expect(response.parsed_body).to eq({ 'error' => 'emails are invalid' })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,16 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
describe 'Submitter API', type: :request do
|
describe 'Submitter API', type: :request do
|
||||||
let!(:account) { create(:account) }
|
let(:account) { create(:account, :with_testing_account) }
|
||||||
let!(:author) { create(:user, account:) }
|
let(:testing_account) { account.testing_accounts.first }
|
||||||
let!(:folder) { create(:template_folder, account:) }
|
let(:author) { create(:user, account:) }
|
||||||
let!(:templates) { create_list(:template, 2, account:, author:, folder:) }
|
let(:testing_author) { create(:user, account: testing_account) }
|
||||||
|
let(:folder) { create(:template_folder, account:) }
|
||||||
|
let(:testing_folder) { create(:template_folder, account: testing_account) }
|
||||||
|
let(:templates) { create_list(:template, 2, account:, author:, folder:) }
|
||||||
|
let(:testing_templates) do
|
||||||
|
create_list(:template, 2, account: testing_account, author: testing_author, folder: testing_folder)
|
||||||
|
end
|
||||||
|
|
||||||
describe 'GET /api/submitters' do
|
describe 'GET /api/submitters' do
|
||||||
it 'returns a list of submitters' do
|
it 'returns a list of submitters' do
|
||||||
@@ -42,6 +48,34 @@ describe 'Submitter API', type: :request do
|
|||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
expect(response.parsed_body).to eq(JSON.parse(submitter_body(submitter).to_json))
|
expect(response.parsed_body).to eq(JSON.parse(submitter_body(submitter).to_json))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'returns an authorization error if test account API token is used with a production submitter' do
|
||||||
|
submitter = create(:submission, :with_submitters, :with_events,
|
||||||
|
template: templates[0],
|
||||||
|
created_by_user: author).submitters.first
|
||||||
|
|
||||||
|
get "/api/submitters/#{submitter.id}", headers: { 'x-auth-token': testing_author.access_token.token }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
expect(response.parsed_body).to eq(
|
||||||
|
JSON.parse({ error: "Submitter #{submitter.id} not found using " \
|
||||||
|
'testing API key; Use production API key to access production submitters.' }.to_json)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an authorization error if production account API token is used with a test submitter' do
|
||||||
|
submitter = create(:submission, :with_submitters, :with_events,
|
||||||
|
template: testing_templates[0],
|
||||||
|
created_by_user: testing_author).submitters.first
|
||||||
|
|
||||||
|
get "/api/submitters/#{submitter.id}", headers: { 'x-auth-token': author.access_token.token }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
expect(response.parsed_body).to eq(
|
||||||
|
JSON.parse({ error: "Submitter #{submitter.id} not found using production API key; " \
|
||||||
|
'Use testing API key to access testing submitters.' }.to_json)
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'PUT /api/submitters' do
|
describe 'PUT /api/submitters' do
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
describe 'Templates API', type: :request do
|
describe 'Templates API', type: :request do
|
||||||
let!(:account) { create(:account) }
|
let(:account) { create(:account, :with_testing_account) }
|
||||||
let!(:author) { create(:user, account:) }
|
let(:testing_account) { account.testing_accounts.first }
|
||||||
let!(:folder) { create(:template_folder, account:) }
|
let(:author) { create(:user, account:) }
|
||||||
let!(:template_preferences) { { 'request_email_subject' => 'Subject text', 'request_email_body' => 'Body Text' } }
|
let(:testing_author) { create(:user, account: testing_account) }
|
||||||
|
let(:folder) { create(:template_folder, account:) }
|
||||||
|
let(:template_preferences) { { 'request_email_subject' => 'Subject text', 'request_email_body' => 'Body Text' } }
|
||||||
|
|
||||||
describe 'GET /api/templates' do
|
describe 'GET /api/templates' do
|
||||||
it 'returns a list of templates' do
|
it 'returns a list of templates' do
|
||||||
@@ -48,6 +50,38 @@ describe 'Templates API', type: :request do
|
|||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
expect(response.parsed_body).to eq(JSON.parse(template_body(template).to_json))
|
expect(response.parsed_body).to eq(JSON.parse(template_body(template).to_json))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'returns an authorization error if test account API token is used with a production template' do
|
||||||
|
template = create(:template, account:,
|
||||||
|
author:,
|
||||||
|
folder:,
|
||||||
|
external_id: SecureRandom.base58(10),
|
||||||
|
preferences: template_preferences)
|
||||||
|
|
||||||
|
get "/api/templates/#{template.id}", headers: { 'x-auth-token': testing_author.access_token.token }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
expect(response.parsed_body).to eq(
|
||||||
|
JSON.parse({ error: "Template #{template.id} not found using testing API key; " \
|
||||||
|
'Use production API key to access production templates.' }.to_json)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an authorization error if production account API token is used with a test template' do
|
||||||
|
template = create(:template, account: testing_account,
|
||||||
|
author: testing_author,
|
||||||
|
folder:,
|
||||||
|
external_id: SecureRandom.base58(10),
|
||||||
|
preferences: template_preferences)
|
||||||
|
|
||||||
|
get "/api/templates/#{template.id}", headers: { 'x-auth-token': author.access_token.token }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
expect(response.parsed_body).to eq(
|
||||||
|
JSON.parse({ error: "Template #{template.id} not found using production API key; " \
|
||||||
|
'Use testing API key to access testing templates.' }.to_json)
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'PUT /api/templates' do
|
describe 'PUT /api/templates' do
|
||||||
@@ -144,8 +178,8 @@ describe 'Templates API', type: :request do
|
|||||||
name: template.name,
|
name: template.name,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
'uuid' => '21637fc9-0655-45df-8952-04ec64949e85',
|
'uuid' => template.fields[0]['uuid'],
|
||||||
'submitter_uuid' => '513848eb-1096-4abc-a743-68596b5aaa4c',
|
'submitter_uuid' => template.submitters[0]['uuid'],
|
||||||
'name' => 'First Name',
|
'name' => 'First Name',
|
||||||
'type' => 'text',
|
'type' => 'text',
|
||||||
'required' => true,
|
'required' => true,
|
||||||
@@ -161,8 +195,8 @@ describe 'Templates API', type: :request do
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'uuid' => '1f97f8e3-dc82-4586-aeea-6ebed6204e46',
|
'uuid' => template.fields[1]['uuid'],
|
||||||
'submitter_uuid' => '513848eb-1096-4abc-a743-68596b5aaa4c',
|
'submitter_uuid' => template.submitters[0]['uuid'],
|
||||||
'name' => '',
|
'name' => '',
|
||||||
'type' => 'signature',
|
'type' => 'signature',
|
||||||
'required' => true,
|
'required' => true,
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe 'Tools API', type: :request do
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
let(:author) { create(:user, account:) }
|
||||||
|
let(:file_path) { Rails.root.join('spec/fixtures/sample-document.pdf') }
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY,
|
||||||
|
value: GenerateCertificate.call.transform_values(&:to_pem))
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST /api/tools/verify' do
|
||||||
|
it 'returns a verification result' do
|
||||||
|
template = create(:template, account:, author:)
|
||||||
|
submission = create(:submission, :with_submitters, :with_events, template:, created_by_user: author)
|
||||||
|
blob = ActiveStorage::Blob.create_and_upload!(
|
||||||
|
io: file_path.open,
|
||||||
|
filename: 'sample-document.pdf',
|
||||||
|
content_type: 'application/pdf'
|
||||||
|
)
|
||||||
|
create(:completed_document, submitter: submission.submitters.first,
|
||||||
|
sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(blob.download)))
|
||||||
|
|
||||||
|
ActiveStorage::Attachment.create!(
|
||||||
|
blob:,
|
||||||
|
name: :documents,
|
||||||
|
record: submission.submitters.first
|
||||||
|
)
|
||||||
|
|
||||||
|
post '/api/tools/verify', headers: { 'x-auth-token': author.access_token.token }, params: {
|
||||||
|
file: Base64.encode64(File.read(file_path))
|
||||||
|
}.to_json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(response.parsed_body['checksum_status']).to eq('verified')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -13,6 +13,7 @@ RSpec.describe 'API Settings' do
|
|||||||
|
|
||||||
it 'shows verify signed PDF page' do
|
it 'shows verify signed PDF page' do
|
||||||
expect(page).to have_content('API')
|
expect(page).to have_content('API')
|
||||||
expect(page).to have_field('X-Auth-Token', with: user.access_token.token)
|
token = user.access_token.token
|
||||||
|
expect(page).to have_field('X-Auth-Token', with: token.sub(token[5..], '*' * token[5..].size))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -3,9 +3,9 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe 'Submit Form' do
|
RSpec.describe 'Submit Form' do
|
||||||
let!(:account) { create(:account) }
|
let(:account) { create(:account) }
|
||||||
let!(:user) { create(:user, account:) }
|
let(:user) { create(:user, account:) }
|
||||||
let!(:template) { create(:template, account:, author: user) }
|
let(:template) { create(:template, account:, author: user) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
sign_in(user)
|
sign_in(user)
|
||||||
@@ -48,8 +48,8 @@ RSpec.describe 'Submit Form' do
|
|||||||
end
|
end
|
||||||
|
|
||||||
context 'when initialized by shared email address' do
|
context 'when initialized by shared email address' do
|
||||||
let!(:submission) { create(:submission, template:, created_by_user: user) }
|
let(:submission) { create(:submission, template:, created_by_user: user) }
|
||||||
let!(:submitters) { template.submitters.map { |s| create(:submitter, submission:, uuid: s['uuid']) } }
|
let(:submitters) { template.submitters.map { |s| create(:submitter, submission:, uuid: s['uuid']) } }
|
||||||
let(:submitter) { submitters.first }
|
let(:submitter) { submitters.first }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
|||||||
@@ -18,13 +18,19 @@ RSpec.describe 'Team Settings' do
|
|||||||
visit settings_users_path
|
visit settings_users_path
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'shows all users' do
|
it 'shows only active users' do
|
||||||
within '.table' do
|
within '.table' do
|
||||||
users.each do |user|
|
users.each do |user|
|
||||||
expect(page).to have_content(user.full_name)
|
expect(page).to have_content(user.full_name)
|
||||||
expect(page).to have_content(user.email)
|
expect(page).to have_content(user.email)
|
||||||
expect(page).to have_no_content(other_user.email)
|
expect(page).to have_link('Edit', href: edit_user_path(user))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
expect(page).to have_button('Remove')
|
||||||
|
expect(page).to have_no_button('Unarchive')
|
||||||
|
|
||||||
|
expect(page).to have_no_content(other_user.full_name)
|
||||||
|
expect(page).to have_no_content(other_user.email)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -89,4 +95,58 @@ RSpec.describe 'Team Settings' do
|
|||||||
expect(page).to have_no_content('User has been removed')
|
expect(page).to have_no_content('User has been removed')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when some users are archived' do
|
||||||
|
let!(:users) { create_list(:user, 2, account:) }
|
||||||
|
let!(:archived_users) { create_list(:user, 2, account:, archived_at: Time.current) }
|
||||||
|
let!(:other_user) { create(:user) }
|
||||||
|
|
||||||
|
it 'shows only active users' do
|
||||||
|
visit settings_users_path
|
||||||
|
|
||||||
|
within '.table' do
|
||||||
|
users.each do |user|
|
||||||
|
expect(page).to have_content(user.full_name)
|
||||||
|
expect(page).to have_content(user.email)
|
||||||
|
end
|
||||||
|
|
||||||
|
archived_users.each do |user|
|
||||||
|
expect(page).to have_no_content(user.full_name)
|
||||||
|
expect(page).to have_no_content(user.email)
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(page).to have_no_content(other_user.full_name)
|
||||||
|
expect(page).to have_no_content(other_user.email)
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(page).to have_link('View Archived', href: settings_archived_users_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows only archived users' do
|
||||||
|
visit settings_archived_users_path
|
||||||
|
|
||||||
|
within '.table' do
|
||||||
|
archived_users.each do |user|
|
||||||
|
expect(page).to have_content(user.full_name)
|
||||||
|
expect(page).to have_content(user.email)
|
||||||
|
expect(page).to have_no_link('Edit', href: edit_user_path(user))
|
||||||
|
end
|
||||||
|
|
||||||
|
users.each do |user|
|
||||||
|
expect(page).to have_no_content(user.full_name)
|
||||||
|
expect(page).to have_no_content(user.email)
|
||||||
|
expect(page).to have_no_link('Edit', href: edit_user_path(user))
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(page).to have_button('Unarchive')
|
||||||
|
expect(page).to have_no_button('Remove')
|
||||||
|
|
||||||
|
expect(page).to have_no_content(other_user.full_name)
|
||||||
|
expect(page).to have_no_content(other_user.email)
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(page).to have_content('Archived Users')
|
||||||
|
expect(page).to have_link('View Active', href: settings_users_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
Reference in new issue
Block a user