mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 14:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c4cbd86f7 | ||
|
|
eaedaa96bb | ||
|
|
bd7ad5fce8 | ||
|
|
759dac1b70 | ||
|
|
38577c6235 | ||
|
|
b24fff0f55 | ||
|
|
9e0efeb092 | ||
|
|
41152f90fc | ||
|
|
4486f8040e | ||
|
|
a98caad968 | ||
|
|
ffb75238b4 | ||
|
|
cba3e74f3a | ||
|
|
8d3f0b5b98 | ||
|
|
a2598b30f7 | ||
|
|
8d11f5ddae | ||
|
|
6ffc88b4ed | ||
|
|
640b451727 | ||
|
|
db6e2eb7ca | ||
|
|
407816b576 | ||
|
|
486cc493e6 | ||
|
|
3eb0b8a89f | ||
|
|
c23eca0ade | ||
|
|
67de153016 | ||
|
|
ba2da69a67 | ||
|
|
111bef14e1 | ||
|
|
22d4c1bd10 | ||
|
|
e3dfb0ae83 | ||
|
|
8d7c3f0e26 | ||
|
|
b11775ebda | ||
|
|
608191183a | ||
|
|
2ba903e170 | ||
|
|
342b64d502 | ||
|
|
1a8f048aaa | ||
|
|
7309581b75 | ||
|
|
36d59c40a7 | ||
|
|
ed3c7c324b | ||
|
|
fd868360a3 | ||
|
|
8dc2ebfbe0 | ||
|
|
997711b34a | ||
|
|
ac01b7129d | ||
|
|
f4b7528c0d | ||
|
|
d7b9ae5df5 | ||
|
|
4db29126e9 | ||
|
|
4cfee2fe45 | ||
|
|
db5dbd6ac2 | ||
|
|
44db775b89 | ||
|
|
8e2e780a1a | ||
|
|
7aba2eb4da | ||
|
|
3e250bfb28 | ||
|
|
9c02ed66f5 | ||
|
|
f5ae8c60d3 | ||
|
|
e90ac391b2 | ||
|
|
0e5f949bcf | ||
|
|
b7c270bef8 | ||
|
|
5f16b506f5 | ||
|
|
176148fc71 | ||
|
|
861daaedf4 | ||
|
|
d8430f4e63 | ||
|
|
dd44864485 | ||
|
|
b3bb29f383 | ||
|
|
b4d137c0de | ||
|
|
62a969d8fe | ||
|
|
961f09e092 | ||
|
|
b2dfa83f18 | ||
|
|
37196ff89f | ||
|
|
b50d982497 | ||
|
|
4a484aca69 | ||
|
|
0bf37d571a | ||
|
|
d4e94ac2bc | ||
|
|
75bc789bb1 | ||
|
|
bc231d5bf1 | ||
|
|
ab9008b3be | ||
|
|
65491936c3 | ||
|
|
e589998840 | ||
|
|
0afba8d11e | ||
|
|
f622ed168f | ||
|
|
5a31df40d8 | ||
|
|
db3b80c96c | ||
|
|
bd28a761d0 | ||
|
|
4f2830a34d | ||
|
|
a5716cd518 | ||
|
|
d035525c7a | ||
|
|
13fa87c449 | ||
|
|
246ccfb049 | ||
|
|
cbffb1e5b8 | ||
|
|
c4b007411d | ||
|
|
04264fe937 | ||
|
|
0c37ad12b7 | ||
|
|
8c5298ed2e | ||
|
|
262118e047 | ||
|
|
e5b63ea2b0 | ||
|
|
746757d402 | ||
|
|
e6e640328b | ||
|
|
845782a69c | ||
|
|
fc6baa1b3b | ||
|
|
47822ecc15 | ||
|
|
3d0c7f1118 | ||
|
|
e9c8e4d325 | ||
|
|
39cc82ce0c | ||
|
|
451f83421a | ||
|
|
5073f2eefb | ||
|
|
231fff5508 | ||
|
|
f1d146eca3 | ||
|
|
b6635fcc4f | ||
|
|
fb5e13ee4c | ||
|
|
ba84741a64 | ||
|
|
65c275ac17 | ||
|
|
825322d489 | ||
|
|
739e2abdf8 | ||
|
|
1d2394e31e | ||
|
|
1b41af798d | ||
|
|
118f4a231b | ||
|
|
e48652f425 | ||
|
|
848f01edf8 | ||
|
|
43fbc42770 | ||
|
|
2c736a0eed | ||
|
|
33ca930055 | ||
|
|
3b396f8421 | ||
|
|
aa77e3a8d3 | ||
|
|
75316d8d87 | ||
|
|
a999109a5c | ||
|
|
bdd33c7d6b | ||
|
|
61c5ee22a0 | ||
|
|
12c5b909e0 | ||
|
|
871ef6dda6 | ||
|
|
40052a2d7c | ||
|
|
347be0137d | ||
|
|
fe6baba8bf | ||
|
|
ed8c313bd4 | ||
|
|
ca0acb34d6 | ||
|
|
34ea639c25 | ||
|
|
680ab9dbed | ||
|
|
9377766e52 | ||
|
|
7a2c37454e | ||
|
|
8f6f418f54 | ||
|
|
9b7745c565 | ||
|
|
a389e8cc38 | ||
|
|
2056301f70 | ||
|
|
93babe3660 | ||
|
|
4aef2d8eb0 | ||
|
|
c5ec2ed230 | ||
|
|
a156d5f6cc | ||
|
|
f84674cddd | ||
|
|
b7b62c197a | ||
|
|
f5350eac36 | ||
|
|
ee6228db15 | ||
|
|
3e434c0878 | ||
|
|
7c5372a8e2 | ||
|
|
6ab68cc36b | ||
|
|
ee2400a5c5 | ||
|
|
80be5d2cf5 | ||
|
|
76cf44a31b | ||
|
|
eb63113227 | ||
|
|
b7045fb3d9 | ||
|
|
11a5b5842d | ||
|
|
362994afd5 | ||
|
|
d7682f4ebd | ||
|
|
56d4fe7fa9 | ||
|
|
93a852ac14 | ||
|
|
964db0ab08 | ||
|
|
8547295da7 | ||
|
|
c75050fa46 | ||
|
|
65372c52aa | ||
|
|
12c767b7d3 | ||
|
|
2fc5e9cfb3 | ||
|
|
bccc78ee65 | ||
|
|
c0793f3ce7 | ||
|
|
a8dd8699a3 | ||
|
|
91bb25e8b6 | ||
|
|
c3f4480cdc | ||
|
|
33e226861e | ||
|
|
4f5acbec9d | ||
|
|
d2c9bbf6ab | ||
|
|
b5e57e1f2f | ||
|
|
c82c8b2272 | ||
|
|
43f1b598a7 | ||
|
|
5081e387d0 | ||
|
|
1e5f55c1d6 | ||
|
|
aa38773be3 | ||
|
|
96530bd809 | ||
|
|
127f95a9d0 | ||
|
|
cf31e12348 | ||
|
|
4adaa531b7 | ||
|
|
c7659563b8 | ||
|
|
7b5494468f | ||
|
|
d12c67fd54 | ||
|
|
e20eacb5ee | ||
|
|
98055f0762 | ||
|
|
a46c42c3a0 | ||
|
|
c3d1503361 | ||
|
|
65b47d27cd | ||
|
|
968c07f8be | ||
|
|
91814ca105 | ||
|
|
9128f24270 | ||
|
|
264897bbd5 | ||
|
|
67d59ff1ba | ||
|
|
81eee56e1e | ||
|
|
92ea8af786 | ||
|
|
df0616f77e | ||
|
|
a337dff85c | ||
|
|
48e349c1d2 | ||
|
|
df9fa0fec7 | ||
|
|
b3e72f0726 | ||
|
|
700ebc4727 | ||
|
|
745a18efca | ||
|
|
b4a8147ff0 | ||
|
|
0543c25054 | ||
|
|
b1a0afea66 | ||
|
|
43c41e2557 | ||
|
|
bc8eb33f05 | ||
|
|
d1cebc92f7 | ||
|
|
bf02cf4058 | ||
|
|
5212773d0a | ||
|
|
bf53204a41 | ||
|
|
092a19966c | ||
|
|
6060a2b798 | ||
|
|
ce12af68b2 | ||
|
|
c05f8d47aa | ||
|
|
f8be19c20e | ||
|
|
2fe32a2145 | ||
|
|
794ea4d0e8 | ||
|
|
307b0f5afd | ||
|
|
9011e3f4b4 | ||
|
|
0b45ce5c0c | ||
|
|
a8ab93a4cc | ||
|
|
d1768be636 | ||
|
|
b2cd7c82a7 | ||
|
|
b88d99afe0 | ||
|
|
4523d38551 | ||
|
|
c4ba892559 | ||
|
|
40f8093e00 | ||
|
|
4375459856 | ||
|
|
b3eb8a70bf | ||
|
|
6245a487b6 | ||
|
|
a89f4c1b10 | ||
|
|
7df729b8b5 | ||
|
|
f1db7c7e82 |
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:
|
||||||
|
|||||||
+28
-3
@@ -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:
|
||||||
@@ -48,6 +48,18 @@ Style/MultipleComparison:
|
|||||||
Style/NumericPredicate:
|
Style/NumericPredicate:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Style/MinMaxComparison:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/EmptyClassDefinition:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/OneClassPerFile:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Layout/MultilineMethodCallIndentation:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
Naming/PredicateMethod:
|
Naming/PredicateMethod:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
@@ -81,7 +93,7 @@ RSpec/AnyInstance:
|
|||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Metrics/BlockNesting:
|
Metrics/BlockNesting:
|
||||||
Max: 5
|
Max: 6
|
||||||
|
|
||||||
Rails/I18nLocaleTexts:
|
Rails/I18nLocaleTexts:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
@@ -97,3 +109,16 @@ Rails/ApplicationController:
|
|||||||
|
|
||||||
Rails/Output:
|
Rails/Output:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/StrongParametersExpect:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/RedirectBackOrTo:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/UnknownEnv:
|
||||||
|
Environments:
|
||||||
|
- development
|
||||||
|
- test
|
||||||
|
- production
|
||||||
|
- local
|
||||||
+34
-34
@@ -1,22 +1,19 @@
|
|||||||
FROM ruby:3.4.2-alpine AS download
|
FROM ruby:4.0.1-alpine AS download
|
||||||
|
|
||||||
WORKDIR /fonts
|
WORKDIR /fonts
|
||||||
|
|
||||||
RUN apk --no-cache add fontforge wget && \
|
RUN apk --no-cache add wget && \
|
||||||
wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && \
|
wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && \
|
||||||
wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && \
|
wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && \
|
||||||
wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && \
|
wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && \
|
||||||
wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && \
|
wget https://raw.githubusercontent.com/impallari/DancingScript/master/OFL.txt && \
|
||||||
wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && \
|
wget https://raw.githubusercontent.com/notofonts/noto-fonts/refs/heads/main/LICENSE && \
|
||||||
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 /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/bblanchon/pdfium-binaries/releases/latest/download/pdfium-linux-musl-$(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")'
|
FROM ruby:4.0.1-alpine AS webpack
|
||||||
|
|
||||||
FROM ruby:3.4.2-alpine AS webpack
|
|
||||||
|
|
||||||
ENV RAILS_ENV=production
|
ENV RAILS_ENV=production
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
@@ -36,22 +33,24 @@ COPY ./config/shakapacker.yml ./config/shakapacker.yml
|
|||||||
COPY ./postcss.config.js ./postcss.config.js
|
COPY ./postcss.config.js ./postcss.config.js
|
||||||
COPY ./tailwind.config.js ./tailwind.config.js
|
COPY ./tailwind.config.js ./tailwind.config.js
|
||||||
COPY ./tailwind.form.config.js ./tailwind.form.config.js
|
COPY ./tailwind.form.config.js ./tailwind.form.config.js
|
||||||
|
COPY ./tailwind.dynamic.config.js ./tailwind.dynamic.config.js
|
||||||
COPY ./tailwind.application.config.js ./tailwind.application.config.js
|
COPY ./tailwind.application.config.js ./tailwind.application.config.js
|
||||||
COPY ./app/javascript ./app/javascript
|
COPY ./app/javascript ./app/javascript
|
||||||
COPY ./app/views ./app/views
|
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 OPENSSL_CONF=/etc/openssl_legacy.cnf
|
||||||
ENV OPENSSL_CONF=/app/openssl_legacy.cnf
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev yaml-dev redis libheif vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
RUN apk add --no-cache libpq vips redis vips-heif fontconfig onnxruntime
|
||||||
|
|
||||||
|
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\
|
||||||
@@ -63,37 +62,38 @@ 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 && ln -sf /usr/lib/libonnxruntime.so.1 $(ruby -e "print Dir[Gem::Specification.find_by_name('onnxruntime').gem_dir + '/vendor/*.so'].first")
|
RUN apk add --no-cache build-base git libpq-dev yaml-dev && bundle install && apk del --no-cache build-base git libpq-dev yaml-dev && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales,resources/unicode_data/properties}'] + Dir['/usr/local/bundle/gems/*/{test,tests,examples,sample,misc,doc,docs}'] + Dir['/usr/local/bundle/gems/*/ext/**/*.{c,h,o,S}']" | xargs rm -rf && ln -sf /usr/lib/libonnxruntime.so.1 $(ruby -e "print Dir[Gem::Specification.find_by_name('onnxruntime').gem_dir + '/vendor/*.so'].first")
|
||||||
|
|
||||||
RUN echo 'https://dl-cdn.alpinelinux.org/alpine/edge/main' >> /etc/apk/repositories && echo 'https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache onnxruntime
|
COPY --chown=docuseal:docuseal ./bin ./bin
|
||||||
|
COPY --chown=docuseal:docuseal ./app ./app
|
||||||
|
COPY --chown=docuseal:docuseal ./config ./config
|
||||||
|
COPY --chown=docuseal:docuseal ./db/migrate ./db/migrate
|
||||||
|
COPY --chown=docuseal:docuseal ./log ./log
|
||||||
|
COPY --chown=docuseal:docuseal ./lib ./lib
|
||||||
|
COPY --chown=docuseal:docuseal ./public ./public
|
||||||
|
COPY --chown=docuseal:docuseal ./tmp ./tmp
|
||||||
|
COPY --chown=docuseal:docuseal LICENSE LICENSE_ADDITIONAL_TERMS README.md Rakefile config.ru .version ./
|
||||||
|
COPY --chown=docuseal:docuseal .version ./public/version
|
||||||
|
|
||||||
COPY ./bin ./bin
|
COPY --chown=docuseal:docuseal --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts/LICENSE /fonts/
|
||||||
COPY ./app ./app
|
|
||||||
COPY ./config ./config
|
|
||||||
COPY ./db/migrate ./db/migrate
|
|
||||||
COPY ./log ./log
|
|
||||||
COPY ./lib ./lib
|
|
||||||
COPY ./public ./public
|
|
||||||
COPY ./tmp ./tmp
|
|
||||||
COPY LICENSE README.md Rakefile config.ru .version ./
|
|
||||||
COPY .version ./public/version
|
|
||||||
|
|
||||||
COPY --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
|
||||||
COPY --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=download /model.onnx /app/tmp/model.onnx
|
COPY --chown=docuseal:docuseal --from=download /model.onnx /app/tmp/model.onnx
|
||||||
COPY --from=webpack /app/public/packs ./public/packs
|
COPY --chown=docuseal:docuseal --from=webpack /app/public/packs ./public/packs
|
||||||
|
|
||||||
RUN ln -s /fonts /app/public/fonts
|
RUN mkdir -p /app/public/fonts && ln -s /fonts/DancingScript-Regular.otf /app/public/fonts/ && \
|
||||||
RUN bundle exec bootsnap precompile -j 1 --gemfile app/ lib/
|
mkdir -p /usr/share/fonts/noto && ln -s /fonts/GoNotoKurrent-Regular.ttf /usr/share/fonts/noto/ && ln -s /fonts/GoNotoKurrent-Bold.ttf /usr/share/fonts/noto/ && fc-cache -f && \
|
||||||
|
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
|
||||||
|
ENV VIPS_MAX_COORD=17000
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["/app/bin/bundle", "exec", "puma", "-C", "/app/config/puma.rb", "--dir", "/app"]
|
CMD ["/app/bin/bundle", "exec", "puma", "-C", "/app/config/puma.rb", "--dir", "/app"]
|
||||||
@@ -2,16 +2,17 @@
|
|||||||
|
|
||||||
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 'addressable'
|
||||||
|
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,30 +22,28 @@ 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 'numo-narray'
|
|
||||||
gem 'oj'
|
gem 'oj'
|
||||||
gem 'onnxruntime'
|
gem 'onnxruntime', require: false
|
||||||
gem 'pagy'
|
gem 'pagy'
|
||||||
gem 'pg', require: false
|
gem 'pg', require: false
|
||||||
gem 'premailer-rails'
|
|
||||||
gem 'pretender'
|
gem 'pretender'
|
||||||
gem 'puma', require: false
|
gem 'puma', require: false
|
||||||
gem 'rack'
|
gem 'rack'
|
||||||
gem 'rails'
|
gem 'rails'
|
||||||
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', require: false
|
||||||
gem 'turbo-rails'
|
gem 'turbo-rails'
|
||||||
gem 'twitter_cldr', require: false
|
gem 'twitter_cldr', require: false
|
||||||
gem 'tzinfo-data'
|
gem 'tzinfo-data'
|
||||||
|
|||||||
+294
-317
@@ -1,29 +1,31 @@
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (8.0.2.1)
|
action_text-trix (2.1.18)
|
||||||
actionpack (= 8.0.2.1)
|
railties
|
||||||
activesupport (= 8.0.2.1)
|
actioncable (8.1.3)
|
||||||
|
actionpack (= 8.1.3)
|
||||||
|
activesupport (= 8.1.3)
|
||||||
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.3)
|
||||||
actionpack (= 8.0.2.1)
|
actionpack (= 8.1.3)
|
||||||
activejob (= 8.0.2.1)
|
activejob (= 8.1.3)
|
||||||
activerecord (= 8.0.2.1)
|
activerecord (= 8.1.3)
|
||||||
activestorage (= 8.0.2.1)
|
activestorage (= 8.1.3)
|
||||||
activesupport (= 8.0.2.1)
|
activesupport (= 8.1.3)
|
||||||
mail (>= 2.8.0)
|
mail (>= 2.8.0)
|
||||||
actionmailer (8.0.2.1)
|
actionmailer (8.1.3)
|
||||||
actionpack (= 8.0.2.1)
|
actionpack (= 8.1.3)
|
||||||
actionview (= 8.0.2.1)
|
actionview (= 8.1.3)
|
||||||
activejob (= 8.0.2.1)
|
activejob (= 8.1.3)
|
||||||
activesupport (= 8.0.2.1)
|
activesupport (= 8.1.3)
|
||||||
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.3)
|
||||||
actionview (= 8.0.2.1)
|
actionview (= 8.1.3)
|
||||||
activesupport (= 8.0.2.1)
|
activesupport (= 8.1.3)
|
||||||
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,55 +33,58 @@ 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.3)
|
||||||
actionpack (= 8.0.2.1)
|
action_text-trix (~> 2.1.15)
|
||||||
activerecord (= 8.0.2.1)
|
actionpack (= 8.1.3)
|
||||||
activestorage (= 8.0.2.1)
|
activerecord (= 8.1.3)
|
||||||
activesupport (= 8.0.2.1)
|
activestorage (= 8.1.3)
|
||||||
|
activesupport (= 8.1.3)
|
||||||
globalid (>= 0.6.0)
|
globalid (>= 0.6.0)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (8.0.2.1)
|
actionview (8.1.3)
|
||||||
activesupport (= 8.0.2.1)
|
activesupport (= 8.1.3)
|
||||||
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.3)
|
||||||
activesupport (= 8.0.2.1)
|
activesupport (= 8.1.3)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (8.0.2.1)
|
activemodel (8.1.3)
|
||||||
activesupport (= 8.0.2.1)
|
activesupport (= 8.1.3)
|
||||||
activerecord (8.0.2.1)
|
activerecord (8.1.3)
|
||||||
activemodel (= 8.0.2.1)
|
activemodel (= 8.1.3)
|
||||||
activesupport (= 8.0.2.1)
|
activesupport (= 8.1.3)
|
||||||
timeout (>= 0.4.0)
|
timeout (>= 0.4.0)
|
||||||
activestorage (8.0.2.1)
|
activestorage (8.1.3)
|
||||||
actionpack (= 8.0.2.1)
|
actionpack (= 8.1.3)
|
||||||
activejob (= 8.0.2.1)
|
activejob (= 8.1.3)
|
||||||
activerecord (= 8.0.2.1)
|
activerecord (= 8.1.3)
|
||||||
activesupport (= 8.0.2.1)
|
activesupport (= 8.1.3)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
activesupport (8.0.2.1)
|
activesupport (8.1.3)
|
||||||
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.9)
|
||||||
public_suffix (>= 2.0.2, < 7.0)
|
public_suffix (>= 2.0.2, < 8.0)
|
||||||
annotaterb (4.14.0)
|
annotaterb (4.22.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.4.0)
|
aws-eventstream (1.4.0)
|
||||||
aws-partitions (1.1197.0)
|
aws-partitions (1.1233.0)
|
||||||
aws-sdk-core (3.240.0)
|
aws-sdk-core (3.244.0)
|
||||||
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)
|
||||||
@@ -87,44 +92,38 @@ GEM
|
|||||||
bigdecimal
|
bigdecimal
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
logger
|
logger
|
||||||
aws-sdk-kms (1.118.0)
|
aws-sdk-kms (1.123.0)
|
||||||
aws-sdk-core (~> 3, >= 3.239.1)
|
aws-sdk-core (~> 3, >= 3.244.0)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sdk-s3 (1.208.0)
|
aws-sdk-s3 (1.218.0)
|
||||||
aws-sdk-core (~> 3, >= 3.234.0)
|
aws-sdk-core (~> 3, >= 3.244.0)
|
||||||
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.129.0)
|
||||||
aws-sdk-core (~> 3, >= 3.210.0)
|
aws-sdk-core (~> 3, >= 3.244.0)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sigv4 (1.12.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.22)
|
||||||
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.1.0)
|
||||||
bindex (0.8.1)
|
bindex (0.8.1)
|
||||||
bootsnap (1.18.4)
|
bootsnap (1.23.0)
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
brakeman (7.0.0)
|
brakeman (8.0.4)
|
||||||
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)
|
||||||
@@ -138,51 +137,50 @@ 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)
|
csv (3.3.5)
|
||||||
addressable
|
|
||||||
csv (3.3.2)
|
|
||||||
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)
|
||||||
devise (4.9.4)
|
devise (5.0.3)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
orm_adapter (~> 0.1)
|
orm_adapter (~> 0.1)
|
||||||
railties (>= 4.1.0)
|
railties (>= 7.0)
|
||||||
responders
|
responders
|
||||||
warden (~> 1.2.3)
|
warden (~> 1.2.3)
|
||||||
devise-two-factor (6.1.0)
|
devise-two-factor (6.4.0)
|
||||||
activesupport (>= 7.0, < 8.1)
|
activesupport (>= 7.2, < 8.2)
|
||||||
devise (~> 4.0)
|
devise (>= 4.0, < 6.0)
|
||||||
railties (>= 7.0, < 8.1)
|
railties (>= 7.2, < 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.2)
|
||||||
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)
|
||||||
@@ -190,117 +188,100 @@ 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.1)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
faraday (1.10.4)
|
faraday (2.14.1)
|
||||||
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.2)
|
||||||
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.4-aarch64-linux-gnu)
|
||||||
ffi (1.17.1-aarch64-linux-gnu)
|
ffi (1.17.4-aarch64-linux-musl)
|
||||||
ffi (1.17.1-aarch64-linux-musl)
|
ffi (1.17.4-arm64-darwin)
|
||||||
ffi (1.17.1-arm64-darwin)
|
ffi (1.17.4-x86_64-linux-gnu)
|
||||||
ffi (1.17.1-x86_64-linux-gnu)
|
ffi (1.17.4-x86_64-linux-musl)
|
||||||
ffi (1.17.1-x86_64-linux-musl)
|
foreman (0.90.0)
|
||||||
foreman (0.88.1)
|
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.61.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.6.0)
|
||||||
google-cloud-storage (1.54.0)
|
google-cloud-storage (1.59.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.2)
|
||||||
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.6.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)
|
i18n (1.14.8)
|
||||||
httpclient (2.8.3)
|
|
||||||
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.17.0)
|
||||||
pp (>= 0.6.0)
|
pp (>= 0.6.0)
|
||||||
|
prism (>= 1.3.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.19.3)
|
||||||
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)
|
||||||
@@ -315,29 +296,29 @@ GEM
|
|||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
railties (>= 4)
|
railties (>= 4)
|
||||||
request_store (~> 1.0)
|
request_store (~> 1.0)
|
||||||
loofah (2.24.1)
|
loofah (2.25.1)
|
||||||
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.3)
|
||||||
minitest (5.25.5)
|
drb (~> 2.0)
|
||||||
msgpack (1.7.5)
|
prism (~> 1.5)
|
||||||
multi_json (1.15.0)
|
msgpack (1.8.0)
|
||||||
multipart-post (2.4.1)
|
multi_json (1.19.1)
|
||||||
mutex_m (0.3.0)
|
net-http (0.9.1)
|
||||||
mysql2 (0.5.6)
|
uri (>= 0.11.1)
|
||||||
net-http-persistent (4.0.5)
|
net-imap (0.6.3)
|
||||||
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)
|
||||||
@@ -346,70 +327,65 @@ 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.2-aarch64-linux-gnu)
|
||||||
mini_portile2 (~> 2.8.2)
|
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.9-aarch64-linux-gnu)
|
nokogiri (1.19.2-aarch64-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.9-aarch64-linux-musl)
|
nokogiri (1.19.2-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.9-arm64-darwin)
|
nokogiri (1.19.2-x86_64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.9-x86_64-linux-gnu)
|
nokogiri (1.19.2-x86_64-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.9-x86_64-linux-musl)
|
numo-narray-alt (0.10.3)
|
||||||
racc (~> 1.4)
|
oj (3.16.16)
|
||||||
numo-narray (0.9.2.1)
|
|
||||||
oj (3.16.11)
|
|
||||||
bigdecimal (>= 3.0)
|
bigdecimal (>= 3.0)
|
||||||
ostruct (>= 0.2)
|
ostruct (>= 0.2)
|
||||||
onnxruntime (0.10.1)
|
|
||||||
ffi
|
|
||||||
onnxruntime (0.10.1-aarch64-linux)
|
onnxruntime (0.10.1-aarch64-linux)
|
||||||
ffi
|
ffi
|
||||||
onnxruntime (0.10.1-arm64-darwin)
|
onnxruntime (0.10.1-arm64-darwin)
|
||||||
ffi
|
ffi
|
||||||
onnxruntime (0.10.1-x86_64-linux)
|
onnxruntime (0.10.1-x86_64-linux)
|
||||||
ffi
|
ffi
|
||||||
openssl (3.3.0)
|
openssl (4.0.1)
|
||||||
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.4.4)
|
||||||
parallel (1.27.0)
|
json
|
||||||
parser (3.3.9.0)
|
uri
|
||||||
|
yaml
|
||||||
|
parallel (1.28.0)
|
||||||
|
parser (3.3.11.1)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
pg (1.5.9)
|
pg (1.6.3-aarch64-linux)
|
||||||
pp (0.6.2)
|
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)
|
pretender (1.0.0)
|
||||||
addressable
|
actionpack (>= 7.2)
|
||||||
css_parser (>= 1.19.0)
|
|
||||||
htmlentities (>= 4.0.0)
|
|
||||||
premailer-rails (1.12.0)
|
|
||||||
actionmailer (>= 3)
|
|
||||||
net-smtp
|
|
||||||
premailer (~> 1.7, >= 1.7.9)
|
|
||||||
pretender (0.5.0)
|
|
||||||
actionpack (>= 6.1)
|
|
||||||
prettyprint (0.2.0)
|
prettyprint (0.2.0)
|
||||||
prism (1.5.1)
|
prism (1.9.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.5)
|
||||||
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.6)
|
||||||
rack-proxy (0.7.7)
|
rack-proxy (0.7.7)
|
||||||
rack
|
rack
|
||||||
rack-session (2.1.1)
|
rack-session (2.1.1)
|
||||||
@@ -417,53 +393,51 @@ 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.3)
|
||||||
actioncable (= 8.0.2.1)
|
actioncable (= 8.1.3)
|
||||||
actionmailbox (= 8.0.2.1)
|
actionmailbox (= 8.1.3)
|
||||||
actionmailer (= 8.0.2.1)
|
actionmailer (= 8.1.3)
|
||||||
actionpack (= 8.0.2.1)
|
actionpack (= 8.1.3)
|
||||||
actiontext (= 8.0.2.1)
|
actiontext (= 8.1.3)
|
||||||
actionview (= 8.0.2.1)
|
actionview (= 8.1.3)
|
||||||
activejob (= 8.0.2.1)
|
activejob (= 8.1.3)
|
||||||
activemodel (= 8.0.2.1)
|
activemodel (= 8.1.3)
|
||||||
activerecord (= 8.0.2.1)
|
activerecord (= 8.1.3)
|
||||||
activestorage (= 8.0.2.1)
|
activestorage (= 8.1.3)
|
||||||
activesupport (= 8.0.2.1)
|
activesupport (= 8.1.3)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 8.0.2.1)
|
railties (= 8.1.3)
|
||||||
rails-dom-testing (2.3.0)
|
rails-dom-testing (2.3.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
minitest
|
minitest
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
rails-html-sanitizer (1.6.2)
|
rails-html-sanitizer (1.7.0)
|
||||||
loofah (~> 2.21)
|
loofah (~> 2.25)
|
||||||
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)
|
railties (8.1.3)
|
||||||
actionview (> 3.1)
|
actionpack (= 8.1.3)
|
||||||
activesupport (> 3.1)
|
activesupport (= 8.1.3)
|
||||||
railties (> 3.1)
|
|
||||||
railties (8.0.2.1)
|
|
||||||
actionpack (= 8.0.2.1)
|
|
||||||
activesupport (= 8.0.2.1)
|
|
||||||
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.2.0)
|
||||||
erb
|
erb
|
||||||
psych (>= 4.0.0)
|
psych (>= 4.0.0)
|
||||||
redis-client (0.23.0)
|
tsort
|
||||||
|
redis-client (0.28.0)
|
||||||
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)
|
||||||
@@ -471,35 +445,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.4.1)
|
||||||
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.8)
|
||||||
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.4)
|
||||||
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.0, < 5.0.0)
|
||||||
rspec-expectations (~> 3.13)
|
rspec-expectations (>= 3.13.0, < 5.0.0)
|
||||||
rspec-mocks (~> 3.13)
|
rspec-mocks (>= 3.13.0, < 5.0.0)
|
||||||
rspec-support (~> 3.13)
|
rspec-support (>= 3.13.0, < 5.0.0)
|
||||||
rspec-support (3.13.2)
|
rspec-support (3.13.7)
|
||||||
rubocop (1.81.1)
|
rubocop (1.86.0)
|
||||||
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)
|
||||||
@@ -507,122 +481,127 @@ 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.49.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.1)
|
||||||
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.1)
|
||||||
shakapacker (8.0.2)
|
shakapacker (9.7.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.2)
|
||||||
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.2-aarch64-linux-gnu)
|
||||||
mini_portile2 (~> 2.8.0)
|
sqlite3 (2.9.2-aarch64-linux-musl)
|
||||||
sqlite3 (2.5.0-aarch64-linux-gnu)
|
sqlite3 (2.9.2-arm64-darwin)
|
||||||
sqlite3 (2.5.0-aarch64-linux-musl)
|
sqlite3 (2.9.2-x86_64-linux-gnu)
|
||||||
sqlite3 (2.5.0-arm64-darwin)
|
sqlite3 (2.9.2-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.8)
|
||||||
thor (1.4.0)
|
thor (1.5.0)
|
||||||
timeout (0.4.3)
|
timeout (0.6.1)
|
||||||
trailblazer-option (0.1.2)
|
trailblazer-option (0.1.2)
|
||||||
turbo-rails (2.0.11)
|
trilogy (2.12.2)
|
||||||
actionpack (>= 6.0.0)
|
bigdecimal
|
||||||
railties (>= 6.0.0)
|
tsort (0.2.0)
|
||||||
twitter_cldr (6.12.1)
|
turbo-rails (2.0.23)
|
||||||
|
actionpack (>= 7.1.0)
|
||||||
|
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.2026.1)
|
||||||
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.1.1)
|
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)
|
||||||
web-console (4.2.1)
|
web-console (4.3.0)
|
||||||
actionview (>= 6.0.0)
|
actionview (>= 8.0.0)
|
||||||
activemodel (>= 6.0.0)
|
|
||||||
bindex (>= 0.4.0)
|
bindex (>= 0.4.0)
|
||||||
railties (>= 6.0.0)
|
railties (>= 8.0.0)
|
||||||
webmock (3.24.0)
|
webmock (3.26.2)
|
||||||
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.5)
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
|
addressable
|
||||||
annotaterb
|
annotaterb
|
||||||
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
|
||||||
@@ -649,20 +628,17 @@ DEPENDENCIES
|
|||||||
jwt
|
jwt
|
||||||
letter_opener_web
|
letter_opener_web
|
||||||
lograge
|
lograge
|
||||||
mysql2
|
numo-narray-alt
|
||||||
numo-narray
|
|
||||||
oj
|
oj
|
||||||
onnxruntime
|
onnxruntime
|
||||||
pagy
|
pagy
|
||||||
pg
|
pg
|
||||||
premailer-rails
|
|
||||||
pretender
|
pretender
|
||||||
pry-rails
|
pry-rails
|
||||||
puma
|
puma
|
||||||
rack
|
rack
|
||||||
rails
|
rails
|
||||||
rails-i18n
|
rails-i18n
|
||||||
rails_autolink
|
|
||||||
rotp
|
rotp
|
||||||
rouge
|
rouge
|
||||||
rqrcode
|
rqrcode
|
||||||
@@ -678,6 +654,7 @@ DEPENDENCIES
|
|||||||
simplecov
|
simplecov
|
||||||
sqlite3
|
sqlite3
|
||||||
strip_attributes
|
strip_attributes
|
||||||
|
trilogy
|
||||||
turbo-rails
|
turbo-rails
|
||||||
twitter_cldr
|
twitter_cldr
|
||||||
tzinfo-data
|
tzinfo-data
|
||||||
@@ -685,7 +662,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,5 @@
|
|||||||
|
Additional Terms
|
||||||
|
|
||||||
|
In accordance with Section 7(b) of the GNU Affero General Public License,
|
||||||
|
a covered work must retain the original DocuSeal attribution in interactive
|
||||||
|
user interfaces.
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<h1 align="center" style="border-bottom: none">
|
<h1 align="center" style="border-bottom: none">
|
||||||
<div>
|
<div>
|
||||||
<a href="https://www.docuseal.com">
|
<a href="https://www.docuseal.com">
|
||||||
<img alt="DocuSeal" src="https://github.com/docusealco/docuseal/assets/5418788/c12cd051-81cd-4402-bc3a-92f2cfdc1b06" width="80" />
|
<img alt="DocuSeal" src="https://github.com/user-attachments/assets/38b45682-ffa4-4919-abde-d2d422325c44" width="80" />
|
||||||
<br>
|
<br>
|
||||||
</a>
|
</a>
|
||||||
DocuSeal
|
DocuSeal
|
||||||
@@ -64,7 +64,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
|
|
||||||
|Heroku|Railway|
|
|Heroku|Railway|
|
||||||
|:--:|:---:|
|
|:--:|:---:|
|
||||||
| [<img alt="Deploy on Heroku" src="https://www.herokucdn.com/deploy/button.svg" height="40">](https://heroku.com/deploy?template=https://github.com/docusealco/docuseal-heroku) | [<img alt="Deploy on Railway" src="https://railway.app/button.svg" height="40">](https://railway.app/template/IGoDnc?referralCode=ruU7JR)|
|
| [<img alt="Deploy on Heroku" src="https://www.herokucdn.com/deploy/button.svg" height="40">](https://heroku.com/deploy?template=https://github.com/docusealco/docuseal-heroku) | [<img alt="Deploy on Railway" src="https://railway.app/button.svg" height="40">](https://railway.com/deploy/IGoDnc?referralCode=ruU7JR)|
|
||||||
|**DigitalOcean**|**Render**|
|
|**DigitalOcean**|**Render**|
|
||||||
| [<img alt="Deploy on DigitalOcean" src="https://www.deploytodo.com/do-btn-blue.svg" height="40">](https://cloud.digitalocean.com/apps/new?repo=https://github.com/docusealco/docuseal-digitalocean/tree/master&refcode=421d50f53990) | [<img alt="Deploy to Render" src="https://render.com/images/deploy-to-render-button.svg" height="40">](https://render.com/deploy?repo=https://github.com/docusealco/docuseal-render)
|
| [<img alt="Deploy on DigitalOcean" src="https://www.deploytodo.com/do-btn-blue.svg" height="40">](https://cloud.digitalocean.com/apps/new?repo=https://github.com/docusealco/docuseal-digitalocean/tree/master&refcode=421d50f53990) | [<img alt="Deploy to Render" src="https://render.com/images/deploy-to-render-button.svg" height="40">](https://render.com/deploy?repo=https://github.com/docusealco/docuseal-render)
|
||||||
|
|
||||||
@@ -97,8 +97,8 @@ At DocuSeal we have expertise and technologies to make documents creation, filli
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Distributed under the AGPLv3 License. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) for more information.
|
Distributed under the AGPLv3 License with Section 7(b) Additional Terms. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) and [LICENSE_ADDITIONAL_TERMS](https://github.com/docusealco/docuseal/blob/master/LICENSE_ADDITIONAL_TERMS) for more information.
|
||||||
Unless otherwise noted, all files © 2023 DocuSeal LLC.
|
Unless otherwise noted, all files © 2023-2026 DocuSeal LLC.
|
||||||
|
|
||||||
## Tools
|
## Tools
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class AccountConfigsController < ApplicationController
|
|||||||
AccountConfig::FORCE_MFA,
|
AccountConfig::FORCE_MFA,
|
||||||
AccountConfig::ALLOW_TO_RESUBMIT,
|
AccountConfig::ALLOW_TO_RESUBMIT,
|
||||||
AccountConfig::ALLOW_TO_DECLINE_KEY,
|
AccountConfig::ALLOW_TO_DECLINE_KEY,
|
||||||
|
AccountConfig::ALLOW_TO_DELEGATE_KEY,
|
||||||
AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
|
AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
|
||||||
AccountConfig::ESIGNING_PREFERENCE_KEY,
|
AccountConfig::ESIGNING_PREFERENCE_KEY,
|
||||||
AccountConfig::FORM_WITH_CONFETTI_KEY,
|
AccountConfig::FORM_WITH_CONFETTI_KEY,
|
||||||
@@ -23,7 +24,8 @@ class AccountConfigsController < ApplicationController
|
|||||||
AccountConfig::WITH_SIGNATURE_ID,
|
AccountConfig::WITH_SIGNATURE_ID,
|
||||||
AccountConfig::COMBINE_PDF_RESULT_KEY,
|
AccountConfig::COMBINE_PDF_RESULT_KEY,
|
||||||
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
|
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
|
||||||
AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY
|
AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY,
|
||||||
|
AccountConfig::ENABLE_MCP_KEY
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
InvalidKey = Class.new(StandardError)
|
InvalidKey = Class.new(StandardError)
|
||||||
@@ -35,7 +37,7 @@ class AccountConfigsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
raise InvalidKey unless ALLOWED_KEYS.include?(@account_config.key)
|
raise InvalidKey unless allowed_keys.include?(@account_config.key)
|
||||||
|
|
||||||
@account_config.destroy!
|
@account_config.destroy!
|
||||||
|
|
||||||
@@ -44,8 +46,12 @@ class AccountConfigsController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def allowed_keys
|
||||||
|
ALLOWED_KEYS
|
||||||
|
end
|
||||||
|
|
||||||
def load_account_config
|
def load_account_config
|
||||||
raise InvalidKey unless ALLOWED_KEYS.include?(account_config_params[:key])
|
raise InvalidKey unless allowed_keys.include?(account_config_params[:key])
|
||||||
|
|
||||||
@account_config =
|
@account_config =
|
||||||
AccountConfig.find_or_initialize_by(account: current_account, key: account_config_params[:key])
|
AccountConfig.find_or_initialize_by(account: current_account, key: account_config_params[:key])
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AccountCustomFieldsController < ApplicationController
|
||||||
|
before_action :load_account_config, only: :create
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize!(:create, Template)
|
||||||
|
|
||||||
|
@account_config.update!(account_config_params)
|
||||||
|
|
||||||
|
render json: @account_config.value
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_account_config
|
||||||
|
@account_config =
|
||||||
|
AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY)
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_config_params
|
||||||
|
params.permit(
|
||||||
|
value: [[:uuid, :name, :type,
|
||||||
|
:required, :readonly, :default_value,
|
||||||
|
:title, :description,
|
||||||
|
{ preferences: {},
|
||||||
|
default_value: [],
|
||||||
|
options: [%i[value uuid]],
|
||||||
|
validation: %i[message pattern min max step],
|
||||||
|
areas: [%i[x y w h cell_w option_uuid]] }]]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
module Api
|
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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -172,7 +172,10 @@ module Api
|
|||||||
Submissions::NormalizeParamUtils.save_default_value_attachments!(attachments, submitters)
|
Submissions::NormalizeParamUtils.save_default_value_attachments!(attachments, submitters)
|
||||||
|
|
||||||
submitters.each do |submitter|
|
submitters.each do |submitter|
|
||||||
SubmissionEvents.create_with_tracking_data(submitter, 'api_complete_form', request) if submitter.completed_at?
|
if submitter.completed_at?
|
||||||
|
Submitters::SubmitValues.maybe_invite_via_field(submitter, request)
|
||||||
|
SubmissionEvents.create_with_tracking_data(submitter, 'api_complete_form', request)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
submissions
|
submissions
|
||||||
@@ -188,7 +191,7 @@ module Api
|
|||||||
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, :require_email_2fa, :order, :invite_by,
|
:require_phone_2fa, :require_email_2fa, :order, :index, :invite_by,
|
||||||
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
|
{ 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,
|
||||||
|
|||||||
@@ -34,11 +34,16 @@ module Api
|
|||||||
render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true, params:)
|
render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true, params:)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# rubocop:disable Metrics/MethodLength
|
||||||
def update
|
def update
|
||||||
if @submitter.completed_at?
|
if @submitter.completed_at?
|
||||||
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_content
|
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if @submitter.declined_at?
|
||||||
|
return render json: { error: 'Submitter has already declined the submission.' }, status: :unprocessable_content
|
||||||
|
end
|
||||||
|
|
||||||
submission = @submitter.submission
|
submission = @submitter.submission
|
||||||
role = submission.template_submitters.find { |e| e['uuid'] == @submitter.uuid }['name']
|
role = submission.template_submitters.find { |e| e['uuid'] == @submitter.uuid }['name']
|
||||||
|
|
||||||
@@ -60,7 +65,10 @@ module Api
|
|||||||
|
|
||||||
@submitter.submission.save!
|
@submitter.submission.save!
|
||||||
|
|
||||||
SubmissionEvents.create_with_tracking_data(@submitter, 'api_complete_form', request) if @submitter.completed_at?
|
if @submitter.completed_at?
|
||||||
|
Submitters::SubmitValues.maybe_invite_via_field(@submitter, request)
|
||||||
|
SubmissionEvents.create_with_tracking_data(@submitter, 'api_complete_form', request)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if @submitter.completed_at?
|
if @submitter.completed_at?
|
||||||
@@ -78,6 +86,7 @@ module Api
|
|||||||
|
|
||||||
render json: { error: e.message }, status: :unprocessable_content
|
render json: { error: e.message }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
# rubocop:enable Metrics/MethodLength
|
||||||
|
|
||||||
def submitter_params
|
def submitter_params
|
||||||
submitter_params = params.key?(:submitter) ? params.require(:submitter) : params
|
submitter_params = params.key?(:submitter) ? params.require(:submitter) : params
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ module Api
|
|||||||
|
|
||||||
ActiveRecord::Associations::Preloader.new(
|
ActiveRecord::Associations::Preloader.new(
|
||||||
records: [@template],
|
records: [@template],
|
||||||
associations: [schema_documents: :preview_images_attachments]
|
associations: [{ schema_documents: :preview_images_attachments }]
|
||||||
).call
|
).call
|
||||||
|
|
||||||
cloned_template = Templates::Clone.call(
|
cloned_template = Templates::Clone.call(
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ module Api
|
|||||||
SearchEntries.enqueue_reindex(@template)
|
SearchEntries.enqueue_reindex(@template)
|
||||||
|
|
||||||
WebhookUrls.enqueue_events(@template, 'template.updated')
|
WebhookUrls.enqueue_events(@template, 'template.updated')
|
||||||
|
WebhookUrls.enqueue_events(@template, 'template.archived') if archived == true
|
||||||
|
|
||||||
render json: @template.as_json(only: %i[id updated_at])
|
render json: @template.as_json(only: %i[id updated_at])
|
||||||
end
|
end
|
||||||
@@ -78,6 +79,8 @@ module Api
|
|||||||
@template.destroy!
|
@template.destroy!
|
||||||
else
|
else
|
||||||
@template.update!(archived_at: Time.current)
|
@template.update!(archived_at: Time.current)
|
||||||
|
|
||||||
|
WebhookUrls.enqueue_events(@template, 'template.archived')
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: @template.as_json(only: %i[id archived_at])
|
render json: @template.as_json(only: %i[id archived_at])
|
||||||
@@ -107,7 +110,8 @@ module Api
|
|||||||
:external_id,
|
:external_id,
|
||||||
:shared_link,
|
:shared_link,
|
||||||
{
|
{
|
||||||
submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email order]],
|
submitters: [%i[name uuid is_requester invite_by_uuid invite_via_field_uuid
|
||||||
|
optional_invite_by_uuid linked_to_uuid email order]],
|
||||||
fields: [[:uuid, :submitter_uuid, :name, :type,
|
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||||
:required, :readonly, :default_value,
|
:required, :readonly, :default_value,
|
||||||
:title, :description, :prefillable,
|
:title, :description, :prefillable,
|
||||||
@@ -116,7 +120,7 @@ module Api
|
|||||||
conditions: [%i[field_uuid value action operation]],
|
conditions: [%i[field_uuid value action operation]],
|
||||||
options: [%i[value uuid]],
|
options: [%i[value uuid]],
|
||||||
validation: %i[message pattern min max step],
|
validation: %i[message pattern min max step],
|
||||||
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
|
areas: [%i[uuid x y w h cell_w attachment_uuid option_uuid page]] }]]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -41,10 +42,6 @@ class ApplicationController < ActionController::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def default_url_options
|
def default_url_options
|
||||||
if request.domain == 'docuseal.com'
|
|
||||||
return { host: 'docuseal.com', protocol: ENV['FORCE_SSL'].present? ? 'https' : 'http' }
|
|
||||||
end
|
|
||||||
|
|
||||||
Docuseal.default_url_options
|
Docuseal.default_url_options
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -59,7 +56,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 +99,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
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class ErrorsController < ActionController::Base
|
|||||||
render json: { status: error_status_code, error: }.compact, status: error_status_code
|
render json: { status: error_status_code, error: }.compact, status: error_status_code
|
||||||
end
|
end
|
||||||
|
|
||||||
f.html { render error_status_code.to_s, status: error_status_code }
|
f.any { render error_status_code.to_s, status: error_status_code }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ class ErrorsController < ActionController::Base
|
|||||||
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
|
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
|
||||||
headers['Access-Control-Allow-Headers'] = '*'
|
headers['Access-Control-Allow-Headers'] = '*'
|
||||||
headers['Access-Control-Max-Age'] = '1728000'
|
headers['Access-Control-Max-Age'] = '1728000'
|
||||||
headers['Access-Control-Allow-Credentials'] = true
|
headers['Access-Control-Allow-Credentials'] = 'true'
|
||||||
end
|
end
|
||||||
|
|
||||||
def error_status_code
|
def error_status_code
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
class EsignSettingsController < ApplicationController
|
class EsignSettingsController < ApplicationController
|
||||||
DEFAULT_CERT_NAME = 'DocuSeal Self-Host Autogenerated'
|
DEFAULT_CERT_NAME = 'DocuSeal Self-Host Autogenerated'
|
||||||
|
|
||||||
CertFormRecord = Struct.new(:name, :file, :password, keyword_init: true) do
|
CertFormRecord = Struct.new(:name, :file, :password) do
|
||||||
include ActiveModel::Validations
|
include ActiveModel::Validations
|
||||||
|
|
||||||
def to_key
|
def to_key
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class McpController < ActionController::API
|
||||||
|
before_action :authenticate_user!
|
||||||
|
before_action :verify_mcp_enabled!
|
||||||
|
|
||||||
|
before_action do
|
||||||
|
authorize!(:manage, :mcp)
|
||||||
|
end
|
||||||
|
|
||||||
|
def call
|
||||||
|
return head :ok if request.raw_post.blank?
|
||||||
|
|
||||||
|
body = JSON.parse(request.raw_post)
|
||||||
|
|
||||||
|
result = Mcp::HandleRequest.call(body, current_user, current_ability)
|
||||||
|
|
||||||
|
if result
|
||||||
|
render json: result
|
||||||
|
else
|
||||||
|
head :accepted
|
||||||
|
end
|
||||||
|
rescue CanCan::AccessDenied
|
||||||
|
render json: { jsonrpc: '2.0', id: nil, error: { code: -32_603, message: 'Forbidden' } }, status: :forbidden
|
||||||
|
rescue JSON::ParserError
|
||||||
|
render json: { jsonrpc: '2.0', id: nil, error: { code: -32_700, message: 'Parse error' } }, status: :bad_request
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def authenticate_user!
|
||||||
|
render json: { error: 'Not authenticated' }, status: :unauthorized unless current_user
|
||||||
|
end
|
||||||
|
|
||||||
|
def verify_mcp_enabled!
|
||||||
|
return if Docuseal.multitenant?
|
||||||
|
|
||||||
|
return if AccountConfig.exists?(account_id: current_user.account_id,
|
||||||
|
key: AccountConfig::ENABLE_MCP_KEY,
|
||||||
|
value: true)
|
||||||
|
|
||||||
|
render json: { error: 'MCP is disabled' }, status: :forbidden
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_user
|
||||||
|
@current_user ||= user_from_api_key
|
||||||
|
end
|
||||||
|
|
||||||
|
def user_from_api_key
|
||||||
|
token = request.headers['Authorization'].to_s[/\ABearer\s+(.+)\z/, 1]
|
||||||
|
|
||||||
|
return if token.blank?
|
||||||
|
|
||||||
|
sha256 = Digest::SHA256.hexdigest(token)
|
||||||
|
|
||||||
|
User.joins(:mcp_tokens).active.find_by(mcp_tokens: { sha256:, archived_at: nil })
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class McpSettingsController < ApplicationController
|
||||||
|
load_and_authorize_resource :mcp_token, parent: false
|
||||||
|
|
||||||
|
before_action do
|
||||||
|
authorize!(:manage, :mcp)
|
||||||
|
end
|
||||||
|
|
||||||
|
def index
|
||||||
|
@mcp_tokens = @mcp_tokens.active.order(id: :desc)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@mcp_token = current_user.mcp_tokens.new(mcp_token_params)
|
||||||
|
|
||||||
|
if @mcp_token.save
|
||||||
|
@mcp_tokens = [@mcp_token]
|
||||||
|
|
||||||
|
render :index, status: :created
|
||||||
|
else
|
||||||
|
render turbo_stream: turbo_stream.replace(:modal, template: 'mcp_settings/new'), status: :unprocessable_content
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@mcp_token.update!(archived_at: Time.current)
|
||||||
|
|
||||||
|
redirect_back fallback_location: settings_mcp_index_path, notice: I18n.t('mcp_token_has_been_removed')
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def mcp_token_params
|
||||||
|
params.require(:mcp_token).permit(:name)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -6,9 +6,17 @@ class PreviewDocumentPageController < ActionController::API
|
|||||||
FORMAT = Templates::ProcessDocument::FORMAT
|
FORMAT = Templates::ProcessDocument::FORMAT
|
||||||
|
|
||||||
def show
|
def show
|
||||||
attachment_uuid = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid], purpose: :attachment)
|
result_data =
|
||||||
|
ApplicationRecord.signed_id_verifier.verified(params[:signed_key], purpose: :attachment)
|
||||||
|
|
||||||
attachment = ActiveStorage::Attachment.find_by(uuid: attachment_uuid) if attachment_uuid
|
attachment =
|
||||||
|
if result_data.is_a?(Array) && result_data.compact_blank.size == 2
|
||||||
|
attachment_id, attachment_uuid = result_data
|
||||||
|
|
||||||
|
ActiveStorage::Attachment.find_by(id: attachment_id, uuid: attachment_uuid)
|
||||||
|
elsif result_data
|
||||||
|
ActiveStorage::Attachment.find_by(uuid: result_data)
|
||||||
|
end
|
||||||
|
|
||||||
return head :not_found unless attachment
|
return head :not_found unless attachment
|
||||||
|
|
||||||
|
|||||||
@@ -172,6 +172,8 @@ class StartFormController < ApplicationController
|
|||||||
submitters: [submitter],
|
submitters: [submitter],
|
||||||
source: :link)
|
source: :link)
|
||||||
|
|
||||||
|
Submissions::CreateFromSubmitters.maybe_set_dynamic_documents(submitter.submission)
|
||||||
|
|
||||||
submitter.account_id = submitter.submission.account_id
|
submitter.account_id = submitter.submission.account_id
|
||||||
|
|
||||||
submitter
|
submitter
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class SubmissionEventsController < ApplicationController
|
|||||||
'email_verified' => 'email_check',
|
'email_verified' => 'email_check',
|
||||||
'click_sms' => 'hand_click',
|
'click_sms' => 'hand_click',
|
||||||
'decline_form' => 'x',
|
'decline_form' => 'x',
|
||||||
|
'delegate_form' => 'user_share',
|
||||||
'start_verification' => 'player_play',
|
'start_verification' => 'player_play',
|
||||||
'complete_verification' => 'check',
|
'complete_verification' => 'check',
|
||||||
'invite_party' => 'user_plus'
|
'invite_party' => 'user_plus'
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class SubmissionsController < ApplicationController
|
|||||||
unless @submission.submitters.all?(&:completed_at?)
|
unless @submission.submitters.all?(&:completed_at?)
|
||||||
ActiveRecord::Associations::Preloader.new(
|
ActiveRecord::Associations::Preloader.new(
|
||||||
records: [@submission],
|
records: [@submission],
|
||||||
associations: [submitters: :start_form_submission_events]
|
associations: [{ submitters: :start_form_submission_events }]
|
||||||
).call
|
).call
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -51,18 +51,7 @@ class SubmissionsController < ApplicationController
|
|||||||
emails: params[:emails],
|
emails: params[:emails],
|
||||||
params: params.merge('send_completed_email' => true))
|
params: params.merge('send_completed_email' => true))
|
||||||
else
|
else
|
||||||
submissions_attrs = submissions_params[:submission].to_h.values
|
create_submissions(@template, submissions_params, params)
|
||||||
|
|
||||||
submissions_attrs, _, new_fields =
|
|
||||||
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_attrs, @template, add_fields: true)
|
|
||||||
|
|
||||||
Submissions.create_from_submitters(template: @template,
|
|
||||||
user: current_user,
|
|
||||||
source: :invite,
|
|
||||||
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
|
|
||||||
submissions_attrs:,
|
|
||||||
new_fields:,
|
|
||||||
params: params.merge('send_completed_email' => true))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
WebhookUrls.enqueue_events(submissions, 'submission.created')
|
WebhookUrls.enqueue_events(submissions, 'submission.created')
|
||||||
@@ -97,6 +86,21 @@ class SubmissionsController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def create_submissions(template, submissions_params, params)
|
||||||
|
submissions_attrs = submissions_params[:submission].to_h.values
|
||||||
|
|
||||||
|
submissions_attrs, _, new_fields =
|
||||||
|
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_attrs, template, add_fields: true)
|
||||||
|
|
||||||
|
Submissions.create_from_submitters(template: template,
|
||||||
|
user: current_user,
|
||||||
|
source: :invite,
|
||||||
|
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
|
||||||
|
submissions_attrs:,
|
||||||
|
new_fields:,
|
||||||
|
params: params.merge('send_completed_email' => true))
|
||||||
|
end
|
||||||
|
|
||||||
def save_template_message(template, params)
|
def save_template_message(template, params)
|
||||||
template.preferences['request_email_subject'] = params[:subject] if params[:subject].present?
|
template.preferences['request_email_subject'] = params[:subject] if params[:subject].present?
|
||||||
template.preferences['request_email_body'] = params[:body] if params[:body].present?
|
template.preferences['request_email_body'] = params[:body] if params[:body].present?
|
||||||
|
|||||||
@@ -27,20 +27,18 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(last_submitter)
|
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||||
|
|
||||||
if last_submitter.completed_at < TTL.ago && !signature_valid && !current_user_submitter?(last_submitter)
|
if !signature_valid && !current_user_submitter?(last_submitter)
|
||||||
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
return head :not_found unless Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||||
|
|
||||||
return head :not_found
|
if last_submitter.completed_at < TTL.ago
|
||||||
|
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return head :not_found
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if params[:combined] == 'true'
|
if params[:combined] == 'true'
|
||||||
url = build_combined_url(@submitter)
|
respond_with_combined(last_submitter)
|
||||||
|
|
||||||
if url
|
|
||||||
render json: [url]
|
|
||||||
else
|
|
||||||
head :not_found
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
render json: build_urls(last_submitter)
|
render json: build_urls(last_submitter)
|
||||||
end
|
end
|
||||||
@@ -48,8 +46,18 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def respond_with_combined(submitter)
|
||||||
|
url = build_combined_url(submitter)
|
||||||
|
|
||||||
|
if url
|
||||||
|
render json: [url]
|
||||||
|
else
|
||||||
|
head :not_found
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def current_user_submitter?(submitter)
|
def current_user_submitter?(submitter)
|
||||||
current_user && current_user.account.submitters.exists?(id: submitter.id)
|
current_user && current_ability.can?(:read, submitter)
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_urls(submitter)
|
def build_urls(submitter)
|
||||||
@@ -57,7 +65,7 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
|
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
|
||||||
|
|
||||||
Submitters.select_attachments_for_download(submitter).map do |attachment|
|
Submitters.select_attachments_for_download(submitter).map do |attachment|
|
||||||
ActiveStorage::Blob.proxy_url(
|
ActiveStorage::Blob.proxy_path(
|
||||||
attachment.blob,
|
attachment.blob,
|
||||||
expires_at: FILES_TTL.from_now.to_i,
|
expires_at: FILES_TTL.from_now.to_i,
|
||||||
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
|
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
|
||||||
@@ -75,7 +83,7 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
|
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
|
||||||
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
|
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
|
||||||
|
|
||||||
ActiveStorage::Blob.proxy_url(
|
ActiveStorage::Blob.proxy_path(
|
||||||
attachment.blob,
|
attachment.blob,
|
||||||
expires_at: FILES_TTL.from_now.to_i,
|
expires_at: FILES_TTL.from_now.to_i,
|
||||||
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
|
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ class SubmissionsExportController < ApplicationController
|
|||||||
load_and_authorize_resource :submission, through: :template, parent: false, only: :index
|
load_and_authorize_resource :submission, through: :template, parent: false, only: :index
|
||||||
|
|
||||||
def index
|
def index
|
||||||
submissions = @submissions.active
|
submissions = params[:archived] == 'true' ? @submissions.archived : @submissions.active
|
||||||
.preload(submitters: { documents_attachments: :blob,
|
|
||||||
attachments_attachments: :blob })
|
submissions = submissions.preload(submitters: { documents_attachments: :blob,
|
||||||
.order(id: :asc)
|
attachments_attachments: :blob })
|
||||||
|
.order(id: :asc)
|
||||||
|
|
||||||
submissions = Submissions.search(current_user, submissions, params[:q], search_values: true)
|
submissions = Submissions.search(current_user, submissions, params[:q], search_values: true)
|
||||||
submissions = Submissions::Filter.call(submissions, current_user, params)
|
submissions = Submissions::Filter.call(submissions, current_user, params)
|
||||||
|
|||||||
@@ -3,21 +3,22 @@
|
|||||||
class SubmitFormController < ApplicationController
|
class SubmitFormController < ApplicationController
|
||||||
layout 'form'
|
layout 'form'
|
||||||
|
|
||||||
around_action :with_browser_locale, only: %i[show completed success]
|
around_action :with_browser_locale, only: %i[show completed success delegated]
|
||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
before_action :load_submitter, only: %i[show update completed]
|
before_action :load_submitter, only: %i[show update completed]
|
||||||
|
before_action :maybe_redirect_delegated, only: %i[show completed]
|
||||||
before_action :maybe_render_locked_page, only: :show
|
before_action :maybe_render_locked_page, only: :show
|
||||||
before_action :maybe_require_link_2fa, only: %i[show update]
|
before_action :maybe_require_link_2fa, only: %i[show]
|
||||||
|
|
||||||
CONFIG_KEYS = [].freeze
|
CONFIG_KEYS = [].freeze
|
||||||
|
|
||||||
def show
|
def show
|
||||||
submission = @submitter.submission
|
submission = @submitter.submission
|
||||||
|
|
||||||
|
return render :email_2fa unless Submitters::AuthorizedForForm.pass_email_2fa?(@submitter, request)
|
||||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
return 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)
|
||||||
|
|
||||||
@@ -48,7 +49,7 @@ class SubmitFormController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
if require_email_2fa?(@submitter)
|
unless Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||||
return render json: { error: I18n.t('verification_required_refresh_the_page_and_pass_2fa') },
|
return render json: { error: I18n.t('verification_required_refresh_the_page_and_pass_2fa') },
|
||||||
status: :unprocessable_content
|
status: :unprocessable_content
|
||||||
end
|
end
|
||||||
@@ -84,18 +85,23 @@ class SubmitFormController < ApplicationController
|
|||||||
def completed
|
def completed
|
||||||
raise ActionController::RoutingError, I18n.t('not_found') if @submitter.account.archived_at?
|
raise ActionController::RoutingError, I18n.t('not_found') if @submitter.account.archived_at?
|
||||||
|
|
||||||
redirect_to submit_form_path(params[:submit_form_slug]) if require_email_2fa?(@submitter)
|
return if Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||||
|
|
||||||
|
redirect_to submit_form_path(params[:submit_form_slug])
|
||||||
end
|
end
|
||||||
|
|
||||||
def success; end
|
def success; end
|
||||||
|
|
||||||
|
def delegated
|
||||||
|
submitter_version = SubmitterVersion.find_by!(slug: params[:slug] || params[:submit_form_slug])
|
||||||
|
|
||||||
|
@submitter = submitter_version.submitter
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def maybe_require_link_2fa
|
def maybe_require_link_2fa
|
||||||
return if @submitter.submission.source != 'link'
|
return if Submitters::AuthorizedForForm.pass_link_2fa?(@submitter, current_user, request)
|
||||||
return unless @submitter.submission.template&.preferences&.dig('shared_link_2fa') == true
|
|
||||||
return if cookies.encrypted[:email_2fa_slug] == @submitter.slug
|
|
||||||
return if @submitter.email == current_user&.email && current_user&.account_id == @submitter.account_id
|
|
||||||
|
|
||||||
redirect_to start_form_path(@submitter.submission.template.slug)
|
redirect_to start_form_path(@submitter.submission.template.slug)
|
||||||
end
|
end
|
||||||
@@ -109,20 +115,22 @@ class SubmitFormController < ApplicationController
|
|||||||
render :declined if @submitter.declined_at?
|
render :declined if @submitter.declined_at?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def maybe_redirect_delegated
|
||||||
|
return if @submitter
|
||||||
|
|
||||||
|
submitter_version = SubmitterVersion.find_by!(slug: params[:slug] || params[:submit_form_slug])
|
||||||
|
|
||||||
|
submitter_version.submitter.submission_events.find_by!(event_type: :delegate_form)
|
||||||
|
|
||||||
|
redirect_to submit_form_delegated_path(submitter_version.slug)
|
||||||
|
end
|
||||||
|
|
||||||
def load_submitter
|
def load_submitter
|
||||||
@submitter = Submitter.find_by!(slug: params[:slug] || params[:submit_form_slug])
|
@submitter = Submitter.find_by(slug: params[:slug] || params[:submit_form_slug])
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_attachments_index(submission)
|
def build_attachments_index(submission)
|
||||||
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
|
||||||
@@ -4,29 +4,38 @@ class SubmitFormDeclineController < ApplicationController
|
|||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
def create
|
before_action :load_submitter
|
||||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
|
||||||
|
|
||||||
return redirect_to submit_form_path(submitter.slug) if submitter.declined_at? ||
|
def create
|
||||||
submitter.completed_at? ||
|
return redirect_to submit_form_path(@submitter.slug) if @submitter.declined_at? ||
|
||||||
submitter.submission.archived_at? ||
|
@submitter.completed_at? ||
|
||||||
submitter.submission.expired? ||
|
@submitter.submission.archived_at? ||
|
||||||
submitter.submission.template&.archived_at?
|
@submitter.submission.expired? ||
|
||||||
|
@submitter.submission.template&.archived_at? ||
|
||||||
|
!Submitters::AuthorizedForForm.call(@submitter,
|
||||||
|
current_user,
|
||||||
|
request)
|
||||||
|
|
||||||
ApplicationRecord.transaction do
|
ApplicationRecord.transaction do
|
||||||
submitter.update!(declined_at: Time.current)
|
@submitter.update!(declined_at: Time.current)
|
||||||
|
|
||||||
SubmissionEvents.create_with_tracking_data(submitter, 'decline_form', request, { reason: params[:reason] })
|
SubmissionEvents.create_with_tracking_data(@submitter, 'decline_form', request, { reason: params[:reason] })
|
||||||
end
|
end
|
||||||
|
|
||||||
user = submitter.submission.created_by_user || submitter.template.author
|
user = @submitter.submission.created_by_user || @submitter.template.author
|
||||||
|
|
||||||
if user.user_configs.find_by(key: UserConfig::RECEIVE_DECLINED_EMAIL)&.value != false
|
if user.user_configs.find_by(key: UserConfig::RECEIVE_DECLINED_EMAIL)&.value != false
|
||||||
SubmitterMailer.declined_email(submitter, user).deliver_later!
|
SubmitterMailer.declined_email(@submitter, user).deliver_later!
|
||||||
end
|
end
|
||||||
|
|
||||||
WebhookUrls.enqueue_events(submitter, 'form.declined')
|
WebhookUrls.enqueue_events(@submitter, 'form.declined')
|
||||||
|
|
||||||
redirect_to submit_form_path(submitter.slug)
|
redirect_to submit_form_path(@submitter.slug)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_submitter
|
||||||
|
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmitFormDelegateController < ApplicationController
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
before_action :load_submitter
|
||||||
|
|
||||||
|
def create
|
||||||
|
return redirect_to submit_form_path(@submitter.slug) if @submitter.declined_at? ||
|
||||||
|
@submitter.completed_at? ||
|
||||||
|
@submitter.submission.archived_at? ||
|
||||||
|
@submitter.submission.expired? ||
|
||||||
|
@submitter.submission.template&.archived_at? ||
|
||||||
|
!Submitters::AuthorizedForForm.call(@submitter,
|
||||||
|
current_user,
|
||||||
|
request)
|
||||||
|
|
||||||
|
@submitter.account.account_configs.find_by!(key: AccountConfig::ALLOW_TO_DELEGATE_KEY, value: true)
|
||||||
|
|
||||||
|
email = Submissions.normalize_email(params[:email])
|
||||||
|
|
||||||
|
return redirect_to submit_form_path(@submitter.slug) if email.blank?
|
||||||
|
|
||||||
|
old_slug = @submitter.slug
|
||||||
|
|
||||||
|
ApplicationRecord.transaction do
|
||||||
|
@submitter.submitter_versions.create!(slug: old_slug, email: @submitter.email,
|
||||||
|
name: @submitter.name, phone: @submitter.phone)
|
||||||
|
|
||||||
|
SubmissionEvents.create_with_tracking_data(@submitter, 'delegate_form', request,
|
||||||
|
{ old_email: @submitter.email, email: })
|
||||||
|
|
||||||
|
@submitter.update!(email:, phone: nil, name: nil, slug: SecureRandom.base58(14))
|
||||||
|
end
|
||||||
|
|
||||||
|
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => @submitter.id)
|
||||||
|
|
||||||
|
redirect_to submit_form_delegated_path(old_slug)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_submitter
|
||||||
|
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -17,7 +17,8 @@ class SubmitFormDownloadController < ApplicationController
|
|||||||
@submitter.submission.template&.archived_at? ||
|
@submitter.submission.template&.archived_at? ||
|
||||||
AccountConfig.exists?(account_id: @submitter.account_id,
|
AccountConfig.exists?(account_id: @submitter.account_id,
|
||||||
key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
|
key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
|
||||||
value: false)
|
value: false) ||
|
||||||
|
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||||
|
|
||||||
last_completed_submitter = @submitter.submission.submitters
|
last_completed_submitter = @submitter.submission.submitters
|
||||||
.where.not(id: @submitter.id)
|
.where.not(id: @submitter.id)
|
||||||
@@ -32,7 +33,7 @@ class SubmitFormDownloadController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
urls = attachments.map do |attachment|
|
urls = attachments.map do |attachment|
|
||||||
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
|
ActiveStorage::Blob.proxy_path(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: urls
|
render json: urls
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ class SubmitFormDrawSignatureController < ApplicationController
|
|||||||
|
|
||||||
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?
|
||||||
|
|
||||||
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at?
|
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at? ||
|
||||||
|
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||||
return redirect_to submit_form_path(@submitter.slug)
|
return redirect_to submit_form_path(@submitter.slug)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ class SubmitFormInviteController < ApplicationController
|
|||||||
next unless attrs
|
next unless attrs
|
||||||
next if attrs[:email].blank?
|
next if attrs[:email].blank?
|
||||||
|
|
||||||
submitter.submission.submitters.create!(**attrs, account_id: submitter.account_id)
|
email = Submissions.normalize_email(attrs[:email])
|
||||||
|
|
||||||
|
submitter.submission.submitters.create!(uuid: attrs[:uuid], email:, account_id: submitter.account_id)
|
||||||
|
|
||||||
SubmissionEvents.create_with_tracking_data(submitter, 'invite_party', request, { uuid: submitter.uuid })
|
SubmissionEvents.create_with_tracking_data(submitter, 'invite_party', request, { uuid: submitter.uuid })
|
||||||
end
|
end
|
||||||
@@ -45,7 +47,8 @@ class SubmitFormInviteController < ApplicationController
|
|||||||
!submitter.completed_at? &&
|
!submitter.completed_at? &&
|
||||||
!submitter.submission.archived_at? &&
|
!submitter.submission.archived_at? &&
|
||||||
!submitter.submission.expired? &&
|
!submitter.submission.expired? &&
|
||||||
!submitter.submission.template&.archived_at?
|
!submitter.submission.template&.archived_at? &&
|
||||||
|
Submitters::AuthorizedForForm.call(submitter, current_user, request)
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_invite_submitters(submitter, key = 'invite_by_uuid')
|
def filter_invite_submitters(submitter, key = 'invite_by_uuid')
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ class SubmitFormValuesController < ApplicationController
|
|||||||
def index
|
def index
|
||||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||||
|
|
||||||
return render json: {} if submitter.completed_at? || submitter.declined_at?
|
return render json: {} if submitter.completed_at? ||
|
||||||
return render json: {} if submitter.submission.template&.archived_at? ||
|
submitter.declined_at? ||
|
||||||
|
submitter.submission.template&.archived_at? ||
|
||||||
submitter.submission.archived_at? ||
|
submitter.submission.archived_at? ||
|
||||||
submitter.submission.expired?
|
submitter.submission.expired? ||
|
||||||
|
!Submitters::AuthorizedForForm.call(submitter, current_user, request)
|
||||||
|
|
||||||
value = submitter.values[params['field_uuid']]
|
value = submitter.values[params['field_uuid']]
|
||||||
attachment = submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present?
|
attachment = submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present?
|
||||||
|
|||||||
@@ -7,25 +7,34 @@ class SubmittersAutocompleteController < ApplicationController
|
|||||||
LIMIT = 100
|
LIMIT = 100
|
||||||
|
|
||||||
def index
|
def index
|
||||||
submitters = search_submitters(@submitters)
|
field = SELECT_COLUMNS.find { |c| c == params[:field] }
|
||||||
|
|
||||||
|
submitters = search_submitters(@submitters, field)
|
||||||
|
|
||||||
arel_columns = SELECT_COLUMNS.map { |col| Submitter.arel_table[col] }
|
arel_columns = SELECT_COLUMNS.map { |col| Submitter.arel_table[col] }
|
||||||
values = submitters.limit(LIMIT).group(arel_columns).pluck(arel_columns)
|
|
||||||
|
values =
|
||||||
|
if field
|
||||||
|
max_ids = submitters.group(field).limit(LIMIT).select(Submitter.arel_table[:id].maximum)
|
||||||
|
|
||||||
|
submitters.where(id: max_ids).order(id: :desc).pluck(arel_columns)
|
||||||
|
else
|
||||||
|
submitters.limit(LIMIT).group(arel_columns).pluck(arel_columns)
|
||||||
|
end
|
||||||
|
|
||||||
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
|
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
|
||||||
attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present?
|
|
||||||
|
|
||||||
render json: attrs
|
render json: attrs
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def search_submitters(submitters)
|
def search_submitters(submitters, field)
|
||||||
if SELECT_COLUMNS.include?(params[:field])
|
if field
|
||||||
if Docuseal.fulltext_search?
|
if Docuseal.fulltext_search?
|
||||||
Submitters.fulltext_search_field(current_user, submitters, params[:q], params[:field])
|
Submitters.fulltext_search_field(current_user, submitters, params[:q], field)
|
||||||
else
|
else
|
||||||
column = Submitter.arel_table[params[:field].to_sym]
|
column = Submitter.arel_table[field.to_sym]
|
||||||
|
|
||||||
term = "#{params[:q].downcase}%"
|
term = "#{params[:q].downcase}%"
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class TemplateDocumentsController < ApplicationController
|
|||||||
FILES_TTL = 5.minutes
|
FILES_TTL = 5.minutes
|
||||||
|
|
||||||
def index
|
def index
|
||||||
render json: @template.schema_documents.map { |d| ActiveStorage::Blob.proxy_url(d.blob, expires_at: FILES_TTL.from_now.to_i) }
|
render json: @template.schema_documents.map { |d| ActiveStorage::Blob.proxy_path(d.blob, expires_at: FILES_TTL.from_now.to_i) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@@ -16,7 +16,7 @@ class TemplateDocumentsController < ApplicationController
|
|||||||
|
|
||||||
old_fields_hash = @template.fields.hash
|
old_fields_hash = @template.fields.hash
|
||||||
|
|
||||||
documents = Templates::CreateAttachments.call(@template, params, extract_fields: true)
|
documents, = Templates::CreateAttachments.call(@template, params, extract_fields: true)
|
||||||
|
|
||||||
schema = documents.map do |doc|
|
schema = documents.map do |doc|
|
||||||
{ attachment_uuid: doc.uuid, name: doc.filename.base }
|
{ attachment_uuid: doc.uuid, name: doc.filename.base }
|
||||||
@@ -27,7 +27,7 @@ class TemplateDocumentsController < ApplicationController
|
|||||||
fields: old_fields_hash == @template.fields.hash ? nil : @template.fields,
|
fields: old_fields_hash == @template.fields.hash ? nil : @template.fields,
|
||||||
submitters: old_fields_hash == @template.fields.hash ? nil : @template.submitters,
|
submitters: old_fields_hash == @template.fields.hash ? nil : @template.submitters,
|
||||||
documents: documents.as_json(
|
documents: documents.as_json(
|
||||||
methods: %i[metadata signed_uuid],
|
methods: %i[metadata signed_key],
|
||||||
include: {
|
include: {
|
||||||
preview_images: { methods: %i[url metadata filename] }
|
preview_images: { methods: %i[url metadata filename] }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class TemplatesCloneAndReplaceController < ApplicationController
|
|||||||
|
|
||||||
ActiveRecord::Associations::Preloader.new(
|
ActiveRecord::Associations::Preloader.new(
|
||||||
records: [@template],
|
records: [@template],
|
||||||
associations: [schema_documents: :preview_images_attachments]
|
associations: [{ schema_documents: :preview_images_attachments }]
|
||||||
).call
|
).call
|
||||||
|
|
||||||
cloned_template = Templates::Clone.call(@template, author: current_user)
|
cloned_template = Templates::Clone.call(@template, author: current_user)
|
||||||
|
|||||||
@@ -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.can?(: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,20 +24,18 @@ 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(
|
||||||
records: [@template],
|
records: [@template],
|
||||||
associations: [schema_documents: [:blob, { preview_images_attachments: :blob }]]
|
associations: [{ schema_documents: [:blob, { preview_images_attachments: :blob }] }]
|
||||||
).call
|
).call
|
||||||
|
|
||||||
@template_data =
|
@template_data =
|
||||||
@template.as_json.merge(
|
@template.as_json.merge(
|
||||||
documents: @template.schema_documents.as_json(
|
documents: @template.schema_documents.as_json(
|
||||||
methods: %i[metadata signed_uuid],
|
methods: %i[metadata signed_key],
|
||||||
include: { preview_images: { methods: %i[url metadata filename] } }
|
include: { preview_images: { methods: %i[url metadata filename] } }
|
||||||
)
|
)
|
||||||
).to_json
|
).to_json
|
||||||
@@ -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
|
||||||
@@ -107,6 +84,8 @@ class TemplatesController < ApplicationController
|
|||||||
else
|
else
|
||||||
@template.update!(archived_at: Time.current)
|
@template.update!(archived_at: Time.current)
|
||||||
|
|
||||||
|
WebhookUrls.enqueue_events(@template, 'template.archived')
|
||||||
|
|
||||||
I18n.t('template_has_been_archived')
|
I18n.t('template_has_been_archived')
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -118,9 +97,11 @@ class TemplatesController < ApplicationController
|
|||||||
def template_params
|
def template_params
|
||||||
params.require(:template).permit(
|
params.require(:template).permit(
|
||||||
:name,
|
:name,
|
||||||
{ schema: [[:attachment_uuid, :google_drive_file_id, :name,
|
{ schema: [[:attachment_uuid, :google_drive_file_id, :name, :dynamic,
|
||||||
{ conditions: [%i[field_uuid value action operation]] }]],
|
{ conditions: [%i[field_uuid value action operation]] }]],
|
||||||
submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid optional_invite_by_uuid email order]],
|
submitters: [%i[name uuid is_requester linked_to_uuid invite_via_field_uuid
|
||||||
|
invite_by_uuid optional_invite_by_uuid email order]],
|
||||||
|
variables_schema: {},
|
||||||
fields: [[:uuid, :submitter_uuid, :name, :type,
|
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||||
:required, :readonly, :default_value,
|
:required, :readonly, :default_value,
|
||||||
:title, :description, :prefillable,
|
:title, :description, :prefillable,
|
||||||
@@ -129,26 +110,7 @@ class TemplatesController < ApplicationController
|
|||||||
conditions: [%i[field_uuid value action operation]],
|
conditions: [%i[field_uuid value action operation]],
|
||||||
options: [%i[value uuid]],
|
options: [%i[value uuid]],
|
||||||
validation: %i[message pattern min max step],
|
validation: %i[message pattern min max step],
|
||||||
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
areas: [%i[uuid 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
|
||||||
@@ -9,32 +9,34 @@ class TemplatesDebugController < ApplicationController
|
|||||||
schema_uuids = @template.schema.index_by { |e| e['attachment_uuid'] }
|
schema_uuids = @template.schema.index_by { |e| e['attachment_uuid'] }
|
||||||
attachment = @template.documents.find { |a| schema_uuids[a.uuid] }
|
attachment = @template.documents.find { |a| schema_uuids[a.uuid] }
|
||||||
|
|
||||||
data = attachment.download
|
if attachment
|
||||||
|
data = attachment.download
|
||||||
|
|
||||||
unless attachment.image?
|
unless attachment.image?
|
||||||
pdf = HexaPDF::Document.new(io: StringIO.new(data))
|
pdf = HexaPDF::Document.new(io: StringIO.new(data))
|
||||||
|
|
||||||
fields = Templates::FindAcroFields.call(pdf, attachment, 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']['fields'] = fields
|
||||||
|
|
||||||
|
@template.update!(fields: Templates::ProcessDocument.normalize_attachment_fields(@template, [attachment]))
|
||||||
end
|
end
|
||||||
|
|
||||||
fields, = Templates::DetectFields.call(StringIO.new(data), attachment:) if fields.blank?
|
|
||||||
|
|
||||||
attachment.metadata['pdf'] ||= {}
|
|
||||||
attachment.metadata['pdf']['fields'] = fields
|
|
||||||
|
|
||||||
@template.update!(fields: Templates::ProcessDocument.normalize_attachment_fields(@template, [attachment]))
|
|
||||||
|
|
||||||
debug_file if DEBUG_FILE.present?
|
debug_file if DEBUG_FILE.present?
|
||||||
|
|
||||||
ActiveRecord::Associations::Preloader.new(
|
ActiveRecord::Associations::Preloader.new(
|
||||||
records: [@template],
|
records: [@template],
|
||||||
associations: [schema_documents: { preview_images_attachments: :blob }]
|
associations: [{ schema_documents: { preview_images_attachments: :blob } }]
|
||||||
).call
|
).call
|
||||||
|
|
||||||
@template_data =
|
@template_data =
|
||||||
@template.as_json.merge(
|
@template.as_json.merge(
|
||||||
documents: @template.schema_documents.as_json(
|
documents: @template.schema_documents.as_json(
|
||||||
methods: %i[metadata signed_uuid],
|
methods: %i[metadata signed_key],
|
||||||
include: { preview_images: { methods: %i[url metadata filename] } }
|
include: { preview_images: { methods: %i[url metadata filename] } }
|
||||||
)
|
)
|
||||||
).to_json
|
).to_json
|
||||||
@@ -58,9 +60,16 @@ class TemplatesDebugController < ApplicationController
|
|||||||
|
|
||||||
params = { files: [file] }
|
params = { files: [file] }
|
||||||
|
|
||||||
documents = Templates::CreateAttachments.call(@template, params)
|
documents, dynamic_documents = Templates::CreateAttachments.call(@template, params,
|
||||||
|
dynamic: DEBUG_FILE.ends_with?('.docx'))
|
||||||
|
|
||||||
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
|
schema = documents.map do |doc|
|
||||||
|
{
|
||||||
|
attachment_uuid: doc.uuid,
|
||||||
|
name: doc.filename.base,
|
||||||
|
dynamic: dynamic_documents.find { |e| e.uuid == doc.uuid }.present?
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
@template.update!(schema:)
|
@template.update!(schema:)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,11 +11,14 @@ class TemplatesDetectFieldsController < ApplicationController
|
|||||||
sse = SSE.new(response.stream)
|
sse = SSE.new(response.stream)
|
||||||
|
|
||||||
documents = @template.schema_documents.preload(:blob)
|
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|
|
documents.each do |document|
|
||||||
io = StringIO.new(document.download)
|
io = StringIO.new(document.download)
|
||||||
|
|
||||||
Templates::DetectFields.call(io, attachment: document) do |(attachment_uuid, page, fields)|
|
Templates::DetectFields.call(io, attachment: document, page_number:) do |(attachment_uuid, page, fields)|
|
||||||
sse.write({ attachment_uuid:, page:, fields: })
|
sse.write({ attachment_uuid:, page:, fields: })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -58,8 +58,8 @@ class TemplatesPreferencesController < ApplicationController
|
|||||||
default_expire_at_duration shared_link_2fa default_expire_at request_email_enabled
|
default_expire_at_duration shared_link_2fa 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],
|
||||||
submitters: [%i[uuid request_email_subject request_email_body]], link_form_fields: []]
|
submitters: [%i[uuid request_email_subject request_email_body]], link_form_fields: [] }]
|
||||||
).tap do |attrs|
|
).tap do |attrs|
|
||||||
attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1'
|
attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1'
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ class TemplatesPreviewController < ApplicationController
|
|||||||
def show
|
def show
|
||||||
ActiveRecord::Associations::Preloader.new(
|
ActiveRecord::Associations::Preloader.new(
|
||||||
records: [@template],
|
records: [@template],
|
||||||
associations: [schema_documents: { preview_images_attachments: :blob }]
|
associations: [{ schema_documents: { preview_images_attachments: :blob } }]
|
||||||
).call
|
).call
|
||||||
|
|
||||||
@template_data =
|
@template_data =
|
||||||
@template.as_json.merge(
|
@template.as_json.merge(
|
||||||
documents: @template.schema_documents.as_json(
|
documents: @template.schema_documents.as_json(
|
||||||
methods: %i[metadata signed_uuid],
|
methods: %i[metadata signed_key],
|
||||||
include: { preview_images: { methods: %i[url metadata filename] } }
|
include: { preview_images: { methods: %i[url metadata filename] } }
|
||||||
)
|
)
|
||||||
).to_json
|
).to_json
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class TemplatesRecipientsController < ApplicationController
|
|||||||
|
|
||||||
def submitters_params
|
def submitters_params
|
||||||
permit_params = { submitters: [%i[name uuid is_requester optional_invite_by_uuid
|
permit_params = { submitters: [%i[name uuid is_requester optional_invite_by_uuid
|
||||||
invite_by_uuid linked_to_uuid email option order]] }
|
invite_by_uuid invite_via_field_uuid linked_to_uuid email option order]] }
|
||||||
|
|
||||||
params.require(:template).permit(permit_params).fetch(:submitters, {}).values.filter_map do |s|
|
params.require(:template).permit(permit_params).fetch(:submitters, {}).values.filter_map do |s|
|
||||||
next if s[:uuid].blank?
|
next if s[:uuid].blank?
|
||||||
@@ -36,6 +36,7 @@ class TemplatesRecipientsController < ApplicationController
|
|||||||
s[:order] = s[:order].to_i if s[:order].present?
|
s[:order] = s[:order].to_i if s[:order].present?
|
||||||
s.delete(:invite_by_uuid) if s[:invite_by_uuid].blank?
|
s.delete(:invite_by_uuid) if s[:invite_by_uuid].blank?
|
||||||
s.delete(:optional_invite_by_uuid) if s[:optional_invite_by_uuid].blank?
|
s.delete(:optional_invite_by_uuid) if s[:optional_invite_by_uuid].blank?
|
||||||
|
s.delete(:invite_via_field_uuid) if s[:invite_via_field_uuid].blank?
|
||||||
|
|
||||||
normalize_option_value(s)
|
normalize_option_value(s)
|
||||||
end
|
end
|
||||||
@@ -53,6 +54,7 @@ class TemplatesRecipientsController < ApplicationController
|
|||||||
attrs.delete(:email)
|
attrs.delete(:email)
|
||||||
attrs.delete(:linked_to_uuid)
|
attrs.delete(:linked_to_uuid)
|
||||||
attrs.delete(:invite_by_uuid)
|
attrs.delete(:invite_by_uuid)
|
||||||
|
attrs.delete(:invite_via_field_uuid)
|
||||||
attrs.delete(:optional_invite_by_uuid)
|
attrs.delete(:optional_invite_by_uuid)
|
||||||
when /\Alinked_to_(.*)\z/
|
when /\Alinked_to_(.*)\z/
|
||||||
attrs[:linked_to_uuid] = ::Regexp.last_match(-1)
|
attrs[:linked_to_uuid] = ::Regexp.last_match(-1)
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ class TemplatesRestoreController < ApplicationController
|
|||||||
def create
|
def create
|
||||||
@template.update!(archived_at: nil)
|
@template.update!(archived_at: nil)
|
||||||
|
|
||||||
|
WebhookUrls.enqueue_events(@template, 'template.updated')
|
||||||
|
|
||||||
redirect_to template_path(@template), notice: I18n.t('template_has_been_unarchived')
|
redirect_to template_path(@template), notice: I18n.t('template_has_been_unarchived')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -12,7 +12,7 @@ class TemplatesUploadsController < ApplicationController
|
|||||||
|
|
||||||
save_template!(@template, url_params)
|
save_template!(@template, url_params)
|
||||||
|
|
||||||
documents = Templates::CreateAttachments.call(@template, url_params || params, extract_fields: true)
|
documents, = Templates::CreateAttachments.call(@template, url_params || params, extract_fields: true)
|
||||||
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
|
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
|
||||||
|
|
||||||
if @template.fields.blank?
|
if @template.fields.blank?
|
||||||
@@ -56,7 +56,7 @@ class TemplatesUploadsController < ApplicationController
|
|||||||
def create_file_params_from_url
|
def create_file_params_from_url
|
||||||
tempfile = Tempfile.new
|
tempfile = Tempfile.new
|
||||||
tempfile.binmode
|
tempfile.binmode
|
||||||
tempfile.write(DownloadUtils.call(params[:url]).body)
|
tempfile.write(DownloadUtils.call(params[:url], validate: true).body)
|
||||||
tempfile.rewind
|
tempfile.rewind
|
||||||
|
|
||||||
filename = URI.decode_www_form_component(params[:filename]) if params[:filename].present?
|
filename = URI.decode_www_form_component(params[:filename]) if params[:filename].present?
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class WebhookEventsController < ApplicationController
|
|||||||
Submissions::SerializeForApi.call(@webhook_event.record)
|
Submissions::SerializeForApi.call(@webhook_event.record)
|
||||||
when 'template.created', 'template.updated'
|
when 'template.created', 'template.updated'
|
||||||
Templates::SerializeForApi.call(@webhook_event.record)
|
Templates::SerializeForApi.call(@webhook_event.record)
|
||||||
when 'submission.archived'
|
when 'submission.archived', 'template.archived'
|
||||||
@webhook_event.record.as_json(only: %i[id archived_at])
|
@webhook_event.record.as_json(only: %i[id archived_at])
|
||||||
end
|
end
|
||||||
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
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import DashboardDropzone from './elements/dashboard_dropzone'
|
|||||||
import RequiredCheckboxGroup from './elements/required_checkbox_group'
|
import RequiredCheckboxGroup from './elements/required_checkbox_group'
|
||||||
import PageContainer from './elements/page_container'
|
import PageContainer from './elements/page_container'
|
||||||
import EmailEditor from './elements/email_editor'
|
import EmailEditor from './elements/email_editor'
|
||||||
|
import MarkdownEditor from './elements/markdown_editor'
|
||||||
import MountOnClick from './elements/mount_on_click'
|
import MountOnClick from './elements/mount_on_click'
|
||||||
import RemoveOnEvent from './elements/remove_on_event'
|
import RemoveOnEvent from './elements/remove_on_event'
|
||||||
import ScrollTo from './elements/scroll_to'
|
import ScrollTo from './elements/scroll_to'
|
||||||
@@ -131,6 +132,7 @@ safeRegisterElement('check-on-click', CheckOnClick)
|
|||||||
safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup)
|
safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup)
|
||||||
safeRegisterElement('page-container', PageContainer)
|
safeRegisterElement('page-container', PageContainer)
|
||||||
safeRegisterElement('email-editor', EmailEditor)
|
safeRegisterElement('email-editor', EmailEditor)
|
||||||
|
safeRegisterElement('markdown-editor', MarkdownEditor)
|
||||||
safeRegisterElement('mount-on-click', MountOnClick)
|
safeRegisterElement('mount-on-click', MountOnClick)
|
||||||
safeRegisterElement('remove-on-event', RemoveOnEvent)
|
safeRegisterElement('remove-on-event', RemoveOnEvent)
|
||||||
safeRegisterElement('scroll-to', ScrollTo)
|
safeRegisterElement('scroll-to', ScrollTo)
|
||||||
@@ -153,23 +155,30 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
|||||||
|
|
||||||
this.appElem.classList.add('md:h-screen')
|
this.appElem.classList.add('md:h-screen')
|
||||||
|
|
||||||
|
const template = reactive(JSON.parse(this.dataset.template))
|
||||||
|
|
||||||
this.app = createApp(TemplateBuilder, {
|
this.app = createApp(TemplateBuilder, {
|
||||||
template: reactive(JSON.parse(this.dataset.template)),
|
template,
|
||||||
|
customFields: reactive(JSON.parse(this.dataset.customFields || '[]')),
|
||||||
|
dynamicDocuments: reactive(JSON.parse(this.dataset.dynamicDocuments || '[]')),
|
||||||
backgroundColor: '#faf7f5',
|
backgroundColor: '#faf7f5',
|
||||||
locale: this.dataset.locale,
|
locale: this.dataset.locale,
|
||||||
withPhone: this.dataset.withPhone === 'true',
|
withPhone: this.dataset.withPhone === 'true',
|
||||||
|
withPrefillable: template.fields?.some((f) => f.prefillable),
|
||||||
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,
|
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',
|
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',
|
||||||
withSendButton: this.dataset.withSendButton !== 'false',
|
withSendButton: this.dataset.withSendButton !== 'false',
|
||||||
withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false',
|
withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false',
|
||||||
withConditions: this.dataset.withConditions === 'true',
|
withConditions: this.dataset.withConditions === 'true',
|
||||||
|
withDynamicDocuments: this.dataset.withDynamicDocuments === 'true',
|
||||||
withGoogleDrive: this.dataset.withGoogleDrive === 'true',
|
withGoogleDrive: this.dataset.withGoogleDrive === 'true',
|
||||||
withReplaceAndCloneUpload: true,
|
withReplaceAndCloneUpload: true,
|
||||||
withDownload: true,
|
withDownload: true,
|
||||||
|
|||||||
@@ -155,3 +155,7 @@ button[disabled] .enabled, button.btn-disabled .enabled {
|
|||||||
.font-courier {
|
.font-courier {
|
||||||
font-family: "Courier New", Consolas, "Liberation Mono", monospace, ui-monospace, SFMono-Regular, Menlo, Monaco;
|
font-family: "Courier New", Consolas, "Liberation Mono", monospace, ui-monospace, SFMono-Regular, Menlo, Monaco;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
markdown-editor [contenteditable] p {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
+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'
|
||||||
|
|||||||
@@ -64,6 +64,22 @@ export default class extends HTMLElement {
|
|||||||
if (action === 'empty' || action === 'unchecked') return this.isEmpty(actual)
|
if (action === 'empty' || action === 'unchecked') return this.isEmpty(actual)
|
||||||
if (action === 'not_empty' || action === 'checked') 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') {
|
if (action === 'equal') {
|
||||||
const list = Array.isArray(actual) ? actual : [actual]
|
const list = Array.isArray(actual) ? actual : [actual]
|
||||||
return list.filter((v) => v !== null && v !== undefined).map(String).includes(String(expected))
|
return list.filter((v) => v !== null && v !== undefined).map(String).includes(String(expected))
|
||||||
|
|||||||
@@ -0,0 +1,382 @@
|
|||||||
|
import { target, targetable } from '@github/catalyst/lib/targetable'
|
||||||
|
import { actionable } from '@github/catalyst/lib/actionable'
|
||||||
|
|
||||||
|
function loadTiptap () {
|
||||||
|
return Promise.all([
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/core'),
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-bold'),
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-italic'),
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-paragraph'),
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-text'),
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-hard-break'),
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-document'),
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-link'),
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-underline'),
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extensions'),
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/markdown'),
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/pm/state'),
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/pm/view')
|
||||||
|
]).then(([core, bold, italic, paragraph, text, hardBreak, document, link, underline, extensions, markdown, pmState, pmView]) => ({
|
||||||
|
Editor: core.Editor,
|
||||||
|
Extension: core.Extension,
|
||||||
|
Bold: bold.default || bold,
|
||||||
|
Italic: italic.default || italic,
|
||||||
|
Paragraph: paragraph.default || paragraph,
|
||||||
|
Text: text.default || text,
|
||||||
|
HardBreak: hardBreak.default || hardBreak,
|
||||||
|
Document: document.default || document,
|
||||||
|
Link: link.default || link,
|
||||||
|
Underline: underline.default || underline,
|
||||||
|
UndoRedo: extensions.UndoRedo,
|
||||||
|
Markdown: markdown.Markdown,
|
||||||
|
Plugin: pmState.Plugin,
|
||||||
|
Decoration: pmView.Decoration,
|
||||||
|
DecorationSet: pmView.DecorationSet
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
class LinkTooltip {
|
||||||
|
constructor (container, editor, templateEl) {
|
||||||
|
this.container = container
|
||||||
|
this.editor = editor
|
||||||
|
|
||||||
|
this.tooltip = templateEl.content.firstElementChild.cloneNode(true)
|
||||||
|
|
||||||
|
this.input = this.tooltip.querySelector('input')
|
||||||
|
this.saveButton = this.tooltip.querySelector('[data-role="link-save"]')
|
||||||
|
this.removeButton = this.tooltip.querySelector('[data-role="link-remove"]')
|
||||||
|
|
||||||
|
container.style.position = 'relative'
|
||||||
|
container.appendChild(this.tooltip)
|
||||||
|
}
|
||||||
|
|
||||||
|
isVisible () {
|
||||||
|
return !this.tooltip.classList.contains('hidden')
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizeUrl (url) {
|
||||||
|
if (!url) return url
|
||||||
|
if (/^{/i.test(url)) return url
|
||||||
|
if (/^https?:\/\//i.test(url)) return url
|
||||||
|
if (/^mailto:/i.test(url)) return url
|
||||||
|
|
||||||
|
return `https://${url}`
|
||||||
|
}
|
||||||
|
|
||||||
|
show (url, pos, { focus = false } = {}) {
|
||||||
|
this.input.value = url || ''
|
||||||
|
this.removeButton.classList.toggle('hidden', !url)
|
||||||
|
|
||||||
|
this.tooltip.classList.remove('hidden')
|
||||||
|
|
||||||
|
const coords = this.editor.view.coordsAtPos(pos)
|
||||||
|
const containerRect = this.container.getBoundingClientRect()
|
||||||
|
|
||||||
|
this.tooltip.style.left = `${coords.left - containerRect.left}px`
|
||||||
|
this.tooltip.style.top = `${coords.bottom - containerRect.top + 4}px`
|
||||||
|
|
||||||
|
if (focus) this.input.focus()
|
||||||
|
|
||||||
|
this.saveHandler = () => {
|
||||||
|
const inputUrl = this.input.value.trim()
|
||||||
|
|
||||||
|
if (inputUrl) {
|
||||||
|
this.editor.chain().focus().extendMarkRange('link').setLink({ href: this.normalizeUrl(inputUrl) }).run()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.removeHandler = () => {
|
||||||
|
this.editor.chain().focus().extendMarkRange('link').unsetLink().run()
|
||||||
|
|
||||||
|
this.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.keyHandler = (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault()
|
||||||
|
this.saveHandler()
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
e.preventDefault()
|
||||||
|
this.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveButton.addEventListener('click', this.saveHandler, { once: true })
|
||||||
|
this.removeButton.addEventListener('click', this.removeHandler, { once: true })
|
||||||
|
this.input.addEventListener('keydown', this.keyHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
hide () {
|
||||||
|
if (this.saveHandler) {
|
||||||
|
this.saveButton.removeEventListener('click', this.saveHandler)
|
||||||
|
this.saveHandler = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.removeHandler) {
|
||||||
|
this.removeButton.removeEventListener('click', this.removeHandler)
|
||||||
|
this.removeHandler = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.keyHandler) {
|
||||||
|
this.input.removeEventListener('keydown', this.keyHandler)
|
||||||
|
this.keyHandler = null
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tooltip.classList.add('hidden')
|
||||||
|
this.currentMark = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default actionable(targetable(class extends HTMLElement {
|
||||||
|
static [target.static] = [
|
||||||
|
'textarea',
|
||||||
|
'editorElement',
|
||||||
|
'boldButton',
|
||||||
|
'italicButton',
|
||||||
|
'underlineButton',
|
||||||
|
'linkButton',
|
||||||
|
'linkTooltipTemplate'
|
||||||
|
]
|
||||||
|
|
||||||
|
async connectedCallback () {
|
||||||
|
if (!this.textarea || !this.editorElement) return
|
||||||
|
|
||||||
|
this.textarea.style.display = 'none'
|
||||||
|
this.adjustShortcutsForPlatform()
|
||||||
|
|
||||||
|
const { Editor, Extension, Bold, Italic, Paragraph, Text, HardBreak, UndoRedo, Document, Link, Underline, Markdown, Plugin, Decoration, DecorationSet } = await loadTiptap()
|
||||||
|
|
||||||
|
const buildDecorations = (doc) => {
|
||||||
|
const decorations = []
|
||||||
|
const regex = /\{\{?[a-zA-Z0-9_.-]+\}\}?/g
|
||||||
|
|
||||||
|
doc.descendants((node, pos) => {
|
||||||
|
if (!node.isText) return
|
||||||
|
|
||||||
|
let match
|
||||||
|
|
||||||
|
while ((match = regex.exec(node.text)) !== null) {
|
||||||
|
decorations.push(
|
||||||
|
Decoration.inline(pos + match.index, pos + match.index + match[0].length, {
|
||||||
|
class: 'bg-amber-100 py-0.5 px-1 rounded'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return DecorationSet.create(doc, decorations)
|
||||||
|
}
|
||||||
|
|
||||||
|
const VariableHighlight = Extension.create({
|
||||||
|
name: 'variableHighlight',
|
||||||
|
addProseMirrorPlugins () {
|
||||||
|
return [new Plugin({
|
||||||
|
state: {
|
||||||
|
init (_, { doc }) {
|
||||||
|
return buildDecorations(doc)
|
||||||
|
},
|
||||||
|
apply (tr, oldSet) {
|
||||||
|
return tr.docChanged ? buildDecorations(tr.doc) : oldSet
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
decorations (state) {
|
||||||
|
return this.getState(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.editor = new Editor({
|
||||||
|
element: this.editorElement,
|
||||||
|
extensions: [
|
||||||
|
Markdown,
|
||||||
|
Document,
|
||||||
|
Paragraph,
|
||||||
|
Text,
|
||||||
|
Bold,
|
||||||
|
Italic,
|
||||||
|
HardBreak.extend({
|
||||||
|
addKeyboardShortcuts () {
|
||||||
|
return {
|
||||||
|
Enter: () => this.editor.commands.setHardBreak()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
UndoRedo,
|
||||||
|
Link.extend({
|
||||||
|
inclusive: true,
|
||||||
|
addKeyboardShortcuts: () => ({
|
||||||
|
'Mod-k': () => {
|
||||||
|
this.toggleLink()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}).configure({
|
||||||
|
openOnClick: false,
|
||||||
|
HTMLAttributes: {
|
||||||
|
class: 'link',
|
||||||
|
'data-turbo': 'false',
|
||||||
|
style: 'color: #2563eb; text-decoration: underline; cursor: text;'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Underline,
|
||||||
|
VariableHighlight
|
||||||
|
],
|
||||||
|
content: (this.textarea.value || '').trim().replace(/ *\n/g, '<br>'),
|
||||||
|
contentType: 'markdown',
|
||||||
|
editorProps: {
|
||||||
|
attributes: {
|
||||||
|
style: 'min-height: 220px',
|
||||||
|
dir: 'auto',
|
||||||
|
class: 'p-3 outline-none focus:outline-none'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onUpdate: ({ editor }) => {
|
||||||
|
this.textarea.value = editor.getMarkdown()
|
||||||
|
this.textarea.dispatchEvent(new Event('input', { bubbles: true }))
|
||||||
|
},
|
||||||
|
onSelectionUpdate: ({ editor }) => {
|
||||||
|
this.updateToolbarState()
|
||||||
|
this.handleLinkTooltip(editor)
|
||||||
|
},
|
||||||
|
onBlur: () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.linkTooltip.tooltip.contains(document.activeElement)) {
|
||||||
|
this.linkTooltip.hide()
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.linkTooltip = new LinkTooltip(this, this.editor, this.linkTooltipTemplate)
|
||||||
|
}
|
||||||
|
|
||||||
|
adjustShortcutsForPlatform () {
|
||||||
|
if ((navigator.userAgentData?.platform || navigator.platform)?.toLowerCase()?.includes('mac')) {
|
||||||
|
this.querySelectorAll('.tooltip[data-tip]').forEach(tooltip => {
|
||||||
|
const tip = tooltip.getAttribute('data-tip')
|
||||||
|
|
||||||
|
if (tip && tip.includes('Ctrl')) {
|
||||||
|
tooltip.setAttribute('data-tip', tip.replace(/Ctrl/g, '⌘'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bold (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.editor.chain().focus().toggleBold().run()
|
||||||
|
this.updateToolbarState()
|
||||||
|
}
|
||||||
|
|
||||||
|
italic (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.editor.chain().focus().toggleItalic().run()
|
||||||
|
this.updateToolbarState()
|
||||||
|
}
|
||||||
|
|
||||||
|
underline (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.editor.chain().focus().toggleUnderline().run()
|
||||||
|
this.updateToolbarState()
|
||||||
|
}
|
||||||
|
|
||||||
|
linkSelection (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.toggleLink()
|
||||||
|
this.updateToolbarState()
|
||||||
|
}
|
||||||
|
|
||||||
|
undo (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.editor.chain().focus().undo().run()
|
||||||
|
this.updateToolbarState()
|
||||||
|
}
|
||||||
|
|
||||||
|
redo (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.editor.chain().focus().redo().run()
|
||||||
|
this.updateToolbarState()
|
||||||
|
}
|
||||||
|
|
||||||
|
updateToolbarState () {
|
||||||
|
this.boldButton.classList.toggle('bg-base-200', this.editor.isActive('bold'))
|
||||||
|
this.italicButton.classList.toggle('bg-base-200', this.editor.isActive('italic'))
|
||||||
|
this.underlineButton.classList.toggle('bg-base-200', this.editor.isActive('underline'))
|
||||||
|
this.linkButton.classList.toggle('bg-base-200', this.editor.isActive('link'))
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLinkTooltip (editor) {
|
||||||
|
const { from } = editor.state.selection
|
||||||
|
const mark = editor.state.doc.resolve(from).marks().find(m => m.type.name === 'link')
|
||||||
|
|
||||||
|
if (!mark) {
|
||||||
|
if (this.linkTooltip.isVisible()) this.linkTooltip.hide()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.linkTooltip.isVisible() && this.linkTooltip.currentMark === mark) return
|
||||||
|
|
||||||
|
let linkStart = from
|
||||||
|
const start = editor.state.doc.resolve(from).start()
|
||||||
|
|
||||||
|
for (let i = from - 1; i >= start; i--) {
|
||||||
|
if (editor.state.doc.resolve(i).marks().some(m => m.eq(mark))) {
|
||||||
|
linkStart = i
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.linkTooltip.hide()
|
||||||
|
this.linkTooltip.show(mark.attrs.href, linkStart > start ? linkStart - 1 : linkStart)
|
||||||
|
this.linkTooltip.currentMark = mark
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleLink () {
|
||||||
|
if (this.editor.isActive('link')) {
|
||||||
|
this.linkTooltip.hide()
|
||||||
|
this.editor.chain().focus().extendMarkRange('link').unsetLink().run()
|
||||||
|
this.updateToolbarState()
|
||||||
|
} else {
|
||||||
|
const { from } = this.editor.state.selection
|
||||||
|
|
||||||
|
this.linkTooltip.hide()
|
||||||
|
this.linkTooltip.show(this.editor.getAttributes('link').href, from, { focus: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
insertVariable (e) {
|
||||||
|
const variable = e.target.closest('[data-variable]')?.dataset.variable
|
||||||
|
|
||||||
|
if (variable) {
|
||||||
|
const { from, to } = this.editor.state.selection
|
||||||
|
|
||||||
|
if (variable.includes('link') && from !== to) {
|
||||||
|
this.editor.chain().focus().setLink({ href: `{${variable}}` }).run()
|
||||||
|
} else {
|
||||||
|
this.editor.chain().focus().insertContent(`{${variable}}`).run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
this.linkTooltip.hide()
|
||||||
|
|
||||||
|
if (this.editor) {
|
||||||
|
this.editor.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
@@ -1,12 +1,18 @@
|
|||||||
export default class extends HTMLElement {
|
export default class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.input.addEventListener('change', (event) => {
|
this.input.addEventListener('change', (event) => {
|
||||||
|
if (!this.target) return
|
||||||
|
|
||||||
|
const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value
|
||||||
|
const dataValue = this.dataset.value === 'false' ? false : this.dataset.value || true
|
||||||
|
|
||||||
if (this.dataset.attribute) {
|
if (this.dataset.attribute) {
|
||||||
this.target[this.dataset.attribute] = event.target.checked
|
this.target[this.dataset.attribute] = value === dataValue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.dataset.className) {
|
if (this.dataset.className) {
|
||||||
this.target.classList.toggle(this.dataset.className, event.target.value !== this.dataset.value)
|
this.target.classList.toggle(this.dataset.className, value !== dataValue)
|
||||||
|
|
||||||
if (this.dataset.className === 'hidden' && this.target.tagName === 'INPUT') {
|
if (this.dataset.className === 'hidden' && this.target.tagName === 'INPUT') {
|
||||||
this.target.disabled = event.target.value !== this.dataset.value
|
this.target.disabled = event.target.value !== this.dataset.value
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
export default class extends HTMLElement {
|
export default class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
const button = this.querySelector('a, button')
|
const button = this.querySelector('a, button, label')
|
||||||
|
|
||||||
|
const target = this.dataset.targetId ? document.getElementById(this.dataset.targetId) : button
|
||||||
|
|
||||||
button.addEventListener('click', () => {
|
button.addEventListener('click', () => {
|
||||||
this.dataset.classes.split(' ').forEach((cls) => {
|
this.dataset.classes.split(' ').forEach((cls) => {
|
||||||
button.classList.toggle(cls)
|
if (this.dataset.action === 'remove') {
|
||||||
|
target.classList.remove(cls)
|
||||||
|
} else if (this.dataset.action === 'add') {
|
||||||
|
target.classList.add(cls)
|
||||||
|
} else {
|
||||||
|
target.classList.toggle(cls)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,6 @@ export default class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get button () {
|
get button () {
|
||||||
return this.querySelector('button')
|
return this.querySelector('button, label')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ safeRegisterElement('submission-form', class extends HTMLElement {
|
|||||||
expand: ['true', 'false'].includes(this.dataset.expand) ? this.dataset.expand === 'true' : null,
|
expand: ['true', 'false'].includes(this.dataset.expand) ? this.dataset.expand === 'true' : null,
|
||||||
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 === 'true',
|
||||||
withFieldLabels: this.dataset.withFieldLabels !== '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',
|
||||||
|
|||||||
@@ -182,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"
|
||||||
@@ -461,7 +461,7 @@ export default {
|
|||||||
phone: IconPhoneCheck,
|
phone: IconPhoneCheck,
|
||||||
payment: IconCreditCard,
|
payment: IconCreditCard,
|
||||||
verification: IconId,
|
verification: IconId,
|
||||||
kba: IconUserScan,
|
kba: IconUserScan
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
image () {
|
image () {
|
||||||
|
|||||||
@@ -161,6 +161,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
fetchOptions: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
completedButton: {
|
completedButton: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -214,7 +219,10 @@ export default {
|
|||||||
download () {
|
download () {
|
||||||
this.isDownloading = true
|
this.isDownloading = true
|
||||||
|
|
||||||
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`).then(async (response) => {
|
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`, {
|
||||||
|
method: 'GET',
|
||||||
|
...this.fetchOptions
|
||||||
|
}).then(async (response) => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const urls = await response.json()
|
const urls = await response.json()
|
||||||
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
|
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
<MarkdownContent :string="field.description" />
|
<MarkdownContent :string="field.description" />
|
||||||
</div>
|
</div>
|
||||||
<AppearsOn :field="field" />
|
<AppearsOn :field="field" />
|
||||||
<div class="text-center">
|
<div class="text-center flex">
|
||||||
<input
|
<input
|
||||||
:id="field.uuid"
|
:id="field.uuid"
|
||||||
ref="input"
|
ref="input"
|
||||||
|
|||||||
@@ -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" />
|
||||||
@@ -395,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"
|
||||||
@@ -407,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"
|
||||||
@@ -418,6 +421,7 @@
|
|||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
:dry-run="dryRun"
|
:dry-run="dryRun"
|
||||||
|
:submitter="submitter"
|
||||||
:previous-value="previousInitialsValue"
|
:previous-value="previousInitialsValue"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
:show-field-names="showFieldNames"
|
:show-field-names="showFieldNames"
|
||||||
@@ -526,6 +530,7 @@
|
|||||||
v-else-if="isInvite"
|
v-else-if="isInvite"
|
||||||
:submitters="inviteSubmitters"
|
:submitters="inviteSubmitters"
|
||||||
:optional-submitters="optionalInviteSubmitters"
|
:optional-submitters="optionalInviteSubmitters"
|
||||||
|
:fetch-options="fetchOptions"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
:authenticity-token="authenticityToken"
|
:authenticity-token="authenticityToken"
|
||||||
:url="baseUrl + submitPath + '/invite'"
|
:url="baseUrl + submitPath + '/invite'"
|
||||||
@@ -539,6 +544,7 @@
|
|||||||
:has-signature-fields="stepFields.some((fields) => fields.some((f) => ['signature', 'initials'].includes(f.type)))"
|
:has-signature-fields="stepFields.some((fields) => fields.some((f) => ['signature', 'initials'].includes(f.type)))"
|
||||||
:has-multiple-documents="hasMultipleDocuments"
|
:has-multiple-documents="hasMultipleDocuments"
|
||||||
:completed-button="completedRedirectUrl ? {} : completedButton"
|
:completed-button="completedRedirectUrl ? {} : completedButton"
|
||||||
|
:fetch-options="fetchOptions"
|
||||||
:completed-message="completedRedirectUrl ? {} : completedMessage"
|
:completed-message="completedRedirectUrl ? {} : completedMessage"
|
||||||
:with-send-copy-button="withSendCopyButton && !completedRedirectUrl"
|
:with-send-copy-button="withSendCopyButton && !completedRedirectUrl"
|
||||||
:with-download-button="withDownloadButton && !completedRedirectUrl && !dryRun"
|
:with-download-button="withDownloadButton && !completedRedirectUrl && !dryRun"
|
||||||
@@ -551,14 +557,18 @@
|
|||||||
class="flex justify-center mt-3 sm:mt-4 mb-0 sm:mb-1 select-none"
|
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
|
<template
|
||||||
v-for="(step, index) in stepFields"
|
v-for="(step, index) in stepFields"
|
||||||
:key="step[0].uuid"
|
:key="step[0].uuid"
|
||||||
href="#"
|
>
|
||||||
class="inline border border-base-300 h-3 w-3 rounded-full mx-1 mt-1"
|
<a
|
||||||
:class="{ 'bg-base-300 steps-progress-current': index === currentStep, 'bg-base-content': (index < currentStep && stepFields[index].every((f) => !f.required || ![null, undefined, ''].includes(values[f.uuid]))) || isCompleted, 'bg-white': index > currentStep }"
|
v-if="!onlyRequiredFields || step.some((f) => f.required)"
|
||||||
@click.prevent="isCompleted ? '' : [saveStep(), goToStep(index, true)]"
|
href="#"
|
||||||
/>
|
class="inline border border-base-300 h-3 w-3 rounded-full mx-1 mt-1"
|
||||||
|
:class="{ 'bg-base-300 steps-progress-current': index === currentStep, 'bg-base-content': (index < currentStep && stepFields[index].every((f) => !f.required || ![null, undefined, ''].includes(values[f.uuid]))) || isCompleted, 'bg-white': index > currentStep }"
|
||||||
|
@click.prevent="isCompleted ? '' : [saveStep(), goToStep(index, true)]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -592,6 +602,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
|
||||||
|
|
||||||
@@ -658,6 +680,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
fetchOptions: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
optionalInviteSubmitters: {
|
optionalInviteSubmitters: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -678,6 +705,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
onlyRequiredFields: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
requireSigningReason: {
|
requireSigningReason: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -914,10 +946,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
|
||||||
}
|
}
|
||||||
@@ -938,7 +972,13 @@ export default {
|
|||||||
submitButtonText () {
|
submitButtonText () {
|
||||||
if (this.alwaysMinimize) {
|
if (this.alwaysMinimize) {
|
||||||
return this.t('submit')
|
return this.t('submit')
|
||||||
} else if (this.stepFields.length === this.currentStep + 1) {
|
} else if (!this.onlyRequiredFields && this.stepFields.length === this.currentStep + 1) {
|
||||||
|
if (this.currentField.type === 'signature') {
|
||||||
|
return this.t('sign_and_complete')
|
||||||
|
} else {
|
||||||
|
return this.t('complete')
|
||||||
|
}
|
||||||
|
} else if (this.onlyRequiredFields && !this.findNextStep(this.currentStep)) {
|
||||||
if (this.currentField.type === 'signature') {
|
if (this.currentField.type === 'signature') {
|
||||||
return this.t('sign_and_complete')
|
return this.t('sign_and_complete')
|
||||||
} else {
|
} else {
|
||||||
@@ -984,7 +1024,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 {
|
||||||
@@ -1011,7 +1053,9 @@ 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 = []
|
||||||
@@ -1066,10 +1110,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 {
|
||||||
@@ -1081,7 +1127,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) => {
|
||||||
@@ -1143,15 +1191,17 @@ 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]
|
||||||
|
|
||||||
if (requiredEmptyStepIndex !== -1) {
|
if (requiredEmptyStepIndex !== -1 && (!this.onlyRequiredFields || this.stepFields[requiredEmptyStepIndex].some((f) => f.required))) {
|
||||||
indexesList.push(requiredEmptyStepIndex)
|
indexesList.push(requiredEmptyStepIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastFilledStepIndex !== -1) {
|
if (lastFilledStepIndex !== -1 && (!this.onlyRequiredFields || this.stepFields[lastFilledStepIndex].some((f) => f.required))) {
|
||||||
indexesList.push(lastFilledStepIndex)
|
indexesList.push(lastFilledStepIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1211,27 +1261,35 @@ export default {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
checkFieldConditions (field) {
|
checkFieldConditions (field, cache = {}) {
|
||||||
|
const cacheKey = field.uuid || field.attachment_uuid
|
||||||
|
|
||||||
|
if (cache[cacheKey] !== undefined) {
|
||||||
|
return cache[cacheKey]
|
||||||
|
}
|
||||||
|
|
||||||
if (field.conditions?.length) {
|
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[cacheKey] = !result.includes(false)
|
||||||
} else {
|
} else {
|
||||||
return true
|
cache[cacheKey] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return cache[cacheKey]
|
||||||
},
|
},
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1241,20 +1299,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
|
||||||
}
|
}
|
||||||
@@ -1266,6 +1354,13 @@ export default {
|
|||||||
return `${this.t('option')} ${index + 1}`
|
return `${this.t('option')} ${index + 1}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
findNextStep (currentStepIndex) {
|
||||||
|
if (this.onlyRequiredFields) {
|
||||||
|
return this.stepFields.find((step, index) => index > currentStepIndex && step.some((f) => f.required))
|
||||||
|
} else {
|
||||||
|
return this.stepFields[currentStepIndex + 1]
|
||||||
|
}
|
||||||
|
},
|
||||||
maybeTrackEmailClick () {
|
maybeTrackEmailClick () {
|
||||||
const { queryParams } = this
|
const { queryParams } = this
|
||||||
|
|
||||||
@@ -1327,9 +1422,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 {
|
||||||
@@ -1379,7 +1474,8 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
return fetch(this.baseUrl + this.submitPath, {
|
return fetch(this.baseUrl + this.submitPath, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData || new FormData(this.$refs.form)
|
body: formData || new FormData(this.$refs.form),
|
||||||
|
...this.fetchOptions
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
currentFieldUuids.forEach((fieldUuid) => {
|
currentFieldUuids.forEach((fieldUuid) => {
|
||||||
@@ -1412,7 +1508,7 @@ export default {
|
|||||||
this.isSubmittingComplete = true
|
this.isSubmittingComplete = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const submitStep = this.currentStep
|
const submitStepIndex = this.currentStep
|
||||||
|
|
||||||
const stepPromise = ['signature', 'phone', 'initials', 'payment', 'verification', 'kba'].includes(this.currentField.type)
|
const stepPromise = ['signature', 'phone', 'initials', 'payment', 'verification', 'kba'].includes(this.currentField.type)
|
||||||
? this.$refs.currentStep.submit
|
? this.$refs.currentStep.submit
|
||||||
@@ -1420,7 +1516,7 @@ export default {
|
|||||||
|
|
||||||
stepPromise().then(async () => {
|
stepPromise().then(async () => {
|
||||||
const emptyRequiredField = this.stepFields.find((fields, index) => {
|
const emptyRequiredField = this.stepFields.find((fields, index) => {
|
||||||
if (forceComplete ? index === submitStep : index >= submitStep) {
|
if (forceComplete ? index === submitStepIndex : index >= submitStepIndex) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1430,7 +1526,7 @@ export default {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const formData = new FormData(this.$refs.form)
|
const formData = new FormData(this.$refs.form)
|
||||||
const isLastStep = (submitStep === this.stepFields.length - 1) || forceComplete
|
const isLastStep = (this.onlyRequiredFields ? !this.findNextStep(submitStepIndex) : (submitStepIndex === this.stepFields.length - 1)) || forceComplete
|
||||||
|
|
||||||
if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length && !this.optionalInviteSubmitters.length) {
|
if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length && !this.optionalInviteSubmitters.length) {
|
||||||
formData.append('completed', 'true')
|
formData.append('completed', 'true')
|
||||||
@@ -1474,7 +1570,7 @@ export default {
|
|||||||
return Promise.reject(new Error(data.error))
|
return Promise.reject(new Error(data.error))
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextStep = (isLastStep && emptyRequiredField) || (forceComplete ? null : this.stepFields[submitStep + 1])
|
const nextStep = (isLastStep && emptyRequiredField) || (forceComplete ? null : this.findNextStep(submitStepIndex))
|
||||||
|
|
||||||
if (nextStep) {
|
if (nextStep) {
|
||||||
if (this.alwaysMinimize) {
|
if (this.alwaysMinimize) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const en = {
|
const en = {
|
||||||
kba: 'KBA',
|
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 long',
|
||||||
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.',
|
||||||
verify_id: 'Verify ID',
|
verify_id: 'Verify ID',
|
||||||
identity_verification: 'Identity verification',
|
identity_verification: 'Identity verification',
|
||||||
@@ -97,6 +97,7 @@ const en = {
|
|||||||
upload: 'Upload',
|
upload: 'Upload',
|
||||||
files: 'Files',
|
files: 'Files',
|
||||||
signature_is_too_small_or_simple_please_redraw: 'Signature is too small or simple. Please redraw.',
|
signature_is_too_small_or_simple_please_redraw: 'Signature is too small or simple. Please redraw.',
|
||||||
|
browser_privacy_settings_block_canvas: 'Your browser privacy settings restrict use of the drawing canvas. Please use a different browser or device, or disable privacy settings that block canvas in order to sign.',
|
||||||
wait_countdown_seconds: 'Wait {countdown} seconds'
|
wait_countdown_seconds: 'Wait {countdown} seconds'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,6 +200,7 @@ const es = {
|
|||||||
upload: 'Subir',
|
upload: 'Subir',
|
||||||
files: 'Archivos',
|
files: 'Archivos',
|
||||||
signature_is_too_small_or_simple_please_redraw: 'La firma es demasiado pequeña o simple. Por favor, vuelve a dibujarla.',
|
signature_is_too_small_or_simple_please_redraw: 'La firma es demasiado pequeña o simple. Por favor, vuelve a dibujarla.',
|
||||||
|
browser_privacy_settings_block_canvas: 'La configuración de privacidad de su navegador restringe el uso del lienzo de dibujo. Utilice un navegador o dispositivo diferente, o desactive la configuración de privacidad que bloquea el lienzo para firmar.',
|
||||||
wait_countdown_seconds: 'Espera {countdown} segundos'
|
wait_countdown_seconds: 'Espera {countdown} segundos'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,6 +303,7 @@ const it = {
|
|||||||
upload: 'Carica',
|
upload: 'Carica',
|
||||||
files: 'File',
|
files: 'File',
|
||||||
signature_is_too_small_or_simple_please_redraw: 'La firma è troppo piccola o semplice. Ridisegnala, per favore.',
|
signature_is_too_small_or_simple_please_redraw: 'La firma è troppo piccola o semplice. Ridisegnala, per favore.',
|
||||||
|
browser_privacy_settings_block_canvas: 'Le impostazioni sulla privacy del browser limitano l\'uso dell\'area di disegno. Utilizza un browser o dispositivo diverso oppure disattiva le impostazioni sulla privacy che bloccano il canvas per firmare.',
|
||||||
wait_countdown_seconds: 'Attendi {countdown} secondi'
|
wait_countdown_seconds: 'Attendi {countdown} secondi'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,6 +406,7 @@ const de = {
|
|||||||
upload: 'Hochladen',
|
upload: 'Hochladen',
|
||||||
files: 'Dateien',
|
files: 'Dateien',
|
||||||
signature_is_too_small_or_simple_please_redraw: 'Die Unterschrift ist zu klein oder zu einfach. Bitte neu zeichnen.',
|
signature_is_too_small_or_simple_please_redraw: 'Die Unterschrift ist zu klein oder zu einfach. Bitte neu zeichnen.',
|
||||||
|
browser_privacy_settings_block_canvas: 'Die Datenschutzeinstellungen Ihres Browsers schränken die Nutzung der Zeichenfläche ein. Bitte verwenden Sie einen anderen Browser oder ein anderes Gerät oder deaktivieren Sie die Datenschutzeinstellungen, die Canvas blockieren, um zu unterschreiben.',
|
||||||
wait_countdown_seconds: 'Bitte {countdown} Sekunden warten'
|
wait_countdown_seconds: 'Bitte {countdown} Sekunden warten'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,6 +509,7 @@ const fr = {
|
|||||||
upload: 'Téléverser',
|
upload: 'Téléverser',
|
||||||
files: 'Fichiers',
|
files: 'Fichiers',
|
||||||
signature_is_too_small_or_simple_please_redraw: 'La signature est trop petite ou trop simple. Veuillez la redessiner.',
|
signature_is_too_small_or_simple_please_redraw: 'La signature est trop petite ou trop simple. Veuillez la redessiner.',
|
||||||
|
browser_privacy_settings_block_canvas: 'Les paramètres de confidentialité de votre navigateur empêchent l\'utilisation du canevas de dessin. Veuillez utiliser un autre navigateur ou appareil, ou désactiver les paramètres de confidentialité qui bloquent le canevas pour signer.',
|
||||||
wait_countdown_seconds: 'Veuillez patienter {countdown} secondes'
|
wait_countdown_seconds: 'Veuillez patienter {countdown} secondes'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -607,6 +612,7 @@ const pl = {
|
|||||||
upload: 'Przesyłanie',
|
upload: 'Przesyłanie',
|
||||||
files: 'Pliki',
|
files: 'Pliki',
|
||||||
signature_is_too_small_or_simple_please_redraw: 'Podpis jest zbyt mały lub zbyt prosty. Proszę narysować go ponownie.',
|
signature_is_too_small_or_simple_please_redraw: 'Podpis jest zbyt mały lub zbyt prosty. Proszę narysować go ponownie.',
|
||||||
|
browser_privacy_settings_block_canvas: 'Ustawienia prywatności przeglądarki blokują użycie obszaru rysowania. Użyj innej przeglądarki lub urządzenia albo wyłącz ustawienia prywatności blokujące canvas, aby podpisać.',
|
||||||
wait_countdown_seconds: 'Poczekaj {countdown} sekund'
|
wait_countdown_seconds: 'Poczekaj {countdown} sekund'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -709,6 +715,7 @@ const uk = {
|
|||||||
upload: 'Завантажити',
|
upload: 'Завантажити',
|
||||||
files: 'Файли',
|
files: 'Файли',
|
||||||
signature_is_too_small_or_simple_please_redraw: 'Підпис занадто маленький або надто простий. Будь ласка, перемалюйте.',
|
signature_is_too_small_or_simple_please_redraw: 'Підпис занадто маленький або надто простий. Будь ласка, перемалюйте.',
|
||||||
|
browser_privacy_settings_block_canvas: 'Налаштування конфіденційності вашого браузера блокують використання полотна для малювання. Будь ласка, скористайтеся іншим браузером або пристроєм, або вимкніть налаштування конфіденційності, що блокують canvas, щоб підписати.',
|
||||||
wait_countdown_seconds: 'Зачекайте {countdown} секунд'
|
wait_countdown_seconds: 'Зачекайте {countdown} секунд'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -811,6 +818,7 @@ const cs = {
|
|||||||
upload: 'Nahrát',
|
upload: 'Nahrát',
|
||||||
files: 'Soubory',
|
files: 'Soubory',
|
||||||
signature_is_too_small_or_simple_please_redraw: 'Podpis je příliš malý nebo jednoduchý. Nakreslete jej prosím znovu.',
|
signature_is_too_small_or_simple_please_redraw: 'Podpis je příliš malý nebo jednoduchý. Nakreslete jej prosím znovu.',
|
||||||
|
browser_privacy_settings_block_canvas: 'Nastavení soukromí vašeho prohlížeče omezuje použití kreslicího plátna. Použijte prosím jiný prohlížeč nebo zařízení, nebo vypněte nastavení soukromí blokující canvas pro podepsání.',
|
||||||
wait_countdown_seconds: 'Počkejte {countdown} sekund'
|
wait_countdown_seconds: 'Počkejte {countdown} sekund'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -913,6 +921,7 @@ const pt = {
|
|||||||
upload: 'Carregar',
|
upload: 'Carregar',
|
||||||
files: 'Arquivos',
|
files: 'Arquivos',
|
||||||
signature_is_too_small_or_simple_please_redraw: 'A assinatura é muito pequena ou simples. Por favor, redesenhe.',
|
signature_is_too_small_or_simple_please_redraw: 'A assinatura é muito pequena ou simples. Por favor, redesenhe.',
|
||||||
|
browser_privacy_settings_block_canvas: 'As configurações de privacidade do seu navegador restringem o uso da área de desenho. Use um navegador ou dispositivo diferente, ou desative as configurações de privacidade que bloqueiam o canvas para assinar.',
|
||||||
wait_countdown_seconds: 'Aguarde {countdown} segundos'
|
wait_countdown_seconds: 'Aguarde {countdown} segundos'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1015,6 +1024,7 @@ const he = {
|
|||||||
upload: 'העלאה',
|
upload: 'העלאה',
|
||||||
files: 'קבצים',
|
files: 'קבצים',
|
||||||
signature_is_too_small_or_simple_please_redraw: 'החתימה קטנה או פשוטה מדי. אנא חתום מחדש.',
|
signature_is_too_small_or_simple_please_redraw: 'החתימה קטנה או פשוטה מדי. אנא חתום מחדש.',
|
||||||
|
browser_privacy_settings_block_canvas: 'הגדרות הפרטיות של הדפדפן שלך מגבילות את השימוש באזור הציור. אנא השתמש בדפדפן או מכשיר אחר, או בטל את הגדרות הפרטיות החוסמות canvas כדי לחתום.',
|
||||||
wait_countdown_seconds: 'המתן {countdown} שניות'
|
wait_countdown_seconds: 'המתן {countdown} שניות'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1117,6 +1127,7 @@ const nl = {
|
|||||||
upload: 'Uploaden',
|
upload: 'Uploaden',
|
||||||
files: 'Bestanden',
|
files: 'Bestanden',
|
||||||
signature_is_too_small_or_simple_please_redraw: 'De handtekening is te klein of te eenvoudig. Teken opnieuw.',
|
signature_is_too_small_or_simple_please_redraw: 'De handtekening is te klein of te eenvoudig. Teken opnieuw.',
|
||||||
|
browser_privacy_settings_block_canvas: 'De privacyinstellingen van uw browser beperken het gebruik van het tekenveld. Gebruik een andere browser of ander apparaat, of schakel de privacyinstellingen uit die canvas blokkeren om te ondertekenen.',
|
||||||
wait_countdown_seconds: 'Wacht {countdown} seconden'
|
wait_countdown_seconds: 'Wacht {countdown} seconden'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1219,6 +1230,7 @@ const ar = {
|
|||||||
upload: 'تحميل',
|
upload: 'تحميل',
|
||||||
files: 'الملفات',
|
files: 'الملفات',
|
||||||
signature_is_too_small_or_simple_please_redraw: 'التوقيع صغير جدًا أو بسيط جدًا. يرجى إعادة رسمه.',
|
signature_is_too_small_or_simple_please_redraw: 'التوقيع صغير جدًا أو بسيط جدًا. يرجى إعادة رسمه.',
|
||||||
|
browser_privacy_settings_block_canvas: 'إعدادات الخصوصية في متصفحك تمنع استخدام لوحة الرسم. يرجى استخدام متصفح أو جهاز مختلف، أو تعطيل إعدادات الخصوصية التي تحظر canvas للتوقيع.',
|
||||||
wait_countdown_seconds: 'انتظر {countdown} ثانية'
|
wait_countdown_seconds: 'انتظر {countdown} ثانية'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1321,6 +1333,7 @@ const ko = {
|
|||||||
upload: '업로드',
|
upload: '업로드',
|
||||||
files: '파일',
|
files: '파일',
|
||||||
signature_is_too_small_or_simple_please_redraw: '서명이 너무 작거나 단순합니다. 다시 그려주세요.',
|
signature_is_too_small_or_simple_please_redraw: '서명이 너무 작거나 단순합니다. 다시 그려주세요.',
|
||||||
|
browser_privacy_settings_block_canvas: '브라우저 개인정보 보호 설정으로 인해 그리기 캔버스를 사용할 수 없습니다. 다른 브라우저나 기기를 사용하거나, 서명을 위해 캔버스를 차단하는 개인정보 보호 설정을 비활성화해 주세요.',
|
||||||
wait_countdown_seconds: '{countdown}초 기다리세요'
|
wait_countdown_seconds: '{countdown}초 기다리세요'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1423,6 +1436,7 @@ const ja = {
|
|||||||
upload: 'アップロード',
|
upload: 'アップロード',
|
||||||
files: 'ファイル',
|
files: 'ファイル',
|
||||||
signature_is_too_small_or_simple_please_redraw: '署名が小さすぎるか単純すぎます。もう一度描いてください。',
|
signature_is_too_small_or_simple_please_redraw: '署名が小さすぎるか単純すぎます。もう一度描いてください。',
|
||||||
|
browser_privacy_settings_block_canvas: 'ブラウザのプライバシー設定により、描画キャンバスの使用が制限されています。別のブラウザまたはデバイスを使用するか、署名するためにキャンバスをブロックするプライバシー設定を無効にしてください。',
|
||||||
wait_countdown_seconds: '{countdown} 秒お待ちください'
|
wait_countdown_seconds: '{countdown} 秒お待ちください'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -150,6 +150,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
||||||
|
import { isCanvasBlocked } from './validate_signature'
|
||||||
import { IconReload, IconTextSize, IconUpload, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
import { IconReload, IconTextSize, IconUpload, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||||
import SignaturePad from 'signature_pad'
|
import SignaturePad from 'signature_pad'
|
||||||
import AppearsOn from './appears_on'
|
import AppearsOn from './appears_on'
|
||||||
@@ -175,6 +176,10 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
submitter: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
dryRun: {
|
dryRun: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -257,6 +262,14 @@ export default {
|
|||||||
|
|
||||||
this.$refs.canvas.getContext('2d').scale(scale, scale)
|
this.$refs.canvas.getContext('2d').scale(scale, scale)
|
||||||
|
|
||||||
|
if (!this.isDrawInitials) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.textInput) {
|
||||||
|
this.initTextInitial()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
this.intersectionObserver?.disconnect()
|
this.intersectionObserver?.disconnect()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -332,12 +345,29 @@ export default {
|
|||||||
|
|
||||||
if (!this.isDrawInitials) {
|
if (!this.isDrawInitials) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$refs.textInput.focus()
|
if (this.$refs.textInput) {
|
||||||
|
if (!this.submitter.name) {
|
||||||
|
this.$refs.textInput.focus()
|
||||||
|
}
|
||||||
|
|
||||||
this.$emit('start')
|
this.initTextInitial()
|
||||||
|
|
||||||
|
this.$emit('start')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
initTextInitial () {
|
||||||
|
if (this.submitter.name) {
|
||||||
|
const parts = this.submitter.name.trim().split(/\s+/)
|
||||||
|
|
||||||
|
this.$refs.textInput.value = (parts.length > 1 ? [parts[0], parts[parts.length - 1]] : parts).map((part) => part[0]?.toUpperCase() || '').join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.$refs.textInput.value) {
|
||||||
|
this.updateWrittenInitials({ target: this.$refs.textInput })
|
||||||
|
}
|
||||||
|
},
|
||||||
async submit () {
|
async submit () {
|
||||||
if (this.modelValue || this.computedPreviousValue) {
|
if (this.modelValue || this.computedPreviousValue) {
|
||||||
if (this.computedPreviousValue) {
|
if (this.computedPreviousValue) {
|
||||||
@@ -392,7 +422,15 @@ export default {
|
|||||||
}
|
}
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
if (this.field.required === true) {
|
if (this.field.required === true) {
|
||||||
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
|
if (isCanvasBlocked()) {
|
||||||
|
alert(this.t('browser_privacy_settings_block_canvas'))
|
||||||
|
|
||||||
|
if (window.Rollbar) {
|
||||||
|
window.Rollbar.info('Canvas blocked')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
|
||||||
|
}
|
||||||
|
|
||||||
return reject(error)
|
return reject(error)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -78,6 +78,11 @@ export default {
|
|||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
fetchOptions: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
optionalSubmitters: {
|
optionalSubmitters: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -108,7 +113,8 @@ export default {
|
|||||||
|
|
||||||
return fetch(this.url, {
|
return fetch(this.url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: new FormData(this.$refs.form)
|
body: new FormData(this.$refs.form),
|
||||||
|
...this.fetchOptions
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
this.$emit('success')
|
this.$emit('success')
|
||||||
|
|||||||
@@ -10,9 +10,15 @@
|
|||||||
<template v-else>{{ field.name || 'Knowledge Based Authentication' }}</template>
|
<template v-else>{{ field.name || 'Knowledge Based Authentication' }}</template>
|
||||||
<span
|
<span
|
||||||
v-if="questions"
|
v-if="questions"
|
||||||
class="float-right text-base font-normal text-neutral-500 mt-1 whitespace-nowrap"
|
class="float-right font-normal text-neutral-500 mt-1 whitespace-nowrap text-right text-sm"
|
||||||
>
|
>
|
||||||
Question {{ currentQuestionIndex + 1 }} / {{ questions.length }}
|
<span>Question {{ currentQuestionIndex + 1 }} / {{ questions.length }}</span>
|
||||||
|
<span
|
||||||
|
v-if="timeLeftSeconds !== null"
|
||||||
|
class="block"
|
||||||
|
>
|
||||||
|
Time left: {{ formattedTimeLeft }}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
@@ -54,7 +60,9 @@
|
|||||||
<div v-else-if="questions && !error">
|
<div v-else-if="questions && !error">
|
||||||
<form @submit.prevent="nextQuestion">
|
<form @submit.prevent="nextQuestion">
|
||||||
<div class="mb-6 px-1">
|
<div class="mb-6 px-1">
|
||||||
<p class="font-semibold mb-4 text-lg">{{ currentQuestion.prompt }}</p>
|
<p class="font-semibold mb-4 text-lg">
|
||||||
|
{{ currentQuestion.prompt }}
|
||||||
|
</p>
|
||||||
<div class="space-y-3.5 mx-auto">
|
<div class="space-y-3.5 mx-auto">
|
||||||
<div
|
<div
|
||||||
v-for="(answer, index) in currentQuestion.answers"
|
v-for="(answer, index) in currentQuestion.answers"
|
||||||
@@ -316,6 +324,8 @@ export default {
|
|||||||
reference: null,
|
reference: null,
|
||||||
answers: {},
|
answers: {},
|
||||||
error: null,
|
error: null,
|
||||||
|
timeLeftSeconds: null,
|
||||||
|
countdownIntervalId: null,
|
||||||
form: {
|
form: {
|
||||||
fn: '',
|
fn: '',
|
||||||
ln: '',
|
ln: '',
|
||||||
@@ -337,6 +347,9 @@ export default {
|
|||||||
isRequiredFieldEmpty () {
|
isRequiredFieldEmpty () {
|
||||||
return this.emptyValueRequiredStep && this.emptyValueRequiredStep[0] !== this.field
|
return this.emptyValueRequiredStep && this.emptyValueRequiredStep[0] !== this.field
|
||||||
},
|
},
|
||||||
|
kbaTimeLimitSeconds () {
|
||||||
|
return 90
|
||||||
|
},
|
||||||
states () {
|
states () {
|
||||||
return [
|
return [
|
||||||
{ code: 'AL', name: 'Alabama' },
|
{ code: 'AL', name: 'Alabama' },
|
||||||
@@ -394,9 +407,53 @@ export default {
|
|||||||
},
|
},
|
||||||
isLastQuestion () {
|
isLastQuestion () {
|
||||||
return this.questions && this.currentQuestionIndex === this.questions.length - 1
|
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: {
|
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 () {
|
nextQuestion () {
|
||||||
if (this.isLastQuestion) {
|
if (this.isLastQuestion) {
|
||||||
this.$emit('submit')
|
this.$emit('submit')
|
||||||
@@ -405,6 +462,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
restartKba () {
|
restartKba () {
|
||||||
|
this.clearCountdown()
|
||||||
|
|
||||||
this.questions = null
|
this.questions = null
|
||||||
this.token = null
|
this.token = null
|
||||||
this.reference = null
|
this.reference = null
|
||||||
@@ -415,6 +474,7 @@ export default {
|
|||||||
async startKba () {
|
async startKba () {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
this.error = null
|
this.error = null
|
||||||
|
this.clearCountdown()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = { ...this.form, submitter_slug: this.submitterSlug }
|
const payload = { ...this.form, submitter_slug: this.submitterSlug }
|
||||||
@@ -437,10 +497,10 @@ export default {
|
|||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!resp.ok) throw new Error('Failed to start KBA')
|
|
||||||
|
|
||||||
const data = await resp.json()
|
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 && data.result.action === 'FAIL') {
|
||||||
if (data.result.detail === 'NO MATCH') {
|
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('Unfortunately, we were unable to start Knowledge Based Authentication with the details provided. Please review and confirm that all your personal details are correct.')
|
||||||
@@ -457,6 +517,8 @@ export default {
|
|||||||
this.questions.forEach(q => {
|
this.questions.forEach(q => {
|
||||||
this.answers[q.id] = null
|
this.answers[q.id] = null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.startCountdown()
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Invalid KBA response')
|
throw new Error('Invalid KBA response')
|
||||||
}
|
}
|
||||||
@@ -467,6 +529,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async submit () {
|
async submit () {
|
||||||
|
this.clearCountdown()
|
||||||
|
|
||||||
this.isSubmitting = true
|
this.isSubmitting = true
|
||||||
this.error = null
|
this.error = null
|
||||||
|
|
||||||
@@ -491,7 +555,11 @@ export default {
|
|||||||
const data = await resp.json()
|
const data = await resp.json()
|
||||||
|
|
||||||
if (data.result?.action !== 'PASS') {
|
if (data.result?.action !== 'PASS') {
|
||||||
this.error = 'Knowledge Based Authentication Failed - make sure you provide correct answers for the Knowledge Based authentication.'
|
if (data.result?.issues?.length) {
|
||||||
|
this.error = `Knowledge Based Authentication Failed - make sure you provide correct details for the Knowledge Based authentication: ${data.result.issues.join(', ')}`
|
||||||
|
} else {
|
||||||
|
this.error = 'Knowledge Based Authentication Failed - make sure you provide correct answers for the Knowledge Based authentication.'
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error('Knowledge Based Authentication Failed')
|
throw new Error('Knowledge Based Authentication Failed')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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 md: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"
|
||||||
@@ -305,14 +309,14 @@
|
|||||||
<script>
|
<script>
|
||||||
import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2, IconQrcode, IconX } from '@tabler/icons-vue'
|
import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2, IconQrcode, IconX } from '@tabler/icons-vue'
|
||||||
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
||||||
import { isValidSignatureCanvas } from './validate_signature'
|
import { isValidSignatureCanvas, isCanvasBlocked } from './validate_signature'
|
||||||
import SignaturePad from 'signature_pad'
|
import SignaturePad from 'signature_pad'
|
||||||
import AppearsOn from './appears_on'
|
import AppearsOn from './appears_on'
|
||||||
import FileDropzone from './dropzone'
|
import FileDropzone from './dropzone'
|
||||||
import MarkdownContent from './markdown_content'
|
import MarkdownContent from './markdown_content'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
|
|
||||||
let isFontLoaded = false
|
let fontLoadPromise = null
|
||||||
|
|
||||||
const scale = 3
|
const scale = 3
|
||||||
|
|
||||||
@@ -336,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,
|
||||||
@@ -390,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,
|
||||||
@@ -401,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()
|
||||||
}
|
}
|
||||||
@@ -448,14 +462,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
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)
|
||||||
@@ -470,13 +477,18 @@ 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)
|
if (this.isTextSignature) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.textInput) {
|
||||||
|
this.initTypedSignature()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
this.intersectionObserver?.disconnect()
|
this.intersectionObserver?.disconnect()
|
||||||
}
|
}
|
||||||
@@ -484,13 +496,66 @@ 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)
|
||||||
|
|
||||||
|
if (this.isTextSignature) {
|
||||||
|
this.loadFont()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
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: this.$refs.textInput })
|
||||||
|
}
|
||||||
|
},
|
||||||
remove () {
|
remove () {
|
||||||
this.$emit('update:model-value', '')
|
this.$emit('update:model-value', '')
|
||||||
|
|
||||||
@@ -498,17 +563,17 @@ export default {
|
|||||||
this.isSignatureStarted = false
|
this.isSignatureStarted = false
|
||||||
},
|
},
|
||||||
loadFont () {
|
loadFont () {
|
||||||
if (!isFontLoaded) {
|
if (!fontLoadPromise) {
|
||||||
const font = new FontFace('Dancing Script', `url(${this.baseUrl}/fonts/DancingScript-Regular.otf) format("opentype")`)
|
const font = new FontFace('Dancing Script', `url(${this.baseUrl}/fonts/DancingScript-Regular.otf) format("opentype")`)
|
||||||
|
|
||||||
font.load().then((loadedFont) => {
|
fontLoadPromise = font.load().then((loadedFont) => {
|
||||||
document.fonts.add(loadedFont)
|
document.fonts.add(loadedFont)
|
||||||
|
|
||||||
isFontLoaded = true
|
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.error('Font loading failed:', error)
|
console.error('Font loading failed:', error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return fontLoadPromise
|
||||||
},
|
},
|
||||||
showQr () {
|
showQr () {
|
||||||
this.isShowQr = true
|
this.isShowQr = true
|
||||||
@@ -608,14 +673,29 @@ export default {
|
|||||||
|
|
||||||
if (this.isTextSignature) {
|
if (this.isTextSignature) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$refs.textInput.focus()
|
if (this.$refs.textInput) {
|
||||||
|
if (!this.submitter.name) {
|
||||||
|
this.$refs.textInput.focus()
|
||||||
|
}
|
||||||
|
|
||||||
this.loadFont()
|
this.initTypedSignature()
|
||||||
|
|
||||||
this.$emit('start')
|
this.$emit('start')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async initTypedSignature () {
|
||||||
|
if (this.submitter.name) {
|
||||||
|
this.$refs.textInput.value = this.submitter.name
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.loadFont()
|
||||||
|
|
||||||
|
if (this.$refs.textInput.value) {
|
||||||
|
this.updateWrittenSignature({ target: this.$refs.textInput })
|
||||||
|
}
|
||||||
|
},
|
||||||
drawImage (event) {
|
drawImage (event) {
|
||||||
this.remove()
|
this.remove()
|
||||||
this.clear()
|
this.clear()
|
||||||
@@ -694,6 +774,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)
|
||||||
}
|
}
|
||||||
@@ -703,7 +790,15 @@ export default {
|
|||||||
|
|
||||||
if (this.isSignatureStarted && this.pad.toData().length > 0 && !isValidSignatureCanvas(this.pad.toData())) {
|
if (this.isSignatureStarted && this.pad.toData().length > 0 && !isValidSignatureCanvas(this.pad.toData())) {
|
||||||
if (this.field.required === true || this.pad.toData().length > 0) {
|
if (this.field.required === true || this.pad.toData().length > 0) {
|
||||||
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
|
if (isCanvasBlocked()) {
|
||||||
|
alert(this.t('browser_privacy_settings_block_canvas'))
|
||||||
|
|
||||||
|
if (window.Rollbar) {
|
||||||
|
window.Rollbar.info('Canvas blocked')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.reject(new Error('Image too small or simple'))
|
return Promise.reject(new Error('Image too small or simple'))
|
||||||
} else {
|
} else {
|
||||||
@@ -759,7 +854,15 @@ export default {
|
|||||||
}
|
}
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
if (this.field.required === true) {
|
if (this.field.required === true) {
|
||||||
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
|
if (isCanvasBlocked()) {
|
||||||
|
alert(this.t('browser_privacy_settings_block_canvas'))
|
||||||
|
|
||||||
|
if (window.Rollbar) {
|
||||||
|
window.Rollbar.info('Canvas blocked')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
|
||||||
|
}
|
||||||
|
|
||||||
return reject(error)
|
return reject(error)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -35,4 +35,24 @@ function isValidSignatureCanvas (data) {
|
|||||||
return validStrokes.length > 0
|
return validStrokes.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
export { isValidSignatureCanvas }
|
function isCanvasBlocked () {
|
||||||
|
try {
|
||||||
|
const testCanvas = document.createElement('canvas')
|
||||||
|
|
||||||
|
testCanvas.width = 2
|
||||||
|
testCanvas.height = 2
|
||||||
|
|
||||||
|
const ctx = testCanvas.getContext('2d')
|
||||||
|
|
||||||
|
ctx.fillStyle = 'rgb(255, 0, 0)'
|
||||||
|
ctx.fillRect(0, 0, 2, 2)
|
||||||
|
|
||||||
|
const pixel = ctx.getImageData(0, 0, 1, 1).data
|
||||||
|
|
||||||
|
return pixel[0] !== 255 || pixel[1] !== 0 || pixel[2] !== 0 || pixel[3] !== 255
|
||||||
|
} catch (e) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { isValidSignatureCanvas, isCanvasBlocked }
|
||||||
@@ -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,142 +24,33 @@
|
|||||||
: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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<AreaTitle
|
||||||
v-if="field?.type && (isSelected || isNameFocus)"
|
ref="title"
|
||||||
class="absolute bg-white rounded-t border overflow-visible whitespace-nowrap flex z-10 field-area-controls"
|
:area="area"
|
||||||
style="top: -25px; height: 25px"
|
:field="field"
|
||||||
@mousedown.stop
|
:template="template"
|
||||||
@pointerdown.stop
|
:selected-areas-ref="selectedAreasRef"
|
||||||
>
|
:get-field-type-index="getFieldTypeIndex"
|
||||||
<FieldSubmitter
|
:default-field="defaultField"
|
||||||
v-if="field.type != 'heading' && field.type != 'strikethrough'"
|
:with-signature-id="withSignatureId"
|
||||||
v-model="field.submitter_uuid"
|
:with-prefillable="withPrefillable"
|
||||||
class="border-r roles-dropdown"
|
:default-submitters="defaultSubmitters"
|
||||||
:compact="true"
|
:editable="editable"
|
||||||
:editable="editable && (!defaultField || defaultField.role !== submitter?.name)"
|
:is-mobile="isMobile"
|
||||||
:allow-add-new="!defaultSubmitters.length"
|
:is-value-input="isValueInput"
|
||||||
:menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px] mt-[1px]'"
|
:is-select-input="isSelectInput"
|
||||||
:submitters="template.submitters"
|
@change="save"
|
||||||
@update:model-value="save"
|
@remove="$emit('remove')"
|
||||||
@click="selectedAreaRef.value = area"
|
@scroll-to="$emit('scroll-to', $event)"
|
||||||
/>
|
@add-custom-field="$emit('add-custom-field')"
|
||||||
<FieldType
|
/>
|
||||||
v-model="field.type"
|
|
||||||
:button-width="27"
|
|
||||||
:editable="editable && !defaultField"
|
|
||||||
:button-classes="'px-1'"
|
|
||||||
:menu-classes="'bg-white rounded-t-none'"
|
|
||||||
@update:model-value="[maybeUpdateOptions(), save()]"
|
|
||||||
@click="selectedAreaRef.value = area"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
v-if="field.type !== 'checkbox' || field.name"
|
|
||||||
ref="name"
|
|
||||||
:contenteditable="editable && !defaultField && field.type !== 'heading'"
|
|
||||||
dir="auto"
|
|
||||||
class="pr-1 cursor-text outline-none block"
|
|
||||||
style="min-width: 2px"
|
|
||||||
@paste.prevent="onPaste"
|
|
||||||
@keydown.enter.prevent="onNameEnter"
|
|
||||||
@focus="onNameFocus"
|
|
||||||
@blur="onNameBlur"
|
|
||||||
>{{ optionIndexText }} {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || defaultName }}</span>
|
|
||||||
<div
|
|
||||||
v-if="isSettingsFocus || isSelectInput || (isValueInput && field.type !== 'heading') || (isNameFocus && !['checkbox', 'phone'].includes(field.type))"
|
|
||||||
class="flex items-center ml-1.5"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
v-if="!isValueInput && !isSelectInput"
|
|
||||||
:id="`required-checkbox-${field.uuid}`"
|
|
||||||
v-model="field.required"
|
|
||||||
type="checkbox"
|
|
||||||
class="checkbox checkbox-xs no-animation rounded"
|
|
||||||
@mousedown.prevent
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
v-if="!isValueInput && !isSelectInput"
|
|
||||||
:for="`required-checkbox-${field.uuid}`"
|
|
||||||
class="label text-xs"
|
|
||||||
@click.prevent="field.required = !field.required"
|
|
||||||
@mousedown.prevent
|
|
||||||
>{{ t('required') }}</label>
|
|
||||||
<input
|
|
||||||
v-if="isValueInput || isSelectInput"
|
|
||||||
:id="`readonly-checkbox-${field.uuid}`"
|
|
||||||
type="checkbox"
|
|
||||||
class="checkbox checkbox-xs no-animation rounded"
|
|
||||||
:checked="!(field.readonly ?? true)"
|
|
||||||
@change="field.readonly = !(field.readonly ?? true)"
|
|
||||||
@mousedown.prevent
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
v-if="isValueInput || isSelectInput"
|
|
||||||
:for="`readonly-checkbox-${field.uuid}`"
|
|
||||||
class="label text-xs"
|
|
||||||
@click.prevent="field.readonly = !(field.readonly ?? true)"
|
|
||||||
@mousedown.prevent
|
|
||||||
>{{ t('editable') }}</label>
|
|
||||||
<span
|
|
||||||
v-if="field.type !== 'payment' && !isValueInput"
|
|
||||||
class="dropdown dropdown-end field-area-settings-dropdown"
|
|
||||||
@mouseenter="renderDropdown = true"
|
|
||||||
@touchstart="renderDropdown = true"
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
ref="settingsButton"
|
|
||||||
tabindex="0"
|
|
||||||
:title="t('settings')"
|
|
||||||
class="cursor-pointer flex items-center"
|
|
||||||
style="height: 25px"
|
|
||||||
@focus="isSettingsFocus = true"
|
|
||||||
@blur="maybeBlurSettings"
|
|
||||||
>
|
|
||||||
<IconDotsVertical class="w-5 h-5" />
|
|
||||||
</label>
|
|
||||||
<ul
|
|
||||||
v-if="renderDropdown"
|
|
||||||
ref="settingsDropdown"
|
|
||||||
tabindex="0"
|
|
||||||
class="dropdown-content menu menu-xs px-2 pb-2 pt-1 shadow rounded-box w-52 z-10 rounded-t-none"
|
|
||||||
:style="{ backgroundColor: 'white' }"
|
|
||||||
@dragstart.prevent.stop
|
|
||||||
@click="closeDropdown"
|
|
||||||
@focusout="maybeBlurSettings"
|
|
||||||
>
|
|
||||||
<FieldSettings
|
|
||||||
:field="field"
|
|
||||||
:default-field="defaultField"
|
|
||||||
:editable="editable"
|
|
||||||
:background-color="'white'"
|
|
||||||
:with-required="false"
|
|
||||||
:with-areas="false"
|
|
||||||
:with-signature-id="withSignatureId"
|
|
||||||
:with-prefillable="withPrefillable"
|
|
||||||
@click-formula="isShowFormulaModal = true"
|
|
||||||
@click-font="isShowFontModal = true"
|
|
||||||
@click-description="isShowDescriptionModal = true"
|
|
||||||
@click-condition="isShowConditionsModal = true"
|
|
||||||
@scroll-to="[selectedAreaRef.value = $event, $emit('scroll-to', $event)]"
|
|
||||||
/>
|
|
||||||
</ul>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
v-else-if="editable"
|
|
||||||
class="pr-1"
|
|
||||||
:title="t('remove')"
|
|
||||||
@click.prevent="$emit('remove')"
|
|
||||||
>
|
|
||||||
<IconX width="14" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
ref="touchValueTarget"
|
ref="touchValueTarget"
|
||||||
class="flex h-full w-full field-area"
|
class="flex h-full w-full field-area"
|
||||||
@@ -250,7 +141,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 +158,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 +185,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,86 +211,26 @@
|
|||||||
<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"
|
||||||
/>
|
/>
|
||||||
<Teleport
|
|
||||||
v-if="isShowFormulaModal"
|
|
||||||
:to="modalContainerEl"
|
|
||||||
>
|
|
||||||
<FormulaModal
|
|
||||||
:field="field"
|
|
||||||
:editable="editable && !defaultField"
|
|
||||||
:default-field="defaultField"
|
|
||||||
:build-default-name="buildDefaultName"
|
|
||||||
@close="isShowFormulaModal = false"
|
|
||||||
/>
|
|
||||||
</Teleport>
|
|
||||||
<Teleport
|
|
||||||
v-if="isShowFontModal"
|
|
||||||
:to="modalContainerEl"
|
|
||||||
>
|
|
||||||
<FontModal
|
|
||||||
:field="field"
|
|
||||||
:editable="editable && !defaultField"
|
|
||||||
:default-field="defaultField"
|
|
||||||
:build-default-name="buildDefaultName"
|
|
||||||
@close="isShowFontModal = false"
|
|
||||||
/>
|
|
||||||
</Teleport>
|
|
||||||
<Teleport
|
|
||||||
v-if="isShowConditionsModal"
|
|
||||||
:to="modalContainerEl"
|
|
||||||
>
|
|
||||||
<ConditionsModal
|
|
||||||
:item="field"
|
|
||||||
:build-default-name="buildDefaultName"
|
|
||||||
:default-field="defaultField"
|
|
||||||
@close="isShowConditionsModal = false"
|
|
||||||
/>
|
|
||||||
</Teleport>
|
|
||||||
<Teleport
|
|
||||||
v-if="isShowDescriptionModal"
|
|
||||||
:to="modalContainerEl"
|
|
||||||
>
|
|
||||||
<DescriptionModal
|
|
||||||
:field="field"
|
|
||||||
:editable="editable && !defaultField"
|
|
||||||
:default-field="defaultField"
|
|
||||||
:build-default-name="buildDefaultName"
|
|
||||||
@close="isShowDescriptionModal = false"
|
|
||||||
/>
|
|
||||||
</Teleport>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import FieldSubmitter from './field_submitter'
|
|
||||||
import FieldType from './field_type'
|
import FieldType from './field_type'
|
||||||
import Field from './field'
|
import Field from './field'
|
||||||
import FieldSettings from './field_settings'
|
import AreaTitle from './area_title'
|
||||||
import FormulaModal from './formula_modal'
|
import { IconCheck } from '@tabler/icons-vue'
|
||||||
import FontModal from './font_modal'
|
|
||||||
import ConditionsModal from './conditions_modal'
|
|
||||||
import DescriptionModal from './description_modal'
|
|
||||||
import { IconX, IconCheck, IconDotsVertical } from '@tabler/icons-vue'
|
|
||||||
import { v4 } from 'uuid'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FieldArea',
|
name: 'FieldArea',
|
||||||
components: {
|
components: {
|
||||||
FieldType,
|
|
||||||
IconCheck,
|
IconCheck,
|
||||||
FieldSettings,
|
AreaTitle
|
||||||
FormulaModal,
|
|
||||||
FontModal,
|
|
||||||
IconDotsVertical,
|
|
||||||
DescriptionModal,
|
|
||||||
ConditionsModal,
|
|
||||||
FieldSubmitter,
|
|
||||||
IconX
|
|
||||||
},
|
},
|
||||||
inject: ['template', 'selectedAreaRef', 'save', 't', 'isInlineSize'],
|
inject: ['template', 'save', 't', 'isInlineSize', 'selectedAreasRef', 'isCmdKeyRef', 'getFieldTypeIndex'],
|
||||||
props: {
|
props: {
|
||||||
area: {
|
area: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -463,22 +295,25 @@ 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,
|
|
||||||
isShowFontModal: false,
|
|
||||||
isShowConditionsModal: false,
|
|
||||||
isContenteditable: false,
|
isContenteditable: false,
|
||||||
isSettingsFocus: false,
|
|
||||||
isShowDescriptionModal: false,
|
|
||||||
isResize: false,
|
isResize: false,
|
||||||
isDragged: false,
|
isDragged: false,
|
||||||
isMoved: false,
|
isMoved: false,
|
||||||
renderDropdown: false,
|
|
||||||
isNameFocus: false,
|
|
||||||
isHeadingSelected: false,
|
isHeadingSelected: false,
|
||||||
textOverflowChars: 0,
|
textOverflowChars: 0,
|
||||||
dragFrom: { x: 0, y: 0 }
|
dragFrom: { x: 0, y: 0 }
|
||||||
@@ -567,11 +402,8 @@ export default {
|
|||||||
return (this.field.type === 'heading' && this.isHeadingSelected) || this.isContenteditable ||
|
return (this.field.type === 'heading' && this.isHeadingSelected) || this.isContenteditable ||
|
||||||
(this.inputMode && (['text', 'number'].includes(this.field.type) || (this.field.type === 'date' && this.field.default_value !== '{{date}}')))
|
(this.inputMode && (['text', 'number'].includes(this.field.type) || (this.field.type === 'date' && this.field.default_value !== '{{date}}')))
|
||||||
},
|
},
|
||||||
modalContainerEl () {
|
|
||||||
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) {
|
||||||
@@ -591,13 +423,6 @@ export default {
|
|||||||
italic: ['bold_italic', 'italic'].includes(this.field.preferences.font_type)
|
italic: ['bold_italic', 'italic'].includes(this.field.preferences.font_type)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
optionIndexText () {
|
|
||||||
if (this.area.option_uuid && this.field.options) {
|
|
||||||
return `${this.field.options.findIndex((o) => o.uuid === this.area.option_uuid) + 1}.`
|
|
||||||
} else {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
cells () {
|
cells () {
|
||||||
const cells = []
|
const cells = []
|
||||||
|
|
||||||
@@ -646,7 +471,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
|
||||||
@@ -677,16 +505,13 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
buildDefaultName: Field.methods.buildDefaultName,
|
buildDefaultName: Field.methods.buildDefaultName,
|
||||||
closeDropdown () {
|
|
||||||
this.$el.getRootNode().activeElement.blur()
|
|
||||||
},
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -764,23 +589,6 @@ export default {
|
|||||||
return number
|
return number
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
maybeBlurSettings (e) {
|
|
||||||
if (!e.relatedTarget || !this.$refs.settingsDropdown.contains(e.relatedTarget)) {
|
|
||||||
this.isSettingsFocus = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onNameFocus (e) {
|
|
||||||
this.selectedAreaRef.value = this.area
|
|
||||||
|
|
||||||
this.isNameFocus = true
|
|
||||||
this.$refs.name.style.minWidth = this.$refs.name.clientWidth + 'px'
|
|
||||||
|
|
||||||
if (!this.field.name) {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.$refs.name.innerText = ' '
|
|
||||||
}, 1)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
startResizeCell (e) {
|
startResizeCell (e) {
|
||||||
this.$el.getRootNode().addEventListener('mousemove', this.onResizeCell)
|
this.$el.getRootNode().addEventListener('mousemove', this.onResizeCell)
|
||||||
this.$el.getRootNode().addEventListener('mouseup', this.stopResizeCell)
|
this.$el.getRootNode().addEventListener('mouseup', this.stopResizeCell)
|
||||||
@@ -815,53 +623,6 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
maybeUpdateOptions () {
|
|
||||||
delete this.field.default_value
|
|
||||||
|
|
||||||
if (!['radio', 'multiple', 'select'].includes(this.field.type)) {
|
|
||||||
delete this.field.options
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.field.type === 'heading') {
|
|
||||||
this.field.readonly = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.field.type === 'strikethrough') {
|
|
||||||
this.field.readonly = true
|
|
||||||
this.field.default_value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (['select', 'multiple', 'radio'].includes(this.field.type)) {
|
|
||||||
this.field.options ||= [{ value: '', uuid: v4() }]
|
|
||||||
}
|
|
||||||
|
|
||||||
(this.field.areas || []).forEach((area) => {
|
|
||||||
if (this.field.type === 'cells') {
|
|
||||||
area.cell_w = area.w * 2 / Math.floor(area.w / area.h)
|
|
||||||
} else {
|
|
||||||
delete area.cell_w
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onNameBlur (e) {
|
|
||||||
if (e.relatedTarget === this.$refs.settingsButton) {
|
|
||||||
this.isSettingsFocus = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = this.$refs.name.innerText.trim()
|
|
||||||
|
|
||||||
this.isNameFocus = false
|
|
||||||
this.$refs.name.style.minWidth = ''
|
|
||||||
|
|
||||||
if (text) {
|
|
||||||
this.field.name = text
|
|
||||||
} else {
|
|
||||||
this.field.name = ''
|
|
||||||
this.$refs.name.innerText = this.defaultName
|
|
||||||
}
|
|
||||||
|
|
||||||
this.save()
|
|
||||||
},
|
|
||||||
onDefaultValueBlur (e) {
|
onDefaultValueBlur (e) {
|
||||||
const text = this.$refs.defaultValue.innerText.trim()
|
const text = this.$refs.defaultValue.innerText.trim()
|
||||||
|
|
||||||
@@ -899,13 +660,19 @@ export default {
|
|||||||
this.$refs.defaultValue.blur()
|
this.$refs.defaultValue.blur()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onNameEnter (e) {
|
|
||||||
this.$refs.name.blur()
|
|
||||||
},
|
|
||||||
resize (e) {
|
resize (e) {
|
||||||
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 +698,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 +747,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 +832,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,9 +850,11 @@ 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?.title?.$refs?.name?.blur()
|
||||||
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
@@ -1088,6 +869,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)
|
||||||
|
|||||||
@@ -0,0 +1,404 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="field?.type && (isSelected || isNameFocus) && !isInMultiSelection"
|
||||||
|
class="absolute bg-white rounded-t border overflow-visible whitespace-nowrap flex z-10 field-area-controls"
|
||||||
|
style="top: -25px; height: 25px"
|
||||||
|
@mousedown.stop
|
||||||
|
@pointerdown.stop
|
||||||
|
>
|
||||||
|
<FieldSubmitter
|
||||||
|
v-if="field.type != 'heading' && field.type != 'strikethrough'"
|
||||||
|
v-model="field.submitter_uuid"
|
||||||
|
class="border-r roles-dropdown"
|
||||||
|
:compact="true"
|
||||||
|
:editable="editable && (!defaultField || defaultField.role !== submitter?.name)"
|
||||||
|
:allow-add-new="!defaultSubmitters.length"
|
||||||
|
:menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px] mt-[1px]'"
|
||||||
|
:submitters="template.submitters"
|
||||||
|
@update:model-value="$emit('change')"
|
||||||
|
@click="selectedAreasRef.value = [area]"
|
||||||
|
/>
|
||||||
|
<FieldType
|
||||||
|
v-model="field.type"
|
||||||
|
:button-width="27"
|
||||||
|
:editable="editable && !defaultField"
|
||||||
|
:button-classes="'px-1'"
|
||||||
|
:menu-classes="'bg-white rounded-t-none'"
|
||||||
|
@update:model-value="[maybeUpdateOptions(), $emit('change')]"
|
||||||
|
@click="selectedAreasRef.value = [area]"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="field.type !== 'checkbox' || field.name"
|
||||||
|
ref="name"
|
||||||
|
:contenteditable="editable && !defaultField && field.type !== 'heading'"
|
||||||
|
dir="auto"
|
||||||
|
class="pr-1 cursor-text outline-none block"
|
||||||
|
style="min-width: 2px"
|
||||||
|
@paste.prevent="onPaste"
|
||||||
|
@keydown.enter.prevent="onNameEnter"
|
||||||
|
@focus="onNameFocus"
|
||||||
|
@blur="onNameBlur"
|
||||||
|
>{{ optionIndexText }} {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || defaultName }}</span>
|
||||||
|
<div
|
||||||
|
v-if="isSettingsFocus || isSelectInput || (isValueInput && field.type !== 'heading') || (isNameFocus && !['checkbox', 'phone'].includes(field.type))"
|
||||||
|
class="flex items-center ml-1.5"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-if="!isValueInput && !isSelectInput"
|
||||||
|
:id="`required-checkbox-${field.uuid}`"
|
||||||
|
v-model="field.required"
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox checkbox-xs no-animation rounded"
|
||||||
|
@mousedown.prevent
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
v-if="!isValueInput && !isSelectInput"
|
||||||
|
:for="`required-checkbox-${field.uuid}`"
|
||||||
|
class="label text-xs"
|
||||||
|
@click.prevent="field.required = !field.required"
|
||||||
|
@mousedown.prevent
|
||||||
|
>{{ t('required') }}</label>
|
||||||
|
<input
|
||||||
|
v-if="isValueInput || isSelectInput"
|
||||||
|
:id="`readonly-checkbox-${field.uuid}`"
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox checkbox-xs no-animation rounded"
|
||||||
|
:checked="!(field.readonly ?? true)"
|
||||||
|
@change="field.readonly = !(field.readonly ?? true)"
|
||||||
|
@mousedown.prevent
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
v-if="isValueInput || isSelectInput"
|
||||||
|
:for="`readonly-checkbox-${field.uuid}`"
|
||||||
|
class="label text-xs"
|
||||||
|
@click.prevent="field.readonly = !(field.readonly ?? true)"
|
||||||
|
@mousedown.prevent
|
||||||
|
>{{ t('editable') }}</label>
|
||||||
|
<span
|
||||||
|
v-if="field.type !== 'payment' && !isValueInput"
|
||||||
|
class="dropdown dropdown-end field-area-settings-dropdown"
|
||||||
|
@mouseenter="renderDropdown = true"
|
||||||
|
@touchstart="renderDropdown = true"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
ref="settingsButton"
|
||||||
|
tabindex="0"
|
||||||
|
:title="t('settings')"
|
||||||
|
class="cursor-pointer flex items-center"
|
||||||
|
style="height: 25px"
|
||||||
|
@focus="isSettingsFocus = true"
|
||||||
|
@blur="maybeBlurSettings"
|
||||||
|
>
|
||||||
|
<IconDotsVertical class="w-5 h-5" />
|
||||||
|
</label>
|
||||||
|
<ul
|
||||||
|
v-if="renderDropdown"
|
||||||
|
ref="settingsDropdown"
|
||||||
|
tabindex="0"
|
||||||
|
class="dropdown-content menu menu-xs px-2 pb-2 pt-1 shadow rounded-box w-52 z-10 rounded-t-none"
|
||||||
|
:style="{ backgroundColor: 'white' }"
|
||||||
|
@dragstart.prevent.stop
|
||||||
|
@click="closeDropdown"
|
||||||
|
@focusout="maybeBlurSettings"
|
||||||
|
>
|
||||||
|
<FieldSettings
|
||||||
|
v-if="isMobile"
|
||||||
|
:field="field"
|
||||||
|
:default-field="defaultField"
|
||||||
|
:editable="editable"
|
||||||
|
:background-color="'white'"
|
||||||
|
:with-required="false"
|
||||||
|
:with-areas="false"
|
||||||
|
:with-signature-id="withSignatureId"
|
||||||
|
:with-prefillable="withPrefillable"
|
||||||
|
@click-formula="isShowFormulaModal = true"
|
||||||
|
@click-font="isShowFontModal = true"
|
||||||
|
@click-description="isShowDescriptionModal = true"
|
||||||
|
@add-custom-field="$emit('add-custom-field')"
|
||||||
|
@click-condition="isShowConditionsModal = true"
|
||||||
|
@save="$emit('change')"
|
||||||
|
@scroll-to="[selectedAreasRef.value = [$event], $emit('scroll-to', $event)]"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="whitespace-normal"
|
||||||
|
>
|
||||||
|
The dots menu is retired in favor of the field context menu. Right-click the field to access field settings. Double-click the field to set a default value.
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-else-if="editable"
|
||||||
|
class="pr-1"
|
||||||
|
:title="t('remove')"
|
||||||
|
@click.prevent="$emit('remove')"
|
||||||
|
>
|
||||||
|
<IconX width="14" />
|
||||||
|
</button>
|
||||||
|
<Teleport
|
||||||
|
v-if="isShowFormulaModal"
|
||||||
|
:to="modalContainerEl"
|
||||||
|
>
|
||||||
|
<FormulaModal
|
||||||
|
:field="field"
|
||||||
|
:editable="editable && !defaultField"
|
||||||
|
:default-field="defaultField"
|
||||||
|
:build-default-name="buildDefaultName"
|
||||||
|
@save="$emit('change')"
|
||||||
|
@close="isShowFormulaModal = false"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
<Teleport
|
||||||
|
v-if="isShowFontModal"
|
||||||
|
:to="modalContainerEl"
|
||||||
|
>
|
||||||
|
<FontModal
|
||||||
|
:field="field"
|
||||||
|
:editable="editable && !defaultField"
|
||||||
|
:default-field="defaultField"
|
||||||
|
:build-default-name="buildDefaultName"
|
||||||
|
@save="$emit('change')"
|
||||||
|
@close="isShowFontModal = false"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
<Teleport
|
||||||
|
v-if="isShowConditionsModal"
|
||||||
|
:to="modalContainerEl"
|
||||||
|
>
|
||||||
|
<ConditionsModal
|
||||||
|
:item="field"
|
||||||
|
:build-default-name="buildDefaultName"
|
||||||
|
:default-field="defaultField"
|
||||||
|
@save="$emit('change')"
|
||||||
|
@close="isShowConditionsModal = false"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
<Teleport
|
||||||
|
v-if="isShowDescriptionModal"
|
||||||
|
:to="modalContainerEl"
|
||||||
|
>
|
||||||
|
<DescriptionModal
|
||||||
|
:field="field"
|
||||||
|
:editable="editable && !defaultField"
|
||||||
|
:default-field="defaultField"
|
||||||
|
:build-default-name="buildDefaultName"
|
||||||
|
@save="$emit('change')"
|
||||||
|
@close="isShowDescriptionModal = false"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import FieldSubmitter from './field_submitter'
|
||||||
|
import FieldType from './field_type'
|
||||||
|
import Field from './field'
|
||||||
|
import FieldSettings from './field_settings'
|
||||||
|
import FormulaModal from './formula_modal'
|
||||||
|
import FontModal from './font_modal'
|
||||||
|
import ConditionsModal from './conditions_modal'
|
||||||
|
import DescriptionModal from './description_modal'
|
||||||
|
import { IconX, IconDotsVertical } from '@tabler/icons-vue'
|
||||||
|
import { v4 } from 'uuid'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'AreaTitle',
|
||||||
|
components: {
|
||||||
|
FieldType,
|
||||||
|
FieldSettings,
|
||||||
|
FormulaModal,
|
||||||
|
FontModal,
|
||||||
|
IconDotsVertical,
|
||||||
|
DescriptionModal,
|
||||||
|
ConditionsModal,
|
||||||
|
FieldSubmitter,
|
||||||
|
IconX
|
||||||
|
},
|
||||||
|
inject: ['t'],
|
||||||
|
props: {
|
||||||
|
template: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
selectedAreasRef: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
getFieldTypeIndex: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
area: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
field: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
defaultField: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
withSignatureId: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
withPrefillable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
defaultSubmitters: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
isMobile: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
isValueInput: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
isSelectInput: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['remove', 'scroll-to', 'add-custom-field', 'change'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isShowFormulaModal: false,
|
||||||
|
isShowFontModal: false,
|
||||||
|
isShowConditionsModal: false,
|
||||||
|
isShowDescriptionModal: false,
|
||||||
|
isSettingsFocus: false,
|
||||||
|
renderDropdown: false,
|
||||||
|
isNameFocus: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
|
fieldLabels: FieldType.computed.fieldLabels,
|
||||||
|
submitter () {
|
||||||
|
return this.template.submitters.find((s) => s.uuid === this.field.submitter_uuid)
|
||||||
|
},
|
||||||
|
isSelected () {
|
||||||
|
return this.selectedAreasRef.value.includes(this.area)
|
||||||
|
},
|
||||||
|
isInMultiSelection () {
|
||||||
|
return this.selectedAreasRef.value.length >= 2 && this.isSelected
|
||||||
|
},
|
||||||
|
optionIndexText () {
|
||||||
|
if (this.area.option_uuid && this.field.options) {
|
||||||
|
return `${this.field.options.findIndex((o) => o.uuid === this.area.option_uuid) + 1}.`
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaultName () {
|
||||||
|
return this.buildDefaultName(this.field)
|
||||||
|
},
|
||||||
|
modalContainerEl () {
|
||||||
|
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
buildDefaultName: Field.methods.buildDefaultName,
|
||||||
|
closeDropdown () {
|
||||||
|
this.$el.getRootNode().activeElement.blur()
|
||||||
|
},
|
||||||
|
maybeBlurSettings (e) {
|
||||||
|
if (!e.relatedTarget || !this.$refs.settingsDropdown.contains(e.relatedTarget)) {
|
||||||
|
this.isSettingsFocus = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNameFocus (e) {
|
||||||
|
this.selectedAreasRef.value = [this.area]
|
||||||
|
|
||||||
|
this.isNameFocus = true
|
||||||
|
this.$refs.name.style.minWidth = this.$refs.name.clientWidth + 'px'
|
||||||
|
|
||||||
|
if (!this.field.name) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$refs.name.innerText = ' '
|
||||||
|
}, 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNameBlur (e) {
|
||||||
|
if (e.relatedTarget === this.$refs.settingsButton) {
|
||||||
|
this.isSettingsFocus = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = this.$refs.name.innerText.trim()
|
||||||
|
|
||||||
|
this.isNameFocus = false
|
||||||
|
this.$refs.name.style.minWidth = ''
|
||||||
|
|
||||||
|
if (text) {
|
||||||
|
this.field.name = text
|
||||||
|
} else {
|
||||||
|
this.field.name = ''
|
||||||
|
this.$refs.name.innerText = this.defaultName
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('change')
|
||||||
|
},
|
||||||
|
onNameEnter (e) {
|
||||||
|
this.$refs.name.blur()
|
||||||
|
},
|
||||||
|
onPaste (e) {
|
||||||
|
const text = (e.clipboardData || window.clipboardData).getData('text/plain')
|
||||||
|
const selection = this.$el.getRootNode().getSelection()
|
||||||
|
|
||||||
|
if (selection.rangeCount) {
|
||||||
|
selection.deleteFromDocument()
|
||||||
|
selection.getRangeAt(0).insertNode(document.createTextNode(text))
|
||||||
|
selection.collapseToEnd()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
maybeUpdateOptions () {
|
||||||
|
delete this.field.default_value
|
||||||
|
|
||||||
|
if (!['radio', 'multiple', 'select'].includes(this.field.type)) {
|
||||||
|
delete this.field.options
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.field.type === 'heading') {
|
||||||
|
this.field.readonly = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.field.type === 'strikethrough') {
|
||||||
|
this.field.readonly = true
|
||||||
|
this.field.default_value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['select', 'multiple', 'radio'].includes(this.field.type)) {
|
||||||
|
this.field.options ||= [{ value: '', uuid: v4() }]
|
||||||
|
}
|
||||||
|
|
||||||
|
(this.field.areas || []).forEach((area) => {
|
||||||
|
if (this.field.type === 'cells') {
|
||||||
|
area.cell_w = area.w * 2 / Math.floor(area.w / area.h)
|
||||||
|
} else {
|
||||||
|
delete area.cell_w
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
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()
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export default {
|
|||||||
acceptFileTypes: {
|
acceptFileTypes: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'image/*, application/pdf, application/zip'
|
default: 'image/*, application/pdf, application/zip, application/json'
|
||||||
},
|
},
|
||||||
withReplaceButton: {
|
withReplaceButton: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
|||||||
@@ -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: []
|
||||||
@@ -142,7 +168,7 @@ export default {
|
|||||||
return this.previewImagesIndex[i] || reactive({
|
return this.previewImagesIndex[i] || reactive({
|
||||||
metadata: { ...lazyloadMetadata },
|
metadata: { ...lazyloadMetadata },
|
||||||
id: Math.random().toString(),
|
id: Math.random().toString(),
|
||||||
url: this.basePreviewUrl + `/preview/${this.document.signed_uuid || this.document.uuid}/${i}.jpg`
|
url: this.basePreviewUrl + `/preview/${this.document.signed_key || this.document.signed_uuid || this.document.uuid}/${i}.jpg`
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -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 () {
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export default {
|
|||||||
acceptFileTypes: {
|
acceptFileTypes: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'image/*, application/pdf, application/zip'
|
default: 'image/*, application/pdf, application/zip, application/json'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['success', 'error', 'loading', 'click-google-drive'],
|
emits: ['success', 'error', 'loading', 'click-google-drive'],
|
||||||
@@ -146,7 +146,7 @@ export default {
|
|||||||
message () {
|
message () {
|
||||||
if (this.isLoading) {
|
if (this.isLoading) {
|
||||||
return this.t('uploading')
|
return this.t('uploading')
|
||||||
} else if (this.acceptFileTypes === 'image/*, application/pdf, application/zip') {
|
} else if (this.acceptFileTypes === 'image/*, application/pdf, application/zip, application/json') {
|
||||||
return this.title || this.t('add_pdf_documents_or_images')
|
return this.title || this.t('add_pdf_documents_or_images')
|
||||||
} else {
|
} else {
|
||||||
return this.title || this.t('add_documents_or_images')
|
return this.title || this.t('add_documents_or_images')
|
||||||
@@ -161,7 +161,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
upload: Upload.methods.upload,
|
upload: Upload.methods.upload,
|
||||||
onDropFiles (e) {
|
onDropFiles (e) {
|
||||||
if (this.acceptFileTypes !== 'image/*, application/pdf, application/zip' || [...e.dataTransfer.files].every((f) => f.type.match(/(?:image\/)|(?:application\/pdf)|(?:application\/zip)/))) {
|
if (this.acceptFileTypes !== 'image/*, application/pdf, application/zip, application/json' || [...e.dataTransfer.files].every((f) => f.type.match(/(?:image\/)|(?:application\/pdf)|(?:application\/zip)|(?:application\/json)/))) {
|
||||||
this.$refs.input.files = e.dataTransfer.files
|
this.$refs.input.files = e.dataTransfer.files
|
||||||
|
|
||||||
this.upload()
|
this.upload()
|
||||||
|
|||||||
@@ -0,0 +1,289 @@
|
|||||||
|
<template>
|
||||||
|
<span
|
||||||
|
class="items-center select-none cursor-pointer relative overflow-visible text-base-content/80 font-sans"
|
||||||
|
:class="[bgColorClass, iconOnlyField ? 'justify-center' : '']"
|
||||||
|
:draggable="editable"
|
||||||
|
:style="[nodeStyle]"
|
||||||
|
@mousedown="selectArea"
|
||||||
|
@dragstart="onDragStart"
|
||||||
|
@contextmenu.prevent.stop="onContextMenu"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="absolute inset-0 pointer-events-none border-solid"
|
||||||
|
:class="borderColorClass"
|
||||||
|
:style="{ borderWidth: (isSelected ? 1 : 0) + 'px' }"
|
||||||
|
/>
|
||||||
|
<component
|
||||||
|
:is="fieldIcons[field?.type || 'text']"
|
||||||
|
v-if="field && !field.default_value"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
:stroke-width="1.5"
|
||||||
|
:class="iconOnlyField ? 'shrink min-h-0 max-h-full max-w-6 opacity-70 m-auto p-0.5' : 'shrink min-h-0 max-h-full max-w-4 opacity-70 mx-0.5 pl-0.5'"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="field?.default_value"
|
||||||
|
class="text-xs overflow-hidden text-ellipsis whitespace-nowrap pr-1 font-normal pl-0.5"
|
||||||
|
>
|
||||||
|
<template v-if="field.default_value === '{{date}}'">
|
||||||
|
{{ t('signing_date') }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ field.default_value }}
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="field && !iconOnlyField"
|
||||||
|
class="text-xs overflow-hidden text-ellipsis whitespace-nowrap pr-1 opacity-70 font-normal pl-0.5"
|
||||||
|
>{{ displayLabel }}</span>
|
||||||
|
<span
|
||||||
|
class="absolute rounded-full bg-white border border-gray-400 shadow-md cursor-nwse-resize z-10"
|
||||||
|
:style="{ width: resizeHandleSize + 'px', height: resizeHandleSize + 'px', right: (-4 / zoom) + 'px', bottom: (-4 / zoom) + 'px' }"
|
||||||
|
@pointerdown.prevent.stop="onResizeStart"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import FieldArea from './area'
|
||||||
|
import FieldType from './field_type'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DynamicArea',
|
||||||
|
props: {
|
||||||
|
fieldUuid: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
areaUuid: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
template: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
nodeStyle: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
selectedAreasRef: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
getPos: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
editor: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
getZoom: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
onAreaContextMenu: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
onAreaResize: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
onAreaDragStart: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
t: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
findFieldArea: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
getFieldTypeIndex: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isResizing: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
fieldArea () {
|
||||||
|
return this.findFieldArea(this.areaUuid)
|
||||||
|
},
|
||||||
|
area () {
|
||||||
|
return this.fieldArea?.area
|
||||||
|
},
|
||||||
|
field () {
|
||||||
|
return this.fieldArea?.field
|
||||||
|
},
|
||||||
|
fieldIcons: FieldArea.computed.fieldIcons,
|
||||||
|
fieldNames: FieldArea.computed.fieldNames,
|
||||||
|
fieldLabels: FieldType.computed.fieldLabels,
|
||||||
|
borderColors () {
|
||||||
|
return [
|
||||||
|
'border-red-500/80',
|
||||||
|
'border-sky-500/80',
|
||||||
|
'border-emerald-500/80',
|
||||||
|
'border-yellow-300/80',
|
||||||
|
'border-purple-600/80',
|
||||||
|
'border-pink-500/80',
|
||||||
|
'border-cyan-500/80',
|
||||||
|
'border-orange-500/80',
|
||||||
|
'border-lime-500/80',
|
||||||
|
'border-indigo-500/80'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
bgColors () {
|
||||||
|
return [
|
||||||
|
'bg-red-100',
|
||||||
|
'bg-sky-100',
|
||||||
|
'bg-emerald-100',
|
||||||
|
'bg-yellow-100',
|
||||||
|
'bg-purple-100',
|
||||||
|
'bg-pink-100',
|
||||||
|
'bg-cyan-100',
|
||||||
|
'bg-orange-100',
|
||||||
|
'bg-lime-100',
|
||||||
|
'bg-indigo-100'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
isSelected () {
|
||||||
|
return this.selectedAreasRef.value.some((a) => a === this.area)
|
||||||
|
},
|
||||||
|
zoom () {
|
||||||
|
return this.getZoom()
|
||||||
|
},
|
||||||
|
submitterIndex () {
|
||||||
|
if (!this.field) return 0
|
||||||
|
|
||||||
|
const submitter = this.template.submitters.find((s) => s.uuid === this.field.submitter_uuid)
|
||||||
|
|
||||||
|
return submitter ? this.template.submitters.indexOf(submitter) : 0
|
||||||
|
},
|
||||||
|
borderColorClass () {
|
||||||
|
return this.borderColors[this.submitterIndex % this.borderColors.length]
|
||||||
|
},
|
||||||
|
bgColorClass () {
|
||||||
|
return this.bgColors[this.submitterIndex % this.bgColors.length]
|
||||||
|
},
|
||||||
|
resizeHandleSize () {
|
||||||
|
return this.zoom > 0 ? Math.round(10 / this.zoom) : 10
|
||||||
|
},
|
||||||
|
iconOnlyField () {
|
||||||
|
return ['radio', 'multiple', 'checkbox', 'initials'].includes(this.field?.type)
|
||||||
|
},
|
||||||
|
defaultName () {
|
||||||
|
if (!this.field) return 'text'
|
||||||
|
|
||||||
|
const typeIndex = this.getFieldTypeIndex(this.field)
|
||||||
|
|
||||||
|
return `${this.fieldLabels[this.field.type] || this.fieldNames[this.field.type] || this.field.type} ${typeIndex + 1}`
|
||||||
|
},
|
||||||
|
displayLabel () {
|
||||||
|
return this.field?.name || this.defaultName
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
selectArea () {
|
||||||
|
this.editor.commands.setNodeSelection(this.getPos())
|
||||||
|
},
|
||||||
|
onDragStart (e) {
|
||||||
|
if (this.isResizing) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const pos = this.getPos()
|
||||||
|
|
||||||
|
if (pos == null) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = this.$el
|
||||||
|
const rect = root.getBoundingClientRect()
|
||||||
|
const zoom = this.zoom || 1
|
||||||
|
const clone = root.cloneNode(true)
|
||||||
|
|
||||||
|
clone.querySelector('[class*="cursor-nwse-resize"]')?.remove()
|
||||||
|
clone.style.cssText = `position:fixed;top:-1000px;width:${rect.width / zoom}px;height:${rect.height / zoom}px;display:${root.style.display};vertical-align:${root.style.verticalAlign};zoom:${zoom}`
|
||||||
|
|
||||||
|
document.body.appendChild(clone)
|
||||||
|
|
||||||
|
e.dataTransfer.setDragImage(clone, e.offsetX, e.offsetY)
|
||||||
|
|
||||||
|
requestAnimationFrame(() => clone.remove())
|
||||||
|
|
||||||
|
e.dataTransfer.effectAllowed = 'move'
|
||||||
|
|
||||||
|
this.onAreaDragStart()
|
||||||
|
},
|
||||||
|
onContextMenu (e) {
|
||||||
|
this.selectArea()
|
||||||
|
this.onAreaContextMenu(this.area, e)
|
||||||
|
},
|
||||||
|
onResizeStart (e) {
|
||||||
|
if (!this.editable) return
|
||||||
|
|
||||||
|
this.isResizing = true
|
||||||
|
|
||||||
|
this.selectArea()
|
||||||
|
|
||||||
|
const handle = e.target
|
||||||
|
|
||||||
|
handle.setPointerCapture(e.pointerId)
|
||||||
|
|
||||||
|
const startX = e.clientX
|
||||||
|
const startY = e.clientY
|
||||||
|
const startWidth = this.$el.offsetWidth
|
||||||
|
const startHeight = this.$el.offsetHeight
|
||||||
|
|
||||||
|
const onResizeMove = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.nodeStyle.width = startWidth + (e.clientX - startX) / this.zoom + 'px'
|
||||||
|
this.nodeStyle.height = startHeight + (e.clientY - startY) / this.zoom + 'px'
|
||||||
|
|
||||||
|
this.onAreaResize(this.$el.getBoundingClientRect())
|
||||||
|
}
|
||||||
|
|
||||||
|
const onResizeEnd = () => {
|
||||||
|
if (!this.isResizing) return
|
||||||
|
|
||||||
|
this.isResizing = false
|
||||||
|
|
||||||
|
handle.removeEventListener('pointermove', onResizeMove)
|
||||||
|
handle.removeEventListener('pointerup', onResizeEnd)
|
||||||
|
|
||||||
|
const pos = this.getPos()
|
||||||
|
|
||||||
|
const tr = this.editor.view.state.tr.setNodeMarkup(pos, undefined, {
|
||||||
|
...this.editor.view.state.doc.nodeAt(pos)?.attrs,
|
||||||
|
width: this.nodeStyle.width,
|
||||||
|
height: this.nodeStyle.height
|
||||||
|
})
|
||||||
|
|
||||||
|
this.editor.view.dispatch(tr)
|
||||||
|
this.editor.commands.setNodeSelection(pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
handle.addEventListener('pointermove', onResizeMove)
|
||||||
|
handle.addEventListener('pointerup', onResizeEnd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,316 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="container"
|
||||||
|
class="relative"
|
||||||
|
style="container-type: inline-size;"
|
||||||
|
>
|
||||||
|
<div ref="shadow" />
|
||||||
|
<template
|
||||||
|
v-for="style in styles"
|
||||||
|
:key="style.innerText"
|
||||||
|
>
|
||||||
|
<Teleport
|
||||||
|
v-if="shadow"
|
||||||
|
:to="style.innerText.includes('@font-face {') ? 'head' : shadow"
|
||||||
|
>
|
||||||
|
<component :is="'style'">
|
||||||
|
{{ style.innerText }}
|
||||||
|
</component>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
<Teleport
|
||||||
|
v-if="shadow"
|
||||||
|
:to="shadow"
|
||||||
|
>
|
||||||
|
<DynamicSection
|
||||||
|
v-for="section in sections"
|
||||||
|
:ref="setSectionRefs"
|
||||||
|
:key="section.id"
|
||||||
|
:container="$refs.container"
|
||||||
|
:editable="editable"
|
||||||
|
:section="section"
|
||||||
|
:section-refs="sectionRefs"
|
||||||
|
:container-width="containerWidth"
|
||||||
|
:attachments-index="attachmentsIndex"
|
||||||
|
:selected-submitter="selectedSubmitter"
|
||||||
|
:drag-field="dragField"
|
||||||
|
:draw-field="drawField"
|
||||||
|
:draw-field-type="drawFieldType"
|
||||||
|
:draw-custom-field="drawCustomField"
|
||||||
|
:render-html-for-save-ref="renderHtmlForSaveRef"
|
||||||
|
:draw-option="drawOption"
|
||||||
|
:attachment-uuid="document.uuid"
|
||||||
|
@update="onSectionUpdate(section, $event)"
|
||||||
|
@draw="$emit('draw', $event)"
|
||||||
|
@add-custom-field="$emit('add-custom-field', $event)"
|
||||||
|
@set-draw="$emit('set-draw', $event)"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import DynamicSection from './dynamic_section.vue'
|
||||||
|
import { dynamicStylesheet, tiptapStylesheet } from './dynamic_editor.js'
|
||||||
|
import { buildVariablesSchema, mergeSchemaProperties } from './dynamic_variables_schema.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TemplateDynamicDocument',
|
||||||
|
components: {
|
||||||
|
DynamicSection
|
||||||
|
},
|
||||||
|
inject: ['baseFetch', 'template'],
|
||||||
|
props: {
|
||||||
|
document: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
selectedSubmitter: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
dragField: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
drawField: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
drawFieldType: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
drawCustomField: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
drawOption: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['update', 'draw', 'set-draw', 'add-custom-field'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
containerWidth: 1040,
|
||||||
|
isMounted: false,
|
||||||
|
sectionRefs: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
renderHtmlForSaveRef: () => ref(false),
|
||||||
|
isDynamic () {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
attachmentsIndex () {
|
||||||
|
return (this.document.attachments || []).reduce((acc, att) => {
|
||||||
|
acc[att.uuid] = att.url
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
bodyDom () {
|
||||||
|
return new DOMParser().parseFromString(this.document.body, 'text/html')
|
||||||
|
},
|
||||||
|
headDom () {
|
||||||
|
return new DOMParser().parseFromString(this.document.head, 'text/html')
|
||||||
|
},
|
||||||
|
sections () {
|
||||||
|
return this.bodyDom.querySelectorAll('section')
|
||||||
|
},
|
||||||
|
styles () {
|
||||||
|
return this.headDom.querySelectorAll('style')
|
||||||
|
},
|
||||||
|
shadow () {
|
||||||
|
if (this.isMounted) {
|
||||||
|
return this.$refs.shadow.attachShadow({ mode: 'open' })
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.isMounted = true
|
||||||
|
|
||||||
|
this.shadow.adoptedStyleSheets.push(dynamicStylesheet, tiptapStylesheet)
|
||||||
|
|
||||||
|
this.containerWidth = this.$refs.container.clientWidth
|
||||||
|
|
||||||
|
this.resizeObserver = new ResizeObserver(() => {
|
||||||
|
if (this.$refs.container) {
|
||||||
|
this.containerWidth = this.$refs.container.clientWidth
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.resizeObserver.observe(this.$refs.container)
|
||||||
|
|
||||||
|
window.addEventListener('beforeunload', this.onBeforeUnload)
|
||||||
|
},
|
||||||
|
beforeUnmount () {
|
||||||
|
window.removeEventListener('beforeunload', this.onBeforeUnload)
|
||||||
|
|
||||||
|
this.resizeObserver.unobserve(this.$refs.container)
|
||||||
|
},
|
||||||
|
beforeUpdate () {
|
||||||
|
this.sectionRefs = []
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
mergeSchemaProperties,
|
||||||
|
removeArea (area) {
|
||||||
|
this.sectionRefs.forEach((sectionRef) => {
|
||||||
|
const pos = sectionRef.findAreaNodePos(area.uuid)
|
||||||
|
|
||||||
|
if (pos !== -1) {
|
||||||
|
sectionRef.editor.chain().focus().deleteRange({ from: pos, to: pos + 1 }).run()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
setSectionRefs (ref) {
|
||||||
|
if (ref) {
|
||||||
|
this.sectionRefs.push(ref)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onBeforeUnload (event) {
|
||||||
|
if (this.saveTimer) {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
event.returnValue = ''
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scrollToArea (area) {
|
||||||
|
this.sectionRefs.forEach(({ editor }) => editor.commands.setNodeSelection(0))
|
||||||
|
|
||||||
|
this.sectionRefs.forEach(({ editor }) => {
|
||||||
|
const el = editor.view.dom.querySelector(`[data-area-uuid="${area.uuid}"]`)
|
||||||
|
|
||||||
|
if (el) {
|
||||||
|
editor.commands.setNodeSelection(editor.view.posAtDOM(el, 0))
|
||||||
|
|
||||||
|
el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onSectionUpdate (section, { editor }) {
|
||||||
|
clearTimeout(this.saveTimer)
|
||||||
|
|
||||||
|
this.saveTimer = setTimeout(async () => {
|
||||||
|
await this.updateSectionAndSave(section, editor)
|
||||||
|
|
||||||
|
delete this.saveTimer
|
||||||
|
}, 1000)
|
||||||
|
},
|
||||||
|
updateVariablesSchema () {
|
||||||
|
this.document.variables_schema = buildVariablesSchema(this.bodyDom.body)
|
||||||
|
},
|
||||||
|
updateSectionAndSave (section, editor) {
|
||||||
|
const target = this.bodyDom.getElementById(section.id)
|
||||||
|
|
||||||
|
if (target) {
|
||||||
|
target.innerHTML = this.getHtmlForSave(editor)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.document.body = this.bodyDom.body.innerHTML
|
||||||
|
|
||||||
|
this.updateVariablesSchema()
|
||||||
|
|
||||||
|
this.$emit('update', this.document)
|
||||||
|
|
||||||
|
return this.saveBody()
|
||||||
|
},
|
||||||
|
updateAndSave () {
|
||||||
|
this.update()
|
||||||
|
|
||||||
|
return this.saveBody()
|
||||||
|
},
|
||||||
|
update () {
|
||||||
|
clearTimeout(this.saveTimer)
|
||||||
|
|
||||||
|
delete this.saveTimer
|
||||||
|
|
||||||
|
this.sectionRefs.forEach(({ section, editor }) => {
|
||||||
|
const target = this.bodyDom.getElementById(section.id)
|
||||||
|
|
||||||
|
target.innerHTML = this.getHtmlForSave(editor)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.document.body = this.bodyDom.body.innerHTML
|
||||||
|
|
||||||
|
this.updateVariablesSchema()
|
||||||
|
|
||||||
|
this.$emit('update', this.document)
|
||||||
|
},
|
||||||
|
getHtmlForSave (editor) {
|
||||||
|
this.renderHtmlForSaveRef.value = true
|
||||||
|
|
||||||
|
const result = editor.getHTML()
|
||||||
|
|
||||||
|
this.renderHtmlForSaveRef.value = false
|
||||||
|
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
saveBody () {
|
||||||
|
clearTimeout(this.saveTimer)
|
||||||
|
|
||||||
|
delete this.saveTimer
|
||||||
|
|
||||||
|
return this.baseFetch(`/templates/${this.template.id}/dynamic_documents/${this.document.uuid}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({ body: this.bodyDom.body.innerHTML }),
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
syncVariablesSchema (existing, parsed, { disable = true } = {}) {
|
||||||
|
for (const key of Object.keys(parsed)) {
|
||||||
|
if (!existing[key]) {
|
||||||
|
existing[key] = parsed[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of Object.keys(existing)) {
|
||||||
|
if (!parsed[key]) {
|
||||||
|
if (disable) {
|
||||||
|
existing[key].disabled = true
|
||||||
|
} else {
|
||||||
|
delete existing[key]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delete existing[key].disabled
|
||||||
|
|
||||||
|
if (!existing[key].form_type) {
|
||||||
|
existing[key].type = parsed[key].type
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed[key].items) {
|
||||||
|
if (!existing[key].items) {
|
||||||
|
existing[key].items = parsed[key].items
|
||||||
|
} else if (existing[key].items.properties && parsed[key].items.properties) {
|
||||||
|
this.syncVariablesSchema(existing[key].items.properties, parsed[key].items.properties, { disable })
|
||||||
|
} else if (!existing[key].items.properties && !parsed[key].items.properties) {
|
||||||
|
existing[key].items.type = parsed[key].items.type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing[key].properties && parsed[key].properties) {
|
||||||
|
this.syncVariablesSchema(existing[key].properties, parsed[key].properties, { disable })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,789 @@
|
|||||||
|
import { Editor, Extension, Node, Mark } from '@tiptap/core'
|
||||||
|
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||||
|
import { Decoration, DecorationSet } from '@tiptap/pm/view'
|
||||||
|
import Document from '@tiptap/extension-document'
|
||||||
|
import Text from '@tiptap/extension-text'
|
||||||
|
import HardBreak from '@tiptap/extension-hard-break'
|
||||||
|
import History from '@tiptap/extension-history'
|
||||||
|
import Gapcursor from '@tiptap/extension-gapcursor'
|
||||||
|
import Dropcursor from '@tiptap/extension-dropcursor'
|
||||||
|
import { createApp, reactive } from 'vue'
|
||||||
|
import DynamicArea from './dynamic_area.vue'
|
||||||
|
import styles from './dynamic_styles.scss'
|
||||||
|
|
||||||
|
export const dynamicStylesheet = new CSSStyleSheet()
|
||||||
|
|
||||||
|
dynamicStylesheet.replaceSync(styles[0][1])
|
||||||
|
|
||||||
|
export const tiptapStylesheet = new CSSStyleSheet()
|
||||||
|
|
||||||
|
tiptapStylesheet.replaceSync(
|
||||||
|
`.ProseMirror {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror {
|
||||||
|
word-wrap: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
white-space: break-spaces;
|
||||||
|
-webkit-font-variant-ligatures: none;
|
||||||
|
font-variant-ligatures: none;
|
||||||
|
font-feature-settings: "liga" 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror [contenteditable="false"] {
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror [contenteditable="false"] [contenteditable="true"] {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.ProseMirror-separator {
|
||||||
|
display: inline !important;
|
||||||
|
border: none !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
width: 0 !important;
|
||||||
|
height: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-gapcursor {
|
||||||
|
display: none;
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-gapcursor:after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
width: 20px;
|
||||||
|
border-top: 1px solid black;
|
||||||
|
animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ProseMirror-cursor-blink {
|
||||||
|
to {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-hideselection *::selection {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-hideselection *::-moz-selection {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-hideselection * {
|
||||||
|
caret-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-focused .ProseMirror-gapcursor {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
dynamic-variable {
|
||||||
|
background-color: #fef3c7;
|
||||||
|
}`)
|
||||||
|
|
||||||
|
function collectDomAttrs (dom) {
|
||||||
|
const attrs = {}
|
||||||
|
|
||||||
|
for (let i = 0; i < dom.attributes.length; i++) {
|
||||||
|
attrs[dom.attributes[i].name] = dom.attributes[i].value
|
||||||
|
}
|
||||||
|
|
||||||
|
return { htmlAttrs: attrs }
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectSpanDomAttrs (dom) {
|
||||||
|
const result = collectDomAttrs(dom)
|
||||||
|
|
||||||
|
if (result.htmlAttrs.style) {
|
||||||
|
const temp = document.createElement('span')
|
||||||
|
|
||||||
|
temp.style.cssText = result.htmlAttrs.style
|
||||||
|
|
||||||
|
if (['bold', '700'].includes(temp.style.fontWeight)) {
|
||||||
|
temp.style.removeProperty('font-weight')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (temp.style.fontStyle === 'italic') {
|
||||||
|
temp.style.removeProperty('font-style')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (temp.style.textDecoration === 'underline') {
|
||||||
|
temp.style.removeProperty('text-decoration')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (temp.style.cssText) {
|
||||||
|
result.htmlAttrs.style = temp.style.cssText
|
||||||
|
} else {
|
||||||
|
delete result.htmlAttrs.style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBlockNode (name, tag, content) {
|
||||||
|
return Node.create({
|
||||||
|
name,
|
||||||
|
group: 'block',
|
||||||
|
content: content || 'block+',
|
||||||
|
addAttributes () {
|
||||||
|
return {
|
||||||
|
htmlAttrs: { default: {} }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag, getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return [tag, node.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomParagraph = Node.create({
|
||||||
|
name: 'paragraph',
|
||||||
|
group: 'block',
|
||||||
|
content: 'inline*',
|
||||||
|
addAttributes () {
|
||||||
|
return {
|
||||||
|
htmlAttrs: { default: {} }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'p', getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return ['p', node.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const CustomHeading = Node.create({
|
||||||
|
name: 'heading',
|
||||||
|
group: 'block',
|
||||||
|
content: 'inline*',
|
||||||
|
addAttributes () {
|
||||||
|
return {
|
||||||
|
htmlAttrs: { default: {} },
|
||||||
|
level: { default: 1 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [1, 2, 3, 4, 5, 6].map((level) => ({
|
||||||
|
tag: `h${level}`,
|
||||||
|
getAttrs: (dom) => ({ ...collectDomAttrs(dom), level })
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return [`h${node.attrs.level}`, node.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const SectionNode = createBlockNode('section', 'section')
|
||||||
|
const ArticleNode = createBlockNode('article', 'article')
|
||||||
|
const DivNode = createBlockNode('div', 'div')
|
||||||
|
const BlockquoteNode = createBlockNode('blockquote', 'blockquote')
|
||||||
|
const PreNode = createBlockNode('pre', 'pre')
|
||||||
|
const OrderedListNode = createBlockNode('orderedList', 'ol', '(listItem | block)+')
|
||||||
|
const BulletListNode = createBlockNode('bulletList', 'ul', '(listItem | block)+')
|
||||||
|
|
||||||
|
const ListItemNode = Node.create({
|
||||||
|
name: 'listItem',
|
||||||
|
content: 'block+',
|
||||||
|
addAttributes () {
|
||||||
|
return {
|
||||||
|
htmlAttrs: { default: {} }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'li', getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return ['li', node.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const TableNode = Node.create({
|
||||||
|
name: 'table',
|
||||||
|
group: 'block',
|
||||||
|
content: '(colgroup | tableHead | tableBody | tableRow)+',
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'table', getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return ['table', node.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const TableHead = Node.create({
|
||||||
|
name: 'tableHead',
|
||||||
|
content: 'tableRow+',
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'thead', getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return ['thead', node.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const TableBody = Node.create({
|
||||||
|
name: 'tableBody',
|
||||||
|
content: 'tableRow+',
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'tbody', getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return ['tbody', node.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const TableRow = Node.create({
|
||||||
|
name: 'tableRow',
|
||||||
|
content: '(tableCell | tableHeader)+',
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'tr', getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return ['tr', node.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const TableCell = Node.create({
|
||||||
|
name: 'tableCell',
|
||||||
|
content: 'block*',
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'td', getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return ['td', node.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const TableHeader = Node.create({
|
||||||
|
name: 'tableHeader',
|
||||||
|
content: 'block*',
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'th', getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return ['th', node.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const ImageNode = Node.create({
|
||||||
|
name: 'image',
|
||||||
|
inline: true,
|
||||||
|
group: 'inline',
|
||||||
|
draggable: true,
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'img', getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return ['img', node.attrs.htmlAttrs]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const ColGroupNode = Node.create({
|
||||||
|
name: 'colgroup',
|
||||||
|
group: 'block',
|
||||||
|
content: 'col*',
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'colgroup', getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return ['colgroup', node.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const ColNode = Node.create({
|
||||||
|
name: 'col',
|
||||||
|
group: 'block',
|
||||||
|
atom: true,
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'col', getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return ['col', node.attrs.htmlAttrs]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const CustomBold = Mark.create({
|
||||||
|
name: 'bold',
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'strong' }, { tag: 'b' }, { style: 'font-weight=bold' }, { style: 'font-weight=700' }]
|
||||||
|
},
|
||||||
|
renderHTML () {
|
||||||
|
return ['strong', 0]
|
||||||
|
},
|
||||||
|
addCommands () {
|
||||||
|
return {
|
||||||
|
toggleBold: () => ({ commands }) => commands.toggleMark(this.name)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addKeyboardShortcuts () {
|
||||||
|
return {
|
||||||
|
'Mod-b': () => this.editor.commands.toggleBold()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const CustomItalic = Mark.create({
|
||||||
|
name: 'italic',
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'em' }, { tag: 'i' }, { style: 'font-style=italic' }]
|
||||||
|
},
|
||||||
|
renderHTML () {
|
||||||
|
return ['em', 0]
|
||||||
|
},
|
||||||
|
addCommands () {
|
||||||
|
return {
|
||||||
|
toggleItalic: () => ({ commands }) => commands.toggleMark(this.name)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addKeyboardShortcuts () {
|
||||||
|
return {
|
||||||
|
'Mod-i': () => this.editor.commands.toggleItalic()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const CustomUnderline = Mark.create({
|
||||||
|
name: 'underline',
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'u' }, { style: 'text-decoration=underline' }]
|
||||||
|
},
|
||||||
|
renderHTML () {
|
||||||
|
return ['u', 0]
|
||||||
|
},
|
||||||
|
addCommands () {
|
||||||
|
return {
|
||||||
|
toggleUnderline: () => ({ commands }) => commands.toggleMark(this.name)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addKeyboardShortcuts () {
|
||||||
|
return {
|
||||||
|
'Mod-u': () => this.editor.commands.toggleUnderline()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const CustomStrike = Mark.create({
|
||||||
|
name: 'strike',
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 's' }, { tag: 'del' }, { tag: 'strike' }, { style: 'text-decoration=line-through' }]
|
||||||
|
},
|
||||||
|
renderHTML () {
|
||||||
|
return ['s', 0]
|
||||||
|
},
|
||||||
|
addCommands () {
|
||||||
|
return {
|
||||||
|
toggleStrike: () => ({ commands }) => commands.toggleMark(this.name)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addKeyboardShortcuts () {
|
||||||
|
return {
|
||||||
|
'Mod-Shift-s': () => this.editor.commands.toggleStrike()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const EmptySpanNode = Node.create({
|
||||||
|
name: 'emptySpan',
|
||||||
|
inline: true,
|
||||||
|
group: 'inline',
|
||||||
|
atom: true,
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{
|
||||||
|
tag: 'span',
|
||||||
|
priority: 60,
|
||||||
|
getAttrs (dom) {
|
||||||
|
if (dom.childNodes.length === 0 && dom.attributes.length > 0) {
|
||||||
|
return collectDomAttrs(dom)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return ['span', node.attrs.htmlAttrs]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const SpanMark = Mark.create({
|
||||||
|
name: 'span',
|
||||||
|
excludes: '',
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'span', getAttrs: collectSpanDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ mark }) {
|
||||||
|
return ['span', mark.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const LinkMark = Mark.create({
|
||||||
|
name: 'link',
|
||||||
|
excludes: '',
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'a', getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ mark }) {
|
||||||
|
return ['a', mark.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const SubscriptMark = Mark.create({
|
||||||
|
name: 'subscript',
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'sub', getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ mark }) {
|
||||||
|
return ['sub', mark.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const SuperscriptMark = Mark.create({
|
||||||
|
name: 'superscript',
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'sup', getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ mark }) {
|
||||||
|
return ['sup', mark.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const TabHandler = Extension.create({
|
||||||
|
name: 'tabHandler',
|
||||||
|
addKeyboardShortcuts () {
|
||||||
|
return {
|
||||||
|
Tab: () => {
|
||||||
|
this.editor.commands.insertContent('\t')
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const variableHighlightKey = new PluginKey('variableHighlight')
|
||||||
|
|
||||||
|
function buildDecorations (doc) {
|
||||||
|
const decorations = []
|
||||||
|
const regex = /\[\[[^\]]*\]\]/g
|
||||||
|
|
||||||
|
doc.descendants((node, pos) => {
|
||||||
|
if (!node.isText) return
|
||||||
|
|
||||||
|
let match
|
||||||
|
|
||||||
|
while ((match = regex.exec(node.text)) !== null) {
|
||||||
|
const from = pos + match.index
|
||||||
|
const to = from + match[0].length
|
||||||
|
|
||||||
|
decorations.push(Decoration.inline(from, to, { nodeName: 'dynamic-variable' }))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return DecorationSet.create(doc, decorations)
|
||||||
|
}
|
||||||
|
|
||||||
|
const VariableHighlight = Extension.create({
|
||||||
|
name: 'variableHighlight',
|
||||||
|
addProseMirrorPlugins () {
|
||||||
|
return [
|
||||||
|
new Plugin({
|
||||||
|
key: variableHighlightKey,
|
||||||
|
state: {
|
||||||
|
init (_, { doc }) {
|
||||||
|
return buildDecorations(doc)
|
||||||
|
},
|
||||||
|
apply (tr, oldSet) {
|
||||||
|
if (tr.docChanged) {
|
||||||
|
return buildDecorations(tr.doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
return oldSet
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
decorations (state) {
|
||||||
|
return this.getState(state)
|
||||||
|
},
|
||||||
|
handleTextInput (view, from, to, text) {
|
||||||
|
if (text !== '[') return false
|
||||||
|
|
||||||
|
const { state } = view
|
||||||
|
const charBefore = state.doc.textBetween(Math.max(from - 1, 0), from)
|
||||||
|
|
||||||
|
if (charBefore !== '[') return false
|
||||||
|
|
||||||
|
const tr = state.tr.insertText('[]]', from, to)
|
||||||
|
|
||||||
|
tr.setSelection(state.selection.constructor.create(tr.doc, from + 1))
|
||||||
|
|
||||||
|
view.dispatch(tr)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export function buildEditor ({ dynamicAreaProps, attachmentsIndex, renderHtmlForSaveRef, onFieldDrop, onFieldDestroy, editorOptions }) {
|
||||||
|
const FieldNode = Node.create({
|
||||||
|
name: 'fieldNode',
|
||||||
|
inline: true,
|
||||||
|
group: 'inline',
|
||||||
|
atom: true,
|
||||||
|
draggable: true,
|
||||||
|
addAttributes () {
|
||||||
|
return {
|
||||||
|
uuid: { default: null },
|
||||||
|
areaUuid: { default: null },
|
||||||
|
width: { default: '124px' },
|
||||||
|
height: { default: null },
|
||||||
|
verticalAlign: { default: 'text-bottom' },
|
||||||
|
display: { default: 'inline-flex' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{
|
||||||
|
tag: 'dynamic-field',
|
||||||
|
getAttrs (dom) {
|
||||||
|
return {
|
||||||
|
uuid: dom.getAttribute('uuid'),
|
||||||
|
areaUuid: dom.getAttribute('area-uuid'),
|
||||||
|
width: dom.style.width,
|
||||||
|
height: dom.style.height,
|
||||||
|
display: dom.style.display,
|
||||||
|
verticalAlign: dom.style.verticalAlign
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
const attrs = {
|
||||||
|
uuid: node.attrs.uuid,
|
||||||
|
'area-uuid': node.attrs.areaUuid,
|
||||||
|
style: `width: ${node.attrs.width}; height: ${node.attrs.height}; display: ${node.attrs.display}; vertical-align: ${node.attrs.verticalAlign};`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!renderHtmlForSaveRef.value) {
|
||||||
|
const fieldArea = dynamicAreaProps.findFieldArea(node.attrs.areaUuid)
|
||||||
|
|
||||||
|
if (fieldArea?.field && fieldArea?.area) {
|
||||||
|
const field = JSON.parse(JSON.stringify(fieldArea.field))
|
||||||
|
const area = JSON.parse(JSON.stringify(fieldArea.area))
|
||||||
|
|
||||||
|
delete field.areas
|
||||||
|
delete field.uuid
|
||||||
|
delete field.submitter_uuid
|
||||||
|
delete area.uuid
|
||||||
|
delete area.attachment_uuid
|
||||||
|
|
||||||
|
attrs['data-field'] = JSON.stringify(field)
|
||||||
|
attrs['data-area'] = JSON.stringify(area)
|
||||||
|
attrs['data-template-id'] = dynamicAreaProps.template.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['dynamic-field', attrs]
|
||||||
|
},
|
||||||
|
addNodeView () {
|
||||||
|
return ({ node, getPos, editor }) => {
|
||||||
|
const dom = document.createElement('span')
|
||||||
|
|
||||||
|
const nodeStyle = reactive({
|
||||||
|
width: node.attrs.width,
|
||||||
|
height: node.attrs.height,
|
||||||
|
verticalAlign: node.attrs.verticalAlign,
|
||||||
|
display: node.attrs.display
|
||||||
|
})
|
||||||
|
|
||||||
|
dom.dataset.areaUuid = node.attrs.areaUuid
|
||||||
|
|
||||||
|
const shadow = dom.attachShadow({ mode: 'open' })
|
||||||
|
|
||||||
|
shadow.adoptedStyleSheets = [dynamicStylesheet]
|
||||||
|
|
||||||
|
const app = createApp(DynamicArea, {
|
||||||
|
fieldUuid: node.attrs.uuid,
|
||||||
|
areaUuid: node.attrs.areaUuid,
|
||||||
|
nodeStyle,
|
||||||
|
getPos,
|
||||||
|
editor,
|
||||||
|
editable: editorOptions.editable,
|
||||||
|
...dynamicAreaProps
|
||||||
|
})
|
||||||
|
|
||||||
|
app.mount(shadow)
|
||||||
|
|
||||||
|
return {
|
||||||
|
dom,
|
||||||
|
update (updatedNode) {
|
||||||
|
if (updatedNode.attrs.areaUuid === node.attrs.areaUuid) {
|
||||||
|
nodeStyle.width = updatedNode.attrs.width
|
||||||
|
nodeStyle.height = updatedNode.attrs.height
|
||||||
|
nodeStyle.verticalAlign = updatedNode.attrs.verticalAlign
|
||||||
|
nodeStyle.display = updatedNode.attrs.display
|
||||||
|
}
|
||||||
|
},
|
||||||
|
destroy () {
|
||||||
|
onFieldDestroy(node)
|
||||||
|
|
||||||
|
app.unmount()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const FieldDropPlugin = Extension.create({
|
||||||
|
name: 'fieldDrop',
|
||||||
|
addProseMirrorPlugins () {
|
||||||
|
return [
|
||||||
|
new Plugin({
|
||||||
|
key: new PluginKey('fieldDrop'),
|
||||||
|
props: {
|
||||||
|
handleDrop: onFieldDrop
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const DynamicImageNode = ImageNode.extend({
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
const { loading, ...attrs } = node.attrs.htmlAttrs
|
||||||
|
|
||||||
|
return ['img', attrs]
|
||||||
|
},
|
||||||
|
addNodeView () {
|
||||||
|
return ({ node }) => {
|
||||||
|
const dom = document.createElement('img')
|
||||||
|
|
||||||
|
const attrs = { ...node.attrs.htmlAttrs }
|
||||||
|
|
||||||
|
const blobUuid = attrs.src?.startsWith('blob:') && attrs.src.slice(5)
|
||||||
|
|
||||||
|
if (blobUuid && attachmentsIndex[blobUuid]) {
|
||||||
|
attrs.src = attachmentsIndex[blobUuid]
|
||||||
|
}
|
||||||
|
|
||||||
|
dom.setAttribute('loading', 'lazy')
|
||||||
|
|
||||||
|
Object.entries(attrs).forEach(([k, v]) => dom.setAttribute(k, v))
|
||||||
|
|
||||||
|
return { dom }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return new Editor({
|
||||||
|
extensions: [
|
||||||
|
Document,
|
||||||
|
Text,
|
||||||
|
HardBreak,
|
||||||
|
History,
|
||||||
|
Gapcursor,
|
||||||
|
Dropcursor,
|
||||||
|
CustomBold,
|
||||||
|
CustomItalic,
|
||||||
|
CustomUnderline,
|
||||||
|
CustomStrike,
|
||||||
|
CustomParagraph,
|
||||||
|
CustomHeading,
|
||||||
|
SectionNode,
|
||||||
|
ArticleNode,
|
||||||
|
DivNode,
|
||||||
|
BlockquoteNode,
|
||||||
|
PreNode,
|
||||||
|
OrderedListNode,
|
||||||
|
BulletListNode,
|
||||||
|
ListItemNode,
|
||||||
|
TableNode,
|
||||||
|
TableHead,
|
||||||
|
TableBody,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableHeader,
|
||||||
|
ColGroupNode,
|
||||||
|
ColNode,
|
||||||
|
DynamicImageNode,
|
||||||
|
EmptySpanNode,
|
||||||
|
LinkMark,
|
||||||
|
SpanMark,
|
||||||
|
SubscriptMark,
|
||||||
|
SuperscriptMark,
|
||||||
|
VariableHighlight,
|
||||||
|
TabHandler,
|
||||||
|
FieldNode,
|
||||||
|
FieldDropPlugin
|
||||||
|
],
|
||||||
|
editorProps: {
|
||||||
|
attributes: {
|
||||||
|
style: 'outline: none'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parseOptions: {
|
||||||
|
preserveWhitespace: true
|
||||||
|
},
|
||||||
|
injectCSS: false,
|
||||||
|
...editorOptions
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="visible"
|
||||||
|
class="absolute z-10 flex items-center gap-0.5 px-1.5 py-1 bg-white border border-base-300 rounded-lg shadow select-none"
|
||||||
|
:style="{ top: (coords.top - 42) + 'px', left: coords.left + 'px', transform: 'translateX(-50%)' }"
|
||||||
|
@mousedown.prevent
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center w-7 h-7 border-none rounded cursor-pointer text-gray-700"
|
||||||
|
:class="isBold ? 'bg-base-200' : 'bg-transparent'"
|
||||||
|
title="Bold"
|
||||||
|
@click="toggleBold"
|
||||||
|
>
|
||||||
|
<IconBold
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center w-7 h-7 border-none rounded cursor-pointer text-gray-700"
|
||||||
|
:class="isItalic ? 'bg-base-200' : 'bg-transparent'"
|
||||||
|
title="Italic"
|
||||||
|
@click="toggleItalic"
|
||||||
|
>
|
||||||
|
<IconItalic
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center w-7 h-7 border-none rounded cursor-pointer text-gray-700"
|
||||||
|
:class="isUnderline ? 'bg-base-200' : 'bg-transparent'"
|
||||||
|
title="Underline"
|
||||||
|
@click="toggleUnderline"
|
||||||
|
>
|
||||||
|
<IconUnderline
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center w-7 h-7 border-none rounded cursor-pointer text-gray-700"
|
||||||
|
:class="isStrike ? 'bg-base-200' : 'bg-transparent'"
|
||||||
|
title="Strikethrough"
|
||||||
|
@click="toggleStrike"
|
||||||
|
>
|
||||||
|
<IconStrikethrough
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div class="w-px h-5 bg-base-300 mx-1" />
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center text-xs h-7 border-none rounded cursor-pointer text-gray-700 bg-transparent"
|
||||||
|
title="Wrap in variable"
|
||||||
|
@click="wrapVariable"
|
||||||
|
>
|
||||||
|
<IconBracketsContain
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
<span class="px-0.5">
|
||||||
|
Variable
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<div class="w-px h-5 bg-base-300 mx-1" />
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center text-xs h-7 border-none rounded cursor-pointer text-gray-700 bg-transparent"
|
||||||
|
title="Wrap in condition"
|
||||||
|
@click="wrapCondition"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.6"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="tabler-icon tabler-icon-brackets-contain"
|
||||||
|
><path d="M7 4h-4v16h4" /><path d="M17 4h4v16h-4" />
|
||||||
|
<text
|
||||||
|
x="12"
|
||||||
|
y="16.5"
|
||||||
|
text-anchor="middle"
|
||||||
|
fill="currentColor"
|
||||||
|
stroke="none"
|
||||||
|
font-size="14"
|
||||||
|
font-weight="600"
|
||||||
|
font-family="ui-sans-serif, system-ui, sans-serif"
|
||||||
|
>if</text>
|
||||||
|
</svg>
|
||||||
|
<span class="px-0.5">
|
||||||
|
Condition
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconBold, IconItalic, IconUnderline, IconStrikethrough, IconBracketsContain } from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DynamicMenu',
|
||||||
|
components: {
|
||||||
|
IconBold,
|
||||||
|
IconItalic,
|
||||||
|
IconUnderline,
|
||||||
|
IconStrikethrough,
|
||||||
|
IconBracketsContain
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
editor: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
coords: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['add-variable', 'add-condition'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isMouseDown: false,
|
||||||
|
isBold: this.editor.isActive('bold'),
|
||||||
|
isItalic: this.editor.isActive('italic'),
|
||||||
|
isUnderline: this.editor.isActive('underline'),
|
||||||
|
isStrike: this.editor.isActive('strike')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
visible () {
|
||||||
|
return !!this.coords && !this.isMouseDown
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.editor.view.dom.addEventListener('mousedown', this.onMouseDown)
|
||||||
|
|
||||||
|
document.addEventListener('mouseup', this.onMouseUp)
|
||||||
|
|
||||||
|
this.editor.on('transaction', this.onTransaction)
|
||||||
|
},
|
||||||
|
beforeUnmount () {
|
||||||
|
if (!this.editor.isDestroyed) {
|
||||||
|
this.editor.view.dom.removeEventListener('mousedown', this.onMouseDown)
|
||||||
|
this.editor.off('transaction', this.onTransaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.removeEventListener('mouseup', this.onMouseUp)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleBold () {
|
||||||
|
this.editor.chain().focus().toggleBold().run()
|
||||||
|
},
|
||||||
|
toggleItalic () {
|
||||||
|
this.editor.chain().focus().toggleItalic().run()
|
||||||
|
},
|
||||||
|
toggleUnderline () {
|
||||||
|
this.editor.chain().focus().toggleUnderline().run()
|
||||||
|
},
|
||||||
|
toggleStrike () {
|
||||||
|
this.editor.chain().focus().toggleStrike().run()
|
||||||
|
},
|
||||||
|
wrapVariable () {
|
||||||
|
const { from, to } = this.editor.state.selection
|
||||||
|
const replacement = '[[variable]]'
|
||||||
|
const varFrom = from + 2
|
||||||
|
const varTo = varFrom + 8
|
||||||
|
|
||||||
|
this.editor.chain().focus()
|
||||||
|
.insertContentAt({ from, to }, replacement)
|
||||||
|
.setTextSelection({ from: varFrom, to: varTo })
|
||||||
|
.run()
|
||||||
|
|
||||||
|
this.$emit('add-variable')
|
||||||
|
},
|
||||||
|
wrapCondition () {
|
||||||
|
const { from, to } = this.editor.state.selection
|
||||||
|
const endText = '[[end]]'
|
||||||
|
const ifText = '[[if:variable]]'
|
||||||
|
|
||||||
|
this.editor.chain().focus()
|
||||||
|
.insertContentAt(to, endText)
|
||||||
|
.insertContentAt(from, ifText)
|
||||||
|
.setTextSelection({ from: from + 5, to: from + 13 })
|
||||||
|
.run()
|
||||||
|
|
||||||
|
this.$emit('add-condition')
|
||||||
|
},
|
||||||
|
onMouseDown () {
|
||||||
|
this.isMouseDown = true
|
||||||
|
},
|
||||||
|
onMouseUp () {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isMouseDown = false
|
||||||
|
}, 1)
|
||||||
|
},
|
||||||
|
onTransaction () {
|
||||||
|
this.isBold = this.editor.isActive('bold')
|
||||||
|
this.isItalic = this.editor.isActive('italic')
|
||||||
|
this.isUnderline = this.editor.isActive('underline')
|
||||||
|
this.isStrike = this.editor.isActive('strike')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,901 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="relative bg-white 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': isDrawMode && editable }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="isDrawMode && editable && cursorHighlightCoords"
|
||||||
|
class="absolute pointer-events-none z-10 bg-black"
|
||||||
|
:style="{ width: '1px', height: cursorHighlightCoords.height + 'px', left: cursorHighlightCoords.x + 'px', top: cursorHighlightCoords.y + 'px' }"
|
||||||
|
/>
|
||||||
|
<div :style="{ zoom: containerWidth / sectionWidthPx }">
|
||||||
|
<section
|
||||||
|
:id="section.id"
|
||||||
|
ref="editorElement"
|
||||||
|
dir="auto"
|
||||||
|
:class="section.classList.value"
|
||||||
|
:style="section.style.cssText"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Teleport
|
||||||
|
v-if="editor"
|
||||||
|
:to="container"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="areaToolbarCoords && selectedField && selectedArea && !isAreaDrag"
|
||||||
|
class="absolute z-10"
|
||||||
|
:style="{ left: areaToolbarCoords.left + 'px', top: areaToolbarCoords.top + 'px' }"
|
||||||
|
>
|
||||||
|
<AreaTitle
|
||||||
|
:area="selectedArea"
|
||||||
|
:field="selectedField"
|
||||||
|
:editable="editable"
|
||||||
|
:template="template"
|
||||||
|
:selected-areas-ref="selectedAreasRef"
|
||||||
|
:get-field-type-index="getFieldTypeIndex"
|
||||||
|
@remove="onRemoveSelectedArea"
|
||||||
|
@change="onSelectedAreaChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DynamicMenu
|
||||||
|
v-if="editable"
|
||||||
|
v-show="!selectedAreasRef.value.length"
|
||||||
|
:editor="editor"
|
||||||
|
:coords="dynamicMenuCoords"
|
||||||
|
@add-variable="dynamicMenuCoords = null"
|
||||||
|
@add-condition="dynamicMenuCoords = null"
|
||||||
|
/>
|
||||||
|
<FieldContextMenu
|
||||||
|
v-if="contextMenu && contextMenuField"
|
||||||
|
:context-menu="contextMenu"
|
||||||
|
:field="contextMenuField"
|
||||||
|
:with-copy-to-all-pages="false"
|
||||||
|
@close="closeContextMenu"
|
||||||
|
@copy="onContextMenuCopy"
|
||||||
|
@delete="onContextMenuDelete"
|
||||||
|
@add-custom-field="$emit('add-custom-field', $event)"
|
||||||
|
@set-draw="$emit('set-draw', $event)"
|
||||||
|
@save="save"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { shallowRef } from 'vue'
|
||||||
|
import { DOMSerializer, Fragment } from '@tiptap/pm/model'
|
||||||
|
import { v4 } from 'uuid'
|
||||||
|
import FieldContextMenu from './field_context_menu.vue'
|
||||||
|
import AreaTitle from './area_title.vue'
|
||||||
|
import DynamicMenu from './dynamic_menu.vue'
|
||||||
|
import { buildEditor } from './dynamic_editor.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DynamicSection',
|
||||||
|
components: {
|
||||||
|
DynamicMenu,
|
||||||
|
FieldContextMenu,
|
||||||
|
AreaTitle
|
||||||
|
},
|
||||||
|
inject: ['template', 'save', 't', 'fieldsDragFieldRef', 'customDragFieldRef', 'selectedAreasRef', 'getFieldTypeIndex', 'fieldTypes', 'withPhone', 'withPayment', 'withVerification', 'withKba', 'backgroundColor'],
|
||||||
|
props: {
|
||||||
|
section: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
sectionRefs: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
containerWidth: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
attachmentsIndex: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
selectedSubmitter: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
dragField: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
drawField: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
drawFieldType: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
drawCustomField: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
renderHtmlForSaveRef: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
drawOption: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
attachmentUuid: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['update', 'draw', 'set-draw', 'add-custom-field'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isAreaDrag: false,
|
||||||
|
areaToolbarCoords: null,
|
||||||
|
dynamicMenuCoords: null,
|
||||||
|
contextMenu: null,
|
||||||
|
cursorHighlightCoords: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
defaultHeight () {
|
||||||
|
return CSS.supports('height', '1lh') ? '1lh' : '1em'
|
||||||
|
},
|
||||||
|
fieldAreaIndex () {
|
||||||
|
return (this.template.fields || []).reduce((acc, field) => {
|
||||||
|
field.areas?.forEach((area) => {
|
||||||
|
acc[area.uuid] = { area, field }
|
||||||
|
})
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
defaultSizes () {
|
||||||
|
return {
|
||||||
|
checkbox: { width: '18px', height: '18px' },
|
||||||
|
radio: { width: '18px', height: '18px' },
|
||||||
|
multiple: { width: '18px', height: '18px' },
|
||||||
|
signature: { width: '140px', height: '50px' },
|
||||||
|
initials: { width: '40px', height: '32px' },
|
||||||
|
stamp: { width: '150px', height: '80px' },
|
||||||
|
kba: { width: '150px', height: '80px' },
|
||||||
|
verification: { width: '150px', height: '80px' },
|
||||||
|
image: { width: '200px', height: '100px' },
|
||||||
|
date: { width: '100px', height: this.defaultHeight },
|
||||||
|
datenow: { width: '100px', height: this.defaultHeight },
|
||||||
|
text: { width: '120px', height: this.defaultHeight },
|
||||||
|
cells: { width: '120px', height: this.defaultHeight },
|
||||||
|
file: { width: '120px', height: this.defaultHeight },
|
||||||
|
payment: { width: '120px', height: this.defaultHeight },
|
||||||
|
number: { width: '80px', height: this.defaultHeight },
|
||||||
|
select: { width: '120px', height: this.defaultHeight },
|
||||||
|
phone: { width: '120px', height: this.defaultHeight }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
editorRef: () => shallowRef(),
|
||||||
|
editor () {
|
||||||
|
return this.editorRef.value
|
||||||
|
},
|
||||||
|
sectionWidthPx () {
|
||||||
|
const pt = parseFloat(this.section.style.width)
|
||||||
|
|
||||||
|
return pt * (96 / 72)
|
||||||
|
},
|
||||||
|
zoom () {
|
||||||
|
return this.containerWidth / this.sectionWidthPx
|
||||||
|
},
|
||||||
|
isDraggingField () {
|
||||||
|
return !!(this.fieldsDragFieldRef?.value || this.customDragFieldRef?.value || this.dragField)
|
||||||
|
},
|
||||||
|
isDrawMode () {
|
||||||
|
return !!(this.drawField || this.drawCustomField || this.drawFieldType)
|
||||||
|
},
|
||||||
|
selectedArea () {
|
||||||
|
return this.selectedAreasRef.value[0]
|
||||||
|
},
|
||||||
|
selectedField () {
|
||||||
|
if (this.selectedArea) {
|
||||||
|
return this.fieldAreaIndex[this.selectedArea.uuid]?.field
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contextMenuField () {
|
||||||
|
if (this.contextMenu?.areaUuid) {
|
||||||
|
return this.fieldAreaIndex[this.contextMenu.areaUuid].field
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
containerWidth () {
|
||||||
|
this.closeContextMenu()
|
||||||
|
|
||||||
|
if (this.dynamicMenuCoords && this.editor && !this.editor.state.selection.empty) {
|
||||||
|
this.$nextTick(() => this.setDynamicMenuCoords(this.editor))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.editorRef.value = buildEditor({
|
||||||
|
dynamicAreaProps: {
|
||||||
|
template: this.template,
|
||||||
|
t: this.t,
|
||||||
|
selectedAreasRef: this.selectedAreasRef,
|
||||||
|
getFieldTypeIndex: this.getFieldTypeIndex,
|
||||||
|
findFieldArea: (areaUuid) => this.fieldAreaIndex[areaUuid],
|
||||||
|
getZoom: () => this.zoom,
|
||||||
|
onAreaContextMenu: this.onAreaContextMenu,
|
||||||
|
onAreaResize: this.onAreaResize,
|
||||||
|
onAreaDragStart: this.onAreaDragStart
|
||||||
|
},
|
||||||
|
attachmentsIndex: this.attachmentsIndex,
|
||||||
|
onFieldDrop: this.onFieldDrop,
|
||||||
|
onFieldDestroy: this.onFieldDestroy,
|
||||||
|
renderHtmlForSaveRef: this.renderHtmlForSaveRef,
|
||||||
|
editorOptions: {
|
||||||
|
element: this.$refs.editorElement,
|
||||||
|
editable: this.editable,
|
||||||
|
content: this.section.innerHTML,
|
||||||
|
onUpdate: (event) => this.$emit('update', event),
|
||||||
|
onSelectionUpdate: this.onSelectionUpdate,
|
||||||
|
onBlur: () => { this.dynamicMenuCoords = null }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.editor.view.dom.addEventListener('paste', this.onEditorPaste, true)
|
||||||
|
this.editor.view.dom.addEventListener('pointerdown', this.onEditorPointerDown, true)
|
||||||
|
this.editor.view.dom.addEventListener('mousemove', this.onEditorMouseMove)
|
||||||
|
this.editor.view.dom.addEventListener('mouseleave', this.onEditorMouseLeave)
|
||||||
|
this.editor.view.dom.addEventListener('keydown', this.onEditorKeyDown)
|
||||||
|
},
|
||||||
|
beforeUnmount () {
|
||||||
|
if (this.editor) {
|
||||||
|
this.editor.view.dom.removeEventListener('paste', this.onEditorPaste, true)
|
||||||
|
this.editor.view.dom.removeEventListener('pointerdown', this.onEditorPointerDown, true)
|
||||||
|
this.editor.view.dom.removeEventListener('mousemove', this.onEditorMouseMove)
|
||||||
|
this.editor.view.dom.removeEventListener('mouseleave', this.onEditorMouseLeave)
|
||||||
|
this.editor.view.dom.removeEventListener('keydown', this.onEditorKeyDown)
|
||||||
|
this.editor.destroy()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
findAreaNodePos (areaUuid) {
|
||||||
|
const el = this.editor.view.dom.querySelector(`[data-area-uuid="${areaUuid}"]`)
|
||||||
|
|
||||||
|
return this.editor.view.posAtDOM(el, 0)
|
||||||
|
},
|
||||||
|
getFieldInsertIndex (pos) {
|
||||||
|
const view = this.editor.view
|
||||||
|
const fields = this.template.fields || []
|
||||||
|
|
||||||
|
if (!fields.length) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
let previousField = null
|
||||||
|
|
||||||
|
view.state.doc.nodesBetween(0, pos, (node, nodePos) => {
|
||||||
|
if (node.type.name !== 'fieldNode') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodePos + node.nodeSize <= pos) {
|
||||||
|
previousField = this.fieldAreaIndex[node.attrs.areaUuid]?.field || previousField
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!previousField) {
|
||||||
|
previousField = this.getPreviousSectionField()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!previousField) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousFieldIndex = fields.indexOf(previousField)
|
||||||
|
|
||||||
|
return previousFieldIndex === -1 ? fields.length : previousFieldIndex + 1
|
||||||
|
},
|
||||||
|
insertFieldInTemplate (field, index) {
|
||||||
|
const currentFieldIndex = this.template.fields.indexOf(field)
|
||||||
|
|
||||||
|
if (currentFieldIndex !== -1) {
|
||||||
|
return currentFieldIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
this.template.fields.splice(index, 0, field)
|
||||||
|
|
||||||
|
return index
|
||||||
|
},
|
||||||
|
getLastFieldInSection (sectionRef) {
|
||||||
|
let lastField = null
|
||||||
|
|
||||||
|
sectionRef.editor.state.doc.descendants((node) => {
|
||||||
|
if (node.type.name === 'fieldNode') {
|
||||||
|
lastField = this.fieldAreaIndex[node.attrs.areaUuid]?.field || lastField
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return lastField
|
||||||
|
},
|
||||||
|
getPreviousSectionField () {
|
||||||
|
const sectionIndex = this.sectionRefs.indexOf(this)
|
||||||
|
|
||||||
|
for (let index = sectionIndex - 1; index >= 0; index -= 1) {
|
||||||
|
const previousField = this.getLastFieldInSection(this.sectionRefs[index])
|
||||||
|
|
||||||
|
if (previousField) {
|
||||||
|
return previousField
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
removeArea (area) {
|
||||||
|
const { field } = this.fieldAreaIndex[area.uuid]
|
||||||
|
const areaIndex = field.areas.indexOf(area)
|
||||||
|
|
||||||
|
if (areaIndex !== -1) {
|
||||||
|
field.areas.splice(areaIndex, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.areas.length === 0) {
|
||||||
|
this.template.fields.splice(this.template.fields.indexOf(field), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pos = this.findAreaNodePos(area.uuid)
|
||||||
|
|
||||||
|
this.editor.chain().focus().deleteRange({ from: pos, to: pos + 1 }).run()
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
|
onSelectionUpdate ({ editor }) {
|
||||||
|
const { selection } = editor.state
|
||||||
|
|
||||||
|
if (selection.node?.type.name === 'fieldNode') {
|
||||||
|
const { areaUuid } = selection.node.attrs
|
||||||
|
|
||||||
|
const field = this.fieldAreaIndex[areaUuid]?.field
|
||||||
|
|
||||||
|
if (field) {
|
||||||
|
const area = field.areas.find((a) => a.uuid === areaUuid)
|
||||||
|
|
||||||
|
if (area) {
|
||||||
|
const dom = editor.view.nodeDOM(selection.from)
|
||||||
|
const areaEl = dom.shadowRoot.firstElementChild
|
||||||
|
|
||||||
|
if (areaEl) {
|
||||||
|
const rect = areaEl.getBoundingClientRect()
|
||||||
|
const containerRect = this.container.getBoundingClientRect()
|
||||||
|
|
||||||
|
this.areaToolbarCoords = {
|
||||||
|
left: rect.left - containerRect.left,
|
||||||
|
top: rect.top - containerRect.top
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectedAreasRef.value = [area]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.areaToolbarCoords = null
|
||||||
|
this.selectedAreasRef.value = []
|
||||||
|
|
||||||
|
if (editor.state.selection.empty) {
|
||||||
|
this.dynamicMenuCoords = null
|
||||||
|
} else {
|
||||||
|
this.setDynamicMenuCoords(editor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setDynamicMenuCoords (editor) {
|
||||||
|
const { from, to } = editor.state.selection
|
||||||
|
const view = editor.view
|
||||||
|
const start = view.coordsAtPos(from)
|
||||||
|
const end = view.coordsAtPos(to)
|
||||||
|
const containerRect = this.container.getBoundingClientRect()
|
||||||
|
const left = (start.left + end.right) / 2 - containerRect.left
|
||||||
|
|
||||||
|
this.dynamicMenuCoords = {
|
||||||
|
top: Math.min(start.top, end.top) - containerRect.top,
|
||||||
|
left: Math.max(80, Math.min(left, containerRect.width - 80))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFieldDestroy (node) {
|
||||||
|
this.selectedAreasRef.value = []
|
||||||
|
|
||||||
|
const { areaUuid } = node.attrs
|
||||||
|
|
||||||
|
let nodeExistsInDoc = false
|
||||||
|
|
||||||
|
this.editor.state.doc.descendants((docNode) => {
|
||||||
|
if (docNode.attrs.areaUuid === areaUuid) {
|
||||||
|
nodeExistsInDoc = true
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (nodeExistsInDoc) return
|
||||||
|
|
||||||
|
const fieldArea = this.fieldAreaIndex[areaUuid]
|
||||||
|
|
||||||
|
if (!fieldArea) return
|
||||||
|
|
||||||
|
const field = fieldArea.field
|
||||||
|
|
||||||
|
const areaIndex = field.areas.findIndex((a) => a.uuid === areaUuid)
|
||||||
|
|
||||||
|
if (areaIndex !== -1) {
|
||||||
|
field.areas.splice(areaIndex, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!field.areas?.length) {
|
||||||
|
this.template.fields.splice(this.template.fields.indexOf(field), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
|
onAreaResize (rect) {
|
||||||
|
const containerRect = this.container.getBoundingClientRect()
|
||||||
|
|
||||||
|
this.areaToolbarCoords = {
|
||||||
|
left: rect.left - containerRect.left,
|
||||||
|
top: rect.top - containerRect.top
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onAreaDragStart () {
|
||||||
|
this.isAreaDrag = true
|
||||||
|
},
|
||||||
|
onAreaContextMenu (area, e) {
|
||||||
|
this.contextMenu = {
|
||||||
|
x: e.clientX,
|
||||||
|
y: e.clientY,
|
||||||
|
areaUuid: area.uuid
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deselectArea () {
|
||||||
|
this.areaToolbarCoords = null
|
||||||
|
this.selectedAreasRef.value = []
|
||||||
|
},
|
||||||
|
closeContextMenu () {
|
||||||
|
this.contextMenu = null
|
||||||
|
},
|
||||||
|
onEditorMouseLeave () {
|
||||||
|
this.cursorHighlightCoords = null
|
||||||
|
},
|
||||||
|
onEditorKeyDown (event) {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
this.editor.chain().setNodeSelection(0).blur().run()
|
||||||
|
this.deselectArea()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onEditorMouseMove (event) {
|
||||||
|
if (!this.isDrawMode || !this.editable) {
|
||||||
|
this.cursorHighlightCoords = null
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const view = this.editor?.view
|
||||||
|
|
||||||
|
if (!view) return
|
||||||
|
|
||||||
|
const pos = view.posAtCoords({ left: event.clientX, top: event.clientY })
|
||||||
|
|
||||||
|
if (!pos) {
|
||||||
|
this.cursorHighlightCoords = null
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const coords = view.coordsAtPos(pos.pos)
|
||||||
|
const outerRect = this.$el.getBoundingClientRect()
|
||||||
|
const lineHeight = coords.bottom - coords.top
|
||||||
|
|
||||||
|
this.cursorHighlightCoords = {
|
||||||
|
x: coords.left - outerRect.left,
|
||||||
|
y: coords.top - outerRect.top,
|
||||||
|
height: lineHeight
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getFieldNode (areaUuid) {
|
||||||
|
const pos = this.findAreaNodePos(areaUuid)
|
||||||
|
|
||||||
|
return this.editor.state.doc.nodeAt(pos)
|
||||||
|
},
|
||||||
|
serializeFieldNodeHtml (areaUuid) {
|
||||||
|
const node = this.getFieldNode(areaUuid)
|
||||||
|
|
||||||
|
if (!node) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const serializer = DOMSerializer.fromSchema(this.editor.state.schema)
|
||||||
|
const container = document.createElement('div')
|
||||||
|
|
||||||
|
container.appendChild(serializer.serializeFragment(Fragment.from(node)))
|
||||||
|
|
||||||
|
return container.innerHTML
|
||||||
|
},
|
||||||
|
async writeHtmlToClipboard ({ html, text }, clipboardData = null) {
|
||||||
|
if (clipboardData) {
|
||||||
|
clipboardData.setData('text/html', html)
|
||||||
|
clipboardData.setData('text/plain', text || html)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navigator.clipboard?.write && window.ClipboardItem) {
|
||||||
|
await navigator.clipboard.write([
|
||||||
|
new ClipboardItem({
|
||||||
|
'text/html': new Blob([html], { type: 'text/html' }),
|
||||||
|
'text/plain': new Blob([text || html], { type: 'text/plain' })
|
||||||
|
})
|
||||||
|
])
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navigator.clipboard?.writeText) {
|
||||||
|
await navigator.clipboard.writeText(text || html)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
async copyFieldAreaToClipboard (areaUuid, clipboardData = null) {
|
||||||
|
const html = this.serializeFieldNodeHtml(areaUuid)
|
||||||
|
|
||||||
|
if (!html) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.writeHtmlToClipboard({ html, text: html }, clipboardData)
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to copy dynamic field:', e)
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onContextMenuCopy () {
|
||||||
|
await this.copyFieldAreaToClipboard(this.contextMenu.areaUuid)
|
||||||
|
|
||||||
|
this.closeContextMenu()
|
||||||
|
},
|
||||||
|
buildCopiedField (payload) {
|
||||||
|
const field = JSON.parse(JSON.stringify(payload.field))
|
||||||
|
const area = {
|
||||||
|
...JSON.parse(JSON.stringify(payload.area)),
|
||||||
|
uuid: v4(),
|
||||||
|
attachment_uuid: this.attachmentUuid
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.templateId !== this.template.id) {
|
||||||
|
delete field.conditions
|
||||||
|
|
||||||
|
if (field.preferences) {
|
||||||
|
delete field.preferences.formula
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newField = {
|
||||||
|
...field,
|
||||||
|
uuid: v4(),
|
||||||
|
submitter_uuid: this.selectedSubmitter.uuid,
|
||||||
|
areas: [area]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['radio', 'multiple'].includes(field.type) && field.options?.length) {
|
||||||
|
const oldOptionUuid = area.option_uuid
|
||||||
|
const optionsMap = {}
|
||||||
|
|
||||||
|
newField.options = field.options.map((opt) => {
|
||||||
|
const newUuid = v4()
|
||||||
|
optionsMap[opt.uuid] = newUuid
|
||||||
|
|
||||||
|
return { ...opt, uuid: newUuid }
|
||||||
|
})
|
||||||
|
|
||||||
|
area.option_uuid = optionsMap[oldOptionUuid] || newField.options[0]?.uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
return { field: newField, area }
|
||||||
|
},
|
||||||
|
onEditorPaste (event) {
|
||||||
|
const clipboardData = event.clipboardData
|
||||||
|
|
||||||
|
if (!clipboardData) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = clipboardData.getData('text/html')
|
||||||
|
const text = clipboardData.getData('text/plain')
|
||||||
|
const clipboardHtml = html || (text.includes('<dynamic-field') ? text : '')
|
||||||
|
|
||||||
|
if (!clipboardHtml || !clipboardHtml.includes('data-field=')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = document.createElement('div')
|
||||||
|
|
||||||
|
container.innerHTML = clipboardHtml
|
||||||
|
|
||||||
|
const fieldNodes = [...container.querySelectorAll('dynamic-field[data-field][data-area]')]
|
||||||
|
|
||||||
|
if (!fieldNodes.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
const { selection } = this.editor.state
|
||||||
|
const { from, to } = selection
|
||||||
|
let insertIndex = this.getFieldInsertIndex(selection.node?.type.name === 'fieldNode' ? to : from)
|
||||||
|
|
||||||
|
let lastArea = null
|
||||||
|
|
||||||
|
fieldNodes.forEach((fieldNode) => {
|
||||||
|
const fieldValue = fieldNode.dataset.field
|
||||||
|
const areaValue = fieldNode.dataset.area
|
||||||
|
const templateId = fieldNode.dataset.templateId
|
||||||
|
|
||||||
|
if (!fieldValue || !areaValue) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { field, area } = this.buildCopiedField({
|
||||||
|
field: JSON.parse(fieldValue),
|
||||||
|
area: JSON.parse(areaValue),
|
||||||
|
templateId: Number(templateId)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.insertFieldInTemplate(field, insertIndex)
|
||||||
|
insertIndex += 1
|
||||||
|
|
||||||
|
fieldNode.setAttribute('uuid', field.uuid)
|
||||||
|
fieldNode.setAttribute('area-uuid', area.uuid)
|
||||||
|
fieldNode.removeAttribute('data-field')
|
||||||
|
fieldNode.removeAttribute('data-area')
|
||||||
|
fieldNode.removeAttribute('data-template-id')
|
||||||
|
|
||||||
|
lastArea = area
|
||||||
|
})
|
||||||
|
|
||||||
|
this.editor.chain().focus().insertContentAt({ from, to }, container.innerHTML).run()
|
||||||
|
|
||||||
|
if (lastArea) {
|
||||||
|
this.editor.commands.setNodeSelection(this.findAreaNodePos(lastArea.uuid))
|
||||||
|
}
|
||||||
|
|
||||||
|
this.closeContextMenu()
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
|
onContextMenuDelete () {
|
||||||
|
const menu = this.contextMenu
|
||||||
|
const fieldArea = this.fieldAreaIndex[menu.areaUuid]
|
||||||
|
|
||||||
|
if (fieldArea) {
|
||||||
|
this.removeArea(fieldArea.area)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.closeContextMenu()
|
||||||
|
this.deselectArea()
|
||||||
|
},
|
||||||
|
onRemoveSelectedArea () {
|
||||||
|
this.removeArea(this.selectedArea)
|
||||||
|
|
||||||
|
this.deselectArea()
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
|
onSelectedAreaChange () {
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
|
onFieldDrop (view, event, _slice, moved) {
|
||||||
|
this.isAreaDrag = false
|
||||||
|
|
||||||
|
if (moved) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const draggedField = this.fieldsDragFieldRef?.value || this.customDragFieldRef?.value || this.dragField
|
||||||
|
|
||||||
|
if (!draggedField) return false
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
const pos = view.posAtCoords({ left: event.clientX, top: event.clientY })
|
||||||
|
|
||||||
|
if (!pos) return false
|
||||||
|
|
||||||
|
this.insertFieldAtRange({
|
||||||
|
sourceField: draggedField,
|
||||||
|
existingField: this.fieldsDragFieldRef?.value,
|
||||||
|
from: pos.pos
|
||||||
|
})
|
||||||
|
|
||||||
|
this.fieldsDragFieldRef.value = null
|
||||||
|
this.customDragFieldRef.value = null
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
onEditorPointerDown (event) {
|
||||||
|
if (!this.isDrawMode || !this.editable || this.isDraggingField) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.button === 2) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const view = this.editor?.view
|
||||||
|
|
||||||
|
if (!view) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const selection = view.state.selection
|
||||||
|
const isTextRangeSelection = !selection.empty && !selection.node
|
||||||
|
|
||||||
|
let from = selection.from
|
||||||
|
let to = selection.to
|
||||||
|
|
||||||
|
if (!isTextRangeSelection) {
|
||||||
|
const pos = view.posAtCoords({ left: event.clientX, top: event.clientY })
|
||||||
|
|
||||||
|
if (!pos) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
from = pos.pos
|
||||||
|
to = pos.pos
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceField = this.drawField || this.drawCustomField || { type: this.drawFieldType }
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
if (this.drawOption && this.drawField) {
|
||||||
|
const areaWithoutOption = this.drawField.areas?.find((a) => !a.option_uuid)
|
||||||
|
|
||||||
|
if (areaWithoutOption && !this.drawField.areas.find((a) => a.option_uuid === this.drawField.options[0].uuid)) {
|
||||||
|
areaWithoutOption.option_uuid = this.drawField.options[0].uuid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const inserted = this.insertFieldAtRange({
|
||||||
|
sourceField,
|
||||||
|
existingField: this.drawField,
|
||||||
|
optionUuid: this.drawOption?.uuid,
|
||||||
|
from,
|
||||||
|
to
|
||||||
|
})
|
||||||
|
|
||||||
|
if (inserted) {
|
||||||
|
this.$emit('draw', inserted)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
buildFieldArea ({ optionUuid = null } = {}) {
|
||||||
|
const area = {
|
||||||
|
uuid: v4(),
|
||||||
|
attachment_uuid: this.attachmentUuid
|
||||||
|
}
|
||||||
|
|
||||||
|
if (optionUuid) {
|
||||||
|
area.option_uuid = optionUuid
|
||||||
|
}
|
||||||
|
|
||||||
|
return area
|
||||||
|
},
|
||||||
|
buildNewField (sourceField, area) {
|
||||||
|
const fieldType = sourceField?.type || 'text'
|
||||||
|
const newField = {
|
||||||
|
name: sourceField?.name || '',
|
||||||
|
uuid: v4(),
|
||||||
|
required: fieldType !== 'checkbox',
|
||||||
|
submitter_uuid: this.selectedSubmitter.uuid,
|
||||||
|
type: fieldType,
|
||||||
|
areas: [area]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['select', 'multiple', 'radio'].includes(fieldType)) {
|
||||||
|
if (sourceField?.options?.length) {
|
||||||
|
newField.options = sourceField.options.map((opt) => ({
|
||||||
|
value: typeof opt === 'string' ? opt : opt.value,
|
||||||
|
uuid: v4()
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
newField.options = [{ value: '', uuid: v4() }, { value: '', uuid: v4() }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fieldType === 'datenow') {
|
||||||
|
newField.type = 'date'
|
||||||
|
newField.readonly = true
|
||||||
|
newField.default_value = '{{date}}'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['stamp', 'heading', 'strikethrough'].includes(fieldType)) {
|
||||||
|
newField.readonly = true
|
||||||
|
|
||||||
|
if (fieldType === 'strikethrough') {
|
||||||
|
newField.default_value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newField
|
||||||
|
},
|
||||||
|
insertFieldAtRange ({ sourceField, existingField = null, optionUuid = null, from, to = from }) {
|
||||||
|
if (!sourceField) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldType = sourceField.type || existingField?.type || 'text'
|
||||||
|
const dims = this.defaultSizes[fieldType]
|
||||||
|
const area = this.buildFieldArea({ optionUuid })
|
||||||
|
const insertIndex = this.getFieldInsertIndex(from)
|
||||||
|
|
||||||
|
let field = existingField
|
||||||
|
const view = this.editor.view
|
||||||
|
|
||||||
|
if (field) {
|
||||||
|
if (!this.template.fields.includes(field)) {
|
||||||
|
this.insertFieldInTemplate(field, insertIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
field.areas = field.areas || []
|
||||||
|
field.areas.push(area)
|
||||||
|
} else {
|
||||||
|
field = this.buildNewField(sourceField, area)
|
||||||
|
this.insertFieldInTemplate(field, insertIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeType = view.state.schema.nodes.fieldNode
|
||||||
|
const fieldNode = nodeType.create({
|
||||||
|
uuid: field.uuid,
|
||||||
|
areaUuid: area.uuid,
|
||||||
|
width: dims.width,
|
||||||
|
height: dims.height
|
||||||
|
})
|
||||||
|
|
||||||
|
const tr = from !== to
|
||||||
|
? view.state.tr.replaceWith(from, to, fieldNode)
|
||||||
|
: view.state.tr.insert(from, fieldNode)
|
||||||
|
|
||||||
|
view.dispatch(tr)
|
||||||
|
|
||||||
|
this.editor.chain().focus().setNodeSelection(from).run()
|
||||||
|
this.save()
|
||||||
|
|
||||||
|
return { area, field, pos: from }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
@config "../../../tailwind.dynamic.config.js";
|
||||||
|
|
||||||
|
@import "tailwindcss/components";
|
||||||
|
@import "tailwindcss/utilities";
|
||||||
|
|
||||||
|
*,
|
||||||
|
::before,
|
||||||
|
::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-width: 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
::before,
|
||||||
|
::after {
|
||||||
|
--tw-content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
:host {
|
||||||
|
all: initial;
|
||||||
|
}
|
||||||
@@ -0,0 +1,422 @@
|
|||||||
|
<template>
|
||||||
|
<div class="group">
|
||||||
|
<div class="flex items-center justify-between py-1.5 px-0.5">
|
||||||
|
<div class="flex items-center space-x-1 min-w-0">
|
||||||
|
<FieldType
|
||||||
|
:model-value="formType"
|
||||||
|
:editable="editable"
|
||||||
|
:button-width="18"
|
||||||
|
:menu-classes="'mt-1.5'"
|
||||||
|
:menu-style="{ backgroundColor: dropdownBgColor }"
|
||||||
|
@update:model-value="onTypeChange"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="truncate"
|
||||||
|
:title="path"
|
||||||
|
>{{ displayName }}</span>
|
||||||
|
<span
|
||||||
|
v-if="isArray"
|
||||||
|
class="text-xs bg-base-200 rounded px-1 flex-shrink-0"
|
||||||
|
>{{ t('list') }}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="editable"
|
||||||
|
class="flex items-center flex-shrink-0"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="dropdown dropdown-end"
|
||||||
|
@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 }"
|
||||||
|
@click="closeDropdown"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
|
||||||
|
@change="onTypeChange($event.target.value)"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="varType in variableTypes"
|
||||||
|
:key="varType"
|
||||||
|
:value="varType"
|
||||||
|
:selected="varType === formType"
|
||||||
|
>{{ t(varType) }}</option>
|
||||||
|
</select>
|
||||||
|
<label
|
||||||
|
:style="{ backgroundColor: dropdownBgColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>{{ t('type') }}</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="formType === 'number'"
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
|
||||||
|
@change="[schema.format = $event.target.value, save()]"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="format in numberFormats"
|
||||||
|
:key="format"
|
||||||
|
:value="format"
|
||||||
|
:selected="format === schema.format || (format === 'none' && !schema.format)"
|
||||||
|
>{{ formatNumber(123456789.567, format) }}</option>
|
||||||
|
</select>
|
||||||
|
<label
|
||||||
|
:style="{ backgroundColor: dropdownBgColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>{{ t('format') }}</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="['text', 'number'].includes(formType)"
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="schema.default_value"
|
||||||
|
:type="formType === 'number' ? 'number' : 'text'"
|
||||||
|
:placeholder="t('default_value')"
|
||||||
|
dir="auto"
|
||||||
|
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0 bg-transparent"
|
||||||
|
@blur="save"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
v-if="schema.default_value"
|
||||||
|
:style="{ backgroundColor: dropdownBgColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>{{ t('default_value') }}</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="formType === 'date'"
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
:value="schema.format || 'MM/DD/YYYY'"
|
||||||
|
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
|
||||||
|
@change="[schema.format = $event.target.value, save()]"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="format in dateFormats"
|
||||||
|
:key="format"
|
||||||
|
:value="format"
|
||||||
|
>{{ formatDate(new Date(), format) }}</option>
|
||||||
|
</select>
|
||||||
|
<label
|
||||||
|
:style="{ backgroundColor: dropdownBgColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>{{ t('format') }}</label>
|
||||||
|
</div>
|
||||||
|
<li
|
||||||
|
v-if="formType === 'date'"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<label class="cursor-pointer py-1.5">
|
||||||
|
<input
|
||||||
|
:checked="schema.default_value === '{{date}}'"
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-xs"
|
||||||
|
@change="[schema.default_value = $event.target.checked ? '{{date}}' : undefined, save()]"
|
||||||
|
>
|
||||||
|
<span class="label-text">{{ t('current_date') }}</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<div
|
||||||
|
v-if="['radio', 'select'].includes(formType)"
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
dir="auto"
|
||||||
|
class="select select-bordered select-xs w-full max-w-xs h-7 !outline-0 font-normal bg-transparent"
|
||||||
|
@change="[schema.default_value = $event.target.value || undefined, save()]"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
value=""
|
||||||
|
:selected="!schema.default_value"
|
||||||
|
>{{ t('none') }}</option>
|
||||||
|
<option
|
||||||
|
v-for="opt in (schema.options || [])"
|
||||||
|
:key="opt"
|
||||||
|
:value="opt"
|
||||||
|
:selected="schema.default_value === opt"
|
||||||
|
>{{ opt }}</option>
|
||||||
|
</select>
|
||||||
|
<label
|
||||||
|
:style="{ backgroundColor: dropdownBgColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>{{ t('default_value') }}</label>
|
||||||
|
</div>
|
||||||
|
<li
|
||||||
|
v-if="formType === 'checkbox'"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<label class="cursor-pointer py-1.5">
|
||||||
|
<input
|
||||||
|
:checked="schema.default_value === true"
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-xs"
|
||||||
|
@change="[schema.default_value = $event.target.checked || undefined, save()]"
|
||||||
|
>
|
||||||
|
<span class="label-text">{{ t('checked') }}</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li @click.stop>
|
||||||
|
<label class="cursor-pointer py-1.5">
|
||||||
|
<input
|
||||||
|
:checked="schema.required !== false"
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-xs"
|
||||||
|
@change="[schema.required = $event.target.checked, save()]"
|
||||||
|
>
|
||||||
|
<span class="label-text">{{ t('required') }}</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="['radio', 'select'].includes(formType) && schema.options"
|
||||||
|
ref="options"
|
||||||
|
class="pl-2 pr-1 pb-1.5 space-y-1.5"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(option, index) in schema.options"
|
||||||
|
:key="index"
|
||||||
|
class="flex space-x-1.5 items-center"
|
||||||
|
>
|
||||||
|
<span class="text-sm w-3.5 select-none">{{ index + 1 }}.</span>
|
||||||
|
<input
|
||||||
|
:value="option"
|
||||||
|
class="w-full input input-primary input-xs text-sm bg-transparent"
|
||||||
|
type="text"
|
||||||
|
dir="auto"
|
||||||
|
:placeholder="`${t('option')} ${index + 1}`"
|
||||||
|
@blur="[schema.options.splice(index, 1, $event.target.value), save()]"
|
||||||
|
@keydown.enter="$event.target.value ? onOptionEnter(index, $event.target.value) : null"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="text-sm w-3.5"
|
||||||
|
tabindex="-1"
|
||||||
|
@click="[schema.options.splice(index, 1), save()]"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="text-center text-sm w-full pb-1"
|
||||||
|
@click="addOptionAndFocus((schema.options || []).length)"
|
||||||
|
>
|
||||||
|
+ {{ t('add_option') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import FieldType from './field_type'
|
||||||
|
import { IconSettings } from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DynamicVariable',
|
||||||
|
components: {
|
||||||
|
FieldType,
|
||||||
|
IconSettings
|
||||||
|
},
|
||||||
|
inject: ['t', 'save', 'backgroundColor'],
|
||||||
|
provide () {
|
||||||
|
return {
|
||||||
|
fieldTypes: ['text', 'number', 'date', 'checkbox', 'radio', 'select']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
path: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
groupKey: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isArray: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
renderDropdown: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
displayName () {
|
||||||
|
if (this.groupKey) {
|
||||||
|
const prefix = this.groupKey + (this.path.startsWith(this.groupKey + '[].') ? '[].' : '.')
|
||||||
|
|
||||||
|
return this.path.slice(prefix.length)
|
||||||
|
} else {
|
||||||
|
return this.path
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dropdownBgColor () {
|
||||||
|
return ['', null, 'transparent'].includes(this.backgroundColor) ? 'white' : this.backgroundColor
|
||||||
|
},
|
||||||
|
schemaTypeToFormType () {
|
||||||
|
return { string: 'text', number: 'number', boolean: 'checkbox', date: 'date' }
|
||||||
|
},
|
||||||
|
formType () {
|
||||||
|
return this.schema.form_type || this.schemaTypeToFormType[this.schema.type] || 'text'
|
||||||
|
},
|
||||||
|
variableTypes () {
|
||||||
|
return ['text', 'number', 'date', 'checkbox', 'radio', 'select']
|
||||||
|
},
|
||||||
|
formTypeToSchemaType () {
|
||||||
|
return { text: 'string', number: 'number', date: 'date', checkbox: 'boolean', radio: 'string', select: 'string' }
|
||||||
|
},
|
||||||
|
numberFormats () {
|
||||||
|
return [
|
||||||
|
'none',
|
||||||
|
'usd',
|
||||||
|
'eur',
|
||||||
|
'gbp',
|
||||||
|
'comma',
|
||||||
|
'dot',
|
||||||
|
'space'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
dateFormats () {
|
||||||
|
const formats = [
|
||||||
|
'MM/DD/YYYY',
|
||||||
|
'DD/MM/YYYY',
|
||||||
|
'YYYY-MM-DD',
|
||||||
|
'DD-MM-YYYY',
|
||||||
|
'DD.MM.YYYY',
|
||||||
|
'MMM D, YYYY',
|
||||||
|
'MMMM D, YYYY',
|
||||||
|
'D MMM YYYY',
|
||||||
|
'D MMMM YYYY'
|
||||||
|
]
|
||||||
|
|
||||||
|
if (Intl.DateTimeFormat().resolvedOptions().timeZone?.includes('Seoul') || navigator.language?.startsWith('ko')) {
|
||||||
|
formats.push('YYYY년 MM월 DD일')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.schema.format && !formats.includes(this.schema.format)) {
|
||||||
|
formats.unshift(this.schema.format)
|
||||||
|
}
|
||||||
|
|
||||||
|
return formats
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onTypeChange (newType) {
|
||||||
|
this.schema.type = this.formTypeToSchemaType[newType] || 'string'
|
||||||
|
this.schema.form_type = newType
|
||||||
|
|
||||||
|
if (['radio', 'select'].includes(newType)) {
|
||||||
|
if (!this.schema.options || !this.schema.options.length) {
|
||||||
|
this.schema.options = ['', '']
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delete this.schema.options
|
||||||
|
delete this.schema.default_value
|
||||||
|
delete this.schema.format
|
||||||
|
}
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
|
onOptionEnter (index, value) {
|
||||||
|
this.schema.options.splice(index, 1, value)
|
||||||
|
this.schema.options.splice(index + 1, 0, '')
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.options.querySelectorAll('input')[index + 1]?.focus()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
addOptionAndFocus (index) {
|
||||||
|
if (!this.schema.options) {
|
||||||
|
this.schema.options = []
|
||||||
|
}
|
||||||
|
|
||||||
|
this.schema.options.splice(index, 0, '')
|
||||||
|
this.save()
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.options.querySelectorAll('input')[index]?.focus()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
formatNumber (number, format) {
|
||||||
|
if (format === 'comma') {
|
||||||
|
return new Intl.NumberFormat('en-US').format(number)
|
||||||
|
} else if (format === 'usd') {
|
||||||
|
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
|
||||||
|
} else if (format === 'gbp') {
|
||||||
|
return new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
|
||||||
|
} else if (format === 'eur') {
|
||||||
|
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
|
||||||
|
} else if (format === 'dot') {
|
||||||
|
return new Intl.NumberFormat('de-DE').format(number)
|
||||||
|
} else if (format === 'space') {
|
||||||
|
return new Intl.NumberFormat('fr-FR').format(number)
|
||||||
|
} else {
|
||||||
|
return number
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formatDate (date, format) {
|
||||||
|
const monthFormats = { M: 'numeric', MM: '2-digit', MMM: 'short', MMMM: 'long' }
|
||||||
|
const dayFormats = { D: 'numeric', DD: '2-digit' }
|
||||||
|
const yearFormats = { YYYY: 'numeric', YY: '2-digit' }
|
||||||
|
|
||||||
|
const parts = new Intl.DateTimeFormat([], {
|
||||||
|
day: dayFormats[format.match(/D+/)],
|
||||||
|
month: monthFormats[format.match(/M+/)],
|
||||||
|
year: yearFormats[format.match(/Y+/)]
|
||||||
|
}).formatToParts(date)
|
||||||
|
|
||||||
|
return format
|
||||||
|
.replace(/D+/, parts.find((p) => p.type === 'day').value)
|
||||||
|
.replace(/M+/, parts.find((p) => p.type === 'month').value)
|
||||||
|
.replace(/Y+/, parts.find((p) => p.type === 'year').value)
|
||||||
|
},
|
||||||
|
closeDropdown () {
|
||||||
|
this.$el.getRootNode().activeElement.blur()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
v-if="!schemaEntries.length"
|
||||||
|
class="text-center py-4 px-2"
|
||||||
|
>
|
||||||
|
<p class="font-medium">
|
||||||
|
{{ t('no_variables') }}
|
||||||
|
</p>
|
||||||
|
<p class="text-sm mt-1">
|
||||||
|
{{ t('no_variables_description') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<template
|
||||||
|
v-for="([key, node], index) in schemaEntries"
|
||||||
|
:key="key"
|
||||||
|
>
|
||||||
|
<div v-if="isGroup(node)">
|
||||||
|
<hr
|
||||||
|
v-if="index > 0"
|
||||||
|
class="border-base-300"
|
||||||
|
>
|
||||||
|
<label class="peer flex items-center py-1.5 cursor-pointer select-none">
|
||||||
|
<span class="w-5 flex justify-center items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="hidden peer"
|
||||||
|
checked
|
||||||
|
>
|
||||||
|
<IconChevronDown
|
||||||
|
class="hidden peer-checked:block ml-0.5"
|
||||||
|
:width="14"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
<IconChevronRight
|
||||||
|
class="block peer-checked:hidden ml-0.5"
|
||||||
|
:width="14"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span class="ml-1">{{ key }}</span>
|
||||||
|
<span
|
||||||
|
v-if="node.type === 'array'"
|
||||||
|
class="text-xs bg-base-200 rounded px-1 ml-1"
|
||||||
|
>{{ t('list') }}</span>
|
||||||
|
</label>
|
||||||
|
<div class="hidden peer-has-[:checked]:block pl-5">
|
||||||
|
<template
|
||||||
|
v-for="[varNode, varPath] in nestedVariables(node, key)"
|
||||||
|
:key="varPath"
|
||||||
|
>
|
||||||
|
<hr class="border-base-300">
|
||||||
|
<DynamicVariable
|
||||||
|
:path="varPath"
|
||||||
|
:group-key="key"
|
||||||
|
:editable="editable"
|
||||||
|
:schema="varNode"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<hr
|
||||||
|
v-if="index > 0"
|
||||||
|
class="border-base-300"
|
||||||
|
>
|
||||||
|
<DynamicVariable
|
||||||
|
:path="key"
|
||||||
|
:editable="editable"
|
||||||
|
:schema="node.type === 'array' && node.items ? node.items : node"
|
||||||
|
:is-array="node.type === 'array'"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import DynamicVariable from './dynamic_variable'
|
||||||
|
import { IconChevronDown, IconChevronRight } from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DynamicVariables',
|
||||||
|
components: {
|
||||||
|
DynamicVariable,
|
||||||
|
IconChevronDown,
|
||||||
|
IconChevronRight
|
||||||
|
},
|
||||||
|
inject: ['t', 'template', 'save', 'backgroundColor'],
|
||||||
|
props: {
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
schemaEntries () {
|
||||||
|
return Object.entries(this.template.variables_schema || {}).filter(([, node]) => !node.disabled)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
isGroup (node) {
|
||||||
|
return (node.type === 'object' && node.properties) || (node.type === 'array' && node.items?.type === 'object' && node.items?.properties)
|
||||||
|
},
|
||||||
|
nestedVariables (node, groupKey) {
|
||||||
|
const properties = node.type === 'array' ? node.items?.properties : node.properties
|
||||||
|
|
||||||
|
if (!properties) return []
|
||||||
|
|
||||||
|
const prefix = node.type === 'array' ? `${groupKey}[]` : groupKey
|
||||||
|
|
||||||
|
return this.collectLeafVariables(properties, prefix)
|
||||||
|
},
|
||||||
|
collectLeafVariables (properties, prefix) {
|
||||||
|
return Object.entries(properties).reduce((result, [key, node]) => {
|
||||||
|
if (node.disabled) return result
|
||||||
|
|
||||||
|
const path = `${prefix}.${key}`
|
||||||
|
|
||||||
|
if (node.type === 'object' && node.properties) {
|
||||||
|
result.push(...this.collectLeafVariables(node.properties, path))
|
||||||
|
} else if (node.type === 'array' && node.items?.properties) {
|
||||||
|
result.push(...this.collectLeafVariables(node.items.properties, `${path}[]`))
|
||||||
|
} else {
|
||||||
|
result.push([node, path])
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,584 @@
|
|||||||
|
const KEYWORDS = ['if', 'else', 'for', 'end']
|
||||||
|
const TYPE_PRIORITY = { string: 3, number: 2, boolean: 1 }
|
||||||
|
const AND_OR_REGEXP = /\s+(AND|OR)\s+/i
|
||||||
|
const COMPARISON_OPERATORS_REGEXP = />=|<=|!=|==|>|<|=/
|
||||||
|
|
||||||
|
function buildTokens (elem, acc = []) {
|
||||||
|
if (elem.nodeType === Node.TEXT_NODE) {
|
||||||
|
if (elem.textContent) {
|
||||||
|
const text = elem.textContent
|
||||||
|
const re = /[[\]]/g
|
||||||
|
let match
|
||||||
|
let found = false
|
||||||
|
|
||||||
|
while ((match = re.exec(text)) !== null) {
|
||||||
|
found = true
|
||||||
|
|
||||||
|
acc.push({
|
||||||
|
elem,
|
||||||
|
value: match[0],
|
||||||
|
textLength: text.length,
|
||||||
|
index: match.index
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
acc.push({ elem, value: '', textLength: 0, index: 0 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const child of elem.childNodes) {
|
||||||
|
buildTokens(child, acc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokensPair (cur, nxt) {
|
||||||
|
if (cur.elem === nxt.elem) {
|
||||||
|
return cur.elem.textContent.slice(cur.index + 1, nxt.index).trim() === ''
|
||||||
|
} else {
|
||||||
|
return cur.elem.textContent.slice(cur.index + 1).trim() === '' &&
|
||||||
|
nxt.elem.textContent.slice(0, nxt.index).trim() === ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTags (tokens) {
|
||||||
|
const normalized = []
|
||||||
|
|
||||||
|
for (let i = 0; i < tokens.length - 1; i++) {
|
||||||
|
const cur = tokens[i]
|
||||||
|
const nxt = tokens[i + 1]
|
||||||
|
|
||||||
|
if (cur.value === '[' && nxt.value === '[' && tokensPair(cur, nxt)) {
|
||||||
|
normalized.push(['open', cur])
|
||||||
|
} else if (cur.value === ']' && nxt.value === ']' && tokensPair(cur, nxt)) {
|
||||||
|
normalized.push(['close', nxt])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags = []
|
||||||
|
|
||||||
|
for (let i = 0; i < normalized.length - 1; i++) {
|
||||||
|
const [curOp, openToken] = normalized[i]
|
||||||
|
const [nxtOp, closeToken] = normalized[i + 1]
|
||||||
|
|
||||||
|
if (curOp === 'open' && nxtOp === 'close') {
|
||||||
|
tags.push({ openToken, closeToken, value: '' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags
|
||||||
|
}
|
||||||
|
|
||||||
|
function findTextNodesInBranch (elements, toElem, acc) {
|
||||||
|
if (!elements || elements.length === 0) return acc
|
||||||
|
|
||||||
|
for (const elem of elements) {
|
||||||
|
if (elem.nodeType === Node.TEXT_NODE) {
|
||||||
|
acc.push(elem)
|
||||||
|
} else {
|
||||||
|
findTextNodesInBranch(Array.from(elem.childNodes), toElem, acc)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (acc.length > 0 && acc[acc.length - 1] === toElem) return acc
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
|
||||||
|
function findTextNodesBetween (fromElem, toElem, acc = []) {
|
||||||
|
if (fromElem === toElem) return [fromElem]
|
||||||
|
|
||||||
|
let currentElement = fromElem
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const parent = currentElement.parentNode
|
||||||
|
|
||||||
|
if (!parent) return acc
|
||||||
|
|
||||||
|
const children = Array.from(parent.childNodes)
|
||||||
|
const startIndex = children.indexOf(currentElement)
|
||||||
|
|
||||||
|
if (startIndex === -1) return acc
|
||||||
|
|
||||||
|
const elementsInBranch = children.slice(startIndex)
|
||||||
|
|
||||||
|
findTextNodesInBranch(elementsInBranch, toElem, acc)
|
||||||
|
|
||||||
|
if (acc.length > 0 && acc[acc.length - 1] === toElem) return acc
|
||||||
|
|
||||||
|
let p = elementsInBranch[0].parentNode
|
||||||
|
|
||||||
|
while (p && !p.nextSibling) {
|
||||||
|
p = p.parentNode
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!p || !p.nextSibling) return acc
|
||||||
|
|
||||||
|
currentElement = p.nextSibling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapTagValues (tags) {
|
||||||
|
for (const tag of tags) {
|
||||||
|
const textNodes = findTextNodesBetween(tag.openToken.elem, tag.closeToken.elem)
|
||||||
|
|
||||||
|
for (const elem of textNodes) {
|
||||||
|
let part
|
||||||
|
|
||||||
|
if (tag.openToken.elem === elem && tag.closeToken.elem === elem) {
|
||||||
|
part = elem.textContent.slice(tag.openToken.index, tag.closeToken.index + 1)
|
||||||
|
} else if (tag.openToken.elem === elem) {
|
||||||
|
part = elem.textContent.slice(tag.openToken.index)
|
||||||
|
} else if (tag.closeToken.elem === elem) {
|
||||||
|
part = elem.textContent.slice(0, tag.closeToken.index + 1)
|
||||||
|
} else {
|
||||||
|
part = elem.textContent
|
||||||
|
}
|
||||||
|
|
||||||
|
tag.value += part
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTagTypeName (tagString) {
|
||||||
|
const val = tagString.replace(/[[\]]/g, '').trim()
|
||||||
|
const parts = val.split(':').map((s) => s.trim())
|
||||||
|
|
||||||
|
if (parts.length === 2 && KEYWORDS.includes(parts[0])) {
|
||||||
|
return [parts[0], parts[1]]
|
||||||
|
} else if (KEYWORDS.includes(val)) {
|
||||||
|
return [val, null]
|
||||||
|
} else {
|
||||||
|
return ['var', val]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSimpleVariable (str) {
|
||||||
|
const s = str.trim()
|
||||||
|
|
||||||
|
return !AND_OR_REGEXP.test(s) &&
|
||||||
|
!COMPARISON_OPERATORS_REGEXP.test(s) &&
|
||||||
|
!s.includes('(') &&
|
||||||
|
!s.includes('!') &&
|
||||||
|
!s.includes('&&') &&
|
||||||
|
!s.includes('||') &&
|
||||||
|
!s.startsWith('"') &&
|
||||||
|
!s.startsWith("'") &&
|
||||||
|
!/^-?\d/.test(s) &&
|
||||||
|
!/^(true|false)$/i.test(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenizeCondition (str) {
|
||||||
|
const tokens = []
|
||||||
|
let pos = 0
|
||||||
|
|
||||||
|
str = str.trim()
|
||||||
|
|
||||||
|
while (pos < str.length) {
|
||||||
|
const rest = str.slice(pos)
|
||||||
|
let m
|
||||||
|
|
||||||
|
if ((m = rest.match(/^\s+/))) {
|
||||||
|
pos += m[0].length
|
||||||
|
} else if ((m = rest.match(/^(>=|<=|!=|==|>|<|=)/))) {
|
||||||
|
tokens.push({ type: 'operator', value: m[1] })
|
||||||
|
pos += m[1].length
|
||||||
|
} else if (rest[0] === '!') {
|
||||||
|
tokens.push({ type: 'not', value: '!' })
|
||||||
|
pos += 1
|
||||||
|
} else if (rest[0] === '(') {
|
||||||
|
tokens.push({ type: 'lparen', value: '(' })
|
||||||
|
pos += 1
|
||||||
|
} else if (rest[0] === ')') {
|
||||||
|
tokens.push({ type: 'rparen', value: ')' })
|
||||||
|
pos += 1
|
||||||
|
} else if (rest.startsWith('&&')) {
|
||||||
|
tokens.push({ type: 'and', value: 'AND' })
|
||||||
|
pos += 2
|
||||||
|
} else if ((m = rest.match(/^AND\b/i))) {
|
||||||
|
tokens.push({ type: 'and', value: 'AND' })
|
||||||
|
pos += 3
|
||||||
|
} else if (rest.startsWith('||')) {
|
||||||
|
tokens.push({ type: 'or', value: 'OR' })
|
||||||
|
pos += 2
|
||||||
|
} else if ((m = rest.match(/^OR\b/i))) {
|
||||||
|
tokens.push({ type: 'or', value: 'OR' })
|
||||||
|
pos += 2
|
||||||
|
} else if ((m = rest.match(/^"([^"]*)"/) || rest.match(/^'([^']*)'/))) {
|
||||||
|
tokens.push({ type: 'string', value: m[1] })
|
||||||
|
pos += m[0].length
|
||||||
|
} else if ((m = rest.match(/^(-?\d+\.?\d*)/))) {
|
||||||
|
tokens.push({ type: 'number', value: m[1].includes('.') ? parseFloat(m[1]) : parseInt(m[1], 10) })
|
||||||
|
pos += m[1].length
|
||||||
|
} else if ((m = rest.match(/^(true|false)\b/i))) {
|
||||||
|
tokens.push({ type: 'boolean', value: m[1].toLowerCase() === 'true' })
|
||||||
|
pos += m[1].length
|
||||||
|
} else if ((m = rest.match(/^([\p{L}_][\p{L}\p{N}_.]*)/u))) {
|
||||||
|
tokens.push({ type: 'variable', value: m[1] })
|
||||||
|
pos += m[1].length
|
||||||
|
} else {
|
||||||
|
pos += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseOrExpr (tokens, pos) {
|
||||||
|
let left, right
|
||||||
|
|
||||||
|
;[left, pos] = parseAndExpr(tokens, pos)
|
||||||
|
|
||||||
|
while (pos < tokens.length && tokens[pos].type === 'or') {
|
||||||
|
pos += 1
|
||||||
|
;[right, pos] = parseAndExpr(tokens, pos)
|
||||||
|
left = { type: 'or', left, right }
|
||||||
|
}
|
||||||
|
|
||||||
|
return [left, pos]
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAndExpr (tokens, pos) {
|
||||||
|
let left, right
|
||||||
|
|
||||||
|
;[left, pos] = parsePrimary(tokens, pos)
|
||||||
|
|
||||||
|
while (pos < tokens.length && tokens[pos].type === 'and') {
|
||||||
|
pos += 1
|
||||||
|
;[right, pos] = parsePrimary(tokens, pos)
|
||||||
|
left = { type: 'and', left, right }
|
||||||
|
}
|
||||||
|
|
||||||
|
return [left, pos]
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePrimary (tokens, pos) {
|
||||||
|
if (pos >= tokens.length) return [null, pos]
|
||||||
|
|
||||||
|
if (tokens[pos].type === 'not') {
|
||||||
|
const [child, p] = parsePrimary(tokens, pos + 1)
|
||||||
|
|
||||||
|
return [{ type: 'not', child }, p]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokens[pos].type === 'lparen') {
|
||||||
|
const [node, p] = parseOrExpr(tokens, pos + 1)
|
||||||
|
|
||||||
|
return [node, p < tokens.length && tokens[p].type === 'rparen' ? p + 1 : p]
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseComparisonOrPresence(tokens, pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseComparisonOrPresence (tokens, pos) {
|
||||||
|
if (pos >= tokens.length || tokens[pos].type !== 'variable') return [null, pos]
|
||||||
|
|
||||||
|
const variableName = tokens[pos].value
|
||||||
|
|
||||||
|
pos += 1
|
||||||
|
|
||||||
|
if (pos < tokens.length && tokens[pos].type === 'operator') {
|
||||||
|
let operator = tokens[pos].value
|
||||||
|
|
||||||
|
if (operator === '=') operator = '=='
|
||||||
|
|
||||||
|
pos += 1
|
||||||
|
|
||||||
|
if (pos < tokens.length && ['string', 'number', 'variable', 'boolean'].includes(tokens[pos].type)) {
|
||||||
|
const valueToken = tokens[pos]
|
||||||
|
|
||||||
|
return [{
|
||||||
|
type: 'comparison',
|
||||||
|
variableName,
|
||||||
|
operator,
|
||||||
|
value: valueToken.value,
|
||||||
|
valueIsVariable: valueToken.type === 'variable'
|
||||||
|
}, pos + 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [{ type: 'presence', variableName }, pos]
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCondition (conditionString) {
|
||||||
|
const stripped = conditionString.trim()
|
||||||
|
|
||||||
|
if (stripped.startsWith('!') && isSimpleVariable(stripped.slice(1))) {
|
||||||
|
return { type: 'not', child: { type: 'presence', variableName: stripped.slice(1) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSimpleVariable(stripped)) {
|
||||||
|
return { type: 'presence', variableName: stripped }
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = tokenizeCondition(stripped)
|
||||||
|
const [ast] = parseOrExpr(tokens, 0)
|
||||||
|
|
||||||
|
return ast
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractConditionVariables (node, acc = []) {
|
||||||
|
if (!node) return acc
|
||||||
|
|
||||||
|
switch (node.type) {
|
||||||
|
case 'or':
|
||||||
|
case 'and':
|
||||||
|
extractConditionVariables(node.left, acc)
|
||||||
|
extractConditionVariables(node.right, acc)
|
||||||
|
break
|
||||||
|
case 'not':
|
||||||
|
extractConditionVariables(node.child, acc)
|
||||||
|
break
|
||||||
|
case 'comparison':
|
||||||
|
acc.push({
|
||||||
|
name: node.variableName,
|
||||||
|
type: node.valueIsVariable ? null : (typeof node.value === 'boolean' ? 'boolean' : (typeof node.value === 'number' ? 'number' : 'string'))
|
||||||
|
})
|
||||||
|
|
||||||
|
if (node.valueIsVariable) {
|
||||||
|
acc.push({ name: node.value, type: null })
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
case 'presence':
|
||||||
|
acc.push({ name: node.variableName, type: 'boolean' })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
|
||||||
|
const IRREGULAR_PLURALS = {
|
||||||
|
people: 'person',
|
||||||
|
men: 'man',
|
||||||
|
women: 'woman',
|
||||||
|
children: 'child',
|
||||||
|
teeth: 'tooth',
|
||||||
|
feet: 'foot',
|
||||||
|
mice: 'mouse',
|
||||||
|
geese: 'goose',
|
||||||
|
oxen: 'ox',
|
||||||
|
criteria: 'criterion',
|
||||||
|
phenomena: 'phenomenon',
|
||||||
|
alumni: 'alumnus',
|
||||||
|
data: 'datum',
|
||||||
|
media: 'medium'
|
||||||
|
}
|
||||||
|
|
||||||
|
function singularize (word) {
|
||||||
|
if (IRREGULAR_PLURALS[word]) return IRREGULAR_PLURALS[word]
|
||||||
|
|
||||||
|
if (word.endsWith('ies')) return word.slice(0, -3) + 'y'
|
||||||
|
if (word.endsWith('ches') || word.endsWith('shes')) return word.slice(0, -2)
|
||||||
|
if (word.endsWith('ses') || word.endsWith('xes') || word.endsWith('zes')) return word.slice(0, -2)
|
||||||
|
if (word.endsWith('s') && !word.endsWith('ss')) return word.slice(0, -1)
|
||||||
|
|
||||||
|
return word
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOperators (tags) {
|
||||||
|
const operators = []
|
||||||
|
const stack = [{ children: operators, operator: null }]
|
||||||
|
|
||||||
|
for (const tag of tags) {
|
||||||
|
const [type, variableName] = parseTagTypeName(tag.value)
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'for':
|
||||||
|
case 'if': {
|
||||||
|
const operator = { type, variableName, tag, children: [] }
|
||||||
|
|
||||||
|
if (type === 'if') {
|
||||||
|
try {
|
||||||
|
operator.condition = parseCondition(variableName)
|
||||||
|
} catch (e) {
|
||||||
|
// ignore parse errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stack[stack.length - 1].children.push(operator)
|
||||||
|
stack.push({ children: operator.children, operator })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'else': {
|
||||||
|
const current = stack[stack.length - 1]
|
||||||
|
|
||||||
|
if (current.operator && current.operator.type === 'if') {
|
||||||
|
current.operator.elseTag = tag
|
||||||
|
current.operator.elseChildren = []
|
||||||
|
current.children = current.operator.elseChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'end': {
|
||||||
|
const popped = stack.pop()
|
||||||
|
|
||||||
|
if (popped.operator) {
|
||||||
|
popped.operator.endTag = tag
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'var':
|
||||||
|
stack[stack.length - 1].children.push({ type, variableName, tag })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return operators
|
||||||
|
}
|
||||||
|
|
||||||
|
function assignNestedSchema (propertiesHash, parentProperties, keyString, value) {
|
||||||
|
const keys = keyString.split('.')
|
||||||
|
const lastKey = keys.pop()
|
||||||
|
|
||||||
|
let currentLevel = null
|
||||||
|
|
||||||
|
if (keys.length > 0 && parentProperties[keys[0]]) {
|
||||||
|
currentLevel = keys.reduce((current, key) => {
|
||||||
|
if (!current[key]) current[key] = { type: 'object', properties: {} }
|
||||||
|
|
||||||
|
return current[key].properties
|
||||||
|
}, parentProperties)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentLevel) {
|
||||||
|
currentLevel = keys.reduce((current, key) => {
|
||||||
|
if (!current[key]) current[key] = { type: 'object', properties: {} }
|
||||||
|
|
||||||
|
return current[key].properties
|
||||||
|
}, propertiesHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentLevel[lastKey] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
function assignNestedSchemaWithPriority (propertiesHash, parentProperties, keyString, newType) {
|
||||||
|
const keys = keyString.split('.')
|
||||||
|
const lastKey = keys.pop()
|
||||||
|
|
||||||
|
let currentLevel = null
|
||||||
|
|
||||||
|
if (keys.length > 0 && parentProperties[keys[0]]) {
|
||||||
|
currentLevel = keys.reduce((current, key) => {
|
||||||
|
if (!current[key]) current[key] = { type: 'object', properties: {} }
|
||||||
|
|
||||||
|
return current[key].properties
|
||||||
|
}, parentProperties)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentLevel) {
|
||||||
|
currentLevel = keys.reduce((current, key) => {
|
||||||
|
if (!current[key]) current[key] = { type: 'object', properties: {} }
|
||||||
|
|
||||||
|
return current[key].properties
|
||||||
|
}, propertiesHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = currentLevel[lastKey]
|
||||||
|
|
||||||
|
if (existing && (TYPE_PRIORITY[newType] || 0) <= (TYPE_PRIORITY[existing.type] || 0)) return
|
||||||
|
|
||||||
|
currentLevel[lastKey] = { type: newType }
|
||||||
|
}
|
||||||
|
|
||||||
|
function processConditionVariables (condition, propertiesHash, parentProperties) {
|
||||||
|
const variables = extractConditionVariables(condition)
|
||||||
|
|
||||||
|
for (const varInfo of variables) {
|
||||||
|
assignNestedSchemaWithPriority(propertiesHash, parentProperties, varInfo.name, varInfo.type || 'boolean')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processOperators (operators, propertiesHash = {}, parentProperties = {}) {
|
||||||
|
if (!operators || operators.length === 0) return propertiesHash
|
||||||
|
|
||||||
|
for (const op of operators) {
|
||||||
|
switch (op.type) {
|
||||||
|
case 'var': {
|
||||||
|
if (!op.variableName.includes('.') && parentProperties[op.variableName]) {
|
||||||
|
const item = parentProperties[op.variableName]
|
||||||
|
|
||||||
|
if (item && item.type === 'object' && item.properties && Object.keys(item.properties).length === 0) {
|
||||||
|
delete item.properties
|
||||||
|
item.type = 'string'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assignNestedSchema(propertiesHash, parentProperties, op.variableName, { type: 'string' })
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'if':
|
||||||
|
if (op.condition) {
|
||||||
|
processConditionVariables(op.condition, propertiesHash, parentProperties)
|
||||||
|
}
|
||||||
|
|
||||||
|
processOperators(op.children, propertiesHash, parentProperties)
|
||||||
|
processOperators(op.elseChildren, propertiesHash, parentProperties)
|
||||||
|
break
|
||||||
|
case 'for': {
|
||||||
|
const parts = op.variableName.split('.')
|
||||||
|
const singularKey = singularize(parts[parts.length - 1])
|
||||||
|
|
||||||
|
let itemProperties = parentProperties[singularKey]?.items
|
||||||
|
itemProperties = itemProperties || propertiesHash[parts[0]]?.items
|
||||||
|
itemProperties = itemProperties || { type: 'object', properties: {} }
|
||||||
|
|
||||||
|
assignNestedSchema(propertiesHash, parentProperties, op.variableName, { type: 'array', items: itemProperties })
|
||||||
|
processOperators(op.children, propertiesHash, { ...parentProperties, [singularKey]: itemProperties })
|
||||||
|
|
||||||
|
if (itemProperties.type === 'object' && itemProperties.properties && Object.keys(itemProperties.properties).length === 0) {
|
||||||
|
delete itemProperties.properties
|
||||||
|
itemProperties.type = 'string'
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return propertiesHash
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeSchemaProperties (target, source) {
|
||||||
|
for (const key of Object.keys(source)) {
|
||||||
|
if (!target[key]) {
|
||||||
|
target[key] = source[key]
|
||||||
|
} else if (target[key].type === 'object' && source[key].type === 'object') {
|
||||||
|
if (!target[key].properties) target[key].properties = {}
|
||||||
|
if (source[key].properties) {
|
||||||
|
mergeSchemaProperties(target[key].properties, source[key].properties)
|
||||||
|
}
|
||||||
|
} else if (target[key].type === 'array' && source[key].type === 'array') {
|
||||||
|
if (source[key].items && source[key].items.properties) {
|
||||||
|
if (!target[key].items) {
|
||||||
|
target[key].items = source[key].items
|
||||||
|
} else if (target[key].items.properties) {
|
||||||
|
mergeSchemaProperties(target[key].items.properties, source[key].items.properties)
|
||||||
|
}
|
||||||
|
} else if (source[key].items && !target[key].items) {
|
||||||
|
target[key].items = source[key].items
|
||||||
|
}
|
||||||
|
} else if ((TYPE_PRIORITY[source[key].type] || 0) > (TYPE_PRIORITY[target[key].type] || 0)) {
|
||||||
|
target[key] = source[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return target
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildVariablesSchema (dom) {
|
||||||
|
const tokens = buildTokens(dom)
|
||||||
|
const tags = mapTagValues(buildTags(tokens))
|
||||||
|
const operators = buildOperators(tags)
|
||||||
|
|
||||||
|
return processOperators(operators)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { buildVariablesSchema, mergeSchemaProperties, buildOperators, buildTokens, buildTags, mapTagValues }
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="list-field group mb-2"
|
class="list-field group"
|
||||||
|
:class="`list-field-${field.type}`"
|
||||||
>
|
>
|
||||||
<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 +19,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 +93,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 +132,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>
|
||||||
@@ -154,8 +161,10 @@
|
|||||||
v-if="field.options && withOptions && (isExpandOptions || field.options.length < 5)"
|
v-if="field.options && withOptions && (isExpandOptions || field.options.length < 5)"
|
||||||
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"
|
||||||
@dragover="onOptionDragover"
|
@dragover="onOptionDragover"
|
||||||
@drop="reorderOptions"
|
@drop="reorderOptions"
|
||||||
|
@dragstart.prevent.stop
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(option, index) in field.options"
|
v-for="(option, index) in field.options"
|
||||||
@@ -184,7 +193,8 @@
|
|||||||
required
|
required
|
||||||
:placeholder="`${t('option')} ${index + 1}`"
|
:placeholder="`${t('option')} ${index + 1}`"
|
||||||
@keydown.enter="option.value ? addOptionAt(index + 1) : null"
|
@keydown.enter="option.value ? addOptionAt(index + 1) : null"
|
||||||
@blur="save"
|
@blur="$emit('save')"
|
||||||
|
@paste="onOptionPaste($event, index)"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
:title="t('draw')"
|
:title="t('draw')"
|
||||||
@@ -208,7 +218,8 @@
|
|||||||
dir="auto"
|
dir="auto"
|
||||||
@keydown.enter="option.value ? addOptionAt(index + 1) : null"
|
@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"
|
||||||
@@ -259,6 +270,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>
|
||||||
@@ -271,6 +283,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>
|
||||||
@@ -282,6 +295,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>
|
||||||
@@ -294,6 +308,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>
|
||||||
@@ -330,7 +345,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,
|
||||||
@@ -341,6 +356,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,
|
||||||
@@ -362,12 +382,11 @@ 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,
|
||||||
@@ -398,7 +417,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 || []
|
||||||
@@ -416,9 +435,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 || {}
|
||||||
|
|
||||||
@@ -429,7 +448,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}`
|
||||||
@@ -451,7 +470,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 () {
|
||||||
@@ -460,6 +479,35 @@ export default {
|
|||||||
closeDropdown () {
|
closeDropdown () {
|
||||||
this.$el.getRootNode().activeElement.blur()
|
this.$el.getRootNode().activeElement.blur()
|
||||||
},
|
},
|
||||||
|
onOptionPaste (e, index) {
|
||||||
|
const text = e.clipboardData.getData('text')
|
||||||
|
|
||||||
|
if (text.includes('\n')) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.isExpandOptions = true
|
||||||
|
|
||||||
|
const lines = text.split(/\r?\n/).map((l) => l.trim()).filter((l) => l)
|
||||||
|
|
||||||
|
if (lines.length > 0) {
|
||||||
|
const currentOption = this.field.options[index]
|
||||||
|
|
||||||
|
currentOption.value = (currentOption.value + lines[0]).trim()
|
||||||
|
|
||||||
|
const newOptions = lines.slice(1).map((line) => ({ value: line, uuid: v4() }))
|
||||||
|
|
||||||
|
this.field.options.splice(index + 1, 0, ...newOptions)
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const inputs = this.$refs.options.querySelectorAll('input')
|
||||||
|
|
||||||
|
inputs[index + newOptions.length]?.focus()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.$emit('save')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
addOptionAt (index) {
|
addOptionAt (index) {
|
||||||
this.isExpandOptions = true
|
this.isExpandOptions = true
|
||||||
|
|
||||||
@@ -473,7 +521,7 @@ export default {
|
|||||||
inputs[insertAt]?.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)
|
||||||
@@ -484,7 +532,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
|
||||||
@@ -526,7 +574,7 @@ export default {
|
|||||||
|
|
||||||
this.isNameFocus = false
|
this.isNameFocus = false
|
||||||
|
|
||||||
this.save()
|
this.$emit('save')
|
||||||
},
|
},
|
||||||
onOptionDragstart (event, option) {
|
onOptionDragstart (event, option) {
|
||||||
this.optionDragRef = option
|
this.optionDragRef = option
|
||||||
@@ -587,7 +635,7 @@ export default {
|
|||||||
if (newOrder.length === this.field.options.length) {
|
if (newOrder.length === this.field.options.length) {
|
||||||
this.field.options.splice(0, this.field.options.length, ...newOrder)
|
this.field.options.splice(0, this.field.options.length, ...newOrder)
|
||||||
|
|
||||||
this.save()
|
this.$emit('save')
|
||||||
}
|
}
|
||||||
|
|
||||||
this.optionDragRef = null
|
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>
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="field.type === 'verification'"
|
v-if="field.type === 'verification'"
|
||||||
class="py-1.5 px-1 relative"
|
class="field-settings-verification-method py-1.5 px-1 relative"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<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,42 +26,16 @@
|
|||||||
{{ 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="field-settings-default-value py-1.5 px-1 relative"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<select
|
<select
|
||||||
: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=""
|
||||||
@@ -88,7 +62,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="['text', 'number'].includes(field.type) && !defaultField"
|
v-if="['text', 'number'].includes(field.type) && !defaultField"
|
||||||
class="py-1.5 px-1 relative"
|
class="field-settings-default-value py-1.5 px-1 relative"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -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"
|
||||||
@@ -110,7 +84,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="['text', 'cells'].includes(field.type)"
|
v-if="['text', 'cells'].includes(field.type)"
|
||||||
class="py-1.5 px-1 relative"
|
class="field-settings-validation py-1.5 px-1 relative"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<select
|
<select
|
||||||
@@ -148,7 +122,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="['text', 'cells'].includes(field.type) && field.validation && lengthValidation"
|
v-if="['text', 'cells'].includes(field.type) && field.validation && lengthValidation"
|
||||||
class="py-1.5 px-1 relative flex space-x-1"
|
class="field-settings-length-validation py-1.5 px-1 relative flex space-x-1"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<div class="w-1/2 relative">
|
<div class="w-1/2 relative">
|
||||||
@@ -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"
|
||||||
@@ -192,7 +166,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="field.type === 'number'"
|
v-if="field.type === 'number'"
|
||||||
class="py-1.5 px-1 relative flex space-x-1"
|
class="field-settings-number-range py-1.5 px-1 relative flex space-x-1"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<div class="w-1/2 relative">
|
<div class="w-1/2 relative">
|
||||||
@@ -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"
|
||||||
@@ -236,13 +210,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="field.type === 'number'"
|
v-if="field.type === 'number'"
|
||||||
class="py-1.5 px-1 relative"
|
class="field-settings-number-format py-1.5 px-1 relative"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<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"
|
||||||
@@ -263,7 +237,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation"
|
v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation"
|
||||||
class="py-1.5 px-1 relative"
|
class="field-settings-custom-validation py-1.5 px-1 relative"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -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"
|
||||||
@@ -285,7 +259,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation"
|
v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation"
|
||||||
class="py-1.5 px-1 relative"
|
class="field-settings-error-message py-1.5 px-1 relative"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -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"
|
||||||
@@ -306,14 +280,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="field.type === 'date'"
|
v-if="field.type === 'date'"
|
||||||
class="py-1.5 px-1 relative"
|
class="field-settings-date-format py-1.5 px-1 relative"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<select
|
<select
|
||||||
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"
|
||||||
@@ -333,13 +307,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="field.type === 'signature'"
|
v-if="field.type === 'signature'"
|
||||||
class="py-1.5 px-1 relative"
|
class="field-settings-signature-format py-1.5 px-1 relative"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<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"
|
||||||
@@ -366,6 +340,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<li
|
<li
|
||||||
v-if="[true, false].includes(withSignatureId) && field.type === 'signature'"
|
v-if="[true, false].includes(withSignatureId) && field.type === 'signature'"
|
||||||
|
class="field-settings-signature-id"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<label class="cursor-pointer py-1.5">
|
<label class="cursor-pointer py-1.5">
|
||||||
@@ -374,13 +349,14 @@
|
|||||||
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>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="withRequired && field.type !== 'phone' && field.type !== 'stamp' && field.type !== 'verification' && field.type !== 'strikethrough' && field.type !== 'heading'"
|
v-if="withRequired && field.type !== 'phone' && field.type !== 'stamp' && field.type !== 'verification' && field.type !== 'strikethrough' && field.type !== 'heading'"
|
||||||
|
class="field-settings-required"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<label class="cursor-pointer py-1.5">
|
<label class="cursor-pointer py-1.5">
|
||||||
@@ -389,13 +365,14 @@
|
|||||||
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>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="field.type == 'stamp'"
|
v-if="field.type == 'stamp'"
|
||||||
|
class="field-settings-with-logo"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<label class="cursor-pointer py-1.5">
|
<label class="cursor-pointer py-1.5">
|
||||||
@@ -403,13 +380,14 @@
|
|||||||
: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>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="field.type == 'checkbox'"
|
v-if="field.type == 'checkbox'"
|
||||||
|
class="field-settings-checked"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<label class="cursor-pointer py-1.5">
|
<label class="cursor-pointer py-1.5">
|
||||||
@@ -417,13 +395,14 @@
|
|||||||
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>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="field.type == 'date'"
|
v-if="field.type == 'date'"
|
||||||
|
class="field-settings-set-signing-date"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<label class="cursor-pointer py-1.5">
|
<label class="cursor-pointer py-1.5">
|
||||||
@@ -431,13 +410,14 @@
|
|||||||
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)"
|
||||||
|
class="field-settings-read-only"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<label class="cursor-pointer py-1.5">
|
<label class="cursor-pointer py-1.5">
|
||||||
@@ -446,13 +426,14 @@
|
|||||||
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'])"
|
||||||
|
class="field-settings-prefillable"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<label class="cursor-pointer py-1.5">
|
<label class="cursor-pointer py-1.5">
|
||||||
@@ -461,7 +442,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 +451,10 @@
|
|||||||
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)"
|
||||||
|
class="field-settings-font"
|
||||||
|
>
|
||||||
<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')"
|
||||||
@@ -485,6 +469,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="field.type != 'stamp' && field.type != 'heading' && field.type != 'strikethrough'"
|
v-if="field.type != 'stamp' && field.type != 'heading' && field.type != 'strikethrough'"
|
||||||
|
class="field-settings-description"
|
||||||
>
|
>
|
||||||
<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"
|
||||||
@@ -499,7 +484,8 @@
|
|||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="field.type != 'stamp' && field.type != 'heading'"
|
v-if="withCondition && field.type != 'stamp' && field.type != 'heading'"
|
||||||
|
class="field-settings-condition"
|
||||||
>
|
>
|
||||||
<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"
|
||||||
@@ -513,7 +499,10 @@
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="field.type == 'number'">
|
<li
|
||||||
|
v-if="field.type == 'number'"
|
||||||
|
class="field-settings-formula"
|
||||||
|
>
|
||||||
<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-formula')"
|
@click="$emit('click-formula')"
|
||||||
@@ -526,11 +515,15 @@
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<hr class="pb-0.5 mt-0.5">
|
<hr
|
||||||
|
v-if="(withCopyToAllPages && canCopyToAllPages) || 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"
|
||||||
:key="index"
|
:key="index"
|
||||||
|
class="field-settings-area"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
@@ -550,7 +543,10 @@
|
|||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="!field.areas?.length || !['radio', 'multiple'].includes(field.type)">
|
<li
|
||||||
|
v-if="!field.areas?.length || !['radio', 'multiple'].includes(field.type)"
|
||||||
|
class="field-settings-draw-new-area"
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="text-sm py-1 px-2"
|
class="text-sm py-1 px-2"
|
||||||
@@ -564,7 +560,10 @@
|
|||||||
</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 && canCopyToAllPages && field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells', 'stamp'].includes(field.type)"
|
||||||
|
class="field-settings-copy-to-all-pages"
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="text-sm py-1 px-2"
|
class="text-sm py-1 px-2"
|
||||||
@@ -577,10 +576,26 @@
|
|||||||
{{ t('copy_to_all_pages') }}
|
{{ t('copy_to_all_pages') }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li
|
||||||
|
v-if="withCustomFields"
|
||||||
|
class="field-settings-save-as-custom-field"
|
||||||
|
>
|
||||||
|
<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 +604,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 +667,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: ''
|
||||||
@@ -650,6 +681,15 @@ export default {
|
|||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
},
|
},
|
||||||
|
canCopyToAllPages () {
|
||||||
|
const firstArea = this.field.areas[0]
|
||||||
|
|
||||||
|
if (firstArea) {
|
||||||
|
return firstArea.page !== null && firstArea.page !== undefined
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
numberFormats () {
|
numberFormats () {
|
||||||
return [
|
return [
|
||||||
'none',
|
'none',
|
||||||
@@ -686,7 +726,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,8 +743,17 @@ 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 || []).filter((e) => e.page !== null && e.page !== undefined).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 +779,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 +796,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 +815,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',
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ export default {
|
|||||||
payment: IconCreditCard,
|
payment: IconCreditCard,
|
||||||
phone: IconPhoneCheck,
|
phone: IconPhoneCheck,
|
||||||
verification: IconId,
|
verification: IconId,
|
||||||
kba: IconUserScan,
|
kba: IconUserScan
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
skipTypes () {
|
skipTypes () {
|
||||||
|
|||||||
@@ -1,21 +1,45 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="withStickySubmitters ? 'sticky top-0 z-[1]' : ''">
|
<div
|
||||||
|
:class="withStickySubmitters ? 'sticky top-0 z-[1]' : ''"
|
||||||
|
:style="withStickySubmitters ? { backgroundColor } : {}"
|
||||||
|
class="flex items-center gap-1"
|
||||||
|
>
|
||||||
<FieldSubmitter
|
<FieldSubmitter
|
||||||
:model-value="selectedSubmitter.uuid"
|
:model-value="selectedSubmitter.uuid"
|
||||||
class="roles-dropdown w-full rounded-lg roles-dropdown"
|
class="roles-dropdown w-full rounded-lg roles-dropdown"
|
||||||
:style="withStickySubmitters ? { backgroundColor } : {}"
|
|
||||||
:submitters="submitters"
|
:submitters="submitters"
|
||||||
:menu-style="{ overflow: 'auto', display: 'flex', flexDirection: 'row', maxHeight: 'calc(100vh - 120px)', backgroundColor: ['', null, 'transparent'].includes(backgroundColor) ? 'white' : backgroundColor }"
|
:menu-style="{ overflow: 'auto', display: 'flex', flexDirection: 'row', maxHeight: 'calc(100vh - 120px)', backgroundColor: ['', null, 'transparent'].includes(backgroundColor) ? 'white' : backgroundColor }"
|
||||||
:editable="editable && !defaultSubmitters.length"
|
:editable="editable && !defaultSubmitters.length"
|
||||||
@new-submitter="save"
|
@new-submitter="save"
|
||||||
@remove="removeSubmitter"
|
@remove="removeSubmitter"
|
||||||
@name-change="save"
|
@name-change="save"
|
||||||
@update:model-value="$emit('change-submitter', submitters.find((s) => s.uuid === $event))"
|
@update:model-value="[$emit('change-submitter', submitters.find((s) => s.uuid === $event)), isShowVariables = false]"
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
v-if="hasDynamicDocuments"
|
||||||
|
class="flex-shrink-0 rounded-md border hover:border-content flex items-center justify-center self-stretch"
|
||||||
|
:class="isShowVariables ? 'border-base-content bg-base-content text-base-100' : 'border-base-300'"
|
||||||
|
style="width: 44px"
|
||||||
|
:title="t('variables')"
|
||||||
|
@click.prevent="toggleVariables"
|
||||||
|
>
|
||||||
|
<IconBracketsContain
|
||||||
|
:width="22"
|
||||||
|
:height="22"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<DynamicVariables
|
||||||
|
v-if="isShowVariables"
|
||||||
|
:editable="editable"
|
||||||
|
class="mt-1"
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
|
v-if="!isShowVariables"
|
||||||
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="fieldsDragFieldRef.value ? reorderFields() : null"
|
@drop="fieldsDragFieldRef.value ? reorderFields() : null"
|
||||||
>
|
>
|
||||||
@@ -24,20 +48,24 @@
|
|||||||
: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)"
|
||||||
@set-draw="$emit('set-draw', $event)"
|
@set-draw="$emit('set-draw', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="submitterDefaultFields.length && editable">
|
<div v-if="!isShowVariables && submitterDefaultFields.length && editable">
|
||||||
<hr class="mb-2">
|
<hr class="mb-2">
|
||||||
<template v-if="isShowFieldSearch">
|
<template v-if="isShowFieldSearch">
|
||||||
<input
|
<input
|
||||||
@@ -105,7 +133,92 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="editable && !onlyDefinedFields"
|
v-if="!isShowVariables && 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="!isShowVariables && 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="!isShowVariables && 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"
|
||||||
>
|
>
|
||||||
@@ -193,7 +306,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="fields.length < 4 && editable && withHelp && !showTourStartForm"
|
v-if="!isShowVariables && fields.length < 4 && editable && withHelp && !showTourStartForm"
|
||||||
class="text-xs p-2 border border-base-200 rounded"
|
class="text-xs p-2 border border-base-200 rounded"
|
||||||
>
|
>
|
||||||
<ul class="list-disc list-outside ml-3">
|
<ul class="list-disc list-outside ml-3">
|
||||||
@@ -209,7 +322,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="withFieldsDetection && editable && fields.length < 2"
|
v-if="!isShowVariables && withFieldsDetection && editable && fields.length < 2 && !template.schema.some((item) => item.dynamic)"
|
||||||
class="my-2"
|
class="my-2"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@@ -246,7 +359,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-show="fields.length < 4 && editable && withHelp && showTourStartForm"
|
v-show="!isShowVariables && 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"
|
||||||
>
|
>
|
||||||
<div class="text-center text-sm">
|
<div class="text-center text-sm">
|
||||||
@@ -266,29 +379,45 @@
|
|||||||
|
|
||||||
<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, IconInnerShadowTop, IconSparkles } from '@tabler/icons-vue'
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
import { IconLock, IconCirclePlus, IconInnerShadowTop, IconSparkles, IconBracketsContain } 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,
|
IconSparkles,
|
||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
FieldSubmitter,
|
FieldSubmitter,
|
||||||
IconDrag,
|
IconDrag,
|
||||||
IconLock
|
IconLock,
|
||||||
|
IconBracketsContain,
|
||||||
|
DynamicVariables: defineAsyncComponent(() => import(/* webpackChunkName: "dynamic-editor" */ './dynamic_variables'))
|
||||||
},
|
},
|
||||||
inject: ['save', 'backgroundColor', 'withPhone', 'withVerification', 'withKba', 'withPayment', 't', 'fieldsDragFieldRef', 'baseFetch'],
|
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,
|
||||||
@@ -372,17 +501,24 @@ 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', 'select-submitter'],
|
emits: ['add-field', 'set-draw', 'set-draw-type', 'set-draw-custom-field', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter', 'set-drag-placeholder', 'select-submitter', 'rebuild-variables-schema', 'remove-field', 'remove-submitter'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
fieldPagesLoaded: null,
|
fieldPagesLoaded: null,
|
||||||
analyzingProgress: 0,
|
analyzingProgress: 0,
|
||||||
defaultFieldsSearch: ''
|
newCustomField: null,
|
||||||
|
showCustomTab: false,
|
||||||
|
defaultFieldsSearch: '',
|
||||||
|
customFieldsSearch: '',
|
||||||
|
isShowVariables: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
fieldNames: FieldType.computed.fieldNames,
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
fieldIcons: FieldType.computed.fieldIcons,
|
fieldIcons: FieldType.computed.fieldIcons,
|
||||||
|
hasDynamicDocuments () {
|
||||||
|
return this.template.schema.some((item) => item.dynamic)
|
||||||
|
},
|
||||||
numberOfPages () {
|
numberOfPages () {
|
||||||
return this.template.documents.reduce((acc, doc) => {
|
return this.template.documents.reduce((acc, doc) => {
|
||||||
return acc + doc.metadata?.pdf?.number_of_pages || doc.preview_images.length
|
return acc + doc.metadata?.pdf?.number_of_pages || doc.preview_images.length
|
||||||
@@ -430,9 +566,30 @@ 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: {
|
||||||
|
toggleVariables () {
|
||||||
|
this.$emit('rebuild-variables-schema')
|
||||||
|
this.isShowVariables = !this.isShowVariables
|
||||||
|
},
|
||||||
onDragstart (event, field) {
|
onDragstart (event, field) {
|
||||||
this.removeDragOverlay(event)
|
this.removeDragOverlay(event)
|
||||||
|
|
||||||
@@ -440,6 +597,65 @@ 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
|
||||||
|
|
||||||
|
if (Array.isArray(customField.areas)) {
|
||||||
|
customField.areas = customField.areas.filter((area) => area.page !== null && area.page !== undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 () {
|
detectFields () {
|
||||||
const fields = []
|
const fields = []
|
||||||
|
|
||||||
@@ -589,6 +805,8 @@ export default {
|
|||||||
this.$emit('change-submitter', this.submitters[0])
|
this.$emit('change-submitter', this.submitters[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.$emit('remove-submitter', submitter)
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
removeField (field, save = true) {
|
removeField (field, save = true) {
|
||||||
@@ -610,9 +828,56 @@ export default {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
field.areas?.forEach((area) => {
|
||||||
|
this.selectedAreasRef.value.splice(this.selectedAreasRef.value.indexOf(area), 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.$emit('remove-field', field)
|
||||||
|
|
||||||
if (save) {
|
if (save) {
|
||||||
this.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: ''
|
||||||
@@ -161,7 +161,7 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
fields () {
|
fields () {
|
||||||
return this.template.fields.reduce((acc, f) => {
|
return this.template.fields.reduce((acc, f) => {
|
||||||
if (f !== this.field && ['number'].includes(f.type) && (!f.preferences?.formula || !f.preferences.formula.includes(this.field.uuid))) {
|
if (f !== this.field && this.isNumberField(f) && (!f.preferences?.formula || !f.preferences.formula.includes(this.field.uuid))) {
|
||||||
acc.push(f)
|
acc.push(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,12 +176,15 @@ export default {
|
|||||||
this.formula = this.humanizeFormula(this.field.preferences.formula || '')
|
this.formula = this.humanizeFormula(this.field.preferences.formula || '')
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
isNumberField (field) {
|
||||||
|
return field.type === 'number' || (['radio', 'select'].includes(field.type) && field.options?.every((o) => String(o.value).match(/^[\d.-]+$/)))
|
||||||
|
},
|
||||||
humanizeFormula (text) {
|
humanizeFormula (text) {
|
||||||
return text.replace(/{{(.*?)}}/g, (match, uuid) => {
|
return text.replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||||
const foundField = this.fields.find((f) => f.uuid === uuid)
|
const foundField = this.template.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}}'
|
||||||
}
|
}
|
||||||
@@ -189,8 +192,8 @@ 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.template.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 +215,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')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export default {
|
|||||||
acceptFileTypes: {
|
acceptFileTypes: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'image/*, application/pdf, application/zip'
|
default: 'image/*, application/pdf, application/zip, application/json'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['add', 'replace', 'replace-and-clone', 'error'],
|
emits: ['add', 'replace', 'replace-and-clone', 'error'],
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
const en = {
|
const en = {
|
||||||
|
fixed: 'Fixed',
|
||||||
|
default: 'Default',
|
||||||
|
save_as_custom_field: 'Save as custom field',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
analyzing_: 'Analyzing...',
|
analyzing_: 'Analyzing...',
|
||||||
download: 'Download',
|
download: 'Download',
|
||||||
downloading_: 'Downloading...',
|
downloading_: 'Downloading...',
|
||||||
view: 'View',
|
view: 'View',
|
||||||
autodetect_fields: 'Autodetect fields',
|
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',
|
||||||
@@ -29,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',
|
||||||
@@ -43,9 +49,15 @@ const en = {
|
|||||||
with_logo: 'With logo',
|
with_logo: 'With logo',
|
||||||
unchecked: 'Unchecked',
|
unchecked: 'Unchecked',
|
||||||
price: 'Price',
|
price: 'Price',
|
||||||
|
type: 'Type',
|
||||||
|
list: 'list',
|
||||||
|
no_variables: 'No variables yet',
|
||||||
|
no_variables_description: 'Add [[variable]] marks to your document to create dynamic content variables.',
|
||||||
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',
|
||||||
@@ -65,6 +77,7 @@ const en = {
|
|||||||
up: 'Up',
|
up: 'Up',
|
||||||
down: 'Down',
|
down: 'Down',
|
||||||
checked: 'Checked',
|
checked: 'Checked',
|
||||||
|
current_date: 'Current date',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
any: 'Any',
|
any: 'Any',
|
||||||
@@ -89,12 +102,14 @@ 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',
|
||||||
condition: 'Condition',
|
condition: 'Condition',
|
||||||
|
make_dynamic: 'Make dynamic',
|
||||||
first_party: 'First Party',
|
first_party: 'First Party',
|
||||||
second_party: 'Second Party',
|
second_party: 'Second Party',
|
||||||
third_party: 'Third Party',
|
third_party: 'Third Party',
|
||||||
@@ -171,6 +186,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',
|
||||||
@@ -181,14 +199,28 @@ const en = {
|
|||||||
learn_more: 'Learn more',
|
learn_more: 'Learn more',
|
||||||
and: 'and',
|
and: 'and',
|
||||||
or: 'or',
|
or: 'or',
|
||||||
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Start a quick tour to learn how to create an send your first document',
|
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Start a quick tour to learn how to create and send your first document',
|
||||||
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',
|
kba: 'KBA',
|
||||||
autodetect_fields: 'Autodetectar campos',
|
autodetect_fields: 'Autodetectar campos',
|
||||||
analyzing_: 'Analizando...',
|
analyzing_: 'Analizando...',
|
||||||
@@ -218,8 +250,15 @@ const es = {
|
|||||||
search_field: 'Campo de búsqueda',
|
search_field: 'Campo de búsqueda',
|
||||||
field_not_found: 'Campo no encontrado',
|
field_not_found: 'Campo no encontrado',
|
||||||
clear: 'Borrar',
|
clear: 'Borrar',
|
||||||
|
type: 'Tipo',
|
||||||
|
list: 'lista',
|
||||||
|
no_variables: 'Aún sin variables',
|
||||||
|
no_variables_description: 'Agregue marcas [[variable]] a su documento para crear variables de contenido dinámico.',
|
||||||
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',
|
||||||
@@ -237,6 +276,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',
|
||||||
@@ -246,6 +287,7 @@ const es = {
|
|||||||
remove_condition: 'Eliminar condición',
|
remove_condition: 'Eliminar condición',
|
||||||
add_condition: 'Agregar condición',
|
add_condition: 'Agregar condición',
|
||||||
condition: 'Condición',
|
condition: 'Condición',
|
||||||
|
make_dynamic: 'Hacer dinámico',
|
||||||
formula: 'Fórmula',
|
formula: 'Fórmula',
|
||||||
edit: 'Editar',
|
edit: 'Editar',
|
||||||
settings: 'Configuración',
|
settings: 'Configuración',
|
||||||
@@ -255,6 +297,7 @@ const es = {
|
|||||||
are_you_sure_: '¿Estás seguro?',
|
are_you_sure_: '¿Estás seguro?',
|
||||||
sign_yourself: 'Firma tú mismo',
|
sign_yourself: 'Firma tú mismo',
|
||||||
checked: 'Seleccionado',
|
checked: 'Seleccionado',
|
||||||
|
current_date: 'Fecha actual',
|
||||||
send: 'Enviar',
|
send: 'Enviar',
|
||||||
remove: 'Eliminar',
|
remove: 'Eliminar',
|
||||||
save: 'Guardar',
|
save: 'Guardar',
|
||||||
@@ -277,6 +320,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',
|
||||||
@@ -361,6 +405,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',
|
||||||
@@ -375,10 +422,24 @@ 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',
|
kba: 'KBA',
|
||||||
autodetect_fields: 'Rileva campi',
|
autodetect_fields: 'Rileva campi',
|
||||||
analyzing_: 'Analisi...',
|
analyzing_: 'Analisi...',
|
||||||
@@ -409,6 +470,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',
|
||||||
@@ -423,9 +487,15 @@ const it = {
|
|||||||
with_logo: 'Con logo',
|
with_logo: 'Con logo',
|
||||||
unchecked: 'Non selezionato',
|
unchecked: 'Non selezionato',
|
||||||
price: 'Prezzo',
|
price: 'Prezzo',
|
||||||
|
type: 'Tipo',
|
||||||
|
list: 'lista',
|
||||||
|
no_variables: 'Ancora nessuna variabile',
|
||||||
|
no_variables_description: 'Aggiungi marcatori [[variable]] al documento per creare variabili di contenuto dinamico.',
|
||||||
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',
|
||||||
@@ -445,6 +515,7 @@ const it = {
|
|||||||
up: 'Su',
|
up: 'Su',
|
||||||
down: 'Giù',
|
down: 'Giù',
|
||||||
checked: 'Selezionato',
|
checked: 'Selezionato',
|
||||||
|
current_date: 'Data corrente',
|
||||||
save: 'Salva',
|
save: 'Salva',
|
||||||
cancel: 'Annulla',
|
cancel: 'Annulla',
|
||||||
any: 'Qualsiasi',
|
any: 'Qualsiasi',
|
||||||
@@ -471,10 +542,12 @@ 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',
|
||||||
condition: 'Condizione',
|
condition: 'Condizione',
|
||||||
|
make_dynamic: 'Rendi dinamico',
|
||||||
first_party: 'Prima parte',
|
first_party: 'Prima parte',
|
||||||
second_party: 'Seconda parte',
|
second_party: 'Seconda parte',
|
||||||
third_party: 'Terza parte',
|
third_party: 'Terza parte',
|
||||||
@@ -551,6 +624,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',
|
||||||
@@ -565,10 +641,24 @@ 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',
|
kba: 'KBA',
|
||||||
autodetect_fields: 'Detectar campos',
|
autodetect_fields: 'Detectar campos',
|
||||||
analyzing_: 'Analisando...',
|
analyzing_: 'Analisando...',
|
||||||
@@ -598,11 +688,18 @@ const pt = {
|
|||||||
search_field: 'Campo de busca',
|
search_field: 'Campo de busca',
|
||||||
field_not_found: 'Campo não encontrado',
|
field_not_found: 'Campo não encontrado',
|
||||||
clear: 'Limpar',
|
clear: 'Limpar',
|
||||||
|
type: 'Tipo',
|
||||||
|
list: 'lista',
|
||||||
|
no_variables: 'Ainda sem variáveis',
|
||||||
|
no_variables_description: 'Adicione marcações [[variable]] ao documento para criar variáveis de conteúdo dinâmico.',
|
||||||
type_value: 'Digite valor',
|
type_value: 'Digite valor',
|
||||||
add_all_required_fields_to_continue: 'Adicione todos os campos obrigatórios para continuar',
|
add_all_required_fields_to_continue: 'Adicione todos os campos obrigatórios para continuar',
|
||||||
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',
|
||||||
@@ -617,6 +714,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',
|
||||||
@@ -626,6 +725,7 @@ const pt = {
|
|||||||
select_value_: 'Selecionar valor...',
|
select_value_: 'Selecionar valor...',
|
||||||
remove_condition: 'Remover condição',
|
remove_condition: 'Remover condição',
|
||||||
condition: 'Condição',
|
condition: 'Condição',
|
||||||
|
make_dynamic: 'Tornar dinâmico',
|
||||||
formula: 'Fórmula',
|
formula: 'Fórmula',
|
||||||
edit: 'Editar',
|
edit: 'Editar',
|
||||||
settings: 'Configurações',
|
settings: 'Configurações',
|
||||||
@@ -635,6 +735,7 @@ const pt = {
|
|||||||
are_you_sure_: 'Tem certeza?',
|
are_you_sure_: 'Tem certeza?',
|
||||||
sign_yourself: 'Assine você mesmo',
|
sign_yourself: 'Assine você mesmo',
|
||||||
checked: 'Marcado',
|
checked: 'Marcado',
|
||||||
|
current_date: 'Data atual',
|
||||||
send: 'Enviar',
|
send: 'Enviar',
|
||||||
remove: 'Remover',
|
remove: 'Remover',
|
||||||
save: 'Salvar',
|
save: 'Salvar',
|
||||||
@@ -657,6 +758,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',
|
||||||
@@ -741,6 +843,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',
|
||||||
@@ -755,10 +860,24 @@ 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',
|
kba: 'KBA',
|
||||||
autodetect_fields: 'Détecter les champs',
|
autodetect_fields: 'Détecter les champs',
|
||||||
analyzing_: 'Analyse...',
|
analyzing_: 'Analyse...',
|
||||||
@@ -789,6 +908,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',
|
||||||
@@ -803,9 +925,15 @@ const fr = {
|
|||||||
with_logo: 'Avec logo',
|
with_logo: 'Avec logo',
|
||||||
unchecked: 'Décoché',
|
unchecked: 'Décoché',
|
||||||
price: 'Prix',
|
price: 'Prix',
|
||||||
|
type: 'Type',
|
||||||
|
list: 'liste',
|
||||||
|
no_variables: 'Pas encore de variables',
|
||||||
|
no_variables_description: 'Ajoutez des balises [[variable]] à votre document pour créer des variables de contenu dynamique.',
|
||||||
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',
|
||||||
@@ -825,6 +953,7 @@ const fr = {
|
|||||||
up: 'Haut',
|
up: 'Haut',
|
||||||
down: 'Bas',
|
down: 'Bas',
|
||||||
checked: 'Coché',
|
checked: 'Coché',
|
||||||
|
current_date: 'Date du jour',
|
||||||
save: 'Enregistrer',
|
save: 'Enregistrer',
|
||||||
cancel: 'Annuler',
|
cancel: 'Annuler',
|
||||||
any: "N'importe lequel",
|
any: "N'importe lequel",
|
||||||
@@ -851,10 +980,12 @@ 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',
|
||||||
condition: 'Condition',
|
condition: 'Condition',
|
||||||
|
make_dynamic: 'Rendre dynamique',
|
||||||
first_party: 'Première partie',
|
first_party: 'Première partie',
|
||||||
second_party: 'Deuxième partie',
|
second_party: 'Deuxième partie',
|
||||||
third_party: 'Troisième partie',
|
third_party: 'Troisième partie',
|
||||||
@@ -931,6 +1062,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',
|
||||||
@@ -945,10 +1079,24 @@ 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',
|
kba: 'KBA',
|
||||||
autodetect_fields: 'Felder erkennen',
|
autodetect_fields: 'Felder erkennen',
|
||||||
analyzing_: 'Analysiere...',
|
analyzing_: 'Analysiere...',
|
||||||
@@ -979,6 +1127,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',
|
||||||
@@ -993,9 +1144,15 @@ const de = {
|
|||||||
with_logo: 'Mit Logo',
|
with_logo: 'Mit Logo',
|
||||||
unchecked: 'Nicht markiert',
|
unchecked: 'Nicht markiert',
|
||||||
price: 'Preis',
|
price: 'Preis',
|
||||||
|
type: 'Typ',
|
||||||
|
list: 'Liste',
|
||||||
|
no_variables: 'Noch keine Variablen',
|
||||||
|
no_variables_description: 'Fügen Sie [[variable]]-Markierungen zu Ihrem Dokument hinzu, um dynamische Inhaltsvariablen zu erstellen.',
|
||||||
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',
|
||||||
@@ -1015,6 +1172,7 @@ const de = {
|
|||||||
up: 'Nach oben',
|
up: 'Nach oben',
|
||||||
down: 'Nach unten',
|
down: 'Nach unten',
|
||||||
checked: 'Markiert',
|
checked: 'Markiert',
|
||||||
|
current_date: 'Aktuelles Datum',
|
||||||
save: 'Speichern',
|
save: 'Speichern',
|
||||||
cancel: 'Abbrechen',
|
cancel: 'Abbrechen',
|
||||||
any: 'Beliebig',
|
any: 'Beliebig',
|
||||||
@@ -1041,10 +1199,12 @@ 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',
|
||||||
condition: 'Bedingung',
|
condition: 'Bedingung',
|
||||||
|
make_dynamic: 'Dynamisch machen',
|
||||||
first_party: 'Erste Partei',
|
first_party: 'Erste Partei',
|
||||||
second_party: 'Zweite Partei',
|
second_party: 'Zweite Partei',
|
||||||
third_party: 'Dritte Partei',
|
third_party: 'Dritte Partei',
|
||||||
@@ -1121,6 +1281,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',
|
||||||
@@ -1135,10 +1298,24 @@ 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',
|
kba: 'KBA',
|
||||||
autodetect_fields: 'Velden detecteren',
|
autodetect_fields: 'Velden detecteren',
|
||||||
analyzing_: 'Analyseren...',
|
analyzing_: 'Analyseren...',
|
||||||
@@ -1169,6 +1346,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',
|
||||||
@@ -1183,9 +1363,15 @@ const nl = {
|
|||||||
with_logo: 'Met logo',
|
with_logo: 'Met logo',
|
||||||
unchecked: 'Niet aangevinkt',
|
unchecked: 'Niet aangevinkt',
|
||||||
price: 'Prijs',
|
price: 'Prijs',
|
||||||
|
type: 'Type',
|
||||||
|
list: 'lijst',
|
||||||
|
no_variables: 'Nog geen variabelen',
|
||||||
|
no_variables_description: 'Voeg [[variable]]-markeringen toe aan uw document om dynamische inhoudsvariabelen te maken.',
|
||||||
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',
|
||||||
@@ -1205,6 +1391,7 @@ const nl = {
|
|||||||
up: 'Omhoog',
|
up: 'Omhoog',
|
||||||
down: 'Omlaag',
|
down: 'Omlaag',
|
||||||
checked: 'Aangevinkt',
|
checked: 'Aangevinkt',
|
||||||
|
current_date: 'Huidige datum',
|
||||||
save: 'Opslaan',
|
save: 'Opslaan',
|
||||||
cancel: 'Annuleren',
|
cancel: 'Annuleren',
|
||||||
any: 'Elke',
|
any: 'Elke',
|
||||||
@@ -1231,10 +1418,12 @@ 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',
|
||||||
condition: 'Voorwaarde',
|
condition: 'Voorwaarde',
|
||||||
|
make_dynamic: 'Dynamisch maken',
|
||||||
first_party: 'Eerste partij',
|
first_party: 'Eerste partij',
|
||||||
second_party: 'Tweede partij',
|
second_party: 'Tweede partij',
|
||||||
third_party: 'Derde partij',
|
third_party: 'Derde partij',
|
||||||
@@ -1311,6 +1500,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',
|
||||||
@@ -1325,7 +1517,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 }
|
||||||
Loaded 100 of 303 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user