mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 14:42:32 -05:00
Compare 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 | ||
|
|
b3e72f0726 | ||
|
|
700ebc4727 | ||
|
|
745a18efca | ||
|
|
b4a8147ff0 | ||
|
|
0543c25054 | ||
|
|
b1a0afea66 | ||
|
|
43c41e2557 | ||
|
|
bc8eb33f05 | ||
|
|
d1cebc92f7 | ||
|
|
bf02cf4058 | ||
|
|
5212773d0a | ||
|
|
bf53204a41 | ||
|
|
092a19966c | ||
|
|
6060a2b798 | ||
|
|
ce12af68b2 | ||
|
|
c05f8d47aa | ||
|
|
f8be19c20e | ||
|
|
2fe32a2145 | ||
|
|
794ea4d0e8 | ||
|
|
307b0f5afd | ||
|
|
9011e3f4b4 | ||
|
|
0b45ce5c0c | ||
|
|
a8ab93a4cc | ||
|
|
d1768be636 | ||
|
|
b2cd7c82a7 | ||
|
|
b88d99afe0 | ||
|
|
4523d38551 | ||
|
|
c4ba892559 | ||
|
|
40f8093e00 | ||
|
|
4375459856 | ||
|
|
b3eb8a70bf | ||
|
|
6245a487b6 | ||
|
|
a89f4c1b10 | ||
|
|
7df729b8b5 | ||
|
|
f1db7c7e82 | ||
|
|
00af69cbf2 | ||
|
|
c6819a19bd | ||
|
|
f5cbcb3d24 | ||
|
|
6ca75c53ef | ||
|
|
e0ec443e10 | ||
|
|
9eed2fc2ef | ||
|
|
b9c6d3ff9f | ||
|
|
03c25a89b2 | ||
|
|
f5b4a0c5ab | ||
|
|
b10ed46ccc | ||
|
|
c55036a1b1 | ||
|
|
962a5dade5 | ||
|
|
369a09a6df | ||
|
|
e9a39e2571 | ||
|
|
1cf9cc18f1 | ||
|
|
1984d4ec8d | ||
|
|
fa99b4ebdc | ||
|
|
83c793a2a5 | ||
|
|
d078727070 | ||
|
|
0d8e2c5ff0 | ||
|
|
ea062d7494 | ||
|
|
d525e0597a | ||
|
|
964b5d4e74 | ||
|
|
abf17eb09f | ||
|
|
178809dad7 | ||
|
|
881d189715 | ||
|
|
48a8973910 | ||
|
|
ae891932c5 | ||
|
|
a8b5e00814 | ||
|
|
e98432d7e4 | ||
|
|
29ff58ed98 | ||
|
|
a621575ffc | ||
|
|
d7c7efc203 | ||
|
|
4f013ca927 | ||
|
|
d9823feef7 | ||
|
|
676d775544 | ||
|
|
840ad17258 | ||
|
|
ce0cd09b70 | ||
|
|
bb752fbc00 | ||
|
|
c71fb04404 | ||
|
|
e856cf8787 | ||
|
|
71aae65bc6 | ||
|
|
7a33444f9b | ||
|
|
e12ec9d986 | ||
|
|
94d1fb7dcb | ||
|
|
6e43a42274 | ||
|
|
0e0aa0259e | ||
|
|
75ab6d39ef | ||
|
|
6f84d84f96 | ||
|
|
cf46593e9b | ||
|
|
1808346348 | ||
|
|
ea59a68499 | ||
|
|
cba2d02990 | ||
|
|
e92fc0d3e1 | ||
|
|
f1eaa77092 | ||
|
|
5934bdec55 | ||
|
|
7a7d3c6ee3 | ||
|
|
cb2f58c04c | ||
|
|
a1c117f1ca | ||
|
|
1aacd98460 | ||
|
|
840162c9ca | ||
|
|
3519b0e009 | ||
|
|
53fae56c51 | ||
|
|
f4552dbaa8 | ||
|
|
dfab4c331c | ||
|
|
d2a0937038 | ||
|
|
fa1ef44b22 | ||
|
|
c1a5c91299 | ||
|
|
e6280e8f5a | ||
|
|
fd2ba57325 | ||
|
|
dc178ef03f | ||
|
|
68936e10c5 | ||
|
|
41f16e7d39 | ||
|
|
453ebbf90a | ||
|
|
7ca290c080 | ||
|
|
dd5b6a6aa6 | ||
|
|
bf2ebb995b | ||
|
|
3e929758fb | ||
|
|
4a53116586 | ||
|
|
2d1981acfa | ||
|
|
c610df269f | ||
|
|
96dac635f5 | ||
|
|
abd579103d | ||
|
|
7ed27bb413 | ||
|
|
180c542a9f | ||
|
|
89613113de | ||
|
|
3102900776 | ||
|
|
1a68053f4e | ||
|
|
5d4a032763 | ||
|
|
7481d27822 | ||
|
|
22afd01fd2 | ||
|
|
fd9eb09dd6 | ||
|
|
c53ed7f984 | ||
|
|
1d87e210be | ||
|
|
5fec952b8c | ||
|
|
c4fbcc103a | ||
|
|
9333a1ba45 | ||
|
|
de5cd92dba | ||
|
|
83e32bec9f | ||
|
|
4603efc04e | ||
|
|
6c73c82a27 | ||
|
|
8e71d9f9e9 | ||
|
|
80f60a5d88 | ||
|
|
2c33ec382d | ||
|
|
884865cffc | ||
|
|
9537fe8c91 | ||
|
|
c8fe36a2b6 | ||
|
|
7ec3282b56 | ||
|
|
ab8c41a520 | ||
|
|
4c1c70ff04 | ||
|
|
d7a6e80bb1 | ||
|
|
f87ef670d1 | ||
|
|
daba2505bc | ||
|
|
71528eda45 | ||
|
|
269e2beeaa | ||
|
|
72dd2c6bc5 | ||
|
|
9758f7a15f | ||
|
|
2521eb5a4a | ||
|
|
bf6afc5e61 | ||
|
|
291d869843 | ||
|
|
cdfecb2a8f | ||
|
|
c0df8e0ec3 | ||
|
|
a21f7d1653 | ||
|
|
79e2514416 | ||
|
|
80a7385183 | ||
|
|
1c8a7b6a7c | ||
|
|
b46ced2b6f | ||
|
|
5ae479644e | ||
|
|
cb66eb1293 | ||
|
|
b4d5a2cad4 | ||
|
|
32691b0c0d | ||
|
|
9163f744a3 | ||
|
|
23ecce51a3 | ||
|
|
747ffa8cf1 |
No files matched your search
@@ -12,7 +12,7 @@ jobs:
|
|||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.4.2
|
ruby-version: 4.0.1
|
||||||
- name: Cache gems
|
- name: Cache gems
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.4.2
|
ruby-version: 4.0.1
|
||||||
- name: Cache gems
|
- name: Cache gems
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 20.9.0
|
node-version: 20.19.0
|
||||||
- name: Cache directory path
|
- name: Cache directory path
|
||||||
id: yarn-cache-dir-path
|
id: yarn-cache-dir-path
|
||||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.4.2
|
ruby-version: 4.0.1
|
||||||
- name: Cache gems
|
- name: Cache gems
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@@ -132,11 +132,11 @@ jobs:
|
|||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.4.2
|
ruby-version: 4.0.1
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 20.9.0
|
node-version: 20.19.0
|
||||||
- name: Install Chrome
|
- name: Install Chrome
|
||||||
uses: browser-actions/setup-chrome@latest
|
uses: browser-actions/setup-chrome@latest
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -1,27 +1,14 @@
|
|||||||
name: Build Docker Images
|
name: Build Docker Images
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
push:
|
||||||
inputs:
|
tags:
|
||||||
version:
|
- "*.*.*"
|
||||||
description: Version
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
image:
|
|
||||||
description: QEMU image
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
default: tonistiigi/binfmt:latest
|
|
||||||
os:
|
|
||||||
description: OS
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
default: ubuntu-24.04-arm
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ${{ inputs.os }}
|
runs-on: ubuntu-24.04-arm
|
||||||
timeout-minutes: 20
|
timeout-minutes: 30
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -34,18 +21,16 @@ jobs:
|
|||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: docuseal/docuseal
|
images: docuseal/docuseal
|
||||||
tags: latest,${{ inputs.version }}
|
tags: type=semver,pattern={{version}}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
with:
|
|
||||||
image: ${{ inputs.image }}
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Create .version file
|
- name: Create .version file
|
||||||
run: echo ${{ inputs.version }} > .version
|
run: echo ${{ github.ref_name }} > .version
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
|
|||||||
@@ -37,3 +37,4 @@ yarn-debug.log*
|
|||||||
/docuseal
|
/docuseal
|
||||||
/ee
|
/ee
|
||||||
dump.rdb
|
dump.rdb
|
||||||
|
*.onnx
|
||||||
+14
-2
@@ -1,4 +1,4 @@
|
|||||||
require:
|
plugins:
|
||||||
- rubocop-performance
|
- rubocop-performance
|
||||||
- rubocop-rails
|
- rubocop-rails
|
||||||
- rubocop-rspec
|
- rubocop-rspec
|
||||||
@@ -10,7 +10,7 @@ AllCops:
|
|||||||
- node_modules/**/*
|
- node_modules/**/*
|
||||||
- bin/*
|
- bin/*
|
||||||
- vendor/**/*
|
- vendor/**/*
|
||||||
TargetRubyVersion: '3.3'
|
TargetRubyVersion: '4.0'
|
||||||
SuggestExtensions: false
|
SuggestExtensions: false
|
||||||
|
|
||||||
Metrics/BlockLength:
|
Metrics/BlockLength:
|
||||||
@@ -45,6 +45,12 @@ Metrics/PerceivedComplexity:
|
|||||||
Style/MultipleComparison:
|
Style/MultipleComparison:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Style/NumericPredicate:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/MinMaxComparison:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
Naming/PredicateMethod:
|
Naming/PredicateMethod:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
@@ -94,3 +100,9 @@ Rails/ApplicationController:
|
|||||||
|
|
||||||
Rails/Output:
|
Rails/Output:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/StrongParametersExpect:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/RedirectBackOrTo:
|
||||||
|
Enabled: false
|
||||||
+28
-22
@@ -1,4 +1,4 @@
|
|||||||
FROM ruby:3.4.2-alpine AS download
|
FROM ruby:4.0.1-alpine AS download
|
||||||
|
|
||||||
WORKDIR /fonts
|
WORKDIR /fonts
|
||||||
|
|
||||||
@@ -9,13 +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://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/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && \
|
||||||
wget https://github.com/impallari/DancingScript/raw/master/OFL.txt && \
|
wget https://github.com/impallari/DancingScript/raw/master/OFL.txt && \
|
||||||
|
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" && \
|
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 && \
|
mkdir -p /pdfium-linux && \
|
||||||
tar -xzf pdfium-linux.tgz -C /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")'
|
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 RAILS_ENV=production
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
@@ -41,16 +42,18 @@ COPY ./app/views ./app/views
|
|||||||
|
|
||||||
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
|
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 RAILS_ENV=production
|
||||||
ENV BUNDLE_WITHOUT="development:test"
|
ENV BUNDLE_WITHOUT="development:test"
|
||||||
ENV LD_PRELOAD=/lib/libgcompat.so.0
|
ENV LD_PRELOAD=/lib/libgcompat.so.0
|
||||||
ENV OPENSSL_CONF=/app/openssl_legacy.cnf
|
ENV OPENSSL_CONF=/etc/openssl_legacy.cnf
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev@edge yaml-dev redis libheif@edge vips-heif@edge gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
RUN apk add --no-cache sqlite-dev libpq-dev vips-dev yaml-dev redis libheif vips-heif gcompat ttf-freefont onnxruntime && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
||||||
|
|
||||||
|
RUN addgroup -g 2000 docuseal && adduser -u 2000 -G docuseal -s /bin/sh -D -h /home/docuseal docuseal
|
||||||
|
|
||||||
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
|
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
|
||||||
\n\
|
\n\
|
||||||
@@ -62,33 +65,36 @@ legacy = legacy_sect\n\
|
|||||||
activate = 1\n\
|
activate = 1\n\
|
||||||
\n\
|
\n\
|
||||||
[legacy_sect]\n\
|
[legacy_sect]\n\
|
||||||
activate = 1' >> /app/openssl_legacy.cnf
|
activate = 1' >> /etc/openssl_legacy.cnf
|
||||||
|
|
||||||
COPY ./Gemfile ./Gemfile.lock ./
|
COPY --chown=docuseal:docuseal ./Gemfile ./Gemfile.lock ./
|
||||||
|
|
||||||
RUN apk add --no-cache build-base && bundle install && apk del --no-cache build-base && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf
|
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 ./bin ./bin
|
COPY --chown=docuseal:docuseal ./bin ./bin
|
||||||
COPY ./app ./app
|
COPY --chown=docuseal:docuseal ./app ./app
|
||||||
COPY ./config ./config
|
COPY --chown=docuseal:docuseal ./config ./config
|
||||||
COPY ./db/migrate ./db/migrate
|
COPY --chown=docuseal:docuseal ./db/migrate ./db/migrate
|
||||||
COPY ./log ./log
|
COPY --chown=docuseal:docuseal ./log ./log
|
||||||
COPY ./lib ./lib
|
COPY --chown=docuseal:docuseal ./lib ./lib
|
||||||
COPY ./public ./public
|
COPY --chown=docuseal:docuseal ./public ./public
|
||||||
COPY ./tmp ./tmp
|
COPY --chown=docuseal:docuseal ./tmp ./tmp
|
||||||
COPY LICENSE README.md Rakefile config.ru .version ./
|
COPY --chown=docuseal:docuseal LICENSE README.md Rakefile config.ru .version ./
|
||||||
COPY .version ./public/version
|
COPY --chown=docuseal:docuseal .version ./public/version
|
||||||
|
|
||||||
COPY --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
COPY --chown=docuseal:docuseal --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
||||||
COPY --from=download /fonts/FreeSans.ttf /usr/share/fonts/freefont
|
COPY --from=download /fonts/FreeSans.ttf /usr/share/fonts/freefont
|
||||||
COPY --from=download /pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so
|
COPY --from=download /pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so
|
||||||
COPY --from=download /pdfium-linux/licenses/pdfium.txt /usr/lib/libpdfium-LICENSE.txt
|
COPY --from=download /pdfium-linux/licenses/pdfium.txt /usr/lib/libpdfium-LICENSE.txt
|
||||||
COPY --from=webpack /app/public/packs ./public/packs
|
COPY --chown=docuseal:docuseal --from=download /model.onnx /app/tmp/model.onnx
|
||||||
|
COPY --chown=docuseal:docuseal --from=webpack /app/public/packs ./public/packs
|
||||||
|
|
||||||
RUN ln -s /fonts /app/public/fonts
|
RUN ln -s /fonts /app/public/fonts && \
|
||||||
RUN bundle exec bootsnap precompile --gemfile app/ lib/
|
bundle exec bootsnap precompile -j 1 --gemfile app/ lib/ && \
|
||||||
|
chown -R docuseal:docuseal /app/tmp/cache
|
||||||
|
|
||||||
WORKDIR /data/docuseal
|
WORKDIR /data/docuseal
|
||||||
|
ENV HOME=/home/docuseal
|
||||||
ENV WORKDIR=/data/docuseal
|
ENV WORKDIR=/data/docuseal
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|||||||
@@ -2,16 +2,16 @@
|
|||||||
|
|
||||||
source 'https://rubygems.org'
|
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-s3', require: false
|
||||||
gem 'aws-sdk-secretsmanager', require: false
|
gem 'aws-sdk-secretsmanager', require: false
|
||||||
gem 'azure-storage-blob', require: false
|
gem 'azure-blob', require: false
|
||||||
gem 'bootsnap', require: false
|
gem 'bootsnap', require: false
|
||||||
gem 'cancancan'
|
gem 'cancancan'
|
||||||
gem 'csv'
|
gem 'csv', require: false
|
||||||
gem 'csv-safe'
|
gem 'csv-safe', require: false
|
||||||
gem 'devise'
|
gem 'devise'
|
||||||
gem 'devise-two-factor'
|
gem 'devise-two-factor'
|
||||||
gem 'dotenv', require: false
|
gem 'dotenv', require: false
|
||||||
@@ -21,10 +21,11 @@ gem 'faraday-follow_redirects'
|
|||||||
gem 'google-cloud-storage', require: false
|
gem 'google-cloud-storage', require: false
|
||||||
gem 'hexapdf'
|
gem 'hexapdf'
|
||||||
gem 'image_processing'
|
gem 'image_processing'
|
||||||
gem 'jwt'
|
gem 'jwt', require: false
|
||||||
gem 'lograge'
|
gem 'lograge'
|
||||||
gem 'mysql2', require: false
|
gem 'numo-narray-alt', require: false
|
||||||
gem 'oj'
|
gem 'oj'
|
||||||
|
gem 'onnxruntime', require: false
|
||||||
gem 'pagy'
|
gem 'pagy'
|
||||||
gem 'pg', require: false
|
gem 'pg', require: false
|
||||||
gem 'premailer-rails'
|
gem 'premailer-rails'
|
||||||
@@ -36,13 +37,14 @@ gem 'rails_autolink'
|
|||||||
gem 'rails-i18n'
|
gem 'rails-i18n'
|
||||||
gem 'rotp'
|
gem 'rotp'
|
||||||
gem 'rouge', require: false
|
gem 'rouge', require: false
|
||||||
gem 'rqrcode'
|
gem 'rqrcode', require: false
|
||||||
gem 'ruby-vips'
|
gem 'ruby-vips'
|
||||||
gem 'rubyXL'
|
gem 'rubyXL', require: false
|
||||||
gem 'shakapacker'
|
gem 'shakapacker'
|
||||||
gem 'sidekiq'
|
gem 'sidekiq'
|
||||||
gem 'sqlite3', require: false
|
gem 'sqlite3', require: false
|
||||||
gem 'strip_attributes'
|
gem 'strip_attributes'
|
||||||
|
gem 'trilogy', github: 'trilogy-libraries/trilogy', glob: 'contrib/ruby/*.gemspec', require: false
|
||||||
gem 'turbo-rails'
|
gem 'turbo-rails'
|
||||||
gem 'twitter_cldr', require: false
|
gem 'twitter_cldr', require: false
|
||||||
gem 'tzinfo-data'
|
gem 'tzinfo-data'
|
||||||
|
|||||||
+301
-286
@@ -1,29 +1,39 @@
|
|||||||
|
GIT
|
||||||
|
remote: https://github.com/trilogy-libraries/trilogy.git
|
||||||
|
revision: 3963d490459df7a2b5bedb42424c3285f25eab22
|
||||||
|
glob: contrib/ruby/*.gemspec
|
||||||
|
specs:
|
||||||
|
trilogy (2.10.0)
|
||||||
|
bigdecimal
|
||||||
|
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (8.0.2.1)
|
action_text-trix (2.1.16)
|
||||||
actionpack (= 8.0.2.1)
|
railties
|
||||||
activesupport (= 8.0.2.1)
|
actioncable (8.1.2)
|
||||||
|
actionpack (= 8.1.2)
|
||||||
|
activesupport (= 8.1.2)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
actionmailbox (8.0.2.1)
|
actionmailbox (8.1.2)
|
||||||
actionpack (= 8.0.2.1)
|
actionpack (= 8.1.2)
|
||||||
activejob (= 8.0.2.1)
|
activejob (= 8.1.2)
|
||||||
activerecord (= 8.0.2.1)
|
activerecord (= 8.1.2)
|
||||||
activestorage (= 8.0.2.1)
|
activestorage (= 8.1.2)
|
||||||
activesupport (= 8.0.2.1)
|
activesupport (= 8.1.2)
|
||||||
mail (>= 2.8.0)
|
mail (>= 2.8.0)
|
||||||
actionmailer (8.0.2.1)
|
actionmailer (8.1.2)
|
||||||
actionpack (= 8.0.2.1)
|
actionpack (= 8.1.2)
|
||||||
actionview (= 8.0.2.1)
|
actionview (= 8.1.2)
|
||||||
activejob (= 8.0.2.1)
|
activejob (= 8.1.2)
|
||||||
activesupport (= 8.0.2.1)
|
activesupport (= 8.1.2)
|
||||||
mail (>= 2.8.0)
|
mail (>= 2.8.0)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
actionpack (8.0.2.1)
|
actionpack (8.1.2)
|
||||||
actionview (= 8.0.2.1)
|
actionview (= 8.1.2)
|
||||||
activesupport (= 8.0.2.1)
|
activesupport (= 8.1.2)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
rack (>= 2.2.4)
|
rack (>= 2.2.4)
|
||||||
rack-session (>= 1.0.1)
|
rack-session (>= 1.0.1)
|
||||||
@@ -31,97 +41,97 @@ GEM
|
|||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.6)
|
rails-html-sanitizer (~> 1.6)
|
||||||
useragent (~> 0.16)
|
useragent (~> 0.16)
|
||||||
actiontext (8.0.2.1)
|
actiontext (8.1.2)
|
||||||
actionpack (= 8.0.2.1)
|
action_text-trix (~> 2.1.15)
|
||||||
activerecord (= 8.0.2.1)
|
actionpack (= 8.1.2)
|
||||||
activestorage (= 8.0.2.1)
|
activerecord (= 8.1.2)
|
||||||
activesupport (= 8.0.2.1)
|
activestorage (= 8.1.2)
|
||||||
|
activesupport (= 8.1.2)
|
||||||
globalid (>= 0.6.0)
|
globalid (>= 0.6.0)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (8.0.2.1)
|
actionview (8.1.2)
|
||||||
activesupport (= 8.0.2.1)
|
activesupport (= 8.1.2)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.11)
|
erubi (~> 1.11)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.6)
|
rails-html-sanitizer (~> 1.6)
|
||||||
activejob (8.0.2.1)
|
activejob (8.1.2)
|
||||||
activesupport (= 8.0.2.1)
|
activesupport (= 8.1.2)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (8.0.2.1)
|
activemodel (8.1.2)
|
||||||
activesupport (= 8.0.2.1)
|
activesupport (= 8.1.2)
|
||||||
activerecord (8.0.2.1)
|
activerecord (8.1.2)
|
||||||
activemodel (= 8.0.2.1)
|
activemodel (= 8.1.2)
|
||||||
activesupport (= 8.0.2.1)
|
activesupport (= 8.1.2)
|
||||||
timeout (>= 0.4.0)
|
timeout (>= 0.4.0)
|
||||||
activestorage (8.0.2.1)
|
activestorage (8.1.2)
|
||||||
actionpack (= 8.0.2.1)
|
actionpack (= 8.1.2)
|
||||||
activejob (= 8.0.2.1)
|
activejob (= 8.1.2)
|
||||||
activerecord (= 8.0.2.1)
|
activerecord (= 8.1.2)
|
||||||
activesupport (= 8.0.2.1)
|
activesupport (= 8.1.2)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
activesupport (8.0.2.1)
|
activesupport (8.1.2)
|
||||||
base64
|
base64
|
||||||
benchmark (>= 0.3)
|
|
||||||
bigdecimal
|
bigdecimal
|
||||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||||
connection_pool (>= 2.2.5)
|
connection_pool (>= 2.2.5)
|
||||||
drb
|
drb
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
|
json
|
||||||
logger (>= 1.4.2)
|
logger (>= 1.4.2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
securerandom (>= 0.3)
|
securerandom (>= 0.3)
|
||||||
tzinfo (~> 2.0, >= 2.0.5)
|
tzinfo (~> 2.0, >= 2.0.5)
|
||||||
uri (>= 0.13.1)
|
uri (>= 0.13.1)
|
||||||
addressable (2.8.7)
|
addressable (2.8.8)
|
||||||
public_suffix (>= 2.0.2, < 7.0)
|
public_suffix (>= 2.0.2, < 8.0)
|
||||||
annotaterb (4.14.0)
|
annotaterb (4.20.0)
|
||||||
|
activerecord (>= 6.0.0)
|
||||||
|
activesupport (>= 6.0.0)
|
||||||
arabic-letter-connector (0.1.1)
|
arabic-letter-connector (0.1.1)
|
||||||
ast (2.4.3)
|
ast (2.4.3)
|
||||||
aws-eventstream (1.3.0)
|
aws-eventstream (1.4.0)
|
||||||
aws-partitions (1.1027.0)
|
aws-partitions (1.1209.0)
|
||||||
aws-sdk-core (3.214.0)
|
aws-sdk-core (3.241.4)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.992.0)
|
aws-partitions (~> 1, >= 1.992.0)
|
||||||
aws-sigv4 (~> 1.9)
|
aws-sigv4 (~> 1.9)
|
||||||
|
base64
|
||||||
|
bigdecimal
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
aws-sdk-kms (1.96.0)
|
logger
|
||||||
aws-sdk-core (~> 3, >= 3.210.0)
|
aws-sdk-kms (1.121.0)
|
||||||
|
aws-sdk-core (~> 3, >= 3.241.4)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sdk-s3 (1.176.1)
|
aws-sdk-s3 (1.212.0)
|
||||||
aws-sdk-core (~> 3, >= 3.210.0)
|
aws-sdk-core (~> 3, >= 3.241.4)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sdk-secretsmanager (1.110.0)
|
aws-sdk-secretsmanager (1.128.0)
|
||||||
aws-sdk-core (~> 3, >= 3.210.0)
|
aws-sdk-core (~> 3, >= 3.241.4)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sigv4 (1.10.1)
|
aws-sigv4 (1.12.1)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
azure-storage-blob (2.0.3)
|
azure-blob (0.8.0)
|
||||||
azure-storage-common (~> 2.0)
|
cgi
|
||||||
nokogiri (~> 1, >= 1.10.8)
|
rexml
|
||||||
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)
|
|
||||||
base64 (0.3.0)
|
base64 (0.3.0)
|
||||||
bcrypt (3.1.20)
|
bcrypt (3.1.21)
|
||||||
benchmark (0.4.1)
|
better_html (2.2.0)
|
||||||
better_html (2.1.1)
|
actionview (>= 7.0)
|
||||||
actionview (>= 6.0)
|
activesupport (>= 7.0)
|
||||||
activesupport (>= 6.0)
|
|
||||||
ast (~> 2.0)
|
ast (~> 2.0)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
parser (>= 2.4)
|
parser (>= 2.4)
|
||||||
smart_properties
|
smart_properties
|
||||||
bigdecimal (3.2.2)
|
bigdecimal (4.0.1)
|
||||||
bindex (0.8.1)
|
bindex (0.8.1)
|
||||||
bootsnap (1.18.4)
|
bootsnap (1.21.1)
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
brakeman (7.0.0)
|
brakeman (7.1.2)
|
||||||
racc
|
racc
|
||||||
builder (3.3.0)
|
builder (3.3.0)
|
||||||
bullet (8.0.0)
|
bullet (8.1.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
uniform_notifier (~> 1.11)
|
uniform_notifier (~> 1.11)
|
||||||
camertron-eprun (1.1.1)
|
camertron-eprun (1.1.1)
|
||||||
@@ -135,28 +145,29 @@ GEM
|
|||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
regexp_parser (>= 1.5, < 3.0)
|
regexp_parser (>= 1.5, < 3.0)
|
||||||
xpath (~> 3.2)
|
xpath (~> 3.2)
|
||||||
|
cgi (0.5.1)
|
||||||
childprocess (5.1.0)
|
childprocess (5.1.0)
|
||||||
logger (~> 1.5)
|
logger (~> 1.5)
|
||||||
chunky_png (1.4.0)
|
chunky_png (1.4.0)
|
||||||
cldr-plurals-runtime-rb (1.1.0)
|
cldr-plurals-runtime-rb (1.1.0)
|
||||||
cmdparse (3.0.7)
|
cmdparse (3.0.7)
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
concurrent-ruby (1.3.5)
|
concurrent-ruby (1.3.6)
|
||||||
connection_pool (2.5.3)
|
connection_pool (3.0.2)
|
||||||
crack (1.0.0)
|
crack (1.0.1)
|
||||||
bigdecimal
|
bigdecimal
|
||||||
rexml
|
rexml
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
css_parser (1.21.0)
|
css_parser (1.21.1)
|
||||||
addressable
|
addressable
|
||||||
csv (3.3.2)
|
csv (3.3.5)
|
||||||
csv-safe (3.3.1)
|
csv-safe (3.3.1)
|
||||||
csv (~> 3.0)
|
csv (~> 3.0)
|
||||||
cuprite (0.15.1)
|
cuprite (0.17)
|
||||||
capybara (~> 3.0)
|
capybara (~> 3.0)
|
||||||
ferrum (~> 0.15.0)
|
ferrum (~> 0.17.0)
|
||||||
date (3.4.1)
|
date (3.5.1)
|
||||||
debug (1.10.0)
|
debug (1.11.1)
|
||||||
irb (~> 1.10)
|
irb (~> 1.10)
|
||||||
reline (>= 0.3.8)
|
reline (>= 0.3.8)
|
||||||
declarative (0.0.20)
|
declarative (0.0.20)
|
||||||
@@ -166,20 +177,20 @@ GEM
|
|||||||
railties (>= 4.1.0)
|
railties (>= 4.1.0)
|
||||||
responders
|
responders
|
||||||
warden (~> 1.2.3)
|
warden (~> 1.2.3)
|
||||||
devise-two-factor (6.1.0)
|
devise-two-factor (6.3.1)
|
||||||
activesupport (>= 7.0, < 8.1)
|
activesupport (>= 7.0, < 8.2)
|
||||||
devise (~> 4.0)
|
devise (>= 4.0, < 5.0)
|
||||||
railties (>= 7.0, < 8.1)
|
railties (>= 7.0, < 8.2)
|
||||||
rotp (~> 6.0)
|
rotp (~> 6.0)
|
||||||
diff-lcs (1.5.1)
|
diff-lcs (1.6.2)
|
||||||
digest-crc (0.6.5)
|
digest-crc (0.7.0)
|
||||||
rake (>= 12.0.0, < 14.0.0)
|
rake (>= 12.0.0, < 14.0.0)
|
||||||
docile (1.4.1)
|
docile (1.4.1)
|
||||||
dotenv (3.1.7)
|
dotenv (3.2.0)
|
||||||
drb (2.2.3)
|
drb (2.2.3)
|
||||||
email_typo (0.2.3)
|
email_typo (0.2.3)
|
||||||
erb (5.0.2)
|
erb (6.0.1)
|
||||||
erb_lint (0.7.0)
|
erb_lint (0.9.0)
|
||||||
activesupport
|
activesupport
|
||||||
better_html (>= 2.0.1)
|
better_html (>= 2.0.1)
|
||||||
parser (>= 2.7.1.4)
|
parser (>= 2.7.1.4)
|
||||||
@@ -187,117 +198,101 @@ GEM
|
|||||||
rubocop (>= 1)
|
rubocop (>= 1)
|
||||||
smart_properties
|
smart_properties
|
||||||
erubi (1.13.1)
|
erubi (1.13.1)
|
||||||
factory_bot (6.5.0)
|
factory_bot (6.5.6)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 6.1.0)
|
||||||
factory_bot_rails (6.4.4)
|
factory_bot_rails (6.5.1)
|
||||||
factory_bot (~> 6.5)
|
factory_bot (~> 6.5)
|
||||||
railties (>= 5.0.0)
|
railties (>= 6.1.0)
|
||||||
faker (3.5.1)
|
faker (3.6.0)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
faraday (1.10.4)
|
faraday (2.14.0)
|
||||||
faraday-em_http (~> 1.0)
|
faraday-net_http (>= 2.0, < 3.5)
|
||||||
faraday-em_synchrony (~> 1.0)
|
json
|
||||||
faraday-excon (~> 1.1)
|
logger
|
||||||
faraday-httpclient (~> 1.0)
|
faraday-follow_redirects (0.5.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 (>= 1, < 3)
|
faraday (>= 1, < 3)
|
||||||
faraday-httpclient (1.0.1)
|
faraday-net_http (3.4.2)
|
||||||
faraday-multipart (1.1.0)
|
net-http (~> 0.5)
|
||||||
multipart-post (~> 2.0)
|
ferrum (0.17.1)
|
||||||
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)
|
|
||||||
addressable (~> 2.5)
|
addressable (~> 2.5)
|
||||||
|
base64 (~> 0.2)
|
||||||
concurrent-ruby (~> 1.1)
|
concurrent-ruby (~> 1.1)
|
||||||
webrick (~> 1.7)
|
webrick (~> 1.7)
|
||||||
websocket-driver (~> 0.7)
|
websocket-driver (~> 0.7)
|
||||||
ffi (1.17.1)
|
ffi (1.17.3)
|
||||||
ffi (1.17.1-aarch64-linux-gnu)
|
ffi (1.17.3-aarch64-linux-gnu)
|
||||||
ffi (1.17.1-aarch64-linux-musl)
|
ffi (1.17.3-aarch64-linux-musl)
|
||||||
ffi (1.17.1-arm64-darwin)
|
ffi (1.17.3-arm64-darwin)
|
||||||
ffi (1.17.1-x86_64-linux-gnu)
|
ffi (1.17.3-x86_64-linux-gnu)
|
||||||
ffi (1.17.1-x86_64-linux-musl)
|
ffi (1.17.3-x86_64-linux-musl)
|
||||||
foreman (0.88.1)
|
foreman (0.90.0)
|
||||||
|
thor (~> 1.4)
|
||||||
geom2d (0.4.1)
|
geom2d (0.4.1)
|
||||||
globalid (1.2.1)
|
globalid (1.3.0)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
google-apis-core (0.15.1)
|
google-apis-core (1.0.2)
|
||||||
addressable (~> 2.5, >= 2.5.1)
|
addressable (~> 2.8, >= 2.8.7)
|
||||||
googleauth (~> 1.9)
|
faraday (~> 2.13)
|
||||||
httpclient (>= 2.8.3, < 3.a)
|
faraday-follow_redirects (~> 0.3)
|
||||||
mini_mime (~> 1.0)
|
googleauth (~> 1.14)
|
||||||
mutex_m
|
mini_mime (~> 1.1)
|
||||||
representable (~> 3.0)
|
representable (~> 3.0)
|
||||||
retriable (>= 2.0, < 4.a)
|
retriable (~> 3.1)
|
||||||
google-apis-iamcredentials_v1 (0.22.0)
|
google-apis-iamcredentials_v1 (0.26.0)
|
||||||
google-apis-core (>= 0.15.0, < 2.a)
|
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-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-env (>= 1.0, < 3.a)
|
||||||
google-cloud-errors (~> 1.0)
|
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)
|
faraday (>= 1.0, < 3.a)
|
||||||
google-cloud-errors (1.4.0)
|
google-cloud-errors (1.5.0)
|
||||||
google-cloud-storage (1.54.0)
|
google-cloud-storage (1.58.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
digest-crc (~> 0.4)
|
digest-crc (~> 0.4)
|
||||||
google-apis-core (~> 0.13)
|
google-apis-core (>= 0.18, < 2)
|
||||||
google-apis-iamcredentials_v1 (~> 0.18)
|
google-apis-iamcredentials_v1 (~> 0.18)
|
||||||
google-apis-storage_v1 (~> 0.38)
|
google-apis-storage_v1 (>= 0.42)
|
||||||
google-cloud-core (~> 1.6)
|
google-cloud-core (~> 1.6)
|
||||||
googleauth (~> 1.9)
|
googleauth (~> 1.9)
|
||||||
mini_mime (~> 1.0)
|
mini_mime (~> 1.0)
|
||||||
google-logging-utils (0.1.0)
|
google-logging-utils (0.2.0)
|
||||||
googleauth (1.12.2)
|
googleauth (1.16.1)
|
||||||
faraday (>= 1.0, < 3.a)
|
faraday (>= 1.0, < 3.a)
|
||||||
google-cloud-env (~> 2.2)
|
google-cloud-env (~> 2.2)
|
||||||
google-logging-utils (~> 0.1)
|
google-logging-utils (~> 0.1)
|
||||||
jwt (>= 1.4, < 3.0)
|
jwt (>= 1.4, < 4.0)
|
||||||
multi_json (~> 1.11)
|
multi_json (~> 1.11)
|
||||||
os (>= 0.9, < 2.0)
|
os (>= 0.9, < 2.0)
|
||||||
signet (>= 0.16, < 2.a)
|
signet (>= 0.16, < 2.a)
|
||||||
hashdiff (1.1.2)
|
hashdiff (1.2.1)
|
||||||
hexapdf (1.4.0)
|
hexapdf (1.5.0)
|
||||||
cmdparse (~> 3.0, >= 3.0.3)
|
cmdparse (~> 3.0, >= 3.0.3)
|
||||||
geom2d (~> 0.4, >= 0.4.1)
|
geom2d (~> 0.4, >= 0.4.1)
|
||||||
openssl (>= 2.2.1)
|
openssl (>= 2.2.1)
|
||||||
strscan (>= 3.1.2)
|
strscan (>= 3.1.2)
|
||||||
htmlentities (4.3.4)
|
htmlentities (4.4.2)
|
||||||
httpclient (2.8.3)
|
i18n (1.14.8)
|
||||||
i18n (1.14.7)
|
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
image_processing (1.13.0)
|
image_processing (1.14.0)
|
||||||
mini_magick (>= 4.9.5, < 5)
|
mini_magick (>= 4.9.5, < 6)
|
||||||
ruby-vips (>= 2.0.17, < 3)
|
ruby-vips (>= 2.0.17, < 3)
|
||||||
io-console (0.8.1)
|
io-console (0.8.2)
|
||||||
irb (1.15.2)
|
irb (1.16.0)
|
||||||
pp (>= 0.6.0)
|
pp (>= 0.6.0)
|
||||||
rdoc (>= 4.0.0)
|
rdoc (>= 4.0.0)
|
||||||
reline (>= 0.4.2)
|
reline (>= 0.4.2)
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.15.0)
|
json (2.18.0)
|
||||||
jwt (2.9.3)
|
jwt (3.1.2)
|
||||||
base64
|
base64
|
||||||
language_server-protocol (3.17.0.5)
|
language_server-protocol (3.17.0.5)
|
||||||
launchy (3.0.1)
|
launchy (3.1.1)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
childprocess (~> 5.0)
|
childprocess (~> 5.0)
|
||||||
|
logger (~> 1.6)
|
||||||
letter_opener (1.10.0)
|
letter_opener (1.10.0)
|
||||||
launchy (>= 2.2, < 4)
|
launchy (>= 2.2, < 4)
|
||||||
letter_opener_web (3.0.0)
|
letter_opener_web (3.0.0)
|
||||||
@@ -312,29 +307,28 @@ GEM
|
|||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
railties (>= 4)
|
railties (>= 4)
|
||||||
request_store (~> 1.0)
|
request_store (~> 1.0)
|
||||||
loofah (2.24.1)
|
loofah (2.25.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
mail (2.8.1)
|
mail (2.9.0)
|
||||||
|
logger
|
||||||
mini_mime (>= 0.1.1)
|
mini_mime (>= 0.1.1)
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
marcel (1.0.4)
|
marcel (1.1.0)
|
||||||
matrix (0.4.2)
|
matrix (0.4.3)
|
||||||
method_source (1.1.0)
|
method_source (1.1.0)
|
||||||
mini_magick (4.13.2)
|
mini_magick (5.3.1)
|
||||||
|
logger
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.9)
|
minitest (6.0.1)
|
||||||
minitest (5.25.5)
|
prism (~> 1.5)
|
||||||
msgpack (1.7.5)
|
msgpack (1.8.0)
|
||||||
multi_json (1.15.0)
|
multi_json (1.19.1)
|
||||||
multipart-post (2.4.1)
|
net-http (0.9.1)
|
||||||
mutex_m (0.3.0)
|
uri (>= 0.11.1)
|
||||||
mysql2 (0.5.6)
|
net-imap (0.6.2)
|
||||||
net-http-persistent (4.0.5)
|
|
||||||
connection_pool (~> 2.2)
|
|
||||||
net-imap (0.5.9)
|
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
net-pop (0.1.2)
|
net-pop (0.1.2)
|
||||||
@@ -343,35 +337,48 @@ GEM
|
|||||||
timeout
|
timeout
|
||||||
net-smtp (0.5.1)
|
net-smtp (0.5.1)
|
||||||
net-protocol
|
net-protocol
|
||||||
nio4r (2.7.4)
|
nio4r (2.7.5)
|
||||||
nokogiri (1.18.9)
|
nokogiri (1.19.0-aarch64-linux-gnu)
|
||||||
mini_portile2 (~> 2.8.2)
|
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.9-aarch64-linux-gnu)
|
nokogiri (1.19.0-aarch64-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.9-aarch64-linux-musl)
|
nokogiri (1.19.0-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.9-arm64-darwin)
|
nokogiri (1.19.0-x86_64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.9-x86_64-linux-gnu)
|
nokogiri (1.19.0-x86_64-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.9-x86_64-linux-musl)
|
numo-narray-alt (0.9.13)
|
||||||
racc (~> 1.4)
|
oj (3.16.13)
|
||||||
oj (3.16.11)
|
|
||||||
bigdecimal (>= 3.0)
|
bigdecimal (>= 3.0)
|
||||||
ostruct (>= 0.2)
|
ostruct (>= 0.2)
|
||||||
openssl (3.3.0)
|
onnxruntime (0.10.1)
|
||||||
|
ffi
|
||||||
|
onnxruntime (0.10.1-aarch64-linux)
|
||||||
|
ffi
|
||||||
|
onnxruntime (0.10.1-arm64-darwin)
|
||||||
|
ffi
|
||||||
|
onnxruntime (0.10.1-x86_64-linux)
|
||||||
|
ffi
|
||||||
|
openssl (4.0.0)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
ostruct (0.6.3)
|
ostruct (0.6.3)
|
||||||
package_json (0.1.0)
|
package_json (0.2.0)
|
||||||
pagy (9.3.3)
|
pagy (43.2.8)
|
||||||
|
json
|
||||||
|
yaml
|
||||||
parallel (1.27.0)
|
parallel (1.27.0)
|
||||||
parser (3.3.9.0)
|
parser (3.3.10.1)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
pg (1.5.9)
|
pg (1.6.3)
|
||||||
pp (0.6.2)
|
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
|
prettyprint
|
||||||
premailer (1.27.0)
|
premailer (1.27.0)
|
||||||
addressable
|
addressable
|
||||||
@@ -381,23 +388,24 @@ GEM
|
|||||||
actionmailer (>= 3)
|
actionmailer (>= 3)
|
||||||
net-smtp
|
net-smtp
|
||||||
premailer (~> 1.7, >= 1.7.9)
|
premailer (~> 1.7, >= 1.7.9)
|
||||||
pretender (0.5.0)
|
pretender (0.6.0)
|
||||||
actionpack (>= 6.1)
|
actionpack (>= 7.1)
|
||||||
prettyprint (0.2.0)
|
prettyprint (0.2.0)
|
||||||
prism (1.5.1)
|
prism (1.8.0)
|
||||||
pry (0.15.0)
|
pry (0.16.0)
|
||||||
coderay (~> 1.1)
|
coderay (~> 1.1)
|
||||||
method_source (~> 1.0)
|
method_source (~> 1.0)
|
||||||
|
reline (>= 0.6.0)
|
||||||
pry-rails (0.3.11)
|
pry-rails (0.3.11)
|
||||||
pry (>= 0.13.0)
|
pry (>= 0.13.0)
|
||||||
psych (5.2.6)
|
psych (5.3.1)
|
||||||
date
|
date
|
||||||
stringio
|
stringio
|
||||||
public_suffix (6.0.1)
|
public_suffix (7.0.2)
|
||||||
puma (6.5.0)
|
puma (7.2.0)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rack (3.2.3)
|
rack (3.2.4)
|
||||||
rack-proxy (0.7.7)
|
rack-proxy (0.7.7)
|
||||||
rack
|
rack
|
||||||
rack-session (2.1.1)
|
rack-session (2.1.1)
|
||||||
@@ -405,22 +413,22 @@ GEM
|
|||||||
rack (>= 3.0.0)
|
rack (>= 3.0.0)
|
||||||
rack-test (2.2.0)
|
rack-test (2.2.0)
|
||||||
rack (>= 1.3)
|
rack (>= 1.3)
|
||||||
rackup (2.2.1)
|
rackup (2.3.1)
|
||||||
rack (>= 3)
|
rack (>= 3)
|
||||||
rails (8.0.2.1)
|
rails (8.1.2)
|
||||||
actioncable (= 8.0.2.1)
|
actioncable (= 8.1.2)
|
||||||
actionmailbox (= 8.0.2.1)
|
actionmailbox (= 8.1.2)
|
||||||
actionmailer (= 8.0.2.1)
|
actionmailer (= 8.1.2)
|
||||||
actionpack (= 8.0.2.1)
|
actionpack (= 8.1.2)
|
||||||
actiontext (= 8.0.2.1)
|
actiontext (= 8.1.2)
|
||||||
actionview (= 8.0.2.1)
|
actionview (= 8.1.2)
|
||||||
activejob (= 8.0.2.1)
|
activejob (= 8.1.2)
|
||||||
activemodel (= 8.0.2.1)
|
activemodel (= 8.1.2)
|
||||||
activerecord (= 8.0.2.1)
|
activerecord (= 8.1.2)
|
||||||
activestorage (= 8.0.2.1)
|
activestorage (= 8.1.2)
|
||||||
activesupport (= 8.0.2.1)
|
activesupport (= 8.1.2)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 8.0.2.1)
|
railties (= 8.1.2)
|
||||||
rails-dom-testing (2.3.0)
|
rails-dom-testing (2.3.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
minitest
|
minitest
|
||||||
@@ -428,30 +436,32 @@ GEM
|
|||||||
rails-html-sanitizer (1.6.2)
|
rails-html-sanitizer (1.6.2)
|
||||||
loofah (~> 2.21)
|
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)
|
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)
|
i18n (>= 0.7, < 2)
|
||||||
railties (>= 8.0.0, < 9)
|
railties (>= 8.0.0, < 9)
|
||||||
rails_autolink (1.1.8)
|
rails_autolink (1.1.8)
|
||||||
actionview (> 3.1)
|
actionview (> 3.1)
|
||||||
activesupport (> 3.1)
|
activesupport (> 3.1)
|
||||||
railties (> 3.1)
|
railties (> 3.1)
|
||||||
railties (8.0.2.1)
|
railties (8.1.2)
|
||||||
actionpack (= 8.0.2.1)
|
actionpack (= 8.1.2)
|
||||||
activesupport (= 8.0.2.1)
|
activesupport (= 8.1.2)
|
||||||
irb (~> 1.13)
|
irb (~> 1.13)
|
||||||
rackup (>= 1.0.0)
|
rackup (>= 1.0.0)
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0, >= 1.2.2)
|
thor (~> 1.0, >= 1.2.2)
|
||||||
|
tsort (>= 0.2)
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.3.0)
|
rake (13.3.1)
|
||||||
rdoc (6.14.2)
|
rdoc (7.1.0)
|
||||||
erb
|
erb
|
||||||
psych (>= 4.0.0)
|
psych (>= 4.0.0)
|
||||||
redis-client (0.23.0)
|
tsort
|
||||||
|
redis-client (0.26.4)
|
||||||
connection_pool
|
connection_pool
|
||||||
regexp_parser (2.11.3)
|
regexp_parser (2.11.3)
|
||||||
reline (0.6.2)
|
reline (0.6.3)
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
representable (3.2.0)
|
representable (3.2.0)
|
||||||
declarative (< 0.1.0)
|
declarative (< 0.1.0)
|
||||||
@@ -459,35 +469,35 @@ GEM
|
|||||||
uber (< 0.2.0)
|
uber (< 0.2.0)
|
||||||
request_store (1.7.0)
|
request_store (1.7.0)
|
||||||
rack (>= 1.4)
|
rack (>= 1.4)
|
||||||
responders (3.1.1)
|
responders (3.2.0)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 7.0)
|
||||||
railties (>= 5.2)
|
railties (>= 7.0)
|
||||||
retriable (3.1.2)
|
retriable (3.1.2)
|
||||||
rexml (3.4.4)
|
rexml (3.4.4)
|
||||||
rotp (6.3.0)
|
rotp (6.3.0)
|
||||||
rouge (4.5.2)
|
rouge (4.7.0)
|
||||||
rqrcode (2.2.0)
|
rqrcode (3.2.0)
|
||||||
chunky_png (~> 1.0)
|
chunky_png (~> 1.0)
|
||||||
rqrcode_core (~> 1.0)
|
rqrcode_core (~> 2.0)
|
||||||
rqrcode_core (1.2.0)
|
rqrcode_core (2.1.0)
|
||||||
rspec-core (3.13.2)
|
rspec-core (3.13.6)
|
||||||
rspec-support (~> 3.13.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-expectations (3.13.3)
|
rspec-expectations (3.13.5)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.13.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-mocks (3.13.2)
|
rspec-mocks (3.13.7)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.13.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-rails (7.1.0)
|
rspec-rails (8.0.2)
|
||||||
actionpack (>= 7.0)
|
actionpack (>= 7.2)
|
||||||
activesupport (>= 7.0)
|
activesupport (>= 7.2)
|
||||||
railties (>= 7.0)
|
railties (>= 7.2)
|
||||||
rspec-core (~> 3.13)
|
rspec-core (~> 3.13)
|
||||||
rspec-expectations (~> 3.13)
|
rspec-expectations (~> 3.13)
|
||||||
rspec-mocks (~> 3.13)
|
rspec-mocks (~> 3.13)
|
||||||
rspec-support (~> 3.13)
|
rspec-support (~> 3.13)
|
||||||
rspec-support (3.13.2)
|
rspec-support (3.13.6)
|
||||||
rubocop (1.81.1)
|
rubocop (1.82.1)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (~> 3.17.0.2)
|
language_server-protocol (~> 3.17.0.2)
|
||||||
lint_roller (~> 1.1.0)
|
lint_roller (~> 1.1.0)
|
||||||
@@ -495,87 +505,90 @@ GEM
|
|||||||
parser (>= 3.3.0.2)
|
parser (>= 3.3.0.2)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 2.9.3, < 3.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)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 2.4.0, < 4.0)
|
unicode-display_width (>= 2.4.0, < 4.0)
|
||||||
rubocop-ast (1.47.1)
|
rubocop-ast (1.49.0)
|
||||||
parser (>= 3.3.7.2)
|
parser (>= 3.3.7.2)
|
||||||
prism (~> 1.4)
|
prism (~> 1.7)
|
||||||
rubocop-performance (1.23.0)
|
rubocop-performance (1.26.1)
|
||||||
rubocop (>= 1.48.1, < 2.0)
|
lint_roller (~> 1.1)
|
||||||
rubocop-ast (>= 1.31.1, < 2.0)
|
rubocop (>= 1.75.0, < 2.0)
|
||||||
rubocop-rails (2.27.0)
|
rubocop-ast (>= 1.47.1, < 2.0)
|
||||||
|
rubocop-rails (2.34.3)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
|
lint_roller (~> 1.1)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.52.0, < 2.0)
|
rubocop (>= 1.75.0, < 2.0)
|
||||||
rubocop-ast (>= 1.31.1, < 2.0)
|
rubocop-ast (>= 1.44.0, < 2.0)
|
||||||
rubocop-rspec (3.3.0)
|
rubocop-rspec (3.9.0)
|
||||||
rubocop (~> 1.61)
|
lint_roller (~> 1.1)
|
||||||
|
rubocop (~> 1.81)
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
ruby-vips (2.2.2)
|
ruby-vips (2.3.0)
|
||||||
ffi (~> 1.12)
|
ffi (~> 1.12)
|
||||||
logger
|
logger
|
||||||
ruby2_keywords (0.0.5)
|
rubyXL (3.4.35)
|
||||||
rubyXL (3.4.33)
|
|
||||||
nokogiri (>= 1.10.8)
|
nokogiri (>= 1.10.8)
|
||||||
rubyzip (>= 1.3.0)
|
rubyzip (>= 3.2.2)
|
||||||
rubyzip (2.3.2)
|
rubyzip (3.2.2)
|
||||||
securerandom (0.4.1)
|
securerandom (0.4.1)
|
||||||
semantic_range (3.1.0)
|
semantic_range (3.1.0)
|
||||||
shakapacker (8.0.2)
|
shakapacker (9.5.0)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
package_json
|
package_json
|
||||||
rack-proxy (>= 0.6.1)
|
rack-proxy (>= 0.6.1)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
semantic_range (>= 2.3.0)
|
semantic_range (>= 2.3.0)
|
||||||
sidekiq (7.3.7)
|
sidekiq (8.1.0)
|
||||||
connection_pool (>= 2.3.0)
|
connection_pool (>= 3.0.0)
|
||||||
logger
|
json (>= 2.16.0)
|
||||||
rack (>= 2.2.4)
|
logger (>= 1.7.0)
|
||||||
redis-client (>= 0.22.2)
|
rack (>= 3.2.0)
|
||||||
signet (0.19.0)
|
redis-client (>= 0.26.0)
|
||||||
|
signet (0.21.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
faraday (>= 0.17.5, < 3.a)
|
faraday (>= 0.17.5, < 3.a)
|
||||||
jwt (>= 1.5, < 3.0)
|
jwt (>= 1.5, < 4.0)
|
||||||
multi_json (~> 1.10)
|
multi_json (~> 1.10)
|
||||||
simplecov (0.22.0)
|
simplecov (0.22.0)
|
||||||
docile (~> 1.1)
|
docile (~> 1.1)
|
||||||
simplecov-html (~> 0.11)
|
simplecov-html (~> 0.11)
|
||||||
simplecov_json_formatter (~> 0.1)
|
simplecov_json_formatter (~> 0.1)
|
||||||
simplecov-html (0.13.1)
|
simplecov-html (0.13.2)
|
||||||
simplecov_json_formatter (0.1.4)
|
simplecov_json_formatter (0.1.4)
|
||||||
smart_properties (1.17.0)
|
smart_properties (1.17.0)
|
||||||
sqlite3 (2.5.0)
|
sqlite3 (2.9.0-aarch64-linux-gnu)
|
||||||
mini_portile2 (~> 2.8.0)
|
sqlite3 (2.9.0-aarch64-linux-musl)
|
||||||
sqlite3 (2.5.0-aarch64-linux-gnu)
|
sqlite3 (2.9.0-arm64-darwin)
|
||||||
sqlite3 (2.5.0-aarch64-linux-musl)
|
sqlite3 (2.9.0-x86_64-linux-gnu)
|
||||||
sqlite3 (2.5.0-arm64-darwin)
|
sqlite3 (2.9.0-x86_64-linux-musl)
|
||||||
sqlite3 (2.5.0-x86_64-linux-gnu)
|
stringio (3.2.0)
|
||||||
sqlite3 (2.5.0-x86_64-linux-musl)
|
strip_attributes (2.0.1)
|
||||||
stringio (3.1.7)
|
|
||||||
strip_attributes (1.14.1)
|
|
||||||
activemodel (>= 3.0, < 9.0)
|
activemodel (>= 3.0, < 9.0)
|
||||||
strscan (3.1.5)
|
strscan (3.1.7)
|
||||||
thor (1.4.0)
|
thor (1.5.0)
|
||||||
timeout (0.4.3)
|
timeout (0.6.0)
|
||||||
trailblazer-option (0.1.2)
|
trailblazer-option (0.1.2)
|
||||||
turbo-rails (2.0.11)
|
tsort (0.2.0)
|
||||||
actionpack (>= 6.0.0)
|
turbo-rails (2.0.21)
|
||||||
railties (>= 6.0.0)
|
actionpack (>= 7.1.0)
|
||||||
twitter_cldr (6.12.1)
|
railties (>= 7.1.0)
|
||||||
|
twitter_cldr (6.14.0)
|
||||||
|
base64
|
||||||
camertron-eprun
|
camertron-eprun
|
||||||
cldr-plurals-runtime-rb (~> 1.1)
|
cldr-plurals-runtime-rb (~> 1.1)
|
||||||
tzinfo
|
tzinfo
|
||||||
tzinfo (2.0.6)
|
tzinfo (2.0.6)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
tzinfo-data (1.2024.2)
|
tzinfo-data (1.2025.3)
|
||||||
tzinfo (>= 1.0.0)
|
tzinfo (>= 1.0.0)
|
||||||
uber (0.1.0)
|
uber (0.1.0)
|
||||||
unicode-display_width (3.2.0)
|
unicode-display_width (3.2.0)
|
||||||
unicode-emoji (~> 4.1)
|
unicode-emoji (~> 4.1)
|
||||||
unicode-emoji (4.1.0)
|
unicode-emoji (4.2.0)
|
||||||
uniform_notifier (1.16.0)
|
uniform_notifier (1.18.0)
|
||||||
uri (1.0.3)
|
uri (1.1.1)
|
||||||
useragent (0.16.11)
|
useragent (0.16.11)
|
||||||
warden (1.2.9)
|
warden (1.2.9)
|
||||||
rack (>= 2.0.9)
|
rack (>= 2.0.9)
|
||||||
@@ -584,24 +597,24 @@ GEM
|
|||||||
activemodel (>= 6.0.0)
|
activemodel (>= 6.0.0)
|
||||||
bindex (>= 0.4.0)
|
bindex (>= 0.4.0)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
webmock (3.24.0)
|
webmock (3.26.1)
|
||||||
addressable (>= 2.8.0)
|
addressable (>= 2.8.0)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff (>= 0.4.0, < 2.0.0)
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
webrick (1.9.1)
|
webrick (1.9.2)
|
||||||
websocket-driver (0.8.0)
|
websocket-driver (0.8.0)
|
||||||
base64
|
base64
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
xpath (3.2.0)
|
xpath (3.2.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
zeitwerk (2.7.3)
|
yaml (0.4.0)
|
||||||
|
zeitwerk (2.7.4)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
aarch64-linux
|
aarch64-linux
|
||||||
aarch64-linux-musl
|
aarch64-linux-musl
|
||||||
arm64-darwin
|
arm64-darwin
|
||||||
ruby
|
|
||||||
x86_64-linux
|
x86_64-linux
|
||||||
x86_64-linux-musl
|
x86_64-linux-musl
|
||||||
|
|
||||||
@@ -610,7 +623,7 @@ DEPENDENCIES
|
|||||||
arabic-letter-connector
|
arabic-letter-connector
|
||||||
aws-sdk-s3
|
aws-sdk-s3
|
||||||
aws-sdk-secretsmanager
|
aws-sdk-secretsmanager
|
||||||
azure-storage-blob
|
azure-blob
|
||||||
better_html
|
better_html
|
||||||
bootsnap
|
bootsnap
|
||||||
brakeman
|
brakeman
|
||||||
@@ -637,8 +650,9 @@ DEPENDENCIES
|
|||||||
jwt
|
jwt
|
||||||
letter_opener_web
|
letter_opener_web
|
||||||
lograge
|
lograge
|
||||||
mysql2
|
numo-narray-alt
|
||||||
oj
|
oj
|
||||||
|
onnxruntime
|
||||||
pagy
|
pagy
|
||||||
pg
|
pg
|
||||||
premailer-rails
|
premailer-rails
|
||||||
@@ -664,6 +678,7 @@ DEPENDENCIES
|
|||||||
simplecov
|
simplecov
|
||||||
sqlite3
|
sqlite3
|
||||||
strip_attributes
|
strip_attributes
|
||||||
|
trilogy!
|
||||||
turbo-rails
|
turbo-rails
|
||||||
twitter_cldr
|
twitter_cldr
|
||||||
tzinfo-data
|
tzinfo-data
|
||||||
@@ -671,7 +686,7 @@ DEPENDENCIES
|
|||||||
webmock
|
webmock
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
ruby 3.4.2p28
|
ruby 4.0.1
|
||||||
|
|
||||||
BUNDLED WITH
|
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
|
||||||
@@ -46,7 +46,9 @@ class AccountsController < ApplicationController
|
|||||||
def destroy
|
def destroy
|
||||||
authorize!(:manage, current_account)
|
authorize!(:manage, current_account)
|
||||||
|
|
||||||
|
true_user.skip_reconfirmation!
|
||||||
true_user.update!(locked_at: Time.current, email: true_user.email.sub('@', '+removed@'))
|
true_user.update!(locked_at: Time.current, email: true_user.email.sub('@', '+removed@'))
|
||||||
|
true_user.account.update!(archived_at: Time.current)
|
||||||
|
|
||||||
# rubocop:disable Layout/LineLength
|
# rubocop:disable Layout/LineLength
|
||||||
render turbo_stream: turbo_stream.replace(
|
render turbo_stream: turbo_stream.replace(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
module Api
|
module Api
|
||||||
class ApiBaseController < ActionController::API
|
class ApiBaseController < ActionController::API
|
||||||
include ActiveStorage::SetCurrent
|
include ActiveStorage::SetCurrent
|
||||||
include Pagy::Backend
|
include Pagy::Method
|
||||||
|
|
||||||
DEFAULT_LIMIT = 10
|
DEFAULT_LIMIT = 10
|
||||||
MAX_LIMIT = 100
|
MAX_LIMIT = 100
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
|
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
|
||||||
|
rescue Submitters::MaliciousFileExtension => e
|
||||||
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
|
render json: { error: e.message }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_new_cookie_signatures_json(submitter, attachment)
|
def build_new_cookie_signatures_json(submitter, attachment)
|
||||||
|
|||||||
@@ -5,34 +5,17 @@ module Api
|
|||||||
load_and_authorize_resource :submission
|
load_and_authorize_resource :submission
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
is_merge = params[:merge] == 'true' &&
|
||||||
|
(@submission.schema_documents || @submission.template.schema_documents).size > 1
|
||||||
|
|
||||||
documents =
|
documents =
|
||||||
if @submission.submitters.all?(&:completed_at?)
|
if @submission.submitters.all?(&:completed_at?)
|
||||||
last_submitter = @submission.submitters.max_by(&:completed_at)
|
build_completed_documents(@submission, merge: is_merge)
|
||||||
|
|
||||||
if last_submitter.documents_attachments.blank?
|
|
||||||
last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(last_submitter)
|
|
||||||
end
|
|
||||||
|
|
||||||
last_submitter.documents_attachments
|
|
||||||
else
|
else
|
||||||
values_hash = Submissions::GeneratePreviewAttachments.build_values_hash(@submission)
|
build_preview_documents(@submission, merge: is_merge)
|
||||||
|
|
||||||
if @submission.preview_documents.present? &&
|
|
||||||
@submission.preview_documents.all? { |s| s.metadata['values_hash'] == values_hash }
|
|
||||||
@submission.preview_documents
|
|
||||||
else
|
|
||||||
ApplicationRecord.no_touching do
|
|
||||||
@submission.preview_documents.each(&:destroy)
|
|
||||||
end
|
|
||||||
|
|
||||||
Submissions::GeneratePreviewAttachments.call(@submission, values_hash:)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
ActiveRecord::Associations::Preloader.new(
|
ActiveRecord::Associations::Preloader.new(records: documents, associations: [:blob]).call
|
||||||
records: documents,
|
|
||||||
associations: [:blob]
|
|
||||||
).call
|
|
||||||
|
|
||||||
expires_at = Accounts.link_expires_at(current_account)
|
expires_at = Accounts.link_expires_at(current_account)
|
||||||
|
|
||||||
@@ -43,5 +26,50 @@ module Api
|
|||||||
end
|
end
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def build_completed_documents(submission, merge: false)
|
||||||
|
last_submitter = submission.submitters.max_by(&:completed_at)
|
||||||
|
|
||||||
|
if merge
|
||||||
|
if submission.merged_document_attachment.blank?
|
||||||
|
submission.merged_document_attachment =
|
||||||
|
Submissions::GenerateCombinedAttachment.call(last_submitter, with_audit: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
[submission.merged_document_attachment]
|
||||||
|
else
|
||||||
|
if last_submitter.documents_attachments.blank?
|
||||||
|
last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(last_submitter)
|
||||||
|
end
|
||||||
|
|
||||||
|
last_submitter.documents_attachments
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_preview_documents(submission, merge: false)
|
||||||
|
values_hash = Submissions::GeneratePreviewAttachments.build_values_hash(submission)
|
||||||
|
|
||||||
|
if merge
|
||||||
|
if submission.preview_merged_document_attachment.present? &&
|
||||||
|
submission.preview_merged_document_attachment.metadata['values_hash'] == values_hash
|
||||||
|
[submission.preview_merged_document_attachment]
|
||||||
|
else
|
||||||
|
ApplicationRecord.no_touching { submission.preview_merged_document_attachment&.destroy }
|
||||||
|
|
||||||
|
Submissions::GeneratePreviewAttachments.call(submission, values_hash:, merge: true)
|
||||||
|
end
|
||||||
|
elsif submission.preview_documents.present? &&
|
||||||
|
submission.preview_documents.all? { |s| s.metadata['values_hash'] == values_hash }
|
||||||
|
submission.preview_documents
|
||||||
|
else
|
||||||
|
ApplicationRecord.no_touching do
|
||||||
|
submission.preview_documents.each(&:destroy)
|
||||||
|
end
|
||||||
|
|
||||||
|
Submissions::GeneratePreviewAttachments.call(submission, values_hash:)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -182,13 +182,13 @@ module Api
|
|||||||
def submissions_params
|
def submissions_params
|
||||||
permitted_attrs = [
|
permitted_attrs = [
|
||||||
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
|
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
|
||||||
:require_phone_2fa, :expire_at, :name,
|
:require_phone_2fa, :require_email_2fa, :expire_at, :name,
|
||||||
{
|
{
|
||||||
variables: {},
|
variables: {},
|
||||||
message: %i[subject body],
|
message: %i[subject body],
|
||||||
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||||
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
|
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
|
||||||
:require_phone_2fa, :order,
|
:require_phone_2fa, :require_email_2fa, :order, :index, :invite_by,
|
||||||
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
|
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
|
||||||
fields: [:name, :uuid, :default_value, :value, :title, :description,
|
fields: [:name, :uuid, :default_value, :value, :title, :description,
|
||||||
:readonly, :required, :validation_pattern, :invalid_message,
|
:readonly, :required, :validation_pattern, :invalid_message,
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ module Api
|
|||||||
|
|
||||||
submitter_params.permit(
|
submitter_params.permit(
|
||||||
:send_email, :send_sms, :reply_to, :completed_redirect_url, :uuid, :name, :email, :role,
|
:send_email, :send_sms, :reply_to, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||||
:completed, :phone, :application_key, :external_id, :go_to_last, :require_phone_2fa,
|
:completed, :phone, :application_key, :external_id, :go_to_last, :require_phone_2fa, :require_email_2fa,
|
||||||
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
||||||
fields: [[:name, :uuid, :default_value, :value, :required,
|
fields: [[:name, :uuid, :default_value, :value, :required,
|
||||||
:readonly, :validation_pattern, :invalid_message,
|
:readonly, :validation_pattern, :invalid_message,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ class ApplicationController < ActionController::Base
|
|||||||
BROWSER_LOCALE_REGEXP = /\A\w{2}(?:-\w{2})?/
|
BROWSER_LOCALE_REGEXP = /\A\w{2}(?:-\w{2})?/
|
||||||
|
|
||||||
include ActiveStorage::SetCurrent
|
include ActiveStorage::SetCurrent
|
||||||
include Pagy::Backend
|
include Pagy::Method
|
||||||
|
|
||||||
check_authorization unless: :devise_controller?
|
check_authorization unless: :devise_controller?
|
||||||
|
|
||||||
@@ -17,12 +17,13 @@ class ApplicationController < ActionController::Base
|
|||||||
|
|
||||||
helper_method :button_title,
|
helper_method :button_title,
|
||||||
:current_account,
|
:current_account,
|
||||||
|
:true_ability,
|
||||||
:form_link_host,
|
:form_link_host,
|
||||||
:svg_icon
|
:svg_icon
|
||||||
|
|
||||||
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
|
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
|
||||||
|
|
||||||
rescue_from Pagy::OverflowError do
|
rescue_from Pagy::RangeError do
|
||||||
redirect_to request.path
|
redirect_to request.path
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -59,7 +60,7 @@ class ApplicationController < ActionController::Base
|
|||||||
|
|
||||||
def pagy_auto(collection, **keyword_args)
|
def pagy_auto(collection, **keyword_args)
|
||||||
if current_ability.can?(:manage, :countless)
|
if current_ability.can?(:manage, :countless)
|
||||||
pagy_countless(collection, **keyword_args)
|
pagy(:countless, collection, **keyword_args)
|
||||||
else
|
else
|
||||||
pagy(collection, **keyword_args)
|
pagy(collection, **keyword_args)
|
||||||
end
|
end
|
||||||
@@ -102,6 +103,10 @@ class ApplicationController < ActionController::Base
|
|||||||
current_user&.account
|
current_user&.account
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def true_ability
|
||||||
|
@true_ability ||= Ability.new(true_user)
|
||||||
|
end
|
||||||
|
|
||||||
def maybe_redirect_to_setup
|
def maybe_redirect_to_setup
|
||||||
redirect_to setup_index_path unless User.exists?
|
redirect_to setup_index_path unless User.exists?
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ class ConsoleRedirectController < ApplicationController
|
|||||||
scope: :console,
|
scope: :console,
|
||||||
exp: 1.minute.from_now.to_i)
|
exp: 1.minute.from_now.to_i)
|
||||||
|
|
||||||
path = Addressable::URI.parse(params[:redir]).path if params[:redir].to_s.starts_with?(Docuseal::CONSOLE_URL)
|
redir_uri = Addressable::URI.parse(params[:redir])
|
||||||
|
path = redir_uri.path if params[:redir].to_s.starts_with?(Docuseal::CONSOLE_URL)
|
||||||
|
|
||||||
redirect_to("#{Docuseal::CONSOLE_URL}#{path}?#{{ auth: }.to_query}", allow_other_host: true)
|
redirect_to "#{Docuseal::CONSOLE_URL}#{path}?#{{ **redir_uri&.query_values, 'auth' => auth }.to_query}",
|
||||||
|
allow_other_host: true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1,4 +1,11 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class InvitationsController < Devise::PasswordsController
|
class InvitationsController < Devise::PasswordsController
|
||||||
|
def update
|
||||||
|
super do |resource|
|
||||||
|
resource.confirmed_at ||= Time.current if resource.errors.empty?
|
||||||
|
|
||||||
|
PasswordsController::Current.user = resource
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -5,6 +5,8 @@ class PasswordsController < Devise::PasswordsController
|
|||||||
skip_before_action :require_no_authentication, only: %i[edit update]
|
skip_before_action :require_no_authentication, only: %i[edit update]
|
||||||
# rubocop:enable Rails/LexicallyScopedActionFilter
|
# rubocop:enable Rails/LexicallyScopedActionFilter
|
||||||
|
|
||||||
|
around_action :with_browser_locale
|
||||||
|
|
||||||
class Current < ActiveSupport::CurrentAttributes
|
class Current < ActiveSupport::CurrentAttributes
|
||||||
attribute :user
|
attribute :user
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ class PersonalizationSettingsController < ApplicationController
|
|||||||
ALLOWED_KEYS = [
|
ALLOWED_KEYS = [
|
||||||
AccountConfig::FORM_COMPLETED_BUTTON_KEY,
|
AccountConfig::FORM_COMPLETED_BUTTON_KEY,
|
||||||
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
|
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
|
||||||
|
AccountConfig::SUBMITTER_INVITATION_REMINDER_EMAIL_KEY,
|
||||||
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
|
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
|
||||||
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY,
|
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY,
|
||||||
AccountConfig::FORM_COMPLETED_MESSAGE_KEY,
|
AccountConfig::FORM_COMPLETED_MESSAGE_KEY,
|
||||||
|
|||||||
@@ -9,7 +9,14 @@ class ProfileController < ApplicationController
|
|||||||
|
|
||||||
def update_contact
|
def update_contact
|
||||||
if current_user.update(contact_params)
|
if current_user.update(contact_params)
|
||||||
redirect_to settings_profile_index_path, notice: I18n.t('contact_information_has_been_update')
|
if current_user.try(:pending_reconfirmation?) && current_user.previous_changes.key?(:unconfirmed_email)
|
||||||
|
SendConfirmationInstructionsJob.perform_async('user_id' => current_user.id)
|
||||||
|
|
||||||
|
redirect_to settings_profile_index_path,
|
||||||
|
notice: I18n.t('a_confirmation_email_has_been_sent_to_the_new_email_address')
|
||||||
|
else
|
||||||
|
redirect_to settings_profile_index_path, notice: I18n.t('contact_information_has_been_update')
|
||||||
|
end
|
||||||
else
|
else
|
||||||
render :index, status: :unprocessable_content
|
render :index, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class SendSubmissionEmailController < ApplicationController
|
|||||||
|
|
||||||
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
||||||
|
|
||||||
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! unless already_sent?(@submitter)
|
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! if can_send?(@submitter)
|
||||||
|
|
||||||
respond_to do |f|
|
respond_to do |f|
|
||||||
f.html { render :success }
|
f.html { render :success }
|
||||||
@@ -39,8 +39,11 @@ class SendSubmissionEmailController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def already_sent?(submitter)
|
def can_send?(submitter)
|
||||||
EmailEvent.exists?(tag: :submitter_documents_copy, email: submitter.email, emailable: submitter,
|
return false if submitter.account.archived_at?
|
||||||
event_type: :send, created_at: SEND_DURATION.ago..Time.current)
|
return false if EmailEvent.exists?(tag: :submitter_documents_copy, email: submitter.email, emailable: submitter,
|
||||||
|
event_type: :send, created_at: SEND_DURATION.ago..Time.current)
|
||||||
|
|
||||||
|
true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -16,7 +16,9 @@ class StartFormController < ApplicationController
|
|||||||
COOKIES_DEFAULTS = { httponly: true, secure: Rails.env.production? }.freeze
|
COOKIES_DEFAULTS = { httponly: true, secure: Rails.env.production? }.freeze
|
||||||
|
|
||||||
def show
|
def show
|
||||||
raise ActionController::RoutingError, I18n.t('not_found') if @template.preferences['require_phone_2fa']
|
if @template.preferences['require_phone_2fa'] || @template.preferences['require_email_2fa']
|
||||||
|
raise ActionController::RoutingError, I18n.t('not_found')
|
||||||
|
end
|
||||||
|
|
||||||
if @template.shared_link?
|
if @template.shared_link?
|
||||||
@submitter = @template.submissions.new(account_id: @template.account_id)
|
@submitter = @template.submissions.new(account_id: @template.account_id)
|
||||||
@@ -81,7 +83,7 @@ class StartFormController < ApplicationController
|
|||||||
|
|
||||||
@submitter = Submitter.where(submission: @template.submissions)
|
@submitter = Submitter.where(submission: @template.submissions)
|
||||||
.where.not(completed_at: nil)
|
.where.not(completed_at: nil)
|
||||||
.find_by!(required_params)
|
.find_by!(required_params.except('name'))
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class SubmissionEventsController < ApplicationController
|
|||||||
'api_complete_form' => 'check',
|
'api_complete_form' => 'check',
|
||||||
'send_reminder_email' => 'mail_forward',
|
'send_reminder_email' => 'mail_forward',
|
||||||
'send_2fa_sms' => '2fa',
|
'send_2fa_sms' => '2fa',
|
||||||
|
'send_2fa_email' => '2fa',
|
||||||
'send_sms' => 'send',
|
'send_sms' => 'send',
|
||||||
'phone_verified' => 'phone_check',
|
'phone_verified' => 'phone_check',
|
||||||
'email_verified' => 'email_check',
|
'email_verified' => 'email_check',
|
||||||
|
|||||||
@@ -12,6 +12,14 @@ class SubmissionsController < ApplicationController
|
|||||||
authorize!(:create, Submission)
|
authorize!(:create, Submission)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
FIELD_ICONS = {
|
||||||
|
'text' => 'text_size', 'signature' => 'writing_sign', 'date' => 'calendar_event',
|
||||||
|
'number' => 'square_number_1', 'image' => 'photo', 'initials' => 'letter_case_upper',
|
||||||
|
'file' => 'paperclip', 'select' => 'select', 'checkbox' => 'checkbox', 'radio' => 'circle_dot',
|
||||||
|
'stamp' => 'rubber_stamp', 'cells' => 'columns_3', 'multiple' => 'checks', 'phone' => 'phone_check',
|
||||||
|
'payment' => 'credit_card', 'verification' => 'id', 'kba' => 'user_scan'
|
||||||
|
}.freeze
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@submission = Submissions.preload_with_pages(@submission)
|
@submission = Submissions.preload_with_pages(@submission)
|
||||||
|
|
||||||
@@ -45,14 +53,15 @@ class SubmissionsController < ApplicationController
|
|||||||
else
|
else
|
||||||
submissions_attrs = submissions_params[:submission].to_h.values
|
submissions_attrs = submissions_params[:submission].to_h.values
|
||||||
|
|
||||||
submissions_attrs, =
|
submissions_attrs, _, new_fields =
|
||||||
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_attrs, @template)
|
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_attrs, @template, add_fields: true)
|
||||||
|
|
||||||
Submissions.create_from_submitters(template: @template,
|
Submissions.create_from_submitters(template: @template,
|
||||||
user: current_user,
|
user: current_user,
|
||||||
source: :invite,
|
source: :invite,
|
||||||
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
|
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
|
||||||
submissions_attrs:,
|
submissions_attrs:,
|
||||||
|
new_fields:,
|
||||||
params: params.merge('send_completed_email' => true))
|
params: params.merge('send_completed_email' => true))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ class SubmissionsPreviewController < ApplicationController
|
|||||||
|
|
||||||
def completed
|
def completed
|
||||||
@submission = Submission.find_by!(slug: params[:submissions_preview_slug])
|
@submission = Submission.find_by!(slug: params[:submissions_preview_slug])
|
||||||
|
|
||||||
|
raise ActionController::RoutingError, I18n.t('not_found') if @submission.account.archived_at?
|
||||||
|
|
||||||
@template = @submission.template
|
@template = @submission.template
|
||||||
|
|
||||||
render :completed, layout: 'form'
|
render :completed, layout: 'form'
|
||||||
@@ -50,8 +53,11 @@ class SubmissionsPreviewController < ApplicationController
|
|||||||
|
|
||||||
def use_signature?(submission)
|
def use_signature?(submission)
|
||||||
return false if current_user && can?(:read, submission)
|
return false if current_user && can?(:read, submission)
|
||||||
return true if submission.submitters.any? { |e| e.preferences['require_phone_2fa'] }
|
return true if submission.submitters.any? do |e|
|
||||||
|
e.preferences['require_phone_2fa'] || e.preferences['require_email_2fa']
|
||||||
|
end
|
||||||
return true if submission.template&.preferences&.dig('require_phone_2fa')
|
return true if submission.template&.preferences&.dig('require_phone_2fa')
|
||||||
|
return true if submission.template&.preferences&.dig('require_email_2fa')
|
||||||
|
|
||||||
!submission_valid_ttl?(submission)
|
!submission_valid_ttl?(submission)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class SubmitFormController < ApplicationController
|
|||||||
submission = @submitter.submission
|
submission = @submitter.submission
|
||||||
|
|
||||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||||
|
return render :email_2fa if require_email_2fa?(@submitter)
|
||||||
|
|
||||||
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
|
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
|
||||||
|
|
||||||
@@ -47,6 +48,11 @@ class SubmitFormController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
if require_email_2fa?(@submitter)
|
||||||
|
return render json: { error: I18n.t('verification_required_refresh_the_page_and_pass_2fa') },
|
||||||
|
status: :unprocessable_content
|
||||||
|
end
|
||||||
|
|
||||||
if @submitter.completed_at?
|
if @submitter.completed_at?
|
||||||
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_content
|
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
@@ -75,7 +81,11 @@ class SubmitFormController < ApplicationController
|
|||||||
render json: { error: e.message }, status: :unprocessable_content
|
render json: { error: e.message }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
def completed; end
|
def completed
|
||||||
|
raise ActionController::RoutingError, I18n.t('not_found') if @submitter.account.archived_at?
|
||||||
|
|
||||||
|
redirect_to submit_form_path(params[:submit_form_slug]) if require_email_2fa?(@submitter)
|
||||||
|
end
|
||||||
|
|
||||||
def success; end
|
def success; end
|
||||||
|
|
||||||
@@ -107,4 +117,12 @@ class SubmitFormController < ApplicationController
|
|||||||
ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments)
|
ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments)
|
||||||
.preload(:blob).index_by(&:uuid)
|
.preload(:blob).index_by(&:uuid)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def require_email_2fa?(submitter)
|
||||||
|
return false if submitter.submission.template&.preferences&.dig('require_email_2fa') != true &&
|
||||||
|
submitter.preferences['require_email_2fa'] != true
|
||||||
|
return false if cookies.encrypted[:email_2fa_slug] == submitter.slug
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmitFormEmail2fasController < ApplicationController
|
||||||
|
around_action :with_browser_locale
|
||||||
|
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
before_action :load_submitter
|
||||||
|
|
||||||
|
COOKIES_TTL = 12.hours
|
||||||
|
COOKIES_DEFAULTS = { httponly: true, secure: Rails.env.production? }.freeze
|
||||||
|
|
||||||
|
def create
|
||||||
|
RateLimit.call("verify-2fa-code-#{@submitter.id}", limit: 2, ttl: 45.seconds, enabled: true)
|
||||||
|
|
||||||
|
value = [@submitter.email.downcase.strip, @submitter.slug].join(':')
|
||||||
|
|
||||||
|
if EmailVerificationCodes.verify(params[:one_time_code].to_s.gsub(/\D/, ''), value)
|
||||||
|
SubmissionEvents.create_with_tracking_data(@submitter, 'email_verified', request, { email: @submitter.email })
|
||||||
|
|
||||||
|
cookies.encrypted[:email_2fa_slug] =
|
||||||
|
{ value: @submitter.slug, expires: COOKIES_TTL.from_now, **COOKIES_DEFAULTS }
|
||||||
|
|
||||||
|
redirect_to submit_form_path(@submitter.slug)
|
||||||
|
else
|
||||||
|
redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:invalid_code)
|
||||||
|
end
|
||||||
|
rescue RateLimit::LimitApproached
|
||||||
|
redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:too_many_attempts)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
if @submitter.submission_events.where(event_type: 'send_2fa_email').exists?(created_at: 15.seconds.ago..)
|
||||||
|
return redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:rate_limit_exceeded)
|
||||||
|
end
|
||||||
|
|
||||||
|
RateLimit.call("send-email-code-#{@submitter.id}", limit: 2, ttl: 45.seconds, enabled: true)
|
||||||
|
|
||||||
|
SendSubmitterVerificationEmailJob.perform_async('submitter_id' => @submitter.id, 'locale' => I18n.locale.to_s)
|
||||||
|
|
||||||
|
redir_params = params[:resend] ? { alert: I18n.t(:code_has_been_resent) } : {}
|
||||||
|
|
||||||
|
redirect_to submit_form_path(@submitter.slug, status: :sent), **redir_params
|
||||||
|
rescue RateLimit::LimitApproached
|
||||||
|
redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:too_many_attempts)
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_submitter
|
||||||
|
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -3,6 +3,12 @@
|
|||||||
class TemplateDocumentsController < ApplicationController
|
class TemplateDocumentsController < ApplicationController
|
||||||
load_and_authorize_resource :template
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
FILES_TTL = 5.minutes
|
||||||
|
|
||||||
|
def index
|
||||||
|
render json: @template.schema_documents.map { |d| ActiveStorage::Blob.proxy_url(d.blob, expires_at: FILES_TTL.from_now.to_i) }
|
||||||
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
if params[:blobs].blank? && params[:files].blank?
|
if params[:blobs].blank? && params[:files].blank?
|
||||||
return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_content
|
return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_content
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesCloneController < ApplicationController
|
||||||
|
load_and_authorize_resource :template, instance_name: :base_template
|
||||||
|
|
||||||
|
def new
|
||||||
|
authorize!(:create, Template)
|
||||||
|
|
||||||
|
@template = Template.new(name: "#{@base_template.name} (#{I18n.t('clone')})")
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
ActiveRecord::Associations::Preloader.new(
|
||||||
|
records: [@base_template],
|
||||||
|
associations: [schema_documents: :preview_images_attachments]
|
||||||
|
).call
|
||||||
|
|
||||||
|
@template = Templates::Clone.call(@base_template, author: current_user,
|
||||||
|
name: params.dig(:template, :name),
|
||||||
|
folder_name: params[:folder_name])
|
||||||
|
|
||||||
|
authorize!(:create, @template)
|
||||||
|
|
||||||
|
if params[:account_id].present? && true_ability.authorize!(:manage, Account.find(params[:account_id]))
|
||||||
|
@template.account_id = params[:account_id]
|
||||||
|
@template.author = true_user if true_user.account_id == @template.account_id
|
||||||
|
@template.folder = @template.account.default_template_folder if @template.account_id != current_account.id
|
||||||
|
else
|
||||||
|
@template.account = current_account
|
||||||
|
end
|
||||||
|
|
||||||
|
Templates.maybe_assign_access(@template)
|
||||||
|
|
||||||
|
if @template.save
|
||||||
|
Templates::CloneAttachments.call(template: @template, original_template: @base_template)
|
||||||
|
|
||||||
|
SearchEntries.enqueue_reindex(@template)
|
||||||
|
|
||||||
|
WebhookUrls.enqueue_events(@template, 'template.created')
|
||||||
|
|
||||||
|
maybe_redirect_to_template(@template)
|
||||||
|
else
|
||||||
|
render turbo_stream: turbo_stream.replace(:modal, partial: 'templates_clone/form'), status: :unprocessable_content
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def maybe_redirect_to_template(template)
|
||||||
|
if template.account == current_account
|
||||||
|
redirect_to(edit_template_path(template))
|
||||||
|
else
|
||||||
|
redirect_back(fallback_location: root_path, notice: I18n.t('template_has_been_cloned'))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -3,8 +3,6 @@
|
|||||||
class TemplatesController < ApplicationController
|
class TemplatesController < ApplicationController
|
||||||
load_and_authorize_resource :template
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
before_action :load_base_template, only: %i[new create]
|
|
||||||
|
|
||||||
def show
|
def show
|
||||||
submissions = @template.submissions.accessible_by(current_ability)
|
submissions = @template.submissions.accessible_by(current_ability)
|
||||||
submissions = submissions.active if @template.archived_at.blank?
|
submissions = submissions.active if @template.archived_at.blank?
|
||||||
@@ -26,9 +24,7 @@ class TemplatesController < ApplicationController
|
|||||||
redirect_to root_path
|
redirect_to root_path
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new; end
|
||||||
@template.name = "#{@base_template.name} (#{I18n.t('clone')})" if @base_template
|
|
||||||
end
|
|
||||||
|
|
||||||
def edit
|
def edit
|
||||||
ActiveRecord::Associations::Preloader.new(
|
ActiveRecord::Associations::Preloader.new(
|
||||||
@@ -48,37 +44,18 @@ class TemplatesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
if @base_template
|
@template.author = current_user
|
||||||
ActiveRecord::Associations::Preloader.new(
|
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
||||||
records: [@base_template],
|
@template.account = current_account
|
||||||
associations: [schema_documents: :preview_images_attachments]
|
|
||||||
).call
|
|
||||||
|
|
||||||
@template = Templates::Clone.call(@base_template, author: current_user,
|
|
||||||
name: params.dig(:template, :name),
|
|
||||||
folder_name: params[:folder_name])
|
|
||||||
else
|
|
||||||
@template.author = current_user
|
|
||||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
|
||||||
end
|
|
||||||
|
|
||||||
if params[:account_id].present? && authorized_clone_account_id?(params[:account_id])
|
|
||||||
@template.account_id = params[:account_id]
|
|
||||||
@template.folder = @template.account.default_template_folder if @template.account_id != current_account.id
|
|
||||||
else
|
|
||||||
@template.account = current_account
|
|
||||||
end
|
|
||||||
|
|
||||||
Templates.maybe_assign_access(@template)
|
Templates.maybe_assign_access(@template)
|
||||||
|
|
||||||
if @template.save
|
if @template.save
|
||||||
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
|
||||||
|
|
||||||
SearchEntries.enqueue_reindex(@template)
|
SearchEntries.enqueue_reindex(@template)
|
||||||
|
|
||||||
WebhookUrls.enqueue_events(@template, 'template.created')
|
WebhookUrls.enqueue_events(@template, 'template.created')
|
||||||
|
|
||||||
maybe_redirect_to_template(@template)
|
redirect_to(edit_template_path(@template))
|
||||||
else
|
else
|
||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_content
|
render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_content
|
||||||
end
|
end
|
||||||
@@ -132,23 +109,4 @@ class TemplatesController < ApplicationController
|
|||||||
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def authorized_clone_account_id?(account_id)
|
|
||||||
true_user.account_id.to_s == account_id.to_s ||
|
|
||||||
true_user.account.linked_accounts.accessible_by(current_ability).exists?(id: account_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def maybe_redirect_to_template(template)
|
|
||||||
if template.account == current_account
|
|
||||||
redirect_to(edit_template_path(@template))
|
|
||||||
else
|
|
||||||
redirect_back(fallback_location: root_path, notice: I18n.t('template_has_been_cloned'))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def load_base_template
|
|
||||||
return if params[:base_template_id].blank?
|
|
||||||
|
|
||||||
@base_template = Template.accessible_by(current_ability).find_by(id: params[:base_template_id])
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
@@ -6,12 +6,18 @@ class TemplatesDebugController < ApplicationController
|
|||||||
DEBUG_FILE = ''
|
DEBUG_FILE = ''
|
||||||
|
|
||||||
def show
|
def show
|
||||||
attachment = @template.documents.first
|
schema_uuids = @template.schema.index_by { |e| e['attachment_uuid'] }
|
||||||
|
attachment = @template.documents.find { |a| schema_uuids[a.uuid] }
|
||||||
|
|
||||||
data = attachment.download
|
data = attachment.download
|
||||||
pdf = HexaPDF::Document.new(io: StringIO.new(data))
|
|
||||||
|
|
||||||
fields = Templates::FindAcroFields.call(pdf, attachment, data)
|
unless attachment.image?
|
||||||
|
pdf = HexaPDF::Document.new(io: StringIO.new(data))
|
||||||
|
|
||||||
|
fields = Templates::FindAcroFields.call(pdf, attachment, data)
|
||||||
|
end
|
||||||
|
|
||||||
|
fields, = Templates::DetectFields.call(StringIO.new(data), attachment:) if fields.blank?
|
||||||
|
|
||||||
attachment.metadata['pdf'] ||= {}
|
attachment.metadata['pdf'] ||= {}
|
||||||
attachment.metadata['pdf']['fields'] = fields
|
attachment.metadata['pdf']['fields'] = fields
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesDetectFieldsController < ApplicationController
|
||||||
|
include ActionController::Live
|
||||||
|
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def create
|
||||||
|
response.headers['Content-Type'] = 'text/event-stream'
|
||||||
|
|
||||||
|
sse = SSE.new(response.stream)
|
||||||
|
|
||||||
|
documents = @template.schema_documents.preload(:blob)
|
||||||
|
documents = documents.where(uuid: params[:attachment_uuid]) if params[:attachment_uuid].present?
|
||||||
|
|
||||||
|
page_number = params[:page].presence&.to_i
|
||||||
|
|
||||||
|
documents.each do |document|
|
||||||
|
io = StringIO.new(document.download)
|
||||||
|
|
||||||
|
Templates::DetectFields.call(io, attachment: document, page_number:) do |(attachment_uuid, page, fields)|
|
||||||
|
sse.write({ attachment_uuid:, page:, fields: })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
sse.write({ completed: true })
|
||||||
|
ensure
|
||||||
|
response.stream.close
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -3,6 +3,15 @@
|
|||||||
class TemplatesPreferencesController < ApplicationController
|
class TemplatesPreferencesController < ApplicationController
|
||||||
load_and_authorize_resource :template
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
RESETTABLE_PREFERENCE_KEYS = {
|
||||||
|
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY => %w[request_email_subject request_email_body submitters],
|
||||||
|
AccountConfig::SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => %w[invitation_reminder_email_subject
|
||||||
|
invitation_reminder_email_body],
|
||||||
|
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => %w[documents_copy_email_subject documents_copy_email_body],
|
||||||
|
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY => %w[completed_notification_email_subject
|
||||||
|
completed_notification_email_body]
|
||||||
|
}.freeze
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@@ -15,19 +24,38 @@ class TemplatesPreferencesController < ApplicationController
|
|||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
authorize!(:update, @template)
|
||||||
|
|
||||||
|
config_key = params[:config_key]
|
||||||
|
preferences_to_delete = RESETTABLE_PREFERENCE_KEYS[config_key]
|
||||||
|
|
||||||
|
return head :ok if preferences_to_delete.blank?
|
||||||
|
|
||||||
|
preferences_to_delete.each do |key|
|
||||||
|
@template.preferences.delete(key)
|
||||||
|
end
|
||||||
|
|
||||||
|
@template.save!
|
||||||
|
|
||||||
|
render turbo_stream: turbo_stream.replace("#{config_key}_form",
|
||||||
|
partial: "templates_preferences/#{config_key}_form"),
|
||||||
|
status: :ok
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def template_params
|
def template_params
|
||||||
params.require(:template).permit(
|
params.require(:template).permit(
|
||||||
preferences: %i[bcc_completed request_email_subject request_email_body
|
preferences: %i[bcc_completed request_email_subject request_email_body
|
||||||
|
invitation_reminder_email_subject invitation_reminder_email_body
|
||||||
documents_copy_email_subject documents_copy_email_body
|
documents_copy_email_subject documents_copy_email_body
|
||||||
documents_copy_email_enabled documents_copy_email_attach_audit
|
documents_copy_email_enabled documents_copy_email_attach_audit
|
||||||
documents_copy_email_attach_documents documents_copy_email_reply_to
|
documents_copy_email_attach_documents documents_copy_email_reply_to
|
||||||
completed_notification_email_attach_documents
|
completed_notification_email_attach_documents
|
||||||
completed_redirect_url validate_unique_submitters
|
completed_redirect_url validate_unique_submitters
|
||||||
require_all_submitters submitters_order require_phone_2fa
|
require_all_submitters submitters_order require_phone_2fa require_email_2fa
|
||||||
default_expire_at_duration shared_link_2fa
|
default_expire_at_duration shared_link_2fa default_expire_at request_email_enabled
|
||||||
default_expire_at request_email_enabled
|
|
||||||
completed_notification_email_subject completed_notification_email_body
|
completed_notification_email_subject completed_notification_email_body
|
||||||
completed_notification_email_enabled completed_notification_email_attach_audit] +
|
completed_notification_email_enabled completed_notification_email_attach_audit] +
|
||||||
[completed_message: %i[title body],
|
[completed_message: %i[title body],
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class TimestampServerController < ApplicationController
|
|||||||
uri = Addressable::URI.parse(url)
|
uri = Addressable::URI.parse(url)
|
||||||
|
|
||||||
conn = Faraday.new(uri.origin) do |c|
|
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
|
end
|
||||||
|
|
||||||
response = conn.post(uri.path, req.to_der,
|
response = conn.post(uri.path, req.to_der,
|
||||||
|
|||||||
@@ -24,10 +24,19 @@ class UsersController < ApplicationController
|
|||||||
def edit; end
|
def edit; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
if User.accessible_by(current_ability).exists?(email: @user.email)
|
existing_user = User.accessible_by(current_ability).find_by(email: @user.email)
|
||||||
@user.errors.add(:email, I18n.t('already_exists'))
|
|
||||||
|
|
||||||
return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_content
|
if existing_user
|
||||||
|
if existing_user.archived_at? &&
|
||||||
|
current_ability.can?(:manage, existing_user) && current_ability.can?(:manage, @user.account)
|
||||||
|
existing_user.assign_attributes(@user.slice(:first_name, :last_name, :role, :account_id))
|
||||||
|
existing_user.archived_at = nil
|
||||||
|
@user = existing_user
|
||||||
|
else
|
||||||
|
@user.errors.add(:email, I18n.t('already_exists'))
|
||||||
|
|
||||||
|
return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_content
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@user.password = SecureRandom.hex if @user.password.blank?
|
@user.password = SecureRandom.hex if @user.password.blank?
|
||||||
@@ -45,7 +54,8 @@ class UsersController < ApplicationController
|
|||||||
def update
|
def update
|
||||||
return redirect_to settings_users_path, notice: I18n.t('unable_to_update_user') if Docuseal.demo?
|
return redirect_to settings_users_path, notice: I18n.t('unable_to_update_user') if Docuseal.demo?
|
||||||
|
|
||||||
attrs = user_params.compact_blank.merge(user_params.slice(:archived_at))
|
attrs = user_params.compact_blank
|
||||||
|
attrs = attrs.merge(user_params.slice(:archived_at)) if current_ability.can?(:create, @user)
|
||||||
|
|
||||||
if params.dig(:user, :account_id).present?
|
if params.dig(:user, :account_id).present?
|
||||||
account = Account.accessible_by(current_ability).find(params.dig(:user, :account_id))
|
account = Account.accessible_by(current_ability).find(params.dig(:user, :account_id))
|
||||||
@@ -56,7 +66,14 @@ class UsersController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
if @user.update(attrs.except(*(current_user == @user ? %i[password otp_required_for_login role] : %i[password])))
|
if @user.update(attrs.except(*(current_user == @user ? %i[password otp_required_for_login role] : %i[password])))
|
||||||
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_updated')
|
if @user.try(:pending_reconfirmation?) && @user.previous_changes.key?(:unconfirmed_email)
|
||||||
|
SendConfirmationInstructionsJob.perform_async('user_id' => @user.id)
|
||||||
|
|
||||||
|
redirect_back fallback_location: settings_users_path,
|
||||||
|
notice: I18n.t('a_confirmation_email_has_been_sent_to_the_new_email_address')
|
||||||
|
else
|
||||||
|
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_updated')
|
||||||
|
end
|
||||||
else
|
else
|
||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_content
|
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class WebhookSettingsController < ApplicationController
|
|||||||
|
|
||||||
@webhook_events = @webhook_events.where(status: params[:status]) if %w[success error].include?(params[:status])
|
@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
|
render :show
|
||||||
end
|
end
|
||||||
@@ -26,7 +26,7 @@ class WebhookSettingsController < ApplicationController
|
|||||||
|
|
||||||
@webhook_events = @webhook_events.where(status: params[:status]) if %w[success error].include?(params[:status])
|
@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
|
end
|
||||||
|
|
||||||
def new; end
|
def new; end
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ import ToggleClasses from './elements/toggle_classes'
|
|||||||
import AutosizeField from './elements/autosize_field'
|
import AutosizeField from './elements/autosize_field'
|
||||||
import GoogleDriveFilePicker from './elements/google_drive_file_picker'
|
import GoogleDriveFilePicker from './elements/google_drive_file_picker'
|
||||||
import OpenModal from './elements/open_modal'
|
import OpenModal from './elements/open_modal'
|
||||||
|
import BarChart from './elements/bar_chart'
|
||||||
|
import FieldCondition from './elements/field_condition'
|
||||||
|
|
||||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||||
|
|
||||||
@@ -140,6 +142,8 @@ safeRegisterElement('toggle-classes', ToggleClasses)
|
|||||||
safeRegisterElement('autosize-field', AutosizeField)
|
safeRegisterElement('autosize-field', AutosizeField)
|
||||||
safeRegisterElement('google-drive-file-picker', GoogleDriveFilePicker)
|
safeRegisterElement('google-drive-file-picker', GoogleDriveFilePicker)
|
||||||
safeRegisterElement('open-modal', OpenModal)
|
safeRegisterElement('open-modal', OpenModal)
|
||||||
|
safeRegisterElement('bar-chart', BarChart)
|
||||||
|
safeRegisterElement('field-condition', FieldCondition)
|
||||||
|
|
||||||
safeRegisterElement('template-builder', class extends HTMLElement {
|
safeRegisterElement('template-builder', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
@@ -151,13 +155,17 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
|||||||
|
|
||||||
this.app = createApp(TemplateBuilder, {
|
this.app = createApp(TemplateBuilder, {
|
||||||
template: reactive(JSON.parse(this.dataset.template)),
|
template: reactive(JSON.parse(this.dataset.template)),
|
||||||
|
customFields: reactive(JSON.parse(this.dataset.customFields || '[]')),
|
||||||
backgroundColor: '#faf7f5',
|
backgroundColor: '#faf7f5',
|
||||||
locale: this.dataset.locale,
|
locale: this.dataset.locale,
|
||||||
withPhone: this.dataset.withPhone === 'true',
|
withPhone: this.dataset.withPhone === 'true',
|
||||||
withVerification: ['true', 'false'].includes(this.dataset.withVerification) ? this.dataset.withVerification === 'true' : null,
|
withVerification: ['true', 'false'].includes(this.dataset.withVerification) ? this.dataset.withVerification === 'true' : null,
|
||||||
|
withKba: ['true', 'false'].includes(this.dataset.withKba) ? this.dataset.withKba === 'true' : null,
|
||||||
withLogo: this.dataset.withLogo !== 'false',
|
withLogo: this.dataset.withLogo !== 'false',
|
||||||
|
withFieldsDetection: this.dataset.withFieldsDetection === 'true',
|
||||||
editable: this.dataset.editable !== 'false',
|
editable: this.dataset.editable !== 'false',
|
||||||
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
||||||
|
withCustomFields: true,
|
||||||
withPayment: this.dataset.withPayment === 'true',
|
withPayment: this.dataset.withPayment === 'true',
|
||||||
isPaymentConnected: this.dataset.isPaymentConnected === 'true',
|
isPaymentConnected: this.dataset.isPaymentConnected === 'true',
|
||||||
withFormula: this.dataset.withFormula === 'true',
|
withFormula: this.dataset.withFormula === 'true',
|
||||||
@@ -166,6 +174,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
|||||||
withConditions: this.dataset.withConditions === 'true',
|
withConditions: this.dataset.withConditions === 'true',
|
||||||
withGoogleDrive: this.dataset.withGoogleDrive === 'true',
|
withGoogleDrive: this.dataset.withGoogleDrive === 'true',
|
||||||
withReplaceAndCloneUpload: true,
|
withReplaceAndCloneUpload: true,
|
||||||
|
withDownload: true,
|
||||||
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
||||||
acceptFileTypes: this.dataset.acceptFileTypes,
|
acceptFileTypes: this.dataset.acceptFileTypes,
|
||||||
showTourStartForm: this.dataset.showTourStartForm === 'true'
|
showTourStartForm: this.dataset.showTourStartForm === 'true'
|
||||||
|
|||||||
+51
-6
@@ -4,15 +4,26 @@ import { isValidSignatureCanvas } from './submission_form/validate_signature'
|
|||||||
|
|
||||||
window.customElements.define('draw-signature', class extends HTMLElement {
|
window.customElements.define('draw-signature', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
const scale = 3
|
this.setCanvasSize()
|
||||||
|
|
||||||
this.canvas.width = this.canvas.parentNode.clientWidth * scale
|
|
||||||
this.canvas.height = this.canvas.parentNode.clientHeight * scale
|
|
||||||
|
|
||||||
this.canvas.getContext('2d').scale(scale, scale)
|
|
||||||
|
|
||||||
this.pad = new SignaturePad(this.canvas)
|
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) {
|
if (this.dataset.color) {
|
||||||
this.pad.penColor = this.dataset.color
|
this.pad.penColor = this.dataset.color
|
||||||
}
|
}
|
||||||
@@ -57,6 +68,40 @@ window.customElements.define('draw-signature', class extends HTMLElement {
|
|||||||
this.updateSubmitButtonVisibility()
|
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 () {
|
updateSubmitButtonVisibility () {
|
||||||
if (this.pad.isEmpty()) {
|
if (this.pad.isEmpty()) {
|
||||||
this.submitButton.style.display = 'none'
|
this.submitButton.style.display = 'none'
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.chartLabels = JSON.parse(this.dataset.labels || '[]')
|
||||||
|
this.chartDatasets = JSON.parse(this.dataset.datasets || '[]')
|
||||||
|
|
||||||
|
this.initChart()
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
if (this.chartInstance) {
|
||||||
|
this.chartInstance.destroy()
|
||||||
|
this.chartInstance = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async initChart () {
|
||||||
|
const { default: Chart } = await import(/* webpackChunkName: "chartjs" */ 'chart.js/auto')
|
||||||
|
|
||||||
|
const canvas = this.querySelector('canvas')
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
|
||||||
|
this.chartInstance = new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: this.chartLabels,
|
||||||
|
datasets: this.chartDatasets
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: true,
|
||||||
|
animation: false,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
grace: '20%',
|
||||||
|
ticks: {
|
||||||
|
precision: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.targetId = this.dataset.targetId
|
||||||
|
this.fieldId = this.dataset.fieldId
|
||||||
|
this.action = (this.dataset.action || '').trim()
|
||||||
|
this.expectedValue = this.dataset.value
|
||||||
|
|
||||||
|
this.targetEl = document.getElementById(this.targetId)
|
||||||
|
this.sourceEl = document.getElementById(this.fieldId)
|
||||||
|
|
||||||
|
this.bindListeners()
|
||||||
|
|
||||||
|
this.evaluateAndApply()
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
this.unbindListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
bindListeners () {
|
||||||
|
this.eventsFor(this.sourceEl).forEach((ev) => {
|
||||||
|
this.sourceEl.addEventListener(ev, this.evaluateAndApply)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
unbindListeners () {
|
||||||
|
this.eventsFor(this.sourceEl).forEach((ev) => {
|
||||||
|
this.sourceEl.removeEventListener(ev, this.evaluateAndApply)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
eventsFor (el) {
|
||||||
|
if (!el) return []
|
||||||
|
|
||||||
|
const tag = el.tagName.toLowerCase()
|
||||||
|
|
||||||
|
if (tag === 'textarea') return ['input']
|
||||||
|
if (tag === 'input') return ['input', 'change']
|
||||||
|
|
||||||
|
return ['change']
|
||||||
|
}
|
||||||
|
|
||||||
|
evaluateAndApply = () => {
|
||||||
|
const fieldConditions = document.querySelectorAll(`field-condition[data-target-id="${this.targetId}"]`)
|
||||||
|
|
||||||
|
const result = [...fieldConditions].reduce((acc, cond) => {
|
||||||
|
if (cond.dataset.operation === 'or') {
|
||||||
|
acc.push(acc.pop() || cond.checkCondition())
|
||||||
|
} else {
|
||||||
|
acc.push(cond.checkCondition())
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
this.apply(!result.includes(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
checkCondition () {
|
||||||
|
const action = this.action
|
||||||
|
const actual = this.getSourceValue()
|
||||||
|
const expected = this.expectedValue
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'contains') return this.contains(actual, expected)
|
||||||
|
|
||||||
|
if (action === 'not_equal') {
|
||||||
|
const list = Array.isArray(actual) ? actual : [actual]
|
||||||
|
return !list.filter((v) => v !== null && v !== undefined).map(String).includes(String(expected))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'does_not_contain') return !this.contains(actual, expected)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
getSourceValue () {
|
||||||
|
const el = this.sourceEl
|
||||||
|
|
||||||
|
if (!el) return
|
||||||
|
|
||||||
|
const tag = el.tagName.toLowerCase()
|
||||||
|
const type = (el.getAttribute('type') || '').toLowerCase()
|
||||||
|
|
||||||
|
if (tag === 'select') return el.value
|
||||||
|
if (tag === 'textarea') return el.value
|
||||||
|
if (tag === 'input' && type === 'checkbox') return el.checked ? (el.value || '1') : null
|
||||||
|
if (tag === 'input') return el.value
|
||||||
|
|
||||||
|
return el.value ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
isEmpty (obj) {
|
||||||
|
if (obj == null) return true
|
||||||
|
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
return obj.length === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj === 'string') {
|
||||||
|
return obj.trim().length === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj === 'object') {
|
||||||
|
return Object.keys(obj).length === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj === false) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
contains (actual, expected) {
|
||||||
|
if (expected === null || expected === undefined) return false
|
||||||
|
|
||||||
|
const exp = String(expected)
|
||||||
|
|
||||||
|
if (Array.isArray(actual)) return actual.filter((v) => v !== null && v !== undefined).map(String).includes(exp)
|
||||||
|
|
||||||
|
if (typeof actual === 'string') return actual.includes(exp)
|
||||||
|
|
||||||
|
return actual !== null && actual !== undefined && String(actual) === exp
|
||||||
|
}
|
||||||
|
|
||||||
|
apply (passed) {
|
||||||
|
const controls = this.targetEl.matches('input, select, textarea, button')
|
||||||
|
? [this.targetEl]
|
||||||
|
: Array.from(this.targetEl.querySelectorAll('input, select, textarea, button'))
|
||||||
|
|
||||||
|
if (passed) {
|
||||||
|
this.targetEl.style.display = ''
|
||||||
|
this.targetEl.labels.forEach((label) => { label.style.display = '' })
|
||||||
|
|
||||||
|
controls.forEach((c) => (c.disabled = false))
|
||||||
|
} else {
|
||||||
|
this.targetEl.style.display = 'none'
|
||||||
|
this.targetEl.labels.forEach((label) => { label.style.display = 'none' })
|
||||||
|
|
||||||
|
controls.forEach((c) => (c.disabled = true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,17 +5,28 @@ export default targetable(class extends HTMLElement {
|
|||||||
static [target.static] = ['canvas', 'input', 'clear', 'button']
|
static [target.static] = ['canvas', 'input', 'clear', 'button']
|
||||||
|
|
||||||
async connectedCallback () {
|
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')
|
const { default: SignaturePad } = await import('signature_pad')
|
||||||
|
|
||||||
|
this.setCanvasSize()
|
||||||
|
|
||||||
this.pad = new SignaturePad(this.canvas)
|
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) => {
|
this.clear.addEventListener('click', (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
@@ -47,4 +58,38 @@ export default targetable(class extends HTMLElement {
|
|||||||
|
|
||||||
this.closest('form').requestSubmit()
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
@@ -37,6 +37,7 @@ safeRegisterElement('submission-form', class extends HTMLElement {
|
|||||||
withSignatureId: this.dataset.withSignatureId === 'true',
|
withSignatureId: this.dataset.withSignatureId === 'true',
|
||||||
requireSigningReason: this.dataset.requireSigningReason === 'true',
|
requireSigningReason: this.dataset.requireSigningReason === 'true',
|
||||||
withConfetti: this.dataset.withConfetti !== 'false',
|
withConfetti: this.dataset.withConfetti !== 'false',
|
||||||
|
withFieldLabels: this.dataset.withFieldLabels !== 'false',
|
||||||
withDisclosure: this.dataset.withDisclosure === 'true',
|
withDisclosure: this.dataset.withDisclosure === 'true',
|
||||||
reuseSignature: this.dataset.reuseSignature !== 'false',
|
reuseSignature: this.dataset.reuseSignature !== 'false',
|
||||||
withTypedSignature: this.dataset.withTypedSignature !== 'false',
|
withTypedSignature: this.dataset.withTypedSignature !== 'false',
|
||||||
|
|||||||
@@ -60,6 +60,11 @@
|
|||||||
class="object-contain mx-auto"
|
class="object-contain mx-auto"
|
||||||
:src="stamp.url"
|
:src="stamp.url"
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
v-else-if="field.type === 'kba' && kba"
|
||||||
|
class="object-contain mx-auto"
|
||||||
|
:src="kba.url"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-else-if="field.type === 'signature' && signature"
|
v-else-if="field.type === 'signature' && signature"
|
||||||
class="flex justify-between h-full gap-1 overflow-hidden w-full"
|
class="flex justify-between h-full gap-1 overflow-hidden w-full"
|
||||||
@@ -177,8 +182,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="field.type === 'cells'"
|
v-else-if="field.type === 'cells'"
|
||||||
class="w-full flex items-center"
|
class="w-full flex"
|
||||||
:class="{ 'justify-end': field.preferences?.align === 'right', ...fontClasses }"
|
:class="{ 'justify-end': field.preferences?.align === 'right', ...alignClasses, ...fontClasses }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(char, index) in modelValue"
|
v-for="(char, index) in modelValue"
|
||||||
@@ -272,7 +277,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import MarkdownContent from './markdown_content'
|
import MarkdownContent from './markdown_content'
|
||||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconId } from '@tabler/icons-vue'
|
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconId, IconUserScan } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FieldArea',
|
name: 'FieldArea',
|
||||||
@@ -391,7 +396,8 @@ export default {
|
|||||||
stamp: this.t('stamp'),
|
stamp: this.t('stamp'),
|
||||||
payment: this.t('payment'),
|
payment: this.t('payment'),
|
||||||
phone: this.t('phone'),
|
phone: this.t('phone'),
|
||||||
verification: this.t('verify_id')
|
verification: this.t('verify_id'),
|
||||||
|
kba: this.t('kba')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
strikethroughWidth () {
|
strikethroughWidth () {
|
||||||
@@ -454,7 +460,8 @@ export default {
|
|||||||
multiple: IconChecks,
|
multiple: IconChecks,
|
||||||
phone: IconPhoneCheck,
|
phone: IconPhoneCheck,
|
||||||
payment: IconCreditCard,
|
payment: IconCreditCard,
|
||||||
verification: IconId
|
verification: IconId,
|
||||||
|
kba: IconUserScan
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
image () {
|
image () {
|
||||||
@@ -471,6 +478,13 @@ export default {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
kba () {
|
||||||
|
if (this.field.type === 'kba') {
|
||||||
|
return this.attachmentsIndex[this.modelValue]
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
signature () {
|
signature () {
|
||||||
if (this.field.type === 'signature') {
|
if (this.field.type === 'signature') {
|
||||||
return this.attachmentsIndex[this.modelValue]
|
return this.attachmentsIndex[this.modelValue]
|
||||||
@@ -553,10 +567,14 @@ export default {
|
|||||||
style.color = this.field.preferences.color
|
style.color = this.field.preferences.color
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.field.preferences?.background) {
|
||||||
|
style.background = this.field.preferences.background
|
||||||
|
}
|
||||||
|
|
||||||
return style
|
return style
|
||||||
},
|
},
|
||||||
isNarrow () {
|
isNarrow () {
|
||||||
return this.area.h > 0 && (this.area.w / this.area.h) > 6
|
return this.area.h > 0 && ((this.area.w * this.pageWidth) / (this.area.h * this.pageHeight)) > 4.5
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|||||||
@@ -163,12 +163,20 @@ export default {
|
|||||||
return fetch(this.baseUrl + '/api/attachments', {
|
return fetch(this.baseUrl + '/api/attachments', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
}).then(resp => resp.json()).then((data) => {
|
}).then(async (resp) => {
|
||||||
return data
|
const data = await resp.json()
|
||||||
|
|
||||||
|
if (resp.status === 422) {
|
||||||
|
alert(data.error)
|
||||||
|
} else {
|
||||||
|
return data
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})).then((result) => {
|
})).then((result) => {
|
||||||
this.$emit('upload', result)
|
if (result && result[0]) {
|
||||||
|
this.$emit('upload', result)
|
||||||
|
}
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
:scroll-el="scrollEl"
|
:scroll-el="scrollEl"
|
||||||
:with-signature-id="withSignatureId"
|
:with-signature-id="withSignatureId"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
:with-label="!isAnonymousChecboxes && showFieldNames"
|
:with-label="withFieldLabels && !isAnonymousChecboxes && showFieldNames"
|
||||||
:current-step="currentStepFields"
|
:current-step="currentStepFields"
|
||||||
:scroll-padding="scrollPadding"
|
:scroll-padding="scrollPadding"
|
||||||
@focus-step="[saveStep(), currentField.type !== 'checkbox' ? isFormVisible = true : '', goToStep($event, false, true)]"
|
@focus-step="[saveStep(), currentField.type !== 'checkbox' ? isFormVisible = true : '', goToStep($event, false, true)]"
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
id="expand_form_button"
|
id="expand_form_button"
|
||||||
class="btn btn-neutral flex text-white absolute bottom-0 w-full mb-3 expand-form-button text-base"
|
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%"
|
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)">
|
<template v-if="['initials', 'signature'].includes(currentField.type)">
|
||||||
<IconWritingSign stroke-width="1.5" />
|
<IconWritingSign stroke-width="1.5" />
|
||||||
@@ -133,6 +133,12 @@
|
|||||||
name="_method"
|
name="_method"
|
||||||
type="hidden"
|
type="hidden"
|
||||||
>
|
>
|
||||||
|
<input
|
||||||
|
v-if="validate === false"
|
||||||
|
value="false"
|
||||||
|
name="validate"
|
||||||
|
type="hidden"
|
||||||
|
>
|
||||||
<div class="md:mt-4">
|
<div class="md:mt-4">
|
||||||
<div v-if="['cells', 'text'].includes(currentField.type)">
|
<div v-if="['cells', 'text'].includes(currentField.type)">
|
||||||
<TextStep
|
<TextStep
|
||||||
@@ -351,8 +357,6 @@
|
|||||||
class="base-checkbox !h-7 !w-7"
|
class="base-checkbox !h-7 !w-7"
|
||||||
:required="field.required"
|
:required="field.required"
|
||||||
:checked="!!values[field.uuid]"
|
:checked="!!values[field.uuid]"
|
||||||
@invalid="$event.target.setCustomValidity(t('please_check_the_box_to_continue'))"
|
|
||||||
@change="$event.target.setCustomValidity($event.target.validity.valueMissing ? t('please_check_the_box_to_continue') : '')"
|
|
||||||
@click="[scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
|
@click="[scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -391,7 +395,9 @@
|
|||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:reason="values[currentField.preferences?.reason_field_uuid]"
|
:reason="values[currentField.preferences?.reason_field_uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
|
:values="values"
|
||||||
:previous-value="previousSignatureValueFor(currentField) || previousSignatureValue"
|
:previous-value="previousSignatureValueFor(currentField) || previousSignatureValue"
|
||||||
|
:touch-attachment-uuid="previousSignatureValue"
|
||||||
:with-typed-signature="withTypedSignature"
|
:with-typed-signature="withTypedSignature"
|
||||||
:remember-signature="rememberSignature"
|
:remember-signature="rememberSignature"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
@@ -403,6 +409,7 @@
|
|||||||
:submitter="submitter"
|
:submitter="submitter"
|
||||||
:show-field-names="showFieldNames"
|
:show-field-names="showFieldNames"
|
||||||
@update:reason="values[currentField.preferences?.reason_field_uuid] = $event"
|
@update:reason="values[currentField.preferences?.reason_field_uuid] = $event"
|
||||||
|
@touch-attachment="attachmentsIndex[previousSignatureValue] ? attachmentsIndex[previousSignatureValue].created_at = new Date() : null"
|
||||||
@attached="attachments.push($event)"
|
@attached="attachments.push($event)"
|
||||||
@start="scrollIntoField(currentField)"
|
@start="scrollIntoField(currentField)"
|
||||||
@minimize="minimizeForm"
|
@minimize="minimizeForm"
|
||||||
@@ -461,6 +468,17 @@
|
|||||||
@focus="scrollIntoField(currentField)"
|
@focus="scrollIntoField(currentField)"
|
||||||
@submit="!isSubmitting && submitStep()"
|
@submit="!isSubmitting && submitStep()"
|
||||||
/>
|
/>
|
||||||
|
<KbaStep
|
||||||
|
v-else-if="currentField.type === 'kba'"
|
||||||
|
ref="currentStep"
|
||||||
|
:key="currentField.uuid"
|
||||||
|
:submitter="submitter"
|
||||||
|
:empty-value-required-step="emptyValueRequiredStep"
|
||||||
|
:field="currentField"
|
||||||
|
:submitter-slug="submitterSlug"
|
||||||
|
:values="values"
|
||||||
|
@submit="!isSubmitting && submitStep()"
|
||||||
|
/>
|
||||||
<VerificationStep
|
<VerificationStep
|
||||||
v-else-if="currentField.type === 'verification'"
|
v-else-if="currentField.type === 'verification'"
|
||||||
ref="currentStep"
|
ref="currentStep"
|
||||||
@@ -476,7 +494,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="(currentField.type !== 'payment' && currentField.type !== 'verification') || submittedValues[currentField.uuid]"
|
v-if="(currentField.type !== 'payment' && currentField.type !== 'verification' && currentField.type !== 'kba') || submittedValues[currentField.uuid]"
|
||||||
:class="currentField.type === 'signature' ? 'mt-2' : 'mt-4 md:mt-6'"
|
:class="currentField.type === 'signature' ? 'mt-2' : 'mt-4 md:mt-6'"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@@ -533,7 +551,7 @@
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="stepFields.length < 80"
|
v-if="stepFields.length < 80"
|
||||||
class="flex justify-center mt-3 sm:mt-4 mb-0 sm:mb-1"
|
class="flex justify-center mt-3 sm:mt-4 mb-0 sm:mb-1 select-none"
|
||||||
>
|
>
|
||||||
<div class="flex items-center flex-wrap steps-progress">
|
<div class="flex items-center flex-wrap steps-progress">
|
||||||
<a
|
<a
|
||||||
@@ -565,6 +583,7 @@ import MultiSelectStep from './multi_select_step'
|
|||||||
import PhoneStep from './phone_step'
|
import PhoneStep from './phone_step'
|
||||||
import PaymentStep from './payment_step'
|
import PaymentStep from './payment_step'
|
||||||
import VerificationStep from './verification_step'
|
import VerificationStep from './verification_step'
|
||||||
|
import KbaStep from './kba_step'
|
||||||
import TextStep from './text_step'
|
import TextStep from './text_step'
|
||||||
import NumberStep from './number_step'
|
import NumberStep from './number_step'
|
||||||
import DateStep from './date_step'
|
import DateStep from './date_step'
|
||||||
@@ -576,6 +595,18 @@ import AppearsOn from './appears_on'
|
|||||||
import i18n from './i18n'
|
import i18n from './i18n'
|
||||||
import { sanitizeUrl } from '@braintree/sanitize-url'
|
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) => {
|
const isEmpty = (obj) => {
|
||||||
if (obj == null) return true
|
if (obj == null) return true
|
||||||
|
|
||||||
@@ -609,6 +640,7 @@ export default {
|
|||||||
AttachmentStep,
|
AttachmentStep,
|
||||||
InitialsStep,
|
InitialsStep,
|
||||||
VerificationStep,
|
VerificationStep,
|
||||||
|
KbaStep,
|
||||||
InviteForm,
|
InviteForm,
|
||||||
MultiSelectStep,
|
MultiSelectStep,
|
||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
@@ -718,6 +750,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
validate: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
withDisclosure: {
|
withDisclosure: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -728,6 +765,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
withFieldLabels: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
withConfetti: {
|
withConfetti: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -887,10 +929,12 @@ export default {
|
|||||||
}, {})
|
}, {})
|
||||||
},
|
},
|
||||||
attachmentConditionsIndex () {
|
attachmentConditionsIndex () {
|
||||||
|
const cache = {}
|
||||||
|
|
||||||
return this.schema.reduce((acc, item) => {
|
return this.schema.reduce((acc, item) => {
|
||||||
if (item.conditions?.length) {
|
if (item.conditions?.length) {
|
||||||
if (item.conditions.every((c) => this.fieldsUuidIndex[c.field_uuid])) {
|
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 {
|
} else {
|
||||||
acc[item.attachment_uuid] = true
|
acc[item.attachment_uuid] = true
|
||||||
}
|
}
|
||||||
@@ -957,7 +1001,9 @@ export default {
|
|||||||
},
|
},
|
||||||
previousInitialsValue () {
|
previousInitialsValue () {
|
||||||
if (this.reuseSignature !== false) {
|
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]
|
return this.values[initialsField?.uuid]
|
||||||
} else {
|
} else {
|
||||||
@@ -984,13 +1030,15 @@ export default {
|
|||||||
return this.readonlyFields.filter((f) => f.conditions?.length)
|
return this.readonlyFields.filter((f) => f.conditions?.length)
|
||||||
},
|
},
|
||||||
readonlyFields () {
|
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 () {
|
stepFields () {
|
||||||
const verificationFields = []
|
const verificationFields = []
|
||||||
|
|
||||||
const sortedFields = this.fields.reduce((acc, f) => {
|
const sortedFields = this.fields.reduce((acc, f) => {
|
||||||
if (f.type === 'verification') {
|
if (f.type === 'verification' || f.type === 'kba') {
|
||||||
verificationFields.push(f)
|
verificationFields.push(f)
|
||||||
} else if (!f.readonly) {
|
} else if (!f.readonly) {
|
||||||
acc.push(f)
|
acc.push(f)
|
||||||
@@ -1039,10 +1087,12 @@ export default {
|
|||||||
sortedFields.push(verificationFields.pop())
|
sortedFields.push(verificationFields.pop())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cache = {}
|
||||||
|
|
||||||
return sortedFields.reduce((acc, f) => {
|
return sortedFields.reduce((acc, f) => {
|
||||||
const prevStep = acc[acc.length - 1]
|
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) {
|
if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox' && !f.description) {
|
||||||
prevStep.push(f)
|
prevStep.push(f)
|
||||||
} else {
|
} else {
|
||||||
@@ -1054,7 +1104,9 @@ export default {
|
|||||||
}, [])
|
}, [])
|
||||||
},
|
},
|
||||||
formulaFields () {
|
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 () {
|
attachmentsIndex () {
|
||||||
return this.attachments.reduce((acc, a) => {
|
return this.attachments.reduce((acc, a) => {
|
||||||
@@ -1116,7 +1168,9 @@ export default {
|
|||||||
this.currentStep = Math.max(stepIndex, 0)
|
this.currentStep = Math.max(stepIndex, 0)
|
||||||
} else if (this.goToLast) {
|
} else if (this.goToLast) {
|
||||||
const requiredEmptyStepIndex = this.stepFields.indexOf(this.stepFields.find((fields) => fields.some((f) => f.required && !this.submittedValues[f.uuid])))
|
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]
|
const indexesList = [this.stepFields.length - 1]
|
||||||
|
|
||||||
@@ -1184,27 +1238,33 @@ export default {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
checkFieldConditions (field) {
|
checkFieldConditions (field, cache = {}) {
|
||||||
|
if (cache[field.uuid] !== undefined) {
|
||||||
|
return cache[field.uuid]
|
||||||
|
}
|
||||||
|
|
||||||
|
cache[field.uuid] = true
|
||||||
|
|
||||||
if (field.conditions?.length) {
|
if (field.conditions?.length) {
|
||||||
const result = field.conditions.reduce((acc, cond) => {
|
const result = field.conditions.reduce((acc, cond) => {
|
||||||
if (cond.operation === 'or') {
|
if (cond.operation === 'or') {
|
||||||
acc.push(acc.pop() || this.checkFieldCondition(cond))
|
acc.push(acc.pop() || this.checkFieldCondition(cond, cache))
|
||||||
} else {
|
} else {
|
||||||
acc.push(this.checkFieldCondition(cond))
|
acc.push(this.checkFieldCondition(cond, cache))
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc
|
return acc
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return !result.includes(false)
|
cache[field.uuid] = !result.includes(false)
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return cache[field.uuid]
|
||||||
},
|
},
|
||||||
checkFieldCondition (condition) {
|
checkFieldCondition (condition, cache = {}) {
|
||||||
const field = this.fieldsUuidIndex[condition.field_uuid]
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1214,20 +1274,50 @@ export default {
|
|||||||
return isEmpty(this.values[condition.field_uuid] ?? defaultValue)
|
return isEmpty(this.values[condition.field_uuid] ?? defaultValue)
|
||||||
} else if (['not_empty', 'checked'].includes(condition.action)) {
|
} else if (['not_empty', 'checked'].includes(condition.action)) {
|
||||||
return !isEmpty(this.values[condition.field_uuid] ?? defaultValue)
|
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) {
|
} else if (['equal', 'contains'].includes(condition.action) && field) {
|
||||||
if (field.options) {
|
if (field.options) {
|
||||||
const option = field.options.find((o) => o.uuid === condition.value)
|
const option = field.options.find((o) => o.uuid === condition.value)
|
||||||
const values = [this.values[condition.field_uuid] ?? defaultValue].flat()
|
|
||||||
|
|
||||||
return values.includes(this.optionValue(option, field.options.indexOf(option)))
|
if (option) {
|
||||||
|
const values = [this.values[condition.field_uuid] ?? defaultValue].flat()
|
||||||
|
|
||||||
|
return values.includes(this.optionValue(option, field.options.indexOf(option)))
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return [this.values[condition.field_uuid] ?? defaultValue].flat().includes(condition.value)
|
return [this.values[condition.field_uuid] ?? defaultValue].flat().includes(condition.value)
|
||||||
}
|
}
|
||||||
} else if (['not_equal', 'does_not_contain'].includes(condition.action) && field) {
|
} else if (['not_equal', 'does_not_contain'].includes(condition.action) && field) {
|
||||||
const option = field.options.find((o) => o.uuid === condition.value)
|
if (field.options) {
|
||||||
const values = [this.values[condition.field_uuid] ?? defaultValue].flat()
|
const option = field.options.find((o) => o.uuid === condition.value)
|
||||||
|
|
||||||
return !values.includes(this.optionValue(option, field.options.indexOf(option)))
|
if (option) {
|
||||||
|
const values = [this.values[condition.field_uuid] ?? defaultValue].flat()
|
||||||
|
|
||||||
|
return !values.includes(this.optionValue(option, field.options.indexOf(option)))
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -1300,9 +1390,9 @@ export default {
|
|||||||
},
|
},
|
||||||
previousSignatureValueFor (field) {
|
previousSignatureValueFor (field) {
|
||||||
if (this.reuseSignature !== false) {
|
if (this.reuseSignature !== false) {
|
||||||
const signatureField = [...this.fields].reverse().find((f) =>
|
const signatureField = this.fields.findLast
|
||||||
f.type === 'signature' && field.preferences?.format === f.preferences?.format && !!this.values[f.uuid]
|
? 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]
|
return this.values[signatureField?.uuid]
|
||||||
} else {
|
} else {
|
||||||
@@ -1387,7 +1477,7 @@ export default {
|
|||||||
|
|
||||||
const submitStep = this.currentStep
|
const submitStep = this.currentStep
|
||||||
|
|
||||||
const stepPromise = ['signature', 'phone', 'initials', 'payment', 'verification'].includes(this.currentField.type)
|
const stepPromise = ['signature', 'phone', 'initials', 'payment', 'verification', 'kba'].includes(this.currentField.type)
|
||||||
? this.$refs.currentStep.submit
|
? this.$refs.currentStep.submit
|
||||||
: () => Promise.resolve({})
|
: () => Promise.resolve({})
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const en = {
|
const en = {
|
||||||
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Please upload an image file',
|
please_upload_an_image_file: 'Please upload an image file',
|
||||||
must_be_characters_length: 'Must be {number} characters length',
|
must_be_characters_length: 'Must be {number} characters length',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.',
|
||||||
@@ -13,7 +14,7 @@ const en = {
|
|||||||
esignature_disclosure: 'eSignature Disclosure',
|
esignature_disclosure: 'eSignature Disclosure',
|
||||||
signature: 'Signature',
|
signature: 'Signature',
|
||||||
initials: 'Initials',
|
initials: 'Initials',
|
||||||
drawn_signature_on_a_touchscreen_device: 'Drawn signature on a touchscreen device',
|
sign_on_the_touchscreen: 'Sign on the touchscreen',
|
||||||
approved: 'Approved',
|
approved: 'Approved',
|
||||||
reviewed: 'Reviewed',
|
reviewed: 'Reviewed',
|
||||||
other: 'Other',
|
other: 'Other',
|
||||||
@@ -100,6 +101,7 @@ const en = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Por favor, sube un archivo de imagen',
|
please_upload_an_image_file: 'Por favor, sube un archivo de imagen',
|
||||||
must_be_characters_length: 'Debe tener {number} caracteres de longitud',
|
must_be_characters_length: 'Debe tener {number} caracteres de longitud',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete todos los campos requeridos para continuar con la verificación de identidad.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete todos los campos requeridos para continuar con la verificación de identidad.',
|
||||||
@@ -120,7 +122,7 @@ const es = {
|
|||||||
select_a_reason: 'Selecciona una razón',
|
select_a_reason: 'Selecciona una razón',
|
||||||
value_is_invalid: 'El valor no es válido',
|
value_is_invalid: 'El valor no es válido',
|
||||||
verification_code_is_invalid: 'El código de verificación no es válido',
|
verification_code_is_invalid: 'El código de verificación no es válido',
|
||||||
drawn_signature_on_a_touchscreen_device: 'Firma dibujada en un dispositivo con pantalla táctil',
|
sign_on_the_touchscreen: 'Firmar en pantalla táctil',
|
||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Escanea el código QR con la aplicación de la cámara para abrir el formulario en el móvil y dibujar tu firma',
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Escanea el código QR con la aplicación de la cámara para abrir el formulario en el móvil y dibujar tu firma',
|
||||||
by_clicking_you_agree_to_the: 'Al hacer clic en "{button}", usted acepta el',
|
by_clicking_you_agree_to_the: 'Al hacer clic en "{button}", usted acepta el',
|
||||||
electronic_signature_disclosure: 'Divulgación de Firma Electrónica',
|
electronic_signature_disclosure: 'Divulgación de Firma Electrónica',
|
||||||
@@ -201,6 +203,7 @@ const es = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Per favore carica un file immagine',
|
please_upload_an_image_file: 'Per favore carica un file immagine',
|
||||||
must_be_characters_length: 'Deve essere lungo {number} caratteri',
|
must_be_characters_length: 'Deve essere lungo {number} caratteri',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: "Compila tutti i campi obbligatori per procedere con la verifica dell'identità.",
|
complete_all_required_fields_to_proceed_with_identity_verification: "Compila tutti i campi obbligatori per procedere con la verifica dell'identità.",
|
||||||
@@ -221,7 +224,7 @@ const it = {
|
|||||||
select_a_reason: 'Seleziona una ragione',
|
select_a_reason: 'Seleziona una ragione',
|
||||||
value_is_invalid: 'Il valore non è valido',
|
value_is_invalid: 'Il valore non è valido',
|
||||||
verification_code_is_invalid: 'Il codice di verifica non è valido',
|
verification_code_is_invalid: 'Il codice di verifica non è valido',
|
||||||
drawn_signature_on_a_touchscreen_device: 'Firma disegnata su un dispositivo con schermo tattile',
|
sign_on_the_touchscreen: 'Firma su schermo tattile',
|
||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: "Scansiona il codice QR con l'app della fotocamera per aprire il modulo sul cellulare e disegnare la tua firma",
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: "Scansiona il codice QR con l'app della fotocamera per aprire il modulo sul cellulare e disegnare la tua firma",
|
||||||
by_clicking_you_agree_to_the: 'Cliccando su "{button}", accetti il',
|
by_clicking_you_agree_to_the: 'Cliccando su "{button}", accetti il',
|
||||||
electronic_signature_disclosure: 'Divulgazione della Firma Elettronica',
|
electronic_signature_disclosure: 'Divulgazione della Firma Elettronica',
|
||||||
@@ -302,6 +305,7 @@ const it = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Bitte laden Sie eine Bilddatei hoch',
|
please_upload_an_image_file: 'Bitte laden Sie eine Bilddatei hoch',
|
||||||
must_be_characters_length: 'Muss {number} Zeichen lang sein',
|
must_be_characters_length: 'Muss {number} Zeichen lang sein',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Füllen Sie alle Pflichtfelder aus, um mit der Identitätsprüfung fortzufahren.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Füllen Sie alle Pflichtfelder aus, um mit der Identitätsprüfung fortzufahren.',
|
||||||
@@ -316,7 +320,7 @@ const de = {
|
|||||||
esignature_disclosure: 'Hinweis zur eSignatur',
|
esignature_disclosure: 'Hinweis zur eSignatur',
|
||||||
signature: 'Unterschrift',
|
signature: 'Unterschrift',
|
||||||
initials: 'Initialen',
|
initials: 'Initialen',
|
||||||
drawn_signature_on_a_touchscreen_device: 'Auf einem Touchscreen-Gerät gezeichnete Unterschrift',
|
sign_on_the_touchscreen: 'Auf Touchscreen signieren',
|
||||||
approved: 'Genehmigt',
|
approved: 'Genehmigt',
|
||||||
reviewed: 'Geprüft',
|
reviewed: 'Geprüft',
|
||||||
other: 'Sonstiges',
|
other: 'Sonstiges',
|
||||||
@@ -403,6 +407,7 @@ const de = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Veuillez téléverser un fichier image',
|
please_upload_an_image_file: 'Veuillez téléverser un fichier image',
|
||||||
must_be_characters_length: 'Doit comporter {number} caractères',
|
must_be_characters_length: 'Doit comporter {number} caractères',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour poursuivre la vérification d'identité.",
|
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour poursuivre la vérification d'identité.",
|
||||||
@@ -417,7 +422,7 @@ const fr = {
|
|||||||
esignature_disclosure: 'Déclaration eSignature',
|
esignature_disclosure: 'Déclaration eSignature',
|
||||||
signature: 'Signature',
|
signature: 'Signature',
|
||||||
initials: 'Initiales',
|
initials: 'Initiales',
|
||||||
drawn_signature_on_a_touchscreen_device: 'Signature dessinée sur un appareil à écran tactile',
|
sign_on_the_touchscreen: 'Signer sur écran tactile',
|
||||||
approved: 'Approuvé',
|
approved: 'Approuvé',
|
||||||
reviewed: 'Révisé',
|
reviewed: 'Révisé',
|
||||||
other: 'Autre',
|
other: 'Autre',
|
||||||
@@ -504,6 +509,7 @@ const fr = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pl = {
|
const pl = {
|
||||||
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Proszę przesłać plik obrazu',
|
please_upload_an_image_file: 'Proszę przesłać plik obrazu',
|
||||||
must_be_characters_length: 'Musi mieć długość {number} znaków',
|
must_be_characters_length: 'Musi mieć długość {number} znaków',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Uzupełnij wszystkie wymagane pola, aby kontynuować weryfikację tożsamości.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Uzupełnij wszystkie wymagane pola, aby kontynuować weryfikację tożsamości.',
|
||||||
@@ -524,7 +530,7 @@ const pl = {
|
|||||||
select_a_reason: 'Wybierz powód',
|
select_a_reason: 'Wybierz powód',
|
||||||
value_is_invalid: 'Wartość jest nieprawidłowa',
|
value_is_invalid: 'Wartość jest nieprawidłowa',
|
||||||
verification_code_is_invalid: 'Kod weryfikacyjny jest nieprawidłowy',
|
verification_code_is_invalid: 'Kod weryfikacyjny jest nieprawidłowy',
|
||||||
drawn_signature_on_a_touchscreen_device: 'Podpis odręczny na urządzeniu z ekranem dotykowym',
|
sign_on_the_touchscreen: 'Podpisz na ekranie dotykowym',
|
||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Zeskanuj kod QR za pomocą aplikacji aparatu, aby otworzyć formularz na telefonie i narysować swój podpis',
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Zeskanuj kod QR za pomocą aplikacji aparatu, aby otworzyć formularz na telefonie i narysować swój podpis',
|
||||||
by_clicking_you_agree_to_the: 'Klikając na "{button}", zgadzasz się na',
|
by_clicking_you_agree_to_the: 'Klikając na "{button}", zgadzasz się na',
|
||||||
electronic_signature_disclosure: 'Ujawnienie Elektronicznej Sygnatury',
|
electronic_signature_disclosure: 'Ujawnienie Elektronicznej Sygnatury',
|
||||||
@@ -605,6 +611,7 @@ const pl = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const uk = {
|
const uk = {
|
||||||
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Будь ласка, завантажте файл зображення',
|
please_upload_an_image_file: 'Будь ласка, завантажте файл зображення',
|
||||||
must_be_characters_length: 'Має містити {number} символів',
|
must_be_characters_length: 'Має містити {number} символів',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: "Заповніть всі обов'язкові поля, щоб продовжити перевірку особи.",
|
complete_all_required_fields_to_proceed_with_identity_verification: "Заповніть всі обов'язкові поля, щоб продовжити перевірку особи.",
|
||||||
@@ -625,7 +632,7 @@ const uk = {
|
|||||||
select_a_reason: 'Виберіть причину',
|
select_a_reason: 'Виберіть причину',
|
||||||
value_is_invalid: 'Значення є неправильним',
|
value_is_invalid: 'Значення є неправильним',
|
||||||
verification_code_is_invalid: 'Код підтвердження є неправильним',
|
verification_code_is_invalid: 'Код підтвердження є неправильним',
|
||||||
drawn_signature_on_a_touchscreen_device: 'Підпис на сенсорному пристрої',
|
sign_on_the_touchscreen: 'Підписати на сенсорному екрані',
|
||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Скануйте QR-код за допомогою програми камери, щоб відкрити форму на мобільному пристрої та намалювати свій підпис',
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Скануйте QR-код за допомогою програми камери, щоб відкрити форму на мобільному пристрої та намалювати свій підпис',
|
||||||
by_clicking_you_agree_to_the: 'Натиснувши на "{button}", ви погоджуєтеся з',
|
by_clicking_you_agree_to_the: 'Натиснувши на "{button}", ви погоджуєтеся з',
|
||||||
electronic_signature_disclosure: 'Розголошення Електронного Підпису',
|
electronic_signature_disclosure: 'Розголошення Електронного Підпису',
|
||||||
@@ -706,6 +713,7 @@ const uk = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cs = {
|
const cs = {
|
||||||
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Nahrajte prosím obrázkový soubor',
|
please_upload_an_image_file: 'Nahrajte prosím obrázkový soubor',
|
||||||
must_be_characters_length: 'Musí mít délku {number} znaků',
|
must_be_characters_length: 'Musí mít délku {number} znaků',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Vyplňte všechna povinná pole, abyste mohli pokračovat v ověření identity.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Vyplňte všechna povinná pole, abyste mohli pokračovat v ověření identity.',
|
||||||
@@ -726,7 +734,7 @@ const cs = {
|
|||||||
select_a_reason: 'Vyberte důvod',
|
select_a_reason: 'Vyberte důvod',
|
||||||
value_is_invalid: 'Hodnota je neplatná',
|
value_is_invalid: 'Hodnota je neplatná',
|
||||||
verification_code_is_invalid: 'Ověřovací kód je neplatný',
|
verification_code_is_invalid: 'Ověřovací kód je neplatný',
|
||||||
drawn_signature_on_a_touchscreen_device: 'Namalovaný podpis na dotykovém zařízení',
|
sign_on_the_touchscreen: 'Podepsat na dotykové obrazovce',
|
||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Naskenujte QR kód pomocí aplikace fotoaparátu, abyste otevřeli formulář na mobilním zařízení a nakreslili svůj podpis',
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Naskenujte QR kód pomocí aplikace fotoaparátu, abyste otevřeli formulář na mobilním zařízení a nakreslili svůj podpis',
|
||||||
by_clicking_you_agree_to_the: 'Kliknutím na "{button}" souhlasíte s',
|
by_clicking_you_agree_to_the: 'Kliknutím na "{button}" souhlasíte s',
|
||||||
electronic_signature_disclosure: 'Zveřejněním Elektronického Podpisu',
|
electronic_signature_disclosure: 'Zveřejněním Elektronického Podpisu',
|
||||||
@@ -807,6 +815,7 @@ const cs = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Por favor, envie um arquivo de imagem',
|
please_upload_an_image_file: 'Por favor, envie um arquivo de imagem',
|
||||||
must_be_characters_length: 'Deve ter {number} caracteres',
|
must_be_characters_length: 'Deve ter {number} caracteres',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Preencha todos os campos obrigatórios para prosseguir com a verificação de identidade.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Preencha todos os campos obrigatórios para prosseguir com a verificação de identidade.',
|
||||||
@@ -827,7 +836,7 @@ const pt = {
|
|||||||
select_a_reason: 'Selecione um motivo',
|
select_a_reason: 'Selecione um motivo',
|
||||||
value_is_invalid: 'Valor é inválido',
|
value_is_invalid: 'Valor é inválido',
|
||||||
verification_code_is_invalid: 'Código de verificação é inválido',
|
verification_code_is_invalid: 'Código de verificação é inválido',
|
||||||
drawn_signature_on_a_touchscreen_device: 'Assinatura desenhada em um dispositivo com tela sensível ao toque',
|
sign_on_the_touchscreen: 'Assinar na tela sensível',
|
||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Escaneie o código QR com o aplicativo da câmera para abrir o formulário no celular e desenhar sua assinatura',
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Escaneie o código QR com o aplicativo da câmera para abrir o formulário no celular e desenhar sua assinatura',
|
||||||
by_clicking_you_agree_to_the: 'Ao clicar em "{button}", você concorda com o',
|
by_clicking_you_agree_to_the: 'Ao clicar em "{button}", você concorda com o',
|
||||||
electronic_signature_disclosure: 'Divulgação de Assinatura Eletrônica',
|
electronic_signature_disclosure: 'Divulgação de Assinatura Eletrônica',
|
||||||
@@ -908,6 +917,7 @@ const pt = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const he = {
|
const he = {
|
||||||
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'אנא העלה קובץ תמונה',
|
please_upload_an_image_file: 'אנא העלה קובץ תמונה',
|
||||||
must_be_characters_length: 'חייב להיות באורך של {number} תווים',
|
must_be_characters_length: 'חייב להיות באורך של {number} תווים',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'מלא את כל השדות הנדרשים כדי להמשיך עם אימות זהות.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'מלא את כל השדות הנדרשים כדי להמשיך עם אימות זהות.',
|
||||||
@@ -928,7 +938,7 @@ const he = {
|
|||||||
select_a_reason: 'בחר סיבה',
|
select_a_reason: 'בחר סיבה',
|
||||||
value_is_invalid: 'ערך לא תקין',
|
value_is_invalid: 'ערך לא תקין',
|
||||||
verification_code_is_invalid: 'קוד האימות אינו תקין',
|
verification_code_is_invalid: 'קוד האימות אינו תקין',
|
||||||
drawn_signature_on_a_touchscreen_device: 'חתימה שנוצרה במכשיר עם מסך מגע',
|
sign_on_the_touchscreen: 'חתום על מסך המגע',
|
||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'סרוק את קוד ה-QR באמצעות אפליקציית המצלמה כדי לפתוח את הטופס במובייל ולצייר את החתימה שלך',
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'סרוק את קוד ה-QR באמצעות אפליקציית המצלמה כדי לפתוח את הטופס במובייל ולצייר את החתימה שלך',
|
||||||
by_clicking_you_agree_to_the: 'על ידי לחיצה על "{button}", אתה מסכים ל',
|
by_clicking_you_agree_to_the: 'על ידי לחיצה על "{button}", אתה מסכים ל',
|
||||||
electronic_signature_disclosure: 'חשיפת חתימה אלקטרונית',
|
electronic_signature_disclosure: 'חשיפת חתימה אלקטרונית',
|
||||||
@@ -1009,6 +1019,7 @@ const he = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nl = {
|
const nl = {
|
||||||
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Upload alstublieft een afbeeldingsbestand',
|
please_upload_an_image_file: 'Upload alstublieft een afbeeldingsbestand',
|
||||||
must_be_characters_length: 'Moet {number} tekens lang zijn',
|
must_be_characters_length: 'Moet {number} tekens lang zijn',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Vul alle verplichte velden in om door te gaan met de identiteitsverificatie.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Vul alle verplichte velden in om door te gaan met de identiteitsverificatie.',
|
||||||
@@ -1029,7 +1040,7 @@ const nl = {
|
|||||||
select_a_reason: 'Selecteer een reden',
|
select_a_reason: 'Selecteer een reden',
|
||||||
value_is_invalid: 'Waarde is ongeldig',
|
value_is_invalid: 'Waarde is ongeldig',
|
||||||
verification_code_is_invalid: 'Verificatiecode is ongeldig',
|
verification_code_is_invalid: 'Verificatiecode is ongeldig',
|
||||||
drawn_signature_on_a_touchscreen_device: 'Getekende handtekening op een apparaat met een touchscreen',
|
sign_on_the_touchscreen: 'Onderteken op touchscreen',
|
||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scan de QR-code met de camera-app om het formulier op mobiel te openen en uw handtekening te tekenen',
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scan de QR-code met de camera-app om het formulier op mobiel te openen en uw handtekening te tekenen',
|
||||||
by_clicking_you_agree_to_the: 'Door op "{button}" te klikken, gaat u akkoord met de',
|
by_clicking_you_agree_to_the: 'Door op "{button}" te klikken, gaat u akkoord met de',
|
||||||
electronic_signature_disclosure: 'Openbaarmaking van Elektronische Handtekening',
|
electronic_signature_disclosure: 'Openbaarmaking van Elektronische Handtekening',
|
||||||
@@ -1110,6 +1121,7 @@ const nl = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ar = {
|
const ar = {
|
||||||
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'يرجى تحميل ملف صورة',
|
please_upload_an_image_file: 'يرجى تحميل ملف صورة',
|
||||||
must_be_characters_length: 'يجب أن يكون الطول {number} حرفًا',
|
must_be_characters_length: 'يجب أن يكون الطول {number} حرفًا',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'أكمل جميع الحقول المطلوبة للمتابعة في التحقق من الهوية.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'أكمل جميع الحقول المطلوبة للمتابعة في التحقق من الهوية.',
|
||||||
@@ -1131,7 +1143,7 @@ const ar = {
|
|||||||
value_is_invalid: 'القيمة غير صالحة',
|
value_is_invalid: 'القيمة غير صالحة',
|
||||||
verification_code_is_invalid: 'رمز التحقق غير صالح',
|
verification_code_is_invalid: 'رمز التحقق غير صالح',
|
||||||
already_paid: 'تم الدفع بالفعل',
|
already_paid: 'تم الدفع بالفعل',
|
||||||
drawn_signature_on_a_touchscreen_device: 'توقيع مرسوم على جهاز بشاشة تعمل باللمس',
|
sign_on_the_touchscreen: 'وقع على شاشة اللمس',
|
||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'امسح رمز الاستجابة السريعة باستخدام تطبيق الكاميرا لفتح النموذج على الهاتف المحمول ورسم توقيعك',
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'امسح رمز الاستجابة السريعة باستخدام تطبيق الكاميرا لفتح النموذج على الهاتف المحمول ورسم توقيعك',
|
||||||
by_clicking_you_agree_to_the: 'بالنقر فوق "{button}"، أنت توافق على',
|
by_clicking_you_agree_to_the: 'بالنقر فوق "{button}"، أنت توافق على',
|
||||||
electronic_signature_disclosure: 'كشف التوقيع الإلكتروني',
|
electronic_signature_disclosure: 'كشف التوقيع الإلكتروني',
|
||||||
@@ -1211,6 +1223,7 @@ const ar = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ko = {
|
const ko = {
|
||||||
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: '이미지 파일을 업로드해 주세요',
|
please_upload_an_image_file: '이미지 파일을 업로드해 주세요',
|
||||||
must_be_characters_length: '{number}자여야 합니다',
|
must_be_characters_length: '{number}자여야 합니다',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: '신원 확인을 진행하려면 모든 필수 필드를 작성하십시오.',
|
complete_all_required_fields_to_proceed_with_identity_verification: '신원 확인을 진행하려면 모든 필수 필드를 작성하십시오.',
|
||||||
@@ -1229,7 +1242,7 @@ const ko = {
|
|||||||
reviewed_by: '검토자',
|
reviewed_by: '검토자',
|
||||||
authored_by: '작성자',
|
authored_by: '작성자',
|
||||||
select_a_reason: '이유 선택',
|
select_a_reason: '이유 선택',
|
||||||
drawn_signature_on_a_touchscreen_device: '터치스크린 장치에서 그린 서명',
|
sign_on_the_touchscreen: '터치스크린에서 서명',
|
||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: '카메라 앱으로 QR 코드를 스캔하여 모바일에서 양식을 열고 서명을 그리세요',
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: '카메라 앱으로 QR 코드를 스캔하여 모바일에서 양식을 열고 서명을 그리세요',
|
||||||
by_clicking_you_agree_to_the: '"{button}"를 클릭함으로써, 다음에 동의하게 됩니다',
|
by_clicking_you_agree_to_the: '"{button}"를 클릭함으로써, 다음에 동의하게 됩니다',
|
||||||
electronic_signature_disclosure: '전자 서명 공개',
|
electronic_signature_disclosure: '전자 서명 공개',
|
||||||
@@ -1312,6 +1325,7 @@ const ko = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ja = {
|
const ja = {
|
||||||
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: '画像ファイルをアップロードしてください',
|
please_upload_an_image_file: '画像ファイルをアップロードしてください',
|
||||||
must_be_characters_length: '{number}文字でなければなりません',
|
must_be_characters_length: '{number}文字でなければなりません',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: '本人確認を進めるには、すべての必須項目を入力してください。',
|
complete_all_required_fields_to_proceed_with_identity_verification: '本人確認を進めるには、すべての必須項目を入力してください。',
|
||||||
@@ -1326,7 +1340,7 @@ const ja = {
|
|||||||
esignature_disclosure: '電子署名開示',
|
esignature_disclosure: '電子署名開示',
|
||||||
signature: '署名',
|
signature: '署名',
|
||||||
initials: 'イニシャル',
|
initials: 'イニシャル',
|
||||||
drawn_signature_on_a_touchscreen_device: 'タッチスクリーンデバイスで描かれた署名',
|
sign_on_the_touchscreen: 'タッチスクリーンで署名',
|
||||||
approved: '承認済み',
|
approved: '承認済み',
|
||||||
reviewed: '確認済み',
|
reviewed: '確認済み',
|
||||||
other: 'その他',
|
other: 'その他',
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<div class="space-x-2 flex flex-none">
|
<div class="space-x-2 flex flex-none">
|
||||||
<span
|
<span
|
||||||
v-if="isDrawInitials"
|
v-if="isDrawInitials"
|
||||||
class="tooltip"
|
class="md:tooltip"
|
||||||
:data-tip="t('type_initial')"
|
:data-tip="t('type_initial')"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
class="tooltip ml-2"
|
class="md:tooltip ml-2"
|
||||||
:data-tip="t('draw_initials')"
|
:data-tip="t('draw_initials')"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="tooltip"
|
class="md:tooltip"
|
||||||
:data-tip="t('click_to_upload')"
|
:data-tip="t('click_to_upload')"
|
||||||
>
|
>
|
||||||
<label class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap upload-image-button">
|
<label class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap upload-image-button">
|
||||||
@@ -123,6 +123,11 @@
|
|||||||
v-if="!isDrawInitials"
|
v-if="!isDrawInitials"
|
||||||
class="absolute top-0 right-0 left-0 bottom-0"
|
class="absolute top-0 right-0 left-0 bottom-0"
|
||||||
/>
|
/>
|
||||||
|
<label
|
||||||
|
v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
|
||||||
|
for="initials_text_input"
|
||||||
|
class="absolute top-0 right-0 left-0 bottom-0"
|
||||||
|
/>
|
||||||
<canvas
|
<canvas
|
||||||
v-show="!modelValue && !computedPreviousValue"
|
v-show="!modelValue && !computedPreviousValue"
|
||||||
ref="canvas"
|
ref="canvas"
|
||||||
|
|||||||
@@ -0,0 +1,580 @@
|
|||||||
|
<template>
|
||||||
|
<label
|
||||||
|
v-if="!error"
|
||||||
|
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
|
||||||
|
>
|
||||||
|
<MarkdownContent
|
||||||
|
v-if="field.title"
|
||||||
|
:string="field.title"
|
||||||
|
/>
|
||||||
|
<template v-else>{{ field.name || 'Knowledge Based Authentication' }}</template>
|
||||||
|
<span
|
||||||
|
v-if="questions"
|
||||||
|
class="float-right font-normal text-neutral-500 mt-1 whitespace-nowrap text-right text-sm"
|
||||||
|
>
|
||||||
|
<span>Question {{ currentQuestionIndex + 1 }} / {{ questions.length }}</span>
|
||||||
|
<span
|
||||||
|
v-if="timeLeftSeconds !== null"
|
||||||
|
class="block"
|
||||||
|
>
|
||||||
|
Time left: {{ formattedTimeLeft }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
v-if="field.description"
|
||||||
|
dir="auto"
|
||||||
|
class="mb-4 px-1 field-description-text"
|
||||||
|
>
|
||||||
|
<MarkdownContent :string="field.description" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="isRequiredFieldEmpty"
|
||||||
|
class="px-1 field-description-text"
|
||||||
|
>
|
||||||
|
{{ t('complete_all_required_fields_to_proceed_with_identity_verification') }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="error"
|
||||||
|
class="mb-4 text-center"
|
||||||
|
>
|
||||||
|
<div class="text-xl mb-4">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="base-button w-full flex justify-center submit-form-button"
|
||||||
|
@click="restartKba"
|
||||||
|
>
|
||||||
|
{{ questions ? 'Restart KBA' : 'Retry' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="isLoading"
|
||||||
|
class="w-full flex space-x-2 justify-center mb-2"
|
||||||
|
>
|
||||||
|
<IconInnerShadowTop
|
||||||
|
width="40"
|
||||||
|
class="animate-spin h-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="questions && !error">
|
||||||
|
<form @submit.prevent="nextQuestion">
|
||||||
|
<div class="mb-6 px-1">
|
||||||
|
<p class="font-semibold mb-4 text-lg">
|
||||||
|
{{ currentQuestion.prompt }}
|
||||||
|
</p>
|
||||||
|
<div class="space-y-3.5 mx-auto">
|
||||||
|
<div
|
||||||
|
v-for="(answer, index) in currentQuestion.answers"
|
||||||
|
:key="`${currentQuestion.id}_${index}`"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
:for="`${currentQuestion.id}_${index}`"
|
||||||
|
class="flex items-center space-x-3 radio-label"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:id="`${currentQuestion.id}_${index}`"
|
||||||
|
v-model="answers[currentQuestion.id]"
|
||||||
|
type="radio"
|
||||||
|
:name="currentQuestion.id"
|
||||||
|
:value="answer.text"
|
||||||
|
class="base-radio !h-7 !w-7"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<span class="text-xl">{{ answer.text }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="base-button w-full flex justify-center submit-form-button"
|
||||||
|
:disabled="isSubmitting || !answers[currentQuestion.id]"
|
||||||
|
>
|
||||||
|
<span class="flex">
|
||||||
|
<IconInnerShadowTop
|
||||||
|
v-if="isSubmitting"
|
||||||
|
class="mr-1 animate-spin"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{{ isLastQuestion ? (isSubmitting ? t('submitting_') : t('complete')) : t('next') }}
|
||||||
|
</span><span
|
||||||
|
v-if="isSubmitting"
|
||||||
|
class="w-6 flex justify-start mr-1"
|
||||||
|
><span>...</span></span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="!error && !isRequiredFieldEmpty">
|
||||||
|
<form @submit.prevent="startKba">
|
||||||
|
<div class="grid grid-cols-6 gap-x-2 md:gap-x-4 md:gap-y-2 mb-4">
|
||||||
|
<div class="col-span-3">
|
||||||
|
<label
|
||||||
|
for="kba_fn"
|
||||||
|
class="label text-sm md:text-base"
|
||||||
|
>First Name</label>
|
||||||
|
<input
|
||||||
|
id="kba_fn"
|
||||||
|
v-model="form.fn"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered !h-10 w-full bg-white"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-3">
|
||||||
|
<label
|
||||||
|
for="kba_ln"
|
||||||
|
class="label text-sm md:text-base"
|
||||||
|
>Last Name</label>
|
||||||
|
<input
|
||||||
|
id="kba_ln"
|
||||||
|
v-model="form.ln"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered !h-10 w-full bg-white"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-6">
|
||||||
|
<label
|
||||||
|
for="kba_addr"
|
||||||
|
class="label text-sm md:text-base"
|
||||||
|
>Address</label>
|
||||||
|
<input
|
||||||
|
id="kba_addr"
|
||||||
|
v-model="form.addr"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered !h-10 w-full bg-white"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label
|
||||||
|
for="kba_city"
|
||||||
|
class="label text-sm md:text-base"
|
||||||
|
>City</label>
|
||||||
|
<input
|
||||||
|
id="kba_city"
|
||||||
|
v-model="form.city"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered !h-10 w-full bg-white"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label
|
||||||
|
for="kba_state"
|
||||||
|
class="label text-sm md:text-base"
|
||||||
|
>State</label>
|
||||||
|
<select
|
||||||
|
id="kba_state"
|
||||||
|
v-model="form.state"
|
||||||
|
class="select select-bordered !h-10 min-h-[2.5rem] w-full bg-white font-normal !text-base"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
value=""
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
Select State
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
v-for="state in states"
|
||||||
|
:key="state.code"
|
||||||
|
:value="state.code"
|
||||||
|
>
|
||||||
|
{{ state.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label
|
||||||
|
for="kba_zip"
|
||||||
|
class="label text-sm md:text-base"
|
||||||
|
>Zip</label>
|
||||||
|
<input
|
||||||
|
id="kba_zip"
|
||||||
|
v-model="form.zip"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered !h-10 w-full bg-white"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-3">
|
||||||
|
<label
|
||||||
|
for="kba_phone"
|
||||||
|
class="label text-sm md:text-base"
|
||||||
|
>Phone</label>
|
||||||
|
<input
|
||||||
|
id="kba_phone"
|
||||||
|
v-model="form.phone"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered !h-10 w-full bg-white"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-3">
|
||||||
|
<label
|
||||||
|
for="kba_email"
|
||||||
|
class="label text-sm md:text-base"
|
||||||
|
>Email</label>
|
||||||
|
<input
|
||||||
|
id="kba_email"
|
||||||
|
v-model="form.email"
|
||||||
|
type="email"
|
||||||
|
class="input input-bordered !h-10 w-full bg-white"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-3">
|
||||||
|
<label
|
||||||
|
for="kba_dob"
|
||||||
|
class="label text-sm md:text-base"
|
||||||
|
>DOB</label>
|
||||||
|
<input
|
||||||
|
id="kba_dob"
|
||||||
|
v-model="form.dob"
|
||||||
|
type="date"
|
||||||
|
class="input input-bordered !h-10 md:w-full bg-white"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-3">
|
||||||
|
<label
|
||||||
|
for="kba_ssn"
|
||||||
|
class="label text-sm md:text-base"
|
||||||
|
>SSN</label>
|
||||||
|
<input
|
||||||
|
id="kba_ssn"
|
||||||
|
v-model="form.ssn"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered !h-10 w-full bg-white"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="base-button w-full flex justify-center submit-form-button"
|
||||||
|
:disabled="isLoading"
|
||||||
|
>
|
||||||
|
<span class="flex">
|
||||||
|
<IconInnerShadowTop
|
||||||
|
v-if="isLoading"
|
||||||
|
class="mr-1 animate-spin"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{{ isLoading ? 'Loading...' : 'Start Verification' }}
|
||||||
|
</span><span
|
||||||
|
v-if="isLoading"
|
||||||
|
class="w-6 flex justify-start mr-1"
|
||||||
|
><span>...</span></span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MarkdownContent from './markdown_content'
|
||||||
|
import { IconInnerShadowTop } from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'KbaStep',
|
||||||
|
components: {
|
||||||
|
MarkdownContent,
|
||||||
|
IconInnerShadowTop
|
||||||
|
},
|
||||||
|
inject: ['baseUrl', 't'],
|
||||||
|
props: {
|
||||||
|
field: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
submitter: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
submitterSlug: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
emptyValueRequiredStep: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['submit'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
isSubmitting: false,
|
||||||
|
questions: null,
|
||||||
|
currentQuestionIndex: 0,
|
||||||
|
token: null,
|
||||||
|
reference: null,
|
||||||
|
answers: {},
|
||||||
|
error: null,
|
||||||
|
timeLeftSeconds: null,
|
||||||
|
countdownIntervalId: null,
|
||||||
|
form: {
|
||||||
|
fn: '',
|
||||||
|
ln: '',
|
||||||
|
addr: '',
|
||||||
|
city: '',
|
||||||
|
state: '',
|
||||||
|
zip: '',
|
||||||
|
dob: '',
|
||||||
|
ssn: '',
|
||||||
|
phone: '',
|
||||||
|
email: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
currentQuestion () {
|
||||||
|
return this.questions ? this.questions[this.currentQuestionIndex] : null
|
||||||
|
},
|
||||||
|
isRequiredFieldEmpty () {
|
||||||
|
return this.emptyValueRequiredStep && this.emptyValueRequiredStep[0] !== this.field
|
||||||
|
},
|
||||||
|
kbaTimeLimitSeconds () {
|
||||||
|
return 90
|
||||||
|
},
|
||||||
|
states () {
|
||||||
|
return [
|
||||||
|
{ code: 'AL', name: 'Alabama' },
|
||||||
|
{ code: 'AK', name: 'Alaska' },
|
||||||
|
{ code: 'AZ', name: 'Arizona' },
|
||||||
|
{ code: 'AR', name: 'Arkansas' },
|
||||||
|
{ code: 'CA', name: 'California' },
|
||||||
|
{ code: 'CO', name: 'Colorado' },
|
||||||
|
{ code: 'CT', name: 'Connecticut' },
|
||||||
|
{ code: 'DE', name: 'Delaware' },
|
||||||
|
{ code: 'DC', name: 'District of Columbia' },
|
||||||
|
{ code: 'FL', name: 'Florida' },
|
||||||
|
{ code: 'GA', name: 'Georgia' },
|
||||||
|
{ code: 'HI', name: 'Hawaii' },
|
||||||
|
{ code: 'ID', name: 'Idaho' },
|
||||||
|
{ code: 'IL', name: 'Illinois' },
|
||||||
|
{ code: 'IN', name: 'Indiana' },
|
||||||
|
{ code: 'IA', name: 'Iowa' },
|
||||||
|
{ code: 'KS', name: 'Kansas' },
|
||||||
|
{ code: 'KY', name: 'Kentucky' },
|
||||||
|
{ code: 'LA', name: 'Louisiana' },
|
||||||
|
{ code: 'ME', name: 'Maine' },
|
||||||
|
{ code: 'MD', name: 'Maryland' },
|
||||||
|
{ code: 'MA', name: 'Massachusetts' },
|
||||||
|
{ code: 'MI', name: 'Michigan' },
|
||||||
|
{ code: 'MN', name: 'Minnesota' },
|
||||||
|
{ code: 'MS', name: 'Mississippi' },
|
||||||
|
{ code: 'MO', name: 'Missouri' },
|
||||||
|
{ code: 'MT', name: 'Montana' },
|
||||||
|
{ code: 'NE', name: 'Nebraska' },
|
||||||
|
{ code: 'NV', name: 'Nevada' },
|
||||||
|
{ code: 'NH', name: 'New Hampshire' },
|
||||||
|
{ code: 'NJ', name: 'New Jersey' },
|
||||||
|
{ code: 'NM', name: 'New Mexico' },
|
||||||
|
{ code: 'NY', name: 'New York' },
|
||||||
|
{ code: 'NC', name: 'North Carolina' },
|
||||||
|
{ code: 'ND', name: 'North Dakota' },
|
||||||
|
{ code: 'OH', name: 'Ohio' },
|
||||||
|
{ code: 'OK', name: 'Oklahoma' },
|
||||||
|
{ code: 'OR', name: 'Oregon' },
|
||||||
|
{ code: 'PA', name: 'Pennsylvania' },
|
||||||
|
{ code: 'RI', name: 'Rhode Island' },
|
||||||
|
{ code: 'SC', name: 'South Carolina' },
|
||||||
|
{ code: 'SD', name: 'South Dakota' },
|
||||||
|
{ code: 'TN', name: 'Tennessee' },
|
||||||
|
{ code: 'TX', name: 'Texas' },
|
||||||
|
{ code: 'UT', name: 'Utah' },
|
||||||
|
{ code: 'VT', name: 'Vermont' },
|
||||||
|
{ code: 'VA', name: 'Virginia' },
|
||||||
|
{ code: 'WA', name: 'Washington' },
|
||||||
|
{ code: 'WV', name: 'West Virginia' },
|
||||||
|
{ code: 'WI', name: 'Wisconsin' },
|
||||||
|
{ code: 'WY', name: 'Wyoming' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
isLastQuestion () {
|
||||||
|
return this.questions && this.currentQuestionIndex === this.questions.length - 1
|
||||||
|
},
|
||||||
|
formattedTimeLeft () {
|
||||||
|
if (this.timeLeftSeconds === null) return ''
|
||||||
|
|
||||||
|
const minutes = Math.floor(this.timeLeftSeconds / 60)
|
||||||
|
const seconds = this.timeLeftSeconds % 60
|
||||||
|
|
||||||
|
return `${minutes}:${String(seconds).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeUnmount () {
|
||||||
|
this.clearCountdown()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clearCountdown () {
|
||||||
|
if (this.countdownIntervalId) {
|
||||||
|
clearInterval(this.countdownIntervalId)
|
||||||
|
this.countdownIntervalId = null
|
||||||
|
}
|
||||||
|
|
||||||
|
this.timeLeftSeconds = null
|
||||||
|
},
|
||||||
|
startCountdown () {
|
||||||
|
this.clearCountdown()
|
||||||
|
|
||||||
|
this.timeLeftSeconds = this.kbaTimeLimitSeconds
|
||||||
|
|
||||||
|
this.countdownIntervalId = setInterval(() => {
|
||||||
|
if (this.timeLeftSeconds === null) return
|
||||||
|
|
||||||
|
this.timeLeftSeconds -= 1
|
||||||
|
|
||||||
|
if (this.timeLeftSeconds <= 0) {
|
||||||
|
this.handleTimeout()
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
},
|
||||||
|
handleTimeout () {
|
||||||
|
this.clearCountdown()
|
||||||
|
|
||||||
|
this.questions = null
|
||||||
|
this.token = null
|
||||||
|
this.reference = null
|
||||||
|
this.answers = {}
|
||||||
|
this.currentQuestionIndex = 0
|
||||||
|
this.error = `Knowledge Based Authentication timed out. You only have ${this.kbaTimeLimitSeconds} seconds to complete the verification. Please retry.`
|
||||||
|
},
|
||||||
|
nextQuestion () {
|
||||||
|
if (this.isLastQuestion) {
|
||||||
|
this.$emit('submit')
|
||||||
|
} else {
|
||||||
|
this.currentQuestionIndex++
|
||||||
|
}
|
||||||
|
},
|
||||||
|
restartKba () {
|
||||||
|
this.clearCountdown()
|
||||||
|
|
||||||
|
this.questions = null
|
||||||
|
this.token = null
|
||||||
|
this.reference = null
|
||||||
|
this.answers = {}
|
||||||
|
this.currentQuestionIndex = 0
|
||||||
|
this.error = null
|
||||||
|
},
|
||||||
|
async startKba () {
|
||||||
|
this.isLoading = true
|
||||||
|
this.error = null
|
||||||
|
this.clearCountdown()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = { ...this.form, submitter_slug: this.submitterSlug }
|
||||||
|
|
||||||
|
if (payload.dob) {
|
||||||
|
payload.dob = payload.dob.replace(/-/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.ssn) {
|
||||||
|
payload.ssn = payload.ssn.replace(/\D/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.phone) {
|
||||||
|
payload.phone = payload.phone.replace(/^\+1/, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = await fetch(this.baseUrl + '/api/kba', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await resp.json()
|
||||||
|
|
||||||
|
if (!resp.ok) throw new Error(data.error || 'Failed to start KBA')
|
||||||
|
|
||||||
|
if (data.result && data.result.action === 'FAIL') {
|
||||||
|
if (data.result.detail === 'NO MATCH') {
|
||||||
|
throw new Error('Unfortunately, we were unable to start Knowledge Based Authentication with the details provided. Please review and confirm that all your personal details are correct.')
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(data.result.detail || 'KBA Start Failed')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.output && data.output.questions && data.output.questions.questions) {
|
||||||
|
this.questions = data.output.questions.questions
|
||||||
|
this.token = data.continuations.questions.template.token
|
||||||
|
this.reference = data.meta.reference
|
||||||
|
|
||||||
|
this.questions.forEach(q => {
|
||||||
|
this.answers[q.id] = null
|
||||||
|
})
|
||||||
|
|
||||||
|
this.startCountdown()
|
||||||
|
} else {
|
||||||
|
throw new Error('Invalid KBA response')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.error = e.message
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async submit () {
|
||||||
|
this.clearCountdown()
|
||||||
|
|
||||||
|
this.isSubmitting = true
|
||||||
|
this.error = null
|
||||||
|
|
||||||
|
const formattedAnswers = Object.keys(this.answers).reduce((acc, key) => {
|
||||||
|
acc[key] = [this.answers[key]]
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(this.baseUrl + `/api/kba/${this.field.uuid}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({
|
||||||
|
token: this.token,
|
||||||
|
answers: formattedAnswers,
|
||||||
|
reference: this.reference,
|
||||||
|
submitter_slug: this.submitterSlug
|
||||||
|
}),
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await resp.json()
|
||||||
|
|
||||||
|
if (data.result?.action !== 'PASS') {
|
||||||
|
if (data.result?.issues?.length) {
|
||||||
|
this.error = `Knowledge Based Authentication Failed - make sure you provide correct details for the Knowledge Based authentication: ${data.result.issues.join(', ')}`
|
||||||
|
} else {
|
||||||
|
this.error = 'Knowledge Based Authentication Failed - make sure you provide correct answers for the Knowledge Based authentication.'
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Knowledge Based Authentication Failed')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resp.ok) {
|
||||||
|
this.error = 'Failed to submit answers'
|
||||||
|
|
||||||
|
throw new Error('Failed to submit answers')
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp
|
||||||
|
} finally {
|
||||||
|
this.isSubmitting = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
<input
|
<input
|
||||||
:id="field.uuid"
|
:id="field.uuid"
|
||||||
ref="phone"
|
ref="phone"
|
||||||
:value="phoneValue"
|
:value="defaultValue && detectedPhoneValueDialCode ? phoneValue.split('+' + detectedPhoneValueDialCode).slice(-1).join('') : phoneValue"
|
||||||
:readonly="!!defaultValue"
|
:readonly="!!defaultValue"
|
||||||
class="base-input !text-2xl !rounded-l-none !border-l-0 !outline-none w-full"
|
class="base-input !text-2xl !rounded-l-none !border-l-0 !outline-none w-full"
|
||||||
autocomplete="tel"
|
autocomplete="tel"
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<div class="space-x-2 flex flex-none">
|
<div class="space-x-2 flex flex-none">
|
||||||
<span
|
<span
|
||||||
v-if="isTextSignature && format !== 'typed_or_upload' && format !== 'typed' && format !== 'upload'"
|
v-if="isTextSignature && format !== 'typed_or_upload' && format !== 'typed' && format !== 'upload'"
|
||||||
class="tooltip"
|
class="md:tooltip"
|
||||||
:data-tip="t('draw_signature')"
|
:data-tip="t('draw_signature')"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-else-if="withTypedSignature && format !== 'drawn_or_upload' && format !== 'typed_or_upload' && format !== 'typed' && format !== 'drawn' && format !== 'upload'"
|
v-else-if="withTypedSignature && format !== 'drawn_or_upload' && format !== 'typed_or_upload' && format !== 'typed' && format !== 'drawn' && format !== 'upload'"
|
||||||
class="tooltip ml-2"
|
class="md:tooltip ml-2"
|
||||||
:class="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
|
:class="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
|
||||||
:data-tip="t('type_text')"
|
:data-tip="t('type_text')"
|
||||||
>
|
>
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="format !== 'typed' && format !== 'drawn' && format !== 'upload' && format !== 'drawn_or_typed'"
|
v-if="format !== 'typed' && format !== 'drawn' && format !== 'upload' && format !== 'drawn_or_typed'"
|
||||||
class="tooltip"
|
class="md:tooltip"
|
||||||
:class="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
|
:class="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
|
||||||
:data-tip="t('take_photo')"
|
:data-tip="t('take_photo')"
|
||||||
>
|
>
|
||||||
@@ -86,8 +86,8 @@
|
|||||||
</a>
|
</a>
|
||||||
<span
|
<span
|
||||||
v-if="withQrButton && !modelValue && !computedPreviousValue && format !== 'typed_or_upload' && format !== 'typed' && format !== 'upload'"
|
v-if="withQrButton && !modelValue && !computedPreviousValue && format !== 'typed_or_upload' && format !== 'typed' && format !== 'upload'"
|
||||||
class="tooltip before:translate-x-[-90%]"
|
class="md:tooltip before:translate-x-[-90%]"
|
||||||
:data-tip="t('drawn_signature_on_a_touchscreen_device')"
|
:data-tip="t('sign_on_the_touchscreen')"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
@@ -127,6 +127,12 @@
|
|||||||
type="hidden"
|
type="hidden"
|
||||||
:name="`values[${field.uuid}]`"
|
:name="`values[${field.uuid}]`"
|
||||||
>
|
>
|
||||||
|
<input
|
||||||
|
v-if="isTouchAttachment"
|
||||||
|
:value="touchAttachmentUuid"
|
||||||
|
type="hidden"
|
||||||
|
name="touch_attachment_uuid"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
v-if="modelValue || computedPreviousValue"
|
v-if="modelValue || computedPreviousValue"
|
||||||
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
|
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
|
||||||
@@ -142,7 +148,7 @@
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="relative"
|
class="relative select-none"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="!modelValue && !computedPreviousValue && !isShowQr && !isTextSignature && isSignatureStarted"
|
v-if="!modelValue && !computedPreviousValue && !isShowQr && !isTextSignature && isSignatureStarted"
|
||||||
@@ -175,9 +181,7 @@
|
|||||||
v-if="isShowQr"
|
v-if="isShowQr"
|
||||||
class="top-0 bottom-0 right-0 left-0 absolute bg-base-content/10 rounded-2xl"
|
class="top-0 bottom-0 right-0 left-0 absolute bg-base-content/10 rounded-2xl"
|
||||||
>
|
>
|
||||||
<div
|
<div class="absolute top-1.5 right-1.5">
|
||||||
class="absolute top-1.5 right-1.5 tooltip"
|
|
||||||
>
|
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="btn btn-sm btn-circle btn-normal btn-outline"
|
class="btn btn-sm btn-circle btn-normal btn-outline"
|
||||||
@@ -211,7 +215,7 @@
|
|||||||
@input="updateWrittenSignature"
|
@input="updateWrittenSignature"
|
||||||
>
|
>
|
||||||
<select
|
<select
|
||||||
v-if="requireSigningReason && !isOtherReason"
|
v-if="withSigningReason && !isOtherReason"
|
||||||
class="select base-input !text-2xl w-full mt-6 text-center"
|
class="select base-input !text-2xl w-full mt-6 text-center"
|
||||||
:class="{ 'text-gray-300': !reason }"
|
:class="{ 'text-gray-300': !reason }"
|
||||||
required
|
required
|
||||||
@@ -226,24 +230,37 @@
|
|||||||
>
|
>
|
||||||
{{ t('select_a_reason') }}
|
{{ t('select_a_reason') }}
|
||||||
</option>
|
</option>
|
||||||
<option
|
<template v-if="field.preferences?.reasons">
|
||||||
v-for="(label, option) in defaultReasons"
|
<option
|
||||||
:key="option"
|
v-for="option in field.preferences.reasons"
|
||||||
:value="option"
|
:key="option"
|
||||||
:selected="reason === option"
|
:value="option"
|
||||||
class="text-base-content"
|
:selected="reason === option"
|
||||||
>
|
class="text-base-content"
|
||||||
{{ label }}
|
>
|
||||||
</option>
|
{{ option }}
|
||||||
<option
|
</option>
|
||||||
value="other"
|
</template>
|
||||||
class="text-base-content"
|
<template v-else>
|
||||||
>
|
<option
|
||||||
{{ t('other') }}
|
v-for="(label, option) in defaultReasons"
|
||||||
</option>
|
:key="option"
|
||||||
|
:value="option"
|
||||||
|
:selected="reason === option"
|
||||||
|
class="text-base-content"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="other"
|
||||||
|
class="text-base-content"
|
||||||
|
>
|
||||||
|
{{ t('other') }}
|
||||||
|
</option>
|
||||||
|
</template>
|
||||||
</select>
|
</select>
|
||||||
<input
|
<input
|
||||||
v-if="requireSigningReason && isOtherReason"
|
v-if="withSigningReason && isOtherReason"
|
||||||
class="base-input !text-2xl w-full mt-6"
|
class="base-input !text-2xl w-full mt-6"
|
||||||
required
|
required
|
||||||
:name="`values[${field.preferences.reason_field_uuid}]`"
|
:name="`values[${field.preferences.reason_field_uuid}]`"
|
||||||
@@ -253,7 +270,7 @@
|
|||||||
@input="$emit('update:reason', $event.target.value)"
|
@input="$emit('update:reason', $event.target.value)"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
v-if="requireSigningReason"
|
v-if="withSigningReason"
|
||||||
hidden
|
hidden
|
||||||
name="with_reason"
|
name="with_reason"
|
||||||
:value="field.preferences.reason_field_uuid"
|
:value="field.preferences.reason_field_uuid"
|
||||||
@@ -268,7 +285,7 @@
|
|||||||
<div
|
<div
|
||||||
v-else-if="withDisclosure"
|
v-else-if="withDisclosure"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="text-base-content/60 text-xs text-center w-full mt-1"
|
class="text-base-content/60 text-xs text-center w-full mt-1 select-none"
|
||||||
>
|
>
|
||||||
{{ t('by_clicking_you_agree_to_the').replace('{button}', buttonText.charAt(0).toUpperCase() + buttonText.slice(1)) }} <a
|
{{ t('by_clicking_you_agree_to_the').replace('{button}', buttonText.charAt(0).toUpperCase() + buttonText.slice(1)) }} <a
|
||||||
href="https://www.docuseal.com/esign-disclosure"
|
href="https://www.docuseal.com/esign-disclosure"
|
||||||
@@ -323,6 +340,10 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
values: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
requireSigningReason: {
|
requireSigningReason: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -377,6 +398,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
touchAttachmentUuid: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
reason: {
|
reason: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -388,13 +414,14 @@ export default {
|
|||||||
default: ''
|
default: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['attached', 'update:model-value', 'start', 'minimize', 'update:reason'],
|
emits: ['attached', 'update:model-value', 'start', 'minimize', 'update:reason', 'touch-attachment'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isSignatureStarted: false,
|
isSignatureStarted: false,
|
||||||
isShowQr: false,
|
isShowQr: false,
|
||||||
isOtherReason: false,
|
isOtherReason: false,
|
||||||
isUsePreviousValue: true,
|
isUsePreviousValue: true,
|
||||||
|
isTouchAttachment: false,
|
||||||
isTextSignature: this.field.preferences?.format === 'typed' || this.field.preferences?.format === 'typed_or_upload',
|
isTextSignature: this.field.preferences?.format === 'typed' || this.field.preferences?.format === 'typed_or_upload',
|
||||||
uploadImageInputKey: Math.random().toString()
|
uploadImageInputKey: Math.random().toString()
|
||||||
}
|
}
|
||||||
@@ -406,6 +433,9 @@ export default {
|
|||||||
format () {
|
format () {
|
||||||
return this.field.preferences?.format
|
return this.field.preferences?.format
|
||||||
},
|
},
|
||||||
|
withSigningReason () {
|
||||||
|
return this.requireSigningReason || this.field.preferences?.reasons?.length
|
||||||
|
},
|
||||||
defaultReasons () {
|
defaultReasons () {
|
||||||
return {
|
return {
|
||||||
[this.t('approved_by')]: this.t('approved'),
|
[this.t('approved_by')]: this.t('approved'),
|
||||||
@@ -424,21 +454,15 @@ export default {
|
|||||||
created () {
|
created () {
|
||||||
this.isSignatureStarted = !!this.computedPreviousValue
|
this.isSignatureStarted = !!this.computedPreviousValue
|
||||||
|
|
||||||
if (this.requireSigningReason) {
|
if (this.withSigningReason) {
|
||||||
this.field.preferences ||= {}
|
this.field.preferences ||= {}
|
||||||
this.field.preferences.reason_field_uuid ||= v4()
|
this.field.preferences.reason_field_uuid ||= v4()
|
||||||
this.isOtherReason = this.reason && !this.defaultReasons[this.reason]
|
this.isOtherReason = this.reason && !this.defaultReasons[this.reason] &&
|
||||||
|
(!this.field.preferences?.reasons?.length || !this.field.preferences.reasons.includes(this.reason))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted () {
|
async mounted () {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => this.setCanvasSize())
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (this.$refs.canvas) {
|
if (this.$refs.canvas) {
|
||||||
this.pad = new SignaturePad(this.$refs.canvas)
|
this.pad = new SignaturePad(this.$refs.canvas)
|
||||||
@@ -453,13 +477,10 @@ export default {
|
|||||||
this.$emit('start')
|
this.$emit('start')
|
||||||
})
|
})
|
||||||
|
|
||||||
this.intersectionObserver = new IntersectionObserver((entries, observer) => {
|
this.intersectionObserver = new IntersectionObserver((entries) => {
|
||||||
entries.forEach(entry => {
|
entries.forEach(entry => {
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
|
this.setCanvasSize()
|
||||||
this.$refs.canvas.height = this.$refs.canvas.parentNode.clientWidth * scale / 3
|
|
||||||
|
|
||||||
this.$refs.canvas.getContext('2d').scale(scale, scale)
|
|
||||||
|
|
||||||
this.intersectionObserver?.disconnect()
|
this.intersectionObserver?.disconnect()
|
||||||
}
|
}
|
||||||
@@ -467,13 +488,62 @@ export default {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this.intersectionObserver.observe(this.$refs.canvas)
|
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 () {
|
beforeUnmount () {
|
||||||
this.intersectionObserver?.disconnect()
|
this.intersectionObserver?.disconnect()
|
||||||
|
this.resizeObserver?.disconnect()
|
||||||
this.stopCheckSignature()
|
this.stopCheckSignature()
|
||||||
},
|
},
|
||||||
methods: {
|
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 () {
|
remove () {
|
||||||
this.$emit('update:model-value', '')
|
this.$emit('update:model-value', '')
|
||||||
|
|
||||||
@@ -677,6 +747,13 @@ export default {
|
|||||||
},
|
},
|
||||||
async submit () {
|
async submit () {
|
||||||
if (this.modelValue || this.computedPreviousValue) {
|
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) {
|
if (this.computedPreviousValue) {
|
||||||
this.$emit('update:model-value', this.computedPreviousValue)
|
this.$emit('update:model-value', this.computedPreviousValue)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ export default {
|
|||||||
docId: this.eidEasyData.doc_id,
|
docId: this.eidEasyData.doc_id,
|
||||||
language: this.locale,
|
language: this.locale,
|
||||||
countryCode: this.countryCode,
|
countryCode: this.countryCode,
|
||||||
|
sandbox: ['demo.docuseal.tech'].includes(location.host),
|
||||||
enabledMethods: {
|
enabledMethods: {
|
||||||
signature: this.eidEasyData.available_methods
|
signature: this.eidEasyData.available_methods
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
@touchstart="startTouchDrag"
|
@touchstart="startTouchDrag"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="isSelected || isDraw"
|
v-if="isSelected || isDraw || isInMultiSelection"
|
||||||
class="top-0 bottom-0 right-0 left-0 absolute border border-1.5 pointer-events-none"
|
class="top-0 bottom-0 right-0 left-0 absolute border border-1.5 pointer-events-none"
|
||||||
:class="activeBorderClasses"
|
:class="activeBorderClasses"
|
||||||
/>
|
/>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
:style="{ left: (cellW / area.w * 100) + '%' }"
|
:style="{ left: (cellW / area.w * 100) + '%' }"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-if="index === 0 && editable"
|
v-if="index === 0 && editable && !isInMultiSelection"
|
||||||
class="h-2.5 w-2.5 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-ew-resize z-10"
|
class="h-2.5 w-2.5 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-ew-resize z-10"
|
||||||
style="left: -4px"
|
style="left: -4px"
|
||||||
@mousedown.stop="startResizeCell"
|
@mousedown.stop="startResizeCell"
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="field?.type && (isSelected || isNameFocus)"
|
v-if="field?.type && (isSelected || isNameFocus) && !isInMultiSelection"
|
||||||
class="absolute bg-white rounded-t border overflow-visible whitespace-nowrap flex z-10 field-area-controls"
|
class="absolute bg-white rounded-t border overflow-visible whitespace-nowrap flex z-10 field-area-controls"
|
||||||
style="top: -25px; height: 25px"
|
style="top: -25px; height: 25px"
|
||||||
@mousedown.stop
|
@mousedown.stop
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
:menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px] mt-[1px]'"
|
:menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px] mt-[1px]'"
|
||||||
:submitters="template.submitters"
|
:submitters="template.submitters"
|
||||||
@update:model-value="save"
|
@update:model-value="save"
|
||||||
@click="selectedAreaRef.value = area"
|
@click="selectedAreasRef.value = [area]"
|
||||||
/>
|
/>
|
||||||
<FieldType
|
<FieldType
|
||||||
v-model="field.type"
|
v-model="field.type"
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
:button-classes="'px-1'"
|
:button-classes="'px-1'"
|
||||||
:menu-classes="'bg-white rounded-t-none'"
|
:menu-classes="'bg-white rounded-t-none'"
|
||||||
@update:model-value="[maybeUpdateOptions(), save()]"
|
@update:model-value="[maybeUpdateOptions(), save()]"
|
||||||
@click="selectedAreaRef.value = area"
|
@click="selectedAreasRef.value = [area]"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
v-if="field.type !== 'checkbox' || field.name"
|
v-if="field.type !== 'checkbox' || field.name"
|
||||||
@@ -134,6 +134,7 @@
|
|||||||
@focusout="maybeBlurSettings"
|
@focusout="maybeBlurSettings"
|
||||||
>
|
>
|
||||||
<FieldSettings
|
<FieldSettings
|
||||||
|
v-if="isMobile"
|
||||||
:field="field"
|
:field="field"
|
||||||
:default-field="defaultField"
|
:default-field="defaultField"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
@@ -145,9 +146,17 @@
|
|||||||
@click-formula="isShowFormulaModal = true"
|
@click-formula="isShowFormulaModal = true"
|
||||||
@click-font="isShowFontModal = true"
|
@click-font="isShowFontModal = true"
|
||||||
@click-description="isShowDescriptionModal = true"
|
@click-description="isShowDescriptionModal = true"
|
||||||
|
@add-custom-field="$emit('add-custom-field')"
|
||||||
@click-condition="isShowConditionsModal = true"
|
@click-condition="isShowConditionsModal = true"
|
||||||
@scroll-to="[selectedAreaRef.value = $event, $emit('scroll-to', $event)]"
|
@save="save"
|
||||||
|
@scroll-to="[selectedAreasRef.value = [$event], $emit('scroll-to', $event)]"
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="whitespace-normal"
|
||||||
|
>
|
||||||
|
The dots menu is retired in favor of the field context menu. Right-click the field to access field settings. Double-click the field to set a default value.
|
||||||
|
</div>
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -250,7 +259,8 @@
|
|||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
v-else-if="field.type === 'cells' && field.default_value"
|
v-else-if="field.type === 'cells' && field.default_value"
|
||||||
class="w-full flex items-center"
|
class="w-full flex"
|
||||||
|
:class="fontClasses"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(char, index) in field.default_value"
|
v-for="(char, index) in field.default_value"
|
||||||
@@ -266,7 +276,7 @@
|
|||||||
ref="defaultValueSelect"
|
ref="defaultValueSelect"
|
||||||
class="bg-transparent outline-none focus:outline-none w-full"
|
class="bg-transparent outline-none focus:outline-none w-full"
|
||||||
@change="[field.default_value = $event.target.value, field.readonly = !!field.default_value?.length, save()]"
|
@change="[field.default_value = $event.target.value, field.readonly = !!field.default_value?.length, save()]"
|
||||||
@focus="selectedAreaRef.value = area"
|
@focus="selectedAreasRef.value = [area]"
|
||||||
@keydown.enter="onDefaultValueEnter"
|
@keydown.enter="onDefaultValueEnter"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
@@ -293,7 +303,7 @@
|
|||||||
:class="{ 'cursor-text': isValueInput }"
|
:class="{ 'cursor-text': isValueInput }"
|
||||||
:placeholder="withFieldPlaceholder && !isValueInput ? defaultField?.title || field.title || field.name || defaultName : (field.type === 'date' ? field.preferences?.format || t('type_value') : t('type_value'))"
|
:placeholder="withFieldPlaceholder && !isValueInput ? defaultField?.title || field.title || field.name || defaultName : (field.type === 'date' ? field.preferences?.format || t('type_value') : t('type_value'))"
|
||||||
@blur="onDefaultValueBlur"
|
@blur="onDefaultValueBlur"
|
||||||
@focus="selectedAreaRef.value = area"
|
@focus="selectedAreasRef.value = [area]"
|
||||||
@paste.prevent="onPaste"
|
@paste.prevent="onPaste"
|
||||||
@keydown.enter="onDefaultValueEnter"
|
@keydown.enter="onDefaultValueEnter"
|
||||||
>{{ field.default_value }}</span>
|
>{{ field.default_value }}</span>
|
||||||
@@ -319,6 +329,7 @@
|
|||||||
<span
|
<span
|
||||||
v-if="field?.type && editable"
|
v-if="field?.type && editable"
|
||||||
class="h-4 w-4 lg:h-2.5 lg:w-2.5 -right-1 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-nwse-resize"
|
class="h-4 w-4 lg:h-2.5 lg:w-2.5 -right-1 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-nwse-resize"
|
||||||
|
:class="{ 'z-30': isInMultiSelection }"
|
||||||
@mousedown.stop="startResize"
|
@mousedown.stop="startResize"
|
||||||
@touchstart="startTouchResize"
|
@touchstart="startTouchResize"
|
||||||
/>
|
/>
|
||||||
@@ -331,6 +342,7 @@
|
|||||||
:editable="editable && !defaultField"
|
:editable="editable && !defaultField"
|
||||||
:default-field="defaultField"
|
:default-field="defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
|
@save="save"
|
||||||
@close="isShowFormulaModal = false"
|
@close="isShowFormulaModal = false"
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
@@ -343,6 +355,7 @@
|
|||||||
:editable="editable && !defaultField"
|
:editable="editable && !defaultField"
|
||||||
:default-field="defaultField"
|
:default-field="defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
|
@save="save"
|
||||||
@close="isShowFontModal = false"
|
@close="isShowFontModal = false"
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
@@ -354,6 +367,7 @@
|
|||||||
:item="field"
|
:item="field"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
:default-field="defaultField"
|
:default-field="defaultField"
|
||||||
|
@save="save"
|
||||||
@close="isShowConditionsModal = false"
|
@close="isShowConditionsModal = false"
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
@@ -366,6 +380,7 @@
|
|||||||
:editable="editable && !defaultField"
|
:editable="editable && !defaultField"
|
||||||
:default-field="defaultField"
|
:default-field="defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
|
@save="save"
|
||||||
@close="isShowDescriptionModal = false"
|
@close="isShowDescriptionModal = false"
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
@@ -398,7 +413,7 @@ export default {
|
|||||||
FieldSubmitter,
|
FieldSubmitter,
|
||||||
IconX
|
IconX
|
||||||
},
|
},
|
||||||
inject: ['template', 'selectedAreaRef', 'save', 't', 'isInlineSize'],
|
inject: ['template', 'save', 't', 'isInlineSize', 'selectedAreasRef', 'isCmdKeyRef', 'getFieldTypeIndex'],
|
||||||
props: {
|
props: {
|
||||||
area: {
|
area: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -463,9 +478,19 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
default: null
|
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 () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isShowFormulaModal: false,
|
isShowFormulaModal: false,
|
||||||
@@ -571,7 +596,7 @@ export default {
|
|||||||
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
||||||
},
|
},
|
||||||
defaultName () {
|
defaultName () {
|
||||||
return this.buildDefaultName(this.field, this.template.fields)
|
return this.buildDefaultName(this.field)
|
||||||
},
|
},
|
||||||
fontClasses () {
|
fontClasses () {
|
||||||
if (!this.field.preferences) {
|
if (!this.field.preferences) {
|
||||||
@@ -646,7 +671,10 @@ export default {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
isSelected () {
|
isSelected () {
|
||||||
return this.selectedAreaRef.value === this.area
|
return this.selectedAreasRef.value.includes(this.area)
|
||||||
|
},
|
||||||
|
isInMultiSelection () {
|
||||||
|
return this.selectedAreasRef.value.length >= 2 && this.isSelected
|
||||||
},
|
},
|
||||||
positionStyle () {
|
positionStyle () {
|
||||||
const { x, y, w, h } = this.area
|
const { x, y, w, h } = this.area
|
||||||
@@ -683,10 +711,10 @@ export default {
|
|||||||
buildAreaOptionValue (area) {
|
buildAreaOptionValue (area) {
|
||||||
const option = this.optionsUuidIndex[area.option_uuid]
|
const option = this.optionsUuidIndex[area.option_uuid]
|
||||||
|
|
||||||
return option.value || `${this.t('option')} ${this.field.options.indexOf(option) + 1}`
|
return option?.value || `${this.t('option')} ${this.field.options.indexOf(option) + 1}`
|
||||||
},
|
},
|
||||||
maybeToggleDefaultValue () {
|
maybeToggleDefaultValue () {
|
||||||
if (!this.editable) {
|
if (!this.editable || this.isCmdKeyRef.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -770,7 +798,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onNameFocus (e) {
|
onNameFocus (e) {
|
||||||
this.selectedAreaRef.value = this.area
|
this.selectedAreasRef.value = [this.area]
|
||||||
|
|
||||||
this.isNameFocus = true
|
this.isNameFocus = true
|
||||||
this.$refs.name.style.minWidth = this.$refs.name.clientWidth + 'px'
|
this.$refs.name.style.minWidth = this.$refs.name.clientWidth + 'px'
|
||||||
@@ -906,6 +934,15 @@ export default {
|
|||||||
if (e.target.id === 'mask') {
|
if (e.target.id === 'mask') {
|
||||||
this.area.w = e.offsetX / e.target.clientWidth - this.area.x
|
this.area.w = e.offsetX / e.target.clientWidth - this.area.x
|
||||||
this.area.h = e.offsetY / e.target.clientHeight - this.area.y
|
this.area.h = e.offsetY / e.target.clientHeight - this.area.y
|
||||||
|
|
||||||
|
if (this.isInMultiSelection) {
|
||||||
|
this.selectedAreasRef.value.forEach((area) => {
|
||||||
|
if (area !== this.area) {
|
||||||
|
area.w = this.area.w
|
||||||
|
area.h = this.area.h
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
drag (e) {
|
drag (e) {
|
||||||
@@ -931,7 +968,7 @@ export default {
|
|||||||
|
|
||||||
const rect = e.target.getBoundingClientRect()
|
const rect = e.target.getBoundingClientRect()
|
||||||
|
|
||||||
this.selectedAreaRef.value = this.area
|
this.selectedAreasRef.value = [this.area]
|
||||||
|
|
||||||
this.dragFrom = { x: rect.left - e.touches[0].clientX, y: rect.top - e.touches[0].clientY }
|
this.dragFrom = { x: rect.left - e.touches[0].clientX, y: rect.top - e.touches[0].clientY }
|
||||||
|
|
||||||
@@ -980,13 +1017,23 @@ export default {
|
|||||||
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (e.metaKey || e.ctrlKey) {
|
||||||
|
if (!this.selectedAreasRef.value.includes(this.area)) {
|
||||||
|
this.selectedAreasRef.value.push(this.area)
|
||||||
|
} else {
|
||||||
|
this.selectedAreasRef.value.splice(this.selectedAreasRef.value.indexOf(this.area), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (this.editable) {
|
if (this.editable) {
|
||||||
this.isDragged = true
|
this.isDragged = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const rect = e.target.getBoundingClientRect()
|
const rect = e.target.getBoundingClientRect()
|
||||||
|
|
||||||
this.selectedAreaRef.value = this.area
|
this.selectedAreasRef.value = [this.area]
|
||||||
|
|
||||||
this.dragFrom = { x: rect.left - e.clientX, y: rect.top - e.clientY }
|
this.dragFrom = { x: rect.left - e.clientX, y: rect.top - e.clientY }
|
||||||
|
|
||||||
@@ -1055,7 +1102,9 @@ export default {
|
|||||||
this.$emit('stop-drag')
|
this.$emit('stop-drag')
|
||||||
},
|
},
|
||||||
startResize () {
|
startResize () {
|
||||||
this.selectedAreaRef.value = this.area
|
if (!this.selectedAreasRef.value.includes(this.area)) {
|
||||||
|
this.selectedAreasRef.value = [this.area]
|
||||||
|
}
|
||||||
|
|
||||||
this.$el.getRootNode().addEventListener('mousemove', this.resize)
|
this.$el.getRootNode().addEventListener('mousemove', this.resize)
|
||||||
this.$el.getRootNode().addEventListener('mouseup', this.stopResize)
|
this.$el.getRootNode().addEventListener('mouseup', this.stopResize)
|
||||||
@@ -1071,7 +1120,9 @@ export default {
|
|||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
startTouchResize (e) {
|
startTouchResize (e) {
|
||||||
this.selectedAreaRef.value = this.area
|
if (!this.selectedAreasRef.value.includes(this.area)) {
|
||||||
|
this.selectedAreasRef.value = [this.area]
|
||||||
|
}
|
||||||
|
|
||||||
this.$refs?.name?.blur()
|
this.$refs?.name?.blur()
|
||||||
|
|
||||||
@@ -1088,6 +1139,15 @@ export default {
|
|||||||
|
|
||||||
this.area.w = (e.touches[0].clientX - rect.left) / rect.width - this.area.x
|
this.area.w = (e.touches[0].clientX - rect.left) / rect.width - this.area.x
|
||||||
this.area.h = (e.touches[0].clientY - rect.top) / rect.height - this.area.y
|
this.area.h = (e.touches[0].clientY - rect.top) / rect.height - this.area.y
|
||||||
|
|
||||||
|
if (this.isInMultiSelection) {
|
||||||
|
this.selectedAreasRef.value.forEach((area) => {
|
||||||
|
if (area !== this.area) {
|
||||||
|
area.w = this.area.w
|
||||||
|
area.h = this.area.h
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
stopTouchResize () {
|
stopTouchResize () {
|
||||||
this.$el.getRootNode().removeEventListener('touchmove', this.touchResize)
|
this.$el.getRootNode().removeEventListener('touchmove', this.touchResize)
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -9,7 +9,7 @@
|
|||||||
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
<div class="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">
|
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||||
<span class="modal-title">
|
<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>
|
</span>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
class="text-base-content"
|
class="text-base-content"
|
||||||
:selected="condition.field_uuid === f.uuid"
|
:selected="condition.field_uuid === f.uuid"
|
||||||
>
|
>
|
||||||
{{ f.name || buildDefaultName(f, template.fields) }}
|
{{ f.name || buildDefaultName(f) }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<select
|
<select
|
||||||
@@ -124,6 +124,16 @@
|
|||||||
{{ option.value || `${t('option')} ${index + 1}` }}
|
{{ option.value || `${t('option')} ${index + 1}` }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
@@ -154,7 +164,7 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: 'ConditionModal',
|
name: 'ConditionModal',
|
||||||
inject: ['t', 'save', 'template', 'withConditions'],
|
inject: ['t', 'template', 'withConditions'],
|
||||||
props: {
|
props: {
|
||||||
item: {
|
item: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -168,9 +178,14 @@ export default {
|
|||||||
buildDefaultName: {
|
buildDefaultName: {
|
||||||
type: Function,
|
type: Function,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
excludeFieldUuids: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['close'],
|
emits: ['close', 'save'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
conditions: this.item.conditions?.[0] ? JSON.parse(JSON.stringify(this.item.conditions)) : [{}]
|
conditions: this.item.conditions?.[0] ? JSON.parse(JSON.stringify(this.item.conditions)) : [{}]
|
||||||
@@ -183,14 +198,14 @@ export default {
|
|||||||
fields () {
|
fields () {
|
||||||
if (this.item.submitter_uuid) {
|
if (this.item.submitter_uuid) {
|
||||||
return this.template.fields.reduce((acc, f) => {
|
return this.template.fields.reduce((acc, f) => {
|
||||||
if (f !== this.item && !this.excludeTypes.includes(f.type) && (!f.conditions?.length || !f.conditions.find((c) => c.field_uuid === this.item.uuid))) {
|
if (f !== this.item && !this.excludeTypes.includes(f.type) && !this.excludeFieldUuids.includes(f.uuid) && (!f.conditions?.length || !f.conditions.find((c) => c.field_uuid === this.item.uuid))) {
|
||||||
acc.push(f)
|
acc.push(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc
|
return acc
|
||||||
}, [])
|
}, [])
|
||||||
} else {
|
} else {
|
||||||
return this.template.fields
|
return this.template.fields.filter((f) => !this.excludeFieldUuids.includes(f.uuid))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -217,6 +232,8 @@ export default {
|
|||||||
actions.push('equal', 'not_equal')
|
actions.push('equal', 'not_equal')
|
||||||
} else if (['multiple'].includes(field.type)) {
|
} else if (['multiple'].includes(field.type)) {
|
||||||
actions.push('contains', 'does_not_contain')
|
actions.push('contains', 'does_not_contain')
|
||||||
|
} else if (field.type === 'number') {
|
||||||
|
actions.push('not_empty', 'empty', 'equal', 'not_equal', 'greater_than', 'less_than')
|
||||||
} else {
|
} else {
|
||||||
actions.push('not_empty', 'empty')
|
actions.push('not_empty', 'empty')
|
||||||
}
|
}
|
||||||
@@ -234,7 +251,7 @@ export default {
|
|||||||
delete this.item.conditions
|
delete this.item.conditions
|
||||||
}
|
}
|
||||||
|
|
||||||
this.save()
|
this.$emit('save')
|
||||||
this.$emit('close')
|
this.$emit('close')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,18 +6,24 @@
|
|||||||
<span
|
<span
|
||||||
ref="contenteditable"
|
ref="contenteditable"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
:contenteditable="editable"
|
:contenteditable="editable && (!editableOnButton || isEditable)"
|
||||||
style="min-width: 2px"
|
:data-placeholder="placeholder"
|
||||||
:class="[iconInline ? 'inline' : 'block', hideIcon ? 'focus:block' : '']"
|
:data-empty="isEmpty"
|
||||||
class="peer outline-none"
|
: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"
|
@paste.prevent="onPaste"
|
||||||
@keydown.enter.prevent="blurContenteditable"
|
@keydown.enter.prevent="blurContenteditable"
|
||||||
|
@input="updateInputValue"
|
||||||
|
@cut="updateInputValue"
|
||||||
@focus="$emit('focus', $event)"
|
@focus="$emit('focus', $event)"
|
||||||
@blur="onBlur"
|
@blur="onBlur"
|
||||||
|
@click="editable && (!editableOnButton || isEditable) ? '' : $emit('click-contenteditable')"
|
||||||
>
|
>
|
||||||
{{ value }}
|
{{ value }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
|
v-if="withButton"
|
||||||
class="relative inline"
|
class="relative inline"
|
||||||
:class="{ 'peer-focus:hidden': hideIcon, 'peer-focus:invisible': !hideIcon }"
|
: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 }"
|
:class="{ invisible: !editable, 'absolute top-1/2 -translate-y-1/2': !iconInline || floatIcon, 'inline align-bottom': iconInline, 'left-0': floatIcon }"
|
||||||
:width="iconWidth + 4"
|
:width="iconWidth + 4"
|
||||||
:stroke-width="iconStrokeWidth"
|
:stroke-width="iconStrokeWidth"
|
||||||
@click="[focusContenteditable(), selectOnEditClick && selectContent()]"
|
@click="clickEdit"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -49,6 +55,16 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
withButton: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
iconInline: {
|
iconInline: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -74,6 +90,16 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
editableOnButton: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
minWidth: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '2px'
|
||||||
|
},
|
||||||
editable: {
|
editable: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -85,21 +111,34 @@ export default {
|
|||||||
default: 2
|
default: 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['update:model-value', 'focus', 'blur'],
|
emits: ['update:model-value', 'focus', 'blur', 'click-contenteditable'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
isEditable: false,
|
||||||
|
inputValue: '',
|
||||||
value: ''
|
value: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
isEmpty () {
|
||||||
|
return !this.inputValue.replace(/\u200B/g, '').replace(/\u00A0/g, ' ').trim()
|
||||||
|
}
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
handler (value) {
|
handler (value) {
|
||||||
this.value = value
|
this.value = value || ''
|
||||||
},
|
},
|
||||||
immediate: true
|
immediate: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mounted () {
|
||||||
|
this.updateInputValue()
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
updateInputValue () {
|
||||||
|
this.inputValue = this.$refs.contenteditable?.textContent || ''
|
||||||
|
},
|
||||||
onPaste (e) {
|
onPaste (e) {
|
||||||
const text = (e.clipboardData || window.clipboardData).getData('text/plain')
|
const text = (e.clipboardData || window.clipboardData).getData('text/plain')
|
||||||
|
|
||||||
@@ -110,6 +149,20 @@ export default {
|
|||||||
selection.getRangeAt(0).insertNode(document.createTextNode(text))
|
selection.getRangeAt(0).insertNode(document.createTextNode(text))
|
||||||
selection.collapseToEnd()
|
selection.collapseToEnd()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.updateInputValue()
|
||||||
|
},
|
||||||
|
clickEdit (e) {
|
||||||
|
this.focusContenteditable()
|
||||||
|
|
||||||
|
if (this.selectOnEditClick) {
|
||||||
|
this.selectContent()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setText (text) {
|
||||||
|
this.$refs.contenteditable.innerText = text
|
||||||
|
|
||||||
|
this.updateInputValue()
|
||||||
},
|
},
|
||||||
selectContent () {
|
selectContent () {
|
||||||
const el = this.$refs.contenteditable
|
const el = this.$refs.contenteditable
|
||||||
@@ -126,13 +179,24 @@ export default {
|
|||||||
},
|
},
|
||||||
onBlur (e) {
|
onBlur (e) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.value = this.$refs.contenteditable.innerText.trim() || this.modelValue
|
if (this.$refs.contenteditable) {
|
||||||
this.$emit('update:model-value', this.value)
|
this.value = this.$refs.contenteditable.innerText.trim() || this.modelValue
|
||||||
|
this.$emit('update:model-value', this.value)
|
||||||
|
}
|
||||||
|
|
||||||
this.$emit('blur', e)
|
this.$emit('blur', e)
|
||||||
|
|
||||||
|
this.isEditable = false
|
||||||
}, 1)
|
}, 1)
|
||||||
},
|
},
|
||||||
focusContenteditable () {
|
focusContenteditable () {
|
||||||
this.$refs.contenteditable.focus()
|
this.isEditable = true
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.contenteditable.focus()
|
||||||
|
|
||||||
|
this.updateInputValue()
|
||||||
|
})
|
||||||
},
|
},
|
||||||
blurContenteditable () {
|
blurContenteditable () {
|
||||||
this.$refs.contenteditable.blur()
|
this.$refs.contenteditable.blur()
|
||||||
|
|||||||
@@ -0,0 +1,271 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="list-field group"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="border border-dashed border-base-300 hover:border-base-content/20 rounded relative group fields-list-item transition-colors"
|
||||||
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between relative group/contenteditable-container">
|
||||||
|
<div
|
||||||
|
v-if="!isNew"
|
||||||
|
class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer"
|
||||||
|
@click="$emit('click', field)"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute top-0 bottom-0 left-0 flex items-center transition-all cursor-grab group-hover:bg-base-200/50"
|
||||||
|
@click="$emit('click', field)"
|
||||||
|
>
|
||||||
|
<IconDrag style="margin-left: 1px" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center p-1 pl-6 space-x-1">
|
||||||
|
<FieldType
|
||||||
|
v-model="field.type"
|
||||||
|
:editable="false"
|
||||||
|
:button-width="20"
|
||||||
|
@click="$emit('click', field)"
|
||||||
|
/>
|
||||||
|
<Contenteditable
|
||||||
|
ref="name"
|
||||||
|
:model-value="field.name"
|
||||||
|
:placeholder="'Field Name'"
|
||||||
|
:icon-inline="true"
|
||||||
|
:icon-width="18"
|
||||||
|
:min-width="isNew ? '100px' : '2px'"
|
||||||
|
:icon-stroke-width="1.6"
|
||||||
|
:editable-on-button="!isNew"
|
||||||
|
:with-button="!isNew"
|
||||||
|
:class="{ 'cursor-pointer': !isNew }"
|
||||||
|
@click-contenteditable="$emit('click', field)"
|
||||||
|
@focus="onNameFocus"
|
||||||
|
@blur="onNameBlur"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex items-center space-x-1"
|
||||||
|
>
|
||||||
|
<PaymentSettings
|
||||||
|
v-if="field.type === 'payment' && !isNew"
|
||||||
|
:field="field"
|
||||||
|
:with-condition="false"
|
||||||
|
:with-force-open="false"
|
||||||
|
@click-description="isShowDescriptionModal = true"
|
||||||
|
@click-formula="isShowFormulaModal = true"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-else-if="!isNew"
|
||||||
|
class="dropdown dropdown-end field-settings-dropdown"
|
||||||
|
@mouseenter="renderDropdown = true"
|
||||||
|
@touchstart="renderDropdown = true"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
tabindex="0"
|
||||||
|
:title="t('settings')"
|
||||||
|
class="cursor-pointer text-transparent group-hover:text-base-content"
|
||||||
|
>
|
||||||
|
<IconSettings
|
||||||
|
:width="18"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<ul
|
||||||
|
v-if="renderDropdown"
|
||||||
|
tabindex="0"
|
||||||
|
class="mt-1.5 dropdown-content menu menu-xs p-2 shadow rounded-box w-52 z-10"
|
||||||
|
:style="{ backgroundColor: dropdownBgColor }"
|
||||||
|
draggable="true"
|
||||||
|
@dragstart.prevent.stop
|
||||||
|
@click="closeDropdown"
|
||||||
|
>
|
||||||
|
<FieldSettings
|
||||||
|
:field="field"
|
||||||
|
:with-signature-id="withSignatureId"
|
||||||
|
:with-prefillable="withPrefillable"
|
||||||
|
:background-color="dropdownBgColor"
|
||||||
|
:with-areas="false"
|
||||||
|
:with-copy-to-all-pages="false"
|
||||||
|
:with-condition="false"
|
||||||
|
@click-formula="isShowFormulaModal = true"
|
||||||
|
@click-font="isShowFontModal = true"
|
||||||
|
@click-description="isShowDescriptionModal = true"
|
||||||
|
@save="$emit('save')"
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
v-if="isNew && !$refs.name"
|
||||||
|
class="relative text-base-content pr-1 field-save-button"
|
||||||
|
:title="t('save')"
|
||||||
|
@click="field.name ? $emit('save', field) : focusName()"
|
||||||
|
>
|
||||||
|
<IconCheck
|
||||||
|
:width="18"
|
||||||
|
:stroke-width="2"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="relative group-hover:text-base-content pr-1 field-remove-button"
|
||||||
|
:class="isNew ? 'text-base-content' : 'text-transparent group-hover:text-base-content'"
|
||||||
|
:title="t('remove')"
|
||||||
|
@click="onRemoveClick"
|
||||||
|
>
|
||||||
|
<IconTrashX
|
||||||
|
:width="18"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Teleport
|
||||||
|
v-if="isShowFormulaModal"
|
||||||
|
:to="modalContainerEl"
|
||||||
|
>
|
||||||
|
<FormulaModal
|
||||||
|
:field="field"
|
||||||
|
:default-field="defaultField"
|
||||||
|
:build-default-name="buildDefaultName"
|
||||||
|
@save="$emit('save')"
|
||||||
|
@close="isShowFormulaModal = false"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
<Teleport
|
||||||
|
v-if="isShowFontModal"
|
||||||
|
:to="modalContainerEl"
|
||||||
|
>
|
||||||
|
<FontModal
|
||||||
|
:field="field"
|
||||||
|
:default-field="defaultField"
|
||||||
|
:build-default-name="buildDefaultName"
|
||||||
|
@save="$emit('save')"
|
||||||
|
@close="isShowFontModal = false"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
<Teleport
|
||||||
|
v-if="isShowDescriptionModal"
|
||||||
|
:to="modalContainerEl"
|
||||||
|
>
|
||||||
|
<DescriptionModal
|
||||||
|
:field="field"
|
||||||
|
:default-field="defaultField"
|
||||||
|
:build-default-name="buildDefaultName"
|
||||||
|
@save="$emit('save')"
|
||||||
|
@close="isShowDescriptionModal = false"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Contenteditable from './contenteditable'
|
||||||
|
import FieldType from './field_type'
|
||||||
|
import PaymentSettings from './payment_settings'
|
||||||
|
import FieldSettings from './field_settings'
|
||||||
|
import FormulaModal from './formula_modal'
|
||||||
|
import FontModal from './font_modal'
|
||||||
|
import DescriptionModal from './description_modal'
|
||||||
|
import { IconTrashX, IconSettings, IconCheck } from '@tabler/icons-vue'
|
||||||
|
import IconDrag from './icon_drag'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CustomField',
|
||||||
|
components: {
|
||||||
|
Contenteditable,
|
||||||
|
IconSettings,
|
||||||
|
IconCheck,
|
||||||
|
FieldSettings,
|
||||||
|
PaymentSettings,
|
||||||
|
IconDrag,
|
||||||
|
FormulaModal,
|
||||||
|
FontModal,
|
||||||
|
DescriptionModal,
|
||||||
|
IconTrashX,
|
||||||
|
FieldType
|
||||||
|
},
|
||||||
|
inject: ['backgroundColor', 't'],
|
||||||
|
props: {
|
||||||
|
field: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isNew: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
withSignatureId: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
withPrefillable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['remove', 'save', 'click'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isShowFormulaModal: false,
|
||||||
|
isShowFontModal: false,
|
||||||
|
isShowDescriptionModal: false,
|
||||||
|
renderDropdown: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
dropdownBgColor () {
|
||||||
|
return ['', null, 'transparent'].includes(this.backgroundColor) ? 'white' : this.backgroundColor
|
||||||
|
},
|
||||||
|
modalContainerEl () {
|
||||||
|
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.field.preferences ||= {}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
if (this.isNew) {
|
||||||
|
this.focusName()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
buildDefaultName () {
|
||||||
|
return this.t('custom')
|
||||||
|
},
|
||||||
|
focusName () {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$refs.name.clickEdit()
|
||||||
|
}, 1)
|
||||||
|
},
|
||||||
|
onNameFocus (e) {
|
||||||
|
if (!this.field.name) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$refs.name.$refs.contenteditable.innerText = ' '
|
||||||
|
}, 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
closeDropdown () {
|
||||||
|
this.$el.getRootNode().activeElement.blur()
|
||||||
|
},
|
||||||
|
onRemoveClick () {
|
||||||
|
if (this.isNew || window.confirm(this.t('are_you_sure_'))) {
|
||||||
|
this.$emit('remove', this.field)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNameBlur (e) {
|
||||||
|
const text = this.$refs.name.$refs.contenteditable.innerText.trim()
|
||||||
|
|
||||||
|
if (text) {
|
||||||
|
this.field.name = text
|
||||||
|
} else {
|
||||||
|
this.$refs.name.setText(this.field.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.field.name) {
|
||||||
|
this.$emit('save')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
<div class="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">
|
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||||
<span class="modal-title">
|
<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>
|
</span>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: 'DescriptionModal',
|
name: 'DescriptionModal',
|
||||||
inject: ['t', 'save', 'template'],
|
inject: ['t', 'template'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -88,7 +88,7 @@ export default {
|
|||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['close'],
|
emits: ['close', 'save'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
description: this.field.description,
|
description: this.field.description,
|
||||||
@@ -103,7 +103,7 @@ export default {
|
|||||||
this.field.description = this.description
|
this.field.description = this.description
|
||||||
this.field.title = this.title
|
this.field.title = this.title
|
||||||
|
|
||||||
this.save()
|
this.$emit('save')
|
||||||
this.$emit('close')
|
this.$emit('close')
|
||||||
},
|
},
|
||||||
resizeTextarea () {
|
resizeTextarea () {
|
||||||
|
|||||||
@@ -13,17 +13,28 @@
|
|||||||
:with-signature-id="withSignatureId"
|
:with-signature-id="withSignatureId"
|
||||||
:with-prefillable="withPrefillable"
|
:with-prefillable="withPrefillable"
|
||||||
:is-drag="isDrag"
|
:is-drag="isDrag"
|
||||||
|
:is-mobile="isMobile"
|
||||||
:with-field-placeholder="withFieldPlaceholder"
|
:with-field-placeholder="withFieldPlaceholder"
|
||||||
:default-fields="defaultFields"
|
:default-fields="defaultFields"
|
||||||
:drag-field-placeholder="dragFieldPlaceholder"
|
:drag-field-placeholder="dragFieldPlaceholder"
|
||||||
:default-submitters="defaultSubmitters"
|
:default-submitters="defaultSubmitters"
|
||||||
:draw-field="drawField"
|
:draw-field="drawField"
|
||||||
:draw-field-type="drawFieldType"
|
:draw-field-type="drawFieldType"
|
||||||
|
:draw-custom-field="drawCustomField"
|
||||||
:selected-submitter="selectedSubmitter"
|
:selected-submitter="selectedSubmitter"
|
||||||
:total-pages="sortedPreviewImages.length"
|
:total-pages="sortedPreviewImages.length"
|
||||||
:image="image"
|
:image="image"
|
||||||
@drop-field="$emit('drop-field', {...$event, attachment_uuid: document.uuid })"
|
:attachment-uuid="document.uuid"
|
||||||
|
:with-fields-detection="withFieldsDetection"
|
||||||
|
@drop-field="$emit('drop-field', { ...$event, attachment_uuid: document.uuid })"
|
||||||
@remove-area="$emit('remove-area', $event)"
|
@remove-area="$emit('remove-area', $event)"
|
||||||
|
@copy-field="$emit('copy-field', $event)"
|
||||||
|
@paste-field="$emit('paste-field', { ...$event, attachment_uuid: document.uuid })"
|
||||||
|
@add-custom-field="$emit('add-custom-field', $event)"
|
||||||
|
@set-draw="$emit('set-draw', $event)"
|
||||||
|
@copy-selected-areas="$emit('copy-selected-areas')"
|
||||||
|
@delete-selected-areas="$emit('delete-selected-areas')"
|
||||||
|
@autodetect-fields="$emit('autodetect-fields', $event)"
|
||||||
@scroll-to="scrollToArea"
|
@scroll-to="scrollToArea"
|
||||||
@draw="$emit('draw', { area: {...$event.area, attachment_uuid: document.uuid }, isTooSmall: $event.isTooSmall })"
|
@draw="$emit('draw', { area: {...$event.area, attachment_uuid: document.uuid }, isTooSmall: $event.isTooSmall })"
|
||||||
/>
|
/>
|
||||||
@@ -88,6 +99,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
isMobile: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
allowDraw: {
|
allowDraw: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -107,6 +123,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
drawCustomField: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
baseUrl: {
|
baseUrl: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -116,9 +137,14 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
withFieldsDetection: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['draw', 'drop-field', 'remove-area'],
|
emits: ['draw', 'drop-field', 'remove-area', 'paste-field', 'copy-field', 'copy-selected-areas', 'delete-selected-areas', 'autodetect-fields', 'add-custom-field', 'set-draw'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
pageRefs: []
|
pageRefs: []
|
||||||
@@ -160,7 +186,15 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
scrollToArea (area) {
|
scrollToArea (area) {
|
||||||
this.$nextTick(() => {
|
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) {
|
setPageRefs (el) {
|
||||||
|
|||||||
@@ -8,6 +8,13 @@
|
|||||||
class="fixed z-20 pointer-events-none"
|
class="fixed z-20 pointer-events-none"
|
||||||
:editable="false"
|
: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
|
<div
|
||||||
v-else-if="dragPlaceholder && (isDefault || isRequired) && !isMask && field"
|
v-else-if="dragPlaceholder && (isDefault || isRequired) && !isMask && field"
|
||||||
ref="dragPlaceholder"
|
ref="dragPlaceholder"
|
||||||
@@ -57,6 +64,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Field from './field'
|
import Field from './field'
|
||||||
|
import CustomField from './custom_field'
|
||||||
import IconDrag from './icon_drag'
|
import IconDrag from './icon_drag'
|
||||||
import FieldType from './field_type'
|
import FieldType from './field_type'
|
||||||
|
|
||||||
@@ -64,6 +72,7 @@ export default {
|
|||||||
name: 'DragPlaceholder',
|
name: 'DragPlaceholder',
|
||||||
components: {
|
components: {
|
||||||
Field,
|
Field,
|
||||||
|
CustomField,
|
||||||
IconDrag
|
IconDrag
|
||||||
},
|
},
|
||||||
inject: ['t', 'backgroundColor'],
|
inject: ['t', 'backgroundColor'],
|
||||||
@@ -87,6 +96,11 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
isCustom: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="list-field group mb-2"
|
class="list-field group"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="border border-base-300 rounded relative group fields-list-item"
|
class="border border-base-300 rounded relative group fields-list-item"
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
:button-width="20"
|
:button-width="20"
|
||||||
:menu-classes="'mt-1.5'"
|
:menu-classes="'mt-1.5'"
|
||||||
:menu-style="{ backgroundColor: dropdownBgColor }"
|
:menu-style="{ backgroundColor: dropdownBgColor }"
|
||||||
@update:model-value="[maybeUpdateOptions(), save()]"
|
@update:model-value="[maybeUpdateOptions(), $emit('save')]"
|
||||||
@click="scrollToFirstArea"
|
@click="scrollToFirstArea"
|
||||||
/>
|
/>
|
||||||
<Contenteditable
|
<Contenteditable
|
||||||
@@ -92,9 +92,12 @@
|
|||||||
<PaymentSettings
|
<PaymentSettings
|
||||||
v-if="field.type === 'payment'"
|
v-if="field.type === 'payment'"
|
||||||
:field="field"
|
:field="field"
|
||||||
|
:with-custom-fields="withCustomFields"
|
||||||
@click-condition="isShowConditionsModal = true"
|
@click-condition="isShowConditionsModal = true"
|
||||||
@click-description="isShowDescriptionModal = true"
|
@click-description="isShowDescriptionModal = true"
|
||||||
|
@add-custom-field="$emit('add-custom-field', $event)"
|
||||||
@click-formula="isShowFormulaModal = true"
|
@click-formula="isShowFormulaModal = true"
|
||||||
|
@save="$emit('save')"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
@@ -128,12 +131,15 @@
|
|||||||
:with-signature-id="withSignatureId"
|
:with-signature-id="withSignatureId"
|
||||||
:with-prefillable="withPrefillable"
|
:with-prefillable="withPrefillable"
|
||||||
:background-color="dropdownBgColor"
|
:background-color="dropdownBgColor"
|
||||||
|
:with-custom-fields="withCustomFields"
|
||||||
@click-formula="isShowFormulaModal = true"
|
@click-formula="isShowFormulaModal = true"
|
||||||
@click-font="isShowFontModal = true"
|
@click-font="isShowFontModal = true"
|
||||||
@click-description="isShowDescriptionModal = true"
|
@click-description="isShowDescriptionModal = true"
|
||||||
@click-condition="isShowConditionsModal = true"
|
@click-condition="isShowConditionsModal = true"
|
||||||
@set-draw="$emit('set-draw', $event)"
|
@set-draw="$emit('set-draw', $event)"
|
||||||
|
@add-custom-field="$emit('add-custom-field', $event)"
|
||||||
@remove-area="removeArea"
|
@remove-area="removeArea"
|
||||||
|
@save="$emit('save')"
|
||||||
@scroll-to="$emit('scroll-to', $event)"
|
@scroll-to="$emit('scroll-to', $event)"
|
||||||
/>
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -155,14 +161,23 @@
|
|||||||
ref="options"
|
ref="options"
|
||||||
class="border-t border-base-300 mx-2 pt-2 space-y-1.5"
|
class="border-t border-base-300 mx-2 pt-2 space-y-1.5"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
|
@dragover="onOptionDragover"
|
||||||
|
@drop="reorderOptions"
|
||||||
@dragstart.prevent.stop
|
@dragstart.prevent.stop
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(option, index) in field.options"
|
v-for="(option, index) in field.options"
|
||||||
:key="option.uuid"
|
:key="option.uuid"
|
||||||
class="flex space-x-1.5 items-center"
|
class="flex space-x-1.5 items-center"
|
||||||
|
:data-option-uuid="option.uuid"
|
||||||
>
|
>
|
||||||
<span class="text-sm w-3.5">
|
<span
|
||||||
|
class="text-sm w-3.5 cursor-grab select-none"
|
||||||
|
:draggable="editable && !defaultField"
|
||||||
|
@dragstart.stop="onOptionDragstart($event, option)"
|
||||||
|
@dragend.stop="optionDragRef = null"
|
||||||
|
@dragover.prevent.stop="onOptionDragover"
|
||||||
|
>
|
||||||
{{ index + 1 }}.
|
{{ index + 1 }}.
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
@@ -176,7 +191,9 @@
|
|||||||
dir="auto"
|
dir="auto"
|
||||||
required
|
required
|
||||||
:placeholder="`${t('option')} ${index + 1}`"
|
:placeholder="`${t('option')} ${index + 1}`"
|
||||||
@blur="save"
|
@keydown.enter="option.value ? addOptionAt(index + 1) : null"
|
||||||
|
@blur="$emit('save')"
|
||||||
|
@paste="onOptionPaste($event, index)"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
:title="t('draw')"
|
:title="t('draw')"
|
||||||
@@ -198,8 +215,10 @@
|
|||||||
:readonly="!editable || defaultField"
|
:readonly="!editable || defaultField"
|
||||||
required
|
required
|
||||||
dir="auto"
|
dir="auto"
|
||||||
|
@keydown.enter="option.value ? addOptionAt(index + 1) : null"
|
||||||
@focus="maybeFocusOnOptionArea(option)"
|
@focus="maybeFocusOnOptionArea(option)"
|
||||||
@blur="save"
|
@blur="$emit('save')"
|
||||||
|
@paste="onOptionPaste($event, index)"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-if="editable && !defaultField"
|
v-if="editable && !defaultField"
|
||||||
@@ -217,7 +236,7 @@
|
|||||||
<button
|
<button
|
||||||
v-else-if="field.options && editable && !defaultField"
|
v-else-if="field.options && editable && !defaultField"
|
||||||
class="field-add-option text-center text-sm w-full pb-1"
|
class="field-add-option text-center text-sm w-full pb-1"
|
||||||
@click="addOption"
|
@click="addOptionAt(field.options.length)"
|
||||||
>
|
>
|
||||||
+ {{ t('add_option') }}
|
+ {{ t('add_option') }}
|
||||||
</button>
|
</button>
|
||||||
@@ -250,6 +269,7 @@
|
|||||||
:editable="editable && !defaultField"
|
:editable="editable && !defaultField"
|
||||||
:default-field="defaultField"
|
:default-field="defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
|
@save="$emit('save')"
|
||||||
@close="isShowFormulaModal = false"
|
@close="isShowFormulaModal = false"
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
@@ -262,6 +282,7 @@
|
|||||||
:editable="editable && !defaultField"
|
:editable="editable && !defaultField"
|
||||||
:default-field="defaultField"
|
:default-field="defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
|
@save="$emit('save')"
|
||||||
@close="isShowFontModal = false"
|
@close="isShowFontModal = false"
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
@@ -273,6 +294,7 @@
|
|||||||
:item="field"
|
:item="field"
|
||||||
:default-field="defaultField"
|
:default-field="defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
|
@save="$emit('save')"
|
||||||
@close="isShowConditionsModal = false"
|
@close="isShowConditionsModal = false"
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
@@ -285,6 +307,7 @@
|
|||||||
:editable="editable && !defaultField"
|
:editable="editable && !defaultField"
|
||||||
:default-field="defaultField"
|
:default-field="defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
|
@save="$emit('save')"
|
||||||
@close="isShowDescriptionModal = false"
|
@close="isShowDescriptionModal = false"
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
@@ -321,7 +344,7 @@ export default {
|
|||||||
IconMathFunction,
|
IconMathFunction,
|
||||||
FieldType
|
FieldType
|
||||||
},
|
},
|
||||||
inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef', 't', 'locale'],
|
inject: ['template', 'backgroundColor', 'selectedAreasRef', 't', 'locale', 'getFieldTypeIndex'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -332,6 +355,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
withCustomFields: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
withPrefillable: {
|
withPrefillable: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -353,17 +381,17 @@ export default {
|
|||||||
default: true
|
default: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['set-draw', 'remove', 'scroll-to'],
|
emits: ['set-draw', 'remove', 'scroll-to', 'save', 'add-custom-field'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isExpandOptions: false,
|
isExpandOptions: false,
|
||||||
isNameFocus: false,
|
isNameFocus: false,
|
||||||
showPaymentModal: false,
|
|
||||||
isShowFormulaModal: false,
|
isShowFormulaModal: false,
|
||||||
isShowFontModal: false,
|
isShowFontModal: false,
|
||||||
isShowConditionsModal: false,
|
isShowConditionsModal: false,
|
||||||
isShowDescriptionModal: false,
|
isShowDescriptionModal: false,
|
||||||
renderDropdown: false
|
renderDropdown: false,
|
||||||
|
optionDragRef: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -388,7 +416,7 @@ export default {
|
|||||||
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
||||||
},
|
},
|
||||||
defaultName () {
|
defaultName () {
|
||||||
return this.buildDefaultName(this.field, this.template.fields)
|
return this.buildDefaultName(this.field)
|
||||||
},
|
},
|
||||||
areas () {
|
areas () {
|
||||||
return this.field.areas || []
|
return this.field.areas || []
|
||||||
@@ -406,9 +434,9 @@ export default {
|
|||||||
removeArea (area) {
|
removeArea (area) {
|
||||||
this.field.areas.splice(this.field.areas.indexOf(area), 1)
|
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) {
|
if (field.type === 'payment' && field.preferences?.price && !field.preferences?.formula) {
|
||||||
const { price, currency } = field.preferences || {}
|
const { price, currency } = field.preferences || {}
|
||||||
|
|
||||||
@@ -419,7 +447,7 @@ export default {
|
|||||||
|
|
||||||
return `${this.fieldNames[field.type]} ${formattedPrice}`
|
return `${this.fieldNames[field.type]} ${formattedPrice}`
|
||||||
} else {
|
} else {
|
||||||
const typeIndex = fields.filter((f) => f.type === field.type).indexOf(field)
|
const typeIndex = this.getFieldTypeIndex(field)
|
||||||
|
|
||||||
if (field.type === 'heading' || field.type === 'strikethrough') {
|
if (field.type === 'heading' || field.type === 'strikethrough') {
|
||||||
return `${this.fieldNames[field.type]} ${typeIndex + 1}`
|
return `${this.fieldNames[field.type]} ${typeIndex + 1}`
|
||||||
@@ -441,7 +469,7 @@ export default {
|
|||||||
const area = this.field.areas.find((a) => a.option_uuid === option.uuid)
|
const area = this.field.areas.find((a) => a.option_uuid === option.uuid)
|
||||||
|
|
||||||
if (area) {
|
if (area) {
|
||||||
this.selectedAreaRef.value = area
|
this.selectedAreasRef.value = [area]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scrollToFirstArea () {
|
scrollToFirstArea () {
|
||||||
@@ -450,18 +478,49 @@ export default {
|
|||||||
closeDropdown () {
|
closeDropdown () {
|
||||||
this.$el.getRootNode().activeElement.blur()
|
this.$el.getRootNode().activeElement.blur()
|
||||||
},
|
},
|
||||||
addOption () {
|
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
|
this.isExpandOptions = true
|
||||||
|
|
||||||
this.field.options.push({ value: '', uuid: v4() })
|
const insertAt = index ?? this.field.options.length
|
||||||
|
|
||||||
|
this.field.options.splice(insertAt, 0, { value: '', uuid: v4() })
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
const inputs = this.$refs.options.querySelectorAll('input')
|
const inputs = this.$refs.options.querySelectorAll('input')
|
||||||
|
|
||||||
inputs[inputs.length - 1]?.focus()
|
inputs[insertAt]?.focus()
|
||||||
})
|
})
|
||||||
|
|
||||||
this.save()
|
this.$emit('save')
|
||||||
},
|
},
|
||||||
removeOption (option) {
|
removeOption (option) {
|
||||||
this.field.options.splice(this.field.options.indexOf(option), 1)
|
this.field.options.splice(this.field.options.indexOf(option), 1)
|
||||||
@@ -472,7 +531,7 @@ export default {
|
|||||||
this.field.areas.splice(this.field.areas.findIndex((a) => a.option_uuid === option.uuid), 1)
|
this.field.areas.splice(this.field.areas.findIndex((a) => a.option_uuid === option.uuid), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.save()
|
this.$emit('save')
|
||||||
},
|
},
|
||||||
maybeUpdateOptions () {
|
maybeUpdateOptions () {
|
||||||
delete this.field.default_value
|
delete this.field.default_value
|
||||||
@@ -514,7 +573,71 @@ export default {
|
|||||||
|
|
||||||
this.isNameFocus = false
|
this.isNameFocus = false
|
||||||
|
|
||||||
this.save()
|
this.$emit('save')
|
||||||
|
},
|
||||||
|
onOptionDragstart (event, option) {
|
||||||
|
this.optionDragRef = option
|
||||||
|
|
||||||
|
const root = this.$el.getRootNode()
|
||||||
|
const hiddenEl = document.createElement('div')
|
||||||
|
|
||||||
|
hiddenEl.style.width = '1px'
|
||||||
|
hiddenEl.style.height = '1px'
|
||||||
|
hiddenEl.style.opacity = '0'
|
||||||
|
hiddenEl.style.position = 'fixed'
|
||||||
|
|
||||||
|
root.querySelector('#docuseal_modal_container')?.appendChild(hiddenEl)
|
||||||
|
event.dataTransfer?.setDragImage(hiddenEl, 0, 0)
|
||||||
|
|
||||||
|
setTimeout(() => { hiddenEl.remove() }, 1000)
|
||||||
|
|
||||||
|
event.dataTransfer.effectAllowed = 'move'
|
||||||
|
},
|
||||||
|
onOptionDragover (e) {
|
||||||
|
if (!this.optionDragRef) return
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
const targetRow = e.target.closest('[data-option-uuid]')
|
||||||
|
|
||||||
|
if (!targetRow) return
|
||||||
|
|
||||||
|
const dragRow = this.$refs.options?.querySelector(`[data-option-uuid="${this.optionDragRef.uuid}"]`)
|
||||||
|
|
||||||
|
if (!dragRow) return
|
||||||
|
if (targetRow === dragRow) return
|
||||||
|
|
||||||
|
const rows = Array.from(this.$refs.options.querySelectorAll('[data-option-uuid]'))
|
||||||
|
|
||||||
|
const currentIndex = rows.indexOf(dragRow)
|
||||||
|
const targetIndex = rows.indexOf(targetRow)
|
||||||
|
|
||||||
|
if (currentIndex < targetIndex) {
|
||||||
|
targetRow.after(dragRow)
|
||||||
|
} else {
|
||||||
|
targetRow.before(dragRow)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reorderOptions (e) {
|
||||||
|
if (!this.optionDragRef) return
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
const rows = Array.from(this.$refs.options.querySelectorAll('[data-option-uuid]'))
|
||||||
|
|
||||||
|
const newOrder = rows
|
||||||
|
.map((el) => this.field.options.find((opt) => opt.uuid === el.dataset.optionUuid))
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
if (newOrder.length === this.field.options.length) {
|
||||||
|
this.field.options.splice(0, this.field.options.length, ...newOrder)
|
||||||
|
|
||||||
|
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
|
<select
|
||||||
:placeholder="t('method')"
|
:placeholder="t('method')"
|
||||||
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
|
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
|
<option
|
||||||
v-for="method in ['QeS', 'AeS']"
|
v-for="method in verificationMethods"
|
||||||
:key="method"
|
:key="method"
|
||||||
:value="method.toLowerCase()"
|
:value="method.toLowerCase()"
|
||||||
:selected="method.toLowerCase() === field.preferences?.method || (method === 'QeS' && !field.preferences?.method)"
|
:selected="method.toLowerCase() === field.preferences?.method || (method === 'QeS' && !field.preferences?.method)"
|
||||||
@@ -26,32 +26,6 @@
|
|||||||
{{ t('method') }}
|
{{ t('method') }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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
|
<div
|
||||||
v-if="['select', 'radio'].includes(field.type) && !defaultField"
|
v-if="['select', 'radio'].includes(field.type) && !defaultField"
|
||||||
class="py-1.5 px-1 relative"
|
class="py-1.5 px-1 relative"
|
||||||
@@ -61,7 +35,7 @@
|
|||||||
:placeholder="t('default_value')"
|
:placeholder="t('default_value')"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="select select-bordered select-xs w-full max-w-xs h-7 !outline-0 font-normal bg-transparent"
|
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
|
<option
|
||||||
value=""
|
value=""
|
||||||
@@ -97,7 +71,7 @@
|
|||||||
dir="auto"
|
dir="auto"
|
||||||
:type="field.type"
|
:type="field.type"
|
||||||
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0 bg-transparent"
|
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0 bg-transparent"
|
||||||
@blur="save"
|
@blur="$emit('save')"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
v-if="field.default_value"
|
v-if="field.default_value"
|
||||||
@@ -159,7 +133,7 @@
|
|||||||
:value="lengthValidation.min"
|
:value="lengthValidation.min"
|
||||||
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
|
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
|
||||||
@input="field.validation.pattern = `.{${$event.target.value || 0},${lengthValidation.max || ''}}`"
|
@input="field.validation.pattern = `.{${$event.target.value || 0},${lengthValidation.max || ''}}`"
|
||||||
@blur="save"
|
@blur="$emit('save')"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
v-if="lengthValidation.min"
|
v-if="lengthValidation.min"
|
||||||
@@ -178,7 +152,7 @@
|
|||||||
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
|
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
|
||||||
:value="lengthValidation.max"
|
:value="lengthValidation.max"
|
||||||
@input="field.validation.pattern = `.{${lengthValidation.min},${$event.target.value || ''}}`"
|
@input="field.validation.pattern = `.{${lengthValidation.min},${$event.target.value || ''}}`"
|
||||||
@blur="save"
|
@blur="$emit('save')"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
v-if="lengthValidation.max"
|
v-if="lengthValidation.max"
|
||||||
@@ -203,7 +177,7 @@
|
|||||||
:value="field.validation?.min"
|
:value="field.validation?.min"
|
||||||
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
|
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]"
|
@input="[field.validation ||= {}, $event.target.value ? field.validation.min = $event.target.value : delete field.validation.min]"
|
||||||
@blur="save"
|
@blur="$emit('save')"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
v-if="field.validation?.min"
|
v-if="field.validation?.min"
|
||||||
@@ -222,7 +196,7 @@
|
|||||||
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
|
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
|
||||||
:value="field.validation?.max"
|
:value="field.validation?.max"
|
||||||
@input="[field.validation ||= {}, $event.target.value ? field.validation.max = $event.target.value : delete 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
|
<label
|
||||||
v-if="field.validation?.max"
|
v-if="field.validation?.max"
|
||||||
@@ -242,7 +216,7 @@
|
|||||||
<select
|
<select
|
||||||
:placeholder="t('format')"
|
:placeholder="t('format')"
|
||||||
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
|
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
|
<option
|
||||||
v-for="format in numberFormats"
|
v-for="format in numberFormats"
|
||||||
@@ -272,7 +246,7 @@
|
|||||||
:placeholder="t('regexp_validation')"
|
:placeholder="t('regexp_validation')"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0 bg-transparent"
|
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0 bg-transparent"
|
||||||
@blur="save"
|
@blur="$emit('save')"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
v-if="field.validation.pattern"
|
v-if="field.validation.pattern"
|
||||||
@@ -293,7 +267,7 @@
|
|||||||
:placeholder="t('error_message')"
|
:placeholder="t('error_message')"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0 bg-transparent"
|
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0 bg-transparent"
|
||||||
@blur="save"
|
@blur="$emit('save')"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
v-if="field.validation.message"
|
v-if="field.validation.message"
|
||||||
@@ -313,7 +287,7 @@
|
|||||||
v-model="field.preferences.format"
|
v-model="field.preferences.format"
|
||||||
:placeholder="t('format')"
|
:placeholder="t('format')"
|
||||||
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
|
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
|
<option
|
||||||
v-for="format in dateFormats"
|
v-for="format in dateFormats"
|
||||||
@@ -339,7 +313,7 @@
|
|||||||
<select
|
<select
|
||||||
:placeholder="t('format')"
|
:placeholder="t('format')"
|
||||||
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
|
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
|
<option
|
||||||
value="any"
|
value="any"
|
||||||
@@ -348,7 +322,7 @@
|
|||||||
{{ t('any') }}
|
{{ t('any') }}
|
||||||
</option>
|
</option>
|
||||||
<option
|
<option
|
||||||
v-for="type in ['drawn', 'typed', 'drawn_or_typed', 'drawn_or_upload', 'upload']"
|
v-for="type in signatureFormats"
|
||||||
:key="type"
|
:key="type"
|
||||||
:value="type"
|
:value="type"
|
||||||
:selected="field.preferences?.format === type"
|
:selected="field.preferences?.format === type"
|
||||||
@@ -374,7 +348,7 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
:disabled="!editable || (defaultField && [true, false].includes(defaultField.required))"
|
:disabled="!editable || (defaultField && [true, false].includes(defaultField.required))"
|
||||||
class="toggle toggle-xs"
|
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>
|
<span class="label-text">{{ t('signature_id') }}</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -389,7 +363,7 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
:disabled="!editable || (defaultField && [true, false].includes(defaultField.required))"
|
:disabled="!editable || (defaultField && [true, false].includes(defaultField.required))"
|
||||||
class="toggle toggle-xs"
|
class="toggle toggle-xs"
|
||||||
@update:model-value="save"
|
@update:model-value="$emit('save')"
|
||||||
>
|
>
|
||||||
<span class="label-text">{{ t('required') }}</span>
|
<span class="label-text">{{ t('required') }}</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -403,7 +377,7 @@
|
|||||||
:checked="field.preferences?.with_logo != false"
|
:checked="field.preferences?.with_logo != false"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="toggle toggle-xs"
|
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>
|
<span class="label-text">{{ t('with_logo') }}</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -417,7 +391,7 @@
|
|||||||
v-model="field.default_value"
|
v-model="field.default_value"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="toggle toggle-xs"
|
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>
|
<span class="label-text">{{ t('checked') }}</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -431,13 +405,13 @@
|
|||||||
v-model="field.readonly"
|
v-model="field.readonly"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="toggle toggle-xs"
|
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>
|
<span class="label-text">{{ t('set_signing_date') }}</span>
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="['text', 'number'].includes(field.type)"
|
v-if="['text', 'number', 'radio', 'multiple', 'select'].includes(field.type)"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<label class="cursor-pointer py-1.5">
|
<label class="cursor-pointer py-1.5">
|
||||||
@@ -446,13 +420,13 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="toggle toggle-xs"
|
class="toggle toggle-xs"
|
||||||
:disabled="!editable || (defaultField && [true, false].includes(defaultField.readonly))"
|
: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>
|
<span class="label-text">{{ t('read_only') }}</span>
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<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
|
@click.stop
|
||||||
>
|
>
|
||||||
<label class="cursor-pointer py-1.5">
|
<label class="cursor-pointer py-1.5">
|
||||||
@@ -461,7 +435,7 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
:disabled="!editable || (defaultField && [true, false].includes(defaultField.prefillable))"
|
:disabled="!editable || (defaultField && [true, false].includes(defaultField.prefillable))"
|
||||||
class="toggle toggle-xs"
|
class="toggle toggle-xs"
|
||||||
@update:model-value="save"
|
@update:model-value="$emit('save')"
|
||||||
>
|
>
|
||||||
<span class="label-text">{{ t('prefillable') }}</span>
|
<span class="label-text">{{ t('prefillable') }}</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -470,7 +444,7 @@
|
|||||||
v-if="field.type != 'stamp'"
|
v-if="field.type != 'stamp'"
|
||||||
class="pb-0.5 mt-0.5"
|
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
|
<label
|
||||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||||
@click="$emit('click-font')"
|
@click="$emit('click-font')"
|
||||||
@@ -499,7 +473,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="field.type != 'stamp' && field.type != 'heading'"
|
v-if="withCondition && field.type != 'stamp' && field.type != 'heading'"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||||
@@ -526,7 +500,10 @@
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</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">
|
<template v-if="withAreas">
|
||||||
<li
|
<li
|
||||||
v-for="(area, index) in sortedAreas"
|
v-for="(area, index) in sortedAreas"
|
||||||
@@ -564,7 +541,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</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
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="text-sm py-1 px-2"
|
class="text-sm py-1 px-2"
|
||||||
@@ -577,10 +554,23 @@
|
|||||||
{{ t('copy_to_all_pages') }}
|
{{ t('copy_to_all_pages') }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script>
|
<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 {
|
export default {
|
||||||
name: 'FieldSettings',
|
name: 'FieldSettings',
|
||||||
@@ -589,17 +579,33 @@ export default {
|
|||||||
IconInfoCircle,
|
IconInfoCircle,
|
||||||
IconMathFunction,
|
IconMathFunction,
|
||||||
IconRouteAltLeft,
|
IconRouteAltLeft,
|
||||||
|
IconForms,
|
||||||
IconCopy,
|
IconCopy,
|
||||||
IconNewSection,
|
IconNewSection,
|
||||||
IconTypography,
|
IconTypography,
|
||||||
IconX
|
IconX
|
||||||
},
|
},
|
||||||
inject: ['template', 'save', 't'],
|
inject: ['template', 't'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
withCondition: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
withCustomFields: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
withCopyToAllPages: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
withSignatureId: {
|
withSignatureId: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -636,7 +642,7 @@ export default {
|
|||||||
default: true
|
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 () {
|
data () {
|
||||||
return {
|
return {
|
||||||
selectedValidation: ''
|
selectedValidation: ''
|
||||||
@@ -686,7 +692,7 @@ export default {
|
|||||||
},
|
},
|
||||||
lengthValidation () {
|
lengthValidation () {
|
||||||
if (this.field.validation?.pattern && this.selectedValidation !== 'custom') {
|
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 {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -703,6 +709,15 @@ export default {
|
|||||||
'^[a-zA-Z]+$': 'letters_only'
|
'^[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 () {
|
sortedAreas () {
|
||||||
return (this.field.areas || []).sort((a, b) => {
|
return (this.field.areas || []).sort((a, b) => {
|
||||||
return this.schemaAttachmentsIndexes[a.attachment_uuid] - this.schemaAttachmentsIndexes[b.attachment_uuid]
|
return this.schemaAttachmentsIndexes[a.attachment_uuid] - this.schemaAttachmentsIndexes[b.attachment_uuid]
|
||||||
@@ -730,7 +745,7 @@ export default {
|
|||||||
delete this.field.validation
|
delete this.field.validation
|
||||||
}
|
}
|
||||||
|
|
||||||
this.save()
|
this.$emit('save')
|
||||||
},
|
},
|
||||||
copyToAllPages (field) {
|
copyToAllPages (field) {
|
||||||
const areaString = JSON.stringify(field.areas[0])
|
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.$emit('scroll-to', this.field.areas[this.field.areas.length - 1])
|
||||||
|
|
||||||
this.save()
|
this.$emit('save')
|
||||||
},
|
},
|
||||||
formatNumber (number, format) {
|
formatNumber (number, format) {
|
||||||
if (format === 'comma') {
|
if (format === 'comma') {
|
||||||
@@ -766,6 +781,9 @@ export default {
|
|||||||
return number
|
return number
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
parseLengthPattern (pattern) {
|
||||||
|
return pattern?.match(/^\.{(?<min>\d+),(?<max>\d+)?}$/)?.groups || null
|
||||||
|
},
|
||||||
formatDate (date, format) {
|
formatDate (date, format) {
|
||||||
const monthFormats = {
|
const monthFormats = {
|
||||||
M: 'numeric',
|
M: 'numeric',
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
v-for="(icon, type) in fieldIconsSorted"
|
v-for="(icon, type) in fieldIconsSorted"
|
||||||
:key="type"
|
:key="type"
|
||||||
>
|
>
|
||||||
<li v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification'))">
|
<li v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification') && (withKba || type != 'kba'))">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="text-sm py-1 px-2"
|
class="text-sm py-1 px-2"
|
||||||
@@ -51,11 +51,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading, IconId, IconCalendarCheck, IconStrikethrough } from '@tabler/icons-vue'
|
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading, IconId, IconCalendarCheck, IconStrikethrough, IconUserScan } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FiledTypeDropdown',
|
name: 'FiledTypeDropdown',
|
||||||
inject: ['withPhone', 'withPayment', 'withVerification', 't', 'fieldTypes'],
|
inject: ['withPhone', 'withPayment', 'withVerification', 'withKba', 't', 'fieldTypes'],
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -114,7 +114,8 @@ export default {
|
|||||||
stamp: this.t('stamp'),
|
stamp: this.t('stamp'),
|
||||||
payment: this.t('payment'),
|
payment: this.t('payment'),
|
||||||
phone: this.t('phone'),
|
phone: this.t('phone'),
|
||||||
verification: this.t('verify_id')
|
verification: this.t('verify_id'),
|
||||||
|
kba: this.t('kba')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fieldLabels () {
|
fieldLabels () {
|
||||||
@@ -134,7 +135,8 @@ export default {
|
|||||||
stamp: this.t('stamp_field'),
|
stamp: this.t('stamp_field'),
|
||||||
payment: this.t('payment_field'),
|
payment: this.t('payment_field'),
|
||||||
phone: this.t('phone_field'),
|
phone: this.t('phone_field'),
|
||||||
verification: this.t('verify_id')
|
verification: this.t('verify_id'),
|
||||||
|
kba: this.t('kba')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fieldIcons () {
|
fieldIcons () {
|
||||||
@@ -157,7 +159,8 @@ export default {
|
|||||||
stamp: IconRubberStamp,
|
stamp: IconRubberStamp,
|
||||||
payment: IconCreditCard,
|
payment: IconCreditCard,
|
||||||
phone: IconPhoneCheck,
|
phone: IconPhoneCheck,
|
||||||
verification: IconId
|
verification: IconId,
|
||||||
|
kba: IconUserScan
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
skipTypes () {
|
skipTypes () {
|
||||||
|
|||||||
@@ -15,22 +15,27 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
ref="fields"
|
ref="fields"
|
||||||
class="fields mb-1 mt-2"
|
class="fields mt-2"
|
||||||
|
:class="{ 'mb-1': !withCustomFields || !customFields.length }"
|
||||||
@dragover.prevent="onFieldDragover"
|
@dragover.prevent="onFieldDragover"
|
||||||
@drop="reorderFields"
|
@drop="fieldsDragFieldRef.value ? reorderFields() : null"
|
||||||
>
|
>
|
||||||
<Field
|
<Field
|
||||||
v-for="field in submitterFields"
|
v-for="field in submitterFields"
|
||||||
:key="field.uuid"
|
:key="field.uuid"
|
||||||
:data-uuid="field.uuid"
|
:data-uuid="field.uuid"
|
||||||
:field="field"
|
:field="field"
|
||||||
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
|
:type-index="getFieldTypeIndex(field)"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
:with-signature-id="withSignatureId"
|
:with-signature-id="withSignatureId"
|
||||||
:with-prefillable="withPrefillable"
|
:with-prefillable="withPrefillable"
|
||||||
:default-field="defaultFieldsIndex[field.name]"
|
:default-field="defaultFieldsIndex[field.name]"
|
||||||
:draggable="editable"
|
:draggable="editable"
|
||||||
|
:with-custom-fields="withCustomFields"
|
||||||
|
class="mb-1.5"
|
||||||
|
@add-custom-field="addCustomField"
|
||||||
@dragstart="[fieldsDragFieldRef.value = field, removeDragOverlay($event), setDragPlaceholder($event)]"
|
@dragstart="[fieldsDragFieldRef.value = field, removeDragOverlay($event), setDragPlaceholder($event)]"
|
||||||
|
@save="save"
|
||||||
@dragend="[fieldsDragFieldRef.value = null, $emit('set-drag-placeholder', null)]"
|
@dragend="[fieldsDragFieldRef.value = null, $emit('set-drag-placeholder', null)]"
|
||||||
@remove="removeField"
|
@remove="removeField"
|
||||||
@scroll-to="$emit('scroll-to-area', $event)"
|
@scroll-to="$emit('scroll-to-area', $event)"
|
||||||
@@ -105,7 +110,92 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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"
|
id="field-types-grid"
|
||||||
class="grid grid-cols-3 gap-1 pb-2 fields-grid"
|
class="grid grid-cols-3 gap-1 pb-2 fields-grid"
|
||||||
>
|
>
|
||||||
@@ -114,7 +204,7 @@
|
|||||||
:key="type"
|
:key="type"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification'))"
|
v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification') && (withKba || type != 'kba'))"
|
||||||
:id="`${type}_type_field_button`"
|
:id="`${type}_type_field_button`"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
class="field-type-button group flex items-center justify-center border border-dashed w-full rounded relative fields-grid-item"
|
class="field-type-button group flex items-center justify-center border border-dashed w-full rounded relative fields-grid-item"
|
||||||
@@ -122,7 +212,7 @@
|
|||||||
:class="drawFieldType === type ? 'border-base-content/40' : 'border-base-300 hover:border-base-content/20'"
|
:class="drawFieldType === type ? 'border-base-content/40' : 'border-base-300 hover:border-base-content/20'"
|
||||||
@dragstart="onDragstart($event, { type: type })"
|
@dragstart="onDragstart($event, { type: type })"
|
||||||
@dragend="$emit('drag-end')"
|
@dragend="$emit('drag-end')"
|
||||||
@click="['file', 'payment', 'verification'].includes(type) ? $emit('add-field', type) : $emit('set-draw-type', type)"
|
@click="['file', 'payment', 'verification', 'kba'].includes(type) ? $emit('add-field', type) : $emit('set-draw-type', type)"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex items-console transition-all cursor-grab h-full absolute left-0"
|
class="flex items-console transition-all cursor-grab h-full absolute left-0"
|
||||||
@@ -208,6 +298,43 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="withFieldsDetection && editable && fields.length < 2"
|
||||||
|
class="my-2"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn w-full"
|
||||||
|
:class="{ 'bg-base-300': fieldPagesLoaded !== null }"
|
||||||
|
@click="fieldPagesLoaded !== null ? null : detectFields()"
|
||||||
|
>
|
||||||
|
<template v-if="fieldPagesLoaded !== null">
|
||||||
|
<IconInnerShadowTop
|
||||||
|
width="22"
|
||||||
|
class="animate-spin"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="analyzingProgress"
|
||||||
|
class="hidden md:inline"
|
||||||
|
>
|
||||||
|
{{ Math.round(analyzingProgress * 100) }}% {{ t('analyzing_') }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="hidden md:inline"
|
||||||
|
>
|
||||||
|
{{ fieldPagesLoaded }} / {{ numberOfPages }} {{ t('processing_') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<IconSparkles width="22" />
|
||||||
|
<span
|
||||||
|
class="hidden md:inline"
|
||||||
|
>
|
||||||
|
{{ t('autodetect_fields') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-show="fields.length < 4 && editable && withHelp && showTourStartForm"
|
v-show="fields.length < 4 && editable && withHelp && showTourStartForm"
|
||||||
class="rounded py-2 px-4 w-full border border-dashed border-base-300"
|
class="rounded py-2 px-4 w-full border border-dashed border-base-300"
|
||||||
@@ -229,32 +356,52 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Field from './field'
|
import Field from './field'
|
||||||
|
import CustomField from './custom_field'
|
||||||
import FieldType from './field_type'
|
import FieldType from './field_type'
|
||||||
import FieldSubmitter from './field_submitter'
|
import FieldSubmitter from './field_submitter'
|
||||||
import { IconLock, IconCirclePlus } from '@tabler/icons-vue'
|
import { IconLock, IconCirclePlus, IconInnerShadowTop, IconSparkles } from '@tabler/icons-vue'
|
||||||
import IconDrag from './icon_drag'
|
import IconDrag from './icon_drag'
|
||||||
|
import { v4 } from 'uuid'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TemplateFields',
|
name: 'TemplateFields',
|
||||||
components: {
|
components: {
|
||||||
Field,
|
Field,
|
||||||
|
CustomField,
|
||||||
FieldType,
|
FieldType,
|
||||||
IconCirclePlus,
|
IconCirclePlus,
|
||||||
|
IconSparkles,
|
||||||
|
IconInnerShadowTop,
|
||||||
FieldSubmitter,
|
FieldSubmitter,
|
||||||
IconDrag,
|
IconDrag,
|
||||||
IconLock
|
IconLock
|
||||||
},
|
},
|
||||||
inject: ['save', 'backgroundColor', 'withPhone', 'withVerification', 'withPayment', 't', 'fieldsDragFieldRef'],
|
inject: ['save', 'backgroundColor', 'withPhone', 'withVerification', 'withKba', 'withPayment', 't', 'fieldsDragFieldRef', 'customDragFieldRef', 'baseFetch', 'selectedAreasRef', 'getFieldTypeIndex'],
|
||||||
props: {
|
props: {
|
||||||
fields: {
|
fields: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
customFields: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
withCustomFields: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
withFieldsSearch: {
|
withFieldsSearch: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
withFieldsDetection: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
withSignatureId: {
|
withSignatureId: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -328,15 +475,25 @@ export default {
|
|||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['add-field', 'set-draw', 'set-draw-type', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter', 'set-drag-placeholder'],
|
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 () {
|
data () {
|
||||||
return {
|
return {
|
||||||
defaultFieldsSearch: ''
|
fieldPagesLoaded: null,
|
||||||
|
analyzingProgress: 0,
|
||||||
|
newCustomField: null,
|
||||||
|
showCustomTab: false,
|
||||||
|
defaultFieldsSearch: '',
|
||||||
|
customFieldsSearch: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
fieldNames: FieldType.computed.fieldNames,
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
fieldIcons: FieldType.computed.fieldIcons,
|
fieldIcons: FieldType.computed.fieldIcons,
|
||||||
|
numberOfPages () {
|
||||||
|
return this.template.documents.reduce((acc, doc) => {
|
||||||
|
return acc + doc.metadata?.pdf?.number_of_pages || doc.preview_images.length
|
||||||
|
}, 0)
|
||||||
|
},
|
||||||
isShowFieldSearch () {
|
isShowFieldSearch () {
|
||||||
if (this.withFieldsSearch === false) {
|
if (this.withFieldsSearch === false) {
|
||||||
return false
|
return false
|
||||||
@@ -379,6 +536,23 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
return this.submitterDefaultFields
|
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: {
|
methods: {
|
||||||
@@ -389,6 +563,139 @@ export default {
|
|||||||
|
|
||||||
this.$emit('set-drag', field)
|
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 = []
|
||||||
|
|
||||||
|
this.fieldPagesLoaded = 0
|
||||||
|
|
||||||
|
this.baseFetch(`/templates/${this.template.id}/detect_fields`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}).then(async (response) => {
|
||||||
|
const reader = response.body.getReader()
|
||||||
|
const decoder = new TextDecoder('utf-8')
|
||||||
|
let buffer = ''
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { value, done } = await reader.read()
|
||||||
|
|
||||||
|
buffer += decoder.decode(value, { stream: true })
|
||||||
|
|
||||||
|
const lines = buffer.split('\n\n')
|
||||||
|
|
||||||
|
buffer = lines.pop()
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith('data: ')) {
|
||||||
|
const jsonStr = line.replace(/^data: /, '')
|
||||||
|
const data = JSON.parse(jsonStr)
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
if ((data.fields || fields).length) {
|
||||||
|
this.template.fields = data.fields || fields
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
} else {
|
||||||
|
alert(data.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
} else if (data.analyzing) {
|
||||||
|
this.analyzingProgress = data.progress
|
||||||
|
} else if (data.completed) {
|
||||||
|
this.fieldPagesLoaded = null
|
||||||
|
|
||||||
|
if (data.submitters) {
|
||||||
|
this.template.submitters = data.submitters
|
||||||
|
this.$emit('select-submitter', this.template.submitters[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
this.template.fields = data.fields || fields
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
|
||||||
|
break
|
||||||
|
} else if (data.fields) {
|
||||||
|
data.fields.forEach((f) => {
|
||||||
|
if (!f.submitter_uuid) {
|
||||||
|
f.submitter_uuid = this.template.submitters[0].uuid
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.fieldPagesLoaded += 1
|
||||||
|
|
||||||
|
fields.push(...data.fields)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (done) break
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Error in streaming message: ', error)
|
||||||
|
}).finally(() => {
|
||||||
|
this.fieldPagesLoaded = null
|
||||||
|
this.analyzingProgress = null
|
||||||
|
this.isFieldsLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
setDragPlaceholder (event) {
|
setDragPlaceholder (event) {
|
||||||
this.$emit('set-drag-placeholder', {
|
this.$emit('set-drag-placeholder', {
|
||||||
offsetX: event.offsetX,
|
offsetX: event.offsetX,
|
||||||
@@ -450,7 +757,7 @@ export default {
|
|||||||
removeSubmitter (submitter) {
|
removeSubmitter (submitter) {
|
||||||
[...this.fields].forEach((field) => {
|
[...this.fields].forEach((field) => {
|
||||||
if (field.submitter_uuid === submitter.uuid) {
|
if (field.submitter_uuid === submitter.uuid) {
|
||||||
this.removeField(field)
|
this.removeField(field, false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -462,7 +769,7 @@ export default {
|
|||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
removeField (field) {
|
removeField (field, save = true) {
|
||||||
this.fields.splice(this.fields.indexOf(field), 1)
|
this.fields.splice(this.fields.indexOf(field), 1)
|
||||||
|
|
||||||
this.fields.forEach((f) => {
|
this.fields.forEach((f) => {
|
||||||
@@ -481,7 +788,54 @@ export default {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
this.save()
|
field.areas?.forEach((area) => {
|
||||||
|
this.selectedAreasRef.value.splice(this.selectedAreasRef.value.indexOf(area), 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (save) {
|
||||||
|
this.save()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeCustomField (field) {
|
||||||
|
this.customFields.splice(this.customFields.indexOf(field), 1)
|
||||||
|
|
||||||
|
if (!this.customFields.length) {
|
||||||
|
this.setFieldsTab('default')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveCustomFields()
|
||||||
|
},
|
||||||
|
onCustomFieldDragover (e) {
|
||||||
|
if (this.customDragFieldRef.value && this.customFields.includes(this.customDragFieldRef.value)) {
|
||||||
|
const container = this.$refs.customFieldsList
|
||||||
|
const targetField = e.target.closest('[data-uuid]')
|
||||||
|
const dragField = container.querySelector(`[data-uuid="${this.customDragFieldRef.value.uuid}"]`)
|
||||||
|
|
||||||
|
if (dragField && targetField && targetField !== dragField) {
|
||||||
|
const fields = Array.from(container.children)
|
||||||
|
const currentIndex = fields.indexOf(dragField)
|
||||||
|
const targetIndex = fields.indexOf(targetField)
|
||||||
|
|
||||||
|
if (currentIndex < targetIndex) {
|
||||||
|
targetField.after(dragField)
|
||||||
|
} else {
|
||||||
|
targetField.before(dragField)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reorderCustomFields () {
|
||||||
|
if (!this.customFields.includes(this.customDragFieldRef.value)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const reorderedFields = Array.from(this.$refs.customFieldsList.children).map((el) => {
|
||||||
|
return this.customFields.find((f) => f.uuid === el.dataset.uuid)
|
||||||
|
}).filter(Boolean)
|
||||||
|
|
||||||
|
this.customFields.splice(0, this.customFields.length, ...reorderedFields)
|
||||||
|
|
||||||
|
this.saveCustomFields()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
<div class="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">
|
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||||
<span class="modal-title">
|
<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>
|
</span>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
@@ -171,7 +171,7 @@
|
|||||||
contenteditable="true"
|
contenteditable="true"
|
||||||
class="outline-none whitespace-nowrap truncate"
|
class="outline-none whitespace-nowrap truncate"
|
||||||
>
|
>
|
||||||
{{ field.default_value || field.name || buildDefaultName(field, template.fields) }}
|
{{ field.default_value || field.name || buildDefaultName(field) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -196,7 +196,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
IconChevronDown
|
IconChevronDown
|
||||||
},
|
},
|
||||||
inject: ['t', 'save', 'template'],
|
inject: ['t', 'template'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -217,7 +217,7 @@ export default {
|
|||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['close'],
|
emits: ['close', 'save'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
preferences: {}
|
preferences: {}
|
||||||
@@ -257,6 +257,7 @@ export default {
|
|||||||
colors () {
|
colors () {
|
||||||
return [
|
return [
|
||||||
{ label: '⬛', value: 'black' },
|
{ label: '⬛', value: 'black' },
|
||||||
|
{ label: '⬜', value: 'white' },
|
||||||
{ label: '🟦', value: 'blue' },
|
{ label: '🟦', value: 'blue' },
|
||||||
{ label: '🟥', value: 'red' }
|
{ label: '🟥', value: 'red' }
|
||||||
]
|
]
|
||||||
@@ -271,6 +272,7 @@ export default {
|
|||||||
'items-center': !this.preferences.valign || this.preferences.valign === 'center',
|
'items-center': !this.preferences.valign || this.preferences.valign === 'center',
|
||||||
'items-start': this.preferences.valign === 'top',
|
'items-start': this.preferences.valign === 'top',
|
||||||
'items-end': this.preferences.valign === 'bottom',
|
'items-end': this.preferences.valign === 'bottom',
|
||||||
|
'bg-black': this.preferences.color === 'white',
|
||||||
'font-bold': ['bold_italic', 'bold'].includes(this.preferences.font_type),
|
'font-bold': ['bold_italic', 'bold'].includes(this.preferences.font_type),
|
||||||
italic: ['bold_italic', 'italic'].includes(this.preferences.font_type)
|
italic: ['bold_italic', 'italic'].includes(this.preferences.font_type)
|
||||||
}
|
}
|
||||||
@@ -322,8 +324,7 @@ export default {
|
|||||||
|
|
||||||
Object.assign(this.field.preferences, this.preferences)
|
Object.assign(this.field.preferences, this.preferences)
|
||||||
|
|
||||||
this.save()
|
this.$emit('save')
|
||||||
|
|
||||||
this.$emit('close')
|
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="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">
|
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||||
<span class="modal-title">
|
<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>
|
</span>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
v-for="f in fields"
|
v-for="f in fields"
|
||||||
:key="f.uuid"
|
: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"
|
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
|
<IconMathFunction
|
||||||
v-if="f.preferences?.formula"
|
v-if="f.preferences?.formula"
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
height="20"
|
height="20"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
/>
|
/>
|
||||||
{{ f.name || buildDefaultName(f, template.fields) }}
|
{{ f.name || buildDefaultName(f) }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -131,7 +131,7 @@ export default {
|
|||||||
IconCodePlus,
|
IconCodePlus,
|
||||||
IconMathFunction
|
IconMathFunction
|
||||||
},
|
},
|
||||||
inject: ['t', 'save', 'template', 'withFormula'],
|
inject: ['t', 'template', 'withFormula'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -152,7 +152,7 @@ export default {
|
|||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['close'],
|
emits: ['close', 'save'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
formula: ''
|
formula: ''
|
||||||
@@ -181,7 +181,7 @@ export default {
|
|||||||
const foundField = this.fields.find((f) => f.uuid === uuid)
|
const foundField = this.fields.find((f) => f.uuid === uuid)
|
||||||
|
|
||||||
if (foundField) {
|
if (foundField) {
|
||||||
return `{{${foundField.name || this.buildDefaultName(foundField, this.template.fields)}}}`
|
return `{{${foundField.name || this.buildDefaultName(foundField)}}}`
|
||||||
} else {
|
} else {
|
||||||
return '{{FIELD NOT FOUND}}'
|
return '{{FIELD NOT FOUND}}'
|
||||||
}
|
}
|
||||||
@@ -190,7 +190,7 @@ export default {
|
|||||||
normalizeFormula (text) {
|
normalizeFormula (text) {
|
||||||
return text.replace(/{{(.*?)}}/g, (match, name) => {
|
return text.replace(/{{(.*?)}}/g, (match, name) => {
|
||||||
const foundField = this.fields.find((f) => {
|
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) {
|
if (foundField) {
|
||||||
@@ -212,11 +212,14 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
this.field.preferences.formula = normalizedFormula
|
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.field.readonly = !!normalizedFormula
|
||||||
}
|
}
|
||||||
|
|
||||||
this.save()
|
this.$emit('save')
|
||||||
|
|
||||||
this.$emit('close')
|
this.$emit('close')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
form="sync_form"
|
:form="`sync_form_${inputId}`"
|
||||||
type="submit"
|
type="submit"
|
||||||
:disabled="isLoading"
|
:disabled="isLoading"
|
||||||
>
|
>
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<form
|
<form
|
||||||
id="sync_form"
|
:id="`sync_form_${inputId}`"
|
||||||
ref="form"
|
ref="form"
|
||||||
class="hidden"
|
class="hidden"
|
||||||
@submit.prevent="upload({ path: uploadUrl })"
|
@submit.prevent="upload({ path: uploadUrl })"
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
const en = {
|
const en = {
|
||||||
|
fixed: 'Fixed',
|
||||||
|
default: 'Default',
|
||||||
|
save_as_custom_field: 'Save as custom field',
|
||||||
|
kba: 'KBA',
|
||||||
|
analyzing_: 'Analyzing...',
|
||||||
|
download: 'Download',
|
||||||
|
downloading_: 'Downloading...',
|
||||||
view: 'View',
|
view: 'View',
|
||||||
|
autodetect_fields: 'Autodetect Fields',
|
||||||
payment_link: 'Payment link',
|
payment_link: 'Payment link',
|
||||||
strikeout: 'Strikeout',
|
strikeout: 'Strikeout',
|
||||||
draw_strikethrough_the_document: 'Draw strikethrough the document',
|
draw_strikethrough_the_document: 'Draw strikethrough the document',
|
||||||
@@ -24,6 +32,9 @@ const en = {
|
|||||||
field_not_found: 'Field not found',
|
field_not_found: 'Field not found',
|
||||||
clear: 'Clear',
|
clear: 'Clear',
|
||||||
align: 'Align',
|
align: 'Align',
|
||||||
|
resize: 'Resize',
|
||||||
|
width: 'Width',
|
||||||
|
height: 'Height',
|
||||||
add_all_required_fields_to_continue: 'Add all required fields to continue',
|
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?',
|
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Uploaded PDF contains form fields. Keep or remove them?',
|
||||||
keep: 'Keep',
|
keep: 'Keep',
|
||||||
@@ -41,6 +52,8 @@ const en = {
|
|||||||
type_value: 'Type value',
|
type_value: 'Type value',
|
||||||
equal: 'Equal',
|
equal: 'Equal',
|
||||||
not_equal: 'Not equal',
|
not_equal: 'Not equal',
|
||||||
|
greater_than: 'Greater than',
|
||||||
|
less_than: 'Less than',
|
||||||
contains: 'Contains',
|
contains: 'Contains',
|
||||||
does_not_contain: 'Does not contain',
|
does_not_contain: 'Does not contain',
|
||||||
not_empty: 'Not empty',
|
not_empty: 'Not empty',
|
||||||
@@ -84,8 +97,9 @@ const en = {
|
|||||||
format: 'Format',
|
format: 'Format',
|
||||||
read_only: 'Read-only',
|
read_only: 'Read-only',
|
||||||
page: 'Page',
|
page: 'Page',
|
||||||
draw_new_area: 'Draw New Area',
|
draw_new_area: 'Draw new area',
|
||||||
copy_to_all_pages: 'Copy to All Pages',
|
copy_to_all_pages: 'Copy to all pages',
|
||||||
|
more: 'More',
|
||||||
add_option: 'Add option',
|
add_option: 'Add option',
|
||||||
option: 'Option',
|
option: 'Option',
|
||||||
options: 'Options',
|
options: 'Options',
|
||||||
@@ -166,6 +180,9 @@ const en = {
|
|||||||
numbers_only: 'Numbers only',
|
numbers_only: 'Numbers only',
|
||||||
letters_only: 'Letters only',
|
letters_only: 'Letters only',
|
||||||
regexp_validation: 'Regexp validation',
|
regexp_validation: 'Regexp validation',
|
||||||
|
custom_validation: 'Custom Validation',
|
||||||
|
length_validation: 'Length Validation',
|
||||||
|
number_range: 'Number Range',
|
||||||
enter_pdf_password: 'Enter PDF password',
|
enter_pdf_password: 'Enter PDF password',
|
||||||
wrong_password: 'Wrong password',
|
wrong_password: 'Wrong password',
|
||||||
currency: 'Currency',
|
currency: 'Currency',
|
||||||
@@ -180,10 +197,29 @@ const en = {
|
|||||||
start_tour: 'Start Tour',
|
start_tour: 'Start Tour',
|
||||||
or_add_from: 'Or add from',
|
or_add_from: 'Or add from',
|
||||||
sync: 'Sync',
|
sync: 'Sync',
|
||||||
syncing: 'Syncing...'
|
syncing: 'Syncing...',
|
||||||
|
copy: 'Copy',
|
||||||
|
paste: 'Paste',
|
||||||
|
select_fields: 'Select Fields',
|
||||||
|
draw_fields: 'Draw Fields',
|
||||||
|
align_left: 'Align Left',
|
||||||
|
align_right: 'Align Right',
|
||||||
|
align_top: 'Align Top',
|
||||||
|
align_bottom: 'Align Bottom',
|
||||||
|
fields_selected: '{count} Fields Selected',
|
||||||
|
field_added: '{count} Field Added',
|
||||||
|
fields_added: '{count} Fields Added'
|
||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
|
fixed: 'Fijo',
|
||||||
|
default: 'Predeterminado',
|
||||||
|
save_as_custom_field: 'Guardar como personalizado',
|
||||||
|
kba: 'KBA',
|
||||||
|
autodetect_fields: 'Autodetectar campos',
|
||||||
|
analyzing_: 'Analizando...',
|
||||||
|
download: 'Descargar',
|
||||||
|
downloading_: 'Descargando...',
|
||||||
view: 'Vista',
|
view: 'Vista',
|
||||||
payment_link: 'Enlace de pago',
|
payment_link: 'Enlace de pago',
|
||||||
strikeout: 'Tachar',
|
strikeout: 'Tachar',
|
||||||
@@ -210,6 +246,9 @@ const es = {
|
|||||||
clear: 'Borrar',
|
clear: 'Borrar',
|
||||||
type_value: 'Escriba valor',
|
type_value: 'Escriba valor',
|
||||||
align: 'Alinear',
|
align: 'Alinear',
|
||||||
|
resize: 'Redimensionar',
|
||||||
|
width: 'Ancho',
|
||||||
|
height: 'Alto',
|
||||||
add_all_required_fields_to_continue: 'Agregar todos los campos requeridos para continuar',
|
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?',
|
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'El PDF cargado tiene campos. ¿Mantenerlos o eliminarlos?',
|
||||||
keep: 'Mantener',
|
keep: 'Mantener',
|
||||||
@@ -227,6 +266,8 @@ const es = {
|
|||||||
price: 'Precio',
|
price: 'Precio',
|
||||||
equal: 'Igual',
|
equal: 'Igual',
|
||||||
not_equal: 'No es igual',
|
not_equal: 'No es igual',
|
||||||
|
greater_than: 'Mayor que',
|
||||||
|
less_than: 'Menor que',
|
||||||
contains: 'Contiene',
|
contains: 'Contiene',
|
||||||
does_not_contain: 'No contiene',
|
does_not_contain: 'No contiene',
|
||||||
not_empty: 'No vacío',
|
not_empty: 'No vacío',
|
||||||
@@ -267,6 +308,7 @@ const es = {
|
|||||||
page: 'Página',
|
page: 'Página',
|
||||||
draw_new_area: 'Dibujar nueva área',
|
draw_new_area: 'Dibujar nueva área',
|
||||||
copy_to_all_pages: 'Copiar a todas las páginas',
|
copy_to_all_pages: 'Copiar a todas las páginas',
|
||||||
|
more: 'Más',
|
||||||
add_option: 'Agregar opción',
|
add_option: 'Agregar opción',
|
||||||
option: 'Opción',
|
option: 'Opción',
|
||||||
options: 'Opciones',
|
options: 'Opciones',
|
||||||
@@ -351,6 +393,9 @@ const es = {
|
|||||||
numbers_only: 'Solo números',
|
numbers_only: 'Solo números',
|
||||||
letters_only: 'Solo letras',
|
letters_only: 'Solo letras',
|
||||||
regexp_validation: 'Validación de expresión regular',
|
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',
|
enter_pdf_password: 'Ingrese la contraseña del PDF',
|
||||||
wrong_password: 'Contraseña incorrecta',
|
wrong_password: 'Contraseña incorrecta',
|
||||||
currency: 'Moneda',
|
currency: 'Moneda',
|
||||||
@@ -365,10 +410,29 @@ const es = {
|
|||||||
start_tour: 'Iniciar guía',
|
start_tour: 'Iniciar guía',
|
||||||
or_add_from: 'O agregar desde',
|
or_add_from: 'O agregar desde',
|
||||||
sync: 'Sincronizar',
|
sync: 'Sincronizar',
|
||||||
syncing: 'Sincronizando...'
|
syncing: 'Sincronizando...',
|
||||||
|
copy: 'Copiar',
|
||||||
|
paste: 'Pegar',
|
||||||
|
select_fields: 'Seleccionar Campos',
|
||||||
|
draw_fields: 'Dibujar Campos',
|
||||||
|
align_left: 'Alinear a la izquierda',
|
||||||
|
align_right: 'Alinear a la derecha',
|
||||||
|
align_top: 'Alinear arriba',
|
||||||
|
align_bottom: 'Alinear abajo',
|
||||||
|
fields_selected: '{count} Campos Seleccionados',
|
||||||
|
field_added: '{count} Campo Añadido',
|
||||||
|
fields_added: '{count} Campos Añadidos'
|
||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
|
fixed: 'Fisso',
|
||||||
|
default: 'Predefinito',
|
||||||
|
save_as_custom_field: 'Salva come personalizzato',
|
||||||
|
kba: 'KBA',
|
||||||
|
autodetect_fields: 'Rileva campi',
|
||||||
|
analyzing_: 'Analisi...',
|
||||||
|
download: 'Scarica',
|
||||||
|
downloading_: 'Download in corso...',
|
||||||
view: 'Vista',
|
view: 'Vista',
|
||||||
payment_link: 'Link di pagamento',
|
payment_link: 'Link di pagamento',
|
||||||
strikeout: 'Barrato',
|
strikeout: 'Barrato',
|
||||||
@@ -394,6 +458,9 @@ const it = {
|
|||||||
field_not_found: 'Campo non trovato',
|
field_not_found: 'Campo non trovato',
|
||||||
clear: 'Cancella',
|
clear: 'Cancella',
|
||||||
align: 'Allinea',
|
align: 'Allinea',
|
||||||
|
resize: 'Ridimensiona',
|
||||||
|
width: 'Larghezza',
|
||||||
|
height: 'Altezza',
|
||||||
add_all_required_fields_to_continue: 'Aggiungi tutti i campi obbligatori per continuare',
|
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?',
|
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Il PDF caricato contiene campi del modulo. Mantenerli o rimuoverli?',
|
||||||
keep: 'Mantieni',
|
keep: 'Mantieni',
|
||||||
@@ -411,6 +478,8 @@ const it = {
|
|||||||
type_value: 'Inserisci valore',
|
type_value: 'Inserisci valore',
|
||||||
equal: 'Uguale',
|
equal: 'Uguale',
|
||||||
not_equal: 'Non uguale',
|
not_equal: 'Non uguale',
|
||||||
|
greater_than: 'Maggiore di',
|
||||||
|
less_than: 'Minore di',
|
||||||
contains: 'Contiene',
|
contains: 'Contiene',
|
||||||
does_not_contain: 'Non contiene',
|
does_not_contain: 'Non contiene',
|
||||||
not_empty: 'Non vuoto',
|
not_empty: 'Non vuoto',
|
||||||
@@ -456,6 +525,7 @@ const it = {
|
|||||||
page: 'Pagina',
|
page: 'Pagina',
|
||||||
draw_new_area: 'Disegna nuova area',
|
draw_new_area: 'Disegna nuova area',
|
||||||
copy_to_all_pages: 'Copia in tutte le pagine',
|
copy_to_all_pages: 'Copia in tutte le pagine',
|
||||||
|
more: 'Altro',
|
||||||
add_option: 'Aggiungi opzione',
|
add_option: 'Aggiungi opzione',
|
||||||
option: 'Opzione',
|
option: 'Opzione',
|
||||||
options: 'Opzioni',
|
options: 'Opzioni',
|
||||||
@@ -536,6 +606,9 @@ const it = {
|
|||||||
numbers_only: 'Solo numeri',
|
numbers_only: 'Solo numeri',
|
||||||
letters_only: 'Solo lettere',
|
letters_only: 'Solo lettere',
|
||||||
regexp_validation: 'Validazione regexp',
|
regexp_validation: 'Validazione regexp',
|
||||||
|
custom_validation: 'Validazione Personalizzata',
|
||||||
|
length_validation: 'Validazione Lunghezza',
|
||||||
|
number_range: 'Intervallo Numerico',
|
||||||
enter_pdf_password: 'Inserisci password PDF',
|
enter_pdf_password: 'Inserisci password PDF',
|
||||||
wrong_password: 'Password errata',
|
wrong_password: 'Password errata',
|
||||||
currency: 'Valuta',
|
currency: 'Valuta',
|
||||||
@@ -550,10 +623,29 @@ const it = {
|
|||||||
start_tour: 'Inizia il tour',
|
start_tour: 'Inizia il tour',
|
||||||
or_add_from: 'O aggiungi da',
|
or_add_from: 'O aggiungi da',
|
||||||
sync: 'Sincronizza',
|
sync: 'Sincronizza',
|
||||||
syncing: 'Sincronizzazione...'
|
syncing: 'Sincronizzazione...',
|
||||||
|
copy: 'Copia',
|
||||||
|
paste: 'Incolla',
|
||||||
|
select_fields: 'Seleziona Campi',
|
||||||
|
draw_fields: 'Disegna Campi',
|
||||||
|
align_left: 'Allinea a sinistra',
|
||||||
|
align_right: 'Allinea a destra',
|
||||||
|
align_top: 'Allinea in alto',
|
||||||
|
align_bottom: 'Allinea in basso',
|
||||||
|
fields_selected: '{count} Campi Selezionati',
|
||||||
|
field_added: '{count} Campo Aggiunto',
|
||||||
|
fields_added: '{count} Campi Aggiunti'
|
||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
|
fixed: 'Fixo',
|
||||||
|
default: 'Padrão',
|
||||||
|
save_as_custom_field: 'Salvar como personalizado',
|
||||||
|
kba: 'KBA',
|
||||||
|
autodetect_fields: 'Detectar campos',
|
||||||
|
analyzing_: 'Analisando...',
|
||||||
|
download: 'Baixar',
|
||||||
|
downloading_: 'Baixando...',
|
||||||
view: 'Visualizar',
|
view: 'Visualizar',
|
||||||
payment_link: 'Link de pagamento',
|
payment_link: 'Link de pagamento',
|
||||||
strikeout: 'Tachado',
|
strikeout: 'Tachado',
|
||||||
@@ -583,6 +675,9 @@ const pt = {
|
|||||||
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'O PDF carregado contém campos. Manter ou removê-los?',
|
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'O PDF carregado contém campos. Manter ou removê-los?',
|
||||||
keep: 'Manter',
|
keep: 'Manter',
|
||||||
align: 'Alinhar',
|
align: 'Alinhar',
|
||||||
|
resize: 'Redimensionar',
|
||||||
|
width: 'Largura',
|
||||||
|
height: 'Altura',
|
||||||
left: 'Esquerda',
|
left: 'Esquerda',
|
||||||
heading: 'Cabeçalho',
|
heading: 'Cabeçalho',
|
||||||
validation: 'Validação',
|
validation: 'Validação',
|
||||||
@@ -597,6 +692,8 @@ const pt = {
|
|||||||
price: 'Preço',
|
price: 'Preço',
|
||||||
equal: 'Igual',
|
equal: 'Igual',
|
||||||
not_equal: 'Não é igual',
|
not_equal: 'Não é igual',
|
||||||
|
greater_than: 'Maior que',
|
||||||
|
less_than: 'Menor que',
|
||||||
contains: 'Contém',
|
contains: 'Contém',
|
||||||
does_not_contain: 'Não contém',
|
does_not_contain: 'Não contém',
|
||||||
not_empty: 'Não vazio',
|
not_empty: 'Não vazio',
|
||||||
@@ -637,6 +734,7 @@ const pt = {
|
|||||||
page: 'Página',
|
page: 'Página',
|
||||||
draw_new_area: 'Desenhar nova área',
|
draw_new_area: 'Desenhar nova área',
|
||||||
copy_to_all_pages: 'Copiar para todas as páginas',
|
copy_to_all_pages: 'Copiar para todas as páginas',
|
||||||
|
more: 'Mais',
|
||||||
add_option: 'Adicionar opção',
|
add_option: 'Adicionar opção',
|
||||||
option: 'Opção',
|
option: 'Opção',
|
||||||
options: 'Opções',
|
options: 'Opções',
|
||||||
@@ -721,6 +819,9 @@ const pt = {
|
|||||||
numbers_only: 'Somente números',
|
numbers_only: 'Somente números',
|
||||||
letters_only: 'Somente letras',
|
letters_only: 'Somente letras',
|
||||||
regexp_validation: 'Validação de expressão regular',
|
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',
|
enter_pdf_password: 'Digite a senha do PDF',
|
||||||
wrong_password: 'Senha incorreta',
|
wrong_password: 'Senha incorreta',
|
||||||
currency: 'Moeda',
|
currency: 'Moeda',
|
||||||
@@ -735,10 +836,29 @@ const pt = {
|
|||||||
start_tour: 'Iniciar tour',
|
start_tour: 'Iniciar tour',
|
||||||
or_add_from: 'Ou adicionar de',
|
or_add_from: 'Ou adicionar de',
|
||||||
sync: 'Sincronizar',
|
sync: 'Sincronizar',
|
||||||
syncing: 'Sincronizando...'
|
syncing: 'Sincronizando...',
|
||||||
|
copy: 'Copiar',
|
||||||
|
paste: 'Colar',
|
||||||
|
select_fields: 'Selecionar Campos',
|
||||||
|
draw_fields: 'Desenhar Campos',
|
||||||
|
align_left: 'Alinhar à esquerda',
|
||||||
|
align_right: 'Alinhar à direita',
|
||||||
|
align_top: 'Alinhar ao topo',
|
||||||
|
align_bottom: 'Alinhar à parte inferior',
|
||||||
|
fields_selected: '{count} Campos Selecionados',
|
||||||
|
field_added: '{count} Campo Adicionado',
|
||||||
|
fields_added: '{count} Campos Adicionados'
|
||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
|
fixed: 'Fixe',
|
||||||
|
default: 'Par défaut',
|
||||||
|
save_as_custom_field: 'Enregistrer comme personnalisé',
|
||||||
|
kba: 'KBA',
|
||||||
|
autodetect_fields: 'Détecter les champs',
|
||||||
|
analyzing_: 'Analyse...',
|
||||||
|
download: 'Télécharger',
|
||||||
|
downloading_: 'Téléchargement...',
|
||||||
view: 'Voir',
|
view: 'Voir',
|
||||||
payment_link: 'Lien de paiement',
|
payment_link: 'Lien de paiement',
|
||||||
strikeout: 'Rature',
|
strikeout: 'Rature',
|
||||||
@@ -764,6 +884,9 @@ const fr = {
|
|||||||
field_not_found: 'Champ introuvable',
|
field_not_found: 'Champ introuvable',
|
||||||
clear: 'Effacer',
|
clear: 'Effacer',
|
||||||
align: 'Aligner',
|
align: 'Aligner',
|
||||||
|
resize: 'Redimensionner',
|
||||||
|
width: 'Largeur',
|
||||||
|
height: 'Hauteur',
|
||||||
add_all_required_fields_to_continue: 'Ajoutez tous les champs obligatoires pour continuer',
|
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 ?',
|
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',
|
keep: 'Conserver',
|
||||||
@@ -781,6 +904,8 @@ const fr = {
|
|||||||
type_value: 'Saisir une valeur',
|
type_value: 'Saisir une valeur',
|
||||||
equal: 'Égal',
|
equal: 'Égal',
|
||||||
not_equal: 'Différent',
|
not_equal: 'Différent',
|
||||||
|
greater_than: 'Supérieur à',
|
||||||
|
less_than: 'Inférieur à',
|
||||||
contains: 'Contient',
|
contains: 'Contient',
|
||||||
does_not_contain: 'Ne contient pas',
|
does_not_contain: 'Ne contient pas',
|
||||||
not_empty: 'Non vide',
|
not_empty: 'Non vide',
|
||||||
@@ -826,6 +951,7 @@ const fr = {
|
|||||||
page: 'Page',
|
page: 'Page',
|
||||||
draw_new_area: 'Dessiner une zone',
|
draw_new_area: 'Dessiner une zone',
|
||||||
copy_to_all_pages: 'Copier sur toutes les pages',
|
copy_to_all_pages: 'Copier sur toutes les pages',
|
||||||
|
more: 'Plus',
|
||||||
add_option: 'Ajouter une option',
|
add_option: 'Ajouter une option',
|
||||||
option: 'Option',
|
option: 'Option',
|
||||||
options: 'Options',
|
options: 'Options',
|
||||||
@@ -906,6 +1032,9 @@ const fr = {
|
|||||||
numbers_only: 'Chiffres uniquement',
|
numbers_only: 'Chiffres uniquement',
|
||||||
letters_only: 'Lettres uniquement',
|
letters_only: 'Lettres uniquement',
|
||||||
regexp_validation: 'Validation par expression régulière',
|
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',
|
enter_pdf_password: 'Saisir le mot de passe du PDF',
|
||||||
wrong_password: 'Mot de passe incorrect',
|
wrong_password: 'Mot de passe incorrect',
|
||||||
currency: 'Devise',
|
currency: 'Devise',
|
||||||
@@ -920,10 +1049,29 @@ const fr = {
|
|||||||
start_tour: 'Démarrer',
|
start_tour: 'Démarrer',
|
||||||
or_add_from: 'Ou ajouter depuis',
|
or_add_from: 'Ou ajouter depuis',
|
||||||
sync: 'Synchroniser',
|
sync: 'Synchroniser',
|
||||||
syncing: 'Synchronisation...'
|
syncing: 'Synchronisation...',
|
||||||
|
copy: 'Copier',
|
||||||
|
paste: 'Coller',
|
||||||
|
select_fields: 'Sélectionner Champs',
|
||||||
|
draw_fields: 'Dessiner Champs',
|
||||||
|
align_left: 'Aligner à gauche',
|
||||||
|
align_right: 'Aligner à droite',
|
||||||
|
align_top: 'Aligner en haut',
|
||||||
|
align_bottom: 'Aligner en bas',
|
||||||
|
fields_selected: '{count} Champs Sélectionnés',
|
||||||
|
field_added: '{count} Champ Ajouté',
|
||||||
|
fields_added: '{count} Champs Ajoutés'
|
||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
|
fixed: 'Fest',
|
||||||
|
default: 'Standard',
|
||||||
|
save_as_custom_field: 'Als benutzerdefiniert speichern',
|
||||||
|
kba: 'KBA',
|
||||||
|
autodetect_fields: 'Felder erkennen',
|
||||||
|
analyzing_: 'Analysiere...',
|
||||||
|
download: 'Download',
|
||||||
|
downloading_: 'Download...',
|
||||||
view: 'Anzeigen',
|
view: 'Anzeigen',
|
||||||
payment_link: 'Zahlungslink',
|
payment_link: 'Zahlungslink',
|
||||||
strikeout: 'Durchstreichen',
|
strikeout: 'Durchstreichen',
|
||||||
@@ -949,6 +1097,9 @@ const de = {
|
|||||||
field_not_found: 'Feld nicht gefunden',
|
field_not_found: 'Feld nicht gefunden',
|
||||||
clear: 'Leeren',
|
clear: 'Leeren',
|
||||||
align: 'Ausrichten',
|
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',
|
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?',
|
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Das hochgeladene PDF enthält Formularfelder. Beibehalten oder entfernen?',
|
||||||
keep: 'Beibehalten',
|
keep: 'Beibehalten',
|
||||||
@@ -966,6 +1117,8 @@ const de = {
|
|||||||
type_value: 'Wert eingeben',
|
type_value: 'Wert eingeben',
|
||||||
equal: 'Gleich',
|
equal: 'Gleich',
|
||||||
not_equal: 'Ungleich',
|
not_equal: 'Ungleich',
|
||||||
|
greater_than: 'Größer als',
|
||||||
|
less_than: 'Kleiner als',
|
||||||
contains: 'Enthält',
|
contains: 'Enthält',
|
||||||
does_not_contain: 'Enthält nicht',
|
does_not_contain: 'Enthält nicht',
|
||||||
not_empty: 'Nicht leer',
|
not_empty: 'Nicht leer',
|
||||||
@@ -1011,6 +1164,7 @@ const de = {
|
|||||||
page: 'Seite',
|
page: 'Seite',
|
||||||
draw_new_area: 'Bereich zeichnen',
|
draw_new_area: 'Bereich zeichnen',
|
||||||
copy_to_all_pages: 'Auf alle Seiten kopieren',
|
copy_to_all_pages: 'Auf alle Seiten kopieren',
|
||||||
|
more: 'Mehr',
|
||||||
add_option: 'Option hinzufügen',
|
add_option: 'Option hinzufügen',
|
||||||
option: 'Option',
|
option: 'Option',
|
||||||
options: 'Optionen',
|
options: 'Optionen',
|
||||||
@@ -1091,6 +1245,9 @@ const de = {
|
|||||||
numbers_only: 'Nur Zahlen',
|
numbers_only: 'Nur Zahlen',
|
||||||
letters_only: 'Nur Buchstaben',
|
letters_only: 'Nur Buchstaben',
|
||||||
regexp_validation: 'RegExp-Validierung',
|
regexp_validation: 'RegExp-Validierung',
|
||||||
|
custom_validation: 'Benutzerdefinierte Validierung',
|
||||||
|
length_validation: 'Längenvalidierung',
|
||||||
|
number_range: 'Zahlenbereich',
|
||||||
enter_pdf_password: 'PDF-Passwort eingeben',
|
enter_pdf_password: 'PDF-Passwort eingeben',
|
||||||
wrong_password: 'Falsches Passwort',
|
wrong_password: 'Falsches Passwort',
|
||||||
currency: 'Währung',
|
currency: 'Währung',
|
||||||
@@ -1105,10 +1262,29 @@ const de = {
|
|||||||
start_tour: 'Tour starten',
|
start_tour: 'Tour starten',
|
||||||
or_add_from: 'Oder hinzufügen aus',
|
or_add_from: 'Oder hinzufügen aus',
|
||||||
sync: 'Synchronisieren',
|
sync: 'Synchronisieren',
|
||||||
syncing: 'Synchronisiere...'
|
syncing: 'Synchronisiere...',
|
||||||
|
copy: 'Kopieren',
|
||||||
|
paste: 'Einfügen',
|
||||||
|
select_fields: 'Felder Auswählen',
|
||||||
|
draw_fields: 'Felder Zeichnen',
|
||||||
|
align_left: 'Links ausrichten',
|
||||||
|
align_right: 'Rechts ausrichten',
|
||||||
|
align_top: 'Oben ausrichten',
|
||||||
|
align_bottom: 'Unten ausrichten',
|
||||||
|
fields_selected: '{count} Felder Ausgewählt',
|
||||||
|
field_added: '{count} Feld Hinzugefügt',
|
||||||
|
fields_added: '{count} Felder Hinzugefügt'
|
||||||
}
|
}
|
||||||
|
|
||||||
const nl = {
|
const nl = {
|
||||||
|
fixed: 'Vast',
|
||||||
|
default: 'Standaard',
|
||||||
|
save_as_custom_field: 'Opslaan als aangepast',
|
||||||
|
kba: 'KBA',
|
||||||
|
autodetect_fields: 'Velden detecteren',
|
||||||
|
analyzing_: 'Analyseren...',
|
||||||
|
download: 'Downloaden',
|
||||||
|
downloading_: 'Downloaden...',
|
||||||
view: 'Bekijken',
|
view: 'Bekijken',
|
||||||
payment_link: 'Betaallink',
|
payment_link: 'Betaallink',
|
||||||
strikeout: 'Doorhalen',
|
strikeout: 'Doorhalen',
|
||||||
@@ -1134,6 +1310,9 @@ const nl = {
|
|||||||
field_not_found: 'Veld niet gevonden',
|
field_not_found: 'Veld niet gevonden',
|
||||||
clear: 'Wissen',
|
clear: 'Wissen',
|
||||||
align: 'Uitlijnen',
|
align: 'Uitlijnen',
|
||||||
|
resize: 'Formaat wijzigen',
|
||||||
|
width: 'Breedte',
|
||||||
|
height: 'Hoogte',
|
||||||
add_all_required_fields_to_continue: 'Voeg alle vereiste velden toe om door te gaan',
|
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?',
|
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Geüploade PDF bevat formuliervelden. Behouden of verwijderen?',
|
||||||
keep: 'Behouden',
|
keep: 'Behouden',
|
||||||
@@ -1151,6 +1330,8 @@ const nl = {
|
|||||||
type_value: 'Typ waarde',
|
type_value: 'Typ waarde',
|
||||||
equal: 'Gelijk aan',
|
equal: 'Gelijk aan',
|
||||||
not_equal: 'Niet gelijk aan',
|
not_equal: 'Niet gelijk aan',
|
||||||
|
greater_than: 'Groter dan',
|
||||||
|
less_than: 'Kleiner dan',
|
||||||
contains: 'Bevat',
|
contains: 'Bevat',
|
||||||
does_not_contain: 'Bevat niet',
|
does_not_contain: 'Bevat niet',
|
||||||
not_empty: 'Niet leeg',
|
not_empty: 'Niet leeg',
|
||||||
@@ -1196,6 +1377,7 @@ const nl = {
|
|||||||
page: 'Pagina',
|
page: 'Pagina',
|
||||||
draw_new_area: 'Nieuw gebied tekenen',
|
draw_new_area: 'Nieuw gebied tekenen',
|
||||||
copy_to_all_pages: 'Kopieer naar alle pag.',
|
copy_to_all_pages: 'Kopieer naar alle pag.',
|
||||||
|
more: 'Meer',
|
||||||
add_option: 'Optie toevoegen',
|
add_option: 'Optie toevoegen',
|
||||||
option: 'Optie',
|
option: 'Optie',
|
||||||
options: 'Opties',
|
options: 'Opties',
|
||||||
@@ -1276,6 +1458,9 @@ const nl = {
|
|||||||
numbers_only: 'Alleen cijfers',
|
numbers_only: 'Alleen cijfers',
|
||||||
letters_only: 'Alleen letters',
|
letters_only: 'Alleen letters',
|
||||||
regexp_validation: 'Regex validatie',
|
regexp_validation: 'Regex validatie',
|
||||||
|
custom_validation: 'Aangepaste Validatie',
|
||||||
|
length_validation: 'Lengte Validatie',
|
||||||
|
number_range: 'Getalbereik',
|
||||||
enter_pdf_password: 'Voer PDF-wachtwoord in',
|
enter_pdf_password: 'Voer PDF-wachtwoord in',
|
||||||
wrong_password: 'Onjuist wachtwoord',
|
wrong_password: 'Onjuist wachtwoord',
|
||||||
currency: 'Valuta',
|
currency: 'Valuta',
|
||||||
@@ -1290,7 +1475,18 @@ const nl = {
|
|||||||
start_tour: 'Rondleiding starten',
|
start_tour: 'Rondleiding starten',
|
||||||
or_add_from: 'Of toevoegen van',
|
or_add_from: 'Of toevoegen van',
|
||||||
sync: 'Synchroniseren',
|
sync: 'Synchroniseren',
|
||||||
syncing: 'Synchroniseren...'
|
syncing: 'Synchroniseren...',
|
||||||
|
copy: 'Kopiëren',
|
||||||
|
paste: 'Plakken',
|
||||||
|
select_fields: 'Velden Selecteren',
|
||||||
|
draw_fields: 'Velden Tekenen',
|
||||||
|
align_left: 'Links uitlijnen',
|
||||||
|
align_right: 'Rechts uitlijnen',
|
||||||
|
align_top: 'Boven uitlijnen',
|
||||||
|
align_bottom: 'Onder uitlijnen',
|
||||||
|
fields_selected: '{count} Velden Geselecteerd',
|
||||||
|
field_added: '{count} Veld Toegevoegd',
|
||||||
|
fields_added: '{count} Velden Toegevoegd'
|
||||||
}
|
}
|
||||||
|
|
||||||
export { en, es, it, pt, fr, de, nl }
|
export { en, es, it, pt, fr, de, nl }
|
||||||
@@ -77,7 +77,7 @@ export default {
|
|||||||
removeSubmitter (submitter) {
|
removeSubmitter (submitter) {
|
||||||
[...this.fields].forEach((field) => {
|
[...this.fields].forEach((field) => {
|
||||||
if (field.submitter_uuid === submitter.uuid) {
|
if (field.submitter_uuid === submitter.uuid) {
|
||||||
this.removeField(field)
|
this.removeField(field, false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -89,10 +89,12 @@ export default {
|
|||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
removeField (field) {
|
removeField (field, save = true) {
|
||||||
this.fields.splice(this.fields.indexOf(field), 1)
|
this.fields.splice(this.fields.indexOf(field), 1)
|
||||||
|
|
||||||
this.save()
|
if (save) {
|
||||||
|
this.save()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,12 +49,17 @@
|
|||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<div
|
||||||
|
v-if="fieldTypes.length && submitterDefaultFields.length"
|
||||||
|
class="bg-base-300"
|
||||||
|
style="height: 1px; margin-top: 1px; margin-bottom: 1px"
|
||||||
|
/>
|
||||||
|
<template v-if="fieldTypes.length || !submitterDefaultFields.length">
|
||||||
<template
|
<template
|
||||||
v-for="(icon, type) in fieldIconsSorted"
|
v-for="(icon, type) in fieldIconsSorted"
|
||||||
:key="type"
|
:key="type"
|
||||||
>
|
>
|
||||||
<li v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification'))">
|
<li v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification') && (withKba || type != 'kba'))">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="text-sm py-1 px-2"
|
class="text-sm py-1 px-2"
|
||||||
@@ -84,7 +89,7 @@ export default {
|
|||||||
IconPlus,
|
IconPlus,
|
||||||
IconX
|
IconX
|
||||||
},
|
},
|
||||||
inject: ['withPhone', 'withPayment', 'withVerification', 'backgroundColor', 't'],
|
inject: ['withPhone', 'withPayment', 'withVerification', 'withKba', 'backgroundColor', 't'],
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="relative select-none mb-4 before:border before:rounded before:top-0 before:bottom-0 before:left-0 before:right-0 before:absolute"
|
class="relative select-none mb-4 before:border before:rounded before:top-0 before:bottom-0 before:left-0 before:right-0 before:absolute"
|
||||||
:class="{ 'cursor-crosshair': allowDraw && editable, 'touch-none': !!drawField }"
|
:class="{ 'cursor-crosshair': allowDraw && editable && !isSelectMode, 'touch-none': !!drawField }"
|
||||||
style="container-type: size"
|
style="container-type: size"
|
||||||
:style="{ aspectRatio: `${width} / ${height}`}"
|
:style="{ aspectRatio: `${width} / ${height}`}"
|
||||||
>
|
>
|
||||||
@@ -17,7 +17,20 @@
|
|||||||
<div
|
<div
|
||||||
class="top-0 bottom-0 left-0 right-0 absolute"
|
class="top-0 bottom-0 left-0 right-0 absolute"
|
||||||
@pointerdown="onStartDraw"
|
@pointerdown="onStartDraw"
|
||||||
|
@contextmenu="openContextMenu"
|
||||||
>
|
>
|
||||||
|
<SelectionBox
|
||||||
|
v-if="showSelectionBox"
|
||||||
|
:selection-box="selectionBox"
|
||||||
|
:page-width="width"
|
||||||
|
:page-height="height"
|
||||||
|
:is-resizing="!!resizeDirection"
|
||||||
|
:is-drawing="!!drawFieldType"
|
||||||
|
:is-drag="isDrag"
|
||||||
|
@move="onSelectionBoxMove"
|
||||||
|
@contextmenu="openSelectionContextMenu"
|
||||||
|
@close-context-menu="closeContextMenu"
|
||||||
|
/>
|
||||||
<FieldArea
|
<FieldArea
|
||||||
v-for="(item, i) in areas"
|
v-for="(item, i) in areas"
|
||||||
:key="i"
|
:key="i"
|
||||||
@@ -34,31 +47,81 @@
|
|||||||
:default-field="defaultFieldsIndex[item.field.name]"
|
:default-field="defaultFieldsIndex[item.field.name]"
|
||||||
:default-submitters="defaultSubmitters"
|
:default-submitters="defaultSubmitters"
|
||||||
:max-page="totalPages - 1"
|
:max-page="totalPages - 1"
|
||||||
|
:is-select-mode="isSelectMode"
|
||||||
|
:is-mobile="isMobile"
|
||||||
@start-resize="resizeDirection = $event"
|
@start-resize="resizeDirection = $event"
|
||||||
@stop-resize="resizeDirection = null"
|
@stop-resize="resizeDirection = null"
|
||||||
@remove="$emit('remove-area', item.area)"
|
@remove="$emit('remove-area', item.area)"
|
||||||
@scroll-to="$emit('scroll-to', $event)"
|
@scroll-to="$emit('scroll-to', $event)"
|
||||||
|
@add-custom-field="$emit('add-custom-field', $event)"
|
||||||
|
@contextmenu="openAreaContextMenu($event, item.area, item.field)"
|
||||||
/>
|
/>
|
||||||
<FieldArea
|
<FieldArea
|
||||||
v-if="newArea"
|
v-for="(area, index) in newAreas"
|
||||||
|
:key="index"
|
||||||
:is-draw="true"
|
:is-draw="true"
|
||||||
:page-width="width"
|
:page-width="width"
|
||||||
:page-height="height"
|
:page-height="height"
|
||||||
:field="{ submitter_uuid: selectedSubmitter.uuid, type: drawField?.type || dragFieldPlaceholder?.type || defaultFieldType }"
|
:field="{ submitter_uuid: selectedSubmitter.uuid, type: newAreaFieldType }"
|
||||||
:area="newArea"
|
:area="area"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="newAreas.length > 1"
|
||||||
|
class="absolute outline-dashed outline-gray-400 pointer-events-none z-20"
|
||||||
|
:style="newAreasBoxStyle"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="selectionRect"
|
||||||
|
class="absolute outline-dashed outline-gray-400 pointer-events-none z-20"
|
||||||
|
:style="selectionRectStyle"
|
||||||
|
/>
|
||||||
|
<FieldContextMenu
|
||||||
|
v-if="contextMenu && contextMenu.field"
|
||||||
|
:context-menu="contextMenu"
|
||||||
|
:field="contextMenu.field"
|
||||||
|
:with-signature-id="withSignatureId"
|
||||||
|
:with-prefillable="withPrefillable"
|
||||||
|
:editable="editable"
|
||||||
|
:default-field="defaultFieldsIndex[contextMenu.field.name]"
|
||||||
|
@copy="handleCopy"
|
||||||
|
@delete="handleDelete"
|
||||||
|
@close="closeContextMenu"
|
||||||
|
@set-draw="$emit('set-draw', $event)"
|
||||||
|
@scroll-to="$emit('scroll-to', $event)"
|
||||||
|
@save="save"
|
||||||
|
@add-custom-field="$emit('add-custom-field', $event)"
|
||||||
|
/>
|
||||||
|
<SelectionContextMenu
|
||||||
|
v-else-if="contextMenu && contextMenu.areas"
|
||||||
|
:context-menu="contextMenu"
|
||||||
|
:editable="editable"
|
||||||
|
:template="template"
|
||||||
|
@copy="handleSelectionCopy"
|
||||||
|
@delete="handleSelectionDelete"
|
||||||
|
@close="closeContextMenu"
|
||||||
|
/>
|
||||||
|
<PageContextMenu
|
||||||
|
v-else-if="contextMenu && !contextMenu.field && !contextMenu.areas"
|
||||||
|
:context-menu="contextMenu"
|
||||||
|
:editable="editable"
|
||||||
|
:with-fields-detection="withFieldsDetection"
|
||||||
|
@paste="handlePaste"
|
||||||
|
@autodetect-fields="handleAutodetectFields"
|
||||||
|
@close="closeContextMenu"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-show="resizeDirection || isDrag || showMask || (drawField && isMobile) || fieldsDragFieldRef.value"
|
v-show="resizeDirection || isDrag || showMask || (drawField && isMobile) || fieldsDragFieldRef.value || customDragFieldRef?.value || selectionRect"
|
||||||
id="mask"
|
id="mask"
|
||||||
ref="mask"
|
ref="mask"
|
||||||
class="top-0 bottom-0 left-0 right-0 absolute"
|
class="top-0 bottom-0 left-0 right-0 absolute"
|
||||||
:class="{ 'z-10': !isMobile, 'cursor-grab': isDrag, 'cursor-nwse-resize': drawField, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }"
|
:class="{ 'z-10': !isMobile, 'cursor-grab': isDrag, 'cursor-nwse-resize': drawField && !isSelectMode, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }"
|
||||||
@pointermove="onPointermove"
|
@pointermove="onPointermove"
|
||||||
@pointerdown="onStartDraw"
|
@pointerdown="onStartDraw"
|
||||||
|
@contextmenu="openContextMenu"
|
||||||
@dragover.prevent="onDragover"
|
@dragover.prevent="onDragover"
|
||||||
@dragenter="onDragenter"
|
@dragenter="onDragenter"
|
||||||
@dragleave="newArea = null"
|
@dragleave="newAreas = []"
|
||||||
@drop="onDrop"
|
@drop="onDrop"
|
||||||
@pointerup="onPointerup"
|
@pointerup="onPointerup"
|
||||||
/>
|
/>
|
||||||
@@ -67,13 +130,21 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import FieldArea from './area'
|
import FieldArea from './area'
|
||||||
|
import FieldContextMenu from './field_context_menu'
|
||||||
|
import SelectionContextMenu from './selection_context_menu'
|
||||||
|
import PageContextMenu from './page_context_menu'
|
||||||
|
import SelectionBox from './selection_box'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TemplatePage',
|
name: 'TemplatePage',
|
||||||
components: {
|
components: {
|
||||||
FieldArea
|
FieldArea,
|
||||||
|
FieldContextMenu,
|
||||||
|
SelectionContextMenu,
|
||||||
|
PageContextMenu,
|
||||||
|
SelectionBox
|
||||||
},
|
},
|
||||||
inject: ['fieldTypes', 'defaultDrawFieldType', 'fieldsDragFieldRef', 'assignDropAreaSize'],
|
inject: ['fieldTypes', 'defaultDrawFieldType', 'fieldsDragFieldRef', 'customDragFieldRef', 'assignDropAreaSize', 'selectedAreasRef', 'template', 'isSelectModeRef', 'save'],
|
||||||
props: {
|
props: {
|
||||||
image: {
|
image: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -84,6 +155,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
isMobile: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
withSignatureId: {
|
withSignatureId: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -142,6 +218,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
drawCustomField: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
editable: {
|
editable: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -155,18 +236,68 @@ export default {
|
|||||||
number: {
|
number: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
attachmentUuid: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
withFieldsDetection: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['draw', 'drop-field', 'remove-area', 'scroll-to'],
|
emits: ['draw', 'drop-field', 'remove-area', 'copy-field', 'paste-field', 'scroll-to', 'copy-selected-areas', 'delete-selected-areas', 'autodetect-fields', 'add-custom-field', 'set-draw'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
areaRefs: [],
|
areaRefs: [],
|
||||||
showMask: false,
|
showMask: false,
|
||||||
resizeDirection: null,
|
resizeDirection: null,
|
||||||
newArea: null
|
newAreas: [],
|
||||||
|
contextMenu: null,
|
||||||
|
selectionRect: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isSelectMode () {
|
||||||
|
return this.isSelectModeRef.value && !this.drawFieldType && this.editable && !this.drawField
|
||||||
|
},
|
||||||
|
pageSelectedAreas () {
|
||||||
|
if (!this.selectedAreasRef.value) return []
|
||||||
|
|
||||||
|
return this.selectedAreasRef.value.filter((a) =>
|
||||||
|
a.attachment_uuid === this.attachmentUuid && a.page === this.number
|
||||||
|
)
|
||||||
|
},
|
||||||
|
showSelectionBox () {
|
||||||
|
return this.pageSelectedAreas.length >= 2 && this.editable
|
||||||
|
},
|
||||||
|
minSelectionBoxHeight () {
|
||||||
|
const ys = this.pageSelectedAreas.map((a) => a.y)
|
||||||
|
|
||||||
|
return Math.max(...ys) - Math.min(...ys)
|
||||||
|
},
|
||||||
|
minSelectionBoxWidth () {
|
||||||
|
const xs = this.pageSelectedAreas.map((a) => a.x)
|
||||||
|
|
||||||
|
return Math.max(...xs) - Math.min(...xs)
|
||||||
|
},
|
||||||
|
selectionBox () {
|
||||||
|
if (!this.pageSelectedAreas.length) return null
|
||||||
|
|
||||||
|
const minX = Math.min(...this.pageSelectedAreas.map((a) => a.x))
|
||||||
|
const minY = Math.min(...this.pageSelectedAreas.map((a) => a.y))
|
||||||
|
const maxX = Math.max(...this.pageSelectedAreas.map((a) => a.x + a.w))
|
||||||
|
const maxY = Math.max(...this.pageSelectedAreas.map((a) => a.y + a.h))
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: minX,
|
||||||
|
y: minY,
|
||||||
|
w: Math.max(maxX - minX, this.minSelectionBoxWidth),
|
||||||
|
h: Math.max(maxY - minY, this.minSelectionBoxHeight)
|
||||||
|
}
|
||||||
|
},
|
||||||
defaultFieldsIndex () {
|
defaultFieldsIndex () {
|
||||||
return this.defaultFields.reduce((acc, field) => {
|
return this.defaultFields.reduce((acc, field) => {
|
||||||
acc[field.name] = field
|
acc[field.name] = field
|
||||||
@@ -174,6 +305,14 @@ export default {
|
|||||||
return acc
|
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 () {
|
defaultFieldType () {
|
||||||
if (this.drawFieldType) {
|
if (this.drawFieldType) {
|
||||||
return this.drawFieldType
|
return this.drawFieldType
|
||||||
@@ -185,11 +324,6 @@ export default {
|
|||||||
return 'text'
|
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 () {
|
resizeDirectionClasses () {
|
||||||
return {
|
return {
|
||||||
nwse: 'cursor-nwse-resize',
|
nwse: 'cursor-nwse-resize',
|
||||||
@@ -201,6 +335,31 @@ export default {
|
|||||||
},
|
},
|
||||||
height () {
|
height () {
|
||||||
return this.image.metadata.height
|
return this.image.metadata.height
|
||||||
|
},
|
||||||
|
newAreasBoxStyle () {
|
||||||
|
if (this.newAreas.length < 2) return {}
|
||||||
|
|
||||||
|
const minX = Math.min(...this.newAreas.map(a => a.x))
|
||||||
|
const minY = Math.min(...this.newAreas.map(a => a.y))
|
||||||
|
const maxX = Math.max(...this.newAreas.map(a => a.x + a.w))
|
||||||
|
const maxY = Math.max(...this.newAreas.map(a => a.y + a.h))
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: minX * 100 + '%',
|
||||||
|
top: minY * 100 + '%',
|
||||||
|
width: (maxX - minX) * 100 + '%',
|
||||||
|
height: (maxY - minY) * 100 + '%'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectionRectStyle () {
|
||||||
|
if (!this.selectionRect) return {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: this.selectionRect.x * 100 + '%',
|
||||||
|
top: this.selectionRect.y * 100 + '%',
|
||||||
|
width: this.selectionRect.w * 100 + '%',
|
||||||
|
height: this.selectionRect.h * 100 + '%'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeUpdate () {
|
beforeUpdate () {
|
||||||
@@ -211,28 +370,183 @@ export default {
|
|||||||
this.image.metadata.width = e.target.naturalWidth
|
this.image.metadata.width = e.target.naturalWidth
|
||||||
this.image.metadata.height = e.target.naturalHeight
|
this.image.metadata.height = e.target.naturalHeight
|
||||||
},
|
},
|
||||||
|
openContextMenu (event) {
|
||||||
|
if (!this.editable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
const rect = this.$refs.image.getBoundingClientRect()
|
||||||
|
|
||||||
|
this.newAreas = []
|
||||||
|
this.showMask = false
|
||||||
|
|
||||||
|
this.contextMenu = {
|
||||||
|
x: event.clientX,
|
||||||
|
y: event.clientY,
|
||||||
|
relativeX: (event.clientX - rect.left) / rect.width,
|
||||||
|
relativeY: (event.clientY - rect.top) / rect.height
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openAreaContextMenu (event, area, field) {
|
||||||
|
if (!this.editable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
const rect = this.$refs.image.getBoundingClientRect()
|
||||||
|
|
||||||
|
this.newAreas = []
|
||||||
|
this.showMask = false
|
||||||
|
|
||||||
|
this.contextMenu = {
|
||||||
|
x: event.clientX,
|
||||||
|
y: event.clientY,
|
||||||
|
relativeX: (event.clientX - rect.left) / rect.width,
|
||||||
|
relativeY: (event.clientY - rect.top) / rect.height,
|
||||||
|
area,
|
||||||
|
field
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openSelectionContextMenu (event) {
|
||||||
|
if (!this.editable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
const rect = this.$el.getBoundingClientRect()
|
||||||
|
|
||||||
|
this.contextMenu = {
|
||||||
|
x: event.clientX,
|
||||||
|
y: event.clientY,
|
||||||
|
relativeX: (event.clientX - rect.left) / rect.width,
|
||||||
|
relativeY: (event.clientY - rect.top) / rect.height,
|
||||||
|
areas: this.selectedAreasRef.value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleSelectionCopy () {
|
||||||
|
this.$emit('copy-selected-areas')
|
||||||
|
|
||||||
|
this.closeContextMenu()
|
||||||
|
},
|
||||||
|
handleSelectionDelete () {
|
||||||
|
this.$emit('delete-selected-areas')
|
||||||
|
|
||||||
|
this.closeContextMenu()
|
||||||
|
},
|
||||||
|
closeContextMenu () {
|
||||||
|
this.contextMenu = null
|
||||||
|
this.newAreas = []
|
||||||
|
this.showMask = false
|
||||||
|
},
|
||||||
|
handleCopy () {
|
||||||
|
if (this.contextMenu.area) {
|
||||||
|
this.selectedAreasRef.value = [this.contextMenu.area]
|
||||||
|
|
||||||
|
this.$emit('copy-field')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.closeContextMenu()
|
||||||
|
},
|
||||||
|
handleDelete () {
|
||||||
|
if (this.contextMenu.area) {
|
||||||
|
this.$emit('remove-area', this.contextMenu.area)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.closeContextMenu()
|
||||||
|
},
|
||||||
|
handlePaste () {
|
||||||
|
this.newAreas = []
|
||||||
|
this.showMask = false
|
||||||
|
|
||||||
|
this.$emit('paste-field', {
|
||||||
|
page: this.number,
|
||||||
|
x: this.contextMenu.relativeX,
|
||||||
|
y: this.contextMenu.relativeY
|
||||||
|
})
|
||||||
|
|
||||||
|
this.closeContextMenu()
|
||||||
|
},
|
||||||
|
handleAutodetectFields () {
|
||||||
|
this.$emit('autodetect-fields', {
|
||||||
|
page: this.number,
|
||||||
|
attachmentUuid: this.attachmentUuid
|
||||||
|
})
|
||||||
|
|
||||||
|
this.closeContextMenu()
|
||||||
|
},
|
||||||
setAreaRefs (el) {
|
setAreaRefs (el) {
|
||||||
if (el) {
|
if (el) {
|
||||||
this.areaRefs.push(el)
|
this.areaRefs.push(el)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDragenter (e) {
|
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, {
|
if (customAreas.length > 1) {
|
||||||
maskW: this.$refs.mask.clientWidth,
|
const refArea = customAreas[0]
|
||||||
maskH: this.$refs.mask.clientHeight
|
|
||||||
})
|
|
||||||
|
|
||||||
this.newArea.x = (e.offsetX - 6) / this.$refs.mask.clientWidth
|
this.newAreas = customAreas.map((customArea) => ({
|
||||||
this.newArea.y = e.offsetY / this.$refs.mask.clientHeight - this.newArea.h / 2
|
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) {
|
onDragover (e) {
|
||||||
this.newArea.x = (e.offsetX - 6) / this.$refs.mask.clientWidth
|
const customField = this.customDragFieldRef?.value
|
||||||
this.newArea.y = e.offsetY / this.$refs.mask.clientHeight - this.newArea.h / 2
|
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) {
|
onDrop (e) {
|
||||||
this.newArea = null
|
this.newAreas = []
|
||||||
|
|
||||||
this.$emit('drop-field', {
|
this.$emit('drop-field', {
|
||||||
x: e.offsetX,
|
x: e.offsetX,
|
||||||
@@ -243,11 +557,25 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
onStartDraw (e) {
|
onStartDraw (e) {
|
||||||
|
if (e.button === 2) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.selectedAreasRef.value.length >= 2) {
|
||||||
|
this.selectedAreasRef.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isSelectMode) {
|
||||||
|
this.startSelectionRect(e)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.allowDraw) {
|
if (!this.allowDraw) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isMobile && !this.drawField) {
|
if (this.isMobile && !this.drawField && !this.drawCustomField) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,65 +586,154 @@ export default {
|
|||||||
this.showMask = true
|
this.showMask = true
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.newArea = {
|
this.newAreas = [{
|
||||||
initialX: e.offsetX / this.$refs.mask.clientWidth,
|
initialX: e.offsetX / this.$refs.mask.clientWidth,
|
||||||
initialY: e.offsetY / this.$refs.mask.clientHeight,
|
initialY: e.offsetY / this.$refs.mask.clientHeight,
|
||||||
x: e.offsetX / this.$refs.mask.clientWidth,
|
x: e.offsetX / this.$refs.mask.clientWidth,
|
||||||
y: e.offsetY / this.$refs.mask.clientHeight,
|
y: e.offsetY / this.$refs.mask.clientHeight,
|
||||||
w: 0,
|
w: 0,
|
||||||
h: 0
|
h: 0
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
startSelectionRect (e) {
|
||||||
|
this.selectedAreasRef.value = []
|
||||||
|
|
||||||
|
this.showMask = true
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const x = e.offsetX / this.$refs.mask.clientWidth
|
||||||
|
const y = e.offsetY / this.$refs.mask.clientHeight
|
||||||
|
|
||||||
|
this.selectionRect = {
|
||||||
|
initialX: x,
|
||||||
|
initialY: y,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
w: 0,
|
||||||
|
h: 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
onSelectionBoxMove (dx, dy) {
|
||||||
|
let clampedDx = dx
|
||||||
|
let clampedDy = dy
|
||||||
|
|
||||||
|
this.pageSelectedAreas.forEach((area) => {
|
||||||
|
const maxDxLeft = -area.x
|
||||||
|
const maxDxRight = 1 - area.w - area.x
|
||||||
|
const maxDyTop = -area.y
|
||||||
|
const maxDyBottom = 1 - area.h - area.y
|
||||||
|
|
||||||
|
if (dx < maxDxLeft) clampedDx = Math.max(clampedDx, maxDxLeft)
|
||||||
|
if (dx > maxDxRight) clampedDx = Math.min(clampedDx, maxDxRight)
|
||||||
|
if (dy < maxDyTop) clampedDy = Math.max(clampedDy, maxDyTop)
|
||||||
|
if (dy > maxDyBottom) clampedDy = Math.min(clampedDy, maxDyBottom)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.pageSelectedAreas.forEach((area) => {
|
||||||
|
area.x += clampedDx
|
||||||
|
area.y += clampedDy
|
||||||
|
})
|
||||||
|
},
|
||||||
onPointermove (e) {
|
onPointermove (e) {
|
||||||
if (this.newArea) {
|
if (this.selectionRect) {
|
||||||
const dx = e.offsetX / this.$refs.mask.clientWidth - this.newArea.initialX
|
const dx = e.offsetX / this.$refs.mask.clientWidth - this.selectionRect.initialX
|
||||||
const dy = e.offsetY / this.$refs.mask.clientHeight - this.newArea.initialY
|
const dy = e.offsetY / this.$refs.mask.clientHeight - this.selectionRect.initialY
|
||||||
|
|
||||||
if (dx > 0) {
|
if (dx > 0) {
|
||||||
this.newArea.x = this.newArea.initialX
|
this.selectionRect.x = this.selectionRect.initialX
|
||||||
} else {
|
} else {
|
||||||
this.newArea.x = e.offsetX / this.$refs.mask.clientWidth
|
this.selectionRect.x = e.offsetX / this.$refs.mask.clientWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dy > 0) {
|
if (dy > 0) {
|
||||||
this.newArea.y = this.newArea.initialY
|
this.selectionRect.y = this.selectionRect.initialY
|
||||||
} else {
|
} else {
|
||||||
this.newArea.y = e.offsetY / this.$refs.mask.clientHeight
|
this.selectionRect.y = e.offsetY / this.$refs.mask.clientHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((this.drawField?.type || this.drawFieldType) === 'cells') {
|
this.selectionRect.w = Math.abs(dx)
|
||||||
this.newArea.cell_w = this.newArea.h * (this.$refs.mask.clientHeight / this.$refs.mask.clientWidth)
|
this.selectionRect.h = Math.abs(dy)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const drawArea = this.newAreas[0]
|
||||||
|
|
||||||
|
if (drawArea?.initialX !== undefined) {
|
||||||
|
const dx = e.offsetX / this.$refs.mask.clientWidth - drawArea.initialX
|
||||||
|
const dy = e.offsetY / this.$refs.mask.clientHeight - drawArea.initialY
|
||||||
|
|
||||||
|
if (dx > 0) {
|
||||||
|
drawArea.x = drawArea.initialX
|
||||||
|
} else {
|
||||||
|
drawArea.x = e.offsetX / this.$refs.mask.clientWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
this.newArea.w = Math.abs(dx)
|
if (dy > 0) {
|
||||||
this.newArea.h = Math.abs(dy)
|
drawArea.y = drawArea.initialY
|
||||||
|
} else {
|
||||||
|
drawArea.y = e.offsetY / this.$refs.mask.clientHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((this.drawField?.type || this.drawCustomField?.type || this.drawFieldType) === 'cells') {
|
||||||
|
drawArea.cell_w = drawArea.h * (this.$refs.mask.clientHeight / this.$refs.mask.clientWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
drawArea.w = Math.abs(dx)
|
||||||
|
drawArea.h = Math.abs(dy)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onPointerup (e) {
|
onPointerup (e) {
|
||||||
if (this.newArea) {
|
if (this.selectionRect) {
|
||||||
const area = {
|
const selRect = this.selectionRect
|
||||||
x: this.newArea.x,
|
const areasToSelect = this.areas || []
|
||||||
y: this.newArea.y,
|
|
||||||
w: this.newArea.w,
|
areasToSelect.forEach((item) => {
|
||||||
h: this.newArea.h,
|
const area = item.area
|
||||||
page: this.number
|
|
||||||
|
if (this.rectsOverlap(selRect, area)) {
|
||||||
|
this.selectedAreasRef.value.push(area)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.selectionRect = null
|
||||||
|
} else {
|
||||||
|
const drawArea = this.newAreas[0]
|
||||||
|
|
||||||
|
if (drawArea?.initialX !== undefined) {
|
||||||
|
const area = {
|
||||||
|
x: drawArea.x,
|
||||||
|
y: drawArea.y,
|
||||||
|
w: drawArea.w,
|
||||||
|
h: drawArea.h,
|
||||||
|
page: this.number
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('cell_w' in drawArea) {
|
||||||
|
area.cell_w = drawArea.cell_w
|
||||||
|
}
|
||||||
|
|
||||||
|
const dx = Math.abs(e.offsetX - this.$refs.mask.clientWidth * drawArea.initialX)
|
||||||
|
const dy = Math.abs(e.offsetY - this.$refs.mask.clientHeight * drawArea.initialY)
|
||||||
|
|
||||||
|
const isTooSmall = dx < 8 && dy < 8
|
||||||
|
|
||||||
|
this.$emit('draw', { area, isTooSmall })
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('cell_w' in this.newArea) {
|
|
||||||
area.cell_w = this.newArea.cell_w
|
|
||||||
}
|
|
||||||
|
|
||||||
const dx = Math.abs(e.offsetX - this.$refs.mask.clientWidth * this.newArea.initialX)
|
|
||||||
const dy = Math.abs(e.offsetY - this.$refs.mask.clientHeight * this.newArea.initialY)
|
|
||||||
|
|
||||||
const isTooSmall = dx < 8 && dy < 8
|
|
||||||
|
|
||||||
this.$emit('draw', { area, isTooSmall })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.showMask = false
|
this.showMask = false
|
||||||
this.newArea = null
|
this.newAreas = []
|
||||||
|
},
|
||||||
|
rectsOverlap (r1, r2) {
|
||||||
|
return !(
|
||||||
|
r1.x + r1.w < r2.x ||
|
||||||
|
r2.x + r2.w < r1.x ||
|
||||||
|
r1.y + r1.h < r2.y ||
|
||||||
|
r2.y + r2.h < r1.y
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="menu"
|
||||||
|
class="fixed z-50 p-1 bg-white shadow-lg rounded-lg border border-neutral-200 cursor-default"
|
||||||
|
style="min-width: 170px"
|
||||||
|
:style="menuStyle"
|
||||||
|
@mousedown.stop
|
||||||
|
@pointerdown.stop
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
|
||||||
|
:class="!hasClipboardData ? 'opacity-50 cursor-not-allowed' : 'hover:bg-neutral-100'"
|
||||||
|
:disabled="!hasClipboardData"
|
||||||
|
@click.stop="!hasClipboardData ? null : $emit('paste')"
|
||||||
|
>
|
||||||
|
<span class="flex items-center space-x-2">
|
||||||
|
<IconClipboard class="w-4 h-4" />
|
||||||
|
<span>{{ t('paste') }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-base-content/60 ml-4">{{ isMac ? '⌘V' : 'Ctrl+V' }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
|
||||||
|
@click.stop="handleToggleSelectMode"
|
||||||
|
>
|
||||||
|
<span class="flex items-center space-x-2">
|
||||||
|
<IconClick
|
||||||
|
v-if="!isSelectModeRef.value"
|
||||||
|
class="w-4 h-4"
|
||||||
|
/>
|
||||||
|
<IconNewSection
|
||||||
|
v-else
|
||||||
|
class="w-4 h-4"
|
||||||
|
/>
|
||||||
|
<span>{{ isSelectModeRef.value ? t('draw_fields') : t('select_fields') }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-base-content/60 ml-4">Tab</span>
|
||||||
|
</button>
|
||||||
|
<hr
|
||||||
|
v-if="showAutodetectFields"
|
||||||
|
class="my-1 border-neutral-200"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-if="showAutodetectFields"
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
|
@click.stop="$emit('autodetect-fields')"
|
||||||
|
>
|
||||||
|
<IconSparkles class="w-4 h-4" />
|
||||||
|
<span>{{ t('autodetect_fields') }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconClipboard, IconClick, IconNewSection, IconSparkles } from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PageContextMenu',
|
||||||
|
components: {
|
||||||
|
IconClipboard,
|
||||||
|
IconClick,
|
||||||
|
IconNewSection,
|
||||||
|
IconSparkles
|
||||||
|
},
|
||||||
|
inject: ['t', 'isSelectModeRef'],
|
||||||
|
props: {
|
||||||
|
contextMenu: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
withFieldsDetection: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['paste', 'close', 'autodetect-fields'],
|
||||||
|
computed: {
|
||||||
|
isMac () {
|
||||||
|
return (navigator.userAgentData?.platform || navigator.platform)?.toLowerCase()?.includes('mac')
|
||||||
|
},
|
||||||
|
hasClipboardData () {
|
||||||
|
try {
|
||||||
|
const clipboard = localStorage.getItem('docuseal_clipboard')
|
||||||
|
|
||||||
|
if (clipboard) {
|
||||||
|
const data = JSON.parse(clipboard)
|
||||||
|
|
||||||
|
return Date.now() - data.timestamp < 3600000
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
menuStyle () {
|
||||||
|
return {
|
||||||
|
left: this.contextMenu.x + 'px',
|
||||||
|
top: this.contextMenu.y + 'px'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showAutodetectFields () {
|
||||||
|
return this.withFieldsDetection && this.editable
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
document.addEventListener('keydown', this.onKeyDown)
|
||||||
|
document.addEventListener('mousedown', this.handleClickOutside)
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.checkMenuPosition()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
beforeUnmount () {
|
||||||
|
document.removeEventListener('keydown', this.onKeyDown)
|
||||||
|
document.removeEventListener('mousedown', this.handleClickOutside)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
checkMenuPosition () {
|
||||||
|
if (this.$refs.menu) {
|
||||||
|
const rect = this.$refs.menu.getBoundingClientRect()
|
||||||
|
const overflow = rect.bottom - window.innerHeight
|
||||||
|
|
||||||
|
if (overflow > 0) {
|
||||||
|
this.contextMenu.y = this.contextMenu.y - overflow - 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onKeyDown (event) {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
this.$emit('close')
|
||||||
|
} else if ((event.ctrlKey || event.metaKey) && event.key === 'v' && this.hasClipboardData) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
this.$emit('paste')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleClickOutside (event) {
|
||||||
|
if (this.$refs.menu && !this.$refs.menu.contains(event.target)) {
|
||||||
|
this.$emit('close')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleToggleSelectMode () {
|
||||||
|
this.isSelectModeRef.value = !this.isSelectModeRef.value
|
||||||
|
|
||||||
|
this.$emit('close')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<span
|
<span
|
||||||
class="dropdown dropdown-end field-settings-dropdown"
|
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
|
<label
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -231,7 +231,10 @@
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li class="mt-1">
|
<li
|
||||||
|
v-if="withCondition"
|
||||||
|
class="mt-1"
|
||||||
|
>
|
||||||
<label
|
<label
|
||||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||||
@click="$emit('click-condition')"
|
@click="$emit('click-condition')"
|
||||||
@@ -244,12 +247,29 @@
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<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'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
const isConnected = ref(false)
|
const isConnected = ref(false)
|
||||||
@@ -261,6 +281,7 @@ export default {
|
|||||||
IconCircleCheck,
|
IconCircleCheck,
|
||||||
IconRouteAltLeft,
|
IconRouteAltLeft,
|
||||||
IconInfoCircle,
|
IconInfoCircle,
|
||||||
|
IconForms,
|
||||||
IconMathFunction,
|
IconMathFunction,
|
||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
IconBrandStripe
|
IconBrandStripe
|
||||||
@@ -270,9 +291,24 @@ export default {
|
|||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
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 () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isLoading: false
|
isLoading: false
|
||||||
|
|||||||
@@ -119,6 +119,7 @@
|
|||||||
<ConditionsModal
|
<ConditionsModal
|
||||||
:item="item"
|
:item="item"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
|
@save="$emit('change')"
|
||||||
@close="isShowConditionsModal = false"
|
@close="isShowConditionsModal = false"
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
@@ -145,7 +146,7 @@ export default {
|
|||||||
GoogleDriveDocumentSettings,
|
GoogleDriveDocumentSettings,
|
||||||
IconSortDescending2
|
IconSortDescending2
|
||||||
},
|
},
|
||||||
inject: ['t'],
|
inject: ['t', 'getFieldTypeIndex'],
|
||||||
props: {
|
props: {
|
||||||
item: {
|
item: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="absolute outline-dashed outline-gray-400 cursor-move"
|
||||||
|
:class="[isResizing || isCmdKeyRef.value ? 'z-0' : 'z-20', { 'pointer-events-none': isDrawing || fieldsDragFieldRef.value || isDrag }]"
|
||||||
|
:style="positionStyle"
|
||||||
|
@pointerdown.stop.prevent="onPointerDown"
|
||||||
|
@contextmenu.stop.prevent="openContextMenu"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'SelectionBox',
|
||||||
|
inject: ['save', 'selectedAreasRef', 'isCmdKeyRef', 'fieldsDragFieldRef'],
|
||||||
|
props: {
|
||||||
|
selectionBox: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
pageWidth: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
pageHeight: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isResizing: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
isDrag: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
isDrawing: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['move', 'contextmenu', 'close-context-menu'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isDragging: false,
|
||||||
|
dragStart: { x: 0, y: 0 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
positionStyle () {
|
||||||
|
const { x, y, w, h } = this.selectionBox
|
||||||
|
|
||||||
|
return {
|
||||||
|
top: y * 100 + '%',
|
||||||
|
left: x * 100 + '%',
|
||||||
|
width: w * 100 + '%',
|
||||||
|
height: h * 100 + '%'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
openContextMenu (e) {
|
||||||
|
this.$emit('contextmenu', e)
|
||||||
|
},
|
||||||
|
onPointerDown (e) {
|
||||||
|
this.$emit('close-context-menu')
|
||||||
|
|
||||||
|
this.startDrag(e)
|
||||||
|
},
|
||||||
|
startDrag (e) {
|
||||||
|
this.isDragging = true
|
||||||
|
this.dragStart = { x: e.clientX, y: e.clientY }
|
||||||
|
|
||||||
|
document.addEventListener('pointermove', this.onDrag)
|
||||||
|
document.addEventListener('pointerup', this.stopDrag)
|
||||||
|
},
|
||||||
|
onDrag (e) {
|
||||||
|
if (!this.isDragging) return
|
||||||
|
|
||||||
|
const parent = this.$el.parentElement
|
||||||
|
|
||||||
|
const rect = parent.getBoundingClientRect()
|
||||||
|
|
||||||
|
const dx = (e.clientX - this.dragStart.x) / rect.width
|
||||||
|
const dy = (e.clientY - this.dragStart.y) / rect.height
|
||||||
|
|
||||||
|
this.$emit('move', dx, dy)
|
||||||
|
|
||||||
|
this.dragStart = { x: e.clientX, y: e.clientY }
|
||||||
|
},
|
||||||
|
stopDrag () {
|
||||||
|
this.isDragging = false
|
||||||
|
|
||||||
|
document.removeEventListener('pointermove', this.onDrag)
|
||||||
|
document.removeEventListener('pointerup', this.stopDrag)
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
v-if="!isShowFontModal && !isShowConditionsModal"
|
||||||
|
ref="menu"
|
||||||
|
class="fixed z-50 p-1 bg-white shadow-lg rounded-lg border border-neutral-200 cursor-default"
|
||||||
|
style="min-width: 170px"
|
||||||
|
:style="menuStyle"
|
||||||
|
@mousedown.stop
|
||||||
|
@pointerdown.stop
|
||||||
|
>
|
||||||
|
<ContextSubmenu
|
||||||
|
:icon="IconLayoutAlignMiddle"
|
||||||
|
:label="t('align')"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
|
@click.stop="alignSelectedAreas('left')"
|
||||||
|
>
|
||||||
|
<IconLayoutAlignLeft class="w-4 h-4" />
|
||||||
|
<span>{{ t('align_left') }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
|
@click.stop="alignSelectedAreas('right')"
|
||||||
|
>
|
||||||
|
<IconLayoutAlignRight class="w-4 h-4" />
|
||||||
|
<span>{{ t('align_right') }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
|
@click.stop="alignSelectedAreas('top')"
|
||||||
|
>
|
||||||
|
<IconLayoutAlignTop class="w-4 h-4" />
|
||||||
|
<span>{{ t('align_top') }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
|
@click.stop="alignSelectedAreas('bottom')"
|
||||||
|
>
|
||||||
|
<IconLayoutAlignBottom class="w-4 h-4" />
|
||||||
|
<span>{{ t('align_bottom') }}</span>
|
||||||
|
</button>
|
||||||
|
</ContextSubmenu>
|
||||||
|
<ContextSubmenu
|
||||||
|
:icon="IconAspectRatio"
|
||||||
|
:label="t('resize')"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
|
@click.stop="resizeSelectedAreas('width')"
|
||||||
|
>
|
||||||
|
<IconArrowsHorizontal class="w-4 h-4" />
|
||||||
|
<span>{{ t('width') }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
|
@click.stop="resizeSelectedAreas('height')"
|
||||||
|
>
|
||||||
|
<IconArrowsVertical class="w-4 h-4" />
|
||||||
|
<span>{{ t('height') }}</span>
|
||||||
|
</button>
|
||||||
|
</ContextSubmenu>
|
||||||
|
<hr
|
||||||
|
v-if="showFont || showCondition"
|
||||||
|
class="my-1 border-neutral-200"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-if="showFont"
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
|
@click.stop="openFontModal"
|
||||||
|
>
|
||||||
|
<IconTypography class="w-4 h-4" />
|
||||||
|
<span>{{ t('font') }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="showCondition"
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
|
@click.stop="openConditionModal"
|
||||||
|
>
|
||||||
|
<IconRouteAltLeft class="w-4 h-4" />
|
||||||
|
<span>{{ t('condition') }}</span>
|
||||||
|
</button>
|
||||||
|
<hr class="my-1 border-neutral-200">
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
|
||||||
|
@click.stop="$emit('copy')"
|
||||||
|
>
|
||||||
|
<span class="flex items-center space-x-2">
|
||||||
|
<IconCopy class="w-4 h-4" />
|
||||||
|
<span>{{ t('copy') }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-base-content/60 ml-4">{{ isMac ? '⌘C' : 'Ctrl+C' }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm text-red-600"
|
||||||
|
@click.stop="$emit('delete')"
|
||||||
|
>
|
||||||
|
<span class="flex items-center space-x-2">
|
||||||
|
<IconTrashX class="w-4 h-4" />
|
||||||
|
<span>{{ t('remove') }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-base-content/60 ml-4">Del</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Teleport
|
||||||
|
v-if="isShowFontModal"
|
||||||
|
:to="modalContainerEl"
|
||||||
|
>
|
||||||
|
<FontModal
|
||||||
|
:field="multiSelectField"
|
||||||
|
:area="contextMenu.area"
|
||||||
|
:editable="editable"
|
||||||
|
:build-default-name="buildDefaultName"
|
||||||
|
@save="handleSaveMultiSelectFontModal"
|
||||||
|
@close="closeModal"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
<Teleport
|
||||||
|
v-if="isShowConditionsModal"
|
||||||
|
:to="modalContainerEl"
|
||||||
|
>
|
||||||
|
<ConditionsModal
|
||||||
|
:item="multiSelectField"
|
||||||
|
:build-default-name="buildDefaultName"
|
||||||
|
:exclude-field-uuids="selectedFields.map(f => f.uuid)"
|
||||||
|
@save="handleSaveMultiSelectConditionsModal"
|
||||||
|
@close="closeModal"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconCopy, IconTrashX, IconTypography, IconRouteAltLeft, IconLayoutAlignLeft, IconLayoutAlignRight, IconLayoutAlignTop, IconLayoutAlignBottom, IconLayoutAlignMiddle, IconAspectRatio, IconArrowsHorizontal, IconArrowsVertical } from '@tabler/icons-vue'
|
||||||
|
import FontModal from './font_modal'
|
||||||
|
import ConditionsModal from './conditions_modal'
|
||||||
|
import ContextSubmenu from './field_context_submenu'
|
||||||
|
import Field from './field'
|
||||||
|
import FieldType from './field_type'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SelectionContextMenu',
|
||||||
|
components: {
|
||||||
|
IconCopy,
|
||||||
|
IconTrashX,
|
||||||
|
IconTypography,
|
||||||
|
IconRouteAltLeft,
|
||||||
|
IconLayoutAlignLeft,
|
||||||
|
IconLayoutAlignRight,
|
||||||
|
IconLayoutAlignTop,
|
||||||
|
IconLayoutAlignBottom,
|
||||||
|
FontModal,
|
||||||
|
IconArrowsHorizontal,
|
||||||
|
IconArrowsVertical,
|
||||||
|
ConditionsModal,
|
||||||
|
ContextSubmenu
|
||||||
|
},
|
||||||
|
inject: ['t', 'save', 'selectedAreasRef', 'getFieldTypeIndex'],
|
||||||
|
props: {
|
||||||
|
contextMenu: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
template: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
withCondition: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['copy', 'delete', 'close'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isShowFontModal: false,
|
||||||
|
isShowConditionsModal: false,
|
||||||
|
multiSelectField: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
modalContainerEl () {
|
||||||
|
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
||||||
|
},
|
||||||
|
selectedFields () {
|
||||||
|
return this.selectedAreasRef.value.map((area) => {
|
||||||
|
return this.template.fields.find((f) => f.areas?.includes(area))
|
||||||
|
}).filter(Boolean)
|
||||||
|
},
|
||||||
|
isMac () {
|
||||||
|
return (navigator.userAgentData?.platform || navigator.platform)?.toLowerCase()?.includes('mac')
|
||||||
|
},
|
||||||
|
menuStyle () {
|
||||||
|
return {
|
||||||
|
left: this.contextMenu.x + 'px',
|
||||||
|
top: this.contextMenu.y + 'px'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showFont () {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
showCondition () {
|
||||||
|
return this.withCondition
|
||||||
|
},
|
||||||
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
|
fieldLabels: FieldType.computed.fieldLabels
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
document.addEventListener('keydown', this.onKeyDown)
|
||||||
|
document.addEventListener('mousedown', this.handleClickOutside)
|
||||||
|
|
||||||
|
this.$nextTick(() => this.checkMenuPosition())
|
||||||
|
},
|
||||||
|
beforeUnmount () {
|
||||||
|
document.removeEventListener('keydown', this.onKeyDown)
|
||||||
|
document.removeEventListener('mousedown', this.handleClickOutside)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
IconLayoutAlignMiddle,
|
||||||
|
IconAspectRatio,
|
||||||
|
buildDefaultName: Field.methods.buildDefaultName,
|
||||||
|
checkMenuPosition () {
|
||||||
|
if (this.$refs.menu) {
|
||||||
|
const rect = this.$refs.menu.getBoundingClientRect()
|
||||||
|
const overflow = rect.bottom - window.innerHeight
|
||||||
|
|
||||||
|
if (overflow > 0) {
|
||||||
|
this.contextMenu.y = this.contextMenu.y - overflow - 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onKeyDown (event) {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
this.$emit('close')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleClickOutside (event) {
|
||||||
|
if (this.$refs.menu && !this.$refs.menu.contains(event.target)) {
|
||||||
|
this.$emit('close')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openFontModal () {
|
||||||
|
this.multiSelectField = {
|
||||||
|
name: this.t('fields_selected').replace('{count}', this.selectedFields.length),
|
||||||
|
preferences: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const preferencesStrings = this.selectedFields.map((f) => JSON.stringify(f.preferences || {}))
|
||||||
|
|
||||||
|
if (preferencesStrings.every((s) => s === preferencesStrings[0])) {
|
||||||
|
this.multiSelectField.preferences = JSON.parse(preferencesStrings[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isShowFontModal = true
|
||||||
|
},
|
||||||
|
openConditionModal () {
|
||||||
|
this.multiSelectField = {
|
||||||
|
name: this.t('fields_selected').replace('{count}', this.selectedFields.length),
|
||||||
|
conditions: []
|
||||||
|
}
|
||||||
|
|
||||||
|
const conditionStrings = this.selectedFields.map((f) => JSON.stringify(f.conditions || []))
|
||||||
|
|
||||||
|
if (conditionStrings.every((s) => s === conditionStrings[0])) {
|
||||||
|
this.multiSelectField.conditions = JSON.parse(conditionStrings[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isShowConditionsModal = true
|
||||||
|
},
|
||||||
|
closeModal () {
|
||||||
|
this.isShowFontModal = false
|
||||||
|
this.isShowConditionsModal = false
|
||||||
|
this.multiSelectField = null
|
||||||
|
|
||||||
|
this.$emit('close')
|
||||||
|
},
|
||||||
|
alignSelectedAreas (direction) {
|
||||||
|
const areas = this.selectedAreasRef.value
|
||||||
|
|
||||||
|
let targetValue
|
||||||
|
|
||||||
|
if (direction === 'left') {
|
||||||
|
targetValue = Math.min(...areas.map(a => a.x))
|
||||||
|
areas.forEach((area) => { area.x = targetValue })
|
||||||
|
} else if (direction === 'right') {
|
||||||
|
targetValue = Math.max(...areas.map(a => a.x + a.w))
|
||||||
|
areas.forEach((area) => { area.x = targetValue - area.w })
|
||||||
|
} else if (direction === 'top') {
|
||||||
|
targetValue = Math.min(...areas.map(a => a.y))
|
||||||
|
areas.forEach((area) => { area.y = targetValue })
|
||||||
|
} else if (direction === 'bottom') {
|
||||||
|
targetValue = Math.max(...areas.map(a => a.y + a.h))
|
||||||
|
areas.forEach((area) => { area.y = targetValue - area.h })
|
||||||
|
}
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
|
||||||
|
this.$emit('close')
|
||||||
|
},
|
||||||
|
resizeSelectedAreas (dimension) {
|
||||||
|
const areas = this.selectedAreasRef.value
|
||||||
|
|
||||||
|
const values = areas.map(a => dimension === 'width' ? a.w : a.h).sort((a, b) => a - b)
|
||||||
|
const medianValue = values[Math.floor(values.length / 2)]
|
||||||
|
|
||||||
|
if (dimension === 'width') {
|
||||||
|
areas.forEach((area) => { area.w = medianValue })
|
||||||
|
} else if (dimension === 'height') {
|
||||||
|
areas.forEach((area) => {
|
||||||
|
const diff = medianValue - area.h
|
||||||
|
area.y = area.y - diff
|
||||||
|
area.h = medianValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
|
||||||
|
this.$emit('close')
|
||||||
|
},
|
||||||
|
handleSaveMultiSelectFontModal () {
|
||||||
|
this.selectedFields.forEach((field) => {
|
||||||
|
field.preferences = { ...field.preferences, ...this.multiSelectField.preferences }
|
||||||
|
})
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
|
||||||
|
this.closeModal()
|
||||||
|
},
|
||||||
|
handleSaveMultiSelectConditionsModal () {
|
||||||
|
this.selectedFields.forEach((field) => {
|
||||||
|
field.conditions = JSON.parse(JSON.stringify(this.multiSelectField.conditions))
|
||||||
|
})
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
|
||||||
|
this.closeModal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -216,6 +216,9 @@ export default {
|
|||||||
queryParams () {
|
queryParams () {
|
||||||
return new URLSearchParams(window.location.search)
|
return new URLSearchParams(window.location.search)
|
||||||
},
|
},
|
||||||
|
uploadUrl () {
|
||||||
|
return `/templates/${this.templateId}/documents`
|
||||||
|
},
|
||||||
googleDriveOauthPath () {
|
googleDriveOauthPath () {
|
||||||
const params = {
|
const params = {
|
||||||
access_type: 'offline',
|
access_type: 'offline',
|
||||||
@@ -279,7 +282,7 @@ export default {
|
|||||||
async upload ({ path } = {}) {
|
async upload ({ path } = {}) {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
|
|
||||||
return this.baseFetch(path || `/templates/${this.templateId}/documents`, {
|
return this.baseFetch(path || this.uploadUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { Accept: 'application/json' },
|
headers: { Accept: 'application/json' },
|
||||||
body: new FormData(this.$refs.form)
|
body: new FormData(this.$refs.form)
|
||||||
@@ -287,7 +290,11 @@ export default {
|
|||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
resp.json().then((data) => {
|
resp.json().then((data) => {
|
||||||
this.$emit('success', data)
|
this.$emit('success', data)
|
||||||
this.$refs.input.value = ''
|
|
||||||
|
if (this.$refs.input) {
|
||||||
|
this.$refs.input.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
})
|
})
|
||||||
} else if (resp.status === 422) {
|
} else if (resp.status === 422) {
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ class ProcessSubmissionExpiredJob
|
|||||||
include Sidekiq::Job
|
include Sidekiq::Job
|
||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submission = Submission.find(params['submission_id'])
|
submission = Submission.find_by(id: params['submission_id'])
|
||||||
|
|
||||||
|
return unless submission
|
||||||
|
|
||||||
return if submission.archived_at?
|
return if submission.archived_at?
|
||||||
return if submission.template&.archived_at?
|
return if submission.template&.archived_at?
|
||||||
|
|||||||
@@ -39,18 +39,26 @@ class ProcessSubmitterCompletionJob
|
|||||||
submission = submitter.submission
|
submission = submitter.submission
|
||||||
|
|
||||||
complete_verification_events, sms_events =
|
complete_verification_events, sms_events =
|
||||||
submitter.submission_events.where(event_type: %i[send_sms send_2fa_sms complete_verification])
|
submitter.submission_events.where(event_type: %i[send_sms send_2fa_sms complete_verification complete_kba])
|
||||||
.partition { |e| e.event_type == 'complete_verification' }
|
.partition { |e| e.event_type == 'complete_verification' || e.event_type == 'complete_kba' }
|
||||||
|
|
||||||
complete_verification_event = complete_verification_events.first
|
complete_verification_event = complete_verification_events.first
|
||||||
|
|
||||||
|
verification_method =
|
||||||
|
if complete_verification_event&.event_type == 'complete_kba'
|
||||||
|
'kba'
|
||||||
|
elsif complete_verification_event
|
||||||
|
complete_verification_event.data['method']
|
||||||
|
end
|
||||||
|
|
||||||
completed_submitter.assign_attributes(
|
completed_submitter.assign_attributes(
|
||||||
submission_id: submitter.submission_id,
|
submission_id: submitter.submission_id,
|
||||||
account_id: submission.account_id,
|
account_id: submission.account_id,
|
||||||
|
is_first: !CompletedSubmitter.exists?(submission: submitter.submission_id, is_first: true),
|
||||||
template_id: submission.template_id,
|
template_id: submission.template_id,
|
||||||
source: submission.source,
|
source: submission.source,
|
||||||
sms_count: sms_events.sum { |e| e.data['segments'] || 1 },
|
sms_count: sms_events.sum { |e| e.data['segments'] || 1 },
|
||||||
verification_method: complete_verification_event&.data&.dig('method'),
|
verification_method:,
|
||||||
completed_at: submitter.completed_at
|
completed_at: submitter.completed_at
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -156,11 +164,13 @@ class ProcessSubmitterCompletionJob
|
|||||||
next_submitter_items =
|
next_submitter_items =
|
||||||
if submission.template_submitters.any? { |s| s['order'] }
|
if submission.template_submitters.any? { |s| s['order'] }
|
||||||
submitter_groups =
|
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'))
|
submitters_index.values_at(*submitter_groups[current_group_index].pluck('uuid'))
|
||||||
.compact.all?(&:completed_at?)
|
.compact.all?(&:completed_at?)
|
||||||
submitter_groups[current_group_index + 1]
|
submitter_groups[current_group_index + 1]
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SendConfirmationInstructionsJob
|
||||||
|
include Sidekiq::Job
|
||||||
|
|
||||||
|
def perform(params = {})
|
||||||
|
user = User.find(params['user_id'])
|
||||||
|
|
||||||
|
user.send_confirmation_instructions
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -8,8 +8,13 @@ class SendFormCompletedWebhookRequestJob
|
|||||||
MAX_ATTEMPTS = 12
|
MAX_ATTEMPTS = 12
|
||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submitter = Submitter.find(params['submitter_id'])
|
submitter = Submitter.find_by(id: params['submitter_id'])
|
||||||
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
|
||||||
|
return unless submitter
|
||||||
|
|
||||||
|
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
|
||||||
|
|
||||||
|
return unless webhook_url
|
||||||
|
|
||||||
attempt = params['attempt'].to_i
|
attempt = params['attempt'].to_i
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,13 @@ class SendFormDeclinedWebhookRequestJob
|
|||||||
MAX_ATTEMPTS = 10
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submitter = Submitter.find(params['submitter_id'])
|
submitter = Submitter.find_by(id: params['submitter_id'])
|
||||||
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
|
||||||
|
return unless submitter
|
||||||
|
|
||||||
|
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
|
||||||
|
|
||||||
|
return unless webhook_url
|
||||||
|
|
||||||
attempt = params['attempt'].to_i
|
attempt = params['attempt'].to_i
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,13 @@ class SendFormStartedWebhookRequestJob
|
|||||||
MAX_ATTEMPTS = 10
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submitter = Submitter.find(params['submitter_id'])
|
submitter = Submitter.find_by(id: params['submitter_id'])
|
||||||
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
|
||||||
|
return unless submitter
|
||||||
|
|
||||||
|
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
|
||||||
|
|
||||||
|
return unless webhook_url
|
||||||
|
|
||||||
attempt = params['attempt'].to_i
|
attempt = params['attempt'].to_i
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,13 @@ class SendFormViewedWebhookRequestJob
|
|||||||
MAX_ATTEMPTS = 10
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submitter = Submitter.find(params['submitter_id'])
|
submitter = Submitter.find_by(id: params['submitter_id'])
|
||||||
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
|
||||||
|
return unless submitter
|
||||||
|
|
||||||
|
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
|
||||||
|
|
||||||
|
return unless webhook_url
|
||||||
|
|
||||||
attempt = params['attempt'].to_i
|
attempt = params['attempt'].to_i
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,13 @@ class SendSubmissionArchivedWebhookRequestJob
|
|||||||
MAX_ATTEMPTS = 10
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submission = Submission.find(params['submission_id'])
|
submission = Submission.find_by(id: params['submission_id'])
|
||||||
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
|
||||||
|
return unless submission
|
||||||
|
|
||||||
|
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
|
||||||
|
|
||||||
|
return unless webhook_url
|
||||||
|
|
||||||
attempt = params['attempt'].to_i
|
attempt = params['attempt'].to_i
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,13 @@ class SendSubmissionCompletedWebhookRequestJob
|
|||||||
MAX_ATTEMPTS = 10
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submission = Submission.find(params['submission_id'])
|
submission = Submission.find_by(id: params['submission_id'])
|
||||||
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
|
||||||
|
return unless submission
|
||||||
|
|
||||||
|
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
|
||||||
|
|
||||||
|
return unless webhook_url
|
||||||
|
|
||||||
attempt = params['attempt'].to_i
|
attempt = params['attempt'].to_i
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,13 @@ class SendSubmissionCreatedWebhookRequestJob
|
|||||||
MAX_ATTEMPTS = 10
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submission = Submission.find(params['submission_id'])
|
submission = Submission.find_by(id: params['submission_id'])
|
||||||
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
|
||||||
|
return unless submission
|
||||||
|
|
||||||
|
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
|
||||||
|
|
||||||
|
return unless webhook_url
|
||||||
|
|
||||||
attempt = params['attempt'].to_i
|
attempt = params['attempt'].to_i
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,13 @@ class SendSubmissionExpiredWebhookRequestJob
|
|||||||
MAX_ATTEMPTS = 10
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submission = Submission.find(params['submission_id'])
|
submission = Submission.find_by(id: params['submission_id'])
|
||||||
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
|
||||||
|
return unless submission
|
||||||
|
|
||||||
|
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
|
||||||
|
|
||||||
|
return unless webhook_url
|
||||||
|
|
||||||
attempt = params['attempt'].to_i
|
attempt = params['attempt'].to_i
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ class SendSubmitterInvitationEmailJob
|
|||||||
submitter = Submitter.find(params['submitter_id'])
|
submitter = Submitter.find(params['submitter_id'])
|
||||||
|
|
||||||
return if submitter.completed_at?
|
return if submitter.completed_at?
|
||||||
|
return if submitter.submission.archived_at?
|
||||||
|
return if submitter.template&.archived_at?
|
||||||
return if submitter.submission.source == 'invite' && !Accounts.can_send_emails?(submitter.account, on_events: true)
|
return if submitter.submission.source == 'invite' && !Accounts.can_send_emails?(submitter.account, on_events: true)
|
||||||
|
|
||||||
unless Accounts.can_send_invitation_emails?(submitter.account)
|
unless Accounts.can_send_invitation_emails?(submitter.account)
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SendSubmitterVerificationEmailJob
|
||||||
|
include Sidekiq::Job
|
||||||
|
|
||||||
|
def perform(params = {})
|
||||||
|
submitter = Submitter.find(params['submitter_id'])
|
||||||
|
|
||||||
|
locale = params['locale'].presence || submitter.account.locale
|
||||||
|
|
||||||
|
SubmitterMailer.otp_verification_email(submitter, locale:).deliver_now!
|
||||||
|
|
||||||
|
SubmissionEvent.create!(submitter_id: params['submitter_id'],
|
||||||
|
event_type: 'send_2fa_email',
|
||||||
|
data: { email: submitter.email })
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -8,8 +8,13 @@ class SendTemplateCreatedWebhookRequestJob
|
|||||||
MAX_ATTEMPTS = 10
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
template = Template.find(params['template_id'])
|
template = Template.find_by(id: params['template_id'])
|
||||||
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
|
||||||
|
return unless template
|
||||||
|
|
||||||
|
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
|
||||||
|
|
||||||
|
return unless webhook_url
|
||||||
|
|
||||||
attempt = params['attempt'].to_i
|
attempt = params['attempt'].to_i
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,13 @@ class SendTemplateUpdatedWebhookRequestJob
|
|||||||
MAX_ATTEMPTS = 10
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
template = Template.find(params['template_id'])
|
template = Template.find_by(id: params['template_id'])
|
||||||
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
|
||||||
|
return unless template
|
||||||
|
|
||||||
|
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
|
||||||
|
|
||||||
|
return unless webhook_url
|
||||||
|
|
||||||
attempt = params['attempt'].to_i
|
attempt = params['attempt'].to_i
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,28 @@ class SendTestWebhookRequestJob
|
|||||||
|
|
||||||
USER_AGENT = 'DocuSeal.com Webhook'
|
USER_AGENT = 'DocuSeal.com Webhook'
|
||||||
|
|
||||||
def perform(params = {})
|
HttpsError = Class.new(StandardError)
|
||||||
submitter = Submitter.find(params['submitter_id'])
|
LocalhostError = Class.new(StandardError)
|
||||||
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
|
||||||
|
|
||||||
return unless webhook_url && submitter
|
def perform(params = {})
|
||||||
|
submitter = Submitter.find_by(id: params['submitter_id'])
|
||||||
|
|
||||||
|
return unless submitter
|
||||||
|
|
||||||
|
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
|
||||||
|
|
||||||
|
return unless webhook_url
|
||||||
|
|
||||||
|
if Docuseal.multitenant?
|
||||||
|
uri = begin
|
||||||
|
URI(webhook_url.url)
|
||||||
|
rescue URI::Error
|
||||||
|
Addressable::URI.parse(webhook_url.url).normalize
|
||||||
|
end
|
||||||
|
|
||||||
|
raise HttpsError, 'Only HTTPS is allowed.' if uri.scheme != 'https'
|
||||||
|
raise LocalhostError, "Can't send to localhost." if uri.host.in?(SendWebhookRequest::LOCALHOSTS)
|
||||||
|
end
|
||||||
|
|
||||||
Faraday.post(webhook_url.url,
|
Faraday.post(webhook_url.url,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -144,6 +144,17 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def otp_verification_email(submitter, locale: nil)
|
||||||
|
@submitter = submitter
|
||||||
|
@otp_code = EmailVerificationCodes.generate([submitter.email.downcase.strip, submitter.slug].join(':'))
|
||||||
|
|
||||||
|
assign_message_metadata('otp_verification_email', submitter)
|
||||||
|
|
||||||
|
I18n.with_locale(locale || submitter.account.locale) do
|
||||||
|
mail(to: submitter.email, subject: I18n.t('email_verification'))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def build_submitter_reply_to(submitter, email_config: nil, documents_copy_email: nil)
|
def build_submitter_reply_to(submitter, email_config: nil, documents_copy_email: nil)
|
||||||
|
|||||||
@@ -60,6 +60,10 @@ class Account < ApplicationRecord
|
|||||||
linked_account_account&.testing?
|
linked_account_account&.testing?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def tz_info
|
||||||
|
@tz_info ||= TZInfo::Timezone.get(ActiveSupport::TimeZone::MAPPING[timezone] || timezone)
|
||||||
|
end
|
||||||
|
|
||||||
def default_template_folder
|
def default_template_folder
|
||||||
super || build_default_template_folder(name: TemplateFolder::DEFAULT_NAME,
|
super || build_default_template_folder(name: TemplateFolder::DEFAULT_NAME,
|
||||||
author_id: users.minimum(:id)).tap(&:save!)
|
author_id: users.minimum(:id)).tap(&:save!)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#
|
#
|
||||||
class AccountConfig < ApplicationRecord
|
class AccountConfig < ApplicationRecord
|
||||||
SUBMITTER_INVITATION_EMAIL_KEY = 'submitter_invitation_email'
|
SUBMITTER_INVITATION_EMAIL_KEY = 'submitter_invitation_email'
|
||||||
|
SUBMITTER_INVITATION_REMINDER_EMAIL_KEY = 'submitter_invitation_reminder_email'
|
||||||
SUBMITTER_COMPLETED_EMAIL_KEY = 'submitter_completed_email'
|
SUBMITTER_COMPLETED_EMAIL_KEY = 'submitter_completed_email'
|
||||||
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY = 'submitter_documents_copy_email'
|
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY = 'submitter_documents_copy_email'
|
||||||
BCC_EMAILS = 'bcc_emails'
|
BCC_EMAILS = 'bcc_emails'
|
||||||
@@ -44,12 +45,16 @@ class AccountConfig < ApplicationRecord
|
|||||||
WITH_SIGNATURE_ID = 'with_signature_id'
|
WITH_SIGNATURE_ID = 'with_signature_id'
|
||||||
WITH_FILE_LINKS_KEY = 'with_file_links'
|
WITH_FILE_LINKS_KEY = 'with_file_links'
|
||||||
WITH_SIGNATURE_ID_REASON_KEY = 'with_signature_id_reason'
|
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_VALUES_KEY = 'with_audit_values'
|
||||||
|
WITH_AUDIT_SENDER_KEY = 'with_audit_sender'
|
||||||
WITH_SUBMITTER_TIMEZONE_KEY = 'with_submitter_timezone'
|
WITH_SUBMITTER_TIMEZONE_KEY = 'with_submitter_timezone'
|
||||||
REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason'
|
REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason'
|
||||||
REUSE_SIGNATURE_KEY = 'reuse_signature'
|
REUSE_SIGNATURE_KEY = 'reuse_signature'
|
||||||
|
WITH_FIELD_LABELS_KEY = 'with_field_labels'
|
||||||
COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key'
|
COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key'
|
||||||
DOCUMENT_FILENAME_FORMAT_KEY = 'document_filename_format'
|
DOCUMENT_FILENAME_FORMAT_KEY = 'document_filename_format'
|
||||||
|
TEMPLATE_CUSTOM_FIELDS_KEY = 'template_custom_fields'
|
||||||
POLICY_LINKS_KEY = 'policy_links'
|
POLICY_LINKS_KEY = 'policy_links'
|
||||||
|
|
||||||
DEFAULT_VALUES = {
|
DEFAULT_VALUES = {
|
||||||
@@ -59,6 +64,12 @@ class AccountConfig < ApplicationRecord
|
|||||||
'body' => I18n.t(:submitter_invitation_email_sign_body)
|
'body' => I18n.t(:submitter_invitation_email_sign_body)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => lambda {
|
||||||
|
{
|
||||||
|
'subject' => I18n.t(:you_are_invited_to_sign_a_document),
|
||||||
|
'body' => I18n.t(:submitter_invitation_email_sign_body)
|
||||||
|
}
|
||||||
|
},
|
||||||
SUBMITTER_COMPLETED_EMAIL_KEY => lambda {
|
SUBMITTER_COMPLETED_EMAIL_KEY => lambda {
|
||||||
{
|
{
|
||||||
'subject' => I18n.t(:template_name_has_been_completed_by_submitters),
|
'subject' => I18n.t(:template_name_has_been_completed_by_submitters),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#
|
#
|
||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
# completed_at :datetime not null
|
# completed_at :datetime not null
|
||||||
|
# is_first :boolean
|
||||||
# sms_count :integer not null
|
# sms_count :integer not null
|
||||||
# source :string not null
|
# source :string not null
|
||||||
# verification_method :string
|
# verification_method :string
|
||||||
@@ -18,8 +19,10 @@
|
|||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
# index_completed_submitters_on_account_id (account_id)
|
# index_completed_submitters_account_id_completed_at_is_first (account_id,completed_at) WHERE (is_first = true)
|
||||||
# index_completed_submitters_on_submitter_id (submitter_id) UNIQUE
|
# index_completed_submitters_on_account_id_and_completed_at (account_id,completed_at)
|
||||||
|
# index_completed_submitters_on_submission_id (submission_id) UNIQUE WHERE (is_first = true)
|
||||||
|
# index_completed_submitters_on_submitter_id (submitter_id) UNIQUE
|
||||||
#
|
#
|
||||||
class CompletedSubmitter < ApplicationRecord
|
class CompletedSubmitter < ApplicationRecord
|
||||||
belongs_to :submitter
|
belongs_to :submitter
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ class Submission < ApplicationRecord
|
|||||||
|
|
||||||
has_one_attached :audit_trail
|
has_one_attached :audit_trail
|
||||||
has_one_attached :combined_document
|
has_one_attached :combined_document
|
||||||
|
has_one_attached :merged_document
|
||||||
|
has_one_attached :preview_merged_document
|
||||||
|
|
||||||
has_many_attached :preview_documents
|
has_many_attached :preview_documents
|
||||||
has_many_attached :documents
|
has_many_attached :documents
|
||||||
@@ -76,8 +78,9 @@ class Submission < ApplicationRecord
|
|||||||
scope :active, -> { where(archived_at: nil) }
|
scope :active, -> { where(archived_at: nil) }
|
||||||
scope :archived, -> { where.not(archived_at: nil) }
|
scope :archived, -> { where.not(archived_at: nil) }
|
||||||
scope :pending, lambda {
|
scope :pending, lambda {
|
||||||
where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
|
where(expire_at: nil).or(where(expire_at: Time.current..))
|
||||||
.and(Submitter.arel_table[:completed_at].eq(nil))).select(1).arel.exists)
|
.where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
|
||||||
|
.and(Submitter.arel_table[:completed_at].eq(nil))).select(1).arel.exists)
|
||||||
}
|
}
|
||||||
scope :completed, lambda {
|
scope :completed, lambda {
|
||||||
where.not(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
|
where.not(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
|
||||||
|
|||||||
Loaded 100 of 242 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user