mirror of
https://github.com/docusealco/docuseal.git
synced 2026-08-07 23:32:07 +00:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -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:
|
||||
|
||||
+8
-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:
|
||||
@@ -100,3 +100,9 @@ Rails/ApplicationController:
|
||||
|
||||
Rails/Output:
|
||||
Enabled: false
|
||||
|
||||
Rails/StrongParametersExpect:
|
||||
Enabled: false
|
||||
|
||||
Rails/RedirectBackOrTo:
|
||||
Enabled: false
|
||||
|
||||
+6
-8
@@ -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/2.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,7 +42,7 @@ 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"
|
||||
@@ -51,7 +51,7 @@ 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
|
||||
|
||||
@@ -69,9 +69,7 @@ activate = 1' >> /etc/openssl_legacy.cnf
|
||||
|
||||
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 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
|
||||
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")
|
||||
|
||||
COPY --chown=docuseal:docuseal ./bin ./bin
|
||||
COPY --chown=docuseal:docuseal ./app ./app
|
||||
|
||||
@@ -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'
|
||||
|
||||
+286
-285
@@ -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.0)
|
||||
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.0)
|
||||
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,86 +505,89 @@ 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)
|
||||
unicode-emoji (4.2.0)
|
||||
uniform_notifier (1.18.0)
|
||||
uri (1.1.1)
|
||||
useragent (0.16.11)
|
||||
warden (1.2.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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -23,7 +23,7 @@ class ApplicationController < ActionController::Base
|
||||
|
||||
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
|
||||
|
||||
rescue_from Pagy::OverflowError do
|
||||
rescue_from Pagy::RangeError do
|
||||
redirect_to request.path
|
||||
end
|
||||
|
||||
@@ -60,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
|
||||
|
||||
@@ -13,7 +13,7 @@ class TemplatesDetectFieldsController < ApplicationController
|
||||
documents = @template.schema_documents.preload(:blob)
|
||||
documents = documents.where(uuid: params[:attachment_uuid]) if params[:attachment_uuid].present?
|
||||
|
||||
page_number = params[:page].present? ? params[:page].to_i : nil
|
||||
page_number = params[:page].presence&.to_i
|
||||
|
||||
documents.each do |document|
|
||||
io = StringIO.new(document.download)
|
||||
|
||||
@@ -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,6 +155,7 @@ 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',
|
||||
@@ -164,6 +165,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -182,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"
|
||||
|
||||
@@ -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"
|
||||
@@ -592,6 +595,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
|
||||
|
||||
@@ -914,10 +929,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
|
||||
}
|
||||
@@ -984,7 +1001,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 {
|
||||
@@ -1011,7 +1030,9 @@ 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 = []
|
||||
@@ -1066,10 +1087,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 {
|
||||
@@ -1081,7 +1104,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) => {
|
||||
@@ -1143,7 +1168,9 @@ 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]
|
||||
|
||||
@@ -1211,27 +1238,33 @@ export default {
|
||||
return true
|
||||
}
|
||||
},
|
||||
checkFieldConditions (field) {
|
||||
checkFieldConditions (field, cache = {}) {
|
||||
if (cache[field.uuid] !== undefined) {
|
||||
return cache[field.uuid]
|
||||
}
|
||||
|
||||
cache[field.uuid] = true
|
||||
|
||||
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)
|
||||
} else {
|
||||
return true
|
||||
cache[field.uuid] = !result.includes(false)
|
||||
}
|
||||
|
||||
return cache[field.uuid]
|
||||
},
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1241,6 +1274,22 @@ 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)
|
||||
@@ -1341,9 +1390,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 {
|
||||
|
||||
@@ -497,10 +497,10 @@ export default {
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
|
||||
if (!resp.ok) throw new Error('Failed to start KBA')
|
||||
|
||||
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.')
|
||||
@@ -555,7 +555,11 @@ export default {
|
||||
const data = await resp.json()
|
||||
|
||||
if (data.result?.action !== 'PASS') {
|
||||
this.error = 'Knowledge Based Authentication Failed - make sure you provide correct answers for the Knowledge Based authentication.'
|
||||
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')
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -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,10 @@ 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.$refs.canvas.getContext('2d').scale(scale, scale)
|
||||
this.setCanvasSize()
|
||||
|
||||
this.intersectionObserver?.disconnect()
|
||||
}
|
||||
@@ -484,13 +488,62 @@ 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)
|
||||
}
|
||||
},
|
||||
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: { value: this.$refs.textInput.value } })
|
||||
}
|
||||
},
|
||||
remove () {
|
||||
this.$emit('update:model-value', '')
|
||||
|
||||
@@ -694,6 +747,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
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
@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"
|
||||
@@ -332,6 +342,7 @@
|
||||
:editable="editable && !defaultField"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@save="save"
|
||||
@close="isShowFormulaModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
@@ -344,6 +355,7 @@
|
||||
:editable="editable && !defaultField"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@save="save"
|
||||
@close="isShowFontModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
@@ -355,6 +367,7 @@
|
||||
:item="field"
|
||||
:build-default-name="buildDefaultName"
|
||||
:default-field="defaultField"
|
||||
@save="save"
|
||||
@close="isShowConditionsModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
@@ -367,6 +380,7 @@
|
||||
:editable="editable && !defaultField"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@save="save"
|
||||
@close="isShowDescriptionModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
@@ -399,7 +413,7 @@ export default {
|
||||
FieldSubmitter,
|
||||
IconX
|
||||
},
|
||||
inject: ['template', 'save', 't', 'isInlineSize', 'selectedAreasRef', 'isCmdKeyRef'],
|
||||
inject: ['template', 'save', 't', 'isInlineSize', 'selectedAreasRef', 'isCmdKeyRef', 'getFieldTypeIndex'],
|
||||
props: {
|
||||
area: {
|
||||
type: Object,
|
||||
@@ -465,13 +479,18 @@ export default {
|
||||
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,
|
||||
@@ -577,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) {
|
||||
|
||||
@@ -20,8 +20,9 @@
|
||||
/>
|
||||
<DragPlaceholder
|
||||
ref="dragPlaceholder"
|
||||
:field="fieldsDragFieldRef.value || toRaw(dragField)"
|
||||
:field="customDragFieldRef.value || fieldsDragFieldRef.value || toRaw(dragField)"
|
||||
:is-field="template.fields.includes(fieldsDragFieldRef.value)"
|
||||
:is-custom="!!customDragFieldRef.value"
|
||||
:is-default="defaultFields.includes(toRaw(dragField))"
|
||||
:is-required="defaultRequiredFields.includes(toRaw(dragField))"
|
||||
/>
|
||||
@@ -362,7 +363,7 @@
|
||||
:is-drag="!!dragField"
|
||||
:input-mode="inputMode"
|
||||
:default-fields="[...defaultRequiredFields, ...defaultFields]"
|
||||
:allow-draw="!onlyDefinedFields || drawField"
|
||||
:allow-draw="!onlyDefinedFields || drawField || drawCustomField"
|
||||
:with-signature-id="withSignatureId"
|
||||
:with-prefillable="withPrefillable"
|
||||
:data-document-uuid="document.uuid"
|
||||
@@ -371,17 +372,20 @@
|
||||
:with-field-placeholder="withFieldPlaceholder"
|
||||
:draw-field="drawField"
|
||||
:draw-field-type="drawFieldType"
|
||||
:draw-custom-field="drawCustomField"
|
||||
:editable="editable"
|
||||
:is-mobile="isMobile"
|
||||
:base-url="baseUrl"
|
||||
:with-fields-detection="withFieldsDetection"
|
||||
@draw="[onDraw($event), withSelectedFieldType ? '' : drawFieldType = '', showDrawField = false]"
|
||||
@draw="[onDraw($event), withSelectedFieldType ? '' : drawFieldType = '', drawCustomField = null, showDrawField = false]"
|
||||
@drop-field="onDropfield"
|
||||
@remove-area="removeArea"
|
||||
@paste-field="pasteField"
|
||||
@copy-field="copyField"
|
||||
@add-custom-field="addCustomField"
|
||||
@set-draw="[drawField = $event.field, drawOption = $event.option]"
|
||||
@copy-selected-areas="copySelectedAreas"
|
||||
@delete-selected-areas="deleteSelectedAreas"
|
||||
@align-selected-areas="alignSelectedAreas"
|
||||
@autodetect-fields="detectFieldsForPage"
|
||||
/>
|
||||
<DocumentControls
|
||||
@@ -436,15 +440,15 @@
|
||||
v-if="withFieldsList && !isMobile"
|
||||
id="fields_list_container"
|
||||
class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block fields-list-container"
|
||||
:class="drawField ? 'overflow-hidden' : 'overflow-y-auto overflow-x-hidden'"
|
||||
:class="drawField || drawCustomField ? 'overflow-hidden' : 'overflow-y-auto overflow-x-hidden'"
|
||||
>
|
||||
<div
|
||||
v-if="showDrawField || drawField"
|
||||
v-if="showDrawField || drawField || drawCustomField"
|
||||
class="sticky inset-0 h-full z-20"
|
||||
:style="{ backgroundColor }"
|
||||
>
|
||||
<div class="bg-base-200 rounded-lg p-5 text-center space-y-4 draw-field-container">
|
||||
<p v-if="(drawField?.type || drawFieldType) === 'strikethrough'">
|
||||
<p v-if="(drawField?.type || drawFieldType || drawCustomField?.type) === 'strikethrough'">
|
||||
{{ t('draw_strikethrough_the_document') }}
|
||||
</p>
|
||||
<p v-else>
|
||||
@@ -458,10 +462,10 @@
|
||||
{{ t('cancel') }}
|
||||
</button>
|
||||
<a
|
||||
v-if="!drawField && !drawOption && !['stamp', 'signature', 'initials', 'heading', 'strikethrough'].includes(drawField?.type || drawFieldType)"
|
||||
v-if="!drawField && !drawOption && !['stamp', 'signature', 'initials', 'heading', 'strikethrough'].includes(drawField?.type || drawFieldType || drawCustomField?.type)"
|
||||
href="#"
|
||||
class="link block mt-3 text-sm"
|
||||
@click.prevent="[addField(drawFieldType), drawField = null, drawOption = null, withSelectedFieldType ? '' : drawFieldType = '', showDrawField = false]"
|
||||
@click.prevent="drawCustomField ? addCustomFieldWithoutDraw() : [addField(drawFieldType), drawField = null, drawOption = null, withSelectedFieldType ? '' : drawFieldType = '', showDrawField = false]"
|
||||
>
|
||||
{{ t('or_add_field_without_drawing') }}
|
||||
</a>
|
||||
@@ -477,6 +481,8 @@
|
||||
:with-help="withHelp"
|
||||
:default-submitters="defaultSubmitters"
|
||||
:draw-field-type="drawFieldType"
|
||||
:custom-fields="customFields"
|
||||
:with-custom-fields="withCustomFields"
|
||||
:with-fields-search="withFieldsSearch"
|
||||
:default-fields="[...defaultRequiredFields, ...defaultFields]"
|
||||
:template="template"
|
||||
@@ -493,6 +499,7 @@
|
||||
@set-draw="[drawField = $event.field, drawOption = $event.option]"
|
||||
@select-submitter="selectedSubmitter = $event"
|
||||
@set-draw-type="[drawFieldType = $event, showDrawField = true]"
|
||||
@set-draw-custom-field="[drawCustomField = $event, showDrawField = true]"
|
||||
@set-drag="dragField = $event"
|
||||
@set-drag-placeholder="$refs.dragPlaceholder.dragPlaceholder = $event"
|
||||
@change-submitter="selectedSubmitter = $event"
|
||||
@@ -632,12 +639,15 @@ export default {
|
||||
isPaymentConnected: this.isPaymentConnected,
|
||||
withFormula: this.withFormula,
|
||||
withConditions: this.withConditions,
|
||||
withCustomFields: this.withCustomFields,
|
||||
isInlineSize: this.isInlineSize,
|
||||
defaultDrawFieldType: this.defaultDrawFieldType,
|
||||
selectedAreasRef: computed(() => this.selectedAreasRef),
|
||||
fieldsDragFieldRef: computed(() => this.fieldsDragFieldRef),
|
||||
customDragFieldRef: computed(() => this.customDragFieldRef),
|
||||
isSelectModeRef: computed(() => this.isSelectModeRef),
|
||||
isCmdKeyRef: computed(() => this.isCmdKeyRef)
|
||||
isCmdKeyRef: computed(() => this.isCmdKeyRef),
|
||||
getFieldTypeIndex: this.getFieldTypeIndex
|
||||
}
|
||||
},
|
||||
props: {
|
||||
@@ -705,6 +715,16 @@ export default {
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
withCustomFields: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
customFields: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
withAddPageButton: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -902,6 +922,7 @@ export default {
|
||||
pendingFieldAttachmentUuids: [],
|
||||
drawField: null,
|
||||
drawFieldType: null,
|
||||
drawCustomField: null,
|
||||
drawOption: null,
|
||||
dragField: null,
|
||||
isDragFile: false
|
||||
@@ -912,6 +933,7 @@ export default {
|
||||
isSelectModeRef: () => ref(false),
|
||||
isCmdKeyRef: () => ref(false),
|
||||
fieldsDragFieldRef: () => ref(),
|
||||
customDragFieldRef: () => ref(),
|
||||
selectedAreasRef: () => ref([]),
|
||||
language () {
|
||||
return this.locale.split('-')[0].toLowerCase()
|
||||
@@ -969,6 +991,18 @@ export default {
|
||||
|
||||
return areas
|
||||
},
|
||||
fieldTypeIndexMap () {
|
||||
const map = {}
|
||||
const typeCounters = {}
|
||||
|
||||
this.template.fields.forEach((f) => {
|
||||
typeCounters[f.type] ||= 0
|
||||
map[f.uuid] = typeCounters[f.type]
|
||||
typeCounters[f.type]++
|
||||
})
|
||||
|
||||
return map
|
||||
},
|
||||
isAllRequiredFieldsAdded () {
|
||||
return !this.defaultRequiredFields?.some((f) => {
|
||||
return !this.template.fields?.some((field) => field.name === f.name)
|
||||
@@ -1062,6 +1096,33 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
toRaw,
|
||||
addCustomField (field) {
|
||||
return this.$refs.fields.addCustomField(field)
|
||||
},
|
||||
getFieldTypeIndex (field) {
|
||||
return this.fieldTypeIndexMap[field.uuid]
|
||||
},
|
||||
addCustomFieldWithoutDraw () {
|
||||
const customField = this.drawCustomField
|
||||
|
||||
const field = JSON.parse(JSON.stringify(customField))
|
||||
|
||||
field.uuid = v4()
|
||||
field.submitter_uuid = this.selectedSubmitter.uuid
|
||||
field.areas = []
|
||||
|
||||
if (field.options?.length) {
|
||||
field.options = field.options.map(opt => ({ ...opt, uuid: v4() }))
|
||||
}
|
||||
|
||||
delete field.conditions
|
||||
|
||||
this.insertField(field)
|
||||
this.save()
|
||||
|
||||
this.drawCustomField = null
|
||||
this.showDrawField = false
|
||||
},
|
||||
toggleSelectMode () {
|
||||
this.isSelectModeRef.value = !this.isSelectModeRef.value
|
||||
|
||||
@@ -1116,27 +1177,6 @@ export default {
|
||||
|
||||
this.debouncedSave()
|
||||
},
|
||||
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()
|
||||
},
|
||||
download () {
|
||||
this.isDownloading = true
|
||||
|
||||
@@ -1514,6 +1554,7 @@ export default {
|
||||
clearDrawField () {
|
||||
this.drawField = null
|
||||
this.drawOption = null
|
||||
this.drawCustomField = null
|
||||
this.showDrawField = false
|
||||
|
||||
if (!this.withSelectedFieldType) {
|
||||
@@ -1951,6 +1992,10 @@ export default {
|
||||
}
|
||||
},
|
||||
onDraw ({ area, isTooSmall }) {
|
||||
if (this.drawCustomField) {
|
||||
return this.onDrawCustomField(area)
|
||||
}
|
||||
|
||||
if (this.drawField) {
|
||||
if (this.drawOption) {
|
||||
const areaWithoutOption = this.drawField.areas?.find((a) => !a.option_uuid)
|
||||
@@ -2016,7 +2061,9 @@ export default {
|
||||
}
|
||||
|
||||
if (type === 'checkbox' && !this.drawFieldType && (this.template.fields[this.template.fields.length - 1]?.type === 'checkbox' || area.w)) {
|
||||
const previousField = [...this.template.fields].reverse().find((f) => f.type === type)
|
||||
const previousField = this.template.fields.findLast
|
||||
? this.template.fields.findLast((f) => f.type === type)
|
||||
: [...this.template.fields].reverse().find((f) => f.type === type)
|
||||
const previousArea = previousField?.areas?.[previousField.areas.length - 1]
|
||||
|
||||
if (previousArea || area.w) {
|
||||
@@ -2061,6 +2108,10 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.customDragFieldRef.value) {
|
||||
return this.dropCustomField(area)
|
||||
}
|
||||
|
||||
const field = this.fieldsDragFieldRef.value || {
|
||||
name: '',
|
||||
uuid: v4(),
|
||||
@@ -2153,10 +2204,132 @@ export default {
|
||||
})
|
||||
}
|
||||
},
|
||||
dropCustomField (area) {
|
||||
const customField = this.customDragFieldRef.value
|
||||
const customAreas = customField.areas || []
|
||||
|
||||
const field = JSON.parse(JSON.stringify(customField))
|
||||
|
||||
field.uuid = v4()
|
||||
field.submitter_uuid = this.selectedSubmitter.uuid
|
||||
field.areas = []
|
||||
|
||||
if (field.options?.length) {
|
||||
field.options = field.options.map(opt => ({ ...opt, uuid: v4() }))
|
||||
}
|
||||
|
||||
delete field.conditions
|
||||
|
||||
const dropX = (area.x - 6) / area.maskW
|
||||
const dropY = area.y / area.maskH
|
||||
|
||||
if (customAreas.length > 0) {
|
||||
const refArea = customAreas[0]
|
||||
|
||||
customAreas.forEach((customArea) => {
|
||||
const fieldArea = {
|
||||
x: dropX + (customArea.x - refArea.x),
|
||||
y: dropY + (customArea.y - refArea.y) - (customArea.h / 2),
|
||||
w: customArea.w,
|
||||
h: customArea.h,
|
||||
page: area.page,
|
||||
attachment_uuid: area.attachment_uuid
|
||||
}
|
||||
|
||||
if (customArea.cell_w) {
|
||||
fieldArea.cell_w = customArea.cell_w
|
||||
}
|
||||
|
||||
if (customArea.option_uuid && field.options?.length) {
|
||||
const optionIndex = customField.options.findIndex(o => o.uuid === customArea.option_uuid)
|
||||
if (optionIndex !== -1) {
|
||||
fieldArea.option_uuid = field.options[optionIndex].uuid
|
||||
}
|
||||
}
|
||||
|
||||
field.areas.push(fieldArea)
|
||||
})
|
||||
} else {
|
||||
const fieldArea = {
|
||||
x: dropX,
|
||||
y: dropY,
|
||||
page: area.page,
|
||||
attachment_uuid: area.attachment_uuid
|
||||
}
|
||||
|
||||
this.assignDropAreaSize(fieldArea, field, area)
|
||||
|
||||
field.areas.push(fieldArea)
|
||||
}
|
||||
|
||||
this.selectedAreasRef.value = [field.areas[0]]
|
||||
|
||||
this.insertField(field)
|
||||
this.save()
|
||||
|
||||
document.activeElement?.blur()
|
||||
},
|
||||
onDrawCustomField (area) {
|
||||
const customField = this.drawCustomField
|
||||
const customAreas = customField.areas || []
|
||||
|
||||
const field = JSON.parse(JSON.stringify(customField))
|
||||
|
||||
field.uuid = v4()
|
||||
field.submitter_uuid = this.selectedSubmitter.uuid
|
||||
field.areas = []
|
||||
|
||||
if (field.options?.length) {
|
||||
field.options = field.options.map(opt => ({ ...opt, uuid: v4() }))
|
||||
}
|
||||
|
||||
delete field.conditions
|
||||
|
||||
const isClick = area.w === 0 || area.h === 0
|
||||
|
||||
const firstArea = {
|
||||
x: area.x,
|
||||
y: area.y,
|
||||
w: area.w || customAreas[0]?.w,
|
||||
h: area.h || customAreas[0]?.h,
|
||||
page: area.page,
|
||||
attachment_uuid: area.attachment_uuid
|
||||
}
|
||||
|
||||
if (!firstArea.w || !firstArea.h) {
|
||||
if (customAreas[0]) {
|
||||
firstArea.w = customAreas[0].w
|
||||
firstArea.h = customAreas[0].h
|
||||
} else {
|
||||
this.setDefaultAreaSize(firstArea, field.type)
|
||||
}
|
||||
}
|
||||
|
||||
if (isClick) {
|
||||
firstArea.x -= firstArea.w / 2
|
||||
firstArea.y -= firstArea.h / 2
|
||||
}
|
||||
|
||||
if (field.options?.length) {
|
||||
firstArea.option_uuid = field.options[0].uuid
|
||||
}
|
||||
|
||||
field.areas.push(firstArea)
|
||||
|
||||
this.selectedAreasRef.value = [field.areas[0]]
|
||||
|
||||
this.insertField(field)
|
||||
this.save()
|
||||
|
||||
this.drawCustomField = null
|
||||
this.showDrawField = false
|
||||
},
|
||||
assignDropAreaSize (fieldArea, field, area) {
|
||||
const fieldType = field.type || 'text'
|
||||
|
||||
const previousField = [...this.template.fields].reverse().find((f) => f.type === fieldType)
|
||||
const previousField = this.template.fields.findLast
|
||||
? this.template.fields.findLast((f) => f.type === fieldType)
|
||||
: [...this.template.fields].reverse().find((f) => f.type === fieldType)
|
||||
|
||||
let baseArea
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -169,18 +179,13 @@ export default {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
withClickSaveEvent: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
excludeFieldUuids: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
emits: ['close', 'click-save'],
|
||||
emits: ['close', 'save'],
|
||||
data () {
|
||||
return {
|
||||
conditions: this.item.conditions?.[0] ? JSON.parse(JSON.stringify(this.item.conditions)) : [{}]
|
||||
@@ -227,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')
|
||||
}
|
||||
@@ -244,12 +251,7 @@ export default {
|
||||
delete this.item.conditions
|
||||
}
|
||||
|
||||
if (this.withClickSaveEvent) {
|
||||
this.$emit('click-save')
|
||||
} else {
|
||||
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()
|
||||
|
||||
@@ -1,546 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-if="!isShowFormulaModal && !isShowFontModal && !isShowConditionsModal && !isShowDescriptionModal"
|
||||
ref="menu"
|
||||
class="fixed z-50 p-1 bg-white shadow-lg rounded-lg border border-base-300 min-w-[170px] cursor-default"
|
||||
:style="menuStyle"
|
||||
@mousedown.stop
|
||||
@pointerdown.stop
|
||||
>
|
||||
<label
|
||||
v-if="showRequired"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm cursor-pointer"
|
||||
@click.stop
|
||||
>
|
||||
<input
|
||||
:checked="isRequired"
|
||||
type="checkbox"
|
||||
class="toggle toggle-xs"
|
||||
@change="handleToggleRequired($event.target.checked)"
|
||||
@click.stop
|
||||
>
|
||||
<span>{{ t('required') }}</span>
|
||||
</label>
|
||||
<label
|
||||
v-if="showReadOnly"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm cursor-pointer"
|
||||
@click.stop
|
||||
>
|
||||
<input
|
||||
:checked="isReadOnly"
|
||||
type="checkbox"
|
||||
class="toggle toggle-xs"
|
||||
@change="handleToggleReadOnly($event.target.checked)"
|
||||
@click.stop
|
||||
>
|
||||
<span>{{ t('read_only') }}</span>
|
||||
</label>
|
||||
<hr
|
||||
v-if="(showRequired || showReadOnly) && (showFont || showDescription || showCondition || showFormula)"
|
||||
class="my-1 border-base-300"
|
||||
>
|
||||
<button
|
||||
v-if="showFont && !isMultiSelection"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-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="showDescription"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="openDescriptionModal"
|
||||
>
|
||||
<IconInfoCircle class="w-4 h-4" />
|
||||
<span>{{ t('description') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="showCondition && !isMultiSelection"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="openConditionModal"
|
||||
>
|
||||
<IconRouteAltLeft class="w-4 h-4" />
|
||||
<span>{{ t('condition') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="showFormula"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="openFormulaModal"
|
||||
>
|
||||
<IconMathFunction class="w-4 h-4" />
|
||||
<span>{{ t('formula') }}</span>
|
||||
</button>
|
||||
<hr
|
||||
v-if="((showFont && !isMultiSelection) || showDescription || (showCondition && !isMultiSelection) || showFormula) && (showCopy || showDelete || showPaste)"
|
||||
class="my-1 border-base-300"
|
||||
>
|
||||
<button
|
||||
v-if="isMultiSelection"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="$emit('align', 'left')"
|
||||
>
|
||||
<IconLayoutAlignLeft class="w-4 h-4" />
|
||||
<span>{{ t('align_left') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="isMultiSelection"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="$emit('align', 'right')"
|
||||
>
|
||||
<IconLayoutAlignRight class="w-4 h-4" />
|
||||
<span>{{ t('align_right') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="isMultiSelection"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="$emit('align', 'top')"
|
||||
>
|
||||
<IconLayoutAlignTop class="w-4 h-4" />
|
||||
<span>{{ t('align_top') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="isMultiSelection"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="$emit('align', 'bottom')"
|
||||
>
|
||||
<IconLayoutAlignBottom class="w-4 h-4" />
|
||||
<span>{{ t('align_bottom') }}</span>
|
||||
</button>
|
||||
<hr
|
||||
v-if="isMultiSelection && (showFont || showCondition)"
|
||||
class="my-1 border-base-300"
|
||||
>
|
||||
<button
|
||||
v-if="showFont && isMultiSelection"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-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 && isMultiSelection"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="openConditionModal"
|
||||
>
|
||||
<IconRouteAltLeft class="w-4 h-4" />
|
||||
<span>{{ t('condition') }}</span>
|
||||
</button>
|
||||
<hr
|
||||
v-if="isMultiSelection"
|
||||
class="my-1 border-base-300"
|
||||
>
|
||||
<button
|
||||
v-if="showCopy"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-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
|
||||
v-if="showDelete"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-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>
|
||||
<button
|
||||
v-if="showPaste"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center justify-between text-sm"
|
||||
:class="!hasClipboardData ? 'opacity-50 cursor-not-allowed' : 'hover:bg-base-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
|
||||
v-if="showSelectFields"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-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-base-300"
|
||||
>
|
||||
<button
|
||||
v-if="showAutodetectFields"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-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>
|
||||
<Teleport
|
||||
v-if="isShowFormulaModal"
|
||||
:to="modalContainerEl"
|
||||
>
|
||||
<FormulaModal
|
||||
:field="field"
|
||||
:editable="editable"
|
||||
:build-default-name="buildDefaultName"
|
||||
@close="closeModal"
|
||||
/>
|
||||
</Teleport>
|
||||
<Teleport
|
||||
v-if="isShowFontModal"
|
||||
:to="modalContainerEl"
|
||||
>
|
||||
<FontModal
|
||||
:field="multiSelectField || field"
|
||||
:area="contextMenu.area"
|
||||
:editable="editable"
|
||||
:build-default-name="buildDefaultName"
|
||||
:with-click-save-event="isMultiSelection"
|
||||
@click-save="handleSaveMultiSelectFontModal"
|
||||
@close="closeModal"
|
||||
/>
|
||||
</Teleport>
|
||||
<Teleport
|
||||
v-if="isShowConditionsModal"
|
||||
:to="modalContainerEl"
|
||||
>
|
||||
<ConditionsModal
|
||||
:item="multiSelectField || field"
|
||||
:build-default-name="buildDefaultName"
|
||||
:exclude-field-uuids="isMultiSelection ? selectedFields.map(f => f.uuid) : []"
|
||||
:with-click-save-event="isMultiSelection"
|
||||
@click-save="handleSaveMultiSelectConditionsModal"
|
||||
@close="closeModal"
|
||||
/>
|
||||
</Teleport>
|
||||
<Teleport
|
||||
v-if="isShowDescriptionModal"
|
||||
:to="modalContainerEl"
|
||||
>
|
||||
<DescriptionModal
|
||||
:field="field"
|
||||
:editable="editable"
|
||||
:build-default-name="buildDefaultName"
|
||||
@close="closeModal"
|
||||
/>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IconCopy, IconClipboard, IconTrashX, IconTypography, IconInfoCircle, IconRouteAltLeft, IconMathFunction, IconClick, IconNewSection, IconLayoutAlignLeft, IconLayoutAlignRight, IconLayoutAlignTop, IconLayoutAlignBottom, IconSparkles } from '@tabler/icons-vue'
|
||||
import FormulaModal from './formula_modal'
|
||||
import FontModal from './font_modal'
|
||||
import ConditionsModal from './conditions_modal'
|
||||
import DescriptionModal from './description_modal'
|
||||
import Field from './field'
|
||||
import FieldType from './field_type.vue'
|
||||
|
||||
export default {
|
||||
name: 'ContextMenu',
|
||||
components: {
|
||||
IconCopy,
|
||||
IconClipboard,
|
||||
IconTrashX,
|
||||
IconTypography,
|
||||
IconInfoCircle,
|
||||
IconRouteAltLeft,
|
||||
IconMathFunction,
|
||||
IconClick,
|
||||
IconNewSection,
|
||||
IconLayoutAlignLeft,
|
||||
IconLayoutAlignRight,
|
||||
IconLayoutAlignTop,
|
||||
IconLayoutAlignBottom,
|
||||
IconSparkles,
|
||||
FormulaModal,
|
||||
FontModal,
|
||||
ConditionsModal,
|
||||
DescriptionModal
|
||||
},
|
||||
inject: ['t', 'save', 'selectedAreasRef', 'isSelectModeRef'],
|
||||
props: {
|
||||
contextMenu: {
|
||||
type: Object,
|
||||
default: null,
|
||||
required: true
|
||||
},
|
||||
field: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
isMultiSelection: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
selectedAreas: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
template: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
withFieldsDetection: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['copy', 'paste', 'delete', 'close', 'align', 'autodetect-fields'],
|
||||
data () {
|
||||
return {
|
||||
isShowFormulaModal: false,
|
||||
isShowFontModal: false,
|
||||
isShowConditionsModal: false,
|
||||
isShowDescriptionModal: false,
|
||||
multiSelectField: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
fieldNames: FieldType.computed.fieldNames,
|
||||
fieldLabels: FieldType.computed.fieldLabels,
|
||||
modalContainerEl () {
|
||||
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
||||
},
|
||||
selectedFields () {
|
||||
if (!this.isMultiSelection) return []
|
||||
|
||||
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')
|
||||
},
|
||||
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'
|
||||
}
|
||||
},
|
||||
showCopy () {
|
||||
return !!this.contextMenu.area || this.isMultiSelection
|
||||
},
|
||||
showPaste () {
|
||||
return !this.contextMenu.area && !this.isMultiSelection
|
||||
},
|
||||
showDelete () {
|
||||
return !!this.contextMenu.area || this.isMultiSelection
|
||||
},
|
||||
showFont () {
|
||||
if (this.isMultiSelection) return true
|
||||
if (!this.field) return false
|
||||
|
||||
return ['text', 'number', 'date', 'select', 'heading'].includes(this.field.type)
|
||||
},
|
||||
showDescription () {
|
||||
if (!this.field) return false
|
||||
|
||||
return !['stamp', 'heading', 'strikethrough'].includes(this.field.type)
|
||||
},
|
||||
showCondition () {
|
||||
if (this.isMultiSelection) return true
|
||||
if (!this.field) return false
|
||||
|
||||
return !['stamp', 'heading'].includes(this.field.type)
|
||||
},
|
||||
showFormula () {
|
||||
if (!this.field) return false
|
||||
|
||||
return this.field.type === 'number'
|
||||
},
|
||||
showRequired () {
|
||||
if (!this.field) return false
|
||||
|
||||
return !['phone', 'stamp', 'verification', 'strikethrough', 'heading'].includes(this.field.type)
|
||||
},
|
||||
showReadOnly () {
|
||||
if (!this.field) return false
|
||||
|
||||
return ['text', 'number'].includes(this.field.type)
|
||||
},
|
||||
isRequired () {
|
||||
return this.field?.required || false
|
||||
},
|
||||
isReadOnly () {
|
||||
return this.field?.readonly || false
|
||||
},
|
||||
showSelectFields () {
|
||||
return !this.contextMenu.area && !this.isMultiSelection
|
||||
},
|
||||
showAutodetectFields () {
|
||||
return this.withFieldsDetection && this.editable && !this.contextMenu.area && !this.isMultiSelection
|
||||
}
|
||||
},
|
||||
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: {
|
||||
buildDefaultName: Field.methods.buildDefaultName,
|
||||
checkMenuPosition () {
|
||||
if (this.$refs.menu) {
|
||||
const rect = this.$refs.menu.getBoundingClientRect()
|
||||
|
||||
if (rect.bottom > window.innerHeight) {
|
||||
this.contextMenu.y = this.contextMenu.y - rect.height
|
||||
}
|
||||
|
||||
if (rect.right > window.innerWidth) {
|
||||
this.contextMenu.x = this.contextMenu.x - rect.width
|
||||
}
|
||||
}
|
||||
},
|
||||
handleToggleRequired (value) {
|
||||
if (this.field) {
|
||||
this.field.required = value
|
||||
this.save()
|
||||
}
|
||||
},
|
||||
handleToggleReadOnly (value) {
|
||||
if (this.field) {
|
||||
this.field.readonly = value
|
||||
this.save()
|
||||
}
|
||||
},
|
||||
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')
|
||||
}
|
||||
},
|
||||
openFontModal () {
|
||||
if (this.isMultiSelection) {
|
||||
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
|
||||
},
|
||||
openDescriptionModal () {
|
||||
this.isShowDescriptionModal = true
|
||||
},
|
||||
openConditionModal () {
|
||||
if (this.isMultiSelection) {
|
||||
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
|
||||
},
|
||||
openFormulaModal () {
|
||||
this.isShowFormulaModal = true
|
||||
},
|
||||
closeModal () {
|
||||
this.isShowFormulaModal = false
|
||||
this.isShowFontModal = false
|
||||
this.isShowConditionsModal = false
|
||||
this.isShowDescriptionModal = false
|
||||
this.multiSelectField = null
|
||||
|
||||
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()
|
||||
},
|
||||
handleToggleSelectMode () {
|
||||
this.isSelectModeRef.value = !this.isSelectModeRef.value
|
||||
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -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,12 +13,14 @@
|
||||
: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"
|
||||
@@ -28,9 +30,10 @@
|
||||
@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')"
|
||||
@align-selected-areas="$emit('align-selected-areas', $event)"
|
||||
@autodetect-fields="$emit('autodetect-fields', $event)"
|
||||
@scroll-to="scrollToArea"
|
||||
@draw="$emit('draw', { area: {...$event.area, attachment_uuid: document.uuid }, isTooSmall: $event.isTooSmall })"
|
||||
@@ -96,6 +99,11 @@ export default {
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
isMobile: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
allowDraw: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -115,6 +123,11 @@ export default {
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
drawCustomField: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
baseUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
@@ -131,7 +144,7 @@ export default {
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['draw', 'drop-field', 'remove-area', 'paste-field', 'copy-field', 'copy-selected-areas', 'delete-selected-areas', 'align-selected-areas', 'autodetect-fields'],
|
||||
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: []
|
||||
@@ -173,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', 'selectedAreasRef', '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}`
|
||||
@@ -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',
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
@@ -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', 'withKba', 'withPayment', 't', 'fieldsDragFieldRef', 'baseFetch', 'selectedAreasRef'],
|
||||
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 = []
|
||||
|
||||
@@ -617,6 +795,47 @@ export default {
|
||||
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,
|
||||
@@ -212,17 +212,12 @@ export default {
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
withClickSaveEvent: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
buildDefaultName: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['close', 'click-save'],
|
||||
emits: ['close', 'save'],
|
||||
data () {
|
||||
return {
|
||||
preferences: {}
|
||||
@@ -262,6 +257,7 @@ export default {
|
||||
colors () {
|
||||
return [
|
||||
{ label: '⬛', value: 'black' },
|
||||
{ label: '⬜', value: 'white' },
|
||||
{ label: '🟦', value: 'blue' },
|
||||
{ label: '🟥', value: 'red' }
|
||||
]
|
||||
@@ -276,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)
|
||||
}
|
||||
@@ -327,12 +324,7 @@ export default {
|
||||
|
||||
Object.assign(this.field.preferences, this.preferences)
|
||||
|
||||
if (this.withClickSaveEvent) {
|
||||
this.$emit('click-save')
|
||||
} else {
|
||||
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')
|
||||
}
|
||||
|
||||
@@ -1,10 +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',
|
||||
@@ -29,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',
|
||||
@@ -46,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',
|
||||
@@ -89,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',
|
||||
@@ -171,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',
|
||||
@@ -200,6 +212,9 @@ const en = {
|
||||
}
|
||||
|
||||
const es = {
|
||||
fixed: 'Fijo',
|
||||
default: 'Predeterminado',
|
||||
save_as_custom_field: 'Guardar como personalizado',
|
||||
kba: 'KBA',
|
||||
autodetect_fields: 'Autodetectar campos',
|
||||
analyzing_: 'Analizando...',
|
||||
@@ -231,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',
|
||||
@@ -248,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',
|
||||
@@ -288,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',
|
||||
@@ -372,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',
|
||||
@@ -401,6 +425,9 @@ const es = {
|
||||
}
|
||||
|
||||
const it = {
|
||||
fixed: 'Fisso',
|
||||
default: 'Predefinito',
|
||||
save_as_custom_field: 'Salva come personalizzato',
|
||||
kba: 'KBA',
|
||||
autodetect_fields: 'Rileva campi',
|
||||
analyzing_: 'Analisi...',
|
||||
@@ -431,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',
|
||||
@@ -448,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',
|
||||
@@ -493,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',
|
||||
@@ -573,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',
|
||||
@@ -602,6 +638,9 @@ const it = {
|
||||
}
|
||||
|
||||
const pt = {
|
||||
fixed: 'Fixo',
|
||||
default: 'Padrão',
|
||||
save_as_custom_field: 'Salvar como personalizado',
|
||||
kba: 'KBA',
|
||||
autodetect_fields: 'Detectar campos',
|
||||
analyzing_: 'Analisando...',
|
||||
@@ -636,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',
|
||||
@@ -650,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',
|
||||
@@ -690,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',
|
||||
@@ -774,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',
|
||||
@@ -803,6 +851,9 @@ const pt = {
|
||||
}
|
||||
|
||||
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...',
|
||||
@@ -833,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',
|
||||
@@ -850,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',
|
||||
@@ -895,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',
|
||||
@@ -975,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',
|
||||
@@ -1004,6 +1064,9 @@ const fr = {
|
||||
}
|
||||
|
||||
const de = {
|
||||
fixed: 'Fest',
|
||||
default: 'Standard',
|
||||
save_as_custom_field: 'Als benutzerdefiniert speichern',
|
||||
kba: 'KBA',
|
||||
autodetect_fields: 'Felder erkennen',
|
||||
analyzing_: 'Analysiere...',
|
||||
@@ -1034,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',
|
||||
@@ -1051,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',
|
||||
@@ -1096,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',
|
||||
@@ -1176,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',
|
||||
@@ -1205,6 +1277,9 @@ const de = {
|
||||
}
|
||||
|
||||
const nl = {
|
||||
fixed: 'Vast',
|
||||
default: 'Standaard',
|
||||
save_as_custom_field: 'Opslaan als aangepast',
|
||||
kba: 'KBA',
|
||||
autodetect_fields: 'Velden detecteren',
|
||||
analyzing_: 'Analyseren...',
|
||||
@@ -1235,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',
|
||||
@@ -1252,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',
|
||||
@@ -1297,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',
|
||||
@@ -1377,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',
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
:is-drag="isDrag"
|
||||
@move="onSelectionBoxMove"
|
||||
@contextmenu="openSelectionContextMenu"
|
||||
@close-context-menu="closeSelectionContextMenu"
|
||||
@close-context-menu="closeContextMenu"
|
||||
/>
|
||||
<FieldArea
|
||||
v-for="(item, i) in areas"
|
||||
@@ -48,52 +48,70 @@
|
||||
: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"
|
||||
/>
|
||||
<ContextMenu
|
||||
v-if="contextMenu"
|
||||
<FieldContextMenu
|
||||
v-if="contextMenu && contextMenu.field"
|
||||
:context-menu="contextMenu"
|
||||
:field="contextMenu.field"
|
||||
:with-signature-id="withSignatureId"
|
||||
:with-prefillable="withPrefillable"
|
||||
:editable="editable"
|
||||
:with-fields-detection="withFieldsDetection"
|
||||
: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"
|
||||
/>
|
||||
<ContextMenu
|
||||
v-if="selectionContextMenu"
|
||||
:context-menu="selectionContextMenu"
|
||||
:editable="editable"
|
||||
:is-multi-selection="true"
|
||||
:selected-areas="selectedAreasRef.value"
|
||||
:template="template"
|
||||
@copy="handleSelectionCopy"
|
||||
@delete="handleSelectionDelete"
|
||||
@align="handleSelectionAlign"
|
||||
@close="closeSelectionContextMenu"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-show="resizeDirection || isDrag || showMask || (drawField && isMobile) || fieldsDragFieldRef.value || selectionRect"
|
||||
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"
|
||||
@@ -103,7 +121,7 @@
|
||||
@contextmenu="openContextMenu"
|
||||
@dragover.prevent="onDragover"
|
||||
@dragenter="onDragenter"
|
||||
@dragleave="newArea = null"
|
||||
@dragleave="newAreas = []"
|
||||
@drop="onDrop"
|
||||
@pointerup="onPointerup"
|
||||
/>
|
||||
@@ -112,17 +130,21 @@
|
||||
|
||||
<script>
|
||||
import FieldArea from './area'
|
||||
import ContextMenu from './context_menu'
|
||||
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,
|
||||
ContextMenu,
|
||||
FieldContextMenu,
|
||||
SelectionContextMenu,
|
||||
PageContextMenu,
|
||||
SelectionBox
|
||||
},
|
||||
inject: ['fieldTypes', 'defaultDrawFieldType', 'fieldsDragFieldRef', 'assignDropAreaSize', 'selectedAreasRef', 'template', 'isSelectModeRef'],
|
||||
inject: ['fieldTypes', 'defaultDrawFieldType', 'fieldsDragFieldRef', 'customDragFieldRef', 'assignDropAreaSize', 'selectedAreasRef', 'template', 'isSelectModeRef', 'save'],
|
||||
props: {
|
||||
image: {
|
||||
type: Object,
|
||||
@@ -133,6 +155,11 @@ export default {
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
isMobile: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
withSignatureId: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -191,6 +218,11 @@ export default {
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
drawCustomField: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -216,16 +248,15 @@ export default {
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['draw', 'drop-field', 'remove-area', 'copy-field', 'paste-field', 'scroll-to', 'copy-selected-areas', 'delete-selected-areas', 'align-selected-areas', 'autodetect-fields'],
|
||||
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,
|
||||
selectionContextMenu: null
|
||||
selectionRect: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -274,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
|
||||
@@ -285,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',
|
||||
@@ -302,6 +336,21 @@ 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 {}
|
||||
|
||||
@@ -331,7 +380,7 @@ export default {
|
||||
|
||||
const rect = this.$refs.image.getBoundingClientRect()
|
||||
|
||||
this.newArea = null
|
||||
this.newAreas = []
|
||||
this.showMask = false
|
||||
|
||||
this.contextMenu = {
|
||||
@@ -351,7 +400,7 @@ export default {
|
||||
|
||||
const rect = this.$refs.image.getBoundingClientRect()
|
||||
|
||||
this.newArea = null
|
||||
this.newAreas = []
|
||||
this.showMask = false
|
||||
|
||||
this.contextMenu = {
|
||||
@@ -364,33 +413,36 @@ export default {
|
||||
}
|
||||
},
|
||||
openSelectionContextMenu (event) {
|
||||
if (!this.editable) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const rect = this.$el.getBoundingClientRect()
|
||||
|
||||
this.selectionContextMenu = {
|
||||
this.contextMenu = {
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
relativeX: (event.clientX - rect.left) / rect.width,
|
||||
relativeY: (event.clientY - rect.top) / rect.height
|
||||
relativeY: (event.clientY - rect.top) / rect.height,
|
||||
areas: this.selectedAreasRef.value
|
||||
}
|
||||
},
|
||||
closeSelectionContextMenu () {
|
||||
this.selectionContextMenu = null
|
||||
},
|
||||
handleSelectionCopy () {
|
||||
this.$emit('copy-selected-areas')
|
||||
this.closeSelectionContextMenu()
|
||||
|
||||
this.closeContextMenu()
|
||||
},
|
||||
handleSelectionDelete () {
|
||||
this.$emit('delete-selected-areas')
|
||||
this.closeSelectionContextMenu()
|
||||
},
|
||||
handleSelectionAlign (direction) {
|
||||
this.$emit('align-selected-areas', direction)
|
||||
this.closeSelectionContextMenu()
|
||||
|
||||
this.closeContextMenu()
|
||||
},
|
||||
closeContextMenu () {
|
||||
this.contextMenu = null
|
||||
this.newArea = null
|
||||
this.newAreas = []
|
||||
this.showMask = false
|
||||
},
|
||||
handleCopy () {
|
||||
@@ -410,7 +462,7 @@ export default {
|
||||
this.closeContextMenu()
|
||||
},
|
||||
handlePaste () {
|
||||
this.newArea = null
|
||||
this.newAreas = []
|
||||
this.showMask = false
|
||||
|
||||
this.$emit('paste-field', {
|
||||
@@ -435,22 +487,66 @@ export default {
|
||||
}
|
||||
},
|
||||
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,
|
||||
@@ -479,7 +575,7 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.isMobile && !this.drawField) {
|
||||
if (this.isMobile && !this.drawField && !this.drawCustomField) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -490,14 +586,14 @@ 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) {
|
||||
@@ -563,28 +659,30 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
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) {
|
||||
this.newArea.x = this.newArea.initialX
|
||||
drawArea.x = drawArea.initialX
|
||||
} else {
|
||||
this.newArea.x = e.offsetX / this.$refs.mask.clientWidth
|
||||
drawArea.x = e.offsetX / this.$refs.mask.clientWidth
|
||||
}
|
||||
|
||||
if (dy > 0) {
|
||||
this.newArea.y = this.newArea.initialY
|
||||
drawArea.y = drawArea.initialY
|
||||
} else {
|
||||
this.newArea.y = e.offsetY / this.$refs.mask.clientHeight
|
||||
drawArea.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)
|
||||
if ((this.drawField?.type || this.drawCustomField?.type || this.drawFieldType) === 'cells') {
|
||||
drawArea.cell_w = drawArea.h * (this.$refs.mask.clientHeight / this.$refs.mask.clientWidth)
|
||||
}
|
||||
|
||||
this.newArea.w = Math.abs(dx)
|
||||
this.newArea.h = Math.abs(dy)
|
||||
drawArea.w = Math.abs(dx)
|
||||
drawArea.h = Math.abs(dy)
|
||||
}
|
||||
},
|
||||
onPointerup (e) {
|
||||
@@ -601,29 +699,33 @@ export default {
|
||||
})
|
||||
|
||||
this.selectionRect = null
|
||||
} else if (this.newArea) {
|
||||
const area = {
|
||||
x: this.newArea.x,
|
||||
y: this.newArea.y,
|
||||
w: this.newArea.w,
|
||||
h: this.newArea.h,
|
||||
page: this.number
|
||||
} 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 !(
|
||||
|
||||
@@ -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,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>
|
||||
@@ -290,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) {
|
||||
|
||||
@@ -164,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]
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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,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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' %>
|
||||
|
||||
@@ -6,4 +6,4 @@
|
||||
<%= button_to nil, user_configs_path, method: :post, params: { user_config: { key: UserConfig::SHOW_APP_TOUR, value: true } }, class: 'hidden', id: 'start_tour_button' %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<template-builder class="grid" data-template="<%= @template_data %>" data-with-sign-yourself-button="<%= !@template.archived_at? %>" data-with-fields-detection="true" data-with-send-button="<%= !@template.archived_at? && can?(:create, @template.submissions.new(account: current_account)) %>" data-locale="<%= I18n.locale %>" data-show-tour-start-form="<%= @show_tour_start_form %>"></template-builder>
|
||||
<template-builder class="grid" data-template="<%= @template_data %>" data-custom-fields="<%= (current_account.account_configs.find_or_initialize_by(key: AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY).value || []).to_json %>" data-with-sign-yourself-button="<%= !@template.archived_at? %>" data-with-fields-detection="true" data-with-send-button="<%= !@template.archived_at? && can?(:create, @template.submissions.new(account: current_account)) %>" data-locale="<%= I18n.locale %>" data-show-tour-start-form="<%= @show_tour_start_form %>"></template-builder>
|
||||
|
||||
@@ -105,8 +105,8 @@
|
||||
</div>
|
||||
<div class="flex items-center space-x-1.5">
|
||||
<div class="join">
|
||||
<% if @pagy.prev %>
|
||||
<%= link_to '«', url_for(page: @pagy.prev, anchor: 'log'), class: 'join-item btn min-h-full h-10' %>
|
||||
<% if @pagy.previous %>
|
||||
<%= link_to '«', url_for(params: request.query_parameters.merge('page' => @pagy.previous), anchor: 'log'), 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 %>
|
||||
@@ -114,7 +114,7 @@
|
||||
<%= "Page #{@pagy.page}" %>
|
||||
</span>
|
||||
<% if @pagy.next %>
|
||||
<%= link_to '»', url_for(page: @pagy.next, anchor: 'log'), class: 'join-item btn min-h-full h-10' %>
|
||||
<%= link_to '»', url_for(params: request.query_parameters.merge('page' => @pagy.next), anchor: 'log'), 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 %>
|
||||
|
||||
@@ -17,7 +17,7 @@ Bundler.require(*Rails.groups)
|
||||
|
||||
module DocuSeal
|
||||
class Application < Rails::Application
|
||||
config.load_defaults 8.0
|
||||
config.load_defaults 8.1
|
||||
|
||||
config.autoload_lib(ignore: %w[assets tasks puma])
|
||||
|
||||
|
||||
+2
-3
@@ -30,9 +30,8 @@ production:
|
||||
<% elsif ENV['DATABASE_URL'].match?(/\Apostgres/) %>
|
||||
<<: *default
|
||||
url: <%= ENV['DATABASE_URL'] %>
|
||||
<% elsif ENV['DATABASE_URL'].match?(/\Amysql/) %>
|
||||
adapter: mysql2
|
||||
<% elsif ENV['DATABASE_URL'].match?(/\Amysql|\Atrilogy/) %>
|
||||
encoding: utf8mb4
|
||||
pool: <%= ENV.fetch('RAILS_MAX_THREADS', 15).to_i + ENV.fetch('SIDEKIQ_THREADS', 5).to_i %>
|
||||
url: <%= ENV['DATABASE_URL'] %>
|
||||
url: <%= ENV['DATABASE_URL'].sub(/\Amysql2?/, 'trilogy') %>
|
||||
<% end %>
|
||||
|
||||
+20
-4
@@ -60,12 +60,28 @@ if ENV['RAILS_ENV'] == 'production'
|
||||
ENV['DATABASE_URL'] = ENV['DATABASE_URL'].to_s.empty? ? database_url : ENV.fetch('DATABASE_URL', nil)
|
||||
end
|
||||
|
||||
unless Process.uid == 2000
|
||||
unless Process.euid == 2000
|
||||
begin
|
||||
Process::Sys.setgid(2000)
|
||||
Process::Sys.setuid(2000)
|
||||
test_file = "#{ENV.fetch('WORKDIR', '.')}/test"
|
||||
|
||||
orig_euid = Process.euid
|
||||
orig_egid = Process.egid
|
||||
|
||||
Process::Sys.setegid(2000)
|
||||
Process::Sys.seteuid(2000)
|
||||
|
||||
File.open(test_file, 'w') { true }
|
||||
rescue StandardError
|
||||
puts 'Unable to run as 2000:2000'
|
||||
Process::Sys.seteuid(orig_euid)
|
||||
Process::Sys.setegid(orig_egid)
|
||||
|
||||
puts "Unable to run as 2000:2000, running as #{orig_euid}:#{orig_egid}"
|
||||
ensure
|
||||
begin
|
||||
File.unlink(test_file)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -85,8 +85,9 @@ Rails.application.configure do
|
||||
domain: ENV.fetch('SMTP_DOMAIN', nil),
|
||||
user_name: ENV.fetch('SMTP_USERNAME', nil),
|
||||
password: ENV.fetch('SMTP_PASSWORD', nil),
|
||||
openssl_verify_mode: ENV['SMTP_SSL_VERIFY'] == 'false' ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER,
|
||||
authentication: ENV.fetch('SMTP_PASSWORD', nil).present? ? ENV.fetch('SMTP_AUTHENTICATION', 'plain') : nil,
|
||||
enable_starttls_auto: ENV['SMTP_ENABLE_STARTTLS_AUTO'] != 'false'
|
||||
enable_starttls: ENV['SMTP_ENABLE_STARTTLS'] != 'false'
|
||||
}.compact
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
autoload :CSV, 'csv'
|
||||
autoload :CSVSafe, 'csv-safe'
|
||||
autoload :RubyXL, 'rubyXL'
|
||||
autoload :Zip, 'zip'
|
||||
autoload :Numo, 'numo/narray'
|
||||
autoload :OnnxRuntime, 'onnxruntime'
|
||||
autoload :RQRCode, 'rqrcode'
|
||||
autoload :ArabicLetterConnector, 'arabic-letter-connector/logic'
|
||||
autoload :JWT, 'jwt'
|
||||
@@ -1,3 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'csv'
|
||||
@@ -1,10 +1,3 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'pagy/extras/countless'
|
||||
|
||||
Pagy::DEFAULT[:limit] = 10
|
||||
Pagy::DEFAULT.freeze
|
||||
|
||||
ActiveSupport.on_load(:action_view) do
|
||||
include Pagy::Frontend
|
||||
end
|
||||
Pagy.options[:limit] = 10
|
||||
|
||||
@@ -29,6 +29,8 @@ en: &en
|
||||
pro: Pro
|
||||
thanks: Thanks
|
||||
private: Private
|
||||
make_all_newly_created_templates_private_to_their_creator_and_admins_by_default: Make all newly created templates private to their creator and admins by default.
|
||||
create_templates_with_admin_access_by_default: Create templates with admin access by default
|
||||
require_email_2fa: Require email 2FA
|
||||
when_checked_each_signer_must_verify_their_email_with_a_one_time_code_before_accessing_the_document: When checked, each signer must verify their email with a one-time code before accessing the document.
|
||||
require_phone_2fa: Require phone 2FA
|
||||
@@ -1029,6 +1031,8 @@ es: &es
|
||||
stripe_account_has_been_connected: La cuenta de Stripe ha sido conectada.
|
||||
re_connect_stripe: Volver a conectar Stripe
|
||||
private: Privado
|
||||
make_all_newly_created_templates_private_to_their_creator_and_admins_by_default: Hacer que todas las plantillas recién creadas sean privadas para su creador y los administradores por defecto.
|
||||
create_templates_with_admin_access_by_default: Crear plantillas con acceso de administrador por defecto
|
||||
require_email_2fa: Requerir 2FA por correo electrónico
|
||||
when_checked_each_signer_must_verify_their_email_with_a_one_time_code_before_accessing_the_document: Cuando está marcado, cada firmante debe verificar su correo electrónico con un código de un solo uso antes de acceder al documento.
|
||||
require_phone_2fa: Requerir 2FA por teléfono
|
||||
@@ -2011,6 +2015,8 @@ it: &it
|
||||
stripe_account_has_been_connected: L'account Stripe è stato collegato.
|
||||
re_connect_stripe: Ricollega Stripe
|
||||
private: Privato
|
||||
make_all_newly_created_templates_private_to_their_creator_and_admins_by_default: Rendere tutte le nuove template private per il creatore e gli amministratori per impostazione predefinita.
|
||||
create_templates_with_admin_access_by_default: Crea modelli con accesso amministratore per impostazione predefinita
|
||||
require_email_2fa: Richiedi 2FA email
|
||||
when_checked_each_signer_must_verify_their_email_with_a_one_time_code_before_accessing_the_document: Quando selezionato, ogni firmatario deve verificare la propria email con un codice monouso prima di accedere al documento.
|
||||
require_phone_2fa: Richiedi 2FA telefono
|
||||
@@ -2994,6 +3000,8 @@ fr: &fr
|
||||
stripe_account_has_been_connected: Le compte Stripe a été connecté.
|
||||
re_connect_stripe: Reconnecter Stripe
|
||||
private: Privé
|
||||
make_all_newly_created_templates_private_to_their_creator_and_admins_by_default: Rendre tous les nouveaux modèles privés pour leur créateur et les administrateurs par défaut.
|
||||
create_templates_with_admin_access_by_default: Créer des modèles avec un accès administrateur par défaut
|
||||
require_email_2fa: Exiger la 2FA par email
|
||||
when_checked_each_signer_must_verify_their_email_with_a_one_time_code_before_accessing_the_document: Lorsque coché, chaque signataire doit vérifier son email avec un code à usage unique avant d'accéder au document.
|
||||
require_phone_2fa: Exiger la 2FA par téléphone
|
||||
@@ -3973,6 +3981,8 @@ pt: &pt
|
||||
stripe_account_has_been_connected: Conta Stripe foi conectada.
|
||||
re_connect_stripe: Reconectar Stripe
|
||||
private: Privado
|
||||
make_all_newly_created_templates_private_to_their_creator_and_admins_by_default: Tornar todos os modelos recém-criados privados para seu criador e administradores por padrão.
|
||||
create_templates_with_admin_access_by_default: Criar modelos com acesso de administrador por padrão
|
||||
require_email_2fa: Exigir 2FA por email
|
||||
when_checked_each_signer_must_verify_their_email_with_a_one_time_code_before_accessing_the_document: Quando marcado, cada signatário deve verificar seu email com um código de uso único antes de acessar o documento.
|
||||
require_phone_2fa: Exigir 2FA por telefone
|
||||
@@ -4941,6 +4951,8 @@ de: &de
|
||||
pro: Pro
|
||||
thanks: Danke
|
||||
private: Privat
|
||||
make_all_newly_created_templates_private_to_their_creator_and_admins_by_default: Alle neu erstellten Vorlagen standardmäßig nur für ihren Ersteller und Administratoren sichtbar machen.
|
||||
create_templates_with_admin_access_by_default: Vorlagen standardmäßig mit Administratorzugriff erstellen
|
||||
require_email_2fa: E-Mail 2FA erforderlich
|
||||
when_checked_each_signer_must_verify_their_email_with_a_one_time_code_before_accessing_the_document: Wenn aktiviert, muss jeder Unterzeichner seine E-Mail mit einem einmaligen Code verifizieren, bevor er auf das Dokument zugreifen kann.
|
||||
require_phone_2fa: Telefon 2FA erforderlich
|
||||
@@ -6311,6 +6323,8 @@ nl: &nl
|
||||
pro: Pro
|
||||
thanks: Bedankt
|
||||
private: Privé
|
||||
make_all_newly_created_templates_private_to_their_creator_and_admins_by_default: Maak alle nieuw aangemaakte sjablonen standaard privé voor hun maker en admins.
|
||||
create_templates_with_admin_access_by_default: Sjablonen standaard met admin-toegang maken
|
||||
require_email_2fa: E-mail 2FA vereist
|
||||
when_checked_each_signer_must_verify_their_email_with_a_one_time_code_before_accessing_the_document: Wanneer aangevinkt, moet elke ondertekenaar zijn e-mail verifiëren met een eenmalige code voordat hij toegang krijgt tot het document.
|
||||
require_phone_2fa: Telefoon 2FA vereist
|
||||
|
||||
@@ -53,6 +53,7 @@ Rails.application.routes.draw do
|
||||
resources :verify_pdf_signature, only: %i[create]
|
||||
resource :mfa_setup, only: %i[show new edit create destroy], controller: 'mfa_setup'
|
||||
resources :account_configs, only: %i[create destroy]
|
||||
resources :account_custom_fields, only: %i[create]
|
||||
resources :user_configs, only: %i[create]
|
||||
resources :encrypted_user_configs, only: %i[destroy]
|
||||
resources :timestamp_server, only: %i[create]
|
||||
|
||||
@@ -8,7 +8,7 @@ default: &default
|
||||
shakapacker_precompile: true
|
||||
webpack_compile_output: true
|
||||
additional_paths: []
|
||||
webpack_loader: 'babel'
|
||||
javascript_transpiler: 'babel'
|
||||
compiler_strategy: digest
|
||||
cache_manifest: false
|
||||
ensure_consistent_versioning: false
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ google:
|
||||
cache_control: "public, max-age=31536000"
|
||||
|
||||
azure:
|
||||
service: AzureStorage
|
||||
service: AzureBlob
|
||||
storage_account_name: <%= ENV['AZURE_STORAGE_ACCOUNT_NAME'] %>
|
||||
storage_access_key: <%= ENV['AZURE_STORAGE_ACCESS_KEY'] %>
|
||||
container: <%= ENV['AZURE_CONTAINER'] %>
|
||||
|
||||
@@ -45,19 +45,25 @@ module ActionMailerConfigsInterceptor
|
||||
def build_smtp_configs_hash(email_configs)
|
||||
value = email_configs.value
|
||||
|
||||
is_tls = value['security'] == 'tls' || (value['security'].blank? && value['port'].to_s == '465')
|
||||
is_ssl = value['security'] == 'ssl'
|
||||
is_noverify = value['security'] == 'noverify'
|
||||
|
||||
enable_starttls = is_noverify ? :enable_starttls_auto : :enable_starttls
|
||||
|
||||
{
|
||||
user_name: value['username'],
|
||||
password: value['password'],
|
||||
address: value['host'],
|
||||
port: value['port'],
|
||||
domain: value['domain'],
|
||||
openssl_verify_mode: value['security'] == 'noverify' ? OpenSSL::SSL::VERIFY_NONE : nil,
|
||||
openssl_verify_mode: is_noverify ? OpenSSL::SSL::VERIFY_NONE : nil,
|
||||
authentication: value['password'].present? ? value.fetch('authentication', 'plain') : nil,
|
||||
enable_starttls_auto: value['security'] != 'tls',
|
||||
enable_starttls => !is_tls && !is_ssl,
|
||||
open_timeout: OPEN_TIMEOUT,
|
||||
read_timeout: READ_TIMEOUT,
|
||||
ssl: value['security'] == 'ssl',
|
||||
tls: value['security'] == 'tls' || (value['security'].blank? && value['port'].to_s == '465')
|
||||
ssl: is_ssl,
|
||||
tls: is_tls
|
||||
}.compact_blank
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,9 +6,16 @@ class NormalizeClientIpMiddleware
|
||||
end
|
||||
|
||||
def call(env)
|
||||
if env['HTTP_CLIENT_IP'].present? && env['HTTP_X_CLIENT_IP'].present? &&
|
||||
env['HTTP_CLIENT_IP'].starts_with?("#{env['HTTP_X_CLIENT_IP']}:")
|
||||
env['HTTP_CLIENT_IP'] = env['HTTP_X_CLIENT_IP']
|
||||
if env['HTTP_CLIENT_IP'].present?
|
||||
if env['HTTP_X_CLIENT_IP'].present? &&
|
||||
env['HTTP_CLIENT_IP'].starts_with?("#{env['HTTP_X_CLIENT_IP']}:")
|
||||
env['HTTP_CLIENT_IP'] = env['HTTP_X_CLIENT_IP']
|
||||
end
|
||||
|
||||
if env['HTTP_X_FORWARDED_FOR'].present? &&
|
||||
env['HTTP_X_FORWARDED_FOR'].sub(/:\d+\z/, '') == env['HTTP_CLIENT_IP']
|
||||
env['HTTP_X_FORWARDED_FOR'] = env['HTTP_CLIENT_IP']
|
||||
end
|
||||
end
|
||||
|
||||
@app.call(env)
|
||||
|
||||
+1
-3
@@ -364,9 +364,7 @@ class Pdfium
|
||||
@closed
|
||||
end
|
||||
|
||||
def form_handle
|
||||
@document.form_handle
|
||||
end
|
||||
delegate :form_handle, to: :@document
|
||||
|
||||
def ensure_not_closed!
|
||||
raise PdfiumError, 'Page is closed.' if closed?
|
||||
|
||||
@@ -9,7 +9,7 @@ Puma::Plugin.create do
|
||||
|
||||
@puma_pid = $PROCESS_ID
|
||||
|
||||
launcher.events.on_booted do
|
||||
launcher.events.after_booted do
|
||||
@redis_server_pid = fork_redis
|
||||
end
|
||||
|
||||
@@ -19,8 +19,8 @@ Puma::Plugin.create do
|
||||
stop_redis_server if Process.pid == @puma_pid
|
||||
end
|
||||
|
||||
launcher.events.on_stopped { stop_redis_server }
|
||||
launcher.events.on_restart { stop_redis_server }
|
||||
launcher.events.after_stopped { stop_redis_server }
|
||||
launcher.events.before_restart { stop_redis_server }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -14,14 +14,14 @@ Puma::Plugin.create do
|
||||
end
|
||||
|
||||
def start(launcher)
|
||||
launcher.events.on_booted do
|
||||
launcher.events.after_booted do
|
||||
next if Puma.stats_hash[:workers].to_i != 0
|
||||
|
||||
start_sidekiq!
|
||||
end
|
||||
|
||||
launcher.events.on_stopped { Thread.new { @sidekiq&.stop }.join }
|
||||
launcher.events.on_restart { Thread.new { @sidekiq&.stop }.join }
|
||||
launcher.events.after_stopped { Thread.new { @sidekiq&.stop }.join }
|
||||
launcher.events.before_restart { Thread.new { @sidekiq&.stop }.join }
|
||||
end
|
||||
|
||||
def fire_event(config, event)
|
||||
|
||||
@@ -61,7 +61,8 @@ module Submissions
|
||||
|
||||
submission.template_submitters << template_submitter
|
||||
|
||||
is_order_sent = submitters_order == 'random' || (template_submitter['order'] || index).zero?
|
||||
is_order_sent = submitters_order == 'random' ||
|
||||
(template_submitter['order'] || submitter_attrs[:index] || index).zero?
|
||||
|
||||
build_submitter(submission:, attrs: submitter_attrs,
|
||||
uuid:, is_order_sent:, user:, params:,
|
||||
@@ -115,6 +116,8 @@ module Submissions
|
||||
next if item['invite_by_uuid'].blank? && item['optional_invite_by_uuid'].blank?
|
||||
next if submission.template_submitters.any? { |e| e['uuid'] == item['uuid'] }
|
||||
|
||||
item = item.merge('order' => submitter_attr['order']) if submitter_attr && submitter_attr['order'].present?
|
||||
|
||||
if index.zero?
|
||||
submission.template_submitters.insert(1, item)
|
||||
elsif submission.template_submitters.size > index
|
||||
@@ -306,7 +309,7 @@ module Submissions
|
||||
uuid = attrs[:uuid].presence
|
||||
uuid ||= submitters.find { |e| e['name'].to_s.casecmp(attrs[:role].to_s).zero? }&.dig('uuid')
|
||||
|
||||
uuid || submitters[index]&.dig('uuid')
|
||||
uuid || submitters[attrs[:index] || index]&.dig('uuid')
|
||||
end
|
||||
|
||||
def build_submitter(submission:, attrs:, uuid:, is_order_sent:, user:, preferences:, params:)
|
||||
|
||||
@@ -19,7 +19,7 @@ module Submissions
|
||||
total_wait_time ||= 0
|
||||
key = ['result_attachments', submitter.id].join(':')
|
||||
|
||||
return submitter.documents if ApplicationRecord.uncached { LockEvent.exists?(key:, event_name: :complete) }
|
||||
return submitter.documents.reload if ApplicationRecord.uncached { LockEvent.exists?(key:, event_name: :complete) }
|
||||
|
||||
events = ApplicationRecord.uncached { LockEvent.where(key:).order(:id).to_a }
|
||||
|
||||
@@ -32,6 +32,8 @@ module Submissions
|
||||
|
||||
LockEvent.create!(key:, event_name: :complete)
|
||||
|
||||
submitter.documents.reset
|
||||
|
||||
documents
|
||||
end
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
|
||||
@@ -116,6 +116,7 @@ module Submissions
|
||||
configs = submission.account.account_configs.where(key: [AccountConfig::WITH_AUDIT_VALUES_KEY,
|
||||
AccountConfig::WITH_SIGNATURE_ID,
|
||||
AccountConfig::WITH_FILE_LINKS_KEY,
|
||||
AccountConfig::WITH_AUDIT_SENDER_KEY,
|
||||
AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY])
|
||||
|
||||
last_submitter = submission.submitters.select(&:completed_at).max_by(&:completed_at)
|
||||
@@ -123,6 +124,7 @@ module Submissions
|
||||
with_signature_id = configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true
|
||||
with_file_links = configs.find { |c| c.key == AccountConfig::WITH_FILE_LINKS_KEY }&.value == true
|
||||
with_audit_values = configs.find { |c| c.key == AccountConfig::WITH_AUDIT_VALUES_KEY }&.value != false
|
||||
with_audit_sender = configs.find { |c| c.key == AccountConfig::WITH_AUDIT_SENDER_KEY }&.value == true
|
||||
with_submitter_timezone = configs.find { |c| c.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true
|
||||
|
||||
timezone = account.timezone
|
||||
@@ -464,6 +466,11 @@ module Submissions
|
||||
name].join(' ')
|
||||
I18n.t('submission_event_names.invite_party_by_html', invited_submitter_name:,
|
||||
submitter_name:)
|
||||
elsif with_audit_sender && (event.event_type == 'send_email' || event.event_type == 'send_sms')
|
||||
[
|
||||
I18n.t("submission_event_names.#{event.event_type}_to_html", submitter_name:),
|
||||
"<b>#{I18n.t(:from)}</b> #{submission.created_by_user.full_name} #{submission.created_by_user.email}"
|
||||
].join("\n")
|
||||
elsif event.event_type.include?('send_')
|
||||
I18n.t("submission_event_names.#{event.event_type}_to_html", submitter_name:)
|
||||
else
|
||||
@@ -472,11 +479,20 @@ module Submissions
|
||||
|
||||
bold_text, normal_text = text.match(%r{<b>(.*?)</b>(.*)}).captures
|
||||
|
||||
text_box = [{ text: bold_text, font: [FONT_NAME, { variant: :bold }] }, normal_text]
|
||||
|
||||
if text.include?("\n")
|
||||
text_box = text.split("\n")[1..].reduce(text_box) do |acc, row|
|
||||
bold_text, normal_text = row.match(%r{<b>(.*?)</b>(.*)}).captures
|
||||
|
||||
[*acc, "\n", { text: bold_text, font: [FONT_NAME, { variant: :bold }] }, normal_text]
|
||||
end
|
||||
end
|
||||
|
||||
[
|
||||
"#{I18n.l(event.event_timestamp.in_time_zone(timezone), format: :long, locale: account.locale)} " \
|
||||
"#{TimeUtils.timezone_abbr(timezone, event.event_timestamp)}",
|
||||
composer.document.layout.formatted_text_box([{ text: bold_text, font: [FONT_NAME, { variant: :bold }] },
|
||||
normal_text])
|
||||
composer.document.layout.formatted_text_box(text_box)
|
||||
]
|
||||
end
|
||||
|
||||
|
||||
@@ -200,7 +200,9 @@ module Submissions
|
||||
|
||||
def fill_submitter_fields(submitter, account, pdfs_index, with_signature_id:, is_flatten:, with_headings: nil,
|
||||
with_submitter_timezone: false, with_signature_id_reason: true, with_file_links: nil)
|
||||
cell_layouter = HexaPDF::Layout::TextLayouter.new(text_valign: :center, text_align: :center)
|
||||
cell_layouters = Hash.new do |hash, valign|
|
||||
hash[valign] = HexaPDF::Layout::TextLayouter.new(text_valign: valign.to_sym, text_align: :center)
|
||||
end
|
||||
|
||||
attachments_data_cache = {}
|
||||
|
||||
@@ -238,7 +240,7 @@ module Submissions
|
||||
width = page.box.width
|
||||
height = page.box.height
|
||||
|
||||
preferences_font_size = field.dig('preferences', 'font_size').then { |num| num.present? ? num.to_i : nil }
|
||||
preferences_font_size = field.dig('preferences', 'font_size').then { |num| num.presence&.to_i }
|
||||
|
||||
font_size = preferences_font_size
|
||||
font_size ||= (([page.box.width, page.box.height].min / A4_SIZE[0].to_f) * FONT_SIZE).to_i
|
||||
@@ -550,6 +552,8 @@ module Submissions
|
||||
)
|
||||
when ->(type) { type == 'cells' && !area['cell_w'].to_f.zero? }
|
||||
cell_width = area['cell_w'] * width
|
||||
cell_valign = field.dig('preferences', 'valign').to_s.presence || 'center'
|
||||
cell_layouter = cell_layouters[cell_valign]
|
||||
|
||||
if (mask = field.dig('preferences', 'mask').presence)
|
||||
value = TextUtils.mask_value(value, mask)
|
||||
@@ -790,10 +794,10 @@ module Submissions
|
||||
def build_pdfs_index(submission, submitter: nil, flatten: true)
|
||||
latest_submitter = find_last_submitter(submission, submitter:)
|
||||
|
||||
Submissions::EnsureResultGenerated.call(latest_submitter) if latest_submitter
|
||||
documents = Submissions::EnsureResultGenerated.call(latest_submitter) if latest_submitter
|
||||
documents ||= submission.schema_documents
|
||||
|
||||
documents = latest_submitter&.documents&.preload(:blob).to_a.presence
|
||||
documents ||= submission.schema_documents.preload(:blob)
|
||||
ActiveRecord::Associations::Preloader.new(records: documents, associations: [:blob]).call
|
||||
|
||||
attachment_uuids = Submissions.filtered_conditions_schema(submission).pluck('attachment_uuid')
|
||||
attachments_index = documents.index_by { |a| a.metadata['original_uuid'] || a.uuid }
|
||||
|
||||
@@ -10,7 +10,8 @@ module Submissions
|
||||
|
||||
Array.wrap(submissions_params).each do |submission|
|
||||
submission[:submitters].each_with_index do |submitter, index|
|
||||
_, new_attachments, new_fields = normalize_submitter_params!(submitter, template, index, add_fields:)
|
||||
_, new_attachments, new_fields =
|
||||
normalize_submitter_params!(submitter, template, submitter[:index] || index, add_fields:)
|
||||
|
||||
attachments.push(*new_attachments)
|
||||
fields.push(*new_fields)
|
||||
|
||||
@@ -14,6 +14,7 @@ module Submissions
|
||||
|
||||
module_function
|
||||
|
||||
# rubocop:disable Metrics
|
||||
def call(submission, submitters = nil, params = {}, with_events: true, with_documents: true, with_values: true,
|
||||
expires_at: Accounts.link_expires_at(Account.new(id: submission.account_id)))
|
||||
submitters ||= submission.submitters.preload(documents_attachments: :blob, attachments_attachments: :blob)
|
||||
@@ -32,6 +33,10 @@ module Submissions
|
||||
json['submission_events'] = Submitters::SerializeForApi.serialize_events(submission.submission_events)
|
||||
end
|
||||
|
||||
if params[:include].to_s.include?('fields')
|
||||
json['fields'] = submission.template_fields || submission.template&.fields
|
||||
end
|
||||
|
||||
if submitters.all?(&:completed_at?)
|
||||
last_submitter = submitters.max_by(&:completed_at)
|
||||
|
||||
@@ -57,6 +62,7 @@ module Submissions
|
||||
|
||||
json
|
||||
end
|
||||
# rubocop:enable Metrics
|
||||
|
||||
def build_status(submission, submitters)
|
||||
if submitters.any?(&:declined_at?)
|
||||
|
||||
@@ -35,7 +35,7 @@ module Submissions
|
||||
conn = Faraday.new(uri.origin) do |c|
|
||||
c.options.read_timeout = TIMEOUT
|
||||
c.options.open_timeout = TIMEOUT
|
||||
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.request_uri, build_payload(digest.digest),
|
||||
|
||||
@@ -55,7 +55,7 @@ module Submitters
|
||||
def serialize_events(events)
|
||||
events.map do |event|
|
||||
event.as_json(only: %i[id submitter_id event_type event_timestamp])
|
||||
.merge('data' => event.data.slice('reason', 'firstname', 'lastname', 'method', 'country'))
|
||||
.merge('data' => event.data.slice('reason', 'firstname', 'lastname', 'method', 'country', 'idcode'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -48,6 +48,10 @@ module Submitters
|
||||
maybe_set_signature_reason!(values, submitter, params)
|
||||
validate_values!(values, submitter, params, request)
|
||||
|
||||
if (touch_attachment_uuid = params[:touch_attachment_uuid].presence)
|
||||
ActiveStorage::Attachment.where(uuid: touch_attachment_uuid, record: submitter).touch_all(:created_at)
|
||||
end
|
||||
|
||||
SubmissionEvents.create_with_tracking_data(submitter, 'complete_form', request) if params[:completed] == 'true'
|
||||
|
||||
submitter.save!
|
||||
@@ -317,17 +321,33 @@ module Submitters
|
||||
end.exclude?(false)
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics
|
||||
def check_field_condition(condition, submitter_values, fields_uuid_index)
|
||||
value = submitter_values[condition['field_uuid']]
|
||||
field = fields_uuid_index[condition['field_uuid']]
|
||||
|
||||
case condition['action']
|
||||
when 'empty', 'unchecked'
|
||||
value.blank?
|
||||
when 'not_empty', 'checked'
|
||||
value.present?
|
||||
when 'equal', 'contains'
|
||||
field = fields_uuid_index[condition['field_uuid']]
|
||||
when ->(action) { action == 'equal' && field&.dig('type') == 'number' }
|
||||
return false if value.blank? || condition['value'].blank?
|
||||
|
||||
(value.to_f - condition['value'].to_f).abs < Float::EPSILON
|
||||
when ->(action) { action == 'not_equal' && field&.dig('type') == 'number' }
|
||||
return false if value.blank? || condition['value'].blank?
|
||||
|
||||
(value.to_f - condition['value'].to_f).abs > Float::EPSILON
|
||||
when 'greater_than'
|
||||
return false if field.nil? || value.blank? || condition['value'].blank?
|
||||
|
||||
value.to_f > condition['value'].to_f
|
||||
when 'less_than'
|
||||
return false if field.nil? || value.blank? || condition['value'].blank?
|
||||
|
||||
value.to_f < condition['value'].to_f
|
||||
when 'equal', 'contains'
|
||||
return true unless field
|
||||
|
||||
values = Array.wrap(value)
|
||||
@@ -340,8 +360,6 @@ module Submitters
|
||||
|
||||
values.include?(option['value'].presence || "#{I18n.t('option')} #{field['options'].index(option) + 1}")
|
||||
when 'not_equal', 'does_not_contain'
|
||||
field = fields_uuid_index[condition['field_uuid']]
|
||||
|
||||
return true unless field
|
||||
return false unless field['options']
|
||||
|
||||
@@ -356,6 +374,7 @@ module Submitters
|
||||
true
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics
|
||||
|
||||
def replace_default_variables(value, attrs, submission, with_time: false)
|
||||
return value if value.in?([true, false]) || value.is_a?(Numeric) || value.is_a?(Array)
|
||||
|
||||
+13
-11
@@ -22,34 +22,36 @@
|
||||
"canvas-confetti": "^1.6.0",
|
||||
"chart.js": "^4.5.1",
|
||||
"codemirror": "^6.0.2",
|
||||
"compression-webpack-plugin": "10.0.0",
|
||||
"css-loader": "^6.7.3",
|
||||
"compression-webpack-plugin": "11.1.0",
|
||||
"css-loader": "^7.1.2",
|
||||
"css-minimizer-webpack-plugin": "^5.0.0",
|
||||
"daisyui": "^3.9.4",
|
||||
"driver.js": "^1.3.5",
|
||||
"mathjs": "^12.4.0",
|
||||
"mini-css-extract-plugin": "^2.7.5",
|
||||
"mini-css-extract-plugin": "^2.10.0",
|
||||
"postcss": "^8.4.31",
|
||||
"postcss-import": "^15.1.0",
|
||||
"postcss-loader": "^7.3.0",
|
||||
"qr-creator": "^1.0.0",
|
||||
"rollbar": "^2.26.4",
|
||||
"sass": "^1.62.1",
|
||||
"sass-loader": "^13.2.2",
|
||||
"shakapacker": "8.0.0",
|
||||
"sass-loader": "^16.0.6",
|
||||
"shakapacker": "9.5.0",
|
||||
"signature_pad": "^4.1.5",
|
||||
"snarkdown": "^2.0.0",
|
||||
"style-loader": "^4.0.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"terser-webpack-plugin": "5.3.8",
|
||||
"terser-webpack-plugin": "5.3.16",
|
||||
"uuid": "^9.0.0",
|
||||
"vue": "^3.3.2",
|
||||
"vue-loader": "^17.1.1",
|
||||
"webpack": "5.94.0",
|
||||
"webpack-assets-manifest": "5",
|
||||
"webpack": "5.104.1",
|
||||
"webpack-assets-manifest": "6.5.0",
|
||||
"webpack-bundle-analyzer": "^4.7.0",
|
||||
"webpack-cli": "5.1.1",
|
||||
"webpack-dev-server": "^4.15.0",
|
||||
"webpack-merge": "5"
|
||||
"webpack-cli": "6.0.1",
|
||||
"webpack-dev-server": "^5.2.3",
|
||||
"webpack-merge": "6.0.1",
|
||||
"webpack-subresource-integrity": "^5.1.0"
|
||||
},
|
||||
"packageManager": "yarn@1.0",
|
||||
"version": "0.1.0",
|
||||
|
||||
Reference in New Issue
Block a user