mirror of
https://github.com/docusealco/docuseal
synced 2026-11-05 14:42:33 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4eb4e8c1f8 | ||
|
|
984df15949 | ||
|
|
aa3bd24332 | ||
|
|
250889623d | ||
|
|
862fa55986 | ||
|
|
0f2889904a | ||
|
|
49f14c7929 | ||
|
|
c3f3c5d5df | ||
|
|
274259f305 | ||
|
|
a15d7a9cf3 | ||
|
|
7d154fc28d | ||
|
|
6727d3be83 | ||
|
|
4bbd5c3bcb | ||
|
|
5995f2e427 | ||
|
|
0910c8e100 | ||
|
|
8df76dfa90 | ||
|
|
78e9e66cb0 | ||
|
|
71c61fc4b3 | ||
|
|
a728aa8689 | ||
|
|
5a3174a720 | ||
|
|
652072d660 | ||
|
|
323f5c0c15 | ||
|
|
229d78c78b | ||
|
|
9d9a955149 | ||
|
|
b40a047190 | ||
|
|
ff9e415823 | ||
|
|
2f7a4fb2b3 | ||
|
|
f8a654a5b6 | ||
|
|
4449475467 | ||
|
|
bcbbf0dd42 | ||
|
|
19b9ba52d4 | ||
|
|
bdc30db2dc | ||
|
|
74634ba96b | ||
|
|
77feb05bad | ||
|
|
a1f05d006e | ||
|
|
cc3dae1335 | ||
|
|
d9debcfb0d | ||
|
|
8770d16d52 | ||
|
|
84953481e4 | ||
|
|
df7aa59f6f | ||
|
|
008b83119d | ||
|
|
15dafcb6ff | ||
|
|
7d291ed888 | ||
|
|
a7221f0750 | ||
|
|
9a5b2a5d30 | ||
|
|
7361a00d42 | ||
|
|
e07d402782 | ||
|
|
a34354bee1 | ||
|
|
84b762b529 | ||
|
|
2a839951b4 | ||
|
|
26d7c2f047 | ||
|
|
151a5b85f3 | ||
|
|
3f506f53b4 | ||
|
|
95f734e4b8 | ||
|
|
ff74574e84 | ||
|
|
843f6c405f | ||
|
|
f8b24ebfce | ||
|
|
d1d26b5424 | ||
|
|
8aa6f45da8 | ||
|
|
48d5bee95b | ||
|
|
98c79d2dec | ||
|
|
0f51238f62 | ||
|
|
b7323b929f | ||
|
|
05eb1537f7 | ||
|
|
bc9f27cfed | ||
|
|
8f457be719 | ||
|
|
6d590e13c9 | ||
|
|
c987ec172a | ||
|
|
49f1637c47 | ||
|
|
13379bd699 | ||
|
|
5ee762c08c | ||
|
|
edb6d561f1 |
No files matched your search
+1
-1
@@ -30,7 +30,7 @@ Metrics/ParameterLists:
|
|||||||
Max: 10
|
Max: 10
|
||||||
|
|
||||||
Metrics/MethodLength:
|
Metrics/MethodLength:
|
||||||
Max: 25
|
Max: 30
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'db/migrate/**'
|
- 'db/migrate/**'
|
||||||
|
|
||||||
|
|||||||
+2
-4
@@ -2,9 +2,7 @@ FROM ruby:3.2.2-alpine3.18 as fonts
|
|||||||
|
|
||||||
WORKDIR /fonts
|
WORKDIR /fonts
|
||||||
|
|
||||||
RUN apk --no-cache add fontforge wget ttf-liberation && cp /usr/share/fonts/liberation/LiberationSans-Regular.ttf /usr/share/fonts/liberation/LiberationSans-Bold.ttf . && wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansArabic/hinted/ttf/NotoSansArabic-Regular.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt
|
RUN apk --no-cache add wget && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt
|
||||||
|
|
||||||
RUN fontforge -lang=py -c 'font1 = fontforge.open("LiberationSans-Regular.ttf"); font2 = fontforge.open("NotoSansArabic-Regular.ttf"); font1.mergeFonts(font2); font1.generate("LiberationSans-Regular.ttf")'
|
|
||||||
|
|
||||||
FROM ruby:3.2.2-alpine3.18 as webpack
|
FROM ruby:3.2.2-alpine3.18 as webpack
|
||||||
|
|
||||||
@@ -55,7 +53,7 @@ COPY ./public ./public
|
|||||||
COPY ./tmp ./tmp
|
COPY ./tmp ./tmp
|
||||||
COPY LICENSE README.md Rakefile config.ru .version ./
|
COPY LICENSE README.md Rakefile config.ru .version ./
|
||||||
|
|
||||||
COPY --from=fonts /fonts/LiberationSans-Regular.ttf /fonts/LiberationSans-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
COPY --from=fonts /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
||||||
COPY --from=webpack /app/public/packs ./public/packs
|
COPY --from=webpack /app/public/packs ./public/packs
|
||||||
|
|
||||||
RUN ln -s /fonts /app/public/fonts
|
RUN ln -s /fonts /app/public/fonts
|
||||||
|
|||||||
+1
-1
@@ -261,7 +261,7 @@ GEM
|
|||||||
signet (>= 0.16, < 2.a)
|
signet (>= 0.16, < 2.a)
|
||||||
hashdiff (1.1.0)
|
hashdiff (1.1.0)
|
||||||
hashery (2.1.2)
|
hashery (2.1.2)
|
||||||
hexapdf (0.34.1)
|
hexapdf (0.37.1)
|
||||||
cmdparse (~> 3.0, >= 3.0.3)
|
cmdparse (~> 3.0, >= 3.0.3)
|
||||||
geom2d (~> 0.4, >= 0.4.1)
|
geom2d (~> 0.4, >= 0.4.1)
|
||||||
openssl (>= 2.2.1)
|
openssl (>= 2.2.1)
|
||||||
|
|||||||
@@ -33,17 +33,29 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
[](https://demo.docuseal.co)
|
[](https://demo.docuseal.co)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- [x] PDF form fields builder (WYSIWYG)
|
- PDF form fields builder (WYSIWYG)
|
||||||
- [x] 11 field types available (Signature, Date, File, Checkbox etc.)
|
- 12 field types available (Signature, Date, File, Checkbox etc.)
|
||||||
- [x] Multiple submitters per document
|
- Multiple submitters per document
|
||||||
- [x] Automated emails via SMTP
|
- Automated emails via SMTP
|
||||||
- [x] Files storage on disk or AWS S3, Google Storage, Azure Cloud
|
- Files storage on disk or AWS S3, Google Storage, Azure Cloud
|
||||||
- [x] Automatic PDF eSignature
|
- Automatic PDF eSignature
|
||||||
- [x] PDF signature verification
|
- PDF signature verification
|
||||||
- [x] Users management
|
- Users management
|
||||||
- [x] Mobile-optimized
|
- Mobile-optimized
|
||||||
- [x] API and Webhooks for integrations
|
- API and Webhooks for integrations
|
||||||
- [x] Easy to deploy in minutes
|
- Easy to deploy in minutes
|
||||||
|
|
||||||
|
## Pro Features
|
||||||
|
- Company logo and white-label
|
||||||
|
- User roles
|
||||||
|
- Automated reminders
|
||||||
|
- Invitation and identify verification via SMS
|
||||||
|
- SSO / SAML
|
||||||
|
- Template creation with HTML API ([Guide](https://www.docuseal.co/guides/create-pdf-document-fillable-form-with-html-api))
|
||||||
|
- Template creation with PDF or DOCX and text tags API ([Guide](https://www.docuseal.co/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form))
|
||||||
|
- Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue) or [JavaScript](https://www.docuseal.co/docs/embedded))
|
||||||
|
- Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue) or [JavaScript](https://www.docuseal.co/docs/embedded))
|
||||||
|
- [Learn more](https://www.docuseal.co/pricing)
|
||||||
|
|
||||||
## Deploy
|
## Deploy
|
||||||
|
|
||||||
@@ -77,7 +89,7 @@ sudo HOST=your-domain-name.com docker compose up
|
|||||||
```
|
```
|
||||||
|
|
||||||
## For Businesses
|
## For Businesses
|
||||||
### Integrate seamless document signing into your web or mobile apps with DocuSeal!
|
### Integrate seamless document signing into your web or mobile apps with DocuSeal
|
||||||
|
|
||||||
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
|
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
**We have a bug bounty program to reward security researchers.**
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api
|
||||||
|
class ActiveStorageBlobsProxyController < ApiBaseController
|
||||||
|
include ActiveStorage::Streaming
|
||||||
|
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
before_action :set_cors_headers
|
||||||
|
|
||||||
|
def show
|
||||||
|
blob_uuid, = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
|
||||||
|
|
||||||
|
if blob_uuid.blank?
|
||||||
|
Rollbar.error('Blob not found') if defined?(Rollbar)
|
||||||
|
|
||||||
|
return head :not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
|
||||||
|
|
||||||
|
if request.headers['Range'].present?
|
||||||
|
send_blob_byte_range_data blob, request.headers['Range']
|
||||||
|
else
|
||||||
|
http_cache_forever public: true do
|
||||||
|
response.headers['Accept-Ranges'] = 'bytes'
|
||||||
|
response.headers['Content-Length'] = blob.byte_size.to_s
|
||||||
|
|
||||||
|
send_blob_stream blob, disposition: params[:disposition]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api
|
||||||
|
class ActiveStorageBlobsProxyLegacyController < ApiBaseController
|
||||||
|
include ActiveStorage::Streaming
|
||||||
|
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
# rubocop:disable Metrics
|
||||||
|
def show
|
||||||
|
Rollbar.info('Blob legacy') if defined?(Rollbar)
|
||||||
|
|
||||||
|
blob = ActiveStorage::Blob.find_signed!(params[:signed_blob_id] || params[:signed_id])
|
||||||
|
|
||||||
|
is_permitted = blob.attachments.any? do |a|
|
||||||
|
(current_user && a.record.account.id == current_user.account_id) ||
|
||||||
|
a.record.account.account_configs.any? { |e| e.key == 'legacy_blob_proxy' } ||
|
||||||
|
a.name == 'logo'
|
||||||
|
end
|
||||||
|
|
||||||
|
unless is_permitted
|
||||||
|
Rollbar.error("Blob account not found: #{blob.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return render json: { error: 'Not authenticated' }, status: :unauthorized
|
||||||
|
end
|
||||||
|
|
||||||
|
if request.headers['Range'].present?
|
||||||
|
send_blob_byte_range_data blob, request.headers['Range']
|
||||||
|
else
|
||||||
|
http_cache_forever public: true do
|
||||||
|
response.headers['Accept-Ranges'] = 'bytes'
|
||||||
|
response.headers['Content-Length'] = blob.byte_size.to_s
|
||||||
|
|
||||||
|
send_blob_stream blob, disposition: params[:disposition]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# rubocop:enable Metrics
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -21,13 +21,13 @@ module Api
|
|||||||
|
|
||||||
if Rails.env.production?
|
if Rails.env.production?
|
||||||
rescue_from CanCan::AccessDenied do |e|
|
rescue_from CanCan::AccessDenied do |e|
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
render json: { error: e.message }, status: :forbidden
|
render json: { error: e.message }, status: :forbidden
|
||||||
end
|
end
|
||||||
|
|
||||||
rescue_from JSON::ParserError do |e|
|
rescue_from JSON::ParserError do |e|
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
render json: { error: "JSON parse error: #{e.message}" }, status: :unprocessable_entity
|
render json: { error: "JSON parse error: #{e.message}" }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
@@ -59,5 +59,13 @@ module Api
|
|||||||
def current_account
|
def current_account
|
||||||
current_user&.account
|
current_user&.account
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_cors_headers
|
||||||
|
headers['Access-Control-Allow-Origin'] = '*'
|
||||||
|
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
|
||||||
|
headers['Access-Control-Allow-Headers'] = '*'
|
||||||
|
headers['Access-Control-Max-Age'] = '1728000'
|
||||||
|
headers['Access-Control-Allow-Credentials'] = true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -33,12 +33,18 @@ module Api
|
|||||||
def show
|
def show
|
||||||
submitters = @submission.submitters.preload(documents_attachments: :blob, attachments_attachments: :blob)
|
submitters = @submission.submitters.preload(documents_attachments: :blob, attachments_attachments: :blob)
|
||||||
|
|
||||||
serialized_submitters = submitters.map do |submitter|
|
submitters.each do |submitter|
|
||||||
Submissions::EnsureResultGenerated.call(submitter) if submitter.completed_at?
|
if submitter.completed_at? && submitter.documents_attachments.blank?
|
||||||
|
submitter.documents_attachments = Submissions::EnsureResultGenerated.call(submitter)
|
||||||
Submitters::SerializeForApi.call(submitter)
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if @submission.audit_trail_attachment.blank? && submitters.all?(&:completed_at?)
|
||||||
|
@submission.audit_trail_attachment = Submissions::GenerateAuditTrail.call(@submission)
|
||||||
|
end
|
||||||
|
|
||||||
|
serialized_submitters = submitters.map { |submitter| Submitters::SerializeForApi.call(submitter) }
|
||||||
|
|
||||||
json = @submission.as_json(
|
json = @submission.as_json(
|
||||||
serialize_params.deep_merge(
|
serialize_params.deep_merge(
|
||||||
include: { submission_events: { only: %i[id submitter_id event_type event_timestamp] } }
|
include: { submission_events: { only: %i[id submitter_id event_type event_timestamp] } }
|
||||||
@@ -68,7 +74,7 @@ module Api
|
|||||||
return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil?
|
return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil?
|
||||||
|
|
||||||
if @template.fields.blank?
|
if @template.fields.blank?
|
||||||
Rollbar.error("Template does not contain fields: #{@template.id}") if defined?(Rollbar)
|
Rollbar.warning("Template does not contain fields: #{@template.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
return render json: { error: 'Template does not contain fields' }, status: :unprocessable_entity
|
return render json: { error: 'Template does not contain fields' }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
@@ -80,8 +86,16 @@ module Api
|
|||||||
|
|
||||||
Submissions.send_signature_requests(submissions)
|
Submissions.send_signature_requests(submissions)
|
||||||
|
|
||||||
|
submissions.each do |submission|
|
||||||
|
if submission.submitters.all?(&:completed_at?) && submission.submitters.last
|
||||||
|
ProcessSubmitterCompletionJob.perform_later(submission.submitters.last)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
render json: submissions.flat_map(&:submitters)
|
render json: submissions.flat_map(&:submitters)
|
||||||
rescue Submitters::NormalizeValues::UnknownFieldName, Submitters::NormalizeValues::UnknownSubmitterName => e
|
rescue Submitters::NormalizeValues::BaseError => e
|
||||||
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
render json: { error: e.message }, status: :unprocessable_entity
|
render json: { error: e.message }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -118,8 +132,13 @@ module Api
|
|||||||
params:
|
params:
|
||||||
)
|
)
|
||||||
|
|
||||||
Submissions::NormalizeParamUtils.save_default_value_attachments!(attachments,
|
submitters = submissions.flat_map(&:submitters)
|
||||||
submissions.flat_map(&:submitters))
|
|
||||||
|
Submissions::NormalizeParamUtils.save_default_value_attachments!(attachments, submitters)
|
||||||
|
|
||||||
|
submitters.each do |submitter|
|
||||||
|
SubmissionEvents.create_with_tracking_data(submitter, 'api_complete_form', request) if submitter.completed_at?
|
||||||
|
end
|
||||||
|
|
||||||
submissions
|
submissions
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ module Api
|
|||||||
def create
|
def create
|
||||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
|
submitter.opened_at = Time.current
|
||||||
|
submitter.save
|
||||||
|
|
||||||
SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request)
|
SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request)
|
||||||
|
|
||||||
SendFormViewedWebhookRequestJob.perform_later(submitter)
|
SendFormViewedWebhookRequestJob.perform_later(submitter)
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ module Api
|
|||||||
@submitter.save!
|
@submitter.save!
|
||||||
|
|
||||||
@submitter.submission.save!
|
@submitter.submission.save!
|
||||||
|
|
||||||
|
SubmissionEvents.create_with_tracking_data(@submitter, 'api_complete_form', request) if @submitter.completed_at?
|
||||||
end
|
end
|
||||||
|
|
||||||
if @submitter.completed_at?
|
if @submitter.completed_at?
|
||||||
|
|||||||
@@ -24,10 +24,16 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
if (folder_name = params.dig(:template, :folder_name))
|
if (folder_name = params[:folder_name] || params.dig(:template, :folder_name))
|
||||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, folder_name)
|
@template.folder = TemplateFolders.find_or_create_by_name(current_user, folder_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
archived = params.key?(:archived) ? params[:archived] : params.dig(:template, :archived)
|
||||||
|
|
||||||
|
if archived.in?([true, false])
|
||||||
|
@template.archived_at = archived == true ? Time.current : nil
|
||||||
|
end
|
||||||
|
|
||||||
@template.update!(template_params)
|
@template.update!(template_params)
|
||||||
|
|
||||||
render json: @template.as_json(only: %i[id updated_at])
|
render json: @template.as_json(only: %i[id updated_at])
|
||||||
@@ -64,15 +70,21 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def template_params
|
def template_params
|
||||||
params.require(:template).permit(
|
permit_params = [
|
||||||
:name,
|
:name,
|
||||||
schema: [%i[attachment_uuid name]],
|
{ schema: [%i[attachment_uuid name]],
|
||||||
submitters: [%i[name uuid]],
|
submitters: [%i[name uuid]],
|
||||||
fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value,
|
fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value,
|
||||||
{ preferences: {},
|
{ preferences: {},
|
||||||
options: [%i[value uuid]],
|
options: [%i[value uuid]],
|
||||||
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
|
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
||||||
)
|
]
|
||||||
|
|
||||||
|
if params.key?(:template)
|
||||||
|
params.require(:template).permit(*permit_params)
|
||||||
|
else
|
||||||
|
params.permit(*permit_params)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -16,7 +16,7 @@ module Api
|
|||||||
render json: {
|
render json: {
|
||||||
schema:,
|
schema:,
|
||||||
documents: documents.as_json(
|
documents: documents.as_json(
|
||||||
methods: [:metadata],
|
methods: %i[metadata signed_uuid],
|
||||||
include: {
|
include: {
|
||||||
preview_images: { methods: %i[url metadata filename] }
|
preview_images: { methods: %i[url metadata filename] }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class ApplicationController < ActionController::Base
|
|||||||
|
|
||||||
if Rails.env.production?
|
if Rails.env.production?
|
||||||
rescue_from CanCan::AccessDenied do |e|
|
rescue_from CanCan::AccessDenied do |e|
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
redirect_to root_path, alert: e.message
|
redirect_to root_path, alert: e.message
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class EncryptedUserConfigsController < ApplicationController
|
||||||
|
load_and_authorize_resource :encrypted_user_config
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@encrypted_user_config.destroy!
|
||||||
|
|
||||||
|
redirect_back(fallback_location: root_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -6,7 +6,9 @@ class PreviewDocumentPageController < ActionController::API
|
|||||||
FORMAT = Templates::ProcessDocument::FORMAT
|
FORMAT = Templates::ProcessDocument::FORMAT
|
||||||
|
|
||||||
def show
|
def show
|
||||||
attachment = ActiveStorage::Attachment.find_by(uuid: params[:attachment_uuid])
|
attachment_uuid = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid], purpose: :attachment)
|
||||||
|
|
||||||
|
attachment = ActiveStorage::Attachment.find_by(uuid: attachment_uuid, name: :preview_images) if attachment_uuid
|
||||||
|
|
||||||
return head :not_found unless attachment
|
return head :not_found unless attachment
|
||||||
|
|
||||||
@@ -21,9 +23,10 @@ class PreviewDocumentPageController < ActionController::API
|
|||||||
find_or_create_document_tempfile_path(attachment)
|
find_or_create_document_tempfile_path(attachment)
|
||||||
end
|
end
|
||||||
|
|
||||||
io = Templates::ProcessDocument.generate_pdf_preview_from_file(attachment, file_path, params[:id].to_i)
|
preview_image =
|
||||||
|
Templates::ProcessDocument.generate_pdf_preview_from_file(attachment, file_path, params[:id].to_i)
|
||||||
|
|
||||||
render plain: io.tap(&:rewind).read, content_type: 'image/jpeg'
|
redirect_to preview_image.url, allow_other_host: true
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_or_create_document_tempfile_path(attachment)
|
def find_or_create_document_tempfile_path(attachment)
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ class SessionsController < Devise::SessionsController
|
|||||||
before_action :configure_permitted_parameters
|
before_action :configure_permitted_parameters
|
||||||
|
|
||||||
def create
|
def create
|
||||||
if Docuseal.multitenant? && !User.exists?(email: sign_in_params[:email])
|
email = sign_in_params[:email].to_s.downcase
|
||||||
|
|
||||||
|
if Docuseal.multitenant? && !User.exists?(email:)
|
||||||
return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)),
|
return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)),
|
||||||
notice: 'Create a new account'
|
notice: 'Create a new account'
|
||||||
end
|
end
|
||||||
|
|
||||||
if User.exists?(email: sign_in_params[:email], otp_required_for_login: true) && sign_in_params[:otp_attempt].blank?
|
if User.exists?(email:, otp_required_for_login: true) && sign_in_params[:otp_attempt].blank?
|
||||||
return render :otp, locals: { resource: User.new(sign_in_params) }, status: :unprocessable_entity
|
return render :otp, locals: { resource: User.new(sign_in_params) }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ class StartFormController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
return redirect_to start_form_path(@template.slug) if @template.archived_at?
|
||||||
|
|
||||||
@submitter = Submitter.where(submission: @template.submissions.where(archived_at: nil))
|
@submitter = Submitter.where(submission: @template.submissions.where(archived_at: nil))
|
||||||
.order(id: :desc)
|
.order(id: :desc)
|
||||||
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
|
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
|
TTL = 20.minutes
|
||||||
|
|
||||||
def index
|
def index
|
||||||
submitter = Submitter.find_by(slug: params[:submitter_slug])
|
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(submitter)
|
Submissions::EnsureResultGenerated.call(submitter)
|
||||||
|
|
||||||
@@ -13,9 +15,18 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(last_submitter)
|
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||||
|
|
||||||
|
return head :not_found unless last_submitter.completed_at?
|
||||||
|
|
||||||
|
if last_submitter.completed_at < TTL.ago &&
|
||||||
|
(current_user.nil? || !current_user.account.submitters.exists?(id: last_submitter.id))
|
||||||
|
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return head :not_found
|
||||||
|
end
|
||||||
|
|
||||||
urls =
|
urls =
|
||||||
Submitters.select_attachments_for_download(last_submitter).map do |attachment|
|
Submitters.select_attachments_for_download(last_submitter).map do |attachment|
|
||||||
helpers.rails_blob_url(attachment)
|
ActiveStorage::Blob.proxy_url(attachment.blob)
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: urls
|
render json: urls
|
||||||
|
|||||||
@@ -6,9 +6,21 @@ class SubmissionsPreviewController < ApplicationController
|
|||||||
|
|
||||||
PRELOAD_ALL_PAGES_AMOUNT = 200
|
PRELOAD_ALL_PAGES_AMOUNT = 200
|
||||||
|
|
||||||
|
TTL = 20.minutes
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@submission = Submission.find_by!(slug: params[:slug])
|
@submission = Submission.find_by!(slug: params[:slug])
|
||||||
|
|
||||||
|
if !@submission.submitters.all?(&:completed_at?) && current_user.blank?
|
||||||
|
raise ActionController::RoutingError, 'Not Found'
|
||||||
|
end
|
||||||
|
|
||||||
|
unless submission_valid_ttl?(@submission)
|
||||||
|
Rollbar.info("TTL: #{@submission.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return redirect_to submissions_preview_completed_path(@submission.slug)
|
||||||
|
end
|
||||||
|
|
||||||
ActiveRecord::Associations::Preloader.new(
|
ActiveRecord::Associations::Preloader.new(
|
||||||
records: [@submission],
|
records: [@submission],
|
||||||
associations: [:template, { template_schema_documents: :blob }]
|
associations: [:template, { template_schema_documents: :blob }]
|
||||||
@@ -26,4 +38,20 @@ class SubmissionsPreviewController < ApplicationController
|
|||||||
|
|
||||||
render 'submissions/show', layout: 'plain'
|
render 'submissions/show', layout: 'plain'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def completed
|
||||||
|
@submission = Submission.find_by!(slug: params[:submissions_preview_slug])
|
||||||
|
|
||||||
|
render :completed, layout: 'plain'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def submission_valid_ttl?(submission)
|
||||||
|
return true if current_user && current_user.account.submissions.exists?(id: submission.id)
|
||||||
|
|
||||||
|
last_submitter = submission.submitters.select(&:completed_at?).max_by(&:completed_at)
|
||||||
|
|
||||||
|
last_submitter && last_submitter.completed_at > TTL.ago
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -33,7 +33,7 @@ class TemplatesController < ApplicationController
|
|||||||
@template_data =
|
@template_data =
|
||||||
@template.as_json.merge(
|
@template.as_json.merge(
|
||||||
documents: @template.schema_documents.as_json(
|
documents: @template.schema_documents.as_json(
|
||||||
methods: [:metadata],
|
methods: %i[metadata signed_uuid],
|
||||||
include: { preview_images: { methods: %i[url metadata filename] } }
|
include: { preview_images: { methods: %i[url metadata filename] } }
|
||||||
)
|
)
|
||||||
).to_json
|
).to_json
|
||||||
@@ -51,12 +51,17 @@ class TemplatesController < ApplicationController
|
|||||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
||||||
end
|
end
|
||||||
|
|
||||||
@template.account = current_account
|
if params[:account_id].present? && authorized_clone_account_id?(params[:account_id])
|
||||||
|
@template.account_id = params[:account_id]
|
||||||
|
@template.folder = @template.account.default_template_folder
|
||||||
|
else
|
||||||
|
@template.account = current_account
|
||||||
|
end
|
||||||
|
|
||||||
if @template.save
|
if @template.save
|
||||||
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
||||||
|
|
||||||
redirect_to edit_template_path(@template)
|
maybe_redirect_to_template(@template)
|
||||||
else
|
else
|
||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_entity
|
render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
@@ -81,6 +86,18 @@ class TemplatesController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def authorized_clone_account_id?(account_id)
|
||||||
|
true_user.account_id.to_s == account_id.to_s || true_user.account.linked_accounts.exists?(id: account_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def maybe_redirect_to_template(template)
|
||||||
|
if template.account == current_account
|
||||||
|
redirect_to(edit_template_path(@template))
|
||||||
|
else
|
||||||
|
redirect_back(fallback_location: root_path, notice: 'Template has been clonned')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def template_params
|
def template_params
|
||||||
params.require(:template).permit(:name)
|
params.require(:template).permit(:name)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class TemplatesPreviewController < ApplicationController
|
|||||||
@template_data =
|
@template_data =
|
||||||
@template.as_json.merge(
|
@template.as_json.merge(
|
||||||
documents: @template.schema_documents.as_json(
|
documents: @template.schema_documents.as_json(
|
||||||
methods: [:metadata],
|
methods: %i[metadata signed_uuid],
|
||||||
include: { preview_images: { methods: %i[url metadata filename] } }
|
include: { preview_images: { methods: %i[url metadata filename] } }
|
||||||
)
|
)
|
||||||
).to_json
|
).to_json
|
||||||
|
|||||||
@@ -307,7 +307,7 @@ export default {
|
|||||||
formattedDate () {
|
formattedDate () {
|
||||||
if (this.field.type === 'date' && this.modelValue) {
|
if (this.field.type === 'date' && this.modelValue) {
|
||||||
return this.formatDate(
|
return this.formatDate(
|
||||||
new Date(this.modelValue),
|
this.modelValue === '{{date}}' ? new Date() : new Date(this.modelValue),
|
||||||
this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
|
this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
v-if="!isCompleted"
|
v-if="!isCompleted"
|
||||||
id="minimize_form_button"
|
id="minimize_form_button"
|
||||||
class="absolute right-0 mr-2 mt-2 top-0 hidden md:block"
|
class="absolute right-0 mr-2 mt-2 top-0 hidden md:block"
|
||||||
title="Minimize"
|
:title="t('minimize')"
|
||||||
@click.prevent="isFormVisible = false"
|
@click.prevent="isFormVisible = false"
|
||||||
>
|
>
|
||||||
<IconArrowsDiagonalMinimize2
|
<IconArrowsDiagonalMinimize2
|
||||||
@@ -40,13 +40,16 @@
|
|||||||
:height="20"
|
:height="20"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<div>
|
<div
|
||||||
|
:class="{ 'md:px-4': isBreakpointMd }"
|
||||||
|
>
|
||||||
<form
|
<form
|
||||||
v-if="!isCompleted"
|
v-if="!isCompleted"
|
||||||
ref="form"
|
ref="form"
|
||||||
:action="submitPath"
|
:action="submitPath"
|
||||||
method="post"
|
method="post"
|
||||||
:class="{ 'md:mx-16': isBreakpointMd }"
|
class="mx-auto"
|
||||||
|
:style="{ maxWidth: isBreakpointMd ? '582px' : '' }"
|
||||||
@submit.prevent="submitStep"
|
@submit.prevent="submitStep"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -254,7 +257,7 @@
|
|||||||
:key="currentField.uuid"
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
:previous-value="previousSignatureValue"
|
:previous-value="previousSignatureValueFor(currentField)"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
:with-typed-signature="withTypedSignature"
|
:with-typed-signature="withTypedSignature"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
@@ -518,6 +521,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
},
|
},
|
||||||
|
i18n: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
withSendCopyButton: {
|
withSendCopyButton: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -566,11 +574,6 @@ export default {
|
|||||||
submitterSlug () {
|
submitterSlug () {
|
||||||
return this.submitter.slug
|
return this.submitter.slug
|
||||||
},
|
},
|
||||||
previousSignatureValue () {
|
|
||||||
const signatureField = [...this.fields].reverse().find((field) => field.type === 'signature' && !!this.values[field.uuid])
|
|
||||||
|
|
||||||
return this.values[signatureField?.uuid]
|
|
||||||
},
|
|
||||||
previousInitialsValue () {
|
previousInitialsValue () {
|
||||||
const initialsField = [...this.fields].reverse().find((field) => field.type === 'initials' && !!this.values[field.uuid])
|
const initialsField = [...this.fields].reverse().find((field) => field.type === 'initials' && !!this.values[field.uuid])
|
||||||
|
|
||||||
@@ -621,7 +624,7 @@ export default {
|
|||||||
|
|
||||||
this.fields.forEach((field) => {
|
this.fields.forEach((field) => {
|
||||||
if (field.default_value && !field.readonly) {
|
if (field.default_value && !field.readonly) {
|
||||||
this.values[field.uuid] = field.default_value
|
this.values[field.uuid] ||= field.default_value
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -675,7 +678,7 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
t (key) {
|
t (key) {
|
||||||
return i18n[this.language?.toLowerCase()]?.[key] || i18n[this.browserLanguage]?.[key] || i18n.en[key] || key
|
return this.i18n[key] || i18n[this.language?.toLowerCase()]?.[key] || i18n[this.browserLanguage]?.[key] || i18n.en[key] || key
|
||||||
},
|
},
|
||||||
maybeTrackEmailClick () {
|
maybeTrackEmailClick () {
|
||||||
const { queryParams } = this
|
const { queryParams } = this
|
||||||
@@ -736,6 +739,13 @@ export default {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
previousSignatureValueFor (field) {
|
||||||
|
const signatureField = [...this.fields].reverse().find((f) =>
|
||||||
|
f.type === 'signature' && field.preferences?.format === f.preferences?.format && !!this.values[f.uuid]
|
||||||
|
)
|
||||||
|
|
||||||
|
return this.values[signatureField?.uuid]
|
||||||
|
},
|
||||||
goToStep (step, scrollToArea = false, clickUpload = false) {
|
goToStep (step, scrollToArea = false, clickUpload = false) {
|
||||||
this.currentStep = this.stepFields.indexOf(step)
|
this.currentStep = this.stepFields.indexOf(step)
|
||||||
this.showFillAllRequiredFields = false
|
this.showFillAllRequiredFields = false
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const en = {
|
|||||||
radio: 'Radio',
|
radio: 'Radio',
|
||||||
cells: 'Cells',
|
cells: 'Cells',
|
||||||
stamp: 'Stamp',
|
stamp: 'Stamp',
|
||||||
|
minimize: 'Minimize',
|
||||||
payment: 'Payment',
|
payment: 'Payment',
|
||||||
phone: 'Phone',
|
phone: 'Phone',
|
||||||
submit_form: 'Submit Form',
|
submit_form: 'Submit Form',
|
||||||
@@ -62,6 +63,7 @@ const en = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
|
minimize: 'Minimizar',
|
||||||
text: 'Texto',
|
text: 'Texto',
|
||||||
signature: 'Firma',
|
signature: 'Firma',
|
||||||
initials: 'Iniciales',
|
initials: 'Iniciales',
|
||||||
@@ -124,6 +126,7 @@ const es = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
|
minimize: 'Minimizza',
|
||||||
text: 'Testo',
|
text: 'Testo',
|
||||||
signature: 'Firma',
|
signature: 'Firma',
|
||||||
initials: 'Iniziali',
|
initials: 'Iniziali',
|
||||||
@@ -186,6 +189,7 @@ const it = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
|
minimize: 'Minimieren',
|
||||||
text: 'Text',
|
text: 'Text',
|
||||||
signature: 'Unterschrift',
|
signature: 'Unterschrift',
|
||||||
initials: 'Initialen',
|
initials: 'Initialen',
|
||||||
@@ -248,6 +252,7 @@ const de = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
|
minimize: 'Réduire',
|
||||||
text: 'Texte',
|
text: 'Texte',
|
||||||
signature: 'Signature',
|
signature: 'Signature',
|
||||||
initials: 'Initiales',
|
initials: 'Initiales',
|
||||||
@@ -310,6 +315,7 @@ const fr = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pl = {
|
const pl = {
|
||||||
|
minimize: 'Zminimalizuj',
|
||||||
text: 'Tekst',
|
text: 'Tekst',
|
||||||
signature: 'Podpis',
|
signature: 'Podpis',
|
||||||
initials: 'Inicjały',
|
initials: 'Inicjały',
|
||||||
@@ -372,6 +378,7 @@ const pl = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const uk = {
|
const uk = {
|
||||||
|
minimize: 'Зменшити',
|
||||||
text: 'Текст',
|
text: 'Текст',
|
||||||
signature: 'Підпис',
|
signature: 'Підпис',
|
||||||
initials: 'Ініціали',
|
initials: 'Ініціали',
|
||||||
@@ -434,6 +441,7 @@ const uk = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cs = {
|
const cs = {
|
||||||
|
minimize: 'Minimalizovat',
|
||||||
text: 'Text',
|
text: 'Text',
|
||||||
signature: 'Podpis',
|
signature: 'Podpis',
|
||||||
initials: 'Iniciály',
|
initials: 'Iniciály',
|
||||||
@@ -496,6 +504,7 @@ const cs = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
|
minimize: 'Minimizar',
|
||||||
text: 'Texto',
|
text: 'Texto',
|
||||||
signature: 'Assinatura',
|
signature: 'Assinatura',
|
||||||
initials: 'Iniciais',
|
initials: 'Iniciais',
|
||||||
@@ -558,6 +567,7 @@ const pt = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const he = {
|
const he = {
|
||||||
|
minimize: 'לקטן',
|
||||||
text: 'טקסט',
|
text: 'טקסט',
|
||||||
signature: 'חתימה',
|
signature: 'חתימה',
|
||||||
initials: 'ראשי תיקיות',
|
initials: 'ראשי תיקיות',
|
||||||
@@ -621,6 +631,7 @@ const he = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nl = {
|
const nl = {
|
||||||
|
minimize: 'Minimaliseren',
|
||||||
text: 'Tekst',
|
text: 'Tekst',
|
||||||
signature: 'Handtekening',
|
signature: 'Handtekening',
|
||||||
initials: 'Initialen',
|
initials: 'Initialen',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<span
|
<span
|
||||||
v-if="isDrawInitials"
|
v-if="isDrawInitials"
|
||||||
class="tooltip"
|
class="tooltip"
|
||||||
:data-tip="t('type_initials')"
|
:data-tip="t('type_initial')"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
id="type_text_button"
|
id="type_text_button"
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
{{ t('clear') }}
|
{{ t('clear') }}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
title="Minimize"
|
:title="t('minimize')"
|
||||||
href="#"
|
href="#"
|
||||||
class="py-1.5 inline md:hidden"
|
class="py-1.5 inline md:hidden"
|
||||||
@click.prevent="$emit('minimize')"
|
@click.prevent="$emit('minimize')"
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
<canvas
|
<canvas
|
||||||
v-show="!modelValue && !computedPreviousValue"
|
v-show="!modelValue && !computedPreviousValue"
|
||||||
ref="canvas"
|
ref="canvas"
|
||||||
class="bg-white border border-base-300 rounded-2xl"
|
class="bg-white border border-base-300 rounded-2xl w-full"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
|
v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
|
||||||
@@ -106,6 +106,8 @@ import { IconReload, IconTextSize, IconSignature, IconArrowsDiagonalMinimize2 }
|
|||||||
import SignaturePad from 'signature_pad'
|
import SignaturePad from 'signature_pad'
|
||||||
import AppearsOn from './appears_on'
|
import AppearsOn from './appears_on'
|
||||||
|
|
||||||
|
const scale = 3
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'InitialsStep',
|
name: 'InitialsStep',
|
||||||
components: {
|
components: {
|
||||||
@@ -171,8 +173,10 @@ export default {
|
|||||||
async mounted () {
|
async mounted () {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (this.$refs.canvas) {
|
if (this.$refs.canvas) {
|
||||||
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth
|
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
|
||||||
this.$refs.canvas.height = this.$refs.canvas.parentNode.clientWidth / 5
|
this.$refs.canvas.height = (this.$refs.canvas.parentNode.clientWidth / 3) * scale
|
||||||
|
|
||||||
|
this.$refs.canvas.getContext('2d').scale(scale, scale)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$refs.textInput?.focus()
|
this.$refs.textInput?.focus()
|
||||||
@@ -190,8 +194,26 @@ export default {
|
|||||||
|
|
||||||
this.$emit('start')
|
this.$emit('start')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.intersectionObserver = new IntersectionObserver((entries, observer) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
|
||||||
|
this.$refs.canvas.height = (this.$refs.canvas.parentNode.clientWidth / 3) * scale
|
||||||
|
|
||||||
|
this.$refs.canvas.getContext('2d').scale(scale, scale)
|
||||||
|
|
||||||
|
this.intersectionObserver?.disconnect()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.intersectionObserver.observe(this.$refs.canvas)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
beforeUnmount () {
|
||||||
|
this.intersectionObserver?.disconnect()
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
remove () {
|
remove () {
|
||||||
this.$emit('update:model-value', '')
|
this.$emit('update:model-value', '')
|
||||||
@@ -209,7 +231,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateWrittenInitials (e) {
|
updateWrittenInitials (e) {
|
||||||
this.isInitialsStarted = true
|
this.isInitialsStarted = !!e.target.value
|
||||||
|
|
||||||
const canvas = this.$refs.canvas
|
const canvas = this.$refs.canvas
|
||||||
const context = canvas.getContext('2d')
|
const context = canvas.getContext('2d')
|
||||||
@@ -222,8 +244,8 @@ export default {
|
|||||||
context.font = fontStyle + ' ' + fontWeight + ' ' + fontSize + ' ' + fontFamily
|
context.font = fontStyle + ' ' + fontWeight + ' ' + fontSize + ' ' + fontFamily
|
||||||
context.textAlign = 'center'
|
context.textAlign = 'center'
|
||||||
|
|
||||||
context.clearRect(0, 0, canvas.width, canvas.height)
|
context.clearRect(0, 0, canvas.width / scale, canvas.height / scale)
|
||||||
context.fillText(e.target.value, canvas.width / 2, canvas.height / 2 + 11)
|
context.fillText(e.target.value, canvas.width / 2 / scale, canvas.height / 2 / scale + 11)
|
||||||
},
|
},
|
||||||
toggleTextInput () {
|
toggleTextInput () {
|
||||||
this.remove()
|
this.remove()
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
>{{ showFieldNames && field.name ? field.name : t('signature') }}</label>
|
>{{ showFieldNames && field.name ? field.name : t('signature') }}</label>
|
||||||
<div class="space-x-2 flex">
|
<div class="space-x-2 flex">
|
||||||
<span
|
<span
|
||||||
v-if="isTextSignature"
|
v-if="isTextSignature && field.preferences?.format !== 'typed'"
|
||||||
class="tooltip"
|
class="tooltip"
|
||||||
:data-tip="t('draw_signature')"
|
:data-tip="t('draw_signature')"
|
||||||
>
|
>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-else-if="withTypedSignature"
|
v-else-if="withTypedSignature && field.preferences?.format !== 'typed' && field.preferences?.format !== 'drawn'"
|
||||||
class="tooltip ml-2"
|
class="tooltip ml-2"
|
||||||
:data-tip="t('type_text')"
|
:data-tip="t('type_text')"
|
||||||
>
|
>
|
||||||
@@ -40,6 +40,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
|
v-if="field.preferences?.format !== 'typed'"
|
||||||
class="tooltip"
|
class="tooltip"
|
||||||
:data-tip="t('take_photo')"
|
:data-tip="t('take_photo')"
|
||||||
>
|
>
|
||||||
@@ -48,6 +49,7 @@
|
|||||||
>
|
>
|
||||||
<IconCamera :width="16" />
|
<IconCamera :width="16" />
|
||||||
<input
|
<input
|
||||||
|
:key="uploadImageInputKey"
|
||||||
type="file"
|
type="file"
|
||||||
hidden
|
hidden
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
@@ -78,7 +80,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
title="Minimize"
|
:title="t('minimize')"
|
||||||
class="py-1.5 inline md:hidden"
|
class="py-1.5 inline md:hidden"
|
||||||
@click.prevent="$emit('minimize')"
|
@click.prevent="$emit('minimize')"
|
||||||
>
|
>
|
||||||
@@ -104,14 +106,14 @@
|
|||||||
v-show="!modelValue && !computedPreviousValue"
|
v-show="!modelValue && !computedPreviousValue"
|
||||||
ref="canvas"
|
ref="canvas"
|
||||||
style="padding: 1px; 0"
|
style="padding: 1px; 0"
|
||||||
class="bg-white border border-base-300 rounded-2xl"
|
class="bg-white border border-base-300 rounded-2xl w-full"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
v-if="isTextSignature"
|
v-if="isTextSignature"
|
||||||
id="signature_text_input"
|
id="signature_text_input"
|
||||||
ref="textInput"
|
ref="textInput"
|
||||||
class="base-input !text-2xl w-full mt-6"
|
class="base-input !text-2xl w-full mt-6"
|
||||||
:required="field.required"
|
:required="field.required && !isSignatureStarted"
|
||||||
:placeholder="`${t('type_signature_here')}...`"
|
:placeholder="`${t('type_signature_here')}...`"
|
||||||
type="text"
|
type="text"
|
||||||
@input="updateWrittenSignature"
|
@input="updateWrittenSignature"
|
||||||
@@ -127,6 +129,8 @@ import AppearsOn from './appears_on'
|
|||||||
|
|
||||||
let isFontLoaded = false
|
let isFontLoaded = false
|
||||||
|
|
||||||
|
const scale = 3
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SignatureStep',
|
name: 'SignatureStep',
|
||||||
components: {
|
components: {
|
||||||
@@ -183,7 +187,8 @@ export default {
|
|||||||
return {
|
return {
|
||||||
isSignatureStarted: !!this.previousValue,
|
isSignatureStarted: !!this.previousValue,
|
||||||
isUsePreviousValue: true,
|
isUsePreviousValue: true,
|
||||||
isTextSignature: false
|
isTextSignature: this.field.preferences?.format === 'typed',
|
||||||
|
uploadImageInputKey: Math.random().toString()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -198,8 +203,10 @@ export default {
|
|||||||
async mounted () {
|
async mounted () {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (this.$refs.canvas) {
|
if (this.$refs.canvas) {
|
||||||
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth
|
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
|
||||||
this.$refs.canvas.height = this.$refs.canvas.parentNode.clientWidth / 3
|
this.$refs.canvas.height = this.$refs.canvas.parentNode.clientWidth * scale / 3
|
||||||
|
|
||||||
|
this.$refs.canvas.getContext('2d').scale(scale, scale)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -215,8 +222,26 @@ export default {
|
|||||||
|
|
||||||
this.$emit('start')
|
this.$emit('start')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.intersectionObserver = new IntersectionObserver((entries, observer) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
|
||||||
|
this.$refs.canvas.height = this.$refs.canvas.parentNode.clientWidth * scale / 3
|
||||||
|
|
||||||
|
this.$refs.canvas.getContext('2d').scale(scale, scale)
|
||||||
|
|
||||||
|
this.intersectionObserver?.disconnect()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.intersectionObserver.observe(this.$refs.canvas)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
beforeUnmount () {
|
||||||
|
this.intersectionObserver?.disconnect()
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
remove () {
|
remove () {
|
||||||
this.$emit('update:model-value', '')
|
this.$emit('update:model-value', '')
|
||||||
@@ -247,7 +272,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateWrittenSignature (e) {
|
updateWrittenSignature (e) {
|
||||||
this.isSignatureStarted = true
|
this.isSignatureStarted = !!e.target.value
|
||||||
|
|
||||||
const canvas = this.$refs.canvas
|
const canvas = this.$refs.canvas
|
||||||
const context = canvas.getContext('2d')
|
const context = canvas.getContext('2d')
|
||||||
@@ -260,8 +285,8 @@ export default {
|
|||||||
context.font = fontStyle + ' ' + fontWeight + ' ' + fontSize + ' ' + fontFamily
|
context.font = fontStyle + ' ' + fontWeight + ' ' + fontSize + ' ' + fontFamily
|
||||||
context.textAlign = 'center'
|
context.textAlign = 'center'
|
||||||
|
|
||||||
context.clearRect(0, 0, canvas.width, canvas.height)
|
context.clearRect(0, 0, canvas.width / scale, canvas.height / scale)
|
||||||
context.fillText(e.target.value, canvas.width / 2, canvas.height / 2 + 11)
|
context.fillText(e.target.value, canvas.width / 2 / scale, canvas.height / 2 / scale + 11)
|
||||||
},
|
},
|
||||||
toggleTextInput () {
|
toggleTextInput () {
|
||||||
this.remove()
|
this.remove()
|
||||||
@@ -297,14 +322,16 @@ export default {
|
|||||||
const context = canvas.getContext('2d')
|
const context = canvas.getContext('2d')
|
||||||
|
|
||||||
const aspectRatio = img.width / img.height
|
const aspectRatio = img.width / img.height
|
||||||
|
const canvasWidth = canvas.width / scale
|
||||||
|
const canvasHeight = canvas.height / scale
|
||||||
|
|
||||||
let targetWidth = canvas.width
|
let targetWidth = canvasWidth
|
||||||
let targetHeight = canvas.height
|
let targetHeight = canvasHeight
|
||||||
|
|
||||||
if (canvas.width / canvas.height > aspectRatio) {
|
if (canvasWidth / canvasHeight > aspectRatio) {
|
||||||
targetWidth = canvas.height * aspectRatio
|
targetWidth = canvasHeight * aspectRatio
|
||||||
} else {
|
} else {
|
||||||
targetHeight = canvas.width / aspectRatio
|
targetHeight = canvasWidth / aspectRatio
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetHeight > targetWidth) {
|
if (targetHeight > targetWidth) {
|
||||||
@@ -313,17 +340,21 @@ export default {
|
|||||||
targetHeight = targetHeight * scale
|
targetHeight = targetHeight * scale
|
||||||
}
|
}
|
||||||
|
|
||||||
const x = (canvas.width - targetWidth) / 2
|
const x = (canvasWidth - targetWidth) / 2
|
||||||
const y = (canvas.height - targetHeight) / 2
|
const y = (canvasHeight - targetHeight) / 2
|
||||||
|
|
||||||
context.clearRect(0, 0, canvas.width, canvas.height)
|
setTimeout(() => {
|
||||||
context.drawImage(img, x, y, targetWidth, targetHeight)
|
context.clearRect(0, 0, canvasWidth, canvasHeight)
|
||||||
|
context.drawImage(img, x, y, targetWidth, targetHeight)
|
||||||
|
|
||||||
this.$emit('start')
|
this.$emit('start')
|
||||||
|
}, 50)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file)
|
||||||
|
|
||||||
|
this.uploadImageInputKey = Math.random().toString()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async submit () {
|
async submit () {
|
||||||
|
|||||||
@@ -123,12 +123,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.isTextArea = this.modelValue?.includes('\n')
|
if (this.modelValue) {
|
||||||
|
this.isTextArea = this.modelValue.toString().includes('\n')
|
||||||
|
|
||||||
if (this.isTextArea) {
|
if (this.isTextArea) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.resizeTextarea()
|
this.resizeTextarea()
|
||||||
})
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -90,7 +90,7 @@
|
|||||||
<button
|
<button
|
||||||
v-else-if="editable"
|
v-else-if="editable"
|
||||||
class="pr-1"
|
class="pr-1"
|
||||||
title="Remove"
|
:title="t('remove')"
|
||||||
@click.prevent="$emit('remove')"
|
@click.prevent="$emit('remove')"
|
||||||
>
|
>
|
||||||
<IconX width="14" />
|
<IconX width="14" />
|
||||||
@@ -98,21 +98,32 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex items-center h-full w-full"
|
class="flex items-center h-full w-full"
|
||||||
|
dir="auto"
|
||||||
:class="[bgColors[submitterIndex], field?.default_value ? '' : 'justify-center']"
|
:class="[bgColors[submitterIndex], field?.default_value ? '' : 'justify-center']"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-if="field"
|
v-if="field"
|
||||||
class="flex justify-center items-center space-x-1 h-full"
|
class="flex justify-center items-center space-x-1 h-full"
|
||||||
|
:class="{'w-full h-full': field.type == 'checkbox'}"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="field?.default_value"
|
v-if="field?.default_value"
|
||||||
:class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars }"
|
:class="{ 'w-full h-full': field.type == 'checkbox', 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref="textContainer"
|
ref="textContainer"
|
||||||
class="flex items-center px-0.5"
|
class="flex items-center px-0.5"
|
||||||
|
:class="{'w-full h-full': field.type == 'checkbox'}"
|
||||||
>
|
>
|
||||||
<span class="whitespace-pre-wrap">{{ field.default_value }}</span>
|
<IconCheck
|
||||||
|
v-if="field.type == 'checkbox'"
|
||||||
|
class="aspect-square mx-auto"
|
||||||
|
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="whitespace-pre-wrap"
|
||||||
|
>{{ field.default_value }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<component
|
<component
|
||||||
@@ -141,13 +152,14 @@
|
|||||||
import FieldSubmitter from './field_submitter'
|
import FieldSubmitter from './field_submitter'
|
||||||
import FieldType from './field_type'
|
import FieldType from './field_type'
|
||||||
import Field from './field'
|
import Field from './field'
|
||||||
import { IconX } from '@tabler/icons-vue'
|
import { IconX, IconCheck } from '@tabler/icons-vue'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FieldArea',
|
name: 'FieldArea',
|
||||||
components: {
|
components: {
|
||||||
FieldType,
|
FieldType,
|
||||||
|
IconCheck,
|
||||||
FieldSubmitter,
|
FieldSubmitter,
|
||||||
IconX
|
IconX
|
||||||
},
|
},
|
||||||
@@ -225,6 +237,16 @@ export default {
|
|||||||
},
|
},
|
||||||
borderColors () {
|
borderColors () {
|
||||||
return [
|
return [
|
||||||
|
'border-red-500/80',
|
||||||
|
'border-sky-500/80',
|
||||||
|
'border-emerald-500/80',
|
||||||
|
'border-yellow-300/80',
|
||||||
|
'border-purple-600/80',
|
||||||
|
'border-pink-500/80',
|
||||||
|
'border-cyan-500/80',
|
||||||
|
'border-orange-500/80',
|
||||||
|
'border-lime-500/80',
|
||||||
|
'border-indigo-500/80',
|
||||||
'border-red-500/80',
|
'border-red-500/80',
|
||||||
'border-sky-500/80',
|
'border-sky-500/80',
|
||||||
'border-emerald-500/80',
|
'border-emerald-500/80',
|
||||||
@@ -239,6 +261,16 @@ export default {
|
|||||||
},
|
},
|
||||||
bgColors () {
|
bgColors () {
|
||||||
return [
|
return [
|
||||||
|
'bg-red-100/80',
|
||||||
|
'bg-sky-100/80',
|
||||||
|
'bg-emerald-100/80',
|
||||||
|
'bg-yellow-100/80',
|
||||||
|
'bg-purple-100/80',
|
||||||
|
'bg-pink-100/80',
|
||||||
|
'bg-cyan-100/80',
|
||||||
|
'bg-orange-100/80',
|
||||||
|
'bg-lime-100/80',
|
||||||
|
'bg-indigo-100/80',
|
||||||
'bg-red-100/80',
|
'bg-red-100/80',
|
||||||
'bg-sky-100/80',
|
'bg-sky-100/80',
|
||||||
'bg-emerald-100/80',
|
'bg-emerald-100/80',
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
<IconPencil
|
<IconPencil
|
||||||
class="cursor-pointer flex-none opacity-0 group-hover/contenteditable-container:opacity-100 group-hover/contenteditable:opacity-100 align-middle peer-focus:hidden"
|
class="cursor-pointer flex-none opacity-0 group-hover/contenteditable-container:opacity-100 group-hover/contenteditable:opacity-100 align-middle peer-focus:hidden"
|
||||||
:style="iconInline ? {} : { right: -(1.1 * iconWidth) + 'px' }"
|
:style="iconInline ? {} : { right: -(1.1 * iconWidth) + 'px' }"
|
||||||
title="Edit"
|
:title="t('edit')"
|
||||||
:class="{ invisible: !editable, 'ml-1': !withRequired, 'absolute': !iconInline, 'inline align-bottom': iconInline }"
|
:class="{ invisible: !editable, 'ml-1': !withRequired, 'absolute': !iconInline, 'inline align-bottom': iconInline }"
|
||||||
:width="iconWidth"
|
:width="iconWidth"
|
||||||
:stroke-width="iconStrokeWidth"
|
:stroke-width="iconStrokeWidth"
|
||||||
@@ -44,6 +44,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
IconPencil
|
IconPencil
|
||||||
},
|
},
|
||||||
|
inject: ['t'],
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export default {
|
|||||||
return this.previewImagesIndex[i] || {
|
return this.previewImagesIndex[i] || {
|
||||||
metadata: lazyloadMetadata,
|
metadata: lazyloadMetadata,
|
||||||
id: Math.random().toString(),
|
id: Math.random().toString(),
|
||||||
url: this.basePreviewUrl + `/preview/${this.document.uuid}/${i}.jpg`
|
url: this.basePreviewUrl + `/preview/${this.document.signed_uuid || this.document.uuid}/${i}.jpg`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-if="field && !field.areas.length"
|
v-if="field && !field.areas.length"
|
||||||
title="Draw"
|
:title="t('draw')"
|
||||||
class="relative cursor-pointer text-transparent group-hover:text-base-content"
|
class="relative cursor-pointer text-transparent group-hover:text-base-content"
|
||||||
@click="$emit('set-draw', { field })"
|
@click="$emit('set-draw', { field })"
|
||||||
>
|
>
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
title="Settings"
|
:title="t('settings')"
|
||||||
class="cursor-pointer text-transparent group-hover:text-base-content"
|
class="cursor-pointer text-transparent group-hover:text-base-content"
|
||||||
>
|
>
|
||||||
<IconSettings
|
<IconSettings
|
||||||
@@ -142,6 +142,43 @@
|
|||||||
{{ t('format') }}
|
{{ t('format') }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="field.type === 'signature'"
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
:placeholder="t('format')"
|
||||||
|
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0"
|
||||||
|
@change="[field.preferences.format = $event.target.value, save()]"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
value="any"
|
||||||
|
:selected="!field.preferences?.format || field.preferences.format === 'any'"
|
||||||
|
>
|
||||||
|
{{ t('any') }}
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="drawn"
|
||||||
|
:selected="field.preferences?.format === 'drawn'"
|
||||||
|
>
|
||||||
|
{{ t('drawn') }}
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="typed"
|
||||||
|
:selected="field.preferences?.format === 'typed'"
|
||||||
|
>
|
||||||
|
{{ t('typed') }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<label
|
||||||
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>
|
||||||
|
{{ t('format') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<li
|
<li
|
||||||
v-if="field.type != 'phone'"
|
v-if="field.type != 'phone'"
|
||||||
@click.stop
|
@click.stop
|
||||||
@@ -156,6 +193,34 @@
|
|||||||
<span class="label-text">{{ t('required') }}</span>
|
<span class="label-text">{{ t('required') }}</span>
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
|
<li
|
||||||
|
v-if="field.type == 'checkbox'"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<label class="cursor-pointer py-1.5">
|
||||||
|
<input
|
||||||
|
v-model="field.default_value"
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-xs"
|
||||||
|
@update:model-value="[field.default_value = $event, field.readonly = $event, save()]"
|
||||||
|
>
|
||||||
|
<span class="label-text">{{ t('checked') }}</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
v-if="field.type == 'date'"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<label class="cursor-pointer py-1.5">
|
||||||
|
<input
|
||||||
|
v-model="field.readonly"
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-xs"
|
||||||
|
@update:model-value="[field.default_value = $event ? '{{date}}' : null, field.readonly = $event, save()]"
|
||||||
|
>
|
||||||
|
<span class="label-text">{{ t('set_signing_date') }}</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="field.type === 'text' && !defaultField"
|
v-if="field.type === 'text' && !defaultField"
|
||||||
@click.stop
|
@click.stop
|
||||||
@@ -217,7 +282,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
class="relative text-transparent group-hover:text-base-content pr-1"
|
class="relative text-transparent group-hover:text-base-content pr-1"
|
||||||
title="Remove"
|
:title="t('remove')"
|
||||||
@click="$emit('remove', field)"
|
@click="$emit('remove', field)"
|
||||||
>
|
>
|
||||||
<IconTrashX
|
<IconTrashX
|
||||||
@@ -256,7 +321,7 @@
|
|||||||
@blur="save"
|
@blur="save"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
title="Draw"
|
:title="t('draw')"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
@click.prevent="$emit('set-draw', { field, option })"
|
@click.prevent="$emit('set-draw', { field, option })"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="submitters.length < 10 && editable">
|
<li v-if="submitters.length < names.length && editable">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="flex px-2"
|
class="flex px-2"
|
||||||
@@ -145,7 +145,7 @@
|
|||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="flex px-2 group justify-between items-center"
|
class="flex px-2 group justify-between items-center"
|
||||||
:class="{ 'active': submitter === selectedSubmitter }"
|
:class="{ 'active': submitter === selectedSubmitter, 'py-0.5': submitters.length > 8 }"
|
||||||
@click.prevent="selectSubmitter(submitter)"
|
@click.prevent="selectSubmitter(submitter)"
|
||||||
>
|
>
|
||||||
<span class="py-1 flex items-center">
|
<span class="py-1 flex items-center">
|
||||||
@@ -163,7 +163,7 @@
|
|||||||
>
|
>
|
||||||
<div class="flex-col pr-1 hidden group-hover:flex -mt-1 h-0">
|
<div class="flex-col pr-1 hidden group-hover:flex -mt-1 h-0">
|
||||||
<button
|
<button
|
||||||
title="Up"
|
:title="t('up')"
|
||||||
class="relative w-2"
|
class="relative w-2"
|
||||||
style="font-size: 10px; margin-bottom: -4px"
|
style="font-size: 10px; margin-bottom: -4px"
|
||||||
@click.prevent.stop="[move(submitter, -1), $refs.label.focus()] "
|
@click.prevent.stop="[move(submitter, -1), $refs.label.focus()] "
|
||||||
@@ -171,7 +171,7 @@
|
|||||||
▲
|
▲
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
title="Down"
|
:title="t('down')"
|
||||||
class="relative w-2"
|
class="relative w-2"
|
||||||
style="font-size: 10px; margin-top: -4px"
|
style="font-size: 10px; margin-top: -4px"
|
||||||
@click.prevent.stop="[move(submitter, 1), $refs.label.focus()] "
|
@click.prevent.stop="[move(submitter, 1), $refs.label.focus()] "
|
||||||
@@ -189,7 +189,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="submitters.length < 10 && editable && allowAddNew">
|
<li v-if="submitters.length < names.length && editable && allowAddNew">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="flex px-2"
|
class="flex px-2"
|
||||||
@@ -268,6 +268,16 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
colors () {
|
colors () {
|
||||||
return [
|
return [
|
||||||
|
'bg-red-500',
|
||||||
|
'bg-sky-500',
|
||||||
|
'bg-emerald-500',
|
||||||
|
'bg-yellow-300',
|
||||||
|
'bg-purple-600',
|
||||||
|
'bg-pink-500',
|
||||||
|
'bg-cyan-500',
|
||||||
|
'bg-orange-500',
|
||||||
|
'bg-lime-500',
|
||||||
|
'bg-indigo-500',
|
||||||
'bg-red-500',
|
'bg-red-500',
|
||||||
'bg-sky-500',
|
'bg-sky-500',
|
||||||
'bg-emerald-500',
|
'bg-emerald-500',
|
||||||
@@ -291,7 +301,17 @@ export default {
|
|||||||
this.t('seventh_party'),
|
this.t('seventh_party'),
|
||||||
this.t('eighth_party'),
|
this.t('eighth_party'),
|
||||||
this.t('ninth_party'),
|
this.t('ninth_party'),
|
||||||
this.t('tenth_party')
|
this.t('tenth_party'),
|
||||||
|
this.t('eleventh_party'),
|
||||||
|
this.t('twelfth_party'),
|
||||||
|
this.t('thirteenth_party'),
|
||||||
|
this.t('fourteenth_party'),
|
||||||
|
this.t('fifteenth_party'),
|
||||||
|
this.t('sixteenth_party'),
|
||||||
|
this.t('seventeenth_party'),
|
||||||
|
this.t('eighteenth_party'),
|
||||||
|
this.t('nineteenth_party'),
|
||||||
|
this.t('twentieth_party')
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
selectedSubmitter () {
|
selectedSubmitter () {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
@click="closeDropdown"
|
@click="closeDropdown"
|
||||||
>
|
>
|
||||||
<template
|
<template
|
||||||
v-for="(icon, type) in fieldIcons"
|
v-for="(icon, type) in fieldIconsSorted"
|
||||||
:key="type"
|
:key="type"
|
||||||
>
|
>
|
||||||
<li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')">
|
<li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')">
|
||||||
@@ -123,6 +123,17 @@ export default {
|
|||||||
payment: IconCreditCard,
|
payment: IconCreditCard,
|
||||||
phone: IconPhoneCheck
|
phone: IconPhoneCheck
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
fieldIconsSorted () {
|
||||||
|
if (this.fieldTypes.length) {
|
||||||
|
return this.fieldTypes.reduce((acc, type) => {
|
||||||
|
acc[type] = this.fieldIcons[type]
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
} else {
|
||||||
|
return this.fieldIcons
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
class="grid grid-cols-3 gap-1 pb-2"
|
class="grid grid-cols-3 gap-1 pb-2"
|
||||||
>
|
>
|
||||||
<template
|
<template
|
||||||
v-for="(icon, type) in fieldIcons"
|
v-for="(icon, type) in fieldIconsSorted"
|
||||||
:key="type"
|
:key="type"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@@ -213,6 +213,17 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
fieldNames: FieldType.computed.fieldNames,
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
fieldIcons: FieldType.computed.fieldIcons,
|
fieldIcons: FieldType.computed.fieldIcons,
|
||||||
|
fieldIconsSorted () {
|
||||||
|
if (this.fieldTypes.length) {
|
||||||
|
return this.fieldTypes.reduce((acc, type) => {
|
||||||
|
acc[type] = this.fieldIcons[type]
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
} else {
|
||||||
|
return this.fieldIcons
|
||||||
|
}
|
||||||
|
},
|
||||||
submitterFields () {
|
submitterFields () {
|
||||||
return this.fields.filter((f) => f.submitter_uuid === this.selectedSubmitter.uuid)
|
return this.fields.filter((f) => f.submitter_uuid === this.selectedSubmitter.uuid)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
const en = {
|
const en = {
|
||||||
are_you_sure: 'Are you sure?',
|
are_you_sure: 'Are you sure?',
|
||||||
sign_yourself: 'Sign Yourself',
|
sign_yourself: 'Sign Yourself',
|
||||||
|
set_signing_date: 'Set signing date',
|
||||||
send: 'Send',
|
send: 'Send',
|
||||||
|
remove: 'Remove',
|
||||||
|
edit: 'Edit',
|
||||||
|
settings: 'Settings',
|
||||||
|
up: 'Up',
|
||||||
|
down: 'Down',
|
||||||
|
checked: 'Checked',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
|
any: 'Any',
|
||||||
|
drawn: 'Drawn',
|
||||||
|
typed: 'Typed',
|
||||||
draw_field_on_the_document: 'Draw {field} field on the document',
|
draw_field_on_the_document: 'Draw {field} field on the document',
|
||||||
click_to_upload: 'Click to upload',
|
click_to_upload: 'Click to upload',
|
||||||
or_drag_and_drop_files: 'or drag and drop files',
|
or_drag_and_drop_files: 'or drag and drop files',
|
||||||
@@ -30,6 +40,17 @@ const en = {
|
|||||||
eighth_party: 'Eighth Party',
|
eighth_party: 'Eighth Party',
|
||||||
ninth_party: 'Ninth Party',
|
ninth_party: 'Ninth Party',
|
||||||
tenth_party: 'Tenth Party',
|
tenth_party: 'Tenth Party',
|
||||||
|
eleventh_party: 'Eleventh Party',
|
||||||
|
twelfth_party: 'Twelfth Party',
|
||||||
|
thirteenth_party: 'Thirteenth Party',
|
||||||
|
fourteenth_party: 'Fourteenth Party',
|
||||||
|
fifteenth_party: 'Fifteenth Party',
|
||||||
|
sixteenth_party: 'Sixteenth Party',
|
||||||
|
seventeenth_party: 'Seventeenth Party',
|
||||||
|
eighteenth_party: 'Eighteenth Party',
|
||||||
|
nineteenth_party: 'Nineteenth Party',
|
||||||
|
twentieth_party: 'Twentieth Party',
|
||||||
|
draw: 'Draw',
|
||||||
add: 'Add',
|
add: 'Add',
|
||||||
or_add_field_without_drawing: 'Or add field without drawing',
|
or_add_field_without_drawing: 'Or add field without drawing',
|
||||||
text: 'Text',
|
text: 'Text',
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
title="Settings"
|
:title="t('settings')"
|
||||||
class="cursor-pointer text-transparent group-hover:text-base-content"
|
class="cursor-pointer text-transparent group-hover:text-base-content"
|
||||||
>
|
>
|
||||||
<IconSettings
|
<IconSettings
|
||||||
@@ -167,7 +167,7 @@ export default {
|
|||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
IconBrandStripe
|
IconBrandStripe
|
||||||
},
|
},
|
||||||
inject: ['backgroundColor', 'save', 'currencies'],
|
inject: ['backgroundColor', 'save', 'currencies', 't'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|||||||
@@ -26,12 +26,14 @@ class ProcessSubmitterCompletionJob < ApplicationJob
|
|||||||
|
|
||||||
user = submission.created_by_user || submitter.template.author
|
user = submission.created_by_user || submitter.template.author
|
||||||
|
|
||||||
if submitter.template.account.users.exists?(id: user.id) &&
|
if submitter.account.users.exists?(id: user.id) && submission.preferences['send_email'] != false
|
||||||
submission.preferences['send_email'] != false
|
if submitter.submission.submitters.map(&:email).exclude?(user.email) &&
|
||||||
SubmitterMailer.completed_email(submitter, user).deliver_later!
|
user.user_configs.find_by(key: UserConfig::RECEIVE_COMPLETED_EMAIL)&.value != false
|
||||||
|
SubmitterMailer.completed_email(submitter, user).deliver_later!
|
||||||
|
end
|
||||||
|
|
||||||
bcc = submission.preferences['bcc_completed'].presence ||
|
bcc = submission.preferences['bcc_completed'].presence ||
|
||||||
submission.template.account.account_configs
|
submission.account.account_configs
|
||||||
.find_by(key: AccountConfig::BCC_EMAILS)&.value.presence
|
.find_by(key: AccountConfig::BCC_EMAILS)&.value.presence
|
||||||
|
|
||||||
bcc.to_s.scan(User::EMAIL_REGEXP).each do |to|
|
bcc.to_s.scan(User::EMAIL_REGEXP).each do |to|
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
class SendFormCompletedWebhookRequestJob < ApplicationJob
|
class SendFormCompletedWebhookRequestJob < ApplicationJob
|
||||||
USER_AGENT = 'DocuSeal.co Webhook'
|
USER_AGENT = 'DocuSeal.co Webhook'
|
||||||
|
|
||||||
def perform(submitter)
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
|
def perform(submitter, params = {})
|
||||||
|
attempt = params[:attempt].to_i
|
||||||
config = Accounts.load_webhook_configs(submitter.submission.account)
|
config = Accounts.load_webhook_configs(submitter.submission.account)
|
||||||
|
|
||||||
return if config.blank? || config.value.blank?
|
return if config.blank? || config.value.blank?
|
||||||
@@ -12,13 +15,22 @@ class SendFormCompletedWebhookRequestJob < ApplicationJob
|
|||||||
|
|
||||||
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
||||||
|
|
||||||
Faraday.post(config.value,
|
resp = Faraday.post(config.value,
|
||||||
{
|
{
|
||||||
event_type: 'form.completed',
|
event_type: 'form.completed',
|
||||||
timestamp: Time.current,
|
timestamp: Time.current,
|
||||||
data: Submitters::SerializeForWebhook.call(submitter)
|
data: Submitters::SerializeForWebhook.call(submitter)
|
||||||
}.to_json,
|
}.to_json,
|
||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
'User-Agent' => USER_AGENT)
|
'User-Agent' => USER_AGENT)
|
||||||
|
|
||||||
|
if resp.status.to_i >= 400 && attempt <= MAX_ATTEMPTS &&
|
||||||
|
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
||||||
|
SendFormCompletedWebhookRequestJob.set(wait: (2**attempt).minutes)
|
||||||
|
.perform_later(submitter, {
|
||||||
|
attempt: attempt + 1,
|
||||||
|
last_status: resp.status.to_i
|
||||||
|
})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -3,20 +3,32 @@
|
|||||||
class SendFormStartedWebhookRequestJob < ApplicationJob
|
class SendFormStartedWebhookRequestJob < ApplicationJob
|
||||||
USER_AGENT = 'DocuSeal.co Webhook'
|
USER_AGENT = 'DocuSeal.co Webhook'
|
||||||
|
|
||||||
def perform(submitter)
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
|
def perform(submitter, params = {})
|
||||||
|
attempt = params[:attempt].to_i
|
||||||
config = Accounts.load_webhook_configs(submitter.submission.account)
|
config = Accounts.load_webhook_configs(submitter.submission.account)
|
||||||
|
|
||||||
return if config.blank? || config.value.blank?
|
return if config.blank? || config.value.blank?
|
||||||
|
|
||||||
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
||||||
|
|
||||||
Faraday.post(config.value,
|
resp = Faraday.post(config.value,
|
||||||
{
|
{
|
||||||
event_type: 'form.started',
|
event_type: 'form.started',
|
||||||
timestamp: Time.current,
|
timestamp: Time.current,
|
||||||
data: Submitters::SerializeForWebhook.call(submitter)
|
data: Submitters::SerializeForWebhook.call(submitter)
|
||||||
}.to_json,
|
}.to_json,
|
||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
'User-Agent' => USER_AGENT)
|
'User-Agent' => USER_AGENT)
|
||||||
|
|
||||||
|
if resp.status.to_i >= 400 && attempt <= MAX_ATTEMPTS &&
|
||||||
|
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
||||||
|
SendFormStartedWebhookRequestJob.set(wait: (2**attempt).minutes)
|
||||||
|
.perform_later(submitter, {
|
||||||
|
attempt: attempt + 1,
|
||||||
|
last_status: resp.status.to_i
|
||||||
|
})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -3,20 +3,32 @@
|
|||||||
class SendFormViewedWebhookRequestJob < ApplicationJob
|
class SendFormViewedWebhookRequestJob < ApplicationJob
|
||||||
USER_AGENT = 'DocuSeal.co Webhook'
|
USER_AGENT = 'DocuSeal.co Webhook'
|
||||||
|
|
||||||
def perform(submitter)
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
|
def perform(submitter, params = {})
|
||||||
|
attempt = params[:attempt].to_i
|
||||||
config = Accounts.load_webhook_configs(submitter.submission.account)
|
config = Accounts.load_webhook_configs(submitter.submission.account)
|
||||||
|
|
||||||
return if config.blank? || config.value.blank?
|
return if config.blank? || config.value.blank?
|
||||||
|
|
||||||
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
||||||
|
|
||||||
Faraday.post(config.value,
|
resp = Faraday.post(config.value,
|
||||||
{
|
{
|
||||||
event_type: 'form.viewed',
|
event_type: 'form.viewed',
|
||||||
timestamp: Time.current,
|
timestamp: Time.current,
|
||||||
data: Submitters::SerializeForWebhook.call(submitter)
|
data: Submitters::SerializeForWebhook.call(submitter)
|
||||||
}.to_json,
|
}.to_json,
|
||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
'User-Agent' => USER_AGENT)
|
'User-Agent' => USER_AGENT)
|
||||||
|
|
||||||
|
if resp.status.to_i >= 400 && attempt <= MAX_ATTEMPTS &&
|
||||||
|
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
||||||
|
SendFormViewedWebhookRequestJob.set(wait: (2**attempt).minutes)
|
||||||
|
.perform_later(submitter, {
|
||||||
|
attempt: attempt + 1,
|
||||||
|
last_status: resp.status.to_i
|
||||||
|
})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -30,6 +30,10 @@ class Account < ApplicationRecord
|
|||||||
foreign_key: :linked_account_id,
|
foreign_key: :linked_account_id,
|
||||||
class_name: 'AccountLinkedAccount',
|
class_name: 'AccountLinkedAccount',
|
||||||
inverse_of: :linked_account
|
inverse_of: :linked_account
|
||||||
|
has_many :linked_account_accounts, dependent: :destroy,
|
||||||
|
foreign_key: :linked_account_id,
|
||||||
|
class_name: 'AccountLinkedAccount',
|
||||||
|
inverse_of: :linked_account
|
||||||
has_many :linked_accounts, through: :account_linked_accounts
|
has_many :linked_accounts, through: :account_linked_accounts
|
||||||
has_many :testing_accounts, through: :account_testing_accounts, source: :linked_account
|
has_many :testing_accounts, through: :account_testing_accounts, source: :linked_account
|
||||||
has_many :active_users, -> { active }, dependent: :destroy,
|
has_many :active_users, -> { active }, dependent: :destroy,
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class Submission < ApplicationRecord
|
|||||||
def audit_trail_url
|
def audit_trail_url
|
||||||
return if audit_trail.blank?
|
return if audit_trail.blank?
|
||||||
|
|
||||||
Rails.application.routes.url_helpers.rails_storage_proxy_url(audit_trail, **Docuseal.default_url_options)
|
ActiveStorage::Blob.proxy_url(audit_trail.blob)
|
||||||
end
|
end
|
||||||
alias audit_log_url audit_trail_url
|
alias audit_log_url audit_trail_url
|
||||||
end
|
end
|
||||||
@@ -46,7 +46,8 @@ class SubmissionEvent < ApplicationRecord
|
|||||||
phone_verified: 'phone_verified',
|
phone_verified: 'phone_verified',
|
||||||
start_form: 'start_form',
|
start_form: 'start_form',
|
||||||
view_form: 'view_form',
|
view_form: 'view_form',
|
||||||
complete_form: 'complete_form'
|
complete_form: 'complete_form',
|
||||||
|
api_complete_form: 'api_complete_form'
|
||||||
}, scope: false
|
}, scope: false
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#
|
#
|
||||||
class UserConfig < ApplicationRecord
|
class UserConfig < ApplicationRecord
|
||||||
SIGNATURE_KEY = 'signature'
|
SIGNATURE_KEY = 'signature'
|
||||||
|
RECEIVE_COMPLETED_EMAIL = 'receive_completed_email'
|
||||||
|
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
|
|
||||||
|
|||||||
@@ -123,26 +123,24 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if !Docuseal.multitenant? %>
|
<% account_config = AccountConfig.where(account: current_account, key: AccountConfig::ESIGNING_PREFERENCE_KEY).first_or_initialize(value: 'multiple') %>
|
||||||
<% account_config = AccountConfig.where(account: current_account, key: AccountConfig::ESIGNING_PREFERENCE_KEY).first_or_initialize(value: 'multiple') %>
|
<% if can?(:manage, account_config) %>
|
||||||
<% if can?(:manage, account_config) %>
|
<div class="px-1 mt-8 max-w-xl">
|
||||||
<div class="px-1 mt-8 max-w-xl">
|
<div class="flex justify-between items-end mb-4 mt-8">
|
||||||
<div class="flex justify-between items-end mb-4 mt-8">
|
<h2 class="text-3xl font-bold">Preferences</h2>
|
||||||
<h2 class="text-3xl font-bold">Preferences</h2>
|
|
||||||
</div>
|
|
||||||
<% 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>
|
|
||||||
Apply multiple PDF digital signatures in the document per each signer
|
|
||||||
</span>
|
|
||||||
<%= f.check_box :value, { class: 'toggle', checked: account_config.value == 'multiple', onchange: 'this.form.requestSubmit()' }, 'multiple', 'single' %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% 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>
|
||||||
|
Apply multiple PDF digital signatures in the document per each signer
|
||||||
|
</span>
|
||||||
|
<%= f.check_box :value, { class: 'toggle', checked: account_config.value == 'multiple', onchange: 'this.form.requestSubmit()' }, 'multiple', 'single' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1 +1 @@
|
|||||||
<%= render 'docuseal_logo' %>
|
<%= render 'start_form/docuseal_logo' %>
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<div dir="auto" class="text-center text-2xl font-semibold">
|
<div dir="auto" class="text-center text-2xl font-semibold">
|
||||||
<%= t('form_has_been_submitted_already') %>
|
<%= t('form_has_been_submitted_already') %>
|
||||||
</div>
|
</div>
|
||||||
<% if Accounts.can_send_emails?(@submitter.submission.template.account) %>
|
<% if Accounts.can_send_emails?(@submitter.account) %>
|
||||||
<div>
|
<div>
|
||||||
<%= button_to button_title(title: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'base-button w-full' %>
|
<%= button_to button_title(title: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'base-button w-full' %>
|
||||||
</div>
|
</div>
|
||||||
@@ -31,4 +31,4 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%= render 'shared/attribution', link_path: '/start' %>
|
<%= render 'shared/attribution', link_path: '/start', account: @submitter.account %>
|
||||||
@@ -37,4 +37,4 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%= render 'shared/attribution', link_path: '/start' %>
|
<%= render 'shared/attribution', link_path: '/start', account: @template.account %>
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<div class="space-x-3 flex items-center">
|
<div class="space-x-3 flex items-center">
|
||||||
<% if @submission.audit_trail.present? %>
|
<% if @submission.audit_trail.present? %>
|
||||||
<a href="<%= rails_blob_path(@submission.audit_trail) %>" class="white-button" target="_blank">
|
<a href="<%= ActiveStorage::Blob.proxy_url(@submission.audit_trail.blob) %>" class="white-button" target="_blank">
|
||||||
<%= svg_icon('external_link', class: 'w-6 h-6') %>
|
<%= svg_icon('external_link', class: 'w-6 h-6') %>
|
||||||
<span class="hidden md:inline">
|
<span class="hidden md:inline">
|
||||||
Audit Log
|
Audit Log
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
<% (@submission.template_schema || @submission.template.schema).each do |item| %>
|
<% (@submission.template_schema || @submission.template.schema).each do |item| %>
|
||||||
<% document = @submission.template_schema_documents.find { |a| item['attachment_uuid'] == a.uuid } %>
|
<% document = @submission.template_schema_documents.find { |a| item['attachment_uuid'] == a.uuid } %>
|
||||||
<a href="#<%= "page-#{document.uuid}-0" %>" onclick="[event.preventDefault(), window[event.target.closest('a').href.split('#')[1]].scrollIntoView({ behavior: 'smooth', block: 'start' })]" class="block cursor-pointer">
|
<a href="#<%= "page-#{document.uuid}-0" %>" onclick="[event.preventDefault(), window[event.target.closest('a').href.split('#')[1]].scrollIntoView({ behavior: 'smooth', block: 'start' })]" class="block cursor-pointer">
|
||||||
<img src="<%= document.preview_images.first.url %>" width="<%= document.preview_images.first.metadata['width'] %>" height="<%= document.preview_images.first.metadata['height'] %>" class="rounded border" loading="lazy">
|
<img src="<%= Docuseal::URL_CACHE.fetch([document.id, document.uuid, 0].join(':'), expires_in: 10.minutes) { document.preview_images.first.url } %>" width="<%= document.preview_images.first.metadata['width'] %>" height="<%= document.preview_images.first.metadata['height'] %>" class="rounded border" loading="lazy">
|
||||||
<div class="pb-2 pt-1.5 text-center" dir="auto">
|
<div class="pb-2 pt-1.5 text-center" dir="auto">
|
||||||
<%= item['name'].presence || document.filename.base %>
|
<%= item['name'].presence || document.filename.base %>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,9 +53,9 @@
|
|||||||
<% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %>
|
<% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %>
|
||||||
<% lazyload_metadata = document.preview_images.first.metadata %>
|
<% lazyload_metadata = document.preview_images.first.metadata %>
|
||||||
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
|
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
|
||||||
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.uuid, "#{index}.jpg")) %>
|
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_uuid, "#{index}.jpg")) %>
|
||||||
<div id="<%= "page-#{document.uuid}-#{index}" %>" class="relative">
|
<div id="<%= "page-#{document.uuid}-#{index}" %>" class="relative">
|
||||||
<img loading="lazy" src="<%= page.url %>" width="<%= page.metadata['width'] %>" class="border rounded mb-4" height="<%= page.metadata['height'] %>">
|
<img loading="lazy" src="<%= Docuseal::URL_CACHE.fetch([document.id, document.uuid, index].join(':'), expires_in: 10.minutes) { page.url } %>" width="<%= page.metadata['width'] %>" class="border rounded mb-4" height="<%= page.metadata['height'] %>">
|
||||||
<div class="top-0 bottom-0 left-0 right-0 absolute">
|
<div class="top-0 bottom-0 left-0 right-0 absolute">
|
||||||
<% document_annots_index[index]&.each do |annot| %>
|
<% document_annots_index[index]&.each do |annot| %>
|
||||||
<%= render 'submissions/annotation', annot: %>
|
<%= render 'submissions/annotation', annot: %>
|
||||||
@@ -144,10 +144,10 @@
|
|||||||
<div dir="auto">
|
<div dir="auto">
|
||||||
<% if field['type'].in?(%w[signature initials]) %>
|
<% if field['type'].in?(%w[signature initials]) %>
|
||||||
<div class="w-full bg-base-300 py-1">
|
<div class="w-full bg-base-300 py-1">
|
||||||
<img class="object-contain mx-auto" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
|
<img class="object-contain mx-auto" style="max-height: <%= field['type'] == 'signature' ? 100 : 50 %>px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
|
||||||
</div>
|
</div>
|
||||||
<% elsif field['type'].in?(['image', 'stamp']) %>
|
<% elsif field['type'].in?(['image', 'stamp']) %>
|
||||||
<img class="object-contain mx-auto max-h-28" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
|
<img class="object-contain mx-auto max-h-28" style="max-height: 200px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
|
||||||
<% elsif field['type'] == 'file' || field['type'] == 'payment' %>
|
<% elsif field['type'] == 'file' || field['type'] == 'payment' %>
|
||||||
<div class="flex flex-col justify-center">
|
<div class="flex flex-col justify-center">
|
||||||
<% Array.wrap(value).each do |val| %>
|
<% Array.wrap(value).each do |val| %>
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
|
||||||
|
<div class="space-y-6 mx-auto">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<% if Docuseal.multitenant? %>
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<%= render 'start_form/docuseal_logo' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="mr-3">
|
||||||
|
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p dir="auto" class="text-lg font-bold mb-1"><%= @submission.template.name %></p>
|
||||||
|
<% if submitter = @submission.submitters.map(&:completed_at).max %>
|
||||||
|
<p dir="auto" class="text-sm"><%= t('signed_on_time', time: l(submitter.to_date, format: :long)) %></p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% if Docuseal.multitenant? %>
|
||||||
|
<div>
|
||||||
|
<%= link_to 'Create free account', registration_path, class: 'white-button w-full' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<%= render 'shared/attribution', link_path: '/start', account: @submission.account %>
|
||||||
@@ -11,11 +11,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div dir="auto">
|
<div dir="auto">
|
||||||
<p class="text-lg font-bold mb-1"><%= @submitter.submission.template.name %></p>
|
<p class="text-lg font-bold mb-1"><%= @submitter.submission.template.name %></p>
|
||||||
<p class="text-sm"><%= t('form_has_been_deleted_by_html', name: @submitter.submission.template.account.name) %></p>
|
<p class="text-sm"><%= t('form_has_been_deleted_by_html', name: @submitter.account.name) %></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%= render 'shared/attribution', link_path: '/start' %>
|
<%= render 'shared/attribution', link_path: '/start', account: @submitter.account %>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<% if Accounts.can_send_emails?(@submitter.submission.template.account) && @submitter.email.present? %>
|
<% if Accounts.can_send_emails?(@submitter.account) && @submitter.email.present? %>
|
||||||
<%= button_to button_title(title: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
|
<%= button_to button_title(title: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
|
||||||
<% if @submitter.submission.template.submitters.size != 1 %>
|
<% if @submitter.submission.template.submitters.size != 1 %>
|
||||||
<div class="divider uppercase"><%= t('or') %></div>
|
<div class="divider uppercase"><%= t('or') %></div>
|
||||||
@@ -25,18 +25,20 @@
|
|||||||
<div class="py-2"></div>
|
<div class="py-2"></div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<download-button data-src="<%= submitter_download_index_path(@submitter.slug) %>" class="base-button w-full">
|
<% if @submitter.completed_at > 15.minutes.ago || (current_user && current_user.account.submitters.exists?(id: @submitter.id)) %>
|
||||||
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
<download-button data-src="<%= submitter_download_index_path(@submitter.slug) %>" class="base-button w-full">
|
||||||
<%= svg_icon('download', class: 'w-6 h-6') %>
|
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
||||||
<span><%= t('download_documents') %></span>
|
<%= svg_icon('download', class: 'w-6 h-6') %>
|
||||||
</span>
|
<span><%= t('download_documents') %></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>
|
||||||
|
</download-button>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% if @submitter.submission.template.submitters.size == 1 && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false %>
|
<% if @submitter.submission.template.submitters.size == 1 && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false && !@submitter.template.archived_at? %>
|
||||||
<div class="divider uppercase"><%= t('or') %></div>
|
<div class="divider uppercase"><%= t('or') %></div>
|
||||||
<div>
|
<div>
|
||||||
<%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@submitter.submission.template.slug), params: { submitter: { email: @submitter.email, phone: @submitter.phone, name: @submitter.name }, resubmit: @submitter.slug }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
|
<%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@submitter.submission.template.slug), params: { submitter: { email: @submitter.email, phone: @submitter.phone, name: @submitter.name }, resubmit: @submitter.slug }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
|
||||||
@@ -44,4 +46,4 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%= render 'shared/attribution', link_path: '/start' %>
|
<%= render 'shared/attribution', link_path: '/start', account: @submitter.account %>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %>
|
<% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %>
|
||||||
<% lazyload_metadata = document.preview_images.last.metadata %>
|
<% lazyload_metadata = document.preview_images.last.metadata %>
|
||||||
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
|
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
|
||||||
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.uuid, "#{index}.jpg")) %>
|
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_uuid, "#{index}.jpg")) %>
|
||||||
<div class="relative my-4 shadow-md">
|
<div class="relative my-4 shadow-md">
|
||||||
<img loading="lazy" src="<%= page.url %>" width="<%= page.metadata['width'] %>" height="<%= page.metadata['height'] %>">
|
<img loading="lazy" src="<%= page.url %>" width="<%= page.metadata['width'] %>" height="<%= page.metadata['height'] %>">
|
||||||
<div id="page-<%= [document.uuid, index].join('-') %>" class="top-0 bottom-0 left-0 right-0 absolute">
|
<div id="page-<%= [document.uuid, index].join('-') %>" class="top-0 bottom-0 left-0 right-0 absolute">
|
||||||
@@ -24,13 +24,13 @@
|
|||||||
<% value = values[field['uuid']].presence || (field['readonly'] ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submitter.submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
|
<% value = values[field['uuid']].presence || (field['readonly'] ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submitter.submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
|
||||||
<% next if value.blank? %>
|
<% next if value.blank? %>
|
||||||
<% next if !field['readonly'] && field['submitter_uuid'] == @submitter.uuid %>
|
<% next if !field['readonly'] && field['submitter_uuid'] == @submitter.uuid %>
|
||||||
<%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submitter.submission.template.account.locale %>
|
<%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submitter.account.locale %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render 'shared/attribution', link_path: '/start' %>
|
<%= render 'shared/attribution', link_path: '/start', account: @submitter.account %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,11 +12,16 @@
|
|||||||
</a>
|
</a>
|
||||||
<folder-autocomplete class="flex justify-between w-full">
|
<folder-autocomplete class="flex justify-between w-full">
|
||||||
<input id="folder_name" placeholder="Folder Name" type="text" class="w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer" name="folder_name" value="<%= params[:folder_name].presence || @base_template&.folder&.name || TemplateFolder::DEFAULT_NAME %>" onblur="window.folder_name.value = window.folder_name.value || 'Default'" autocomplete="off">
|
<input id="folder_name" placeholder="Folder Name" type="text" class="w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer" name="folder_name" value="<%= params[:folder_name].presence || @base_template&.folder&.name || TemplateFolder::DEFAULT_NAME %>" onblur="window.folder_name.value = window.folder_name.value || 'Default'" autocomplete="off">
|
||||||
<a href="#" onclick="[event.preventDefault(), window.folder_name.value = '', window.folder_name.focus()]" class="shrink-0 link peer-focus:hidden mr-1.5">
|
<a href="#" onclick="[event.preventDefault(), window.folder_name.value = '', window.folder_name.focus()]" class="shrink-0 link peer-focus:hidden mr-1.5">
|
||||||
Change Folder
|
Change Folder
|
||||||
</a>
|
</a>
|
||||||
</folder-autocomplete>
|
</folder-autocomplete>
|
||||||
</div>
|
</div>
|
||||||
|
<% if @base_template && (can?(:manage, :tenants) || true_user != current_user) && true_user.account.account_linked_accounts.exists?(account_type: :linked) %>
|
||||||
|
<div class="form-control mb-4">
|
||||||
|
<%= select_tag :account_id, options_for_select([true_user.account, *true_user.account.linked_accounts.where.not(id: true_user.account.testing_accounts)].map { |e| [e.name, e.id] }, current_account.id), required: true, class: 'base-select' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.button button_title(title: @base_template ? 'Submit' : 'Create', disabled_with: 'Creating'), class: 'base-button' %>
|
<%= f.button button_title(title: @base_template ? 'Submit' : 'Create', disabled_with: 'Creating'), class: 'base-button' %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ module DocuSeal
|
|||||||
|
|
||||||
config.active_storage.routes_prefix = ''
|
config.active_storage.routes_prefix = ''
|
||||||
|
|
||||||
|
config.active_storage.draw_routes = ENV['MULTITENANT'] != 'true'
|
||||||
|
|
||||||
config.i18n.available_locales = %i[en en-US en-GB es-ES fr-FR pt-PT de-DE es it de fr pl uk cs pt he nl]
|
config.i18n.available_locales = %i[en en-US en-GB es-ES fr-FR pt-PT de-DE es it de fr pl uk cs pt he nl]
|
||||||
config.i18n.fallbacks = [:en]
|
config.i18n.fallbacks = [:en]
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ Rails.application.configure do
|
|||||||
end
|
end
|
||||||
|
|
||||||
config.active_storage.resolve_model_to_route = :rails_storage_proxy if ENV['ACTIVE_STORAGE_PUBLIC'] != 'true'
|
config.active_storage.resolve_model_to_route = :rails_storage_proxy if ENV['ACTIVE_STORAGE_PUBLIC'] != 'true'
|
||||||
|
config.active_storage.service_urls_expire_in = 15.minutes
|
||||||
|
|
||||||
# Mount Action Cable outside main process or domain.
|
# Mount Action Cable outside main process or domain.
|
||||||
# config.action_cable.mount_path = nil
|
# config.action_cable.mount_path = nil
|
||||||
@@ -107,7 +108,7 @@ Rails.application.configure do
|
|||||||
logger.formatter = config.log_formatter
|
logger.formatter = config.log_formatter
|
||||||
config.logger = ActiveSupport::TaggedLogging.new(logger)
|
config.logger = ActiveSupport::TaggedLogging.new(logger)
|
||||||
|
|
||||||
encryption_secret = Digest::SHA256.hexdigest(ENV['SECRET_KEY_BASE'].to_s)
|
encryption_secret = ENV['ENCRYPTION_SECRET'].presence || Digest::SHA256.hexdigest(ENV['SECRET_KEY_BASE'].to_s)
|
||||||
|
|
||||||
config.active_record.encryption = {
|
config.active_record.encryption = {
|
||||||
primary_key: encryption_secret.first(32),
|
primary_key: encryption_secret.first(32),
|
||||||
@@ -121,12 +122,26 @@ Rails.application.configure do
|
|||||||
config.active_record.dump_schema_after_migration = false
|
config.active_record.dump_schema_after_migration = false
|
||||||
|
|
||||||
config.lograge.enabled = true
|
config.lograge.enabled = true
|
||||||
config.lograge.formatter = Lograge::Formatters::Json.new
|
|
||||||
config.lograge.base_controller_class = ['ActionController::API', 'ActionController::Base']
|
config.lograge.base_controller_class = ['ActionController::API', 'ActionController::Base']
|
||||||
|
|
||||||
config.lograge.custom_payload do |controller|
|
if ENV['MULTITENANT'] == 'true'
|
||||||
{
|
config.lograge.formatter = ->(data) { data.except(:path, :location).to_json }
|
||||||
fwd: controller.request.ip
|
|
||||||
}
|
config.lograge.custom_payload do |controller|
|
||||||
|
{
|
||||||
|
fwd: controller.request.ip.to_s[/\A\d+\.(.*)/, 1],
|
||||||
|
params: controller.request.params&.slice(:id),
|
||||||
|
host: controller.request.host,
|
||||||
|
uid: controller.instance_variable_get(:@current_user).try(:id)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
config.lograge.formatter = Lograge::Formatters::Json.new
|
||||||
|
|
||||||
|
config.lograge.custom_payload do |controller|
|
||||||
|
{
|
||||||
|
fwd: controller.request.ip
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -5,8 +5,43 @@ ActiveSupport.on_load(:active_storage_attachment) do
|
|||||||
|
|
||||||
has_many_attached :preview_images
|
has_many_attached :preview_images
|
||||||
|
|
||||||
|
def signed_uuid
|
||||||
|
@signed_uuid ||= ApplicationRecord.signed_id_verifier.generate(uuid, expires_in: 6.hours, purpose: :attachment)
|
||||||
|
end
|
||||||
|
|
||||||
def preview_image_url
|
def preview_image_url
|
||||||
preview_images.joins(:blob).find_by(blob: { filename: '0.jpg' })&.url
|
first_page = preview_images.joins(:blob).find_by(blob: { filename: '0.jpg' })
|
||||||
|
|
||||||
|
return unless first_page
|
||||||
|
|
||||||
|
ActiveStorage::Blob.proxy_url(first_page.blob)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ActiveSupport.on_load(:active_storage_blob) do
|
||||||
|
attribute :uuid, :string, default: -> { SecureRandom.uuid }
|
||||||
|
|
||||||
|
def self.proxy_url(blob, expires_in: nil)
|
||||||
|
Rails.application.routes.url_helpers.blobs_proxy_url(
|
||||||
|
signed_uuid: blob.signed_uuid(expires_in:), filename: blob.filename,
|
||||||
|
**Docuseal.default_url_options
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def uuid
|
||||||
|
super || begin
|
||||||
|
new_uuid = SecureRandom.uuid
|
||||||
|
update_columns(uuid: new_uuid)
|
||||||
|
new_uuid
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def signed_uuid(expires_in: nil)
|
||||||
|
ApplicationRecord.signed_id_verifier.generate([uuid, 'blob'], expires_in:)
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete
|
||||||
|
service.delete(key)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ Devise.setup do |config|
|
|||||||
# This can reduce the time taken to boot the app but if your application
|
# This can reduce the time taken to boot the app but if your application
|
||||||
# requires the Devise mappings to be loaded during boot time the application
|
# requires the Devise mappings to be loaded during boot time the application
|
||||||
# won't boot properly.
|
# won't boot properly.
|
||||||
# config.reload_routes = true
|
config.reload_routes = false
|
||||||
|
|
||||||
# ==> Configuration for :database_authenticatable
|
# ==> Configuration for :database_authenticatable
|
||||||
# For bcrypt, this is the cost for hashing the password and defaults to 12. If
|
# For bcrypt, this is the cost for hashing the password and defaults to 12. If
|
||||||
|
|||||||
@@ -6,7 +6,22 @@ if defined?(Rollbar)
|
|||||||
Rollbar.configure do |config|
|
Rollbar.configure do |config|
|
||||||
config.access_token = ENV.fetch('ROLLBAR_ACCESS_TOKEN', nil)
|
config.access_token = ENV.fetch('ROLLBAR_ACCESS_TOKEN', nil)
|
||||||
|
|
||||||
|
config.transform << proc do |options|
|
||||||
|
data = options[:payload]['data']
|
||||||
|
|
||||||
|
if data[:request]
|
||||||
|
data[:request][:cookies] = {}
|
||||||
|
data[:request][:session] = {}
|
||||||
|
data[:request][:url] =
|
||||||
|
data[:request][:url].to_s.sub(%r{(/[sde]/)\w{8}}, '\1********').sub(/\A(.*?)--(.*)/, '\1--********')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
config.enabled = true
|
config.enabled = true
|
||||||
|
config.collect_user_ip = false
|
||||||
|
config.anonymize_user_ip = true
|
||||||
|
config.scrub_headers += %w[X-Auth-Token Cookie X-Csrf-Token Referer]
|
||||||
|
config.scrub_fields += %i[slug uuid attachment_uuid]
|
||||||
|
|
||||||
config.exception_level_filters['ActionController::RoutingError'] = 'ignore'
|
config.exception_level_filters['ActionController::RoutingError'] = 'ignore'
|
||||||
|
|
||||||
|
|||||||
+17
-2
@@ -53,6 +53,7 @@ Rails.application.routes.draw do
|
|||||||
resources :verify_pdf_signature, only: %i[create]
|
resources :verify_pdf_signature, only: %i[create]
|
||||||
resource :mfa_setup, only: %i[show new edit create destroy], controller: 'mfa_setup'
|
resource :mfa_setup, only: %i[show new edit create destroy], controller: 'mfa_setup'
|
||||||
resources :account_configs, only: %i[create]
|
resources :account_configs, only: %i[create]
|
||||||
|
resources :encrypted_user_configs, only: %i[destroy]
|
||||||
resources :timestamp_server, only: %i[create]
|
resources :timestamp_server, only: %i[create]
|
||||||
resources :dashboard, only: %i[index]
|
resources :dashboard, only: %i[index]
|
||||||
resources :setup, only: %i[index create]
|
resources :setup, only: %i[index create]
|
||||||
@@ -80,7 +81,19 @@ Rails.application.routes.draw do
|
|||||||
resource :code_modal, only: %i[show], controller: 'templates_code_modal'
|
resource :code_modal, only: %i[show], controller: 'templates_code_modal'
|
||||||
resources :submissions_export, only: %i[index new]
|
resources :submissions_export, only: %i[index new]
|
||||||
end
|
end
|
||||||
resources :preview_document_page, only: %i[show], path: '/preview/:attachment_uuid'
|
resources :preview_document_page, only: %i[show], path: '/preview/:signed_uuid'
|
||||||
|
resource :blobs_proxy, only: %i[show], path: '/blobs_proxy/:signed_uuid/*filename',
|
||||||
|
controller: 'api/active_storage_blobs_proxy'
|
||||||
|
|
||||||
|
if Docuseal.multitenant?
|
||||||
|
resource :blobs_proxy_legacy, only: %i[show],
|
||||||
|
path: '/blobs/proxy/:signed_id/*filename',
|
||||||
|
controller: 'api/active_storage_blobs_proxy_legacy',
|
||||||
|
as: :rails_blob
|
||||||
|
get '/disk/:encoded_key/*filename' => 'active_storage/disk#show', as: :rails_disk_service
|
||||||
|
put '/disk/:encoded_token' => 'active_storage/disk#update', as: :update_rails_disk_service
|
||||||
|
post '/direct_uploads' => 'active_storage/direct_uploads#create', as: :rails_direct_uploads
|
||||||
|
end
|
||||||
|
|
||||||
resources :start_form, only: %i[show update], path: 'd', param: 'slug' do
|
resources :start_form, only: %i[show update], path: 'd', param: 'slug' do
|
||||||
get :completed
|
get :completed
|
||||||
@@ -94,7 +107,9 @@ Rails.application.routes.draw do
|
|||||||
get :completed
|
get :completed
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :submissions_preview, only: %i[show], path: 'e', param: 'slug'
|
resources :submissions_preview, only: %i[show], path: 'e', param: 'slug' do
|
||||||
|
get :completed
|
||||||
|
end
|
||||||
|
|
||||||
resources :send_submission_email, only: %i[create] do
|
resources :send_submission_email, only: %i[create] do
|
||||||
get :success, on: :collection
|
get :success, on: :collection
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddUuidToActiveStorageBlobs < ActiveRecord::Migration[7.1]
|
||||||
|
def change
|
||||||
|
add_column :active_storage_blobs, :uuid, :string
|
||||||
|
|
||||||
|
add_index :active_storage_blobs, :uuid, unique: true
|
||||||
|
end
|
||||||
|
end
|
||||||
+3
-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_02_03_113455) do
|
ActiveRecord::Schema[7.1].define(version: 2024_02_18_082157) 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"
|
||||||
|
|
||||||
@@ -74,7 +74,9 @@ ActiveRecord::Schema[7.1].define(version: 2024_02_03_113455) do
|
|||||||
t.bigint "byte_size", null: false
|
t.bigint "byte_size", null: false
|
||||||
t.string "checksum"
|
t.string "checksum"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
|
t.string "uuid"
|
||||||
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
|
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
|
||||||
|
t.index ["uuid"], name: "index_active_storage_blobs_on_uuid", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "active_storage_variant_records", force: :cascade do |t|
|
create_table "active_storage_variant_records", force: :cascade do |t|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Docuseal
|
module Docuseal
|
||||||
|
URL_CACHE = ActiveSupport::Cache::MemoryStore.new
|
||||||
PRODUCT_URL = 'https://www.docuseal.co'
|
PRODUCT_URL = 'https://www.docuseal.co'
|
||||||
NEWSLETTER_URL = "#{PRODUCT_URL}/newsletters".freeze
|
NEWSLETTER_URL = "#{PRODUCT_URL}/newsletters".freeze
|
||||||
ENQUIRIES_URL = "#{PRODUCT_URL}/enquiries".freeze
|
ENQUIRIES_URL = "#{PRODUCT_URL}/enquiries".freeze
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ module Params
|
|||||||
|
|
||||||
validator.call
|
validator.call
|
||||||
rescue InvalidParameterError => e
|
rescue InvalidParameterError => e
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
raise e unless validator.dry_run?
|
raise e unless validator.dry_run?
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
|
|||||||
@@ -70,6 +70,18 @@ class PdfProcessor < HexaPDF::Content::Processor
|
|||||||
page.contents = process_handler_instance.contents
|
page.contents = process_handler_instance.contents
|
||||||
end
|
end
|
||||||
|
|
||||||
|
page[:Annots].to_a.each do |annot|
|
||||||
|
next unless annot
|
||||||
|
|
||||||
|
text = annot[:Contents].to_s.squish
|
||||||
|
|
||||||
|
next unless text.starts_with?('{{') && text.ends_with?('}}')
|
||||||
|
|
||||||
|
result_handler.call({ text:, rect: annot[:Rect] }, page, acc)
|
||||||
|
|
||||||
|
page[:Annots].delete(annot)
|
||||||
|
end
|
||||||
|
|
||||||
process_handler_instance.result.each do |item|
|
process_handler_instance.result.each do |item|
|
||||||
result_handler.call(item, page, acc)
|
result_handler.call(item, page, acc)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ module SubmissionEvents
|
|||||||
phone_verified: 'Phone verified',
|
phone_verified: 'Phone verified',
|
||||||
start_form: 'Submission started',
|
start_form: 'Submission started',
|
||||||
view_form: 'Form viewed',
|
view_form: 'Form viewed',
|
||||||
complete_form: 'Submission completed'
|
complete_form: 'Submission completed',
|
||||||
|
api_complete_form: 'Submission completed via API'
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|||||||
+2
-2
@@ -28,7 +28,7 @@ module Submissions
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create_from_emails(template:, user:, emails:, source:, mark_as_sent: false, params: {})
|
def create_from_emails(template:, user:, emails:, source:, mark_as_sent: false, params: {})
|
||||||
preferences = Submitters.normalize_preferences(template.account, user, params)
|
preferences = Submitters.normalize_preferences(user.account, user, params)
|
||||||
|
|
||||||
parse_emails(emails).uniq.map do |email|
|
parse_emails(emails).uniq.map do |email|
|
||||||
submission = template.submissions.new(created_by_user: user,
|
submission = template.submissions.new(created_by_user: user,
|
||||||
@@ -66,7 +66,7 @@ module Submissions
|
|||||||
first_submitter =
|
first_submitter =
|
||||||
submission.template_submitters.filter_map { |s| submitters.find { |e| e.uuid == s['uuid'] } }.first
|
submission.template_submitters.filter_map { |s| submitters.find { |e| e.uuid == s['uuid'] } }.first
|
||||||
|
|
||||||
Submitters.send_signature_requests([first_submitter])
|
Submitters.send_signature_requests([first_submitter]) if first_submitter
|
||||||
else
|
else
|
||||||
Submitters.send_signature_requests(submitters)
|
Submitters.send_signature_requests(submitters)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ module Submissions
|
|||||||
module_function
|
module_function
|
||||||
|
|
||||||
def call(template:, user:, submissions_attrs:, source:, submitters_order:, mark_as_sent: false, params: {})
|
def call(template:, user:, submissions_attrs:, source:, submitters_order:, mark_as_sent: false, params: {})
|
||||||
preferences = Submitters.normalize_preferences(template.account, user, params)
|
preferences = Submitters.normalize_preferences(user.account, user, params)
|
||||||
|
|
||||||
Array.wrap(submissions_attrs).map do |attrs|
|
Array.wrap(submissions_attrs).map do |attrs|
|
||||||
submission_preferences = Submitters.normalize_preferences(template.account, user, attrs)
|
submission_preferences = Submitters.normalize_preferences(user.account, user, attrs)
|
||||||
submission_preferences = preferences.merge(submission_preferences)
|
submission_preferences = preferences.merge(submission_preferences)
|
||||||
|
|
||||||
set_submission_preferences = submission_preferences.slice('send_email', 'bcc_completed')
|
set_submission_preferences = submission_preferences.slice('send_email', 'bcc_completed')
|
||||||
@@ -109,8 +109,10 @@ module Submissions
|
|||||||
field['description'] = attrs['description'] if attrs['description'].present?
|
field['description'] = attrs['description'] if attrs['description'].present?
|
||||||
field['readonly'] = attrs['readonly'] if attrs.key?('readonly')
|
field['readonly'] = attrs['readonly'] if attrs.key?('readonly')
|
||||||
field['required'] = attrs['required'] if attrs.key?('required')
|
field['required'] = attrs['required'] if attrs.key?('required')
|
||||||
field['default_value'] = attrs['default_value'] if attrs['default_value'].present? &&
|
|
||||||
!field['type'].in?(%w[signature image initials file])
|
if attrs['default_value'].present? && !field['type'].in?(%w[signature image initials file])
|
||||||
|
field['default_value'] = Submitters::NormalizeValues.normalize_value(field, attrs['default_value'])
|
||||||
|
end
|
||||||
|
|
||||||
return field if attrs['validation_pattern'].blank?
|
return field if attrs['validation_pattern'].blank?
|
||||||
|
|
||||||
|
|||||||
@@ -23,9 +23,11 @@ module Submissions
|
|||||||
else
|
else
|
||||||
submitter.document_generation_events.create!(event_name: events.present? ? :retry : :start)
|
submitter.document_generation_events.create!(event_name: events.present? ? :retry : :start)
|
||||||
|
|
||||||
GenerateResultAttachments.call(submitter)
|
documents = GenerateResultAttachments.call(submitter)
|
||||||
|
|
||||||
submitter.document_generation_events.create!(event_name: :complete)
|
submitter.document_generation_events.create!(event_name: :complete)
|
||||||
|
|
||||||
|
documents
|
||||||
end
|
end
|
||||||
rescue ActiveRecord::RecordNotUnique
|
rescue ActiveRecord::RecordNotUnique
|
||||||
sleep WAIT_FOR_RETRY
|
sleep WAIT_FOR_RETRY
|
||||||
@@ -49,7 +51,7 @@ module Submissions
|
|||||||
DocumentGenerationEvent.where(submitter:).order(:created_at).last
|
DocumentGenerationEvent.where(submitter:).order(:created_at).last
|
||||||
end
|
end
|
||||||
|
|
||||||
break last_event if last_event.event_name.in?(%w[complete fail])
|
break submitter.documents if last_event.event_name.in?(%w[complete fail])
|
||||||
|
|
||||||
raise WaitForCompleteTimeout if total_wait_time > CHECK_COMPLETE_TIMEOUT
|
raise WaitForCompleteTimeout if total_wait_time > CHECK_COMPLETE_TIMEOUT
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ module Submissions
|
|||||||
module GenerateAuditTrail
|
module GenerateAuditTrail
|
||||||
FONT_SIZE = 9
|
FONT_SIZE = 9
|
||||||
TEXT_COLOR = '525252'
|
TEXT_COLOR = '525252'
|
||||||
FONT_PATH = '/fonts/LiberationSans-Regular.ttf'
|
FONT_PATH = '/fonts/GoNotoKurrent-Regular.ttf'
|
||||||
FONT_BOLD_PATH = '/fonts/LiberationSans-Bold.ttf'
|
FONT_BOLD_PATH = '/fonts/GoNotoKurrent-Bold.ttf'
|
||||||
FONT_NAME = if File.exist?(FONT_PATH)
|
FONT_NAME = if File.exist?(FONT_PATH)
|
||||||
FONT_PATH
|
FONT_PATH
|
||||||
else
|
else
|
||||||
@@ -28,6 +28,7 @@ module Submissions
|
|||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
RTL_REGEXP = TextUtils::RTL_REGEXP
|
RTL_REGEXP = TextUtils::RTL_REGEXP
|
||||||
|
MAX_IMAGE_HEIGHT = 100
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
@@ -61,7 +62,7 @@ module Submissions
|
|||||||
style.frame = style.create_frame(canvas.context, 50)
|
style.frame = style.create_frame(canvas.context, 50)
|
||||||
end
|
end
|
||||||
|
|
||||||
composer.style(:base, font: FONT_NAME, font_size: FONT_SIZE, fill_color: TEXT_COLOR, line_spacing: 1.2)
|
composer.style(:base, font: FONT_NAME, font_size: FONT_SIZE, fill_color: TEXT_COLOR, line_spacing: 1)
|
||||||
composer.style(:link, fill_color: 'hp-blue-light', underline: true)
|
composer.style(:link, fill_color: 'hp-blue-light', underline: true)
|
||||||
|
|
||||||
composer.new_page
|
composer.new_page
|
||||||
@@ -71,16 +72,16 @@ module Submissions
|
|||||||
|
|
||||||
column.text(account.linked_account_account&.testing? ? 'Testing Log - Not for Production Use' : 'Audit Log',
|
column.text(account.linked_account_account&.testing? ? 'Testing Log - Not for Production Use' : 'Audit Log',
|
||||||
font_size: 16,
|
font_size: 16,
|
||||||
padding: [15, 0, 0, 0],
|
padding: [10, 0, 0, 0],
|
||||||
position: :float, position_hint: :right)
|
position: :float, text_align: :right)
|
||||||
end
|
end
|
||||||
|
|
||||||
composer.column(columns: 1) do |column|
|
composer.column(columns: 1) do |column|
|
||||||
column.text("Envelope ID: #{submission.id}", font_size: 12, padding: [20, 0, 10, 0], position: :float)
|
column.text("Envelope ID: #{submission.id}", font_size: 12, padding: [15, 0, 8, 0], position: :float)
|
||||||
|
|
||||||
unless submission.source.in?(%w[embed api])
|
unless submission.source.in?(%w[embed api])
|
||||||
column.formatted_text([{ link: verify_url, text: 'Verify', style: :link }],
|
column.formatted_text([{ link: verify_url, text: 'Verify', style: :link }],
|
||||||
font_size: 9, padding: [22, 0, 10, 0], position: :float, align: :right)
|
font_size: 9, padding: [15, 0, 10, 0], position: :float, text_align: :right)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -97,7 +98,7 @@ module Submissions
|
|||||||
end
|
end
|
||||||
|
|
||||||
link =
|
link =
|
||||||
Rails.application.routes.url_helpers.rails_blob_url(document, **Docuseal.default_url_options)
|
ActiveStorage::Blob.proxy_url(document.blob)
|
||||||
|
|
||||||
[
|
[
|
||||||
composer.document.layout.formatted_text_box(
|
composer.document.layout.formatted_text_box(
|
||||||
@@ -114,13 +115,13 @@ module Submissions
|
|||||||
{ text: 'Generated at: ', font: [FONT_BOLD_NAME, { variant: :bold }] },
|
{ text: 'Generated at: ', font: [FONT_BOLD_NAME, { variant: :bold }] },
|
||||||
"#{I18n.l(document.created_at.in_time_zone(account.timezone), format: :long, locale: account.locale)} " \
|
"#{I18n.l(document.created_at.in_time_zone(account.timezone), format: :long, locale: account.locale)} " \
|
||||||
"#{TimeUtils.timezone_abbr(account.timezone, document.created_at)}"
|
"#{TimeUtils.timezone_abbr(account.timezone, document.created_at)}"
|
||||||
], line_spacing: 1.8
|
], line_spacing: 1.3
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
if documents_data.present?
|
if documents_data.present?
|
||||||
composer.table(documents_data, cell_style: { padding: [0, 0, 25, 0], border: { width: 0 } })
|
composer.table(documents_data, cell_style: { padding: [0, 0, 20, 0], border: { width: 0 } })
|
||||||
|
|
||||||
composer.draw_box(divider)
|
composer.draw_box(divider)
|
||||||
end
|
end
|
||||||
@@ -151,7 +152,7 @@ module Submissions
|
|||||||
submitter.email && { text: "#{submitter.email}\n", font: [FONT_BOLD_NAME, { variant: :bold }] },
|
submitter.email && { text: "#{submitter.email}\n", font: [FONT_BOLD_NAME, { variant: :bold }] },
|
||||||
submitter.name && { text: "#{TextUtils.maybe_rtl_reverse(submitter.name)}\n" },
|
submitter.name && { text: "#{TextUtils.maybe_rtl_reverse(submitter.name)}\n" },
|
||||||
submitter.phone && { text: "#{submitter.phone}\n" }
|
submitter.phone && { text: "#{submitter.phone}\n" }
|
||||||
].compact_blank, line_spacing: 1.8, padding: [0, 20, 0, 0]
|
].compact_blank, line_spacing: 1.3, padding: [0, 20, 0, 0]
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@@ -167,7 +168,7 @@ module Submissions
|
|||||||
completed_event.data['sid'] && { text: "Session ID: #{completed_event.data['sid']}\n" },
|
completed_event.data['sid'] && { text: "Session ID: #{completed_event.data['sid']}\n" },
|
||||||
completed_event.data['ua'] && { text: "User agent: #{completed_event.data['ua']}\n" },
|
completed_event.data['ua'] && { text: "User agent: #{completed_event.data['ua']}\n" },
|
||||||
"\n"
|
"\n"
|
||||||
].compact_blank, line_spacing: 1.8, padding: [10, 20, 20, 0]
|
].compact_blank, line_spacing: 1.3, padding: [10, 20, 20, 0]
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@@ -192,8 +193,8 @@ module Submissions
|
|||||||
font_size: 6
|
font_size: 6
|
||||||
}
|
}
|
||||||
].compact_blank,
|
].compact_blank,
|
||||||
align: field['name'].to_s.match?(RTL_REGEXP) ? :right : :left,
|
text_align: field['name'].to_s.match?(RTL_REGEXP) ? :right : :left,
|
||||||
line_spacing: 1.8, padding: [0, 0, 5, 0]
|
line_spacing: 1.3, padding: [0, 0, 2, 0]
|
||||||
),
|
),
|
||||||
if field['type'].in?(%w[image signature initials stamp])
|
if field['type'].in?(%w[image signature initials stamp])
|
||||||
attachment = submitter.attachments.find { |a| a.uuid == value }
|
attachment = submitter.attachments.find { |a| a.uuid == value }
|
||||||
@@ -207,11 +208,16 @@ module Submissions
|
|||||||
width = field['type'] == 'initials' ? 100 : 200
|
width = field['type'] == 'initials' ? 100 : 200
|
||||||
height = resized_image.height * (width.to_f / resized_image.width)
|
height = resized_image.height * (width.to_f / resized_image.width)
|
||||||
|
|
||||||
composer.image(io, width:, height:, margin: [0, 0, 10, 0])
|
if height > MAX_IMAGE_HEIGHT
|
||||||
|
width = (MAX_IMAGE_HEIGHT / height) * width
|
||||||
|
height = MAX_IMAGE_HEIGHT
|
||||||
|
end
|
||||||
|
|
||||||
|
composer.image(io, width:, height:, margin: [5, 0, 10, 0])
|
||||||
composer.formatted_text_box([{ text: '' }])
|
composer.formatted_text_box([{ text: '' }])
|
||||||
elsif field['type'].in?(%w[file payment])
|
elsif field['type'].in?(%w[file payment])
|
||||||
if field['type'] == 'payment'
|
if field['type'] == 'payment'
|
||||||
unit = CURRENCY_SYMBOLS[field['preferences']['currency']]
|
unit = CURRENCY_SYMBOLS[field['preferences']['currency']] || field['preferences']['currency']
|
||||||
|
|
||||||
price = ApplicationController.helpers.number_to_currency(field['preferences']['price'], unit:)
|
price = ApplicationController.helpers.number_to_currency(field['preferences']['price'], unit:)
|
||||||
|
|
||||||
@@ -222,7 +228,7 @@ module Submissions
|
|||||||
Array.wrap(value).map do |uuid|
|
Array.wrap(value).map do |uuid|
|
||||||
attachment = submitter.attachments.find { |a| a.uuid == uuid }
|
attachment = submitter.attachments.find { |a| a.uuid == uuid }
|
||||||
link =
|
link =
|
||||||
Rails.application.routes.url_helpers.rails_blob_url(attachment, **Docuseal.default_url_options)
|
ActiveStorage::Blob.proxy_url(attachment.blob)
|
||||||
|
|
||||||
{ link:, text: "#{attachment.filename}\n", style: :link }
|
{ link:, text: "#{attachment.filename}\n", style: :link }
|
||||||
end,
|
end,
|
||||||
@@ -236,9 +242,10 @@ module Submissions
|
|||||||
end
|
end
|
||||||
|
|
||||||
value = value.join(', ') if value.is_a?(Array)
|
value = value.join(', ') if value.is_a?(Array)
|
||||||
|
value = Submissions::GenerateResultAttachments::REPLACE_EMOJI[value] || value
|
||||||
|
|
||||||
composer.formatted_text_box([{ text: TextUtils.maybe_rtl_reverse(value.to_s.presence || 'n/a') }],
|
composer.formatted_text_box([{ text: TextUtils.maybe_rtl_reverse(value.to_s.presence || 'n/a') }],
|
||||||
align: value.to_s.match?(RTL_REGEXP) ? :right : :left,
|
text_align: value.to_s.match?(RTL_REGEXP) ? :right : :left,
|
||||||
padding: [0, 0, 10, 0])
|
padding: [0, 0, 10, 0])
|
||||||
end
|
end
|
||||||
]
|
]
|
||||||
@@ -269,7 +276,7 @@ module Submissions
|
|||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
composer.table(events_data, cell_style: { padding: [0, 0, 20, 0], border: { width: 0 } }) if events_data.present?
|
composer.table(events_data, cell_style: { padding: [0, 0, 12, 0], border: { width: 0 } }) if events_data.present?
|
||||||
|
|
||||||
io = StringIO.new
|
io = StringIO.new
|
||||||
|
|
||||||
@@ -278,6 +285,7 @@ module Submissions
|
|||||||
sign_params = {
|
sign_params = {
|
||||||
reason: SIGN_REASON,
|
reason: SIGN_REASON,
|
||||||
certificate: pkcs.certificate,
|
certificate: pkcs.certificate,
|
||||||
|
doc_mdp_permissions: :no_changes,
|
||||||
key: pkcs.key,
|
key: pkcs.key,
|
||||||
certificate_chain: pkcs.ca_certs || []
|
certificate_chain: pkcs.ca_certs || []
|
||||||
}
|
}
|
||||||
@@ -306,8 +314,8 @@ module Submissions
|
|||||||
font_size: 20,
|
font_size: 20,
|
||||||
font: [FONT_BOLD_NAME, { variant: :bold }],
|
font: [FONT_BOLD_NAME, { variant: :bold }],
|
||||||
width: 100,
|
width: 100,
|
||||||
padding: [12, 0, 0, 8],
|
padding: [5, 0, 0, 8],
|
||||||
position: :float, position_hint: :left)
|
position: :float, text_align: :left)
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics
|
# rubocop:enable Metrics
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ module Submissions
|
|||||||
submission_data += submitter.documents.map.with_index(1) do |attachment, index|
|
submission_data += submitter.documents.map.with_index(1) do |attachment, index|
|
||||||
{
|
{
|
||||||
name: "Document #{index}",
|
name: "Document #{index}",
|
||||||
value: attachment.url
|
value: ActiveStorage::Blob.proxy_url(attachment.blob)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -131,9 +131,13 @@ module Submissions
|
|||||||
|
|
||||||
value =
|
value =
|
||||||
if template_field_type.in?(%w[image signature])
|
if template_field_type.in?(%w[image signature])
|
||||||
attachments_index[submitter_value]&.url
|
attachment = attachments_index[submitter_value]
|
||||||
|
ActiveStorage::Blob.proxy_url(attachment.blob) if attachment
|
||||||
elsif template_field_type == 'file'
|
elsif template_field_type == 'file'
|
||||||
Array.wrap(submitter_value).compact_blank.filter_map { |e| attachments_index[e]&.url }
|
Array.wrap(submitter_value).compact_blank.filter_map do |e|
|
||||||
|
attachment = attachments_index[e]
|
||||||
|
ActiveStorage::Blob.proxy_url(attachment.blob) if attachment
|
||||||
|
end
|
||||||
else
|
else
|
||||||
submitter_value
|
submitter_value
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
module Submissions
|
module Submissions
|
||||||
module GenerateResultAttachments
|
module GenerateResultAttachments
|
||||||
FONT_SIZE = 11
|
FONT_SIZE = 11
|
||||||
FONT_PATH = '/fonts/LiberationSans-Regular.ttf'
|
FONT_PATH = '/fonts/GoNotoKurrent-Regular.ttf'
|
||||||
FONT_NAME = if File.exist?(FONT_PATH)
|
FONT_NAME = if File.exist?(FONT_PATH)
|
||||||
FONT_PATH
|
FONT_PATH
|
||||||
else
|
else
|
||||||
@@ -21,11 +21,16 @@ module Submissions
|
|||||||
A4_SIZE = [595, 842].freeze
|
A4_SIZE = [595, 842].freeze
|
||||||
SUPPORTED_IMAGE_TYPES = ['image/png', 'image/jpeg'].freeze
|
SUPPORTED_IMAGE_TYPES = ['image/png', 'image/jpeg'].freeze
|
||||||
|
|
||||||
|
REPLACE_EMOJI = {
|
||||||
|
'✔️' => 'V',
|
||||||
|
'✔' => 'V'
|
||||||
|
}.freeze
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
# rubocop:disable Metrics
|
# rubocop:disable Metrics
|
||||||
def call(submitter)
|
def call(submitter)
|
||||||
cell_layouter = HexaPDF::Layout::TextLayouter.new(valign: :center, align: :center)
|
cell_layouter = HexaPDF::Layout::TextLayouter.new(text_valign: :center, text_align: :center)
|
||||||
|
|
||||||
template = submitter.submission.template
|
template = submitter.submission.template
|
||||||
|
|
||||||
@@ -60,8 +65,8 @@ module Submissions
|
|||||||
|
|
||||||
value = submitter.values[field['uuid']]
|
value = submitter.values[field['uuid']]
|
||||||
|
|
||||||
layouter = HexaPDF::Layout::TextLayouter.new(valign: :center,
|
layouter = HexaPDF::Layout::TextLayouter.new(text_valign: :center,
|
||||||
align: value.to_s.match?(RTL_REGEXP) ? :right : :left,
|
text_align: value.to_s.match?(RTL_REGEXP) ? :right : :left,
|
||||||
font: pdf.fonts.add(FONT_NAME), font_size:)
|
font: pdf.fonts.add(FONT_NAME), font_size:)
|
||||||
|
|
||||||
next if Array.wrap(value).compact_blank.blank?
|
next if Array.wrap(value).compact_blank.blank?
|
||||||
@@ -126,7 +131,7 @@ module Submissions
|
|||||||
height - (area['y'] * height) - lines[..next_index].sum(&:height) + height_diff
|
height - (area['y'] * height) - lines[..next_index].sum(&:height) + height_diff
|
||||||
],
|
],
|
||||||
A: { Type: :Action, S: :URI,
|
A: { Type: :Action, S: :URI,
|
||||||
URI: h.rails_blob_url(attachment, **Docuseal.default_url_options) }
|
URI: ActiveStorage::Blob.proxy_url(attachment.blob) }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -173,6 +178,7 @@ module Submissions
|
|||||||
end
|
end
|
||||||
|
|
||||||
value = TextUtils.maybe_rtl_reverse(Array.wrap(value).join(', '))
|
value = TextUtils.maybe_rtl_reverse(Array.wrap(value).join(', '))
|
||||||
|
value = REPLACE_EMOJI[value] || value
|
||||||
|
|
||||||
text = HexaPDF::Layout::TextFragment.create(value, font: pdf.fonts.add(FONT_NAME),
|
text = HexaPDF::Layout::TextFragment.create(value, font: pdf.fonts.add(FONT_NAME),
|
||||||
font_size:)
|
font_size:)
|
||||||
@@ -294,6 +300,12 @@ module Submissions
|
|||||||
HexaPDF::Document.new(io: StringIO.new(attachment.download))
|
HexaPDF::Document.new(io: StringIO.new(attachment.download))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
pdf.acro_form&.flatten
|
||||||
|
rescue StandardError => e
|
||||||
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
end
|
||||||
|
|
||||||
[attachment.uuid, pdf]
|
[attachment.uuid, pdf]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -329,8 +341,6 @@ module Submissions
|
|||||||
def fetch_sign_reason(submitter)
|
def fetch_sign_reason(submitter)
|
||||||
reason_name = submitter.email || submitter.name || submitter.phone
|
reason_name = submitter.email || submitter.name || submitter.phone
|
||||||
|
|
||||||
return sign_reason(reason_name) if Docuseal.multitenant?
|
|
||||||
|
|
||||||
config =
|
config =
|
||||||
if Docuseal.multitenant?
|
if Docuseal.multitenant?
|
||||||
AccountConfig.where(account: submitter.account, key: AccountConfig::ESIGNING_PREFERENCE_KEY)
|
AccountConfig.where(account: submitter.account, key: AccountConfig::ESIGNING_PREFERENCE_KEY)
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Submitters
|
||||||
|
module GenerateFontImage
|
||||||
|
WIDTH = 3000
|
||||||
|
HEIGHT = 160
|
||||||
|
|
||||||
|
FONTS = {
|
||||||
|
'Dancing Script Regular' => '/fonts/DancingScript-Regular.otf',
|
||||||
|
'Go Noto Kurrent-Bold Bold' => '/fonts/GoNotoKurrent-Bold.ttf'
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
FONT_ALIASES = {
|
||||||
|
'initials' => 'Go Noto Kurrent-Bold Bold',
|
||||||
|
'signature' => 'Dancing Script Regular'
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def call(text, font: nil)
|
||||||
|
font = FONT_ALIASES[font] || font
|
||||||
|
|
||||||
|
text_image = Vips::Image.text(text, font:, fontfile: FONTS[font],
|
||||||
|
width: WIDTH, height: HEIGHT, wrap: :none)
|
||||||
|
|
||||||
|
text_mask = Vips::Image.black(text_image.width, text_image.height)
|
||||||
|
|
||||||
|
text_mask.bandjoin(text_image).copy(interpretation: :b_w).write_to_buffer('.png')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -4,8 +4,11 @@ module Submitters
|
|||||||
module NormalizeValues
|
module NormalizeValues
|
||||||
CHECKSUM_CACHE_STORE = ActiveSupport::Cache::MemoryStore.new
|
CHECKSUM_CACHE_STORE = ActiveSupport::Cache::MemoryStore.new
|
||||||
|
|
||||||
UnknownFieldName = Class.new(StandardError)
|
BaseError = Class.new(StandardError)
|
||||||
UnknownSubmitterName = Class.new(StandardError)
|
|
||||||
|
UnknownFieldName = Class.new(BaseError)
|
||||||
|
InvalidDefaultValue = Class.new(BaseError)
|
||||||
|
UnknownSubmitterName = Class.new(BaseError)
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
@@ -28,25 +31,47 @@ module Submitters
|
|||||||
|
|
||||||
next if key.blank?
|
next if key.blank?
|
||||||
|
|
||||||
if fields_uuid_index[key]['type'].in?(%w[initials signature image file]) && value.present?
|
field = fields_uuid_index[key]
|
||||||
new_value, new_attachments = normalize_attachment_value(value, template.account, for_submitter)
|
|
||||||
|
if field['type'].in?(%w[initials signature image file]) && value.present?
|
||||||
|
new_value, new_attachments =
|
||||||
|
normalize_attachment_value(value, field['type'], template.account, attachments, for_submitter)
|
||||||
|
|
||||||
attachments.push(*new_attachments)
|
attachments.push(*new_attachments)
|
||||||
|
|
||||||
value = new_value
|
value = new_value
|
||||||
end
|
end
|
||||||
|
|
||||||
[key, value]
|
[key, normalize_value(field, value)]
|
||||||
end.to_h
|
end.to_h
|
||||||
|
|
||||||
[normalized_values, attachments]
|
[normalized_values, attachments]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def normalize_value(field, value)
|
||||||
|
if field['type'] == 'text' && value.present?
|
||||||
|
value.to_s
|
||||||
|
elsif field['type'] == 'date' && value.present?
|
||||||
|
if value.is_a?(Integer)
|
||||||
|
Time.zone.at(value.to_s.first(10).to_i).to_date
|
||||||
|
else
|
||||||
|
Date.parse(value).to_s
|
||||||
|
end
|
||||||
|
else
|
||||||
|
value
|
||||||
|
end
|
||||||
|
rescue Date::Error => e
|
||||||
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
|
value
|
||||||
|
end
|
||||||
|
|
||||||
def fetch_fields(template, submitter_name: nil, for_submitter: nil)
|
def fetch_fields(template, submitter_name: nil, for_submitter: nil)
|
||||||
if submitter_name
|
if submitter_name
|
||||||
submitter =
|
submitter =
|
||||||
template.submitters.find { |e| e['name'] == submitter_name } ||
|
template.submitters.find { |e| e['name'] == submitter_name } ||
|
||||||
raise(UnknownSubmitterName, "Unknown submitter role: #{submitter_name}")
|
raise(UnknownSubmitterName,
|
||||||
|
"Unknown submitter role: #{submitter_name}. Template defines #{template.submitters.pluck('name')}")
|
||||||
end
|
end
|
||||||
|
|
||||||
fields = for_submitter&.submission&.template_fields || template.fields
|
fields = for_submitter&.submission&.template_fields || template.fields
|
||||||
@@ -66,20 +91,37 @@ module Submitters
|
|||||||
.merge(fields.index_by { |e| e['name'].to_s.downcase })
|
.merge(fields.index_by { |e| e['name'].to_s.downcase })
|
||||||
end
|
end
|
||||||
|
|
||||||
def normalize_attachment_value(value, account, for_submitter = nil)
|
def normalize_attachment_value(value, type, account, attachments, for_submitter = nil)
|
||||||
if value.is_a?(Array)
|
if value.is_a?(Array)
|
||||||
new_attachments = value.map { |v| find_or_build_attachment(v, account, for_submitter) }
|
new_attachments = value.map do |v|
|
||||||
|
new_attachment = find_or_build_attachment(v, type, account, for_submitter)
|
||||||
|
|
||||||
|
attachments.find { |a| a.blob_id == new_attachment.blob_id } || new_attachment
|
||||||
|
end
|
||||||
|
|
||||||
[new_attachments.map(&:uuid), new_attachments]
|
[new_attachments.map(&:uuid), new_attachments]
|
||||||
else
|
else
|
||||||
new_attachment = find_or_build_attachment(value, account, for_submitter)
|
new_attachment = find_or_build_attachment(value, type, account, for_submitter)
|
||||||
|
|
||||||
[new_attachment.uuid, new_attachment]
|
existing_attachment = attachments.find { |a| a.blob_id == new_attachment.blob_id }
|
||||||
|
|
||||||
|
attachment = existing_attachment || new_attachment
|
||||||
|
|
||||||
|
[attachment.uuid, attachment]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_or_build_attachment(value, account, for_submitter = nil)
|
def find_or_build_attachment(value, type, account, for_submitter = nil)
|
||||||
blob = find_or_create_blobs(account, value)
|
blob =
|
||||||
|
if value.match?(%r{\Ahttps?://})
|
||||||
|
find_or_create_blob_from_url(account, value)
|
||||||
|
elsif type.in?(%w[signature initials]) && value.length < 50
|
||||||
|
find_or_create_blob_from_text(account, value, type)
|
||||||
|
elsif (data = Base64.decode64(value)) && Marcel::MimeType.for(data).include?('image')
|
||||||
|
find_or_create_blob_from_base64(account, data, type)
|
||||||
|
else
|
||||||
|
raise InvalidDefaultValue, "Invalid value, url, base64 or text < 50 chars is expected: #{value.first(50)}..."
|
||||||
|
end
|
||||||
|
|
||||||
attachment = for_submitter.attachments.find_by(blob_id: blob.id) if for_submitter
|
attachment = for_submitter.attachments.find_by(blob_id: blob.id) if for_submitter
|
||||||
|
|
||||||
@@ -91,7 +133,31 @@ module Submitters
|
|||||||
attachment
|
attachment
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_or_create_blobs(account, url)
|
def find_or_create_blob_from_base64(account, data, type)
|
||||||
|
checksum = Digest::MD5.base64digest(data)
|
||||||
|
|
||||||
|
blob = find_blob_by_checksum(checksum, account)
|
||||||
|
|
||||||
|
blob || ActiveStorage::Blob.create_and_upload!(
|
||||||
|
io: StringIO.new(data),
|
||||||
|
filename: "#{type}.png"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_or_create_blob_from_text(account, text, type)
|
||||||
|
data = Submitters::GenerateFontImage.call(text, font: type)
|
||||||
|
|
||||||
|
checksum = Digest::MD5.base64digest(data)
|
||||||
|
|
||||||
|
blob = find_blob_by_checksum(checksum, account)
|
||||||
|
|
||||||
|
blob || ActiveStorage::Blob.create_and_upload!(
|
||||||
|
io: StringIO.new(data),
|
||||||
|
filename: "#{type}.png"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_or_create_blob_from_url(account, url)
|
||||||
cache_key = [account.id, url].join(':')
|
cache_key = [account.id, url].join(':')
|
||||||
checksum = CHECKSUM_CACHE_STORE.fetch(cache_key)
|
checksum = CHECKSUM_CACHE_STORE.fetch(cache_key)
|
||||||
|
|
||||||
@@ -114,11 +180,15 @@ module Submitters
|
|||||||
end
|
end
|
||||||
|
|
||||||
def find_blob_by_checksum(checksum, account)
|
def find_blob_by_checksum(checksum, account)
|
||||||
ActiveStorage::Blob
|
blob = ActiveStorage::Blob.find_by(checksum:)
|
||||||
.joins('JOIN active_storage_attachments ON active_storage_attachments.blob_id = active_storage_blobs.id')
|
|
||||||
.where(active_storage_attachments: { record_id: account.submitters.select(:id),
|
return unless blob
|
||||||
record_type: 'Submitter' })
|
|
||||||
.find_by(checksum:)
|
return blob unless blob.attachments.exists?
|
||||||
|
|
||||||
|
return blob if account.submitters.exists?(id: blob.attachments.where(record_type: 'Submitter').select(:record_id))
|
||||||
|
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def conn
|
def conn
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ module Submitters
|
|||||||
serialize_params = {
|
serialize_params = {
|
||||||
include: {},
|
include: {},
|
||||||
only: %i[id slug uuid name email phone completed_at external_id
|
only: %i[id slug uuid name email phone completed_at external_id
|
||||||
metadata opened_at sent_at created_at updated_at],
|
submission_id metadata opened_at sent_at created_at updated_at],
|
||||||
methods: %i[status application_key]
|
methods: %i[status application_key]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,15 +16,24 @@ module Submitters
|
|||||||
submitter_name = (submitter.submission.template_submitters ||
|
submitter_name = (submitter.submission.template_submitters ||
|
||||||
submitter.submission.template.submitters).find { |e| e['uuid'] == submitter.uuid }['name']
|
submitter.submission.template.submitters).find { |e| e['uuid'] == submitter.uuid }['name']
|
||||||
|
|
||||||
submitter.as_json(methods: %i[application_key],
|
submitter.as_json(methods: %i[status application_key],
|
||||||
include: [template: { only: %i[id name external_id created_at updated_at] }])
|
only: %i[id submission_id email phone name ua ip sent_at opened_at
|
||||||
|
completed_at created_at updated_at external_id metadata])
|
||||||
.except('uuid', 'values', 'slug')
|
.except('uuid', 'values', 'slug')
|
||||||
.merge('values' => values,
|
.merge('role' => submitter_name,
|
||||||
|
'preferences' => submitter.preferences.except('default_values'),
|
||||||
|
'values' => values,
|
||||||
'documents' => documents,
|
'documents' => documents,
|
||||||
'audit_log_url' => submitter.submission.audit_log_url,
|
'audit_log_url' => submitter.submission.audit_log_url,
|
||||||
'submission_url' => r.submissions_preview_url(submitter.submission.slug,
|
'submission_url' => r.submissions_preview_url(submitter.submission.slug,
|
||||||
**Docuseal.default_url_options),
|
**Docuseal.default_url_options),
|
||||||
'role' => submitter_name)
|
'template' => submitter.template.as_json(only: %i[id name external_id created_at
|
||||||
|
updated_at]),
|
||||||
|
'submission' => {
|
||||||
|
**submitter.submission.slice(:id, :audit_log_url, :created_at),
|
||||||
|
status: submitter.submission.submitters.all?(&:completed_at?) ? 'completed' : 'pending',
|
||||||
|
url: r.submissions_preview_url(submitter.submission.slug, **Docuseal.default_url_options)
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_values_array(submitter)
|
def build_values_array(submitter)
|
||||||
@@ -67,7 +76,7 @@ module Submitters
|
|||||||
def rails_storage_proxy_url(attachment)
|
def rails_storage_proxy_url(attachment)
|
||||||
return if attachment.blank?
|
return if attachment.blank?
|
||||||
|
|
||||||
r.rails_storage_proxy_url(attachment, **Docuseal.default_url_options)
|
ActiveStorage::Blob.proxy_url(attachment.blob)
|
||||||
end
|
end
|
||||||
|
|
||||||
def r
|
def r
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ module Submitters
|
|||||||
end
|
end
|
||||||
|
|
||||||
def replace_default_variables(value, attrs, template, with_time: false)
|
def replace_default_variables(value, attrs, template, with_time: false)
|
||||||
|
return value if value.in?([true, false])
|
||||||
return if value.blank?
|
return if value.blank?
|
||||||
|
|
||||||
value.to_s.gsub(VARIABLE_REGEXP) do |e|
|
value.to_s.gsub(VARIABLE_REGEXP) do |e|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ module Templates
|
|||||||
pdf.pages.flat_map.with_index do |page, index|
|
pdf.pages.flat_map.with_index do |page, index|
|
||||||
annotations = page.objects.deref!(page.attributes[:Annots]) || []
|
annotations = page.objects.deref!(page.attributes[:Annots]) || []
|
||||||
annotations.filter_map do |annot|
|
annotations.filter_map do |annot|
|
||||||
|
next if annot.nil?
|
||||||
next if annot[:A].blank? || annot[:A][:URI].blank?
|
next if annot[:A].blank? || annot[:A][:URI].blank?
|
||||||
next unless annot[:Subtype] == :Link
|
next unless annot[:Subtype] == :Link
|
||||||
next if !annot[:A][:URI].starts_with?('https://') && !annot[:A][:URI].starts_with?('http://')
|
next if !annot[:A][:URI].starts_with?('https://') && !annot[:A][:URI].starts_with?('http://')
|
||||||
|
|||||||
@@ -98,8 +98,6 @@ module Templates
|
|||||||
record: attachment
|
record: attachment
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
io
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
+1
-1
@@ -42,7 +42,7 @@ module TimeUtils
|
|||||||
|
|
||||||
I18n.l(date, format: i18n_format, locale:)
|
I18n.l(date, format: i18n_format, locale:)
|
||||||
rescue Date::Error => e
|
rescue Date::Error => e
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
string
|
string
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ RSpec.describe 'Submission Preview' do
|
|||||||
let(:submitters) { template.submitters.map { |s| create(:submitter, submission:, uuid: s['uuid']) } }
|
let(:submitters) { template.submitters.map { |s| create(:submitter, submission:, uuid: s['uuid']) } }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
sign_in(user)
|
||||||
|
|
||||||
visit submissions_preview_path(slug: submission.slug)
|
visit submissions_preview_path(slug: submission.slug)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user