mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 14:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
911e55ccc3 | ||
|
|
41fedfcc40 | ||
|
|
2f9fc95af3 | ||
|
|
11b0b16ca7 | ||
|
|
f8cb7ffdab | ||
|
|
8b59c0aaa0 | ||
|
|
bb2fb7a0c2 | ||
|
|
8f8b36617a | ||
|
|
ff53436fd4 | ||
|
|
cf09a4f733 | ||
|
|
2303c21cea | ||
|
|
89e797b95f | ||
|
|
20375c3a42 | ||
|
|
339ceda18d | ||
|
|
97ce32fd52 | ||
|
|
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 |
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:
|
||||||
|
|||||||
+25
-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:
|
||||||
@@ -51,6 +51,15 @@ Style/NumericPredicate:
|
|||||||
Style/MinMaxComparison:
|
Style/MinMaxComparison:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Style/EmptyClassDefinition:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/OneClassPerFile:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Layout/MultilineMethodCallIndentation:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
Naming/PredicateMethod:
|
Naming/PredicateMethod:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
@@ -84,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
|
||||||
@@ -100,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
|
||||||
+16
-20
@@ -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.1.0/model_704_int8.onnx" && \
|
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" && \
|
||||||
wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" && \
|
|
||||||
mkdir -p /pdfium-linux && \
|
mkdir -p /pdfium-linux && \
|
||||||
tar -xzf pdfium-linux.tgz -C /pdfium-linux
|
tar -xzf pdfium-linux.tgz -C /pdfium-linux
|
||||||
|
|
||||||
RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")'
|
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,22 @@ 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=/etc/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 addgroup -g 2000 docuseal && adduser -u 2000 -G docuseal -s /bin/sh -D -h /home/docuseal docuseal
|
||||||
|
|
||||||
@@ -69,9 +66,7 @@ activate = 1' >> /etc/openssl_legacy.cnf
|
|||||||
|
|
||||||
COPY --chown=docuseal:docuseal ./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 ./bin ./bin
|
||||||
COPY --chown=docuseal:docuseal ./app ./app
|
COPY --chown=docuseal:docuseal ./app ./app
|
||||||
@@ -81,23 +76,24 @@ COPY --chown=docuseal:docuseal ./log ./log
|
|||||||
COPY --chown=docuseal:docuseal ./lib ./lib
|
COPY --chown=docuseal:docuseal ./lib ./lib
|
||||||
COPY --chown=docuseal:docuseal ./public ./public
|
COPY --chown=docuseal:docuseal ./public ./public
|
||||||
COPY --chown=docuseal:docuseal ./tmp ./tmp
|
COPY --chown=docuseal:docuseal ./tmp ./tmp
|
||||||
COPY --chown=docuseal:docuseal LICENSE README.md Rakefile config.ru .version ./
|
COPY --chown=docuseal:docuseal LICENSE LICENSE_ADDITIONAL_TERMS README.md Rakefile config.ru .version ./
|
||||||
COPY --chown=docuseal:docuseal .version ./public/version
|
COPY --chown=docuseal:docuseal .version ./public/version
|
||||||
|
|
||||||
COPY --chown=docuseal:docuseal --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
COPY --chown=docuseal:docuseal --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts/LICENSE /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 --chown=docuseal:docuseal --from=download /model.onnx /app/tmp/model.onnx
|
COPY --chown=docuseal:docuseal --from=download /model.onnx /app/tmp/model.onnx
|
||||||
COPY --chown=docuseal:docuseal --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/ && \
|
||||||
|
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/ && \
|
bundle exec bootsnap precompile -j 1 --gemfile app/ lib/ && \
|
||||||
chown -R docuseal:docuseal /app/tmp/cache
|
chown -R docuseal:docuseal /app/tmp/cache
|
||||||
|
|
||||||
WORKDIR /data/docuseal
|
WORKDIR /data/docuseal
|
||||||
ENV HOME=/home/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'
|
||||||
|
|||||||
+295
-318
@@ -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.9.0)
|
||||||
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,124 +327,117 @@ 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.2)
|
||||||
base64 (>= 0.1.0)
|
base64 (>= 0.1.0)
|
||||||
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
|
||||||
@@ -53,7 +53,9 @@ class AccountsController < ApplicationController
|
|||||||
# rubocop:disable Layout/LineLength
|
# rubocop:disable Layout/LineLength
|
||||||
render turbo_stream: turbo_stream.replace(
|
render turbo_stream: turbo_stream.replace(
|
||||||
:account_delete_button,
|
:account_delete_button,
|
||||||
html: helpers.tag.p(I18n.t('your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account'))
|
html: helpers.tag.p(I18n.t('your_account_will_be_permanently_deleted_within_2_months_please_contact_us_if_you_want_to_keep_your_account')) +
|
||||||
|
helpers.tag.br +
|
||||||
|
helpers.tag.p(I18n.t('your_email_address_has_been_released_immediately_you_can_now_be_added_to_your_company_team_without_waiting_for_the_deletion_period_to_end'))
|
||||||
)
|
)
|
||||||
# rubocop:enable Layout/LineLength
|
# rubocop:enable Layout/LineLength
|
||||||
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
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ module Api
|
|||||||
def create
|
def create
|
||||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
|
unless can_upload?(submitter)
|
||||||
|
Rollbar.error("Can't upload: #{submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content
|
||||||
|
end
|
||||||
|
|
||||||
if params[:type].in?(%w[initials signature])
|
if params[:type].in?(%w[initials signature])
|
||||||
image = Vips::Image.new_from_file(params[:file].path)
|
image = Vips::Image.new_from_file(params[:file].path)
|
||||||
|
|
||||||
@@ -40,6 +46,14 @@ module Api
|
|||||||
render json: { error: e.message }, status: :unprocessable_content
|
render json: { error: e.message }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def can_upload?(submitter)
|
||||||
|
!submitter.declined_at? &&
|
||||||
|
!submitter.completed_at? &&
|
||||||
|
!submitter.submission.archived_at? &&
|
||||||
|
!submitter.submission.expired? &&
|
||||||
|
!submitter.submission.template&.archived_at?
|
||||||
|
end
|
||||||
|
|
||||||
def build_new_cookie_signatures_json(submitter, attachment)
|
def build_new_cookie_signatures_json(submitter, attachment)
|
||||||
values =
|
values =
|
||||||
begin
|
begin
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ module Api
|
|||||||
params:)
|
params:)
|
||||||
else
|
else
|
||||||
submissions_attrs, attachments =
|
submissions_attrs, attachments =
|
||||||
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_params, template)
|
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_params, template, purpose: :api)
|
||||||
|
|
||||||
submissions = Submissions.create_from_submitters(
|
submissions = Submissions.create_from_submitters(
|
||||||
template:,
|
template:,
|
||||||
@@ -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,18 +34,24 @@ 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']
|
||||||
|
|
||||||
normalized_params, new_attachments = Submissions::NormalizeParamUtils.normalize_submitter_params!(
|
normalized_params, new_attachments = Submissions::NormalizeParamUtils.normalize_submitter_params!(
|
||||||
submitter_params.merge(role:),
|
submitter_params.merge(role:),
|
||||||
@submitter.template || Template.new(submitters: submission.template_submitters, account: @submitter.account),
|
@submitter.template || Template.new(submitters: submission.template_submitters, account: @submitter.account),
|
||||||
for_submitter: @submitter
|
for_submitter: @submitter,
|
||||||
|
purpose: :api
|
||||||
)
|
)
|
||||||
|
|
||||||
Submissions::CreateFromSubmitters.maybe_set_template_fields(submission, [normalized_params],
|
Submissions::CreateFromSubmitters.maybe_set_template_fields(submission, [normalized_params],
|
||||||
@@ -60,7 +66,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 +87,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?
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ class ApplicationController < ActionController::Base
|
|||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -42,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
|
||||||
|
|
||||||
@@ -60,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
|
||||||
|
|||||||
@@ -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?
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class SubmissionsDebugController < ApplicationController
|
|||||||
def index
|
def index
|
||||||
@submitter = Submitter.preload({ attachments_attachments: :blob },
|
@submitter = Submitter.preload({ attachments_attachments: :blob },
|
||||||
submission: { template: { documents_attachments: :blob } })
|
submission: { template: { documents_attachments: :blob } })
|
||||||
.find_by(slug: params[:submitter_slug])
|
.find_by(slug: params[:submit_form_slug])
|
||||||
|
|
||||||
respond_to do |f|
|
respond_to do |f|
|
||||||
f.html do
|
f.html do
|
||||||
|
|||||||
@@ -1,40 +1,17 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class SubmissionsDownloadController < ApplicationController
|
class SubmissionsDownloadController < ApplicationController
|
||||||
skip_before_action :authenticate_user!
|
load_and_authorize_resource :submission
|
||||||
skip_authorization_check
|
|
||||||
|
|
||||||
TTL = 40.minutes
|
|
||||||
FILES_TTL = 5.minutes
|
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
last_submitter = @submission.submitters.where.not(completed_at: nil).order(:completed_at).last
|
||||||
|
|
||||||
signature_valid =
|
|
||||||
if @submitter&.slug == params[:submitter_slug]
|
|
||||||
true
|
|
||||||
else
|
|
||||||
@submitter = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
@submitter ||= Submitter.find_by!(slug: params[:submitter_slug])
|
|
||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(@submitter)
|
|
||||||
|
|
||||||
last_submitter = @submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
|
|
||||||
|
|
||||||
return head :not_found unless last_submitter
|
return head :not_found unless last_submitter
|
||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(last_submitter)
|
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||||
|
|
||||||
if last_submitter.completed_at < TTL.ago && !signature_valid && !current_user_submitter?(last_submitter)
|
|
||||||
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
|
||||||
|
|
||||||
return head :not_found
|
|
||||||
end
|
|
||||||
|
|
||||||
if params[:combined] == 'true'
|
if params[:combined] == 'true'
|
||||||
url = build_combined_url(@submitter)
|
url = Submitters.build_combined_url(last_submitter)
|
||||||
|
|
||||||
if url
|
if url
|
||||||
render json: [url]
|
render json: [url]
|
||||||
@@ -42,43 +19,7 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
head :not_found
|
head :not_found
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
render json: build_urls(last_submitter)
|
render json: Submitters.build_document_urls(last_submitter)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def current_user_submitter?(submitter)
|
|
||||||
current_user && current_user.account.submitters.exists?(id: submitter.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_urls(submitter)
|
|
||||||
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
|
|
||||||
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
|
|
||||||
|
|
||||||
Submitters.select_attachments_for_download(submitter).map do |attachment|
|
|
||||||
ActiveStorage::Blob.proxy_url(
|
|
||||||
attachment.blob,
|
|
||||||
expires_at: FILES_TTL.from_now.to_i,
|
|
||||||
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_combined_url(submitter)
|
|
||||||
return if submitter.submission.submitters.exists?(completed_at: nil)
|
|
||||||
return if submitter.submission.submitters.order(:completed_at).last != submitter
|
|
||||||
|
|
||||||
attachment = submitter.submission.combined_document_attachment
|
|
||||||
attachment ||= Submissions::EnsureCombinedGenerated.call(submitter)
|
|
||||||
|
|
||||||
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
|
|
||||||
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
|
|
||||||
|
|
||||||
ActiveStorage::Blob.proxy_url(
|
|
||||||
attachment.blob,
|
|
||||||
expires_at: FILES_TTL.from_now.to_i,
|
|
||||||
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -10,13 +10,15 @@ class SubmissionsPreviewController < ApplicationController
|
|||||||
TTL = 40.minutes
|
TTL = 40.minutes
|
||||||
|
|
||||||
def show
|
def show
|
||||||
submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
@sig_submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
||||||
|
|
||||||
signature_valid =
|
signature_valid =
|
||||||
if submitter && submitter.submission.slug == params[:slug]
|
if @sig_submitter && @sig_submitter.submission.slug == params[:slug]
|
||||||
@submission = submitter.submission
|
@submission = @sig_submitter.submission
|
||||||
|
|
||||||
true
|
true
|
||||||
|
else
|
||||||
|
@sig_submitter = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
@submission ||= Submission.find_by!(slug: params[:slug])
|
@submission ||= Submission.find_by!(slug: params[:slug])
|
||||||
@@ -36,7 +38,7 @@ class SubmissionsPreviewController < ApplicationController
|
|||||||
|
|
||||||
@submission = Submissions.preload_with_pages(@submission)
|
@submission = Submissions.preload_with_pages(@submission)
|
||||||
|
|
||||||
render 'submissions/show', layout: 'plain'
|
render 'submissions/show', layout: 'plain', locals: { is_preview: true }
|
||||||
end
|
end
|
||||||
|
|
||||||
def completed
|
def completed
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmissionsPreviewDownloadController < ApplicationController
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
TTL = 40.minutes
|
||||||
|
|
||||||
|
def index
|
||||||
|
@submission = Submission.find_by!(slug: params[:submission_slug] || params[:submissions_preview_slug])
|
||||||
|
|
||||||
|
last_submitter = @submission.submitters.where.not(completed_at: nil).order(:completed_at).last
|
||||||
|
|
||||||
|
return head :not_found unless last_submitter
|
||||||
|
|
||||||
|
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||||
|
|
||||||
|
unless current_user_submission?(@submission)
|
||||||
|
if use_2fa?(@submission)
|
||||||
|
Rollbar.info("2FA download error: #{last_submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return head :not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
if last_submitter.completed_at < TTL.ago
|
||||||
|
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return head :not_found
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if params[:combined] == 'true'
|
||||||
|
respond_with_combined(last_submitter)
|
||||||
|
else
|
||||||
|
render json: Submitters.build_document_urls(last_submitter)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def respond_with_combined(submitter)
|
||||||
|
url = Submitters.build_combined_url(submitter)
|
||||||
|
|
||||||
|
if url
|
||||||
|
render json: [url]
|
||||||
|
else
|
||||||
|
head :not_found
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_user_submission?(submission)
|
||||||
|
current_user && current_ability.can?(:read, submission)
|
||||||
|
end
|
||||||
|
|
||||||
|
def use_2fa?(submission)
|
||||||
|
return true if submission.submitters.any? do |e|
|
||||||
|
e.preferences['require_phone_2fa'] || e.preferences['require_email_2fa']
|
||||||
|
end
|
||||||
|
return true if submission.template&.preferences&.dig('require_phone_2fa')
|
||||||
|
return true if submission.template&.preferences&.dig('require_email_2fa')
|
||||||
|
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmitFormCompletedDownloadController < ApplicationController
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
TTL = 40.minutes
|
||||||
|
FILES_TTL = 5.minutes
|
||||||
|
|
||||||
|
def index
|
||||||
|
@submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
||||||
|
|
||||||
|
signature_valid =
|
||||||
|
if @submitter&.slug == submitter_slug
|
||||||
|
true
|
||||||
|
else
|
||||||
|
@submitter = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
@submitter ||= Submitter.find_by!(slug: submitter_slug)
|
||||||
|
|
||||||
|
Submissions::EnsureResultGenerated.call(@submitter)
|
||||||
|
|
||||||
|
last_submitter = @submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
|
||||||
|
|
||||||
|
return head :not_found unless last_submitter
|
||||||
|
|
||||||
|
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||||
|
|
||||||
|
if !signature_valid && !current_user_submitter?(last_submitter)
|
||||||
|
unless Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||||
|
Rollbar.info("2FA download error: #{last_submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return head :not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
if last_submitter.completed_at < TTL.ago
|
||||||
|
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return head :not_found
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if params[:combined] == 'true'
|
||||||
|
respond_with_combined(last_submitter)
|
||||||
|
else
|
||||||
|
render json: Submitters.build_document_urls(last_submitter)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def submitter_slug
|
||||||
|
params[:submit_form_slug] || params[:submitter_slug] || params[:submitter_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
def respond_with_combined(submitter)
|
||||||
|
url = Submitters.build_combined_url(submitter)
|
||||||
|
|
||||||
|
if url
|
||||||
|
render json: [url]
|
||||||
|
else
|
||||||
|
head :not_found
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_user_submitter?(submitter)
|
||||||
|
current_user && current_ability.can?(:read, submitter)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -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
|
||||||
@@ -9,7 +9,7 @@ class SubmitFormDownloadController < ApplicationController
|
|||||||
def index
|
def index
|
||||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||||
|
|
||||||
return redirect_to submitter_download_index_path(@submitter.slug) if @submitter.completed_at?
|
return redirect_to submit_form_documents_path(@submitter.slug) if @submitter.completed_at?
|
||||||
|
|
||||||
return head :unprocessable_content if @submitter.declined_at? ||
|
return head :unprocessable_content if @submitter.declined_at? ||
|
||||||
@submitter.submission.archived_at? ||
|
@submitter.submission.archived_at? ||
|
||||||
@@ -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}%"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmittersDownloadController < ApplicationController
|
||||||
|
load_and_authorize_resource :submitter
|
||||||
|
|
||||||
|
def index
|
||||||
|
Submissions::EnsureResultGenerated.call(@submitter)
|
||||||
|
|
||||||
|
render json: Submitters.build_document_urls(@submitter)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class SubmittersSendEmailController < ApplicationController
|
class SubmittersSendEmailController < ApplicationController
|
||||||
load_and_authorize_resource :submitter, id_param: :submitter_slug, find_by: :slug
|
load_and_authorize_resource :submitter
|
||||||
|
|
||||||
def create
|
def create
|
||||||
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
|
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class TemplatesCloneController < ApplicationController
|
|||||||
def create
|
def create
|
||||||
ActiveRecord::Associations::Preloader.new(
|
ActiveRecord::Associations::Preloader.new(
|
||||||
records: [@base_template],
|
records: [@base_template],
|
||||||
associations: [schema_documents: :preview_images_attachments]
|
associations: [{ schema_documents: :preview_images_attachments }]
|
||||||
).call
|
).call
|
||||||
|
|
||||||
@template = Templates::Clone.call(@base_template, author: current_user,
|
@template = Templates::Clone.call(@base_template, author: current_user,
|
||||||
@@ -21,7 +21,7 @@ class TemplatesCloneController < ApplicationController
|
|||||||
|
|
||||||
authorize!(:create, @template)
|
authorize!(:create, @template)
|
||||||
|
|
||||||
if params[:account_id].present? && true_ability.authorize!(:manage, Account.find(params[:account_id]))
|
if params[:account_id].present? && true_ability.can?(:manage, Account.find(params[:account_id]))
|
||||||
@template.account_id = params[:account_id]
|
@template.account_id = params[:account_id]
|
||||||
@template.author = true_user if true_user.account_id == @template.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
|
@template.folder = @template.account.default_template_folder if @template.account_id != current_account.id
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ class TemplatesController < ApplicationController
|
|||||||
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
|
||||||
@@ -84,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
|
||||||
|
|
||||||
@@ -95,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,
|
||||||
@@ -106,7 +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
|
||||||
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
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class TemplatesDetectFieldsController < ApplicationController
|
|||||||
documents = @template.schema_documents.preload(:blob)
|
documents = @template.schema_documents.preload(:blob)
|
||||||
documents = documents.where(uuid: params[:attachment_uuid]) if params[:attachment_uuid].present?
|
documents = documents.where(uuid: params[:attachment_uuid]) if params[:attachment_uuid].present?
|
||||||
|
|
||||||
page_number = params[:page].present? ? params[:page].to_i : nil
|
page_number = params[:page].presence&.to_i
|
||||||
|
|
||||||
documents.each do |document|
|
documents.each do |document|
|
||||||
io = StringIO.new(document.download)
|
io = StringIO.new(document.download)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -16,7 +16,19 @@ class UsersController < ApplicationController
|
|||||||
@users.active.where.not(role: 'integration')
|
@users.active.where.not(role: 'integration')
|
||||||
end
|
end
|
||||||
|
|
||||||
@pagy, @users = pagy(@users.preload(account: :account_accesses).where(account: current_account).order(id: :desc))
|
@users = @users.preload(account: :account_accesses).where(account: current_account).order(id: :desc)
|
||||||
|
|
||||||
|
respond_to do |format|
|
||||||
|
format.html do
|
||||||
|
@pagy, @users = pagy(@users)
|
||||||
|
end
|
||||||
|
|
||||||
|
if current_ability.can?(:manage, current_account)
|
||||||
|
format.csv do
|
||||||
|
send_data Users.generate_csv(@users), filename: "users-#{Time.current.iso8601}.csv", type: 'text/csv'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def new; end
|
def new; end
|
||||||
|
|||||||
@@ -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')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,12 +31,13 @@ safeRegisterElement('submission-form', class extends HTMLElement {
|
|||||||
isDemo: this.dataset.isDemo === 'true',
|
isDemo: this.dataset.isDemo === 'true',
|
||||||
attribution: this.dataset.attribution !== 'false',
|
attribution: this.dataset.attribution !== 'false',
|
||||||
scrollPadding: this.dataset.scrollPadding || '-80px',
|
scrollPadding: this.dataset.scrollPadding || '-80px',
|
||||||
|
signatureText: this.dataset.signatureText,
|
||||||
language: this.dataset.language,
|
language: this.dataset.language,
|
||||||
dryRun: this.dataset.dryRun === 'true',
|
dryRun: this.dataset.dryRun === 'true',
|
||||||
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"
|
||||||
|
|||||||
@@ -0,0 +1,874 @@
|
|||||||
|
/*
|
||||||
|
* Portions of this file are derived from the Dentaku gem
|
||||||
|
* https://github.com/rubysolo/dentaku
|
||||||
|
*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2012 Solomon White
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*
|
||||||
|
* Modifications and JavaScript port
|
||||||
|
* Copyright (c) 2026 DocuSeal, LLC
|
||||||
|
*/
|
||||||
|
|
||||||
|
class CalculatorError extends Error { constructor (msg) { super(msg); this.name = 'CalculatorError' } }
|
||||||
|
class ParseError extends CalculatorError { constructor (msg) { super(msg); this.name = 'ParseError' } }
|
||||||
|
class TokenizerError extends CalculatorError { constructor (msg) { super(msg); this.name = 'TokenizerError' } }
|
||||||
|
class ArgumentError extends CalculatorError { constructor (msg) { super(msg); this.name = 'ArgumentError' } }
|
||||||
|
class ZeroDivisionError extends CalculatorError { constructor () { super('divided by 0'); this.name = 'ZeroDivisionError' } }
|
||||||
|
class UnboundVariableError extends CalculatorError {
|
||||||
|
constructor (vars) {
|
||||||
|
super('no value provided for variables: ' + vars.join(', '))
|
||||||
|
this.name = 'UnboundVariableError'
|
||||||
|
this.unbound = vars
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Token {
|
||||||
|
constructor (category, value, raw) {
|
||||||
|
this.category = category
|
||||||
|
this.value = value
|
||||||
|
this.raw = raw == null ? String(value) : String(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
is (cat) { return this.category === cat }
|
||||||
|
}
|
||||||
|
|
||||||
|
const OPERATOR_NAMES = {
|
||||||
|
'^': 'pow',
|
||||||
|
'+': 'add',
|
||||||
|
'-': 'subtract',
|
||||||
|
'*': 'multiply',
|
||||||
|
'/': 'divide',
|
||||||
|
'%': 'mod',
|
||||||
|
'|': 'bitor',
|
||||||
|
'&': 'bitand',
|
||||||
|
'<<': 'bitshiftleft',
|
||||||
|
'>>': 'bitshiftright'
|
||||||
|
}
|
||||||
|
const COMPARATOR_NAMES = {
|
||||||
|
'<=': 'le',
|
||||||
|
'>=': 'ge',
|
||||||
|
'!=': 'ne',
|
||||||
|
'<>': 'ne',
|
||||||
|
'<': 'lt',
|
||||||
|
'>': 'gt',
|
||||||
|
'==': 'eq',
|
||||||
|
'=': 'eq'
|
||||||
|
}
|
||||||
|
const GROUPING_NAMES = { '(': 'open', ')': 'close', ',': 'comma' }
|
||||||
|
|
||||||
|
function buildScanners (caseSensitive) {
|
||||||
|
return [
|
||||||
|
(s) => {
|
||||||
|
const m = /^\s+/.exec(s)
|
||||||
|
return m ? { tokens: [], length: m[0].length } : null
|
||||||
|
},
|
||||||
|
(s) => {
|
||||||
|
const m = /^null\b/i.exec(s)
|
||||||
|
return m ? { tokens: [new Token('null', null, m[0])], length: m[0].length } : null
|
||||||
|
},
|
||||||
|
(s) => {
|
||||||
|
const m = /^((?:\d+(?:\.\d+)?|\.\d+)(?:[eE][+-]?\d+)?)\b/.exec(s)
|
||||||
|
if (!m) return null
|
||||||
|
return { tokens: [new Token('numeric', Number(m[0]), m[0])], length: m[0].length }
|
||||||
|
},
|
||||||
|
(s) => {
|
||||||
|
const m = /^0x[0-9a-f]+\b/i.exec(s)
|
||||||
|
if (!m) return null
|
||||||
|
return { tokens: [new Token('numeric', parseInt(m[0].slice(2), 16), m[0])], length: m[0].length }
|
||||||
|
},
|
||||||
|
(s) => {
|
||||||
|
const m = /^"([^"]*)"/.exec(s)
|
||||||
|
if (!m) return null
|
||||||
|
return { tokens: [new Token('string', m[1], m[0])], length: m[0].length }
|
||||||
|
},
|
||||||
|
(s) => {
|
||||||
|
const m = /^'([^']*)'/.exec(s)
|
||||||
|
if (!m) return null
|
||||||
|
return { tokens: [new Token('string', m[1], m[0])], length: m[0].length }
|
||||||
|
},
|
||||||
|
(s, last) => {
|
||||||
|
if (s[0] !== '-') return null
|
||||||
|
const ok = last == null ||
|
||||||
|
last.is('operator') ||
|
||||||
|
last.is('comparator') ||
|
||||||
|
last.is('combinator') ||
|
||||||
|
last.value === 'open' ||
|
||||||
|
last.value === 'comma' ||
|
||||||
|
last.value === 'array_start'
|
||||||
|
if (!ok) return null
|
||||||
|
return { tokens: [new Token('operator', 'negate', '-')], length: 1 }
|
||||||
|
},
|
||||||
|
(s) => {
|
||||||
|
const m = /^(and|or|&&|\|\|)\s/i.exec(s)
|
||||||
|
if (!m) return null
|
||||||
|
const raw = m[1].toLowerCase()
|
||||||
|
const name = raw === '&&' ? 'and' : raw === '||' ? 'or' : raw
|
||||||
|
return { tokens: [new Token('combinator', name, m[0])], length: m[0].length }
|
||||||
|
},
|
||||||
|
(s) => {
|
||||||
|
const m = /^(<<|>>|\^|\+|-|\*|\/|%|\||&)/.exec(s)
|
||||||
|
if (!m) return null
|
||||||
|
return { tokens: [new Token('operator', OPERATOR_NAMES[m[0]], m[0])], length: m[0].length }
|
||||||
|
},
|
||||||
|
(s) => {
|
||||||
|
const m = /^(\(|\)|,)/.exec(s)
|
||||||
|
if (!m) return null
|
||||||
|
return { tokens: [new Token('grouping', GROUPING_NAMES[m[0]], m[0])], length: m[0].length }
|
||||||
|
},
|
||||||
|
(s) => {
|
||||||
|
if (s[0] === '{') return { tokens: [new Token('array', 'array_start', '{')], length: 1 }
|
||||||
|
if (s[0] === '}') return { tokens: [new Token('array', 'array_end', '}')], length: 1 }
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
(s) => {
|
||||||
|
const m = /^(<=|>=|!=|<>|==|=|<|>)/.exec(s)
|
||||||
|
if (!m) return null
|
||||||
|
return { tokens: [new Token('comparator', COMPARATOR_NAMES[m[0]], m[0])], length: m[0].length }
|
||||||
|
},
|
||||||
|
(s) => {
|
||||||
|
const m = /^(true|false)\b/i.exec(s)
|
||||||
|
if (!m) return null
|
||||||
|
return { tokens: [new Token('logical', m[0].toLowerCase() === 'true', m[0])], length: m[0].length }
|
||||||
|
},
|
||||||
|
(s) => {
|
||||||
|
const m = /^(\w+!?)\s*\(/.exec(s)
|
||||||
|
if (!m) return null
|
||||||
|
const name = m[1].toLowerCase()
|
||||||
|
return {
|
||||||
|
tokens: [
|
||||||
|
new Token('function', name, m[1]),
|
||||||
|
new Token('grouping', 'open', '(')
|
||||||
|
],
|
||||||
|
length: m[0].length
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(s) => {
|
||||||
|
const m = /^[A-Za-z_][\w.]*\b/.exec(s)
|
||||||
|
if (!m) return null
|
||||||
|
const value = caseSensitive ? m[0] : m[0].toLowerCase()
|
||||||
|
return { tokens: [new Token('identifier', value, m[0])], length: m[0].length }
|
||||||
|
},
|
||||||
|
(s) => {
|
||||||
|
const m = /^`([^`]*)`/.exec(s)
|
||||||
|
if (!m) return null
|
||||||
|
return { tokens: [new Token('identifier', m[1], m[0])], length: m[0].length }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenize (input, options) {
|
||||||
|
options = options || {}
|
||||||
|
let remaining = String(input).replace(/\/\*[\s\S]*?\*\//g, '')
|
||||||
|
const scanners = buildScanners(options.caseSensitive === true)
|
||||||
|
const tokens = []
|
||||||
|
let nesting = 0
|
||||||
|
|
||||||
|
while (remaining.length > 0) {
|
||||||
|
let matched = false
|
||||||
|
for (let i = 0; i < scanners.length; i++) {
|
||||||
|
const result = scanners[i](remaining, tokens.length ? tokens[tokens.length - 1] : null)
|
||||||
|
if (result) {
|
||||||
|
for (const tok of result.tokens) {
|
||||||
|
if (tok.category === 'grouping' && tok.value === 'open') nesting++
|
||||||
|
if (tok.category === 'grouping' && tok.value === 'close') {
|
||||||
|
nesting--
|
||||||
|
if (nesting < 0) throw new TokenizerError('too many closing parentheses')
|
||||||
|
}
|
||||||
|
tokens.push(tok)
|
||||||
|
}
|
||||||
|
remaining = remaining.slice(result.length)
|
||||||
|
matched = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!matched) throw new TokenizerError("parse error at: '" + remaining + "'")
|
||||||
|
}
|
||||||
|
if (nesting > 0) throw new TokenizerError('too many opening parentheses')
|
||||||
|
return tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
function uniq (arr) {
|
||||||
|
const seen = new Set()
|
||||||
|
const out = []
|
||||||
|
for (const x of arr) { if (!seen.has(x)) { seen.add(x); out.push(x) } }
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
function toNumber (val) {
|
||||||
|
if (typeof val === 'number') return val
|
||||||
|
if (typeof val === 'boolean') return val ? 1 : 0
|
||||||
|
if (val == null) throw new ArgumentError("'" + val + "' is not coercible to numeric")
|
||||||
|
if (typeof val === 'string') {
|
||||||
|
if (val.length === 0) throw new ArgumentError("'' is not coercible to numeric")
|
||||||
|
const n = Number(val)
|
||||||
|
if (isNaN(n)) throw new ArgumentError("'" + val + "' is not coercible to numeric")
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
throw new ArgumentError("'" + val + "' is not coercible to numeric")
|
||||||
|
}
|
||||||
|
|
||||||
|
class Node {
|
||||||
|
static precedence = 0
|
||||||
|
static arity = null
|
||||||
|
static rightAssociative = false
|
||||||
|
static resolveClass () { return this }
|
||||||
|
static minParamCount () { return this.arity }
|
||||||
|
static maxParamCount () { return this.arity }
|
||||||
|
value () { return null }
|
||||||
|
dependencies () { return [] }
|
||||||
|
get type () { return null }
|
||||||
|
}
|
||||||
|
|
||||||
|
class NilNode extends Node { value () { return null } }
|
||||||
|
|
||||||
|
class Numeric extends Node {
|
||||||
|
constructor (token) { super(); this.v = token.value }
|
||||||
|
value () { return this.v }
|
||||||
|
get type () { return 'numeric' }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Logical extends Node {
|
||||||
|
constructor (token) { super(); this.v = token.value }
|
||||||
|
value () { return this.v }
|
||||||
|
get type () { return 'logical' }
|
||||||
|
}
|
||||||
|
|
||||||
|
class StringNode extends Node {
|
||||||
|
constructor (token) { super(); this.v = token.value }
|
||||||
|
value () { return this.v }
|
||||||
|
get type () { return 'string' }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Identifier extends Node {
|
||||||
|
constructor (token) { super(); this.ident = token.value }
|
||||||
|
value (ctx) {
|
||||||
|
if (ctx && (this.ident in ctx)) {
|
||||||
|
const v = ctx[this.ident]
|
||||||
|
if (v instanceof Node) return v.value(ctx)
|
||||||
|
if (typeof v === 'function') return v()
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
throw new UnboundVariableError([this.ident])
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies (ctx) {
|
||||||
|
if (ctx && (this.ident in ctx)) {
|
||||||
|
const v = ctx[this.ident]
|
||||||
|
return v instanceof Node ? v.dependencies(ctx) : []
|
||||||
|
}
|
||||||
|
return [this.ident]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Grouping extends Node {
|
||||||
|
constructor (node) { super(); this.node = node }
|
||||||
|
value (ctx) { return this.node.value(ctx) }
|
||||||
|
get type () { return this.node.type }
|
||||||
|
dependencies (ctx) { return this.node.dependencies(ctx) }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Operation extends Node {
|
||||||
|
constructor (left, right) { super(); this.left = left; this.right = right }
|
||||||
|
dependencies (ctx) {
|
||||||
|
const l = this.left ? this.left.dependencies(ctx) : []
|
||||||
|
const r = this.right ? this.right.dependencies(ctx) : []
|
||||||
|
return uniq(l.concat(r))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Arithmetic extends Operation { get type () { return 'numeric' } }
|
||||||
|
|
||||||
|
class Addition extends Arithmetic {
|
||||||
|
static precedence = 10
|
||||||
|
value (ctx) {
|
||||||
|
const l = this.left.value(ctx)
|
||||||
|
const r = this.right.value(ctx)
|
||||||
|
if (typeof l === 'string' || typeof r === 'string') return String(l) + String(r)
|
||||||
|
return toNumber(l) + toNumber(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Subtraction extends Arithmetic {
|
||||||
|
static precedence = 10
|
||||||
|
value (ctx) { return toNumber(this.left.value(ctx)) - toNumber(this.right.value(ctx)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Multiplication extends Arithmetic {
|
||||||
|
static precedence = 20
|
||||||
|
value (ctx) { return toNumber(this.left.value(ctx)) * toNumber(this.right.value(ctx)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Division extends Arithmetic {
|
||||||
|
static precedence = 20
|
||||||
|
value (ctx) {
|
||||||
|
const r = toNumber(this.right.value(ctx))
|
||||||
|
if (r === 0) throw new ZeroDivisionError()
|
||||||
|
return toNumber(this.left.value(ctx)) / r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Modulo extends Arithmetic {
|
||||||
|
static arity = 2
|
||||||
|
static precedence = 20
|
||||||
|
static resolveClass (nextTok) {
|
||||||
|
if (!nextTok) return Percentage
|
||||||
|
if (nextTok.category === 'operator') return Percentage
|
||||||
|
if (nextTok.category === 'comparator') return Percentage
|
||||||
|
if (nextTok.category === 'combinator') return Percentage
|
||||||
|
if (nextTok.category === 'grouping' && (nextTok.value === 'close' || nextTok.value === 'comma')) return Percentage
|
||||||
|
if (nextTok.category === 'array' && nextTok.value === 'array_end') return Percentage
|
||||||
|
return Modulo
|
||||||
|
}
|
||||||
|
|
||||||
|
value (ctx) {
|
||||||
|
const r = toNumber(this.right.value(ctx))
|
||||||
|
if (r === 0) throw new ZeroDivisionError()
|
||||||
|
return toNumber(this.left.value(ctx)) % r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Percentage extends Arithmetic {
|
||||||
|
static arity = 1
|
||||||
|
static precedence = 30
|
||||||
|
constructor (child) { super(child, null) }
|
||||||
|
value (ctx) { return toNumber(this.left.value(ctx)) * 0.01 }
|
||||||
|
dependencies (ctx) { return this.left.dependencies(ctx) }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Exponentiation extends Arithmetic {
|
||||||
|
static precedence = 30
|
||||||
|
static rightAssociative = true
|
||||||
|
value (ctx) { return Math.pow(toNumber(this.left.value(ctx)), toNumber(this.right.value(ctx))) }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Negation extends Arithmetic {
|
||||||
|
static arity = 1
|
||||||
|
static precedence = 40
|
||||||
|
static rightAssociative = true
|
||||||
|
constructor (node) { super(node, null) }
|
||||||
|
value (ctx) { return -toNumber(this.left.value(ctx)) }
|
||||||
|
dependencies (ctx) { return this.left.dependencies(ctx) }
|
||||||
|
}
|
||||||
|
|
||||||
|
class BitwiseOr extends Operation {
|
||||||
|
static precedence = 10
|
||||||
|
value (ctx) { return toNumber(this.left.value(ctx)) | toNumber(this.right.value(ctx)) }
|
||||||
|
get type () { return 'numeric' }
|
||||||
|
}
|
||||||
|
class BitwiseAnd extends Operation {
|
||||||
|
static precedence = 10
|
||||||
|
value (ctx) { return toNumber(this.left.value(ctx)) & toNumber(this.right.value(ctx)) }
|
||||||
|
get type () { return 'numeric' }
|
||||||
|
}
|
||||||
|
class BitwiseShiftLeft extends Operation {
|
||||||
|
static precedence = 10
|
||||||
|
value (ctx) { return toNumber(this.left.value(ctx)) << toNumber(this.right.value(ctx)) }
|
||||||
|
get type () { return 'numeric' }
|
||||||
|
}
|
||||||
|
class BitwiseShiftRight extends Operation {
|
||||||
|
static precedence = 10
|
||||||
|
value (ctx) { return toNumber(this.left.value(ctx)) >> toNumber(this.right.value(ctx)) }
|
||||||
|
get type () { return 'numeric' }
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmpCast (v) {
|
||||||
|
if (typeof v === 'string' && /^-?\d*\.?\d+$/.test(v)) return Number(v)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
class Comparator extends Operation {
|
||||||
|
static precedence = 5
|
||||||
|
get type () { return 'logical' }
|
||||||
|
}
|
||||||
|
class LessThan extends Comparator { value (ctx) { return cmpCast(this.left.value(ctx)) < cmpCast(this.right.value(ctx)) } }
|
||||||
|
class LessThanOrEqual extends Comparator { value (ctx) { return cmpCast(this.left.value(ctx)) <= cmpCast(this.right.value(ctx)) } }
|
||||||
|
class GreaterThan extends Comparator { value (ctx) { return cmpCast(this.left.value(ctx)) > cmpCast(this.right.value(ctx)) } }
|
||||||
|
class GreaterThanOrEqual extends Comparator { value (ctx) { return cmpCast(this.left.value(ctx)) >= cmpCast(this.right.value(ctx)) } }
|
||||||
|
class NotEqual extends Comparator {
|
||||||
|
value (ctx) {
|
||||||
|
const l = cmpCast(this.left.value(ctx))
|
||||||
|
const r = cmpCast(this.right.value(ctx))
|
||||||
|
return l !== r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Equal extends Comparator {
|
||||||
|
value (ctx) {
|
||||||
|
const l = cmpCast(this.left.value(ctx))
|
||||||
|
const r = cmpCast(this.right.value(ctx))
|
||||||
|
return l === r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Combinator extends Operation {
|
||||||
|
static precedence = 0
|
||||||
|
get type () { return 'logical' }
|
||||||
|
}
|
||||||
|
class And extends Combinator { value (ctx) { return Boolean(this.left.value(ctx)) && Boolean(this.right.value(ctx)) } }
|
||||||
|
class Or extends Combinator { value (ctx) { return Boolean(this.left.value(ctx)) || Boolean(this.right.value(ctx)) } }
|
||||||
|
|
||||||
|
class ArrayNode extends Node {
|
||||||
|
static arity = null
|
||||||
|
static minParamCount () { return 0 }
|
||||||
|
static maxParamCount () { return Infinity }
|
||||||
|
constructor (...items) {
|
||||||
|
super()
|
||||||
|
this.items = items
|
||||||
|
}
|
||||||
|
|
||||||
|
value (ctx) { return this.items.map((i) => i.value(ctx)) }
|
||||||
|
dependencies (ctx) {
|
||||||
|
return uniq([].concat.apply([], this.items.map((i) => i.dependencies(ctx))))
|
||||||
|
}
|
||||||
|
|
||||||
|
get type () { return 'array' }
|
||||||
|
}
|
||||||
|
|
||||||
|
class FunctionNode extends Node {
|
||||||
|
static arity = null
|
||||||
|
static minParamCount () { return 0 }
|
||||||
|
static maxParamCount () { return Infinity }
|
||||||
|
constructor (...args) {
|
||||||
|
super()
|
||||||
|
this.args = args
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies (ctx) {
|
||||||
|
return uniq([].concat.apply([], this.args.map((a) => a.dependencies(ctx))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const FUNCTION_REGISTRY = Object.create(null)
|
||||||
|
function registerFunctionClass (name, cls) { FUNCTION_REGISTRY[name.toLowerCase()] = cls }
|
||||||
|
function getFunctionClass (name) { return FUNCTION_REGISTRY[name.toLowerCase()] }
|
||||||
|
|
||||||
|
function defineFunction (name, spec) {
|
||||||
|
const min = spec.min == null ? 0 : spec.min
|
||||||
|
const max = spec.max == null ? Infinity : spec.max
|
||||||
|
const call = spec.call
|
||||||
|
const type = spec.type || 'numeric'
|
||||||
|
class F extends FunctionNode {
|
||||||
|
static arity = (min === max) ? min : null
|
||||||
|
static minParamCount () { return min }
|
||||||
|
static maxParamCount () { return max }
|
||||||
|
value (ctx) {
|
||||||
|
const vals = this.args.map((a) => a.value(ctx))
|
||||||
|
return call(vals, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
get type () { return type }
|
||||||
|
}
|
||||||
|
registerFunctionClass(name, F)
|
||||||
|
return F
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenNumeric (vals) {
|
||||||
|
const out = [];
|
||||||
|
(function walk (v) {
|
||||||
|
if (Array.isArray(v)) v.forEach(walk)
|
||||||
|
else out.push(toNumber(v))
|
||||||
|
})(vals)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
defineFunction('abs', { min: 1, max: 1, call: (v) => Math.abs(toNumber(v[0])) })
|
||||||
|
defineFunction('min', { min: 1, call: (v) => Math.min.apply(null, flattenNumeric(v)) })
|
||||||
|
defineFunction('max', { min: 1, call: (v) => Math.max.apply(null, flattenNumeric(v)) })
|
||||||
|
defineFunction('sum', { min: 1, call: (v) => flattenNumeric(v).reduce((a, b) => a + b, 0) })
|
||||||
|
defineFunction('avg', {
|
||||||
|
min: 1,
|
||||||
|
call: (v) => {
|
||||||
|
const nums = flattenNumeric(v)
|
||||||
|
if (nums.length === 0) return 0
|
||||||
|
return nums.reduce((a, b) => a + b, 0) / nums.length
|
||||||
|
}
|
||||||
|
})
|
||||||
|
defineFunction('count', {
|
||||||
|
min: 1,
|
||||||
|
max: 1,
|
||||||
|
call: (v) => Array.isArray(v[0]) ? v[0].length : (v[0] == null ? 0 : 1)
|
||||||
|
})
|
||||||
|
defineFunction('round', {
|
||||||
|
min: 1,
|
||||||
|
max: 2,
|
||||||
|
call: (v) => {
|
||||||
|
const n = toNumber(v[0])
|
||||||
|
const p = v.length > 1 ? toNumber(v[1]) : 0
|
||||||
|
const f = Math.pow(10, p)
|
||||||
|
return Math.round(n * f) / f
|
||||||
|
}
|
||||||
|
})
|
||||||
|
defineFunction('roundup', {
|
||||||
|
min: 1,
|
||||||
|
max: 2,
|
||||||
|
call: (v) => {
|
||||||
|
const n = toNumber(v[0])
|
||||||
|
const p = v.length > 1 ? toNumber(v[1]) : 0
|
||||||
|
const f = Math.pow(10, p)
|
||||||
|
return (n < 0 ? -Math.floor(-n * f) : Math.ceil(n * f)) / f
|
||||||
|
}
|
||||||
|
})
|
||||||
|
defineFunction('rounddown', {
|
||||||
|
min: 1,
|
||||||
|
max: 2,
|
||||||
|
call: (v) => {
|
||||||
|
const n = toNumber(v[0])
|
||||||
|
const p = v.length > 1 ? toNumber(v[1]) : 0
|
||||||
|
const f = Math.pow(10, p)
|
||||||
|
return (n < 0 ? -Math.ceil(-n * f) : Math.floor(n * f)) / f
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const MATH_FNS = {
|
||||||
|
sqrt: Math.sqrt,
|
||||||
|
cbrt: Math.cbrt,
|
||||||
|
sin: Math.sin,
|
||||||
|
cos: Math.cos,
|
||||||
|
tan: Math.tan,
|
||||||
|
asin: Math.asin,
|
||||||
|
acos: Math.acos,
|
||||||
|
atan: Math.atan,
|
||||||
|
sinh: Math.sinh,
|
||||||
|
cosh: Math.cosh,
|
||||||
|
tanh: Math.tanh,
|
||||||
|
exp: Math.exp,
|
||||||
|
log: Math.log,
|
||||||
|
log2: Math.log2,
|
||||||
|
log10: Math.log10,
|
||||||
|
floor: Math.floor,
|
||||||
|
ceil: Math.ceil
|
||||||
|
}
|
||||||
|
Object.keys(MATH_FNS).forEach((name) => {
|
||||||
|
const fn = MATH_FNS[name]
|
||||||
|
defineFunction(name, { min: 1, max: 1, call: (v) => fn(toNumber(v[0])) })
|
||||||
|
})
|
||||||
|
defineFunction('atan2', { min: 2, max: 2, call: (v) => Math.atan2(toNumber(v[0]), toNumber(v[1])) })
|
||||||
|
defineFunction('hypot', { min: 1, call: (v) => Math.hypot.apply(null, flattenNumeric(v)) })
|
||||||
|
defineFunction('pow', { min: 2, max: 2, call: (v) => Math.pow(toNumber(v[0]), toNumber(v[1])) })
|
||||||
|
defineFunction('pi', { min: 0, max: 0, call: () => Math.PI })
|
||||||
|
defineFunction('e', { min: 0, max: 0, call: () => Math.E })
|
||||||
|
|
||||||
|
defineFunction('not', { min: 1, max: 1, type: 'logical', call: (v) => !v[0] })
|
||||||
|
defineFunction('and', { min: 1, type: 'logical', call: (v) => v.every((x) => Boolean(x)) })
|
||||||
|
defineFunction('or', { min: 1, type: 'logical', call: (v) => v.some((x) => Boolean(x)) })
|
||||||
|
defineFunction('xor', {
|
||||||
|
min: 2,
|
||||||
|
type: 'logical',
|
||||||
|
call: (v) => v.reduce((acc, x) => acc !== Boolean(x), false)
|
||||||
|
})
|
||||||
|
|
||||||
|
class IfFunction extends FunctionNode {
|
||||||
|
static arity = 3
|
||||||
|
static minParamCount () { return 3 }
|
||||||
|
static maxParamCount () { return 3 }
|
||||||
|
value (ctx) {
|
||||||
|
return this.args[0].value(ctx) ? this.args[1].value(ctx) : this.args[2].value(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
get type () { return this.args[1].type }
|
||||||
|
dependencies (ctx) {
|
||||||
|
try {
|
||||||
|
return this.args[0].value(ctx) ? this.args[1].dependencies(ctx) : this.args[2].dependencies(ctx)
|
||||||
|
} catch (e) {
|
||||||
|
return uniq([].concat.apply([], this.args.map((a) => a.dependencies(ctx))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registerFunctionClass('if', IfFunction)
|
||||||
|
|
||||||
|
defineFunction('length', { min: 1, max: 1, call: (v) => v[0] == null ? 0 : String(v[0]).length })
|
||||||
|
defineFunction('upcase', { min: 1, max: 1, type: 'string', call: (v) => String(v[0]).toUpperCase() })
|
||||||
|
defineFunction('downcase', { min: 1, max: 1, type: 'string', call: (v) => String(v[0]).toLowerCase() })
|
||||||
|
defineFunction('concat', { min: 1, type: 'string', call: (v) => v.map((x) => String(x)).join('') })
|
||||||
|
defineFunction('left', {
|
||||||
|
min: 2,
|
||||||
|
max: 2,
|
||||||
|
type: 'string',
|
||||||
|
call: (v) => String(v[0]).slice(0, toNumber(v[1]))
|
||||||
|
})
|
||||||
|
defineFunction('right', {
|
||||||
|
min: 2,
|
||||||
|
max: 2,
|
||||||
|
type: 'string',
|
||||||
|
call: (v) => {
|
||||||
|
const n = toNumber(v[1])
|
||||||
|
return n === 0 ? '' : String(v[0]).slice(-n)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
defineFunction('mid', {
|
||||||
|
min: 3,
|
||||||
|
max: 3,
|
||||||
|
type: 'string',
|
||||||
|
call: (v) => {
|
||||||
|
const s = String(v[0])
|
||||||
|
const start = toNumber(v[1]) - 1
|
||||||
|
const len = toNumber(v[2])
|
||||||
|
return s.slice(start, start + len)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
defineFunction('trim', { min: 1, max: 1, type: 'string', call: (v) => String(v[0]).trim() })
|
||||||
|
defineFunction('contains', {
|
||||||
|
min: 2,
|
||||||
|
max: 2,
|
||||||
|
type: 'logical',
|
||||||
|
call: (v) => {
|
||||||
|
if (Array.isArray(v[0])) return v[0].indexOf(v[1]) !== -1
|
||||||
|
return String(v[0]).indexOf(String(v[1])) !== -1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const OP_CLASSES = {
|
||||||
|
add: Addition,
|
||||||
|
subtract: Subtraction,
|
||||||
|
multiply: Multiplication,
|
||||||
|
divide: Division,
|
||||||
|
pow: Exponentiation,
|
||||||
|
negate: Negation,
|
||||||
|
mod: Modulo,
|
||||||
|
bitor: BitwiseOr,
|
||||||
|
bitand: BitwiseAnd,
|
||||||
|
bitshiftleft: BitwiseShiftLeft,
|
||||||
|
bitshiftright: BitwiseShiftRight,
|
||||||
|
lt: LessThan,
|
||||||
|
gt: GreaterThan,
|
||||||
|
le: LessThanOrEqual,
|
||||||
|
ge: GreaterThanOrEqual,
|
||||||
|
ne: NotEqual,
|
||||||
|
eq: Equal,
|
||||||
|
and: And,
|
||||||
|
or: Or
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOperationClass (cls) {
|
||||||
|
return cls && (cls.prototype instanceof Operation)
|
||||||
|
}
|
||||||
|
|
||||||
|
function parse (tokens) {
|
||||||
|
if (!tokens || tokens.length === 0) return new NilNode()
|
||||||
|
|
||||||
|
const output = []
|
||||||
|
const operations = []
|
||||||
|
const arities = []
|
||||||
|
const skip = new Set()
|
||||||
|
|
||||||
|
function consume (count) {
|
||||||
|
if (count == null) count = 2
|
||||||
|
const op = operations.pop()
|
||||||
|
if (!op) throw new ParseError('invalid statement')
|
||||||
|
|
||||||
|
const outputSize = output.length
|
||||||
|
const opMin = (typeof op.minParamCount === 'function') ? op.minParamCount() : null
|
||||||
|
const opMax = (typeof op.maxParamCount === 'function') ? op.maxParamCount() : null
|
||||||
|
|
||||||
|
const actualArgs = (op.arity != null) ? op.arity : count
|
||||||
|
const minSize = (op.arity != null) ? op.arity : (opMin != null ? opMin : count)
|
||||||
|
const maxSize = (op.arity != null) ? op.arity : (opMax != null ? opMax : count)
|
||||||
|
|
||||||
|
if (outputSize < minSize || actualArgs < minSize) {
|
||||||
|
throw new ParseError((op.name || 'operator') + ' has too few operands (given ' + outputSize + ', expected ' + minSize + ')')
|
||||||
|
}
|
||||||
|
if ((outputSize > maxSize && operations.length === 0) || actualArgs > maxSize) {
|
||||||
|
throw new ParseError((op.name || 'operator') + ' has too many operands (given ' + outputSize + ', expected ' + maxSize + ')')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (op === ArrayNode && output.length === 0) {
|
||||||
|
output.push(new ArrayNode())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outputSize < actualArgs) throw new ParseError('invalid statement')
|
||||||
|
const args = []
|
||||||
|
for (let i = 0; i < actualArgs; i++) args.unshift(output.pop())
|
||||||
|
|
||||||
|
// eslint-disable-next-line new-cap
|
||||||
|
output.push(new op(...args))
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOperator (token, lookahead) {
|
||||||
|
let cls = OP_CLASSES[token.value]
|
||||||
|
if (!cls) throw new ParseError('Unknown operator ' + token.value)
|
||||||
|
if (typeof cls.resolveClass === 'function') cls = cls.resolveClass(lookahead)
|
||||||
|
const prec = cls.precedence
|
||||||
|
if (cls.rightAssociative) {
|
||||||
|
while (operations.length > 0) {
|
||||||
|
const top = operations[operations.length - 1]
|
||||||
|
if (isOperationClass(top) && prec < top.precedence) consume()
|
||||||
|
else break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
while (operations.length > 0) {
|
||||||
|
const top = operations[operations.length - 1]
|
||||||
|
if (isOperationClass(top) && prec <= top.precedence) consume()
|
||||||
|
else break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
operations.push(cls)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFunction (token) {
|
||||||
|
const fn = getFunctionClass(token.value)
|
||||||
|
if (!fn) throw new ParseError('Undefined function ' + token.value)
|
||||||
|
arities.push(0)
|
||||||
|
operations.push(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleArray (token) {
|
||||||
|
if (token.value === 'array_start') {
|
||||||
|
operations.push(ArrayNode)
|
||||||
|
arities.push(0)
|
||||||
|
} else {
|
||||||
|
while (operations.length > 0 && operations[operations.length - 1] !== ArrayNode) consume()
|
||||||
|
if (operations[operations.length - 1] !== ArrayNode) throw new ParseError('Unbalanced bracket')
|
||||||
|
consume(arities.pop() + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleGrouping (token, lookahead, index) {
|
||||||
|
if (token.value === 'open') {
|
||||||
|
if (lookahead && lookahead.value === 'close') {
|
||||||
|
skip.add(index + 1)
|
||||||
|
arities.pop()
|
||||||
|
consume(0)
|
||||||
|
} else {
|
||||||
|
operations.push(Grouping)
|
||||||
|
}
|
||||||
|
} else if (token.value === 'close') {
|
||||||
|
while (operations.length > 0 && operations[operations.length - 1] !== Grouping) consume()
|
||||||
|
const lparen = operations.pop()
|
||||||
|
if (lparen !== Grouping) throw new ParseError('Unbalanced parenthesis')
|
||||||
|
const top = operations[operations.length - 1]
|
||||||
|
if (top && (top.prototype instanceof FunctionNode)) consume(arities.pop() + 1)
|
||||||
|
} else if (token.value === 'comma') {
|
||||||
|
if (arities.length === 0) throw new ParseError('invalid statement')
|
||||||
|
arities[arities.length - 1] += 1
|
||||||
|
while (operations.length > 0 &&
|
||||||
|
operations[operations.length - 1] !== Grouping &&
|
||||||
|
operations[operations.length - 1] !== ArrayNode) consume()
|
||||||
|
} else {
|
||||||
|
throw new ParseError('Unknown grouping token ' + token.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < tokens.length; i++) {
|
||||||
|
if (skip.has(i)) continue
|
||||||
|
const token = tokens[i]
|
||||||
|
const lookahead = tokens[i + 1]
|
||||||
|
|
||||||
|
switch (token.category) {
|
||||||
|
case 'numeric': output.push(new Numeric(token)); break
|
||||||
|
case 'logical': output.push(new Logical(token)); break
|
||||||
|
case 'string': output.push(new StringNode(token)); break
|
||||||
|
case 'identifier': output.push(new Identifier(token)); break
|
||||||
|
case 'null': output.push(new NilNode()); break
|
||||||
|
case 'operator':
|
||||||
|
case 'comparator':
|
||||||
|
case 'combinator':
|
||||||
|
handleOperator(token, lookahead); break
|
||||||
|
case 'function': handleFunction(token); break
|
||||||
|
case 'array': handleArray(token); break
|
||||||
|
case 'grouping': handleGrouping(token, lookahead, i); break
|
||||||
|
default:
|
||||||
|
throw new ParseError('Not implemented for tokens of category ' + token.category)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (operations.length > 0) consume()
|
||||||
|
|
||||||
|
if (output.length !== 1) throw new ParseError('Invalid statement')
|
||||||
|
return output[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
class Calculator {
|
||||||
|
constructor (options) {
|
||||||
|
options = options || {}
|
||||||
|
this.caseSensitive = options.caseSensitive === true
|
||||||
|
this.memory = Object.create(null)
|
||||||
|
this._astCache = Object.create(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
key (k) { return this.caseSensitive ? k : String(k).toLowerCase() }
|
||||||
|
|
||||||
|
normalize (data) {
|
||||||
|
const out = Object.create(null)
|
||||||
|
if (!data) return out
|
||||||
|
for (const k of Object.keys(data)) out[this.key(k)] = data[k]
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
store (data) { Object.assign(this.memory, this.normalize(data)); return this }
|
||||||
|
bind (data) { return this.store(data) }
|
||||||
|
clear () { this.memory = Object.create(null); return this }
|
||||||
|
|
||||||
|
ast (expression) {
|
||||||
|
const cached = this._astCache[expression]
|
||||||
|
if (cached) return cached
|
||||||
|
const node = parse(tokenize(expression, { caseSensitive: this.caseSensitive }))
|
||||||
|
this._astCache[expression] = node
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenize (expression) {
|
||||||
|
return tokenize(expression, { caseSensitive: this.caseSensitive })
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies (expression) {
|
||||||
|
return this.ast(expression).dependencies(this.memory)
|
||||||
|
}
|
||||||
|
|
||||||
|
evaluate (expression, data, onError) {
|
||||||
|
try {
|
||||||
|
return this.evaluateStrict(expression, data)
|
||||||
|
} catch (e) {
|
||||||
|
if (typeof data === 'function') return data(expression, e)
|
||||||
|
if (typeof onError === 'function') return onError(expression, e)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
evaluateStrict (expression, data) {
|
||||||
|
const ctx = Object.assign(Object.create(null), this.memory, this.normalize(data))
|
||||||
|
const node = this.ast(expression)
|
||||||
|
return node.value(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
addFunction (name, spec) {
|
||||||
|
defineFunction(name, {
|
||||||
|
min: spec.minArgs == null ? (spec.min == null ? 0 : spec.min) : spec.minArgs,
|
||||||
|
max: spec.maxArgs == null ? (spec.max == null ? Infinity : spec.max) : spec.maxArgs,
|
||||||
|
type: spec.type || 'numeric',
|
||||||
|
call: spec.call
|
||||||
|
})
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Calculator,
|
||||||
|
tokenize,
|
||||||
|
parse,
|
||||||
|
CalculatorError,
|
||||||
|
ParseError,
|
||||||
|
TokenizerError,
|
||||||
|
UnboundVariableError,
|
||||||
|
ZeroDivisionError,
|
||||||
|
ArgumentError
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addFunction (name, spec) { defineFunction(name, spec) }
|
||||||
@@ -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,11 +395,15 @@
|
|||||||
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"
|
||||||
:require-signing-reason="requireSigningReason"
|
:require-signing-reason="requireSigningReason"
|
||||||
|
:signature-text="signatureText"
|
||||||
|
:signature-src="signatureSrc"
|
||||||
:button-text="submitButtonText"
|
:button-text="submitButtonText"
|
||||||
:dry-run="dryRun"
|
:dry-run="dryRun"
|
||||||
:with-disclosure="withDisclosure"
|
:with-disclosure="withDisclosure"
|
||||||
@@ -407,6 +411,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 +423,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 +532,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 +546,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 +559,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 +604,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 +682,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 +707,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,
|
||||||
@@ -800,6 +834,16 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
signatureText: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
signatureSrc: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
previousSignatureValue: {
|
previousSignatureValue: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -914,10 +958,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 +984,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 +1036,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 +1065,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 +1122,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 +1139,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 +1203,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 +1273,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,6 +1311,22 @@ 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)
|
||||||
@@ -1280,6 +1366,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
|
||||||
|
|
||||||
@@ -1341,9 +1434,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 {
|
||||||
@@ -1393,7 +1486,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) => {
|
||||||
@@ -1426,7 +1520,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
|
||||||
@@ -1434,7 +1528,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1444,7 +1538,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')
|
||||||
@@ -1488,7 +1582,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) {
|
||||||
|
|||||||
@@ -68,34 +68,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted () {
|
async mounted () {
|
||||||
const {
|
const { Calculator } = await import('./calculator')
|
||||||
create,
|
|
||||||
evaluateDependencies,
|
|
||||||
addDependencies,
|
|
||||||
subtractDependencies,
|
|
||||||
divideDependencies,
|
|
||||||
multiplyDependencies,
|
|
||||||
powDependencies,
|
|
||||||
roundDependencies,
|
|
||||||
absDependencies,
|
|
||||||
sinDependencies,
|
|
||||||
tanDependencies,
|
|
||||||
cosDependencies
|
|
||||||
} = await import('mathjs')
|
|
||||||
|
|
||||||
this.math = create({
|
this.math = new Calculator()
|
||||||
evaluateDependencies,
|
|
||||||
addDependencies,
|
|
||||||
subtractDependencies,
|
|
||||||
divideDependencies,
|
|
||||||
multiplyDependencies,
|
|
||||||
powDependencies,
|
|
||||||
roundDependencies,
|
|
||||||
absDependencies,
|
|
||||||
sinDependencies,
|
|
||||||
tanDependencies,
|
|
||||||
cosDependencies
|
|
||||||
})
|
|
||||||
|
|
||||||
this.isMathLoaded = true
|
this.isMathLoaded = true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -497,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.')
|
||||||
@@ -555,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')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,34 +165,9 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.field.preferences?.formula) {
|
if (this.field.preferences?.formula) {
|
||||||
const {
|
const { Calculator } = await import('./calculator')
|
||||||
create,
|
|
||||||
evaluateDependencies,
|
|
||||||
addDependencies,
|
|
||||||
subtractDependencies,
|
|
||||||
divideDependencies,
|
|
||||||
multiplyDependencies,
|
|
||||||
powDependencies,
|
|
||||||
roundDependencies,
|
|
||||||
absDependencies,
|
|
||||||
sinDependencies,
|
|
||||||
tanDependencies,
|
|
||||||
cosDependencies
|
|
||||||
} = await import('mathjs')
|
|
||||||
|
|
||||||
this.math = create({
|
this.math = new Calculator()
|
||||||
evaluateDependencies,
|
|
||||||
addDependencies,
|
|
||||||
subtractDependencies,
|
|
||||||
divideDependencies,
|
|
||||||
multiplyDependencies,
|
|
||||||
powDependencies,
|
|
||||||
roundDependencies,
|
|
||||||
absDependencies,
|
|
||||||
sinDependencies,
|
|
||||||
tanDependencies,
|
|
||||||
cosDependencies
|
|
||||||
})
|
|
||||||
|
|
||||||
this.isMathLoaded = true
|
this.isMathLoaded = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,25 +398,41 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
touchAttachmentUuid: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
reason: {
|
reason: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
signatureText: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
signatureSrc: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
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,
|
||||||
isTextSignature: this.field.preferences?.format === 'typed' || this.field.preferences?.format === 'typed_or_upload',
|
isTouchAttachment: false,
|
||||||
|
isTextSignature: !this.signatureSrc && (!!this.signatureText || this.field.preferences?.format === 'typed' || this.field.preferences?.format === 'typed_or_upload'),
|
||||||
uploadImageInputKey: Math.random().toString()
|
uploadImageInputKey: Math.random().toString()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -448,14 +472,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 +487,20 @@ 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.signatureSrc) {
|
||||||
|
this.$nextTick(() => this.drawSignatureSrc())
|
||||||
|
} else if (this.isTextSignature) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.textInput) {
|
||||||
|
this.initTypedSignature()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
this.intersectionObserver?.disconnect()
|
this.intersectionObserver?.disconnect()
|
||||||
}
|
}
|
||||||
@@ -484,13 +508,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 +575,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 +685,73 @@ 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.signatureText) {
|
||||||
|
this.$refs.textInput.value = this.signatureText
|
||||||
|
} else 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 })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
drawSignatureSrc () {
|
||||||
|
const canvas = this.$refs.canvas
|
||||||
|
|
||||||
|
if (!canvas) return
|
||||||
|
|
||||||
|
const img = new Image()
|
||||||
|
|
||||||
|
img.crossOrigin = 'anonymous'
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
const context = canvas.getContext('2d')
|
||||||
|
|
||||||
|
const aspectRatio = img.width / img.height
|
||||||
|
const canvasWidth = canvas.width / scale
|
||||||
|
const canvasHeight = canvas.height / scale
|
||||||
|
|
||||||
|
let targetWidth = canvasWidth
|
||||||
|
let targetHeight = canvasHeight
|
||||||
|
|
||||||
|
if (canvasWidth / canvasHeight > aspectRatio) {
|
||||||
|
targetWidth = canvasHeight * aspectRatio
|
||||||
|
} else {
|
||||||
|
targetHeight = canvasWidth / aspectRatio
|
||||||
|
}
|
||||||
|
|
||||||
|
const x = (canvasWidth - targetWidth) / 2
|
||||||
|
const y = (canvasHeight - targetHeight) / 2
|
||||||
|
|
||||||
|
context.clearRect(0, 0, canvasWidth, canvasHeight)
|
||||||
|
context.drawImage(img, x, y, targetWidth, targetHeight)
|
||||||
|
|
||||||
|
this.isSignatureStarted = true
|
||||||
|
|
||||||
|
this.$emit('start')
|
||||||
|
}
|
||||||
|
|
||||||
|
img.onerror = () => {
|
||||||
|
console.error(`Failed to load signature image from ${this.signatureSrc}. The remote server must send an Access-Control-Allow-Origin header to allow CORS access.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
img.src = this.signatureSrc
|
||||||
|
},
|
||||||
drawImage (event) {
|
drawImage (event) {
|
||||||
this.remove()
|
this.remove()
|
||||||
this.clear()
|
this.clear()
|
||||||
@@ -694,6 +830,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 +846,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 +910,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 }
|
||||||
@@ -20,8 +20,11 @@
|
|||||||
{{ t('complete_all_required_fields_to_proceed_with_identity_verification') }}
|
{{ t('complete_all_required_fields_to_proceed_with_identity_verification') }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
|
<div v-if="errorMessage">
|
||||||
|
{{ errorMessage }}
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="isLoading"
|
v-else-if="isLoading"
|
||||||
class="w-full flex space-x-2 justify-center mb-2"
|
class="w-full flex space-x-2 justify-center mb-2"
|
||||||
>
|
>
|
||||||
<IconInnerShadowTop
|
<IconInnerShadowTop
|
||||||
@@ -96,6 +99,7 @@ export default {
|
|||||||
isMathLoaded: false,
|
isMathLoaded: false,
|
||||||
redirectUrl: '',
|
redirectUrl: '',
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
errorMessage: '',
|
||||||
eidEasyData: {}
|
eidEasyData: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -119,6 +123,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
|
||||||
},
|
},
|
||||||
@@ -161,6 +166,12 @@ export default {
|
|||||||
}).then(async (resp) => {
|
}).then(async (resp) => {
|
||||||
this.eidEasyData = await resp.json()
|
this.eidEasyData = await resp.json()
|
||||||
|
|
||||||
|
if (this.eidEasyData.error) {
|
||||||
|
this.errorMessage = this.eidEasyData.error
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (this.eidEasyData.check_completed) {
|
if (this.eidEasyData.check_completed) {
|
||||||
this.$emit('submit')
|
this.$emit('submit')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,135 +31,26 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<AreaTitle
|
||||||
v-if="field?.type && (isSelected || isNameFocus) && !isInMultiSelection"
|
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="selectedAreasRef.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="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
|
|
||||||
: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="[selectedAreasRef.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"
|
||||||
@@ -323,83 +215,22 @@
|
|||||||
@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', 'save', 't', 'isInlineSize', 'selectedAreasRef', 'isCmdKeyRef'],
|
inject: ['template', 'save', 't', 'isInlineSize', 'selectedAreasRef', 'isCmdKeyRef', 'getFieldTypeIndex'],
|
||||||
props: {
|
props: {
|
||||||
area: {
|
area: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -465,26 +296,24 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
isMobile: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
isSelectMode: {
|
isSelectMode: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: 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 }
|
||||||
@@ -573,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) {
|
||||||
@@ -597,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 = []
|
||||||
|
|
||||||
@@ -686,9 +505,6 @@ 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]
|
||||||
|
|
||||||
@@ -773,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.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)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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)
|
||||||
@@ -824,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()
|
||||||
|
|
||||||
@@ -908,9 +660,6 @@ 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
|
||||||
@@ -1105,7 +854,7 @@ export default {
|
|||||||
this.selectedAreasRef.value = [this.area]
|
this.selectedAreasRef.value = [this.area]
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$refs?.name?.blur()
|
this.$refs?.title?.$refs?.name?.blur()
|
||||||
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -20,8 +20,9 @@
|
|||||||
/>
|
/>
|
||||||
<DragPlaceholder
|
<DragPlaceholder
|
||||||
ref="dragPlaceholder"
|
ref="dragPlaceholder"
|
||||||
:field="fieldsDragFieldRef.value || toRaw(dragField)"
|
:field="customDragFieldRef.value || fieldsDragFieldRef.value || toRaw(dragField)"
|
||||||
:is-field="template.fields.includes(fieldsDragFieldRef.value)"
|
:is-field="template.fields.includes(fieldsDragFieldRef.value)"
|
||||||
|
:is-custom="!!customDragFieldRef.value"
|
||||||
:is-default="defaultFields.includes(toRaw(dragField))"
|
:is-default="defaultFields.includes(toRaw(dragField))"
|
||||||
:is-required="defaultRequiredFields.includes(toRaw(dragField))"
|
:is-required="defaultRequiredFields.includes(toRaw(dragField))"
|
||||||
/>
|
/>
|
||||||
@@ -80,7 +81,7 @@
|
|||||||
/>
|
/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<form
|
<form
|
||||||
v-if="withSignYourselfButton && template.submitters.length < 2"
|
v-if="withSignYourselfButton && undefinedSubmitters.length < 2 && (!template.variables_schema || Object.keys(template.variables_schema).length === 0)"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
data-turbo="false"
|
data-turbo="false"
|
||||||
class="inline"
|
class="inline"
|
||||||
@@ -273,6 +274,8 @@
|
|||||||
:accept-file-types="acceptFileTypes"
|
:accept-file-types="acceptFileTypes"
|
||||||
:with-replace-button="withUploadButton"
|
:with-replace-button="withUploadButton"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
|
:dynamic-documents="dynamicDocuments"
|
||||||
|
:with-dynamic-documents="withDynamicDocuments"
|
||||||
:template="template"
|
:template="template"
|
||||||
@scroll-to="scrollIntoDocument(item)"
|
@scroll-to="scrollIntoDocument(item)"
|
||||||
@remove="onDocumentRemove"
|
@remove="onDocumentRemove"
|
||||||
@@ -351,10 +354,27 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<template
|
<template
|
||||||
v-for="document in sortedDocuments"
|
v-for="(document, index) in sortedDocuments"
|
||||||
:key="document.uuid"
|
:key="document.uuid"
|
||||||
>
|
>
|
||||||
|
<DynamicDocument
|
||||||
|
v-if="template.schema[index].dynamic"
|
||||||
|
:ref="setDocumentRefs"
|
||||||
|
:editable="editable"
|
||||||
|
:document="dynamicDocuments.find((dynamicDocument) => dynamicDocument.uuid === document.uuid)"
|
||||||
|
:selected-submitter="selectedSubmitter"
|
||||||
|
:drag-field="dragField"
|
||||||
|
:draw-field="drawField"
|
||||||
|
:draw-field-type="drawFieldType"
|
||||||
|
:draw-custom-field="drawCustomField"
|
||||||
|
:draw-option="drawOption"
|
||||||
|
@update="onDynamicDocumentUpdate"
|
||||||
|
@draw="clearDrawField"
|
||||||
|
@add-custom-field="addCustomField"
|
||||||
|
@set-draw="[drawField = $event.field, drawOption = $event.option]"
|
||||||
|
/>
|
||||||
<Document
|
<Document
|
||||||
|
v-else
|
||||||
:ref="setDocumentRefs"
|
:ref="setDocumentRefs"
|
||||||
:areas-index="fieldAreasIndex[document.uuid]"
|
:areas-index="fieldAreasIndex[document.uuid]"
|
||||||
:selected-submitter="selectedSubmitter"
|
:selected-submitter="selectedSubmitter"
|
||||||
@@ -362,7 +382,7 @@
|
|||||||
:is-drag="!!dragField"
|
:is-drag="!!dragField"
|
||||||
:input-mode="inputMode"
|
:input-mode="inputMode"
|
||||||
:default-fields="[...defaultRequiredFields, ...defaultFields]"
|
:default-fields="[...defaultRequiredFields, ...defaultFields]"
|
||||||
:allow-draw="!onlyDefinedFields || drawField"
|
:allow-draw="!onlyDefinedFields || drawField || drawCustomField"
|
||||||
:with-signature-id="withSignatureId"
|
:with-signature-id="withSignatureId"
|
||||||
:with-prefillable="withPrefillable"
|
:with-prefillable="withPrefillable"
|
||||||
:data-document-uuid="document.uuid"
|
:data-document-uuid="document.uuid"
|
||||||
@@ -371,17 +391,20 @@
|
|||||||
:with-field-placeholder="withFieldPlaceholder"
|
:with-field-placeholder="withFieldPlaceholder"
|
||||||
:draw-field="drawField"
|
:draw-field="drawField"
|
||||||
:draw-field-type="drawFieldType"
|
:draw-field-type="drawFieldType"
|
||||||
|
:draw-custom-field="drawCustomField"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
|
:is-mobile="isMobile"
|
||||||
:base-url="baseUrl"
|
:base-url="baseUrl"
|
||||||
:with-fields-detection="withFieldsDetection"
|
:with-fields-detection="withFieldsDetection"
|
||||||
@draw="[onDraw($event), withSelectedFieldType ? '' : drawFieldType = '', showDrawField = false]"
|
@draw="[onDraw($event), withSelectedFieldType ? '' : drawFieldType = '', drawCustomField = null, showDrawField = false]"
|
||||||
@drop-field="onDropfield"
|
@drop-field="onDropfield"
|
||||||
@remove-area="removeArea"
|
@remove-area="removeArea"
|
||||||
@paste-field="pasteField"
|
@paste-field="pasteField"
|
||||||
@copy-field="copyField"
|
@copy-field="copyField"
|
||||||
|
@add-custom-field="addCustomField"
|
||||||
|
@set-draw="[drawField = $event.field, drawOption = $event.option]"
|
||||||
@copy-selected-areas="copySelectedAreas"
|
@copy-selected-areas="copySelectedAreas"
|
||||||
@delete-selected-areas="deleteSelectedAreas"
|
@delete-selected-areas="deleteSelectedAreas"
|
||||||
@align-selected-areas="alignSelectedAreas"
|
|
||||||
@autodetect-fields="detectFieldsForPage"
|
@autodetect-fields="detectFieldsForPage"
|
||||||
/>
|
/>
|
||||||
<DocumentControls
|
<DocumentControls
|
||||||
@@ -436,15 +459,15 @@
|
|||||||
v-if="withFieldsList && !isMobile"
|
v-if="withFieldsList && !isMobile"
|
||||||
id="fields_list_container"
|
id="fields_list_container"
|
||||||
class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block fields-list-container"
|
class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block fields-list-container"
|
||||||
:class="drawField ? 'overflow-hidden' : 'overflow-y-auto overflow-x-hidden'"
|
:class="drawField || drawCustomField ? 'overflow-hidden' : 'overflow-y-auto overflow-x-hidden'"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="showDrawField || drawField"
|
v-if="showDrawField || drawField || drawCustomField"
|
||||||
class="sticky inset-0 h-full z-20"
|
class="sticky inset-0 h-full z-20"
|
||||||
:style="{ backgroundColor }"
|
:style="{ backgroundColor }"
|
||||||
>
|
>
|
||||||
<div class="bg-base-200 rounded-lg p-5 text-center space-y-4 draw-field-container">
|
<div class="bg-base-200 rounded-lg p-5 text-center space-y-4 draw-field-container">
|
||||||
<p v-if="(drawField?.type || drawFieldType) === 'strikethrough'">
|
<p v-if="(drawField?.type || drawFieldType || drawCustomField?.type) === 'strikethrough'">
|
||||||
{{ t('draw_strikethrough_the_document') }}
|
{{ t('draw_strikethrough_the_document') }}
|
||||||
</p>
|
</p>
|
||||||
<p v-else>
|
<p v-else>
|
||||||
@@ -458,10 +481,10 @@
|
|||||||
{{ t('cancel') }}
|
{{ t('cancel') }}
|
||||||
</button>
|
</button>
|
||||||
<a
|
<a
|
||||||
v-if="!drawField && !drawOption && !['stamp', 'signature', 'initials', 'heading', 'strikethrough'].includes(drawField?.type || drawFieldType)"
|
v-if="!drawField && !drawOption && !['stamp', 'signature', 'initials', 'heading', 'strikethrough'].includes(drawField?.type || drawFieldType || drawCustomField?.type)"
|
||||||
href="#"
|
href="#"
|
||||||
class="link block mt-3 text-sm"
|
class="link block mt-3 text-sm"
|
||||||
@click.prevent="[addField(drawFieldType), drawField = null, drawOption = null, withSelectedFieldType ? '' : drawFieldType = '', showDrawField = false]"
|
@click.prevent="drawCustomField ? addCustomFieldWithoutDraw() : [addField(drawFieldType), drawField = null, drawOption = null, withSelectedFieldType ? '' : drawFieldType = '', showDrawField = false]"
|
||||||
>
|
>
|
||||||
{{ t('or_add_field_without_drawing') }}
|
{{ t('or_add_field_without_drawing') }}
|
||||||
</a>
|
</a>
|
||||||
@@ -477,6 +500,8 @@
|
|||||||
:with-help="withHelp"
|
:with-help="withHelp"
|
||||||
:default-submitters="defaultSubmitters"
|
:default-submitters="defaultSubmitters"
|
||||||
:draw-field-type="drawFieldType"
|
:draw-field-type="drawFieldType"
|
||||||
|
:custom-fields="customFields"
|
||||||
|
:with-custom-fields="withCustomFields"
|
||||||
:with-fields-search="withFieldsSearch"
|
:with-fields-search="withFieldsSearch"
|
||||||
:default-fields="[...defaultRequiredFields, ...defaultFields]"
|
:default-fields="[...defaultRequiredFields, ...defaultFields]"
|
||||||
:template="template"
|
:template="template"
|
||||||
@@ -491,13 +516,17 @@
|
|||||||
:show-tour-start-form="showTourStartForm"
|
:show-tour-start-form="showTourStartForm"
|
||||||
@add-field="addField"
|
@add-field="addField"
|
||||||
@set-draw="[drawField = $event.field, drawOption = $event.option]"
|
@set-draw="[drawField = $event.field, drawOption = $event.option]"
|
||||||
|
@remove-field="onRemoveField"
|
||||||
|
@remove-submitter="onRemoveSubmitter"
|
||||||
@select-submitter="selectedSubmitter = $event"
|
@select-submitter="selectedSubmitter = $event"
|
||||||
@set-draw-type="[drawFieldType = $event, showDrawField = true]"
|
@set-draw-type="[drawFieldType = $event, showDrawField = true]"
|
||||||
|
@set-draw-custom-field="[drawCustomField = $event, showDrawField = true]"
|
||||||
@set-drag="dragField = $event"
|
@set-drag="dragField = $event"
|
||||||
@set-drag-placeholder="$refs.dragPlaceholder.dragPlaceholder = $event"
|
@set-drag-placeholder="$refs.dragPlaceholder.dragPlaceholder = $event"
|
||||||
@change-submitter="selectedSubmitter = $event"
|
@change-submitter="selectedSubmitter = $event"
|
||||||
@drag-end="[dragField = null, $refs.dragPlaceholder.dragPlaceholder = null]"
|
@drag-end="[dragField = null, $refs.dragPlaceholder.dragPlaceholder = null]"
|
||||||
@scroll-to-area="scrollToArea"
|
@scroll-to-area="scrollToArea"
|
||||||
|
@rebuild-variables-schema="rebuildVariablesSchema"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -585,12 +614,13 @@ import MobileFields from './mobile_fields'
|
|||||||
import FieldSubmitter from './field_submitter'
|
import FieldSubmitter from './field_submitter'
|
||||||
import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments, IconDownload } from '@tabler/icons-vue'
|
import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments, IconDownload } from '@tabler/icons-vue'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
import { ref, computed, toRaw } from 'vue'
|
import { ref, computed, toRaw, defineAsyncComponent } from 'vue'
|
||||||
import * as i18n from './i18n'
|
import * as i18n from './i18n'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TemplateBuilder',
|
name: 'TemplateBuilder',
|
||||||
components: {
|
components: {
|
||||||
|
DynamicDocument: defineAsyncComponent(() => import(/* webpackChunkName: "dynamic-editor" */ './dynamic_document')),
|
||||||
Upload,
|
Upload,
|
||||||
DragPlaceholder,
|
DragPlaceholder,
|
||||||
Document,
|
Document,
|
||||||
@@ -632,12 +662,15 @@ export default {
|
|||||||
isPaymentConnected: this.isPaymentConnected,
|
isPaymentConnected: this.isPaymentConnected,
|
||||||
withFormula: this.withFormula,
|
withFormula: this.withFormula,
|
||||||
withConditions: this.withConditions,
|
withConditions: this.withConditions,
|
||||||
|
withCustomFields: this.withCustomFields,
|
||||||
isInlineSize: this.isInlineSize,
|
isInlineSize: this.isInlineSize,
|
||||||
defaultDrawFieldType: this.defaultDrawFieldType,
|
defaultDrawFieldType: this.defaultDrawFieldType,
|
||||||
selectedAreasRef: computed(() => this.selectedAreasRef),
|
selectedAreasRef: computed(() => this.selectedAreasRef),
|
||||||
fieldsDragFieldRef: computed(() => this.fieldsDragFieldRef),
|
fieldsDragFieldRef: computed(() => this.fieldsDragFieldRef),
|
||||||
|
customDragFieldRef: computed(() => this.customDragFieldRef),
|
||||||
isSelectModeRef: computed(() => this.isSelectModeRef),
|
isSelectModeRef: computed(() => this.isSelectModeRef),
|
||||||
isCmdKeyRef: computed(() => this.isCmdKeyRef)
|
isCmdKeyRef: computed(() => this.isCmdKeyRef),
|
||||||
|
getFieldTypeIndex: this.getFieldTypeIndex
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
@@ -705,6 +738,26 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
withCustomFields: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
withPrefillable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
dynamicDocuments: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
customFields: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
withAddPageButton: {
|
withAddPageButton: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -758,7 +811,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'
|
||||||
},
|
},
|
||||||
baseUrl: {
|
baseUrl: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -821,6 +874,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
withDynamicDocuments: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
withDocumentsList: {
|
withDocumentsList: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -902,6 +960,7 @@ export default {
|
|||||||
pendingFieldAttachmentUuids: [],
|
pendingFieldAttachmentUuids: [],
|
||||||
drawField: null,
|
drawField: null,
|
||||||
drawFieldType: null,
|
drawFieldType: null,
|
||||||
|
drawCustomField: null,
|
||||||
drawOption: null,
|
drawOption: null,
|
||||||
dragField: null,
|
dragField: null,
|
||||||
isDragFile: false
|
isDragFile: false
|
||||||
@@ -912,16 +971,20 @@ export default {
|
|||||||
isSelectModeRef: () => ref(false),
|
isSelectModeRef: () => ref(false),
|
||||||
isCmdKeyRef: () => ref(false),
|
isCmdKeyRef: () => ref(false),
|
||||||
fieldsDragFieldRef: () => ref(),
|
fieldsDragFieldRef: () => ref(),
|
||||||
|
customDragFieldRef: () => ref(),
|
||||||
selectedAreasRef: () => ref([]),
|
selectedAreasRef: () => ref([]),
|
||||||
language () {
|
language () {
|
||||||
return this.locale.split('-')[0].toLowerCase()
|
return this.locale.split('-')[0].toLowerCase()
|
||||||
},
|
},
|
||||||
withPrefillable () {
|
undefinedSubmitters () {
|
||||||
if (this.template.fields) {
|
return this.template.submitters.filter((submitter) => {
|
||||||
return this.template.fields.some((f) => f.prefillable)
|
return !submitter.invite_by_uuid &&
|
||||||
} else {
|
!submitter.optional_invite_by_uuid &&
|
||||||
return false
|
!submitter.invite_via_field_uuid &&
|
||||||
}
|
!submitter.linked_to_uuid &&
|
||||||
|
!submitter.is_requester &&
|
||||||
|
!submitter.email
|
||||||
|
})
|
||||||
},
|
},
|
||||||
isInlineSize () {
|
isInlineSize () {
|
||||||
return CSS.supports('container-type: size')
|
return CSS.supports('container-type: size')
|
||||||
@@ -969,6 +1032,18 @@ export default {
|
|||||||
|
|
||||||
return areas
|
return areas
|
||||||
},
|
},
|
||||||
|
fieldTypeIndexMap () {
|
||||||
|
const map = {}
|
||||||
|
const typeCounters = {}
|
||||||
|
|
||||||
|
this.template.fields.forEach((f) => {
|
||||||
|
typeCounters[f.type] ||= 0
|
||||||
|
map[f.uuid] = typeCounters[f.type]
|
||||||
|
typeCounters[f.type]++
|
||||||
|
})
|
||||||
|
|
||||||
|
return map
|
||||||
|
},
|
||||||
isAllRequiredFieldsAdded () {
|
isAllRequiredFieldsAdded () {
|
||||||
return !this.defaultRequiredFields?.some((f) => {
|
return !this.defaultRequiredFields?.some((f) => {
|
||||||
return !this.template.fields?.some((field) => field.name === f.name)
|
return !this.template.fields?.some((field) => field.name === f.name)
|
||||||
@@ -1062,6 +1137,59 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toRaw,
|
toRaw,
|
||||||
|
addCustomField (field) {
|
||||||
|
return this.$refs.fields.addCustomField(field)
|
||||||
|
},
|
||||||
|
getFieldTypeIndex (field) {
|
||||||
|
return this.fieldTypeIndexMap[field.uuid]
|
||||||
|
},
|
||||||
|
addCustomFieldWithoutDraw () {
|
||||||
|
const customField = this.drawCustomField
|
||||||
|
|
||||||
|
const field = JSON.parse(JSON.stringify(customField))
|
||||||
|
|
||||||
|
field.uuid = v4()
|
||||||
|
field.submitter_uuid = this.selectedSubmitter.uuid
|
||||||
|
field.areas = []
|
||||||
|
|
||||||
|
if (field.options?.length) {
|
||||||
|
field.options = field.options.map(opt => ({ ...opt, uuid: v4() }))
|
||||||
|
}
|
||||||
|
|
||||||
|
delete field.conditions
|
||||||
|
|
||||||
|
this.insertField(field)
|
||||||
|
this.save()
|
||||||
|
|
||||||
|
this.drawCustomField = null
|
||||||
|
this.showDrawField = false
|
||||||
|
},
|
||||||
|
onRemoveField (field) {
|
||||||
|
if (this.dynamicDocuments.length) {
|
||||||
|
field.areas?.forEach((area) => {
|
||||||
|
this.documentRefs.forEach((documentRef) => {
|
||||||
|
if (documentRef.isDynamic && documentRef.document.uuid === area.attachment_uuid) {
|
||||||
|
documentRef.removeArea(area)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRemoveSubmitter (submitter) {
|
||||||
|
if (this.dynamicDocuments.length) {
|
||||||
|
this.template.fields.forEach((field) => {
|
||||||
|
if (field.submitter_uuid === submitter.uuid) {
|
||||||
|
field.areas?.forEach((area) => {
|
||||||
|
this.documentRefs.forEach((documentRef) => {
|
||||||
|
if (documentRef.isDynamic && documentRef.document.uuid === area.attachment_uuid) {
|
||||||
|
documentRef.removeArea(area)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
toggleSelectMode () {
|
toggleSelectMode () {
|
||||||
this.isSelectModeRef.value = !this.isSelectModeRef.value
|
this.isSelectModeRef.value = !this.isSelectModeRef.value
|
||||||
|
|
||||||
@@ -1116,27 +1244,6 @@ export default {
|
|||||||
|
|
||||||
this.debouncedSave()
|
this.debouncedSave()
|
||||||
},
|
},
|
||||||
alignSelectedAreas (direction) {
|
|
||||||
const areas = this.selectedAreasRef.value
|
|
||||||
|
|
||||||
let targetValue
|
|
||||||
|
|
||||||
if (direction === 'left') {
|
|
||||||
targetValue = Math.min(...areas.map(a => a.x))
|
|
||||||
areas.forEach((area) => { area.x = targetValue })
|
|
||||||
} else if (direction === 'right') {
|
|
||||||
targetValue = Math.max(...areas.map(a => a.x + a.w))
|
|
||||||
areas.forEach((area) => { area.x = targetValue - area.w })
|
|
||||||
} else if (direction === 'top') {
|
|
||||||
targetValue = Math.min(...areas.map(a => a.y))
|
|
||||||
areas.forEach((area) => { area.y = targetValue })
|
|
||||||
} else if (direction === 'bottom') {
|
|
||||||
targetValue = Math.max(...areas.map(a => a.y + a.h))
|
|
||||||
areas.forEach((area) => { area.y = targetValue - area.h })
|
|
||||||
}
|
|
||||||
|
|
||||||
this.save()
|
|
||||||
},
|
|
||||||
download () {
|
download () {
|
||||||
this.isDownloading = true
|
this.isDownloading = true
|
||||||
|
|
||||||
@@ -1514,6 +1621,7 @@ export default {
|
|||||||
clearDrawField () {
|
clearDrawField () {
|
||||||
this.drawField = null
|
this.drawField = null
|
||||||
this.drawOption = null
|
this.drawOption = null
|
||||||
|
this.drawCustomField = null
|
||||||
this.showDrawField = false
|
this.showDrawField = false
|
||||||
|
|
||||||
if (!this.withSelectedFieldType) {
|
if (!this.withSelectedFieldType) {
|
||||||
@@ -1951,6 +2059,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDraw ({ area, isTooSmall }) {
|
onDraw ({ area, isTooSmall }) {
|
||||||
|
if (this.drawCustomField) {
|
||||||
|
return this.onDrawCustomField(area)
|
||||||
|
}
|
||||||
|
|
||||||
if (this.drawField) {
|
if (this.drawField) {
|
||||||
if (this.drawOption) {
|
if (this.drawOption) {
|
||||||
const areaWithoutOption = this.drawField.areas?.find((a) => !a.option_uuid)
|
const areaWithoutOption = this.drawField.areas?.find((a) => !a.option_uuid)
|
||||||
@@ -2016,7 +2128,9 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'checkbox' && !this.drawFieldType && (this.template.fields[this.template.fields.length - 1]?.type === 'checkbox' || area.w)) {
|
if (type === 'checkbox' && !this.drawFieldType && (this.template.fields[this.template.fields.length - 1]?.type === 'checkbox' || area.w)) {
|
||||||
const previousField = [...this.template.fields].reverse().find((f) => f.type === type)
|
const previousField = this.template.fields.findLast
|
||||||
|
? this.template.fields.findLast((f) => f.type === type)
|
||||||
|
: [...this.template.fields].reverse().find((f) => f.type === type)
|
||||||
const previousArea = previousField?.areas?.[previousField.areas.length - 1]
|
const previousArea = previousField?.areas?.[previousField.areas.length - 1]
|
||||||
|
|
||||||
if (previousArea || area.w) {
|
if (previousArea || area.w) {
|
||||||
@@ -2061,6 +2175,10 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.customDragFieldRef.value) {
|
||||||
|
return this.dropCustomField(area)
|
||||||
|
}
|
||||||
|
|
||||||
const field = this.fieldsDragFieldRef.value || {
|
const field = this.fieldsDragFieldRef.value || {
|
||||||
name: '',
|
name: '',
|
||||||
uuid: v4(),
|
uuid: v4(),
|
||||||
@@ -2153,10 +2271,132 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
dropCustomField (area) {
|
||||||
|
const customField = this.customDragFieldRef.value
|
||||||
|
const customAreas = customField.areas || []
|
||||||
|
|
||||||
|
const field = JSON.parse(JSON.stringify(customField))
|
||||||
|
|
||||||
|
field.uuid = v4()
|
||||||
|
field.submitter_uuid = this.selectedSubmitter.uuid
|
||||||
|
field.areas = []
|
||||||
|
|
||||||
|
if (field.options?.length) {
|
||||||
|
field.options = field.options.map(opt => ({ ...opt, uuid: v4() }))
|
||||||
|
}
|
||||||
|
|
||||||
|
delete field.conditions
|
||||||
|
|
||||||
|
const dropX = (area.x - 6) / area.maskW
|
||||||
|
const dropY = area.y / area.maskH
|
||||||
|
|
||||||
|
if (customAreas.length > 0) {
|
||||||
|
const refArea = customAreas[0]
|
||||||
|
|
||||||
|
customAreas.forEach((customArea) => {
|
||||||
|
const fieldArea = {
|
||||||
|
x: dropX + (customArea.x - refArea.x),
|
||||||
|
y: dropY + (customArea.y - refArea.y) - (customArea.h / 2),
|
||||||
|
w: customArea.w,
|
||||||
|
h: customArea.h,
|
||||||
|
page: area.page,
|
||||||
|
attachment_uuid: area.attachment_uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
if (customArea.cell_w) {
|
||||||
|
fieldArea.cell_w = customArea.cell_w
|
||||||
|
}
|
||||||
|
|
||||||
|
if (customArea.option_uuid && field.options?.length) {
|
||||||
|
const optionIndex = customField.options.findIndex(o => o.uuid === customArea.option_uuid)
|
||||||
|
if (optionIndex !== -1) {
|
||||||
|
fieldArea.option_uuid = field.options[optionIndex].uuid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
field.areas.push(fieldArea)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const fieldArea = {
|
||||||
|
x: dropX,
|
||||||
|
y: dropY,
|
||||||
|
page: area.page,
|
||||||
|
attachment_uuid: area.attachment_uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
this.assignDropAreaSize(fieldArea, field, area)
|
||||||
|
|
||||||
|
field.areas.push(fieldArea)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectedAreasRef.value = [field.areas[0]]
|
||||||
|
|
||||||
|
this.insertField(field)
|
||||||
|
this.save()
|
||||||
|
|
||||||
|
document.activeElement?.blur()
|
||||||
|
},
|
||||||
|
onDrawCustomField (area) {
|
||||||
|
const customField = this.drawCustomField
|
||||||
|
const customAreas = customField.areas || []
|
||||||
|
|
||||||
|
const field = JSON.parse(JSON.stringify(customField))
|
||||||
|
|
||||||
|
field.uuid = v4()
|
||||||
|
field.submitter_uuid = this.selectedSubmitter.uuid
|
||||||
|
field.areas = []
|
||||||
|
|
||||||
|
if (field.options?.length) {
|
||||||
|
field.options = field.options.map(opt => ({ ...opt, uuid: v4() }))
|
||||||
|
}
|
||||||
|
|
||||||
|
delete field.conditions
|
||||||
|
|
||||||
|
const isClick = area.w === 0 || area.h === 0
|
||||||
|
|
||||||
|
const firstArea = {
|
||||||
|
x: area.x,
|
||||||
|
y: area.y,
|
||||||
|
w: area.w || customAreas[0]?.w,
|
||||||
|
h: area.h || customAreas[0]?.h,
|
||||||
|
page: area.page,
|
||||||
|
attachment_uuid: area.attachment_uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!firstArea.w || !firstArea.h) {
|
||||||
|
if (customAreas[0]) {
|
||||||
|
firstArea.w = customAreas[0].w
|
||||||
|
firstArea.h = customAreas[0].h
|
||||||
|
} else {
|
||||||
|
this.setDefaultAreaSize(firstArea, field.type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isClick) {
|
||||||
|
firstArea.x -= firstArea.w / 2
|
||||||
|
firstArea.y -= firstArea.h / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.options?.length) {
|
||||||
|
firstArea.option_uuid = field.options[0].uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
field.areas.push(firstArea)
|
||||||
|
|
||||||
|
this.selectedAreasRef.value = [field.areas[0]]
|
||||||
|
|
||||||
|
this.insertField(field)
|
||||||
|
this.save()
|
||||||
|
|
||||||
|
this.drawCustomField = null
|
||||||
|
this.showDrawField = false
|
||||||
|
},
|
||||||
assignDropAreaSize (fieldArea, field, area) {
|
assignDropAreaSize (fieldArea, field, area) {
|
||||||
const fieldType = field.type || 'text'
|
const fieldType = field.type || 'text'
|
||||||
|
|
||||||
const previousField = [...this.template.fields].reverse().find((f) => f.type === fieldType)
|
const previousField = this.template.fields.findLast
|
||||||
|
? this.template.fields.findLast((f) => f.type === fieldType)
|
||||||
|
: [...this.template.fields].reverse().find((f) => f.type === fieldType)
|
||||||
|
|
||||||
let baseArea
|
let baseArea
|
||||||
|
|
||||||
@@ -2307,60 +2547,80 @@ export default {
|
|||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
|
removeAreasByAttachmentUuid (attachmentUuid) {
|
||||||
|
const removedFieldUuids = []
|
||||||
|
|
||||||
|
this.selectedAreasRef.value = this.selectedAreasRef.value.filter((area) => area.attachment_uuid !== attachmentUuid)
|
||||||
|
|
||||||
|
this.template.fields.forEach((field) => {
|
||||||
|
[...(field.areas || [])].forEach((area) => {
|
||||||
|
if (area.attachment_uuid === attachmentUuid) {
|
||||||
|
field.areas.splice(field.areas.indexOf(area), 1)
|
||||||
|
|
||||||
|
removedFieldUuids.push(field.uuid)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.template.fields = this.template.fields.reduce((acc, field) => {
|
||||||
|
if (removedFieldUuids.includes(field.uuid) && !field.areas?.length) {
|
||||||
|
this.removeFieldConditions(field)
|
||||||
|
} else {
|
||||||
|
acc.push(field)
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
},
|
||||||
onDocumentRemove (item) {
|
onDocumentRemove (item) {
|
||||||
if (window.confirm(this.t('are_you_sure_'))) {
|
if (window.confirm(this.t('are_you_sure_'))) {
|
||||||
this.template.schema.splice(this.template.schema.indexOf(item), 1)
|
this.template.schema.splice(this.template.schema.indexOf(item), 1)
|
||||||
|
|
||||||
const removedFieldUuids = []
|
this.removeAreasByAttachmentUuid(item.attachment_uuid)
|
||||||
|
|
||||||
this.template.fields.forEach((field) => {
|
|
||||||
[...(field.areas || [])].forEach((area) => {
|
|
||||||
if (area.attachment_uuid === item.attachment_uuid) {
|
|
||||||
field.areas.splice(field.areas.indexOf(area), 1)
|
|
||||||
|
|
||||||
removedFieldUuids.push(field.uuid)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
this.template.fields = this.template.fields.reduce((acc, f) => {
|
|
||||||
if (removedFieldUuids.includes(f.uuid) && !f.areas?.length) {
|
|
||||||
this.removeFieldConditions(f)
|
|
||||||
} else {
|
|
||||||
acc.push(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDocumentReplace (data) {
|
onDocumentReplace (data) {
|
||||||
const { replaceSchemaItem, schema, documents } = data
|
const { replaceSchemaItem, schema, documents } = data
|
||||||
|
const isReplacingDynamicDocument = !!replaceSchemaItem.dynamic
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
const { google_drive_file_id, ...cleanedReplaceSchemaItem } = replaceSchemaItem
|
const { google_drive_file_id, dynamic, ...cleanedReplaceSchemaItem } = replaceSchemaItem
|
||||||
|
|
||||||
this.template.schema.splice(this.template.schema.indexOf(replaceSchemaItem), 1, { ...cleanedReplaceSchemaItem, ...schema[0] })
|
this.template.schema.splice(this.template.schema.indexOf(replaceSchemaItem), 1, { ...cleanedReplaceSchemaItem, ...schema[0] })
|
||||||
this.template.documents.push(...documents)
|
this.template.documents.push(...documents)
|
||||||
|
|
||||||
|
if (isReplacingDynamicDocument) {
|
||||||
|
this.removeAreasByAttachmentUuid(replaceSchemaItem.attachment_uuid)
|
||||||
|
|
||||||
|
const dynamicDocumentIndex = this.dynamicDocuments.findIndex((doc) => doc.uuid === replaceSchemaItem.attachment_uuid)
|
||||||
|
|
||||||
|
if (dynamicDocumentIndex !== -1) {
|
||||||
|
this.dynamicDocuments.splice(dynamicDocumentIndex, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (data.fields) {
|
if (data.fields) {
|
||||||
this.template.fields = data.fields
|
this.template.fields = data.fields
|
||||||
|
|
||||||
const removedFieldUuids = []
|
if (isReplacingDynamicDocument) {
|
||||||
|
this.removeAreasByAttachmentUuid(replaceSchemaItem.attachment_uuid)
|
||||||
|
} else {
|
||||||
|
const removedFieldUuids = []
|
||||||
|
|
||||||
this.template.fields.forEach((field) => {
|
this.template.fields.forEach((field) => {
|
||||||
[...(field.areas || [])].forEach((area) => {
|
[...(field.areas || [])].forEach((area) => {
|
||||||
if (area.attachment_uuid === replaceSchemaItem.attachment_uuid) {
|
if (area.attachment_uuid === replaceSchemaItem.attachment_uuid) {
|
||||||
field.areas.splice(field.areas.indexOf(area), 1)
|
field.areas.splice(field.areas.indexOf(area), 1)
|
||||||
|
|
||||||
removedFieldUuids.push(field.uuid)
|
removedFieldUuids.push(field.uuid)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
this.template.fields =
|
this.template.fields =
|
||||||
this.template.fields.filter((f) => !removedFieldUuids.includes(f.uuid) || f.areas?.length)
|
this.template.fields.filter((f) => !removedFieldUuids.includes(f.uuid) || f.areas?.length)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.submitters) {
|
if (data.submitters) {
|
||||||
@@ -2371,13 +2631,15 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.template.fields.forEach((field) => {
|
if (!isReplacingDynamicDocument) {
|
||||||
(field.areas || []).forEach((area) => {
|
this.template.fields.forEach((field) => {
|
||||||
if (area.attachment_uuid === replaceSchemaItem.attachment_uuid) {
|
(field.areas || []).forEach((area) => {
|
||||||
area.attachment_uuid = schema[0].attachment_uuid
|
if (area.attachment_uuid === replaceSchemaItem.attachment_uuid) {
|
||||||
}
|
area.attachment_uuid = schema[0].attachment_uuid
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
|
||||||
if (this.onUpload) {
|
if (this.onUpload) {
|
||||||
this.onUpload(this.template)
|
this.onUpload(this.template)
|
||||||
@@ -2473,7 +2735,15 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
this.isSaving = true
|
this.isSaving = true
|
||||||
|
|
||||||
this.save().then(() => {
|
const dynamicDocumentRefs = this.documentRefs.filter((ref) => ref.isDynamic)
|
||||||
|
|
||||||
|
dynamicDocumentRefs.map((ref) => ref.update())
|
||||||
|
|
||||||
|
this.rebuildVariablesSchema({ disable: false })
|
||||||
|
|
||||||
|
const dynamicDocumentSaves = dynamicDocumentRefs.map((ref) => ref.saveBody())
|
||||||
|
|
||||||
|
Promise.all([this.save(), ...dynamicDocumentSaves]).then(() => {
|
||||||
window.Turbo.visit(`/templates/${this.template.id}`)
|
window.Turbo.visit(`/templates/${this.template.id}`)
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.isSaving = false
|
this.isSaving = false
|
||||||
@@ -2484,9 +2754,13 @@ export default {
|
|||||||
scrollToArea (area) {
|
scrollToArea (area) {
|
||||||
const documentRef = this.documentRefs.find((a) => a.document.uuid === area.attachment_uuid)
|
const documentRef = this.documentRefs.find((a) => a.document.uuid === area.attachment_uuid)
|
||||||
|
|
||||||
documentRef.scrollToArea(area)
|
if (documentRef.isDynamic) {
|
||||||
|
this.selectedAreasRef.value = []
|
||||||
|
} else {
|
||||||
|
this.selectedAreasRef.value = [area]
|
||||||
|
}
|
||||||
|
|
||||||
this.selectedAreasRef.value = [area]
|
documentRef.scrollToArea(area)
|
||||||
},
|
},
|
||||||
baseFetch (path, options = {}) {
|
baseFetch (path, options = {}) {
|
||||||
return fetch(this.baseUrl + path, {
|
return fetch(this.baseUrl + path, {
|
||||||
@@ -2712,7 +2986,8 @@ export default {
|
|||||||
name: this.template.name,
|
name: this.template.name,
|
||||||
schema: this.template.schema,
|
schema: this.template.schema,
|
||||||
submitters: this.template.submitters,
|
submitters: this.template.submitters,
|
||||||
fields: this.template.fields
|
fields: this.template.fields,
|
||||||
|
variables_schema: this.template.variables_schema
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
@@ -2721,6 +2996,44 @@ export default {
|
|||||||
this.onSave(this.template)
|
this.onSave(this.template)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
onDynamicDocumentUpdate () {
|
||||||
|
this.rebuildVariablesSchema()
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$el.closest('template-builder')) {
|
||||||
|
this.$el.closest('template-builder').dataset.dynamicDocuments = JSON.stringify(this.dynamicDocuments)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
|
rebuildVariablesSchema ({ disable = true } = {}) {
|
||||||
|
const dynamicDocumentRef = this.documentRefs.find((e) => e.isDynamic)
|
||||||
|
|
||||||
|
if (dynamicDocumentRef) {
|
||||||
|
const parsed = {}
|
||||||
|
|
||||||
|
this.documentRefs.forEach((ref) => {
|
||||||
|
if (ref.isDynamic) {
|
||||||
|
ref.updateVariablesSchema()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.dynamicDocuments.forEach((doc) => {
|
||||||
|
if (doc.variables_schema) {
|
||||||
|
dynamicDocumentRef.mergeSchemaProperties(parsed, doc.variables_schema)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!this.template.variables_schema) {
|
||||||
|
this.template.variables_schema = parsed
|
||||||
|
} else {
|
||||||
|
dynamicDocumentRef.syncVariablesSchema(this.template.variables_schema, parsed, { disable })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.template.variables_schema = {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -169,18 +179,13 @@ export default {
|
|||||||
type: Function,
|
type: Function,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
withClickSaveEvent: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
excludeFieldUuids: {
|
excludeFieldUuids: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['close', 'click-save'],
|
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)) : [{}]
|
||||||
@@ -227,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')
|
||||||
}
|
}
|
||||||
@@ -244,12 +251,7 @@ export default {
|
|||||||
delete this.item.conditions
|
delete this.item.conditions
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.withClickSaveEvent) {
|
this.$emit('save')
|
||||||
this.$emit('click-save')
|
|
||||||
} else {
|
|
||||||
this.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()
|
||||||
|
|||||||
@@ -1,546 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
v-if="!isShowFormulaModal && !isShowFontModal && !isShowConditionsModal && !isShowDescriptionModal"
|
|
||||||
ref="menu"
|
|
||||||
class="fixed z-50 p-1 bg-white shadow-lg rounded-lg border border-base-300 min-w-[170px] cursor-default"
|
|
||||||
:style="menuStyle"
|
|
||||||
@mousedown.stop
|
|
||||||
@pointerdown.stop
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
v-if="showRequired"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm cursor-pointer"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
:checked="isRequired"
|
|
||||||
type="checkbox"
|
|
||||||
class="toggle toggle-xs"
|
|
||||||
@change="handleToggleRequired($event.target.checked)"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<span>{{ t('required') }}</span>
|
|
||||||
</label>
|
|
||||||
<label
|
|
||||||
v-if="showReadOnly"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm cursor-pointer"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
:checked="isReadOnly"
|
|
||||||
type="checkbox"
|
|
||||||
class="toggle toggle-xs"
|
|
||||||
@change="handleToggleReadOnly($event.target.checked)"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<span>{{ t('read_only') }}</span>
|
|
||||||
</label>
|
|
||||||
<hr
|
|
||||||
v-if="(showRequired || showReadOnly) && (showFont || showDescription || showCondition || showFormula)"
|
|
||||||
class="my-1 border-base-300"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
v-if="showFont && !isMultiSelection"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="openFontModal"
|
|
||||||
>
|
|
||||||
<IconTypography class="w-4 h-4" />
|
|
||||||
<span>{{ t('font') }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="showDescription"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="openDescriptionModal"
|
|
||||||
>
|
|
||||||
<IconInfoCircle class="w-4 h-4" />
|
|
||||||
<span>{{ t('description') }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="showCondition && !isMultiSelection"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="openConditionModal"
|
|
||||||
>
|
|
||||||
<IconRouteAltLeft class="w-4 h-4" />
|
|
||||||
<span>{{ t('condition') }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="showFormula"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="openFormulaModal"
|
|
||||||
>
|
|
||||||
<IconMathFunction class="w-4 h-4" />
|
|
||||||
<span>{{ t('formula') }}</span>
|
|
||||||
</button>
|
|
||||||
<hr
|
|
||||||
v-if="((showFont && !isMultiSelection) || showDescription || (showCondition && !isMultiSelection) || showFormula) && (showCopy || showDelete || showPaste)"
|
|
||||||
class="my-1 border-base-300"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
v-if="isMultiSelection"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="$emit('align', 'left')"
|
|
||||||
>
|
|
||||||
<IconLayoutAlignLeft class="w-4 h-4" />
|
|
||||||
<span>{{ t('align_left') }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="isMultiSelection"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="$emit('align', 'right')"
|
|
||||||
>
|
|
||||||
<IconLayoutAlignRight class="w-4 h-4" />
|
|
||||||
<span>{{ t('align_right') }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="isMultiSelection"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="$emit('align', 'top')"
|
|
||||||
>
|
|
||||||
<IconLayoutAlignTop class="w-4 h-4" />
|
|
||||||
<span>{{ t('align_top') }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="isMultiSelection"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="$emit('align', 'bottom')"
|
|
||||||
>
|
|
||||||
<IconLayoutAlignBottom class="w-4 h-4" />
|
|
||||||
<span>{{ t('align_bottom') }}</span>
|
|
||||||
</button>
|
|
||||||
<hr
|
|
||||||
v-if="isMultiSelection && (showFont || showCondition)"
|
|
||||||
class="my-1 border-base-300"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
v-if="showFont && isMultiSelection"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="openFontModal"
|
|
||||||
>
|
|
||||||
<IconTypography class="w-4 h-4" />
|
|
||||||
<span>{{ t('font') }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="showCondition && isMultiSelection"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="openConditionModal"
|
|
||||||
>
|
|
||||||
<IconRouteAltLeft class="w-4 h-4" />
|
|
||||||
<span>{{ t('condition') }}</span>
|
|
||||||
</button>
|
|
||||||
<hr
|
|
||||||
v-if="isMultiSelection"
|
|
||||||
class="my-1 border-base-300"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
v-if="showCopy"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center justify-between text-sm"
|
|
||||||
@click.stop="$emit('copy')"
|
|
||||||
>
|
|
||||||
<span class="flex items-center space-x-2">
|
|
||||||
<IconCopy class="w-4 h-4" />
|
|
||||||
<span>{{ t('copy') }}</span>
|
|
||||||
</span>
|
|
||||||
<span class="text-xs text-base-content/60 ml-4">{{ isMac ? '⌘C' : 'Ctrl+C' }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="showDelete"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center justify-between text-sm text-red-600"
|
|
||||||
@click.stop="$emit('delete')"
|
|
||||||
>
|
|
||||||
<span class="flex items-center space-x-2">
|
|
||||||
<IconTrashX class="w-4 h-4" />
|
|
||||||
<span>{{ t('remove') }}</span>
|
|
||||||
</span>
|
|
||||||
<span class="text-xs text-base-content/60 ml-4">Del</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="showPaste"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center justify-between text-sm"
|
|
||||||
:class="!hasClipboardData ? 'opacity-50 cursor-not-allowed' : 'hover:bg-base-100'"
|
|
||||||
:disabled="!hasClipboardData"
|
|
||||||
@click.stop="!hasClipboardData ? null : $emit('paste')"
|
|
||||||
>
|
|
||||||
<span class="flex items-center space-x-2">
|
|
||||||
<IconClipboard class="w-4 h-4" />
|
|
||||||
<span>{{ t('paste') }}</span>
|
|
||||||
</span>
|
|
||||||
<span class="text-xs text-base-content/60 ml-4">{{ isMac ? '⌘V' : 'Ctrl+V' }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="showSelectFields"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center justify-between text-sm"
|
|
||||||
@click.stop="handleToggleSelectMode"
|
|
||||||
>
|
|
||||||
<span class="flex items-center space-x-2">
|
|
||||||
<IconClick
|
|
||||||
v-if="!isSelectModeRef.value"
|
|
||||||
class="w-4 h-4"
|
|
||||||
/>
|
|
||||||
<IconNewSection
|
|
||||||
v-else
|
|
||||||
class="w-4 h-4"
|
|
||||||
/>
|
|
||||||
<span>{{ isSelectModeRef.value ? t('draw_fields') : t('select_fields') }}</span>
|
|
||||||
</span>
|
|
||||||
<span class="text-xs text-base-content/60 ml-4">Tab</span>
|
|
||||||
</button>
|
|
||||||
<hr
|
|
||||||
v-if="showAutodetectFields"
|
|
||||||
class="my-1 border-base-300"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
v-if="showAutodetectFields"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="$emit('autodetect-fields')"
|
|
||||||
>
|
|
||||||
<IconSparkles class="w-4 h-4" />
|
|
||||||
<span>{{ t('autodetect_fields') }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<Teleport
|
|
||||||
v-if="isShowFormulaModal"
|
|
||||||
:to="modalContainerEl"
|
|
||||||
>
|
|
||||||
<FormulaModal
|
|
||||||
:field="field"
|
|
||||||
:editable="editable"
|
|
||||||
:build-default-name="buildDefaultName"
|
|
||||||
@close="closeModal"
|
|
||||||
/>
|
|
||||||
</Teleport>
|
|
||||||
<Teleport
|
|
||||||
v-if="isShowFontModal"
|
|
||||||
:to="modalContainerEl"
|
|
||||||
>
|
|
||||||
<FontModal
|
|
||||||
:field="multiSelectField || field"
|
|
||||||
:area="contextMenu.area"
|
|
||||||
:editable="editable"
|
|
||||||
:build-default-name="buildDefaultName"
|
|
||||||
:with-click-save-event="isMultiSelection"
|
|
||||||
@click-save="handleSaveMultiSelectFontModal"
|
|
||||||
@close="closeModal"
|
|
||||||
/>
|
|
||||||
</Teleport>
|
|
||||||
<Teleport
|
|
||||||
v-if="isShowConditionsModal"
|
|
||||||
:to="modalContainerEl"
|
|
||||||
>
|
|
||||||
<ConditionsModal
|
|
||||||
:item="multiSelectField || field"
|
|
||||||
:build-default-name="buildDefaultName"
|
|
||||||
:exclude-field-uuids="isMultiSelection ? selectedFields.map(f => f.uuid) : []"
|
|
||||||
:with-click-save-event="isMultiSelection"
|
|
||||||
@click-save="handleSaveMultiSelectConditionsModal"
|
|
||||||
@close="closeModal"
|
|
||||||
/>
|
|
||||||
</Teleport>
|
|
||||||
<Teleport
|
|
||||||
v-if="isShowDescriptionModal"
|
|
||||||
:to="modalContainerEl"
|
|
||||||
>
|
|
||||||
<DescriptionModal
|
|
||||||
:field="field"
|
|
||||||
:editable="editable"
|
|
||||||
:build-default-name="buildDefaultName"
|
|
||||||
@close="closeModal"
|
|
||||||
/>
|
|
||||||
</Teleport>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { IconCopy, IconClipboard, IconTrashX, IconTypography, IconInfoCircle, IconRouteAltLeft, IconMathFunction, IconClick, IconNewSection, IconLayoutAlignLeft, IconLayoutAlignRight, IconLayoutAlignTop, IconLayoutAlignBottom, IconSparkles } from '@tabler/icons-vue'
|
|
||||||
import FormulaModal from './formula_modal'
|
|
||||||
import FontModal from './font_modal'
|
|
||||||
import ConditionsModal from './conditions_modal'
|
|
||||||
import DescriptionModal from './description_modal'
|
|
||||||
import Field from './field'
|
|
||||||
import FieldType from './field_type.vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'ContextMenu',
|
|
||||||
components: {
|
|
||||||
IconCopy,
|
|
||||||
IconClipboard,
|
|
||||||
IconTrashX,
|
|
||||||
IconTypography,
|
|
||||||
IconInfoCircle,
|
|
||||||
IconRouteAltLeft,
|
|
||||||
IconMathFunction,
|
|
||||||
IconClick,
|
|
||||||
IconNewSection,
|
|
||||||
IconLayoutAlignLeft,
|
|
||||||
IconLayoutAlignRight,
|
|
||||||
IconLayoutAlignTop,
|
|
||||||
IconLayoutAlignBottom,
|
|
||||||
IconSparkles,
|
|
||||||
FormulaModal,
|
|
||||||
FontModal,
|
|
||||||
ConditionsModal,
|
|
||||||
DescriptionModal
|
|
||||||
},
|
|
||||||
inject: ['t', 'save', 'selectedAreasRef', 'isSelectModeRef'],
|
|
||||||
props: {
|
|
||||||
contextMenu: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
field: {
|
|
||||||
type: Object,
|
|
||||||
default: null
|
|
||||||
},
|
|
||||||
editable: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
isMultiSelection: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
selectedAreas: {
|
|
||||||
type: Array,
|
|
||||||
default: () => []
|
|
||||||
},
|
|
||||||
template: {
|
|
||||||
type: Object,
|
|
||||||
default: null
|
|
||||||
},
|
|
||||||
withFieldsDetection: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
emits: ['copy', 'paste', 'delete', 'close', 'align', 'autodetect-fields'],
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
isShowFormulaModal: false,
|
|
||||||
isShowFontModal: false,
|
|
||||||
isShowConditionsModal: false,
|
|
||||||
isShowDescriptionModal: false,
|
|
||||||
multiSelectField: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
fieldNames: FieldType.computed.fieldNames,
|
|
||||||
fieldLabels: FieldType.computed.fieldLabels,
|
|
||||||
modalContainerEl () {
|
|
||||||
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
|
||||||
},
|
|
||||||
selectedFields () {
|
|
||||||
if (!this.isMultiSelection) return []
|
|
||||||
|
|
||||||
return this.selectedAreasRef.value.map((area) => {
|
|
||||||
return this.template.fields.find((f) => f.areas?.includes(area))
|
|
||||||
}).filter(Boolean)
|
|
||||||
},
|
|
||||||
isMac () {
|
|
||||||
return (navigator.userAgentData?.platform || navigator.platform)?.toLowerCase()?.includes('mac')
|
|
||||||
},
|
|
||||||
hasClipboardData () {
|
|
||||||
try {
|
|
||||||
const clipboard = localStorage.getItem('docuseal_clipboard')
|
|
||||||
|
|
||||||
if (clipboard) {
|
|
||||||
const data = JSON.parse(clipboard)
|
|
||||||
|
|
||||||
return Date.now() - data.timestamp < 3600000
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
menuStyle () {
|
|
||||||
return {
|
|
||||||
left: this.contextMenu.x + 'px',
|
|
||||||
top: this.contextMenu.y + 'px'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
showCopy () {
|
|
||||||
return !!this.contextMenu.area || this.isMultiSelection
|
|
||||||
},
|
|
||||||
showPaste () {
|
|
||||||
return !this.contextMenu.area && !this.isMultiSelection
|
|
||||||
},
|
|
||||||
showDelete () {
|
|
||||||
return !!this.contextMenu.area || this.isMultiSelection
|
|
||||||
},
|
|
||||||
showFont () {
|
|
||||||
if (this.isMultiSelection) return true
|
|
||||||
if (!this.field) return false
|
|
||||||
|
|
||||||
return ['text', 'number', 'date', 'select', 'heading'].includes(this.field.type)
|
|
||||||
},
|
|
||||||
showDescription () {
|
|
||||||
if (!this.field) return false
|
|
||||||
|
|
||||||
return !['stamp', 'heading', 'strikethrough'].includes(this.field.type)
|
|
||||||
},
|
|
||||||
showCondition () {
|
|
||||||
if (this.isMultiSelection) return true
|
|
||||||
if (!this.field) return false
|
|
||||||
|
|
||||||
return !['stamp', 'heading'].includes(this.field.type)
|
|
||||||
},
|
|
||||||
showFormula () {
|
|
||||||
if (!this.field) return false
|
|
||||||
|
|
||||||
return this.field.type === 'number'
|
|
||||||
},
|
|
||||||
showRequired () {
|
|
||||||
if (!this.field) return false
|
|
||||||
|
|
||||||
return !['phone', 'stamp', 'verification', 'strikethrough', 'heading'].includes(this.field.type)
|
|
||||||
},
|
|
||||||
showReadOnly () {
|
|
||||||
if (!this.field) return false
|
|
||||||
|
|
||||||
return ['text', 'number'].includes(this.field.type)
|
|
||||||
},
|
|
||||||
isRequired () {
|
|
||||||
return this.field?.required || false
|
|
||||||
},
|
|
||||||
isReadOnly () {
|
|
||||||
return this.field?.readonly || false
|
|
||||||
},
|
|
||||||
showSelectFields () {
|
|
||||||
return !this.contextMenu.area && !this.isMultiSelection
|
|
||||||
},
|
|
||||||
showAutodetectFields () {
|
|
||||||
return this.withFieldsDetection && this.editable && !this.contextMenu.area && !this.isMultiSelection
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted () {
|
|
||||||
document.addEventListener('keydown', this.onKeyDown)
|
|
||||||
document.addEventListener('mousedown', this.handleClickOutside)
|
|
||||||
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.checkMenuPosition()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
beforeUnmount () {
|
|
||||||
document.removeEventListener('keydown', this.onKeyDown)
|
|
||||||
document.removeEventListener('mousedown', this.handleClickOutside)
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
buildDefaultName: Field.methods.buildDefaultName,
|
|
||||||
checkMenuPosition () {
|
|
||||||
if (this.$refs.menu) {
|
|
||||||
const rect = this.$refs.menu.getBoundingClientRect()
|
|
||||||
|
|
||||||
if (rect.bottom > window.innerHeight) {
|
|
||||||
this.contextMenu.y = this.contextMenu.y - rect.height
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rect.right > window.innerWidth) {
|
|
||||||
this.contextMenu.x = this.contextMenu.x - rect.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleToggleRequired (value) {
|
|
||||||
if (this.field) {
|
|
||||||
this.field.required = value
|
|
||||||
this.save()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleToggleReadOnly (value) {
|
|
||||||
if (this.field) {
|
|
||||||
this.field.readonly = value
|
|
||||||
this.save()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onKeyDown (event) {
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
event.preventDefault()
|
|
||||||
event.stopPropagation()
|
|
||||||
|
|
||||||
this.$emit('close')
|
|
||||||
} else if ((event.ctrlKey || event.metaKey) && event.key === 'v' && this.hasClipboardData) {
|
|
||||||
event.preventDefault()
|
|
||||||
event.stopPropagation()
|
|
||||||
|
|
||||||
this.$emit('paste')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleClickOutside (event) {
|
|
||||||
if (this.$refs.menu && !this.$refs.menu.contains(event.target)) {
|
|
||||||
this.$emit('close')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openFontModal () {
|
|
||||||
if (this.isMultiSelection) {
|
|
||||||
this.multiSelectField = {
|
|
||||||
name: this.t('fields_selected').replace('{count}', this.selectedFields.length),
|
|
||||||
preferences: {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const preferencesStrings = this.selectedFields.map((f) => JSON.stringify(f.preferences || {}))
|
|
||||||
|
|
||||||
if (preferencesStrings.every((s) => s === preferencesStrings[0])) {
|
|
||||||
this.multiSelectField.preferences = JSON.parse(preferencesStrings[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isShowFontModal = true
|
|
||||||
},
|
|
||||||
openDescriptionModal () {
|
|
||||||
this.isShowDescriptionModal = true
|
|
||||||
},
|
|
||||||
openConditionModal () {
|
|
||||||
if (this.isMultiSelection) {
|
|
||||||
this.multiSelectField = {
|
|
||||||
name: this.t('fields_selected').replace('{count}', this.selectedFields.length),
|
|
||||||
conditions: []
|
|
||||||
}
|
|
||||||
|
|
||||||
const conditionStrings = this.selectedFields.map((f) => JSON.stringify(f.conditions || []))
|
|
||||||
|
|
||||||
if (conditionStrings.every((s) => s === conditionStrings[0])) {
|
|
||||||
this.multiSelectField.conditions = JSON.parse(conditionStrings[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isShowConditionsModal = true
|
|
||||||
},
|
|
||||||
openFormulaModal () {
|
|
||||||
this.isShowFormulaModal = true
|
|
||||||
},
|
|
||||||
closeModal () {
|
|
||||||
this.isShowFormulaModal = false
|
|
||||||
this.isShowFontModal = false
|
|
||||||
this.isShowConditionsModal = false
|
|
||||||
this.isShowDescriptionModal = false
|
|
||||||
this.multiSelectField = null
|
|
||||||
|
|
||||||
this.$emit('close')
|
|
||||||
},
|
|
||||||
handleSaveMultiSelectFontModal () {
|
|
||||||
this.selectedFields.forEach((field) => {
|
|
||||||
field.preferences = { ...field.preferences, ...this.multiSelectField.preferences }
|
|
||||||
})
|
|
||||||
|
|
||||||
this.save()
|
|
||||||
|
|
||||||
this.closeModal()
|
|
||||||
},
|
|
||||||
handleSaveMultiSelectConditionsModal () {
|
|
||||||
this.selectedFields.forEach((field) => {
|
|
||||||
field.conditions = JSON.parse(JSON.stringify(this.multiSelectField.conditions))
|
|
||||||
})
|
|
||||||
|
|
||||||
this.save()
|
|
||||||
|
|
||||||
this.closeModal()
|
|
||||||
},
|
|
||||||
handleToggleSelectMode () {
|
|
||||||
this.isSelectModeRef.value = !this.isSelectModeRef.value
|
|
||||||
|
|
||||||
this.$emit('close')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -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,12 +13,14 @@
|
|||||||
: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"
|
||||||
@@ -28,9 +30,10 @@
|
|||||||
@remove-area="$emit('remove-area', $event)"
|
@remove-area="$emit('remove-area', $event)"
|
||||||
@copy-field="$emit('copy-field', $event)"
|
@copy-field="$emit('copy-field', $event)"
|
||||||
@paste-field="$emit('paste-field', { ...$event, attachment_uuid: document.uuid })"
|
@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')"
|
@copy-selected-areas="$emit('copy-selected-areas')"
|
||||||
@delete-selected-areas="$emit('delete-selected-areas')"
|
@delete-selected-areas="$emit('delete-selected-areas')"
|
||||||
@align-selected-areas="$emit('align-selected-areas', $event)"
|
|
||||||
@autodetect-fields="$emit('autodetect-fields', $event)"
|
@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 })"
|
||||||
@@ -96,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,
|
||||||
@@ -115,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,
|
||||||
@@ -131,7 +144,7 @@ export default {
|
|||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['draw', 'drop-field', 'remove-area', 'paste-field', 'copy-field', 'copy-selected-areas', 'delete-selected-areas', 'align-selected-areas', 'autodetect-fields'],
|
emits: ['draw', 'drop-field', 'remove-area', 'paste-field', 'copy-field', 'copy-selected-areas', 'delete-selected-areas', 'autodetect-fields', 'add-custom-field', 'set-draw'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
pageRefs: []
|
pageRefs: []
|
||||||
@@ -155,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`
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -173,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,785 @@
|
|||||||
|
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 {
|
||||||
|
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>
|
||||||
Loaded 100 of 310 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user