mirror of
https://github.com/docusealco/docuseal
synced 2026-11-05 06:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
202943f58d | ||
|
|
6dd15b2091 | ||
|
|
1c55e4f18b | ||
|
|
6cc36b1728 | ||
|
|
37751cd5c1 | ||
|
|
80d882cd20 | ||
|
|
8bef75e570 | ||
|
|
077eab7005 | ||
|
|
715c70d422 | ||
|
|
a3c23f9d53 | ||
|
|
c1e67a8af0 | ||
|
|
2fdc294293 | ||
|
|
1f49ba069e | ||
|
|
43ef0fc0d0 | ||
|
|
a1a920e536 | ||
|
|
dec33b4331 | ||
|
|
432f290633 | ||
|
|
cf98616a27 | ||
|
|
1a720cf1ad | ||
|
|
557c6c34f2 | ||
|
|
943c76505d | ||
|
|
66eb83e6a7 | ||
|
|
0844427357 | ||
|
|
72e9d9ea2d | ||
|
|
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 |
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
-4
@@ -2,7 +2,9 @@ FROM ruby:3.2.2-alpine3.18 as fonts
|
|||||||
|
|
||||||
WORKDIR /fonts
|
WORKDIR /fonts
|
||||||
|
|
||||||
RUN apk --no-cache add wget && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt
|
RUN 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("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
|
||||||
|
|
||||||
@@ -37,11 +39,11 @@ ENV BUNDLE_WITHOUT="development:test"
|
|||||||
|
|
||||||
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
|
||||||
@@ -54,6 +56,7 @@ COPY ./tmp ./tmp
|
|||||||
COPY LICENSE README.md Rakefile config.ru .version ./
|
COPY LICENSE README.md Rakefile config.ru .version ./
|
||||||
|
|
||||||
COPY --from=fonts /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-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
|
||||||
@@ -63,4 +66,4 @@ WORKDIR /data/docuseal
|
|||||||
ENV WORKDIR=/data/docuseal
|
ENV WORKDIR=/data/docuseal
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["/app/bin/rails", "server"]
|
CMD ["/app/bin/bundle", "exec", "puma", "-C", "/app/config/puma.rb", "--dir", "/app"]
|
||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
- PDF signature verification
|
- PDF signature verification
|
||||||
- Users management
|
- Users management
|
||||||
- Mobile-optimized
|
- Mobile-optimized
|
||||||
|
- Signing available in 12 Languages
|
||||||
- API and Webhooks for integrations
|
- API and Webhooks for integrations
|
||||||
- Easy to deploy in minutes
|
- Easy to deploy in minutes
|
||||||
|
|
||||||
@@ -50,11 +51,12 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
- User roles
|
- User roles
|
||||||
- Automated reminders
|
- Automated reminders
|
||||||
- Invitation and identify verification via SMS
|
- Invitation and identify verification via SMS
|
||||||
|
- Conditional fields and formulas
|
||||||
- SSO / SAML
|
- SSO / SAML
|
||||||
- Template creation with HTML API ([Guide](https://www.docuseal.co/guides/create-pdf-document-fillable-form-with-html-api))
|
- Template creation with HTML API ([Guide](https://www.docuseal.co/guides/create-pdf-document-fillable-form-with-html-api))
|
||||||
- Template creation with PDF or DOCX and text tags API ([Guide](https://www.docuseal.co/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form))
|
- 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 signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) 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))
|
- Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded))
|
||||||
- [Learn more](https://www.docuseal.co/pricing)
|
- [Learn more](https://www.docuseal.co/pricing)
|
||||||
|
|
||||||
## Deploy
|
## Deploy
|
||||||
@@ -64,9 +66,6 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
| [<img alt="Deploy on Heroku" src="https://www.herokucdn.com/deploy/button.svg" height="40">](https://heroku.com/deploy?template=https://github.com/docusealco/docuseal-heroku) | [<img alt="Deploy on Railway" src="https://railway.app/button.svg" height="40">](https://railway.app/template/IGoDnc?referralCode=ruU7JR)|
|
| [<img alt="Deploy on Heroku" src="https://www.herokucdn.com/deploy/button.svg" height="40">](https://heroku.com/deploy?template=https://github.com/docusealco/docuseal-heroku) | [<img alt="Deploy on Railway" src="https://railway.app/button.svg" height="40">](https://railway.app/template/IGoDnc?referralCode=ruU7JR)|
|
||||||
|**DigitalOcean**|**Render**|
|
|**DigitalOcean**|**Render**|
|
||||||
| [<img alt="Deploy on DigitalOcean" src="https://www.deploytodo.com/do-btn-blue.svg" height="40">](https://cloud.digitalocean.com/apps/new?repo=https://github.com/docusealco/docuseal-digitalocean/tree/master&refcode=421d50f53990) | [<img alt="Deploy to Render" src="https://render.com/images/deploy-to-render-button.svg" height="40">](https://render.com/deploy?repo=https://github.com/docusealco/docuseal-render)
|
| [<img alt="Deploy on DigitalOcean" src="https://www.deploytodo.com/do-btn-blue.svg" height="40">](https://cloud.digitalocean.com/apps/new?repo=https://github.com/docusealco/docuseal-digitalocean/tree/master&refcode=421d50f53990) | [<img alt="Deploy to Render" src="https://render.com/images/deploy-to-render-button.svg" height="40">](https://render.com/deploy?repo=https://github.com/docusealco/docuseal-render)
|
||||||
|**Koyeb**|**Elestio**|
|
|
||||||
| [<img alt="Deploy on Koyeb" src="https://www.koyeb.com/static/images/deploy/button.svg" height="40">](https://app.koyeb.com/deploy?name=docuseal&type=docker&image=docker.io/docuseal/docuseal&env[PORT]=8000&env[DATABASE_URL]=CHANGE_ME&env[SECRET_KEY_BASE]=CHANGE_ME&ports=8000;http;/) | [<img alt="Deploy on Elestio" src="https://pub-da36157c854648669813f3f76c526c2b.r2.dev/deploy-on-elestio-black.png">](https://dash.elest.io/deploy?soft=DocuSeal&id=339) |
|
|
||||||
|
|
||||||
|
|
||||||
#### Docker
|
#### Docker
|
||||||
|
|
||||||
@@ -99,3 +98,9 @@ At DocuSeal we have expertise and technologies to make documents creation, filli
|
|||||||
|
|
||||||
Distributed under the AGPLv3 License. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) for more information.
|
Distributed under the AGPLv3 License. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) for more information.
|
||||||
Unless otherwise noted, all files © 2023 DocuSeal LLC.
|
Unless otherwise noted, all files © 2023 DocuSeal LLC.
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
- [Signature Maker](https://www.docuseal.co/online-signature)
|
||||||
|
- [Sign Document Online](https://www.docuseal.co/sign-documents-online)
|
||||||
|
- [Fill PDF Online](https://www.docuseal.co/fill-pdf)
|
||||||
@@ -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])
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ module Api
|
|||||||
before_action :set_cors_headers
|
before_action :set_cors_headers
|
||||||
|
|
||||||
def show
|
def show
|
||||||
blob_uuid, = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
|
blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
|
||||||
|
|
||||||
if blob_uuid.blank?
|
if blob_uuid.blank? || (purp.present? && purp != 'blob') || (exp && exp < Time.current.to_i)
|
||||||
Rollbar.error('Blob not found') if defined?(Rollbar)
|
Rollbar.error('Blob not found') if defined?(Rollbar)
|
||||||
|
|
||||||
return head :not_found
|
return head :not_found
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ module Api
|
|||||||
def show
|
def show
|
||||||
Rollbar.info('Blob legacy') if defined?(Rollbar)
|
Rollbar.info('Blob legacy') if defined?(Rollbar)
|
||||||
|
|
||||||
blob = ActiveStorage::Blob.find_signed!(params[:signed_blob_id] || params[:signed_id])
|
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|
|
is_permitted = blob.attachments.any? do |a|
|
||||||
(current_user && a.record.account.id == current_user.account_id) ||
|
(current_user && a.record.account.id == current_user.account_id) ||
|
||||||
|
|||||||
@@ -19,10 +19,14 @@ 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.warning(e) if defined?(Rollbar)
|
|
||||||
|
|
||||||
render json: { error: e.message }, status: :forbidden
|
render json: { error: e.message }, status: :forbidden
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -39,8 +43,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
|
||||||
|
|||||||
@@ -161,11 +161,11 @@ module Api
|
|||||||
|
|
||||||
def submissions_params
|
def submissions_params
|
||||||
permitted_attrs = [
|
permitted_attrs = [
|
||||||
:send_email, :send_sms, :bcc_completed, :completed_redirect_url,
|
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to,
|
||||||
{
|
{
|
||||||
message: %i[subject body],
|
message: %i[subject body],
|
||||||
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||||
:completed, :phone, :application_key, :external_id,
|
:completed, :phone, :application_key, :external_id, :reply_to,
|
||||||
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
||||||
fields: [%i[name default_value title description
|
fields: [%i[name default_value title description
|
||||||
readonly validation_pattern invalid_message]] }]]
|
readonly validation_pattern invalid_message]] }]]
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Api
|
|
||||||
class SubmittersAutocompleteController < ApiBaseController
|
|
||||||
load_and_authorize_resource :submitter, parent: false
|
|
||||||
|
|
||||||
SELECT_COLUMNS = %w[email phone name].freeze
|
|
||||||
LIMIT = 100
|
|
||||||
|
|
||||||
def index
|
|
||||||
submitters = search_submitters(@submitters)
|
|
||||||
|
|
||||||
values = submitters.limit(LIMIT).group(SELECT_COLUMNS.join(', ')).pluck(SELECT_COLUMNS.join(', '))
|
|
||||||
|
|
||||||
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
|
|
||||||
attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present?
|
|
||||||
|
|
||||||
render json: attrs
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def search_submitters(submitters)
|
|
||||||
if SELECT_COLUMNS.include?(params[:field])
|
|
||||||
column = Submitter.arel_table[params[:field].to_sym]
|
|
||||||
|
|
||||||
term = "%#{params[:q].downcase}%"
|
|
||||||
|
|
||||||
submitters.where(column.lower.matches(term))
|
|
||||||
else
|
|
||||||
Submitters.search(submitters, params[:q])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
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)
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Api
|
|
||||||
class TemplateFoldersAutocompleteController < ApiBaseController
|
|
||||||
load_and_authorize_resource :template_folder, parent: false
|
|
||||||
|
|
||||||
LIMIT = 100
|
|
||||||
|
|
||||||
def index
|
|
||||||
template_folders = @template_folders.joins(:templates).where(templates: { archived_at: nil }).distinct
|
|
||||||
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
|
|
||||||
|
|
||||||
render json: template_folders.as_json(only: %i[name archived_at])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -20,16 +20,7 @@ module Api
|
|||||||
|
|
||||||
Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
|
Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
|
||||||
|
|
||||||
render json: cloned_template.as_json(serialize_params)
|
render json: Templates::SerializeForApi.call(cloned_template)
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def serialize_params
|
|
||||||
{
|
|
||||||
include: { author: { only: %i[id email first_name last_name] },
|
|
||||||
documents: { only: %i[id uuid], methods: %i[url filename] } }
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -7,10 +7,31 @@ module Api
|
|||||||
def index
|
def index
|
||||||
templates = filter_templates(@templates, params)
|
templates = filter_templates(@templates, params)
|
||||||
|
|
||||||
templates = paginate(templates.preload(:author, documents_attachments: :blob))
|
templates = paginate(templates.preload(:author, :folder))
|
||||||
|
|
||||||
|
schema_documents =
|
||||||
|
ActiveStorage::Attachment.where(record_id: templates.map(&:id),
|
||||||
|
record_type: 'Template',
|
||||||
|
name: :documents,
|
||||||
|
uuid: templates.flat_map { |t| t.schema.pluck('attachment_uuid') })
|
||||||
|
.preload(:blob)
|
||||||
|
|
||||||
|
preview_image_attachments =
|
||||||
|
ActiveStorage::Attachment.joins(:blob)
|
||||||
|
.where(blob: { filename: '0.jpg' })
|
||||||
|
.where(record_id: schema_documents.map(&:id),
|
||||||
|
record_type: 'ActiveStorage::Attachment',
|
||||||
|
name: :preview_images)
|
||||||
|
.preload(:blob)
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
data: templates.as_json(serialize_params),
|
data: templates.map do |t|
|
||||||
|
Templates::SerializeForApi.call(
|
||||||
|
t,
|
||||||
|
schema_documents.select { |e| e.record_id == t.id },
|
||||||
|
preview_image_attachments
|
||||||
|
)
|
||||||
|
end,
|
||||||
pagination: {
|
pagination: {
|
||||||
count: templates.size,
|
count: templates.size,
|
||||||
next: templates.last&.id,
|
next: templates.last&.id,
|
||||||
@@ -20,7 +41,7 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
render json: @template.as_json(serialize_params)
|
render json: Templates::SerializeForApi.call(@template)
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@@ -28,6 +49,14 @@ module Api
|
|||||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, folder_name)
|
@template.folder = TemplateFolders.find_or_create_by_name(current_user, folder_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Array.wrap(params[:roles].presence || params.dig(:template, :roles).presence).each_with_index do |role, index|
|
||||||
|
if (item = @template.submitters[index])
|
||||||
|
item['name'] = role
|
||||||
|
else
|
||||||
|
@template.submitters << { 'name' => role, 'uuid' => SecureRandom.uuid }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
archived = params.key?(:archived) ? params[:archived] : params.dig(:template, :archived)
|
archived = params.key?(:archived) ? params[:archived] : params.dig(:template, :archived)
|
||||||
|
|
||||||
if archived.in?([true, false])
|
if archived.in?([true, false])
|
||||||
@@ -61,29 +90,11 @@ 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 = [
|
|
||||||
:name,
|
|
||||||
{ schema: [%i[attachment_uuid name]],
|
|
||||||
submitters: [%i[name uuid]],
|
|
||||||
fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value,
|
|
||||||
{ preferences: {},
|
|
||||||
options: [%i[value uuid]],
|
|
||||||
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
|
||||||
]
|
|
||||||
|
|
||||||
if params.key?(:template)
|
if params.key?(:template)
|
||||||
params.require(:template).permit(*permit_params)
|
params.require(:template).permit(:name, submitters: [%i[name uuid]])
|
||||||
else
|
else
|
||||||
params.permit(*permit_params)
|
params.permit(:name, submitters: [%i[name uuid]])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Api
|
|
||||||
class TemplatesDocumentsController < ApiBaseController
|
|
||||||
load_and_authorize_resource :template
|
|
||||||
|
|
||||||
def create
|
|
||||||
return head :unprocessable_entity if params[:blobs].blank? && params[:files].blank?
|
|
||||||
|
|
||||||
documents = Templates::CreateAttachments.call(@template, params)
|
|
||||||
|
|
||||||
schema = documents.map do |doc|
|
|
||||||
{ attachment_uuid: doc.uuid, name: doc.filename.base }
|
|
||||||
end
|
|
||||||
|
|
||||||
render json: {
|
|
||||||
schema:,
|
|
||||||
documents: documents.as_json(
|
|
||||||
methods: %i[metadata signed_uuid],
|
|
||||||
include: {
|
|
||||||
preview_images: { methods: %i[url metadata filename] }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
rescue Templates::CreateAttachments::PdfEncrypted
|
|
||||||
render json: { error: 'PDF encrypted' }, status: :unprocessable_entity
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -22,6 +22,12 @@ 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.warning(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class PreviewDocumentPageController < ActionController::API
|
|||||||
def show
|
def show
|
||||||
attachment_uuid = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid], purpose: :attachment)
|
attachment_uuid = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid], purpose: :attachment)
|
||||||
|
|
||||||
attachment = ActiveStorage::Attachment.find_by(uuid: attachment_uuid, name: :preview_images) if attachment_uuid
|
attachment = ActiveStorage::Attachment.find_by(uuid: attachment_uuid) if attachment_uuid
|
||||||
|
|
||||||
return head :not_found unless attachment
|
return head :not_found unless 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 }
|
||||||
|
|||||||
@@ -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,10 +4,20 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
TTL = 20.minutes
|
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)
|
||||||
|
|
||||||
@@ -17,18 +27,24 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
|
|
||||||
return head :not_found unless last_submitter.completed_at?
|
return head :not_found unless last_submitter.completed_at?
|
||||||
|
|
||||||
if last_submitter.completed_at < TTL.ago &&
|
if last_submitter.completed_at < TTL.ago && !signature_valid && !current_user_submitter?(last_submitter)
|
||||||
(current_user.nil? || !current_user.account.submitters.exists?(id: last_submitter.id))
|
|
||||||
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
return head :not_found
|
return head :not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
urls =
|
render json: build_urls(last_submitter)
|
||||||
Submitters.select_attachments_for_download(last_submitter).map do |attachment|
|
end
|
||||||
ActiveStorage::Blob.proxy_url(attachment.blob)
|
|
||||||
end
|
|
||||||
|
|
||||||
render json: urls
|
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,37 +4,31 @@ 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
|
||||||
|
|
||||||
TTL = 20.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?
|
||||||
|
|
||||||
|
signature_valid =
|
||||||
|
if submitter && submitter.submission.slug == params[:slug]
|
||||||
|
@submission = submitter.submission
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
@submission ||= Submission.find_by!(slug: params[:slug])
|
||||||
|
|
||||||
if !@submission.submitters.all?(&:completed_at?) && current_user.blank?
|
if !@submission.submitters.all?(&:completed_at?) && current_user.blank?
|
||||||
raise ActionController::RoutingError, 'Not Found'
|
raise ActionController::RoutingError, 'Not Found'
|
||||||
end
|
end
|
||||||
|
|
||||||
unless submission_valid_ttl?(@submission)
|
if !submission_valid_ttl?(@submission) && !signature_valid
|
||||||
Rollbar.info("TTL: #{@submission.id}") if defined?(Rollbar)
|
Rollbar.info("TTL: #{@submission.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
return redirect_to submissions_preview_completed_path(@submission.slug)
|
return redirect_to submissions_preview_completed_path(@submission.slug)
|
||||||
end
|
end
|
||||||
|
|
||||||
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 'submissions/show', layout: 'plain'
|
render 'submissions/show', layout: 'plain'
|
||||||
end
|
end
|
||||||
@@ -42,7 +36,7 @@ class SubmissionsPreviewController < ApplicationController
|
|||||||
def completed
|
def completed
|
||||||
@submission = Submission.find_by!(slug: params[:submissions_preview_slug])
|
@submission = Submission.find_by!(slug: params[:submissions_preview_slug])
|
||||||
|
|
||||||
render :completed, layout: 'plain'
|
render :completed, layout: 'form'
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmittersAutocompleteController < ApplicationController
|
||||||
|
load_and_authorize_resource :submitter, parent: false
|
||||||
|
|
||||||
|
SELECT_COLUMNS = %w[email phone name].freeze
|
||||||
|
LIMIT = 100
|
||||||
|
|
||||||
|
def index
|
||||||
|
submitters = search_submitters(@submitters)
|
||||||
|
|
||||||
|
values = submitters.limit(LIMIT).group(SELECT_COLUMNS.join(', ')).pluck(SELECT_COLUMNS.join(', '))
|
||||||
|
|
||||||
|
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
|
||||||
|
attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present?
|
||||||
|
|
||||||
|
render json: attrs
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def search_submitters(submitters)
|
||||||
|
if SELECT_COLUMNS.include?(params[:field])
|
||||||
|
column = Submitter.arel_table[params[:field].to_sym]
|
||||||
|
|
||||||
|
term = "#{params[:q].downcase}%"
|
||||||
|
|
||||||
|
submitters.where(column.matches(term, false, true))
|
||||||
|
else
|
||||||
|
Submitters.search(submitters, params[:q])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplateDocumentsController < ApplicationController
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def create
|
||||||
|
return head :unprocessable_entity if params[:blobs].blank? && params[:files].blank?
|
||||||
|
|
||||||
|
documents = Templates::CreateAttachments.call(@template, params)
|
||||||
|
|
||||||
|
schema = documents.map do |doc|
|
||||||
|
{ attachment_uuid: doc.uuid, name: doc.filename.base }
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
schema:,
|
||||||
|
documents: documents.as_json(
|
||||||
|
methods: %i[metadata signed_uuid],
|
||||||
|
include: {
|
||||||
|
preview_images: { methods: %i[url metadata filename] }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
rescue Templates::CreateAttachments::PdfEncrypted
|
||||||
|
render json: { error: 'PDF encrypted' }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplateFoldersAutocompleteController < ApplicationController
|
||||||
|
load_and_authorize_resource :template_folder, parent: false
|
||||||
|
|
||||||
|
LIMIT = 100
|
||||||
|
|
||||||
|
def index
|
||||||
|
template_folders = @template_folders.joins(:templates).where(templates: { archived_at: nil }).distinct
|
||||||
|
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
|
||||||
|
|
||||||
|
render json: template_folders.as_json(only: %i[name archived_at])
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -6,7 +6,7 @@ class TemplatesArchivedSubmissionsController < ApplicationController
|
|||||||
|
|
||||||
def index
|
def index
|
||||||
@submissions = @submissions.where.not(archived_at: nil)
|
@submissions = @submissions.where.not(archived_at: nil)
|
||||||
@submissions = Submissions.search(@submissions, params[:q])
|
@submissions = Submissions.search(@submissions, params[:q], search_values: true)
|
||||||
|
|
||||||
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
|
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class TemplatesController < ApplicationController
|
|||||||
def show
|
def show
|
||||||
submissions = @template.submissions.accessible_by(current_ability)
|
submissions = @template.submissions.accessible_by(current_ability)
|
||||||
submissions = submissions.active if @template.archived_at.blank?
|
submissions = submissions.active if @template.archived_at.blank?
|
||||||
submissions = Submissions.search(submissions, params[:q])
|
submissions = Submissions.search(submissions, params[:q], search_values: true)
|
||||||
|
|
||||||
@base_submissions = submissions
|
@base_submissions = submissions
|
||||||
|
|
||||||
@@ -67,6 +67,12 @@ class TemplatesController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@template.update!(template_params)
|
||||||
|
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
notice =
|
notice =
|
||||||
if params[:permanently].present?
|
if params[:permanently].present?
|
||||||
@@ -86,6 +92,22 @@ class TemplatesController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def template_params
|
||||||
|
params.require(:template).permit(
|
||||||
|
:name,
|
||||||
|
{ schema: [%i[attachment_uuid name]],
|
||||||
|
submitters: [%i[name uuid]],
|
||||||
|
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||||
|
:required, :readonly, :default_value,
|
||||||
|
:title, :description,
|
||||||
|
{ preferences: {},
|
||||||
|
conditions: [%i[field_uuid value action]],
|
||||||
|
options: [%i[value uuid]],
|
||||||
|
validation: %i[message pattern],
|
||||||
|
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
def authorized_clone_account_id?(account_id)
|
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)
|
true_user.account_id.to_s == account_id.to_s || true_user.account.linked_accounts.exists?(id: account_id)
|
||||||
end
|
end
|
||||||
@@ -98,10 +120,6 @@ class TemplatesController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def template_params
|
|
||||||
params.require(:template).permit(:name)
|
|
||||||
end
|
|
||||||
|
|
||||||
def load_base_template
|
def load_base_template
|
||||||
return if params[:base_template_id].blank?
|
return if params[:base_template_id].blank?
|
||||||
|
|
||||||
|
|||||||
@@ -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) => {
|
||||||
@@ -89,7 +91,10 @@ window.customElements.define('template-builder', class extends HTMLElement {
|
|||||||
withPhone: this.dataset.withPhone === 'true',
|
withPhone: this.dataset.withPhone === 'true',
|
||||||
withLogo: this.dataset.withLogo !== 'false',
|
withLogo: this.dataset.withLogo !== 'false',
|
||||||
editable: this.dataset.editable !== 'false',
|
editable: this.dataset.editable !== 'false',
|
||||||
|
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
||||||
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')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ export default class extends HTMLElement {
|
|||||||
fetch = (text, resolve) => {
|
fetch = (text, resolve) => {
|
||||||
const queryParams = new URLSearchParams({ q: text })
|
const queryParams = new URLSearchParams({ q: text })
|
||||||
|
|
||||||
fetch('/api/template_folders_autocomplete?' + queryParams).then(async (resp) => {
|
fetch('/template_folders_autocomplete?' + queryParams).then(async (resp) => {
|
||||||
const items = await resp.json()
|
const items = await resp.json()
|
||||||
|
|
||||||
resolve(items)
|
resolve(items)
|
||||||
|
|||||||
@@ -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('/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([])
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ window.customElements.define('submission-form', class extends HTMLElement {
|
|||||||
attribution: this.dataset.attribution !== 'false',
|
attribution: this.dataset.attribution !== 'false',
|
||||||
withConfetti: this.dataset.withConfetti !== 'false',
|
withConfetti: this.dataset.withConfetti !== 'false',
|
||||||
withTypedSignature: this.dataset.withTypedSignature !== 'false',
|
withTypedSignature: this.dataset.withTypedSignature !== 'false',
|
||||||
|
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
||||||
values: reactive(JSON.parse(this.dataset.values)),
|
values: reactive(JSON.parse(this.dataset.values)),
|
||||||
completedButton: JSON.parse(this.dataset.completedButton),
|
completedButton: JSON.parse(this.dataset.completedButton),
|
||||||
completedRedirectUrl: this.dataset.completedRedirectUrl,
|
completedRedirectUrl: this.dataset.completedRedirectUrl,
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -72,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"
|
||||||
@@ -82,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"
|
||||||
@@ -121,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)"
|
||||||
@@ -178,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
|
||||||
@@ -388,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'
|
||||||
@@ -396,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: {
|
||||||
@@ -409,6 +490,7 @@ export default {
|
|||||||
ImageStep,
|
ImageStep,
|
||||||
SignatureStep,
|
SignatureStep,
|
||||||
AppearsOn,
|
AppearsOn,
|
||||||
|
IconWritingSign,
|
||||||
AttachmentStep,
|
AttachmentStep,
|
||||||
InitialsStep,
|
InitialsStep,
|
||||||
MultiSelectStep,
|
MultiSelectStep,
|
||||||
@@ -416,7 +498,10 @@ export default {
|
|||||||
DateStep,
|
DateStep,
|
||||||
IconArrowsDiagonal,
|
IconArrowsDiagonal,
|
||||||
TextStep,
|
TextStep,
|
||||||
|
NumberStep,
|
||||||
|
FormulaFieldAreas,
|
||||||
PhoneStep,
|
PhoneStep,
|
||||||
|
MarkdownContent,
|
||||||
PaymentStep,
|
PaymentStep,
|
||||||
IconArrowsDiagonalMinimize2,
|
IconArrowsDiagonalMinimize2,
|
||||||
FormCompleted
|
FormCompleted
|
||||||
@@ -451,6 +536,11 @@ export default {
|
|||||||
return () => {}
|
return () => {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
expand: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
withConfetti: {
|
withConfetti: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -521,6 +611,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
},
|
},
|
||||||
|
authenticityToken: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
i18n: {
|
i18n: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -550,7 +645,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,
|
||||||
@@ -568,19 +663,23 @@ export default {
|
|||||||
queryParams () {
|
queryParams () {
|
||||||
return new URLSearchParams(window.location.search)
|
return new URLSearchParams(window.location.search)
|
||||||
},
|
},
|
||||||
authenticityToken () {
|
|
||||||
return document.querySelector('meta[name="csrf-token"]')?.content
|
|
||||||
},
|
|
||||||
submitterSlug () {
|
submitterSlug () {
|
||||||
return this.submitter.slug
|
return this.submitter.slug
|
||||||
},
|
},
|
||||||
|
fieldsUuidIndex () {
|
||||||
|
return this.fields.reduce((acc, f) => {
|
||||||
|
acc[f.uuid] = f
|
||||||
|
|
||||||
|
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])
|
||||||
|
|
||||||
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) {
|
||||||
@@ -599,15 +698,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
|
||||||
@@ -619,6 +723,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))
|
||||||
|
|
||||||
@@ -651,6 +760,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()
|
||||||
@@ -680,6 +793,40 @@ export default {
|
|||||||
t (key) {
|
t (key) {
|
||||||
return this.i18n[key] || 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
|
||||||
|
|
||||||
@@ -757,7 +904,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()
|
||||||
@@ -765,7 +912,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({})
|
||||||
@@ -775,7 +922,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
|
||||||
@@ -797,7 +946,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)
|
||||||
@@ -847,7 +1002,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',
|
||||||
@@ -16,6 +17,7 @@ const en = {
|
|||||||
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',
|
||||||
@@ -68,6 +70,7 @@ const es = {
|
|||||||
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',
|
||||||
@@ -80,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',
|
||||||
@@ -131,6 +135,7 @@ const it = {
|
|||||||
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',
|
||||||
@@ -142,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',
|
||||||
@@ -194,6 +200,7 @@ const de = {
|
|||||||
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',
|
||||||
@@ -205,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',
|
||||||
@@ -257,6 +265,7 @@ const fr = {
|
|||||||
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',
|
||||||
@@ -268,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',
|
||||||
@@ -320,6 +330,7 @@ const pl = {
|
|||||||
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',
|
||||||
@@ -331,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',
|
||||||
@@ -383,6 +395,7 @@ const uk = {
|
|||||||
signature: 'Підпис',
|
signature: 'Підпис',
|
||||||
initials: 'Ініціали',
|
initials: 'Ініціали',
|
||||||
date: 'Дата',
|
date: 'Дата',
|
||||||
|
number: 'Число',
|
||||||
image: 'Зображення',
|
image: 'Зображення',
|
||||||
file: 'Файл',
|
file: 'Файл',
|
||||||
select: 'Вибрати',
|
select: 'Вибрати',
|
||||||
@@ -394,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: "З'являється на",
|
||||||
@@ -446,6 +460,7 @@ const cs = {
|
|||||||
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',
|
||||||
@@ -457,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',
|
||||||
@@ -509,6 +525,7 @@ const pt = {
|
|||||||
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',
|
||||||
@@ -520,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',
|
||||||
@@ -570,8 +588,9 @@ const he = {
|
|||||||
minimize: 'לקטן',
|
minimize: 'לקטן',
|
||||||
text: 'טקסט',
|
text: 'טקסט',
|
||||||
signature: 'חתימה',
|
signature: 'חתימה',
|
||||||
initials: 'ראשי תיקיות',
|
initials: 'ראשי תיבות',
|
||||||
date: 'תאריך',
|
date: 'תאריך',
|
||||||
|
number: 'מספר',
|
||||||
image: 'תמונה',
|
image: 'תמונה',
|
||||||
file: 'קובץ',
|
file: 'קובץ',
|
||||||
select: 'בחר',
|
select: 'בחר',
|
||||||
@@ -583,6 +602,7 @@ const he = {
|
|||||||
payment: 'תשלום',
|
payment: 'תשלום',
|
||||||
phone: 'טלפון',
|
phone: 'טלפון',
|
||||||
submit_form: 'שלח טופס',
|
submit_form: 'שלח טופס',
|
||||||
|
sign_now: 'חתום כעת',
|
||||||
type_here_: 'הקלד כאן',
|
type_here_: 'הקלד כאן',
|
||||||
optional: 'אופציונלי',
|
optional: 'אופציונלי',
|
||||||
option: 'אפשרות',
|
option: 'אפשרות',
|
||||||
@@ -636,6 +656,7 @@ const nl = {
|
|||||||
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',
|
||||||
@@ -648,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',
|
||||||
@@ -694,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,9 +1,20 @@
|
|||||||
<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"
|
||||||
@@ -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'],
|
||||||
|
|||||||
@@ -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,9 +1,20 @@
|
|||||||
<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 && field.preferences?.format !== 'typed'"
|
v-if="isTextSignature && field.preferences?.format !== 'typed'"
|
||||||
@@ -30,7 +41,7 @@
|
|||||||
<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" />
|
||||||
@@ -45,7 +56,7 @@
|
|||||||
: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
|
||||||
@@ -91,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"
|
||||||
@@ -126,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
|
||||||
|
|
||||||
@@ -137,6 +156,7 @@ export default {
|
|||||||
AppearsOn,
|
AppearsOn,
|
||||||
IconReload,
|
IconReload,
|
||||||
IconCamera,
|
IconCamera,
|
||||||
|
MarkdownContent,
|
||||||
IconTextSize,
|
IconTextSize,
|
||||||
IconSignature,
|
IconSignature,
|
||||||
IconArrowsDiagonalMinimize2
|
IconArrowsDiagonalMinimize2
|
||||||
@@ -366,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) {
|
||||||
@@ -409,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'],
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
v-model="field.submitter_uuid"
|
v-model="field.submitter_uuid"
|
||||||
class="border-r"
|
class="border-r"
|
||||||
:compact="true"
|
:compact="true"
|
||||||
:editable="editable && !defaultField"
|
:editable="editable && (!defaultField || defaultField.role !== submitter?.name)"
|
||||||
:allow-add-new="!defaultSubmitters.length"
|
:allow-add-new="!defaultSubmitters.length"
|
||||||
:menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px] mt-[1px]'"
|
:menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px] mt-[1px]'"
|
||||||
:submitters="template.submitters"
|
:submitters="template.submitters"
|
||||||
@@ -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,
|
||||||
@@ -428,6 +444,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
authenticityToken: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
withDocumentsList: {
|
withDocumentsList: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -443,6 +464,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 +498,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
selectedAreaRef: () => ref(),
|
selectedAreaRef: () => ref(),
|
||||||
|
fieldsDragFieldRef: () => ref(),
|
||||||
fieldAreasIndex () {
|
fieldAreasIndex () {
|
||||||
const areas = {}
|
const areas = {}
|
||||||
|
|
||||||
@@ -502,7 +534,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 +787,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 +830,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 +838,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 +906,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()
|
||||||
},
|
},
|
||||||
@@ -980,7 +1028,11 @@ export default {
|
|||||||
baseFetch (path, options = {}) {
|
baseFetch (path, options = {}) {
|
||||||
return fetch(this.baseUrl + path, {
|
return fetch(this.baseUrl + path, {
|
||||||
...options,
|
...options,
|
||||||
headers: { ...this.fetchOptions.headers, ...options.headers }
|
headers: {
|
||||||
|
'X-CSRF-Token': this.authenticityToken,
|
||||||
|
...this.fetchOptions.headers,
|
||||||
|
...options.headers
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
save ({ force } = { force: false }) {
|
save ({ force } = { force: false }) {
|
||||||
@@ -996,7 +1048,7 @@ export default {
|
|||||||
|
|
||||||
this.pushUndo()
|
this.pushUndo()
|
||||||
|
|
||||||
return this.baseFetch(`/api/templates/${this.template.id}`, {
|
return this.baseFetch(`/templates/${this.template.id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
template: {
|
template: {
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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"
|
||||||
@@ -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"
|
||||||
@@ -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"
|
||||||
>
|
>
|
||||||
@@ -180,7 +228,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</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">
|
||||||
@@ -193,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
|
||||||
@@ -222,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">
|
||||||
@@ -235,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
|
||||||
@@ -249,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)">
|
||||||
@@ -308,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
|
||||||
@@ -337,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)"
|
||||||
@@ -350,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"
|
||||||
>
|
>
|
||||||
@@ -359,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>
|
||||||
|
|
||||||
@@ -366,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 {
|
||||||
@@ -377,7 +526,13 @@ export default {
|
|||||||
IconShape,
|
IconShape,
|
||||||
PaymentSettings,
|
PaymentSettings,
|
||||||
IconNewSection,
|
IconNewSection,
|
||||||
|
IconInfoCircle,
|
||||||
|
FormulaModal,
|
||||||
|
DescriptionModal,
|
||||||
|
ConditionsModal,
|
||||||
|
IconRouteAltLeft,
|
||||||
IconTrashX,
|
IconTrashX,
|
||||||
|
IconMathFunction,
|
||||||
IconCopy,
|
IconCopy,
|
||||||
FieldType
|
FieldType
|
||||||
},
|
},
|
||||||
@@ -403,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',
|
||||||
@@ -422,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 || []
|
||||||
@@ -452,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',
|
||||||
@@ -485,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(() => {
|
||||||
@@ -515,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()
|
||||||
@@ -533,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()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,12 +113,13 @@ 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,
|
||||||
|
|||||||
@@ -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)"
|
||||||
@@ -36,32 +36,63 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="submitterDefaultFields.length">
|
<div v-if="submitterDefaultFields.length">
|
||||||
<hr class="mb-2">
|
<hr class="mb-2">
|
||||||
<template
|
<template v-if="isShowFieldSearch">
|
||||||
v-for="field in submitterDefaultFields"
|
<input
|
||||||
:key="field.name"
|
v-model="defaultFieldsSearch"
|
||||||
|
:placeholder="t('search_field')"
|
||||||
|
class="input input-ghost input-xs px-0 text-base mb-2 !outline-0 !rounded bg-transparent w-full"
|
||||||
|
>
|
||||||
|
<hr class="mb-2">
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
class="overflow-auto relative"
|
||||||
|
:style="{
|
||||||
|
maxHeight: isShowFieldSearch ? '210px' : '',
|
||||||
|
minHeight: isShowFieldSearch ? '210px' : ''
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
:style="{ backgroundColor: backgroundColor }"
|
v-if="!filteredSubmitterDefaultFields.length && defaultFieldsSearch"
|
||||||
draggable="true"
|
class="top-0 bottom-0 text-center absolute flex items-center justify-center w-full flex-col"
|
||||||
class="border border-base-300 rounded rounded-tr-none relative group mb-2"
|
|
||||||
@dragstart="onDragstart({ type: 'text', ...field })"
|
|
||||||
@dragend="$emit('drag-end')"
|
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between relative cursor-grab">
|
<div>
|
||||||
<div class="flex items-center p-1 space-x-1">
|
{{ t('field_not_found') }}
|
||||||
<IconDrag />
|
</div>
|
||||||
<FieldType
|
<a
|
||||||
:model-value="field.type || 'text'"
|
href="#"
|
||||||
:editable="false"
|
class="link"
|
||||||
:button-width="20"
|
@click.prevent="defaultFieldsSearch = ''"
|
||||||
/>
|
>
|
||||||
<span class="block pl-0.5">
|
{{ t('clear') }}
|
||||||
{{ field.name }}
|
</a>
|
||||||
</span>
|
</div>
|
||||||
|
<template
|
||||||
|
v-for="field in filteredSubmitterDefaultFields"
|
||||||
|
:key="field.name"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
|
draggable="true"
|
||||||
|
class="default-field border border-base-300 rounded rounded-tr-none relative group mb-2"
|
||||||
|
@dragstart="onDragstart({ type: 'text', ...field })"
|
||||||
|
@dragend="$emit('drag-end')"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between relative cursor-grab">
|
||||||
|
<div class="flex items-center p-1 space-x-1">
|
||||||
|
<IconDrag />
|
||||||
|
<FieldType
|
||||||
|
:model-value="field.type || 'text'"
|
||||||
|
:editable="false"
|
||||||
|
:button-width="20"
|
||||||
|
/>
|
||||||
|
<span class="block pl-0.5">
|
||||||
|
{{ field.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="editable && !onlyDefinedFields"
|
v-if="editable && !onlyDefinedFields"
|
||||||
@@ -74,7 +105,7 @@
|
|||||||
<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 +124,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 +185,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,
|
||||||
@@ -207,12 +238,15 @@ 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 () {
|
data () {
|
||||||
return {
|
return {
|
||||||
dragField: null
|
defaultFieldsSearch: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
fieldNames: FieldType.computed.fieldNames,
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
fieldIcons: FieldType.computed.fieldIcons,
|
fieldIcons: FieldType.computed.fieldIcons,
|
||||||
|
isShowFieldSearch () {
|
||||||
|
return this.submitterDefaultFields.length > 15
|
||||||
|
},
|
||||||
fieldIconsSorted () {
|
fieldIconsSorted () {
|
||||||
if (this.fieldTypes.length) {
|
if (this.fieldTypes.length) {
|
||||||
return this.fieldTypes.reduce((acc, type) => {
|
return this.fieldTypes.reduce((acc, type) => {
|
||||||
@@ -231,6 +265,13 @@ export default {
|
|||||||
return this.defaultFields.filter((f) => {
|
return this.defaultFields.filter((f) => {
|
||||||
return !this.submitterFields.find((field) => field.name === f.name) && (!f.role || f.role === this.selectedSubmitter.name)
|
return !this.submitterFields.find((field) => field.name === f.name) && (!f.role || f.role === this.selectedSubmitter.name)
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
filteredSubmitterDefaultFields () {
|
||||||
|
if (this.defaultFieldsSearch) {
|
||||||
|
return this.submitterDefaultFields.filter((f) => f.name.toLowerCase().includes(this.defaultFieldsSearch.toLowerCase()))
|
||||||
|
} else {
|
||||||
|
return this.submitterDefaultFields
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -239,7 +280,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)
|
||||||
@@ -283,6 +324,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,4 +1,24 @@
|
|||||||
const en = {
|
const en = {
|
||||||
|
search_field: 'Search field',
|
||||||
|
field_not_found: 'Field not found',
|
||||||
|
clear: 'Clear',
|
||||||
|
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',
|
||||||
@@ -13,6 +33,7 @@ const en = {
|
|||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
any: 'Any',
|
any: 'Any',
|
||||||
drawn: 'Drawn',
|
drawn: 'Drawn',
|
||||||
|
formula: 'Formula',
|
||||||
typed: 'Typed',
|
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',
|
||||||
@@ -30,6 +51,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',
|
||||||
@@ -54,6 +76,7 @@ const en = {
|
|||||||
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') {
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export default {
|
|||||||
|
|
||||||
this.isProcessing = true
|
this.isProcessing = true
|
||||||
|
|
||||||
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
|
this.baseFetch(`/templates/${this.templateId}/documents`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ blobs }),
|
body: JSON.stringify({ blobs }),
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
@@ -132,7 +132,7 @@ export default {
|
|||||||
} else if (resp.status === 422) {
|
} else if (resp.status === 422) {
|
||||||
resp.json().then((data) => {
|
resp.json().then((data) => {
|
||||||
if (data.error === 'PDF encrypted') {
|
if (data.error === 'PDF encrypted') {
|
||||||
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
|
this.baseFetch(`/templates/${this.templateId}/documents`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ blobs, password: prompt('Enter PDF password') }),
|
body: JSON.stringify({ blobs, password: prompt('Enter PDF password') }),
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
@@ -151,7 +151,7 @@ export default {
|
|||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
|
this.baseFetch(`/templates/${this.templateId}/documents`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: new FormData(this.$refs.form)
|
body: new FormData(this.$refs.form)
|
||||||
}).then((resp) => {
|
}).then((resp) => {
|
||||||
@@ -167,7 +167,7 @@ export default {
|
|||||||
|
|
||||||
formData.append('password', prompt('Enter PDF password'))
|
formData.append('password', prompt('Enter PDF password'))
|
||||||
|
|
||||||
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
|
this.baseFetch(`/templates/${this.templateId}/documents`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
}).then(async (resp) => {
|
}).then(async (resp) => {
|
||||||
|
|||||||
@@ -15,21 +15,25 @@ 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 && attempt <= MAX_ATTEMPTS &&
|
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||||
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
||||||
SendFormCompletedWebhookRequestJob.set(wait: (2**attempt).minutes)
|
SendFormCompletedWebhookRequestJob.set(wait: (2**attempt).minutes)
|
||||||
.perform_later(submitter, {
|
.perform_later(submitter, {
|
||||||
attempt: attempt + 1,
|
attempt: attempt + 1,
|
||||||
last_status: resp.status.to_i
|
last_status: resp&.status.to_i
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -13,21 +13,25 @@ class SendFormStartedWebhookRequestJob < 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.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 && attempt <= MAX_ATTEMPTS &&
|
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||||
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
||||||
SendFormStartedWebhookRequestJob.set(wait: (2**attempt).minutes)
|
SendFormStartedWebhookRequestJob.set(wait: (2**attempt).minutes)
|
||||||
.perform_later(submitter, {
|
.perform_later(submitter, {
|
||||||
attempt: attempt + 1,
|
attempt: attempt + 1,
|
||||||
last_status: resp.status.to_i
|
last_status: resp&.status.to_i
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -13,21 +13,25 @@ class SendFormViewedWebhookRequestJob < 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.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 && attempt <= MAX_ATTEMPTS &&
|
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||||
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
||||||
SendFormViewedWebhookRequestJob.set(wait: (2**attempt).minutes)
|
SendFormViewedWebhookRequestJob.set(wait: (2**attempt).minutes)
|
||||||
.perform_later(submitter, {
|
.perform_later(submitter, {
|
||||||
attempt: attempt + 1,
|
attempt: attempt + 1,
|
||||||
last_status: resp.status.to_i
|
last_status: resp&.status.to_i
|
||||||
})
|
})
|
||||||
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'
|
||||||
|
|
||||||
@@ -29,7 +30,8 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
to: @submitter.friendly_name,
|
to: @submitter.friendly_name,
|
||||||
from: from_address_for_submitter(submitter),
|
from: from_address_for_submitter(submitter),
|
||||||
subject:,
|
subject:,
|
||||||
reply_to: (submitter.submission.created_by_user || submitter.template.author)&.friendly_name&.sub(/\+\w+@/, '@')
|
reply_to: submitter.preferences['reply_to'].presence ||
|
||||||
|
(submitter.submission.created_by_user || submitter.template.author)&.friendly_name&.sub(/\+\w+@/, '@')
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -61,9 +63,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)
|
||||||
|
|
||||||
@@ -82,6 +85,9 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
|
|
||||||
mail(from: from_address_for_submitter(submitter),
|
mail(from: from_address_for_submitter(submitter),
|
||||||
to: to || @submitter.friendly_name,
|
to: to || @submitter.friendly_name,
|
||||||
|
reply_to: @submitter.preferences['reply_to'].presence ||
|
||||||
|
(@submitter.submission.created_by_user ||
|
||||||
|
@submitter.template.author)&.friendly_name&.sub(/\+\w+@/, '@'),
|
||||||
subject:)
|
subject:)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -5,6 +5,10 @@
|
|||||||
if (this.field.scrollHeight > this.field.clientHeight) {
|
if (this.field.scrollHeight > this.field.clientHeight) {
|
||||||
this.field.classList.remove('text-[1.5vw]', 'lg:text-base');
|
this.field.classList.remove('text-[1.5vw]', 'lg:text-base');
|
||||||
this.field.classList.add('text-[1.0vw]', 'lg:text-[0.70rem]');
|
this.field.classList.add('text-[1.0vw]', 'lg:text-[0.70rem]');
|
||||||
|
|
||||||
|
if (this.field.scrollHeight > this.field.clientHeight) {
|
||||||
|
this.field.classList.add('text-[0.8vw]', 'lg:text-[0.50rem]');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
get field() {
|
get field() {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% if can?(:read, User) %>
|
<% if can?(:read, User) %>
|
||||||
<li>
|
<li>
|
||||||
<%= link_to 'Team', settings_users_path, class: 'text-base hover:bg-base-300' %>
|
<%= link_to 'Users', settings_users_path, class: 'text-base hover:bg-base-300' %>
|
||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% unless Docuseal.multitenant? %>
|
<% unless Docuseal.multitenant? %>
|
||||||
@@ -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,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="<%= ActiveStorage::Blob.proxy_url(@submission.audit_trail.blob) %>" 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>
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -20,6 +20,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</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? %>
|
<% if Docuseal.multitenant? %>
|
||||||
<div>
|
<div>
|
||||||
<%= link_to 'Create free account', registration_path, class: 'white-button w-full' %>
|
<%= link_to 'Create free account', registration_path, class: 'white-button w-full' %>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
<div class="py-2"></div>
|
<div class="py-2"></div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if @submitter.completed_at > 15.minutes.ago || (current_user && current_user.account.submitters.exists?(id: @submitter.id)) %>
|
<% if @submitter.completed_at > 30.minutes.ago || (current_user && current_user.account.submitters.exists?(id: @submitter.id)) %>
|
||||||
<download-button data-src="<%= submitter_download_index_path(@submitter.slug) %>" class="base-button w-full">
|
<download-button data-src="<%= submitter_download_index_path(@submitter.slug) %>" class="base-button w-full">
|
||||||
<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') %>
|
||||||
|
|||||||
@@ -21,9 +21,10 @@
|
|||||||
<%= render 'submissions/annotation', annot: %>
|
<%= render 'submissions/annotation', annot: %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
|
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
|
||||||
<% value = values[field['uuid']].presence || (field['readonly'] ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submitter.submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
|
<% value = values[field['uuid']].presence || (field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submitter.submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
|
||||||
<% next if value.blank? %>
|
<% next if value.blank? %>
|
||||||
<% next if !field['readonly'] && field['submitter_uuid'] == @submitter.uuid %>
|
<% next if !field['readonly'] && field['submitter_uuid'] == @submitter.uuid %>
|
||||||
|
<% next if field.dig('preferences', 'formula') && field['submitter_uuid'] == @submitter.uuid %>
|
||||||
<%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submitter.account.locale %>
|
<%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submitter.account.locale %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<%= auto_link(simple_format(h(ReplaceEmailVariables.call(local_assigns[:content], submitter: local_assigns[:submitter], sig: local_assigns[:sig])))) %>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<% if @email_config %>
|
<% if @email_config %>
|
||||||
<%= auto_link(simple_format(h(ReplaceEmailVariables.call(@email_config.value['body'], submitter: @submitter)))) %>
|
<%= render 'custom_content', content: @email_config.value['body'], submitter: @submitter %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<p>Hi there,</p>
|
<p>Hi there,</p>
|
||||||
<p>"<%= @submitter.submission.template.name %>" form has been completed by <%= @submitter.submission.submitters.order(:completed_at).map { |e| e.name || e.email || e.phone }.join(', ') %>.</p>
|
<p>"<%= @submitter.submission.template.name %>" form has been completed by <%= @submitter.submission.submitters.order(:completed_at).map { |e| e.name || e.email || e.phone }.join(', ') %>.</p>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<% if @email_config %>
|
<% if @email_config %>
|
||||||
<%= auto_link(simple_format(h(ReplaceEmailVariables.call(@email_config.value['body'], submitter: @submitter)))) %>
|
<%= render 'custom_content', content: @email_config.value['body'], submitter: @submitter, sig: @sig %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<p>Hi there,</p>
|
<p>Hi there,</p>
|
||||||
<p>Please check the copy of your "<%= @submitter.submission.template.name %>" submission in the email attachments.</p>
|
<p>Please check the copy of your "<%= @submitter.submission.template.name %>" submission in the email attachments.</p>
|
||||||
<p>Alternatively, you can review and download your copy using:</p>
|
<p>Alternatively, you can review and download your copy using:</p>
|
||||||
<p>
|
<p>
|
||||||
<%= link_to @submitter.template.name, submissions_preview_url(@submitter.submission.slug) %>
|
<%= link_to @submitter.template.name, submissions_preview_url(@submitter.submission.slug, { sig: @sig }.compact) %>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Thanks,<br><%= @current_account.name %>
|
Thanks,<br><%= @current_account.name %>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<% if @email_config || @body.present? %>
|
<% if @email_config || @body.present? %>
|
||||||
<% body = (@body.presence || @email_config.value['body']).to_s %>
|
<% body = (@body.presence || @email_config.value['body']).to_s %>
|
||||||
<%= auto_link(simple_format(h(ReplaceEmailVariables.call(body, submitter: @submitter)))) %>
|
<%= render 'custom_content', content: body, submitter: @submitter %>
|
||||||
<% if !body.include?(ReplaceEmailVariables::SUBMITTER_LINK) && !body.include?(ReplaceEmailVariables::SUBMITTER_ID) && !body.include?(ReplaceEmailVariables::SUBMISSION_LINK) && !body.include?(ReplaceEmailVariables::TEMPLATE_ID) && !@submitter.submission.source.in?(%w[api embed]) %>
|
<% if !body.include?(ReplaceEmailVariables::SUBMITTER_LINK) && !body.include?(ReplaceEmailVariables::SUBMITTER_ID) && !body.include?(ReplaceEmailVariables::SUBMISSION_LINK) && !body.include?(ReplaceEmailVariables::TEMPLATE_ID) && !@submitter.submission.source.in?(%w[api embed]) %>
|
||||||
<p><%= link_to nil, submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email')) %></p>
|
<p><%= link_to nil, submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email')) %></p>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -3,6 +3,11 @@
|
|||||||
<% if @base_template %>
|
<% if @base_template %>
|
||||||
<%= hidden_field_tag :base_template_id, @base_template.id %>
|
<%= hidden_field_tag :base_template_id, @base_template.id %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% if @base_template && (can?(:manage, :tenants) || true_user != current_user) && true_user.account.account_linked_accounts.exists? %>
|
||||||
|
<div class="form-control -mb-2 mt-2">
|
||||||
|
<%= select_tag :account_id, options_for_select([true_user.account, *true_user.account.linked_accounts].uniq.map { |e| [e.name, e.id] }, current_account.id), required: true, class: 'base-select' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
<div class="form-control mt-6">
|
<div class="form-control mt-6">
|
||||||
<%= f.text_field :name, required: true, placeholder: 'Document Name', class: 'base-input', dir: 'auto' %>
|
<%= f.text_field :name, required: true, placeholder: 'Document Name', class: 'base-input', dir: 'auto' %>
|
||||||
</div>
|
</div>
|
||||||
@@ -17,11 +22,6 @@
|
|||||||
</a>
|
</a>
|
||||||
</folder-autocomplete>
|
</folder-autocomplete>
|
||||||
</div>
|
</div>
|
||||||
<% if @base_template && (can?(:manage, :tenants) || true_user != current_user) && true_user.account.account_linked_accounts.exists?(account_type: :linked) %>
|
|
||||||
<div class="form-control mb-4">
|
|
||||||
<%= select_tag :account_id, options_for_select([true_user.account, *true_user.account.linked_accounts.where.not(id: true_user.account.testing_accounts)].map { |e| [e.name, e.id] }, current_account.id), required: true, class: 'base-select' %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.button button_title(title: @base_template ? 'Submit' : 'Create', disabled_with: 'Creating'), class: 'base-button' %>
|
<%= f.button button_title(title: @base_template ? 'Submit' : 'Create', disabled_with: 'Creating'), class: 'base-button' %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loaded 100 of 136 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user