mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 14:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3db7d47746 | ||
|
|
95899336b8 | ||
|
|
f6691c110e | ||
|
|
749722a9df | ||
|
|
265b668cb4 | ||
|
|
0527e6a5c1 | ||
|
|
d989b61d19 | ||
|
|
c1f3e6ff13 | ||
|
|
3c0fff2d82 | ||
|
|
bd599c264f | ||
|
|
0274093c89 | ||
|
|
5845b5bdfd | ||
|
|
3dd0ba6b0b | ||
|
|
e79306f8a3 | ||
|
|
0171b0c2dd | ||
|
|
a963ef53d3 | ||
|
|
344f302c76 | ||
|
|
17ded282b2 | ||
|
|
2ce57df485 | ||
|
|
849289287e | ||
|
|
704fbc2393 | ||
|
|
168fc8bd03 | ||
|
|
c5598d9efe | ||
|
|
f02315726d | ||
|
|
e56dcb59c0 | ||
|
|
c74f350ea2 | ||
|
|
af9d015cf2 | ||
|
|
633e5ac5e4 | ||
|
|
2d0f323854 | ||
|
|
cfe5610ec9 | ||
|
|
55616ac321 | ||
|
|
8f2d1deb38 | ||
|
|
e2763ceb80 | ||
|
|
10462f3140 | ||
|
|
3aef92486b | ||
|
|
c1a9ee0bbb | ||
|
|
398fae82cb | ||
|
|
a16443f961 | ||
|
|
ad4e0e1d27 | ||
|
|
493a2108e9 | ||
|
|
c2c9bb64b1 | ||
|
|
bb1426c0af | ||
|
|
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
@@ -8,8 +8,7 @@
|
|||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"vue/no-deprecated-html-element-is": 0,
|
"vue/no-deprecated-html-element-is": 0,
|
||||||
"vue/no-mutating-props": 0,
|
"vue/no-mutating-props": 0
|
||||||
"vue/no-v-html": 0
|
|
||||||
},
|
},
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 2022,
|
"ecmaVersion": 2022,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+7
-5
@@ -2,9 +2,9 @@ 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 fontforge 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://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && 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")'
|
RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")'
|
||||||
|
|
||||||
FROM ruby:3.2.2-alpine3.18 as webpack
|
FROM ruby:3.2.2-alpine3.18 as webpack
|
||||||
|
|
||||||
@@ -36,14 +36,15 @@ 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
|
||||||
|
|
||||||
RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils vips-heif libc6-compat ttf-freefont && mkdir /fonts
|
RUN apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils vips-heif libc6-compat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
||||||
|
|
||||||
COPY ./Gemfile ./Gemfile.lock ./
|
COPY ./Gemfile ./Gemfile.lock ./
|
||||||
|
|
||||||
RUN bundle update --bundler && bundle install && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf
|
RUN apk add --no-cache build-base && bundle update --bundler && bundle install && apk del build-base && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf
|
||||||
|
|
||||||
COPY ./bin ./bin
|
COPY ./bin ./bin
|
||||||
COPY ./app ./app
|
COPY ./app ./app
|
||||||
@@ -55,7 +56,8 @@ 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=fonts /fonts/FreeSans.ttf /usr/share/fonts/freefont
|
||||||
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.**
|
||||||
@@ -8,9 +8,12 @@ class AccountConfigsController < ApplicationController
|
|||||||
AccountConfig::ALLOW_TYPED_SIGNATURE,
|
AccountConfig::ALLOW_TYPED_SIGNATURE,
|
||||||
AccountConfig::FORCE_MFA,
|
AccountConfig::FORCE_MFA,
|
||||||
AccountConfig::ALLOW_TO_RESUBMIT,
|
AccountConfig::ALLOW_TO_RESUBMIT,
|
||||||
AccountConfig::ESIGNING_PREFERENCE_KEY
|
AccountConfig::ESIGNING_PREFERENCE_KEY,
|
||||||
|
AccountConfig::FORM_WITH_CONFETTI_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 +23,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
|
||||||
@@ -19,15 +19,21 @@ module Api
|
|||||||
render json: { error: e.message }, status: :unprocessable_entity
|
render json: { error: e.message }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
|
rescue_from RateLimit::LimitApproached do |e|
|
||||||
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
|
render json: { error: 'Too many requests' }, status: :too_many_requests
|
||||||
|
end
|
||||||
|
|
||||||
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
|
||||||
@@ -39,8 +45,8 @@ module Api
|
|||||||
result = relation.order(id: :desc)
|
result = relation.order(id: :desc)
|
||||||
.limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min)
|
.limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min)
|
||||||
|
|
||||||
result = result.where(relation.arel_table[:id].lt(params[:after])) if params[:after].present?
|
result = result.where(id: ...params[:after].to_i) if params[:after].present?
|
||||||
result = result.where(relation.arel_table[:id].gt(params[:before])) if params[:before].present?
|
result = result.where(id: (params[:before].to_i + 1)...) if params[:before].present?
|
||||||
|
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
@@ -59,5 +65,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
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ module Api
|
|||||||
submitters = submitters.where(external_id: params[:application_key]) if params[:application_key].present?
|
submitters = submitters.where(external_id: params[:application_key]) if params[:application_key].present?
|
||||||
submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present?
|
submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present?
|
||||||
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
|
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
|
||||||
|
submitters = maybe_filder_by_completed_at(submitters, params)
|
||||||
|
|
||||||
submitters = paginate(
|
submitters = paginate(
|
||||||
submitters.preload(:template, :submission, :submission_events,
|
submitters.preload(:template, :submission, :submission_events,
|
||||||
@@ -81,6 +82,18 @@ module Api
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def maybe_filder_by_completed_at(submitters, params)
|
||||||
|
if params[:completed_after].present?
|
||||||
|
submitters = submitters.where(completed_at: Time.zone.parse(params[:completed_after])..)
|
||||||
|
end
|
||||||
|
|
||||||
|
if params[:completed_before].present?
|
||||||
|
submitters = submitters.where(completed_at: ..Time.zone.parse(params[:completed_before]))
|
||||||
|
end
|
||||||
|
|
||||||
|
submitters
|
||||||
|
end
|
||||||
|
|
||||||
def assign_submitter_attrs(submitter, attrs)
|
def assign_submitter_attrs(submitter, attrs)
|
||||||
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
|
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
|
||||||
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '') if attrs.key?(:phone)
|
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '') if attrs.key?(:phone)
|
||||||
|
|||||||
@@ -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,22 +82,18 @@ 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,
|
||||||
{ schema: [%i[attachment_uuid name]],
|
{ schema: [%i[attachment_uuid name]],
|
||||||
submitters: [%i[name uuid]],
|
submitters: [%i[name uuid]],
|
||||||
fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value,
|
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||||
|
:required, :readonly, :default_value,
|
||||||
|
:title, :description,
|
||||||
{ preferences: {},
|
{ preferences: {},
|
||||||
|
conditions: [%i[field_uuid value action]],
|
||||||
options: [%i[value uuid]],
|
options: [%i[value uuid]],
|
||||||
|
validation: %i[message pattern],
|
||||||
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] }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,9 +22,15 @@ class ApplicationController < ActionController::Base
|
|||||||
redirect_to request.path
|
redirect_to request.path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
rescue_from RateLimit::LimitApproached do |e|
|
||||||
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
|
redirect_to request.referer, alert: 'Too many requests', status: :too_many_requests
|
||||||
|
end
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ class ConsoleRedirectController < ApplicationController
|
|||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
if request.path == '/upgrade'
|
||||||
|
params[:redir] = Docuseal.multitenant? ? "#{Docuseal::CONSOLE_URL}/plans" : "#{Docuseal::CONSOLE_URL}/on_premise"
|
||||||
|
end
|
||||||
|
|
||||||
return redirect_to(new_user_session_path({ redir: params[:redir] }.compact)) if true_user.blank?
|
return redirect_to(new_user_session_path({ redir: params[:redir] }.compact)) if true_user.blank?
|
||||||
|
|
||||||
auth = JsonWebToken.encode(uuid: true_user.uuid,
|
auth = JsonWebToken.encode(uuid: true_user.uuid,
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -22,6 +22,8 @@ class ErrorsController < ActionController::Base
|
|||||||
|
|
||||||
respond_to do |f|
|
respond_to do |f|
|
||||||
f.json do
|
f.json do
|
||||||
|
set_cors_headers
|
||||||
|
|
||||||
render json: { status: error_status_code }, status: error_status_code
|
render json: { status: error_status_code }, status: error_status_code
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -31,6 +33,14 @@ class ErrorsController < ActionController::Base
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
def error_status_code
|
def error_status_code
|
||||||
@error_status_code ||=
|
@error_status_code ||=
|
||||||
ActionDispatch::ExceptionWrapper.new(request.env,
|
ActionDispatch::ExceptionWrapper.new(request.env,
|
||||||
|
|||||||
@@ -1,26 +1,51 @@
|
|||||||
# 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 =
|
if @account_config.value != false && @account_config.value.blank?
|
||||||
current_account.account_configs.find_or_initialize_by(key: account_config_params[:key])
|
@account_config.destroy!
|
||||||
|
else
|
||||||
authorize!(:create, account_config)
|
@account_config.save!
|
||||||
|
end
|
||||||
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!
|
||||||
|
|
||||||
|
return attrs if attrs[:value].is_a?(String)
|
||||||
|
|
||||||
attrs[:value]&.transform_values! do |value|
|
attrs[:value]&.transform_values! do |value|
|
||||||
if value.in?(%w[true false])
|
if value.in?(%w[true false])
|
||||||
value == 'true'
|
value == 'true'
|
||||||
|
|||||||
@@ -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,18 @@ 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!
|
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
||||||
|
|
||||||
|
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
|
||||||
@@ -20,6 +20,7 @@ import FolderAutocomplete from './elements/folder_autocomplete'
|
|||||||
import SignatureForm from './elements/signature_form'
|
import SignatureForm from './elements/signature_form'
|
||||||
import SubmitForm from './elements/submit_form'
|
import SubmitForm from './elements/submit_form'
|
||||||
import PromptPassword from './elements/prompt_password'
|
import PromptPassword from './elements/prompt_password'
|
||||||
|
import EmailsTextarea from './elements/emails_textarea'
|
||||||
|
|
||||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||||
|
|
||||||
@@ -53,6 +54,7 @@ window.customElements.define('folder-autocomplete', FolderAutocomplete)
|
|||||||
window.customElements.define('signature-form', SignatureForm)
|
window.customElements.define('signature-form', SignatureForm)
|
||||||
window.customElements.define('submit-form', SubmitForm)
|
window.customElements.define('submit-form', SubmitForm)
|
||||||
window.customElements.define('prompt-password', PromptPassword)
|
window.customElements.define('prompt-password', PromptPassword)
|
||||||
|
window.customElements.define('emails-textarea', EmailsTextarea)
|
||||||
|
|
||||||
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
|
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
|
||||||
document.addEventListener('turbo:submit-end', async (event) => {
|
document.addEventListener('turbo:submit-end', async (event) => {
|
||||||
@@ -90,6 +92,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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
const emailRegexp = /([^@;,<>\s]+@[^@;,<>\s]+)/g
|
||||||
|
|
||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
if (this.dataset.limit) {
|
||||||
|
this.textarea.addEventListener('input', () => {
|
||||||
|
const emails = this.textarea.value.match(emailRegexp) || []
|
||||||
|
|
||||||
|
this.updateCounter(emails.length)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCounter (count) {
|
||||||
|
let counter = document.getElementById('emails_counter')
|
||||||
|
let bulkMessage = document.getElementById('bulk_message')
|
||||||
|
|
||||||
|
if (count < 2) {
|
||||||
|
counter?.remove()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((count + 10) > this.dataset.limit) {
|
||||||
|
if (!counter) {
|
||||||
|
counter = document.createElement('span')
|
||||||
|
|
||||||
|
counter.id = 'emails_counter'
|
||||||
|
counter.classList.add('text-xs', 'right-0', 'absolute')
|
||||||
|
counter.style.bottom = '-15px'
|
||||||
|
|
||||||
|
this.textarea.parentNode.append(counter)
|
||||||
|
}
|
||||||
|
|
||||||
|
counter.innerText = `${count} / ${this.dataset.limit}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.dataset.bulkEnabled !== 'true') {
|
||||||
|
if (!bulkMessage) {
|
||||||
|
bulkMessage = document.createElement('span')
|
||||||
|
|
||||||
|
bulkMessage.id = 'bulk_message'
|
||||||
|
bulkMessage.classList.add('text-xs', 'left-0', 'absolute')
|
||||||
|
bulkMessage.style.bottom = '-15px'
|
||||||
|
|
||||||
|
this.textarea.parentNode.append(bulkMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
bulkMessage.innerHTML = '<a class="link" data-turbo="false" href="/upgrade">Upgrade</a> to bulk send multiple recipients'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get textarea () {
|
||||||
|
return this.querySelector('textarea')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -27,6 +28,8 @@ export default class extends HTMLElement {
|
|||||||
|
|
||||||
if (textarea && item[field]) {
|
if (textarea && item[field]) {
|
||||||
textarea.value = textarea.value.replace(/[^;,\s]+$/, item[field] + ' ')
|
textarea.value = textarea.value.replace(/[^;,\s]+$/, item[field] + ' ')
|
||||||
|
|
||||||
|
textarea.dispatchEvent(new Event('input', { bubbles: true }))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -37,7 +40,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 +52,8 @@ export default class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
resolve([])
|
resolve([])
|
||||||
|
}).finally(() => {
|
||||||
|
this.currentFetch = null
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
resolve([])
|
resolve([])
|
||||||
|
|||||||
@@ -157,6 +157,7 @@
|
|||||||
ref="textContainer"
|
ref="textContainer"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="flex items-center px-0.5 w-full"
|
class="flex items-center px-0.5 w-full"
|
||||||
|
:class="alignClasses[field.preferences?.align]"
|
||||||
>
|
>
|
||||||
<span v-if="Array.isArray(modelValue)">
|
<span v-if="Array.isArray(modelValue)">
|
||||||
{{ modelValue.join(', ') }}
|
{{ modelValue.join(', ') }}
|
||||||
@@ -167,13 +168,14 @@
|
|||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
class="whitespace-pre-wrap"
|
class="whitespace-pre-wrap"
|
||||||
|
:class="{ 'w-full': field.preferences?.align }"
|
||||||
>{{ modelValue }}</span>
|
>{{ modelValue }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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 +242,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'),
|
||||||
@@ -252,6 +255,13 @@ export default {
|
|||||||
phone: this.t('phone')
|
phone: this.t('phone')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
alignClasses () {
|
||||||
|
return {
|
||||||
|
center: 'text-center',
|
||||||
|
left: 'text-left',
|
||||||
|
right: 'text-right'
|
||||||
|
}
|
||||||
|
},
|
||||||
option () {
|
option () {
|
||||||
return this.field.options.find((o) => o.uuid === this.area.option_uuid)
|
return this.field.options.find((o) => o.uuid === this.area.option_uuid)
|
||||||
},
|
},
|
||||||
@@ -260,6 +270,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,
|
||||||
@@ -336,17 +347,19 @@ export default {
|
|||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
modelValue () {
|
modelValue () {
|
||||||
if (this.field.type === 'text' && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > this.modelValue.length)) {
|
this.$nextTick(() => {
|
||||||
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? this.modelValue.length : 0
|
if (['date', 'text', 'number'].includes(this.field.type) && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.modelValue}`.length)) {
|
||||||
}
|
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? `${this.modelValue}`.length : 0
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
if (this.field.type === 'text' && this.$refs.textContainer) {
|
this.$nextTick(() => {
|
||||||
this.$nextTick(() => {
|
if (['date', 'text', 'number'].includes(this.field.type) && this.$refs.textContainer) {
|
||||||
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? this.modelValue.length : 0
|
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? `${this.modelValue}`.length : 0
|
||||||
})
|
}
|
||||||
}
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
optionValue (option) {
|
optionValue (option) {
|
||||||
|
|||||||
@@ -41,6 +41,13 @@
|
|||||||
:name="`values[${field.uuid}][]`"
|
:name="`values[${field.uuid}][]`"
|
||||||
>
|
>
|
||||||
</template>
|
</template>
|
||||||
|
<div
|
||||||
|
v-if="field.description && !modelValue.length"
|
||||||
|
dir="auto"
|
||||||
|
class="mb-3 px-1"
|
||||||
|
>
|
||||||
|
<MarkdownContent :string="field.description" />
|
||||||
|
</div>
|
||||||
<FileDropzone
|
<FileDropzone
|
||||||
:message="`${t('upload')} ${field.name || t('files')}${field.required ? '' : ` (${t('optional')})`}`"
|
:message="`${t('upload')} ${field.name || t('files')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
@@ -53,12 +60,14 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import FileDropzone from './dropzone'
|
import FileDropzone from './dropzone'
|
||||||
|
import MarkdownContent from './markdown_content'
|
||||||
import { IconPaperclip, IconTrashX } from '@tabler/icons-vue'
|
import { IconPaperclip, IconTrashX } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AttachmentStep',
|
name: 'AttachmentStep',
|
||||||
components: {
|
components: {
|
||||||
FileDropzone,
|
FileDropzone,
|
||||||
|
MarkdownContent,
|
||||||
IconPaperclip,
|
IconPaperclip,
|
||||||
IconTrashX
|
IconTrashX
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
<div class="space-y-3 mt-5">
|
<div class="space-y-3 mt-5">
|
||||||
<a
|
<a
|
||||||
v-if="completedButton.url"
|
v-if="completedButton.url"
|
||||||
:href="completedButton.url"
|
:href="sanitizeHref(completedButton.url)"
|
||||||
|
rel="noopener noreferrer nofollow"
|
||||||
class="white-button flex items-center w-full"
|
class="white-button flex items-center w-full"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
@@ -215,6 +216,11 @@ export default {
|
|||||||
|
|
||||||
this.isDownloading = false
|
this.isDownloading = false
|
||||||
},
|
},
|
||||||
|
sanitizeHref (href) {
|
||||||
|
if (href && href.trim().match(/^((?:https?:\/\/)|\/)/)) {
|
||||||
|
return href.replace(/javascript:/g, '')
|
||||||
|
}
|
||||||
|
},
|
||||||
downloadSafariIos (urls) {
|
downloadSafariIos (urls) {
|
||||||
const fileRequests = urls.map((url) => {
|
const fileRequests = urls.map((url) => {
|
||||||
return fetch(url).then(async (resp) => {
|
return fetch(url).then(async (resp) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
function cropCanvasAndExportToPNG (canvas) {
|
function cropCanvasAndExportToPNG (canvas, { errorOnTooSmall } = { errorOnTooSmall: false }) {
|
||||||
const ctx = canvas.getContext('2d')
|
const ctx = canvas.getContext('2d')
|
||||||
|
|
||||||
const width = canvas.width
|
const width = canvas.width
|
||||||
@@ -33,6 +33,10 @@ function cropCanvasAndExportToPNG (canvas) {
|
|||||||
croppedCanvas.height = croppedHeight
|
croppedCanvas.height = croppedHeight
|
||||||
const croppedCtx = croppedCanvas.getContext('2d')
|
const croppedCtx = croppedCanvas.getContext('2d')
|
||||||
|
|
||||||
|
if (errorOnTooSmall && (croppedWidth < 20 || croppedHeight < 20)) {
|
||||||
|
return Promise.reject(new Error('Image too small'))
|
||||||
|
}
|
||||||
|
|
||||||
croppedCtx.drawImage(canvas, leftmost, topmost, croppedWidth, croppedHeight, 0, 0, croppedWidth, croppedHeight)
|
croppedCtx.drawImage(canvas, leftmost, topmost, croppedWidth, croppedHeight, 0, 0, croppedWidth, croppedHeight)
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div dir="auto">
|
<div dir="auto">
|
||||||
<div class="flex justify-between items-center w-full mb-2">
|
<div
|
||||||
|
class="flex justify-between items-center w-full"
|
||||||
|
:class="{ 'mb-2': !field.description }"
|
||||||
|
>
|
||||||
<label
|
<label
|
||||||
:for="field.uuid"
|
:for="field.uuid"
|
||||||
class="label text-2xl"
|
class="label text-2xl"
|
||||||
>{{ field.name && showFieldNames ? field.name : t('date') }}
|
>
|
||||||
<template v-if="!field.required">({{ t('optional') }})</template>
|
<MarkdownContent
|
||||||
|
v-if="field.title"
|
||||||
|
:string="field.title"
|
||||||
|
/>
|
||||||
|
<template v-else>
|
||||||
|
{{ field.name && showFieldNames ? field.name : t('date') }}
|
||||||
|
<template v-if="!field.required">({{ t('optional') }})</template>
|
||||||
|
</template>
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline btn-sm !normal-case font-normal"
|
class="btn btn-outline btn-sm !normal-case font-normal"
|
||||||
@@ -15,6 +25,13 @@
|
|||||||
{{ t('set_today') }}
|
{{ t('set_today') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="field.description"
|
||||||
|
class="mb-3 px-1"
|
||||||
|
dir="auto"
|
||||||
|
>
|
||||||
|
<MarkdownContent :string="field.description" />
|
||||||
|
</div>
|
||||||
<AppearsOn :field="field" />
|
<AppearsOn :field="field" />
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<input
|
<input
|
||||||
@@ -33,11 +50,13 @@
|
|||||||
<script>
|
<script>
|
||||||
import { IconCalendarCheck } from '@tabler/icons-vue'
|
import { IconCalendarCheck } from '@tabler/icons-vue'
|
||||||
import AppearsOn from './appears_on'
|
import AppearsOn from './appears_on'
|
||||||
|
import MarkdownContent from './markdown_content'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DateStep',
|
name: 'DateStep',
|
||||||
components: {
|
components: {
|
||||||
IconCalendarCheck,
|
IconCalendarCheck,
|
||||||
|
MarkdownContent,
|
||||||
AppearsOn
|
AppearsOn
|
||||||
},
|
},
|
||||||
inject: ['t'],
|
inject: ['t'],
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -79,17 +101,32 @@
|
|||||||
/>
|
/>
|
||||||
<div v-else-if="currentField.type === 'select'">
|
<div v-else-if="currentField.type === 'select'">
|
||||||
<label
|
<label
|
||||||
v-if="showFieldNames && currentField.name"
|
v-if="showFieldNames && (currentField.name || currentField.title)"
|
||||||
:for="currentField.uuid"
|
:for="currentField.uuid"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="label text-2xl mb-2"
|
class="label text-2xl"
|
||||||
>{{ currentField.name }}
|
:class="{ 'mb-2': !currentField.description }"
|
||||||
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
>
|
||||||
|
<MarkdownContent
|
||||||
|
v-if="currentField.title"
|
||||||
|
:string="currentField.title"
|
||||||
|
/>
|
||||||
|
<template v-else>
|
||||||
|
{{ currentField.name }}
|
||||||
|
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
||||||
|
</template>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="py-1"
|
class="py-1"
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
v-if="currentField.description"
|
||||||
|
dir="auto"
|
||||||
|
class="mb-3 px-1"
|
||||||
|
>
|
||||||
|
<MarkdownContent :string="currentField.description" />
|
||||||
|
</div>
|
||||||
<AppearsOn :field="currentField" />
|
<AppearsOn :field="currentField" />
|
||||||
<select
|
<select
|
||||||
:id="currentField.uuid"
|
:id="currentField.uuid"
|
||||||
@@ -118,13 +155,28 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-else-if="currentField.type === 'radio'">
|
<div v-else-if="currentField.type === 'radio'">
|
||||||
<label
|
<label
|
||||||
v-if="showFieldNames && currentField.name"
|
v-if="showFieldNames && (currentField.name || currentField.title)"
|
||||||
:for="currentField.uuid"
|
:for="currentField.uuid"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="label text-2xl mb-2"
|
class="label text-2xl"
|
||||||
>{{ currentField.name }}
|
:class="{ 'mb-2': !currentField.description }"
|
||||||
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
>
|
||||||
|
<MarkdownContent
|
||||||
|
v-if="currentField.title"
|
||||||
|
:string="currentField.title"
|
||||||
|
/>
|
||||||
|
<template v-else>
|
||||||
|
{{ currentField.name }}
|
||||||
|
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
||||||
|
</template>
|
||||||
</label>
|
</label>
|
||||||
|
<div
|
||||||
|
v-if="currentField.description"
|
||||||
|
dir="auto"
|
||||||
|
class="mb-3 px-1"
|
||||||
|
>
|
||||||
|
<MarkdownContent :string="currentField.description" />
|
||||||
|
</div>
|
||||||
<div class="flex w-full max-h-44 overflow-y-auto">
|
<div class="flex w-full max-h-44 overflow-y-auto">
|
||||||
<div
|
<div
|
||||||
v-if="!showFieldNames || (currentField.options.every((e) => !e.value) && currentField.options.length > 4)"
|
v-if="!showFieldNames || (currentField.options.every((e) => !e.value) && currentField.options.length > 4)"
|
||||||
@@ -175,66 +227,77 @@
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-else-if="currentField.type === 'checkbox'"
|
v-else-if="currentField.type === 'checkbox'"
|
||||||
class="flex w-full max-h-44 overflow-y-auto"
|
|
||||||
>
|
>
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
name="cast_boolean"
|
|
||||||
value="true"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="space-y-3.5 mx-auto"
|
v-if="currentField.description"
|
||||||
|
dir="auto"
|
||||||
|
class="mb-3 px-1"
|
||||||
>
|
>
|
||||||
<template v-if="isAnonymousChecboxes || !showFieldNames">
|
<MarkdownContent :string="currentField.description" />
|
||||||
<span class="text-xl">
|
</div>
|
||||||
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? t('submit') : t('next') }}</span>.
|
<div
|
||||||
</span>
|
class="flex w-full max-h-44 overflow-y-auto"
|
||||||
<input
|
>
|
||||||
v-for="field in currentStepFields"
|
<input
|
||||||
:key="field.uuid"
|
type="hidden"
|
||||||
type="hidden"
|
name="cast_boolean"
|
||||||
:name="`values[${field.uuid}]`"
|
value="true"
|
||||||
:value="!!values[field.uuid]"
|
>
|
||||||
>
|
<div
|
||||||
</template>
|
class="space-y-3.5 mx-auto"
|
||||||
<template v-else>
|
>
|
||||||
<div
|
<template v-if="isAnonymousChecboxes || !showFieldNames">
|
||||||
v-for="(field, index) in currentStepFields"
|
<span class="text-xl">
|
||||||
:key="field.uuid"
|
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? t('submit') : t('next') }}</span>.
|
||||||
>
|
</span>
|
||||||
<label
|
<input
|
||||||
:for="field.uuid"
|
v-for="field in currentStepFields"
|
||||||
class="flex items-center space-x-3"
|
:key="field.uuid"
|
||||||
|
type="hidden"
|
||||||
|
:name="`values[${field.uuid}]`"
|
||||||
|
:value="!!values[field.uuid]"
|
||||||
>
|
>
|
||||||
<input
|
</template>
|
||||||
type="hidden"
|
<template v-else>
|
||||||
:name="`values[${field.uuid}]`"
|
<div
|
||||||
:value="!!values[field.uuid]"
|
v-for="(field, index) in currentStepFields"
|
||||||
|
:key="field.uuid"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
:for="field.uuid"
|
||||||
|
class="flex items-center space-x-3"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
:id="field.uuid"
|
type="hidden"
|
||||||
type="checkbox"
|
:name="`values[${field.uuid}]`"
|
||||||
class="base-checkbox !h-7 !w-7"
|
:value="!!values[field.uuid]"
|
||||||
:oninvalid="`this.setCustomValidity('${t('please_check_the_box_to_continue')}')`"
|
>
|
||||||
:onchange="`this.setCustomValidity(validity.valueMissing ? '${t('please_check_the_box_to_continue')}' : '');`"
|
<input
|
||||||
:required="field.required"
|
:id="field.uuid"
|
||||||
:checked="!!values[field.uuid]"
|
type="checkbox"
|
||||||
@click="[scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
|
class="base-checkbox !h-7 !w-7"
|
||||||
>
|
:oninvalid="`this.setCustomValidity('${t('please_check_the_box_to_continue')}')`"
|
||||||
<span
|
:onchange="`this.setCustomValidity(validity.valueMissing ? '${t('please_check_the_box_to_continue')}' : '');`"
|
||||||
v-if="field.title"
|
:required="field.required"
|
||||||
class="text-xl"
|
:checked="!!values[field.uuid]"
|
||||||
v-html="field.title"
|
@click="[scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
|
||||||
/>
|
>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-if="field.title"
|
||||||
class="text-xl"
|
class="text-xl"
|
||||||
>
|
>
|
||||||
{{ field.name || field.type + ' ' + (index + 1) }}
|
<MarkdownContent :string="field.title" />
|
||||||
</span>
|
</span>
|
||||||
</label>
|
<span
|
||||||
</div>
|
v-else
|
||||||
</template>
|
class="text-xl"
|
||||||
|
>
|
||||||
|
{{ field.name || field.type + ' ' + (index + 1) }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ImageStep
|
<ImageStep
|
||||||
@@ -254,7 +317,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 +448,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 +457,32 @@ 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 MarkdownContent from './markdown_content'
|
||||||
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 +490,7 @@ export default {
|
|||||||
ImageStep,
|
ImageStep,
|
||||||
SignatureStep,
|
SignatureStep,
|
||||||
AppearsOn,
|
AppearsOn,
|
||||||
|
IconWritingSign,
|
||||||
AttachmentStep,
|
AttachmentStep,
|
||||||
InitialsStep,
|
InitialsStep,
|
||||||
MultiSelectStep,
|
MultiSelectStep,
|
||||||
@@ -413,7 +498,10 @@ export default {
|
|||||||
DateStep,
|
DateStep,
|
||||||
IconArrowsDiagonal,
|
IconArrowsDiagonal,
|
||||||
TextStep,
|
TextStep,
|
||||||
|
NumberStep,
|
||||||
|
FormulaFieldAreas,
|
||||||
PhoneStep,
|
PhoneStep,
|
||||||
|
MarkdownContent,
|
||||||
PaymentStep,
|
PaymentStep,
|
||||||
IconArrowsDiagonalMinimize2,
|
IconArrowsDiagonalMinimize2,
|
||||||
FormCompleted
|
FormCompleted
|
||||||
@@ -448,6 +536,11 @@ export default {
|
|||||||
return () => {}
|
return () => {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
expand: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
withConfetti: {
|
withConfetti: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -518,6 +611,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 +640,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 +664,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 +677,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 +696,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' && !f.description) {
|
||||||
} 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 +721,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 +758,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 +789,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 +884,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 +902,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 +910,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 +920,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 +944,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)
|
||||||
@@ -837,7 +1000,11 @@ export default {
|
|||||||
this.isSubmitting = false
|
this.isSubmitting = false
|
||||||
})
|
})
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.log(error)
|
if (error?.message === 'Image too small') {
|
||||||
|
alert('Signature is too small - please redraw.')
|
||||||
|
} else {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.isSubmitting = false
|
this.isSubmitting = false
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -24,9 +24,17 @@
|
|||||||
:name="`values[${field.uuid}]`"
|
:name="`values[${field.uuid}]`"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div
|
||||||
|
v-if="!modelValue"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="field.description"
|
||||||
|
dir="auto"
|
||||||
|
class="mb-3 px-1"
|
||||||
|
>
|
||||||
|
<MarkdownContent :string="field.description" />
|
||||||
|
</div>
|
||||||
<FileDropzone
|
<FileDropzone
|
||||||
v-if="!modelValue"
|
|
||||||
:message="`${t('upload')} ${field.name || t('image')}${field.required ? '' : ` (${t('optional')})`}`"
|
:message="`${t('upload')} ${field.name || t('image')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
:accept="'image/*'"
|
:accept="'image/*'"
|
||||||
@@ -39,12 +47,14 @@
|
|||||||
<script>
|
<script>
|
||||||
import FileDropzone from './dropzone'
|
import FileDropzone from './dropzone'
|
||||||
import { IconReload } from '@tabler/icons-vue'
|
import { IconReload } from '@tabler/icons-vue'
|
||||||
|
import MarkdownContent from './markdown_content'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ImageStep',
|
name: 'ImageStep',
|
||||||
components: {
|
components: {
|
||||||
FileDropzone,
|
FileDropzone,
|
||||||
IconReload
|
IconReload,
|
||||||
|
MarkdownContent
|
||||||
},
|
},
|
||||||
inject: ['t'],
|
inject: ['t'],
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -1,14 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<div dir="auto">
|
<div dir="auto">
|
||||||
<div class="flex justify-between items-center w-full mb-2">
|
<div
|
||||||
|
class="flex justify-between items-center w-full"
|
||||||
|
:class="{ 'mb-2': !field.description }"
|
||||||
|
>
|
||||||
<label
|
<label
|
||||||
class="label text-2xl"
|
class="label text-2xl"
|
||||||
>{{ showFieldNames && field.name ? field.name : t('initials') }}</label>
|
>
|
||||||
|
<MarkdownContent
|
||||||
|
v-if="field.title"
|
||||||
|
:string="field.title"
|
||||||
|
/>
|
||||||
|
<template v-else>
|
||||||
|
{{ showFieldNames && field.name ? field.name : t('initials') }}
|
||||||
|
</template>
|
||||||
|
</label>
|
||||||
<div class="space-x-2 flex">
|
<div class="space-x-2 flex">
|
||||||
<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 +69,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')"
|
||||||
@@ -70,6 +81,13 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="field.description"
|
||||||
|
dir="auto"
|
||||||
|
class="mb-3 px-1"
|
||||||
|
>
|
||||||
|
<MarkdownContent :string="field.description" />
|
||||||
|
</div>
|
||||||
<AppearsOn :field="field" />
|
<AppearsOn :field="field" />
|
||||||
<input
|
<input
|
||||||
:value="modelValue || computedPreviousValue"
|
:value="modelValue || computedPreviousValue"
|
||||||
@@ -105,6 +123,7 @@ import { cropCanvasAndExportToPNG } from './crop_canvas'
|
|||||||
import { IconReload, IconTextSize, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
import { IconReload, IconTextSize, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||||
import SignaturePad from 'signature_pad'
|
import SignaturePad from 'signature_pad'
|
||||||
import AppearsOn from './appears_on'
|
import AppearsOn from './appears_on'
|
||||||
|
import MarkdownContent from './markdown_content'
|
||||||
|
|
||||||
const scale = 3
|
const scale = 3
|
||||||
|
|
||||||
@@ -115,6 +134,7 @@ export default {
|
|||||||
IconReload,
|
IconReload,
|
||||||
IconTextSize,
|
IconTextSize,
|
||||||
IconSignature,
|
IconSignature,
|
||||||
|
MarkdownContent,
|
||||||
IconArrowsDiagonalMinimize2
|
IconArrowsDiagonalMinimize2
|
||||||
},
|
},
|
||||||
inject: ['baseUrl', 't'],
|
inject: ['baseUrl', 't'],
|
||||||
@@ -231,7 +251,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,75 @@
|
|||||||
|
<template>
|
||||||
|
<span>
|
||||||
|
<template
|
||||||
|
v-for="(item, index) in items"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
v-if="item.startsWith('<a') && item.endsWith('</a>')"
|
||||||
|
:href="sanitizeHref(extractAttr(item, 'href'))"
|
||||||
|
rel="noopener noreferrer nofollow"
|
||||||
|
:class="extractAttr(item, 'class') || 'link'"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{{ extractText(item) }}
|
||||||
|
</a>
|
||||||
|
<b
|
||||||
|
v-else-if="item.startsWith('<b>') || item.startsWith('<strong>')"
|
||||||
|
>
|
||||||
|
{{ extractText(item) }}
|
||||||
|
</b>
|
||||||
|
<i
|
||||||
|
v-else-if="item.startsWith('<i>') || item.startsWith('<em>')"
|
||||||
|
>
|
||||||
|
{{ extractText(item) }}
|
||||||
|
</i>
|
||||||
|
<br
|
||||||
|
v-else-if="item === '<br>' || item === '\n'"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-else
|
||||||
|
>
|
||||||
|
{{ item }}
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import snarkdown from 'snarkdown'
|
||||||
|
|
||||||
|
const htmlSplitRegexp = /(<a.+?<\/a>|<i>.+?<\/i>|<b>.+?<\/b>|<em>.+?<\/em>|<strong>.+?<\/strong>|<br>)/
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'MarkdownContent',
|
||||||
|
props: {
|
||||||
|
string: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
items () {
|
||||||
|
return snarkdown(this.string.replace(/\n/g, '<br>')).split(htmlSplitRegexp)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
sanitizeHref (href) {
|
||||||
|
if (href && href.trim().match(/^((?:https?:\/\/)|\/)/)) {
|
||||||
|
return href.replace(/javascript:/g, '')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extractAttr (text, attr) {
|
||||||
|
if (text.includes(attr)) {
|
||||||
|
return text.split(attr).pop().split('"')[1]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extractText (text) {
|
||||||
|
if (text) {
|
||||||
|
return text.match(/>(.+?)</)?.[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,10 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<label
|
<label
|
||||||
v-if="showFieldNames && field.name"
|
v-if="showFieldNames && (field.name || field.title)"
|
||||||
:for="field.uuid"
|
:for="field.uuid"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="label text-2xl mb-2"
|
class="label text-2xl"
|
||||||
>{{ field.name }}</label>
|
:class="{ 'mb-2': !field.description }"
|
||||||
|
><MarkdownContent
|
||||||
|
v-if="field.title"
|
||||||
|
:string="field.title"
|
||||||
|
/>
|
||||||
|
<template v-else>{{ field.name }}</template>
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
v-if="field.description"
|
||||||
|
dir="auto"
|
||||||
|
class="mb-3 px-1"
|
||||||
|
>
|
||||||
|
<MarkdownContent :string="field.description" />
|
||||||
|
</div>
|
||||||
<div class="flex w-full max-h-44 overflow-y-auto">
|
<div class="flex w-full max-h-44 overflow-y-auto">
|
||||||
<input
|
<input
|
||||||
v-if="modelValue.length === 0"
|
v-if="modelValue.length === 0"
|
||||||
@@ -53,8 +66,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import MarkdownContent from './markdown_content'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MultiSelectStep',
|
name: 'MultiSelectStep',
|
||||||
|
components: {
|
||||||
|
MarkdownContent
|
||||||
|
},
|
||||||
inject: ['t', 'scrollIntoField'],
|
inject: ['t', 'scrollIntoField'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<template>
|
||||||
|
<label
|
||||||
|
v-if="showFieldNames && (field.name || field.title)"
|
||||||
|
:for="field.uuid"
|
||||||
|
dir="auto"
|
||||||
|
class="label text-2xl"
|
||||||
|
:class="{ 'mb-2': !field.description }"
|
||||||
|
><MarkdownContent
|
||||||
|
v-if="field.title"
|
||||||
|
:string="field.title"
|
||||||
|
/>
|
||||||
|
<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"
|
||||||
|
dir="auto"
|
||||||
|
class="mb-3 px-1"
|
||||||
|
>
|
||||||
|
<MarkdownContent :string="field.description" />
|
||||||
|
</div>
|
||||||
|
<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'
|
||||||
|
import MarkdownContent from './markdown_content'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TextStep',
|
||||||
|
components: {
|
||||||
|
AppearsOn,
|
||||||
|
MarkdownContent
|
||||||
|
},
|
||||||
|
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>
|
||||||
@@ -2,10 +2,25 @@
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
:for="isCodeSent ? 'one_time_code' : field.uuid"
|
:for="isCodeSent ? 'one_time_code' : field.uuid"
|
||||||
class="label text-2xl mb-2"
|
class="label text-2xl"
|
||||||
>{{ showFieldNames && field.name ? field.name : t('verified_phone_number') }}
|
:class="{ 'mb-2': !field.description }"
|
||||||
<template v-if="!field.required">({{ t('optional') }})</template>
|
>
|
||||||
|
<MarkdownContent
|
||||||
|
v-if="field.title"
|
||||||
|
:string="field.title"
|
||||||
|
/>
|
||||||
|
<template v-else>
|
||||||
|
{{ showFieldNames && field.name ? field.name : t('verified_phone_number') }}
|
||||||
|
<template v-if="!field.required">({{ t('optional') }})</template>
|
||||||
|
</template>
|
||||||
</label>
|
</label>
|
||||||
|
<div
|
||||||
|
v-if="field.description"
|
||||||
|
dir="auto"
|
||||||
|
class="mb-3 px-1"
|
||||||
|
>
|
||||||
|
<MarkdownContent :string="field.description" />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
@@ -53,14 +68,13 @@
|
|||||||
class="base-input !text-2xl w-full"
|
class="base-input !text-2xl w-full"
|
||||||
autocomplete="tel"
|
autocomplete="tel"
|
||||||
pattern="^\+[0-9\s\-]+$"
|
pattern="^\+[0-9\s\-]+$"
|
||||||
:oninvalid="`this.value ? this.setCustomValidity('${t('use_international_format')}...') : ''`"
|
|
||||||
oninput="this.setCustomValidity('')"
|
|
||||||
type="tel"
|
type="tel"
|
||||||
inputmode="tel"
|
inputmode="tel"
|
||||||
:required="field.required"
|
:required="field.required"
|
||||||
placeholder="+1 234 567-8900"
|
placeholder="+1 234 567-8900"
|
||||||
:name="`values[${field.uuid}]`"
|
:name="`values[${field.uuid}]`"
|
||||||
@input="$emit('update:model-value', $event.target.value)"
|
@invalid="$event.target.value ? $event.target.setCustomValidity(`${t('use_international_format')}...`) : ''"
|
||||||
|
@input="[$event.target.setCustomValidity(''), $emit('update:model-value', $event.target.value)]"
|
||||||
@focus="$emit('focus')"
|
@focus="$emit('focus')"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,6 +82,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import MarkdownContent from './markdown_content'
|
||||||
|
|
||||||
function throttle (func, delay) {
|
function throttle (func, delay) {
|
||||||
let lastCallTime = 0
|
let lastCallTime = 0
|
||||||
|
|
||||||
@@ -83,6 +99,9 @@ function throttle (func, delay) {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PhoneStep',
|
name: 'PhoneStep',
|
||||||
|
components: {
|
||||||
|
MarkdownContent
|
||||||
|
},
|
||||||
inject: ['t', 'baseUrl'],
|
inject: ['t', 'baseUrl'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<div dir="auto">
|
<div dir="auto">
|
||||||
<div class="flex justify-between items-center w-full mb-2">
|
<div
|
||||||
|
class="flex justify-between items-center w-full"
|
||||||
|
:class="{ 'mb-2': !field.description }"
|
||||||
|
>
|
||||||
<label
|
<label
|
||||||
class="label text-2xl"
|
class="label text-2xl"
|
||||||
>{{ showFieldNames && field.name ? field.name : t('signature') }}</label>
|
>
|
||||||
|
<MarkdownContent
|
||||||
|
v-if="field.title"
|
||||||
|
:string="field.title"
|
||||||
|
/>
|
||||||
|
<template v-else>
|
||||||
|
{{ showFieldNames && field.name ? field.name : t('signature') }}
|
||||||
|
</template>
|
||||||
|
</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,14 +34,14 @@
|
|||||||
</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')"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
id="type_text_button"
|
id="type_text_button"
|
||||||
href="#"
|
href="#"
|
||||||
class="btn btn-outline btn-sm font-medium"
|
class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap"
|
||||||
@click.prevent="toggleTextInput"
|
@click.prevent="toggleTextInput"
|
||||||
>
|
>
|
||||||
<IconTextSize :width="16" />
|
<IconTextSize :width="16" />
|
||||||
@@ -40,14 +51,16 @@
|
|||||||
</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')"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="btn btn-outline btn-sm font-medium"
|
class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap"
|
||||||
>
|
>
|
||||||
<IconCamera :width="16" />
|
<IconCamera :width="16" />
|
||||||
<input
|
<input
|
||||||
|
:key="uploadImageInputKey"
|
||||||
type="file"
|
type="file"
|
||||||
hidden
|
hidden
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
@@ -78,7 +91,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')"
|
||||||
>
|
>
|
||||||
@@ -89,6 +102,13 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="field.description"
|
||||||
|
dir="auto"
|
||||||
|
class="mb-3 px-1"
|
||||||
|
>
|
||||||
|
<MarkdownContent :string="field.description" />
|
||||||
|
</div>
|
||||||
<AppearsOn :field="field" />
|
<AppearsOn :field="field" />
|
||||||
<input
|
<input
|
||||||
:value="modelValue || computedPreviousValue"
|
:value="modelValue || computedPreviousValue"
|
||||||
@@ -111,7 +131,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"
|
||||||
@@ -124,6 +144,7 @@ import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonal
|
|||||||
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
||||||
import SignaturePad from 'signature_pad'
|
import SignaturePad from 'signature_pad'
|
||||||
import AppearsOn from './appears_on'
|
import AppearsOn from './appears_on'
|
||||||
|
import MarkdownContent from './markdown_content'
|
||||||
|
|
||||||
let isFontLoaded = false
|
let isFontLoaded = false
|
||||||
|
|
||||||
@@ -135,6 +156,7 @@ export default {
|
|||||||
AppearsOn,
|
AppearsOn,
|
||||||
IconReload,
|
IconReload,
|
||||||
IconCamera,
|
IconCamera,
|
||||||
|
MarkdownContent,
|
||||||
IconTextSize,
|
IconTextSize,
|
||||||
IconSignature,
|
IconSignature,
|
||||||
IconArrowsDiagonalMinimize2
|
IconArrowsDiagonalMinimize2
|
||||||
@@ -185,7 +207,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 +292,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 +363,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 () {
|
||||||
@@ -361,8 +386,8 @@ export default {
|
|||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve, reject) => {
|
||||||
cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => {
|
cropCanvasAndExportToPNG(this.$refs.canvas, { errorOnTooSmall: true }).then(async (blob) => {
|
||||||
const file = new File([blob], 'signature.png', { type: 'image/png' })
|
const file = new File([blob], 'signature.png', { type: 'image/png' })
|
||||||
|
|
||||||
if (this.isDirectUpload) {
|
if (this.isDirectUpload) {
|
||||||
@@ -404,6 +429,8 @@ export default {
|
|||||||
return resolve(attachment)
|
return resolve(attachment)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
return reject(error)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<label
|
<label
|
||||||
v-if="showFieldNames && field.name"
|
v-if="showFieldNames && (field.name || field.title)"
|
||||||
:for="field.uuid"
|
:for="field.uuid"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="label text-2xl"
|
class="label text-2xl"
|
||||||
:class="{ 'mb-2': !field.description }"
|
:class="{ 'mb-2': !field.description }"
|
||||||
><template v-if="field.title"><span v-html="field.title" /></template>
|
><MarkdownContent
|
||||||
|
v-if="field.title"
|
||||||
|
:string="field.title"
|
||||||
|
/>
|
||||||
<template v-else>{{ field.name }}</template>
|
<template v-else>{{ field.name }}</template>
|
||||||
<template v-if="!field.required">({{ t('optional') }})</template>
|
<template v-if="!field.required">({{ t('optional') }})</template>
|
||||||
</label>
|
</label>
|
||||||
@@ -15,9 +18,11 @@
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="field.description"
|
v-if="field.description"
|
||||||
class="mb-3 px-1 text-lg"
|
dir="auto"
|
||||||
v-html="field.description"
|
class="mb-3 px-1"
|
||||||
/>
|
>
|
||||||
|
<MarkdownContent :string="field.description" />
|
||||||
|
</div>
|
||||||
<AppearsOn :field="field" />
|
<AppearsOn :field="field" />
|
||||||
<div class="items-center flex">
|
<div class="items-center flex">
|
||||||
<input
|
<input
|
||||||
@@ -30,11 +35,11 @@
|
|||||||
:class="{ '!pr-11 -mr-10': !field.validation?.pattern }"
|
:class="{ '!pr-11 -mr-10': !field.validation?.pattern }"
|
||||||
:required="field.required"
|
:required="field.required"
|
||||||
:pattern="field.validation?.pattern"
|
:pattern="field.validation?.pattern"
|
||||||
:oninvalid="field.validation?.message ? `this.setCustomValidity(${JSON.stringify(field.validation.message)})` : ''"
|
|
||||||
:oninput="field.validation?.message ? `this.setCustomValidity('')` : ''"
|
|
||||||
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
|
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||||
type="text"
|
type="text"
|
||||||
:name="`values[${field.uuid}]`"
|
:name="`values[${field.uuid}]`"
|
||||||
|
@invalid="field.validation?.message ? $event.target.setCustomValidity(field.validation.message) : ''"
|
||||||
|
@input="field.validation?.message ? $event.target.setCustomValidity('') : ''"
|
||||||
@focus="$emit('focus')"
|
@focus="$emit('focus')"
|
||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
@@ -69,11 +74,13 @@
|
|||||||
<script>
|
<script>
|
||||||
import { IconAlignBoxLeftTop } from '@tabler/icons-vue'
|
import { IconAlignBoxLeftTop } from '@tabler/icons-vue'
|
||||||
import AppearsOn from './appears_on'
|
import AppearsOn from './appears_on'
|
||||||
|
import MarkdownContent from './markdown_content'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TextStep',
|
name: 'TextStep',
|
||||||
components: {
|
components: {
|
||||||
IconAlignBoxLeftTop,
|
IconAlignBoxLeftTop,
|
||||||
|
MarkdownContent,
|
||||||
AppearsOn
|
AppearsOn
|
||||||
},
|
},
|
||||||
inject: ['t'],
|
inject: ['t'],
|
||||||
|
|||||||
@@ -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" />
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
<div
|
<div
|
||||||
class="flex items-center h-full w-full"
|
class="flex items-center h-full w-full"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
:class="[bgColors[submitterIndex], field?.default_value ? '' : 'justify-center']"
|
:class="[bgColors[submitterIndex], field?.default_value ? (alignClasses[field.preferences?.align] || '') : 'justify-center']"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-if="field"
|
v-if="field"
|
||||||
@@ -206,9 +206,18 @@ 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)
|
||||||
|
},
|
||||||
|
alignClasses () {
|
||||||
|
return {
|
||||||
|
center: 'justify-center',
|
||||||
|
left: 'justify-start',
|
||||||
|
right: 'justify-end'
|
||||||
|
}
|
||||||
|
},
|
||||||
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}.`
|
||||||
@@ -299,19 +308,22 @@ export default {
|
|||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'field.default_value' () {
|
'field.default_value' () {
|
||||||
if (this.field.type === 'text' && this.field.default_value && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > this.field.default_value.length)) {
|
|
||||||
this.textOverflowChars = this.$el.clientHeight < this.$refs.textContainer.clientHeight ? this.field.default_value.length : 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted () {
|
|
||||||
if (this.field.type === 'text' && this.field.default_value && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > this.field.default_value)) {
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.textOverflowChars = this.$el.clientHeight < this.$refs.textContainer.clientHeight ? this.field.default_value.length : 0
|
if (['date', 'text', 'number'].includes(this.field.type) && this.field.default_value && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.field.default_value}`.length)) {
|
||||||
|
this.textOverflowChars = this.$el.clientHeight < this.$refs.textContainer.clientHeight ? `${this.field.default_value}`.length : 0
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mounted () {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (['date', 'text', 'number'].includes(this.field.type) && this.field.default_value && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.field.default_value}`.length)) {
|
||||||
|
this.textOverflowChars = this.$el.clientHeight < this.$refs.textContainer.clientHeight ? `${this.field.default_value}`.length : 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
buildDefaultName: Field.methods.buildDefaultName,
|
||||||
onNameFocus (e) {
|
onNameFocus (e) {
|
||||||
this.selectedAreaRef.value = this.area
|
this.selectedAreaRef.value = this.area
|
||||||
|
|
||||||
@@ -340,7 +352,7 @@ export default {
|
|||||||
},
|
},
|
||||||
onResizeCell (e) {
|
onResizeCell (e) {
|
||||||
if (e.target.id === 'mask') {
|
if (e.target.id === 'mask') {
|
||||||
const positionX = e.layerX / (e.target.clientWidth - 1)
|
const positionX = e.offsetX / (e.target.clientWidth - 1)
|
||||||
|
|
||||||
if (positionX > this.area.x) {
|
if (positionX > this.area.x) {
|
||||||
this.area.cell_w = positionX - this.area.x
|
this.area.cell_w = positionX - this.area.x
|
||||||
@@ -386,16 +398,16 @@ export default {
|
|||||||
},
|
},
|
||||||
resize (e) {
|
resize (e) {
|
||||||
if (e.target.id === 'mask') {
|
if (e.target.id === 'mask') {
|
||||||
this.area.w = e.layerX / e.target.clientWidth - this.area.x
|
this.area.w = e.offsetX / e.target.clientWidth - this.area.x
|
||||||
this.area.h = e.layerY / e.target.clientHeight - this.area.y
|
this.area.h = e.offsetY / e.target.clientHeight - this.area.y
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
drag (e) {
|
drag (e) {
|
||||||
if (e.target.id === 'mask') {
|
if (e.target.id === 'mask') {
|
||||||
this.isDragged = true
|
this.isDragged = true
|
||||||
|
|
||||||
this.area.x = (e.layerX - this.dragFrom.x) / e.target.clientWidth
|
this.area.x = (e.offsetX - this.dragFrom.x) / e.target.clientWidth
|
||||||
this.area.y = (e.layerY - this.dragFrom.y) / e.target.clientHeight
|
this.area.y = (e.offsetY - this.dragFrom.y) / e.target.clientHeight
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
startDrag (e) {
|
startDrag (e) {
|
||||||
|
|||||||
@@ -94,11 +94,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
id="main_container"
|
||||||
class="flex"
|
class="flex"
|
||||||
:class="$slots.buttons || withTitle ? 'md:max-h-[calc(100%_-_60px)]' : 'md:max-h-[100%]'"
|
:class="$slots.buttons || withTitle ? 'md:max-h-[calc(100%_-_60px)]' : 'md:max-h-[100%]'"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="withDocumentsList"
|
v-if="withDocumentsList"
|
||||||
|
id="documents_container"
|
||||||
ref="previews"
|
ref="previews"
|
||||||
:style="{ 'display': isBreakpointLg ? 'none' : 'initial' }"
|
:style="{ 'display': isBreakpointLg ? 'none' : 'initial' }"
|
||||||
class="overflow-y-auto overflow-x-hidden w-52 flex-none pr-3 mt-0.5 pt-0.5 hidden lg:block"
|
class="overflow-y-auto overflow-x-hidden w-52 flex-none pr-3 mt-0.5 pt-0.5 hidden lg:block"
|
||||||
@@ -134,7 +136,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full overflow-y-hidden md:overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5">
|
<div
|
||||||
|
id="pages_container"
|
||||||
|
class="w-full overflow-y-hidden md:overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
ref="documents"
|
ref="documents"
|
||||||
class="pr-3.5 pl-0.5"
|
class="pr-3.5 pl-0.5"
|
||||||
@@ -200,6 +205,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="withFieldsList"
|
v-if="withFieldsList"
|
||||||
|
id="fields_list_container"
|
||||||
class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block"
|
class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block"
|
||||||
:class="drawField ? 'overflow-hidden' : 'overflow-y-auto overflow-x-hidden'"
|
:class="drawField ? 'overflow-hidden' : 'overflow-y-auto overflow-x-hidden'"
|
||||||
>
|
>
|
||||||
@@ -273,6 +279,7 @@
|
|||||||
@select="startFieldDraw($event)"
|
@select="startFieldDraw($event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="docuseal_modal_container" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -321,7 +328,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 +375,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 +459,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 +493,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
selectedAreaRef: () => ref(),
|
selectedAreaRef: () => ref(),
|
||||||
|
fieldsDragFieldRef: () => ref(),
|
||||||
fieldAreasIndex () {
|
fieldAreasIndex () {
|
||||||
const areas = {}
|
const areas = {}
|
||||||
|
|
||||||
@@ -502,7 +529,7 @@ export default {
|
|||||||
submitter.name = name
|
submitter.name = name
|
||||||
|
|
||||||
if (existingSubmittersUuids.filter(Boolean).length) {
|
if (existingSubmittersUuids.filter(Boolean).length) {
|
||||||
submitter.uuid = existingSubmittersUuids[index] || v4()
|
submitter.uuid = existingSubmittersUuids[index] || submitter.uuid || v4()
|
||||||
} else {
|
} else {
|
||||||
submitter.uuid ||= v4()
|
submitter.uuid ||= v4()
|
||||||
}
|
}
|
||||||
@@ -755,7 +782,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 +825,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 +833,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 +901,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,
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
<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>
|
||||||
|
{{ field.name || buildDefaultName(field, template.fields) }}
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="text-xl"
|
||||||
|
@click.prevent="$emit('close')"
|
||||||
|
>×</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<form @submit.prevent="saveAndClose">
|
||||||
|
<div class="space-y-1 mb-1">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
dir="auto"
|
||||||
|
class="label text-sm"
|
||||||
|
for="description_field"
|
||||||
|
>
|
||||||
|
{{ t('description') }}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="description_field"
|
||||||
|
ref="textarea"
|
||||||
|
v-model="description"
|
||||||
|
dir="auto"
|
||||||
|
class="base-textarea !text-base w-full"
|
||||||
|
@input="resizeTextarea"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
dir="auto"
|
||||||
|
class="label text-sm"
|
||||||
|
for="title_field"
|
||||||
|
>
|
||||||
|
{{ t('display_title') }} ({{ t('optional') }})
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="title_field"
|
||||||
|
v-model="title"
|
||||||
|
dir="auto"
|
||||||
|
class="base-input !text-base w-full"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="base-button w-full mt-4"
|
||||||
|
>
|
||||||
|
{{ t('save') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'DescriptionModal',
|
||||||
|
inject: ['t', 'save', 'template'],
|
||||||
|
props: {
|
||||||
|
field: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
buildDefaultName: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['close'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
description: this.field.description,
|
||||||
|
title: this.field.title
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.resizeTextarea()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
saveAndClose () {
|
||||||
|
this.field.description = this.description
|
||||||
|
this.field.title = this.title
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
this.$emit('close')
|
||||||
|
},
|
||||||
|
resizeTextarea () {
|
||||||
|
const textarea = this.$refs.textarea
|
||||||
|
|
||||||
|
textarea.style.height = 'auto'
|
||||||
|
textarea.style.height = textarea.scrollHeight + 'px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -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`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="group pb-2"
|
class="list-field group mb-2"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="border border-base-300 rounded rounded-tr-none relative group"
|
class="border border-base-300 rounded rounded-tr-none relative group"
|
||||||
@@ -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
|
||||||
@@ -94,15 +116,41 @@
|
|||||||
@click="closeDropdown"
|
@click="closeDropdown"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="field.type === 'text' && !defaultField"
|
v-if="['number'].includes(field.type)"
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
class="select select-bordered select-xs w-full max-w-xs h-7 !outline-0 font-normal"
|
||||||
|
@change="[field.preferences ||= {}, field.preferences.align = $event.target.value, save()]"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="value in ['left', 'right', 'center']"
|
||||||
|
:key="value"
|
||||||
|
:selected="field.preferences?.align ? value === field.preferences.align : value === 'left'"
|
||||||
|
:value="value"
|
||||||
|
>
|
||||||
|
{{ t(value) }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<label
|
||||||
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>
|
||||||
|
{{ t('align') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="['text', 'number'].includes(field.type) && !defaultField"
|
||||||
class="py-1.5 px-1 relative"
|
class="py-1.5 px-1 relative"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
v-model="field.default_value"
|
v-model="field.default_value"
|
||||||
type="text"
|
|
||||||
:placeholder="t('default_value')"
|
:placeholder="t('default_value')"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
|
:type="field.type"
|
||||||
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
|
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
|
||||||
@blur="save"
|
@blur="save"
|
||||||
>
|
>
|
||||||
@@ -142,8 +190,45 @@
|
|||||||
{{ 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' && field.type != 'stamp'"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<label class="cursor-pointer py-1.5">
|
<label class="cursor-pointer py-1.5">
|
||||||
@@ -156,6 +241,20 @@
|
|||||||
<span class="label-text">{{ t('required') }}</span>
|
<span class="label-text">{{ t('required') }}</span>
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
|
<li
|
||||||
|
v-if="field.type == 'stamp'"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<label class="cursor-pointer py-1.5">
|
||||||
|
<input
|
||||||
|
:checked="field.preferences?.with_logo != false"
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-xs"
|
||||||
|
@change="[field.preferences ||= {}, field.preferences.with_logo = field.preferences.with_logo == false, save()]"
|
||||||
|
>
|
||||||
|
<span class="label-text">{{ t('with_logo') }}</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="field.type == 'checkbox'"
|
v-if="field.type == 'checkbox'"
|
||||||
@click.stop
|
@click.stop
|
||||||
@@ -185,7 +284,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="field.type === 'text' && !defaultField"
|
v-if="['text', 'number'].includes(field.type) && !defaultField"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<label class="cursor-pointer py-1.5">
|
<label class="cursor-pointer py-1.5">
|
||||||
@@ -198,9 +297,56 @@
|
|||||||
<span class="label-text">{{ t('read_only') }}</span>
|
<span class="label-text">{{ t('read_only') }}</span>
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
|
<hr
|
||||||
|
v-if="field.type != 'stamp'"
|
||||||
|
class="pb-0.5 mt-0.5"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-if="field.type != 'stamp'"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||||
|
@click="isShowDescriptionModal = !isShowDescriptionModal"
|
||||||
|
>
|
||||||
|
<IconInfoCircle
|
||||||
|
width="18"
|
||||||
|
/>
|
||||||
|
<span class="text-sm">
|
||||||
|
{{ t('description') }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
v-if="field.type != 'stamp'"
|
||||||
|
>
|
||||||
|
<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">
|
<hr class="pb-0.5 mt-0.5">
|
||||||
<li
|
<li
|
||||||
v-for="(area, index) in field.areas || []"
|
v-for="(area, index) in sortedAreas"
|
||||||
:key="index"
|
:key="index"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@@ -212,7 +358,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 +392,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 +418,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 +431,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 +447,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 +462,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 +475,36 @@
|
|||||||
</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>
|
||||||
|
<Teleport
|
||||||
|
v-if="isShowDescriptionModal"
|
||||||
|
:to="modalContainerEl"
|
||||||
|
>
|
||||||
|
<DescriptionModal
|
||||||
|
:field="field"
|
||||||
|
:build-default-name="buildDefaultName"
|
||||||
|
@close="isShowDescriptionModal = false"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -329,7 +512,10 @@
|
|||||||
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 DescriptionModal from './description_modal'
|
||||||
|
import { IconInfoCircle, 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 +526,13 @@ export default {
|
|||||||
IconShape,
|
IconShape,
|
||||||
PaymentSettings,
|
PaymentSettings,
|
||||||
IconNewSection,
|
IconNewSection,
|
||||||
|
IconInfoCircle,
|
||||||
|
FormulaModal,
|
||||||
|
DescriptionModal,
|
||||||
|
ConditionsModal,
|
||||||
|
IconRouteAltLeft,
|
||||||
IconTrashX,
|
IconTrashX,
|
||||||
|
IconMathFunction,
|
||||||
IconCopy,
|
IconCopy,
|
||||||
FieldType
|
FieldType
|
||||||
},
|
},
|
||||||
@@ -366,11 +558,29 @@ export default {
|
|||||||
return {
|
return {
|
||||||
isNameFocus: false,
|
isNameFocus: false,
|
||||||
showPaymentModal: false,
|
showPaymentModal: false,
|
||||||
|
isShowFormulaModal: false,
|
||||||
|
isShowConditionsModal: false,
|
||||||
|
isShowDescriptionModal: false,
|
||||||
renderDropdown: false
|
renderDropdown: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
fieldNames: FieldType.computed.fieldNames,
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
|
schemaAttachmentsIndexes () {
|
||||||
|
return (this.template.schema || []).reduce((acc, item, index) => {
|
||||||
|
acc[item.attachment_uuid] = index
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
sortedAreas () {
|
||||||
|
return (this.field.areas || []).sort((a, b) => {
|
||||||
|
return this.schemaAttachmentsIndexes[a.attachment_uuid] - this.schemaAttachmentsIndexes[b.attachment_uuid]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
modalContainerEl () {
|
||||||
|
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
||||||
|
},
|
||||||
dateFormats () {
|
dateFormats () {
|
||||||
return [
|
return [
|
||||||
'MM/DD/YYYY',
|
'MM/DD/YYYY',
|
||||||
@@ -385,22 +595,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 +610,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 +661,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(() => {
|
||||||
@@ -478,7 +693,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
scrollToFirstArea () {
|
scrollToFirstArea () {
|
||||||
return this.field.areas?.[0] && this.$emit('scroll-to', this.field.areas[0])
|
return this.sortedAreas[0] && this.$emit('scroll-to', this.sortedAreas[0])
|
||||||
},
|
},
|
||||||
closeDropdown () {
|
closeDropdown () {
|
||||||
document.activeElement.blur()
|
document.activeElement.blur()
|
||||||
@@ -496,7 +711,12 @@ export default {
|
|||||||
},
|
},
|
||||||
removeOption (option) {
|
removeOption (option) {
|
||||||
this.field.options.splice(this.field.options.indexOf(option), 1)
|
this.field.options.splice(this.field.options.indexOf(option), 1)
|
||||||
this.field.areas.splice(this.field.areas.findIndex((a) => a.option_uuid === option.uuid), 1)
|
|
||||||
|
const optionIndex = this.field.areas.findIndex((a) => a.option_uuid === option.uuid)
|
||||||
|
|
||||||
|
if (optionIndex !== -1) {
|
||||||
|
this.field.areas.splice(this.field.areas.findIndex((a) => a.option_uuid === option.uuid), 1)
|
||||||
|
}
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div :class="withStickySubmitters ? 'sticky top-0 z-10' : ''">
|
<div :class="withStickySubmitters ? 'sticky top-0 z-10' : ''">
|
||||||
<FieldSubmitter
|
<FieldSubmitter
|
||||||
:model-value="selectedSubmitter.uuid"
|
:model-value="selectedSubmitter.uuid"
|
||||||
class="w-full rounded-lg"
|
class="roles-dropdown w-full rounded-lg"
|
||||||
:class="{ 'bg-base-100': withStickySubmitters }"
|
:class="{ 'bg-base-100': withStickySubmitters }"
|
||||||
:submitters="submitters"
|
:submitters="submitters"
|
||||||
:editable="editable && !defaultSubmitters.length"
|
:editable="editable && !defaultSubmitters.length"
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
ref="fields"
|
ref="fields"
|
||||||
class="mb-1 mt-2"
|
class="fields mb-1 mt-2"
|
||||||
@dragover.prevent="onFieldDragover"
|
@dragover.prevent="onFieldDragover"
|
||||||
@drop="reorderFields"
|
@drop="reorderFields"
|
||||||
>
|
>
|
||||||
@@ -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)"
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
<div
|
<div
|
||||||
:style="{ backgroundColor: backgroundColor }"
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
class="border border-base-300 rounded rounded-tr-none relative group mb-2"
|
class="default-field border border-base-300 rounded rounded-tr-none relative group mb-2"
|
||||||
@dragstart="onDragstart({ type: 'text', ...field })"
|
@dragstart="onDragstart({ type: 'text', ...field })"
|
||||||
@dragend="$emit('drag-end')"
|
@dragend="$emit('drag-end')"
|
||||||
>
|
>
|
||||||
@@ -68,13 +68,13 @@
|
|||||||
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
|
||||||
v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')"
|
v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
class="group flex items-center justify-center border border-dashed border-base-300 hover:border-base-content/20 w-full rounded relative"
|
class="field-type-button group flex items-center justify-center border border-dashed border-base-300 hover:border-base-content/20 w-full rounded relative"
|
||||||
:style="{ backgroundColor: backgroundColor }"
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
@dragstart="onDragstart({ type: type })"
|
@dragstart="onDragstart({ type: type })"
|
||||||
@dragend="$emit('drag-end')"
|
@dragend="$emit('drag-end')"
|
||||||
@@ -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,37 @@
|
|||||||
const en = {
|
const en = {
|
||||||
|
align: 'Align',
|
||||||
|
left: 'Left',
|
||||||
|
right: 'Right',
|
||||||
|
center: 'Center',
|
||||||
|
description: 'Description',
|
||||||
|
display_title: 'Display title',
|
||||||
|
with_logo: 'With logo',
|
||||||
|
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 +48,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 +69,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'
|
||||||
@@ -160,8 +162,8 @@ export default {
|
|||||||
},
|
},
|
||||||
onDrop (e) {
|
onDrop (e) {
|
||||||
this.$emit('drop-field', {
|
this.$emit('drop-field', {
|
||||||
x: e.layerX,
|
x: e.offsetX,
|
||||||
y: e.layerY,
|
y: e.offsetY,
|
||||||
maskW: this.$refs.mask.clientWidth,
|
maskW: this.$refs.mask.clientWidth,
|
||||||
maskH: this.$refs.mask.clientHeight,
|
maskH: this.$refs.mask.clientHeight,
|
||||||
page: this.number
|
page: this.number
|
||||||
@@ -184,10 +186,10 @@ export default {
|
|||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.newArea = {
|
this.newArea = {
|
||||||
initialX: e.layerX / this.$refs.mask.clientWidth,
|
initialX: e.offsetX / this.$refs.mask.clientWidth,
|
||||||
initialY: e.layerY / this.$refs.mask.clientHeight,
|
initialY: e.offsetY / this.$refs.mask.clientHeight,
|
||||||
x: e.layerX / this.$refs.mask.clientWidth,
|
x: e.offsetX / this.$refs.mask.clientWidth,
|
||||||
y: e.layerY / this.$refs.mask.clientHeight,
|
y: e.offsetY / this.$refs.mask.clientHeight,
|
||||||
w: 0,
|
w: 0,
|
||||||
h: 0
|
h: 0
|
||||||
}
|
}
|
||||||
@@ -195,19 +197,19 @@ export default {
|
|||||||
},
|
},
|
||||||
onPointermove (e) {
|
onPointermove (e) {
|
||||||
if (this.newArea) {
|
if (this.newArea) {
|
||||||
const dx = e.layerX / this.$refs.mask.clientWidth - this.newArea.initialX
|
const dx = e.offsetX / this.$refs.mask.clientWidth - this.newArea.initialX
|
||||||
const dy = e.layerY / this.$refs.mask.clientHeight - this.newArea.initialY
|
const dy = e.offsetY / this.$refs.mask.clientHeight - this.newArea.initialY
|
||||||
|
|
||||||
if (dx > 0) {
|
if (dx > 0) {
|
||||||
this.newArea.x = this.newArea.initialX
|
this.newArea.x = this.newArea.initialX
|
||||||
} else {
|
} else {
|
||||||
this.newArea.x = e.layerX / this.$refs.mask.clientWidth
|
this.newArea.x = e.offsetX / this.$refs.mask.clientWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dy > 0) {
|
if (dy > 0) {
|
||||||
this.newArea.y = this.newArea.initialY
|
this.newArea.y = this.newArea.initialY
|
||||||
} else {
|
} else {
|
||||||
this.newArea.y = e.layerY / this.$refs.mask.clientHeight
|
this.newArea.y = e.offsetY / this.$refs.mask.clientHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.drawField?.type === 'cells') {
|
if (this.drawField?.type === 'cells') {
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -3,10 +3,9 @@
|
|||||||
<head>
|
<head>
|
||||||
<title></title>
|
<title></title>
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||||
<style>
|
<style></style>
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body dir="<%= yield&.match?(TextUtils::RTL_REGEXP) ? 'rtl' : 'auto' %>">
|
<body dir="<%= TextUtils.rtl?(yield) ? 'rtl' : 'auto' %>">
|
||||||
<%= yield %>
|
<%= yield %>
|
||||||
<%= render partial: 'shared/mailer_attribution' %>
|
<%= render partial: 'shared/mailer_attribution' %>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<%= render 'form_toggle_options' %>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<% account_config = AccountConfig.where(account: current_account, key: AccountConfig::FORM_WITH_CONFETTI_KEY).first_or_initialize(value: true) %>
|
||||||
|
<% if can?(:manage, account_config) %>
|
||||||
|
<div class="px-1 mt-2">
|
||||||
|
<%= 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>
|
||||||
|
Show confetti on successful completion
|
||||||
|
</span>
|
||||||
|
<%= f.check_box :value, { class: 'toggle', checked: account_config.value != false, onchange: 'this.form.requestSubmit()' }, '1', '0' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
@@ -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>
|
||||||
@@ -9,8 +9,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="text-4xl font-bold mb-4 mt-8">Company Logo</p>
|
<p class="text-4xl font-bold mb-4 mt-8">Company Logo</p>
|
||||||
<%= render 'logo_form' %>
|
<%= render 'logo_form' %>
|
||||||
<p class="text-4xl font-bold mb-4 mt-8">Submitter Form</p>
|
<p class="text-4xl font-bold mb-4 mt-8">Submission Form</p>
|
||||||
<%= render 'form_completed_button_form' %>
|
<%= render 'form_completed_button_form' %>
|
||||||
|
<%= render 'form_customization_settings' %>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-0 md:w-52"></div>
|
<div class="w-0 md:w-52"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -31,40 +31,42 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<a href="<%= edit_user_signature_path %>" data-turbo-frame="modal" class="base-button w-full">Update Signature</a>
|
<a href="<%= edit_user_signature_path %>" data-turbo-frame="modal" class="base-button w-full">Update Signature</a>
|
||||||
<p class="text-2xl font-bold mt-8 mb-4">Change Password</p>
|
<% if true_user == current_user && !current_account.testing? %>
|
||||||
<%= form_for current_user, url: update_password_settings_profile_index_path, method: :patch, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
|
<p class="text-2xl font-bold mt-8 mb-4">Change Password</p>
|
||||||
<div class="form-control">
|
<%= form_for current_user, url: update_password_settings_profile_index_path, method: :patch, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
|
||||||
<%= f.label :password, 'New password', class: 'label' %>
|
<div class="form-control">
|
||||||
<%= f.password_field :password, autocomplete: 'off', class: 'base-input' %>
|
<%= f.label :password, 'New password', class: 'label' %>
|
||||||
</div>
|
<%= f.password_field :password, autocomplete: 'off', class: 'base-input' %>
|
||||||
<div class="form-control">
|
</div>
|
||||||
<%= f.label :password_confirmation, 'Confirm new password', class: 'label' %>
|
<div class="form-control">
|
||||||
<%= f.password_field :password_confirmation, autocomplete: 'off', class: 'base-input' %>
|
<%= f.label :password_confirmation, 'Confirm new password', class: 'label' %>
|
||||||
</div>
|
<%= f.password_field :password_confirmation, autocomplete: 'off', class: 'base-input' %>
|
||||||
<div class="form-control pt-2">
|
</div>
|
||||||
<%= f.button button_title(title: 'Update', disabled_with: 'Updating'), class: 'base-button' %>
|
<div class="form-control pt-2">
|
||||||
|
<%= f.button button_title(title: 'Update', disabled_with: 'Updating'), class: 'base-button' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<p class="text-2xl font-bold mt-8 mb-4">Two-Factor Authentication</p>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<% if current_user.otp_required_for_login %>
|
||||||
|
<p class="flex items-center space-x-1">
|
||||||
|
<%= svg_icon('circle_check', class: 'stroke-success inline flex-none w-5 h-5') %>
|
||||||
|
<span>
|
||||||
|
2FA has been configured.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<a href="<%= edit_mfa_setup_path %>" data-turbo-frame="modal" class="white-button w-full !px-8">🔓 Remove 2FA</a>
|
||||||
|
<% else %>
|
||||||
|
<p class="flex items-center space-x-1">
|
||||||
|
<%= svg_icon('info_circle', class: 'stroke-warning inline flex-none w-5 h-5') %>
|
||||||
|
<span>
|
||||||
|
2FA is not configured.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<a href="<%= new_mfa_setup_path %>" data-turbo-frame="modal" class="base-button w-full !px-8">🔒 Set up 2FA</a>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<p class="text-2xl font-bold mt-8 mb-4">Two-Factor Authentication</p>
|
|
||||||
<div class="space-y-4">
|
|
||||||
<% if current_user.otp_required_for_login %>
|
|
||||||
<p class="flex items-center space-x-1">
|
|
||||||
<%= svg_icon('circle_check', class: 'stroke-success inline flex-none w-5 h-5') %>
|
|
||||||
<span>
|
|
||||||
2FA has been configured.
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<a href="<%= edit_mfa_setup_path %>" data-turbo-frame="modal" class="white-button w-full !px-8">🔓 Remove 2FA</a>
|
|
||||||
<% else %>
|
|
||||||
<p class="flex items-center space-x-1">
|
|
||||||
<%= svg_icon('info_circle', class: 'stroke-warning inline flex-none w-5 h-5') %>
|
|
||||||
<span>
|
|
||||||
2FA is not configured.
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<a href="<%= new_mfa_setup_path %>" data-turbo-frame="modal" class="base-button w-full !px-8">🔒 Set up 2FA</a>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="w-0 md:w-52"></div>
|
<div class="w-0 md:w-52"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render 'shared/settings_nav_extra' %>
|
<%= render 'shared/settings_nav_extra' %>
|
||||||
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) %>
|
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user != true_user || !current_account.testing?) %>
|
||||||
<li>
|
<li>
|
||||||
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premise" }.to_query}", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premise" }.to_query}", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
||||||
Plans
|
Plans
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) %>
|
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user != true_user || !current_account.testing?) %>
|
||||||
<li>
|
<li>
|
||||||
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/api") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/api") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
||||||
<% if Docuseal.multitenant? %> API <% else %> Console <% end %>
|
<% if Docuseal.multitenant? %> API <% else %> Console <% end %>
|
||||||
|
|||||||
@@ -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 %>
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
|
<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
|
||||||
<% if template.submitters.size == 1 %>
|
<% if template.submitters.size == 1 %>
|
||||||
<submitter-item class="form-control">
|
<submitter-item class="form-control">
|
||||||
<submitters-autocomplete data-field="email">
|
<emails-textarea data-bulk-enabled="<%= Docuseal.demo? || !Docuseal.multitenant? || can?(:manage, :bulk_send) %>" data-limit="<%= Docuseal.multitenant? ? (can?(:manage, :bulk_send) ? 50 : 1) : nil %>">
|
||||||
<autoresize-textarea>
|
<submitters-autocomplete data-field="email" class="block relative">
|
||||||
<%= f.text_area :emails, required: true, class: 'base-textarea w-full', placeholder: 'Type emails here...' %>
|
<autoresize-textarea>
|
||||||
</autoresize-textarea>
|
<%= f.text_area :emails, required: true, class: 'base-textarea w-full', placeholder: 'Type emails here...' %>
|
||||||
</submitters-autocomplete>
|
</autoresize-textarea>
|
||||||
|
</submitters-autocomplete>
|
||||||
|
</emails-textarea>
|
||||||
</submitter-item>
|
</submitter-item>
|
||||||
<% else %>
|
<% else %>
|
||||||
<dynamic-list class="space-y-4">
|
<dynamic-list class="space-y-4">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<field-value dir="auto" class="flex absolute text-[1.5vw] lg:text-base" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%">
|
<% align = field.dig('preferences', 'align') %>
|
||||||
|
<field-value dir="auto" class="flex absolute text-[1.5vw] lg:text-base <%= align == 'right' ? 'justify-end' : (align == 'center' ? 'justify-center' : '') %>" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%">
|
||||||
<% if field['type'].in?(['signature', 'image', 'initials', 'stamp']) %>
|
<% if field['type'].in?(['signature', 'image', 'initials', 'stamp']) %>
|
||||||
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy">
|
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy">
|
||||||
<% elsif field['type'].in?(['file', 'payment']) %>
|
<% elsif field['type'].in?(['file', 'payment']) %>
|
||||||
@@ -31,7 +32,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 %>
|
||||||
Loaded 100 of 150 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user