Compare commits

...
90 Commits
Author SHA1 Message Date
Alex Turchyn 4eb4e8c1f8 fix file link generation 2024-02-21 00:38:33 +02:00
Pete Matsyburka 984df15949 pass account local var in attribution 2024-02-20 21:30:00 +02:00
Pete Matsyburka aa3bd24332 add cors headers 2024-02-20 18:25:41 +02:00
Pete Matsyburka 250889623d sort field types 2024-02-20 12:00:16 +02:00
Alex Turchyn 862fa55986 delay signature rendering 2024-02-20 01:13:43 +02:00
Pete Matsyburka 0f2889904a allow to pass signature as base64 2024-02-20 00:22:24 +02:00
Pete Matsyburka 49f14c7929 adjust rollbar configs 2024-02-19 23:53:16 +02:00
Pete Matsyburka c3f3c5d5df add missing i18n 2024-02-19 23:52:47 +02:00
Pete Matsyburka 274259f305 adjust page preview loading 2024-02-19 23:46:24 +02:00
Pete Matsyburka a15d7a9cf3 adjust csv export 2024-02-19 22:53:49 +02:00
Pete Matsyburka 7d154fc28d use message verifier purpose 2024-02-19 22:36:48 +02:00
Pete Matsyburka 6727d3be83 adjust blob proxy 2024-02-19 17:07:37 +02:00
Alex Turchyn 4bbd5c3bcb fix signature re-uploading issue 2024-02-18 23:13:15 +02:00
Pete Matsyburka 5995f2e427 show multiple signatures toggle on cloud 2024-02-18 23:13:15 +02:00
Pete Matsyburka 0910c8e100 redirect on archived template 2024-02-18 23:13:15 +02:00
Pete Matsyburka 8df76dfa90 download completed ttl 2024-02-18 23:13:14 +02:00
Pete Matsyburka 78e9e66cb0 use uuid blobs controller 2024-02-18 16:38:43 +02:00
Pete Matsyburka 71c61fc4b3 allow to use separate encryption key via env 2024-02-18 12:32:08 +02:00
Pete Matsyburka a728aa8689 add blobs proxy controller 2024-02-18 11:36:50 +02:00
Pete Matsyburka 5a3174a720 add uuid to active storage blob 2024-02-18 10:42:11 +02:00
Pete Matsyburka 652072d660 cache page url 2024-02-18 10:04:56 +02:00
Pete Matsyburka 323f5c0c15 use signed uuid in previews 2024-02-17 22:32:52 +02:00
Pete Matsyburka 229d78c78b add security.md 2024-02-17 18:22:36 +02:00
Pete Matsyburka 9d9a955149 do not use image variants deletion 2024-02-17 13:36:58 +02:00
Pete Matsyburka b40a047190 update readme 2024-02-17 12:13:56 +02:00
Pete Matsyburka ff9e415823 generate default signature initials from font 2024-02-17 01:41:07 +02:00
Pete Matsyburka 2f7a4fb2b3 fix i18n 2024-02-16 20:01:21 +02:00
Pete Matsyburka f8a654a5b6 reduce error log 2024-02-16 19:44:59 +02:00
Pete Matsyburka 4449475467 fix audit log currency 2024-02-16 19:00:18 +02:00
Pete Matsyburka bcbbf0dd42 fix large images in audit log 2024-02-16 18:59:10 +02:00
Pete Matsyburka 19b9ba52d4 adjust webhook retries 2024-02-16 18:47:02 +02:00
Pete Matsyburka bdc30db2dc fix typed signature field 2024-02-16 18:22:40 +02:00
Pete Matsyburka 74634ba96b mark as opened on form view 2024-02-16 17:59:33 +02:00
Pete Matsyburka 77feb05bad change font 2024-02-16 11:34:30 +02:00
Pete Matsyburka a1f05d006e fix the same file in default values 2024-02-16 01:33:35 +02:00
Pete Matsyburka cc3dae1335 do not send completed email to self-signed 2024-02-16 01:02:39 +02:00
Pete Matsyburka d9debcfb0d process annotations 2024-02-15 23:20:29 +02:00
Pete Matsyburka 8770d16d52 update pdf gem 2024-02-15 22:26:05 +02:00
Pete Matsyburka 84953481e4 adjust form i18n 2024-02-15 22:13:14 +02:00
Pete Matsyburka df7aa59f6f allow to clone templates to linked accounts 2024-02-15 11:32:08 +02:00
Pete Matsyburka 008b83119d better invalid role error message 2024-02-15 10:34:10 +02:00
Pete Matsyburka 15dafcb6ff downcase email when sign in 2024-02-15 10:27:04 +02:00
Pete Matsyburka 7d291ed888 add linked_account_accounts 2024-02-15 09:40:57 +02:00
Pete Matsyburka a7221f0750 fix form field size 2024-02-15 00:58:47 +02:00
Pete Matsyburka 9a5b2a5d30 add receive completed email config 2024-02-14 22:13:07 +02:00
Pete Matsyburka 7361a00d42 use universal font 2024-02-14 20:00:45 +02:00
Pete Matsyburka e07d402782 fix testing account email message 2024-02-13 23:05:31 +02:00
Pete Matsyburka a34354bee1 fix checkmark 2024-02-13 22:23:42 +02:00
Pete Matsyburka 84b762b529 report errors 2024-02-13 22:02:15 +02:00
Pete Matsyburka 2a839951b4 clone tenant tempaltes 2024-02-13 18:24:51 +02:00
Pete Matsyburka 26d7c2f047 add signature format settings 2024-02-12 21:36:06 +02:00
Pete Matsyburka 151a5b85f3 configure rollbar 2024-02-12 17:36:47 +02:00
Pete Matsyburka 3f506f53b4 add encrypted user configs controller 2024-02-12 12:26:22 +02:00
Pete Matsyburka 95f734e4b8 adjust cloud logs 2024-02-11 15:25:10 +02:00
Pete Matsyburka ff74574e84 adjust api completion 2024-02-10 21:03:12 +02:00
Pete Matsyburka 843f6c405f max 20 signer parties 2024-02-10 11:31:20 +02:00
Pete Matsyburka f8b24ebfce retry webhooks on non 200 status 2024-02-10 11:31:19 +02:00
Pete Matsyburka d1d26b5424 increase canvas resolution 2024-02-09 22:33:18 +02:00
Pete Matsyburka 8aa6f45da8 fix signature image reupload 2024-02-09 21:24:30 +02:00
Pete Matsyburka 48d5bee95b parse unix date 2024-02-09 15:03:20 +02:00
Pete Matsyburka 98c79d2dec skip anotation if nil 2024-02-09 00:43:13 +02:00
Pete Matsyburka 0f51238f62 fix default date 2024-02-08 23:29:18 +02:00
Pete Matsyburka b7323b929f set signing date option 2024-02-08 22:55:13 +02:00
Pete Matsyburka 05eb1537f7 replace checkmark emoji 2024-02-08 21:44:27 +02:00
Pete Matsyburka bc9f27cfed allow to aunarchive template 2024-02-08 20:49:10 +02:00
Pete Matsyburka 8f457be719 fix default values 2024-02-08 20:12:08 +02:00
Pete Matsyburka 6d590e13c9 flatten pdf form when signing 2024-02-08 18:06:55 +02:00
Pete Matsyburka c987ec172a fix canvas intersection observer 2024-02-08 12:11:10 +02:00
Pete Matsyburka 49f1637c47 add more submitter parties 2024-02-08 11:54:54 +02:00
Pete Matsyburka 13379bd699 resize canvas on visibility change 2024-02-07 01:47:57 +02:00
Pete Matsyburka 5ee762c08c adjust update tempalte api 2024-02-06 20:07:15 +02:00
Pete Matsyburka edb6d561f1 allow to add checked checkboxes in builder 2024-02-05 23:03:45 +02:00
Pete Matsyburka 1b90f77e82 skip errors in pdf processor 2024-02-05 13:54:55 +02:00
Pete Matsyburka c37785ecbc update on-prem links 2024-02-03 22:52:41 +02:00
Pete Matsyburka c67188edf8 ability to share template with testing account 2024-02-03 22:37:24 +02:00
Pete Matsyburka 8630c68631 add account_id to submissions 2024-02-03 18:56:45 +02:00
Pete Matsyburka ddcb5c5d40 adjust params validator 2024-02-03 11:37:05 +02:00
Pete Matsyburka 07f0fbb50f add submitter metadata 2024-02-03 10:29:45 +02:00
Pete Matsyburka 3781b0aa72 application_key -> external_id 2024-02-03 10:29:45 +02:00
Pete Matsyburka 42a5cea521 remove verify link from api source 2024-02-02 19:29:01 +02:00
Pete Matsyburka fb6000ca2c add builder title container id 2024-02-01 20:59:03 +02:00
Pete Matsyburka 59f98db754 fix NL i18n 2024-02-01 15:14:52 +02:00
Pete Matsyburka 6add842a27 api match lowercase field names 2024-02-01 13:34:30 +02:00
Pete Matsyburka 43ca4a6365 update readme contact link 2024-02-01 12:48:20 +02:00
Pete Matsyburka 68196d6af0 better on prem console sign up 2024-01-31 02:52:43 +02:00
Pete Matsyburka a582d39bb9 add preview image url to templates 2024-01-30 17:53:25 +02:00
Pete Matsyburka 7a3e89c4d8 allow to disable resubmit button 2024-01-30 17:03:58 +02:00
Alex Turchyn 87f036ad88 optimize template page for mobile devices 2024-01-30 11:45:52 +02:00
Pete Matsyburka 5738b8dd58 fix submissions params validator 2024-01-30 01:46:30 +02:00
Pete Matsyburka 8d73371e0f do not add email signing link automatically with API 2024-01-29 23:37:22 +02:00
128 changed files with 1803 additions and 636 deletions

No files matched your search

+2 -2
View File
@@ -30,7 +30,7 @@ Metrics/ParameterLists:
Max: 10 Max: 10
Metrics/MethodLength: Metrics/MethodLength:
Max: 25 Max: 30
Exclude: Exclude:
- 'db/migrate/**' - 'db/migrate/**'
@@ -44,7 +44,7 @@ Layout/LineLength:
AllowedPatterns: ['\A\s*#'] AllowedPatterns: ['\A\s*#']
Metrics/AbcSize: Metrics/AbcSize:
Max: 35 Max: 40
Metrics/ModuleLength: Metrics/ModuleLength:
Max: 500 Max: 500
+2 -4
View File
@@ -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
View File
@@ -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)
+25 -13
View File
@@ -33,17 +33,29 @@ DocuSeal is an open source platform that provides secure and efficient digital d
[![Demo](https://github.com/docusealco/docuseal/assets/5418788/d8703ea3-361a-423f-8bfe-eff1bd9dbe14)](https://demo.docuseal.co) [![Demo](https://github.com/docusealco/docuseal/assets/5418788/d8703ea3-361a-423f-8bfe-eff1bd9dbe14)](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,11 +89,11 @@ 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.
[Book a Meeting](https://calendly.com/kriti-docuseal/30min) [Book a Meeting](https://www.docuseal.co/contact)
## License ## License
+5
View File
@@ -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.**
@@ -7,6 +7,7 @@ class AccountConfigsController < ApplicationController
ALLOWED_KEYS = [ ALLOWED_KEYS = [
AccountConfig::ALLOW_TYPED_SIGNATURE, AccountConfig::ALLOW_TYPED_SIGNATURE,
AccountConfig::FORCE_MFA, AccountConfig::FORCE_MFA,
AccountConfig::ALLOW_TO_RESUBMIT,
AccountConfig::ESIGNING_PREFERENCE_KEY AccountConfig::ESIGNING_PREFERENCE_KEY
].freeze ].freeze
@@ -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
+10 -2
View File
@@ -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
+31 -12
View File
@@ -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
@@ -132,8 +151,8 @@ module Api
include: { include: {
submitters: { only: %i[id slug uuid name email phone submitters: { only: %i[id slug uuid name email phone
completed_at opened_at sent_at completed_at opened_at sent_at
created_at updated_at], created_at updated_at external_id metadata],
methods: %i[status] }, methods: %i[status application_key] },
template: { only: %i[id name created_at updated_at] }, template: { only: %i[id name created_at updated_at] },
created_by_user: { only: %i[id email first_name last_name] } created_by_user: { only: %i[id email first_name last_name] }
} }
@@ -146,8 +165,8 @@ module Api
{ {
message: %i[subject body], message: %i[subject body],
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role, submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :completed, :phone, :application_key, :external_id,
{ values: {}, readonly_fields: [], message: %i[subject body], { metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
fields: [%i[name default_value title description fields: [%i[name default_value title description
readonly validation_pattern invalid_message]] }]] readonly validation_pattern invalid_message]] }]]
} }
@@ -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)
+10 -4
View File
@@ -7,7 +7,8 @@ module Api
def index def index
submitters = Submitters.search(@submitters, params[:q]) submitters = Submitters.search(@submitters, params[:q])
submitters = submitters.where(application_key: params[:application_key]) if params[:application_key].present? submitters = submitters.where(external_id: params[:application_key]) if params[:application_key].present?
submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present?
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present? submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
submitters = paginate( submitters = paginate(
@@ -54,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?
@@ -69,8 +72,9 @@ module Api
submitter_params = params.key?(:submitter) ? params.require(:submitter) : params submitter_params = params.key?(:submitter) ? params.require(:submitter) : params
submitter_params.permit( submitter_params.permit(
:send_email, :send_sms, :uuid, :name, :email, :role, :completed, :phone, :application_key, :send_email, :send_sms, :uuid, :name, :email, :role,
{ values: {}, readonly_fields: [], message: %i[subject body], :completed, :phone, :application_key, :external_id,
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
fields: [%i[name default_value readonly validation_pattern invalid_message]] } fields: [%i[name default_value readonly validation_pattern invalid_message]] }
) )
end end
@@ -82,7 +86,9 @@ module Api
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '') if attrs.key?(:phone) submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '') if attrs.key?(:phone)
submitter.values = submitter.values.merge(attrs[:values].to_unsafe_h) if attrs[:values].present? submitter.values = submitter.values.merge(attrs[:values].to_unsafe_h) if attrs[:values].present?
submitter.completed_at = attrs[:completed] ? Time.current : submitter.completed_at submitter.completed_at = attrs[:completed] ? Time.current : submitter.completed_at
submitter.application_key = attrs[:application_key] if attrs.key?(:application_key) submitter.external_id = attrs[:application_key] if attrs.key?(:application_key)
submitter.external_id = attrs[:external_id] if attrs.key?(:external_id)
submitter.metadata = attrs[:metadata] if attrs.key?(:metadata)
assign_submission_fields(submitter.submission) assign_submission_fields(submitter.submission)
assign_preferences(submitter, attrs) assign_preferences(submitter, attrs)
@@ -7,11 +7,13 @@ module Api
def create def create
authorize!(:manage, @template) authorize!(:manage, @template)
cloned_template = Templates::Clone.call(@template, cloned_template = Templates::Clone.call(
author: current_user, @template,
name: params[:name], author: current_user,
application_key: params[:application_key], name: params[:name],
folder_name: params[:folder_name]) external_id: params[:external_id].presence || params[:application_key],
folder_name: params[:folder_name]
)
cloned_template.source = :api cloned_template.source = :api
cloned_template.save! cloned_template.save!
+34 -15
View File
@@ -5,11 +5,7 @@ module Api
load_and_authorize_resource :template load_and_authorize_resource :template
def index def index
templates = Templates.search(@templates, params[:q]) templates = filter_templates(@templates, params)
templates = params[:archived] ? templates.archived : templates.active
templates = templates.where(application_key: params[:application_key]) if params[:application_key].present?
templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present?
templates = paginate(templates.preload(:author, documents_attachments: :blob)) templates = paginate(templates.preload(:author, documents_attachments: :blob))
@@ -28,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])
@@ -49,23 +51,40 @@ module Api
private private
def filter_templates(templates, params)
templates = Templates.search(templates, params[:q])
templates = params[:archived] ? templates.archived : templates.active
templates = templates.where(external_id: params[:application_key]) if params[:application_key].present?
templates = templates.where(external_id: params[:external_id]) if params[:external_id].present?
templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present?
templates
end
def serialize_params def serialize_params
{ {
methods: %i[application_key],
include: { author: { only: %i[id email first_name last_name] }, include: { author: { only: %i[id email first_name last_name] },
documents: { only: %i[id uuid], methods: %i[url filename] } } documents: { only: %i[id uuid], methods: %i[url preview_image_url filename] } }
} }
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] }
} }
+1 -1
View File
@@ -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
+7 -1
View File
@@ -52,7 +52,13 @@ class DashboardController < ApplicationController
def filter_templates(templates) def filter_templates(templates)
rel = templates.active.preload(:author).order(id: :desc) rel = templates.active.preload(:author).order(id: :desc)
rel = rel.where(folder_id: current_account.default_template_folder.id) if params[:q].blank?
if params[:q].blank?
shared_template_ids =
TemplateSharing.where(account_id: [current_account.id, TemplateSharing::ALL_ID]).select(:template_id)
rel = rel.where(folder_id: current_account.default_template_folder.id).or(rel.where(id: shared_template_ids))
end
Templates.search(rel, params[:q]) Templates.search(rel, params[:q])
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 -2
View File
@@ -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
+7 -2
View File
@@ -10,10 +10,13 @@ class StartFormController < ApplicationController
before_action :load_template before_action :load_template
def show def show
@submitter = @template.submissions.new.submitters.new(uuid: @template.submitters.first['uuid']) @submitter = @template.submissions.new(account_id: @template.account_id)
.submitters.new(uuid: @template.submitters.first['uuid'])
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 }
@@ -57,7 +60,8 @@ class StartFormController < ApplicationController
ip: request.remote_ip, ip: request.remote_ip,
ua: request.user_agent, ua: request.user_agent,
values: resubmit_submitter&.preferences&.fetch('default_values', nil) || {}, values: resubmit_submitter&.preferences&.fetch('default_values', nil) || {},
preferences: resubmit_submitter&.preferences.presence || { 'send_email' => true } preferences: resubmit_submitter&.preferences.presence || { 'send_email' => true },
metadata: resubmit_submitter&.metadata.presence || {}
) )
if submitter.values.present? if submitter.values.present?
@@ -67,6 +71,7 @@ class StartFormController < ApplicationController
end end
submitter.submission ||= Submission.new(template:, submitter.submission ||= Submission.new(template:,
account_id: template.account_id,
template_submitters: template.submitters, template_submitters: template.submitters,
source: :link) source: :link)
+1 -1
View File
@@ -75,6 +75,6 @@ class SubmissionsController < ApplicationController
end end
def load_template def load_template
@template = current_account.templates.find(params[:template_id]) @template = Template.accessible_by(current_ability).find(params[:template_id])
end end
end end
@@ -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
@@ -0,0 +1,21 @@
# frozen_string_literal: true
class TemplateSharingsTestingController < ApplicationController
load_and_authorize_resource :template, parent: true
before_action do
authorize!(:manage, TemplateSharing.new(template: @template))
end
def create
testing_account = Accounts.find_or_create_testing_user(true_user.account).account
if params[:value] == '1'
TemplateSharing.create!(ability: :manage, account: testing_account, template: @template)
else
TemplateSharing.find_by(template: @template, account: testing_account)&.destroy!
end
head :ok
end
end
@@ -0,0 +1,7 @@
# frozen_string_literal: true
class TemplatesCodeModalController < ApplicationController
load_and_authorize_resource :template
def show; end
end
+23 -5
View File
@@ -6,7 +6,7 @@ class TemplatesController < ApplicationController
before_action :load_base_template, only: %i[new create] before_action :load_base_template, only: %i[new create]
def show def show
submissions = @template.submissions submissions = @template.submissions.accessible_by(current_ability)
submissions = submissions.active if @template.archived_at.blank? submissions = submissions.active if @template.archived_at.blank?
submissions = Submissions.search(submissions, params[:q]) submissions = Submissions.search(submissions, params[:q])
@@ -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
@@ -47,15 +47,21 @@ class TemplatesController < ApplicationController
name: params.dig(:template, :name), name: params.dig(:template, :name),
folder_name: params[:folder_name]) folder_name: params[:folder_name])
else else
@template.account = current_account
@template.author = current_user @template.author = current_user
@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
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
@@ -80,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
@@ -87,6 +105,6 @@ class TemplatesController < ApplicationController
def load_base_template def load_base_template
return if params[:base_template_id].blank? return if params[:base_template_id].blank?
@base_template = current_account.templates.find_by(id: params[:base_template_id]) @base_template = Template.accessible_by(current_ability).find_by(id: params[:base_template_id])
end end
end end
@@ -0,0 +1,22 @@
# frozen_string_literal: true
class TemplatesPreviewController < ApplicationController
load_and_authorize_resource :template
def show
ActiveRecord::Associations::Preloader.new(
records: [@template],
associations: [schema_documents: { preview_images_attachments: :blob }]
).call
@template_data =
@template.as_json.merge(
documents: @template.schema_documents.as_json(
methods: %i[metadata signed_uuid],
include: { preview_images: { methods: %i[url metadata filename] } }
)
).to_json
render :show, layout: 'plain'
end
end
@@ -7,7 +7,7 @@ class TestingAccountsController < ApplicationController
authorize!(:manage, current_account) authorize!(:manage, current_account)
authorize!(:manage, current_user) authorize!(:manage, current_user)
impersonate_user(Accounts.find_or_create_testing_user(current_account)) impersonate_user(Accounts.find_or_create_testing_user(true_user.account))
redirect_back(fallback_location: root_path) redirect_back(fallback_location: root_path)
end end
+1
View File
@@ -88,6 +88,7 @@ window.customElements.define('template-builder', class extends HTMLElement {
backgroundColor: '#faf7f5', backgroundColor: '#faf7f5',
withPhone: this.dataset.withPhone === 'true', withPhone: this.dataset.withPhone === 'true',
withLogo: this.dataset.withLogo !== 'false', withLogo: this.dataset.withLogo !== 'false',
editable: this.dataset.editable !== 'false',
withPayment: this.dataset.withPayment === 'true', withPayment: this.dataset.withPayment === 'true',
currencies: (this.dataset.currencies || '').split(',').filter(Boolean), currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
acceptFileTypes: this.dataset.acceptFileTypes, acceptFileTypes: this.dataset.acceptFileTypes,
+1 -1
View File
@@ -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 {
+21 -11
View File
@@ -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
+14 -3
View File
@@ -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',
@@ -644,7 +655,7 @@ const nl = {
page: 'Pagina', page: 'Pagina',
select_your_option: 'Selecteer uw optie', select_your_option: 'Selecteer uw optie',
complete_hightlighted_checkboxes_and_click: 'Voltooi de gemarkeerde selectievakjes en klik', complete_hightlighted_checkboxes_and_click: 'Voltooi de gemarkeerde selectievakjes en klik',
submit: 'Verzenden', submit: 'Voltooien',
next: 'Volgende', next: 'Volgende',
click_to_upload: 'Klik om te uploaden', click_to_upload: 'Klik om te uploaden',
or_drag_and_drop_files: 'of sleep en plaats bestanden', or_drag_and_drop_files: 'of sleep en plaats bestanden',
@@ -664,11 +675,11 @@ const nl = {
use_international_format: 'Gebruik internationaal formaat: +1xxx', use_international_format: 'Gebruik internationaal formaat: +1xxx',
six_digits_code: '6-cijferige code', six_digits_code: '6-cijferige code',
change_phone_number: 'Wijzig telefoonnummer', change_phone_number: 'Wijzig telefoonnummer',
sending: 'Verzenden...', sending: 'Voltooien...',
resend_code: 'Code opnieuw verzenden', resend_code: 'Code opnieuw verzenden',
verification_code_has_been_resent: 'Verificatiecode is opnieuw verzonden via SMS', verification_code_has_been_resent: 'Verificatiecode is opnieuw verzonden via SMS',
please_fill_all_required_fields: 'Vul alle verplichte velden in', please_fill_all_required_fields: 'Vul alle verplichte velden in',
set_today: 'Stel vandaag in', set_today: 'Vandaag',
toggle_multiline_text: 'Schakel meerregelige tekst in/uit', toggle_multiline_text: 'Schakel meerregelige tekst in/uit',
draw_signature: 'Handtekening tekenen', draw_signature: 'Handtekening tekenen',
type_initial: 'Typ initialen', type_initial: 'Typ 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 () {
+7 -5
View File
@@ -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: {
+36 -4
View File
@@ -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',
@@ -5,6 +5,7 @@
> >
<div <div
v-if="$slots.buttons || withTitle" v-if="$slots.buttons || withTitle"
id="title_container"
class="flex justify-between py-1.5 items-center pr-4 top-0 z-10" class="flex justify-between py-1.5 items-center pr-4 top-0 z-10"
:class="{ sticky: withStickySubmitters || isBreakpointLg }" :class="{ sticky: withStickySubmitters || isBreakpointLg }"
:style="{ backgroundColor }" :style="{ backgroundColor }"
@@ -61,6 +62,7 @@
</span> </span>
</a> </a>
<button <button
v-if="editable"
class="base-button" class="base-button"
:class="{ disabled: isSaving }" :class="{ disabled: isSaving }"
v-bind="isSaving ? { disabled: true } : {}" v-bind="isSaving ? { disabled: true } : {}"
@@ -79,6 +81,15 @@
{{ t('save') }} {{ t('save') }}
</span> </span>
</button> </button>
<a
v-else
:href="`/templates/${template.id}`"
class="base-button"
>
<span class="hidden md:inline">
{{ t('back') }}
</span>
</a>
</template> </template>
</div> </div>
</div> </div>
@@ -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,
+1 -1
View File
@@ -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`
} }
}) })
}, },
+69 -4
View File
@@ -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 () {
+12 -1
View File
@@ -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: {
+12 -1
View File
@@ -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)
}, },
+21
View File
@@ -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,
+6 -4
View File
@@ -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
+4
View File
@@ -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,
+1
View File
@@ -27,6 +27,7 @@ class AccountConfig < ApplicationRecord
BCC_EMAILS = 'bcc_emails' BCC_EMAILS = 'bcc_emails'
FORCE_MFA = 'force_mfa' FORCE_MFA = 'force_mfa'
ALLOW_TYPED_SIGNATURE = 'allow_typed_signature' ALLOW_TYPED_SIGNATURE = 'allow_typed_signature'
ALLOW_TO_RESUBMIT = 'allow_to_resubmit'
SUBMITTER_REMAILERS = 'submitter_reminders' SUBMITTER_REMAILERS = 'submitter_reminders'
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button' FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
FORM_WITH_CONFETTI_KEY = 'form_with_confetti' FORM_WITH_CONFETTI_KEY = 'form_with_confetti'
+4 -2
View File
@@ -15,11 +15,13 @@
# template_submitters :text # template_submitters :text
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# account_id :bigint not null
# created_by_user_id :bigint # created_by_user_id :bigint
# template_id :bigint not null # template_id :bigint not null
# #
# Indexes # Indexes
# #
# index_submissions_on_account_id (account_id)
# index_submissions_on_created_by_user_id (created_by_user_id) # index_submissions_on_created_by_user_id (created_by_user_id)
# index_submissions_on_slug (slug) UNIQUE # index_submissions_on_slug (slug) UNIQUE
# index_submissions_on_template_id (template_id) # index_submissions_on_template_id (template_id)
@@ -31,7 +33,7 @@
# #
class Submission < ApplicationRecord class Submission < ApplicationRecord
belongs_to :template belongs_to :template
has_one :account, through: :template belongs_to :account
belongs_to :created_by_user, class_name: 'User', optional: true belongs_to :created_by_user, class_name: 'User', optional: true
has_many :submitters, dependent: :destroy has_many :submitters, dependent: :destroy
@@ -74,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
+2 -1
View File
@@ -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
+25 -18
View File
@@ -4,23 +4,24 @@
# #
# Table name: submitters # Table name: submitters
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# application_key :string # completed_at :datetime
# completed_at :datetime # email :string
# email :string # ip :string
# ip :string # metadata :text not null
# name :string # name :string
# opened_at :datetime # opened_at :datetime
# phone :string # phone :string
# preferences :text not null # preferences :text not null
# sent_at :datetime # sent_at :datetime
# slug :string not null # slug :string not null
# ua :string # ua :string
# uuid :string not null # uuid :string not null
# values :text not null # values :text not null
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# submission_id :bigint not null # external_id :string
# submission_id :bigint not null
# #
# Indexes # Indexes
# #
@@ -35,14 +36,16 @@
class Submitter < ApplicationRecord class Submitter < ApplicationRecord
belongs_to :submission belongs_to :submission
has_one :template, through: :submission has_one :template, through: :submission
has_one :account, through: :template has_one :account, through: :submission
attribute :values, :string, default: -> { {} } attribute :values, :string, default: -> { {} }
attribute :preferences, :string, default: -> { {} } attribute :preferences, :string, default: -> { {} }
attribute :metadata, :string, default: -> { {} }
attribute :slug, :string, default: -> { SecureRandom.base58(14) } attribute :slug, :string, default: -> { SecureRandom.base58(14) }
serialize :values, coder: JSON serialize :values, coder: JSON
serialize :preferences, coder: JSON serialize :preferences, coder: JSON
serialize :metadata, coder: JSON
has_many_attached :documents has_many_attached :documents
has_many_attached :attachments has_many_attached :attachments
@@ -62,6 +65,10 @@ class Submitter < ApplicationRecord
end end
end end
def application_key
external_id
end
def friendly_name def friendly_name
if name.present? && email.present? && email.exclude?(',') if name.present? && email.present? && email.exclude?(',')
%("#{name.delete('"')}" <#{email}>) %("#{name.delete('"')}" <#{email}>)
+19 -14
View File
@@ -4,20 +4,20 @@
# #
# Table name: templates # Table name: templates
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# application_key :string # archived_at :datetime
# archived_at :datetime # fields :text not null
# fields :text not null # name :string not null
# name :string not null # schema :text not null
# schema :text not null # slug :string not null
# slug :string not null # source :text not null
# source :text not null # submitters :text not null
# submitters :text not null # created_at :datetime not null
# created_at :datetime not null # updated_at :datetime not null
# updated_at :datetime not null # account_id :bigint not null
# account_id :bigint not null # author_id :bigint not null
# author_id :bigint not null # external_id :string
# folder_id :bigint not null # folder_id :bigint not null
# #
# Indexes # Indexes
# #
@@ -57,10 +57,15 @@ class Template < ApplicationRecord
class_name: 'ActiveStorage::Attachment', dependent: :destroy, as: :record, inverse_of: :record class_name: 'ActiveStorage::Attachment', dependent: :destroy, as: :record, inverse_of: :record
has_many :submissions, dependent: :destroy has_many :submissions, dependent: :destroy
has_many :template_sharings, dependent: :destroy
scope :active, -> { where(archived_at: nil) } scope :active, -> { where(archived_at: nil) }
scope :archived, -> { where.not(archived_at: nil) } scope :archived, -> { where.not(archived_at: nil) }
def application_key
external_id
end
private private
def maybe_set_default_folder def maybe_set_default_folder
+28
View File
@@ -0,0 +1,28 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: template_sharings
#
# id :bigint not null, primary key
# ability :string not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# template_id :bigint not null
#
# Indexes
#
# index_template_sharings_on_account_id_and_template_id (account_id,template_id) UNIQUE
# index_template_sharings_on_template_id (template_id)
#
# Foreign Keys
#
# fk_rails_... (template_id => templates.id)
#
class TemplateSharing < ApplicationRecord
ALL_ID = -1
belongs_to :template
belongs_to :account, optional: true
end
+1
View File
@@ -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
+12
View File
@@ -63,6 +63,18 @@
</div> </div>
<% end %> <% end %>
<% end %> <% end %>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::ALLOW_TO_RESUBMIT) %>
<% 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>
Allow to resubmit completed forms
</span>
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false, onchange: 'this.form.requestSubmit()' %>
</div>
<% end %>
<% end %>
</div> </div>
<% end %> <% end %>
<% if can?(:manage, current_account) && Docuseal.multitenant? && true_user == current_user %> <% if can?(:manage, current_account) && Docuseal.multitenant? && true_user == current_user %>
+10 -2
View File
@@ -1,6 +1,14 @@
<div class="max-w-lg mx-auto px-2"> <div class="max-w-lg mx-auto px-2">
<%= render 'devise/shared/select_server' if Docuseal.multitenant? %> <% if params[:redir].to_s.ends_with?('on_premise') %>
<h1 class="text-3xl font-bold text-center mt-8 mb-6">Create Free Account</h1> <h1 class="text-3xl font-bold text-center mt-8 mb-6">DocuSeal Console</h1>
<div class="alert my-4 text-sm">
Sign up in DocuSeal Console to upgrade.
On-premises app is completely standalone, Console is used only to manage your license.
</div>
<% else %>
<%= render 'devise/shared/select_server' if Docuseal.multitenant? %>
<h1 class="text-3xl font-bold text-center mt-8 mb-6">Create Free Account</h1>
<% end %>
<%= form_for(User.new, html: { class: 'space-y-6' }, url: new_registration_path, data: { turbo: params[:redir].blank? }, method: :get) do |f| %> <%= form_for(User.new, html: { class: 'space-y-6' }, url: new_registration_path, data: { turbo: params[:redir].blank? }, method: :get) do |f| %>
<% if params[:redir].present? %> <% if params[:redir].present? %>
<%= hidden_field_tag :redir, params[:redir] %> <%= hidden_field_tag :redir, params[:redir] %>
+17 -19
View File
@@ -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>
+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 8l-4 4l4 4" />
<path d="M17 8l4 4l-4 4" />
<path d="M14 4l-4 16" />
</svg>

After

Width:  |  Height:  |  Size: 369 B

@@ -5,7 +5,7 @@
<p> <p>
Send automatic email reminders to your recipients. Send automatic email reminders to your recipients.
<br> <br>
<a class="link font-medium" target="_blank" href="<%= "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">Learn More</a> <a class="link font-medium" target="_blank" href="<%= Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premise" }.to_query}" %>">Learn More</a>
</p> </p>
</div> </div>
</div> </div>
@@ -5,7 +5,7 @@
<p> <p>
Display your company name and logo when signing documents. Display your company name and logo when signing documents.
<br> <br>
<a class="link font-medium" target="_blank" href="<%= "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">Learn More</a> <a class="link font-medium" target="_blank" href="<%= Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premise" }.to_query}" %>">Learn More</a>
</p> </p>
</div> </div>
</div> </div>
+1 -1
View File
@@ -1,4 +1,4 @@
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'hidden md:inline-flex btn btn-warning btn-sm', data: { prefetch: false } do %> <%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premise" }.to_query}", class: 'hidden md:inline-flex btn btn-warning btn-sm', data: { prefetch: false } do %>
Upgrade Upgrade
<% end %> <% end %>
<% if signed_in? && current_user != true_user %> <% if signed_in? && current_user != true_user %>
+1 -1
View File
@@ -69,7 +69,7 @@
<%= render 'shared/settings_nav_extra' %> <%= render 'shared/settings_nav_extra' %>
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) %> <% if !Docuseal.demo? && can?(:manage, EncryptedConfig) %>
<li> <li>
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %> <%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premise" }.to_query}", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
Plans Plans
<span class="badge badge-warning">New</span> <span class="badge badge-warning">New</span>
<% end %> <% end %>
+1 -1
View File
@@ -5,7 +5,7 @@
<p class="text-gray-700"> <p class="text-gray-700">
Unlock with DocuSeal Pro Unlock with DocuSeal Pro
<br> <br>
<a class="link font-medium" target="_blank" href="<%= "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">Learn More</a> <a class="link font-medium" target="_blank" href="<%= Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premise" }.to_query}" %>">Learn More</a>
</p> </p>
</div> </div>
</div> </div>
+1 -1
View File
@@ -5,7 +5,7 @@
<p class="text-gray-700"> <p class="text-gray-700">
Unlock with DocuSeal Pro Unlock with DocuSeal Pro
<br> <br>
<a class="link font-medium" target="_blank" href="<%= "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">Learn More</a> <a class="link font-medium" target="_blank" href="<%= Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premise" }.to_query}" %>">Learn More</a>
</p> </p>
</div> </div>
</div> </div>
+1 -1
View File
@@ -1 +1 @@
<%= render 'docuseal_logo' %> <%= render 'start_form/docuseal_logo' %>
+3 -3
View File
@@ -19,16 +19,16 @@
<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>
<% end %> <% end %>
<% if @template.submitters.to_a.size == 1 %> <% if @template.submitters.to_a.size == 1 && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false %>
<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(@template.slug), params: { submitter: { email: params[:email] }, 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(@template.slug), params: { submitter: { email: params[:email] }, resubmit: @submitter.slug }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
</div> </div>
<% end %> <% end %>
</div> </div>
</div> </div>
<%= render 'shared/attribution', link_path: '/start' %> <%= render 'shared/attribution', link_path: '/start', account: @submitter.account %>
+1 -1
View File
@@ -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 %>
+1 -175
View File
@@ -6,181 +6,7 @@
</div> </div>
</div> </div>
<div class="divider uppercase normal-case mt-5 mb-6 text-neutral-700">Or embed on your website</div> <div class="divider uppercase normal-case mt-5 mb-6 text-neutral-700">Or embed on your website</div>
<toggle-visible data-element-ids="[&quot;js_1&quot;,&quot;react_1&quot;,&quot;vue_1&quot;]" class="block relative" data-catalyst=""> <%= render 'templates/embedding', template: %>
<ul class="items-center w-full text-sm font-medium text-gray-900 my-4 space-y-2 sm:space-y-0 sm:flex sm:space-x-2">
<li class="w-full h-10 text-sm font-medium flex items-center relative group py-3.5">
<input type="radio" name="option_1" id="js_radio_1" value="js_1" data-action="change:toggle-visible#trigger" class="relative peer z-10 hidden" checked="checked">
<label for="js_radio_1" class="absolute border-neutral-focus space-x-2 border rounded-xl left-0 right-0 top-0 bottom-0 flex items-center justify-center group-hover:bg-neutral group-hover:text-white peer-checked:btn-neutral">
<span><svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" x="0px" y="0px" width="100" height="100" viewBox="0 0 48 48">
<path fill="#ffd600" d="M6,42V6h36v36H6z"></path><path fill="#000001" d="M29.538 32.947c.692 1.124 1.444 2.201 3.037 2.201 1.338 0 2.04-.665 2.04-1.585 0-1.101-.726-1.492-2.198-2.133l-.807-.344c-2.329-.988-3.878-2.226-3.878-4.841 0-2.41 1.845-4.244 4.728-4.244 2.053 0 3.528.711 4.592 2.573l-2.514 1.607c-.553-.988-1.151-1.377-2.078-1.377-.946 0-1.545.597-1.545 1.377 0 .964.6 1.354 1.985 1.951l.807.344C36.452 29.645 38 30.839 38 33.523 38 36.415 35.716 38 32.65 38c-2.999 0-4.702-1.505-5.65-3.368L29.538 32.947zM17.952 33.029c.506.906 1.275 1.603 2.381 1.603 1.058 0 1.667-.418 1.667-2.043V22h3.333v11.101c0 3.367-1.953 4.899-4.805 4.899-2.577 0-4.437-1.746-5.195-3.368L17.952 33.029z"></path>
</svg>
</span>
<span>JavaScript</span>
</label>
</li>
<li class="w-full h-10 text-sm font-medium flex items-center relative group py-3.5">
<input type="radio" name="option_1" id="react_radio_1" value="react_1" data-action="change:toggle-visible#trigger" class="relative peer z-10 hidden">
<label for="react_radio_1" class="absolute border-neutral-focus space-x-2 border rounded-xl left-0 right-0 top-0 bottom-0 flex items-center justify-center group-hover:bg-neutral group-hover:text-white peer-checked:btn-neutral">
<span><svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="-11.5 -10.23174 23 20.46348">
<circle cx="0" cy="0" r="2.05" fill="#61dafb"></circle>
<g stroke="#61dafb" stroke-width="1" fill="none">
<ellipse rx="11" ry="4.2"></ellipse>
<ellipse rx="11" ry="4.2" transform="rotate(60)"></ellipse>
<ellipse rx="11" ry="4.2" transform="rotate(120)"></ellipse>
</g>
</svg>
</span>
<span>React</span>
</label>
</li>
<li class="w-full h-10 text-sm font-medium flex items-center relative group py-3.5">
<input type="radio" name="option_1" id="vue_radio_1" value="vue_1" data-action="change:toggle-visible#trigger" class="relative peer z-10 hidden">
<label for="vue_radio_1" class="absolute border-neutral-focus space-x-2 border rounded-xl left-0 right-0 top-0 bottom-0 flex items-center justify-center group-hover:bg-neutral group-hover:text-white peer-checked:btn-neutral">
<span><svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" version="1.1" viewBox="0 0 261.76 226.69"><g transform="matrix(1.3333 0 0 -1.3333 -76.311 313.34)"><g transform="translate(178.06 235.01)"><path d="m0 0-22.669-39.264-22.669 39.264h-75.491l98.16-170.02 98.16 170.02z" fill="#41b883"></path></g><g transform="translate(178.06 235.01)"><path d="m0 0-22.669-39.264-22.669 39.264h-36.227l58.896-102.01 58.896 102.01z" fill="#34495e"></path></g></g></svg>
</span>
<span>Vue</span>
</label>
</li>
</ul>
</toggle-visible>
<div id="js_1" class="block my-4">
<div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex">
<%= link_to 'Learn More', console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<clipboard-copy data-text="<script src=&quot;<%= Docuseal::CDN_URL %>/js/form.js&quot;></script>
<docuseal-form data-src=&quot;<%= start_form_url(slug: template.slug) %>&quot;></docuseal-form>
">
<label class="btn btn-ghost text-gray-100">
<input type="radio" class="peer hidden">
<span class="peer-checked:hidden flex items-center space-x-2">
<%= svg_icon('copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copy
</span>
</span>
<span class="hidden peer-checked:flex items-center space-x-2">
<%= svg_icon('clipboard_copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copied
</span>
</span>
</label>
</clipboard-copy>
</span>
<pre class="before:!m-0 pl-6 pb-4 overflow-auto"><code class="overflow-hidden w-full"><span style="color: #f4bf75">&lt;script </span><span style="color: #6a9fb5">src=</span><span style="color: #90a959">"<%= Docuseal::CDN_URL %>/js/form.js"</span><span style="color: #f4bf75">&gt;&lt;/script&gt;</span>
<span style="color: #f4bf75">&lt;docuseal-form</span> <span style="color: #6a9fb5">data-src=</span><span style="color: #90a959">"<%= start_form_url(slug: template.slug) %>"</span><span style="color: #f4bf75">&gt;</span><span style="color: #f4bf75">&lt;/docuseal-form&gt;</span>
</code></pre>
</div>
</div>
<div id="react_1" class="block my-4 hidden">
<div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex">
<%= link_to 'Learn More', console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<clipboard-copy data-text="import React from &quot;react&quot;
import { DocusealForm } from '@docuseal/react'
export function App() {
return (
<DocusealForm
src=&quot;<%= start_form_url(slug: template.slug) %>&quot;
/>
);
}
">
<label class="btn btn-ghost text-white">
<input type="radio" class="peer hidden">
<span class="peer-checked:hidden flex items-center space-x-2">
<%= svg_icon('copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copy
</span>
</span>
<span class="hidden peer-checked:flex items-center space-x-2">
<%= svg_icon('clipboard_copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copied
</span>
</span>
</label>
</clipboard-copy>
</span>
<pre class="before:!m-0 pl-6 pb-4 overflow-auto"><code class="overflow-hidden w-full"><span style="color: #aa759f">import</span> <span style="color: #d0d0d0;background-color: #151515">React</span> <span style="color: #aa759f">from</span> <span style="color: #90a959">"</span><span style="color: #90a959">react</span><span style="color: #90a959">"</span>
<span style="color: #aa759f">import</span> <span style="color: #d0d0d0">{</span> <span style="color: #d0d0d0;background-color: #151515">DocusealForm</span> <span style="color: #d0d0d0">}</span> <span style="color: #aa759f">from</span> <span style="color: #90a959">'</span><span style="color: #90a959">@docuseal/react</span><span style="color: #90a959">'</span>
<span style="color: #aa759f">export</span> <span style="color: #d28445">function</span> <span style="color: #d0d0d0;background-color: #151515">App</span><span style="color: #d0d0d0">()</span> <span style="color: #d0d0d0">{</span>
<span style="color: #aa759f">return </span><span style="color: #d0d0d0">(</span>
<span style="color: #d0d0d0">&lt;</span><span style="color: #f4bf75">DocusealForm</span>
<span style="color: #6a9fb5">src</span><span style="color: #d0d0d0">=</span><span style="color: #90a959">"<%= start_form_url(slug: template.slug) %>"</span>
<span style="color: #d0d0d0">/&gt;</span>
<span style="color: #d0d0d0">);</span>
<span style="color: #d0d0d0">}</span>
</code></pre>
</div>
</div>
<div id="vue_1" class="block my-4 hidden">
<div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex">
<%= link_to 'Learn More', console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<clipboard-copy data-text="<template>
<DocusealForm
:src=&quot;'<%= start_form_url(slug: template.slug) %>'&quot;
/>
</template>
<script>
import { DocusealForm } from '@docuseal/vue'
export default {
name: 'App',
components: {
DocusealForm
}
}
</script>
">
<label class="btn btn-ghost text-white">
<input type="radio" class="peer hidden">
<span class="peer-checked:hidden flex items-center space-x-2">
<%= svg_icon('copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copy
</span>
</span>
<span class="hidden peer-checked:flex items-center space-x-2">
<%= svg_icon('clipboard_copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copied
</span>
</span>
</label>
</clipboard-copy>
</span>
<pre class="before:!m-0 pl-6 pb-4 overflow-auto"><code class="overflow-hidden w-full"><span style="color: #f4bf75">&lt;template&gt;</span>
<span style="color: #f4bf75">&lt;DocusealForm</span>
<span style="color: #6a9fb5">:src=</span><span style="color: #90a959">"'<%= start_form_url(slug: template.slug) %>'"</span>
<span style="color: #f4bf75">/&gt;</span>
<span style="color: #f4bf75">&lt;/template&gt;</span>
<span style="color: #f4bf75">&lt;</span><span style="color: #f4bf75">script</span><span style="color: #f4bf75">&gt;</span>
<span style="color: #aa759f">import</span> <span style="color: #d0d0d0">{</span> <span style="color: #d0d0d0;background-color: #151515">DocusealForm</span> <span style="color: #d0d0d0">}</span> <span style="color: #aa759f">from</span> <span style="color: #90a959">'</span><span style="color: #90a959">@docuseal/vue</span><span style="color: #90a959">'</span>
<span style="color: #aa759f">export</span> <span style="color: #aa759f">default</span> <span style="color: #d0d0d0">{</span>
<span style="color: #6a9fb5">name</span><span style="color: #d0d0d0">:</span> <span style="color: #90a959">'</span><span style="color: #90a959">App</span><span style="color: #90a959">'</span><span style="color: #d0d0d0">,</span>
<span style="color: #6a9fb5">components</span><span style="color: #d0d0d0">:</span> <span style="color: #d0d0d0">{</span>
<span style="color: #d0d0d0;background-color: #151515">DocusealForm</span>
<span style="color: #d0d0d0">}</span>
<span style="color: #d0d0d0">}</span>
<span style="color: #f4bf75">&lt;/</span><span style="color: #f4bf75">script</span><span style="color: #f4bf75">&gt;</span>
</code></pre>
</div>
</div>
<div class="mt-4"> <div class="mt-4">
<a href="#" class="link text-center block" data-action="click:turbo-modal#close">Close</a> <a href="#" class="link text-center block" data-action="click:turbo-modal#close">Close</a>
</div> </div>
@@ -1,5 +1,5 @@
<div class="mt-2 mb-1"> <div class="mt-2 mb-1">
<div class="tooltip w-full" data-tip="Unlock with Enterpise"> <div class="tooltip w-full" data-tip="Unlock with Enterpise">
<%= link_to submitter.sent_at? ? 'Re-send SMS' : 'Send SMS', "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}", class: 'btn btn-sm btn-primary text-gray-400 w-full' %> <%= link_to submitter.sent_at? ? 'Re-send SMS' : 'Send SMS', Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premise" }.to_query}", class: 'btn btn-sm btn-primary text-gray-400 w-full' %>
</div> </div>
</div> </div>
+6 -6
View File
@@ -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 %>
+2 -2
View File
@@ -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 %>
+15 -13
View File
@@ -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) %> <% 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 %>
+3 -3
View File
@@ -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>
@@ -1,7 +1,7 @@
<% if @email_config || @body.present? %> <% if @email_config || @body.present? %>
<% body = (@body.presence || @email_config.value['body']).to_s %> <% body = (@body.presence || @email_config.value['body']).to_s %>
<%= auto_link(simple_format(h(ReplaceEmailVariables.call(body, submitter: @submitter)))) %> <%= auto_link(simple_format(h(ReplaceEmailVariables.call(body, submitter: @submitter)))) %>
<% if !body.include?(ReplaceEmailVariables::SUBMITTER_LINK) && !body.include?(ReplaceEmailVariables::SUBMITTER_ID) && !body.include?(ReplaceEmailVariables::SUBMISSION_LINK) && !body.include?(ReplaceEmailVariables::TEMPLATE_ID) %> <% if !body.include?(ReplaceEmailVariables::SUBMITTER_LINK) && !body.include?(ReplaceEmailVariables::SUBMITTER_ID) && !body.include?(ReplaceEmailVariables::SUBMISSION_LINK) && !body.include?(ReplaceEmailVariables::TEMPLATE_ID) && !@submitter.submission.source.in?(%w[api embed]) %>
<p><%= link_to nil, submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email')) %></p> <p><%= link_to nil, submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email')) %></p>
<% end %> <% end %>
<% else %> <% else %>
+175
View File
@@ -0,0 +1,175 @@
<toggle-visible data-element-ids="[&quot;js_1&quot;,&quot;react_1&quot;,&quot;vue_1&quot;]" class="block relative" data-catalyst="">
<ul class="items-center w-full text-sm font-medium text-gray-900 my-4 space-y-2 sm:space-y-0 sm:flex sm:space-x-2">
<li class="w-full h-10 text-sm font-medium flex items-center relative group py-3.5">
<input type="radio" name="option_1" id="js_radio_1" value="js_1" data-action="change:toggle-visible#trigger" class="relative peer z-10 hidden" checked="checked">
<label for="js_radio_1" class="absolute border-neutral-focus space-x-2 border rounded-xl left-0 right-0 top-0 bottom-0 flex items-center justify-center group-hover:bg-neutral group-hover:text-white peer-checked:btn-neutral">
<span><svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" x="0px" y="0px" width="100" height="100" viewBox="0 0 48 48">
<path fill="#ffd600" d="M6,42V6h36v36H6z"></path><path fill="#000001" d="M29.538 32.947c.692 1.124 1.444 2.201 3.037 2.201 1.338 0 2.04-.665 2.04-1.585 0-1.101-.726-1.492-2.198-2.133l-.807-.344c-2.329-.988-3.878-2.226-3.878-4.841 0-2.41 1.845-4.244 4.728-4.244 2.053 0 3.528.711 4.592 2.573l-2.514 1.607c-.553-.988-1.151-1.377-2.078-1.377-.946 0-1.545.597-1.545 1.377 0 .964.6 1.354 1.985 1.951l.807.344C36.452 29.645 38 30.839 38 33.523 38 36.415 35.716 38 32.65 38c-2.999 0-4.702-1.505-5.65-3.368L29.538 32.947zM17.952 33.029c.506.906 1.275 1.603 2.381 1.603 1.058 0 1.667-.418 1.667-2.043V22h3.333v11.101c0 3.367-1.953 4.899-4.805 4.899-2.577 0-4.437-1.746-5.195-3.368L17.952 33.029z"></path>
</svg>
</span>
<span>JavaScript</span>
</label>
</li>
<li class="w-full h-10 text-sm font-medium flex items-center relative group py-3.5">
<input type="radio" name="option_1" id="react_radio_1" value="react_1" data-action="change:toggle-visible#trigger" class="relative peer z-10 hidden">
<label for="react_radio_1" class="absolute border-neutral-focus space-x-2 border rounded-xl left-0 right-0 top-0 bottom-0 flex items-center justify-center group-hover:bg-neutral group-hover:text-white peer-checked:btn-neutral">
<span><svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="-11.5 -10.23174 23 20.46348">
<circle cx="0" cy="0" r="2.05" fill="#61dafb"></circle>
<g stroke="#61dafb" stroke-width="1" fill="none">
<ellipse rx="11" ry="4.2"></ellipse>
<ellipse rx="11" ry="4.2" transform="rotate(60)"></ellipse>
<ellipse rx="11" ry="4.2" transform="rotate(120)"></ellipse>
</g>
</svg>
</span>
<span>React</span>
</label>
</li>
<li class="w-full h-10 text-sm font-medium flex items-center relative group py-3.5">
<input type="radio" name="option_1" id="vue_radio_1" value="vue_1" data-action="change:toggle-visible#trigger" class="relative peer z-10 hidden">
<label for="vue_radio_1" class="absolute border-neutral-focus space-x-2 border rounded-xl left-0 right-0 top-0 bottom-0 flex items-center justify-center group-hover:bg-neutral group-hover:text-white peer-checked:btn-neutral">
<span><svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" version="1.1" viewBox="0 0 261.76 226.69"><g transform="matrix(1.3333 0 0 -1.3333 -76.311 313.34)"><g transform="translate(178.06 235.01)"><path d="m0 0-22.669-39.264-22.669 39.264h-75.491l98.16-170.02 98.16 170.02z" fill="#41b883"></path></g><g transform="translate(178.06 235.01)"><path d="m0 0-22.669-39.264-22.669 39.264h-36.227l58.896-102.01 58.896 102.01z" fill="#34495e"></path></g></g></svg>
</span>
<span>Vue</span>
</label>
</li>
</ul>
</toggle-visible>
<div id="js_1" class="block my-4">
<div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex">
<%= link_to 'Learn More', console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<clipboard-copy data-text="<script src=&quot;<%= Docuseal::CDN_URL %>/js/form.js&quot;></script>
<docuseal-form data-src=&quot;<%= start_form_url(slug: template.slug) %>&quot;></docuseal-form>
">
<label class="btn btn-ghost text-gray-100">
<input type="radio" class="peer hidden">
<span class="peer-checked:hidden flex items-center space-x-2">
<%= svg_icon('copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copy
</span>
</span>
<span class="hidden peer-checked:flex items-center space-x-2">
<%= svg_icon('clipboard_copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copied
</span>
</span>
</label>
</clipboard-copy>
</span>
<pre class="before:!m-0 pl-6 pb-4 overflow-auto"><code class="overflow-hidden w-full"><span style="color: #f4bf75">&lt;script </span><span style="color: #6a9fb5">src=</span><span style="color: #90a959">"<%= Docuseal::CDN_URL %>/js/form.js"</span><span style="color: #f4bf75">&gt;&lt;/script&gt;</span>
<span style="color: #f4bf75">&lt;docuseal-form</span> <span style="color: #6a9fb5">data-src=</span><span style="color: #90a959">"<%= start_form_url(slug: template.slug) %>"</span><span style="color: #f4bf75">&gt;</span><span style="color: #f4bf75">&lt;/docuseal-form&gt;</span>
</code></pre>
</div>
</div>
<div id="react_1" class="block my-4 hidden">
<div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex">
<%= link_to 'Learn More', console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<clipboard-copy data-text="import React from &quot;react&quot;
import { DocusealForm } from '@docuseal/react'
export function App() {
return (
<DocusealForm
src=&quot;<%= start_form_url(slug: template.slug) %>&quot;
/>
);
}
">
<label class="btn btn-ghost text-white">
<input type="radio" class="peer hidden">
<span class="peer-checked:hidden flex items-center space-x-2">
<%= svg_icon('copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copy
</span>
</span>
<span class="hidden peer-checked:flex items-center space-x-2">
<%= svg_icon('clipboard_copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copied
</span>
</span>
</label>
</clipboard-copy>
</span>
<pre class="before:!m-0 pl-6 pb-4 overflow-auto"><code class="overflow-hidden w-full"><span style="color: #aa759f">import</span> <span style="color: #d0d0d0;background-color: #151515">React</span> <span style="color: #aa759f">from</span> <span style="color: #90a959">"</span><span style="color: #90a959">react</span><span style="color: #90a959">"</span>
<span style="color: #aa759f">import</span> <span style="color: #d0d0d0">{</span> <span style="color: #d0d0d0;background-color: #151515">DocusealForm</span> <span style="color: #d0d0d0">}</span> <span style="color: #aa759f">from</span> <span style="color: #90a959">'</span><span style="color: #90a959">@docuseal/react</span><span style="color: #90a959">'</span>
<span style="color: #aa759f">export</span> <span style="color: #d28445">function</span> <span style="color: #d0d0d0;background-color: #151515">App</span><span style="color: #d0d0d0">()</span> <span style="color: #d0d0d0">{</span>
<span style="color: #aa759f">return </span><span style="color: #d0d0d0">(</span>
<span style="color: #d0d0d0">&lt;</span><span style="color: #f4bf75">DocusealForm</span>
<span style="color: #6a9fb5">src</span><span style="color: #d0d0d0">=</span><span style="color: #90a959">"<%= start_form_url(slug: template.slug) %>"</span>
<span style="color: #d0d0d0">/&gt;</span>
<span style="color: #d0d0d0">);</span>
<span style="color: #d0d0d0">}</span>
</code></pre>
</div>
</div>
<div id="vue_1" class="block my-4 hidden">
<div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex">
<%= link_to 'Learn More', console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<clipboard-copy data-text="<template>
<DocusealForm
:src=&quot;'<%= start_form_url(slug: template.slug) %>'&quot;
/>
</template>
<script>
import { DocusealForm } from '@docuseal/vue'
export default {
name: 'App',
components: {
DocusealForm
}
}
</script>
">
<label class="btn btn-ghost text-white">
<input type="radio" class="peer hidden">
<span class="peer-checked:hidden flex items-center space-x-2">
<%= svg_icon('copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copy
</span>
</span>
<span class="hidden peer-checked:flex items-center space-x-2">
<%= svg_icon('clipboard_copy', class: 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
Copied
</span>
</span>
</label>
</clipboard-copy>
</span>
<pre class="before:!m-0 pl-6 pb-4 overflow-auto"><code class="overflow-hidden w-full"><span style="color: #f4bf75">&lt;template&gt;</span>
<span style="color: #f4bf75">&lt;DocusealForm</span>
<span style="color: #6a9fb5">:src=</span><span style="color: #90a959">"'<%= start_form_url(slug: template.slug) %>'"</span>
<span style="color: #f4bf75">/&gt;</span>
<span style="color: #f4bf75">&lt;/template&gt;</span>
<span style="color: #f4bf75">&lt;</span><span style="color: #f4bf75">script</span><span style="color: #f4bf75">&gt;</span>
<span style="color: #aa759f">import</span> <span style="color: #d0d0d0">{</span> <span style="color: #d0d0d0;background-color: #151515">DocusealForm</span> <span style="color: #d0d0d0">}</span> <span style="color: #aa759f">from</span> <span style="color: #90a959">'</span><span style="color: #90a959">@docuseal/vue</span><span style="color: #90a959">'</span>
<span style="color: #aa759f">export</span> <span style="color: #aa759f">default</span> <span style="color: #d0d0d0">{</span>
<span style="color: #6a9fb5">name</span><span style="color: #d0d0d0">:</span> <span style="color: #90a959">'</span><span style="color: #90a959">App</span><span style="color: #90a959">'</span><span style="color: #d0d0d0">,</span>
<span style="color: #6a9fb5">components</span><span style="color: #d0d0d0">:</span> <span style="color: #d0d0d0">{</span>
<span style="color: #d0d0d0;background-color: #151515">DocusealForm</span>
<span style="color: #d0d0d0">}</span>
<span style="color: #d0d0d0">}</span>
<span style="color: #f4bf75">&lt;/</span><span style="color: #f4bf75">script</span><span style="color: #f4bf75">&gt;</span>
</code></pre>
</div>
</div>
+53 -45
View File
@@ -1,12 +1,12 @@
<% status_badges = { 'awaiting' => 'badge-info', 'sent' => 'badge-info', 'completed' => 'badge-success', 'opened' => 'badge-warning' } %> <% status_badges = { 'awaiting' => 'badge-info', 'sent' => 'badge-info', 'completed' => 'badge-success', 'opened' => 'badge-warning' } %>
<a href="<%= submission_path(submission) %>" class="bg-base-200 w-full flex flex-col md:flex-row space-y-4 md:space-y-0 md:justify-between rounded-2xl px-6 py-5 md:items-center"> <a href="<%= submission_path(submission) %>" class="bg-base-200 w-full flex flex-col md:flex-row space-y-4 md:space-y-0 md:justify-between rounded-2xl px-5 md:px-6 py-5 md:items-center">
<% submitters = (submission.template_submitters || submission.template.submitters).filter_map { |item| submission.submitters.find { |e| e.uuid == item['uuid'] } } %> <% submitters = (submission.template_submitters || submission.template.submitters).filter_map { |item| submission.submitters.find { |e| e.uuid == item['uuid'] } } %>
<% is_submission_completed = submitters.all?(&:completed_at?) && submitters.size.positive? %> <% is_submission_completed = submitters.all?(&:completed_at?) && submitters.size.positive? %>
<% if submitters.size == 1 %> <% if submitters.size == 1 %>
<div> <div>
<% submitter = submitters.first %> <% submitter = submitters.first %>
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<span class="flex items-center space-x-3"> <span class="flex flex-col md:flex-row md:items-center gap-3">
<div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>"> <div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<span class="badge <%= status_badges[submitter.status] %> md:w-32 badge-lg bg-opacity-50 uppercase text-sm font-semibold"> <span class="badge <%= status_badges[submitter.status] %> md:w-32 badge-lg bg-opacity-50 uppercase text-sm font-semibold">
<%= submitter.status %> <%= submitter.status %>
@@ -20,14 +20,14 @@
</div> </div>
<div class="flex space-x-2 items-center"> <div class="flex space-x-2 items-center">
<% if submitter.completed_at? %> <% if submitter.completed_at? %>
<form onsubmit="event.preventDefault()"> <form onsubmit="event.preventDefault()" class="flex-1 md:flex-none">
<button onclick="event.stopPropagation()"> <button onclick="event.stopPropagation()" class="w-full md:w-fit">
<download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="btn btn-sm btn-neutral text-white md:w-36"> <download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="btn btn-sm btn-neutral text-white w-full md:w-36">
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton"> <span class="flex items-center justify-center space-x-1 md:space-x-2" data-target="download-button.defaultButton">
<%= svg_icon('download', class: 'w-5 h-5 stroke-2') %> <%= svg_icon('download', class: 'w-5 h-5 stroke-2') %>
<span class="inline">Download</span> <span class="inline">Download</span>
</span> </span>
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton"> <span class="flex items-center justify-center space-x-1 md:space-x-2 hidden" data-target="download-button.loadingButton">
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin') %> <%= svg_icon('loader', class: 'w-5 h-5 animate-spin') %>
<span class="inline">Downloa...</span> <span class="inline">Downloa...</span>
</span> </span>
@@ -36,26 +36,30 @@
</form> </form>
<% else %> <% else %>
<% if current_user.email == submitter.email %> <% if current_user.email == submitter.email %>
<form data-turbo="false" method="get" action="<%= submit_form_url(slug: submitter.slug) %>"> <form data-turbo="false" method="get" action="<%= submit_form_url(slug: submitter.slug) %>" class="flex-1 md:flex-none">
<button onclick="event.stopPropagation()" class="btn btn-sm btn-neutral btn-outline bg-white md:w-36 flex"> <button onclick="event.stopPropagation()" class="btn btn-sm btn-neutral btn-outline bg-white w-full md:w-36 flex">
<span class="flex items-center justify-center space-x-2"> <span class="flex items-center justify-center space-x-1 md:space-x-2">
<%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %> <%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %>
<span class="hidden md:inline">Sign now</span> <span class="inline shrink-0">Sign now</span>
</span> </span>
</button> </button>
</form> </form>
<% else %> <% else %>
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-sm btn-neutral text-white md:w-36 flex', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Link', copy_title_md: 'Copy', copied_title_md: 'Copied' %> <div class="flex-1 md:flex-none">
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-sm btn-neutral text-white md:w-36 flex', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Link', copy_title_md: 'Copy', copied_title_md: 'Copied' %>
</div>
<% end %> <% end %>
<% end %> <% end %>
<span class="btn btn-outline btn-sm w-20 md:w-24">View</span> <div class="flex-1 md:flex-none">
<span class="btn btn-outline btn-sm w-full md:w-24">View</span>
</div>
<% if !submission.archived_at? && can?(:destroy, submission) %> <% if !submission.archived_at? && can?(:destroy, submission) %>
<%= button_to button_title(title: nil, disabled_with: 'Remov', icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm', form: { class: 'flex' }, title: 'Delete', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %> <%= button_to button_title(title: nil, disabled_with: 'Remov', icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: 'Delete', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %>
<% end %> <% end %>
</div> </div>
<% else %> <% else %>
<div class="space-y-1 w-full md:mr-2"> <div class="space-y-1 w-full md:mr-2">
<div class="flex items-center space-x-3"> <div class="flex flex-col md:flex-row md:items-center gap-3">
<% if is_submission_completed %> <% if is_submission_completed %>
<% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %> <% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %>
<div class="tooltip flex" data-tip="<%= l(latest_submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>"> <div class="tooltip flex" data-tip="<%= l(latest_submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
@@ -64,10 +68,10 @@
</span> </span>
</div> </div>
<% end %> <% end %>
<div class="w-full"> <div class="w-full <%= is_submission_completed ? 'space-y-1' : 'space-y-4' %> md:space-y-0">
<% submitters.each_with_index do |submitter, index| %> <% submitters.each_with_index do |submitter, index| %>
<div class="flex justify-between items-center space-x-3"> <div class="relative flex justify-between items-start md:items-center space-x-3">
<span class="flex items-center space-x-3"> <span class="flex flex-col md:flex-row md:items-center gap-2">
<% unless is_submission_completed %> <% unless is_submission_completed %>
<div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>"> <div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<span class="badge md:w-24 <%= status_badges[submitter.status] %> bg-opacity-50 uppercase text-xs font-semibold"> <span class="badge md:w-24 <%= status_badges[submitter.status] %> bg-opacity-50 uppercase text-xs font-semibold">
@@ -82,31 +86,31 @@
<% if submitter.completed_at? && !is_submission_completed %> <% if submitter.completed_at? && !is_submission_completed %>
<form onsubmit="event.preventDefault()"> <form onsubmit="event.preventDefault()">
<button onclick="event.stopPropagation()"> <button onclick="event.stopPropagation()">
<download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="btn btn-xs btn-neutral text-white md:w-36"> <download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="absolute md:relative top-0 right-0 btn btn-xs btn-neutral text-white md:w-36">
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton"> <span class="flex items-center justify-center space-x-1 md:space-x-2" data-target="download-button.defaultButton">
<%= svg_icon('download', class: 'w-4 h-4 stroke-2') %> <%= svg_icon('download', class: 'w-4 h-4 stroke-2') %>
<span class="hidden md:inline">Download</span> <span class="inline">Download</span>
</span> </span>
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton"> <span class="flex items-center justify-center space-x-1 md:space-x-2 hidden" data-target="download-button.loadingButton">
<%= svg_icon('loader', class: 'w-4 h-4 animate-spin') %> <%= svg_icon('loader', class: 'w-4 h-4 animate-spin') %>
<span class="hidden md:inline">Downloa...</span> <span class="inline">Downloa...</span>
</span> </span>
</download-button> </download-button>
</button> </button>
</form> </form>
<% elsif !is_submission_completed %> <% elsif !is_submission_completed %>
<div class="flex items-center space-x-3"> <div class="relative flex items-center space-x-3">
<% if current_user.email == submitter.email %> <% if current_user.email == submitter.email %>
<form data-turbo="false" method="get" action="<%= submit_form_url(slug: submitter.slug) %>"> <form data-turbo="false" method="get" action="<%= submit_form_url(slug: submitter.slug) %>">
<button onclick="event.stopPropagation()" class="btn btn-xs btn-outline btn-neutral bg-white md:w-36"> <button onclick="event.stopPropagation()" class="absolute md:relative top-0 right-0 btn btn-xs btn-outline btn-neutral bg-white w-28 md:w-36">
<span class="flex items-center justify-center space-x-2"> <span class="flex items-center justify-center space-x-1 md:space-x-2">
<%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %> <%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %>
<span class="hidden md:inline">Sign now</span> <span class="inline shrink-0">Sign now</span>
</span> </span>
</button> </button>
</form> </form>
<% else %> <% else %>
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-xs text-xs btn-neutral text-white md:w-36 flex', icon_class: 'w-4 h-4 text-white', copy_title: 'Copy Link' %> <%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'absolute md:relative top-0 right-0 btn btn-xs text-xs btn-neutral text-white w-28 md:w-36 flex', icon_class: 'w-4 h-4 text-white', copy_title: 'Copy Link', copy_title_md: 'Copy Link', copied_title_md: 'Copied' %>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
@@ -115,27 +119,31 @@
</div> </div>
</div> </div>
</div> </div>
<div class="flex space-x-2 items-center"> <div class="flex space-x-1 md:space-x-2 items-center">
<% if is_submission_completed %> <% if is_submission_completed %>
<% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %> <% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %>
<form onsubmit="event.preventDefault()"> <div class="flex-1 md:flex-none">
<button onclick="event.stopPropagation()"> <form onsubmit="event.preventDefault()" class="w-full md:w-fit">
<download-button data-src="<%= submitter_download_index_path(latest_submitter.slug) %>" class="btn btn-sm btn-neutral text-white md:w-36"> <button onclick="event.stopPropagation()">
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton"> <download-button data-src="<%= submitter_download_index_path(latest_submitter.slug) %>" class="btn btn-sm btn-neutral text-white md:w-36">
<%= svg_icon('download', class: 'w-5 h-5 stroke-2') %> <span class="flex items-center justify-center space-x-1 md:space-x-2" data-target="download-button.defaultButton">
<span class="inline">Download</span> <%= svg_icon('download', class: 'w-5 h-5 stroke-2') %>
</span> <span class="inline">Download</span>
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton"> </span>
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin') %> <span class="flex items-center justify-center space-x-1 md:space-x-2 hidden" data-target="download-button.loadingButton">
<span class="inline">Downloa...</span> <%= svg_icon('loader', class: 'w-5 h-5 animate-spin') %>
</span> <span class="inline">Downloa...</span>
</download-button> </span>
</button> </download-button>
</form> </button>
</form>
</div>
<% end %> <% end %>
<span class="btn btn-outline btn-sm w-20 md:w-24">View</span> <div class="flex-1 md:flex-none">
<span class="btn btn-outline btn-sm w-full md:w-24">View</span>
</div>
<% unless submission.archived_at? %> <% unless submission.archived_at? %>
<%= button_to button_title(title: nil, disabled_with: 'Remov', icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm', form: { class: 'flex' }, title: 'Delete', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %> <%= button_to button_title(title: nil, disabled_with: 'Remov', icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: 'Delete', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
+4 -1
View File
@@ -7,6 +7,9 @@
<p class="flex items-center space-x-1 text-xs text-base-content/60"> <p class="flex items-center space-x-1 text-xs text-base-content/60">
<%= svg_icon('user', class: 'w-4 h-4') %> <%= svg_icon('user', class: 'w-4 h-4') %>
<span><%= template.author.full_name.presence || template.author.email.to_s.sub(/\+\w+@/, '@') %></span> <span><%= template.author.full_name.presence || template.author.email.to_s.sub(/\+\w+@/, '@') %></span>
<% if template.account_id != current_account.id %>
<span class="badge badge-neutral badge-outline badge-sm text-[10px] uppercase">shared</span>
<% end %>
</p> </p>
<p class="flex text-xs text-base-content/60"> <p class="flex text-xs text-base-content/60">
<span class="flex items-center space-x-1 w-1/2"> <span class="flex items-center space-x-1 w-1/2">
@@ -24,7 +27,7 @@
</a> </a>
<div class="absolute top-0 bottom-0 w-0 flex items-center hidden md:group-hover:flex" style="right: 37px"> <div class="absolute top-0 bottom-0 w-0 flex items-center hidden md:group-hover:flex" style="right: 37px">
<div class="space-y-1"> <div class="space-y-1">
<% if can?(:update, template) && !template.archived_at? %> <% if can?(:update, template) && !template.archived_at? && template.account_id == current_account.id %>
<span class="tooltip tooltip-left" data-tip="Move"> <span class="tooltip tooltip-left" data-tip="Move">
<a href="<%= edit_template_folder_path(template.id) %>" data-turbo-frame="modal" class="btn btn-xs hover:btn-outline bg-base-200 btn-circle"> <a href="<%= edit_template_folder_path(template.id) %>" data-turbo-frame="modal" class="btn btn-xs hover:btn-outline bg-base-200 btn-circle">
<%= svg_icon('folder_share', class: 'w-4 h-4') %> <%= svg_icon('folder_share', class: 'w-4 h-4') %>
+71 -35
View File
@@ -1,50 +1,86 @@
<div class="flex flex-col items-start md:flex-row space-y-2 md:space-y-0 md:justify-between md:items-start mb-3"> <div class="flex flex-col items-start md:flex-row space-y-2 md:space-y-0 md:space-x-2 md:justify-between md:items-start mb-6 md:mb-3">
<div> <div class="relative flex items-start justify-between w-full space-x-0">
<h1 class="text-4xl font-semibold mr-4" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;"> <div>
<%= template.name %> <h1 class="text-4xl font-semibold" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;">
<% if template.archived_at? %> <%= template.name %>
<span class="badge badge-outline badge-lg align-middle">Archived</span> <% if template.archived_at? %>
<% end %> <span class="badge badge-outline badge-lg align-middle">Archived</span>
</h1> <% end %>
<div class="flex items-center"> </h1>
<a href="<%= folder_path(@template.folder) %>" class="flex items-center space-x-1 mt-1 peer"> <% if @template.account_id == current_account.id %>
<%= svg_icon('folder', class: 'w-5 h-5 flex-shrink-0') %> <div class="flex items-center justify-between">
<span class="text-sm"> <div class="flex items-center">
<%= @template.folder.name %> <a href="<%= folder_path(@template.folder) %>" class="flex items-center space-x-1 mt-1 peer">
</span> <%= svg_icon('folder', class: 'w-5 h-5 flex-shrink-0') %>
</a> <span class="text-sm">
<% if can?(:update, template) %> <%= @template.folder.name %>
<span class="pl-1 tooltip tooltip-right md:opacity-0 hover:opacity-100 peer-hover:opacity-100" data-tip="Move"> </span>
<a href="<%= edit_template_folder_path(template.id) %>" data-turbo-frame="modal"> </a>
<%= svg_icon('pencil_share', class: 'w-5 h-5') %> <% if can?(:update, template) %>
</a> <span class="pl-1 tooltip tooltip-right md:opacity-0 hover:opacity-100 peer-hover:opacity-100" data-tip="Move">
</span> <a href="<%= edit_template_folder_path(template.id) %>" data-turbo-frame="modal">
<%= svg_icon('pencil_share', class: 'w-5 h-5') %>
</a>
</span>
<% end %>
</div>
</div>
<% else %>
<div class="flex items-center justify-between">
<div class="flex items-center">
<div class="flex items-center space-x-1 mt-1 peer">
<span class="badge badge-neutral badge-outline badge-md text-xs text-white uppercase">shared</span>
</div>
</div>
</div>
<% end %> <% end %>
</div> </div>
</div>
<div class="flex md:justify-between space-x-2 flex-none pt-1">
<% if !template.archived_at? %> <% if !template.archived_at? %>
<%= render 'shared/clipboard_copy', text: start_form_url(slug: @template.slug), class: 'btn btn-sm btn-neutral text-white', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Link', copied_title: 'Copied', copy_title_md: 'Link', copied_title_md: 'Copied' %> <div class="flex items-center space-x-2">
<% if can?(:update, template) && (Docuseal.multitenant? || current_account.testing? || current_account.id == 1) %>
<div class="tooltip" data-tip="API and Embedding">
<%= link_to template_code_modal_path(template), class: 'btn btn-ghost btn-sm flex-1 hidden md:flex', data: { turbo_frame: :modal } do %>
<span class="flex items-center justify-center space-x-2">
<%= svg_icon('code', class: 'w-6 h-6') %>
</span>
<% end %>
</div>
<% end %>
<%= render 'shared/clipboard_copy', text: start_form_url(slug: @template.slug), class: 'absolute md:relative bottom-0 right-0 btn btn-xs md:btn-sm whitespace-nowrap btn-neutral text-white mt-1 px-2', icon_class: 'w-4 h-4 md:w-6 md:h-6 text-white', copy_title: 'Link', copied_title: 'Copied', copy_title_md: 'Link', copied_title_md: 'Copied' %>
</div>
<% end %> <% end %>
</div>
<div class="flex space-x-2 w-full md:w-fit md:justify-between md:flex-none md:pt-1">
<% if !template.archived_at? && can?(:destroy, template) %> <% if !template.archived_at? && can?(:destroy, template) %>
<%= button_to button_title(title: 'Archive', disabled_with: 'Archiving', title_class: 'hidden md:inline', icon: svg_icon('archive', class: 'w-6 h-6')), template_path(template), class: 'btn btn-outline btn-sm', method: :delete, data: { turbo_confirm: 'Are you sure?' } %> <%= button_to button_title(title: 'Archive', disabled_with: 'Archiving', title_class: 'inline', icon: svg_icon('archive', class: 'w-6 h-6')), template_path(template), class: 'btn btn-outline btn-sm w-full', form_class: 'flex-1', method: :delete, data: { turbo_confirm: 'Are you sure?' } %>
<% end %> <% end %>
<% if can?(:create, template) %> <% if can?(:create, template) %>
<%= link_to new_template_path(base_template_id: template.id), class: 'btn btn-outline btn-sm', data: { turbo_frame: :modal } do %> <%= link_to new_template_path(base_template_id: template.id), class: 'btn btn-outline btn-sm flex-1', data: { turbo_frame: :modal } do %>
<%= svg_icon('copy', class: 'w-6 h-6') %> <span class="flex items-center justify-center space-x-2">
<span class="hidden md:inline">Clone</span> <%= svg_icon('copy', class: 'w-6 h-6') %>
<span class="inline">Clone</span>
</span>
<% end %> <% end %>
<% end %> <% end %>
<% if !template.archived_at? && can?(:update, template) %> <% if !template.archived_at? %>
<%= link_to edit_template_path(template), class: 'btn btn-outline btn-sm' do %> <% if can?(:update, template) %>
<span class="flex items-center justify-center space-x-2"> <%= link_to edit_template_path(template), class: 'btn btn-outline btn-sm flex-1' do %>
<%= svg_icon('pencil', class: 'w-6 h-6') %> <span class="flex items-center justify-center space-x-2">
<span>Edit</span> <%= svg_icon('pencil', class: 'w-6 h-6') %>
</span> <span>Edit</span>
</span>
<% end %>
<% elsif can?(:read, template) %>
<%= link_to template_preview_path(template), class: 'btn btn-outline btn-sm flex-1' do %>
<span class="flex items-center justify-center space-x-2">
<%= svg_icon('file_text', class: 'w-6 h-6') %>
<span>Preview</span>
</span>
<% end %>
<% end %> <% end %>
<% end %> <% end %>
<% if template.archived_at? && can?(:create, template) %> <% if template.archived_at? && can?(:create, template) %>
<%= button_to button_title(title: 'Restore', disabled_with: 'Restoring', icon: svg_icon('rotate', class: 'w-6 h-6')), template_restore_index_path(template), class: 'btn btn-outline btn-sm' %> <%= button_to button_title(title: 'Restore', disabled_with: 'Restoring', icon: svg_icon('rotate', class: 'w-6 h-6')), template_restore_index_path(template), class: 'btn btn-outline btn-sm flex-1' %>
<% end %> <% end %>
</div> </div>
</div> </div>
+6 -1
View File
@@ -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>
+17 -15
View File
@@ -1,22 +1,24 @@
<%= render 'title', template: @template %> <%= render 'title', template: @template %>
<% is_show_tabs = @pagy.pages > 1 || params[:status].present? %> <% is_show_tabs = @pagy.pages > 1 || params[:status].present? %>
<% if !@pagy.count.zero? || params[:q].present? %> <% if !@pagy.count.zero? || params[:q].present? %>
<div class="flex justify-between md:items-end flex-col md:flex-row <%= is_show_tabs ? 'mb-4' : 'mb-6' %>"> <div class="<%= is_show_tabs ? 'mb-4' : 'mb-6' %>">
<p class="text-3xl font-bold">Submissions</p> <div class="flex justify-between items-center md:items-end">
<div class="flex justify-end space-x-2 mt-3 md:mt-0"> <p class="text-3xl font-bold">Submissions</p>
<% if params[:q].present? || params[:status].present? || @pagy.pages > 1 %> <div class="flex justify-end space-x-2">
<%= render 'shared/search_input' %> <% if params[:q].present? || params[:status].present? || @pagy.pages > 1 %>
<% end %> <%= render 'shared/search_input' %>
<%= link_to new_template_submissions_export_path(@template), class: 'btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('download', class: 'w-6 h-6 stroke-2') %>
<span>Export</span>
<% end %>
<% if !@template.archived_at? && can?(:create, Submission) %>
<%= link_to new_template_submission_path(@template), class: 'white-button !border', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span>Add <span class="hidden md:inline">Recipients</span></span>
<% end %> <% end %>
<% end %> <%= link_to new_template_submissions_export_path(@template), class: 'hidden md:flex btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('download', class: 'w-6 h-6 stroke-2') %>
<span>Export</span>
<% end %>
<% if !@template.archived_at? && can?(:create, Submission) %>
<%= link_to new_template_submission_path(@template), class: 'white-button !border', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span>Add <span class="hidden md:inline">Recipients</span></span>
<% end %>
<% end %>
</div>
</div> </div>
</div> </div>
<% end %> <% end %>
@@ -0,0 +1,11 @@
<div class="alert">
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
<div>
<p class="font-bold">API and Embedding</p>
<p class="text-gray-700">
Unlock with DocuSeal Pro
<br>
<a class="link font-medium" target="_blank" href="<%= Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premise" }.to_query}" %>">Learn More</a>
</p>
</div>
</div>
Whitespace-only changes.
@@ -0,0 +1,36 @@
<%= render 'shared/turbo_modal', title: 'API and Embedding', close_after_submit: false do %>
<div>
<label class="text-sm font-semibold" for="template_id">
Template ID
</label>
<div class="flex gap-2 mb-4 mt-2">
<input id="template_id" type="text" value="<%= @template.id %>" class="base-input w-full" autocomplete="off" readonly>
<%= render 'shared/clipboard_copy', icon: 'copy', text: @template.id, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
</div>
</div>
<div>
<label class="text-sm font-semibold" for="embedding_url">
Embedding URL
</label>
<div class="flex gap-2 mb-4 mt-2">
<input id="embedding_url" type="text" value="<%= start_form_url(slug: @template.slug) %>" class="base-input w-full" autocomplete="off" readonly>
<%= render 'shared/clipboard_copy', icon: 'copy', text: start_form_url(slug: @template.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
</div>
</div>
<%= render 'templates/embedding', template: @template %>
<% if can?(:manage, TemplateSharing.new(template: @template)) %>
<%= form_for '', url: template_sharings_testing_index_path, method: :post, html: { class: 'mt-1' } do |f| %>
<%= f.hidden_field :template_id, value: @template.id %>
<div class="flex items-center justify-between">
<span>
Share template with Test Environment
</span>
<%= f.check_box :value, class: 'toggle', checked: @template.template_sharings.exists?(account_id: current_account.testing_accounts), onchange: 'this.form.requestSubmit()' %>
</div>
<% end %>
<div class="mb-4">
</div>
<% end %>
<%= render 'templates_code_modal/preferences' %>
<%= render 'templates_code_modal/placeholder' %>
<% end %>
@@ -0,0 +1 @@
<template-builder class="grid" data-editable="false" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-template="<%= @template_data %>"></template-builder>
+1 -1
View File
@@ -5,7 +5,7 @@
<option value="editor" disabled>Editor</option> <option value="editor" disabled>Editor</option>
<option value="viewer" disabled>Viewer</option> <option value="viewer" disabled>Viewer</option>
<% end %> <% end %>
<a class="text-sm mt-3 px-4 py-2 bg-base-300 rounded-full block" target="_blank" href="<%= "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>"> <a class="text-sm mt-3 px-4 py-2 bg-base-300 rounded-full block" target="_blank" href="<%= Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premise" }.to_query}" %>">
<%= svg_icon('info_circle', class: 'w-4 h-4 inline align-text-bottom') %> <%= svg_icon('info_circle', class: 'w-4 h-4 inline align-text-bottom') %>
Unlock more user roles with DocuSeal Pro. Unlock more user roles with DocuSeal Pro.
<span class="link font-medium">Learn More</a> <span class="link font-medium">Learn More</a>
+2
View File
@@ -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]
+21 -6
View File
@@ -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
+39
View File
@@ -4,6 +4,45 @@ ActiveSupport.on_load(:active_storage_attachment) do
attribute :uuid, :string, default: -> { SecureRandom.uuid } attribute :uuid, :string, default: -> { SecureRandom.uuid }
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
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
ActiveStorage::LogSubscriber.detach_from(:active_storage) if Rails.env.production? ActiveStorage::LogSubscriber.detach_from(:active_storage) if Rails.env.production?
+1 -1
View File
@@ -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
+15
View File
@@ -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'
+1 -1
View File
@@ -216,7 +216,7 @@ nl:
signed_on_time: 'Ondertekend op %{time}' signed_on_time: 'Ondertekend op %{time}'
form_has_been_submitted_already: Formulier is al ingediend form_has_been_submitted_already: Formulier is al ingediend
send_copy_to_email: Stuur kopie naar E-mail send_copy_to_email: Stuur kopie naar E-mail
sending: Verzenden sending: Voltooien
resubmit: Opnieuw indienen resubmit: Opnieuw indienen
form_has_been_deleted_by_html: 'Formulier is verwijderd door <span class="font-semibold">%{name}</span>.' form_has_been_deleted_by_html: 'Formulier is verwijderd door <span class="font-semibold">%{name}</span>.'
or: of or: of
+20 -2
View File
@@ -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]
@@ -70,14 +71,29 @@ Rails.application.routes.draw do
end end
resources :templates_archived, only: %i[index], path: 'archived' resources :templates_archived, only: %i[index], path: 'archived'
resources :folders, only: %i[show edit update destroy], controller: 'template_folders' resources :folders, only: %i[show edit update destroy], controller: 'template_folders'
resources :template_sharings_testing, only: %i[create]
resources :templates, only: %i[new create edit show destroy] do resources :templates, only: %i[new create edit show destroy] do
resources :restore, only: %i[create], controller: 'templates_restore' resources :restore, only: %i[create], controller: 'templates_restore'
resources :archived, only: %i[index], controller: 'templates_archived_submissions' resources :archived, only: %i[index], controller: 'templates_archived_submissions'
resources :submissions, only: %i[new create] resources :submissions, only: %i[new create]
resource :folder, only: %i[edit update], controller: 'templates_folders' resource :folder, only: %i[edit update], controller: 'templates_folders'
resource :preview, only: %i[show], controller: 'templates_preview'
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
@@ -91,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,8 @@
# frozen_string_literal: true
class RenameApplicationKeyToExternalId < ActiveRecord::Migration[7.1]
def change
rename_column :templates, :application_key, :external_id
rename_column :submitters, :application_key, :external_id
end
end
@@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddSubmitterMetadata < ActiveRecord::Migration[7.1]
class MigrationSubmitter < ApplicationRecord
self.table_name = 'submitters'
end
def change
add_column :submitters, :metadata, :text
MigrationSubmitter.where(metadata: nil).update_all(metadata: '{}')
change_column_null :submitters, :metadata, false
end
end
@@ -0,0 +1,26 @@
# frozen_string_literal: true
class AddAccountIdToSubmissions < ActiveRecord::Migration[7.1]
class MigrationSubmission < ApplicationRecord
self.table_name = 'submissions'
end
class MigrationTemplate < ApplicationRecord
self.table_name = 'templates'
end
class MigrationAccount < ApplicationRecord
self.table_name = 'accounts'
end
def change
add_reference :submissions, :account, index: true, null: true
MigrationAccount.all.each do |account|
MigrationSubmission.where(template_id: MigrationTemplate.where(account_id: account.id).select(:id))
.update_all(account_id: account.id)
end
change_column_null :submissions, :account_id, false
end
end
Loaded 100 of 128 files, more files were not shown because too many files have changed in this diff. Show more