mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 22:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2505768e4d | ||
|
|
23a1b70534 | ||
|
|
b34616ecbc | ||
|
|
0111252f6f | ||
|
|
a561cc5f73 | ||
|
|
519e6f1f79 | ||
|
|
da871b5d3a | ||
|
|
ae3f939d23 | ||
|
|
fb6ede564e | ||
|
|
1c701d4fe8 | ||
|
|
c743cc689d | ||
|
|
8432bec3d6 | ||
|
|
8e72b0b0e4 | ||
|
|
19e4bbc01e | ||
|
|
56a9e412c8 | ||
|
|
4b75623b86 | ||
|
|
e1ea42a997 | ||
|
|
ee37710e57 | ||
|
|
ae99218789 | ||
|
|
9fe8214ca0 | ||
|
|
6ca44a5cc3 | ||
|
|
801abd197b | ||
|
|
970977e469 | ||
|
|
f83674b934 | ||
|
|
57c7dc1b5e | ||
|
|
e9277f4eeb | ||
|
|
4052b6fe13 | ||
|
|
608347b1e6 | ||
|
|
345fe674d4 | ||
|
|
92f8901a67 | ||
|
|
fcd40308ee | ||
|
|
2bd023e5d0 | ||
|
|
a697d6330e | ||
|
|
ab56e8be20 | ||
|
|
b9a906b569 | ||
|
|
02adbae758 | ||
|
|
866aab7fac | ||
|
|
c81b4d855b | ||
|
|
4ad58fc285 | ||
|
|
8227dd4f6a | ||
|
|
beb677734f | ||
|
|
03db636d51 | ||
|
|
b83abc9499 | ||
|
|
9738cdcf8f | ||
|
|
179ae01551 | ||
|
|
295860a9de | ||
|
|
0a6c15b6b8 | ||
|
|
18f80020ee | ||
|
|
3768fb94bf | ||
|
|
7b2cadcb91 | ||
|
|
4eb4e8c1f8 | ||
|
|
984df15949 | ||
|
|
aa3bd24332 | ||
|
|
250889623d | ||
|
|
862fa55986 | ||
|
|
0f2889904a | ||
|
|
49f14c7929 | ||
|
|
c3f3c5d5df | ||
|
|
274259f305 | ||
|
|
a15d7a9cf3 | ||
|
|
7d154fc28d | ||
|
|
6727d3be83 | ||
|
|
4bbd5c3bcb | ||
|
|
5995f2e427 | ||
|
|
0910c8e100 | ||
|
|
8df76dfa90 | ||
|
|
78e9e66cb0 | ||
|
|
71c61fc4b3 | ||
|
|
a728aa8689 | ||
|
|
5a3174a720 | ||
|
|
652072d660 | ||
|
|
323f5c0c15 | ||
|
|
229d78c78b | ||
|
|
9d9a955149 | ||
|
|
b40a047190 | ||
|
|
ff9e415823 | ||
|
|
2f7a4fb2b3 | ||
|
|
f8a654a5b6 | ||
|
|
4449475467 | ||
|
|
bcbbf0dd42 | ||
|
|
19b9ba52d4 | ||
|
|
bdc30db2dc | ||
|
|
74634ba96b | ||
|
|
77feb05bad | ||
|
|
a1f05d006e | ||
|
|
cc3dae1335 | ||
|
|
d9debcfb0d | ||
|
|
8770d16d52 | ||
|
|
84953481e4 | ||
|
|
df7aa59f6f | ||
|
|
008b83119d | ||
|
|
15dafcb6ff | ||
|
|
7d291ed888 | ||
|
|
a7221f0750 | ||
|
|
9a5b2a5d30 | ||
|
|
7361a00d42 | ||
|
|
e07d402782 | ||
|
|
a34354bee1 | ||
|
|
84b762b529 | ||
|
|
2a839951b4 | ||
|
|
26d7c2f047 | ||
|
|
151a5b85f3 | ||
|
|
3f506f53b4 |
No files matched your search
@@ -59,7 +59,7 @@ jobs:
|
|||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 16.13.1
|
node-version: 20.9.0
|
||||||
- name: Cache directory path
|
- name: Cache directory path
|
||||||
id: yarn-cache-dir-path
|
id: yarn-cache-dir-path
|
||||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||||
@@ -104,7 +104,7 @@ jobs:
|
|||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 16.13.1
|
node-version: 20.9.0
|
||||||
- name: Install Chrome
|
- name: Install Chrome
|
||||||
uses: browser-actions/setup-chrome@latest
|
uses: browser-actions/setup-chrome@latest
|
||||||
- name: Cache node_modules
|
- name: Cache node_modules
|
||||||
|
|||||||
+3
-4
@@ -2,9 +2,7 @@ FROM ruby:3.2.2-alpine3.18 as fonts
|
|||||||
|
|
||||||
WORKDIR /fonts
|
WORKDIR /fonts
|
||||||
|
|
||||||
RUN apk --no-cache add fontforge wget ttf-liberation && cp /usr/share/fonts/liberation/LiberationSans-Regular.ttf /usr/share/fonts/liberation/LiberationSans-Bold.ttf . && wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansArabic/hinted/ttf/NotoSansArabic-Regular.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt
|
RUN apk --no-cache add wget && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt
|
||||||
|
|
||||||
RUN fontforge -lang=py -c 'font1 = fontforge.open("LiberationSans-Regular.ttf"); font2 = fontforge.open("NotoSansArabic-Regular.ttf"); font1.mergeFonts(font2); font1.generate("LiberationSans-Regular.ttf")'
|
|
||||||
|
|
||||||
FROM ruby:3.2.2-alpine3.18 as webpack
|
FROM ruby:3.2.2-alpine3.18 as webpack
|
||||||
|
|
||||||
@@ -36,6 +34,7 @@ FROM ruby:3.2.2-alpine3.18 as app
|
|||||||
|
|
||||||
ENV RAILS_ENV=production
|
ENV RAILS_ENV=production
|
||||||
ENV BUNDLE_WITHOUT="development:test"
|
ENV BUNDLE_WITHOUT="development:test"
|
||||||
|
ENV PIDFILE=/dev/null
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -55,7 +54,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
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ gem 'rack'
|
|||||||
gem 'rails'
|
gem 'rails'
|
||||||
gem 'rails_autolink'
|
gem 'rails_autolink'
|
||||||
gem 'rails-i18n'
|
gem 'rails-i18n'
|
||||||
gem 'rollbar', require: ENV.key?('ROLLBAR_ACCESS_TOKEN')
|
|
||||||
gem 'rotp'
|
gem 'rotp'
|
||||||
gem 'rqrcode'
|
gem 'rqrcode'
|
||||||
gem 'ruby-vips'
|
gem 'ruby-vips'
|
||||||
|
|||||||
+1
-3
@@ -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)
|
||||||
@@ -433,7 +433,6 @@ GEM
|
|||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
retriable (3.1.2)
|
retriable (3.1.2)
|
||||||
rexml (3.2.6)
|
rexml (3.2.6)
|
||||||
rollbar (3.4.2)
|
|
||||||
rotp (6.3.0)
|
rotp (6.3.0)
|
||||||
rqrcode (2.2.0)
|
rqrcode (2.2.0)
|
||||||
chunky_png (~> 1.0)
|
chunky_png (~> 1.0)
|
||||||
@@ -605,7 +604,6 @@ DEPENDENCIES
|
|||||||
rails
|
rails
|
||||||
rails-i18n
|
rails-i18n
|
||||||
rails_autolink
|
rails_autolink
|
||||||
rollbar
|
|
||||||
rotp
|
rotp
|
||||||
rqrcode
|
rqrcode
|
||||||
rspec-rails
|
rspec-rails
|
||||||
|
|||||||
@@ -33,17 +33,30 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
[](https://demo.docuseal.co)
|
[](https://demo.docuseal.co)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- [x] PDF form fields builder (WYSIWYG)
|
- PDF form fields builder (WYSIWYG)
|
||||||
- [x] 11 field types available (Signature, Date, File, Checkbox etc.)
|
- 12 field types available (Signature, Date, File, Checkbox etc.)
|
||||||
- [x] Multiple submitters per document
|
- Multiple submitters per document
|
||||||
- [x] Automated emails via SMTP
|
- Automated emails via SMTP
|
||||||
- [x] Files storage on disk or AWS S3, Google Storage, Azure Cloud
|
- Files storage on disk or AWS S3, Google Storage, Azure Cloud
|
||||||
- [x] Automatic PDF eSignature
|
- Automatic PDF eSignature
|
||||||
- [x] PDF signature verification
|
- PDF signature verification
|
||||||
- [x] Users management
|
- Users management
|
||||||
- [x] Mobile-optimized
|
- Mobile-optimized
|
||||||
- [x] API and Webhooks for integrations
|
- Signing available in 12 Languages
|
||||||
- [x] Easy to deploy in minutes
|
- API and Webhooks for integrations
|
||||||
|
- 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 field tags API ([Guide](https://www.docuseal.co/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form))
|
||||||
|
- Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue) or [JavaScript](https://www.docuseal.co/docs/embedded))
|
||||||
|
- Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue) or [JavaScript](https://www.docuseal.co/docs/embedded))
|
||||||
|
- [Learn more](https://www.docuseal.co/pricing)
|
||||||
|
|
||||||
## Deploy
|
## Deploy
|
||||||
|
|
||||||
@@ -77,7 +90,7 @@ sudo HOST=your-domain-name.com docker compose up
|
|||||||
```
|
```
|
||||||
|
|
||||||
## For Businesses
|
## For Businesses
|
||||||
### Integrate seamless document signing into your web or mobile apps with DocuSeal!
|
### Integrate seamless document signing into your web or mobile apps with DocuSeal
|
||||||
|
|
||||||
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
|
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Reporting a Vulnerability
|
||||||
|
|
||||||
|
If you come across any security concern or vulnarability, please report the information via email to security@docuseal.co instead of opening a GitHub issue. We will promptly respond and will collaborate with you to validate the issue, and resolve it ASAP.
|
||||||
|
|
||||||
|
**We have a bug bounty program to reward security researchers.**
|
||||||
@@ -11,6 +11,8 @@ class AccountConfigsController < ApplicationController
|
|||||||
AccountConfig::ESIGNING_PREFERENCE_KEY
|
AccountConfig::ESIGNING_PREFERENCE_KEY
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
|
InvalidKey = Class.new(StandardError)
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@account_config.update!(account_config_params)
|
@account_config.update!(account_config_params)
|
||||||
|
|
||||||
@@ -20,7 +22,7 @@ class AccountConfigsController < ApplicationController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def load_account_config
|
def load_account_config
|
||||||
return head :not_found unless ALLOWED_KEYS.include?(account_config_params[:key])
|
raise InvalidKey unless ALLOWED_KEYS.include?(account_config_params[:key])
|
||||||
|
|
||||||
@account_config =
|
@account_config =
|
||||||
AccountConfig.find_or_initialize_by(account: current_account, key: account_config_params[:key])
|
AccountConfig.find_or_initialize_by(account: current_account, key: account_config_params[:key])
|
||||||
|
|||||||
@@ -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, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
|
||||||
|
|
||||||
|
if blob_uuid.blank? || (purp.present? && purp != 'blob') || (exp && exp < Time.current.to_i)
|
||||||
|
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,43 @@
|
|||||||
|
# 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])
|
||||||
|
|
||||||
|
return head :not_found unless blob
|
||||||
|
|
||||||
|
is_permitted = blob.attachments.any? do |a|
|
||||||
|
(current_user && a.record.account.id == current_user.account_id) ||
|
||||||
|
a.record.account.account_configs.any? { |e| e.key == 'legacy_blob_proxy' } ||
|
||||||
|
a.name == 'logo'
|
||||||
|
end
|
||||||
|
|
||||||
|
unless is_permitted
|
||||||
|
Rollbar.error("Blob account not found: #{blob.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return render json: { error: 'Not authenticated' }, status: :unauthorized
|
||||||
|
end
|
||||||
|
|
||||||
|
if request.headers['Range'].present?
|
||||||
|
send_blob_byte_range_data blob, request.headers['Range']
|
||||||
|
else
|
||||||
|
http_cache_forever public: true do
|
||||||
|
response.headers['Accept-Ranges'] = 'bytes'
|
||||||
|
response.headers['Content-Length'] = blob.byte_size.to_s
|
||||||
|
|
||||||
|
send_blob_stream blob, disposition: params[:disposition]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# rubocop:enable Metrics
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -21,13 +21,13 @@ module Api
|
|||||||
|
|
||||||
if Rails.env.production?
|
if Rails.env.production?
|
||||||
rescue_from CanCan::AccessDenied do |e|
|
rescue_from CanCan::AccessDenied do |e|
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
render json: { error: e.message }, status: :forbidden
|
render json: { error: e.message }, status: :forbidden
|
||||||
end
|
end
|
||||||
|
|
||||||
rescue_from JSON::ParserError do |e|
|
rescue_from JSON::ParserError do |e|
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
render json: { error: "JSON parse error: #{e.message}" }, status: :unprocessable_entity
|
render json: { error: "JSON parse error: #{e.message}" }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
@@ -59,5 +59,13 @@ module Api
|
|||||||
def current_account
|
def current_account
|
||||||
current_user&.account
|
current_user&.account
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_cors_headers
|
||||||
|
headers['Access-Control-Allow-Origin'] = '*'
|
||||||
|
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
|
||||||
|
headers['Access-Control-Allow-Headers'] = '*'
|
||||||
|
headers['Access-Control-Max-Age'] = '1728000'
|
||||||
|
headers['Access-Control-Allow-Credentials'] = true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -74,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
|
||||||
@@ -93,7 +93,9 @@ module Api
|
|||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ module Api
|
|||||||
if SELECT_COLUMNS.include?(params[:field])
|
if SELECT_COLUMNS.include?(params[:field])
|
||||||
column = Submitter.arel_table[params[:field].to_sym]
|
column = Submitter.arel_table[params[:field].to_sym]
|
||||||
|
|
||||||
term = "%#{params[:q].downcase}%"
|
term = "#{params[:q].downcase}%"
|
||||||
|
|
||||||
submitters.where(column.lower.matches(term))
|
submitters.where(column.matches(term, false, true))
|
||||||
else
|
else
|
||||||
Submitters.search(submitters, params[:q])
|
Submitters.search(submitters, params[:q])
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -20,16 +20,7 @@ module Api
|
|||||||
|
|
||||||
Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
|
Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
|
||||||
|
|
||||||
render json: cloned_template.as_json(serialize_params)
|
render json: Templates::SerializeForApi.call(cloned_template)
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def serialize_params
|
|
||||||
{
|
|
||||||
include: { author: { only: %i[id email first_name last_name] },
|
|
||||||
documents: { only: %i[id uuid], methods: %i[url filename] } }
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -7,10 +7,31 @@ module Api
|
|||||||
def index
|
def index
|
||||||
templates = filter_templates(@templates, params)
|
templates = filter_templates(@templates, params)
|
||||||
|
|
||||||
templates = paginate(templates.preload(:author, documents_attachments: :blob))
|
templates = paginate(templates.preload(:author, :folder))
|
||||||
|
|
||||||
|
schema_documents =
|
||||||
|
ActiveStorage::Attachment.where(record_id: templates.map(&:id),
|
||||||
|
record_type: 'Template',
|
||||||
|
name: :documents,
|
||||||
|
uuid: templates.flat_map { |t| t.schema.pluck('attachment_uuid') })
|
||||||
|
.preload(:blob)
|
||||||
|
|
||||||
|
preview_image_attachments =
|
||||||
|
ActiveStorage::Attachment.joins(:blob)
|
||||||
|
.where(blob: { filename: '0.jpg' })
|
||||||
|
.where(record_id: schema_documents.map(&:id),
|
||||||
|
record_type: 'ActiveStorage::Attachment',
|
||||||
|
name: :preview_images)
|
||||||
|
.preload(:blob)
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
data: templates.as_json(serialize_params),
|
data: templates.map do |t|
|
||||||
|
Templates::SerializeForApi.call(
|
||||||
|
t,
|
||||||
|
schema_documents.select { |e| e.record_id == t.id },
|
||||||
|
preview_image_attachments
|
||||||
|
)
|
||||||
|
end,
|
||||||
pagination: {
|
pagination: {
|
||||||
count: templates.size,
|
count: templates.size,
|
||||||
next: templates.last&.id,
|
next: templates.last&.id,
|
||||||
@@ -20,7 +41,7 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
render json: @template.as_json(serialize_params)
|
render json: Templates::SerializeForApi.call(@template)
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@@ -61,14 +82,6 @@ module Api
|
|||||||
templates
|
templates
|
||||||
end
|
end
|
||||||
|
|
||||||
def serialize_params
|
|
||||||
{
|
|
||||||
methods: %i[application_key],
|
|
||||||
include: { author: { only: %i[id email first_name last_name] },
|
|
||||||
documents: { only: %i[id uuid], methods: %i[url preview_image_url filename] } }
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def template_params
|
def template_params
|
||||||
permit_params = [
|
permit_params = [
|
||||||
:name,
|
:name,
|
||||||
@@ -76,6 +89,7 @@ module Api
|
|||||||
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: {},
|
||||||
|
conditions: [%i[field_uuid value action]],
|
||||||
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]] }]] }
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ module Api
|
|||||||
render json: {
|
render json: {
|
||||||
schema:,
|
schema:,
|
||||||
documents: documents.as_json(
|
documents: documents.as_json(
|
||||||
methods: [:metadata],
|
methods: %i[metadata signed_uuid],
|
||||||
include: {
|
include: {
|
||||||
preview_images: { methods: %i[url metadata filename] }
|
preview_images: { methods: %i[url metadata filename] }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class ApplicationController < ActionController::Base
|
|||||||
|
|
||||||
if Rails.env.production?
|
if Rails.env.production?
|
||||||
rescue_from CanCan::AccessDenied do |e|
|
rescue_from CanCan::AccessDenied do |e|
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
redirect_to root_path, alert: e.message
|
redirect_to root_path, alert: e.message
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -54,10 +54,14 @@ class DashboardController < ApplicationController
|
|||||||
rel = templates.active.preload(:author).order(id: :desc)
|
rel = templates.active.preload(:author).order(id: :desc)
|
||||||
|
|
||||||
if params[:q].blank?
|
if params[:q].blank?
|
||||||
shared_template_ids =
|
if Docuseal.multitenant? && !current_account.testing?
|
||||||
TemplateSharing.where(account_id: [current_account.id, TemplateSharing::ALL_ID]).select(:template_id)
|
rel = rel.where(folder_id: current_account.default_template_folder.id)
|
||||||
|
else
|
||||||
|
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))
|
rel = rel.where(folder_id: current_account.default_template_folder.id).or(rel.where(id: shared_template_ids))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Templates.search(rel, params[:q])
|
Templates.search(rel, params[:q])
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,23 +1,42 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class PersonalizationSettingsController < ApplicationController
|
class PersonalizationSettingsController < ApplicationController
|
||||||
|
ALLOWED_KEYS = [
|
||||||
|
AccountConfig::FORM_COMPLETED_BUTTON_KEY,
|
||||||
|
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
|
||||||
|
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
|
||||||
|
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
InvalidKey = Class.new(StandardError)
|
||||||
|
|
||||||
|
before_action :load_and_authorize_account_config, only: :create
|
||||||
|
|
||||||
def show
|
def show
|
||||||
authorize!(:read, AccountConfig)
|
authorize!(:read, AccountConfig)
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
account_config =
|
@account_config.save!
|
||||||
current_account.account_configs.find_or_initialize_by(key: account_config_params[:key])
|
|
||||||
|
|
||||||
authorize!(:create, account_config)
|
|
||||||
|
|
||||||
account_config.update!(account_config_params)
|
|
||||||
|
|
||||||
redirect_back(fallback_location: settings_personalization_path, notice: 'Settings have been saved.')
|
redirect_back(fallback_location: settings_personalization_path, notice: 'Settings have been saved.')
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def load_and_authorize_account_config
|
||||||
|
@account_config =
|
||||||
|
current_account.account_configs.find_or_initialize_by(key: account_config_params[:key])
|
||||||
|
|
||||||
|
@account_config.assign_attributes(account_config_params)
|
||||||
|
|
||||||
|
authorize!(:create, @account_config)
|
||||||
|
|
||||||
|
raise InvalidKey unless ALLOWED_KEYS.include?(@account_config.key)
|
||||||
|
|
||||||
|
@account_config
|
||||||
|
end
|
||||||
|
|
||||||
def account_config_params
|
def account_config_params
|
||||||
attrs = params.require(:account_config).permit!
|
attrs = params.require(:account_config).permit!
|
||||||
|
|
||||||
|
|||||||
@@ -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) 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)
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class RegistrationsController < Devise::RegistrationsController
|
|
||||||
prepend_before_action :require_no_authentication, only: [:show]
|
|
||||||
prepend_before_action :maybe_redirect_if_signed_in, only: [:show]
|
|
||||||
|
|
||||||
def show; end
|
|
||||||
|
|
||||||
def create
|
|
||||||
super
|
|
||||||
|
|
||||||
Accounts.create_default_template(resource.account) if resource.account.persisted?
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def after_sign_up_path_for(...)
|
|
||||||
if params[:redir].present?
|
|
||||||
return console_redirect_index_path(redir: params[:redir]) if params[:redir].starts_with?(Docuseal::CONSOLE_URL)
|
|
||||||
|
|
||||||
return params[:redir]
|
|
||||||
end
|
|
||||||
|
|
||||||
super
|
|
||||||
end
|
|
||||||
|
|
||||||
def maybe_redirect_if_signed_in
|
|
||||||
return unless signed_in?
|
|
||||||
return if params[:redir].blank?
|
|
||||||
|
|
||||||
redirect_to after_sign_up_path_for(current_user), allow_other_host: true
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_flash_message(key, kind, options = {})
|
|
||||||
return if key == :alert && kind == 'already_authenticated'
|
|
||||||
|
|
||||||
super
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_resource(_hash = {})
|
|
||||||
account = Account.new(account_params)
|
|
||||||
account.timezone = Accounts.normalize_timezone(account.timezone)
|
|
||||||
|
|
||||||
self.resource = account.users.new(user_params)
|
|
||||||
|
|
||||||
account.name ||= resource.full_name if params[:action] == 'create'
|
|
||||||
end
|
|
||||||
|
|
||||||
def user_params
|
|
||||||
return {} if params[:user].blank?
|
|
||||||
|
|
||||||
params.require(:user).permit(:first_name, :last_name, :email, :password).compact_blank.tap do |attrs|
|
|
||||||
attrs[:password] ||= SecureRandom.hex if params[:action] == 'create'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def account_params
|
|
||||||
return {} if params[:account].blank?
|
|
||||||
|
|
||||||
params.require(:account).permit(:name, :timezone).compact_blank
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -12,13 +12,16 @@ class SendSubmissionEmailController < ApplicationController
|
|||||||
def create
|
def create
|
||||||
@submitter =
|
@submitter =
|
||||||
if params[:template_slug]
|
if params[:template_slug]
|
||||||
Submitter.joins(submission: :template).find_by!(email: params[:email],
|
Submitter.joins(submission: :template).find_by!(email: params[:email].to_s.downcase,
|
||||||
template: { slug: params[:template_slug] })
|
template: { slug: params[:template_slug] })
|
||||||
|
elsif params[:submission_slug]
|
||||||
|
Submitter.joins(:submission).find_by!(email: params[:email].to_s.downcase,
|
||||||
|
submission: { slug: params[:submission_slug] })
|
||||||
else
|
else
|
||||||
Submitter.find_by!(slug: params[:submitter_slug])
|
Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
end
|
end
|
||||||
|
|
||||||
SubmitterMailer.documents_copy_email(@submitter).deliver_later!
|
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later!
|
||||||
|
|
||||||
respond_to do |f|
|
respond_to do |f|
|
||||||
f.html { redirect_to success_send_submission_email_index_path }
|
f.html { redirect_to success_send_submission_email_index_path }
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ class SessionsController < Devise::SessionsController
|
|||||||
before_action :configure_permitted_parameters
|
before_action :configure_permitted_parameters
|
||||||
|
|
||||||
def create
|
def create
|
||||||
if Docuseal.multitenant? && !User.exists?(email: sign_in_params[:email])
|
email = sign_in_params[:email].to_s.downcase
|
||||||
|
|
||||||
|
if Docuseal.multitenant? && !User.exists?(email:)
|
||||||
return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)),
|
return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)),
|
||||||
notice: 'Create a new account'
|
notice: 'Create a new account'
|
||||||
end
|
end
|
||||||
|
|
||||||
if User.exists?(email: sign_in_params[:email], otp_required_for_login: true) && sign_in_params[:otp_attempt].blank?
|
if User.exists?(email:, otp_required_for_login: true) && sign_in_params[:otp_attempt].blank?
|
||||||
return render :otp, locals: { resource: User.new(sign_in_params) }, status: :unprocessable_entity
|
return render :otp, locals: { resource: User.new(sign_in_params) }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ class StartFormController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
return redirect_to start_form_path(@template.slug) if @template.archived_at?
|
||||||
|
|
||||||
@submitter = Submitter.where(submission: @template.submissions.where(archived_at: nil))
|
@submitter = Submitter.where(submission: @template.submissions.where(archived_at: nil))
|
||||||
.order(id: :desc)
|
.order(id: :desc)
|
||||||
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
|
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
|
||||||
|
|||||||
@@ -6,23 +6,8 @@ class SubmissionsController < ApplicationController
|
|||||||
|
|
||||||
load_and_authorize_resource :submission, only: %i[show destroy]
|
load_and_authorize_resource :submission, only: %i[show destroy]
|
||||||
|
|
||||||
PRELOAD_ALL_PAGES_AMOUNT = 200
|
|
||||||
|
|
||||||
def show
|
def show
|
||||||
ActiveRecord::Associations::Preloader.new(
|
@submission = Submissions.preload_with_pages(@submission)
|
||||||
records: [@submission],
|
|
||||||
associations: [:template, { template_schema_documents: :blob }]
|
|
||||||
).call
|
|
||||||
|
|
||||||
total_pages =
|
|
||||||
@submission.template_schema_documents.sum { |e| e.metadata.dig('pdf', 'number_of_pages').to_i }
|
|
||||||
|
|
||||||
if total_pages < PRELOAD_ALL_PAGES_AMOUNT
|
|
||||||
ActiveRecord::Associations::Preloader.new(
|
|
||||||
records: @submission.template_schema_documents,
|
|
||||||
associations: [:blob, { preview_images_attachments: :blob }]
|
|
||||||
).call
|
|
||||||
end
|
|
||||||
|
|
||||||
render :show, layout: 'plain'
|
render :show, layout: 'plain'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,8 +4,20 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
|
TTL = 40.minutes
|
||||||
|
FILES_TTL = 5.minutes
|
||||||
|
|
||||||
def index
|
def index
|
||||||
submitter = Submitter.find_by(slug: params[:submitter_slug])
|
submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
||||||
|
|
||||||
|
signature_valid =
|
||||||
|
if submitter&.slug == params[:submitter_slug]
|
||||||
|
true
|
||||||
|
else
|
||||||
|
submitter = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
submitter ||= Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(submitter)
|
Submissions::EnsureResultGenerated.call(submitter)
|
||||||
|
|
||||||
@@ -13,11 +25,26 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(last_submitter)
|
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||||
|
|
||||||
urls =
|
return head :not_found unless last_submitter.completed_at?
|
||||||
Submitters.select_attachments_for_download(last_submitter).map do |attachment|
|
|
||||||
helpers.rails_blob_url(attachment)
|
|
||||||
end
|
|
||||||
|
|
||||||
render json: urls
|
if last_submitter.completed_at < TTL.ago && !signature_valid && !current_user_submitter?(last_submitter)
|
||||||
|
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return head :not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: build_urls(last_submitter)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def current_user_submitter?(submitter)
|
||||||
|
current_user && current_user.account.submitters.exists?(id: submitter.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_urls(submitter)
|
||||||
|
Submitters.select_attachments_for_download(submitter).map do |attachment|
|
||||||
|
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.minutes.from_now.to_i)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -4,26 +4,48 @@ class SubmissionsPreviewController < ApplicationController
|
|||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
PRELOAD_ALL_PAGES_AMOUNT = 200
|
TTL = 40.minutes
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@submission = Submission.find_by!(slug: params[:slug])
|
submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
||||||
|
|
||||||
ActiveRecord::Associations::Preloader.new(
|
signature_valid =
|
||||||
records: [@submission],
|
if submitter && submitter.submission.slug == params[:slug]
|
||||||
associations: [:template, { template_schema_documents: :blob }]
|
@submission = submitter.submission
|
||||||
).call
|
|
||||||
|
|
||||||
total_pages =
|
true
|
||||||
@submission.template_schema_documents.sum { |e| e.metadata.dig('pdf', 'number_of_pages').to_i }
|
end
|
||||||
|
|
||||||
if total_pages < PRELOAD_ALL_PAGES_AMOUNT
|
@submission ||= Submission.find_by!(slug: params[:slug])
|
||||||
ActiveRecord::Associations::Preloader.new(
|
|
||||||
records: @submission.template_schema_documents,
|
if !@submission.submitters.all?(&:completed_at?) && current_user.blank?
|
||||||
associations: [:blob, { preview_images_attachments: :blob }]
|
raise ActionController::RoutingError, 'Not Found'
|
||||||
).call
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if !submission_valid_ttl?(@submission) && !signature_valid
|
||||||
|
Rollbar.info("TTL: #{@submission.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return redirect_to submissions_preview_completed_path(@submission.slug)
|
||||||
|
end
|
||||||
|
|
||||||
|
@submission = Submissions.preload_with_pages(@submission)
|
||||||
|
|
||||||
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: 'form'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def submission_valid_ttl?(submission)
|
||||||
|
return true if current_user && current_user.account.submissions.exists?(id: submission.id)
|
||||||
|
|
||||||
|
last_submitter = submission.submitters.select(&:completed_at?).max_by(&:completed_at)
|
||||||
|
|
||||||
|
last_submitter && last_submitter.completed_at > TTL.ago
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -33,7 +33,7 @@ class TemplatesController < ApplicationController
|
|||||||
@template_data =
|
@template_data =
|
||||||
@template.as_json.merge(
|
@template.as_json.merge(
|
||||||
documents: @template.schema_documents.as_json(
|
documents: @template.schema_documents.as_json(
|
||||||
methods: [:metadata],
|
methods: %i[metadata signed_uuid],
|
||||||
include: { preview_images: { methods: %i[url metadata filename] } }
|
include: { preview_images: { methods: %i[url metadata filename] } }
|
||||||
)
|
)
|
||||||
).to_json
|
).to_json
|
||||||
@@ -51,12 +51,17 @@ class TemplatesController < ApplicationController
|
|||||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
||||||
end
|
end
|
||||||
|
|
||||||
@template.account = current_account
|
if params[:account_id].present? && authorized_clone_account_id?(params[:account_id])
|
||||||
|
@template.account_id = params[:account_id]
|
||||||
|
@template.folder = @template.account.default_template_folder
|
||||||
|
else
|
||||||
|
@template.account = current_account
|
||||||
|
end
|
||||||
|
|
||||||
if @template.save
|
if @template.save
|
||||||
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
||||||
|
|
||||||
redirect_to edit_template_path(@template)
|
maybe_redirect_to_template(@template)
|
||||||
else
|
else
|
||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_entity
|
render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
@@ -81,6 +86,18 @@ class TemplatesController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def authorized_clone_account_id?(account_id)
|
||||||
|
true_user.account_id.to_s == account_id.to_s || true_user.account.linked_accounts.exists?(id: account_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def maybe_redirect_to_template(template)
|
||||||
|
if template.account == current_account
|
||||||
|
redirect_to(edit_template_path(@template))
|
||||||
|
else
|
||||||
|
redirect_back(fallback_location: root_path, notice: 'Template has been clonned')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def template_params
|
def template_params
|
||||||
params.require(:template).permit(:name)
|
params.require(:template).permit(:name)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class TemplatesPreviewController < ApplicationController
|
|||||||
@template_data =
|
@template_data =
|
||||||
@template.as_json.merge(
|
@template.as_json.merge(
|
||||||
documents: @template.schema_documents.as_json(
|
documents: @template.schema_documents.as_json(
|
||||||
methods: [:metadata],
|
methods: %i[metadata signed_uuid],
|
||||||
include: { preview_images: { methods: %i[url metadata filename] } }
|
include: { preview_images: { methods: %i[url metadata filename] } }
|
||||||
)
|
)
|
||||||
).to_json
|
).to_json
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class UserConfigsController < ApplicationController
|
||||||
|
before_action :load_user_config
|
||||||
|
authorize_resource :user_config
|
||||||
|
|
||||||
|
ALLOWED_KEYS = [
|
||||||
|
UserConfig::RECEIVE_COMPLETED_EMAIL
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
InvalidKey = Class.new(StandardError)
|
||||||
|
|
||||||
|
def create
|
||||||
|
@user_config.update!(user_config_params)
|
||||||
|
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_user_config
|
||||||
|
raise InvalidKey unless ALLOWED_KEYS.include?(user_config_params[:key])
|
||||||
|
|
||||||
|
@user_config =
|
||||||
|
UserConfig.find_or_initialize_by(user: current_user, key: user_config_params[:key])
|
||||||
|
end
|
||||||
|
|
||||||
|
def user_config_params
|
||||||
|
params.required(:user_config).permit!.tap do |attrs|
|
||||||
|
attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -90,6 +90,8 @@ window.customElements.define('template-builder', class extends HTMLElement {
|
|||||||
withLogo: this.dataset.withLogo !== 'false',
|
withLogo: this.dataset.withLogo !== 'false',
|
||||||
editable: this.dataset.editable !== 'false',
|
editable: this.dataset.editable !== 'false',
|
||||||
withPayment: this.dataset.withPayment === 'true',
|
withPayment: this.dataset.withPayment === 'true',
|
||||||
|
withFormula: this.dataset.withFormula === 'true',
|
||||||
|
withConditions: this.dataset.withConditions === 'true',
|
||||||
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
||||||
acceptFileTypes: this.dataset.acceptFileTypes,
|
acceptFileTypes: this.dataset.acceptFileTypes,
|
||||||
isDirectUpload: this.dataset.isDirectUpload === 'true'
|
isDirectUpload: this.dataset.isDirectUpload === 'true'
|
||||||
|
|||||||
@@ -75,16 +75,16 @@ button[disabled] .enabled {
|
|||||||
@apply select base-input w-full font-normal;
|
@apply select base-input w-full font-normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-bottom-start:before {
|
.tooltip-bottom-end:before {
|
||||||
transform: translateX(-30%);
|
transform: translateX(-95%);
|
||||||
top: var(--tooltip-offset);
|
top: var(--tooltip-offset);
|
||||||
left: 100%;
|
left: 100%;
|
||||||
right: auto;
|
right: auto;
|
||||||
bottom: auto;
|
bottom: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-bottom-start:after {
|
.tooltip-bottom-end:after {
|
||||||
transform: translateX(-75%);
|
transform: translateX(-25%);
|
||||||
border-color: transparent transparent var(--tooltip-color) transparent;
|
border-color: transparent transparent var(--tooltip-color) transparent;
|
||||||
top: var(--tooltip-tail-offset);
|
top: var(--tooltip-tail-offset);
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@@ -125,3 +125,7 @@ button[disabled] .enabled {
|
|||||||
outline-offset: 3px;
|
outline-offset: 3px;
|
||||||
outline-color: hsl(var(--bc) / 0.2);
|
outline-color: hsl(var(--bc) / 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
select:required:invalid {
|
||||||
|
color: gray !important;
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ export default class extends HTMLElement {
|
|||||||
preventSubmit: 1,
|
preventSubmit: 1,
|
||||||
minLength: 1,
|
minLength: 1,
|
||||||
showOnFocus: true,
|
showOnFocus: true,
|
||||||
|
debounceWaitMs: 200,
|
||||||
onSelect: this.onSelect,
|
onSelect: this.onSelect,
|
||||||
render: this.render,
|
render: this.render,
|
||||||
fetch: this.fetch
|
fetch: this.fetch
|
||||||
@@ -37,7 +38,9 @@ export default class extends HTMLElement {
|
|||||||
if (q) {
|
if (q) {
|
||||||
const queryParams = new URLSearchParams({ q, field: this.dataset.field })
|
const queryParams = new URLSearchParams({ q, field: this.dataset.field })
|
||||||
|
|
||||||
fetch('/api/submitters_autocomplete?' + queryParams).then(async (resp) => {
|
this.currentFetch ||= fetch('/api/submitters_autocomplete?' + queryParams)
|
||||||
|
|
||||||
|
this.currentFetch.then(async (resp) => {
|
||||||
const items = await resp.json()
|
const items = await resp.json()
|
||||||
|
|
||||||
if (q.length < 3) {
|
if (q.length < 3) {
|
||||||
@@ -47,6 +50,8 @@ export default class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
resolve([])
|
resolve([])
|
||||||
|
}).finally(() => {
|
||||||
|
this.currentFetch = null
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
resolve([])
|
resolve([])
|
||||||
|
|||||||
@@ -173,7 +173,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp } from '@tabler/icons-vue'
|
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1 } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FieldArea',
|
name: 'FieldArea',
|
||||||
@@ -240,6 +240,7 @@ export default {
|
|||||||
signature: this.t('signature'),
|
signature: this.t('signature'),
|
||||||
initials: this.t('initials'),
|
initials: this.t('initials'),
|
||||||
date: this.t('date'),
|
date: this.t('date'),
|
||||||
|
number: this.t('number'),
|
||||||
image: this.t('image'),
|
image: this.t('image'),
|
||||||
file: this.t('file'),
|
file: this.t('file'),
|
||||||
select: this.t('select'),
|
select: this.t('select'),
|
||||||
@@ -260,6 +261,7 @@ export default {
|
|||||||
text: IconTextSize,
|
text: IconTextSize,
|
||||||
signature: IconWritingSign,
|
signature: IconWritingSign,
|
||||||
date: IconCalendarEvent,
|
date: IconCalendarEvent,
|
||||||
|
number: IconSquareNumber1,
|
||||||
image: IconPhoto,
|
image: IconPhoto,
|
||||||
initials: IconLetterCaseUpper,
|
initials: IconLetterCaseUpper,
|
||||||
file: IconPaperclip,
|
file: IconPaperclip,
|
||||||
|
|||||||
@@ -8,13 +8,24 @@
|
|||||||
:current-step="currentStepFields"
|
:current-step="currentStepFields"
|
||||||
@focus-step="[saveStep(), goToStep($event, false, true), currentField.type !== 'checkbox' ? isFormVisible = true : '']"
|
@focus-step="[saveStep(), goToStep($event, false, true), currentField.type !== 'checkbox' ? isFormVisible = true : '']"
|
||||||
/>
|
/>
|
||||||
|
<FormulaFieldAreas
|
||||||
|
v-if="formulaFields.length"
|
||||||
|
:fields="formulaFields"
|
||||||
|
:values="values"
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="!isFormVisible"
|
v-if="!isFormVisible"
|
||||||
id="expand_form_button"
|
id="expand_form_button"
|
||||||
class="btn btn-neutral flex text-white absolute rounded-none border-x-0 md:border md:rounded-full bottom-0 w-full md:mb-4 text-base"
|
class="btn btn-neutral flex text-white absolute rounded-none border-x-0 md:border md:rounded-full bottom-0 w-full md:mb-4 text-base"
|
||||||
@click.prevent="isFormVisible = true"
|
@click.prevent="[isFormVisible = true, scrollIntoField(currentField)]"
|
||||||
>
|
>
|
||||||
{{ t('submit_form') }}
|
<template v-if="['initials', 'signature'].includes(currentField.type)">
|
||||||
|
<IconWritingSign stroke-width="1.5" />
|
||||||
|
{{ t('sign_now') }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ t('submit_form') }}
|
||||||
|
</template>
|
||||||
<IconArrowsDiagonal
|
<IconArrowsDiagonal
|
||||||
class="absolute right-0 mr-4"
|
class="absolute right-0 mr-4"
|
||||||
:width="20"
|
:width="20"
|
||||||
@@ -32,7 +43,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 +51,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
|
||||||
@@ -69,6 +83,14 @@
|
|||||||
@focus="scrollIntoField(currentField)"
|
@focus="scrollIntoField(currentField)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<NumberStep
|
||||||
|
v-else-if="currentField.type === 'number'"
|
||||||
|
:key="currentField.uuid"
|
||||||
|
v-model="values[currentField.uuid]"
|
||||||
|
:show-field-names="showFieldNames"
|
||||||
|
:field="currentField"
|
||||||
|
@focus="scrollIntoField(currentField)"
|
||||||
|
/>
|
||||||
<DateStep
|
<DateStep
|
||||||
v-else-if="currentField.type === 'date'"
|
v-else-if="currentField.type === 'date'"
|
||||||
:key="currentField.uuid"
|
:key="currentField.uuid"
|
||||||
@@ -254,7 +276,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"
|
||||||
@@ -385,6 +407,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import FieldAreas from './areas'
|
import FieldAreas from './areas'
|
||||||
|
import FormulaFieldAreas from './formula_areas'
|
||||||
import ImageStep from './image_step'
|
import ImageStep from './image_step'
|
||||||
import SignatureStep from './signature_step'
|
import SignatureStep from './signature_step'
|
||||||
import InitialsStep from './initials_step'
|
import InitialsStep from './initials_step'
|
||||||
@@ -393,12 +416,31 @@ import MultiSelectStep from './multi_select_step'
|
|||||||
import PhoneStep from './phone_step'
|
import PhoneStep from './phone_step'
|
||||||
import PaymentStep from './payment_step'
|
import PaymentStep from './payment_step'
|
||||||
import TextStep from './text_step'
|
import TextStep from './text_step'
|
||||||
|
import NumberStep from './number_step'
|
||||||
import DateStep from './date_step'
|
import DateStep from './date_step'
|
||||||
import FormCompleted from './completed'
|
import FormCompleted from './completed'
|
||||||
import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
import { IconInnerShadowTop, IconArrowsDiagonal, IconWritingSign, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||||
import AppearsOn from './appears_on'
|
import AppearsOn from './appears_on'
|
||||||
import i18n from './i18n'
|
import i18n from './i18n'
|
||||||
|
|
||||||
|
const isEmpty = (obj) => {
|
||||||
|
if (obj == null) return true
|
||||||
|
|
||||||
|
if (Array.isArray(obj) || typeof obj === 'string') {
|
||||||
|
return obj.length === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj === 'object') {
|
||||||
|
return Object.keys(obj).length === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj === false) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SubmissionForm',
|
name: 'SubmissionForm',
|
||||||
components: {
|
components: {
|
||||||
@@ -406,6 +448,7 @@ export default {
|
|||||||
ImageStep,
|
ImageStep,
|
||||||
SignatureStep,
|
SignatureStep,
|
||||||
AppearsOn,
|
AppearsOn,
|
||||||
|
IconWritingSign,
|
||||||
AttachmentStep,
|
AttachmentStep,
|
||||||
InitialsStep,
|
InitialsStep,
|
||||||
MultiSelectStep,
|
MultiSelectStep,
|
||||||
@@ -413,6 +456,8 @@ export default {
|
|||||||
DateStep,
|
DateStep,
|
||||||
IconArrowsDiagonal,
|
IconArrowsDiagonal,
|
||||||
TextStep,
|
TextStep,
|
||||||
|
NumberStep,
|
||||||
|
FormulaFieldAreas,
|
||||||
PhoneStep,
|
PhoneStep,
|
||||||
PaymentStep,
|
PaymentStep,
|
||||||
IconArrowsDiagonalMinimize2,
|
IconArrowsDiagonalMinimize2,
|
||||||
@@ -448,6 +493,11 @@ export default {
|
|||||||
return () => {}
|
return () => {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
expand: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
withConfetti: {
|
withConfetti: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -518,6 +568,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,
|
||||||
@@ -542,7 +597,7 @@ export default {
|
|||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isCompleted: false,
|
isCompleted: false,
|
||||||
isFormVisible: true,
|
isFormVisible: this.expand !== false,
|
||||||
showFillAllRequiredFields: false,
|
showFillAllRequiredFields: false,
|
||||||
currentStep: 0,
|
currentStep: 0,
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
@@ -566,10 +621,12 @@ export default {
|
|||||||
submitterSlug () {
|
submitterSlug () {
|
||||||
return this.submitter.slug
|
return this.submitter.slug
|
||||||
},
|
},
|
||||||
previousSignatureValue () {
|
fieldsUuidIndex () {
|
||||||
const signatureField = [...this.fields].reverse().find((field) => field.type === 'signature' && !!this.values[field.uuid])
|
return this.fields.reduce((acc, f) => {
|
||||||
|
acc[f.uuid] = f
|
||||||
|
|
||||||
return this.values[signatureField?.uuid]
|
return acc
|
||||||
|
}, {})
|
||||||
},
|
},
|
||||||
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])
|
||||||
@@ -577,7 +634,7 @@ export default {
|
|||||||
return this.values[initialsField?.uuid]
|
return this.values[initialsField?.uuid]
|
||||||
},
|
},
|
||||||
isAnonymousChecboxes () {
|
isAnonymousChecboxes () {
|
||||||
return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name) && this.currentStepFields.length > 4
|
return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name && !e.required) && this.currentStepFields.length > 4
|
||||||
},
|
},
|
||||||
isButtonDisabled () {
|
isButtonDisabled () {
|
||||||
if (this.recalculateButtonDisabledKey) {
|
if (this.recalculateButtonDisabledKey) {
|
||||||
@@ -596,15 +653,20 @@ export default {
|
|||||||
return this.fields.filter((f) => !f.readonly).reduce((acc, f) => {
|
return this.fields.filter((f) => !f.readonly).reduce((acc, f) => {
|
||||||
const prevStep = acc[acc.length - 1]
|
const prevStep = acc[acc.length - 1]
|
||||||
|
|
||||||
if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox') {
|
if (this.checkFieldConditions(f)) {
|
||||||
prevStep.push(f)
|
if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox') {
|
||||||
} else {
|
prevStep.push(f)
|
||||||
acc.push([f])
|
} else {
|
||||||
|
acc.push([f])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc
|
return acc
|
||||||
}, [])
|
}, [])
|
||||||
},
|
},
|
||||||
|
formulaFields () {
|
||||||
|
return this.fields.filter((f) => f.preferences?.formula)
|
||||||
|
},
|
||||||
attachmentsIndex () {
|
attachmentsIndex () {
|
||||||
return this.attachments.reduce((acc, a) => {
|
return this.attachments.reduce((acc, a) => {
|
||||||
acc[a.uuid] = a
|
acc[a.uuid] = a
|
||||||
@@ -616,6 +678,11 @@ export default {
|
|||||||
return `/s/${this.submitterSlug}`
|
return `/s/${this.submitterSlug}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
expand (value) {
|
||||||
|
this.isFormVisible = value
|
||||||
|
}
|
||||||
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.submittedValues = JSON.parse(JSON.stringify(this.values))
|
this.submittedValues = JSON.parse(JSON.stringify(this.values))
|
||||||
|
|
||||||
@@ -648,6 +715,10 @@ export default {
|
|||||||
this.currentStep = Math.min(...indexesList)
|
this.currentStep = Math.min(...indexesList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (document.body?.clientWidth >= 768 && this.expand !== true && ['signature', 'initials', 'file', 'image'].includes(this.currentField?.type)) {
|
||||||
|
this.isFormVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
|
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
const root = this.$root.$el.parentNode.getRootNode()
|
const root = this.$root.$el.parentNode.getRootNode()
|
||||||
@@ -675,7 +746,41 @@ 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
|
||||||
|
},
|
||||||
|
checkFieldConditions (field) {
|
||||||
|
if (field.conditions?.length) {
|
||||||
|
return field.conditions.reduce((acc, c) => {
|
||||||
|
if (['empty', 'unchecked'].includes(c.action)) {
|
||||||
|
return acc && isEmpty(this.values[c.field_uuid])
|
||||||
|
} else if (['not_empty', 'checked'].includes(c.action)) {
|
||||||
|
return acc && !isEmpty(this.values[c.field_uuid])
|
||||||
|
} else if (['equal', 'contains'].includes(c.action)) {
|
||||||
|
const field = this.fieldsUuidIndex[c.field_uuid]
|
||||||
|
const option = field.options.find((o) => o.uuid === c.value)
|
||||||
|
const values = [this.values[c.field_uuid]].flat()
|
||||||
|
|
||||||
|
return acc && values.includes(this.optionValue(option, field.options.indexOf(option)))
|
||||||
|
} else if (['not_equal', 'does_not_contain'].includes(c.action)) {
|
||||||
|
const field = this.fieldsUuidIndex[c.field_uuid]
|
||||||
|
const option = field.options.find((o) => o.uuid === c.value)
|
||||||
|
const values = [this.values[c.field_uuid]].flat()
|
||||||
|
|
||||||
|
return acc && !values.includes(this.optionValue(option, field.options.indexOf(option)))
|
||||||
|
} else {
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
}, true)
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
optionValue (option, index) {
|
||||||
|
if (option.value) {
|
||||||
|
return option.value
|
||||||
|
} else {
|
||||||
|
return `${this.t('option')} ${index + 1}`
|
||||||
|
}
|
||||||
},
|
},
|
||||||
maybeTrackEmailClick () {
|
maybeTrackEmailClick () {
|
||||||
const { queryParams } = this
|
const { queryParams } = this
|
||||||
@@ -736,6 +841,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
|
||||||
@@ -747,7 +859,7 @@ export default {
|
|||||||
this.scrollIntoField(step[0])
|
this.scrollIntoField(step[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$refs.form.querySelector('input[type="date"], input[type="text"], select')?.focus()
|
this.$refs.form.querySelector('input[type="date"], input[type="number"], input[type="text"], select')?.focus()
|
||||||
|
|
||||||
if (clickUpload && !this.values[this.currentField.uuid] && ['file', 'image'].includes(this.currentField.type)) {
|
if (clickUpload && !this.values[this.currentField.uuid] && ['file', 'image'].includes(this.currentField.type)) {
|
||||||
this.$refs.form.querySelector('input[type="file"]')?.click()
|
this.$refs.form.querySelector('input[type="file"]')?.click()
|
||||||
@@ -755,7 +867,7 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
saveStep (formData) {
|
saveStep (formData) {
|
||||||
const currentFieldUuid = this.currentField.uuid
|
const currentFieldUuids = this.currentStepFields.map((f) => f.uuid)
|
||||||
|
|
||||||
if (this.isCompleted) {
|
if (this.isCompleted) {
|
||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
@@ -765,7 +877,9 @@ export default {
|
|||||||
body: formData || new FormData(this.$refs.form)
|
body: formData || new FormData(this.$refs.form)
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
this.submittedValues[currentFieldUuid] = this.values[currentFieldUuid]
|
currentFieldUuids.forEach((fieldUuid) => {
|
||||||
|
this.submittedValues[fieldUuid] = this.values[fieldUuid]
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return response
|
return response
|
||||||
@@ -787,7 +901,13 @@ export default {
|
|||||||
|
|
||||||
stepPromise().then(async () => {
|
stepPromise().then(async () => {
|
||||||
const emptyRequiredField = this.stepFields.find((fields, index) => {
|
const emptyRequiredField = this.stepFields.find((fields, index) => {
|
||||||
return index < this.currentStep && fields[0].required && (fields[0].type === 'phone' || !this.allowToSkip) && (this.submittedValues[fields[0].uuid] === true ? false : !this.submittedValues[fields[0].uuid]?.length)
|
if (index >= this.currentStep) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields.some((f) => {
|
||||||
|
return f.required && (f.type === 'phone' || !this.allowToSkip) && isEmpty(this.submittedValues[f.uuid])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const formData = new FormData(this.$refs.form)
|
const formData = new FormData(this.$refs.form)
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
<template>
|
||||||
|
<template
|
||||||
|
v-for="(field, fieldIndex) in fields"
|
||||||
|
:key="field.uuid"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-for="(area, areaIndex) in field.areas"
|
||||||
|
:key="areaIndex"
|
||||||
|
>
|
||||||
|
<Teleport
|
||||||
|
v-if="findPageElementForArea(area)"
|
||||||
|
:to="findPageElementForArea(area)"
|
||||||
|
>
|
||||||
|
<FieldArea
|
||||||
|
v-if="isMathLoaded"
|
||||||
|
:model-value="calculateFormula(field)"
|
||||||
|
:field="field"
|
||||||
|
:area="area"
|
||||||
|
:submittable="false"
|
||||||
|
:field-index="fieldIndex"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import FieldArea from './area'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'FormulaFieldAreas',
|
||||||
|
components: {
|
||||||
|
FieldArea
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
fields: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isMathLoaded: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted () {
|
||||||
|
const {
|
||||||
|
create,
|
||||||
|
evaluateDependencies,
|
||||||
|
addDependencies,
|
||||||
|
subtractDependencies,
|
||||||
|
divideDependencies,
|
||||||
|
multiplyDependencies,
|
||||||
|
powDependencies,
|
||||||
|
roundDependencies,
|
||||||
|
absDependencies,
|
||||||
|
sinDependencies,
|
||||||
|
tanDependencies,
|
||||||
|
cosDependencies
|
||||||
|
} = await import('mathjs')
|
||||||
|
|
||||||
|
this.math = create({
|
||||||
|
evaluateDependencies,
|
||||||
|
addDependencies,
|
||||||
|
subtractDependencies,
|
||||||
|
divideDependencies,
|
||||||
|
multiplyDependencies,
|
||||||
|
powDependencies,
|
||||||
|
roundDependencies,
|
||||||
|
absDependencies,
|
||||||
|
sinDependencies,
|
||||||
|
tanDependencies,
|
||||||
|
cosDependencies
|
||||||
|
})
|
||||||
|
|
||||||
|
this.isMathLoaded = true
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
findPageElementForArea (area) {
|
||||||
|
return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${area.attachment_uuid}-${area.page}`)
|
||||||
|
},
|
||||||
|
calculateFormula (field) {
|
||||||
|
const transformedFormula = field.preferences.formula.replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||||
|
return this.values[uuid] || 0.0
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.math.evaluate(transformedFormula.toLowerCase())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -3,6 +3,7 @@ const en = {
|
|||||||
signature: 'Signature',
|
signature: 'Signature',
|
||||||
initials: 'Initials',
|
initials: 'Initials',
|
||||||
date: 'Date',
|
date: 'Date',
|
||||||
|
number: 'Number',
|
||||||
image: 'Image',
|
image: 'Image',
|
||||||
take_photo: 'Take photo',
|
take_photo: 'Take photo',
|
||||||
file: 'File',
|
file: 'File',
|
||||||
@@ -12,9 +13,11 @@ 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',
|
||||||
|
sign_now: 'Sign Now',
|
||||||
type_here_: 'Type here...',
|
type_here_: 'Type here...',
|
||||||
optional: 'optional',
|
optional: 'optional',
|
||||||
option: 'Option',
|
option: 'Option',
|
||||||
@@ -62,10 +65,12 @@ const en = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
|
minimize: 'Minimizar',
|
||||||
text: 'Texto',
|
text: 'Texto',
|
||||||
signature: 'Firma',
|
signature: 'Firma',
|
||||||
initials: 'Iniciales',
|
initials: 'Iniciales',
|
||||||
date: 'Fecha',
|
date: 'Fecha',
|
||||||
|
number: 'Número',
|
||||||
image: 'Imagen',
|
image: 'Imagen',
|
||||||
file: 'Archivo',
|
file: 'Archivo',
|
||||||
select: 'Seleccionar',
|
select: 'Seleccionar',
|
||||||
@@ -78,6 +83,7 @@ const es = {
|
|||||||
phone: 'Teléfono',
|
phone: 'Teléfono',
|
||||||
take_photo: 'Tomar foto',
|
take_photo: 'Tomar foto',
|
||||||
submit_form: 'Enviar Formulario',
|
submit_form: 'Enviar Formulario',
|
||||||
|
sign_now: 'Firmar ahora',
|
||||||
type_here_: 'Escribe aquí...',
|
type_here_: 'Escribe aquí...',
|
||||||
optional: 'opcional',
|
optional: 'opcional',
|
||||||
appears_on: 'Aparece en',
|
appears_on: 'Aparece en',
|
||||||
@@ -124,10 +130,12 @@ const es = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
|
minimize: 'Minimizza',
|
||||||
text: 'Testo',
|
text: 'Testo',
|
||||||
signature: 'Firma',
|
signature: 'Firma',
|
||||||
initials: 'Iniziali',
|
initials: 'Iniziali',
|
||||||
date: 'Data',
|
date: 'Data',
|
||||||
|
number: 'Numero',
|
||||||
image: 'Immagine',
|
image: 'Immagine',
|
||||||
file: 'File',
|
file: 'File',
|
||||||
select: 'Seleziona',
|
select: 'Seleziona',
|
||||||
@@ -139,6 +147,7 @@ const it = {
|
|||||||
payment: 'Pagamento',
|
payment: 'Pagamento',
|
||||||
phone: 'Telefono',
|
phone: 'Telefono',
|
||||||
submit_form: 'Invia Modulo',
|
submit_form: 'Invia Modulo',
|
||||||
|
sign_now: 'Firma ora',
|
||||||
type_here_: 'Digita qui...',
|
type_here_: 'Digita qui...',
|
||||||
optional: 'opzionale',
|
optional: 'opzionale',
|
||||||
appears_on: 'Compare su',
|
appears_on: 'Compare su',
|
||||||
@@ -186,10 +195,12 @@ const it = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
|
minimize: 'Minimieren',
|
||||||
text: 'Text',
|
text: 'Text',
|
||||||
signature: 'Unterschrift',
|
signature: 'Unterschrift',
|
||||||
initials: 'Initialen',
|
initials: 'Initialen',
|
||||||
date: 'Datum',
|
date: 'Datum',
|
||||||
|
number: 'Nummer',
|
||||||
image: 'Bild',
|
image: 'Bild',
|
||||||
file: 'Datei',
|
file: 'Datei',
|
||||||
select: 'Auswählen',
|
select: 'Auswählen',
|
||||||
@@ -201,6 +212,7 @@ const de = {
|
|||||||
payment: 'Zahlung',
|
payment: 'Zahlung',
|
||||||
phone: 'Telefon',
|
phone: 'Telefon',
|
||||||
submit_form: 'Formular absenden',
|
submit_form: 'Formular absenden',
|
||||||
|
sign_now: 'Jetzt unterschreiben',
|
||||||
type_here_: 'Hier eingeben...',
|
type_here_: 'Hier eingeben...',
|
||||||
optional: 'optional',
|
optional: 'optional',
|
||||||
appears_on: 'Erscheint auf',
|
appears_on: 'Erscheint auf',
|
||||||
@@ -248,10 +260,12 @@ const de = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
|
minimize: 'Réduire',
|
||||||
text: 'Texte',
|
text: 'Texte',
|
||||||
signature: 'Signature',
|
signature: 'Signature',
|
||||||
initials: 'Initiales',
|
initials: 'Initiales',
|
||||||
date: 'Date',
|
date: 'Date',
|
||||||
|
number: 'Numéro',
|
||||||
image: 'Image',
|
image: 'Image',
|
||||||
file: 'Fichier',
|
file: 'Fichier',
|
||||||
select: 'Choisir',
|
select: 'Choisir',
|
||||||
@@ -263,6 +277,7 @@ const fr = {
|
|||||||
payment: 'Paiement',
|
payment: 'Paiement',
|
||||||
phone: 'Téléphone',
|
phone: 'Téléphone',
|
||||||
submit_form: 'Envoyer le Formulaire',
|
submit_form: 'Envoyer le Formulaire',
|
||||||
|
sign_now: 'Signer maintenant',
|
||||||
type_here_: 'Tapez ici...',
|
type_here_: 'Tapez ici...',
|
||||||
optional: 'facultatif',
|
optional: 'facultatif',
|
||||||
appears_on: 'Apparaît sur',
|
appears_on: 'Apparaît sur',
|
||||||
@@ -310,10 +325,12 @@ const fr = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pl = {
|
const pl = {
|
||||||
|
minimize: 'Zminimalizuj',
|
||||||
text: 'Tekst',
|
text: 'Tekst',
|
||||||
signature: 'Podpis',
|
signature: 'Podpis',
|
||||||
initials: 'Inicjały',
|
initials: 'Inicjały',
|
||||||
date: 'Data',
|
date: 'Data',
|
||||||
|
number: 'Numer',
|
||||||
image: 'Obraz',
|
image: 'Obraz',
|
||||||
file: 'Plik',
|
file: 'Plik',
|
||||||
select: 'Wybierz',
|
select: 'Wybierz',
|
||||||
@@ -325,6 +342,7 @@ const pl = {
|
|||||||
payment: 'Płatność',
|
payment: 'Płatność',
|
||||||
phone: 'Telefon',
|
phone: 'Telefon',
|
||||||
submit_form: 'Wyślij Formularz',
|
submit_form: 'Wyślij Formularz',
|
||||||
|
sign_now: 'Podpisz teraz',
|
||||||
type_here_: 'Wpisz tutaj...',
|
type_here_: 'Wpisz tutaj...',
|
||||||
optional: 'opcjonalny',
|
optional: 'opcjonalny',
|
||||||
appears_on: 'Pojawia się na',
|
appears_on: 'Pojawia się na',
|
||||||
@@ -372,10 +390,12 @@ const pl = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const uk = {
|
const uk = {
|
||||||
|
minimize: 'Зменшити',
|
||||||
text: 'Текст',
|
text: 'Текст',
|
||||||
signature: 'Підпис',
|
signature: 'Підпис',
|
||||||
initials: 'Ініціали',
|
initials: 'Ініціали',
|
||||||
date: 'Дата',
|
date: 'Дата',
|
||||||
|
number: 'Число',
|
||||||
image: 'Зображення',
|
image: 'Зображення',
|
||||||
file: 'Файл',
|
file: 'Файл',
|
||||||
select: 'Вибрати',
|
select: 'Вибрати',
|
||||||
@@ -387,6 +407,7 @@ const uk = {
|
|||||||
payment: 'Платіж',
|
payment: 'Платіж',
|
||||||
phone: 'Телефон',
|
phone: 'Телефон',
|
||||||
submit_form: 'Надіслати Форму',
|
submit_form: 'Надіслати Форму',
|
||||||
|
sign_now: 'Підписати зараз',
|
||||||
type_here_: 'Введіть тут',
|
type_here_: 'Введіть тут',
|
||||||
optional: 'необов’язково',
|
optional: 'необов’язково',
|
||||||
appears_on: "З'являється на",
|
appears_on: "З'являється на",
|
||||||
@@ -434,10 +455,12 @@ const uk = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cs = {
|
const cs = {
|
||||||
|
minimize: 'Minimalizovat',
|
||||||
text: 'Text',
|
text: 'Text',
|
||||||
signature: 'Podpis',
|
signature: 'Podpis',
|
||||||
initials: 'Iniciály',
|
initials: 'Iniciály',
|
||||||
date: 'Datum',
|
date: 'Datum',
|
||||||
|
number: 'Číslo',
|
||||||
image: 'Obrázek',
|
image: 'Obrázek',
|
||||||
file: 'Soubor',
|
file: 'Soubor',
|
||||||
select: 'Vybrat',
|
select: 'Vybrat',
|
||||||
@@ -449,6 +472,7 @@ const cs = {
|
|||||||
payment: 'Platba',
|
payment: 'Platba',
|
||||||
phone: 'Telefon',
|
phone: 'Telefon',
|
||||||
submit_form: 'Odeslat formulář',
|
submit_form: 'Odeslat formulář',
|
||||||
|
sign_now: 'Podepsat nyní',
|
||||||
type_here_: 'Zadejte zde',
|
type_here_: 'Zadejte zde',
|
||||||
optional: 'volitelné',
|
optional: 'volitelné',
|
||||||
appears_on: 'Zobrazuje se na',
|
appears_on: 'Zobrazuje se na',
|
||||||
@@ -496,10 +520,12 @@ const cs = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
|
minimize: 'Minimizar',
|
||||||
text: 'Texto',
|
text: 'Texto',
|
||||||
signature: 'Assinatura',
|
signature: 'Assinatura',
|
||||||
initials: 'Iniciais',
|
initials: 'Iniciais',
|
||||||
date: 'Data',
|
date: 'Data',
|
||||||
|
number: 'Número',
|
||||||
image: 'Imagem',
|
image: 'Imagem',
|
||||||
file: 'Arquivo',
|
file: 'Arquivo',
|
||||||
select: 'Selecionar',
|
select: 'Selecionar',
|
||||||
@@ -511,6 +537,7 @@ const pt = {
|
|||||||
payment: 'Pagamento',
|
payment: 'Pagamento',
|
||||||
phone: 'Telefone',
|
phone: 'Telefone',
|
||||||
submit_form: 'Enviar Formulário',
|
submit_form: 'Enviar Formulário',
|
||||||
|
sign_now: 'Assinar agora',
|
||||||
type_here_: 'Digite aqui',
|
type_here_: 'Digite aqui',
|
||||||
optional: 'opcional',
|
optional: 'opcional',
|
||||||
appears_on: 'Aparece em',
|
appears_on: 'Aparece em',
|
||||||
@@ -558,10 +585,12 @@ const pt = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const he = {
|
const he = {
|
||||||
|
minimize: 'לקטן',
|
||||||
text: 'טקסט',
|
text: 'טקסט',
|
||||||
signature: 'חתימה',
|
signature: 'חתימה',
|
||||||
initials: 'ראשי תיקיות',
|
initials: 'ראשי תיבות',
|
||||||
date: 'תאריך',
|
date: 'תאריך',
|
||||||
|
number: 'מספר',
|
||||||
image: 'תמונה',
|
image: 'תמונה',
|
||||||
file: 'קובץ',
|
file: 'קובץ',
|
||||||
select: 'בחר',
|
select: 'בחר',
|
||||||
@@ -573,6 +602,7 @@ const he = {
|
|||||||
payment: 'תשלום',
|
payment: 'תשלום',
|
||||||
phone: 'טלפון',
|
phone: 'טלפון',
|
||||||
submit_form: 'שלח טופס',
|
submit_form: 'שלח טופס',
|
||||||
|
sign_now: 'חתום כעת',
|
||||||
type_here_: 'הקלד כאן',
|
type_here_: 'הקלד כאן',
|
||||||
optional: 'אופציונלי',
|
optional: 'אופציונלי',
|
||||||
option: 'אפשרות',
|
option: 'אפשרות',
|
||||||
@@ -621,10 +651,12 @@ const he = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nl = {
|
const nl = {
|
||||||
|
minimize: 'Minimaliseren',
|
||||||
text: 'Tekst',
|
text: 'Tekst',
|
||||||
signature: 'Handtekening',
|
signature: 'Handtekening',
|
||||||
initials: 'Initialen',
|
initials: 'Initialen',
|
||||||
date: 'Datum',
|
date: 'Datum',
|
||||||
|
number: 'Nummer',
|
||||||
image: 'Afbeelding',
|
image: 'Afbeelding',
|
||||||
take_photo: 'Maak een foto',
|
take_photo: 'Maak een foto',
|
||||||
file: 'Bestand',
|
file: 'Bestand',
|
||||||
@@ -637,6 +669,7 @@ const nl = {
|
|||||||
payment: 'Betaling',
|
payment: 'Betaling',
|
||||||
phone: 'Telefoon',
|
phone: 'Telefoon',
|
||||||
submit_form: 'Formulier verzenden',
|
submit_form: 'Formulier verzenden',
|
||||||
|
sign_now: 'Nu ondertekenen',
|
||||||
type_here_: 'Typ hier...',
|
type_here_: 'Typ hier...',
|
||||||
optional: 'Optioneel',
|
optional: 'Optioneel',
|
||||||
option: 'Optie',
|
option: 'Optie',
|
||||||
@@ -683,6 +716,72 @@ const nl = {
|
|||||||
files: 'Bestanden'
|
files: 'Bestanden'
|
||||||
}
|
}
|
||||||
|
|
||||||
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl }
|
const ar = {
|
||||||
|
text: 'نص',
|
||||||
|
signature: 'توقيع',
|
||||||
|
initials: 'الاختصارات',
|
||||||
|
date: 'تاريخ',
|
||||||
|
number: 'رقم',
|
||||||
|
image: 'صورة',
|
||||||
|
take_photo: 'التقاط صورة',
|
||||||
|
file: 'ملف',
|
||||||
|
select: 'اختيار',
|
||||||
|
checkbox: 'خانة اختيار',
|
||||||
|
multiple: 'متعدد',
|
||||||
|
radio: 'راديو',
|
||||||
|
cells: 'خلايا',
|
||||||
|
stamp: 'ختم',
|
||||||
|
minimize: 'تصغير',
|
||||||
|
payment: 'الدفع',
|
||||||
|
phone: 'هاتف',
|
||||||
|
submit_form: 'إرسال النموذج',
|
||||||
|
sign_now: 'وقع الآن',
|
||||||
|
type_here_: 'اكتب هنا',
|
||||||
|
optional: 'اختياري',
|
||||||
|
option: 'خيار',
|
||||||
|
appears_on: 'يظهر على',
|
||||||
|
page: 'صفحة',
|
||||||
|
select_your_option: 'اختر خيارك',
|
||||||
|
complete_hightlighted_checkboxes_and_click: 'أكمل الخانات المميزة وانقر',
|
||||||
|
submit: 'إرسال',
|
||||||
|
next: 'التالي',
|
||||||
|
click_to_upload: 'انقر للتحميل',
|
||||||
|
or_drag_and_drop_files: 'أو اسحب وأسقط الملفات',
|
||||||
|
send_copy_via_email: 'إرسال نسخة عبر البريد الإلكتروني',
|
||||||
|
download: 'تحميل',
|
||||||
|
clear: 'مسح',
|
||||||
|
redraw: 'إعادة الرسم',
|
||||||
|
draw_initials: 'ارسم الاختصارات',
|
||||||
|
type_signature_here: 'اكتب التوقيع هنا',
|
||||||
|
type_initial_here: 'اكتب الاختصارات هنا',
|
||||||
|
form_has_been_completed: 'تم إكمال النموذج!',
|
||||||
|
create_a_free_account: 'إنشاء حساب مجاني',
|
||||||
|
signed_with: 'تم التوقيع بواسطة',
|
||||||
|
please_check_the_box_to_continue: 'الرجاء التحقق من الخانة للمتابعة',
|
||||||
|
open_source_documents_software: 'برنامج وثائق مفتوح المصدر',
|
||||||
|
verified_phone_number: 'تحقق من رقم الهاتف',
|
||||||
|
use_international_format: 'استخدم الشكل الدولي: +1xxx',
|
||||||
|
six_digits_code: 'رمز مكون من 6 أرقام',
|
||||||
|
change_phone_number: 'تغيير رقم الهاتف',
|
||||||
|
sending: 'جارٍ الإرسال',
|
||||||
|
resend_code: 'إعادة إرسال الرمز',
|
||||||
|
verification_code_has_been_resent: 'تم إعادة إرسال رمز التحقق عبر الرسائل القصيرة',
|
||||||
|
please_fill_all_required_fields: 'الرجاء ملء جميع الحقول المطلوبة',
|
||||||
|
set_today: 'تعيين اليوم',
|
||||||
|
toggle_multiline_text: 'تبديل النصوص متعددة الأسطر',
|
||||||
|
draw_signature: 'ارسم التوقيع',
|
||||||
|
type_initial: 'اكتب الاختصارات',
|
||||||
|
draw: 'ارسم',
|
||||||
|
type: 'اكتب',
|
||||||
|
type_text: 'اكتب نصًا',
|
||||||
|
email_has_been_sent: 'تم إرسال البريد الإلكتروني',
|
||||||
|
processing: 'جارٍ المعالجة',
|
||||||
|
pay_with_strip: 'الدفع بواسطة Stripe',
|
||||||
|
reupload: 'إعادة التحميل',
|
||||||
|
upload: 'تحميل',
|
||||||
|
files: 'الملفات'
|
||||||
|
}
|
||||||
|
|
||||||
|
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar }
|
||||||
|
|
||||||
export default i18n
|
export default i18n
|
||||||
@@ -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')"
|
||||||
@@ -231,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')
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<label
|
||||||
|
v-if="showFieldNames && field.name"
|
||||||
|
:for="field.uuid"
|
||||||
|
dir="auto"
|
||||||
|
class="label text-2xl"
|
||||||
|
:class="{ 'mb-2': !field.description }"
|
||||||
|
><template v-if="field.title"><span v-html="field.title" /></template>
|
||||||
|
<template v-else>{{ field.name }}</template>
|
||||||
|
<template v-if="!field.required">({{ t('optional') }})</template>
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="py-1"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="field.description"
|
||||||
|
class="mb-3 px-1 text-lg"
|
||||||
|
v-html="field.description"
|
||||||
|
/>
|
||||||
|
<AppearsOn :field="field" />
|
||||||
|
<div class="items-center flex">
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="cast_number"
|
||||||
|
value="true"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:id="field.uuid"
|
||||||
|
v-model="number"
|
||||||
|
type="number"
|
||||||
|
class="base-input !text-2xl w-full"
|
||||||
|
step="any"
|
||||||
|
:required="field.required"
|
||||||
|
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||||
|
:name="`values[${field.uuid}]`"
|
||||||
|
@focus="$emit('focus')"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import AppearsOn from './appears_on'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TextStep',
|
||||||
|
components: {
|
||||||
|
AppearsOn
|
||||||
|
},
|
||||||
|
inject: ['t'],
|
||||||
|
props: {
|
||||||
|
field: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
showFieldNames: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: [String, Number],
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['update:model-value', 'focus'],
|
||||||
|
computed: {
|
||||||
|
number: {
|
||||||
|
set (value) {
|
||||||
|
this.$emit('update:model-value', value)
|
||||||
|
},
|
||||||
|
get () {
|
||||||
|
return this.modelValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -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')"
|
||||||
>
|
>
|
||||||
@@ -111,7 +113,7 @@
|
|||||||
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"
|
||||||
@@ -185,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: {
|
||||||
@@ -269,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')
|
||||||
@@ -340,16 +343,18 @@ export default {
|
|||||||
const x = (canvasWidth - targetWidth) / 2
|
const x = (canvasWidth - targetWidth) / 2
|
||||||
const y = (canvasHeight - targetHeight) / 2
|
const y = (canvasHeight - targetHeight) / 2
|
||||||
|
|
||||||
context.clearRect(0, 0, canvasWidth, canvasHeight)
|
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)
|
||||||
|
|
||||||
event.target.value = null
|
this.uploadImageInputKey = Math.random().toString()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async submit () {
|
async submit () {
|
||||||
|
|||||||
@@ -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" />
|
||||||
@@ -206,9 +206,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
defaultName: Field.computed.defaultName,
|
|
||||||
fieldNames: FieldType.computed.fieldNames,
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
fieldIcons: FieldType.computed.fieldIcons,
|
fieldIcons: FieldType.computed.fieldIcons,
|
||||||
|
defaultName () {
|
||||||
|
return this.buildDefaultName(this.field, this.template.fields)
|
||||||
|
},
|
||||||
optionIndexText () {
|
optionIndexText () {
|
||||||
if (this.area.option_uuid && this.field.options) {
|
if (this.area.option_uuid && this.field.options) {
|
||||||
return `${this.field.options.findIndex((o) => o.uuid === this.area.option_uuid) + 1}.`
|
return `${this.field.options.findIndex((o) => o.uuid === this.area.option_uuid) + 1}.`
|
||||||
@@ -312,6 +314,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
buildDefaultName: Field.methods.buildDefaultName,
|
||||||
onNameFocus (e) {
|
onNameFocus (e) {
|
||||||
this.selectedAreaRef.value = this.area
|
this.selectedAreaRef.value = this.area
|
||||||
|
|
||||||
|
|||||||
@@ -273,6 +273,7 @@
|
|||||||
@select="startFieldDraw($event)"
|
@select="startFieldDraw($event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="docuseal_modal_container" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -321,7 +322,11 @@ export default {
|
|||||||
backgroundColor: this.backgroundColor,
|
backgroundColor: this.backgroundColor,
|
||||||
withPhone: this.withPhone,
|
withPhone: this.withPhone,
|
||||||
withPayment: this.withPayment,
|
withPayment: this.withPayment,
|
||||||
selectedAreaRef: computed(() => this.selectedAreaRef)
|
withFormula: this.withFormula,
|
||||||
|
withConditions: this.withConditions,
|
||||||
|
defaultDrawFieldType: this.defaultDrawFieldType,
|
||||||
|
selectedAreaRef: computed(() => this.selectedAreaRef),
|
||||||
|
fieldsDragFieldRef: computed(() => this.fieldsDragFieldRef)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
@@ -364,6 +369,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
defaultDrawFieldType: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'text'
|
||||||
|
},
|
||||||
currencies: {
|
currencies: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -443,6 +453,16 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
withFormula: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
withConditions: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
onlyDefinedFields: {
|
onlyDefinedFields: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -467,6 +487,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
selectedAreaRef: () => ref(),
|
selectedAreaRef: () => ref(),
|
||||||
|
fieldsDragFieldRef: () => ref(),
|
||||||
fieldAreasIndex () {
|
fieldAreasIndex () {
|
||||||
const areas = {}
|
const areas = {}
|
||||||
|
|
||||||
@@ -755,7 +776,9 @@ export default {
|
|||||||
|
|
||||||
let type = (pageMask.clientWidth * area.w) < 35 ? 'checkbox' : 'text'
|
let type = (pageMask.clientWidth * area.w) < 35 ? 'checkbox' : 'text'
|
||||||
|
|
||||||
if (this.fieldTypes.length !== 0 && !this.fieldTypes.includes(type)) {
|
if (this.defaultDrawFieldType && this.defaultDrawFieldType !== 'text') {
|
||||||
|
type = this.defaultDrawFieldType
|
||||||
|
} else if (this.fieldTypes.length !== 0 && !this.fieldTypes.includes(type)) {
|
||||||
type = this.fieldTypes[0]
|
type = this.fieldTypes[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -796,7 +819,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDropfield (area) {
|
onDropfield (area) {
|
||||||
const field = {
|
const field = this.fieldsDragFieldRef.value || {
|
||||||
name: '',
|
name: '',
|
||||||
uuid: v4(),
|
uuid: v4(),
|
||||||
submitter_uuid: this.selectedSubmitter.uuid,
|
submitter_uuid: this.selectedSubmitter.uuid,
|
||||||
@@ -804,17 +827,19 @@ export default {
|
|||||||
...this.dragField
|
...this.dragField
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['select', 'multiple', 'radio'].includes(field.type)) {
|
if (!this.fieldsDragFieldRef.value) {
|
||||||
field.options = [{ value: '', uuid: v4() }]
|
if (['select', 'multiple', 'radio'].includes(field.type)) {
|
||||||
}
|
field.options = [{ value: '', uuid: v4() }]
|
||||||
|
}
|
||||||
|
|
||||||
if (field.type === 'stamp') {
|
if (field.type === 'stamp') {
|
||||||
field.readonly = true
|
field.readonly = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.type === 'date') {
|
if (field.type === 'date') {
|
||||||
field.preferences = {
|
field.preferences = {
|
||||||
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
|
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -870,11 +895,23 @@ export default {
|
|||||||
fieldArea.cell_w = baseArea.cell_w || (baseArea.w / 5)
|
fieldArea.cell_w = baseArea.cell_w || (baseArea.w / 5)
|
||||||
}
|
}
|
||||||
|
|
||||||
field.areas = [fieldArea]
|
field.areas ||= []
|
||||||
|
|
||||||
|
const lastArea = field.areas[field.areas.length - 1]
|
||||||
|
|
||||||
|
if (lastArea) {
|
||||||
|
fieldArea.x -= lastArea.w / 2
|
||||||
|
fieldArea.w = lastArea.w
|
||||||
|
fieldArea.h = lastArea.h
|
||||||
|
}
|
||||||
|
|
||||||
|
field.areas.push(fieldArea)
|
||||||
|
|
||||||
this.selectedAreaRef.value = fieldArea
|
this.selectedAreaRef.value = fieldArea
|
||||||
|
|
||||||
this.template.fields.push(field)
|
if (this.template.fields.indexOf(field) === -1) {
|
||||||
|
this.template.fields.push(field)
|
||||||
|
}
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="modal modal-open items-start !animate-none overflow-y-auto"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="absolute top-0 bottom-0 right-0 left-0"
|
||||||
|
@click.prevent="$emit('close')"
|
||||||
|
/>
|
||||||
|
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
||||||
|
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||||
|
<span>
|
||||||
|
{{ t('condition') }} - {{ field.name || buildDefaultName(field, template.fields) }}
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="text-xl"
|
||||||
|
@click.prevent="$emit('close')"
|
||||||
|
>×</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
v-if="!withConditions"
|
||||||
|
class="bg-base-300 rounded-xl py-2 px-3 text-center"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="https://www.docuseal.co/pricing"
|
||||||
|
target="_blank"
|
||||||
|
class="link"
|
||||||
|
>Available in Pro</a>
|
||||||
|
</div>
|
||||||
|
<form @submit.prevent="validateSaveAndClose">
|
||||||
|
<div class="my-4">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<select
|
||||||
|
class="select select-bordered select-sm w-full bg-white h-11 pl-4 text-base font-normal"
|
||||||
|
required
|
||||||
|
@change="[
|
||||||
|
newCondition.field_uuid = $event.target.value,
|
||||||
|
delete newCondition.value,
|
||||||
|
(conditionActions.includes(newCondition.action) ? '' : newCondition.action = conditionActions[0])
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
value=""
|
||||||
|
disabled
|
||||||
|
:selected="!newCondition.field_uuid"
|
||||||
|
>
|
||||||
|
{{ t('select_field_') }}
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
v-for="f in fields"
|
||||||
|
:key="f.uuid"
|
||||||
|
:value="f.uuid"
|
||||||
|
:selected="newCondition.field_uuid === f.uuid"
|
||||||
|
>
|
||||||
|
{{ f.name || buildDefaultName(f, template.fields) }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
v-model="newCondition.action"
|
||||||
|
class="select select-bordered select-sm w-full h-11 pl-4 text-base font-normal"
|
||||||
|
:class="{ 'bg-white': newCondition.field_uuid, 'bg-base-300': !newCondition.field_uuid }"
|
||||||
|
:required="newCondition.field_uuid"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="action in conditionActions"
|
||||||
|
:key="action"
|
||||||
|
:value="action"
|
||||||
|
>
|
||||||
|
{{ t(action) }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
v-if="conditionField?.options?.length"
|
||||||
|
class="select select-bordered select-sm w-full bg-white h-11 pl-4 text-base font-normal"
|
||||||
|
required
|
||||||
|
@change="newCondition.value = $event.target.value"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
value=""
|
||||||
|
disabled
|
||||||
|
selected
|
||||||
|
>
|
||||||
|
{{ t('select_value_') }}
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
v-for="(option, index) in conditionField.options"
|
||||||
|
:key="option.uuid"
|
||||||
|
:value="option.uuid"
|
||||||
|
:selected="newCondition.value === option.uuid"
|
||||||
|
>
|
||||||
|
{{ option.value || `${t('option')} ${index + 1}` }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="base-button w-full mt-2"
|
||||||
|
>
|
||||||
|
{{ t('save') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<div
|
||||||
|
v-if="field.conditions?.[0]?.field_uuid"
|
||||||
|
class="text-center w-full mt-4"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="link"
|
||||||
|
@click="[newCondition.field_uuid = null, delete field.conditions, validateSaveAndClose()]"
|
||||||
|
>
|
||||||
|
{{ t('remove_condition') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'ConditionModal',
|
||||||
|
inject: ['t', 'save', 'template', 'withConditions'],
|
||||||
|
props: {
|
||||||
|
field: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
buildDefaultName: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['close'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
newCondition: this.field.conditions?.[0] || {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
conditionField () {
|
||||||
|
return this.fields.find((f) => f.uuid === this.newCondition.field_uuid)
|
||||||
|
},
|
||||||
|
conditionActions () {
|
||||||
|
return this.fieldActions(this.conditionField)
|
||||||
|
},
|
||||||
|
fields () {
|
||||||
|
return this.template.fields.reduce((acc, f) => {
|
||||||
|
if (f !== this.field && f.submitter_uuid === this.field.submitter_uuid) {
|
||||||
|
acc.push(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.field.conditions ||= []
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fieldActions (field) {
|
||||||
|
const actions = []
|
||||||
|
|
||||||
|
if (!field) {
|
||||||
|
return actions
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.type === 'checkbox') {
|
||||||
|
actions.push('checked', 'unchecked')
|
||||||
|
} else if (['radio', 'select'].includes(field.type)) {
|
||||||
|
actions.push('equal', 'not_equal')
|
||||||
|
} else if (['multiple'].includes(field.type)) {
|
||||||
|
actions.push('contains', 'does_not_contain')
|
||||||
|
} else {
|
||||||
|
actions.push('not_empty', 'empty')
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions
|
||||||
|
},
|
||||||
|
validateSaveAndClose () {
|
||||||
|
if (!this.withConditions) {
|
||||||
|
return alert('Available only in Pro')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.newCondition.field_uuid) {
|
||||||
|
this.field.conditions = [this.newCondition]
|
||||||
|
} else {
|
||||||
|
delete this.field.conditions
|
||||||
|
}
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
this.$emit('close')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
<IconPencil
|
<IconPencil
|
||||||
class="cursor-pointer flex-none opacity-0 group-hover/contenteditable-container:opacity-100 group-hover/contenteditable:opacity-100 align-middle peer-focus:hidden"
|
class="cursor-pointer flex-none opacity-0 group-hover/contenteditable-container:opacity-100 group-hover/contenteditable:opacity-100 align-middle peer-focus:hidden"
|
||||||
:style="iconInline ? {} : { right: -(1.1 * iconWidth) + 'px' }"
|
:style="iconInline ? {} : { right: -(1.1 * iconWidth) + 'px' }"
|
||||||
title="Edit"
|
:title="t('edit')"
|
||||||
:class="{ invisible: !editable, 'ml-1': !withRequired, 'absolute': !iconInline, 'inline align-bottom': iconInline }"
|
:class="{ invisible: !editable, 'ml-1': !withRequired, 'absolute': !iconInline, 'inline align-bottom': iconInline }"
|
||||||
:width="iconWidth"
|
:width="iconWidth"
|
||||||
:stroke-width="iconStrokeWidth"
|
:stroke-width="iconStrokeWidth"
|
||||||
@@ -44,6 +44,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
IconPencil
|
IconPencil
|
||||||
},
|
},
|
||||||
|
inject: ['t'],
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export default {
|
|||||||
return this.previewImagesIndex[i] || {
|
return this.previewImagesIndex[i] || {
|
||||||
metadata: lazyloadMetadata,
|
metadata: lazyloadMetadata,
|
||||||
id: Math.random().toString(),
|
id: Math.random().toString(),
|
||||||
url: this.basePreviewUrl + `/preview/${this.document.uuid}/${i}.jpg`
|
url: this.basePreviewUrl + `/preview/${this.document.signed_uuid || this.document.uuid}/${i}.jpg`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-if="field && !field.areas.length"
|
v-if="field && !field.areas.length"
|
||||||
title="Draw"
|
:title="t('draw')"
|
||||||
class="relative cursor-pointer text-transparent group-hover:text-base-content"
|
class="relative cursor-pointer text-transparent group-hover:text-base-content"
|
||||||
@click="$emit('set-draw', { field })"
|
@click="$emit('set-draw', { field })"
|
||||||
>
|
>
|
||||||
@@ -65,6 +65,28 @@
|
|||||||
:stroke-width="1.6"
|
:stroke-width="1.6"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="field.preferences?.formula"
|
||||||
|
class="relative cursor-pointer text-transparent group-hover:text-base-content"
|
||||||
|
:title="t('formula')"
|
||||||
|
@click="isShowFormulaModal = true"
|
||||||
|
>
|
||||||
|
<IconMathFunction
|
||||||
|
:width="18"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="field.conditions?.length"
|
||||||
|
class="relative cursor-pointer text-transparent group-hover:text-base-content"
|
||||||
|
:title="t('condition')"
|
||||||
|
@click="isShowConditionsModal = true"
|
||||||
|
>
|
||||||
|
<IconRouteAltLeft
|
||||||
|
:width="18"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
<PaymentSettings
|
<PaymentSettings
|
||||||
v-if="field.type === 'payment'"
|
v-if="field.type === 'payment'"
|
||||||
:field="field"
|
:field="field"
|
||||||
@@ -77,7 +99,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 +164,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
|
||||||
@@ -199,6 +258,33 @@
|
|||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<hr class="pb-0.5 mt-0.5">
|
<hr class="pb-0.5 mt-0.5">
|
||||||
|
<li>
|
||||||
|
<label
|
||||||
|
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||||
|
@click="isShowConditionsModal = !isShowConditionsModal"
|
||||||
|
>
|
||||||
|
<IconRouteAltLeft
|
||||||
|
width="18"
|
||||||
|
/>
|
||||||
|
<span class="text-sm">
|
||||||
|
{{ t('condition') }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li v-if="field.type == 'number'">
|
||||||
|
<label
|
||||||
|
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||||
|
@click="isShowFormulaModal = true"
|
||||||
|
>
|
||||||
|
<IconMathFunction
|
||||||
|
width="18"
|
||||||
|
/>
|
||||||
|
<span class="text-sm">
|
||||||
|
{{ t('formula') }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<hr class="pb-0.5 mt-0.5">
|
||||||
<li
|
<li
|
||||||
v-for="(area, index) in field.areas || []"
|
v-for="(area, index) in field.areas || []"
|
||||||
:key="index"
|
:key="index"
|
||||||
@@ -212,7 +298,8 @@
|
|||||||
:width="20"
|
:width="20"
|
||||||
:stroke-width="1.6"
|
:stroke-width="1.6"
|
||||||
/>
|
/>
|
||||||
{{ t('page') }} {{ area.page + 1 }}
|
{{ t('page') }}
|
||||||
|
<template v-if="template.schema.length > 1">{{ template.schema.findIndex((item) => item.attachment_uuid === area.attachment_uuid) + 1 }}-</template>{{ area.page + 1 }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="!field.areas?.length || !['radio', 'multiple'].includes(field.type)">
|
<li v-if="!field.areas?.length || !['radio', 'multiple'].includes(field.type)">
|
||||||
@@ -245,7 +332,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
|
||||||
@@ -271,7 +358,7 @@
|
|||||||
{{ index + 1 }}.
|
{{ index + 1 }}.
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
v-if="['radio', 'multiple'].includes(field.type) && (index > 0 || field.areas.find((a) => a.option_uuid) || !field.areas.length) && !field.areas.find((a) => a.option_uuid === option.uuid)"
|
v-if="editable && ['radio', 'multiple'].includes(field.type) && (index > 0 || field.areas.find((a) => a.option_uuid) || !field.areas.length) && !field.areas.find((a) => a.option_uuid === option.uuid)"
|
||||||
class="items-center flex w-full"
|
class="items-center flex w-full"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -284,7 +371,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 })"
|
||||||
>
|
>
|
||||||
@@ -300,12 +387,14 @@
|
|||||||
class="w-full input input-primary input-xs text-sm bg-transparent"
|
class="w-full input input-primary input-xs text-sm bg-transparent"
|
||||||
:placeholder="`${t('option')} ${index + 1}`"
|
:placeholder="`${t('option')} ${index + 1}`"
|
||||||
type="text"
|
type="text"
|
||||||
|
:readonly="!editable"
|
||||||
required
|
required
|
||||||
dir="auto"
|
dir="auto"
|
||||||
@focus="maybeFocusOnOptionArea(option)"
|
@focus="maybeFocusOnOptionArea(option)"
|
||||||
@blur="save"
|
@blur="save"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
v-if="editable"
|
||||||
class="text-sm w-3.5"
|
class="text-sm w-3.5"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
@click="removeOption(option)"
|
@click="removeOption(option)"
|
||||||
@@ -313,8 +402,12 @@
|
|||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="field.options && !editable"
|
||||||
|
class="pb-1"
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="field.options"
|
v-else-if="field.options && editable"
|
||||||
class="text-center text-sm w-full pb-1"
|
class="text-center text-sm w-full pb-1"
|
||||||
@click="addOption"
|
@click="addOption"
|
||||||
>
|
>
|
||||||
@@ -322,6 +415,26 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Teleport
|
||||||
|
v-if="isShowFormulaModal"
|
||||||
|
:to="modalContainerEl"
|
||||||
|
>
|
||||||
|
<FormulaModal
|
||||||
|
:field="field"
|
||||||
|
:build-default-name="buildDefaultName"
|
||||||
|
@close="isShowFormulaModal = false"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
<Teleport
|
||||||
|
v-if="isShowConditionsModal"
|
||||||
|
:to="modalContainerEl"
|
||||||
|
>
|
||||||
|
<ConditionsModal
|
||||||
|
:field="field"
|
||||||
|
:build-default-name="buildDefaultName"
|
||||||
|
@close="isShowConditionsModal = false"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -329,7 +442,9 @@
|
|||||||
import Contenteditable from './contenteditable'
|
import Contenteditable from './contenteditable'
|
||||||
import FieldType from './field_type'
|
import FieldType from './field_type'
|
||||||
import PaymentSettings from './payment_settings'
|
import PaymentSettings from './payment_settings'
|
||||||
import { IconShape, IconNewSection, IconTrashX, IconCopy, IconSettings } from '@tabler/icons-vue'
|
import FormulaModal from './formula_modal'
|
||||||
|
import ConditionsModal from './conditions_modal'
|
||||||
|
import { IconRouteAltLeft, IconMathFunction, IconShape, IconNewSection, IconTrashX, IconCopy, IconSettings } from '@tabler/icons-vue'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -340,7 +455,11 @@ export default {
|
|||||||
IconShape,
|
IconShape,
|
||||||
PaymentSettings,
|
PaymentSettings,
|
||||||
IconNewSection,
|
IconNewSection,
|
||||||
|
FormulaModal,
|
||||||
|
ConditionsModal,
|
||||||
|
IconRouteAltLeft,
|
||||||
IconTrashX,
|
IconTrashX,
|
||||||
|
IconMathFunction,
|
||||||
IconCopy,
|
IconCopy,
|
||||||
FieldType
|
FieldType
|
||||||
},
|
},
|
||||||
@@ -366,11 +485,16 @@ export default {
|
|||||||
return {
|
return {
|
||||||
isNameFocus: false,
|
isNameFocus: false,
|
||||||
showPaymentModal: false,
|
showPaymentModal: false,
|
||||||
|
isShowFormulaModal: false,
|
||||||
|
isShowConditionsModal: false,
|
||||||
renderDropdown: false
|
renderDropdown: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
fieldNames: FieldType.computed.fieldNames,
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
|
modalContainerEl () {
|
||||||
|
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
||||||
|
},
|
||||||
dateFormats () {
|
dateFormats () {
|
||||||
return [
|
return [
|
||||||
'MM/DD/YYYY',
|
'MM/DD/YYYY',
|
||||||
@@ -385,22 +509,7 @@ export default {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
defaultName () {
|
defaultName () {
|
||||||
if (this.field.type === 'payment' && this.field.preferences?.price) {
|
return this.buildDefaultName(this.field, this.template.fields)
|
||||||
const { price, currency } = this.field.preferences || {}
|
|
||||||
|
|
||||||
const formattedPrice = new Intl.NumberFormat([], {
|
|
||||||
style: 'currency',
|
|
||||||
currency
|
|
||||||
}).format(price)
|
|
||||||
|
|
||||||
return `${this.fieldNames[this.field.type]} ${formattedPrice}`
|
|
||||||
} else {
|
|
||||||
const typeIndex = this.template.fields.filter((f) => f.type === this.field.type).indexOf(this.field)
|
|
||||||
|
|
||||||
const suffix = { multiple: this.t('select'), radio: this.t('group') }[this.field.type] || this.t('field')
|
|
||||||
|
|
||||||
return `${this.fieldNames[this.field.type]} ${suffix} ${typeIndex + 1}`
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
areas () {
|
areas () {
|
||||||
return this.field.areas || []
|
return this.field.areas || []
|
||||||
@@ -415,6 +524,24 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
buildDefaultName (field, fields) {
|
||||||
|
if (field.type === 'payment' && field.preferences?.price) {
|
||||||
|
const { price, currency } = field.preferences || {}
|
||||||
|
|
||||||
|
const formattedPrice = new Intl.NumberFormat([], {
|
||||||
|
style: 'currency',
|
||||||
|
currency
|
||||||
|
}).format(price)
|
||||||
|
|
||||||
|
return `${this.fieldNames[field.type]} ${formattedPrice}`
|
||||||
|
} else {
|
||||||
|
const typeIndex = fields.filter((f) => f.type === field.type).indexOf(field)
|
||||||
|
|
||||||
|
const suffix = { multiple: this.t('select'), radio: this.t('group') }[field.type] || this.t('field')
|
||||||
|
|
||||||
|
return `${this.fieldNames[field.type]} ${suffix} ${typeIndex + 1}`
|
||||||
|
}
|
||||||
|
},
|
||||||
formatDate (date, format) {
|
formatDate (date, format) {
|
||||||
const monthFormats = {
|
const monthFormats = {
|
||||||
M: 'numeric',
|
M: 'numeric',
|
||||||
@@ -448,11 +575,13 @@ export default {
|
|||||||
const areaString = JSON.stringify(field.areas[0])
|
const areaString = JSON.stringify(field.areas[0])
|
||||||
|
|
||||||
this.template.documents.forEach((attachment) => {
|
this.template.documents.forEach((attachment) => {
|
||||||
attachment.preview_images.forEach((page) => {
|
const numberOfPages = attachment.metadata?.pdf?.number_of_pages || attachment.preview_images.length
|
||||||
if (!field.areas.find((area) => area.attachment_uuid === attachment.uuid && area.page === parseInt(page.filename))) {
|
|
||||||
field.areas.push({ ...JSON.parse(areaString), attachment_uuid: attachment.uuid, page: parseInt(page.filename) })
|
for (let page = 0; page <= numberOfPages - 1; page++) {
|
||||||
|
if (!field.areas.find((area) => area.attachment_uuid === attachment.uuid && area.page === page)) {
|
||||||
|
field.areas.push({ ...JSON.parse(areaString), attachment_uuid: attachment.uuid, page })
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
|||||||
@@ -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()] "
|
||||||
|
|||||||
@@ -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')">
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp } from '@tabler/icons-vue'
|
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1 } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FiledTypeDropdown',
|
name: 'FiledTypeDropdown',
|
||||||
@@ -94,6 +94,7 @@ export default {
|
|||||||
signature: this.t('signature'),
|
signature: this.t('signature'),
|
||||||
initials: this.t('initials'),
|
initials: this.t('initials'),
|
||||||
date: this.t('date'),
|
date: this.t('date'),
|
||||||
|
number: this.t('number'),
|
||||||
image: this.t('image'),
|
image: this.t('image'),
|
||||||
file: this.t('file'),
|
file: this.t('file'),
|
||||||
select: this.t('select'),
|
select: this.t('select'),
|
||||||
@@ -112,17 +113,29 @@ export default {
|
|||||||
signature: IconWritingSign,
|
signature: IconWritingSign,
|
||||||
initials: IconLetterCaseUpper,
|
initials: IconLetterCaseUpper,
|
||||||
date: IconCalendarEvent,
|
date: IconCalendarEvent,
|
||||||
|
number: IconSquareNumber1,
|
||||||
image: IconPhoto,
|
image: IconPhoto,
|
||||||
file: IconPaperclip,
|
|
||||||
checkbox: IconCheckbox,
|
checkbox: IconCheckbox,
|
||||||
|
multiple: IconChecks,
|
||||||
|
file: IconPaperclip,
|
||||||
radio: IconCircleDot,
|
radio: IconCircleDot,
|
||||||
select: IconSelect,
|
select: IconSelect,
|
||||||
multiple: IconChecks,
|
|
||||||
cells: IconColumns3,
|
cells: IconColumns3,
|
||||||
stamp: IconRubberStamp,
|
stamp: IconRubberStamp,
|
||||||
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: {
|
||||||
|
|||||||
@@ -24,11 +24,11 @@
|
|||||||
:data-uuid="field.uuid"
|
:data-uuid="field.uuid"
|
||||||
:field="field"
|
:field="field"
|
||||||
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
|
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
|
||||||
:editable="editable && (!dragField || dragField !== field)"
|
:editable="editable && (!fieldsDragFieldRef.value || fieldsDragFieldRef.value !== field)"
|
||||||
:default-field="defaultFields.find((f) => f.name === field.name)"
|
:default-field="defaultFields.find((f) => f.name === field.name)"
|
||||||
:draggable="editable"
|
:draggable="editable"
|
||||||
@dragstart="dragField = field"
|
@dragstart="fieldsDragFieldRef.value = field"
|
||||||
@dragend="dragField = null"
|
@dragend="fieldsDragFieldRef.value = null"
|
||||||
@remove="removeField"
|
@remove="removeField"
|
||||||
@scroll-to="$emit('scroll-to-area', $event)"
|
@scroll-to="$emit('scroll-to-area', $event)"
|
||||||
@set-draw="$emit('set-draw', $event)"
|
@set-draw="$emit('set-draw', $event)"
|
||||||
@@ -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
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
<div
|
<div
|
||||||
v-else-if="type == 'phone' && (fieldTypes.length === 0 || fieldTypes.includes(type))"
|
v-else-if="type == 'phone' && (fieldTypes.length === 0 || fieldTypes.includes(type))"
|
||||||
class="tooltip tooltip-bottom flex"
|
class="tooltip tooltip-bottom flex"
|
||||||
:class="{'tooltip-bottom-start': !withPayment, 'tooltip-bottom': withPayment }"
|
:class="{'tooltip-bottom-end': withPayment, 'tooltip-bottom': !withPayment }"
|
||||||
data-tip="Unlock SMS-verified phone number field with paid plan. Use text field for phone numbers without verification."
|
data-tip="Unlock SMS-verified phone number field with paid plan. Use text field for phone numbers without verification."
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@@ -154,7 +154,7 @@ export default {
|
|||||||
IconDrag,
|
IconDrag,
|
||||||
IconLock
|
IconLock
|
||||||
},
|
},
|
||||||
inject: ['save', 'backgroundColor', 'withPhone', 'withPayment', 't'],
|
inject: ['save', 'backgroundColor', 'withPhone', 'withPayment', 't', 'fieldsDragFieldRef'],
|
||||||
props: {
|
props: {
|
||||||
fields: {
|
fields: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@@ -205,14 +205,20 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['set-draw', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter'],
|
emits: ['set-draw', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter'],
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
dragField: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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)
|
||||||
},
|
},
|
||||||
@@ -228,7 +234,7 @@ export default {
|
|||||||
},
|
},
|
||||||
onFieldDragover (e) {
|
onFieldDragover (e) {
|
||||||
const targetField = e.target.closest('[data-uuid]')
|
const targetField = e.target.closest('[data-uuid]')
|
||||||
const dragField = this.$refs.fields.querySelector(`[data-uuid="${this.dragField.uuid}"]`)
|
const dragField = this.$refs.fields.querySelector(`[data-uuid="${this.fieldsDragFieldRef.value.uuid}"]`)
|
||||||
|
|
||||||
if (dragField && targetField && targetField !== dragField) {
|
if (dragField && targetField && targetField !== dragField) {
|
||||||
const fields = Array.from(this.$refs.fields.children)
|
const fields = Array.from(this.$refs.fields.children)
|
||||||
@@ -272,6 +278,14 @@ export default {
|
|||||||
removeField (field) {
|
removeField (field) {
|
||||||
this.fields.splice(this.fields.indexOf(field), 1)
|
this.fields.splice(this.fields.indexOf(field), 1)
|
||||||
|
|
||||||
|
this.fields.forEach((f) => {
|
||||||
|
(f.conditions || []).forEach((c) => {
|
||||||
|
if (c.field_uuid === field.uuid) {
|
||||||
|
f.conditions.splice(f.conditions.indexOf(c), 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
addField (type, area = null) {
|
addField (type, area = null) {
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="modal modal-open items-start !animate-none overflow-y-auto"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="absolute top-0 bottom-0 right-0 left-0"
|
||||||
|
@click.prevent="$emit('close')"
|
||||||
|
/>
|
||||||
|
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
||||||
|
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||||
|
<span>
|
||||||
|
{{ t('formula') }} - {{ field.name || buildDefaultName(field, template.fields) }}
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="text-xl"
|
||||||
|
@click.prevent="$emit('close')"
|
||||||
|
>×</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
v-if="!withFormula"
|
||||||
|
class="bg-base-300 rounded-xl py-2 px-3 text-center"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="https://www.docuseal.co/pricing"
|
||||||
|
target="_blank"
|
||||||
|
class="link"
|
||||||
|
>Available in Pro</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex-inline mb-2 gap-2 space-y-1">
|
||||||
|
<button
|
||||||
|
v-for="f in fields"
|
||||||
|
:key="f.uuid"
|
||||||
|
class="mr-1 btn btn-neutral btn-outline border-base-content/20 btn-sm normal-case font-normal bg-white !rounded-xl"
|
||||||
|
@click.prevent="insertTextUnderCursor(`{{${f.name || buildDefaultName(f, template.fields)}}}`)"
|
||||||
|
>
|
||||||
|
<IconCodePlus
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
stroke-width="1.5"
|
||||||
|
/>
|
||||||
|
{{ f.name || buildDefaultName(f, template.fields) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="flex">
|
||||||
|
<textarea
|
||||||
|
ref="textarea"
|
||||||
|
v-model="formula"
|
||||||
|
class="base-textarea !rounded-xl !text-base font-mono w-full !outline-0 !ring-0 !px-3"
|
||||||
|
required="true"
|
||||||
|
@input="resizeTextarea"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 mt-1">
|
||||||
|
<div
|
||||||
|
target="blank"
|
||||||
|
class="text-sm mb-2 inline space-x-2"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="bg-base-200 px-2 rounded-xl"
|
||||||
|
@click="insertTextUnderCursor(' + ')"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="bg-base-200 px-2 rounded-xl"
|
||||||
|
@click="insertTextUnderCursor(' - ')"
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="bg-base-200 px-2 rounded-xl"
|
||||||
|
@click="insertTextUnderCursor(' * ')"
|
||||||
|
>
|
||||||
|
*
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="bg-base-200 px-2 rounded-xl"
|
||||||
|
@click="insertTextUnderCursor(' / ')"
|
||||||
|
>
|
||||||
|
/
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="bg-base-200 px-2 rounded-xl"
|
||||||
|
@click="insertTextUnderCursor(' % ')"
|
||||||
|
>
|
||||||
|
%
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="bg-base-200 px-2 rounded-xl"
|
||||||
|
@click="insertTextUnderCursor('^')"
|
||||||
|
>
|
||||||
|
^
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="bg-base-200 px-2 rounded-xl"
|
||||||
|
@click="insertTextUnderCursor('round()')"
|
||||||
|
>
|
||||||
|
round(n, d)
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="bg-base-200 px-2 rounded-xl"
|
||||||
|
@click="insertTextUnderCursor('abs()')"
|
||||||
|
>
|
||||||
|
abs(n)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="base-button w-full"
|
||||||
|
@click.prevent="validateSaveAndClose"
|
||||||
|
>
|
||||||
|
{{ t('save') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconCodePlus } from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'FormulaModal',
|
||||||
|
components: {
|
||||||
|
IconCodePlus
|
||||||
|
},
|
||||||
|
inject: ['t', 'save', 'template', 'withFormula'],
|
||||||
|
props: {
|
||||||
|
field: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
buildDefaultName: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['close'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
formula: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
fields () {
|
||||||
|
return this.template.fields.reduce((acc, f) => {
|
||||||
|
if (f !== this.field && f.submitter_uuid === this.field.submitter_uuid && ['number'].includes(f.type) && !f.preferences?.formula) {
|
||||||
|
acc.push(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.field.preferences ||= {}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.formula = this.humanizeFormula(this.field.preferences.formula || '')
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
humanizeFormula (text) {
|
||||||
|
return text.replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||||
|
const foundField = this.fields.find((f) => f.uuid === uuid)
|
||||||
|
|
||||||
|
if (foundField) {
|
||||||
|
return `{{${foundField.name || this.buildDefaultName(foundField, this.template.fields)}}}`
|
||||||
|
} else {
|
||||||
|
return '{{FIELD NOT FOUND}}'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
normalizeFormula (text) {
|
||||||
|
return text.replace(/{{(.*?)}}/g, (match, name) => {
|
||||||
|
const foundField = this.fields.find((f) => {
|
||||||
|
return (f.name || this.buildDefaultName(f, this.template.fields)).trim() === name.trim()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (foundField) {
|
||||||
|
return `{{${foundField.uuid}}}`
|
||||||
|
} else {
|
||||||
|
return '{{FIELD NOT FOUND}}'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
validateSaveAndClose () {
|
||||||
|
if (!this.withFormula) {
|
||||||
|
return alert('Available only in Pro')
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedFormula = this.normalizeFormula(this.formula)
|
||||||
|
|
||||||
|
if (normalizedFormula.includes('FIELD NOT FOUND')) {
|
||||||
|
alert('Some fields are missing in the formula.')
|
||||||
|
} else {
|
||||||
|
this.field.preferences.formula = normalizedFormula
|
||||||
|
this.field.readonly = !!normalizedFormula
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
|
||||||
|
this.$emit('close')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
insertTextUnderCursor (textToInsert) {
|
||||||
|
const textarea = this.$refs.textarea
|
||||||
|
|
||||||
|
const selectionEnd = textarea.selectionEnd
|
||||||
|
const cursorPos = selectionEnd
|
||||||
|
|
||||||
|
const newText = textarea.value.substring(0, cursorPos) + textToInsert + textarea.value.substring(cursorPos)
|
||||||
|
|
||||||
|
this.formula = newText
|
||||||
|
|
||||||
|
textarea.setSelectionRange(cursorPos + textToInsert.length, cursorPos + textToInsert.length)
|
||||||
|
|
||||||
|
textarea.focus()
|
||||||
|
},
|
||||||
|
resizeTextarea () {
|
||||||
|
const textarea = this.$refs.textarea
|
||||||
|
|
||||||
|
textarea.style.height = 'auto'
|
||||||
|
textarea.style.height = textarea.scrollHeight + 'px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,11 +1,30 @@
|
|||||||
const en = {
|
const en = {
|
||||||
|
unchecked: 'Unchecked',
|
||||||
|
equal: 'Equal',
|
||||||
|
not_equal: 'Not equal',
|
||||||
|
contains: 'Contains',
|
||||||
|
does_not_contain: 'Does not contain',
|
||||||
|
not_empty: 'Not empty',
|
||||||
|
empty: 'Empty',
|
||||||
|
select_field_: 'Select field...',
|
||||||
|
select_value_: 'Select value...',
|
||||||
|
remove_condition: 'Remove condition',
|
||||||
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',
|
set_signing_date: 'Set signing date',
|
||||||
send: 'Send',
|
send: 'Send',
|
||||||
|
remove: 'Remove',
|
||||||
|
edit: 'Edit',
|
||||||
|
settings: 'Settings',
|
||||||
|
up: 'Up',
|
||||||
|
down: 'Down',
|
||||||
checked: 'Checked',
|
checked: 'Checked',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
|
any: 'Any',
|
||||||
|
drawn: 'Drawn',
|
||||||
|
formula: 'Formula',
|
||||||
|
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',
|
||||||
@@ -22,6 +41,7 @@ const en = {
|
|||||||
copy_to_all_pages: 'Copy to All Pages',
|
copy_to_all_pages: 'Copy to All Pages',
|
||||||
add_option: 'Add option',
|
add_option: 'Add option',
|
||||||
option: 'Option',
|
option: 'Option',
|
||||||
|
condition: 'Condition',
|
||||||
first_party: 'First Party',
|
first_party: 'First Party',
|
||||||
second_party: 'Second Party',
|
second_party: 'Second Party',
|
||||||
third_party: 'Third Party',
|
third_party: 'Third Party',
|
||||||
@@ -42,9 +62,11 @@ const en = {
|
|||||||
eighteenth_party: 'Eighteenth Party',
|
eighteenth_party: 'Eighteenth Party',
|
||||||
nineteenth_party: 'Nineteenth Party',
|
nineteenth_party: 'Nineteenth Party',
|
||||||
twentieth_party: 'Twentieth 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',
|
||||||
|
number: 'Number',
|
||||||
signature: 'Signature',
|
signature: 'Signature',
|
||||||
initials: 'Initials',
|
initials: 'Initials',
|
||||||
date: 'Date',
|
date: 'Date',
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-show="resizeDirection || isMove || isDrag || showMask || (drawField && isMobile)"
|
v-show="resizeDirection || isMove || isDrag || showMask || (drawField && isMobile) || fieldsDragFieldRef.value"
|
||||||
id="mask"
|
id="mask"
|
||||||
ref="mask"
|
ref="mask"
|
||||||
class="top-0 bottom-0 left-0 right-0 absolute"
|
class="top-0 bottom-0 left-0 right-0 absolute"
|
||||||
@@ -61,7 +61,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
FieldArea
|
FieldArea
|
||||||
},
|
},
|
||||||
inject: ['fieldTypes'],
|
inject: ['fieldTypes', 'defaultDrawFieldType', 'fieldsDragFieldRef'],
|
||||||
props: {
|
props: {
|
||||||
image: {
|
image: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -123,7 +123,9 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
defaultFieldType () {
|
defaultFieldType () {
|
||||||
if (this.fieldTypes.length !== 0 && !this.fieldTypes.includes('text')) {
|
if (this.defaultDrawFieldType && this.defaultDrawFieldType !== 'text') {
|
||||||
|
return this.defaultDrawFieldType
|
||||||
|
} else if (this.fieldTypes.length !== 0 && !this.fieldTypes.includes('text')) {
|
||||||
return this.fieldTypes[0]
|
return this.fieldTypes[0]
|
||||||
} else {
|
} else {
|
||||||
return 'text'
|
return 'text'
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
title="Settings"
|
:title="t('settings')"
|
||||||
class="cursor-pointer text-transparent group-hover:text-base-content"
|
class="cursor-pointer text-transparent group-hover:text-base-content"
|
||||||
>
|
>
|
||||||
<IconSettings
|
<IconSettings
|
||||||
@@ -167,7 +167,7 @@ export default {
|
|||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
IconBrandStripe
|
IconBrandStripe
|
||||||
},
|
},
|
||||||
inject: ['backgroundColor', 'save', 'currencies'],
|
inject: ['backgroundColor', 'save', 'currencies', 't'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|||||||
@@ -26,12 +26,14 @@ class ProcessSubmitterCompletionJob < ApplicationJob
|
|||||||
|
|
||||||
user = submission.created_by_user || submitter.template.author
|
user = submission.created_by_user || submitter.template.author
|
||||||
|
|
||||||
if submitter.template.account.users.exists?(id: user.id) &&
|
if submitter.account.users.exists?(id: user.id) && submission.preferences['send_email'] != false
|
||||||
submission.preferences['send_email'] != false
|
if submitter.submission.submitters.map(&:email).exclude?(user.email) &&
|
||||||
SubmitterMailer.completed_email(submitter, user).deliver_later!
|
user.user_configs.find_by(key: UserConfig::RECEIVE_COMPLETED_EMAIL)&.value != false
|
||||||
|
SubmitterMailer.completed_email(submitter, user).deliver_later!
|
||||||
|
end
|
||||||
|
|
||||||
bcc = submission.preferences['bcc_completed'].presence ||
|
bcc = submission.preferences['bcc_completed'].presence ||
|
||||||
submission.template.account.account_configs
|
submission.account.account_configs
|
||||||
.find_by(key: AccountConfig::BCC_EMAILS)&.value.presence
|
.find_by(key: AccountConfig::BCC_EMAILS)&.value.presence
|
||||||
|
|
||||||
bcc.to_s.scan(User::EMAIL_REGEXP).each do |to|
|
bcc.to_s.scan(User::EMAIL_REGEXP).each do |to|
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
class SendFormCompletedWebhookRequestJob < ApplicationJob
|
class SendFormCompletedWebhookRequestJob < ApplicationJob
|
||||||
USER_AGENT = 'DocuSeal.co Webhook'
|
USER_AGENT = 'DocuSeal.co Webhook'
|
||||||
|
|
||||||
NotSuccessStatus = Class.new(StandardError)
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(submitter)
|
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?
|
||||||
@@ -14,17 +15,26 @@ class SendFormCompletedWebhookRequestJob < ApplicationJob
|
|||||||
|
|
||||||
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
||||||
|
|
||||||
resp = Faraday.post(config.value,
|
resp = begin
|
||||||
{
|
Faraday.post(config.value,
|
||||||
event_type: 'form.completed',
|
{
|
||||||
timestamp: Time.current,
|
event_type: 'form.completed',
|
||||||
data: Submitters::SerializeForWebhook.call(submitter)
|
timestamp: Time.current,
|
||||||
}.to_json,
|
data: Submitters::SerializeForWebhook.call(submitter)
|
||||||
'Content-Type' => 'application/json',
|
}.to_json,
|
||||||
'User-Agent' => USER_AGENT)
|
'Content-Type' => 'application/json',
|
||||||
|
'User-Agent' => USER_AGENT)
|
||||||
|
rescue Faraday::Error
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
if resp.status.to_i >= 400 && (!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||||
raise NotSuccessStatus, resp.status.to_s
|
(!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
|
end
|
||||||
@@ -3,26 +3,36 @@
|
|||||||
class SendFormStartedWebhookRequestJob < ApplicationJob
|
class SendFormStartedWebhookRequestJob < ApplicationJob
|
||||||
USER_AGENT = 'DocuSeal.co Webhook'
|
USER_AGENT = 'DocuSeal.co Webhook'
|
||||||
|
|
||||||
NotSuccessStatus = Class.new(StandardError)
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(submitter)
|
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
|
||||||
|
|
||||||
resp = Faraday.post(config.value,
|
resp = begin
|
||||||
{
|
Faraday.post(config.value,
|
||||||
event_type: 'form.started',
|
{
|
||||||
timestamp: Time.current,
|
event_type: 'form.started',
|
||||||
data: Submitters::SerializeForWebhook.call(submitter)
|
timestamp: Time.current,
|
||||||
}.to_json,
|
data: Submitters::SerializeForWebhook.call(submitter)
|
||||||
'Content-Type' => 'application/json',
|
}.to_json,
|
||||||
'User-Agent' => USER_AGENT)
|
'Content-Type' => 'application/json',
|
||||||
|
'User-Agent' => USER_AGENT)
|
||||||
|
rescue Faraday::Error
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
if resp.status.to_i >= 400 && (!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||||
raise NotSuccessStatus, resp.status.to_s
|
(!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
|
end
|
||||||
@@ -3,26 +3,36 @@
|
|||||||
class SendFormViewedWebhookRequestJob < ApplicationJob
|
class SendFormViewedWebhookRequestJob < ApplicationJob
|
||||||
USER_AGENT = 'DocuSeal.co Webhook'
|
USER_AGENT = 'DocuSeal.co Webhook'
|
||||||
|
|
||||||
NotSuccessStatus = Class.new(StandardError)
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(submitter)
|
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
|
||||||
|
|
||||||
resp = Faraday.post(config.value,
|
resp = begin
|
||||||
{
|
Faraday.post(config.value,
|
||||||
event_type: 'form.viewed',
|
{
|
||||||
timestamp: Time.current,
|
event_type: 'form.viewed',
|
||||||
data: Submitters::SerializeForWebhook.call(submitter)
|
timestamp: Time.current,
|
||||||
}.to_json,
|
data: Submitters::SerializeForWebhook.call(submitter)
|
||||||
'Content-Type' => 'application/json',
|
}.to_json,
|
||||||
'User-Agent' => USER_AGENT)
|
'Content-Type' => 'application/json',
|
||||||
|
'User-Agent' => USER_AGENT)
|
||||||
|
rescue Faraday::Error
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
if resp.status.to_i >= 400 && (!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||||
raise NotSuccessStatus, resp.status.to_s
|
(!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
|
end
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
class SubmitterMailer < ApplicationMailer
|
class SubmitterMailer < ApplicationMailer
|
||||||
MAX_ATTACHMENTS_SIZE = 10.megabytes
|
MAX_ATTACHMENTS_SIZE = 10.megabytes
|
||||||
|
SIGN_TTL = 1.hour + 20.minutes
|
||||||
|
|
||||||
DEFAULT_INVITATION_SUBJECT = 'You are invited to submit a form'
|
DEFAULT_INVITATION_SUBJECT = 'You are invited to submit a form'
|
||||||
|
|
||||||
@@ -61,9 +62,10 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
subject:)
|
subject:)
|
||||||
end
|
end
|
||||||
|
|
||||||
def documents_copy_email(submitter, to: nil)
|
def documents_copy_email(submitter, to: nil, sig: false)
|
||||||
@current_account = submitter.submission.template.account
|
@current_account = submitter.submission.template.account
|
||||||
@submitter = submitter
|
@submitter = submitter
|
||||||
|
@sig = submitter.signed_id(expires_in: SIGN_TTL, purpose: :download_completed) if sig
|
||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(@submitter)
|
Submissions::EnsureResultGenerated.call(@submitter)
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class Account < ApplicationRecord
|
|||||||
has_many :template_folders, dependent: :destroy
|
has_many :template_folders, dependent: :destroy
|
||||||
has_one :default_template_folder, -> { where(name: TemplateFolder::DEFAULT_NAME) },
|
has_one :default_template_folder, -> { where(name: TemplateFolder::DEFAULT_NAME) },
|
||||||
class_name: 'TemplateFolder', dependent: :destroy, inverse_of: :account
|
class_name: 'TemplateFolder', dependent: :destroy, inverse_of: :account
|
||||||
has_many :submissions, through: :templates
|
has_many :submissions, dependent: :destroy
|
||||||
has_many :submitters, through: :submissions
|
has_many :submitters, through: :submissions
|
||||||
has_many :account_linked_accounts, dependent: :destroy
|
has_many :account_linked_accounts, dependent: :destroy
|
||||||
has_many :account_testing_accounts, -> { testing }, dependent: :destroy,
|
has_many :account_testing_accounts, -> { testing }, dependent: :destroy,
|
||||||
@@ -30,6 +30,10 @@ class Account < ApplicationRecord
|
|||||||
foreign_key: :linked_account_id,
|
foreign_key: :linked_account_id,
|
||||||
class_name: 'AccountLinkedAccount',
|
class_name: 'AccountLinkedAccount',
|
||||||
inverse_of: :linked_account
|
inverse_of: :linked_account
|
||||||
|
has_many :linked_account_accounts, dependent: :destroy,
|
||||||
|
foreign_key: :linked_account_id,
|
||||||
|
class_name: 'AccountLinkedAccount',
|
||||||
|
inverse_of: :linked_account
|
||||||
has_many :linked_accounts, through: :account_linked_accounts
|
has_many :linked_accounts, through: :account_linked_accounts
|
||||||
has_many :testing_accounts, through: :account_testing_accounts, source: :linked_account
|
has_many :testing_accounts, through: :account_testing_accounts, source: :linked_account
|
||||||
has_many :active_users, -> { active }, dependent: :destroy,
|
has_many :active_users, -> { active }, dependent: :destroy,
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class Submission < ApplicationRecord
|
|||||||
def audit_trail_url
|
def audit_trail_url
|
||||||
return if audit_trail.blank?
|
return if audit_trail.blank?
|
||||||
|
|
||||||
Rails.application.routes.url_helpers.rails_storage_proxy_url(audit_trail, **Docuseal.default_url_options)
|
ActiveStorage::Blob.proxy_url(audit_trail.blob)
|
||||||
end
|
end
|
||||||
alias audit_log_url audit_trail_url
|
alias audit_log_url audit_trail_url
|
||||||
end
|
end
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
|
# index_submission_events_on_created_at (created_at)
|
||||||
# index_submission_events_on_submission_id (submission_id)
|
# index_submission_events_on_submission_id (submission_id)
|
||||||
# index_submission_events_on_submitter_id (submitter_id)
|
# index_submission_events_on_submitter_id (submitter_id)
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
<div class="max-w-lg mx-auto px-2">
|
|
||||||
<h1 class="text-3xl font-bold text-center mt-8">Profile Details</h1>
|
|
||||||
<%= form_for('', as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: new_registration_path) do |f| %>
|
|
||||||
<% if params[:redir].present? %>
|
|
||||||
<%= hidden_field_tag :redir, params[:redir] %>
|
|
||||||
<% end %>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<%= render 'devise/shared/error_messages', resource: %>
|
|
||||||
<%= f.fields_for resource do |ff| %>
|
|
||||||
<div class="grid gap-2 md:grid-cols-2 md:gap-4">
|
|
||||||
<div class="form-control">
|
|
||||||
<%= ff.label :first_name, class: 'label' %>
|
|
||||||
<%= ff.text_field :first_name, required: true, class: 'base-input' %>
|
|
||||||
</div>
|
|
||||||
<div class="form-control">
|
|
||||||
<%= ff.label :last_name, class: 'label' %>
|
|
||||||
<%= ff.text_field :last_name, required: true, class: 'base-input' %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<%= f.fields_for resource do |ff| %>
|
|
||||||
<div class="form-control <%= 'hidden' if (params[:oauth_callback] || params[:sign_up]) && resource.email? %>">
|
|
||||||
<%= ff.label :email, class: 'label' %>
|
|
||||||
<%= ff.email_field :email, value: EmailTypo.call(resource.email), required: true, class: 'base-input' %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<%= f.fields_for resource.account do |ff| %>
|
|
||||||
<set-timezone data-input-id="_account_timezone"></set-timezone>
|
|
||||||
<%= ff.hidden_field :timezone %>
|
|
||||||
<div class="form-control">
|
|
||||||
<%= ff.label :name, 'Company name (optional)', class: 'label' %>
|
|
||||||
<%= ff.text_field :name, class: 'base-input' %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<%= f.fields_for resource do |ff| %>
|
|
||||||
<div class="form-control <%= 'hidden' if params[:oauth_callback] %>">
|
|
||||||
<%= ff.label :password, class: 'label' %>
|
|
||||||
<%= ff.password_field :password, required: !params[:oauth_callback], class: 'base-input' %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<div class="form-control">
|
|
||||||
<%= f.button button_title(title: 'Sign up', disabled_with: 'Signing up'), class: 'base-button' %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<%= render 'devise/shared/links' %>
|
|
||||||
</div>
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<div class="max-w-lg mx-auto px-2">
|
|
||||||
<% if params[:redir].to_s.ends_with?('on_premise') %>
|
|
||||||
<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| %>
|
|
||||||
<% if params[:redir].present? %>
|
|
||||||
<%= hidden_field_tag :redir, params[:redir] %>
|
|
||||||
<% end %>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="form-control">
|
|
||||||
<%= f.label :email, class: 'label' %>
|
|
||||||
<%= f.email_field :email, autofocus: true, autocomplete: 'email', class: 'base-input', placeholder: 'Enter email to continue' %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-control">
|
|
||||||
<%= f.button button_title(title: 'Sign up', disabled_with: 'Sign up'), name: 'sign_up', value: true, class: 'base-button' %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% if devise_mapping.omniauthable? %>
|
|
||||||
<div class="space-y-4">
|
|
||||||
<% if User.omniauth_providers.include?(:google_oauth2) %>
|
|
||||||
<%= form_for '', url: omniauth_authorize_path(resource_name, :google_oauth2), data: { turbo: false }, method: :post do |f| %>
|
|
||||||
<set-timezone data-input-id="state" data-params="true"></set-timezone>
|
|
||||||
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query %>
|
|
||||||
<%= f.button button_title(title: 'Sign up with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), class: 'white-button w-full mt-4' %>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
<% if User.omniauth_providers.include?(:microsoft_office365) %>
|
|
||||||
<%= form_for '', url: omniauth_authorize_path(resource_name, :microsoft_office365), data: { turbo: false }, method: :post do |f| %>
|
|
||||||
<set-timezone data-input-id="state_microsoft" data-params="true"></set-timezone>
|
|
||||||
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query, id: 'state_microsoft' %>
|
|
||||||
<%= f.button button_title(title: 'Sign up with Microsoft', icon: svg_icon('brand_microsoft', class: 'w-6 h-6')), class: 'white-button w-full' %>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<%= render 'devise/shared/links' %>
|
|
||||||
</div>
|
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= ff.label :security_label, 'SMTP Security', class: 'label' %>
|
<%= ff.label :security_label, 'SMTP Security', class: 'label' %>
|
||||||
<div class="flex items-center space-x-6">
|
<div class="flex items-center space-x-6">
|
||||||
<% [%w[None none], %w[SSL ssl], %w[TLS tls]].each do |(label, val)| %>
|
<% [%w[Auto none], %w[SSL ssl], %w[TLS tls]].each do |(label, val)| %>
|
||||||
<%= ff.label :security, value: val, for: "#{val}_radio", class: 'label' do %>
|
<%= ff.label :security, value: val, for: "#{val}_radio", class: 'label' do %>
|
||||||
<%= ff.radio_button :security, val, checked: (value['security'].blank? && val == 'none') || value['security'] == val, id: "#{val}_radio", class: 'base-radio mr-2' %>
|
<%= ff.radio_button :security, val, checked: (value['security'].blank? && val == 'none') || value['security'] == val, id: "#{val}_radio", class: 'base-radio mr-2' %>
|
||||||
<%= label %>
|
<%= label %>
|
||||||
|
|||||||
@@ -110,7 +110,7 @@
|
|||||||
<span>
|
<span>
|
||||||
Timeserver URL
|
Timeserver URL
|
||||||
</span>
|
</span>
|
||||||
<span class="tooltip" data-tip="URL of the trusted timeserver to be used to generate timestamp signatures.">
|
<span class="tooltip" data-tip="URL of the trusted RFC 3161 timeserver to be used to generate timestamp signatures.">
|
||||||
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -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>
|
||||||
@@ -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.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>
|
<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}" %>" data-turbo="false">Learn More</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2,6 +2,20 @@
|
|||||||
<%= render 'shared/settings_nav' %>
|
<%= render 'shared/settings_nav' %>
|
||||||
<div class="flex-grow max-w-xl mx-auto">
|
<div class="flex-grow max-w-xl mx-auto">
|
||||||
<h1 class="text-4xl font-bold mb-4">Email Notifications</h1>
|
<h1 class="text-4xl font-bold mb-4">Email Notifications</h1>
|
||||||
|
<div class="mt-2 mb-1">
|
||||||
|
<% user_config = UserConfig.find_or_initialize_by(user: current_user, key: UserConfig::RECEIVE_COMPLETED_EMAIL) %>
|
||||||
|
<% if can?(:manage, user_config) %>
|
||||||
|
<%= form_for user_config, url: user_configs_path, method: :post do |f| %>
|
||||||
|
<%= f.hidden_field :key %>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>
|
||||||
|
Receive notification emails on completed submission
|
||||||
|
</span>
|
||||||
|
<%= f.check_box :value, class: 'toggle', checked: user_config.value != false, onchange: 'this.form.requestSubmit()' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
<%= render 'email_stats' %>
|
<%= render 'email_stats' %>
|
||||||
<%= render 'bcc_form', config: @bcc_config %>
|
<%= render 'bcc_form', config: @bcc_config %>
|
||||||
<div class="flex justify-between items-end mb-4 mt-8">
|
<div class="flex justify-between items-end mb-4 mt-8">
|
||||||
|
|||||||
@@ -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.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>
|
<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}" %>" data-turbo="false">Learn More</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -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.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>
|
<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}" %>" data-turbo="false">Learn More</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -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.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>
|
<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}" %>" data-turbo="false">Learn More</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1 +1 @@
|
|||||||
<%= render 'docuseal_logo' %>
|
<%= render 'start_form/docuseal_logo' %>
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<div dir="auto" class="text-center text-2xl font-semibold">
|
<div dir="auto" class="text-center text-2xl font-semibold">
|
||||||
<%= t('form_has_been_submitted_already') %>
|
<%= t('form_has_been_submitted_already') %>
|
||||||
</div>
|
</div>
|
||||||
<% if Accounts.can_send_emails?(@submitter.submission.template.account) %>
|
<% if Accounts.can_send_emails?(@submitter.account) %>
|
||||||
<div>
|
<div>
|
||||||
<%= button_to button_title(title: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'base-button w-full' %>
|
<%= button_to button_title(title: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'base-button w-full' %>
|
||||||
</div>
|
</div>
|
||||||
@@ -31,4 +31,4 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%= render 'shared/attribution', link_path: '/start' %>
|
<%= render 'shared/attribution', link_path: '/start', account: @submitter.account %>
|
||||||
@@ -37,4 +37,4 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%= render 'shared/attribution', link_path: '/start' %>
|
<%= render 'shared/attribution', link_path: '/start', account: @template.account %>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% elsif field['type'] == 'date' %>
|
<% elsif field['type'] == 'date' && value != '{{date}}' %>
|
||||||
<autosize-field></autosize-field>
|
<autosize-field></autosize-field>
|
||||||
<div class="flex items-center px-0.5">
|
<div class="flex items-center px-0.5">
|
||||||
<%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), local_assigns[:locale]) %>
|
<%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), local_assigns[:locale]) %>
|
||||||
|
|||||||
@@ -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, expires_at: 4.hours.from_now) %>" 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
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if last_submitter = @submission.submitters.to_a.select(&:completed_at?).max_by(&:completed_at) %>
|
<% if last_submitter = @submission.submitters.to_a.select(&:completed_at?).max_by(&:completed_at) %>
|
||||||
<download-button data-src="<%= submitter_download_index_path(last_submitter.slug) %>" class="base-button">
|
<download-button data-src="<%= submitter_download_index_path(last_submitter.slug, { sig: params[:sig] }.compact) %>" class="base-button">
|
||||||
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
||||||
<%= svg_icon('download', class: 'w-6 h-6') %>
|
<%= svg_icon('download', class: 'w-6 h-6') %>
|
||||||
<span class="hidden md:inline">Download</span>
|
<span class="hidden md:inline">Download</span>
|
||||||
@@ -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: %>
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if signed_in? && submitter && submitter.phone && !submitter.completed_at && can?(:update, submitter) %>
|
<% if signed_in? && submitter && submitter.phone && !submitter.completed_at && can?(:update, submitter) %>
|
||||||
<%= render 'send_sms_button', submitter: %>
|
<%= render 'submissions/send_sms_button', submitter: %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if signed_in? && submitter && !submitter.completed_at? && can?(:create, submitter) %>
|
<% if signed_in? && submitter && !submitter.completed_at? && can?(:create, submitter) %>
|
||||||
<div class="mt-2 mb-1">
|
<div class="mt-2 mb-1">
|
||||||
@@ -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,45 @@
|
|||||||
|
<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 Accounts.can_send_emails?(@submission.account) %>
|
||||||
|
<%= form_for '', url: send_submission_email_index_path, method: :post, html: { class: 'space-y-4', onsubmit: 'event.submitter.disabled = true' } do |f| %>
|
||||||
|
<div dir="auto" class="form-control !mt-0">
|
||||||
|
<%= f.hidden_field :submission_slug, value: @submission.slug %>
|
||||||
|
<%= f.label :email, t('email'), class: 'label' %>
|
||||||
|
<%= f.email_field :email, value: current_user&.email || params[:email], required: true, class: 'base-input', placeholder: t('send_copy_to_email') %>
|
||||||
|
</div>
|
||||||
|
<div dir="auto" class="form-control">
|
||||||
|
<%= f.button button_title(title: t('send_copy_to_email'), disabled_with: t('starting')), class: 'base-button' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% if Docuseal.multitenant? %>
|
||||||
|
<div class="divider uppercase"><%= t('or') %></div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% if Docuseal.multitenant? %>
|
||||||
|
<div>
|
||||||
|
<%= link_to 'Create free account', registration_path, class: 'white-button w-full' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<%= render 'shared/attribution', link_path: '/start', account: @submission.account %>
|
||||||
@@ -11,11 +11,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div dir="auto">
|
<div dir="auto">
|
||||||
<p class="text-lg font-bold mb-1"><%= @submitter.submission.template.name %></p>
|
<p class="text-lg font-bold mb-1"><%= @submitter.submission.template.name %></p>
|
||||||
<p class="text-sm"><%= t('form_has_been_deleted_by_html', name: @submitter.submission.template.account.name) %></p>
|
<p class="text-sm"><%= t('form_has_been_deleted_by_html', name: @submitter.account.name) %></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%= render 'shared/attribution', link_path: '/start' %>
|
<%= render 'shared/attribution', link_path: '/start', account: @submitter.account %>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<% if Accounts.can_send_emails?(@submitter.submission.template.account) && @submitter.email.present? %>
|
<% if Accounts.can_send_emails?(@submitter.account) && @submitter.email.present? %>
|
||||||
<%= button_to button_title(title: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
|
<%= button_to button_title(title: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
|
||||||
<% if @submitter.submission.template.submitters.size != 1 %>
|
<% if @submitter.submission.template.submitters.size != 1 %>
|
||||||
<div class="divider uppercase"><%= t('or') %></div>
|
<div class="divider uppercase"><%= t('or') %></div>
|
||||||
@@ -25,18 +25,20 @@
|
|||||||
<div class="py-2"></div>
|
<div class="py-2"></div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<download-button data-src="<%= submitter_download_index_path(@submitter.slug) %>" class="base-button w-full">
|
<% if @submitter.completed_at > 30.minutes.ago || (current_user && current_user.account.submitters.exists?(id: @submitter.id)) %>
|
||||||
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
<download-button data-src="<%= submitter_download_index_path(@submitter.slug) %>" class="base-button w-full">
|
||||||
<%= svg_icon('download', class: 'w-6 h-6') %>
|
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
||||||
<span><%= t('download_documents') %></span>
|
<%= svg_icon('download', class: 'w-6 h-6') %>
|
||||||
</span>
|
<span><%= t('download_documents') %></span>
|
||||||
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
|
</span>
|
||||||
<%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
|
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
|
||||||
<span><%= t('downloading') %></span>
|
<%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
|
||||||
</span>
|
<span><%= t('downloading') %></span>
|
||||||
</download-button>
|
</span>
|
||||||
|
</download-button>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% if @submitter.submission.template.submitters.size == 1 && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false %>
|
<% if @submitter.submission.template.submitters.size == 1 && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false && !@submitter.template.archived_at? %>
|
||||||
<div class="divider uppercase"><%= t('or') %></div>
|
<div class="divider uppercase"><%= t('or') %></div>
|
||||||
<div>
|
<div>
|
||||||
<%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@submitter.submission.template.slug), params: { submitter: { email: @submitter.email, phone: @submitter.phone, name: @submitter.name }, resubmit: @submitter.slug }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
|
<%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@submitter.submission.template.slug), params: { submitter: { email: @submitter.email, phone: @submitter.phone, name: @submitter.name }, resubmit: @submitter.slug }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
|
||||||
@@ -44,4 +46,4 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%= render 'shared/attribution', link_path: '/start' %>
|
<%= render 'shared/attribution', link_path: '/start', account: @submitter.account %>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %>
|
<% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %>
|
||||||
<% lazyload_metadata = document.preview_images.last.metadata %>
|
<% lazyload_metadata = document.preview_images.last.metadata %>
|
||||||
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
|
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
|
||||||
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.uuid, "#{index}.jpg")) %>
|
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_uuid, "#{index}.jpg")) %>
|
||||||
<div class="relative my-4 shadow-md">
|
<div class="relative my-4 shadow-md">
|
||||||
<img loading="lazy" src="<%= page.url %>" width="<%= page.metadata['width'] %>" height="<%= page.metadata['height'] %>">
|
<img loading="lazy" src="<%= page.url %>" width="<%= page.metadata['width'] %>" height="<%= page.metadata['height'] %>">
|
||||||
<div id="page-<%= [document.uuid, index].join('-') %>" class="top-0 bottom-0 left-0 right-0 absolute">
|
<div id="page-<%= [document.uuid, index].join('-') %>" class="top-0 bottom-0 left-0 right-0 absolute">
|
||||||
@@ -24,13 +24,14 @@
|
|||||||
<% 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 %>
|
<% next if field.dig('preferences', 'formula') && field['submitter_uuid'] == @submitter.uuid %>
|
||||||
|
<%= 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,11 +1,11 @@
|
|||||||
<% if @email_config %>
|
<% if @email_config %>
|
||||||
<%= auto_link(simple_format(h(ReplaceEmailVariables.call(@email_config.value['body'], submitter: @submitter)))) %>
|
<%= auto_link(simple_format(h(ReplaceEmailVariables.call(@email_config.value['body'], submitter: @submitter, sig: @sig)))) %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<p>Hi there,</p>
|
<p>Hi there,</p>
|
||||||
<p>Please check the copy of your "<%= @submitter.submission.template.name %>" submission in the email attachments.</p>
|
<p>Please check the copy of your "<%= @submitter.submission.template.name %>" submission in the email attachments.</p>
|
||||||
<p>Alternatively, you can review and download your copy using:</p>
|
<p>Alternatively, you can review and download your copy using:</p>
|
||||||
<p>
|
<p>
|
||||||
<%= link_to @submitter.template.name, submissions_preview_url(@submitter.submission.slug) %>
|
<%= link_to @submitter.template.name, submissions_preview_url(@submitter.submission.slug, { sig: @sig }.compact) %>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Thanks,<br><%= @current_account.name %>
|
Thanks,<br><%= @current_account.name %>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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.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>
|
<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}" %>" data-turbo="false">Learn More</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -22,6 +22,8 @@ module DocuSeal
|
|||||||
|
|
||||||
config.active_storage.routes_prefix = ''
|
config.active_storage.routes_prefix = ''
|
||||||
|
|
||||||
|
config.active_storage.draw_routes = ENV['MULTITENANT'] != 'true'
|
||||||
|
|
||||||
config.i18n.available_locales = %i[en en-US en-GB es-ES fr-FR pt-PT de-DE es it de fr pl uk cs pt he nl]
|
config.i18n.available_locales = %i[en en-US en-GB es-ES fr-FR pt-PT de-DE es it de fr pl uk cs pt he nl]
|
||||||
config.i18n.fallbacks = [:en]
|
config.i18n.fallbacks = [:en]
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ Rails.application.configure do
|
|||||||
end
|
end
|
||||||
|
|
||||||
config.active_storage.resolve_model_to_route = :rails_storage_proxy if ENV['ACTIVE_STORAGE_PUBLIC'] != 'true'
|
config.active_storage.resolve_model_to_route = :rails_storage_proxy if ENV['ACTIVE_STORAGE_PUBLIC'] != 'true'
|
||||||
|
config.active_storage.service_urls_expire_in = 15.minutes
|
||||||
|
|
||||||
# Mount Action Cable outside main process or domain.
|
# Mount Action Cable outside main process or domain.
|
||||||
# config.action_cable.mount_path = nil
|
# config.action_cable.mount_path = nil
|
||||||
@@ -107,7 +108,7 @@ Rails.application.configure do
|
|||||||
logger.formatter = config.log_formatter
|
logger.formatter = config.log_formatter
|
||||||
config.logger = ActiveSupport::TaggedLogging.new(logger)
|
config.logger = ActiveSupport::TaggedLogging.new(logger)
|
||||||
|
|
||||||
encryption_secret = Digest::SHA256.hexdigest(ENV['SECRET_KEY_BASE'].to_s)
|
encryption_secret = ENV['ENCRYPTION_SECRET'].presence || Digest::SHA256.hexdigest(ENV['SECRET_KEY_BASE'].to_s)
|
||||||
|
|
||||||
config.active_record.encryption = {
|
config.active_record.encryption = {
|
||||||
primary_key: encryption_secret.first(32),
|
primary_key: encryption_secret.first(32),
|
||||||
@@ -127,9 +128,15 @@ Rails.application.configure do
|
|||||||
config.lograge.formatter = ->(data) { data.except(:path, :location).to_json }
|
config.lograge.formatter = ->(data) { data.except(:path, :location).to_json }
|
||||||
|
|
||||||
config.lograge.custom_payload do |controller|
|
config.lograge.custom_payload do |controller|
|
||||||
|
params = controller.request.params || {}
|
||||||
|
|
||||||
{
|
{
|
||||||
fwd: controller.request.ip.to_s[/\A\d+\.(.*)/, 1],
|
fwd: controller.request.ip,
|
||||||
params: controller.request.params&.slice(:id),
|
params: {
|
||||||
|
id: params[:id],
|
||||||
|
sig: (params[:signed_uuid] || params[:signed_id]).to_s.split('--').first,
|
||||||
|
slug: (params[:slug] || params[:submission_slug] || params[:template_slug]).to_s.last(5)
|
||||||
|
}.compact_blank,
|
||||||
host: controller.request.host,
|
host: controller.request.host,
|
||||||
uid: controller.instance_variable_get(:@current_user).try(:id)
|
uid: controller.instance_variable_get(:@current_user).try(:id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,37 @@ ActiveSupport.on_load(:active_storage_attachment) do
|
|||||||
|
|
||||||
has_many_attached :preview_images
|
has_many_attached :preview_images
|
||||||
|
|
||||||
def preview_image_url
|
def signed_uuid
|
||||||
preview_images.joins(:blob).find_by(blob: { filename: '0.jpg' })&.url
|
@signed_uuid ||= ApplicationRecord.signed_id_verifier.generate(uuid, expires_in: 6.hours, purpose: :attachment)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ActiveSupport.on_load(:active_storage_blob) do
|
||||||
|
attribute :uuid, :string, default: -> { SecureRandom.uuid }
|
||||||
|
|
||||||
|
def self.proxy_url(blob, expires_at: nil)
|
||||||
|
Rails.application.routes.url_helpers.blobs_proxy_url(
|
||||||
|
signed_uuid: blob.signed_uuid(expires_at:), 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_at: nil)
|
||||||
|
expires_at = expires_at.to_i if expires_at
|
||||||
|
|
||||||
|
ApplicationRecord.signed_id_verifier.generate([uuid, 'blob', expires_at].compact)
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete
|
||||||
|
service.delete(key)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ Devise.setup do |config|
|
|||||||
# This can reduce the time taken to boot the app but if your application
|
# This can reduce the time taken to boot the app but if your application
|
||||||
# requires the Devise mappings to be loaded during boot time the application
|
# requires the Devise mappings to be loaded during boot time the application
|
||||||
# won't boot properly.
|
# won't boot properly.
|
||||||
# config.reload_routes = true
|
config.reload_routes = false
|
||||||
|
|
||||||
# ==> Configuration for :database_authenticatable
|
# ==> Configuration for :database_authenticatable
|
||||||
# For bcrypt, this is the cost for hashing the password and defaults to 12. If
|
# For bcrypt, this is the cost for hashing the password and defaults to 12. If
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'rollbar' if ENV.key?('ROLLBAR_ACCESS_TOKEN')
|
|
||||||
|
|
||||||
if defined?(Rollbar)
|
|
||||||
Rollbar.configure do |config|
|
|
||||||
config.access_token = ENV.fetch('ROLLBAR_ACCESS_TOKEN', nil)
|
|
||||||
|
|
||||||
config.enabled = true
|
|
||||||
|
|
||||||
config.exception_level_filters['ActionController::RoutingError'] = 'ignore'
|
|
||||||
|
|
||||||
config.environment = ENV['ROLLBAR_ENV'].presence || Rails.env
|
|
||||||
end
|
|
||||||
end
|
|
||||||
+30
-9
@@ -65,21 +65,21 @@ fr: &fr
|
|||||||
email: Email
|
email: Email
|
||||||
digitally_signed_by: Signé numériquement par
|
digitally_signed_by: Signé numériquement par
|
||||||
role: Rôle
|
role: Rôle
|
||||||
provide_your_email_to_start: Entrez votre email pour commencer
|
provide_your_email_to_start: Entrez votre adresse email pour commencer
|
||||||
start: Démarrer
|
start: Démarrer
|
||||||
starting: Démarrage
|
starting: Démarrage
|
||||||
form_has_been_deleted_by_html: 'Le formulaire a été supprimé par <span class="font-semibold">%{name}</span>.'
|
form_has_been_deleted_by_html: 'Le formulaire a été supprimé par <span class="font-semibold">%{name}</span>.'
|
||||||
invited_by_html: 'Invité par <span class="font-semibold">%{name}</span>'
|
invited_by_html: 'Invité par <span class="font-semibold">%{name}</span>'
|
||||||
you_have_been_invited_to_submit_a_form: Vous avez été invité à soumettre un formulaire
|
you_have_been_invited_to_submit_a_form: Vous avez été invité à remplir un formulaire
|
||||||
signed_on_time: 'Signé à %{time}'
|
signed_on_time: 'Signé le %{time}'
|
||||||
form_has_been_submitted_already: Le formulaire a déjà été soumis
|
form_has_been_submitted_already: Le formulaire a déjà été complété
|
||||||
send_copy_to_email: Envoyer une copie par email
|
send_copy_to_email: Envoyer une copie par e-mail
|
||||||
sending: Envoi
|
sending: Envoi en cours
|
||||||
resubmit: Soumettre à nouveau
|
resubmit: Compléter à nouveau
|
||||||
or: ou
|
or: ou
|
||||||
download_documents: Télécharger des documents
|
download_documents: Télécharger les documents
|
||||||
downloading: Téléchargement
|
downloading: Téléchargement
|
||||||
completed_successfully: Terminé avec succès
|
completed_successfully: Complété avec succès
|
||||||
|
|
||||||
pt: &pt
|
pt: &pt
|
||||||
role: Função
|
role: Função
|
||||||
@@ -224,6 +224,27 @@ nl:
|
|||||||
downloading: Downloaden
|
downloading: Downloaden
|
||||||
completed_successfully: Succesvol voltooid
|
completed_successfully: Succesvol voltooid
|
||||||
|
|
||||||
|
ar:
|
||||||
|
email: البريد الإلكتروني
|
||||||
|
digitally_signed_by: تم التوقيع الرقمي بواسطة
|
||||||
|
role: الدور
|
||||||
|
provide_your_email_to_start: قدم بريدك الإلكتروني للبدء
|
||||||
|
start: بدء
|
||||||
|
starting: بداية
|
||||||
|
form_has_been_deleted_by_html: 'تم حذف الاستمارة بواسطة <span class="font-semibold">%{name}</span>.'
|
||||||
|
invited_by_html: 'تمت الدعوة بواسطة <span class="font-semibold">%{name}</span>'
|
||||||
|
you_have_been_invited_to_submit_a_form: تمت دعوتك لتقديم استمارة
|
||||||
|
signed_on_time: 'تم التوقيع في %{time}'
|
||||||
|
form_has_been_submitted_already: تم تقديم الاستمارة بالفعل
|
||||||
|
send_copy_to_email: إرسال نسخة إلى البريد الإلكتروني
|
||||||
|
sending: جارٍ الإرسال
|
||||||
|
resubmit: إعادة التقديم
|
||||||
|
form_has_been_deleted_by_html: 'تم حذف الاستمارة بواسطة <span class="font-semibold">%{name}</span>.'
|
||||||
|
or: أو
|
||||||
|
download_documents: تحميل الوثائق
|
||||||
|
downloading: جارٍ التحميل
|
||||||
|
completed_successfully: تم الانتهاء بنجاح
|
||||||
|
|
||||||
en-US:
|
en-US:
|
||||||
<<: *en
|
<<: *en
|
||||||
date:
|
date:
|
||||||
|
|||||||
+18
-11
@@ -17,15 +17,6 @@ Rails.application.routes.draw do
|
|||||||
end
|
end
|
||||||
|
|
||||||
devise_scope :user do
|
devise_scope :user do
|
||||||
if Docuseal.multitenant?
|
|
||||||
resource :registration, only: %i[show], path: 'sign_up'
|
|
||||||
unauthenticated do
|
|
||||||
resource :registration, only: %i[create], path: 'new' do
|
|
||||||
get '' => :new, as: :new
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
resource :invitation, only: %i[update] do
|
resource :invitation, only: %i[update] do
|
||||||
get '' => :edit
|
get '' => :edit
|
||||||
end
|
end
|
||||||
@@ -53,6 +44,8 @@ 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 :user_configs, only: %i[create]
|
||||||
|
resources :encrypted_user_configs, only: %i[destroy]
|
||||||
resources :timestamp_server, only: %i[create]
|
resources :timestamp_server, only: %i[create]
|
||||||
resources :dashboard, only: %i[index]
|
resources :dashboard, only: %i[index]
|
||||||
resources :setup, only: %i[index create]
|
resources :setup, only: %i[index create]
|
||||||
@@ -80,7 +73,19 @@ Rails.application.routes.draw do
|
|||||||
resource :code_modal, only: %i[show], controller: 'templates_code_modal'
|
resource :code_modal, only: %i[show], controller: 'templates_code_modal'
|
||||||
resources :submissions_export, only: %i[index new]
|
resources :submissions_export, only: %i[index new]
|
||||||
end
|
end
|
||||||
resources :preview_document_page, only: %i[show], path: '/preview/:attachment_uuid'
|
resources :preview_document_page, only: %i[show], path: '/preview/:signed_uuid'
|
||||||
|
resource :blobs_proxy, only: %i[show], path: '/blobs_proxy/:signed_uuid/*filename',
|
||||||
|
controller: 'api/active_storage_blobs_proxy'
|
||||||
|
|
||||||
|
if Docuseal.multitenant?
|
||||||
|
resource :blobs_proxy_legacy, only: %i[show],
|
||||||
|
path: '/blobs/proxy/:signed_id/*filename',
|
||||||
|
controller: 'api/active_storage_blobs_proxy_legacy',
|
||||||
|
as: :rails_blob
|
||||||
|
get '/disk/:encoded_key/*filename' => 'active_storage/disk#show', as: :rails_disk_service
|
||||||
|
put '/disk/:encoded_token' => 'active_storage/disk#update', as: :update_rails_disk_service
|
||||||
|
post '/direct_uploads' => 'active_storage/direct_uploads#create', as: :rails_direct_uploads
|
||||||
|
end
|
||||||
|
|
||||||
resources :start_form, only: %i[show update], path: 'd', param: 'slug' do
|
resources :start_form, only: %i[show update], path: 'd', param: 'slug' do
|
||||||
get :completed
|
get :completed
|
||||||
@@ -94,7 +99,9 @@ Rails.application.routes.draw do
|
|||||||
get :completed
|
get :completed
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :submissions_preview, only: %i[show], path: 'e', param: 'slug'
|
resources :submissions_preview, only: %i[show], path: 'e', param: 'slug' do
|
||||||
|
get :completed
|
||||||
|
end
|
||||||
|
|
||||||
resources :send_submission_email, only: %i[create] do
|
resources :send_submission_email, only: %i[create] do
|
||||||
get :success, on: :collection
|
get :success, on: :collection
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddUuidToActiveStorageBlobs < ActiveRecord::Migration[7.1]
|
||||||
|
def change
|
||||||
|
add_column :active_storage_blobs, :uuid, :string
|
||||||
|
|
||||||
|
add_index :active_storage_blobs, :uuid, unique: true
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddIndexOnSubmissionEvents < ActiveRecord::Migration[7.1]
|
||||||
|
def change
|
||||||
|
add_index :submission_events, :created_at
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddIndexOnBlobsChecksum < ActiveRecord::Migration[7.1]
|
||||||
|
def change
|
||||||
|
add_index :active_storage_blobs, :checksum
|
||||||
|
end
|
||||||
|
end
|
||||||
+5
-1
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.1].define(version: 2024_02_03_113455) do
|
ActiveRecord::Schema[7.1].define(version: 2024_02_28_083356) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
|
||||||
@@ -74,7 +74,10 @@ ActiveRecord::Schema[7.1].define(version: 2024_02_03_113455) do
|
|||||||
t.bigint "byte_size", null: false
|
t.bigint "byte_size", null: false
|
||||||
t.string "checksum"
|
t.string "checksum"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
|
t.string "uuid"
|
||||||
|
t.index ["checksum"], name: "index_active_storage_blobs_on_checksum"
|
||||||
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
|
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
|
||||||
|
t.index ["uuid"], name: "index_active_storage_blobs_on_uuid", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "active_storage_variant_records", force: :cascade do |t|
|
create_table "active_storage_variant_records", force: :cascade do |t|
|
||||||
@@ -134,6 +137,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_02_03_113455) do
|
|||||||
t.datetime "event_timestamp", null: false
|
t.datetime "event_timestamp", null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["created_at"], name: "index_submission_events_on_created_at"
|
||||||
t.index ["submission_id"], name: "index_submission_events_on_submission_id"
|
t.index ["submission_id"], name: "index_submission_events_on_submission_id"
|
||||||
t.index ["submitter_id"], name: "index_submission_events_on_submitter_id"
|
t.index ["submitter_id"], name: "index_submission_events_on_submitter_id"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Docuseal
|
module Docuseal
|
||||||
|
URL_CACHE = ActiveSupport::Cache::MemoryStore.new
|
||||||
PRODUCT_URL = 'https://www.docuseal.co'
|
PRODUCT_URL = 'https://www.docuseal.co'
|
||||||
NEWSLETTER_URL = "#{PRODUCT_URL}/newsletters".freeze
|
NEWSLETTER_URL = "#{PRODUCT_URL}/newsletters".freeze
|
||||||
ENQUIRIES_URL = "#{PRODUCT_URL}/enquiries".freeze
|
ENQUIRIES_URL = "#{PRODUCT_URL}/enquiries".freeze
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ module Params
|
|||||||
|
|
||||||
validator.call
|
validator.call
|
||||||
rescue InvalidParameterError => e
|
rescue InvalidParameterError => e
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
raise e unless validator.dry_run?
|
raise e unless validator.dry_run?
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
|
|||||||
+14
-2
@@ -51,7 +51,7 @@ class PdfProcessor < HexaPDF::Content::Processor
|
|||||||
handler.call(self, operator, operands)
|
handler.call(self, operator, operands)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.call(data, process_handler, result_handler, acc = {})
|
def self.call(data, process_handler, result_handler, acc = {}, remove_tags: true)
|
||||||
doc = HexaPDF::Document.new(io: StringIO.new(data))
|
doc = HexaPDF::Document.new(io: StringIO.new(data))
|
||||||
|
|
||||||
doc.pages.each do |page|
|
doc.pages.each do |page|
|
||||||
@@ -67,7 +67,19 @@ class PdfProcessor < HexaPDF::Content::Processor
|
|||||||
|
|
||||||
page.process_contents(processor)
|
page.process_contents(processor)
|
||||||
|
|
||||||
page.contents = process_handler_instance.contents
|
page.contents = process_handler_instance.contents if process_handler_instance.result.present? && remove_tags
|
||||||
|
end
|
||||||
|
|
||||||
|
page[:Annots].to_a.each do |annot|
|
||||||
|
next unless annot
|
||||||
|
|
||||||
|
text = annot[:Contents].to_s.squish
|
||||||
|
|
||||||
|
next unless text.starts_with?('{{') && text.ends_with?('}}')
|
||||||
|
|
||||||
|
result_handler.call({ text:, rect: annot[:Rect] }, page, acc)
|
||||||
|
|
||||||
|
page[:Annots].delete(annot)
|
||||||
end
|
end
|
||||||
|
|
||||||
process_handler_instance.result.each do |item|
|
process_handler_instance.result.each do |item|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ module ReplaceEmailVariables
|
|||||||
module_function
|
module_function
|
||||||
|
|
||||||
# rubocop:disable Metrics
|
# rubocop:disable Metrics
|
||||||
def call(text, submitter:, tracking_event_type: 'click_email')
|
def call(text, submitter:, tracking_event_type: 'click_email', sig: nil)
|
||||||
submitter_link = build_submitter_link(submitter, tracking_event_type)
|
submitter_link = build_submitter_link(submitter, tracking_event_type)
|
||||||
|
|
||||||
submission_link = build_submission_link(submitter.submission) if submitter.submission
|
submission_link = build_submission_link(submitter.submission) if submitter.submission
|
||||||
@@ -35,18 +35,18 @@ module ReplaceEmailVariables
|
|||||||
if text.include?(SUBMISSION_SUBMITTERS)
|
if text.include?(SUBMISSION_SUBMITTERS)
|
||||||
text = text.gsub(SUBMISSION_SUBMITTERS, build_submission_submitters(submitter.submission))
|
text = text.gsub(SUBMISSION_SUBMITTERS, build_submission_submitters(submitter.submission))
|
||||||
end
|
end
|
||||||
text = text.gsub(DOCUMENTS_LINKS, build_documents_links_text(submitter))
|
text = text.gsub(DOCUMENTS_LINKS, build_documents_links_text(submitter, sig))
|
||||||
text = text.gsub(DOCUMENTS_LINK, build_documents_links_text(submitter))
|
text = text.gsub(DOCUMENTS_LINK, build_documents_links_text(submitter, sig))
|
||||||
|
|
||||||
text = text.gsub(ACCOUNT_NAME, submitter.template.account.name) if submitter.template
|
text = text.gsub(ACCOUNT_NAME, submitter.account.name) if submitter.account
|
||||||
|
|
||||||
text
|
text
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics
|
# rubocop:enable Metrics
|
||||||
|
|
||||||
def build_documents_links_text(submitter)
|
def build_documents_links_text(submitter, sig = nil)
|
||||||
Rails.application.routes.url_helpers.submissions_preview_url(
|
Rails.application.routes.url_helpers.submissions_preview_url(
|
||||||
submitter.submission.slug, **Docuseal.default_url_options
|
submitter.submission.slug, { sig:, **Docuseal.default_url_options }.compact
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+25
-4
@@ -3,6 +3,8 @@
|
|||||||
module Submissions
|
module Submissions
|
||||||
DEFAULT_SUBMITTERS_ORDER = 'random'
|
DEFAULT_SUBMITTERS_ORDER = 'random'
|
||||||
|
|
||||||
|
PRELOAD_ALL_PAGES_AMOUNT = 200
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def search(submissions, keyword)
|
def search(submissions, keyword)
|
||||||
@@ -27,10 +29,29 @@ module Submissions
|
|||||||
submission.save!
|
submission.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_from_emails(template:, user:, emails:, source:, mark_as_sent: false, params: {})
|
def preload_with_pages(submission)
|
||||||
preferences = Submitters.normalize_preferences(template.account, user, params)
|
ActiveRecord::Associations::Preloader.new(
|
||||||
|
records: [submission],
|
||||||
|
associations: [:template, { template_schema_documents: :blob }]
|
||||||
|
).call
|
||||||
|
|
||||||
parse_emails(emails).uniq.map do |email|
|
total_pages =
|
||||||
|
submission.template_schema_documents.sum { |e| e.metadata.dig('pdf', 'number_of_pages').to_i }
|
||||||
|
|
||||||
|
if total_pages < PRELOAD_ALL_PAGES_AMOUNT
|
||||||
|
ActiveRecord::Associations::Preloader.new(
|
||||||
|
records: submission.template_schema_documents,
|
||||||
|
associations: [:blob, { preview_images_attachments: :blob }]
|
||||||
|
).call
|
||||||
|
end
|
||||||
|
|
||||||
|
submission
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_from_emails(template:, user:, emails:, source:, mark_as_sent: false, params: {})
|
||||||
|
preferences = Submitters.normalize_preferences(user.account, user, params)
|
||||||
|
|
||||||
|
parse_emails(emails, user).uniq.map do |email|
|
||||||
submission = template.submissions.new(created_by_user: user,
|
submission = template.submissions.new(created_by_user: user,
|
||||||
account_id: user.account_id,
|
account_id: user.account_id,
|
||||||
source:,
|
source:,
|
||||||
@@ -45,7 +66,7 @@ module Submissions
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse_emails(emails)
|
def parse_emails(emails, _user)
|
||||||
emails = emails.to_s.scan(User::EMAIL_REGEXP) unless emails.is_a?(Array)
|
emails = emails.to_s.scan(User::EMAIL_REGEXP) unless emails.is_a?(Array)
|
||||||
|
|
||||||
emails
|
emails
|
||||||
|
|||||||
Loaded 100 of 116 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user