mirror of
https://github.com/docusealco/docuseal.git
synced 2026-08-07 07:14:43 +00:00
Compare commits
137 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 33ca930055 | |||
| 3b396f8421 | |||
| aa77e3a8d3 | |||
| 75316d8d87 | |||
| a999109a5c | |||
| bdd33c7d6b | |||
| 61c5ee22a0 | |||
| 12c5b909e0 | |||
| 871ef6dda6 | |||
| 40052a2d7c | |||
| 347be0137d | |||
| fe6baba8bf | |||
| ed8c313bd4 | |||
| ca0acb34d6 | |||
| 34ea639c25 | |||
| 680ab9dbed | |||
| 9377766e52 | |||
| 7a2c37454e | |||
| 8f6f418f54 | |||
| 9b7745c565 | |||
| a389e8cc38 | |||
| 2056301f70 | |||
| 93babe3660 | |||
| 4aef2d8eb0 | |||
| c5ec2ed230 | |||
| a156d5f6cc | |||
| f84674cddd | |||
| b7b62c197a | |||
| f5350eac36 | |||
| ee6228db15 | |||
| 3e434c0878 | |||
| 7c5372a8e2 | |||
| 6ab68cc36b | |||
| ee2400a5c5 | |||
| 80be5d2cf5 | |||
| 76cf44a31b | |||
| eb63113227 | |||
| b7045fb3d9 | |||
| 11a5b5842d | |||
| 362994afd5 | |||
| d7682f4ebd | |||
| 56d4fe7fa9 | |||
| 93a852ac14 | |||
| 964db0ab08 | |||
| 8547295da7 | |||
| c75050fa46 | |||
| 65372c52aa | |||
| 12c767b7d3 | |||
| 2fc5e9cfb3 | |||
| bccc78ee65 | |||
| c0793f3ce7 | |||
| a8dd8699a3 | |||
| 91bb25e8b6 | |||
| c3f4480cdc | |||
| 33e226861e | |||
| 4f5acbec9d | |||
| d2c9bbf6ab | |||
| b5e57e1f2f | |||
| c82c8b2272 | |||
| 43f1b598a7 | |||
| 5081e387d0 | |||
| 1e5f55c1d6 | |||
| aa38773be3 | |||
| 96530bd809 | |||
| 127f95a9d0 | |||
| cf31e12348 | |||
| 4adaa531b7 | |||
| c7659563b8 | |||
| 7b5494468f | |||
| d12c67fd54 | |||
| e20eacb5ee | |||
| 98055f0762 | |||
| a46c42c3a0 | |||
| c3d1503361 | |||
| 65b47d27cd | |||
| 968c07f8be | |||
| 91814ca105 | |||
| 9128f24270 | |||
| 264897bbd5 | |||
| 67d59ff1ba | |||
| 81eee56e1e | |||
| 92ea8af786 | |||
| df0616f77e | |||
| a337dff85c | |||
| 48e349c1d2 | |||
| df9fa0fec7 | |||
| b3e72f0726 | |||
| 700ebc4727 | |||
| 745a18efca | |||
| b4a8147ff0 | |||
| 0543c25054 | |||
| b1a0afea66 | |||
| 43c41e2557 | |||
| bc8eb33f05 | |||
| d1cebc92f7 | |||
| bf02cf4058 | |||
| 5212773d0a | |||
| bf53204a41 | |||
| 092a19966c | |||
| 6060a2b798 | |||
| ce12af68b2 | |||
| c05f8d47aa | |||
| f8be19c20e | |||
| 2fe32a2145 | |||
| 794ea4d0e8 | |||
| 307b0f5afd | |||
| 9011e3f4b4 | |||
| 0b45ce5c0c | |||
| a8ab93a4cc | |||
| d1768be636 | |||
| b2cd7c82a7 | |||
| b88d99afe0 | |||
| 4523d38551 | |||
| c4ba892559 | |||
| 40f8093e00 | |||
| 4375459856 | |||
| b3eb8a70bf | |||
| 6245a487b6 | |||
| a89f4c1b10 | |||
| 7df729b8b5 | |||
| f1db7c7e82 | |||
| 00af69cbf2 | |||
| c6819a19bd | |||
| f5cbcb3d24 | |||
| 6ca75c53ef | |||
| e0ec443e10 | |||
| 9eed2fc2ef | |||
| b9c6d3ff9f | |||
| 03c25a89b2 | |||
| f5b4a0c5ab | |||
| b10ed46ccc | |||
| c55036a1b1 | |||
| 962a5dade5 | |||
| 369a09a6df | |||
| e9a39e2571 | |||
| 1cf9cc18f1 | |||
| 1984d4ec8d |
@@ -12,7 +12,7 @@ jobs:
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.4.2
|
||||
ruby-version: 4.0.1
|
||||
- name: Cache gems
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.4.2
|
||||
ruby-version: 4.0.1
|
||||
- name: Cache gems
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 20.9.0
|
||||
node-version: 20.19.0
|
||||
- name: Cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.4.2
|
||||
ruby-version: 4.0.1
|
||||
- name: Cache gems
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -132,11 +132,11 @@ jobs:
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.4.2
|
||||
ruby-version: 4.0.1
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 20.9.0
|
||||
node-version: 20.19.0
|
||||
- name: Install Chrome
|
||||
uses: browser-actions/setup-chrome@latest
|
||||
with:
|
||||
|
||||
+11
-2
@@ -1,4 +1,4 @@
|
||||
require:
|
||||
plugins:
|
||||
- rubocop-performance
|
||||
- rubocop-rails
|
||||
- rubocop-rspec
|
||||
@@ -10,7 +10,7 @@ AllCops:
|
||||
- node_modules/**/*
|
||||
- bin/*
|
||||
- vendor/**/*
|
||||
TargetRubyVersion: '3.3'
|
||||
TargetRubyVersion: '4.0'
|
||||
SuggestExtensions: false
|
||||
|
||||
Metrics/BlockLength:
|
||||
@@ -48,6 +48,9 @@ Style/MultipleComparison:
|
||||
Style/NumericPredicate:
|
||||
Enabled: false
|
||||
|
||||
Style/MinMaxComparison:
|
||||
Enabled: false
|
||||
|
||||
Naming/PredicateMethod:
|
||||
Enabled: false
|
||||
|
||||
@@ -97,3 +100,9 @@ Rails/ApplicationController:
|
||||
|
||||
Rails/Output:
|
||||
Enabled: false
|
||||
|
||||
Rails/StrongParametersExpect:
|
||||
Enabled: false
|
||||
|
||||
Rails/RedirectBackOrTo:
|
||||
Enabled: false
|
||||
|
||||
+28
-26
@@ -1,4 +1,4 @@
|
||||
FROM ruby:3.4.2-alpine AS download
|
||||
FROM ruby:4.0.1-alpine AS download
|
||||
|
||||
WORKDIR /fonts
|
||||
|
||||
@@ -9,14 +9,14 @@ RUN apk --no-cache add fontforge wget && \
|
||||
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 && \
|
||||
wget -O /model.onnx "https://github.com/docusealco/fields-detection/releases/download/1.1.0/model_704_int8.onnx" && \
|
||||
wget -O /model.onnx "https://github.com/docusealco/fields-detection/releases/download/2.0.0/model_704_int8.onnx" && \
|
||||
wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" && \
|
||||
mkdir -p /pdfium-linux && \
|
||||
tar -xzf pdfium-linux.tgz -C /pdfium-linux
|
||||
|
||||
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.4.2-alpine AS webpack
|
||||
FROM ruby:4.0.1-alpine AS webpack
|
||||
|
||||
ENV RAILS_ENV=production
|
||||
ENV NODE_ENV=production
|
||||
@@ -42,16 +42,18 @@ COPY ./app/views ./app/views
|
||||
|
||||
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
|
||||
|
||||
FROM ruby:3.4.2-alpine AS app
|
||||
FROM ruby:4.0.1-alpine AS app
|
||||
|
||||
ENV RAILS_ENV=production
|
||||
ENV BUNDLE_WITHOUT="development:test"
|
||||
ENV LD_PRELOAD=/lib/libgcompat.so.0
|
||||
ENV OPENSSL_CONF=/app/openssl_legacy.cnf
|
||||
ENV OPENSSL_CONF=/etc/openssl_legacy.cnf
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev yaml-dev redis libheif vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
||||
RUN apk add --no-cache sqlite-dev libpq-dev vips-dev yaml-dev redis libheif vips-heif gcompat ttf-freefont onnxruntime && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
||||
|
||||
RUN addgroup -g 2000 docuseal && adduser -u 2000 -G docuseal -s /bin/sh -D -h /home/docuseal docuseal
|
||||
|
||||
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
|
||||
\n\
|
||||
@@ -63,36 +65,36 @@ legacy = legacy_sect\n\
|
||||
activate = 1\n\
|
||||
\n\
|
||||
[legacy_sect]\n\
|
||||
activate = 1' >> /app/openssl_legacy.cnf
|
||||
activate = 1' >> /etc/openssl_legacy.cnf
|
||||
|
||||
COPY ./Gemfile ./Gemfile.lock ./
|
||||
COPY --chown=docuseal:docuseal ./Gemfile ./Gemfile.lock ./
|
||||
|
||||
RUN apk add --no-cache build-base && bundle install && apk del --no-cache 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 && ln -sf /usr/lib/libonnxruntime.so.1 $(ruby -e "print Dir[Gem::Specification.find_by_name('onnxruntime').gem_dir + '/vendor/*.so'].first")
|
||||
RUN apk add --no-cache build-base git && bundle install && apk del --no-cache build-base git && 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 && ln -sf /usr/lib/libonnxruntime.so.1 $(ruby -e "print Dir[Gem::Specification.find_by_name('onnxruntime').gem_dir + '/vendor/*.so'].first")
|
||||
|
||||
RUN echo 'https://dl-cdn.alpinelinux.org/alpine/edge/main' >> /etc/apk/repositories && echo 'https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache onnxruntime
|
||||
COPY --chown=docuseal:docuseal ./bin ./bin
|
||||
COPY --chown=docuseal:docuseal ./app ./app
|
||||
COPY --chown=docuseal:docuseal ./config ./config
|
||||
COPY --chown=docuseal:docuseal ./db/migrate ./db/migrate
|
||||
COPY --chown=docuseal:docuseal ./log ./log
|
||||
COPY --chown=docuseal:docuseal ./lib ./lib
|
||||
COPY --chown=docuseal:docuseal ./public ./public
|
||||
COPY --chown=docuseal:docuseal ./tmp ./tmp
|
||||
COPY --chown=docuseal:docuseal LICENSE README.md Rakefile config.ru .version ./
|
||||
COPY --chown=docuseal:docuseal .version ./public/version
|
||||
|
||||
COPY ./bin ./bin
|
||||
COPY ./app ./app
|
||||
COPY ./config ./config
|
||||
COPY ./db/migrate ./db/migrate
|
||||
COPY ./log ./log
|
||||
COPY ./lib ./lib
|
||||
COPY ./public ./public
|
||||
COPY ./tmp ./tmp
|
||||
COPY LICENSE README.md Rakefile config.ru .version ./
|
||||
COPY .version ./public/version
|
||||
|
||||
COPY --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
||||
COPY --chown=docuseal:docuseal --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
||||
COPY --from=download /fonts/FreeSans.ttf /usr/share/fonts/freefont
|
||||
COPY --from=download /pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so
|
||||
COPY --from=download /pdfium-linux/licenses/pdfium.txt /usr/lib/libpdfium-LICENSE.txt
|
||||
COPY --from=download /model.onnx /app/tmp/model.onnx
|
||||
COPY --from=webpack /app/public/packs ./public/packs
|
||||
COPY --chown=docuseal:docuseal --from=download /model.onnx /app/tmp/model.onnx
|
||||
COPY --chown=docuseal:docuseal --from=webpack /app/public/packs ./public/packs
|
||||
|
||||
RUN ln -s /fonts /app/public/fonts
|
||||
RUN bundle exec bootsnap precompile -j 1 --gemfile app/ lib/
|
||||
RUN ln -s /fonts /app/public/fonts && \
|
||||
bundle exec bootsnap precompile -j 1 --gemfile app/ lib/ && \
|
||||
chown -R docuseal:docuseal /app/tmp/cache
|
||||
|
||||
WORKDIR /data/docuseal
|
||||
ENV HOME=/home/docuseal
|
||||
ENV WORKDIR=/data/docuseal
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
source 'https://rubygems.org'
|
||||
|
||||
ruby '3.4.2'
|
||||
ruby '4.0.1'
|
||||
|
||||
gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic'
|
||||
gem 'arabic-letter-connector', require: false
|
||||
gem 'aws-sdk-s3', require: false
|
||||
gem 'aws-sdk-secretsmanager', require: false
|
||||
gem 'azure-storage-blob', require: false
|
||||
gem 'azure-blob', require: false
|
||||
gem 'bootsnap', require: false
|
||||
gem 'cancancan'
|
||||
gem 'csv'
|
||||
gem 'csv-safe'
|
||||
gem 'csv', require: false
|
||||
gem 'csv-safe', require: false
|
||||
gem 'devise'
|
||||
gem 'devise-two-factor'
|
||||
gem 'dotenv', require: false
|
||||
@@ -21,12 +21,11 @@ gem 'faraday-follow_redirects'
|
||||
gem 'google-cloud-storage', require: false
|
||||
gem 'hexapdf'
|
||||
gem 'image_processing'
|
||||
gem 'jwt'
|
||||
gem 'jwt', require: false
|
||||
gem 'lograge'
|
||||
gem 'mysql2', require: false
|
||||
gem 'numo-narray'
|
||||
gem 'numo-narray-alt', require: false
|
||||
gem 'oj'
|
||||
gem 'onnxruntime'
|
||||
gem 'onnxruntime', require: false
|
||||
gem 'pagy'
|
||||
gem 'pg', require: false
|
||||
gem 'premailer-rails'
|
||||
@@ -38,13 +37,14 @@ gem 'rails_autolink'
|
||||
gem 'rails-i18n'
|
||||
gem 'rotp'
|
||||
gem 'rouge', require: false
|
||||
gem 'rqrcode'
|
||||
gem 'rqrcode', require: false
|
||||
gem 'ruby-vips'
|
||||
gem 'rubyXL'
|
||||
gem 'rubyXL', require: false
|
||||
gem 'shakapacker'
|
||||
gem 'sidekiq'
|
||||
gem 'sqlite3', require: false
|
||||
gem 'strip_attributes'
|
||||
gem 'trilogy', github: 'trilogy-libraries/trilogy', glob: 'contrib/ruby/*.gemspec', require: false
|
||||
gem 'turbo-rails'
|
||||
gem 'twitter_cldr', require: false
|
||||
gem 'tzinfo-data'
|
||||
|
||||
+287
-286
@@ -1,29 +1,39 @@
|
||||
GIT
|
||||
remote: https://github.com/trilogy-libraries/trilogy.git
|
||||
revision: 3963d490459df7a2b5bedb42424c3285f25eab22
|
||||
glob: contrib/ruby/*.gemspec
|
||||
specs:
|
||||
trilogy (2.10.0)
|
||||
bigdecimal
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
action_text-trix (2.1.16)
|
||||
railties
|
||||
actioncable (8.1.2)
|
||||
actionpack (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
zeitwerk (~> 2.6)
|
||||
actionmailbox (8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
activejob (= 8.0.2.1)
|
||||
activerecord (= 8.0.2.1)
|
||||
activestorage (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
actionmailbox (8.1.2)
|
||||
actionpack (= 8.1.2)
|
||||
activejob (= 8.1.2)
|
||||
activerecord (= 8.1.2)
|
||||
activestorage (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
mail (>= 2.8.0)
|
||||
actionmailer (8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
actionview (= 8.0.2.1)
|
||||
activejob (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
actionmailer (8.1.2)
|
||||
actionpack (= 8.1.2)
|
||||
actionview (= 8.1.2)
|
||||
activejob (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
mail (>= 2.8.0)
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (8.0.2.1)
|
||||
actionview (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
actionpack (8.1.2)
|
||||
actionview (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
nokogiri (>= 1.8.5)
|
||||
rack (>= 2.2.4)
|
||||
rack-session (>= 1.0.1)
|
||||
@@ -31,55 +41,58 @@ GEM
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
useragent (~> 0.16)
|
||||
actiontext (8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
activerecord (= 8.0.2.1)
|
||||
activestorage (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
actiontext (8.1.2)
|
||||
action_text-trix (~> 2.1.15)
|
||||
actionpack (= 8.1.2)
|
||||
activerecord (= 8.1.2)
|
||||
activestorage (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
actionview (8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
activejob (8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
activejob (8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
activerecord (8.0.2.1)
|
||||
activemodel (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
activemodel (8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
activerecord (8.1.2)
|
||||
activemodel (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
timeout (>= 0.4.0)
|
||||
activestorage (8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
activejob (= 8.0.2.1)
|
||||
activerecord (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
activestorage (8.1.2)
|
||||
actionpack (= 8.1.2)
|
||||
activejob (= 8.1.2)
|
||||
activerecord (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
marcel (~> 1.0)
|
||||
activesupport (8.0.2.1)
|
||||
activesupport (8.1.2)
|
||||
base64
|
||||
benchmark (>= 0.3)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||
connection_pool (>= 2.2.5)
|
||||
drb
|
||||
i18n (>= 1.6, < 2)
|
||||
json
|
||||
logger (>= 1.4.2)
|
||||
minitest (>= 5.1)
|
||||
securerandom (>= 0.3)
|
||||
tzinfo (~> 2.0, >= 2.0.5)
|
||||
uri (>= 0.13.1)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
annotaterb (4.14.0)
|
||||
addressable (2.8.8)
|
||||
public_suffix (>= 2.0.2, < 8.0)
|
||||
annotaterb (4.20.0)
|
||||
activerecord (>= 6.0.0)
|
||||
activesupport (>= 6.0.0)
|
||||
arabic-letter-connector (0.1.1)
|
||||
ast (2.4.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1197.0)
|
||||
aws-sdk-core (3.240.0)
|
||||
aws-partitions (1.1209.0)
|
||||
aws-sdk-core (3.241.4)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
@@ -87,44 +100,38 @@ GEM
|
||||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.118.0)
|
||||
aws-sdk-core (~> 3, >= 3.239.1)
|
||||
aws-sdk-kms (1.121.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.4)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.208.0)
|
||||
aws-sdk-core (~> 3, >= 3.234.0)
|
||||
aws-sdk-s3 (1.212.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.4)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-secretsmanager (1.110.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-secretsmanager (1.128.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.4)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.12.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
azure-storage-blob (2.0.3)
|
||||
azure-storage-common (~> 2.0)
|
||||
nokogiri (~> 1, >= 1.10.8)
|
||||
azure-storage-common (2.0.4)
|
||||
faraday (~> 1.0)
|
||||
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
|
||||
net-http-persistent (~> 4.0)
|
||||
nokogiri (~> 1, >= 1.10.8)
|
||||
azure-blob (0.8.0)
|
||||
cgi
|
||||
rexml
|
||||
base64 (0.3.0)
|
||||
bcrypt (3.1.20)
|
||||
benchmark (0.4.1)
|
||||
better_html (2.1.1)
|
||||
actionview (>= 6.0)
|
||||
activesupport (>= 6.0)
|
||||
bcrypt (3.1.21)
|
||||
better_html (2.2.0)
|
||||
actionview (>= 7.0)
|
||||
activesupport (>= 7.0)
|
||||
ast (~> 2.0)
|
||||
erubi (~> 1.4)
|
||||
parser (>= 2.4)
|
||||
smart_properties
|
||||
bigdecimal (3.2.2)
|
||||
bigdecimal (4.0.1)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.18.4)
|
||||
bootsnap (1.21.1)
|
||||
msgpack (~> 1.2)
|
||||
brakeman (7.0.0)
|
||||
brakeman (7.1.2)
|
||||
racc
|
||||
builder (3.3.0)
|
||||
bullet (8.0.0)
|
||||
bullet (8.1.0)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.11)
|
||||
camertron-eprun (1.1.1)
|
||||
@@ -138,28 +145,29 @@ GEM
|
||||
rack-test (>= 0.6.3)
|
||||
regexp_parser (>= 1.5, < 3.0)
|
||||
xpath (~> 3.2)
|
||||
cgi (0.5.1)
|
||||
childprocess (5.1.0)
|
||||
logger (~> 1.5)
|
||||
chunky_png (1.4.0)
|
||||
cldr-plurals-runtime-rb (1.1.0)
|
||||
cmdparse (3.0.7)
|
||||
coderay (1.1.3)
|
||||
concurrent-ruby (1.3.5)
|
||||
connection_pool (2.5.3)
|
||||
crack (1.0.0)
|
||||
concurrent-ruby (1.3.6)
|
||||
connection_pool (3.0.2)
|
||||
crack (1.0.1)
|
||||
bigdecimal
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
css_parser (1.21.0)
|
||||
css_parser (1.21.1)
|
||||
addressable
|
||||
csv (3.3.2)
|
||||
csv (3.3.5)
|
||||
csv-safe (3.3.1)
|
||||
csv (~> 3.0)
|
||||
cuprite (0.15.1)
|
||||
cuprite (0.17)
|
||||
capybara (~> 3.0)
|
||||
ferrum (~> 0.15.0)
|
||||
date (3.4.1)
|
||||
debug (1.10.0)
|
||||
ferrum (~> 0.17.0)
|
||||
date (3.5.1)
|
||||
debug (1.11.1)
|
||||
irb (~> 1.10)
|
||||
reline (>= 0.3.8)
|
||||
declarative (0.0.20)
|
||||
@@ -169,20 +177,20 @@ GEM
|
||||
railties (>= 4.1.0)
|
||||
responders
|
||||
warden (~> 1.2.3)
|
||||
devise-two-factor (6.1.0)
|
||||
activesupport (>= 7.0, < 8.1)
|
||||
devise (~> 4.0)
|
||||
railties (>= 7.0, < 8.1)
|
||||
devise-two-factor (6.3.1)
|
||||
activesupport (>= 7.0, < 8.2)
|
||||
devise (>= 4.0, < 5.0)
|
||||
railties (>= 7.0, < 8.2)
|
||||
rotp (~> 6.0)
|
||||
diff-lcs (1.5.1)
|
||||
digest-crc (0.6.5)
|
||||
diff-lcs (1.6.2)
|
||||
digest-crc (0.7.0)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
docile (1.4.1)
|
||||
dotenv (3.1.7)
|
||||
dotenv (3.2.0)
|
||||
drb (2.2.3)
|
||||
email_typo (0.2.3)
|
||||
erb (5.0.2)
|
||||
erb_lint (0.7.0)
|
||||
erb (6.0.1)
|
||||
erb_lint (0.9.0)
|
||||
activesupport
|
||||
better_html (>= 2.0.1)
|
||||
parser (>= 2.7.1.4)
|
||||
@@ -190,117 +198,101 @@ GEM
|
||||
rubocop (>= 1)
|
||||
smart_properties
|
||||
erubi (1.13.1)
|
||||
factory_bot (6.5.0)
|
||||
activesupport (>= 5.0.0)
|
||||
factory_bot_rails (6.4.4)
|
||||
factory_bot (6.5.6)
|
||||
activesupport (>= 6.1.0)
|
||||
factory_bot_rails (6.5.1)
|
||||
factory_bot (~> 6.5)
|
||||
railties (>= 5.0.0)
|
||||
faker (3.5.1)
|
||||
railties (>= 6.1.0)
|
||||
faker (3.6.0)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (1.10.4)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
faraday-httpclient (~> 1.0)
|
||||
faraday-multipart (~> 1.0)
|
||||
faraday-net_http (~> 1.0)
|
||||
faraday-net_http_persistent (~> 1.0)
|
||||
faraday-patron (~> 1.0)
|
||||
faraday-rack (~> 1.0)
|
||||
faraday-retry (~> 1.0)
|
||||
ruby2_keywords (>= 0.0.4)
|
||||
faraday-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.0)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-follow_redirects (0.3.0)
|
||||
faraday (2.14.1)
|
||||
faraday-net_http (>= 2.0, < 3.5)
|
||||
json
|
||||
logger
|
||||
faraday-follow_redirects (0.5.0)
|
||||
faraday (>= 1, < 3)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.1.0)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (1.0.2)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.3)
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
ferrum (0.15)
|
||||
faraday-net_http (3.4.2)
|
||||
net-http (~> 0.5)
|
||||
ferrum (0.17.1)
|
||||
addressable (~> 2.5)
|
||||
base64 (~> 0.2)
|
||||
concurrent-ruby (~> 1.1)
|
||||
webrick (~> 1.7)
|
||||
websocket-driver (~> 0.7)
|
||||
ffi (1.17.1)
|
||||
ffi (1.17.1-aarch64-linux-gnu)
|
||||
ffi (1.17.1-aarch64-linux-musl)
|
||||
ffi (1.17.1-arm64-darwin)
|
||||
ffi (1.17.1-x86_64-linux-gnu)
|
||||
ffi (1.17.1-x86_64-linux-musl)
|
||||
foreman (0.88.1)
|
||||
ffi (1.17.3)
|
||||
ffi (1.17.3-aarch64-linux-gnu)
|
||||
ffi (1.17.3-aarch64-linux-musl)
|
||||
ffi (1.17.3-arm64-darwin)
|
||||
ffi (1.17.3-x86_64-linux-gnu)
|
||||
ffi (1.17.3-x86_64-linux-musl)
|
||||
foreman (0.90.0)
|
||||
thor (~> 1.4)
|
||||
geom2d (0.4.1)
|
||||
globalid (1.2.1)
|
||||
globalid (1.3.0)
|
||||
activesupport (>= 6.1)
|
||||
google-apis-core (0.15.1)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (~> 1.9)
|
||||
httpclient (>= 2.8.3, < 3.a)
|
||||
mini_mime (~> 1.0)
|
||||
mutex_m
|
||||
google-apis-core (1.0.2)
|
||||
addressable (~> 2.8, >= 2.8.7)
|
||||
faraday (~> 2.13)
|
||||
faraday-follow_redirects (~> 0.3)
|
||||
googleauth (~> 1.14)
|
||||
mini_mime (~> 1.1)
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.a)
|
||||
google-apis-iamcredentials_v1 (0.22.0)
|
||||
retriable (~> 3.1)
|
||||
google-apis-iamcredentials_v1 (0.26.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-apis-storage_v1 (0.49.0)
|
||||
google-apis-storage_v1 (0.59.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-cloud-core (1.7.1)
|
||||
google-cloud-core (1.8.0)
|
||||
google-cloud-env (>= 1.0, < 3.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (2.2.1)
|
||||
google-cloud-env (2.3.1)
|
||||
base64 (~> 0.2)
|
||||
faraday (>= 1.0, < 3.a)
|
||||
google-cloud-errors (1.4.0)
|
||||
google-cloud-storage (1.54.0)
|
||||
google-cloud-errors (1.5.0)
|
||||
google-cloud-storage (1.58.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
google-apis-core (~> 0.13)
|
||||
google-apis-core (>= 0.18, < 2)
|
||||
google-apis-iamcredentials_v1 (~> 0.18)
|
||||
google-apis-storage_v1 (~> 0.38)
|
||||
google-apis-storage_v1 (>= 0.42)
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (~> 1.9)
|
||||
mini_mime (~> 1.0)
|
||||
google-logging-utils (0.1.0)
|
||||
googleauth (1.12.2)
|
||||
google-logging-utils (0.2.0)
|
||||
googleauth (1.16.1)
|
||||
faraday (>= 1.0, < 3.a)
|
||||
google-cloud-env (~> 2.2)
|
||||
google-logging-utils (~> 0.1)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
jwt (>= 1.4, < 4.0)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
hashdiff (1.1.2)
|
||||
hexapdf (1.4.0)
|
||||
hashdiff (1.2.1)
|
||||
hexapdf (1.5.0)
|
||||
cmdparse (~> 3.0, >= 3.0.3)
|
||||
geom2d (~> 0.4, >= 0.4.1)
|
||||
openssl (>= 2.2.1)
|
||||
strscan (>= 3.1.2)
|
||||
htmlentities (4.3.4)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.14.7)
|
||||
htmlentities (4.4.2)
|
||||
i18n (1.14.8)
|
||||
concurrent-ruby (~> 1.0)
|
||||
image_processing (1.13.0)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
image_processing (1.14.0)
|
||||
mini_magick (>= 4.9.5, < 6)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
io-console (0.8.1)
|
||||
irb (1.15.2)
|
||||
io-console (0.8.2)
|
||||
irb (1.16.0)
|
||||
pp (>= 0.6.0)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
jmespath (1.6.2)
|
||||
json (2.15.0)
|
||||
jwt (2.9.3)
|
||||
json (2.18.1)
|
||||
jwt (3.1.2)
|
||||
base64
|
||||
language_server-protocol (3.17.0.5)
|
||||
launchy (3.0.1)
|
||||
launchy (3.1.1)
|
||||
addressable (~> 2.8)
|
||||
childprocess (~> 5.0)
|
||||
logger (~> 1.6)
|
||||
letter_opener (1.10.0)
|
||||
launchy (>= 2.2, < 4)
|
||||
letter_opener_web (3.0.0)
|
||||
@@ -315,29 +307,28 @@ GEM
|
||||
activesupport (>= 4)
|
||||
railties (>= 4)
|
||||
request_store (~> 1.0)
|
||||
loofah (2.24.1)
|
||||
loofah (2.25.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
mail (2.8.1)
|
||||
mail (2.9.0)
|
||||
logger
|
||||
mini_mime (>= 0.1.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
marcel (1.0.4)
|
||||
matrix (0.4.2)
|
||||
marcel (1.1.0)
|
||||
matrix (0.4.3)
|
||||
method_source (1.1.0)
|
||||
mini_magick (4.13.2)
|
||||
mini_magick (5.3.1)
|
||||
logger
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.9)
|
||||
minitest (5.25.5)
|
||||
msgpack (1.7.5)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.4.1)
|
||||
mutex_m (0.3.0)
|
||||
mysql2 (0.5.6)
|
||||
net-http-persistent (4.0.5)
|
||||
connection_pool (~> 2.2)
|
||||
net-imap (0.5.9)
|
||||
minitest (6.0.1)
|
||||
prism (~> 1.5)
|
||||
msgpack (1.8.0)
|
||||
multi_json (1.19.1)
|
||||
net-http (0.9.1)
|
||||
uri (>= 0.11.1)
|
||||
net-imap (0.6.2)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
@@ -346,22 +337,19 @@ GEM
|
||||
timeout
|
||||
net-smtp (0.5.1)
|
||||
net-protocol
|
||||
nio4r (2.7.4)
|
||||
nokogiri (1.18.9)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
nio4r (2.7.5)
|
||||
nokogiri (1.19.0-aarch64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.9-aarch64-linux-gnu)
|
||||
nokogiri (1.19.0-aarch64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.9-aarch64-linux-musl)
|
||||
nokogiri (1.19.0-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.9-arm64-darwin)
|
||||
nokogiri (1.19.0-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.9-x86_64-linux-gnu)
|
||||
nokogiri (1.19.0-x86_64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.9-x86_64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
numo-narray (0.9.2.1)
|
||||
oj (3.16.11)
|
||||
numo-narray-alt (0.9.13)
|
||||
oj (3.16.13)
|
||||
bigdecimal (>= 3.0)
|
||||
ostruct (>= 0.2)
|
||||
onnxruntime (0.10.1)
|
||||
@@ -372,18 +360,25 @@ GEM
|
||||
ffi
|
||||
onnxruntime (0.10.1-x86_64-linux)
|
||||
ffi
|
||||
openssl (3.3.0)
|
||||
openssl (4.0.0)
|
||||
orm_adapter (0.5.0)
|
||||
os (1.1.4)
|
||||
ostruct (0.6.3)
|
||||
package_json (0.1.0)
|
||||
pagy (9.3.3)
|
||||
package_json (0.2.0)
|
||||
pagy (43.2.8)
|
||||
json
|
||||
yaml
|
||||
parallel (1.27.0)
|
||||
parser (3.3.9.0)
|
||||
parser (3.3.10.1)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
pg (1.5.9)
|
||||
pp (0.6.2)
|
||||
pg (1.6.3)
|
||||
pg (1.6.3-aarch64-linux)
|
||||
pg (1.6.3-aarch64-linux-musl)
|
||||
pg (1.6.3-arm64-darwin)
|
||||
pg (1.6.3-x86_64-linux)
|
||||
pg (1.6.3-x86_64-linux-musl)
|
||||
pp (0.6.3)
|
||||
prettyprint
|
||||
premailer (1.27.0)
|
||||
addressable
|
||||
@@ -393,23 +388,24 @@ GEM
|
||||
actionmailer (>= 3)
|
||||
net-smtp
|
||||
premailer (~> 1.7, >= 1.7.9)
|
||||
pretender (0.5.0)
|
||||
actionpack (>= 6.1)
|
||||
pretender (0.6.0)
|
||||
actionpack (>= 7.1)
|
||||
prettyprint (0.2.0)
|
||||
prism (1.5.1)
|
||||
pry (0.15.0)
|
||||
prism (1.8.0)
|
||||
pry (0.16.0)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
reline (>= 0.6.0)
|
||||
pry-rails (0.3.11)
|
||||
pry (>= 0.13.0)
|
||||
psych (5.2.6)
|
||||
psych (5.3.1)
|
||||
date
|
||||
stringio
|
||||
public_suffix (6.0.1)
|
||||
puma (6.5.0)
|
||||
public_suffix (7.0.2)
|
||||
puma (7.2.0)
|
||||
nio4r (~> 2.0)
|
||||
racc (1.8.1)
|
||||
rack (3.2.3)
|
||||
rack (3.2.4)
|
||||
rack-proxy (0.7.7)
|
||||
rack
|
||||
rack-session (2.1.1)
|
||||
@@ -417,22 +413,22 @@ GEM
|
||||
rack (>= 3.0.0)
|
||||
rack-test (2.2.0)
|
||||
rack (>= 1.3)
|
||||
rackup (2.2.1)
|
||||
rackup (2.3.1)
|
||||
rack (>= 3)
|
||||
rails (8.0.2.1)
|
||||
actioncable (= 8.0.2.1)
|
||||
actionmailbox (= 8.0.2.1)
|
||||
actionmailer (= 8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
actiontext (= 8.0.2.1)
|
||||
actionview (= 8.0.2.1)
|
||||
activejob (= 8.0.2.1)
|
||||
activemodel (= 8.0.2.1)
|
||||
activerecord (= 8.0.2.1)
|
||||
activestorage (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
rails (8.1.2)
|
||||
actioncable (= 8.1.2)
|
||||
actionmailbox (= 8.1.2)
|
||||
actionmailer (= 8.1.2)
|
||||
actionpack (= 8.1.2)
|
||||
actiontext (= 8.1.2)
|
||||
actionview (= 8.1.2)
|
||||
activejob (= 8.1.2)
|
||||
activemodel (= 8.1.2)
|
||||
activerecord (= 8.1.2)
|
||||
activestorage (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 8.0.2.1)
|
||||
railties (= 8.1.2)
|
||||
rails-dom-testing (2.3.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
@@ -440,30 +436,32 @@ GEM
|
||||
rails-html-sanitizer (1.6.2)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||
rails-i18n (8.0.1)
|
||||
rails-i18n (8.1.0)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 8.0.0, < 9)
|
||||
rails_autolink (1.1.8)
|
||||
actionview (> 3.1)
|
||||
activesupport (> 3.1)
|
||||
railties (> 3.1)
|
||||
railties (8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
railties (8.1.2)
|
||||
actionpack (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
irb (~> 1.13)
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
tsort (>= 0.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.3.0)
|
||||
rdoc (6.14.2)
|
||||
rake (13.3.1)
|
||||
rdoc (7.1.0)
|
||||
erb
|
||||
psych (>= 4.0.0)
|
||||
redis-client (0.23.0)
|
||||
tsort
|
||||
redis-client (0.26.4)
|
||||
connection_pool
|
||||
regexp_parser (2.11.3)
|
||||
reline (0.6.2)
|
||||
reline (0.6.3)
|
||||
io-console (~> 0.5)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
@@ -471,35 +469,35 @@ GEM
|
||||
uber (< 0.2.0)
|
||||
request_store (1.7.0)
|
||||
rack (>= 1.4)
|
||||
responders (3.1.1)
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
responders (3.2.0)
|
||||
actionpack (>= 7.0)
|
||||
railties (>= 7.0)
|
||||
retriable (3.1.2)
|
||||
rexml (3.4.4)
|
||||
rotp (6.3.0)
|
||||
rouge (4.5.2)
|
||||
rqrcode (2.2.0)
|
||||
rouge (4.7.0)
|
||||
rqrcode (3.2.0)
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 1.0)
|
||||
rqrcode_core (1.2.0)
|
||||
rspec-core (3.13.2)
|
||||
rqrcode_core (~> 2.0)
|
||||
rqrcode_core (2.1.0)
|
||||
rspec-core (3.13.6)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-expectations (3.13.3)
|
||||
rspec-expectations (3.13.5)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-mocks (3.13.2)
|
||||
rspec-mocks (3.13.7)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-rails (7.1.0)
|
||||
actionpack (>= 7.0)
|
||||
activesupport (>= 7.0)
|
||||
railties (>= 7.0)
|
||||
rspec-rails (8.0.2)
|
||||
actionpack (>= 7.2)
|
||||
activesupport (>= 7.2)
|
||||
railties (>= 7.2)
|
||||
rspec-core (~> 3.13)
|
||||
rspec-expectations (~> 3.13)
|
||||
rspec-mocks (~> 3.13)
|
||||
rspec-support (~> 3.13)
|
||||
rspec-support (3.13.2)
|
||||
rubocop (1.81.1)
|
||||
rspec-support (3.13.6)
|
||||
rubocop (1.82.1)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
@@ -507,87 +505,90 @@ GEM
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.47.1, < 2.0)
|
||||
rubocop-ast (>= 1.48.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.47.1)
|
||||
rubocop-ast (1.49.0)
|
||||
parser (>= 3.3.7.2)
|
||||
prism (~> 1.4)
|
||||
rubocop-performance (1.23.0)
|
||||
rubocop (>= 1.48.1, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
rubocop-rails (2.27.0)
|
||||
prism (~> 1.7)
|
||||
rubocop-performance (1.26.1)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (>= 1.75.0, < 2.0)
|
||||
rubocop-ast (>= 1.47.1, < 2.0)
|
||||
rubocop-rails (2.34.3)
|
||||
activesupport (>= 4.2.0)
|
||||
lint_roller (~> 1.1)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.52.0, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
rubocop-rspec (3.3.0)
|
||||
rubocop (~> 1.61)
|
||||
rubocop (>= 1.75.0, < 2.0)
|
||||
rubocop-ast (>= 1.44.0, < 2.0)
|
||||
rubocop-rspec (3.9.0)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (~> 1.81)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-vips (2.2.2)
|
||||
ruby-vips (2.3.0)
|
||||
ffi (~> 1.12)
|
||||
logger
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyXL (3.4.33)
|
||||
rubyXL (3.4.35)
|
||||
nokogiri (>= 1.10.8)
|
||||
rubyzip (>= 1.3.0)
|
||||
rubyzip (2.3.2)
|
||||
rubyzip (>= 3.2.2)
|
||||
rubyzip (3.2.2)
|
||||
securerandom (0.4.1)
|
||||
semantic_range (3.1.0)
|
||||
shakapacker (8.0.2)
|
||||
shakapacker (9.5.0)
|
||||
activesupport (>= 5.2)
|
||||
package_json
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 5.2)
|
||||
semantic_range (>= 2.3.0)
|
||||
sidekiq (7.3.7)
|
||||
connection_pool (>= 2.3.0)
|
||||
logger
|
||||
rack (>= 2.2.4)
|
||||
redis-client (>= 0.22.2)
|
||||
signet (0.19.0)
|
||||
sidekiq (8.1.0)
|
||||
connection_pool (>= 3.0.0)
|
||||
json (>= 2.16.0)
|
||||
logger (>= 1.7.0)
|
||||
rack (>= 3.2.0)
|
||||
redis-client (>= 0.26.0)
|
||||
signet (0.21.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
jwt (>= 1.5, < 4.0)
|
||||
multi_json (~> 1.10)
|
||||
simplecov (0.22.0)
|
||||
docile (~> 1.1)
|
||||
simplecov-html (~> 0.11)
|
||||
simplecov_json_formatter (~> 0.1)
|
||||
simplecov-html (0.13.1)
|
||||
simplecov-html (0.13.2)
|
||||
simplecov_json_formatter (0.1.4)
|
||||
smart_properties (1.17.0)
|
||||
sqlite3 (2.5.0)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
sqlite3 (2.5.0-aarch64-linux-gnu)
|
||||
sqlite3 (2.5.0-aarch64-linux-musl)
|
||||
sqlite3 (2.5.0-arm64-darwin)
|
||||
sqlite3 (2.5.0-x86_64-linux-gnu)
|
||||
sqlite3 (2.5.0-x86_64-linux-musl)
|
||||
stringio (3.1.7)
|
||||
strip_attributes (1.14.1)
|
||||
sqlite3 (2.9.0-aarch64-linux-gnu)
|
||||
sqlite3 (2.9.0-aarch64-linux-musl)
|
||||
sqlite3 (2.9.0-arm64-darwin)
|
||||
sqlite3 (2.9.0-x86_64-linux-gnu)
|
||||
sqlite3 (2.9.0-x86_64-linux-musl)
|
||||
stringio (3.2.0)
|
||||
strip_attributes (2.0.1)
|
||||
activemodel (>= 3.0, < 9.0)
|
||||
strscan (3.1.5)
|
||||
thor (1.4.0)
|
||||
timeout (0.4.3)
|
||||
strscan (3.1.7)
|
||||
thor (1.5.0)
|
||||
timeout (0.6.0)
|
||||
trailblazer-option (0.1.2)
|
||||
turbo-rails (2.0.11)
|
||||
actionpack (>= 6.0.0)
|
||||
railties (>= 6.0.0)
|
||||
twitter_cldr (6.12.1)
|
||||
tsort (0.2.0)
|
||||
turbo-rails (2.0.21)
|
||||
actionpack (>= 7.1.0)
|
||||
railties (>= 7.1.0)
|
||||
twitter_cldr (6.14.0)
|
||||
base64
|
||||
camertron-eprun
|
||||
cldr-plurals-runtime-rb (~> 1.1)
|
||||
tzinfo
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
tzinfo-data (1.2024.2)
|
||||
tzinfo-data (1.2025.3)
|
||||
tzinfo (>= 1.0.0)
|
||||
uber (0.1.0)
|
||||
unicode-display_width (3.2.0)
|
||||
unicode-emoji (~> 4.1)
|
||||
unicode-emoji (4.1.0)
|
||||
uniform_notifier (1.16.0)
|
||||
uri (1.0.3)
|
||||
unicode-emoji (4.2.0)
|
||||
uniform_notifier (1.18.0)
|
||||
uri (1.1.1)
|
||||
useragent (0.16.11)
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
@@ -596,24 +597,24 @@ GEM
|
||||
activemodel (>= 6.0.0)
|
||||
bindex (>= 0.4.0)
|
||||
railties (>= 6.0.0)
|
||||
webmock (3.24.0)
|
||||
webmock (3.26.1)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webrick (1.9.1)
|
||||
webrick (1.9.2)
|
||||
websocket-driver (0.8.0)
|
||||
base64
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
zeitwerk (2.7.3)
|
||||
yaml (0.4.0)
|
||||
zeitwerk (2.7.4)
|
||||
|
||||
PLATFORMS
|
||||
aarch64-linux
|
||||
aarch64-linux-musl
|
||||
arm64-darwin
|
||||
ruby
|
||||
x86_64-linux
|
||||
x86_64-linux-musl
|
||||
|
||||
@@ -622,7 +623,7 @@ DEPENDENCIES
|
||||
arabic-letter-connector
|
||||
aws-sdk-s3
|
||||
aws-sdk-secretsmanager
|
||||
azure-storage-blob
|
||||
azure-blob
|
||||
better_html
|
||||
bootsnap
|
||||
brakeman
|
||||
@@ -649,8 +650,7 @@ DEPENDENCIES
|
||||
jwt
|
||||
letter_opener_web
|
||||
lograge
|
||||
mysql2
|
||||
numo-narray
|
||||
numo-narray-alt
|
||||
oj
|
||||
onnxruntime
|
||||
pagy
|
||||
@@ -678,6 +678,7 @@ DEPENDENCIES
|
||||
simplecov
|
||||
sqlite3
|
||||
strip_attributes
|
||||
trilogy!
|
||||
turbo-rails
|
||||
twitter_cldr
|
||||
tzinfo-data
|
||||
@@ -685,7 +686,7 @@ DEPENDENCIES
|
||||
webmock
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.4.2p28
|
||||
ruby 4.0.1
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.3
|
||||
4.0.3
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<h1 align="center" style="border-bottom: none">
|
||||
<div>
|
||||
<a href="https://www.docuseal.com">
|
||||
<img alt="DocuSeal" src="https://github.com/docusealco/docuseal/assets/5418788/c12cd051-81cd-4402-bc3a-92f2cfdc1b06" width="80" />
|
||||
<img alt="DocuSeal" src="https://github.com/user-attachments/assets/38b45682-ffa4-4919-abde-d2d422325c44" width="80" />
|
||||
<br>
|
||||
</a>
|
||||
DocuSeal
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AccountCustomFieldsController < ApplicationController
|
||||
before_action :load_account_config, only: :create
|
||||
|
||||
def create
|
||||
authorize!(:create, Template)
|
||||
|
||||
@account_config.update!(account_config_params)
|
||||
|
||||
render json: @account_config.value
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_account_config
|
||||
@account_config =
|
||||
AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY)
|
||||
end
|
||||
|
||||
def account_config_params
|
||||
params.permit(
|
||||
value: [[:uuid, :name, :type,
|
||||
:required, :readonly, :default_value,
|
||||
:title, :description,
|
||||
{ preferences: {},
|
||||
default_value: [],
|
||||
options: [%i[value uuid]],
|
||||
validation: %i[message pattern min max step],
|
||||
areas: [%i[x y w h cell_w option_uuid]] }]]
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -3,7 +3,7 @@
|
||||
module Api
|
||||
class ApiBaseController < ActionController::API
|
||||
include ActiveStorage::SetCurrent
|
||||
include Pagy::Backend
|
||||
include Pagy::Method
|
||||
|
||||
DEFAULT_LIMIT = 10
|
||||
MAX_LIMIT = 100
|
||||
|
||||
@@ -5,34 +5,17 @@ module Api
|
||||
load_and_authorize_resource :submission
|
||||
|
||||
def index
|
||||
is_merge = params[:merge] == 'true' &&
|
||||
(@submission.schema_documents || @submission.template.schema_documents).size > 1
|
||||
|
||||
documents =
|
||||
if @submission.submitters.all?(&:completed_at?)
|
||||
last_submitter = @submission.submitters.max_by(&:completed_at)
|
||||
|
||||
if last_submitter.documents_attachments.blank?
|
||||
last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(last_submitter)
|
||||
end
|
||||
|
||||
last_submitter.documents_attachments
|
||||
build_completed_documents(@submission, merge: is_merge)
|
||||
else
|
||||
values_hash = Submissions::GeneratePreviewAttachments.build_values_hash(@submission)
|
||||
|
||||
if @submission.preview_documents.present? &&
|
||||
@submission.preview_documents.all? { |s| s.metadata['values_hash'] == values_hash }
|
||||
@submission.preview_documents
|
||||
else
|
||||
ApplicationRecord.no_touching do
|
||||
@submission.preview_documents.each(&:destroy)
|
||||
end
|
||||
|
||||
Submissions::GeneratePreviewAttachments.call(@submission, values_hash:)
|
||||
end
|
||||
build_preview_documents(@submission, merge: is_merge)
|
||||
end
|
||||
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: documents,
|
||||
associations: [:blob]
|
||||
).call
|
||||
ActiveRecord::Associations::Preloader.new(records: documents, associations: [:blob]).call
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
@@ -43,5 +26,50 @@ module Api
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_completed_documents(submission, merge: false)
|
||||
last_submitter = submission.submitters.max_by(&:completed_at)
|
||||
|
||||
if merge
|
||||
if submission.merged_document_attachment.blank?
|
||||
submission.merged_document_attachment =
|
||||
Submissions::GenerateCombinedAttachment.call(last_submitter, with_audit: false)
|
||||
end
|
||||
|
||||
[submission.merged_document_attachment]
|
||||
else
|
||||
if last_submitter.documents_attachments.blank?
|
||||
last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(last_submitter)
|
||||
end
|
||||
|
||||
last_submitter.documents_attachments
|
||||
end
|
||||
end
|
||||
|
||||
def build_preview_documents(submission, merge: false)
|
||||
values_hash = Submissions::GeneratePreviewAttachments.build_values_hash(submission)
|
||||
|
||||
if merge
|
||||
if submission.preview_merged_document_attachment.present? &&
|
||||
submission.preview_merged_document_attachment.metadata['values_hash'] == values_hash
|
||||
[submission.preview_merged_document_attachment]
|
||||
else
|
||||
ApplicationRecord.no_touching { submission.preview_merged_document_attachment&.destroy }
|
||||
|
||||
Submissions::GeneratePreviewAttachments.call(submission, values_hash:, merge: true)
|
||||
end
|
||||
elsif submission.preview_documents.present? &&
|
||||
submission.preview_documents.all? { |s| s.metadata['values_hash'] == values_hash }
|
||||
submission.preview_documents
|
||||
else
|
||||
ApplicationRecord.no_touching do
|
||||
submission.preview_documents.each(&:destroy)
|
||||
end
|
||||
|
||||
Submissions::GeneratePreviewAttachments.call(submission, values_hash:)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -188,7 +188,7 @@ module Api
|
||||
message: %i[subject body],
|
||||
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
|
||||
:require_phone_2fa, :require_email_2fa, :order, :invite_by,
|
||||
:require_phone_2fa, :require_email_2fa, :order, :index, :invite_by,
|
||||
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
|
||||
fields: [:name, :uuid, :default_value, :value, :title, :description,
|
||||
:readonly, :required, :validation_pattern, :invalid_message,
|
||||
|
||||
@@ -4,7 +4,7 @@ class ApplicationController < ActionController::Base
|
||||
BROWSER_LOCALE_REGEXP = /\A\w{2}(?:-\w{2})?/
|
||||
|
||||
include ActiveStorage::SetCurrent
|
||||
include Pagy::Backend
|
||||
include Pagy::Method
|
||||
|
||||
check_authorization unless: :devise_controller?
|
||||
|
||||
@@ -17,12 +17,13 @@ class ApplicationController < ActionController::Base
|
||||
|
||||
helper_method :button_title,
|
||||
:current_account,
|
||||
:true_ability,
|
||||
:form_link_host,
|
||||
:svg_icon
|
||||
|
||||
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
|
||||
|
||||
rescue_from Pagy::OverflowError do
|
||||
rescue_from Pagy::RangeError do
|
||||
redirect_to request.path
|
||||
end
|
||||
|
||||
@@ -59,7 +60,7 @@ class ApplicationController < ActionController::Base
|
||||
|
||||
def pagy_auto(collection, **keyword_args)
|
||||
if current_ability.can?(:manage, :countless)
|
||||
pagy_countless(collection, **keyword_args)
|
||||
pagy(:countless, collection, **keyword_args)
|
||||
else
|
||||
pagy(collection, **keyword_args)
|
||||
end
|
||||
@@ -102,6 +103,10 @@ class ApplicationController < ActionController::Base
|
||||
current_user&.account
|
||||
end
|
||||
|
||||
def true_ability
|
||||
@true_ability ||= Ability.new(true_user)
|
||||
end
|
||||
|
||||
def maybe_redirect_to_setup
|
||||
redirect_to setup_index_path unless User.exists?
|
||||
end
|
||||
|
||||
@@ -17,7 +17,7 @@ class SubmissionsController < ApplicationController
|
||||
'number' => 'square_number_1', 'image' => 'photo', 'initials' => 'letter_case_upper',
|
||||
'file' => 'paperclip', 'select' => 'select', 'checkbox' => 'checkbox', 'radio' => 'circle_dot',
|
||||
'stamp' => 'rubber_stamp', 'cells' => 'columns_3', 'multiple' => 'checks', 'phone' => 'phone_check',
|
||||
'payment' => 'credit_card', 'verification' => 'id'
|
||||
'payment' => 'credit_card', 'verification' => 'id', 'kba' => 'user_scan'
|
||||
}.freeze
|
||||
|
||||
def show
|
||||
|
||||
@@ -27,20 +27,18 @@ class SubmissionsDownloadController < ApplicationController
|
||||
|
||||
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||
|
||||
if last_submitter.completed_at < TTL.ago && !signature_valid && !current_user_submitter?(last_submitter)
|
||||
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
||||
if !signature_valid && !current_user_submitter?(last_submitter)
|
||||
return head :not_found unless Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||
|
||||
return head :not_found
|
||||
if last_submitter.completed_at < TTL.ago
|
||||
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
||||
|
||||
return head :not_found
|
||||
end
|
||||
end
|
||||
|
||||
if params[:combined] == 'true'
|
||||
url = build_combined_url(@submitter)
|
||||
|
||||
if url
|
||||
render json: [url]
|
||||
else
|
||||
head :not_found
|
||||
end
|
||||
respond_with_combined(last_submitter)
|
||||
else
|
||||
render json: build_urls(last_submitter)
|
||||
end
|
||||
@@ -48,8 +46,18 @@ class SubmissionsDownloadController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def respond_with_combined(submitter)
|
||||
url = build_combined_url(submitter)
|
||||
|
||||
if url
|
||||
render json: [url]
|
||||
else
|
||||
head :not_found
|
||||
end
|
||||
end
|
||||
|
||||
def current_user_submitter?(submitter)
|
||||
current_user && current_user.account.submitters.exists?(id: submitter.id)
|
||||
current_user && current_ability.can?(:read, submitter)
|
||||
end
|
||||
|
||||
def build_urls(submitter)
|
||||
|
||||
@@ -9,15 +9,15 @@ class SubmitFormController < ApplicationController
|
||||
|
||||
before_action :load_submitter, only: %i[show update completed]
|
||||
before_action :maybe_render_locked_page, only: :show
|
||||
before_action :maybe_require_link_2fa, only: %i[show update]
|
||||
before_action :maybe_require_link_2fa, only: %i[show]
|
||||
|
||||
CONFIG_KEYS = [].freeze
|
||||
|
||||
def show
|
||||
submission = @submitter.submission
|
||||
|
||||
return render :email_2fa unless Submitters::AuthorizedForForm.pass_email_2fa?(@submitter, request)
|
||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||
return render :email_2fa if require_email_2fa?(@submitter)
|
||||
|
||||
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
|
||||
|
||||
@@ -48,7 +48,7 @@ class SubmitFormController < ApplicationController
|
||||
end
|
||||
|
||||
def update
|
||||
if require_email_2fa?(@submitter)
|
||||
unless Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||
return render json: { error: I18n.t('verification_required_refresh_the_page_and_pass_2fa') },
|
||||
status: :unprocessable_content
|
||||
end
|
||||
@@ -84,7 +84,9 @@ class SubmitFormController < ApplicationController
|
||||
def completed
|
||||
raise ActionController::RoutingError, I18n.t('not_found') if @submitter.account.archived_at?
|
||||
|
||||
redirect_to submit_form_path(params[:submit_form_slug]) if require_email_2fa?(@submitter)
|
||||
return if Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||
|
||||
redirect_to submit_form_path(params[:submit_form_slug])
|
||||
end
|
||||
|
||||
def success; end
|
||||
@@ -92,10 +94,7 @@ class SubmitFormController < ApplicationController
|
||||
private
|
||||
|
||||
def maybe_require_link_2fa
|
||||
return if @submitter.submission.source != 'link'
|
||||
return unless @submitter.submission.template&.preferences&.dig('shared_link_2fa') == true
|
||||
return if cookies.encrypted[:email_2fa_slug] == @submitter.slug
|
||||
return if @submitter.email == current_user&.email && current_user&.account_id == @submitter.account_id
|
||||
return if Submitters::AuthorizedForForm.pass_link_2fa?(@submitter, current_user, request)
|
||||
|
||||
redirect_to start_form_path(@submitter.submission.template.slug)
|
||||
end
|
||||
@@ -117,12 +116,4 @@ class SubmitFormController < ApplicationController
|
||||
ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments)
|
||||
.preload(:blob).index_by(&:uuid)
|
||||
end
|
||||
|
||||
def require_email_2fa?(submitter)
|
||||
return false if submitter.submission.template&.preferences&.dig('require_email_2fa') != true &&
|
||||
submitter.preferences['require_email_2fa'] != true
|
||||
return false if cookies.encrypted[:email_2fa_slug] == submitter.slug
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,7 +11,9 @@ class SubmitFormDeclineController < ApplicationController
|
||||
submitter.completed_at? ||
|
||||
submitter.submission.archived_at? ||
|
||||
submitter.submission.expired? ||
|
||||
submitter.submission.template&.archived_at?
|
||||
submitter.submission.template&.archived_at? ||
|
||||
!Submitters::AuthorizedForForm.call(submitter, current_user,
|
||||
request)
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
submitter.update!(declined_at: Time.current)
|
||||
|
||||
@@ -17,7 +17,8 @@ class SubmitFormDownloadController < ApplicationController
|
||||
@submitter.submission.template&.archived_at? ||
|
||||
AccountConfig.exists?(account_id: @submitter.account_id,
|
||||
key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
|
||||
value: false)
|
||||
value: false) ||
|
||||
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||
|
||||
last_completed_submitter = @submitter.submission.submitters
|
||||
.where.not(id: @submitter.id)
|
||||
|
||||
@@ -12,7 +12,8 @@ class SubmitFormDrawSignatureController < ApplicationController
|
||||
|
||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||
|
||||
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at?
|
||||
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at? ||
|
||||
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||
return redirect_to submit_form_path(@submitter.slug)
|
||||
end
|
||||
|
||||
|
||||
@@ -45,7 +45,8 @@ class SubmitFormInviteController < ApplicationController
|
||||
!submitter.completed_at? &&
|
||||
!submitter.submission.archived_at? &&
|
||||
!submitter.submission.expired? &&
|
||||
!submitter.submission.template&.archived_at?
|
||||
!submitter.submission.template&.archived_at? &&
|
||||
Submitters::AuthorizedForForm.call(submitter, current_user, request)
|
||||
end
|
||||
|
||||
def filter_invite_submitters(submitter, key = 'invite_by_uuid')
|
||||
|
||||
@@ -7,10 +7,12 @@ class SubmitFormValuesController < ApplicationController
|
||||
def index
|
||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
|
||||
return render json: {} if submitter.completed_at? || submitter.declined_at?
|
||||
return render json: {} if submitter.submission.template&.archived_at? ||
|
||||
return render json: {} if submitter.completed_at? ||
|
||||
submitter.declined_at? ||
|
||||
submitter.submission.template&.archived_at? ||
|
||||
submitter.submission.archived_at? ||
|
||||
submitter.submission.expired?
|
||||
submitter.submission.expired? ||
|
||||
!Submitters::AuthorizedForForm.call(submitter, current_user, request)
|
||||
|
||||
value = submitter.values[params['field_uuid']]
|
||||
attachment = submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present?
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TemplatesCloneController < ApplicationController
|
||||
load_and_authorize_resource :template, instance_name: :base_template
|
||||
|
||||
def new
|
||||
authorize!(:create, Template)
|
||||
|
||||
@template = Template.new(name: "#{@base_template.name} (#{I18n.t('clone')})")
|
||||
end
|
||||
|
||||
def create
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [@base_template],
|
||||
associations: [schema_documents: :preview_images_attachments]
|
||||
).call
|
||||
|
||||
@template = Templates::Clone.call(@base_template, author: current_user,
|
||||
name: params.dig(:template, :name),
|
||||
folder_name: params[:folder_name])
|
||||
|
||||
authorize!(:create, @template)
|
||||
|
||||
if params[:account_id].present? && true_ability.authorize!(:manage, Account.find(params[:account_id]))
|
||||
@template.account_id = params[:account_id]
|
||||
@template.author = true_user if true_user.account_id == @template.account_id
|
||||
@template.folder = @template.account.default_template_folder if @template.account_id != current_account.id
|
||||
else
|
||||
@template.account = current_account
|
||||
end
|
||||
|
||||
Templates.maybe_assign_access(@template)
|
||||
|
||||
if @template.save
|
||||
Templates::CloneAttachments.call(template: @template, original_template: @base_template)
|
||||
|
||||
SearchEntries.enqueue_reindex(@template)
|
||||
|
||||
WebhookUrls.enqueue_events(@template, 'template.created')
|
||||
|
||||
maybe_redirect_to_template(@template)
|
||||
else
|
||||
render turbo_stream: turbo_stream.replace(:modal, partial: 'templates_clone/form'), status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def maybe_redirect_to_template(template)
|
||||
if template.account == current_account
|
||||
redirect_to(edit_template_path(template))
|
||||
else
|
||||
redirect_back(fallback_location: root_path, notice: I18n.t('template_has_been_cloned'))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3,8 +3,6 @@
|
||||
class TemplatesController < ApplicationController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
before_action :load_base_template, only: %i[new create]
|
||||
|
||||
def show
|
||||
submissions = @template.submissions.accessible_by(current_ability)
|
||||
submissions = submissions.active if @template.archived_at.blank?
|
||||
@@ -26,9 +24,7 @@ class TemplatesController < ApplicationController
|
||||
redirect_to root_path
|
||||
end
|
||||
|
||||
def new
|
||||
@template.name = "#{@base_template.name} (#{I18n.t('clone')})" if @base_template
|
||||
end
|
||||
def new; end
|
||||
|
||||
def edit
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
@@ -48,37 +44,18 @@ class TemplatesController < ApplicationController
|
||||
end
|
||||
|
||||
def create
|
||||
if @base_template
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [@base_template],
|
||||
associations: [schema_documents: :preview_images_attachments]
|
||||
).call
|
||||
|
||||
@template = Templates::Clone.call(@base_template, author: current_user,
|
||||
name: params.dig(:template, :name),
|
||||
folder_name: params[:folder_name])
|
||||
else
|
||||
@template.author = current_user
|
||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
||||
end
|
||||
|
||||
if params[:account_id].present? && authorized_clone_account_id?(params[:account_id])
|
||||
@template.account_id = params[:account_id]
|
||||
@template.folder = @template.account.default_template_folder if @template.account_id != current_account.id
|
||||
else
|
||||
@template.account = current_account
|
||||
end
|
||||
@template.author = current_user
|
||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
||||
@template.account = current_account
|
||||
|
||||
Templates.maybe_assign_access(@template)
|
||||
|
||||
if @template.save
|
||||
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
||||
|
||||
SearchEntries.enqueue_reindex(@template)
|
||||
|
||||
WebhookUrls.enqueue_events(@template, 'template.created')
|
||||
|
||||
maybe_redirect_to_template(@template)
|
||||
redirect_to(edit_template_path(@template))
|
||||
else
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_content
|
||||
end
|
||||
@@ -132,23 +109,4 @@ class TemplatesController < ApplicationController
|
||||
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
||||
)
|
||||
end
|
||||
|
||||
def authorized_clone_account_id?(account_id)
|
||||
true_user.account_id.to_s == account_id.to_s ||
|
||||
true_user.account.linked_accounts.accessible_by(current_ability).exists?(id: account_id)
|
||||
end
|
||||
|
||||
def maybe_redirect_to_template(template)
|
||||
if template.account == current_account
|
||||
redirect_to(edit_template_path(@template))
|
||||
else
|
||||
redirect_back(fallback_location: root_path, notice: I18n.t('template_has_been_cloned'))
|
||||
end
|
||||
end
|
||||
|
||||
def load_base_template
|
||||
return if params[:base_template_id].blank?
|
||||
|
||||
@base_template = Template.accessible_by(current_ability).find_by(id: params[:base_template_id])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,11 +11,14 @@ class TemplatesDetectFieldsController < ApplicationController
|
||||
sse = SSE.new(response.stream)
|
||||
|
||||
documents = @template.schema_documents.preload(:blob)
|
||||
documents = documents.where(uuid: params[:attachment_uuid]) if params[:attachment_uuid].present?
|
||||
|
||||
page_number = params[:page].presence&.to_i
|
||||
|
||||
documents.each do |document|
|
||||
io = StringIO.new(document.download)
|
||||
|
||||
Templates::DetectFields.call(io, attachment: document) do |(attachment_uuid, page, fields)|
|
||||
Templates::DetectFields.call(io, attachment: document, page_number:) do |(attachment_uuid, page, fields)|
|
||||
sse.write({ attachment_uuid:, page:, fields: })
|
||||
end
|
||||
end
|
||||
|
||||
@@ -56,7 +56,7 @@ class TemplatesUploadsController < ApplicationController
|
||||
def create_file_params_from_url
|
||||
tempfile = Tempfile.new
|
||||
tempfile.binmode
|
||||
tempfile.write(DownloadUtils.call(params[:url]).body)
|
||||
tempfile.write(DownloadUtils.call(params[:url], validate: true).body)
|
||||
tempfile.rewind
|
||||
|
||||
filename = URI.decode_www_form_component(params[:filename]) if params[:filename].present?
|
||||
|
||||
@@ -32,7 +32,7 @@ class TimestampServerController < ApplicationController
|
||||
uri = Addressable::URI.parse(url)
|
||||
|
||||
conn = Faraday.new(uri.origin) do |c|
|
||||
c.basic_auth(uri.user, uri.password) if uri.password.present?
|
||||
c.request :authorization, :basic, uri.user, uri.password if uri.password.present?
|
||||
end
|
||||
|
||||
response = conn.post(uri.path, req.to_der,
|
||||
|
||||
@@ -15,7 +15,7 @@ class WebhookSettingsController < ApplicationController
|
||||
|
||||
@webhook_events = @webhook_events.where(status: params[:status]) if %w[success error].include?(params[:status])
|
||||
|
||||
@pagy, @webhook_events = pagy_countless(@webhook_events.order(id: :desc))
|
||||
@pagy, @webhook_events = pagy(:countless, @webhook_events.order(id: :desc))
|
||||
|
||||
render :show
|
||||
end
|
||||
@@ -26,7 +26,7 @@ class WebhookSettingsController < ApplicationController
|
||||
|
||||
@webhook_events = @webhook_events.where(status: params[:status]) if %w[success error].include?(params[:status])
|
||||
|
||||
@pagy, @webhook_events = pagy_countless(@webhook_events.order(id: :desc))
|
||||
@pagy, @webhook_events = pagy(:countless, @webhook_events.order(id: :desc))
|
||||
end
|
||||
|
||||
def new; end
|
||||
|
||||
@@ -155,14 +155,17 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
||||
|
||||
this.app = createApp(TemplateBuilder, {
|
||||
template: reactive(JSON.parse(this.dataset.template)),
|
||||
customFields: reactive(JSON.parse(this.dataset.customFields || '[]')),
|
||||
backgroundColor: '#faf7f5',
|
||||
locale: this.dataset.locale,
|
||||
withPhone: this.dataset.withPhone === 'true',
|
||||
withVerification: ['true', 'false'].includes(this.dataset.withVerification) ? this.dataset.withVerification === 'true' : null,
|
||||
withKba: ['true', 'false'].includes(this.dataset.withKba) ? this.dataset.withKba === 'true' : null,
|
||||
withLogo: this.dataset.withLogo !== 'false',
|
||||
withFieldsDetection: this.dataset.withFieldsDetection === 'true',
|
||||
editable: this.dataset.editable !== 'false',
|
||||
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
||||
withCustomFields: true,
|
||||
withPayment: this.dataset.withPayment === 'true',
|
||||
isPaymentConnected: this.dataset.isPaymentConnected === 'true',
|
||||
withFormula: this.dataset.withFormula === 'true',
|
||||
|
||||
+51
-6
@@ -4,15 +4,26 @@ import { isValidSignatureCanvas } from './submission_form/validate_signature'
|
||||
|
||||
window.customElements.define('draw-signature', class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
const scale = 3
|
||||
|
||||
this.canvas.width = this.canvas.parentNode.clientWidth * scale
|
||||
this.canvas.height = this.canvas.parentNode.clientHeight * scale
|
||||
|
||||
this.canvas.getContext('2d').scale(scale, scale)
|
||||
this.setCanvasSize()
|
||||
|
||||
this.pad = new SignaturePad(this.canvas)
|
||||
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (!this.canvas) return
|
||||
|
||||
const { width, height } = this.canvas
|
||||
|
||||
this.setCanvasSize()
|
||||
|
||||
if (this.canvas.width !== width || this.canvas.height !== height) {
|
||||
this.redrawCanvas(width, height)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.resizeObserver.observe(this.canvas.parentNode)
|
||||
|
||||
if (this.dataset.color) {
|
||||
this.pad.penColor = this.dataset.color
|
||||
}
|
||||
@@ -57,6 +68,40 @@ window.customElements.define('draw-signature', class extends HTMLElement {
|
||||
this.updateSubmitButtonVisibility()
|
||||
}
|
||||
|
||||
disconnectedCallback () {
|
||||
if (this.resizeObserver) {
|
||||
this.resizeObserver.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
setCanvasSize () {
|
||||
const scale = 3
|
||||
|
||||
const width = this.canvas.parentNode.clientWidth
|
||||
const height = this.canvas.parentNode.clientHeight
|
||||
|
||||
if (this.canvas.width !== width * scale || this.canvas.height !== height * scale) {
|
||||
this.canvas.width = width * scale
|
||||
this.canvas.height = height * scale
|
||||
|
||||
this.canvas.getContext('2d').scale(scale, scale)
|
||||
}
|
||||
}
|
||||
|
||||
redrawCanvas (oldWidth, oldHeight) {
|
||||
if (this.pad && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0) {
|
||||
const sx = this.canvas.width / oldWidth
|
||||
const sy = this.canvas.height / oldHeight
|
||||
|
||||
const scaledData = this.pad.toData().map((stroke) => ({
|
||||
...stroke,
|
||||
points: stroke.points.map((p) => ({ ...p, x: p.x * sx, y: p.y * sy }))
|
||||
}))
|
||||
|
||||
this.pad.fromData(scaledData)
|
||||
}
|
||||
}
|
||||
|
||||
updateSubmitButtonVisibility () {
|
||||
if (this.pad.isEmpty()) {
|
||||
this.submitButton.style.display = 'none'
|
||||
|
||||
@@ -64,6 +64,22 @@ export default class extends HTMLElement {
|
||||
if (action === 'empty' || action === 'unchecked') return this.isEmpty(actual)
|
||||
if (action === 'not_empty' || action === 'checked') return !this.isEmpty(actual)
|
||||
|
||||
if (['equal', 'not_equal', 'greater_than', 'less_than'].includes(action) && this.sourceEl?.getAttribute('type') === 'number') {
|
||||
if (this.isEmpty(actual) || this.isEmpty(expected)) return false
|
||||
|
||||
const actualNumber = parseFloat(actual)
|
||||
const expectedNumber = parseFloat(expected)
|
||||
|
||||
if (Number.isNaN(actualNumber) || Number.isNaN(expectedNumber)) return false
|
||||
|
||||
if (action === 'equal') return Math.abs(actualNumber - expectedNumber) < Number.EPSILON
|
||||
if (action === 'not_equal') return Math.abs(actualNumber - expectedNumber) > Number.EPSILON
|
||||
if (action === 'greater_than') return actualNumber > expectedNumber
|
||||
if (action === 'less_than') return actualNumber < expectedNumber
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
if (action === 'equal') {
|
||||
const list = Array.isArray(actual) ? actual : [actual]
|
||||
return list.filter((v) => v !== null && v !== undefined).map(String).includes(String(expected))
|
||||
|
||||
@@ -5,17 +5,28 @@ export default targetable(class extends HTMLElement {
|
||||
static [target.static] = ['canvas', 'input', 'clear', 'button']
|
||||
|
||||
async connectedCallback () {
|
||||
const scale = 3
|
||||
|
||||
this.canvas.width = this.canvas.parentNode.clientWidth * scale
|
||||
this.canvas.height = this.canvas.parentNode.clientHeight * scale
|
||||
|
||||
this.canvas.getContext('2d').scale(scale, scale)
|
||||
|
||||
const { default: SignaturePad } = await import('signature_pad')
|
||||
|
||||
this.setCanvasSize()
|
||||
|
||||
this.pad = new SignaturePad(this.canvas)
|
||||
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (!this.canvas) return
|
||||
|
||||
const { width, height } = this.canvas
|
||||
|
||||
this.setCanvasSize()
|
||||
|
||||
if (this.canvas.width !== width || this.canvas.height !== height) {
|
||||
this.redrawCanvas(width, height)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.resizeObserver.observe(this.canvas.parentNode)
|
||||
|
||||
this.clear.addEventListener('click', (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
@@ -47,4 +58,38 @@ export default targetable(class extends HTMLElement {
|
||||
|
||||
this.closest('form').requestSubmit()
|
||||
}
|
||||
|
||||
disconnectedCallback () {
|
||||
if (this.resizeObserver) {
|
||||
this.resizeObserver.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
setCanvasSize () {
|
||||
const scale = 3
|
||||
|
||||
const width = this.canvas.parentNode.clientWidth
|
||||
const height = this.canvas.parentNode.clientWidth / 2.5
|
||||
|
||||
if (this.canvas.width !== width * scale || this.canvas.height !== height * scale) {
|
||||
this.canvas.width = width * scale
|
||||
this.canvas.height = height * scale
|
||||
|
||||
this.canvas.getContext('2d').scale(scale, scale)
|
||||
}
|
||||
}
|
||||
|
||||
redrawCanvas (oldWidth, oldHeight) {
|
||||
if (this.pad && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0) {
|
||||
const sx = this.canvas.width / oldWidth
|
||||
const sy = this.canvas.height / oldHeight
|
||||
|
||||
const scaledData = this.pad.toData().map((stroke) => ({
|
||||
...stroke,
|
||||
points: stroke.points.map((p) => ({ ...p, x: p.x * sx, y: p.y * sy }))
|
||||
}))
|
||||
|
||||
this.pad.fromData(scaledData)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -12,6 +12,6 @@ export default class extends HTMLElement {
|
||||
}
|
||||
|
||||
get button () {
|
||||
return this.querySelector('button')
|
||||
return this.querySelector('button, label')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,11 @@
|
||||
class="object-contain mx-auto"
|
||||
:src="stamp.url"
|
||||
>
|
||||
<img
|
||||
v-else-if="field.type === 'kba' && kba"
|
||||
class="object-contain mx-auto"
|
||||
:src="kba.url"
|
||||
>
|
||||
<div
|
||||
v-else-if="field.type === 'signature' && signature"
|
||||
class="flex justify-between h-full gap-1 overflow-hidden w-full"
|
||||
@@ -177,8 +182,8 @@
|
||||
</div>
|
||||
<div
|
||||
v-else-if="field.type === 'cells'"
|
||||
class="w-full flex items-center"
|
||||
:class="{ 'justify-end': field.preferences?.align === 'right', ...fontClasses }"
|
||||
class="w-full flex"
|
||||
:class="{ 'justify-end': field.preferences?.align === 'right', ...alignClasses, ...fontClasses }"
|
||||
>
|
||||
<div
|
||||
v-for="(char, index) in modelValue"
|
||||
@@ -272,7 +277,7 @@
|
||||
|
||||
<script>
|
||||
import MarkdownContent from './markdown_content'
|
||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconId } from '@tabler/icons-vue'
|
||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconId, IconUserScan } from '@tabler/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'FieldArea',
|
||||
@@ -391,7 +396,8 @@ export default {
|
||||
stamp: this.t('stamp'),
|
||||
payment: this.t('payment'),
|
||||
phone: this.t('phone'),
|
||||
verification: this.t('verify_id')
|
||||
verification: this.t('verify_id'),
|
||||
kba: this.t('kba')
|
||||
}
|
||||
},
|
||||
strikethroughWidth () {
|
||||
@@ -454,7 +460,8 @@ export default {
|
||||
multiple: IconChecks,
|
||||
phone: IconPhoneCheck,
|
||||
payment: IconCreditCard,
|
||||
verification: IconId
|
||||
verification: IconId,
|
||||
kba: IconUserScan
|
||||
}
|
||||
},
|
||||
image () {
|
||||
@@ -471,6 +478,13 @@ export default {
|
||||
return null
|
||||
}
|
||||
},
|
||||
kba () {
|
||||
if (this.field.type === 'kba') {
|
||||
return this.attachmentsIndex[this.modelValue]
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
signature () {
|
||||
if (this.field.type === 'signature') {
|
||||
return this.attachmentsIndex[this.modelValue]
|
||||
|
||||
@@ -161,6 +161,11 @@ export default {
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
fetchOptions: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({})
|
||||
},
|
||||
completedButton: {
|
||||
type: Object,
|
||||
required: false,
|
||||
@@ -214,7 +219,10 @@ export default {
|
||||
download () {
|
||||
this.isDownloading = true
|
||||
|
||||
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`).then(async (response) => {
|
||||
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`, {
|
||||
method: 'GET',
|
||||
...this.fetchOptions
|
||||
}).then(async (response) => {
|
||||
if (response.ok) {
|
||||
const urls = await response.json()
|
||||
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<MarkdownContent :string="field.description" />
|
||||
</div>
|
||||
<AppearsOn :field="field" />
|
||||
<div class="text-center">
|
||||
<div class="text-center flex">
|
||||
<input
|
||||
:id="field.uuid"
|
||||
ref="input"
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
id="expand_form_button"
|
||||
class="btn btn-neutral flex text-white absolute bottom-0 w-full mb-3 expand-form-button text-base"
|
||||
style="width: 96%; margin-left: 2%"
|
||||
@click.prevent="[isFormVisible = true, scrollIntoField(currentField)]"
|
||||
@click.prevent="[isFormVisible = true, $nextTick(() => scrollIntoField(currentField))]"
|
||||
>
|
||||
<template v-if="['initials', 'signature'].includes(currentField.type)">
|
||||
<IconWritingSign stroke-width="1.5" />
|
||||
@@ -395,7 +395,9 @@
|
||||
v-model="values[currentField.uuid]"
|
||||
:reason="values[currentField.preferences?.reason_field_uuid]"
|
||||
:field="currentField"
|
||||
:values="values"
|
||||
:previous-value="previousSignatureValueFor(currentField) || previousSignatureValue"
|
||||
:touch-attachment-uuid="previousSignatureValue"
|
||||
:with-typed-signature="withTypedSignature"
|
||||
:remember-signature="rememberSignature"
|
||||
:attachments-index="attachmentsIndex"
|
||||
@@ -407,6 +409,7 @@
|
||||
:submitter="submitter"
|
||||
:show-field-names="showFieldNames"
|
||||
@update:reason="values[currentField.preferences?.reason_field_uuid] = $event"
|
||||
@touch-attachment="attachmentsIndex[previousSignatureValue] ? attachmentsIndex[previousSignatureValue].created_at = new Date() : null"
|
||||
@attached="attachments.push($event)"
|
||||
@start="scrollIntoField(currentField)"
|
||||
@minimize="minimizeForm"
|
||||
@@ -418,6 +421,7 @@
|
||||
v-model="values[currentField.uuid]"
|
||||
:field="currentField"
|
||||
:dry-run="dryRun"
|
||||
:submitter="submitter"
|
||||
:previous-value="previousInitialsValue"
|
||||
:attachments-index="attachmentsIndex"
|
||||
:show-field-names="showFieldNames"
|
||||
@@ -465,6 +469,17 @@
|
||||
@focus="scrollIntoField(currentField)"
|
||||
@submit="!isSubmitting && submitStep()"
|
||||
/>
|
||||
<KbaStep
|
||||
v-else-if="currentField.type === 'kba'"
|
||||
ref="currentStep"
|
||||
:key="currentField.uuid"
|
||||
:submitter="submitter"
|
||||
:empty-value-required-step="emptyValueRequiredStep"
|
||||
:field="currentField"
|
||||
:submitter-slug="submitterSlug"
|
||||
:values="values"
|
||||
@submit="!isSubmitting && submitStep()"
|
||||
/>
|
||||
<VerificationStep
|
||||
v-else-if="currentField.type === 'verification'"
|
||||
ref="currentStep"
|
||||
@@ -480,7 +495,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="(currentField.type !== 'payment' && currentField.type !== 'verification') || submittedValues[currentField.uuid]"
|
||||
v-if="(currentField.type !== 'payment' && currentField.type !== 'verification' && currentField.type !== 'kba') || submittedValues[currentField.uuid]"
|
||||
:class="currentField.type === 'signature' ? 'mt-2' : 'mt-4 md:mt-6'"
|
||||
>
|
||||
<button
|
||||
@@ -515,6 +530,7 @@
|
||||
v-else-if="isInvite"
|
||||
:submitters="inviteSubmitters"
|
||||
:optional-submitters="optionalInviteSubmitters"
|
||||
:fetch-options="fetchOptions"
|
||||
:submitter-slug="submitterSlug"
|
||||
:authenticity-token="authenticityToken"
|
||||
:url="baseUrl + submitPath + '/invite'"
|
||||
@@ -528,6 +544,7 @@
|
||||
:has-signature-fields="stepFields.some((fields) => fields.some((f) => ['signature', 'initials'].includes(f.type)))"
|
||||
:has-multiple-documents="hasMultipleDocuments"
|
||||
:completed-button="completedRedirectUrl ? {} : completedButton"
|
||||
:fetch-options="fetchOptions"
|
||||
:completed-message="completedRedirectUrl ? {} : completedMessage"
|
||||
:with-send-copy-button="withSendCopyButton && !completedRedirectUrl"
|
||||
:with-download-button="withDownloadButton && !completedRedirectUrl && !dryRun"
|
||||
@@ -540,14 +557,18 @@
|
||||
class="flex justify-center mt-3 sm:mt-4 mb-0 sm:mb-1 select-none"
|
||||
>
|
||||
<div class="flex items-center flex-wrap steps-progress">
|
||||
<a
|
||||
<template
|
||||
v-for="(step, index) in stepFields"
|
||||
:key="step[0].uuid"
|
||||
href="#"
|
||||
class="inline border border-base-300 h-3 w-3 rounded-full mx-1 mt-1"
|
||||
:class="{ 'bg-base-300 steps-progress-current': index === currentStep, 'bg-base-content': (index < currentStep && stepFields[index].every((f) => !f.required || ![null, undefined, ''].includes(values[f.uuid]))) || isCompleted, 'bg-white': index > currentStep }"
|
||||
@click.prevent="isCompleted ? '' : [saveStep(), goToStep(index, true)]"
|
||||
/>
|
||||
>
|
||||
<a
|
||||
v-if="!onlyRequiredFields || step.some((f) => f.required)"
|
||||
href="#"
|
||||
class="inline border border-base-300 h-3 w-3 rounded-full mx-1 mt-1"
|
||||
:class="{ 'bg-base-300 steps-progress-current': index === currentStep, 'bg-base-content': (index < currentStep && stepFields[index].every((f) => !f.required || ![null, undefined, ''].includes(values[f.uuid]))) || isCompleted, 'bg-white': index > currentStep }"
|
||||
@click.prevent="isCompleted ? '' : [saveStep(), goToStep(index, true)]"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -569,6 +590,7 @@ import MultiSelectStep from './multi_select_step'
|
||||
import PhoneStep from './phone_step'
|
||||
import PaymentStep from './payment_step'
|
||||
import VerificationStep from './verification_step'
|
||||
import KbaStep from './kba_step'
|
||||
import TextStep from './text_step'
|
||||
import NumberStep from './number_step'
|
||||
import DateStep from './date_step'
|
||||
@@ -580,6 +602,18 @@ import AppearsOn from './appears_on'
|
||||
import i18n from './i18n'
|
||||
import { sanitizeUrl } from '@braintree/sanitize-url'
|
||||
|
||||
if (typeof URL.canParse !== 'function') {
|
||||
URL.canParse = function (url, base) {
|
||||
try {
|
||||
const parsed = new URL(url, base)
|
||||
|
||||
return !!parsed
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isEmpty = (obj) => {
|
||||
if (obj == null) return true
|
||||
|
||||
@@ -613,6 +647,7 @@ export default {
|
||||
AttachmentStep,
|
||||
InitialsStep,
|
||||
VerificationStep,
|
||||
KbaStep,
|
||||
InviteForm,
|
||||
MultiSelectStep,
|
||||
IconInnerShadowTop,
|
||||
@@ -645,6 +680,11 @@ export default {
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
fetchOptions: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({})
|
||||
},
|
||||
optionalInviteSubmitters: {
|
||||
type: Array,
|
||||
required: false,
|
||||
@@ -665,6 +705,11 @@ export default {
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
onlyRequiredFields: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
requireSigningReason: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -901,10 +946,12 @@ export default {
|
||||
}, {})
|
||||
},
|
||||
attachmentConditionsIndex () {
|
||||
const cache = {}
|
||||
|
||||
return this.schema.reduce((acc, item) => {
|
||||
if (item.conditions?.length) {
|
||||
if (item.conditions.every((c) => this.fieldsUuidIndex[c.field_uuid])) {
|
||||
acc[item.attachment_uuid] = this.checkFieldConditions(item)
|
||||
acc[item.attachment_uuid] = this.checkFieldConditions(item, cache)
|
||||
} else {
|
||||
acc[item.attachment_uuid] = true
|
||||
}
|
||||
@@ -925,7 +972,13 @@ export default {
|
||||
submitButtonText () {
|
||||
if (this.alwaysMinimize) {
|
||||
return this.t('submit')
|
||||
} else if (this.stepFields.length === this.currentStep + 1) {
|
||||
} else if (!this.onlyRequiredFields && this.stepFields.length === this.currentStep + 1) {
|
||||
if (this.currentField.type === 'signature') {
|
||||
return this.t('sign_and_complete')
|
||||
} else {
|
||||
return this.t('complete')
|
||||
}
|
||||
} else if (this.onlyRequiredFields && !this.findNextStep(this.currentStep)) {
|
||||
if (this.currentField.type === 'signature') {
|
||||
return this.t('sign_and_complete')
|
||||
} else {
|
||||
@@ -971,7 +1024,9 @@ export default {
|
||||
},
|
||||
previousInitialsValue () {
|
||||
if (this.reuseSignature !== false) {
|
||||
const initialsField = [...this.fields].reverse().find((field) => field.type === 'initials' && !!this.values[field.uuid])
|
||||
const initialsField = this.fields.findLast
|
||||
? this.fields.findLast((field) => field.type === 'initials' && !!this.values[field.uuid])
|
||||
: [...this.fields].reverse().find((field) => field.type === 'initials' && !!this.values[field.uuid])
|
||||
|
||||
return this.values[initialsField?.uuid]
|
||||
} else {
|
||||
@@ -998,13 +1053,15 @@ export default {
|
||||
return this.readonlyFields.filter((f) => f.conditions?.length)
|
||||
},
|
||||
readonlyFields () {
|
||||
return this.fields.filter((f) => f.readonly && this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f))
|
||||
const cache = {}
|
||||
|
||||
return this.fields.filter((f) => f.readonly && this.checkFieldConditions(f, cache) && this.checkFieldDocumentsConditions(f))
|
||||
},
|
||||
stepFields () {
|
||||
const verificationFields = []
|
||||
|
||||
const sortedFields = this.fields.reduce((acc, f) => {
|
||||
if (f.type === 'verification') {
|
||||
if (f.type === 'verification' || f.type === 'kba') {
|
||||
verificationFields.push(f)
|
||||
} else if (!f.readonly) {
|
||||
acc.push(f)
|
||||
@@ -1053,10 +1110,12 @@ export default {
|
||||
sortedFields.push(verificationFields.pop())
|
||||
}
|
||||
|
||||
const cache = {}
|
||||
|
||||
return sortedFields.reduce((acc, f) => {
|
||||
const prevStep = acc[acc.length - 1]
|
||||
|
||||
if (this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f)) {
|
||||
if (this.checkFieldConditions(f, cache) && this.checkFieldDocumentsConditions(f)) {
|
||||
if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox' && !f.description) {
|
||||
prevStep.push(f)
|
||||
} else {
|
||||
@@ -1068,7 +1127,9 @@ export default {
|
||||
}, [])
|
||||
},
|
||||
formulaFields () {
|
||||
return this.fields.filter((f) => f.preferences?.formula && f.type !== 'payment' && this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f))
|
||||
const cache = {}
|
||||
|
||||
return this.fields.filter((f) => f.preferences?.formula && f.type !== 'payment' && this.checkFieldConditions(f, cache) && this.checkFieldDocumentsConditions(f))
|
||||
},
|
||||
attachmentsIndex () {
|
||||
return this.attachments.reduce((acc, a) => {
|
||||
@@ -1130,15 +1191,17 @@ export default {
|
||||
this.currentStep = Math.max(stepIndex, 0)
|
||||
} else if (this.goToLast) {
|
||||
const requiredEmptyStepIndex = this.stepFields.indexOf(this.stepFields.find((fields) => fields.some((f) => f.required && !this.submittedValues[f.uuid])))
|
||||
const lastFilledStepIndex = this.stepFields.indexOf([...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.submittedValues[f.uuid]))) + 1
|
||||
const lastFilledStepIndex = this.stepFields.indexOf(this.stepFields.findLast
|
||||
? this.stepFields.findLast((fields) => fields.some((f) => !!this.submittedValues[f.uuid]))
|
||||
: [...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.submittedValues[f.uuid]))) + 1
|
||||
|
||||
const indexesList = [this.stepFields.length - 1]
|
||||
|
||||
if (requiredEmptyStepIndex !== -1) {
|
||||
if (requiredEmptyStepIndex !== -1 && (!this.onlyRequiredFields || this.stepFields[requiredEmptyStepIndex].some((f) => f.required))) {
|
||||
indexesList.push(requiredEmptyStepIndex)
|
||||
}
|
||||
|
||||
if (lastFilledStepIndex !== -1) {
|
||||
if (lastFilledStepIndex !== -1 && (!this.onlyRequiredFields || this.stepFields[lastFilledStepIndex].some((f) => f.required))) {
|
||||
indexesList.push(lastFilledStepIndex)
|
||||
}
|
||||
|
||||
@@ -1198,27 +1261,35 @@ export default {
|
||||
return true
|
||||
}
|
||||
},
|
||||
checkFieldConditions (field) {
|
||||
checkFieldConditions (field, cache = {}) {
|
||||
const cacheKey = field.uuid || field.attachment_uuid
|
||||
|
||||
if (cache[cacheKey] !== undefined) {
|
||||
return cache[cacheKey]
|
||||
}
|
||||
|
||||
if (field.conditions?.length) {
|
||||
const result = field.conditions.reduce((acc, cond) => {
|
||||
if (cond.operation === 'or') {
|
||||
acc.push(acc.pop() || this.checkFieldCondition(cond))
|
||||
acc.push(acc.pop() || this.checkFieldCondition(cond, cache))
|
||||
} else {
|
||||
acc.push(this.checkFieldCondition(cond))
|
||||
acc.push(this.checkFieldCondition(cond, cache))
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
return !result.includes(false)
|
||||
cache[cacheKey] = !result.includes(false)
|
||||
} else {
|
||||
return true
|
||||
cache[cacheKey] = true
|
||||
}
|
||||
|
||||
return cache[cacheKey]
|
||||
},
|
||||
checkFieldCondition (condition) {
|
||||
checkFieldCondition (condition, cache = {}) {
|
||||
const field = this.fieldsUuidIndex[condition.field_uuid]
|
||||
|
||||
if (['not_empty', 'checked', 'equal', 'contains'].includes(condition.action) && field && !this.checkFieldConditions(field)) {
|
||||
if (['not_empty', 'checked', 'equal', 'contains', 'greater_than', 'less_than'].includes(condition.action) && field && !this.checkFieldConditions(field, cache)) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1228,20 +1299,50 @@ export default {
|
||||
return isEmpty(this.values[condition.field_uuid] ?? defaultValue)
|
||||
} else if (['not_empty', 'checked'].includes(condition.action)) {
|
||||
return !isEmpty(this.values[condition.field_uuid] ?? defaultValue)
|
||||
} else if (field?.type === 'number' && ['equal', 'not_equal', 'greater_than', 'less_than'].includes(condition.action)) {
|
||||
const value = this.values[condition.field_uuid] ?? defaultValue
|
||||
|
||||
if (isEmpty(value) || isEmpty(condition.value)) return false
|
||||
|
||||
const actual = parseFloat(value)
|
||||
const expected = parseFloat(condition.value)
|
||||
|
||||
if (Number.isNaN(actual) || Number.isNaN(expected)) return false
|
||||
|
||||
if (condition.action === 'equal') return Math.abs(actual - expected) < Number.EPSILON
|
||||
if (condition.action === 'not_equal') return Math.abs(actual - expected) > Number.EPSILON
|
||||
if (condition.action === 'greater_than') return actual > expected
|
||||
if (condition.action === 'less_than') return actual < expected
|
||||
|
||||
return false
|
||||
} else if (['equal', 'contains'].includes(condition.action) && field) {
|
||||
if (field.options) {
|
||||
const option = field.options.find((o) => o.uuid === condition.value)
|
||||
const values = [this.values[condition.field_uuid] ?? defaultValue].flat()
|
||||
|
||||
return values.includes(this.optionValue(option, field.options.indexOf(option)))
|
||||
if (option) {
|
||||
const values = [this.values[condition.field_uuid] ?? defaultValue].flat()
|
||||
|
||||
return values.includes(this.optionValue(option, field.options.indexOf(option)))
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return [this.values[condition.field_uuid] ?? defaultValue].flat().includes(condition.value)
|
||||
}
|
||||
} else if (['not_equal', 'does_not_contain'].includes(condition.action) && field) {
|
||||
const option = field.options.find((o) => o.uuid === condition.value)
|
||||
const values = [this.values[condition.field_uuid] ?? defaultValue].flat()
|
||||
if (field.options) {
|
||||
const option = field.options.find((o) => o.uuid === condition.value)
|
||||
|
||||
return !values.includes(this.optionValue(option, field.options.indexOf(option)))
|
||||
if (option) {
|
||||
const values = [this.values[condition.field_uuid] ?? defaultValue].flat()
|
||||
|
||||
return !values.includes(this.optionValue(option, field.options.indexOf(option)))
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
@@ -1253,6 +1354,13 @@ export default {
|
||||
return `${this.t('option')} ${index + 1}`
|
||||
}
|
||||
},
|
||||
findNextStep (currentStepIndex) {
|
||||
if (this.onlyRequiredFields) {
|
||||
return this.stepFields.find((step, index) => index > currentStepIndex && step.some((f) => f.required))
|
||||
} else {
|
||||
return this.stepFields[currentStepIndex + 1]
|
||||
}
|
||||
},
|
||||
maybeTrackEmailClick () {
|
||||
const { queryParams } = this
|
||||
|
||||
@@ -1314,9 +1422,9 @@ export default {
|
||||
},
|
||||
previousSignatureValueFor (field) {
|
||||
if (this.reuseSignature !== false) {
|
||||
const signatureField = [...this.fields].reverse().find((f) =>
|
||||
f.type === 'signature' && field.preferences?.format === f.preferences?.format && !!this.values[f.uuid]
|
||||
)
|
||||
const signatureField = this.fields.findLast
|
||||
? this.fields.findLast((f) => f.type === 'signature' && field.preferences?.format === f.preferences?.format && !!this.values[f.uuid])
|
||||
: [...this.fields].reverse().find((f) => f.type === 'signature' && field.preferences?.format === f.preferences?.format && !!this.values[f.uuid])
|
||||
|
||||
return this.values[signatureField?.uuid]
|
||||
} else {
|
||||
@@ -1366,7 +1474,8 @@ export default {
|
||||
} else {
|
||||
return fetch(this.baseUrl + this.submitPath, {
|
||||
method: 'POST',
|
||||
body: formData || new FormData(this.$refs.form)
|
||||
body: formData || new FormData(this.$refs.form),
|
||||
...this.fetchOptions
|
||||
}).then((response) => {
|
||||
if (response.status === 200) {
|
||||
currentFieldUuids.forEach((fieldUuid) => {
|
||||
@@ -1399,15 +1508,15 @@ export default {
|
||||
this.isSubmittingComplete = true
|
||||
}
|
||||
|
||||
const submitStep = this.currentStep
|
||||
const submitStepIndex = this.currentStep
|
||||
|
||||
const stepPromise = ['signature', 'phone', 'initials', 'payment', 'verification'].includes(this.currentField.type)
|
||||
const stepPromise = ['signature', 'phone', 'initials', 'payment', 'verification', 'kba'].includes(this.currentField.type)
|
||||
? this.$refs.currentStep.submit
|
||||
: () => Promise.resolve({})
|
||||
|
||||
stepPromise().then(async () => {
|
||||
const emptyRequiredField = this.stepFields.find((fields, index) => {
|
||||
if (forceComplete ? index === submitStep : index >= submitStep) {
|
||||
if (forceComplete ? index === submitStepIndex : index >= submitStepIndex) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1417,7 +1526,7 @@ export default {
|
||||
})
|
||||
|
||||
const formData = new FormData(this.$refs.form)
|
||||
const isLastStep = (submitStep === this.stepFields.length - 1) || forceComplete
|
||||
const isLastStep = (this.onlyRequiredFields ? !this.findNextStep(submitStepIndex) : (submitStepIndex === this.stepFields.length - 1)) || forceComplete
|
||||
|
||||
if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length && !this.optionalInviteSubmitters.length) {
|
||||
formData.append('completed', 'true')
|
||||
@@ -1461,7 +1570,7 @@ export default {
|
||||
return Promise.reject(new Error(data.error))
|
||||
}
|
||||
|
||||
const nextStep = (isLastStep && emptyRequiredField) || (forceComplete ? null : this.stepFields[submitStep + 1])
|
||||
const nextStep = (isLastStep && emptyRequiredField) || (forceComplete ? null : this.findNextStep(submitStepIndex))
|
||||
|
||||
if (nextStep) {
|
||||
if (this.alwaysMinimize) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const en = {
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'Please upload an image file',
|
||||
must_be_characters_length: 'Must be {number} characters length',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.',
|
||||
@@ -100,6 +101,7 @@ const en = {
|
||||
}
|
||||
|
||||
const es = {
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'Por favor, sube un archivo de imagen',
|
||||
must_be_characters_length: 'Debe tener {number} caracteres de longitud',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete todos los campos requeridos para continuar con la verificación de identidad.',
|
||||
@@ -201,6 +203,7 @@ const es = {
|
||||
}
|
||||
|
||||
const it = {
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'Per favore carica un file immagine',
|
||||
must_be_characters_length: 'Deve essere lungo {number} caratteri',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: "Compila tutti i campi obbligatori per procedere con la verifica dell'identità.",
|
||||
@@ -302,6 +305,7 @@ const it = {
|
||||
}
|
||||
|
||||
const de = {
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'Bitte laden Sie eine Bilddatei hoch',
|
||||
must_be_characters_length: 'Muss {number} Zeichen lang sein',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Füllen Sie alle Pflichtfelder aus, um mit der Identitätsprüfung fortzufahren.',
|
||||
@@ -403,6 +407,7 @@ const de = {
|
||||
}
|
||||
|
||||
const fr = {
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'Veuillez téléverser un fichier image',
|
||||
must_be_characters_length: 'Doit comporter {number} caractères',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour poursuivre la vérification d'identité.",
|
||||
@@ -504,6 +509,7 @@ const fr = {
|
||||
}
|
||||
|
||||
const pl = {
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'Proszę przesłać plik obrazu',
|
||||
must_be_characters_length: 'Musi mieć długość {number} znaków',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Uzupełnij wszystkie wymagane pola, aby kontynuować weryfikację tożsamości.',
|
||||
@@ -605,6 +611,7 @@ const pl = {
|
||||
}
|
||||
|
||||
const uk = {
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'Будь ласка, завантажте файл зображення',
|
||||
must_be_characters_length: 'Має містити {number} символів',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: "Заповніть всі обов'язкові поля, щоб продовжити перевірку особи.",
|
||||
@@ -706,6 +713,7 @@ const uk = {
|
||||
}
|
||||
|
||||
const cs = {
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'Nahrajte prosím obrázkový soubor',
|
||||
must_be_characters_length: 'Musí mít délku {number} znaků',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Vyplňte všechna povinná pole, abyste mohli pokračovat v ověření identity.',
|
||||
@@ -807,6 +815,7 @@ const cs = {
|
||||
}
|
||||
|
||||
const pt = {
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'Por favor, envie um arquivo de imagem',
|
||||
must_be_characters_length: 'Deve ter {number} caracteres',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Preencha todos os campos obrigatórios para prosseguir com a verificação de identidade.',
|
||||
@@ -908,6 +917,7 @@ const pt = {
|
||||
}
|
||||
|
||||
const he = {
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'אנא העלה קובץ תמונה',
|
||||
must_be_characters_length: 'חייב להיות באורך של {number} תווים',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: 'מלא את כל השדות הנדרשים כדי להמשיך עם אימות זהות.',
|
||||
@@ -1009,6 +1019,7 @@ const he = {
|
||||
}
|
||||
|
||||
const nl = {
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'Upload alstublieft een afbeeldingsbestand',
|
||||
must_be_characters_length: 'Moet {number} tekens lang zijn',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Vul alle verplichte velden in om door te gaan met de identiteitsverificatie.',
|
||||
@@ -1110,6 +1121,7 @@ const nl = {
|
||||
}
|
||||
|
||||
const ar = {
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'يرجى تحميل ملف صورة',
|
||||
must_be_characters_length: 'يجب أن يكون الطول {number} حرفًا',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: 'أكمل جميع الحقول المطلوبة للمتابعة في التحقق من الهوية.',
|
||||
@@ -1211,6 +1223,7 @@ const ar = {
|
||||
}
|
||||
|
||||
const ko = {
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: '이미지 파일을 업로드해 주세요',
|
||||
must_be_characters_length: '{number}자여야 합니다',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: '신원 확인을 진행하려면 모든 필수 필드를 작성하십시오.',
|
||||
@@ -1312,6 +1325,7 @@ const ko = {
|
||||
}
|
||||
|
||||
const ja = {
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: '画像ファイルをアップロードしてください',
|
||||
must_be_characters_length: '{number}文字でなければなりません',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: '本人確認を進めるには、すべての必須項目を入力してください。',
|
||||
|
||||
@@ -175,6 +175,10 @@ export default {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
submitter: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
dryRun: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -257,6 +261,14 @@ export default {
|
||||
|
||||
this.$refs.canvas.getContext('2d').scale(scale, scale)
|
||||
|
||||
if (!this.isDrawInitials) {
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.textInput) {
|
||||
this.initTextInitial()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.intersectionObserver?.disconnect()
|
||||
}
|
||||
})
|
||||
@@ -332,12 +344,27 @@ export default {
|
||||
|
||||
if (!this.isDrawInitials) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.textInput.focus()
|
||||
if (this.$refs.textInput) {
|
||||
if (!this.submitter.name) {
|
||||
this.$refs.textInput.focus()
|
||||
}
|
||||
|
||||
this.$emit('start')
|
||||
this.initTextInitial()
|
||||
|
||||
this.$emit('start')
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
initTextInitial () {
|
||||
if (this.submitter.name) {
|
||||
this.$refs.textInput.value = this.submitter.name.trim().split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0]?.toUpperCase() || '').join('')
|
||||
}
|
||||
|
||||
if (this.$refs.textInput.value) {
|
||||
this.updateWrittenInitials({ target: this.$refs.textInput })
|
||||
}
|
||||
},
|
||||
async submit () {
|
||||
if (this.modelValue || this.computedPreviousValue) {
|
||||
if (this.computedPreviousValue) {
|
||||
|
||||
@@ -78,6 +78,11 @@ export default {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
fetchOptions: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({})
|
||||
},
|
||||
optionalSubmitters: {
|
||||
type: Array,
|
||||
required: false,
|
||||
@@ -108,7 +113,8 @@ export default {
|
||||
|
||||
return fetch(this.url, {
|
||||
method: 'POST',
|
||||
body: new FormData(this.$refs.form)
|
||||
body: new FormData(this.$refs.form),
|
||||
...this.fetchOptions
|
||||
}).then((response) => {
|
||||
if (response.status === 200) {
|
||||
this.$emit('success')
|
||||
|
||||
@@ -0,0 +1,580 @@
|
||||
<template>
|
||||
<label
|
||||
v-if="!error"
|
||||
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
|
||||
>
|
||||
<MarkdownContent
|
||||
v-if="field.title"
|
||||
:string="field.title"
|
||||
/>
|
||||
<template v-else>{{ field.name || 'Knowledge Based Authentication' }}</template>
|
||||
<span
|
||||
v-if="questions"
|
||||
class="float-right font-normal text-neutral-500 mt-1 whitespace-nowrap text-right text-sm"
|
||||
>
|
||||
<span>Question {{ currentQuestionIndex + 1 }} / {{ questions.length }}</span>
|
||||
<span
|
||||
v-if="timeLeftSeconds !== null"
|
||||
class="block"
|
||||
>
|
||||
Time left: {{ formattedTimeLeft }}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
v-if="field.description"
|
||||
dir="auto"
|
||||
class="mb-4 px-1 field-description-text"
|
||||
>
|
||||
<MarkdownContent :string="field.description" />
|
||||
</div>
|
||||
<div
|
||||
v-if="isRequiredFieldEmpty"
|
||||
class="px-1 field-description-text"
|
||||
>
|
||||
{{ t('complete_all_required_fields_to_proceed_with_identity_verification') }}
|
||||
</div>
|
||||
<div
|
||||
v-if="error"
|
||||
class="mb-4 text-center"
|
||||
>
|
||||
<div class="text-xl mb-4">
|
||||
{{ error }}
|
||||
</div>
|
||||
<button
|
||||
class="base-button w-full flex justify-center submit-form-button"
|
||||
@click="restartKba"
|
||||
>
|
||||
{{ questions ? 'Restart KBA' : 'Retry' }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="w-full flex space-x-2 justify-center mb-2"
|
||||
>
|
||||
<IconInnerShadowTop
|
||||
width="40"
|
||||
class="animate-spin h-10"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="questions && !error">
|
||||
<form @submit.prevent="nextQuestion">
|
||||
<div class="mb-6 px-1">
|
||||
<p class="font-semibold mb-4 text-lg">
|
||||
{{ currentQuestion.prompt }}
|
||||
</p>
|
||||
<div class="space-y-3.5 mx-auto">
|
||||
<div
|
||||
v-for="(answer, index) in currentQuestion.answers"
|
||||
:key="`${currentQuestion.id}_${index}`"
|
||||
>
|
||||
<label
|
||||
:for="`${currentQuestion.id}_${index}`"
|
||||
class="flex items-center space-x-3 radio-label"
|
||||
>
|
||||
<input
|
||||
:id="`${currentQuestion.id}_${index}`"
|
||||
v-model="answers[currentQuestion.id]"
|
||||
type="radio"
|
||||
:name="currentQuestion.id"
|
||||
:value="answer.text"
|
||||
class="base-radio !h-7 !w-7"
|
||||
required
|
||||
>
|
||||
<span class="text-xl">{{ answer.text }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<button
|
||||
type="submit"
|
||||
class="base-button w-full flex justify-center submit-form-button"
|
||||
:disabled="isSubmitting || !answers[currentQuestion.id]"
|
||||
>
|
||||
<span class="flex">
|
||||
<IconInnerShadowTop
|
||||
v-if="isSubmitting"
|
||||
class="mr-1 animate-spin"
|
||||
/>
|
||||
<span>
|
||||
{{ isLastQuestion ? (isSubmitting ? t('submitting_') : t('complete')) : t('next') }}
|
||||
</span><span
|
||||
v-if="isSubmitting"
|
||||
class="w-6 flex justify-start mr-1"
|
||||
><span>...</span></span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div v-else-if="!error && !isRequiredFieldEmpty">
|
||||
<form @submit.prevent="startKba">
|
||||
<div class="grid grid-cols-6 gap-x-2 md:gap-x-4 md:gap-y-2 mb-4">
|
||||
<div class="col-span-3">
|
||||
<label
|
||||
for="kba_fn"
|
||||
class="label text-sm md:text-base"
|
||||
>First Name</label>
|
||||
<input
|
||||
id="kba_fn"
|
||||
v-model="form.fn"
|
||||
type="text"
|
||||
class="input input-bordered !h-10 w-full bg-white"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
<div class="col-span-3">
|
||||
<label
|
||||
for="kba_ln"
|
||||
class="label text-sm md:text-base"
|
||||
>Last Name</label>
|
||||
<input
|
||||
id="kba_ln"
|
||||
v-model="form.ln"
|
||||
type="text"
|
||||
class="input input-bordered !h-10 w-full bg-white"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
<div class="col-span-6">
|
||||
<label
|
||||
for="kba_addr"
|
||||
class="label text-sm md:text-base"
|
||||
>Address</label>
|
||||
<input
|
||||
id="kba_addr"
|
||||
v-model="form.addr"
|
||||
type="text"
|
||||
class="input input-bordered !h-10 w-full bg-white"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label
|
||||
for="kba_city"
|
||||
class="label text-sm md:text-base"
|
||||
>City</label>
|
||||
<input
|
||||
id="kba_city"
|
||||
v-model="form.city"
|
||||
type="text"
|
||||
class="input input-bordered !h-10 w-full bg-white"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label
|
||||
for="kba_state"
|
||||
class="label text-sm md:text-base"
|
||||
>State</label>
|
||||
<select
|
||||
id="kba_state"
|
||||
v-model="form.state"
|
||||
class="select select-bordered !h-10 min-h-[2.5rem] w-full bg-white font-normal !text-base"
|
||||
required
|
||||
>
|
||||
<option
|
||||
value=""
|
||||
disabled
|
||||
>
|
||||
Select State
|
||||
</option>
|
||||
<option
|
||||
v-for="state in states"
|
||||
:key="state.code"
|
||||
:value="state.code"
|
||||
>
|
||||
{{ state.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label
|
||||
for="kba_zip"
|
||||
class="label text-sm md:text-base"
|
||||
>Zip</label>
|
||||
<input
|
||||
id="kba_zip"
|
||||
v-model="form.zip"
|
||||
type="text"
|
||||
class="input input-bordered !h-10 w-full bg-white"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
<div class="col-span-3">
|
||||
<label
|
||||
for="kba_phone"
|
||||
class="label text-sm md:text-base"
|
||||
>Phone</label>
|
||||
<input
|
||||
id="kba_phone"
|
||||
v-model="form.phone"
|
||||
type="text"
|
||||
class="input input-bordered !h-10 w-full bg-white"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
<div class="col-span-3">
|
||||
<label
|
||||
for="kba_email"
|
||||
class="label text-sm md:text-base"
|
||||
>Email</label>
|
||||
<input
|
||||
id="kba_email"
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
class="input input-bordered !h-10 w-full bg-white"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
<div class="col-span-3">
|
||||
<label
|
||||
for="kba_dob"
|
||||
class="label text-sm md:text-base"
|
||||
>DOB</label>
|
||||
<input
|
||||
id="kba_dob"
|
||||
v-model="form.dob"
|
||||
type="date"
|
||||
class="input input-bordered !h-10 md:w-full bg-white"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
<div class="col-span-3">
|
||||
<label
|
||||
for="kba_ssn"
|
||||
class="label text-sm md:text-base"
|
||||
>SSN</label>
|
||||
<input
|
||||
id="kba_ssn"
|
||||
v-model="form.ssn"
|
||||
type="text"
|
||||
class="input input-bordered !h-10 w-full bg-white"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<button
|
||||
type="submit"
|
||||
class="base-button w-full flex justify-center submit-form-button"
|
||||
:disabled="isLoading"
|
||||
>
|
||||
<span class="flex">
|
||||
<IconInnerShadowTop
|
||||
v-if="isLoading"
|
||||
class="mr-1 animate-spin"
|
||||
/>
|
||||
<span>
|
||||
{{ isLoading ? 'Loading...' : 'Start Verification' }}
|
||||
</span><span
|
||||
v-if="isLoading"
|
||||
class="w-6 flex justify-start mr-1"
|
||||
><span>...</span></span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MarkdownContent from './markdown_content'
|
||||
import { IconInnerShadowTop } from '@tabler/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'KbaStep',
|
||||
components: {
|
||||
MarkdownContent,
|
||||
IconInnerShadowTop
|
||||
},
|
||||
inject: ['baseUrl', 't'],
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
submitter: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
submitterSlug: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
emptyValueRequiredStep: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
values: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['submit'],
|
||||
data () {
|
||||
return {
|
||||
isLoading: false,
|
||||
isSubmitting: false,
|
||||
questions: null,
|
||||
currentQuestionIndex: 0,
|
||||
token: null,
|
||||
reference: null,
|
||||
answers: {},
|
||||
error: null,
|
||||
timeLeftSeconds: null,
|
||||
countdownIntervalId: null,
|
||||
form: {
|
||||
fn: '',
|
||||
ln: '',
|
||||
addr: '',
|
||||
city: '',
|
||||
state: '',
|
||||
zip: '',
|
||||
dob: '',
|
||||
ssn: '',
|
||||
phone: '',
|
||||
email: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentQuestion () {
|
||||
return this.questions ? this.questions[this.currentQuestionIndex] : null
|
||||
},
|
||||
isRequiredFieldEmpty () {
|
||||
return this.emptyValueRequiredStep && this.emptyValueRequiredStep[0] !== this.field
|
||||
},
|
||||
kbaTimeLimitSeconds () {
|
||||
return 90
|
||||
},
|
||||
states () {
|
||||
return [
|
||||
{ code: 'AL', name: 'Alabama' },
|
||||
{ code: 'AK', name: 'Alaska' },
|
||||
{ code: 'AZ', name: 'Arizona' },
|
||||
{ code: 'AR', name: 'Arkansas' },
|
||||
{ code: 'CA', name: 'California' },
|
||||
{ code: 'CO', name: 'Colorado' },
|
||||
{ code: 'CT', name: 'Connecticut' },
|
||||
{ code: 'DE', name: 'Delaware' },
|
||||
{ code: 'DC', name: 'District of Columbia' },
|
||||
{ code: 'FL', name: 'Florida' },
|
||||
{ code: 'GA', name: 'Georgia' },
|
||||
{ code: 'HI', name: 'Hawaii' },
|
||||
{ code: 'ID', name: 'Idaho' },
|
||||
{ code: 'IL', name: 'Illinois' },
|
||||
{ code: 'IN', name: 'Indiana' },
|
||||
{ code: 'IA', name: 'Iowa' },
|
||||
{ code: 'KS', name: 'Kansas' },
|
||||
{ code: 'KY', name: 'Kentucky' },
|
||||
{ code: 'LA', name: 'Louisiana' },
|
||||
{ code: 'ME', name: 'Maine' },
|
||||
{ code: 'MD', name: 'Maryland' },
|
||||
{ code: 'MA', name: 'Massachusetts' },
|
||||
{ code: 'MI', name: 'Michigan' },
|
||||
{ code: 'MN', name: 'Minnesota' },
|
||||
{ code: 'MS', name: 'Mississippi' },
|
||||
{ code: 'MO', name: 'Missouri' },
|
||||
{ code: 'MT', name: 'Montana' },
|
||||
{ code: 'NE', name: 'Nebraska' },
|
||||
{ code: 'NV', name: 'Nevada' },
|
||||
{ code: 'NH', name: 'New Hampshire' },
|
||||
{ code: 'NJ', name: 'New Jersey' },
|
||||
{ code: 'NM', name: 'New Mexico' },
|
||||
{ code: 'NY', name: 'New York' },
|
||||
{ code: 'NC', name: 'North Carolina' },
|
||||
{ code: 'ND', name: 'North Dakota' },
|
||||
{ code: 'OH', name: 'Ohio' },
|
||||
{ code: 'OK', name: 'Oklahoma' },
|
||||
{ code: 'OR', name: 'Oregon' },
|
||||
{ code: 'PA', name: 'Pennsylvania' },
|
||||
{ code: 'RI', name: 'Rhode Island' },
|
||||
{ code: 'SC', name: 'South Carolina' },
|
||||
{ code: 'SD', name: 'South Dakota' },
|
||||
{ code: 'TN', name: 'Tennessee' },
|
||||
{ code: 'TX', name: 'Texas' },
|
||||
{ code: 'UT', name: 'Utah' },
|
||||
{ code: 'VT', name: 'Vermont' },
|
||||
{ code: 'VA', name: 'Virginia' },
|
||||
{ code: 'WA', name: 'Washington' },
|
||||
{ code: 'WV', name: 'West Virginia' },
|
||||
{ code: 'WI', name: 'Wisconsin' },
|
||||
{ code: 'WY', name: 'Wyoming' }
|
||||
]
|
||||
},
|
||||
isLastQuestion () {
|
||||
return this.questions && this.currentQuestionIndex === this.questions.length - 1
|
||||
},
|
||||
formattedTimeLeft () {
|
||||
if (this.timeLeftSeconds === null) return ''
|
||||
|
||||
const minutes = Math.floor(this.timeLeftSeconds / 60)
|
||||
const seconds = this.timeLeftSeconds % 60
|
||||
|
||||
return `${minutes}:${String(seconds).padStart(2, '0')}`
|
||||
}
|
||||
},
|
||||
beforeUnmount () {
|
||||
this.clearCountdown()
|
||||
},
|
||||
methods: {
|
||||
clearCountdown () {
|
||||
if (this.countdownIntervalId) {
|
||||
clearInterval(this.countdownIntervalId)
|
||||
this.countdownIntervalId = null
|
||||
}
|
||||
|
||||
this.timeLeftSeconds = null
|
||||
},
|
||||
startCountdown () {
|
||||
this.clearCountdown()
|
||||
|
||||
this.timeLeftSeconds = this.kbaTimeLimitSeconds
|
||||
|
||||
this.countdownIntervalId = setInterval(() => {
|
||||
if (this.timeLeftSeconds === null) return
|
||||
|
||||
this.timeLeftSeconds -= 1
|
||||
|
||||
if (this.timeLeftSeconds <= 0) {
|
||||
this.handleTimeout()
|
||||
}
|
||||
}, 1000)
|
||||
},
|
||||
handleTimeout () {
|
||||
this.clearCountdown()
|
||||
|
||||
this.questions = null
|
||||
this.token = null
|
||||
this.reference = null
|
||||
this.answers = {}
|
||||
this.currentQuestionIndex = 0
|
||||
this.error = `Knowledge Based Authentication timed out. You only have ${this.kbaTimeLimitSeconds} seconds to complete the verification. Please retry.`
|
||||
},
|
||||
nextQuestion () {
|
||||
if (this.isLastQuestion) {
|
||||
this.$emit('submit')
|
||||
} else {
|
||||
this.currentQuestionIndex++
|
||||
}
|
||||
},
|
||||
restartKba () {
|
||||
this.clearCountdown()
|
||||
|
||||
this.questions = null
|
||||
this.token = null
|
||||
this.reference = null
|
||||
this.answers = {}
|
||||
this.currentQuestionIndex = 0
|
||||
this.error = null
|
||||
},
|
||||
async startKba () {
|
||||
this.isLoading = true
|
||||
this.error = null
|
||||
this.clearCountdown()
|
||||
|
||||
try {
|
||||
const payload = { ...this.form, submitter_slug: this.submitterSlug }
|
||||
|
||||
if (payload.dob) {
|
||||
payload.dob = payload.dob.replace(/-/g, '')
|
||||
}
|
||||
|
||||
if (payload.ssn) {
|
||||
payload.ssn = payload.ssn.replace(/\D/g, '')
|
||||
}
|
||||
|
||||
if (payload.phone) {
|
||||
payload.phone = payload.phone.replace(/^\+1/, '')
|
||||
}
|
||||
|
||||
const resp = await fetch(this.baseUrl + '/api/kba', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
|
||||
const data = await resp.json()
|
||||
|
||||
if (!resp.ok) throw new Error(data.error || 'Failed to start KBA')
|
||||
|
||||
if (data.result && data.result.action === 'FAIL') {
|
||||
if (data.result.detail === 'NO MATCH') {
|
||||
throw new Error('Unfortunately, we were unable to start Knowledge Based Authentication with the details provided. Please review and confirm that all your personal details are correct.')
|
||||
}
|
||||
|
||||
throw new Error(data.result.detail || 'KBA Start Failed')
|
||||
}
|
||||
|
||||
if (data.output && data.output.questions && data.output.questions.questions) {
|
||||
this.questions = data.output.questions.questions
|
||||
this.token = data.continuations.questions.template.token
|
||||
this.reference = data.meta.reference
|
||||
|
||||
this.questions.forEach(q => {
|
||||
this.answers[q.id] = null
|
||||
})
|
||||
|
||||
this.startCountdown()
|
||||
} else {
|
||||
throw new Error('Invalid KBA response')
|
||||
}
|
||||
} catch (e) {
|
||||
this.error = e.message
|
||||
} finally {
|
||||
this.isLoading = false
|
||||
}
|
||||
},
|
||||
async submit () {
|
||||
this.clearCountdown()
|
||||
|
||||
this.isSubmitting = true
|
||||
this.error = null
|
||||
|
||||
const formattedAnswers = Object.keys(this.answers).reduce((acc, key) => {
|
||||
acc[key] = [this.answers[key]]
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
try {
|
||||
const resp = await fetch(this.baseUrl + `/api/kba/${this.field.uuid}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
token: this.token,
|
||||
answers: formattedAnswers,
|
||||
reference: this.reference,
|
||||
submitter_slug: this.submitterSlug
|
||||
}),
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
|
||||
const data = await resp.json()
|
||||
|
||||
if (data.result?.action !== 'PASS') {
|
||||
if (data.result?.issues?.length) {
|
||||
this.error = `Knowledge Based Authentication Failed - make sure you provide correct details for the Knowledge Based authentication: ${data.result.issues.join(', ')}`
|
||||
} else {
|
||||
this.error = 'Knowledge Based Authentication Failed - make sure you provide correct answers for the Knowledge Based authentication.'
|
||||
}
|
||||
|
||||
throw new Error('Knowledge Based Authentication Failed')
|
||||
}
|
||||
|
||||
if (!resp.ok) {
|
||||
this.error = 'Failed to submit answers'
|
||||
|
||||
throw new Error('Failed to submit answers')
|
||||
}
|
||||
|
||||
return resp
|
||||
} finally {
|
||||
this.isSubmitting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -127,6 +127,12 @@
|
||||
type="hidden"
|
||||
:name="`values[${field.uuid}]`"
|
||||
>
|
||||
<input
|
||||
v-if="isTouchAttachment"
|
||||
:value="touchAttachmentUuid"
|
||||
type="hidden"
|
||||
name="touch_attachment_uuid"
|
||||
>
|
||||
<img
|
||||
v-if="modelValue || computedPreviousValue"
|
||||
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
|
||||
@@ -175,9 +181,7 @@
|
||||
v-if="isShowQr"
|
||||
class="top-0 bottom-0 right-0 left-0 absolute bg-base-content/10 rounded-2xl"
|
||||
>
|
||||
<div
|
||||
class="absolute top-1.5 right-1.5 md:tooltip"
|
||||
>
|
||||
<div class="absolute top-1.5 right-1.5">
|
||||
<a
|
||||
href="#"
|
||||
class="btn btn-sm btn-circle btn-normal btn-outline"
|
||||
@@ -312,7 +316,7 @@ import FileDropzone from './dropzone'
|
||||
import MarkdownContent from './markdown_content'
|
||||
import { v4 } from 'uuid'
|
||||
|
||||
let isFontLoaded = false
|
||||
let fontLoadPromise = null
|
||||
|
||||
const scale = 3
|
||||
|
||||
@@ -336,6 +340,10 @@ export default {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
values: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
requireSigningReason: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -390,6 +398,11 @@ export default {
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
touchAttachmentUuid: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
reason: {
|
||||
type: String,
|
||||
required: false,
|
||||
@@ -401,13 +414,14 @@ export default {
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
emits: ['attached', 'update:model-value', 'start', 'minimize', 'update:reason'],
|
||||
emits: ['attached', 'update:model-value', 'start', 'minimize', 'update:reason', 'touch-attachment'],
|
||||
data () {
|
||||
return {
|
||||
isSignatureStarted: false,
|
||||
isShowQr: false,
|
||||
isOtherReason: false,
|
||||
isUsePreviousValue: true,
|
||||
isTouchAttachment: false,
|
||||
isTextSignature: this.field.preferences?.format === 'typed' || this.field.preferences?.format === 'typed_or_upload',
|
||||
uploadImageInputKey: Math.random().toString()
|
||||
}
|
||||
@@ -448,14 +462,7 @@ export default {
|
||||
}
|
||||
},
|
||||
async mounted () {
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.canvas) {
|
||||
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
|
||||
this.$refs.canvas.height = this.$refs.canvas.parentNode.clientWidth * scale / 3
|
||||
|
||||
this.$refs.canvas.getContext('2d').scale(scale, scale)
|
||||
}
|
||||
})
|
||||
this.$nextTick(() => this.setCanvasSize())
|
||||
|
||||
if (this.$refs.canvas) {
|
||||
this.pad = new SignaturePad(this.$refs.canvas)
|
||||
@@ -470,13 +477,18 @@ export default {
|
||||
this.$emit('start')
|
||||
})
|
||||
|
||||
this.intersectionObserver = new IntersectionObserver((entries, observer) => {
|
||||
this.intersectionObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
|
||||
this.$refs.canvas.height = this.$refs.canvas.parentNode.clientWidth * scale / 3
|
||||
this.setCanvasSize()
|
||||
|
||||
this.$refs.canvas.getContext('2d').scale(scale, scale)
|
||||
if (this.isTextSignature) {
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.textInput) {
|
||||
this.initTypedSignature()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.intersectionObserver?.disconnect()
|
||||
}
|
||||
@@ -484,13 +496,66 @@ export default {
|
||||
})
|
||||
|
||||
this.intersectionObserver.observe(this.$refs.canvas)
|
||||
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (!this.$refs.canvas) return
|
||||
|
||||
const { width, height } = this.$refs.canvas
|
||||
|
||||
this.setCanvasSize()
|
||||
|
||||
if (this.$refs.canvas.width !== width || this.$refs.canvas.height !== height) {
|
||||
this.redrawCanvas(width, height)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.resizeObserver.observe(this.$refs.canvas.parentNode)
|
||||
|
||||
if (this.isTextSignature) {
|
||||
this.loadFont()
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeUnmount () {
|
||||
this.intersectionObserver?.disconnect()
|
||||
this.resizeObserver?.disconnect()
|
||||
this.stopCheckSignature()
|
||||
},
|
||||
methods: {
|
||||
setCanvasSize () {
|
||||
const canvas = this.$refs.canvas
|
||||
|
||||
if (canvas) {
|
||||
const width = canvas.parentNode.clientWidth
|
||||
const height = width / 3
|
||||
|
||||
if (canvas.width !== width * scale || canvas.height !== height * scale) {
|
||||
canvas.width = width * scale
|
||||
canvas.height = height * scale
|
||||
|
||||
canvas.getContext('2d').scale(scale, scale)
|
||||
}
|
||||
}
|
||||
},
|
||||
redrawCanvas (oldWidth, oldHeight) {
|
||||
const canvas = this.$refs.canvas
|
||||
|
||||
if (this.pad && !this.isTextSignature && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0) {
|
||||
const sx = canvas.width / oldWidth
|
||||
const sy = canvas.height / oldHeight
|
||||
|
||||
const scaledData = this.pad.toData().map((stroke) => ({
|
||||
...stroke,
|
||||
points: stroke.points.map((p) => ({ ...p, x: p.x * sx, y: p.y * sy }))
|
||||
}))
|
||||
|
||||
this.pad.fromData(scaledData)
|
||||
} else if (this.isTextSignature && this.$refs.textInput) {
|
||||
this.updateWrittenSignature({ target: this.$refs.textInput })
|
||||
}
|
||||
},
|
||||
remove () {
|
||||
this.$emit('update:model-value', '')
|
||||
|
||||
@@ -498,17 +563,17 @@ export default {
|
||||
this.isSignatureStarted = false
|
||||
},
|
||||
loadFont () {
|
||||
if (!isFontLoaded) {
|
||||
if (!fontLoadPromise) {
|
||||
const font = new FontFace('Dancing Script', `url(${this.baseUrl}/fonts/DancingScript-Regular.otf) format("opentype")`)
|
||||
|
||||
font.load().then((loadedFont) => {
|
||||
fontLoadPromise = font.load().then((loadedFont) => {
|
||||
document.fonts.add(loadedFont)
|
||||
|
||||
isFontLoaded = true
|
||||
}).catch((error) => {
|
||||
console.error('Font loading failed:', error)
|
||||
})
|
||||
}
|
||||
|
||||
return fontLoadPromise
|
||||
},
|
||||
showQr () {
|
||||
this.isShowQr = true
|
||||
@@ -608,14 +673,29 @@ export default {
|
||||
|
||||
if (this.isTextSignature) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.textInput.focus()
|
||||
if (this.$refs.textInput) {
|
||||
if (!this.submitter.name) {
|
||||
this.$refs.textInput.focus()
|
||||
}
|
||||
|
||||
this.loadFont()
|
||||
this.initTypedSignature()
|
||||
|
||||
this.$emit('start')
|
||||
this.$emit('start')
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
async initTypedSignature () {
|
||||
if (this.submitter.name) {
|
||||
this.$refs.textInput.value = this.submitter.name
|
||||
}
|
||||
|
||||
await this.loadFont()
|
||||
|
||||
if (this.$refs.textInput.value) {
|
||||
this.updateWrittenSignature({ target: this.$refs.textInput })
|
||||
}
|
||||
},
|
||||
drawImage (event) {
|
||||
this.remove()
|
||||
this.clear()
|
||||
@@ -694,6 +774,13 @@ export default {
|
||||
},
|
||||
async submit () {
|
||||
if (this.modelValue || this.computedPreviousValue) {
|
||||
if (this.touchAttachmentUuid && this.computedPreviousValue === this.touchAttachmentUuid && !Object.values(this.values).includes(this.touchAttachmentUuid)) {
|
||||
this.isTouchAttachment = true
|
||||
this.$emit('touch-attachment', this.touchAttachmentUuid)
|
||||
} else {
|
||||
this.isTouchAttachment = false
|
||||
}
|
||||
|
||||
if (this.computedPreviousValue) {
|
||||
this.$emit('update:model-value', this.computedPreviousValue)
|
||||
}
|
||||
|
||||
@@ -119,6 +119,7 @@ export default {
|
||||
docId: this.eidEasyData.doc_id,
|
||||
language: this.locale,
|
||||
countryCode: this.countryCode,
|
||||
sandbox: ['demo.docuseal.tech'].includes(location.host),
|
||||
enabledMethods: {
|
||||
signature: this.eidEasyData.available_methods
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
@touchstart="startTouchDrag"
|
||||
>
|
||||
<div
|
||||
v-if="isSelected || isDraw"
|
||||
v-if="isSelected || isDraw || isInMultiSelection"
|
||||
class="top-0 bottom-0 right-0 left-0 absolute border border-1.5 pointer-events-none"
|
||||
:class="activeBorderClasses"
|
||||
/>
|
||||
@@ -24,7 +24,7 @@
|
||||
:style="{ left: (cellW / area.w * 100) + '%' }"
|
||||
>
|
||||
<span
|
||||
v-if="index === 0 && editable"
|
||||
v-if="index === 0 && editable && !isInMultiSelection"
|
||||
class="h-2.5 w-2.5 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-ew-resize z-10"
|
||||
style="left: -4px"
|
||||
@mousedown.stop="startResizeCell"
|
||||
@@ -32,7 +32,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="field?.type && (isSelected || isNameFocus)"
|
||||
v-if="field?.type && (isSelected || isNameFocus) && !isInMultiSelection"
|
||||
class="absolute bg-white rounded-t border overflow-visible whitespace-nowrap flex z-10 field-area-controls"
|
||||
style="top: -25px; height: 25px"
|
||||
@mousedown.stop
|
||||
@@ -48,7 +48,7 @@
|
||||
: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"
|
||||
@update:model-value="save"
|
||||
@click="selectedAreaRef.value = area"
|
||||
@click="selectedAreasRef.value = [area]"
|
||||
/>
|
||||
<FieldType
|
||||
v-model="field.type"
|
||||
@@ -57,7 +57,7 @@
|
||||
:button-classes="'px-1'"
|
||||
:menu-classes="'bg-white rounded-t-none'"
|
||||
@update:model-value="[maybeUpdateOptions(), save()]"
|
||||
@click="selectedAreaRef.value = area"
|
||||
@click="selectedAreasRef.value = [area]"
|
||||
/>
|
||||
<span
|
||||
v-if="field.type !== 'checkbox' || field.name"
|
||||
@@ -134,6 +134,7 @@
|
||||
@focusout="maybeBlurSettings"
|
||||
>
|
||||
<FieldSettings
|
||||
v-if="isMobile"
|
||||
:field="field"
|
||||
:default-field="defaultField"
|
||||
:editable="editable"
|
||||
@@ -145,9 +146,17 @@
|
||||
@click-formula="isShowFormulaModal = true"
|
||||
@click-font="isShowFontModal = true"
|
||||
@click-description="isShowDescriptionModal = true"
|
||||
@add-custom-field="$emit('add-custom-field')"
|
||||
@click-condition="isShowConditionsModal = true"
|
||||
@scroll-to="[selectedAreaRef.value = $event, $emit('scroll-to', $event)]"
|
||||
@save="save"
|
||||
@scroll-to="[selectedAreasRef.value = [$event], $emit('scroll-to', $event)]"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="whitespace-normal"
|
||||
>
|
||||
The dots menu is retired in favor of the field context menu. Right-click the field to access field settings. Double-click the field to set a default value.
|
||||
</div>
|
||||
</ul>
|
||||
</span>
|
||||
</div>
|
||||
@@ -250,7 +259,8 @@
|
||||
</span>
|
||||
<div
|
||||
v-else-if="field.type === 'cells' && field.default_value"
|
||||
class="w-full flex items-center"
|
||||
class="w-full flex"
|
||||
:class="fontClasses"
|
||||
>
|
||||
<div
|
||||
v-for="(char, index) in field.default_value"
|
||||
@@ -266,7 +276,7 @@
|
||||
ref="defaultValueSelect"
|
||||
class="bg-transparent outline-none focus:outline-none w-full"
|
||||
@change="[field.default_value = $event.target.value, field.readonly = !!field.default_value?.length, save()]"
|
||||
@focus="selectedAreaRef.value = area"
|
||||
@focus="selectedAreasRef.value = [area]"
|
||||
@keydown.enter="onDefaultValueEnter"
|
||||
>
|
||||
<option
|
||||
@@ -293,7 +303,7 @@
|
||||
:class="{ 'cursor-text': isValueInput }"
|
||||
:placeholder="withFieldPlaceholder && !isValueInput ? defaultField?.title || field.title || field.name || defaultName : (field.type === 'date' ? field.preferences?.format || t('type_value') : t('type_value'))"
|
||||
@blur="onDefaultValueBlur"
|
||||
@focus="selectedAreaRef.value = area"
|
||||
@focus="selectedAreasRef.value = [area]"
|
||||
@paste.prevent="onPaste"
|
||||
@keydown.enter="onDefaultValueEnter"
|
||||
>{{ field.default_value }}</span>
|
||||
@@ -319,6 +329,7 @@
|
||||
<span
|
||||
v-if="field?.type && editable"
|
||||
class="h-4 w-4 lg:h-2.5 lg:w-2.5 -right-1 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-nwse-resize"
|
||||
:class="{ 'z-30': isInMultiSelection }"
|
||||
@mousedown.stop="startResize"
|
||||
@touchstart="startTouchResize"
|
||||
/>
|
||||
@@ -331,6 +342,7 @@
|
||||
:editable="editable && !defaultField"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@save="save"
|
||||
@close="isShowFormulaModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
@@ -343,6 +355,7 @@
|
||||
:editable="editable && !defaultField"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@save="save"
|
||||
@close="isShowFontModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
@@ -354,6 +367,7 @@
|
||||
:item="field"
|
||||
:build-default-name="buildDefaultName"
|
||||
:default-field="defaultField"
|
||||
@save="save"
|
||||
@close="isShowConditionsModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
@@ -366,6 +380,7 @@
|
||||
:editable="editable && !defaultField"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@save="save"
|
||||
@close="isShowDescriptionModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
@@ -398,7 +413,7 @@ export default {
|
||||
FieldSubmitter,
|
||||
IconX
|
||||
},
|
||||
inject: ['template', 'selectedAreaRef', 'save', 't', 'isInlineSize'],
|
||||
inject: ['template', 'save', 't', 'isInlineSize', 'selectedAreasRef', 'isCmdKeyRef', 'getFieldTypeIndex'],
|
||||
props: {
|
||||
area: {
|
||||
type: Object,
|
||||
@@ -463,9 +478,19 @@ export default {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
isMobile: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
isSelectMode: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to'],
|
||||
emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to', 'add-custom-field'],
|
||||
data () {
|
||||
return {
|
||||
isShowFormulaModal: false,
|
||||
@@ -571,7 +596,7 @@ export default {
|
||||
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
||||
},
|
||||
defaultName () {
|
||||
return this.buildDefaultName(this.field, this.template.fields)
|
||||
return this.buildDefaultName(this.field)
|
||||
},
|
||||
fontClasses () {
|
||||
if (!this.field.preferences) {
|
||||
@@ -646,7 +671,10 @@ export default {
|
||||
]
|
||||
},
|
||||
isSelected () {
|
||||
return this.selectedAreaRef.value === this.area
|
||||
return this.selectedAreasRef.value.includes(this.area)
|
||||
},
|
||||
isInMultiSelection () {
|
||||
return this.selectedAreasRef.value.length >= 2 && this.isSelected
|
||||
},
|
||||
positionStyle () {
|
||||
const { x, y, w, h } = this.area
|
||||
@@ -683,10 +711,10 @@ export default {
|
||||
buildAreaOptionValue (area) {
|
||||
const option = this.optionsUuidIndex[area.option_uuid]
|
||||
|
||||
return option.value || `${this.t('option')} ${this.field.options.indexOf(option) + 1}`
|
||||
return option?.value || `${this.t('option')} ${this.field.options.indexOf(option) + 1}`
|
||||
},
|
||||
maybeToggleDefaultValue () {
|
||||
if (!this.editable) {
|
||||
if (!this.editable || this.isCmdKeyRef.value) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -770,7 +798,7 @@ export default {
|
||||
}
|
||||
},
|
||||
onNameFocus (e) {
|
||||
this.selectedAreaRef.value = this.area
|
||||
this.selectedAreasRef.value = [this.area]
|
||||
|
||||
this.isNameFocus = true
|
||||
this.$refs.name.style.minWidth = this.$refs.name.clientWidth + 'px'
|
||||
@@ -906,6 +934,15 @@ export default {
|
||||
if (e.target.id === 'mask') {
|
||||
this.area.w = e.offsetX / e.target.clientWidth - this.area.x
|
||||
this.area.h = e.offsetY / e.target.clientHeight - this.area.y
|
||||
|
||||
if (this.isInMultiSelection) {
|
||||
this.selectedAreasRef.value.forEach((area) => {
|
||||
if (area !== this.area) {
|
||||
area.w = this.area.w
|
||||
area.h = this.area.h
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
drag (e) {
|
||||
@@ -931,7 +968,7 @@ export default {
|
||||
|
||||
const rect = e.target.getBoundingClientRect()
|
||||
|
||||
this.selectedAreaRef.value = this.area
|
||||
this.selectedAreasRef.value = [this.area]
|
||||
|
||||
this.dragFrom = { x: rect.left - e.touches[0].clientX, y: rect.top - e.touches[0].clientY }
|
||||
|
||||
@@ -980,13 +1017,23 @@ export default {
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
if (!this.selectedAreasRef.value.includes(this.area)) {
|
||||
this.selectedAreasRef.value.push(this.area)
|
||||
} else {
|
||||
this.selectedAreasRef.value.splice(this.selectedAreasRef.value.indexOf(this.area), 1)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (this.editable) {
|
||||
this.isDragged = true
|
||||
}
|
||||
|
||||
const rect = e.target.getBoundingClientRect()
|
||||
|
||||
this.selectedAreaRef.value = this.area
|
||||
this.selectedAreasRef.value = [this.area]
|
||||
|
||||
this.dragFrom = { x: rect.left - e.clientX, y: rect.top - e.clientY }
|
||||
|
||||
@@ -1055,7 +1102,9 @@ export default {
|
||||
this.$emit('stop-drag')
|
||||
},
|
||||
startResize () {
|
||||
this.selectedAreaRef.value = this.area
|
||||
if (!this.selectedAreasRef.value.includes(this.area)) {
|
||||
this.selectedAreasRef.value = [this.area]
|
||||
}
|
||||
|
||||
this.$el.getRootNode().addEventListener('mousemove', this.resize)
|
||||
this.$el.getRootNode().addEventListener('mouseup', this.stopResize)
|
||||
@@ -1071,7 +1120,9 @@ export default {
|
||||
this.save()
|
||||
},
|
||||
startTouchResize (e) {
|
||||
this.selectedAreaRef.value = this.area
|
||||
if (!this.selectedAreasRef.value.includes(this.area)) {
|
||||
this.selectedAreasRef.value = [this.area]
|
||||
}
|
||||
|
||||
this.$refs?.name?.blur()
|
||||
|
||||
@@ -1088,6 +1139,15 @@ export default {
|
||||
|
||||
this.area.w = (e.touches[0].clientX - rect.left) / rect.width - this.area.x
|
||||
this.area.h = (e.touches[0].clientY - rect.top) / rect.height - this.area.y
|
||||
|
||||
if (this.isInMultiSelection) {
|
||||
this.selectedAreasRef.value.forEach((area) => {
|
||||
if (area !== this.area) {
|
||||
area.w = this.area.w
|
||||
area.h = this.area.h
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
stopTouchResize () {
|
||||
this.$el.getRootNode().removeEventListener('touchmove', this.touchResize)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@
|
||||
<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 class="modal-title">
|
||||
{{ t('condition') }} - {{ (defaultField ? (defaultField.title || item.title || item.name) : item.name) || buildDefaultName(item, template.fields) }}
|
||||
{{ t('condition') }} - {{ (defaultField ? (defaultField.title || item.title || item.name) : item.name) || buildDefaultName(item) }}
|
||||
</span>
|
||||
<a
|
||||
href="#"
|
||||
@@ -83,7 +83,7 @@
|
||||
class="text-base-content"
|
||||
:selected="condition.field_uuid === f.uuid"
|
||||
>
|
||||
{{ f.name || buildDefaultName(f, template.fields) }}
|
||||
{{ f.name || buildDefaultName(f) }}
|
||||
</option>
|
||||
</select>
|
||||
<select
|
||||
@@ -124,6 +124,16 @@
|
||||
{{ option.value || `${t('option')} ${index + 1}` }}
|
||||
</option>
|
||||
</select>
|
||||
<input
|
||||
v-else-if="conditionField(condition)?.type === 'number' && ['equal', 'not_equal', 'greater_than', 'less_than'].includes(condition.action)"
|
||||
v-model="condition.value"
|
||||
type="number"
|
||||
step="any"
|
||||
class="input input-bordered input-sm w-full bg-white h-11 pl-4 text-base font-normal"
|
||||
:class="{ 'text-gray-300': !condition.value }"
|
||||
:placeholder="t('type_value')"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
@@ -154,7 +164,7 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'ConditionModal',
|
||||
inject: ['t', 'save', 'template', 'withConditions'],
|
||||
inject: ['t', 'template', 'withConditions'],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
@@ -168,9 +178,14 @@ export default {
|
||||
buildDefaultName: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
excludeFieldUuids: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
emits: ['close'],
|
||||
emits: ['close', 'save'],
|
||||
data () {
|
||||
return {
|
||||
conditions: this.item.conditions?.[0] ? JSON.parse(JSON.stringify(this.item.conditions)) : [{}]
|
||||
@@ -183,14 +198,14 @@ export default {
|
||||
fields () {
|
||||
if (this.item.submitter_uuid) {
|
||||
return this.template.fields.reduce((acc, f) => {
|
||||
if (f !== this.item && !this.excludeTypes.includes(f.type) && (!f.conditions?.length || !f.conditions.find((c) => c.field_uuid === this.item.uuid))) {
|
||||
if (f !== this.item && !this.excludeTypes.includes(f.type) && !this.excludeFieldUuids.includes(f.uuid) && (!f.conditions?.length || !f.conditions.find((c) => c.field_uuid === this.item.uuid))) {
|
||||
acc.push(f)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
} else {
|
||||
return this.template.fields
|
||||
return this.template.fields.filter((f) => !this.excludeFieldUuids.includes(f.uuid))
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -217,6 +232,8 @@ export default {
|
||||
actions.push('equal', 'not_equal')
|
||||
} else if (['multiple'].includes(field.type)) {
|
||||
actions.push('contains', 'does_not_contain')
|
||||
} else if (field.type === 'number') {
|
||||
actions.push('not_empty', 'empty', 'equal', 'not_equal', 'greater_than', 'less_than')
|
||||
} else {
|
||||
actions.push('not_empty', 'empty')
|
||||
}
|
||||
@@ -234,7 +251,7 @@ export default {
|
||||
delete this.item.conditions
|
||||
}
|
||||
|
||||
this.save()
|
||||
this.$emit('save')
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,18 +6,24 @@
|
||||
<span
|
||||
ref="contenteditable"
|
||||
dir="auto"
|
||||
:contenteditable="editable"
|
||||
style="min-width: 2px"
|
||||
:class="[iconInline ? 'inline' : 'block', hideIcon ? 'focus:block' : '']"
|
||||
class="peer outline-none"
|
||||
:contenteditable="editable && (!editableOnButton || isEditable)"
|
||||
:data-placeholder="placeholder"
|
||||
:data-empty="isEmpty"
|
||||
:style="{ minWidth }"
|
||||
:class="[iconInline ? (isEmpty ? 'inline-block' : 'inline') : 'block', hideIcon ? 'focus:block' : '']"
|
||||
class="peer relative inline-block outline-none before:pointer-events-none before:absolute before:left-0 before:top-0 before:select-none before:whitespace-pre before:text-neutral-400 before:content-[attr(data-placeholder)] before:opacity-0 data-[empty=true]:before:opacity-100"
|
||||
@paste.prevent="onPaste"
|
||||
@keydown.enter.prevent="blurContenteditable"
|
||||
@input="updateInputValue"
|
||||
@cut="updateInputValue"
|
||||
@focus="$emit('focus', $event)"
|
||||
@blur="onBlur"
|
||||
@click="editable && (!editableOnButton || isEditable) ? '' : $emit('click-contenteditable')"
|
||||
>
|
||||
{{ value }}
|
||||
</span>
|
||||
<span
|
||||
v-if="withButton"
|
||||
class="relative inline"
|
||||
:class="{ 'peer-focus:hidden': hideIcon, 'peer-focus:invisible': !hideIcon }"
|
||||
>
|
||||
@@ -28,7 +34,7 @@
|
||||
:class="{ invisible: !editable, 'absolute top-1/2 -translate-y-1/2': !iconInline || floatIcon, 'inline align-bottom': iconInline, 'left-0': floatIcon }"
|
||||
:width="iconWidth + 4"
|
||||
:stroke-width="iconStrokeWidth"
|
||||
@click="[focusContenteditable(), selectOnEditClick && selectContent()]"
|
||||
@click="clickEdit"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
@@ -49,6 +55,16 @@ export default {
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
withButton: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
iconInline: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -74,6 +90,16 @@ export default {
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
editableOnButton: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
minWidth: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '2px'
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -85,21 +111,34 @@ export default {
|
||||
default: 2
|
||||
}
|
||||
},
|
||||
emits: ['update:model-value', 'focus', 'blur'],
|
||||
emits: ['update:model-value', 'focus', 'blur', 'click-contenteditable'],
|
||||
data () {
|
||||
return {
|
||||
isEditable: false,
|
||||
inputValue: '',
|
||||
value: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isEmpty () {
|
||||
return !this.inputValue.replace(/\u200B/g, '').replace(/\u00A0/g, ' ').trim()
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
modelValue: {
|
||||
handler (value) {
|
||||
this.value = value
|
||||
this.value = value || ''
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.updateInputValue()
|
||||
},
|
||||
methods: {
|
||||
updateInputValue () {
|
||||
this.inputValue = this.$refs.contenteditable?.textContent || ''
|
||||
},
|
||||
onPaste (e) {
|
||||
const text = (e.clipboardData || window.clipboardData).getData('text/plain')
|
||||
|
||||
@@ -110,6 +149,20 @@ export default {
|
||||
selection.getRangeAt(0).insertNode(document.createTextNode(text))
|
||||
selection.collapseToEnd()
|
||||
}
|
||||
|
||||
this.updateInputValue()
|
||||
},
|
||||
clickEdit (e) {
|
||||
this.focusContenteditable()
|
||||
|
||||
if (this.selectOnEditClick) {
|
||||
this.selectContent()
|
||||
}
|
||||
},
|
||||
setText (text) {
|
||||
this.$refs.contenteditable.innerText = text
|
||||
|
||||
this.updateInputValue()
|
||||
},
|
||||
selectContent () {
|
||||
const el = this.$refs.contenteditable
|
||||
@@ -126,13 +179,24 @@ export default {
|
||||
},
|
||||
onBlur (e) {
|
||||
setTimeout(() => {
|
||||
this.value = this.$refs.contenteditable.innerText.trim() || this.modelValue
|
||||
this.$emit('update:model-value', this.value)
|
||||
if (this.$refs.contenteditable) {
|
||||
this.value = this.$refs.contenteditable.innerText.trim() || this.modelValue
|
||||
this.$emit('update:model-value', this.value)
|
||||
}
|
||||
|
||||
this.$emit('blur', e)
|
||||
|
||||
this.isEditable = false
|
||||
}, 1)
|
||||
},
|
||||
focusContenteditable () {
|
||||
this.$refs.contenteditable.focus()
|
||||
this.isEditable = true
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.$refs.contenteditable.focus()
|
||||
|
||||
this.updateInputValue()
|
||||
})
|
||||
},
|
||||
blurContenteditable () {
|
||||
this.$refs.contenteditable.blur()
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
<template>
|
||||
<div
|
||||
class="list-field group"
|
||||
>
|
||||
<div
|
||||
class="border border-dashed border-base-300 hover:border-base-content/20 rounded relative group fields-list-item transition-colors"
|
||||
:style="{ backgroundColor: backgroundColor }"
|
||||
>
|
||||
<div class="flex items-center justify-between relative group/contenteditable-container">
|
||||
<div
|
||||
v-if="!isNew"
|
||||
class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer"
|
||||
@click="$emit('click', field)"
|
||||
/>
|
||||
<div
|
||||
class="absolute top-0 bottom-0 left-0 flex items-center transition-all cursor-grab group-hover:bg-base-200/50"
|
||||
@click="$emit('click', field)"
|
||||
>
|
||||
<IconDrag style="margin-left: 1px" />
|
||||
</div>
|
||||
<div class="flex items-center p-1 pl-6 space-x-1">
|
||||
<FieldType
|
||||
v-model="field.type"
|
||||
:editable="false"
|
||||
:button-width="20"
|
||||
@click="$emit('click', field)"
|
||||
/>
|
||||
<Contenteditable
|
||||
ref="name"
|
||||
:model-value="field.name"
|
||||
:placeholder="'Field Name'"
|
||||
:icon-inline="true"
|
||||
:icon-width="18"
|
||||
:min-width="isNew ? '100px' : '2px'"
|
||||
:icon-stroke-width="1.6"
|
||||
:editable-on-button="!isNew"
|
||||
:with-button="!isNew"
|
||||
:class="{ 'cursor-pointer': !isNew }"
|
||||
@click-contenteditable="$emit('click', field)"
|
||||
@focus="onNameFocus"
|
||||
@blur="onNameBlur"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center space-x-1"
|
||||
>
|
||||
<PaymentSettings
|
||||
v-if="field.type === 'payment' && !isNew"
|
||||
:field="field"
|
||||
:with-condition="false"
|
||||
:with-force-open="false"
|
||||
@click-description="isShowDescriptionModal = true"
|
||||
@click-formula="isShowFormulaModal = true"
|
||||
/>
|
||||
<span
|
||||
v-else-if="!isNew"
|
||||
class="dropdown dropdown-end field-settings-dropdown"
|
||||
@mouseenter="renderDropdown = true"
|
||||
@touchstart="renderDropdown = true"
|
||||
>
|
||||
<label
|
||||
tabindex="0"
|
||||
:title="t('settings')"
|
||||
class="cursor-pointer text-transparent group-hover:text-base-content"
|
||||
>
|
||||
<IconSettings
|
||||
:width="18"
|
||||
:stroke-width="1.6"
|
||||
/>
|
||||
</label>
|
||||
<ul
|
||||
v-if="renderDropdown"
|
||||
tabindex="0"
|
||||
class="mt-1.5 dropdown-content menu menu-xs p-2 shadow rounded-box w-52 z-10"
|
||||
:style="{ backgroundColor: dropdownBgColor }"
|
||||
draggable="true"
|
||||
@dragstart.prevent.stop
|
||||
@click="closeDropdown"
|
||||
>
|
||||
<FieldSettings
|
||||
:field="field"
|
||||
:with-signature-id="withSignatureId"
|
||||
:with-prefillable="withPrefillable"
|
||||
:background-color="dropdownBgColor"
|
||||
:with-areas="false"
|
||||
:with-copy-to-all-pages="false"
|
||||
:with-condition="false"
|
||||
@click-formula="isShowFormulaModal = true"
|
||||
@click-font="isShowFontModal = true"
|
||||
@click-description="isShowDescriptionModal = true"
|
||||
@save="$emit('save')"
|
||||
/>
|
||||
</ul>
|
||||
</span>
|
||||
<button
|
||||
v-if="isNew && !$refs.name"
|
||||
class="relative text-base-content pr-1 field-save-button"
|
||||
:title="t('save')"
|
||||
@click="field.name ? $emit('save', field) : focusName()"
|
||||
>
|
||||
<IconCheck
|
||||
:width="18"
|
||||
:stroke-width="2"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="relative group-hover:text-base-content pr-1 field-remove-button"
|
||||
:class="isNew ? 'text-base-content' : 'text-transparent group-hover:text-base-content'"
|
||||
:title="t('remove')"
|
||||
@click="onRemoveClick"
|
||||
>
|
||||
<IconTrashX
|
||||
:width="18"
|
||||
:stroke-width="1.6"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Teleport
|
||||
v-if="isShowFormulaModal"
|
||||
:to="modalContainerEl"
|
||||
>
|
||||
<FormulaModal
|
||||
:field="field"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@save="$emit('save')"
|
||||
@close="isShowFormulaModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
<Teleport
|
||||
v-if="isShowFontModal"
|
||||
:to="modalContainerEl"
|
||||
>
|
||||
<FontModal
|
||||
:field="field"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@save="$emit('save')"
|
||||
@close="isShowFontModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
<Teleport
|
||||
v-if="isShowDescriptionModal"
|
||||
:to="modalContainerEl"
|
||||
>
|
||||
<DescriptionModal
|
||||
:field="field"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@save="$emit('save')"
|
||||
@close="isShowDescriptionModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Contenteditable from './contenteditable'
|
||||
import FieldType from './field_type'
|
||||
import PaymentSettings from './payment_settings'
|
||||
import FieldSettings from './field_settings'
|
||||
import FormulaModal from './formula_modal'
|
||||
import FontModal from './font_modal'
|
||||
import DescriptionModal from './description_modal'
|
||||
import { IconTrashX, IconSettings, IconCheck } from '@tabler/icons-vue'
|
||||
import IconDrag from './icon_drag'
|
||||
|
||||
export default {
|
||||
name: 'CustomField',
|
||||
components: {
|
||||
Contenteditable,
|
||||
IconSettings,
|
||||
IconCheck,
|
||||
FieldSettings,
|
||||
PaymentSettings,
|
||||
IconDrag,
|
||||
FormulaModal,
|
||||
FontModal,
|
||||
DescriptionModal,
|
||||
IconTrashX,
|
||||
FieldType
|
||||
},
|
||||
inject: ['backgroundColor', 't'],
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isNew: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
withSignatureId: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
withPrefillable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['remove', 'save', 'click'],
|
||||
data () {
|
||||
return {
|
||||
isShowFormulaModal: false,
|
||||
isShowFontModal: false,
|
||||
isShowDescriptionModal: false,
|
||||
renderDropdown: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
dropdownBgColor () {
|
||||
return ['', null, 'transparent'].includes(this.backgroundColor) ? 'white' : this.backgroundColor
|
||||
},
|
||||
modalContainerEl () {
|
||||
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.field.preferences ||= {}
|
||||
},
|
||||
mounted () {
|
||||
if (this.isNew) {
|
||||
this.focusName()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
buildDefaultName () {
|
||||
return this.t('custom')
|
||||
},
|
||||
focusName () {
|
||||
setTimeout(() => {
|
||||
this.$refs.name.clickEdit()
|
||||
}, 1)
|
||||
},
|
||||
onNameFocus (e) {
|
||||
if (!this.field.name) {
|
||||
setTimeout(() => {
|
||||
this.$refs.name.$refs.contenteditable.innerText = ' '
|
||||
}, 1)
|
||||
}
|
||||
},
|
||||
closeDropdown () {
|
||||
this.$el.getRootNode().activeElement.blur()
|
||||
},
|
||||
onRemoveClick () {
|
||||
if (this.isNew || window.confirm(this.t('are_you_sure_'))) {
|
||||
this.$emit('remove', this.field)
|
||||
}
|
||||
},
|
||||
onNameBlur (e) {
|
||||
const text = this.$refs.name.$refs.contenteditable.innerText.trim()
|
||||
|
||||
if (text) {
|
||||
this.field.name = text
|
||||
} else {
|
||||
this.$refs.name.setText(this.field.name)
|
||||
}
|
||||
|
||||
if (this.field.name) {
|
||||
this.$emit('save')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -9,7 +9,7 @@
|
||||
<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 class="modal-title">
|
||||
{{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field, template.fields) }}
|
||||
{{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field) }}
|
||||
</span>
|
||||
<a
|
||||
href="#"
|
||||
@@ -67,7 +67,7 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'DescriptionModal',
|
||||
inject: ['t', 'save', 'template'],
|
||||
inject: ['t', 'template'],
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
@@ -88,7 +88,7 @@ export default {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['close'],
|
||||
emits: ['close', 'save'],
|
||||
data () {
|
||||
return {
|
||||
description: this.field.description,
|
||||
@@ -103,7 +103,7 @@ export default {
|
||||
this.field.description = this.description
|
||||
this.field.title = this.title
|
||||
|
||||
this.save()
|
||||
this.$emit('save')
|
||||
this.$emit('close')
|
||||
},
|
||||
resizeTextarea () {
|
||||
|
||||
@@ -13,17 +13,28 @@
|
||||
:with-signature-id="withSignatureId"
|
||||
:with-prefillable="withPrefillable"
|
||||
:is-drag="isDrag"
|
||||
:is-mobile="isMobile"
|
||||
:with-field-placeholder="withFieldPlaceholder"
|
||||
:default-fields="defaultFields"
|
||||
:drag-field-placeholder="dragFieldPlaceholder"
|
||||
:default-submitters="defaultSubmitters"
|
||||
:draw-field="drawField"
|
||||
:draw-field-type="drawFieldType"
|
||||
:draw-custom-field="drawCustomField"
|
||||
:selected-submitter="selectedSubmitter"
|
||||
:total-pages="sortedPreviewImages.length"
|
||||
:image="image"
|
||||
@drop-field="$emit('drop-field', {...$event, attachment_uuid: document.uuid })"
|
||||
:attachment-uuid="document.uuid"
|
||||
:with-fields-detection="withFieldsDetection"
|
||||
@drop-field="$emit('drop-field', { ...$event, attachment_uuid: document.uuid })"
|
||||
@remove-area="$emit('remove-area', $event)"
|
||||
@copy-field="$emit('copy-field', $event)"
|
||||
@paste-field="$emit('paste-field', { ...$event, attachment_uuid: document.uuid })"
|
||||
@add-custom-field="$emit('add-custom-field', $event)"
|
||||
@set-draw="$emit('set-draw', $event)"
|
||||
@copy-selected-areas="$emit('copy-selected-areas')"
|
||||
@delete-selected-areas="$emit('delete-selected-areas')"
|
||||
@autodetect-fields="$emit('autodetect-fields', $event)"
|
||||
@scroll-to="scrollToArea"
|
||||
@draw="$emit('draw', { area: {...$event.area, attachment_uuid: document.uuid }, isTooSmall: $event.isTooSmall })"
|
||||
/>
|
||||
@@ -88,6 +99,11 @@ export default {
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
isMobile: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
allowDraw: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -107,6 +123,11 @@ export default {
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
drawCustomField: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
baseUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
@@ -116,9 +137,14 @@ export default {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
withFieldsDetection: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['draw', 'drop-field', 'remove-area'],
|
||||
emits: ['draw', 'drop-field', 'remove-area', 'paste-field', 'copy-field', 'copy-selected-areas', 'delete-selected-areas', 'autodetect-fields', 'add-custom-field', 'set-draw'],
|
||||
data () {
|
||||
return {
|
||||
pageRefs: []
|
||||
@@ -160,7 +186,15 @@ export default {
|
||||
methods: {
|
||||
scrollToArea (area) {
|
||||
this.$nextTick(() => {
|
||||
this.pageRefs[area.page].areaRefs.find((e) => e.area === area).$el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
const pageRef = this.pageRefs[area.page]
|
||||
|
||||
if (pageRef && pageRef.areaRefs) {
|
||||
const areaRef = pageRef.areaRefs.find((e) => e.area === area)
|
||||
|
||||
if (areaRef && areaRef.$el) {
|
||||
areaRef.$el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
setPageRefs (el) {
|
||||
|
||||
@@ -8,6 +8,13 @@
|
||||
class="fixed z-20 pointer-events-none"
|
||||
:editable="false"
|
||||
/>
|
||||
<CustomField
|
||||
v-else-if="dragPlaceholder && isCustom && !isMask && field"
|
||||
ref="dragPlaceholder"
|
||||
:style="dragPlaceholderStyle"
|
||||
:field="field"
|
||||
class="fixed z-20 pointer-events-none opacity-90"
|
||||
/>
|
||||
<div
|
||||
v-else-if="dragPlaceholder && (isDefault || isRequired) && !isMask && field"
|
||||
ref="dragPlaceholder"
|
||||
@@ -57,6 +64,7 @@
|
||||
|
||||
<script>
|
||||
import Field from './field'
|
||||
import CustomField from './custom_field'
|
||||
import IconDrag from './icon_drag'
|
||||
import FieldType from './field_type'
|
||||
|
||||
@@ -64,6 +72,7 @@ export default {
|
||||
name: 'DragPlaceholder',
|
||||
components: {
|
||||
Field,
|
||||
CustomField,
|
||||
IconDrag
|
||||
},
|
||||
inject: ['t', 'backgroundColor'],
|
||||
@@ -87,6 +96,11 @@ export default {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
isCustom: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="list-field group mb-2"
|
||||
class="list-field group"
|
||||
>
|
||||
<div
|
||||
class="border border-base-300 rounded relative group fields-list-item"
|
||||
@@ -18,7 +18,7 @@
|
||||
:button-width="20"
|
||||
:menu-classes="'mt-1.5'"
|
||||
:menu-style="{ backgroundColor: dropdownBgColor }"
|
||||
@update:model-value="[maybeUpdateOptions(), save()]"
|
||||
@update:model-value="[maybeUpdateOptions(), $emit('save')]"
|
||||
@click="scrollToFirstArea"
|
||||
/>
|
||||
<Contenteditable
|
||||
@@ -92,9 +92,12 @@
|
||||
<PaymentSettings
|
||||
v-if="field.type === 'payment'"
|
||||
:field="field"
|
||||
:with-custom-fields="withCustomFields"
|
||||
@click-condition="isShowConditionsModal = true"
|
||||
@click-description="isShowDescriptionModal = true"
|
||||
@add-custom-field="$emit('add-custom-field', $event)"
|
||||
@click-formula="isShowFormulaModal = true"
|
||||
@save="$emit('save')"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
@@ -128,12 +131,15 @@
|
||||
:with-signature-id="withSignatureId"
|
||||
:with-prefillable="withPrefillable"
|
||||
:background-color="dropdownBgColor"
|
||||
:with-custom-fields="withCustomFields"
|
||||
@click-formula="isShowFormulaModal = true"
|
||||
@click-font="isShowFontModal = true"
|
||||
@click-description="isShowDescriptionModal = true"
|
||||
@click-condition="isShowConditionsModal = true"
|
||||
@set-draw="$emit('set-draw', $event)"
|
||||
@add-custom-field="$emit('add-custom-field', $event)"
|
||||
@remove-area="removeArea"
|
||||
@save="$emit('save')"
|
||||
@scroll-to="$emit('scroll-to', $event)"
|
||||
/>
|
||||
</ul>
|
||||
@@ -154,8 +160,10 @@
|
||||
v-if="field.options && withOptions && (isExpandOptions || field.options.length < 5)"
|
||||
ref="options"
|
||||
class="border-t border-base-300 mx-2 pt-2 space-y-1.5"
|
||||
draggable="true"
|
||||
@dragover="onOptionDragover"
|
||||
@drop="reorderOptions"
|
||||
@dragstart.prevent.stop
|
||||
>
|
||||
<div
|
||||
v-for="(option, index) in field.options"
|
||||
@@ -184,7 +192,8 @@
|
||||
required
|
||||
:placeholder="`${t('option')} ${index + 1}`"
|
||||
@keydown.enter="option.value ? addOptionAt(index + 1) : null"
|
||||
@blur="save"
|
||||
@blur="$emit('save')"
|
||||
@paste="onOptionPaste($event, index)"
|
||||
>
|
||||
<button
|
||||
:title="t('draw')"
|
||||
@@ -208,7 +217,8 @@
|
||||
dir="auto"
|
||||
@keydown.enter="option.value ? addOptionAt(index + 1) : null"
|
||||
@focus="maybeFocusOnOptionArea(option)"
|
||||
@blur="save"
|
||||
@blur="$emit('save')"
|
||||
@paste="onOptionPaste($event, index)"
|
||||
>
|
||||
<button
|
||||
v-if="editable && !defaultField"
|
||||
@@ -259,6 +269,7 @@
|
||||
:editable="editable && !defaultField"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@save="$emit('save')"
|
||||
@close="isShowFormulaModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
@@ -271,6 +282,7 @@
|
||||
:editable="editable && !defaultField"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@save="$emit('save')"
|
||||
@close="isShowFontModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
@@ -282,6 +294,7 @@
|
||||
:item="field"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@save="$emit('save')"
|
||||
@close="isShowConditionsModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
@@ -294,6 +307,7 @@
|
||||
:editable="editable && !defaultField"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@save="$emit('save')"
|
||||
@close="isShowDescriptionModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
@@ -330,7 +344,7 @@ export default {
|
||||
IconMathFunction,
|
||||
FieldType
|
||||
},
|
||||
inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef', 't', 'locale'],
|
||||
inject: ['template', 'backgroundColor', 'selectedAreasRef', 't', 'locale', 'getFieldTypeIndex'],
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
@@ -341,6 +355,11 @@ export default {
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
withCustomFields: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
withPrefillable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -362,12 +381,11 @@ export default {
|
||||
default: true
|
||||
}
|
||||
},
|
||||
emits: ['set-draw', 'remove', 'scroll-to'],
|
||||
emits: ['set-draw', 'remove', 'scroll-to', 'save', 'add-custom-field'],
|
||||
data () {
|
||||
return {
|
||||
isExpandOptions: false,
|
||||
isNameFocus: false,
|
||||
showPaymentModal: false,
|
||||
isShowFormulaModal: false,
|
||||
isShowFontModal: false,
|
||||
isShowConditionsModal: false,
|
||||
@@ -398,7 +416,7 @@ export default {
|
||||
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
||||
},
|
||||
defaultName () {
|
||||
return this.buildDefaultName(this.field, this.template.fields)
|
||||
return this.buildDefaultName(this.field)
|
||||
},
|
||||
areas () {
|
||||
return this.field.areas || []
|
||||
@@ -416,9 +434,9 @@ export default {
|
||||
removeArea (area) {
|
||||
this.field.areas.splice(this.field.areas.indexOf(area), 1)
|
||||
|
||||
this.save()
|
||||
this.$emit('save')
|
||||
},
|
||||
buildDefaultName (field, fields) {
|
||||
buildDefaultName (field) {
|
||||
if (field.type === 'payment' && field.preferences?.price && !field.preferences?.formula) {
|
||||
const { price, currency } = field.preferences || {}
|
||||
|
||||
@@ -429,7 +447,7 @@ export default {
|
||||
|
||||
return `${this.fieldNames[field.type]} ${formattedPrice}`
|
||||
} else {
|
||||
const typeIndex = fields.filter((f) => f.type === field.type).indexOf(field)
|
||||
const typeIndex = this.getFieldTypeIndex(field)
|
||||
|
||||
if (field.type === 'heading' || field.type === 'strikethrough') {
|
||||
return `${this.fieldNames[field.type]} ${typeIndex + 1}`
|
||||
@@ -451,7 +469,7 @@ export default {
|
||||
const area = this.field.areas.find((a) => a.option_uuid === option.uuid)
|
||||
|
||||
if (area) {
|
||||
this.selectedAreaRef.value = area
|
||||
this.selectedAreasRef.value = [area]
|
||||
}
|
||||
},
|
||||
scrollToFirstArea () {
|
||||
@@ -460,6 +478,35 @@ export default {
|
||||
closeDropdown () {
|
||||
this.$el.getRootNode().activeElement.blur()
|
||||
},
|
||||
onOptionPaste (e, index) {
|
||||
const text = e.clipboardData.getData('text')
|
||||
|
||||
if (text.includes('\n')) {
|
||||
e.preventDefault()
|
||||
|
||||
this.isExpandOptions = true
|
||||
|
||||
const lines = text.split(/\r?\n/).map((l) => l.trim()).filter((l) => l)
|
||||
|
||||
if (lines.length > 0) {
|
||||
const currentOption = this.field.options[index]
|
||||
|
||||
currentOption.value = (currentOption.value + lines[0]).trim()
|
||||
|
||||
const newOptions = lines.slice(1).map((line) => ({ value: line, uuid: v4() }))
|
||||
|
||||
this.field.options.splice(index + 1, 0, ...newOptions)
|
||||
|
||||
this.$nextTick(() => {
|
||||
const inputs = this.$refs.options.querySelectorAll('input')
|
||||
|
||||
inputs[index + newOptions.length]?.focus()
|
||||
})
|
||||
|
||||
this.$emit('save')
|
||||
}
|
||||
}
|
||||
},
|
||||
addOptionAt (index) {
|
||||
this.isExpandOptions = true
|
||||
|
||||
@@ -473,7 +520,7 @@ export default {
|
||||
inputs[insertAt]?.focus()
|
||||
})
|
||||
|
||||
this.save()
|
||||
this.$emit('save')
|
||||
},
|
||||
removeOption (option) {
|
||||
this.field.options.splice(this.field.options.indexOf(option), 1)
|
||||
@@ -484,7 +531,7 @@ export default {
|
||||
this.field.areas.splice(this.field.areas.findIndex((a) => a.option_uuid === option.uuid), 1)
|
||||
}
|
||||
|
||||
this.save()
|
||||
this.$emit('save')
|
||||
},
|
||||
maybeUpdateOptions () {
|
||||
delete this.field.default_value
|
||||
@@ -526,7 +573,7 @@ export default {
|
||||
|
||||
this.isNameFocus = false
|
||||
|
||||
this.save()
|
||||
this.$emit('save')
|
||||
},
|
||||
onOptionDragstart (event, option) {
|
||||
this.optionDragRef = option
|
||||
@@ -587,7 +634,7 @@ export default {
|
||||
if (newOrder.length === this.field.options.length) {
|
||||
this.field.options.splice(0, this.field.options.length, ...newOrder)
|
||||
|
||||
this.save()
|
||||
this.$emit('save')
|
||||
}
|
||||
|
||||
this.optionDragRef = null
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<Teleport :to="modalContainerEl">
|
||||
<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 class="modal-title">
|
||||
{{ title }}
|
||||
</span>
|
||||
<a
|
||||
href="#"
|
||||
class="text-xl modal-close-button"
|
||||
@click.prevent="$emit('close')"
|
||||
>×</a>
|
||||
</div>
|
||||
<form @submit.prevent="$emit('save')">
|
||||
<slot />
|
||||
<button
|
||||
class="base-button w-full mt-4 modal-save-button"
|
||||
type="submit"
|
||||
>
|
||||
{{ t('save') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ContextModal',
|
||||
inject: ['t'],
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
modalContainerEl: {
|
||||
type: Element,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['close', 'save']
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<button
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
|
||||
@click.stop="isOpen ? close() : open()"
|
||||
>
|
||||
<span class="flex items-center space-x-2">
|
||||
<component
|
||||
:is="icon || 'span'"
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
<span>{{ label }}</span>
|
||||
</span>
|
||||
<IconChevronRight class="w-4 h-4" />
|
||||
</button>
|
||||
<div
|
||||
v-if="isOpen"
|
||||
ref="submenu"
|
||||
class="absolute p-1 z-50 left-full bg-white shadow-lg rounded-lg border border-neutral-200 cursor-default"
|
||||
style="min-width: 170px"
|
||||
:style="submenuStyle"
|
||||
:class="menuClass"
|
||||
@click.stop
|
||||
>
|
||||
<slot>
|
||||
<button
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between space-x-2 text-sm cursor-pointer"
|
||||
@click="handleSelect(option.value)"
|
||||
>
|
||||
<span class="whitespace-nowrap">{{ option.label }}</span>
|
||||
<IconCheck
|
||||
v-if="modelValue === option.value"
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
</button>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IconChevronRight, IconCheck } from '@tabler/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'ContextSubmenu',
|
||||
components: {
|
||||
IconChevronRight,
|
||||
IconCheck
|
||||
},
|
||||
props: {
|
||||
icon: {
|
||||
type: [Function],
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
default: null
|
||||
},
|
||||
menuClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
emits: ['select', 'update:modelValue'],
|
||||
data () {
|
||||
return {
|
||||
isOpen: false,
|
||||
topOffset: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
submenuStyle () {
|
||||
return {
|
||||
top: this.topOffset + 'px'
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeUnmount () {
|
||||
this.clearTimeout()
|
||||
},
|
||||
methods: {
|
||||
handleMouseEnter () {
|
||||
clearTimeout(this.closeTimeout)
|
||||
|
||||
this.openTimeout = setTimeout(() => this.open(), 200)
|
||||
},
|
||||
handleMouseLeave () {
|
||||
clearTimeout(this.openTimeout)
|
||||
|
||||
this.closeTimeout = setTimeout(() => this.close(), 200)
|
||||
},
|
||||
open () {
|
||||
this.clearTimeout()
|
||||
|
||||
this.isOpen = true
|
||||
this.topOffset = 0
|
||||
|
||||
this.$nextTick(() => setTimeout(() => this.adjustPosition(), 0))
|
||||
},
|
||||
clearTimeout () {
|
||||
if (this.openTimeout) {
|
||||
clearTimeout(this.openTimeout)
|
||||
}
|
||||
|
||||
if (this.closeTimeout) {
|
||||
clearTimeout(this.closeTimeout)
|
||||
}
|
||||
},
|
||||
close () {
|
||||
this.clearTimeout()
|
||||
|
||||
this.isOpen = false
|
||||
},
|
||||
handleSelect (value) {
|
||||
this.$emit('select', value)
|
||||
this.$emit('update:modelValue', value)
|
||||
},
|
||||
adjustPosition () {
|
||||
if (!this.$refs.submenu) return
|
||||
|
||||
const rect = this.$refs.submenu.getBoundingClientRect()
|
||||
const overflow = rect.bottom - window.innerHeight
|
||||
|
||||
if (overflow > 0) {
|
||||
this.topOffset = -overflow - 4
|
||||
} else {
|
||||
this.topOffset = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -7,10 +7,10 @@
|
||||
<select
|
||||
:placeholder="t('method')"
|
||||
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
|
||||
@change="[field.preferences ||= {}, field.preferences.method = $event.target.value, save()]"
|
||||
@change="[field.preferences ||= {}, field.preferences.method = $event.target.value, $emit('save')]"
|
||||
>
|
||||
<option
|
||||
v-for="method in ['QeS', 'AeS']"
|
||||
v-for="method in verificationMethods"
|
||||
:key="method"
|
||||
:value="method.toLowerCase()"
|
||||
:selected="method.toLowerCase() === field.preferences?.method || (method === 'QeS' && !field.preferences?.method)"
|
||||
@@ -26,32 +26,6 @@
|
||||
{{ t('method') }}
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
v-if="['cells'].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 bg-transparent"
|
||||
@change="[field.preferences ||= {}, field.preferences.align = $event.target.value, save()]"
|
||||
>
|
||||
<option
|
||||
v-for="value in ['left', 'right', field.type === 'cells' ? null : 'center'].filter(Boolean)"
|
||||
:key="value"
|
||||
:selected="field.preferences?.align ? value === field.preferences.align : value === 'left'"
|
||||
:value="value"
|
||||
>
|
||||
{{ t(value) }}
|
||||
</option>
|
||||
</select>
|
||||
<label
|
||||
:style="{ backgroundColor }"
|
||||
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||
style="font-size: 8px"
|
||||
>
|
||||
{{ t('align') }}
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
v-if="['select', 'radio'].includes(field.type) && !defaultField"
|
||||
class="py-1.5 px-1 relative"
|
||||
@@ -61,7 +35,7 @@
|
||||
:placeholder="t('default_value')"
|
||||
dir="auto"
|
||||
class="select select-bordered select-xs w-full max-w-xs h-7 !outline-0 font-normal bg-transparent"
|
||||
@change="[field.default_value = $event.target.value, !field.default_value && delete field.default_value, save()]"
|
||||
@change="[field.default_value = $event.target.value, !field.default_value && delete field.default_value, $emit('save')]"
|
||||
>
|
||||
<option
|
||||
value=""
|
||||
@@ -97,7 +71,7 @@
|
||||
dir="auto"
|
||||
:type="field.type"
|
||||
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0 bg-transparent"
|
||||
@blur="save"
|
||||
@blur="$emit('save')"
|
||||
>
|
||||
<label
|
||||
v-if="field.default_value"
|
||||
@@ -159,7 +133,7 @@
|
||||
:value="lengthValidation.min"
|
||||
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
|
||||
@input="field.validation.pattern = `.{${$event.target.value || 0},${lengthValidation.max || ''}}`"
|
||||
@blur="save"
|
||||
@blur="$emit('save')"
|
||||
>
|
||||
<label
|
||||
v-if="lengthValidation.min"
|
||||
@@ -178,7 +152,7 @@
|
||||
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
|
||||
:value="lengthValidation.max"
|
||||
@input="field.validation.pattern = `.{${lengthValidation.min},${$event.target.value || ''}}`"
|
||||
@blur="save"
|
||||
@blur="$emit('save')"
|
||||
>
|
||||
<label
|
||||
v-if="lengthValidation.max"
|
||||
@@ -203,7 +177,7 @@
|
||||
:value="field.validation?.min"
|
||||
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
|
||||
@input="[field.validation ||= {}, $event.target.value ? field.validation.min = $event.target.value : delete field.validation.min]"
|
||||
@blur="save"
|
||||
@blur="$emit('save')"
|
||||
>
|
||||
<label
|
||||
v-if="field.validation?.min"
|
||||
@@ -222,7 +196,7 @@
|
||||
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
|
||||
:value="field.validation?.max"
|
||||
@input="[field.validation ||= {}, $event.target.value ? field.validation.max = $event.target.value : delete field.validation.max]"
|
||||
@blur="save"
|
||||
@blur="$emit('save')"
|
||||
>
|
||||
<label
|
||||
v-if="field.validation?.max"
|
||||
@@ -242,7 +216,7 @@
|
||||
<select
|
||||
:placeholder="t('format')"
|
||||
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
|
||||
@change="[field.preferences ||= {}, field.preferences.format = $event.target.value, save()]"
|
||||
@change="[field.preferences ||= {}, field.preferences.format = $event.target.value, $emit('save')]"
|
||||
>
|
||||
<option
|
||||
v-for="format in numberFormats"
|
||||
@@ -272,7 +246,7 @@
|
||||
:placeholder="t('regexp_validation')"
|
||||
dir="auto"
|
||||
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0 bg-transparent"
|
||||
@blur="save"
|
||||
@blur="$emit('save')"
|
||||
>
|
||||
<label
|
||||
v-if="field.validation.pattern"
|
||||
@@ -293,7 +267,7 @@
|
||||
:placeholder="t('error_message')"
|
||||
dir="auto"
|
||||
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0 bg-transparent"
|
||||
@blur="save"
|
||||
@blur="$emit('save')"
|
||||
>
|
||||
<label
|
||||
v-if="field.validation.message"
|
||||
@@ -313,7 +287,7 @@
|
||||
v-model="field.preferences.format"
|
||||
:placeholder="t('format')"
|
||||
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
|
||||
@change="save"
|
||||
@change="$emit('save')"
|
||||
>
|
||||
<option
|
||||
v-for="format in dateFormats"
|
||||
@@ -339,7 +313,7 @@
|
||||
<select
|
||||
:placeholder="t('format')"
|
||||
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
|
||||
@change="[field.preferences.format = $event.target.value, save()]"
|
||||
@change="[field.preferences.format = $event.target.value, $emit('save')]"
|
||||
>
|
||||
<option
|
||||
value="any"
|
||||
@@ -348,7 +322,7 @@
|
||||
{{ t('any') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="type in ['drawn', 'typed', 'drawn_or_typed', 'drawn_or_upload', 'upload']"
|
||||
v-for="type in signatureFormats"
|
||||
:key="type"
|
||||
:value="type"
|
||||
:selected="field.preferences?.format === type"
|
||||
@@ -374,7 +348,7 @@
|
||||
type="checkbox"
|
||||
:disabled="!editable || (defaultField && [true, false].includes(defaultField.required))"
|
||||
class="toggle toggle-xs"
|
||||
@change="[field.preferences ||= {}, field.preferences.with_signature_id = $event.target.checked, save()]"
|
||||
@change="[field.preferences ||= {}, field.preferences.with_signature_id = $event.target.checked, $emit('save')]"
|
||||
>
|
||||
<span class="label-text">{{ t('signature_id') }}</span>
|
||||
</label>
|
||||
@@ -389,7 +363,7 @@
|
||||
type="checkbox"
|
||||
:disabled="!editable || (defaultField && [true, false].includes(defaultField.required))"
|
||||
class="toggle toggle-xs"
|
||||
@update:model-value="save"
|
||||
@update:model-value="$emit('save')"
|
||||
>
|
||||
<span class="label-text">{{ t('required') }}</span>
|
||||
</label>
|
||||
@@ -403,7 +377,7 @@
|
||||
: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()]"
|
||||
@change="[field.preferences ||= {}, field.preferences.with_logo = field.preferences.with_logo == false, $emit('save')]"
|
||||
>
|
||||
<span class="label-text">{{ t('with_logo') }}</span>
|
||||
</label>
|
||||
@@ -417,7 +391,7 @@
|
||||
v-model="field.default_value"
|
||||
type="checkbox"
|
||||
class="toggle toggle-xs"
|
||||
@update:model-value="[field.default_value = $event, field.readonly = $event, save()]"
|
||||
@update:model-value="[field.default_value = $event, field.readonly = $event, $emit('save')]"
|
||||
>
|
||||
<span class="label-text">{{ t('checked') }}</span>
|
||||
</label>
|
||||
@@ -431,13 +405,13 @@
|
||||
v-model="field.readonly"
|
||||
type="checkbox"
|
||||
class="toggle toggle-xs"
|
||||
@update:model-value="[field.default_value = $event ? '{{date}}' : null, field.readonly = $event, save()]"
|
||||
@update:model-value="[field.default_value = $event ? '{{date}}' : null, field.readonly = $event, $emit('save')]"
|
||||
>
|
||||
<span class="label-text">{{ t('set_signing_date') }}</span>
|
||||
</label>
|
||||
</li>
|
||||
<li
|
||||
v-if="['text', 'number'].includes(field.type)"
|
||||
v-if="['text', 'number', 'radio', 'multiple', 'select'].includes(field.type)"
|
||||
@click.stop
|
||||
>
|
||||
<label class="cursor-pointer py-1.5">
|
||||
@@ -446,13 +420,13 @@
|
||||
type="checkbox"
|
||||
class="toggle toggle-xs"
|
||||
:disabled="!editable || (defaultField && [true, false].includes(defaultField.readonly))"
|
||||
@update:model-value="save"
|
||||
@update:model-value="$emit('save')"
|
||||
>
|
||||
<span class="label-text">{{ t('read_only') }}</span>
|
||||
</label>
|
||||
</li>
|
||||
<li
|
||||
v-if="withPrefillable && ['text', 'number', 'cells', 'date', 'checkbox', 'select', 'radio', 'phone'].includes(field['type'])"
|
||||
v-if="withPrefillable && prefillableFieldTypes.includes(field['type'])"
|
||||
@click.stop
|
||||
>
|
||||
<label class="cursor-pointer py-1.5">
|
||||
@@ -461,7 +435,7 @@
|
||||
type="checkbox"
|
||||
:disabled="!editable || (defaultField && [true, false].includes(defaultField.prefillable))"
|
||||
class="toggle toggle-xs"
|
||||
@update:model-value="save"
|
||||
@update:model-value="$emit('save')"
|
||||
>
|
||||
<span class="label-text">{{ t('prefillable') }}</span>
|
||||
</label>
|
||||
@@ -470,7 +444,7 @@
|
||||
v-if="field.type != 'stamp'"
|
||||
class="pb-0.5 mt-0.5"
|
||||
>
|
||||
<li v-if="['text', 'number', 'date', 'select', 'heading'].includes(field.type)">
|
||||
<li v-if="['text', 'number', 'date', 'select', 'heading', 'cells'].includes(field.type)">
|
||||
<label
|
||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||
@click="$emit('click-font')"
|
||||
@@ -499,7 +473,7 @@
|
||||
</label>
|
||||
</li>
|
||||
<li
|
||||
v-if="field.type != 'stamp' && field.type != 'heading'"
|
||||
v-if="withCondition && field.type != 'stamp' && field.type != 'heading'"
|
||||
>
|
||||
<label
|
||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||
@@ -526,7 +500,10 @@
|
||||
</span>
|
||||
</label>
|
||||
</li>
|
||||
<hr class="pb-0.5 mt-0.5">
|
||||
<hr
|
||||
v-if="withCopyToAllPages || withAreas || withCustomFields"
|
||||
class="pb-0.5 mt-0.5"
|
||||
>
|
||||
<template v-if="withAreas">
|
||||
<li
|
||||
v-for="(area, index) in sortedAreas"
|
||||
@@ -564,7 +541,7 @@
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
<li v-if="field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells', 'stamp'].includes(field.type)">
|
||||
<li v-if="withCopyToAllPages && field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells', 'stamp'].includes(field.type)">
|
||||
<a
|
||||
href="#"
|
||||
class="text-sm py-1 px-2"
|
||||
@@ -577,10 +554,23 @@
|
||||
{{ t('copy_to_all_pages') }}
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="withCustomFields">
|
||||
<a
|
||||
href="#"
|
||||
class="text-sm py-1 px-2"
|
||||
@click.prevent="$emit('add-custom-field', field)"
|
||||
>
|
||||
<IconForms
|
||||
:width="20"
|
||||
:stroke-width="1.6"
|
||||
/>
|
||||
{{ t('save_as_custom_field') }}
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IconRouteAltLeft, IconTypography, IconShape, IconX, IconMathFunction, IconNewSection, IconInfoCircle, IconCopy } from '@tabler/icons-vue'
|
||||
import { IconRouteAltLeft, IconTypography, IconShape, IconX, IconMathFunction, IconNewSection, IconInfoCircle, IconCopy, IconForms } from '@tabler/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'FieldSettings',
|
||||
@@ -589,17 +579,33 @@ export default {
|
||||
IconInfoCircle,
|
||||
IconMathFunction,
|
||||
IconRouteAltLeft,
|
||||
IconForms,
|
||||
IconCopy,
|
||||
IconNewSection,
|
||||
IconTypography,
|
||||
IconX
|
||||
},
|
||||
inject: ['template', 'save', 't'],
|
||||
inject: ['template', 't'],
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
withCondition: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
withCustomFields: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
withCopyToAllPages: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
withSignatureId: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -636,7 +642,7 @@ export default {
|
||||
default: true
|
||||
}
|
||||
},
|
||||
emits: ['set-draw', 'scroll-to', 'click-formula', 'click-description', 'click-condition', 'click-font', 'remove-area'],
|
||||
emits: ['set-draw', 'scroll-to', 'click-formula', 'click-description', 'click-condition', 'click-font', 'remove-area', 'save', 'add-custom-field'],
|
||||
data () {
|
||||
return {
|
||||
selectedValidation: ''
|
||||
@@ -686,7 +692,7 @@ export default {
|
||||
},
|
||||
lengthValidation () {
|
||||
if (this.field.validation?.pattern && this.selectedValidation !== 'custom') {
|
||||
return this.field.validation.pattern.match(/^\.{(?<min>\d+),(?<max>\d+)?}$/)?.groups
|
||||
return this.parseLengthPattern(this.field.validation.pattern)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
@@ -703,6 +709,15 @@ export default {
|
||||
'^[a-zA-Z]+$': 'letters_only'
|
||||
}
|
||||
},
|
||||
signatureFormats () {
|
||||
return ['drawn', 'typed', 'drawn_or_typed', 'drawn_or_upload', 'upload']
|
||||
},
|
||||
verificationMethods () {
|
||||
return ['QeS', 'AeS']
|
||||
},
|
||||
prefillableFieldTypes () {
|
||||
return ['text', 'number', 'cells', 'date', 'checkbox', 'select', 'radio', 'phone']
|
||||
},
|
||||
sortedAreas () {
|
||||
return (this.field.areas || []).sort((a, b) => {
|
||||
return this.schemaAttachmentsIndexes[a.attachment_uuid] - this.schemaAttachmentsIndexes[b.attachment_uuid]
|
||||
@@ -730,7 +745,7 @@ export default {
|
||||
delete this.field.validation
|
||||
}
|
||||
|
||||
this.save()
|
||||
this.$emit('save')
|
||||
},
|
||||
copyToAllPages (field) {
|
||||
const areaString = JSON.stringify(field.areas[0])
|
||||
@@ -747,7 +762,7 @@ export default {
|
||||
|
||||
this.$emit('scroll-to', this.field.areas[this.field.areas.length - 1])
|
||||
|
||||
this.save()
|
||||
this.$emit('save')
|
||||
},
|
||||
formatNumber (number, format) {
|
||||
if (format === 'comma') {
|
||||
@@ -766,6 +781,9 @@ export default {
|
||||
return number
|
||||
}
|
||||
},
|
||||
parseLengthPattern (pattern) {
|
||||
return pattern?.match(/^\.{(?<min>\d+),(?<max>\d+)?}$/)?.groups || null
|
||||
},
|
||||
formatDate (date, format) {
|
||||
const monthFormats = {
|
||||
M: 'numeric',
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
v-for="(icon, type) in fieldIconsSorted"
|
||||
:key="type"
|
||||
>
|
||||
<li v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification'))">
|
||||
<li v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification') && (withKba || type != 'kba'))">
|
||||
<a
|
||||
href="#"
|
||||
class="text-sm py-1 px-2"
|
||||
@@ -51,11 +51,11 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading, IconId, IconCalendarCheck, IconStrikethrough } from '@tabler/icons-vue'
|
||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading, IconId, IconCalendarCheck, IconStrikethrough, IconUserScan } from '@tabler/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'FiledTypeDropdown',
|
||||
inject: ['withPhone', 'withPayment', 'withVerification', 't', 'fieldTypes'],
|
||||
inject: ['withPhone', 'withPayment', 'withVerification', 'withKba', 't', 'fieldTypes'],
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
@@ -114,7 +114,8 @@ export default {
|
||||
stamp: this.t('stamp'),
|
||||
payment: this.t('payment'),
|
||||
phone: this.t('phone'),
|
||||
verification: this.t('verify_id')
|
||||
verification: this.t('verify_id'),
|
||||
kba: this.t('kba')
|
||||
}
|
||||
},
|
||||
fieldLabels () {
|
||||
@@ -134,7 +135,8 @@ export default {
|
||||
stamp: this.t('stamp_field'),
|
||||
payment: this.t('payment_field'),
|
||||
phone: this.t('phone_field'),
|
||||
verification: this.t('verify_id')
|
||||
verification: this.t('verify_id'),
|
||||
kba: this.t('kba')
|
||||
}
|
||||
},
|
||||
fieldIcons () {
|
||||
@@ -157,7 +159,8 @@ export default {
|
||||
stamp: IconRubberStamp,
|
||||
payment: IconCreditCard,
|
||||
phone: IconPhoneCheck,
|
||||
verification: IconId
|
||||
verification: IconId,
|
||||
kba: IconUserScan
|
||||
}
|
||||
},
|
||||
skipTypes () {
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
</div>
|
||||
<div
|
||||
ref="fields"
|
||||
class="fields mb-1 mt-2"
|
||||
class="fields mt-2"
|
||||
:class="{ 'mb-1': !withCustomFields || !customFields.length }"
|
||||
@dragover.prevent="onFieldDragover"
|
||||
@drop="fieldsDragFieldRef.value ? reorderFields() : null"
|
||||
>
|
||||
@@ -24,13 +25,17 @@
|
||||
:key="field.uuid"
|
||||
:data-uuid="field.uuid"
|
||||
:field="field"
|
||||
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
|
||||
:type-index="getFieldTypeIndex(field)"
|
||||
:editable="editable"
|
||||
:with-signature-id="withSignatureId"
|
||||
:with-prefillable="withPrefillable"
|
||||
:default-field="defaultFieldsIndex[field.name]"
|
||||
:draggable="editable"
|
||||
:with-custom-fields="withCustomFields"
|
||||
class="mb-1.5"
|
||||
@add-custom-field="addCustomField"
|
||||
@dragstart="[fieldsDragFieldRef.value = field, removeDragOverlay($event), setDragPlaceholder($event)]"
|
||||
@save="save"
|
||||
@dragend="[fieldsDragFieldRef.value = null, $emit('set-drag-placeholder', null)]"
|
||||
@remove="removeField"
|
||||
@scroll-to="$emit('scroll-to-area', $event)"
|
||||
@@ -105,7 +110,92 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="editable && !onlyDefinedFields"
|
||||
v-if="editable && withCustomFields && (customFields.length || newCustomField)"
|
||||
class="tabs w-full mb-1.5"
|
||||
>
|
||||
<a
|
||||
class="tab tab-bordered w-1/2 border-base-300"
|
||||
:class="{ 'tab-active': !showCustomTab }"
|
||||
:style="{ '--tab-border': showCustomTab ? '0px' : '0.5px' }"
|
||||
@click="setFieldsTab('default')"
|
||||
>{{ t('default') }}</a>
|
||||
<a
|
||||
class="tab tab-bordered w-1/2 border-base-300"
|
||||
:class="{ 'tab-active': showCustomTab }"
|
||||
:style="{ '--tab-border': showCustomTab ? '0.5px' : '0px' }"
|
||||
@click="setFieldsTab('custom')"
|
||||
>{{ t('custom') }}</a>
|
||||
</div>
|
||||
<div
|
||||
v-if="showCustomTab && editable && (customFields.length || newCustomField)"
|
||||
ref="customFields"
|
||||
class="custom-fields"
|
||||
@dragover.prevent="onCustomFieldDragover"
|
||||
@drop="customDragFieldRef.value ? reorderCustomFields() : null"
|
||||
>
|
||||
<template v-if="isShowCustomFieldSearch">
|
||||
<input
|
||||
v-model="customFieldsSearch"
|
||||
:placeholder="t('search_field')"
|
||||
class="input input-ghost input-xs px-0 text-base mb-1 !outline-0 !rounded bg-transparent w-full"
|
||||
>
|
||||
<hr class="mb-2">
|
||||
</template>
|
||||
<div
|
||||
ref="customFieldsList"
|
||||
class="overflow-auto relative"
|
||||
:style="{
|
||||
maxHeight: isShowCustomFieldSearch ? '320px' : '',
|
||||
minHeight: '320px'
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="!filteredCustomFields.length && customFieldsSearch"
|
||||
class="top-0 bottom-0 text-center absolute flex items-center justify-center w-full flex-col"
|
||||
>
|
||||
<div>
|
||||
{{ t('field_not_found') }}
|
||||
</div>
|
||||
<a
|
||||
href="#"
|
||||
class="link"
|
||||
@click.prevent="customFieldsSearch = ''"
|
||||
>
|
||||
{{ t('clear') }}
|
||||
</a>
|
||||
</div>
|
||||
<CustomField
|
||||
v-if="newCustomField"
|
||||
:key="newCustomField.uuid"
|
||||
ref="newCustomField"
|
||||
:data-uuid="newCustomField.uuid"
|
||||
:is-new="true"
|
||||
:field="newCustomField"
|
||||
:draggable="false"
|
||||
class="mb-1.5"
|
||||
@save="onNewCustomFieldSave"
|
||||
@remove="newCustomField = null"
|
||||
/>
|
||||
<CustomField
|
||||
v-for="field in filteredCustomFields"
|
||||
:key="field.uuid"
|
||||
:data-uuid="field.uuid"
|
||||
:field="field"
|
||||
:draggable="true"
|
||||
class="mb-1.5"
|
||||
:with-signature-id="withSignatureId"
|
||||
:with-prefillable="withPrefillable"
|
||||
@save="saveCustomFields"
|
||||
@click="$emit('set-draw-custom-field', field)"
|
||||
@dragstart="[customDragFieldRef.value = field, removeDragOverlay($event), setDragPlaceholder($event)]"
|
||||
@dragend="[customDragFieldRef.value = null, $emit('set-drag-placeholder', null)]"
|
||||
@remove="removeCustomField"
|
||||
@set-draw="$emit('set-draw', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="editable && !onlyDefinedFields && (!showCustomTab || (!customFields.length && !newCustomField))"
|
||||
id="field-types-grid"
|
||||
class="grid grid-cols-3 gap-1 pb-2 fields-grid"
|
||||
>
|
||||
@@ -114,7 +204,7 @@
|
||||
:key="type"
|
||||
>
|
||||
<button
|
||||
v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification'))"
|
||||
v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification') && (withKba || type != 'kba'))"
|
||||
:id="`${type}_type_field_button`"
|
||||
draggable="true"
|
||||
class="field-type-button group flex items-center justify-center border border-dashed w-full rounded relative fields-grid-item"
|
||||
@@ -122,7 +212,7 @@
|
||||
:class="drawFieldType === type ? 'border-base-content/40' : 'border-base-300 hover:border-base-content/20'"
|
||||
@dragstart="onDragstart($event, { type: type })"
|
||||
@dragend="$emit('drag-end')"
|
||||
@click="['file', 'payment', 'verification'].includes(type) ? $emit('add-field', type) : $emit('set-draw-type', type)"
|
||||
@click="['file', 'payment', 'verification', 'kba'].includes(type) ? $emit('add-field', type) : $emit('set-draw-type', type)"
|
||||
>
|
||||
<div
|
||||
class="flex items-console transition-all cursor-grab h-full absolute left-0"
|
||||
@@ -266,15 +356,18 @@
|
||||
|
||||
<script>
|
||||
import Field from './field'
|
||||
import CustomField from './custom_field'
|
||||
import FieldType from './field_type'
|
||||
import FieldSubmitter from './field_submitter'
|
||||
import { IconLock, IconCirclePlus, IconInnerShadowTop, IconSparkles } from '@tabler/icons-vue'
|
||||
import IconDrag from './icon_drag'
|
||||
import { v4 } from 'uuid'
|
||||
|
||||
export default {
|
||||
name: 'TemplateFields',
|
||||
components: {
|
||||
Field,
|
||||
CustomField,
|
||||
FieldType,
|
||||
IconCirclePlus,
|
||||
IconSparkles,
|
||||
@@ -283,12 +376,22 @@ export default {
|
||||
IconDrag,
|
||||
IconLock
|
||||
},
|
||||
inject: ['save', 'backgroundColor', 'withPhone', 'withVerification', 'withPayment', 't', 'fieldsDragFieldRef', 'baseFetch'],
|
||||
inject: ['save', 'backgroundColor', 'withPhone', 'withVerification', 'withKba', 'withPayment', 't', 'fieldsDragFieldRef', 'customDragFieldRef', 'baseFetch', 'selectedAreasRef', 'getFieldTypeIndex'],
|
||||
props: {
|
||||
fields: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
customFields: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
withCustomFields: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
withFieldsSearch: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -372,12 +475,15 @@ export default {
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['add-field', 'set-draw', 'set-draw-type', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter', 'set-drag-placeholder', 'select-submitter'],
|
||||
emits: ['add-field', 'set-draw', 'set-draw-type', 'set-draw-custom-field', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter', 'set-drag-placeholder', 'select-submitter'],
|
||||
data () {
|
||||
return {
|
||||
fieldPagesLoaded: null,
|
||||
analyzingProgress: 0,
|
||||
defaultFieldsSearch: ''
|
||||
newCustomField: null,
|
||||
showCustomTab: false,
|
||||
defaultFieldsSearch: '',
|
||||
customFieldsSearch: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -430,6 +536,23 @@ export default {
|
||||
} else {
|
||||
return this.submitterDefaultFields
|
||||
}
|
||||
},
|
||||
isShowCustomFieldSearch () {
|
||||
return this.customFields.length > 8
|
||||
},
|
||||
filteredCustomFields () {
|
||||
if (this.customFieldsSearch) {
|
||||
return this.customFields.filter((f) => f.name.toLowerCase().includes(this.customFieldsSearch.toLowerCase()))
|
||||
} else {
|
||||
return this.customFields
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
try {
|
||||
this.showCustomTab = localStorage.getItem('docuseal_builder_tab') === 'custom'
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -440,6 +563,61 @@ export default {
|
||||
|
||||
this.$emit('set-drag', field)
|
||||
},
|
||||
onNewCustomFieldSave () {
|
||||
if (this.newCustomField.name) {
|
||||
this.customFields.unshift(this.newCustomField)
|
||||
this.newCustomField = null
|
||||
|
||||
this.saveCustomFields()
|
||||
} else {
|
||||
this.newCustomField = null
|
||||
}
|
||||
},
|
||||
addCustomField (field) {
|
||||
const customField = JSON.parse(JSON.stringify(field))
|
||||
|
||||
customField.uuid = v4()
|
||||
|
||||
delete customField.submitter_uuid
|
||||
delete customField.prefillable
|
||||
delete customField.conditions
|
||||
|
||||
customField.areas?.forEach((area) => {
|
||||
delete area.attachment_uuid
|
||||
delete area.page
|
||||
})
|
||||
|
||||
if (customField.name) {
|
||||
this.customFields.unshift(customField)
|
||||
this.saveCustomFields()
|
||||
} else {
|
||||
this.newCustomField = customField
|
||||
}
|
||||
|
||||
this.setFieldsTab('custom')
|
||||
},
|
||||
setFieldsTab (type) {
|
||||
try {
|
||||
localStorage.setItem('docuseal_builder_tab', type)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
this.showCustomTab = type === 'custom'
|
||||
},
|
||||
saveCustomFields () {
|
||||
return this.baseFetch('/account_custom_fields', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
value: this.customFields
|
||||
}),
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}).then(async (resp) => {
|
||||
const fields = await resp.json()
|
||||
|
||||
this.customFields.splice(0, this.customFields.length, ...fields)
|
||||
})
|
||||
},
|
||||
detectFields () {
|
||||
const fields = []
|
||||
|
||||
@@ -579,7 +757,7 @@ export default {
|
||||
removeSubmitter (submitter) {
|
||||
[...this.fields].forEach((field) => {
|
||||
if (field.submitter_uuid === submitter.uuid) {
|
||||
this.removeField(field)
|
||||
this.removeField(field, false)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -591,7 +769,7 @@ export default {
|
||||
|
||||
this.save()
|
||||
},
|
||||
removeField (field) {
|
||||
removeField (field, save = true) {
|
||||
this.fields.splice(this.fields.indexOf(field), 1)
|
||||
|
||||
this.fields.forEach((f) => {
|
||||
@@ -610,7 +788,54 @@ export default {
|
||||
})
|
||||
})
|
||||
|
||||
this.save()
|
||||
field.areas?.forEach((area) => {
|
||||
this.selectedAreasRef.value.splice(this.selectedAreasRef.value.indexOf(area), 1)
|
||||
})
|
||||
|
||||
if (save) {
|
||||
this.save()
|
||||
}
|
||||
},
|
||||
removeCustomField (field) {
|
||||
this.customFields.splice(this.customFields.indexOf(field), 1)
|
||||
|
||||
if (!this.customFields.length) {
|
||||
this.setFieldsTab('default')
|
||||
}
|
||||
|
||||
this.saveCustomFields()
|
||||
},
|
||||
onCustomFieldDragover (e) {
|
||||
if (this.customDragFieldRef.value && this.customFields.includes(this.customDragFieldRef.value)) {
|
||||
const container = this.$refs.customFieldsList
|
||||
const targetField = e.target.closest('[data-uuid]')
|
||||
const dragField = container.querySelector(`[data-uuid="${this.customDragFieldRef.value.uuid}"]`)
|
||||
|
||||
if (dragField && targetField && targetField !== dragField) {
|
||||
const fields = Array.from(container.children)
|
||||
const currentIndex = fields.indexOf(dragField)
|
||||
const targetIndex = fields.indexOf(targetField)
|
||||
|
||||
if (currentIndex < targetIndex) {
|
||||
targetField.after(dragField)
|
||||
} else {
|
||||
targetField.before(dragField)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
reorderCustomFields () {
|
||||
if (!this.customFields.includes(this.customDragFieldRef.value)) {
|
||||
return
|
||||
}
|
||||
|
||||
const reorderedFields = Array.from(this.$refs.customFieldsList.children).map((el) => {
|
||||
return this.customFields.find((f) => f.uuid === el.dataset.uuid)
|
||||
}).filter(Boolean)
|
||||
|
||||
this.customFields.splice(0, this.customFields.length, ...reorderedFields)
|
||||
|
||||
this.saveCustomFields()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<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 class="modal-title">
|
||||
{{ t('font') }} - {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field, template.fields) }}
|
||||
{{ t('font') }} - {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field) }}
|
||||
</span>
|
||||
<a
|
||||
href="#"
|
||||
@@ -171,7 +171,7 @@
|
||||
contenteditable="true"
|
||||
class="outline-none whitespace-nowrap truncate"
|
||||
>
|
||||
{{ field.default_value || field.name || buildDefaultName(field, template.fields) }}
|
||||
{{ field.default_value || field.name || buildDefaultName(field) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -196,7 +196,7 @@ export default {
|
||||
components: {
|
||||
IconChevronDown
|
||||
},
|
||||
inject: ['t', 'save', 'template'],
|
||||
inject: ['t', 'template'],
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
@@ -217,7 +217,7 @@ export default {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['close'],
|
||||
emits: ['close', 'save'],
|
||||
data () {
|
||||
return {
|
||||
preferences: {}
|
||||
@@ -257,6 +257,7 @@ export default {
|
||||
colors () {
|
||||
return [
|
||||
{ label: '⬛', value: 'black' },
|
||||
{ label: '⬜', value: 'white' },
|
||||
{ label: '🟦', value: 'blue' },
|
||||
{ label: '🟥', value: 'red' }
|
||||
]
|
||||
@@ -271,6 +272,7 @@ export default {
|
||||
'items-center': !this.preferences.valign || this.preferences.valign === 'center',
|
||||
'items-start': this.preferences.valign === 'top',
|
||||
'items-end': this.preferences.valign === 'bottom',
|
||||
'bg-black': this.preferences.color === 'white',
|
||||
'font-bold': ['bold_italic', 'bold'].includes(this.preferences.font_type),
|
||||
italic: ['bold_italic', 'italic'].includes(this.preferences.font_type)
|
||||
}
|
||||
@@ -322,8 +324,7 @@ export default {
|
||||
|
||||
Object.assign(this.field.preferences, this.preferences)
|
||||
|
||||
this.save()
|
||||
|
||||
this.$emit('save')
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<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 class="modal-title">
|
||||
{{ t('formula') }} - {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field, template.fields) }}
|
||||
{{ t('formula') }} - {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field) }}
|
||||
</span>
|
||||
<a
|
||||
href="#"
|
||||
@@ -33,7 +33,7 @@
|
||||
v-for="f in fields"
|
||||
:key="f.uuid"
|
||||
class="mr-1 flex 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)}}}`)"
|
||||
@click.prevent="insertTextUnderCursor(`{{${f.name || buildDefaultName(f)}}}`)"
|
||||
>
|
||||
<IconMathFunction
|
||||
v-if="f.preferences?.formula"
|
||||
@@ -47,7 +47,7 @@
|
||||
height="20"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
{{ f.name || buildDefaultName(f, template.fields) }}
|
||||
{{ f.name || buildDefaultName(f) }}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
@@ -131,7 +131,7 @@ export default {
|
||||
IconCodePlus,
|
||||
IconMathFunction
|
||||
},
|
||||
inject: ['t', 'save', 'template', 'withFormula'],
|
||||
inject: ['t', 'template', 'withFormula'],
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
@@ -152,7 +152,7 @@ export default {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['close'],
|
||||
emits: ['close', 'save'],
|
||||
data () {
|
||||
return {
|
||||
formula: ''
|
||||
@@ -181,7 +181,7 @@ export default {
|
||||
const foundField = this.fields.find((f) => f.uuid === uuid)
|
||||
|
||||
if (foundField) {
|
||||
return `{{${foundField.name || this.buildDefaultName(foundField, this.template.fields)}}}`
|
||||
return `{{${foundField.name || this.buildDefaultName(foundField)}}}`
|
||||
} else {
|
||||
return '{{FIELD NOT FOUND}}'
|
||||
}
|
||||
@@ -190,7 +190,7 @@ export default {
|
||||
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()
|
||||
return (f.name || this.buildDefaultName(f)).trim() === name.trim()
|
||||
})
|
||||
|
||||
if (foundField) {
|
||||
@@ -212,11 +212,14 @@ export default {
|
||||
} else {
|
||||
this.field.preferences.formula = normalizedFormula
|
||||
|
||||
if (this.field.type !== 'payment') {
|
||||
if (this.field.type === 'payment') {
|
||||
delete this.field.preferences.price
|
||||
delete this.field.preferences.payment_link_id
|
||||
} else {
|
||||
this.field.readonly = !!normalizedFormula
|
||||
}
|
||||
|
||||
this.save()
|
||||
this.$emit('save')
|
||||
|
||||
this.$emit('close')
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
form="sync_form"
|
||||
:form="`sync_form_${inputId}`"
|
||||
type="submit"
|
||||
:disabled="isLoading"
|
||||
>
|
||||
@@ -40,7 +40,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
<form
|
||||
id="sync_form"
|
||||
:id="`sync_form_${inputId}`"
|
||||
ref="form"
|
||||
class="hidden"
|
||||
@submit.prevent="upload({ path: uploadUrl })"
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
const en = {
|
||||
fixed: 'Fixed',
|
||||
default: 'Default',
|
||||
save_as_custom_field: 'Save as custom field',
|
||||
kba: 'KBA',
|
||||
analyzing_: 'Analyzing...',
|
||||
download: 'Download',
|
||||
downloading_: 'Downloading...',
|
||||
view: 'View',
|
||||
autodetect_fields: 'Autodetect fields',
|
||||
autodetect_fields: 'Autodetect Fields',
|
||||
payment_link: 'Payment link',
|
||||
strikeout: 'Strikeout',
|
||||
draw_strikethrough_the_document: 'Draw strikethrough the document',
|
||||
@@ -28,6 +32,9 @@ const en = {
|
||||
field_not_found: 'Field not found',
|
||||
clear: 'Clear',
|
||||
align: 'Align',
|
||||
resize: 'Resize',
|
||||
width: 'Width',
|
||||
height: 'Height',
|
||||
add_all_required_fields_to_continue: 'Add all required fields to continue',
|
||||
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Uploaded PDF contains form fields. Keep or remove them?',
|
||||
keep: 'Keep',
|
||||
@@ -45,6 +52,8 @@ const en = {
|
||||
type_value: 'Type value',
|
||||
equal: 'Equal',
|
||||
not_equal: 'Not equal',
|
||||
greater_than: 'Greater than',
|
||||
less_than: 'Less than',
|
||||
contains: 'Contains',
|
||||
does_not_contain: 'Does not contain',
|
||||
not_empty: 'Not empty',
|
||||
@@ -88,8 +97,9 @@ const en = {
|
||||
format: 'Format',
|
||||
read_only: 'Read-only',
|
||||
page: 'Page',
|
||||
draw_new_area: 'Draw New Area',
|
||||
copy_to_all_pages: 'Copy to All Pages',
|
||||
draw_new_area: 'Draw new area',
|
||||
copy_to_all_pages: 'Copy to all pages',
|
||||
more: 'More',
|
||||
add_option: 'Add option',
|
||||
option: 'Option',
|
||||
options: 'Options',
|
||||
@@ -170,6 +180,9 @@ const en = {
|
||||
numbers_only: 'Numbers only',
|
||||
letters_only: 'Letters only',
|
||||
regexp_validation: 'Regexp validation',
|
||||
custom_validation: 'Custom Validation',
|
||||
length_validation: 'Length Validation',
|
||||
number_range: 'Number Range',
|
||||
enter_pdf_password: 'Enter PDF password',
|
||||
wrong_password: 'Wrong password',
|
||||
currency: 'Currency',
|
||||
@@ -184,10 +197,25 @@ const en = {
|
||||
start_tour: 'Start Tour',
|
||||
or_add_from: 'Or add from',
|
||||
sync: 'Sync',
|
||||
syncing: 'Syncing...'
|
||||
syncing: 'Syncing...',
|
||||
copy: 'Copy',
|
||||
paste: 'Paste',
|
||||
select_fields: 'Select Fields',
|
||||
draw_fields: 'Draw Fields',
|
||||
align_left: 'Align Left',
|
||||
align_right: 'Align Right',
|
||||
align_top: 'Align Top',
|
||||
align_bottom: 'Align Bottom',
|
||||
fields_selected: '{count} Fields Selected',
|
||||
field_added: '{count} Field Added',
|
||||
fields_added: '{count} Fields Added'
|
||||
}
|
||||
|
||||
const es = {
|
||||
fixed: 'Fijo',
|
||||
default: 'Predeterminado',
|
||||
save_as_custom_field: 'Guardar como personalizado',
|
||||
kba: 'KBA',
|
||||
autodetect_fields: 'Autodetectar campos',
|
||||
analyzing_: 'Analizando...',
|
||||
download: 'Descargar',
|
||||
@@ -218,6 +246,9 @@ const es = {
|
||||
clear: 'Borrar',
|
||||
type_value: 'Escriba valor',
|
||||
align: 'Alinear',
|
||||
resize: 'Redimensionar',
|
||||
width: 'Ancho',
|
||||
height: 'Alto',
|
||||
add_all_required_fields_to_continue: 'Agregar todos los campos requeridos para continuar',
|
||||
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'El PDF cargado tiene campos. ¿Mantenerlos o eliminarlos?',
|
||||
keep: 'Mantener',
|
||||
@@ -235,6 +266,8 @@ const es = {
|
||||
price: 'Precio',
|
||||
equal: 'Igual',
|
||||
not_equal: 'No es igual',
|
||||
greater_than: 'Mayor que',
|
||||
less_than: 'Menor que',
|
||||
contains: 'Contiene',
|
||||
does_not_contain: 'No contiene',
|
||||
not_empty: 'No vacío',
|
||||
@@ -275,6 +308,7 @@ const es = {
|
||||
page: 'Página',
|
||||
draw_new_area: 'Dibujar nueva área',
|
||||
copy_to_all_pages: 'Copiar a todas las páginas',
|
||||
more: 'Más',
|
||||
add_option: 'Agregar opción',
|
||||
option: 'Opción',
|
||||
options: 'Opciones',
|
||||
@@ -359,6 +393,9 @@ const es = {
|
||||
numbers_only: 'Solo números',
|
||||
letters_only: 'Solo letras',
|
||||
regexp_validation: 'Validación de expresión regular',
|
||||
custom_validation: 'Validación Personalizada',
|
||||
length_validation: 'Validación de Longitud',
|
||||
number_range: 'Rango de Números',
|
||||
enter_pdf_password: 'Ingrese la contraseña del PDF',
|
||||
wrong_password: 'Contraseña incorrecta',
|
||||
currency: 'Moneda',
|
||||
@@ -373,10 +410,25 @@ const es = {
|
||||
start_tour: 'Iniciar guía',
|
||||
or_add_from: 'O agregar desde',
|
||||
sync: 'Sincronizar',
|
||||
syncing: 'Sincronizando...'
|
||||
syncing: 'Sincronizando...',
|
||||
copy: 'Copiar',
|
||||
paste: 'Pegar',
|
||||
select_fields: 'Seleccionar Campos',
|
||||
draw_fields: 'Dibujar Campos',
|
||||
align_left: 'Alinear a la izquierda',
|
||||
align_right: 'Alinear a la derecha',
|
||||
align_top: 'Alinear arriba',
|
||||
align_bottom: 'Alinear abajo',
|
||||
fields_selected: '{count} Campos Seleccionados',
|
||||
field_added: '{count} Campo Añadido',
|
||||
fields_added: '{count} Campos Añadidos'
|
||||
}
|
||||
|
||||
const it = {
|
||||
fixed: 'Fisso',
|
||||
default: 'Predefinito',
|
||||
save_as_custom_field: 'Salva come personalizzato',
|
||||
kba: 'KBA',
|
||||
autodetect_fields: 'Rileva campi',
|
||||
analyzing_: 'Analisi...',
|
||||
download: 'Scarica',
|
||||
@@ -406,6 +458,9 @@ const it = {
|
||||
field_not_found: 'Campo non trovato',
|
||||
clear: 'Cancella',
|
||||
align: 'Allinea',
|
||||
resize: 'Ridimensiona',
|
||||
width: 'Larghezza',
|
||||
height: 'Altezza',
|
||||
add_all_required_fields_to_continue: 'Aggiungi tutti i campi obbligatori per continuare',
|
||||
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Il PDF caricato contiene campi del modulo. Mantenerli o rimuoverli?',
|
||||
keep: 'Mantieni',
|
||||
@@ -423,6 +478,8 @@ const it = {
|
||||
type_value: 'Inserisci valore',
|
||||
equal: 'Uguale',
|
||||
not_equal: 'Non uguale',
|
||||
greater_than: 'Maggiore di',
|
||||
less_than: 'Minore di',
|
||||
contains: 'Contiene',
|
||||
does_not_contain: 'Non contiene',
|
||||
not_empty: 'Non vuoto',
|
||||
@@ -468,6 +525,7 @@ const it = {
|
||||
page: 'Pagina',
|
||||
draw_new_area: 'Disegna nuova area',
|
||||
copy_to_all_pages: 'Copia in tutte le pagine',
|
||||
more: 'Altro',
|
||||
add_option: 'Aggiungi opzione',
|
||||
option: 'Opzione',
|
||||
options: 'Opzioni',
|
||||
@@ -548,6 +606,9 @@ const it = {
|
||||
numbers_only: 'Solo numeri',
|
||||
letters_only: 'Solo lettere',
|
||||
regexp_validation: 'Validazione regexp',
|
||||
custom_validation: 'Validazione Personalizzata',
|
||||
length_validation: 'Validazione Lunghezza',
|
||||
number_range: 'Intervallo Numerico',
|
||||
enter_pdf_password: 'Inserisci password PDF',
|
||||
wrong_password: 'Password errata',
|
||||
currency: 'Valuta',
|
||||
@@ -562,10 +623,25 @@ const it = {
|
||||
start_tour: 'Inizia il tour',
|
||||
or_add_from: 'O aggiungi da',
|
||||
sync: 'Sincronizza',
|
||||
syncing: 'Sincronizzazione...'
|
||||
syncing: 'Sincronizzazione...',
|
||||
copy: 'Copia',
|
||||
paste: 'Incolla',
|
||||
select_fields: 'Seleziona Campi',
|
||||
draw_fields: 'Disegna Campi',
|
||||
align_left: 'Allinea a sinistra',
|
||||
align_right: 'Allinea a destra',
|
||||
align_top: 'Allinea in alto',
|
||||
align_bottom: 'Allinea in basso',
|
||||
fields_selected: '{count} Campi Selezionati',
|
||||
field_added: '{count} Campo Aggiunto',
|
||||
fields_added: '{count} Campi Aggiunti'
|
||||
}
|
||||
|
||||
const pt = {
|
||||
fixed: 'Fixo',
|
||||
default: 'Padrão',
|
||||
save_as_custom_field: 'Salvar como personalizado',
|
||||
kba: 'KBA',
|
||||
autodetect_fields: 'Detectar campos',
|
||||
analyzing_: 'Analisando...',
|
||||
download: 'Baixar',
|
||||
@@ -599,6 +675,9 @@ const pt = {
|
||||
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'O PDF carregado contém campos. Manter ou removê-los?',
|
||||
keep: 'Manter',
|
||||
align: 'Alinhar',
|
||||
resize: 'Redimensionar',
|
||||
width: 'Largura',
|
||||
height: 'Altura',
|
||||
left: 'Esquerda',
|
||||
heading: 'Cabeçalho',
|
||||
validation: 'Validação',
|
||||
@@ -613,6 +692,8 @@ const pt = {
|
||||
price: 'Preço',
|
||||
equal: 'Igual',
|
||||
not_equal: 'Não é igual',
|
||||
greater_than: 'Maior que',
|
||||
less_than: 'Menor que',
|
||||
contains: 'Contém',
|
||||
does_not_contain: 'Não contém',
|
||||
not_empty: 'Não vazio',
|
||||
@@ -653,6 +734,7 @@ const pt = {
|
||||
page: 'Página',
|
||||
draw_new_area: 'Desenhar nova área',
|
||||
copy_to_all_pages: 'Copiar para todas as páginas',
|
||||
more: 'Mais',
|
||||
add_option: 'Adicionar opção',
|
||||
option: 'Opção',
|
||||
options: 'Opções',
|
||||
@@ -737,6 +819,9 @@ const pt = {
|
||||
numbers_only: 'Somente números',
|
||||
letters_only: 'Somente letras',
|
||||
regexp_validation: 'Validação de expressão regular',
|
||||
custom_validation: 'Validação Personalizada',
|
||||
length_validation: 'Validação de Comprimento',
|
||||
number_range: 'Intervalo de Números',
|
||||
enter_pdf_password: 'Digite a senha do PDF',
|
||||
wrong_password: 'Senha incorreta',
|
||||
currency: 'Moeda',
|
||||
@@ -751,10 +836,25 @@ const pt = {
|
||||
start_tour: 'Iniciar tour',
|
||||
or_add_from: 'Ou adicionar de',
|
||||
sync: 'Sincronizar',
|
||||
syncing: 'Sincronizando...'
|
||||
syncing: 'Sincronizando...',
|
||||
copy: 'Copiar',
|
||||
paste: 'Colar',
|
||||
select_fields: 'Selecionar Campos',
|
||||
draw_fields: 'Desenhar Campos',
|
||||
align_left: 'Alinhar à esquerda',
|
||||
align_right: 'Alinhar à direita',
|
||||
align_top: 'Alinhar ao topo',
|
||||
align_bottom: 'Alinhar à parte inferior',
|
||||
fields_selected: '{count} Campos Selecionados',
|
||||
field_added: '{count} Campo Adicionado',
|
||||
fields_added: '{count} Campos Adicionados'
|
||||
}
|
||||
|
||||
const fr = {
|
||||
fixed: 'Fixe',
|
||||
default: 'Par défaut',
|
||||
save_as_custom_field: 'Enregistrer comme personnalisé',
|
||||
kba: 'KBA',
|
||||
autodetect_fields: 'Détecter les champs',
|
||||
analyzing_: 'Analyse...',
|
||||
download: 'Télécharger',
|
||||
@@ -784,6 +884,9 @@ const fr = {
|
||||
field_not_found: 'Champ introuvable',
|
||||
clear: 'Effacer',
|
||||
align: 'Aligner',
|
||||
resize: 'Redimensionner',
|
||||
width: 'Largeur',
|
||||
height: 'Hauteur',
|
||||
add_all_required_fields_to_continue: 'Ajoutez tous les champs obligatoires pour continuer',
|
||||
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Le PDF téléversé contient des champs de formulaire. Les conserver ou les supprimer ?',
|
||||
keep: 'Conserver',
|
||||
@@ -801,6 +904,8 @@ const fr = {
|
||||
type_value: 'Saisir une valeur',
|
||||
equal: 'Égal',
|
||||
not_equal: 'Différent',
|
||||
greater_than: 'Supérieur à',
|
||||
less_than: 'Inférieur à',
|
||||
contains: 'Contient',
|
||||
does_not_contain: 'Ne contient pas',
|
||||
not_empty: 'Non vide',
|
||||
@@ -846,6 +951,7 @@ const fr = {
|
||||
page: 'Page',
|
||||
draw_new_area: 'Dessiner une zone',
|
||||
copy_to_all_pages: 'Copier sur toutes les pages',
|
||||
more: 'Plus',
|
||||
add_option: 'Ajouter une option',
|
||||
option: 'Option',
|
||||
options: 'Options',
|
||||
@@ -926,6 +1032,9 @@ const fr = {
|
||||
numbers_only: 'Chiffres uniquement',
|
||||
letters_only: 'Lettres uniquement',
|
||||
regexp_validation: 'Validation par expression régulière',
|
||||
custom_validation: 'Validation Personnalisée',
|
||||
length_validation: 'Validation de Longueur',
|
||||
number_range: 'Plage de Nombres',
|
||||
enter_pdf_password: 'Saisir le mot de passe du PDF',
|
||||
wrong_password: 'Mot de passe incorrect',
|
||||
currency: 'Devise',
|
||||
@@ -940,10 +1049,25 @@ const fr = {
|
||||
start_tour: 'Démarrer',
|
||||
or_add_from: 'Ou ajouter depuis',
|
||||
sync: 'Synchroniser',
|
||||
syncing: 'Synchronisation...'
|
||||
syncing: 'Synchronisation...',
|
||||
copy: 'Copier',
|
||||
paste: 'Coller',
|
||||
select_fields: 'Sélectionner Champs',
|
||||
draw_fields: 'Dessiner Champs',
|
||||
align_left: 'Aligner à gauche',
|
||||
align_right: 'Aligner à droite',
|
||||
align_top: 'Aligner en haut',
|
||||
align_bottom: 'Aligner en bas',
|
||||
fields_selected: '{count} Champs Sélectionnés',
|
||||
field_added: '{count} Champ Ajouté',
|
||||
fields_added: '{count} Champs Ajoutés'
|
||||
}
|
||||
|
||||
const de = {
|
||||
fixed: 'Fest',
|
||||
default: 'Standard',
|
||||
save_as_custom_field: 'Als benutzerdefiniert speichern',
|
||||
kba: 'KBA',
|
||||
autodetect_fields: 'Felder erkennen',
|
||||
analyzing_: 'Analysiere...',
|
||||
download: 'Download',
|
||||
@@ -973,6 +1097,9 @@ const de = {
|
||||
field_not_found: 'Feld nicht gefunden',
|
||||
clear: 'Leeren',
|
||||
align: 'Ausrichten',
|
||||
resize: 'Größe ändern',
|
||||
width: 'Breite',
|
||||
height: 'Höhe',
|
||||
add_all_required_fields_to_continue: 'Fügen Sie alle erforderlichen Felder hinzu, um fortzufahren',
|
||||
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Das hochgeladene PDF enthält Formularfelder. Beibehalten oder entfernen?',
|
||||
keep: 'Beibehalten',
|
||||
@@ -990,6 +1117,8 @@ const de = {
|
||||
type_value: 'Wert eingeben',
|
||||
equal: 'Gleich',
|
||||
not_equal: 'Ungleich',
|
||||
greater_than: 'Größer als',
|
||||
less_than: 'Kleiner als',
|
||||
contains: 'Enthält',
|
||||
does_not_contain: 'Enthält nicht',
|
||||
not_empty: 'Nicht leer',
|
||||
@@ -1035,6 +1164,7 @@ const de = {
|
||||
page: 'Seite',
|
||||
draw_new_area: 'Bereich zeichnen',
|
||||
copy_to_all_pages: 'Auf alle Seiten kopieren',
|
||||
more: 'Mehr',
|
||||
add_option: 'Option hinzufügen',
|
||||
option: 'Option',
|
||||
options: 'Optionen',
|
||||
@@ -1115,6 +1245,9 @@ const de = {
|
||||
numbers_only: 'Nur Zahlen',
|
||||
letters_only: 'Nur Buchstaben',
|
||||
regexp_validation: 'RegExp-Validierung',
|
||||
custom_validation: 'Benutzerdefinierte Validierung',
|
||||
length_validation: 'Längenvalidierung',
|
||||
number_range: 'Zahlenbereich',
|
||||
enter_pdf_password: 'PDF-Passwort eingeben',
|
||||
wrong_password: 'Falsches Passwort',
|
||||
currency: 'Währung',
|
||||
@@ -1129,10 +1262,25 @@ const de = {
|
||||
start_tour: 'Tour starten',
|
||||
or_add_from: 'Oder hinzufügen aus',
|
||||
sync: 'Synchronisieren',
|
||||
syncing: 'Synchronisiere...'
|
||||
syncing: 'Synchronisiere...',
|
||||
copy: 'Kopieren',
|
||||
paste: 'Einfügen',
|
||||
select_fields: 'Felder Auswählen',
|
||||
draw_fields: 'Felder Zeichnen',
|
||||
align_left: 'Links ausrichten',
|
||||
align_right: 'Rechts ausrichten',
|
||||
align_top: 'Oben ausrichten',
|
||||
align_bottom: 'Unten ausrichten',
|
||||
fields_selected: '{count} Felder Ausgewählt',
|
||||
field_added: '{count} Feld Hinzugefügt',
|
||||
fields_added: '{count} Felder Hinzugefügt'
|
||||
}
|
||||
|
||||
const nl = {
|
||||
fixed: 'Vast',
|
||||
default: 'Standaard',
|
||||
save_as_custom_field: 'Opslaan als aangepast',
|
||||
kba: 'KBA',
|
||||
autodetect_fields: 'Velden detecteren',
|
||||
analyzing_: 'Analyseren...',
|
||||
download: 'Downloaden',
|
||||
@@ -1162,6 +1310,9 @@ const nl = {
|
||||
field_not_found: 'Veld niet gevonden',
|
||||
clear: 'Wissen',
|
||||
align: 'Uitlijnen',
|
||||
resize: 'Formaat wijzigen',
|
||||
width: 'Breedte',
|
||||
height: 'Hoogte',
|
||||
add_all_required_fields_to_continue: 'Voeg alle vereiste velden toe om door te gaan',
|
||||
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Geüploade PDF bevat formuliervelden. Behouden of verwijderen?',
|
||||
keep: 'Behouden',
|
||||
@@ -1179,6 +1330,8 @@ const nl = {
|
||||
type_value: 'Typ waarde',
|
||||
equal: 'Gelijk aan',
|
||||
not_equal: 'Niet gelijk aan',
|
||||
greater_than: 'Groter dan',
|
||||
less_than: 'Kleiner dan',
|
||||
contains: 'Bevat',
|
||||
does_not_contain: 'Bevat niet',
|
||||
not_empty: 'Niet leeg',
|
||||
@@ -1224,6 +1377,7 @@ const nl = {
|
||||
page: 'Pagina',
|
||||
draw_new_area: 'Nieuw gebied tekenen',
|
||||
copy_to_all_pages: 'Kopieer naar alle pag.',
|
||||
more: 'Meer',
|
||||
add_option: 'Optie toevoegen',
|
||||
option: 'Optie',
|
||||
options: 'Opties',
|
||||
@@ -1304,6 +1458,9 @@ const nl = {
|
||||
numbers_only: 'Alleen cijfers',
|
||||
letters_only: 'Alleen letters',
|
||||
regexp_validation: 'Regex validatie',
|
||||
custom_validation: 'Aangepaste Validatie',
|
||||
length_validation: 'Lengte Validatie',
|
||||
number_range: 'Getalbereik',
|
||||
enter_pdf_password: 'Voer PDF-wachtwoord in',
|
||||
wrong_password: 'Onjuist wachtwoord',
|
||||
currency: 'Valuta',
|
||||
@@ -1318,7 +1475,18 @@ const nl = {
|
||||
start_tour: 'Rondleiding starten',
|
||||
or_add_from: 'Of toevoegen van',
|
||||
sync: 'Synchroniseren',
|
||||
syncing: 'Synchroniseren...'
|
||||
syncing: 'Synchroniseren...',
|
||||
copy: 'Kopiëren',
|
||||
paste: 'Plakken',
|
||||
select_fields: 'Velden Selecteren',
|
||||
draw_fields: 'Velden Tekenen',
|
||||
align_left: 'Links uitlijnen',
|
||||
align_right: 'Rechts uitlijnen',
|
||||
align_top: 'Boven uitlijnen',
|
||||
align_bottom: 'Onder uitlijnen',
|
||||
fields_selected: '{count} Velden Geselecteerd',
|
||||
field_added: '{count} Veld Toegevoegd',
|
||||
fields_added: '{count} Velden Toegevoegd'
|
||||
}
|
||||
|
||||
export { en, es, it, pt, fr, de, nl }
|
||||
|
||||
@@ -2,87 +2,20 @@
|
||||
<svg
|
||||
height="40"
|
||||
width="40"
|
||||
version="1.1"
|
||||
style="color: #e0753f"
|
||||
viewBox="0 0 180 180"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 511.998 511.998"
|
||||
xml:space="preserve"
|
||||
>
|
||||
<path
|
||||
style="fill:#AA968C;"
|
||||
d="M503.999,247.999c0,128.13-111.033,240-248,240S8,376.129,8,247.999s111.033-224,248-224
|
||||
S503.999,119.869,503.999,247.999z"
|
||||
/>
|
||||
<path
|
||||
style="fill:#AA968C;"
|
||||
d="M255.999,23.999C119.033,23.999,8,119.868,8,247.999c0,24.631,4.138,48.647,11.74,71.397
|
||||
c25.518,34.29,66.232,56.602,112.26,56.602c53.893,0,100.6-30.495,124-75.13c23.4,44.636,70.107,75.13,124,75.13
|
||||
c46.028,0,86.743-22.312,112.26-56.602c7.602-22.75,11.74-46.767,11.74-71.397C503.999,119.868,392.966,23.999,255.999,23.999z"
|
||||
fill="currentColor"
|
||||
d="M 178.224 72.09 c -0.296 -1.463 -0.627 -2.919 -0.996 -4.364 -0.293 -1.151 -0.616 -2.293 -0.956 -3.433 -0.301 -1.008 -0.612 -2.014 -0.95 -3.012 -0.531 -1.578 -1.113 -3.142 -1.735 -4.694 -0.216 -0.54 -0.433 -1.082 -0.661 -1.618 -0.195 -0.462 -0.399 -0.917 -0.601 -1.375 -0.262 -0.591 -0.53 -1.177 -0.804 -1.762 -0.074 -0.159 -0.151 -0.315 -0.226 -0.474 -0.209 -0.441 -0.422 -0.881 -0.638 -1.318 -0.076 -0.154 -0.153 -0.306 -0.229 -0.459 -0.236 -0.471 -0.477 -0.939 -0.721 -1.406 -0.053 -0.101 -0.105 -0.201 -0.158 -0.302 -1.143 -2.16 -2.367 -4.269 -3.68 -6.322 -0.116 -0.181 -0.237 -0.359 -0.355 -0.539 -0.094 -0.144 -0.189 -0.288 -0.284 -0.432 -0.284 -0.431 -0.57 -0.861 -0.862 -1.287 -0.112 -0.164 -0.225 -0.326 -0.338 -0.489 -0.193 -0.279 -0.382 -0.56 -0.579 -0.836 -0.089 -0.125 -0.182 -0.249 -0.273 -0.374 -0.13 -0.182 -0.264 -0.362 -0.395 -0.542 -0.277 -0.38 -0.556 -0.76 -0.838 -1.135 -0.15 -0.199 -0.303 -0.395 -0.454 -0.593 -0.21 -0.274 -0.417 -0.552 -0.63 -0.823 -0.055 -0.069 -0.111 -0.136 -0.166 -0.205 -0.482 -0.61 -0.971 -1.216 -1.47 -1.814 -0.129 -0.155 -0.262 -0.306 -0.392 -0.461 -0.402 -0.476 -0.808 -0.95 -1.22 -1.417 -0.186 -0.212 -0.375 -0.422 -0.563 -0.631 -0.384 -0.428 -0.773 -0.854 -1.167 -1.276 -0.176 -0.189 -0.351 -0.379 -0.529 -0.567 -0.564 -0.595 -1.134 -1.186 -1.716 -1.768 -1.091 -1.091 -2.207 -2.15 -3.346 -3.178 -1.016 -0.919 -2.05 -1.815 -3.103 -2.684 -0.772 -0.636 -1.557 -1.255 -2.348 -1.864 -3.465 -2.67 -7.112 -5.075 -10.927 -7.209 -2.869 -1.604 -5.83 -3.06 -8.883 -4.351 -2.443 -1.033 -4.922 -1.948 -7.428 -2.756 -8.879 -2.863 -18.13 -4.318 -27.605 -4.318 -3.19 0 -6.354 0.169 -9.488 0.496 -4.036 0.421 -8.019 1.114 -11.94 2.073 -1.732 0.423 -3.452 0.892 -5.157 1.42 -2.856 0.883 -5.673 1.912 -8.447 3.085 -2.645 1.118 -5.222 2.357 -7.729 3.711 -2.574 1.39 -5.073 2.901 -7.494 4.533 -1.195 0.805 -2.37 1.64 -3.527 2.503 -1.156 0.864 -2.292 1.756 -3.408 2.676 -0.553 0.456 -1.1 0.919 -1.643 1.389 -1.649 1.427 -3.252 2.92 -4.806 4.473 -2.582 2.582 -4.991 5.299 -7.222 8.138 -0.892 1.135 -1.756 2.292 -2.59 3.467 -0.417 0.588 -0.827 1.18 -1.23 1.778 -0.403 0.597 -0.798 1.199 -1.186 1.806 -0.388 0.607 -0.769 1.218 -1.143 1.835 -2.241 3.697 -4.216 7.562 -5.916 11.582 -1.095 2.589 -2.059 5.217 -2.901 7.877 -0.153 0.482 -0.3 0.965 -0.444 1.449 -0.339 1.14 -0.663 2.282 -0.956 3.433 -0.369 1.446 -0.7 2.901 -0.996 4.364 -1.034 5.121 -1.618 10.343 -1.749 15.637 -0.018 0.757 -0.028 1.514 -0.028 2.274 0 1.123 0.02 2.244 0.062 3.361 0.285 7.82 1.568 15.475 3.825 22.879 0.044 0.147 0.088 0.295 0.133 0.441 0.877 2.823 1.894 5.608 3.054 8.35 0.85 2.009 1.769 3.98 2.755 5.912 0.539 1.057 1.105 2.099 1.685 3.132 4.013 7.142 8.98 13.698 14.846 19.564 7.713 7.713 16.611 13.878 26.477 18.352 0.705 0.32 1.415 0.632 2.131 0.935 2.081 0.88 4.185 1.679 6.313 2.396 9.217 3.106 18.85 4.677 28.719 4.677 8.031 0 15.902 -1.047 23.522 -3.107 0.633 -0.172 1.266 -0.35 1.895 -0.535 0.757 -0.222 1.509 -0.456 2.26 -0.698 0.717 -0.232 1.431 -0.474 2.145 -0.723 1.752 -0.616 3.49 -1.281 5.211 -2.009 0.755 -0.319 1.503 -0.651 2.247 -0.989 1.237 -0.563 2.459 -1.15 3.664 -1.766 0.644 -0.328 1.283 -0.665 1.917 -1.009 1.654 -0.896 3.274 -1.848 4.865 -2.844 5.736 -3.591 11.06 -7.827 15.912 -12.679 0.775 -0.775 1.534 -1.562 2.278 -2.36 5.204 -5.59 9.636 -11.754 13.246 -18.417 0.343 -0.634 0.68 -1.274 1.009 -1.917 0.482 -0.944 0.943 -1.9 1.392 -2.863 0.471 -1.007 0.928 -2.021 1.364 -3.049 1.22 -2.886 2.281 -5.82 3.187 -8.793 0.559 -1.833 1.056 -3.68 1.494 -5.542 0.108 -0.458 0.211 -0.916 0.312 -1.376 0.194 -0.883 0.373 -1.77 0.539 -2.659 1.02 -5.455 1.542 -11.02 1.542 -16.663 0 -6.074 -0.595 -12.058 -1.776 -17.911 z m -161.733 19.614 c -1.118 -56.662 44.604 -74.877 60.998 -67.647 2.187 0.965 4.732 2.431 7.042 2.96 5.295 1.213 13.432 -3.113 13.521 6.273 0.078 8.156 -3.389 13.108 -10.797 16.177 -7.539 3.124 -14.777 9.181 -19.95 15.493 -21.487 26.216 -31.231 68.556 -7.565 94.296 -13.679 -5.545 -42.418 -25.467 -43.248 -67.552 z m 91.109 72.619 c -0.053 0.008 -4.171 0.775 -4.171 0.775 0 0 -15.862 -22.957 -23.509 -21.719 11.291 16.04 12.649 22.625 12.649 22.625 -0.053 0.001 -0.107 0.001 -0.161 0.003 -51.831 2.131 -42.785 -64.026 -28.246 -86.502 -1.555 13.073 8.878 39.992 39.034 44.1 9.495 1.293 32.302 -3.275 41.015 -11.38 0.098 1.825 0.163 3.85 0.159 6.013 -0.046 23.538 -13.47 42.743 -36.77 46.085 z m 30.575 -15.708 c 9.647 -9.263 12.869 -27.779 9.103 -44.137 -4.608 -20.011 -28.861 -32.383 -40.744 -35.564 5.766 -8.089 27.908 -14.274 39.567 5.363 -5.172 -10.519 -13.556 -23.023 -1.732 -33.128 12.411 13.329 19.411 29.94 20.161 48.7 0.75 18.753 -6.64 41.768 -26.355 58.765 z"
|
||||
/>
|
||||
<circle
|
||||
style="fill:#C8AF9B;"
|
||||
cx="256"
|
||||
cy="351.999"
|
||||
r="136"
|
||||
fill="currentColor"
|
||||
cx="71.927"
|
||||
cy="32.004"
|
||||
r="2.829"
|
||||
/>
|
||||
<g>
|
||||
<circle
|
||||
style="fill:#464655;"
|
||||
cx="132"
|
||||
cy="203.999"
|
||||
r="28"
|
||||
/>
|
||||
<circle
|
||||
style="fill:#464655;"
|
||||
cx="380"
|
||||
cy="203.999"
|
||||
r="28"
|
||||
/>
|
||||
<path
|
||||
style="fill:#464655;"
|
||||
d="M269.949,284.516c-7.672,10.741-20.227,10.741-27.899,0l-12.101-16.941
|
||||
c-7.672-10.741-3.15-19.53,10.05-19.53h32c13.2,0,17.723,8.788,10.05,19.53L269.949,284.516z"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
style="fill:#AA968C;"
|
||||
d="M 350.964 399.998 C 316.628 399.998 299.021 351.998 255.882 351.998 C 212.742 351.998 195.135 399.998 160.801 399.998 C 145.395 399.998 131.723 394.147 120.621 374.798 C 131.595 439.03 187.893 487.998 255.881 487.998 C 323.868 487.998 380.168 439.03 391.14 374.798 C 380.04 394.148 366.368 399.998 350.964 399.998 Z"
|
||||
/>
|
||||
<g>
|
||||
<path
|
||||
style="fill:#8C7873;"
|
||||
d="M32,423.998c-3.172,0-6.18-1.906-7.43-5.031c-1.641-4.105,0.359-8.758,4.461-10.402l160.008-64
|
||||
c4.062-1.617,8.758,0.352,10.398,4.457s-0.359,8.758-4.461,10.402l-160.008,64C34,423.811,32.992,423.998,32,423.998z"
|
||||
/>
|
||||
<path
|
||||
style="fill:#8C7873;"
|
||||
d="M15.992,375.995c-3.547,0-6.781-2.375-7.727-5.965c-1.125-4.273,1.422-8.648,5.695-9.773l152-40
|
||||
c4.289-1.121,8.648,1.426,9.773,5.703c1.125,4.273-1.422,8.648-5.695,9.773l-152,40C17.351,375.913,16.672,375.995,15.992,375.995z
|
||||
"
|
||||
/>
|
||||
<path
|
||||
style="fill:#8C7873;"
|
||||
d="M7.992,335.995c-3.812,0-7.187-2.73-7.867-6.609c-0.773-4.352,2.133-8.5,6.484-9.27l136-24
|
||||
c4.328-0.77,8.508,2.141,9.266,6.488c0.773,4.352-2.133,8.5-6.484,9.27l-136,24C8.922,335.956,8.453,335.995,7.992,335.995z"
|
||||
/>
|
||||
<path
|
||||
style="fill:#8C7873;"
|
||||
d="M480,423.998c3.172,0,6.18-1.906,7.43-5.031c1.641-4.105-0.359-8.758-4.461-10.402l-160.008-64
|
||||
c-4.063-1.617-8.758,0.352-10.398,4.457s0.359,8.758,4.461,10.402l160.008,64C478,423.811,479.007,423.998,480,423.998z"
|
||||
/>
|
||||
<path
|
||||
style="fill:#8C7873;"
|
||||
d="M496.007,375.995c3.547,0,6.781-2.375,7.727-5.965c1.125-4.273-1.422-8.648-5.695-9.773l-152-40
|
||||
c-4.289-1.121-8.648,1.426-9.773,5.703c-1.125,4.273,1.422,8.648,5.695,9.773l152,40
|
||||
C494.648,375.913,495.328,375.995,496.007,375.995z"
|
||||
/>
|
||||
<path
|
||||
style="fill:#8C7873;"
|
||||
d="M504.007,335.995c3.813,0,7.188-2.73,7.867-6.609c0.773-4.352-2.133-8.5-6.484-9.27l-136-24
|
||||
c-4.328-0.77-8.508,2.141-9.266,6.488c-0.773,4.352,2.133,8.5,6.484,9.27l136,24C503.078,335.956,503.546,335.995,504.007,335.995z
|
||||
"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ export default {
|
||||
removeSubmitter (submitter) {
|
||||
[...this.fields].forEach((field) => {
|
||||
if (field.submitter_uuid === submitter.uuid) {
|
||||
this.removeField(field)
|
||||
this.removeField(field, false)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -89,10 +89,12 @@ export default {
|
||||
|
||||
this.save()
|
||||
},
|
||||
removeField (field) {
|
||||
removeField (field, save = true) {
|
||||
this.fields.splice(this.fields.indexOf(field), 1)
|
||||
|
||||
this.save()
|
||||
if (save) {
|
||||
this.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
v-for="(icon, type) in fieldIconsSorted"
|
||||
:key="type"
|
||||
>
|
||||
<li v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification'))">
|
||||
<li v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification') && (withKba || type != 'kba'))">
|
||||
<a
|
||||
href="#"
|
||||
class="text-sm py-1 px-2"
|
||||
@@ -89,7 +89,7 @@ export default {
|
||||
IconPlus,
|
||||
IconX
|
||||
},
|
||||
inject: ['withPhone', 'withPayment', 'withVerification', 'backgroundColor', 't'],
|
||||
inject: ['withPhone', 'withPayment', 'withVerification', 'withKba', 'backgroundColor', 't'],
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative select-none mb-4 before:border before:rounded before:top-0 before:bottom-0 before:left-0 before:right-0 before:absolute"
|
||||
:class="{ 'cursor-crosshair': allowDraw && editable, 'touch-none': !!drawField }"
|
||||
:class="{ 'cursor-crosshair': allowDraw && editable && !isSelectMode, 'touch-none': !!drawField }"
|
||||
style="container-type: size"
|
||||
:style="{ aspectRatio: `${width} / ${height}`}"
|
||||
>
|
||||
@@ -17,7 +17,20 @@
|
||||
<div
|
||||
class="top-0 bottom-0 left-0 right-0 absolute"
|
||||
@pointerdown="onStartDraw"
|
||||
@contextmenu="openContextMenu"
|
||||
>
|
||||
<SelectionBox
|
||||
v-if="showSelectionBox"
|
||||
:selection-box="selectionBox"
|
||||
:page-width="width"
|
||||
:page-height="height"
|
||||
:is-resizing="!!resizeDirection"
|
||||
:is-drawing="!!drawFieldType"
|
||||
:is-drag="isDrag"
|
||||
@move="onSelectionBoxMove"
|
||||
@contextmenu="openSelectionContextMenu"
|
||||
@close-context-menu="closeContextMenu"
|
||||
/>
|
||||
<FieldArea
|
||||
v-for="(item, i) in areas"
|
||||
:key="i"
|
||||
@@ -34,31 +47,81 @@
|
||||
:default-field="defaultFieldsIndex[item.field.name]"
|
||||
:default-submitters="defaultSubmitters"
|
||||
:max-page="totalPages - 1"
|
||||
:is-select-mode="isSelectMode"
|
||||
:is-mobile="isMobile"
|
||||
@start-resize="resizeDirection = $event"
|
||||
@stop-resize="resizeDirection = null"
|
||||
@remove="$emit('remove-area', item.area)"
|
||||
@scroll-to="$emit('scroll-to', $event)"
|
||||
@add-custom-field="$emit('add-custom-field', $event)"
|
||||
@contextmenu="openAreaContextMenu($event, item.area, item.field)"
|
||||
/>
|
||||
<FieldArea
|
||||
v-if="newArea"
|
||||
v-for="(area, index) in newAreas"
|
||||
:key="index"
|
||||
:is-draw="true"
|
||||
:page-width="width"
|
||||
:page-height="height"
|
||||
:field="{ submitter_uuid: selectedSubmitter.uuid, type: drawField?.type || dragFieldPlaceholder?.type || defaultFieldType }"
|
||||
:area="newArea"
|
||||
:field="{ submitter_uuid: selectedSubmitter.uuid, type: newAreaFieldType }"
|
||||
:area="area"
|
||||
/>
|
||||
<div
|
||||
v-if="newAreas.length > 1"
|
||||
class="absolute outline-dashed outline-gray-400 pointer-events-none z-20"
|
||||
:style="newAreasBoxStyle"
|
||||
/>
|
||||
<div
|
||||
v-if="selectionRect"
|
||||
class="absolute outline-dashed outline-gray-400 pointer-events-none z-20"
|
||||
:style="selectionRectStyle"
|
||||
/>
|
||||
<FieldContextMenu
|
||||
v-if="contextMenu && contextMenu.field"
|
||||
:context-menu="contextMenu"
|
||||
:field="contextMenu.field"
|
||||
:with-signature-id="withSignatureId"
|
||||
:with-prefillable="withPrefillable"
|
||||
:editable="editable"
|
||||
:default-field="defaultFieldsIndex[contextMenu.field.name]"
|
||||
@copy="handleCopy"
|
||||
@delete="handleDelete"
|
||||
@close="closeContextMenu"
|
||||
@set-draw="$emit('set-draw', $event)"
|
||||
@scroll-to="$emit('scroll-to', $event)"
|
||||
@save="save"
|
||||
@add-custom-field="$emit('add-custom-field', $event)"
|
||||
/>
|
||||
<SelectionContextMenu
|
||||
v-else-if="contextMenu && contextMenu.areas"
|
||||
:context-menu="contextMenu"
|
||||
:editable="editable"
|
||||
:template="template"
|
||||
@copy="handleSelectionCopy"
|
||||
@delete="handleSelectionDelete"
|
||||
@close="closeContextMenu"
|
||||
/>
|
||||
<PageContextMenu
|
||||
v-else-if="contextMenu && !contextMenu.field && !contextMenu.areas"
|
||||
:context-menu="contextMenu"
|
||||
:editable="editable"
|
||||
:with-fields-detection="withFieldsDetection"
|
||||
@paste="handlePaste"
|
||||
@autodetect-fields="handleAutodetectFields"
|
||||
@close="closeContextMenu"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-show="resizeDirection || isDrag || showMask || (drawField && isMobile) || fieldsDragFieldRef.value"
|
||||
v-show="resizeDirection || isDrag || showMask || (drawField && isMobile) || fieldsDragFieldRef.value || customDragFieldRef?.value || selectionRect"
|
||||
id="mask"
|
||||
ref="mask"
|
||||
class="top-0 bottom-0 left-0 right-0 absolute"
|
||||
:class="{ 'z-10': !isMobile, 'cursor-grab': isDrag, 'cursor-nwse-resize': drawField, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }"
|
||||
:class="{ 'z-10': !isMobile, 'cursor-grab': isDrag, 'cursor-nwse-resize': drawField && !isSelectMode, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }"
|
||||
@pointermove="onPointermove"
|
||||
@pointerdown="onStartDraw"
|
||||
@contextmenu="openContextMenu"
|
||||
@dragover.prevent="onDragover"
|
||||
@dragenter="onDragenter"
|
||||
@dragleave="newArea = null"
|
||||
@dragleave="newAreas = []"
|
||||
@drop="onDrop"
|
||||
@pointerup="onPointerup"
|
||||
/>
|
||||
@@ -67,13 +130,21 @@
|
||||
|
||||
<script>
|
||||
import FieldArea from './area'
|
||||
import FieldContextMenu from './field_context_menu'
|
||||
import SelectionContextMenu from './selection_context_menu'
|
||||
import PageContextMenu from './page_context_menu'
|
||||
import SelectionBox from './selection_box'
|
||||
|
||||
export default {
|
||||
name: 'TemplatePage',
|
||||
components: {
|
||||
FieldArea
|
||||
FieldArea,
|
||||
FieldContextMenu,
|
||||
SelectionContextMenu,
|
||||
PageContextMenu,
|
||||
SelectionBox
|
||||
},
|
||||
inject: ['fieldTypes', 'defaultDrawFieldType', 'fieldsDragFieldRef', 'assignDropAreaSize'],
|
||||
inject: ['fieldTypes', 'defaultDrawFieldType', 'fieldsDragFieldRef', 'customDragFieldRef', 'assignDropAreaSize', 'selectedAreasRef', 'template', 'isSelectModeRef', 'save'],
|
||||
props: {
|
||||
image: {
|
||||
type: Object,
|
||||
@@ -84,6 +155,11 @@ export default {
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
isMobile: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
withSignatureId: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -142,6 +218,11 @@ export default {
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
drawCustomField: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -155,18 +236,68 @@ export default {
|
||||
number: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
attachmentUuid: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
withFieldsDetection: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['draw', 'drop-field', 'remove-area', 'scroll-to'],
|
||||
emits: ['draw', 'drop-field', 'remove-area', 'copy-field', 'paste-field', 'scroll-to', 'copy-selected-areas', 'delete-selected-areas', 'autodetect-fields', 'add-custom-field', 'set-draw'],
|
||||
data () {
|
||||
return {
|
||||
areaRefs: [],
|
||||
showMask: false,
|
||||
resizeDirection: null,
|
||||
newArea: null
|
||||
newAreas: [],
|
||||
contextMenu: null,
|
||||
selectionRect: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isSelectMode () {
|
||||
return this.isSelectModeRef.value && !this.drawFieldType && this.editable && !this.drawField && !this.drawCustomField
|
||||
},
|
||||
pageSelectedAreas () {
|
||||
if (!this.selectedAreasRef.value) return []
|
||||
|
||||
return this.selectedAreasRef.value.filter((a) =>
|
||||
a.attachment_uuid === this.attachmentUuid && a.page === this.number
|
||||
)
|
||||
},
|
||||
showSelectionBox () {
|
||||
return this.pageSelectedAreas.length >= 2 && this.editable
|
||||
},
|
||||
minSelectionBoxHeight () {
|
||||
const ys = this.pageSelectedAreas.map((a) => a.y)
|
||||
|
||||
return Math.max(...ys) - Math.min(...ys)
|
||||
},
|
||||
minSelectionBoxWidth () {
|
||||
const xs = this.pageSelectedAreas.map((a) => a.x)
|
||||
|
||||
return Math.max(...xs) - Math.min(...xs)
|
||||
},
|
||||
selectionBox () {
|
||||
if (!this.pageSelectedAreas.length) return null
|
||||
|
||||
const minX = Math.min(...this.pageSelectedAreas.map((a) => a.x))
|
||||
const minY = Math.min(...this.pageSelectedAreas.map((a) => a.y))
|
||||
const maxX = Math.max(...this.pageSelectedAreas.map((a) => a.x + a.w))
|
||||
const maxY = Math.max(...this.pageSelectedAreas.map((a) => a.y + a.h))
|
||||
|
||||
return {
|
||||
x: minX,
|
||||
y: minY,
|
||||
w: Math.max(maxX - minX, this.minSelectionBoxWidth),
|
||||
h: Math.max(maxY - minY, this.minSelectionBoxHeight)
|
||||
}
|
||||
},
|
||||
defaultFieldsIndex () {
|
||||
return this.defaultFields.reduce((acc, field) => {
|
||||
acc[field.name] = field
|
||||
@@ -174,6 +305,14 @@ export default {
|
||||
return acc
|
||||
}, {})
|
||||
},
|
||||
newAreaFieldType () {
|
||||
if (this.drawField?.type) return this.drawField.type
|
||||
if (this.drawCustomField?.type) return this.drawCustomField.type
|
||||
if (this.dragFieldPlaceholder?.type) return this.dragFieldPlaceholder.type
|
||||
if (this.customDragFieldRef?.value?.type) return this.customDragFieldRef.value.type
|
||||
|
||||
return this.defaultFieldType
|
||||
},
|
||||
defaultFieldType () {
|
||||
if (this.drawFieldType) {
|
||||
return this.drawFieldType
|
||||
@@ -185,11 +324,6 @@ export default {
|
||||
return 'text'
|
||||
}
|
||||
},
|
||||
isMobile () {
|
||||
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
|
||||
|
||||
return isMobileSafariIos || /android|iphone|ipad/i.test(navigator.userAgent)
|
||||
},
|
||||
resizeDirectionClasses () {
|
||||
return {
|
||||
nwse: 'cursor-nwse-resize',
|
||||
@@ -201,6 +335,31 @@ export default {
|
||||
},
|
||||
height () {
|
||||
return this.image.metadata.height
|
||||
},
|
||||
newAreasBoxStyle () {
|
||||
if (this.newAreas.length < 2) return {}
|
||||
|
||||
const minX = Math.min(...this.newAreas.map(a => a.x))
|
||||
const minY = Math.min(...this.newAreas.map(a => a.y))
|
||||
const maxX = Math.max(...this.newAreas.map(a => a.x + a.w))
|
||||
const maxY = Math.max(...this.newAreas.map(a => a.y + a.h))
|
||||
|
||||
return {
|
||||
left: minX * 100 + '%',
|
||||
top: minY * 100 + '%',
|
||||
width: (maxX - minX) * 100 + '%',
|
||||
height: (maxY - minY) * 100 + '%'
|
||||
}
|
||||
},
|
||||
selectionRectStyle () {
|
||||
if (!this.selectionRect) return {}
|
||||
|
||||
return {
|
||||
left: this.selectionRect.x * 100 + '%',
|
||||
top: this.selectionRect.y * 100 + '%',
|
||||
width: this.selectionRect.w * 100 + '%',
|
||||
height: this.selectionRect.h * 100 + '%'
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeUpdate () {
|
||||
@@ -211,28 +370,183 @@ export default {
|
||||
this.image.metadata.width = e.target.naturalWidth
|
||||
this.image.metadata.height = e.target.naturalHeight
|
||||
},
|
||||
openContextMenu (event) {
|
||||
if (!this.editable) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const rect = this.$refs.image.getBoundingClientRect()
|
||||
|
||||
this.newAreas = []
|
||||
this.showMask = false
|
||||
|
||||
this.contextMenu = {
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
relativeX: (event.clientX - rect.left) / rect.width,
|
||||
relativeY: (event.clientY - rect.top) / rect.height
|
||||
}
|
||||
},
|
||||
openAreaContextMenu (event, area, field) {
|
||||
if (!this.editable) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const rect = this.$refs.image.getBoundingClientRect()
|
||||
|
||||
this.newAreas = []
|
||||
this.showMask = false
|
||||
|
||||
this.contextMenu = {
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
relativeX: (event.clientX - rect.left) / rect.width,
|
||||
relativeY: (event.clientY - rect.top) / rect.height,
|
||||
area,
|
||||
field
|
||||
}
|
||||
},
|
||||
openSelectionContextMenu (event) {
|
||||
if (!this.editable) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const rect = this.$el.getBoundingClientRect()
|
||||
|
||||
this.contextMenu = {
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
relativeX: (event.clientX - rect.left) / rect.width,
|
||||
relativeY: (event.clientY - rect.top) / rect.height,
|
||||
areas: this.selectedAreasRef.value
|
||||
}
|
||||
},
|
||||
handleSelectionCopy () {
|
||||
this.$emit('copy-selected-areas')
|
||||
|
||||
this.closeContextMenu()
|
||||
},
|
||||
handleSelectionDelete () {
|
||||
this.$emit('delete-selected-areas')
|
||||
|
||||
this.closeContextMenu()
|
||||
},
|
||||
closeContextMenu () {
|
||||
this.contextMenu = null
|
||||
this.newAreas = []
|
||||
this.showMask = false
|
||||
},
|
||||
handleCopy () {
|
||||
if (this.contextMenu.area) {
|
||||
this.selectedAreasRef.value = [this.contextMenu.area]
|
||||
|
||||
this.$emit('copy-field')
|
||||
}
|
||||
|
||||
this.closeContextMenu()
|
||||
},
|
||||
handleDelete () {
|
||||
if (this.contextMenu.area) {
|
||||
this.$emit('remove-area', this.contextMenu.area)
|
||||
}
|
||||
|
||||
this.closeContextMenu()
|
||||
},
|
||||
handlePaste () {
|
||||
this.newAreas = []
|
||||
this.showMask = false
|
||||
|
||||
this.$emit('paste-field', {
|
||||
page: this.number,
|
||||
x: this.contextMenu.relativeX,
|
||||
y: this.contextMenu.relativeY
|
||||
})
|
||||
|
||||
this.closeContextMenu()
|
||||
},
|
||||
handleAutodetectFields () {
|
||||
this.$emit('autodetect-fields', {
|
||||
page: this.number,
|
||||
attachmentUuid: this.attachmentUuid
|
||||
})
|
||||
|
||||
this.closeContextMenu()
|
||||
},
|
||||
setAreaRefs (el) {
|
||||
if (el) {
|
||||
this.areaRefs.push(el)
|
||||
}
|
||||
},
|
||||
onDragenter (e) {
|
||||
this.newArea = {}
|
||||
const customField = this.customDragFieldRef?.value
|
||||
const customAreas = customField?.areas || []
|
||||
const dropX = (e.offsetX - 6) / this.$refs.mask.clientWidth
|
||||
const dropY = e.offsetY / this.$refs.mask.clientHeight
|
||||
|
||||
this.assignDropAreaSize(this.newArea, this.dragFieldPlaceholder, {
|
||||
maskW: this.$refs.mask.clientWidth,
|
||||
maskH: this.$refs.mask.clientHeight
|
||||
})
|
||||
if (customAreas.length > 1) {
|
||||
const refArea = customAreas[0]
|
||||
|
||||
this.newArea.x = (e.offsetX - 6) / this.$refs.mask.clientWidth
|
||||
this.newArea.y = e.offsetY / this.$refs.mask.clientHeight - this.newArea.h / 2
|
||||
this.newAreas = customAreas.map((customArea) => ({
|
||||
x: dropX + (customArea.x - refArea.x),
|
||||
y: dropY + (customArea.y - refArea.y) - (customArea.h / 2),
|
||||
w: customArea.w,
|
||||
h: customArea.h
|
||||
}))
|
||||
} else {
|
||||
const newArea = {}
|
||||
|
||||
if (customAreas.length === 1) {
|
||||
newArea.w = customAreas[0].w
|
||||
newArea.h = customAreas[0].h
|
||||
} else if (customField) {
|
||||
this.assignDropAreaSize(newArea, customField, {
|
||||
maskW: this.$refs.mask.clientWidth,
|
||||
maskH: this.$refs.mask.clientHeight
|
||||
})
|
||||
} else {
|
||||
this.assignDropAreaSize(newArea, this.dragFieldPlaceholder, {
|
||||
maskW: this.$refs.mask.clientWidth,
|
||||
maskH: this.$refs.mask.clientHeight
|
||||
})
|
||||
}
|
||||
|
||||
newArea.x = dropX
|
||||
newArea.y = dropY - newArea.h / 2
|
||||
|
||||
this.newAreas = [newArea]
|
||||
}
|
||||
},
|
||||
onDragover (e) {
|
||||
this.newArea.x = (e.offsetX - 6) / this.$refs.mask.clientWidth
|
||||
this.newArea.y = e.offsetY / this.$refs.mask.clientHeight - this.newArea.h / 2
|
||||
const customField = this.customDragFieldRef?.value
|
||||
const customAreas = customField?.areas || []
|
||||
const dropX = (e.offsetX - 6) / this.$refs.mask.clientWidth
|
||||
const dropY = e.offsetY / this.$refs.mask.clientHeight
|
||||
|
||||
if (customAreas.length > 1) {
|
||||
const refArea = customAreas[0]
|
||||
|
||||
this.newAreas.forEach((newArea, index) => {
|
||||
const customArea = customAreas[index]
|
||||
|
||||
newArea.x = dropX + (customArea.x - refArea.x)
|
||||
newArea.y = dropY + (customArea.y - refArea.y) - (customArea.h / 2)
|
||||
})
|
||||
} else if (this.newAreas.length) {
|
||||
this.newAreas[0].x = dropX
|
||||
this.newAreas[0].y = dropY - this.newAreas[0].h / 2
|
||||
}
|
||||
},
|
||||
onDrop (e) {
|
||||
this.newArea = null
|
||||
this.newAreas = []
|
||||
|
||||
this.$emit('drop-field', {
|
||||
x: e.offsetX,
|
||||
@@ -243,11 +557,25 @@ export default {
|
||||
})
|
||||
},
|
||||
onStartDraw (e) {
|
||||
if (e.button === 2) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.selectedAreasRef.value.length >= 2) {
|
||||
this.selectedAreasRef.value = []
|
||||
}
|
||||
|
||||
if (this.isSelectMode) {
|
||||
this.startSelectionRect(e)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.allowDraw) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.isMobile && !this.drawField) {
|
||||
if (this.isMobile && !this.drawField && !this.drawCustomField) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -258,65 +586,154 @@ export default {
|
||||
this.showMask = true
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.newArea = {
|
||||
this.newAreas = [{
|
||||
initialX: e.offsetX / this.$refs.mask.clientWidth,
|
||||
initialY: e.offsetY / this.$refs.mask.clientHeight,
|
||||
x: e.offsetX / this.$refs.mask.clientWidth,
|
||||
y: e.offsetY / this.$refs.mask.clientHeight,
|
||||
w: 0,
|
||||
h: 0
|
||||
}]
|
||||
})
|
||||
},
|
||||
startSelectionRect (e) {
|
||||
this.selectedAreasRef.value = []
|
||||
|
||||
this.showMask = true
|
||||
|
||||
this.$nextTick(() => {
|
||||
const x = e.offsetX / this.$refs.mask.clientWidth
|
||||
const y = e.offsetY / this.$refs.mask.clientHeight
|
||||
|
||||
this.selectionRect = {
|
||||
initialX: x,
|
||||
initialY: y,
|
||||
x,
|
||||
y,
|
||||
w: 0,
|
||||
h: 0
|
||||
}
|
||||
})
|
||||
},
|
||||
onSelectionBoxMove (dx, dy) {
|
||||
let clampedDx = dx
|
||||
let clampedDy = dy
|
||||
|
||||
this.pageSelectedAreas.forEach((area) => {
|
||||
const maxDxLeft = -area.x
|
||||
const maxDxRight = 1 - area.w - area.x
|
||||
const maxDyTop = -area.y
|
||||
const maxDyBottom = 1 - area.h - area.y
|
||||
|
||||
if (dx < maxDxLeft) clampedDx = Math.max(clampedDx, maxDxLeft)
|
||||
if (dx > maxDxRight) clampedDx = Math.min(clampedDx, maxDxRight)
|
||||
if (dy < maxDyTop) clampedDy = Math.max(clampedDy, maxDyTop)
|
||||
if (dy > maxDyBottom) clampedDy = Math.min(clampedDy, maxDyBottom)
|
||||
})
|
||||
|
||||
this.pageSelectedAreas.forEach((area) => {
|
||||
area.x += clampedDx
|
||||
area.y += clampedDy
|
||||
})
|
||||
},
|
||||
onPointermove (e) {
|
||||
if (this.newArea) {
|
||||
const dx = e.offsetX / this.$refs.mask.clientWidth - this.newArea.initialX
|
||||
const dy = e.offsetY / this.$refs.mask.clientHeight - this.newArea.initialY
|
||||
if (this.selectionRect) {
|
||||
const dx = e.offsetX / this.$refs.mask.clientWidth - this.selectionRect.initialX
|
||||
const dy = e.offsetY / this.$refs.mask.clientHeight - this.selectionRect.initialY
|
||||
|
||||
if (dx > 0) {
|
||||
this.newArea.x = this.newArea.initialX
|
||||
this.selectionRect.x = this.selectionRect.initialX
|
||||
} else {
|
||||
this.newArea.x = e.offsetX / this.$refs.mask.clientWidth
|
||||
this.selectionRect.x = e.offsetX / this.$refs.mask.clientWidth
|
||||
}
|
||||
|
||||
if (dy > 0) {
|
||||
this.newArea.y = this.newArea.initialY
|
||||
this.selectionRect.y = this.selectionRect.initialY
|
||||
} else {
|
||||
this.newArea.y = e.offsetY / this.$refs.mask.clientHeight
|
||||
this.selectionRect.y = e.offsetY / this.$refs.mask.clientHeight
|
||||
}
|
||||
|
||||
if ((this.drawField?.type || this.drawFieldType) === 'cells') {
|
||||
this.newArea.cell_w = this.newArea.h * (this.$refs.mask.clientHeight / this.$refs.mask.clientWidth)
|
||||
this.selectionRect.w = Math.abs(dx)
|
||||
this.selectionRect.h = Math.abs(dy)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const drawArea = this.newAreas[0]
|
||||
|
||||
if (drawArea?.initialX !== undefined) {
|
||||
const dx = e.offsetX / this.$refs.mask.clientWidth - drawArea.initialX
|
||||
const dy = e.offsetY / this.$refs.mask.clientHeight - drawArea.initialY
|
||||
|
||||
if (dx > 0) {
|
||||
drawArea.x = drawArea.initialX
|
||||
} else {
|
||||
drawArea.x = e.offsetX / this.$refs.mask.clientWidth
|
||||
}
|
||||
|
||||
this.newArea.w = Math.abs(dx)
|
||||
this.newArea.h = Math.abs(dy)
|
||||
if (dy > 0) {
|
||||
drawArea.y = drawArea.initialY
|
||||
} else {
|
||||
drawArea.y = e.offsetY / this.$refs.mask.clientHeight
|
||||
}
|
||||
|
||||
if ((this.drawField?.type || this.drawCustomField?.type || this.drawFieldType) === 'cells') {
|
||||
drawArea.cell_w = drawArea.h * (this.$refs.mask.clientHeight / this.$refs.mask.clientWidth)
|
||||
}
|
||||
|
||||
drawArea.w = Math.abs(dx)
|
||||
drawArea.h = Math.abs(dy)
|
||||
}
|
||||
},
|
||||
onPointerup (e) {
|
||||
if (this.newArea) {
|
||||
const area = {
|
||||
x: this.newArea.x,
|
||||
y: this.newArea.y,
|
||||
w: this.newArea.w,
|
||||
h: this.newArea.h,
|
||||
page: this.number
|
||||
if (this.selectionRect) {
|
||||
const selRect = this.selectionRect
|
||||
const areasToSelect = this.areas || []
|
||||
|
||||
areasToSelect.forEach((item) => {
|
||||
const area = item.area
|
||||
|
||||
if (this.rectsOverlap(selRect, area)) {
|
||||
this.selectedAreasRef.value.push(area)
|
||||
}
|
||||
})
|
||||
|
||||
this.selectionRect = null
|
||||
} else {
|
||||
const drawArea = this.newAreas[0]
|
||||
|
||||
if (drawArea?.initialX !== undefined) {
|
||||
const area = {
|
||||
x: drawArea.x,
|
||||
y: drawArea.y,
|
||||
w: drawArea.w,
|
||||
h: drawArea.h,
|
||||
page: this.number
|
||||
}
|
||||
|
||||
if ('cell_w' in drawArea) {
|
||||
area.cell_w = drawArea.cell_w
|
||||
}
|
||||
|
||||
const dx = Math.abs(e.offsetX - this.$refs.mask.clientWidth * drawArea.initialX)
|
||||
const dy = Math.abs(e.offsetY - this.$refs.mask.clientHeight * drawArea.initialY)
|
||||
|
||||
const isTooSmall = dx < 8 && dy < 8
|
||||
|
||||
this.$emit('draw', { area, isTooSmall })
|
||||
}
|
||||
|
||||
if ('cell_w' in this.newArea) {
|
||||
area.cell_w = this.newArea.cell_w
|
||||
}
|
||||
|
||||
const dx = Math.abs(e.offsetX - this.$refs.mask.clientWidth * this.newArea.initialX)
|
||||
const dy = Math.abs(e.offsetY - this.$refs.mask.clientHeight * this.newArea.initialY)
|
||||
|
||||
const isTooSmall = dx < 8 && dy < 8
|
||||
|
||||
this.$emit('draw', { area, isTooSmall })
|
||||
}
|
||||
|
||||
this.showMask = false
|
||||
this.newArea = null
|
||||
this.newAreas = []
|
||||
},
|
||||
rectsOverlap (r1, r2) {
|
||||
return !(
|
||||
r1.x + r1.w < r2.x ||
|
||||
r2.x + r2.w < r1.x ||
|
||||
r1.y + r1.h < r2.y ||
|
||||
r2.y + r2.h < r1.y
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<div
|
||||
ref="menu"
|
||||
class="fixed z-50 p-1 bg-white shadow-lg rounded-lg border border-neutral-200 cursor-default"
|
||||
style="min-width: 170px"
|
||||
:style="menuStyle"
|
||||
@mousedown.stop
|
||||
@pointerdown.stop
|
||||
>
|
||||
<button
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
|
||||
:class="!hasClipboardData ? 'opacity-50 cursor-not-allowed' : 'hover:bg-neutral-100'"
|
||||
:disabled="!hasClipboardData"
|
||||
@click.stop="!hasClipboardData ? null : $emit('paste')"
|
||||
>
|
||||
<span class="flex items-center space-x-2">
|
||||
<IconClipboard class="w-4 h-4" />
|
||||
<span>{{ t('paste') }}</span>
|
||||
</span>
|
||||
<span class="text-xs text-base-content/60 ml-4">{{ isMac ? '⌘V' : 'Ctrl+V' }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
|
||||
@click.stop="handleToggleSelectMode"
|
||||
>
|
||||
<span class="flex items-center space-x-2">
|
||||
<IconClick
|
||||
v-if="!isSelectModeRef.value"
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
<IconNewSection
|
||||
v-else
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
<span>{{ isSelectModeRef.value ? t('draw_fields') : t('select_fields') }}</span>
|
||||
</span>
|
||||
<span class="text-xs text-base-content/60 ml-4">Tab</span>
|
||||
</button>
|
||||
<hr
|
||||
v-if="showAutodetectFields"
|
||||
class="my-1 border-neutral-200"
|
||||
>
|
||||
<button
|
||||
v-if="showAutodetectFields"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="$emit('autodetect-fields')"
|
||||
>
|
||||
<IconSparkles class="w-4 h-4" />
|
||||
<span>{{ t('autodetect_fields') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IconClipboard, IconClick, IconNewSection, IconSparkles } from '@tabler/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'PageContextMenu',
|
||||
components: {
|
||||
IconClipboard,
|
||||
IconClick,
|
||||
IconNewSection,
|
||||
IconSparkles
|
||||
},
|
||||
inject: ['t', 'isSelectModeRef'],
|
||||
props: {
|
||||
contextMenu: {
|
||||
type: Object,
|
||||
default: null,
|
||||
required: true
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
withFieldsDetection: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['paste', 'close', 'autodetect-fields'],
|
||||
computed: {
|
||||
isMac () {
|
||||
return (navigator.userAgentData?.platform || navigator.platform)?.toLowerCase()?.includes('mac')
|
||||
},
|
||||
hasClipboardData () {
|
||||
try {
|
||||
const clipboard = localStorage.getItem('docuseal_clipboard')
|
||||
|
||||
if (clipboard) {
|
||||
const data = JSON.parse(clipboard)
|
||||
|
||||
return Date.now() - data.timestamp < 3600000
|
||||
}
|
||||
|
||||
return false
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
},
|
||||
menuStyle () {
|
||||
return {
|
||||
left: this.contextMenu.x + 'px',
|
||||
top: this.contextMenu.y + 'px'
|
||||
}
|
||||
},
|
||||
showAutodetectFields () {
|
||||
return this.withFieldsDetection && this.editable
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
document.addEventListener('keydown', this.onKeyDown)
|
||||
document.addEventListener('mousedown', this.handleClickOutside)
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.checkMenuPosition()
|
||||
})
|
||||
},
|
||||
beforeUnmount () {
|
||||
document.removeEventListener('keydown', this.onKeyDown)
|
||||
document.removeEventListener('mousedown', this.handleClickOutside)
|
||||
},
|
||||
methods: {
|
||||
checkMenuPosition () {
|
||||
if (this.$refs.menu) {
|
||||
const rect = this.$refs.menu.getBoundingClientRect()
|
||||
const overflow = rect.bottom - window.innerHeight
|
||||
|
||||
if (overflow > 0) {
|
||||
this.contextMenu.y = this.contextMenu.y - overflow - 4
|
||||
}
|
||||
}
|
||||
},
|
||||
onKeyDown (event) {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
this.$emit('close')
|
||||
} else if ((event.ctrlKey || event.metaKey) && event.key === 'v' && this.hasClipboardData) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
this.$emit('paste')
|
||||
}
|
||||
},
|
||||
handleClickOutside (event) {
|
||||
if (this.$refs.menu && !this.$refs.menu.contains(event.target)) {
|
||||
this.$emit('close')
|
||||
}
|
||||
},
|
||||
handleToggleSelectMode () {
|
||||
this.isSelectModeRef.value = !this.isSelectModeRef.value
|
||||
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<span
|
||||
class="dropdown dropdown-end field-settings-dropdown"
|
||||
:class="{ 'dropdown-open': ((!field.preferences?.price && !field.preferences?.formula && !field.preferences?.price_id && !field.preferences?.payment_link_id) || !isConnected) && !isLoading }"
|
||||
:class="{ 'dropdown-open': withForceOpen && ((!field.preferences?.price && !field.preferences?.formula && !field.preferences?.price_id && !field.preferences?.payment_link_id) || !isConnected) && !isLoading }"
|
||||
>
|
||||
<label
|
||||
tabindex="0"
|
||||
@@ -231,7 +231,10 @@
|
||||
</span>
|
||||
</label>
|
||||
</li>
|
||||
<li class="mt-1">
|
||||
<li
|
||||
v-if="withCondition"
|
||||
class="mt-1"
|
||||
>
|
||||
<label
|
||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||
@click="$emit('click-condition')"
|
||||
@@ -244,12 +247,29 @@
|
||||
</span>
|
||||
</label>
|
||||
</li>
|
||||
<hr
|
||||
v-if="withCustomFields"
|
||||
class="pb-0.5 mt-0.5"
|
||||
>
|
||||
<li v-if="withCustomFields">
|
||||
<a
|
||||
href="#"
|
||||
class="text-sm py-1 px-2"
|
||||
@click.prevent="$emit('add-custom-field', field)"
|
||||
>
|
||||
<IconForms
|
||||
:width="20"
|
||||
:stroke-width="1.6"
|
||||
/>
|
||||
{{ t('save_as_custom_field') }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IconMathFunction, IconSettings, IconCircleCheck, IconInfoCircle, IconBrandStripe, IconInnerShadowTop, IconRouteAltLeft } from '@tabler/icons-vue'
|
||||
import { IconMathFunction, IconSettings, IconCircleCheck, IconInfoCircle, IconBrandStripe, IconInnerShadowTop, IconRouteAltLeft, IconForms } from '@tabler/icons-vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const isConnected = ref(false)
|
||||
@@ -261,6 +281,7 @@ export default {
|
||||
IconCircleCheck,
|
||||
IconRouteAltLeft,
|
||||
IconInfoCircle,
|
||||
IconForms,
|
||||
IconMathFunction,
|
||||
IconInnerShadowTop,
|
||||
IconBrandStripe
|
||||
@@ -270,9 +291,24 @@ export default {
|
||||
field: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
withForceOpen: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
withCustomFields: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
withCondition: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
emits: ['click-condition', 'click-description', 'click-formula'],
|
||||
emits: ['click-condition', 'click-description', 'click-formula', 'add-custom-field'],
|
||||
data () {
|
||||
return {
|
||||
isLoading: false
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
<ConditionsModal
|
||||
:item="item"
|
||||
:build-default-name="buildDefaultName"
|
||||
@save="$emit('change')"
|
||||
@close="isShowConditionsModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
@@ -145,7 +146,7 @@ export default {
|
||||
GoogleDriveDocumentSettings,
|
||||
IconSortDescending2
|
||||
},
|
||||
inject: ['t'],
|
||||
inject: ['t', 'getFieldTypeIndex'],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div
|
||||
class="absolute outline-dashed outline-gray-400 cursor-move"
|
||||
:class="[isResizing || isCmdKeyRef.value ? 'z-0' : 'z-20', { 'pointer-events-none': isDrawing || fieldsDragFieldRef.value || isDrag }]"
|
||||
:style="positionStyle"
|
||||
@pointerdown.stop.prevent="onPointerDown"
|
||||
@contextmenu.stop.prevent="openContextMenu"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SelectionBox',
|
||||
inject: ['save', 'selectedAreasRef', 'isCmdKeyRef', 'fieldsDragFieldRef'],
|
||||
props: {
|
||||
selectionBox: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
pageWidth: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
pageHeight: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
isResizing: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isDrag: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isDrawing: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['move', 'contextmenu', 'close-context-menu'],
|
||||
data () {
|
||||
return {
|
||||
isDragging: false,
|
||||
dragStart: { x: 0, y: 0 }
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
positionStyle () {
|
||||
const { x, y, w, h } = this.selectionBox
|
||||
|
||||
return {
|
||||
top: y * 100 + '%',
|
||||
left: x * 100 + '%',
|
||||
width: w * 100 + '%',
|
||||
height: h * 100 + '%'
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openContextMenu (e) {
|
||||
this.$emit('contextmenu', e)
|
||||
},
|
||||
onPointerDown (e) {
|
||||
this.$emit('close-context-menu')
|
||||
|
||||
this.startDrag(e)
|
||||
},
|
||||
startDrag (e) {
|
||||
this.isDragging = true
|
||||
this.dragStart = { x: e.clientX, y: e.clientY }
|
||||
|
||||
document.addEventListener('pointermove', this.onDrag)
|
||||
document.addEventListener('pointerup', this.stopDrag)
|
||||
},
|
||||
onDrag (e) {
|
||||
if (!this.isDragging) return
|
||||
|
||||
const parent = this.$el.parentElement
|
||||
|
||||
const rect = parent.getBoundingClientRect()
|
||||
|
||||
const dx = (e.clientX - this.dragStart.x) / rect.width
|
||||
const dy = (e.clientY - this.dragStart.y) / rect.height
|
||||
|
||||
this.$emit('move', dx, dy)
|
||||
|
||||
this.dragStart = { x: e.clientX, y: e.clientY }
|
||||
},
|
||||
stopDrag () {
|
||||
this.isDragging = false
|
||||
|
||||
document.removeEventListener('pointermove', this.onDrag)
|
||||
document.removeEventListener('pointerup', this.stopDrag)
|
||||
|
||||
this.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,348 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-if="!isShowFontModal && !isShowConditionsModal"
|
||||
ref="menu"
|
||||
class="fixed z-50 p-1 bg-white shadow-lg rounded-lg border border-neutral-200 cursor-default"
|
||||
style="min-width: 170px"
|
||||
:style="menuStyle"
|
||||
@mousedown.stop
|
||||
@pointerdown.stop
|
||||
>
|
||||
<ContextSubmenu
|
||||
:icon="IconLayoutAlignMiddle"
|
||||
:label="t('align')"
|
||||
>
|
||||
<button
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="alignSelectedAreas('left')"
|
||||
>
|
||||
<IconLayoutAlignLeft class="w-4 h-4" />
|
||||
<span>{{ t('align_left') }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="alignSelectedAreas('right')"
|
||||
>
|
||||
<IconLayoutAlignRight class="w-4 h-4" />
|
||||
<span>{{ t('align_right') }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="alignSelectedAreas('top')"
|
||||
>
|
||||
<IconLayoutAlignTop class="w-4 h-4" />
|
||||
<span>{{ t('align_top') }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="alignSelectedAreas('bottom')"
|
||||
>
|
||||
<IconLayoutAlignBottom class="w-4 h-4" />
|
||||
<span>{{ t('align_bottom') }}</span>
|
||||
</button>
|
||||
</ContextSubmenu>
|
||||
<ContextSubmenu
|
||||
:icon="IconAspectRatio"
|
||||
:label="t('resize')"
|
||||
>
|
||||
<button
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="resizeSelectedAreas('width')"
|
||||
>
|
||||
<IconArrowsHorizontal class="w-4 h-4" />
|
||||
<span>{{ t('width') }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="resizeSelectedAreas('height')"
|
||||
>
|
||||
<IconArrowsVertical class="w-4 h-4" />
|
||||
<span>{{ t('height') }}</span>
|
||||
</button>
|
||||
</ContextSubmenu>
|
||||
<hr
|
||||
v-if="showFont || showCondition"
|
||||
class="my-1 border-neutral-200"
|
||||
>
|
||||
<button
|
||||
v-if="showFont"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="openFontModal"
|
||||
>
|
||||
<IconTypography class="w-4 h-4" />
|
||||
<span>{{ t('font') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="showCondition"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="openConditionModal"
|
||||
>
|
||||
<IconRouteAltLeft class="w-4 h-4" />
|
||||
<span>{{ t('condition') }}</span>
|
||||
</button>
|
||||
<hr class="my-1 border-neutral-200">
|
||||
<button
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
|
||||
@click.stop="$emit('copy')"
|
||||
>
|
||||
<span class="flex items-center space-x-2">
|
||||
<IconCopy class="w-4 h-4" />
|
||||
<span>{{ t('copy') }}</span>
|
||||
</span>
|
||||
<span class="text-xs text-base-content/60 ml-4">{{ isMac ? '⌘C' : 'Ctrl+C' }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm text-red-600"
|
||||
@click.stop="$emit('delete')"
|
||||
>
|
||||
<span class="flex items-center space-x-2">
|
||||
<IconTrashX class="w-4 h-4" />
|
||||
<span>{{ t('remove') }}</span>
|
||||
</span>
|
||||
<span class="text-xs text-base-content/60 ml-4">Del</span>
|
||||
</button>
|
||||
</div>
|
||||
<Teleport
|
||||
v-if="isShowFontModal"
|
||||
:to="modalContainerEl"
|
||||
>
|
||||
<FontModal
|
||||
:field="multiSelectField"
|
||||
:area="contextMenu.area"
|
||||
:editable="editable"
|
||||
:build-default-name="buildDefaultName"
|
||||
@save="handleSaveMultiSelectFontModal"
|
||||
@close="closeModal"
|
||||
/>
|
||||
</Teleport>
|
||||
<Teleport
|
||||
v-if="isShowConditionsModal"
|
||||
:to="modalContainerEl"
|
||||
>
|
||||
<ConditionsModal
|
||||
:item="multiSelectField"
|
||||
:build-default-name="buildDefaultName"
|
||||
:exclude-field-uuids="selectedFields.map(f => f.uuid)"
|
||||
@save="handleSaveMultiSelectConditionsModal"
|
||||
@close="closeModal"
|
||||
/>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IconCopy, IconTrashX, IconTypography, IconRouteAltLeft, IconLayoutAlignLeft, IconLayoutAlignRight, IconLayoutAlignTop, IconLayoutAlignBottom, IconLayoutAlignMiddle, IconAspectRatio, IconArrowsHorizontal, IconArrowsVertical } from '@tabler/icons-vue'
|
||||
import FontModal from './font_modal'
|
||||
import ConditionsModal from './conditions_modal'
|
||||
import ContextSubmenu from './field_context_submenu'
|
||||
import Field from './field'
|
||||
import FieldType from './field_type'
|
||||
|
||||
export default {
|
||||
name: 'SelectionContextMenu',
|
||||
components: {
|
||||
IconCopy,
|
||||
IconTrashX,
|
||||
IconTypography,
|
||||
IconRouteAltLeft,
|
||||
IconLayoutAlignLeft,
|
||||
IconLayoutAlignRight,
|
||||
IconLayoutAlignTop,
|
||||
IconLayoutAlignBottom,
|
||||
FontModal,
|
||||
IconArrowsHorizontal,
|
||||
IconArrowsVertical,
|
||||
ConditionsModal,
|
||||
ContextSubmenu
|
||||
},
|
||||
inject: ['t', 'save', 'selectedAreasRef', 'getFieldTypeIndex'],
|
||||
props: {
|
||||
contextMenu: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
template: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
withCondition: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
emits: ['copy', 'delete', 'close'],
|
||||
data () {
|
||||
return {
|
||||
isShowFontModal: false,
|
||||
isShowConditionsModal: false,
|
||||
multiSelectField: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
modalContainerEl () {
|
||||
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
||||
},
|
||||
selectedFields () {
|
||||
return this.selectedAreasRef.value.map((area) => {
|
||||
return this.template.fields.find((f) => f.areas?.includes(area))
|
||||
}).filter(Boolean)
|
||||
},
|
||||
isMac () {
|
||||
return (navigator.userAgentData?.platform || navigator.platform)?.toLowerCase()?.includes('mac')
|
||||
},
|
||||
menuStyle () {
|
||||
return {
|
||||
left: this.contextMenu.x + 'px',
|
||||
top: this.contextMenu.y + 'px'
|
||||
}
|
||||
},
|
||||
showFont () {
|
||||
return true
|
||||
},
|
||||
showCondition () {
|
||||
return this.withCondition
|
||||
},
|
||||
fieldNames: FieldType.computed.fieldNames,
|
||||
fieldLabels: FieldType.computed.fieldLabels
|
||||
},
|
||||
mounted () {
|
||||
document.addEventListener('keydown', this.onKeyDown)
|
||||
document.addEventListener('mousedown', this.handleClickOutside)
|
||||
|
||||
this.$nextTick(() => this.checkMenuPosition())
|
||||
},
|
||||
beforeUnmount () {
|
||||
document.removeEventListener('keydown', this.onKeyDown)
|
||||
document.removeEventListener('mousedown', this.handleClickOutside)
|
||||
},
|
||||
methods: {
|
||||
IconLayoutAlignMiddle,
|
||||
IconAspectRatio,
|
||||
buildDefaultName: Field.methods.buildDefaultName,
|
||||
checkMenuPosition () {
|
||||
if (this.$refs.menu) {
|
||||
const rect = this.$refs.menu.getBoundingClientRect()
|
||||
const overflow = rect.bottom - window.innerHeight
|
||||
|
||||
if (overflow > 0) {
|
||||
this.contextMenu.y = this.contextMenu.y - overflow - 4
|
||||
}
|
||||
}
|
||||
},
|
||||
onKeyDown (event) {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
this.$emit('close')
|
||||
}
|
||||
},
|
||||
handleClickOutside (event) {
|
||||
if (this.$refs.menu && !this.$refs.menu.contains(event.target)) {
|
||||
this.$emit('close')
|
||||
}
|
||||
},
|
||||
openFontModal () {
|
||||
this.multiSelectField = {
|
||||
name: this.t('fields_selected').replace('{count}', this.selectedFields.length),
|
||||
preferences: {}
|
||||
}
|
||||
|
||||
const preferencesStrings = this.selectedFields.map((f) => JSON.stringify(f.preferences || {}))
|
||||
|
||||
if (preferencesStrings.every((s) => s === preferencesStrings[0])) {
|
||||
this.multiSelectField.preferences = JSON.parse(preferencesStrings[0])
|
||||
}
|
||||
|
||||
this.isShowFontModal = true
|
||||
},
|
||||
openConditionModal () {
|
||||
this.multiSelectField = {
|
||||
name: this.t('fields_selected').replace('{count}', this.selectedFields.length),
|
||||
conditions: []
|
||||
}
|
||||
|
||||
const conditionStrings = this.selectedFields.map((f) => JSON.stringify(f.conditions || []))
|
||||
|
||||
if (conditionStrings.every((s) => s === conditionStrings[0])) {
|
||||
this.multiSelectField.conditions = JSON.parse(conditionStrings[0])
|
||||
}
|
||||
|
||||
this.isShowConditionsModal = true
|
||||
},
|
||||
closeModal () {
|
||||
this.isShowFontModal = false
|
||||
this.isShowConditionsModal = false
|
||||
this.multiSelectField = null
|
||||
|
||||
this.$emit('close')
|
||||
},
|
||||
alignSelectedAreas (direction) {
|
||||
const areas = this.selectedAreasRef.value
|
||||
|
||||
let targetValue
|
||||
|
||||
if (direction === 'left') {
|
||||
targetValue = Math.min(...areas.map(a => a.x))
|
||||
areas.forEach((area) => { area.x = targetValue })
|
||||
} else if (direction === 'right') {
|
||||
targetValue = Math.max(...areas.map(a => a.x + a.w))
|
||||
areas.forEach((area) => { area.x = targetValue - area.w })
|
||||
} else if (direction === 'top') {
|
||||
targetValue = Math.min(...areas.map(a => a.y))
|
||||
areas.forEach((area) => { area.y = targetValue })
|
||||
} else if (direction === 'bottom') {
|
||||
targetValue = Math.max(...areas.map(a => a.y + a.h))
|
||||
areas.forEach((area) => { area.y = targetValue - area.h })
|
||||
}
|
||||
|
||||
this.save()
|
||||
|
||||
this.$emit('close')
|
||||
},
|
||||
resizeSelectedAreas (dimension) {
|
||||
const areas = this.selectedAreasRef.value
|
||||
|
||||
const values = areas.map(a => dimension === 'width' ? a.w : a.h).sort((a, b) => a - b)
|
||||
const medianValue = values[Math.floor(values.length / 2)]
|
||||
|
||||
if (dimension === 'width') {
|
||||
areas.forEach((area) => { area.w = medianValue })
|
||||
} else if (dimension === 'height') {
|
||||
areas.forEach((area) => {
|
||||
const diff = medianValue - area.h
|
||||
area.y = area.y - diff
|
||||
area.h = medianValue
|
||||
})
|
||||
}
|
||||
|
||||
this.save()
|
||||
|
||||
this.$emit('close')
|
||||
},
|
||||
handleSaveMultiSelectFontModal () {
|
||||
this.selectedFields.forEach((field) => {
|
||||
field.preferences = { ...field.preferences, ...this.multiSelectField.preferences }
|
||||
})
|
||||
|
||||
this.save()
|
||||
|
||||
this.closeModal()
|
||||
},
|
||||
handleSaveMultiSelectConditionsModal () {
|
||||
this.selectedFields.forEach((field) => {
|
||||
field.conditions = JSON.parse(JSON.stringify(this.multiSelectField.conditions))
|
||||
})
|
||||
|
||||
this.save()
|
||||
|
||||
this.closeModal()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -216,6 +216,9 @@ export default {
|
||||
queryParams () {
|
||||
return new URLSearchParams(window.location.search)
|
||||
},
|
||||
uploadUrl () {
|
||||
return `/templates/${this.templateId}/documents`
|
||||
},
|
||||
googleDriveOauthPath () {
|
||||
const params = {
|
||||
access_type: 'offline',
|
||||
@@ -279,7 +282,7 @@ export default {
|
||||
async upload ({ path } = {}) {
|
||||
this.isLoading = true
|
||||
|
||||
return this.baseFetch(path || `/templates/${this.templateId}/documents`, {
|
||||
return this.baseFetch(path || this.uploadUrl, {
|
||||
method: 'POST',
|
||||
headers: { Accept: 'application/json' },
|
||||
body: new FormData(this.$refs.form)
|
||||
@@ -287,7 +290,11 @@ export default {
|
||||
if (resp.ok) {
|
||||
resp.json().then((data) => {
|
||||
this.$emit('success', data)
|
||||
this.$refs.input.value = ''
|
||||
|
||||
if (this.$refs.input) {
|
||||
this.$refs.input.value = ''
|
||||
}
|
||||
|
||||
this.isLoading = false
|
||||
})
|
||||
} else if (resp.status === 422) {
|
||||
|
||||
@@ -39,11 +39,18 @@ class ProcessSubmitterCompletionJob
|
||||
submission = submitter.submission
|
||||
|
||||
complete_verification_events, sms_events =
|
||||
submitter.submission_events.where(event_type: %i[send_sms send_2fa_sms complete_verification])
|
||||
.partition { |e| e.event_type == 'complete_verification' }
|
||||
submitter.submission_events.where(event_type: %i[send_sms send_2fa_sms complete_verification complete_kba])
|
||||
.partition { |e| e.event_type == 'complete_verification' || e.event_type == 'complete_kba' }
|
||||
|
||||
complete_verification_event = complete_verification_events.first
|
||||
|
||||
verification_method =
|
||||
if complete_verification_event&.event_type == 'complete_kba'
|
||||
'kba'
|
||||
elsif complete_verification_event
|
||||
complete_verification_event.data['method']
|
||||
end
|
||||
|
||||
completed_submitter.assign_attributes(
|
||||
submission_id: submitter.submission_id,
|
||||
account_id: submission.account_id,
|
||||
@@ -51,7 +58,7 @@ class ProcessSubmitterCompletionJob
|
||||
template_id: submission.template_id,
|
||||
source: submission.source,
|
||||
sms_count: sms_events.sum { |e| e.data['segments'] || 1 },
|
||||
verification_method: complete_verification_event&.data&.dig('method'),
|
||||
verification_method:,
|
||||
completed_at: submitter.completed_at
|
||||
)
|
||||
|
||||
@@ -157,11 +164,13 @@ class ProcessSubmitterCompletionJob
|
||||
next_submitter_items =
|
||||
if submission.template_submitters.any? { |s| s['order'] }
|
||||
submitter_groups =
|
||||
submission.template_submitters.group_by.with_index { |s, index| s['order'] || index }
|
||||
submission.template_submitters
|
||||
.group_by.with_index { |s, index| s['order'] || index }
|
||||
.sort_by(&:first).pluck(1)
|
||||
|
||||
current_group_index = submitter_groups.find { |_, group| group.any? { |s| s['uuid'] == submitter.uuid } }&.first
|
||||
current_group_index = submitter_groups.index { |group| group.any? { |s| s['uuid'] == submitter.uuid } }
|
||||
|
||||
if submitter_groups[current_group_index + 1] &&
|
||||
if current_group_index && submitter_groups[current_group_index + 1] &&
|
||||
submitters_index.values_at(*submitter_groups[current_group_index].pluck('uuid'))
|
||||
.compact.all?(&:completed_at?)
|
||||
submitter_groups[current_group_index + 1]
|
||||
|
||||
@@ -26,7 +26,7 @@ class SendTestWebhookRequestJob
|
||||
Addressable::URI.parse(webhook_url.url).normalize
|
||||
end
|
||||
|
||||
raise HttpsError, 'Only HTTPS is allowed.' if uri.scheme != 'https'
|
||||
raise HttpsError, 'Only HTTPS is allowed.' if uri.scheme != 'https' || [443, nil].exclude?(uri.port)
|
||||
raise LocalhostError, "Can't send to localhost." if uri.host.in?(SendWebhookRequest::LOCALHOSTS)
|
||||
end
|
||||
|
||||
|
||||
@@ -47,12 +47,14 @@ class AccountConfig < ApplicationRecord
|
||||
WITH_SIGNATURE_ID_REASON_KEY = 'with_signature_id_reason'
|
||||
RECIPIENT_FORM_FIELDS_KEY = 'recipient_form_fields'
|
||||
WITH_AUDIT_VALUES_KEY = 'with_audit_values'
|
||||
WITH_AUDIT_SENDER_KEY = 'with_audit_sender'
|
||||
WITH_SUBMITTER_TIMEZONE_KEY = 'with_submitter_timezone'
|
||||
REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason'
|
||||
REUSE_SIGNATURE_KEY = 'reuse_signature'
|
||||
WITH_FIELD_LABELS_KEY = 'with_field_labels'
|
||||
COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key'
|
||||
DOCUMENT_FILENAME_FORMAT_KEY = 'document_filename_format'
|
||||
TEMPLATE_CUSTOM_FIELDS_KEY = 'template_custom_fields'
|
||||
POLICY_LINKS_KEY = 'policy_links'
|
||||
|
||||
DEFAULT_VALUES = {
|
||||
|
||||
@@ -63,6 +63,8 @@ class Submission < ApplicationRecord
|
||||
|
||||
has_one_attached :audit_trail
|
||||
has_one_attached :combined_document
|
||||
has_one_attached :merged_document
|
||||
has_one_attached :preview_merged_document
|
||||
|
||||
has_many_attached :preview_documents
|
||||
has_many_attached :documents
|
||||
@@ -76,8 +78,9 @@ class Submission < ApplicationRecord
|
||||
scope :active, -> { where(archived_at: nil) }
|
||||
scope :archived, -> { where.not(archived_at: nil) }
|
||||
scope :pending, lambda {
|
||||
where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
|
||||
.and(Submitter.arel_table[:completed_at].eq(nil))).select(1).arel.exists)
|
||||
where(expire_at: nil).or(where(expire_at: Time.current..))
|
||||
.where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
|
||||
.and(Submitter.arel_table[:completed_at].eq(nil))).select(1).arel.exists)
|
||||
}
|
||||
scope :completed, lambda {
|
||||
where.not(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
|
||||
|
||||
@@ -57,6 +57,9 @@ class SubmissionEvent < ApplicationRecord
|
||||
start_form: 'start_form',
|
||||
start_verification: 'start_verification',
|
||||
complete_verification: 'complete_verification',
|
||||
start_kba: 'start_kba',
|
||||
complete_kba: 'complete_kba',
|
||||
fail_kba: 'fail_kba',
|
||||
view_form: 'view_form',
|
||||
invite_party: 'invite_party',
|
||||
complete_form: 'complete_form',
|
||||
|
||||
@@ -235,7 +235,7 @@
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if can?(:manage, :personalization_advanced) %>
|
||||
<% if !Docuseal.multitenant? || can?(:manage, :personalization_advanced) %>
|
||||
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::WITH_FILE_LINKS_KEY) %>
|
||||
<% if can?(:manage, account_config) %>
|
||||
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= ff.label :password, 'Password', class: 'label' %>
|
||||
<%= ff.password_field :password, value: value['password'], class: 'base-input' %>
|
||||
<%= ff.password_field :password, class: 'base-input', required: value['password'].present?, placeholder: value['password'].present? ? '*************' : '' %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
@@ -39,7 +39,7 @@
|
||||
<div class="form-control">
|
||||
<%= ff.label :security_label, 'SMTP Security', class: 'label' %>
|
||||
<div class="flex items-center space-x-6">
|
||||
<% [%w[Auto none], %w[SSL ssl], %w[TLS tls], %w[Noverify noverify]].each do |(label, val)| %>
|
||||
<% [%w[STARTTLS none], %w[TLS tls], %w[SSL ssl], %w[Noverify noverify]].each do |(label, val)| %>
|
||||
<%= 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' %>
|
||||
<%= label %>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M10 9a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /><path d="M4 8v-2a2 2 0 0 1 2 -2h2" /><path d="M4 16v2a2 2 0 0 0 2 2h2" /><path d="M16 4h2a2 2 0 0 1 2 2v2" /><path d="M16 20h2a2 2 0 0 0 2 -2v-2" /><path d="M8 16a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 525 B |
@@ -22,7 +22,7 @@
|
||||
<% if params[:modal].present? %>
|
||||
<% url_params = Rails.application.routes.recognize_path(params[:modal], method: :get) %>
|
||||
<% if url_params[:action] == 'new' %>
|
||||
<open-modal src="<%= params[:modal] %>"></open-modal>
|
||||
<open-modal src="<%= url_for(url_params) %>"></open-modal>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<turbo-frame id="modal"></turbo-frame>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"id": "/",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/logo.svg",
|
||||
"src": "/favicon.svg",
|
||||
"type": "image/svg+xml",
|
||||
"sizes": "any"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<% uuid = local_assigns[:uuid] || SecureRandom.uuid %>
|
||||
<label id="<%= local_assigns[:button_id] %>" for="<%= uuid %>" class="<%= local_assigns[:btn_class] %>"><%= local_assigns[:btn_text] %></label>
|
||||
<input type="checkbox" id="<%= uuid %>" class="modal-toggle">
|
||||
<div id="<%= local_assigns[:id] %>" class="modal items-start !animate-none overflow-y-auto">
|
||||
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none">
|
||||
|
||||
@@ -1,32 +1,4 @@
|
||||
<svg height="<%= local_assigns.fetch(:height, '37') %>" width="<%= local_assigns.fetch(:width, '37') %>" class="<%= local_assigns[:class] %>" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 511.998 511.998" xml:space="preserve">
|
||||
<path style="fill:#AA968C;" d="M503.999,247.999c0,128.13-111.033,240-248,240S8,376.129,8,247.999s111.033-224,248-224
|
||||
S503.999,119.869,503.999,247.999z" />
|
||||
<path style="fill:#AA968C;" d="M255.999,23.999C119.033,23.999,8,119.868,8,247.999c0,24.631,4.138,48.647,11.74,71.397
|
||||
c25.518,34.29,66.232,56.602,112.26,56.602c53.893,0,100.6-30.495,124-75.13c23.4,44.636,70.107,75.13,124,75.13
|
||||
c46.028,0,86.743-22.312,112.26-56.602c7.602-22.75,11.74-46.767,11.74-71.397C503.999,119.868,392.966,23.999,255.999,23.999z" />
|
||||
<circle style="fill:#C8AF9B;" cx="256" cy="351.999" r="136" />
|
||||
<g>
|
||||
<circle style="fill:#464655;" cx="132" cy="203.999" r="28" />
|
||||
<circle style="fill:#464655;" cx="380" cy="203.999" r="28" />
|
||||
<path style="fill:#464655;" d="M269.949,284.516c-7.672,10.741-20.227,10.741-27.899,0l-12.101-16.941
|
||||
c-7.672-10.741-3.15-19.53,10.05-19.53h32c13.2,0,17.723,8.788,10.05,19.53L269.949,284.516z" />
|
||||
</g>
|
||||
<path style="fill:#AA968C;" d="M 350.964 399.998 C 316.628 399.998 299.021 351.998 255.882 351.998 C 212.742 351.998 195.135 399.998 160.801 399.998 C 145.395 399.998 131.723 394.147 120.621 374.798 C 131.595 439.03 187.893 487.998 255.881 487.998 C 323.868 487.998 380.168 439.03 391.14 374.798 C 380.04 394.148 366.368 399.998 350.964 399.998 Z" />
|
||||
<g>
|
||||
<path style="fill:#8C7873;" d="M32,423.998c-3.172,0-6.18-1.906-7.43-5.031c-1.641-4.105,0.359-8.758,4.461-10.402l160.008-64
|
||||
c4.062-1.617,8.758,0.352,10.398,4.457s-0.359,8.758-4.461,10.402l-160.008,64C34,423.811,32.992,423.998,32,423.998z" />
|
||||
<path style="fill:#8C7873;" d="M15.992,375.995c-3.547,0-6.781-2.375-7.727-5.965c-1.125-4.273,1.422-8.648,5.695-9.773l152-40
|
||||
c4.289-1.121,8.648,1.426,9.773,5.703c1.125,4.273-1.422,8.648-5.695,9.773l-152,40C17.351,375.913,16.672,375.995,15.992,375.995z
|
||||
" />
|
||||
<path style="fill:#8C7873;" d="M7.992,335.995c-3.812,0-7.187-2.73-7.867-6.609c-0.773-4.352,2.133-8.5,6.484-9.27l136-24
|
||||
c4.328-0.77,8.508,2.141,9.266,6.488c0.773,4.352-2.133,8.5-6.484,9.27l-136,24C8.922,335.956,8.453,335.995,7.992,335.995z" />
|
||||
<path style="fill:#8C7873;" d="M480,423.998c3.172,0,6.18-1.906,7.43-5.031c1.641-4.105-0.359-8.758-4.461-10.402l-160.008-64
|
||||
c-4.063-1.617-8.758,0.352-10.398,4.457s0.359,8.758,4.461,10.402l160.008,64C478,423.811,479.007,423.998,480,423.998z" />
|
||||
<path style="fill:#8C7873;" d="M496.007,375.995c3.547,0,6.781-2.375,7.727-5.965c1.125-4.273-1.422-8.648-5.695-9.773l-152-40
|
||||
c-4.289-1.121-8.648,1.426-9.773,5.703c-1.125,4.273,1.422,8.648,5.695,9.773l152,40
|
||||
C494.648,375.913,495.328,375.995,496.007,375.995z" />
|
||||
<path style="fill:#8C7873;" d="M504.007,335.995c3.813,0,7.188-2.73,7.867-6.609c0.773-4.352-2.133-8.5-6.484-9.27l-136-24
|
||||
c-4.328-0.77-8.508,2.141-9.266,6.488c-0.773,4.352,2.133,8.5,6.484,9.27l136,24C503.078,335.956,503.546,335.995,504.007,335.995z
|
||||
" />
|
||||
</g>
|
||||
<svg class="<%= local_assigns[:class] %>" height="<%= local_assigns.fetch(:height, '37') %>" width="<%= local_assigns.fetch(:width, '37') %>" style="color: #e0753f" viewBox="0 0 180 180" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="currentColor" d="M 178.224 72.09 c -0.296 -1.463 -0.627 -2.919 -0.996 -4.364 -0.293 -1.151 -0.616 -2.293 -0.956 -3.433 -0.301 -1.008 -0.612 -2.014 -0.95 -3.012 -0.531 -1.578 -1.113 -3.142 -1.735 -4.694 -0.216 -0.54 -0.433 -1.082 -0.661 -1.618 -0.195 -0.462 -0.399 -0.917 -0.601 -1.375 -0.262 -0.591 -0.53 -1.177 -0.804 -1.762 -0.074 -0.159 -0.151 -0.315 -0.226 -0.474 -0.209 -0.441 -0.422 -0.881 -0.638 -1.318 -0.076 -0.154 -0.153 -0.306 -0.229 -0.459 -0.236 -0.471 -0.477 -0.939 -0.721 -1.406 -0.053 -0.101 -0.105 -0.201 -0.158 -0.302 -1.143 -2.16 -2.367 -4.269 -3.68 -6.322 -0.116 -0.181 -0.237 -0.359 -0.355 -0.539 -0.094 -0.144 -0.189 -0.288 -0.284 -0.432 -0.284 -0.431 -0.57 -0.861 -0.862 -1.287 -0.112 -0.164 -0.225 -0.326 -0.338 -0.489 -0.193 -0.279 -0.382 -0.56 -0.579 -0.836 -0.089 -0.125 -0.182 -0.249 -0.273 -0.374 -0.13 -0.182 -0.264 -0.362 -0.395 -0.542 -0.277 -0.38 -0.556 -0.76 -0.838 -1.135 -0.15 -0.199 -0.303 -0.395 -0.454 -0.593 -0.21 -0.274 -0.417 -0.552 -0.63 -0.823 -0.055 -0.069 -0.111 -0.136 -0.166 -0.205 -0.482 -0.61 -0.971 -1.216 -1.47 -1.814 -0.129 -0.155 -0.262 -0.306 -0.392 -0.461 -0.402 -0.476 -0.808 -0.95 -1.22 -1.417 -0.186 -0.212 -0.375 -0.422 -0.563 -0.631 -0.384 -0.428 -0.773 -0.854 -1.167 -1.276 -0.176 -0.189 -0.351 -0.379 -0.529 -0.567 -0.564 -0.595 -1.134 -1.186 -1.716 -1.768 -1.091 -1.091 -2.207 -2.15 -3.346 -3.178 -1.016 -0.919 -2.05 -1.815 -3.103 -2.684 -0.772 -0.636 -1.557 -1.255 -2.348 -1.864 -3.465 -2.67 -7.112 -5.075 -10.927 -7.209 -2.869 -1.604 -5.83 -3.06 -8.883 -4.351 -2.443 -1.033 -4.922 -1.948 -7.428 -2.756 -8.879 -2.863 -18.13 -4.318 -27.605 -4.318 -3.19 0 -6.354 0.169 -9.488 0.496 -4.036 0.421 -8.019 1.114 -11.94 2.073 -1.732 0.423 -3.452 0.892 -5.157 1.42 -2.856 0.883 -5.673 1.912 -8.447 3.085 -2.645 1.118 -5.222 2.357 -7.729 3.711 -2.574 1.39 -5.073 2.901 -7.494 4.533 -1.195 0.805 -2.37 1.64 -3.527 2.503 -1.156 0.864 -2.292 1.756 -3.408 2.676 -0.553 0.456 -1.1 0.919 -1.643 1.389 -1.649 1.427 -3.252 2.92 -4.806 4.473 -2.582 2.582 -4.991 5.299 -7.222 8.138 -0.892 1.135 -1.756 2.292 -2.59 3.467 -0.417 0.588 -0.827 1.18 -1.23 1.778 -0.403 0.597 -0.798 1.199 -1.186 1.806 -0.388 0.607 -0.769 1.218 -1.143 1.835 -2.241 3.697 -4.216 7.562 -5.916 11.582 -1.095 2.589 -2.059 5.217 -2.901 7.877 -0.153 0.482 -0.3 0.965 -0.444 1.449 -0.339 1.14 -0.663 2.282 -0.956 3.433 -0.369 1.446 -0.7 2.901 -0.996 4.364 -1.034 5.121 -1.618 10.343 -1.749 15.637 -0.018 0.757 -0.028 1.514 -0.028 2.274 0 1.123 0.02 2.244 0.062 3.361 0.285 7.82 1.568 15.475 3.825 22.879 0.044 0.147 0.088 0.295 0.133 0.441 0.877 2.823 1.894 5.608 3.054 8.35 0.85 2.009 1.769 3.98 2.755 5.912 0.539 1.057 1.105 2.099 1.685 3.132 4.013 7.142 8.98 13.698 14.846 19.564 7.713 7.713 16.611 13.878 26.477 18.352 0.705 0.32 1.415 0.632 2.131 0.935 2.081 0.88 4.185 1.679 6.313 2.396 9.217 3.106 18.85 4.677 28.719 4.677 8.031 0 15.902 -1.047 23.522 -3.107 0.633 -0.172 1.266 -0.35 1.895 -0.535 0.757 -0.222 1.509 -0.456 2.26 -0.698 0.717 -0.232 1.431 -0.474 2.145 -0.723 1.752 -0.616 3.49 -1.281 5.211 -2.009 0.755 -0.319 1.503 -0.651 2.247 -0.989 1.237 -0.563 2.459 -1.15 3.664 -1.766 0.644 -0.328 1.283 -0.665 1.917 -1.009 1.654 -0.896 3.274 -1.848 4.865 -2.844 5.736 -3.591 11.06 -7.827 15.912 -12.679 0.775 -0.775 1.534 -1.562 2.278 -2.36 5.204 -5.59 9.636 -11.754 13.246 -18.417 0.343 -0.634 0.68 -1.274 1.009 -1.917 0.482 -0.944 0.943 -1.9 1.392 -2.863 0.471 -1.007 0.928 -2.021 1.364 -3.049 1.22 -2.886 2.281 -5.82 3.187 -8.793 0.559 -1.833 1.056 -3.68 1.494 -5.542 0.108 -0.458 0.211 -0.916 0.312 -1.376 0.194 -0.883 0.373 -1.77 0.539 -2.659 1.02 -5.455 1.542 -11.02 1.542 -16.663 0 -6.074 -0.595 -12.058 -1.776 -17.911 z m -161.733 19.614 c -1.118 -56.662 44.604 -74.877 60.998 -67.647 2.187 0.965 4.732 2.431 7.042 2.96 5.295 1.213 13.432 -3.113 13.521 6.273 0.078 8.156 -3.389 13.108 -10.797 16.177 -7.539 3.124 -14.777 9.181 -19.95 15.493 -21.487 26.216 -31.231 68.556 -7.565 94.296 -13.679 -5.545 -42.418 -25.467 -43.248 -67.552 z m 91.109 72.619 c -0.053 0.008 -4.171 0.775 -4.171 0.775 0 0 -15.862 -22.957 -23.509 -21.719 11.291 16.04 12.649 22.625 12.649 22.625 -0.053 0.001 -0.107 0.001 -0.161 0.003 -51.831 2.131 -42.785 -64.026 -28.246 -86.502 -1.555 13.073 8.878 39.992 39.034 44.1 9.495 1.293 32.302 -3.275 41.015 -11.38 0.098 1.825 0.163 3.85 0.159 6.013 -0.046 23.538 -13.47 42.743 -36.77 46.085 z m 30.575 -15.708 c 9.647 -9.263 12.869 -27.779 9.103 -44.137 -4.608 -20.011 -28.861 -32.383 -40.744 -35.564 5.766 -8.089 27.908 -14.274 39.567 5.363 -5.172 -10.519 -13.556 -23.023 -1.732 -33.128 12.411 13.329 19.411 29.94 20.161 48.7 0.75 18.753 -6.64 41.768 -26.355 58.765 z" />
|
||||
<circle fill="currentColor" cx="71.927" cy="32.004" r="2.829" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 4.8 KiB |
@@ -13,10 +13,10 @@
|
||||
<meta property="og:image" content="">
|
||||
<meta name="twitter:image" content="">
|
||||
<% else %>
|
||||
<meta property="og:image" content="<%= root_url %>preview.png">
|
||||
<meta property="og:image" content="<%= content_for(:preview_image_url).presence || "#{root_url}preview.png" %>">
|
||||
<meta property="og:image:width" content="800">
|
||||
<meta property="og:image:height" content="800">
|
||||
<meta name="twitter:image" content="<%= root_url %>preview.png">
|
||||
<meta name="twitter:image" content="<%= content_for(:preview_image_url).presence || "#{root_url}preview.png" %>">
|
||||
<% end %>
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:creator" content="@docusealco">
|
||||
@@ -27,4 +27,5 @@
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<meta name="theme-color" content="#faf7f5">
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
<% link = pagy_anchor(pagy) %>
|
||||
<% if pagy.pages > 1 %>
|
||||
<div class="flex my-6 justify-center md:justify-between">
|
||||
<div class="hidden md:block text-sm">
|
||||
@@ -12,8 +11,8 @@
|
||||
<div class="flex items-center space-x-1.5">
|
||||
<%= local_assigns[:right_additional_html] %>
|
||||
<div class="join">
|
||||
<% if pagy.prev %>
|
||||
<%== link.call(pagy.prev, '«', classes: 'join-item btn min-h-full h-10') %>
|
||||
<% if pagy.previous %>
|
||||
<%= link_to '«', url_for(params: request.query_parameters.merge('page' => pagy.previous)), class: 'join-item btn min-h-full h-10' %>
|
||||
<% else %>
|
||||
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">«</span>
|
||||
<% end %>
|
||||
@@ -23,7 +22,7 @@
|
||||
<% if local_assigns[:next_page_path].present? %>
|
||||
<%= link_to '»', local_assigns[:next_page_path], class: 'join-item btn min-h-full h-10' %>
|
||||
<% elsif pagy.next %>
|
||||
<%== link.call(pagy.next, '»', classes: 'join-item btn min-h-full h-10') %>
|
||||
<%= link_to '»', url_for(params: request.query_parameters.merge('page' => pagy.next)), class: 'join-item btn min-h-full h-10' %>
|
||||
<% else %>
|
||||
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">»</span>
|
||||
<% end %>
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
<form action="<%= url_for %>" method="get" class="items-center flex">
|
||||
<% if params[:status].present? %>
|
||||
<input name="status" value="<%= params[:status] %>" class="hidden">
|
||||
<% end %>
|
||||
<% Submissions::Filter::ALLOWED_PARAMS.each do |key| %>
|
||||
<% if params[key].present? %>
|
||||
<input name="<%= key %>" value="<%= params[key] %>" class="hidden">
|
||||
@@ -9,7 +6,7 @@
|
||||
<% end %>
|
||||
<% if params[:q].present? %>
|
||||
<div class="relative">
|
||||
<a href="<%= url_for(params.to_unsafe_h.except(:q)) %>" title="<%= t('clear') %>" class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-auto text-neutral text-2xl font-extralight">
|
||||
<a href="<%= url_for(params: request.query_parameters.except('q')) %>" title="<%= t('clear') %>" class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-auto text-neutral text-2xl font-extralight">
|
||||
×
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
<a href="mailto:<%= Docuseal::SUPPORT_EMAIL %>" target="_blank" class="w-full block mt-4 underline text-center">
|
||||
<%= Docuseal::SUPPORT_EMAIL %>
|
||||
</a>
|
||||
<% if Docuseal.version.present? %>
|
||||
<% if Docuseal.version.present? && !Docuseal.multitenant? && can?(:manage, EncryptedConfig) %>
|
||||
<a href="https://github.com/docusealco/docuseal/releases" target="_blank" class="badge badge-outline text-xs block mx-auto mt-4">
|
||||
v<%= Docuseal.version %>
|
||||
</a>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= fff.label :secret_access_key, class: 'label' %>
|
||||
<%= fff.password_field :secret_access_key, value: configs['secret_access_key'], required: true, class: 'base-input' %>
|
||||
<%= fff.password_field :secret_access_key, required: true, class: 'base-input', placeholder: configs['secret_access_key'].present? ? '*************' : '' %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= fff.label :storage_access_key, 'Storage Access Key', class: 'label' %>
|
||||
<%= fff.password_field :storage_access_key, value: configs['storage_access_key'], required: true, class: 'base-input' %>
|
||||
<%= fff.password_field :storage_access_key, required: true, class: 'base-input', placeholder: configs['storage_access_key'].present? ? '*************' : '' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= fff.label :credentials, 'Credentials (JSON key content)', class: 'label' %>
|
||||
<%= fff.text_area :credentials, value: configs['credentials'], required: true, class: 'base-textarea w-full font-mono', rows: 4 %>
|
||||
<%= fff.text_area :credentials, required: true, class: 'base-textarea w-full font-mono', rows: 4, placeholder: configs['credentials'].present? ? "{\n**REDACTED**\n}" : '' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -30,14 +30,14 @@
|
||||
<div class="grid <%= 'md:grid-cols-2 gap-1' if submitters.size == 1 %>">
|
||||
<submitters-autocomplete data-field="email">
|
||||
<linked-input data-target-id="<%= "detailed_email_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
||||
<%= tag.input type: 'email', multiple: true, name: 'submission[1][submitters][][email]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: (local_assigns[:require_email_2fa] == true || local_assigns[:prefillable_fields].present? ? t(:email) : "#{t('email')} (#{t('optional')})"), value: item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : ''), id: "detailed_email_#{item['uuid']}", required: local_assigns[:require_email_2fa] == true %>
|
||||
<%= tag.input type: 'email', multiple: true, name: 'submission[1][submitters][][email]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: (local_assigns[:require_email_2fa] == true || local_assigns[:prefillable_fields].present? ? t(:email) : "#{t('email')} (#{t('optional')})"), value: item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : ''), id: "detailed_email_#{item['uuid']}", required: local_assigns[:require_email_2fa] == true && index.zero? %>
|
||||
</linked-input>
|
||||
</submitters-autocomplete>
|
||||
<% has_phone_field = true %>
|
||||
<custom-validation data-invalid-message="<%= t('use_international_format_1xxx_') %>">
|
||||
<submitters-autocomplete data-field="phone">
|
||||
<linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
||||
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: local_assigns[:require_phone_2fa] == true || local_assigns[:prefillable_fields].present? ? t(:phone) : "#{t('phone')} (#{t('optional')})", id: "detailed_phone_#{item['uuid']}", required: local_assigns[:require_phone_2fa] == true %>
|
||||
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: local_assigns[:require_phone_2fa] == true || local_assigns[:prefillable_fields].present? ? t(:phone) : "#{t('phone')} (#{t('optional')})", id: "detailed_phone_#{item['uuid']}", required: local_assigns[:require_phone_2fa] == true && index.zero? %>
|
||||
</linked-input>
|
||||
</submitters-autocomplete>
|
||||
</custom-validation>
|
||||
@@ -99,6 +99,7 @@
|
||||
<% if has_phone_field %>
|
||||
<%= render 'send_sms', f: %>
|
||||
<% end %>
|
||||
<%= render 'extra_fields', f: %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= f.button button_title(title: t('add_recipients')), class: 'base-button' %>
|
||||
|
||||
@@ -47,8 +47,9 @@
|
||||
</dynamic-list>
|
||||
<% end %>
|
||||
<div>
|
||||
<%= render 'send_email', f:, template: %>
|
||||
<%= render('submitters_order', f:, template:) if Accounts.can_send_emails?(current_account) %>
|
||||
<%= render 'send_email', f:, template: %>
|
||||
<%= render 'extra_fields', f: %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= content_for(:submit_button) || capture do %>
|
||||
|
||||
@@ -58,7 +58,8 @@
|
||||
</dynamic-list>
|
||||
<div>
|
||||
<%= render('submitters_order', f:, template:) if Accounts.can_send_emails?(current_account) %>
|
||||
<%= render 'send_sms', f: %>
|
||||
<%= render 'send_sms', f:, checked: true %>
|
||||
<%= render 'extra_phone_fields', f: %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= f.button button_title(title: t('add_recipients')), class: 'base-button' %>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% elsif field['type'].in?(['image', 'initials', 'stamp']) && attachments_index[value].image? %>
|
||||
<% elsif field['type'].in?(['image', 'initials', 'stamp', 'kba']) && attachments_index[value].image? %>
|
||||
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy">
|
||||
<% elsif field['type'].in?(['file', 'payment', 'image']) %>
|
||||
<autosize-field></autosize-field>
|
||||
@@ -60,7 +60,7 @@
|
||||
<% end %>
|
||||
<% elsif field['type'] == 'cells' && area['cell_w'].to_f > 0.0 %>
|
||||
<% cell_width = area['cell_w'] / area['w'] * 100 %>
|
||||
<div class="w-full flex items-center <%= 'justify-end' if align == 'right' %>">
|
||||
<div class="w-full flex <%= valign == 'top' ? 'items-start' : (valign == 'bottom' ? 'items-end' : 'items-center') %> <%= 'justify-end' if align == 'right' %>">
|
||||
<% (0..(area['w'] / area['cell_w']).ceil).each do |index| %>
|
||||
<% if value[index] %>
|
||||
<div class="text-center flex-none" style="width: <%= cell_width %>%;"><%= value[index] %></div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<% require_phone_2fa = @template.preferences['require_phone_2fa'] == true %>
|
||||
<% require_email_2fa = @template.preferences['require_email_2fa'] == true %>
|
||||
<% prefillable_fields = @template.fields.select { |f| f['prefillable'] } %>
|
||||
<% default_tab = cookies.permanent[:add_recipients_tab].presence || 'email' %>
|
||||
<% recipient_form_fields = Accounts.load_recipient_form_fields(current_account) if prefillable_fields.blank? %>
|
||||
<% only_detailed = require_phone_2fa || require_email_2fa || prefillable_fields.present? || recipient_form_fields.present? %>
|
||||
<%= render 'shared/turbo_modal_large', title: params[:selfsign] ? t('add_recipients') : t('add_new_recipients') do %>
|
||||
@@ -8,25 +9,25 @@
|
||||
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center px-2 mt-4 block">
|
||||
<div class="flex justify-center">
|
||||
<% options.each_with_index do |(label, value), index| %>
|
||||
<div>
|
||||
<%= radio_button_tag 'option', value, value == (only_detailed ? 'detailed' : 'email'), class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %>
|
||||
<%= content_tag(value == 'list' ? 'span' : 'toggle-cookies', data: { value:, key: 'add_recipients_tab' }) do %>
|
||||
<%= radio_button_tag 'option', value, value == (only_detailed ? 'detailed' : default_tab), class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %>
|
||||
<label for="option_<%= value %>" class="block bg-base-200 md:min-w-[112px] text-sm font-semibold whitespace-nowrap py-1.5 px-4 peer-checked:bg-base-300 <%= 'hidden sm:inline-block' if value == 'list' %> <%= 'rounded-l-3xl' if index.zero? %> <%= 'rounded-r-3xl sm:rounded-r-none' if value == 'detailed' %> <%= 'rounded-r-3xl' if index == options.size - 1 %>">
|
||||
<%= label %>
|
||||
</label>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</toggle-visible>
|
||||
<div class="px-5 mb-5 mt-4">
|
||||
<% unless only_detailed %>
|
||||
<div id="email">
|
||||
<div id="email" class="<%= 'hidden' if default_tab != 'email' %>">
|
||||
<%= render 'email_form', template: @template %>
|
||||
</div>
|
||||
<div id="phone" class="hidden">
|
||||
<div id="phone" class="<%= 'hidden' if default_tab != 'phone' %>">
|
||||
<%= render 'phone_form', template: @template %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div id="detailed" class="<%= 'hidden' unless only_detailed %>">
|
||||
<div id="detailed" class="<%= 'hidden' if !only_detailed && default_tab != 'detailed' %>">
|
||||
<%= render 'detailed_form', template: @template, require_phone_2fa:, require_email_2fa:, prefillable_fields:, recipient_form_fields: %>
|
||||
</div>
|
||||
<div id="list" class="hidden">
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% elsif @submission.submitters.to_a.size == 1 && !@submission.expired? && !@submission.submitters.to_a.first.declined_at? && !@submission.archived_at? %>
|
||||
<% elsif @submission.submitters.to_a.size == 1 && !@submission.expired? && !@submission.submitters.to_a.first.declined_at? && !@submission.archived_at? && !@submission.template&.archived_at? %>
|
||||
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: @submission.submitters.to_a.first.slug, host: form_link_host), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy_share_link'), copied_title: t('copied_to_clipboard') %>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -133,7 +133,7 @@
|
||||
<% bg_class = bg_classes[submitter_index % bg_classes.size] %>
|
||||
<div class="absolute overflow-visible" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%;">
|
||||
<div class="flex h-full w-full bg-opacity-80 justify-center items-center <%= bg_class %>">
|
||||
<%= svg_icon(SubmissionsController::FIELD_ICONS[field['type']], class: 'max-h-10 w-full h-full stroke-2 opacity-50') %>
|
||||
<%= svg_icon(SubmissionsController::FIELD_ICONS.fetch(field['type'], 'text_size'), class: 'max-h-10 w-full h-full stroke-2 opacity-50') %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -159,7 +159,7 @@
|
||||
<%= (@submission.template_submitters || @submission.template.submitters).find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %>
|
||||
</span>
|
||||
</div>
|
||||
<% if signed_in? && can?(:update, @submission) && submitter && !submitter.completed_at? && !submitter.declined_at? && !@submission.archived_at? && !@submission.expired? && !submitter.start_form_submission_events.any? %>
|
||||
<% if signed_in? && can?(:update, @submission) && submitter && !submitter.completed_at? && !submitter.declined_at? && !@submission.archived_at? && !@submission.template&.archived_at? && !@submission.expired? && !submitter.start_form_submission_events.any? %>
|
||||
<span class="tooltip tooltip-left" data-tip="<%= t('edit') %>">
|
||||
<%= link_to edit_submitter_path(submitter), class: 'shrink-0 inline md:hidden md:group-hover:inline', data: { turbo_frame: 'modal' } do %>
|
||||
<%= svg_icon('pencil', class: 'w-5 h-5') %>
|
||||
@@ -225,15 +225,15 @@
|
||||
</span>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if signed_in? && submitter && submitter.email && !submitter.completed_at && !@submission.archived_at? && can?(:update, @submission) && Accounts.can_send_emails?(current_account) && !@submission.expired? && !submitter.declined_at? %>
|
||||
<% if signed_in? && submitter && submitter.email && !submitter.completed_at && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:update, @submission) && Accounts.can_send_emails?(current_account) && !@submission.expired? && !submitter.declined_at? %>
|
||||
<div class="mt-2 mb-1">
|
||||
<%= button_to button_title(title: submitter.sent_at? ? t('re_send_email') : t('send_email'), disabled_with: t('sending')), submitter_send_email_index_path(submitter_slug: submitter.slug), class: 'btn btn-sm btn-primary w-full' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if signed_in? && submitter && submitter.phone && !submitter.completed_at && !@submission.archived_at? && can?(:update, @submission) && !@submission.expired? && !submitter.declined_at? %>
|
||||
<% if signed_in? && submitter && submitter.phone && !submitter.completed_at && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:update, @submission) && !@submission.expired? && !submitter.declined_at? %>
|
||||
<%= render 'submissions/send_sms_button', submitter: %>
|
||||
<% end %>
|
||||
<% if signed_in? && submitter && !submitter.completed_at? && !@submission.archived_at? && can?(:create, @submission) && !@submission.expired? && !submitter.declined_at? %>
|
||||
<% if signed_in? && submitter && !submitter.completed_at? && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:create, @submission) && !@submission.expired? && !submitter.declined_at? %>
|
||||
<div class="mt-2 mb-1">
|
||||
<a class="btn btn-sm btn-primary w-full" target="_blank" href="<%= submit_form_path(slug: submitter.slug) %>">
|
||||
<%= t('sign_in_person') %>
|
||||
@@ -262,7 +262,7 @@
|
||||
<div class="w-full bg-base-300 py-1">
|
||||
<img class="object-contain mx-auto" style="max-height: <%= field['type'] == 'signature' ? 100 : 50 %>px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
|
||||
</div>
|
||||
<% elsif field['type'].in?(['image', 'stamp']) && attachments_index[value].image? %>
|
||||
<% elsif field['type'].in?(['image', 'stamp', 'kba']) && attachments_index[value].image? %>
|
||||
<img class="object-contain mx-auto max-h-28" style="max-height: 200px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
|
||||
<% elsif field['type'].in?(['file', 'payment', 'image']) %>
|
||||
<div class="flex flex-col justify-center">
|
||||
|
||||
@@ -35,19 +35,19 @@
|
||||
<% if is_show_tabs %>
|
||||
<div class="flex items-center flex-col md:flex-row md:flex-wrap gap-2 mb-4">
|
||||
<div class="flex items-center md:items-end flex-col md:flex-row gap-2 w-full md:w-fit">
|
||||
<a href="<%= url_for(params.to_unsafe_h.except(:status)) %>" class="<%= params[:status].blank? ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
|
||||
<a href="<%= url_for(params: request.query_parameters.except('status')) %>" class="<%= params[:status].blank? ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
|
||||
<div class="flex items-center space-x-1">
|
||||
<%= svg_icon('list', class: 'w-5 h-5') %>
|
||||
<span class="font-normal"><%= t('all') %></span>
|
||||
</div>
|
||||
</a>
|
||||
<a href="<%= url_for(params.to_unsafe_h.merge(status: :pending)) %>" class="<%= params[:status] == 'pending' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
|
||||
<a href="<%= url_for(params: request.query_parameters.merge('status' => 'pending')) %>" class="<%= params[:status] == 'pending' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
|
||||
<div class="flex items-center space-x-1">
|
||||
<%= svg_icon('clock', class: 'w-5 h-5') %>
|
||||
<span class="font-normal"><%= t('pending') %></span>
|
||||
</div>
|
||||
</a>
|
||||
<a href="<%= url_for(params.to_unsafe_h.merge(status: :completed)) %>" class="<%= params[:status] == 'completed' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
|
||||
<a href="<%= url_for(params: request.query_parameters.merge('status' => 'completed')) %>" class="<%= params[:status] == 'completed' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
|
||||
<div class="flex items-center space-x-1">
|
||||
<%= svg_icon('circle_check', class: 'w-5 h-5') %>
|
||||
<span class="font-normal"><%= t('completed') %></span>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<%= svg_icon(icon, class: 'w-5 h-5 shrink-0') %>
|
||||
<span class="font-normal truncate"><%= t(params[:status]) %></span>
|
||||
<% end %>
|
||||
<%= link_to url_for(params.to_unsafe_h.except(:status)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
||||
<%= link_to url_for(params: request.query_parameters.except('status')), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -16,7 +16,7 @@
|
||||
<%= svg_icon('folder', class: 'w-5 h-5 shrink-0') %>
|
||||
<span class="font-normal truncate"><%= params[:folder] %></span>
|
||||
<% end %>
|
||||
<%= link_to url_for(params.to_unsafe_h.except(:folder)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
||||
<%= link_to url_for(params: request.query_parameters.except('folder')), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -27,7 +27,7 @@
|
||||
<%= svg_icon('user', class: 'w-5 h-5 shrink-0') %>
|
||||
<span class="font-normal truncate"><%= current_account.users.accessible_by(current_ability).where(account: current_account).find_by(email: params[:author])&.full_name || 'NA' %></span>
|
||||
<% end %>
|
||||
<%= link_to url_for(params.to_unsafe_h.except(:author)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
||||
<%= link_to url_for(params: request.query_parameters.except('author')), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -46,7 +46,7 @@
|
||||
<% end %>
|
||||
</span>
|
||||
<% end %>
|
||||
<%= link_to url_for(params.to_unsafe_h.except(:completed_at_from, :completed_at_to)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
||||
<%= link_to url_for(params: request.query_parameters.except('completed_at_from', 'completed_at_to')), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -65,7 +65,7 @@
|
||||
<% end %>
|
||||
</span>
|
||||
<% end %>
|
||||
<%= link_to url_for(params.to_unsafe_h.except(:created_at_to, :created_at_from)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
||||
<%= link_to url_for(params: request.query_parameters.except('created_at_to', 'created_at_from')), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
<% if params[:with_remove] %>
|
||||
<div class="text-center w-full mt-4">
|
||||
<%= link_to t('remove_filter'), "#{params[:path]}?#{params.to_unsafe_h.slice(:q).merge(local_assigns[:default_params]).to_query}", class: 'link', data: { turbo_frame: :_top } %>
|
||||
<%= link_to t('remove_filter'), "#{params[:path]}?#{request.query_parameters.slice('q').merge(local_assigns[:default_params]).to_query}", class: 'link', data: { turbo_frame: :_top } %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<% page_blob_struct = Struct.new(:url, :metadata, keyword_init: true) %>
|
||||
<% schema = Submissions.filtered_conditions_schema(@submitter.submission, values:, include_submitter_uuid: @submitter.uuid) %>
|
||||
<% font_scale = 1000.0 / PdfUtils::US_LETTER_W %>
|
||||
<% decline_modal_checkbox_uuid = nil %>
|
||||
<div style="max-height: -webkit-fill-available;">
|
||||
<div id="scrollbox">
|
||||
<div class="mx-auto block pb-72" style="max-width: 1000px">
|
||||
@@ -20,12 +21,7 @@
|
||||
</div>
|
||||
<div class="flex items-center space-x-2" style="margin-left: 20px; flex-shrink: 0">
|
||||
<% if @form_configs[:with_decline] %>
|
||||
<% decline_modal_checkbox_uuid = SecureRandom.uuid %>
|
||||
<div>
|
||||
<%= render 'shared/html_modal', title: t(:decline), btn_text: t(:decline), btn_class: 'btn btn-sm !px-5', button_id: 'decline_button', uuid: decline_modal_checkbox_uuid do %>
|
||||
<%= render 'submit_form/decline_form', submitter: @submitter %>
|
||||
<% end %>
|
||||
</div>
|
||||
<label id="decline_button" for="<%= decline_modal_checkbox_uuid = SecureRandom.uuid %>" class="btn btn-sm !px-5"><%= t(:decline) %></label>
|
||||
<% end %>
|
||||
<% if @form_configs[:with_partial_download] %>
|
||||
<download-button data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm !px-4">
|
||||
@@ -102,11 +98,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fixed bottom-0 w-full h-0 z-20">
|
||||
<div class="fixed bottom-0 w-full h-0 z-50">
|
||||
<div class="mx-auto" style="max-width: 1000px">
|
||||
<div class="relative md:mx-32">
|
||||
<%= render 'submit_form/submission_form', attachments_index: @attachments_index, submitter: @submitter, signature_attachment: @signature_attachment, configs: @form_configs, dry_run: local_assigns[:dry_run], expand: local_assigns[:expand], scroll_padding: local_assigns.fetch(:scroll_padding, '-110px'), schema: %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% if @form_configs[:with_decline] %>
|
||||
<%= render 'shared/html_modal', title: t(:decline), uuid: decline_modal_checkbox_uuid do %>
|
||||
<%= render 'submit_form/decline_form', submitter: @submitter %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%= render 'scripts/autosize_field' %>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user