mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 14:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
No files matched your search
+6
-6
@@ -10,7 +10,7 @@ RUN apk --no-cache add fontforge wget && \
|
|||||||
wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && \
|
wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && \
|
||||||
wget https://github.com/impallari/DancingScript/raw/master/OFL.txt && \
|
wget https://github.com/impallari/DancingScript/raw/master/OFL.txt && \
|
||||||
wget -O /model.onnx "https://github.com/docusealco/fields-detection/releases/download/2.0.0/model_704_int8.onnx" && \
|
wget -O /model.onnx "https://github.com/docusealco/fields-detection/releases/download/2.0.0/model_704_int8.onnx" && \
|
||||||
wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" && \
|
wget -O pdfium-linux.tgz "https://github.com/bblanchon/pdfium-binaries/releases/latest/download/pdfium-linux-musl-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" && \
|
||||||
mkdir -p /pdfium-linux && \
|
mkdir -p /pdfium-linux && \
|
||||||
tar -xzf pdfium-linux.tgz -C /pdfium-linux
|
tar -xzf pdfium-linux.tgz -C /pdfium-linux
|
||||||
|
|
||||||
@@ -36,6 +36,7 @@ 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
|
||||||
@@ -46,13 +47,11 @@ 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
|
||||||
ENV VIPS_MAX_COORD=10000
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add --no-cache sqlite-dev libpq-dev vips-dev yaml-dev redis libheif vips-heif gcompat ttf-freefont onnxruntime && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
RUN apk add --no-cache libpq vips redis vips-heif ttf-freefont onnxruntime && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
||||||
|
|
||||||
RUN addgroup -g 2000 docuseal && adduser -u 2000 -G docuseal -s /bin/sh -D -h /home/docuseal docuseal
|
RUN addgroup -g 2000 docuseal && adduser -u 2000 -G docuseal -s /bin/sh -D -h /home/docuseal docuseal
|
||||||
|
|
||||||
@@ -70,7 +69,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 git && bundle install && apk del --no-cache build-base git && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf && ln -sf /usr/lib/libonnxruntime.so.1 $(ruby -e "print Dir[Gem::Specification.find_by_name('onnxruntime').gem_dir + '/vendor/*.so'].first")
|
RUN 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")
|
||||||
|
|
||||||
COPY --chown=docuseal:docuseal ./bin ./bin
|
COPY --chown=docuseal:docuseal ./bin ./bin
|
||||||
COPY --chown=docuseal:docuseal ./app ./app
|
COPY --chown=docuseal:docuseal ./app ./app
|
||||||
@@ -80,7 +79,7 @@ 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
|
||||||
@@ -97,6 +96,7 @@ RUN ln -s /fonts /app/public/fonts && \
|
|||||||
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"]
|
||||||
@@ -4,6 +4,7 @@ source 'https://rubygems.org'
|
|||||||
|
|
||||||
ruby '4.0.1'
|
ruby '4.0.1'
|
||||||
|
|
||||||
|
gem 'addressable'
|
||||||
gem 'arabic-letter-connector', require: false
|
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
|
||||||
@@ -28,7 +29,6 @@ gem 'oj'
|
|||||||
gem 'onnxruntime', require: false
|
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'
|
||||||
@@ -43,7 +43,7 @@ gem 'shakapacker'
|
|||||||
gem 'sidekiq'
|
gem 'sidekiq'
|
||||||
gem 'sqlite3', require: false
|
gem 'sqlite3', require: false
|
||||||
gem 'strip_attributes'
|
gem 'strip_attributes'
|
||||||
gem 'trilogy', github: 'trilogy-libraries/trilogy', glob: 'contrib/ruby/*.gemspec', require: false
|
gem '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'
|
||||||
|
|||||||
+85
-100
@@ -1,39 +1,31 @@
|
|||||||
GIT
|
|
||||||
remote: https://github.com/trilogy-libraries/trilogy.git
|
|
||||||
revision: 3963d490459df7a2b5bedb42424c3285f25eab22
|
|
||||||
glob: contrib/ruby/*.gemspec
|
|
||||||
specs:
|
|
||||||
trilogy (2.10.0)
|
|
||||||
bigdecimal
|
|
||||||
|
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
action_text-trix (2.1.16)
|
action_text-trix (2.1.17)
|
||||||
railties
|
railties
|
||||||
actioncable (8.1.2)
|
actioncable (8.1.3)
|
||||||
actionpack (= 8.1.2)
|
actionpack (= 8.1.3)
|
||||||
activesupport (= 8.1.2)
|
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.1.2)
|
actionmailbox (8.1.3)
|
||||||
actionpack (= 8.1.2)
|
actionpack (= 8.1.3)
|
||||||
activejob (= 8.1.2)
|
activejob (= 8.1.3)
|
||||||
activerecord (= 8.1.2)
|
activerecord (= 8.1.3)
|
||||||
activestorage (= 8.1.2)
|
activestorage (= 8.1.3)
|
||||||
activesupport (= 8.1.2)
|
activesupport (= 8.1.3)
|
||||||
mail (>= 2.8.0)
|
mail (>= 2.8.0)
|
||||||
actionmailer (8.1.2)
|
actionmailer (8.1.3)
|
||||||
actionpack (= 8.1.2)
|
actionpack (= 8.1.3)
|
||||||
actionview (= 8.1.2)
|
actionview (= 8.1.3)
|
||||||
activejob (= 8.1.2)
|
activejob (= 8.1.3)
|
||||||
activesupport (= 8.1.2)
|
activesupport (= 8.1.3)
|
||||||
mail (>= 2.8.0)
|
mail (>= 2.8.0)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
actionpack (8.1.2)
|
actionpack (8.1.3)
|
||||||
actionview (= 8.1.2)
|
actionview (= 8.1.3)
|
||||||
activesupport (= 8.1.2)
|
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)
|
||||||
@@ -41,36 +33,36 @@ 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.1.2)
|
actiontext (8.1.3)
|
||||||
action_text-trix (~> 2.1.15)
|
action_text-trix (~> 2.1.15)
|
||||||
actionpack (= 8.1.2)
|
actionpack (= 8.1.3)
|
||||||
activerecord (= 8.1.2)
|
activerecord (= 8.1.3)
|
||||||
activestorage (= 8.1.2)
|
activestorage (= 8.1.3)
|
||||||
activesupport (= 8.1.2)
|
activesupport (= 8.1.3)
|
||||||
globalid (>= 0.6.0)
|
globalid (>= 0.6.0)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (8.1.2)
|
actionview (8.1.3)
|
||||||
activesupport (= 8.1.2)
|
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.1.2)
|
activejob (8.1.3)
|
||||||
activesupport (= 8.1.2)
|
activesupport (= 8.1.3)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (8.1.2)
|
activemodel (8.1.3)
|
||||||
activesupport (= 8.1.2)
|
activesupport (= 8.1.3)
|
||||||
activerecord (8.1.2)
|
activerecord (8.1.3)
|
||||||
activemodel (= 8.1.2)
|
activemodel (= 8.1.3)
|
||||||
activesupport (= 8.1.2)
|
activesupport (= 8.1.3)
|
||||||
timeout (>= 0.4.0)
|
timeout (>= 0.4.0)
|
||||||
activestorage (8.1.2)
|
activestorage (8.1.3)
|
||||||
actionpack (= 8.1.2)
|
actionpack (= 8.1.3)
|
||||||
activejob (= 8.1.2)
|
activejob (= 8.1.3)
|
||||||
activerecord (= 8.1.2)
|
activerecord (= 8.1.3)
|
||||||
activesupport (= 8.1.2)
|
activesupport (= 8.1.3)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
activesupport (8.1.2)
|
activesupport (8.1.3)
|
||||||
base64
|
base64
|
||||||
bigdecimal
|
bigdecimal
|
||||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||||
@@ -116,7 +108,7 @@ GEM
|
|||||||
cgi
|
cgi
|
||||||
rexml
|
rexml
|
||||||
base64 (0.3.0)
|
base64 (0.3.0)
|
||||||
bcrypt (3.1.21)
|
bcrypt (3.1.22)
|
||||||
better_html (2.2.0)
|
better_html (2.2.0)
|
||||||
actionview (>= 7.0)
|
actionview (>= 7.0)
|
||||||
activesupport (>= 7.0)
|
activesupport (>= 7.0)
|
||||||
@@ -158,8 +150,6 @@ GEM
|
|||||||
bigdecimal
|
bigdecimal
|
||||||
rexml
|
rexml
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
css_parser (1.21.1)
|
|
||||||
addressable
|
|
||||||
csv (3.3.5)
|
csv (3.3.5)
|
||||||
csv-safe (3.3.1)
|
csv-safe (3.3.1)
|
||||||
csv (~> 3.0)
|
csv (~> 3.0)
|
||||||
@@ -171,16 +161,16 @@ GEM
|
|||||||
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.3.1)
|
devise-two-factor (6.4.0)
|
||||||
activesupport (>= 7.0, < 8.2)
|
activesupport (>= 7.2, < 8.2)
|
||||||
devise (>= 4.0, < 5.0)
|
devise (>= 4.0, < 6.0)
|
||||||
railties (>= 7.0, < 8.2)
|
railties (>= 7.2, < 8.2)
|
||||||
rotp (~> 6.0)
|
rotp (~> 6.0)
|
||||||
diff-lcs (1.6.2)
|
diff-lcs (1.6.2)
|
||||||
digest-crc (0.7.0)
|
digest-crc (0.7.0)
|
||||||
@@ -189,7 +179,7 @@ GEM
|
|||||||
dotenv (3.2.0)
|
dotenv (3.2.0)
|
||||||
drb (2.2.3)
|
drb (2.2.3)
|
||||||
email_typo (0.2.3)
|
email_typo (0.2.3)
|
||||||
erb (6.0.1)
|
erb (6.0.2)
|
||||||
erb_lint (0.9.0)
|
erb_lint (0.9.0)
|
||||||
activesupport
|
activesupport
|
||||||
better_html (>= 2.0.1)
|
better_html (>= 2.0.1)
|
||||||
@@ -272,19 +262,19 @@ GEM
|
|||||||
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.4.2)
|
|
||||||
i18n (1.14.8)
|
i18n (1.14.8)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
image_processing (1.14.0)
|
image_processing (1.14.0)
|
||||||
mini_magick (>= 4.9.5, < 6)
|
mini_magick (>= 4.9.5, < 6)
|
||||||
ruby-vips (>= 2.0.17, < 3)
|
ruby-vips (>= 2.0.17, < 3)
|
||||||
io-console (0.8.2)
|
io-console (0.8.2)
|
||||||
irb (1.16.0)
|
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.18.1)
|
json (2.19.2)
|
||||||
jwt (3.1.2)
|
jwt (3.1.2)
|
||||||
base64
|
base64
|
||||||
language_server-protocol (3.17.0.5)
|
language_server-protocol (3.17.0.5)
|
||||||
@@ -306,7 +296,7 @@ GEM
|
|||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
railties (>= 4)
|
railties (>= 4)
|
||||||
request_store (~> 1.0)
|
request_store (~> 1.0)
|
||||||
loofah (2.25.0)
|
loofah (2.25.1)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
mail (2.9.0)
|
mail (2.9.0)
|
||||||
@@ -321,13 +311,14 @@ GEM
|
|||||||
mini_magick (5.3.1)
|
mini_magick (5.3.1)
|
||||||
logger
|
logger
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
minitest (6.0.1)
|
minitest (6.0.2)
|
||||||
|
drb (~> 2.0)
|
||||||
prism (~> 1.5)
|
prism (~> 1.5)
|
||||||
msgpack (1.8.0)
|
msgpack (1.8.0)
|
||||||
multi_json (1.19.1)
|
multi_json (1.19.1)
|
||||||
net-http (0.9.1)
|
net-http (0.9.1)
|
||||||
uri (>= 0.11.1)
|
uri (>= 0.11.1)
|
||||||
net-imap (0.6.2)
|
net-imap (0.6.3)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
net-pop (0.1.2)
|
net-pop (0.1.2)
|
||||||
@@ -337,17 +328,17 @@ GEM
|
|||||||
net-smtp (0.5.1)
|
net-smtp (0.5.1)
|
||||||
net-protocol
|
net-protocol
|
||||||
nio4r (2.7.5)
|
nio4r (2.7.5)
|
||||||
nokogiri (1.19.1-aarch64-linux-gnu)
|
nokogiri (1.19.2-aarch64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.19.1-aarch64-linux-musl)
|
nokogiri (1.19.2-aarch64-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.19.1-arm64-darwin)
|
nokogiri (1.19.2-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.19.1-x86_64-linux-gnu)
|
nokogiri (1.19.2-x86_64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.19.1-x86_64-linux-musl)
|
nokogiri (1.19.2-x86_64-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
numo-narray-alt (0.9.13)
|
numo-narray-alt (0.10.3)
|
||||||
oj (3.16.13)
|
oj (3.16.13)
|
||||||
bigdecimal (>= 3.0)
|
bigdecimal (>= 3.0)
|
||||||
ostruct (>= 0.2)
|
ostruct (>= 0.2)
|
||||||
@@ -376,18 +367,10 @@ GEM
|
|||||||
pg (1.6.3-x86_64-linux-musl)
|
pg (1.6.3-x86_64-linux-musl)
|
||||||
pp (0.6.3)
|
pp (0.6.3)
|
||||||
prettyprint
|
prettyprint
|
||||||
premailer (1.27.0)
|
|
||||||
addressable
|
|
||||||
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.6.0)
|
pretender (0.6.0)
|
||||||
actionpack (>= 7.1)
|
actionpack (>= 7.1)
|
||||||
prettyprint (0.2.0)
|
prettyprint (0.2.0)
|
||||||
prism (1.8.0)
|
prism (1.9.0)
|
||||||
pry (0.16.0)
|
pry (0.16.0)
|
||||||
coderay (~> 1.1)
|
coderay (~> 1.1)
|
||||||
method_source (~> 1.0)
|
method_source (~> 1.0)
|
||||||
@@ -411,33 +394,33 @@ GEM
|
|||||||
rack (>= 1.3)
|
rack (>= 1.3)
|
||||||
rackup (2.3.1)
|
rackup (2.3.1)
|
||||||
rack (>= 3)
|
rack (>= 3)
|
||||||
rails (8.1.2)
|
rails (8.1.3)
|
||||||
actioncable (= 8.1.2)
|
actioncable (= 8.1.3)
|
||||||
actionmailbox (= 8.1.2)
|
actionmailbox (= 8.1.3)
|
||||||
actionmailer (= 8.1.2)
|
actionmailer (= 8.1.3)
|
||||||
actionpack (= 8.1.2)
|
actionpack (= 8.1.3)
|
||||||
actiontext (= 8.1.2)
|
actiontext (= 8.1.3)
|
||||||
actionview (= 8.1.2)
|
actionview (= 8.1.3)
|
||||||
activejob (= 8.1.2)
|
activejob (= 8.1.3)
|
||||||
activemodel (= 8.1.2)
|
activemodel (= 8.1.3)
|
||||||
activerecord (= 8.1.2)
|
activerecord (= 8.1.3)
|
||||||
activestorage (= 8.1.2)
|
activestorage (= 8.1.3)
|
||||||
activesupport (= 8.1.2)
|
activesupport (= 8.1.3)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 8.1.2)
|
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.1.0)
|
rails-i18n (8.1.0)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
railties (>= 8.0.0, < 9)
|
railties (>= 8.0.0, < 9)
|
||||||
railties (8.1.2)
|
railties (8.1.3)
|
||||||
actionpack (= 8.1.2)
|
actionpack (= 8.1.3)
|
||||||
activesupport (= 8.1.2)
|
activesupport (= 8.1.3)
|
||||||
irb (~> 1.13)
|
irb (~> 1.13)
|
||||||
rackup (>= 1.0.0)
|
rackup (>= 1.0.0)
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
@@ -446,7 +429,7 @@ GEM
|
|||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.3.1)
|
rake (13.3.1)
|
||||||
rdoc (7.1.0)
|
rdoc (7.2.0)
|
||||||
erb
|
erb
|
||||||
psych (>= 4.0.0)
|
psych (>= 4.0.0)
|
||||||
tsort
|
tsort
|
||||||
@@ -560,8 +543,10 @@ GEM
|
|||||||
activemodel (>= 3.0, < 9.0)
|
activemodel (>= 3.0, < 9.0)
|
||||||
strscan (3.1.7)
|
strscan (3.1.7)
|
||||||
thor (1.5.0)
|
thor (1.5.0)
|
||||||
timeout (0.6.0)
|
timeout (0.6.1)
|
||||||
trailblazer-option (0.1.2)
|
trailblazer-option (0.1.2)
|
||||||
|
trilogy (2.10.0)
|
||||||
|
bigdecimal
|
||||||
tsort (0.2.0)
|
tsort (0.2.0)
|
||||||
turbo-rails (2.0.21)
|
turbo-rails (2.0.21)
|
||||||
actionpack (>= 7.1.0)
|
actionpack (>= 7.1.0)
|
||||||
@@ -601,7 +586,7 @@ GEM
|
|||||||
xpath (3.2.0)
|
xpath (3.2.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
yaml (0.4.0)
|
yaml (0.4.0)
|
||||||
zeitwerk (2.7.4)
|
zeitwerk (2.7.5)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
aarch64-linux
|
aarch64-linux
|
||||||
@@ -611,6 +596,7 @@ PLATFORMS
|
|||||||
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
|
||||||
@@ -647,7 +633,6 @@ DEPENDENCIES
|
|||||||
onnxruntime
|
onnxruntime
|
||||||
pagy
|
pagy
|
||||||
pg
|
pg
|
||||||
premailer-rails
|
|
||||||
pretender
|
pretender
|
||||||
pry-rails
|
pry-rails
|
||||||
puma
|
puma
|
||||||
@@ -669,7 +654,7 @@ DEPENDENCIES
|
|||||||
simplecov
|
simplecov
|
||||||
sqlite3
|
sqlite3
|
||||||
strip_attributes
|
strip_attributes
|
||||||
trilogy!
|
trilogy
|
||||||
turbo-rails
|
turbo-rails
|
||||||
twitter_cldr
|
twitter_cldr
|
||||||
tzinfo-data
|
tzinfo-data
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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])
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ module Api
|
|||||||
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']
|
||||||
|
|
||||||
|
|||||||
@@ -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])
|
||||||
@@ -117,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]] }]]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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?
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ class SubmissionsExportController < ApplicationController
|
|||||||
load_and_authorize_resource :submission, through: :template, parent: false, only: :index
|
load_and_authorize_resource :submission, through: :template, parent: false, only: :index
|
||||||
|
|
||||||
def index
|
def index
|
||||||
submissions = @submissions.active
|
submissions = params[:archived] == 'true' ? @submissions.archived : @submissions.active
|
||||||
.preload(submitters: { documents_attachments: :blob,
|
|
||||||
attachments_attachments: :blob })
|
submissions = submissions.preload(submitters: { documents_attachments: :blob,
|
||||||
.order(id: :asc)
|
attachments_attachments: :blob })
|
||||||
|
.order(id: :asc)
|
||||||
|
|
||||||
submissions = Submissions.search(current_user, submissions, params[:q], search_values: true)
|
submissions = Submissions.search(current_user, submissions, params[:q], search_values: true)
|
||||||
submissions = Submissions::Filter.call(submissions, current_user, params)
|
submissions = Submissions::Filter.call(submissions, current_user, params)
|
||||||
|
|||||||
@@ -3,11 +3,12 @@
|
|||||||
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]
|
before_action :maybe_require_link_2fa, only: %i[show]
|
||||||
|
|
||||||
@@ -91,6 +92,12 @@ class SubmitFormController < ApplicationController
|
|||||||
|
|
||||||
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
|
||||||
@@ -108,8 +115,18 @@ 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)
|
||||||
|
|||||||
@@ -4,31 +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? ||
|
||||||
!Submitters::AuthorizedForForm.call(submitter, current_user,
|
@submitter.submission.template&.archived_at? ||
|
||||||
request)
|
!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
|
||||||
@@ -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}%"
|
||||||
|
|
||||||
|
|||||||
@@ -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] }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class TemplatesController < ApplicationController
|
|||||||
@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,10 +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_via_field_uuid
|
submitters: [%i[name uuid is_requester linked_to_uuid invite_via_field_uuid
|
||||||
invite_by_uuid optional_invite_by_uuid email order]],
|
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,
|
||||||
@@ -107,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,21 +9,23 @@ 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(
|
||||||
@@ -34,7 +36,7 @@ class TemplatesDebugController < ApplicationController
|
|||||||
@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
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class TemplatesPreviewController < ApplicationController
|
|||||||
@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
|
||||||
|
|||||||
@@ -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?
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -155,12 +155,16 @@ 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 || '[]')),
|
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',
|
||||||
@@ -174,6 +178,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
|||||||
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,
|
||||||
|
|||||||
@@ -36,15 +36,11 @@ function loadTiptap () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class LinkTooltip {
|
class LinkTooltip {
|
||||||
constructor (container, editor) {
|
constructor (container, editor, templateEl) {
|
||||||
this.container = container
|
this.container = container
|
||||||
this.editor = editor
|
this.editor = editor
|
||||||
|
|
||||||
const template = document.createElement('template')
|
this.tooltip = templateEl.content.firstElementChild.cloneNode(true)
|
||||||
|
|
||||||
template.innerHTML = container.dataset.linkTooltipHtml
|
|
||||||
|
|
||||||
this.tooltip = template.content.firstElementChild
|
|
||||||
|
|
||||||
this.input = this.tooltip.querySelector('input')
|
this.input = this.tooltip.querySelector('input')
|
||||||
this.saveButton = this.tooltip.querySelector('[data-role="link-save"]')
|
this.saveButton = this.tooltip.querySelector('[data-role="link-save"]')
|
||||||
@@ -140,7 +136,8 @@ export default actionable(targetable(class extends HTMLElement {
|
|||||||
'boldButton',
|
'boldButton',
|
||||||
'italicButton',
|
'italicButton',
|
||||||
'underlineButton',
|
'underlineButton',
|
||||||
'linkButton'
|
'linkButton',
|
||||||
|
'linkTooltipTemplate'
|
||||||
]
|
]
|
||||||
|
|
||||||
async connectedCallback () {
|
async connectedCallback () {
|
||||||
@@ -256,7 +253,7 @@ export default actionable(targetable(class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.linkTooltip = new LinkTooltip(this, this.editor)
|
this.linkTooltip = new LinkTooltip(this, this.editor, this.linkTooltipTemplate)
|
||||||
}
|
}
|
||||||
|
|
||||||
adjustShortcutsForPlatform () {
|
adjustShortcutsForPlatform () {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ safeRegisterElement('submission-form', class extends HTMLElement {
|
|||||||
expand: ['true', 'false'].includes(this.dataset.expand) ? this.dataset.expand === 'true' : null,
|
expand: ['true', 'false'].includes(this.dataset.expand) ? this.dataset.expand === 'true' : null,
|
||||||
withSignatureId: this.dataset.withSignatureId === 'true',
|
withSignatureId: this.dataset.withSignatureId === 'true',
|
||||||
requireSigningReason: this.dataset.requireSigningReason === 'true',
|
requireSigningReason: this.dataset.requireSigningReason === 'true',
|
||||||
withConfetti: this.dataset.withConfetti !== 'false',
|
withConfetti: this.dataset.withConfetti === 'true',
|
||||||
withFieldLabels: this.dataset.withFieldLabels !== 'false',
|
withFieldLabels: this.dataset.withFieldLabels !== 'false',
|
||||||
withDisclosure: this.dataset.withDisclosure === 'true',
|
withDisclosure: this.dataset.withDisclosure === 'true',
|
||||||
reuseSignature: this.dataset.reuseSignature !== 'false',
|
reuseSignature: this.dataset.reuseSignature !== 'false',
|
||||||
|
|||||||
@@ -359,7 +359,9 @@ export default {
|
|||||||
},
|
},
|
||||||
initTextInitial () {
|
initTextInitial () {
|
||||||
if (this.submitter.name) {
|
if (this.submitter.name) {
|
||||||
this.$refs.textInput.value = this.submitter.name.trim().split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0]?.toUpperCase() || '').join('')
|
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) {
|
if (this.$refs.textInput.value) {
|
||||||
|
|||||||
@@ -31,144 +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
|
|
||||||
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="save"
|
|
||||||
@scroll-to="[selectedAreasRef.value = [$event], $emit('scroll-to', $event)]"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="whitespace-normal"
|
|
||||||
>
|
|
||||||
The dots menu is retired in favor of the field context menu. Right-click the field to access field settings. Double-click the field to set a default value.
|
|
||||||
</div>
|
|
||||||
</ul>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<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"
|
||||||
@@ -333,85 +215,20 @@
|
|||||||
@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"
|
|
||||||
@save="save"
|
|
||||||
@close="isShowFormulaModal = false"
|
|
||||||
/>
|
|
||||||
</Teleport>
|
|
||||||
<Teleport
|
|
||||||
v-if="isShowFontModal"
|
|
||||||
:to="modalContainerEl"
|
|
||||||
>
|
|
||||||
<FontModal
|
|
||||||
:field="field"
|
|
||||||
:editable="editable && !defaultField"
|
|
||||||
:default-field="defaultField"
|
|
||||||
:build-default-name="buildDefaultName"
|
|
||||||
@save="save"
|
|
||||||
@close="isShowFontModal = false"
|
|
||||||
/>
|
|
||||||
</Teleport>
|
|
||||||
<Teleport
|
|
||||||
v-if="isShowConditionsModal"
|
|
||||||
:to="modalContainerEl"
|
|
||||||
>
|
|
||||||
<ConditionsModal
|
|
||||||
:item="field"
|
|
||||||
:build-default-name="buildDefaultName"
|
|
||||||
:default-field="defaultField"
|
|
||||||
@save="save"
|
|
||||||
@close="isShowConditionsModal = false"
|
|
||||||
/>
|
|
||||||
</Teleport>
|
|
||||||
<Teleport
|
|
||||||
v-if="isShowDescriptionModal"
|
|
||||||
:to="modalContainerEl"
|
|
||||||
>
|
|
||||||
<DescriptionModal
|
|
||||||
:field="field"
|
|
||||||
:editable="editable && !defaultField"
|
|
||||||
:default-field="defaultField"
|
|
||||||
:build-default-name="buildDefaultName"
|
|
||||||
@save="save"
|
|
||||||
@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', 'getFieldTypeIndex'],
|
inject: ['template', 'save', 't', 'isInlineSize', 'selectedAreasRef', 'isCmdKeyRef', 'getFieldTypeIndex'],
|
||||||
props: {
|
props: {
|
||||||
@@ -493,17 +310,10 @@ export default {
|
|||||||
emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to', 'add-custom-field'],
|
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 }
|
||||||
@@ -592,9 +402,6 @@ 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)
|
return this.buildDefaultName(this.field)
|
||||||
},
|
},
|
||||||
@@ -616,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 = []
|
||||||
|
|
||||||
@@ -705,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]
|
||||||
|
|
||||||
@@ -792,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)
|
||||||
@@ -843,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()
|
||||||
|
|
||||||
@@ -927,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
|
||||||
@@ -1124,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>
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
/>
|
/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<form
|
<form
|
||||||
v-if="withSignYourselfButton && undefinedSubmitters.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"
|
||||||
@@ -274,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"
|
||||||
@@ -352,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"
|
||||||
@@ -497,6 +516,8 @@
|
|||||||
: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-draw-custom-field="[drawCustomField = $event, showDrawField = true]"
|
||||||
@@ -505,6 +526,7 @@
|
|||||||
@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>
|
||||||
@@ -592,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,
|
||||||
@@ -720,6 +743,16 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
withPrefillable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
dynamicDocuments: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
customFields: {
|
customFields: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -778,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,
|
||||||
@@ -841,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,
|
||||||
@@ -948,13 +986,6 @@ export default {
|
|||||||
!submitter.email
|
!submitter.email
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
withPrefillable () {
|
|
||||||
if (this.template.fields) {
|
|
||||||
return this.template.fields.some((f) => f.prefillable)
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isInlineSize () {
|
isInlineSize () {
|
||||||
return CSS.supports('container-type: size')
|
return CSS.supports('container-type: size')
|
||||||
},
|
},
|
||||||
@@ -1133,6 +1164,32 @@ export default {
|
|||||||
this.drawCustomField = null
|
this.drawCustomField = null
|
||||||
this.showDrawField = false
|
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
|
||||||
|
|
||||||
@@ -2490,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) {
|
||||||
@@ -2554,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)
|
||||||
@@ -2656,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
|
||||||
@@ -2667,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, {
|
||||||
@@ -2895,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' }
|
||||||
@@ -2904,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 = {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -168,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`
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export default {
|
|||||||
acceptFileTypes: {
|
acceptFileTypes: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'image/*, application/pdf, application/zip'
|
default: 'image/*, application/pdf, application/zip, application/json'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['success', 'error', 'loading', 'click-google-drive'],
|
emits: ['success', 'error', 'loading', 'click-google-drive'],
|
||||||
@@ -146,7 +146,7 @@ export default {
|
|||||||
message () {
|
message () {
|
||||||
if (this.isLoading) {
|
if (this.isLoading) {
|
||||||
return this.t('uploading')
|
return this.t('uploading')
|
||||||
} else if (this.acceptFileTypes === 'image/*, application/pdf, application/zip') {
|
} else if (this.acceptFileTypes === 'image/*, application/pdf, application/zip, application/json') {
|
||||||
return this.title || this.t('add_pdf_documents_or_images')
|
return this.title || this.t('add_pdf_documents_or_images')
|
||||||
} else {
|
} else {
|
||||||
return this.title || this.t('add_documents_or_images')
|
return this.title || this.t('add_documents_or_images')
|
||||||
@@ -161,7 +161,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
upload: Upload.methods.upload,
|
upload: Upload.methods.upload,
|
||||||
onDropFiles (e) {
|
onDropFiles (e) {
|
||||||
if (this.acceptFileTypes !== 'image/*, application/pdf, application/zip' || [...e.dataTransfer.files].every((f) => f.type.match(/(?:image\/)|(?:application\/pdf)|(?:application\/zip)/))) {
|
if (this.acceptFileTypes !== 'image/*, application/pdf, application/zip, application/json' || [...e.dataTransfer.files].every((f) => f.type.match(/(?:image\/)|(?:application\/pdf)|(?:application\/zip)|(?:application\/json)/))) {
|
||||||
this.$refs.input.files = e.dataTransfer.files
|
this.$refs.input.files = e.dataTransfer.files
|
||||||
|
|
||||||
this.upload()
|
this.upload()
|
||||||
|
|||||||
@@ -0,0 +1,289 @@
|
|||||||
|
<template>
|
||||||
|
<span
|
||||||
|
class="items-center select-none cursor-pointer relative overflow-visible text-base-content/80 font-sans"
|
||||||
|
:class="[bgColorClass, iconOnlyField ? 'justify-center' : '']"
|
||||||
|
:draggable="editable"
|
||||||
|
:style="[nodeStyle]"
|
||||||
|
@mousedown="selectArea"
|
||||||
|
@dragstart="onDragStart"
|
||||||
|
@contextmenu.prevent.stop="onContextMenu"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="absolute inset-0 pointer-events-none border-solid"
|
||||||
|
:class="borderColorClass"
|
||||||
|
:style="{ borderWidth: (isSelected ? 1 : 0) + 'px' }"
|
||||||
|
/>
|
||||||
|
<component
|
||||||
|
:is="fieldIcons[field?.type || 'text']"
|
||||||
|
v-if="field && !field.default_value"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
:stroke-width="1.5"
|
||||||
|
:class="iconOnlyField ? 'shrink min-h-0 max-h-full max-w-6 opacity-70 m-auto p-0.5' : 'shrink min-h-0 max-h-full max-w-4 opacity-70 mx-0.5 pl-0.5'"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="field?.default_value"
|
||||||
|
class="text-xs overflow-hidden text-ellipsis whitespace-nowrap pr-1 font-normal pl-0.5"
|
||||||
|
>
|
||||||
|
<template v-if="field.default_value === '{{date}}'">
|
||||||
|
{{ t('signing_date') }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ field.default_value }}
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="field && !iconOnlyField"
|
||||||
|
class="text-xs overflow-hidden text-ellipsis whitespace-nowrap pr-1 opacity-70 font-normal pl-0.5"
|
||||||
|
>{{ displayLabel }}</span>
|
||||||
|
<span
|
||||||
|
class="absolute rounded-full bg-white border border-gray-400 shadow-md cursor-nwse-resize z-10"
|
||||||
|
:style="{ width: resizeHandleSize + 'px', height: resizeHandleSize + 'px', right: (-4 / zoom) + 'px', bottom: (-4 / zoom) + 'px' }"
|
||||||
|
@pointerdown.prevent.stop="onResizeStart"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import FieldArea from './area'
|
||||||
|
import FieldType from './field_type'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DynamicArea',
|
||||||
|
props: {
|
||||||
|
fieldUuid: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
areaUuid: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
template: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
nodeStyle: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
selectedAreasRef: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
getPos: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
editor: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
getZoom: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
onAreaContextMenu: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
onAreaResize: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
onAreaDragStart: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
t: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
findFieldArea: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
getFieldTypeIndex: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isResizing: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
fieldArea () {
|
||||||
|
return this.findFieldArea(this.areaUuid)
|
||||||
|
},
|
||||||
|
area () {
|
||||||
|
return this.fieldArea?.area
|
||||||
|
},
|
||||||
|
field () {
|
||||||
|
return this.fieldArea?.field
|
||||||
|
},
|
||||||
|
fieldIcons: FieldArea.computed.fieldIcons,
|
||||||
|
fieldNames: FieldArea.computed.fieldNames,
|
||||||
|
fieldLabels: FieldType.computed.fieldLabels,
|
||||||
|
borderColors () {
|
||||||
|
return [
|
||||||
|
'border-red-500/80',
|
||||||
|
'border-sky-500/80',
|
||||||
|
'border-emerald-500/80',
|
||||||
|
'border-yellow-300/80',
|
||||||
|
'border-purple-600/80',
|
||||||
|
'border-pink-500/80',
|
||||||
|
'border-cyan-500/80',
|
||||||
|
'border-orange-500/80',
|
||||||
|
'border-lime-500/80',
|
||||||
|
'border-indigo-500/80'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
bgColors () {
|
||||||
|
return [
|
||||||
|
'bg-red-100',
|
||||||
|
'bg-sky-100',
|
||||||
|
'bg-emerald-100',
|
||||||
|
'bg-yellow-100',
|
||||||
|
'bg-purple-100',
|
||||||
|
'bg-pink-100',
|
||||||
|
'bg-cyan-100',
|
||||||
|
'bg-orange-100',
|
||||||
|
'bg-lime-100',
|
||||||
|
'bg-indigo-100'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
isSelected () {
|
||||||
|
return this.selectedAreasRef.value.some((a) => a === this.area)
|
||||||
|
},
|
||||||
|
zoom () {
|
||||||
|
return this.getZoom()
|
||||||
|
},
|
||||||
|
submitterIndex () {
|
||||||
|
if (!this.field) return 0
|
||||||
|
|
||||||
|
const submitter = this.template.submitters.find((s) => s.uuid === this.field.submitter_uuid)
|
||||||
|
|
||||||
|
return submitter ? this.template.submitters.indexOf(submitter) : 0
|
||||||
|
},
|
||||||
|
borderColorClass () {
|
||||||
|
return this.borderColors[this.submitterIndex % this.borderColors.length]
|
||||||
|
},
|
||||||
|
bgColorClass () {
|
||||||
|
return this.bgColors[this.submitterIndex % this.bgColors.length]
|
||||||
|
},
|
||||||
|
resizeHandleSize () {
|
||||||
|
return this.zoom > 0 ? Math.round(10 / this.zoom) : 10
|
||||||
|
},
|
||||||
|
iconOnlyField () {
|
||||||
|
return ['radio', 'multiple', 'checkbox', 'initials'].includes(this.field?.type)
|
||||||
|
},
|
||||||
|
defaultName () {
|
||||||
|
if (!this.field) return 'text'
|
||||||
|
|
||||||
|
const typeIndex = this.getFieldTypeIndex(this.field)
|
||||||
|
|
||||||
|
return `${this.fieldLabels[this.field.type] || this.fieldNames[this.field.type] || this.field.type} ${typeIndex + 1}`
|
||||||
|
},
|
||||||
|
displayLabel () {
|
||||||
|
return this.field?.name || this.defaultName
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
selectArea () {
|
||||||
|
this.editor.commands.setNodeSelection(this.getPos())
|
||||||
|
},
|
||||||
|
onDragStart (e) {
|
||||||
|
if (this.isResizing) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const pos = this.getPos()
|
||||||
|
|
||||||
|
if (pos == null) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = this.$el
|
||||||
|
const rect = root.getBoundingClientRect()
|
||||||
|
const zoom = this.zoom || 1
|
||||||
|
const clone = root.cloneNode(true)
|
||||||
|
|
||||||
|
clone.querySelector('[class*="cursor-nwse-resize"]')?.remove()
|
||||||
|
clone.style.cssText = `position:fixed;top:-1000px;width:${rect.width / zoom}px;height:${rect.height / zoom}px;display:${root.style.display};vertical-align:${root.style.verticalAlign};zoom:${zoom}`
|
||||||
|
|
||||||
|
document.body.appendChild(clone)
|
||||||
|
|
||||||
|
e.dataTransfer.setDragImage(clone, e.offsetX, e.offsetY)
|
||||||
|
|
||||||
|
requestAnimationFrame(() => clone.remove())
|
||||||
|
|
||||||
|
e.dataTransfer.effectAllowed = 'move'
|
||||||
|
|
||||||
|
this.onAreaDragStart()
|
||||||
|
},
|
||||||
|
onContextMenu (e) {
|
||||||
|
this.selectArea()
|
||||||
|
this.onAreaContextMenu(this.area, e)
|
||||||
|
},
|
||||||
|
onResizeStart (e) {
|
||||||
|
if (!this.editable) return
|
||||||
|
|
||||||
|
this.isResizing = true
|
||||||
|
|
||||||
|
this.selectArea()
|
||||||
|
|
||||||
|
const handle = e.target
|
||||||
|
|
||||||
|
handle.setPointerCapture(e.pointerId)
|
||||||
|
|
||||||
|
const startX = e.clientX
|
||||||
|
const startY = e.clientY
|
||||||
|
const startWidth = this.$el.offsetWidth
|
||||||
|
const startHeight = this.$el.offsetHeight
|
||||||
|
|
||||||
|
const onResizeMove = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.nodeStyle.width = startWidth + (e.clientX - startX) / this.zoom + 'px'
|
||||||
|
this.nodeStyle.height = startHeight + (e.clientY - startY) / this.zoom + 'px'
|
||||||
|
|
||||||
|
this.onAreaResize(this.$el.getBoundingClientRect())
|
||||||
|
}
|
||||||
|
|
||||||
|
const onResizeEnd = () => {
|
||||||
|
if (!this.isResizing) return
|
||||||
|
|
||||||
|
this.isResizing = false
|
||||||
|
|
||||||
|
handle.removeEventListener('pointermove', onResizeMove)
|
||||||
|
handle.removeEventListener('pointerup', onResizeEnd)
|
||||||
|
|
||||||
|
const pos = this.getPos()
|
||||||
|
|
||||||
|
const tr = this.editor.view.state.tr.setNodeMarkup(pos, undefined, {
|
||||||
|
...this.editor.view.state.doc.nodeAt(pos)?.attrs,
|
||||||
|
width: this.nodeStyle.width,
|
||||||
|
height: this.nodeStyle.height
|
||||||
|
})
|
||||||
|
|
||||||
|
this.editor.view.dispatch(tr)
|
||||||
|
this.editor.commands.setNodeSelection(pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
handle.addEventListener('pointermove', onResizeMove)
|
||||||
|
handle.addEventListener('pointerup', onResizeEnd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,316 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="container"
|
||||||
|
class="relative"
|
||||||
|
style="container-type: inline-size;"
|
||||||
|
>
|
||||||
|
<div ref="shadow" />
|
||||||
|
<template
|
||||||
|
v-for="style in styles"
|
||||||
|
:key="style.innerText"
|
||||||
|
>
|
||||||
|
<Teleport
|
||||||
|
v-if="shadow"
|
||||||
|
:to="style.innerText.includes('@font-face {') ? 'head' : shadow"
|
||||||
|
>
|
||||||
|
<component :is="'style'">
|
||||||
|
{{ style.innerText }}
|
||||||
|
</component>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
<Teleport
|
||||||
|
v-if="shadow"
|
||||||
|
:to="shadow"
|
||||||
|
>
|
||||||
|
<DynamicSection
|
||||||
|
v-for="section in sections"
|
||||||
|
:ref="setSectionRefs"
|
||||||
|
:key="section.id"
|
||||||
|
:container="$refs.container"
|
||||||
|
:editable="editable"
|
||||||
|
:section="section"
|
||||||
|
:section-refs="sectionRefs"
|
||||||
|
:container-width="containerWidth"
|
||||||
|
:attachments-index="attachmentsIndex"
|
||||||
|
:selected-submitter="selectedSubmitter"
|
||||||
|
:drag-field="dragField"
|
||||||
|
:draw-field="drawField"
|
||||||
|
:draw-field-type="drawFieldType"
|
||||||
|
:draw-custom-field="drawCustomField"
|
||||||
|
:render-html-for-save-ref="renderHtmlForSaveRef"
|
||||||
|
:draw-option="drawOption"
|
||||||
|
:attachment-uuid="document.uuid"
|
||||||
|
@update="onSectionUpdate(section, $event)"
|
||||||
|
@draw="$emit('draw', $event)"
|
||||||
|
@add-custom-field="$emit('add-custom-field', $event)"
|
||||||
|
@set-draw="$emit('set-draw', $event)"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import DynamicSection from './dynamic_section.vue'
|
||||||
|
import { dynamicStylesheet, tiptapStylesheet } from './dynamic_editor.js'
|
||||||
|
import { buildVariablesSchema, mergeSchemaProperties } from './dynamic_variables_schema.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TemplateDynamicDocument',
|
||||||
|
components: {
|
||||||
|
DynamicSection
|
||||||
|
},
|
||||||
|
inject: ['baseFetch', 'template'],
|
||||||
|
props: {
|
||||||
|
document: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
selectedSubmitter: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
dragField: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
drawField: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
drawFieldType: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
drawCustomField: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
drawOption: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['update', 'draw', 'set-draw', 'add-custom-field'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
containerWidth: 1040,
|
||||||
|
isMounted: false,
|
||||||
|
sectionRefs: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
renderHtmlForSaveRef: () => ref(false),
|
||||||
|
isDynamic () {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
attachmentsIndex () {
|
||||||
|
return (this.document.attachments || []).reduce((acc, att) => {
|
||||||
|
acc[att.uuid] = att.url
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
bodyDom () {
|
||||||
|
return new DOMParser().parseFromString(this.document.body, 'text/html')
|
||||||
|
},
|
||||||
|
headDom () {
|
||||||
|
return new DOMParser().parseFromString(this.document.head, 'text/html')
|
||||||
|
},
|
||||||
|
sections () {
|
||||||
|
return this.bodyDom.querySelectorAll('section')
|
||||||
|
},
|
||||||
|
styles () {
|
||||||
|
return this.headDom.querySelectorAll('style')
|
||||||
|
},
|
||||||
|
shadow () {
|
||||||
|
if (this.isMounted) {
|
||||||
|
return this.$refs.shadow.attachShadow({ mode: 'open' })
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.isMounted = true
|
||||||
|
|
||||||
|
this.shadow.adoptedStyleSheets.push(dynamicStylesheet, tiptapStylesheet)
|
||||||
|
|
||||||
|
this.containerWidth = this.$refs.container.clientWidth
|
||||||
|
|
||||||
|
this.resizeObserver = new ResizeObserver(() => {
|
||||||
|
if (this.$refs.container) {
|
||||||
|
this.containerWidth = this.$refs.container.clientWidth
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.resizeObserver.observe(this.$refs.container)
|
||||||
|
|
||||||
|
window.addEventListener('beforeunload', this.onBeforeUnload)
|
||||||
|
},
|
||||||
|
beforeUnmount () {
|
||||||
|
window.removeEventListener('beforeunload', this.onBeforeUnload)
|
||||||
|
|
||||||
|
this.resizeObserver.unobserve(this.$refs.container)
|
||||||
|
},
|
||||||
|
beforeUpdate () {
|
||||||
|
this.sectionRefs = []
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
mergeSchemaProperties,
|
||||||
|
removeArea (area) {
|
||||||
|
this.sectionRefs.forEach((sectionRef) => {
|
||||||
|
const pos = sectionRef.findAreaNodePos(area.uuid)
|
||||||
|
|
||||||
|
if (pos !== -1) {
|
||||||
|
sectionRef.editor.chain().focus().deleteRange({ from: pos, to: pos + 1 }).run()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
setSectionRefs (ref) {
|
||||||
|
if (ref) {
|
||||||
|
this.sectionRefs.push(ref)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onBeforeUnload (event) {
|
||||||
|
if (this.saveTimer) {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
event.returnValue = ''
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scrollToArea (area) {
|
||||||
|
this.sectionRefs.forEach(({ editor }) => editor.commands.setNodeSelection(0))
|
||||||
|
|
||||||
|
this.sectionRefs.forEach(({ editor }) => {
|
||||||
|
const el = editor.view.dom.querySelector(`[data-area-uuid="${area.uuid}"]`)
|
||||||
|
|
||||||
|
if (el) {
|
||||||
|
editor.commands.setNodeSelection(editor.view.posAtDOM(el, 0))
|
||||||
|
|
||||||
|
el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onSectionUpdate (section, { editor }) {
|
||||||
|
clearTimeout(this.saveTimer)
|
||||||
|
|
||||||
|
this.saveTimer = setTimeout(async () => {
|
||||||
|
await this.updateSectionAndSave(section, editor)
|
||||||
|
|
||||||
|
delete this.saveTimer
|
||||||
|
}, 1000)
|
||||||
|
},
|
||||||
|
updateVariablesSchema () {
|
||||||
|
this.document.variables_schema = buildVariablesSchema(this.bodyDom.body)
|
||||||
|
},
|
||||||
|
updateSectionAndSave (section, editor) {
|
||||||
|
const target = this.bodyDom.getElementById(section.id)
|
||||||
|
|
||||||
|
if (target) {
|
||||||
|
target.innerHTML = this.getHtmlForSave(editor)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.document.body = this.bodyDom.body.innerHTML
|
||||||
|
|
||||||
|
this.updateVariablesSchema()
|
||||||
|
|
||||||
|
this.$emit('update', this.document)
|
||||||
|
|
||||||
|
return this.saveBody()
|
||||||
|
},
|
||||||
|
updateAndSave () {
|
||||||
|
this.update()
|
||||||
|
|
||||||
|
return this.saveBody()
|
||||||
|
},
|
||||||
|
update () {
|
||||||
|
clearTimeout(this.saveTimer)
|
||||||
|
|
||||||
|
delete this.saveTimer
|
||||||
|
|
||||||
|
this.sectionRefs.forEach(({ section, editor }) => {
|
||||||
|
const target = this.bodyDom.getElementById(section.id)
|
||||||
|
|
||||||
|
target.innerHTML = this.getHtmlForSave(editor)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.document.body = this.bodyDom.body.innerHTML
|
||||||
|
|
||||||
|
this.updateVariablesSchema()
|
||||||
|
|
||||||
|
this.$emit('update', this.document)
|
||||||
|
},
|
||||||
|
getHtmlForSave (editor) {
|
||||||
|
this.renderHtmlForSaveRef.value = true
|
||||||
|
|
||||||
|
const result = editor.getHTML()
|
||||||
|
|
||||||
|
this.renderHtmlForSaveRef.value = false
|
||||||
|
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
saveBody () {
|
||||||
|
clearTimeout(this.saveTimer)
|
||||||
|
|
||||||
|
delete this.saveTimer
|
||||||
|
|
||||||
|
return this.baseFetch(`/templates/${this.template.id}/dynamic_documents/${this.document.uuid}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({ body: this.bodyDom.body.innerHTML }),
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
syncVariablesSchema (existing, parsed, { disable = true } = {}) {
|
||||||
|
for (const key of Object.keys(parsed)) {
|
||||||
|
if (!existing[key]) {
|
||||||
|
existing[key] = parsed[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of Object.keys(existing)) {
|
||||||
|
if (!parsed[key]) {
|
||||||
|
if (disable) {
|
||||||
|
existing[key].disabled = true
|
||||||
|
} else {
|
||||||
|
delete existing[key]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delete existing[key].disabled
|
||||||
|
|
||||||
|
if (!existing[key].form_type) {
|
||||||
|
existing[key].type = parsed[key].type
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed[key].items) {
|
||||||
|
if (!existing[key].items) {
|
||||||
|
existing[key].items = parsed[key].items
|
||||||
|
} else if (existing[key].items.properties && parsed[key].items.properties) {
|
||||||
|
this.syncVariablesSchema(existing[key].items.properties, parsed[key].items.properties, { disable })
|
||||||
|
} else if (!existing[key].items.properties && !parsed[key].items.properties) {
|
||||||
|
existing[key].items.type = parsed[key].items.type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing[key].properties && parsed[key].properties) {
|
||||||
|
this.syncVariablesSchema(existing[key].properties, parsed[key].properties, { disable })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,789 @@
|
|||||||
|
import { Editor, Extension, Node, Mark } from '@tiptap/core'
|
||||||
|
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||||
|
import { Decoration, DecorationSet } from '@tiptap/pm/view'
|
||||||
|
import Document from '@tiptap/extension-document'
|
||||||
|
import Text from '@tiptap/extension-text'
|
||||||
|
import HardBreak from '@tiptap/extension-hard-break'
|
||||||
|
import History from '@tiptap/extension-history'
|
||||||
|
import Gapcursor from '@tiptap/extension-gapcursor'
|
||||||
|
import Dropcursor from '@tiptap/extension-dropcursor'
|
||||||
|
import { createApp, reactive } from 'vue'
|
||||||
|
import DynamicArea from './dynamic_area.vue'
|
||||||
|
import styles from './dynamic_styles.scss'
|
||||||
|
|
||||||
|
export const dynamicStylesheet = new CSSStyleSheet()
|
||||||
|
|
||||||
|
dynamicStylesheet.replaceSync(styles[0][1])
|
||||||
|
|
||||||
|
export const tiptapStylesheet = new CSSStyleSheet()
|
||||||
|
|
||||||
|
tiptapStylesheet.replaceSync(
|
||||||
|
`.ProseMirror {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror {
|
||||||
|
word-wrap: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
white-space: break-spaces;
|
||||||
|
-webkit-font-variant-ligatures: none;
|
||||||
|
font-variant-ligatures: none;
|
||||||
|
font-feature-settings: "liga" 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror [contenteditable="false"] {
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror [contenteditable="false"] [contenteditable="true"] {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.ProseMirror-separator {
|
||||||
|
display: inline !important;
|
||||||
|
border: none !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
width: 0 !important;
|
||||||
|
height: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-gapcursor {
|
||||||
|
display: none;
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-gapcursor:after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
width: 20px;
|
||||||
|
border-top: 1px solid black;
|
||||||
|
animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ProseMirror-cursor-blink {
|
||||||
|
to {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-hideselection *::selection {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-hideselection *::-moz-selection {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-hideselection * {
|
||||||
|
caret-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-focused .ProseMirror-gapcursor {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
dynamic-variable {
|
||||||
|
background-color: #fef3c7;
|
||||||
|
}`)
|
||||||
|
|
||||||
|
function collectDomAttrs (dom) {
|
||||||
|
const attrs = {}
|
||||||
|
|
||||||
|
for (let i = 0; i < dom.attributes.length; i++) {
|
||||||
|
attrs[dom.attributes[i].name] = dom.attributes[i].value
|
||||||
|
}
|
||||||
|
|
||||||
|
return { htmlAttrs: attrs }
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectSpanDomAttrs (dom) {
|
||||||
|
const result = collectDomAttrs(dom)
|
||||||
|
|
||||||
|
if (result.htmlAttrs.style) {
|
||||||
|
const temp = document.createElement('span')
|
||||||
|
|
||||||
|
temp.style.cssText = result.htmlAttrs.style
|
||||||
|
|
||||||
|
if (['bold', '700'].includes(temp.style.fontWeight)) {
|
||||||
|
temp.style.removeProperty('font-weight')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (temp.style.fontStyle === 'italic') {
|
||||||
|
temp.style.removeProperty('font-style')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (temp.style.textDecoration === 'underline') {
|
||||||
|
temp.style.removeProperty('text-decoration')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (temp.style.cssText) {
|
||||||
|
result.htmlAttrs.style = temp.style.cssText
|
||||||
|
} else {
|
||||||
|
delete result.htmlAttrs.style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBlockNode (name, tag, content) {
|
||||||
|
return Node.create({
|
||||||
|
name,
|
||||||
|
group: 'block',
|
||||||
|
content: content || 'block+',
|
||||||
|
addAttributes () {
|
||||||
|
return {
|
||||||
|
htmlAttrs: { default: {} }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag, getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return [tag, node.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomParagraph = Node.create({
|
||||||
|
name: 'paragraph',
|
||||||
|
group: 'block',
|
||||||
|
content: 'inline*',
|
||||||
|
addAttributes () {
|
||||||
|
return {
|
||||||
|
htmlAttrs: { default: {} }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'p', getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return ['p', node.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const CustomHeading = Node.create({
|
||||||
|
name: 'heading',
|
||||||
|
group: 'block',
|
||||||
|
content: 'inline*',
|
||||||
|
addAttributes () {
|
||||||
|
return {
|
||||||
|
htmlAttrs: { default: {} },
|
||||||
|
level: { default: 1 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [1, 2, 3, 4, 5, 6].map((level) => ({
|
||||||
|
tag: `h${level}`,
|
||||||
|
getAttrs: (dom) => ({ ...collectDomAttrs(dom), level })
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return [`h${node.attrs.level}`, node.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const SectionNode = createBlockNode('section', 'section')
|
||||||
|
const ArticleNode = createBlockNode('article', 'article')
|
||||||
|
const DivNode = createBlockNode('div', 'div')
|
||||||
|
const BlockquoteNode = createBlockNode('blockquote', 'blockquote')
|
||||||
|
const PreNode = createBlockNode('pre', 'pre')
|
||||||
|
const OrderedListNode = createBlockNode('orderedList', 'ol', '(listItem | block)+')
|
||||||
|
const BulletListNode = createBlockNode('bulletList', 'ul', '(listItem | block)+')
|
||||||
|
|
||||||
|
const ListItemNode = Node.create({
|
||||||
|
name: 'listItem',
|
||||||
|
content: 'block+',
|
||||||
|
addAttributes () {
|
||||||
|
return {
|
||||||
|
htmlAttrs: { default: {} }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'li', getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return ['li', node.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const TableNode = Node.create({
|
||||||
|
name: 'table',
|
||||||
|
group: 'block',
|
||||||
|
content: '(colgroup | tableHead | tableBody | tableRow)+',
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'table', getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return ['table', node.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const TableHead = Node.create({
|
||||||
|
name: 'tableHead',
|
||||||
|
content: 'tableRow+',
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'thead', getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return ['thead', node.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const TableBody = Node.create({
|
||||||
|
name: 'tableBody',
|
||||||
|
content: 'tableRow+',
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'tbody', getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return ['tbody', node.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const TableRow = Node.create({
|
||||||
|
name: 'tableRow',
|
||||||
|
content: '(tableCell | tableHeader)+',
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'tr', getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return ['tr', node.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const TableCell = Node.create({
|
||||||
|
name: 'tableCell',
|
||||||
|
content: 'block*',
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'td', getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return ['td', node.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const TableHeader = Node.create({
|
||||||
|
name: 'tableHeader',
|
||||||
|
content: 'block*',
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'th', getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return ['th', node.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const ImageNode = Node.create({
|
||||||
|
name: 'image',
|
||||||
|
inline: true,
|
||||||
|
group: 'inline',
|
||||||
|
draggable: true,
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'img', getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return ['img', node.attrs.htmlAttrs]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const ColGroupNode = Node.create({
|
||||||
|
name: 'colgroup',
|
||||||
|
group: 'block',
|
||||||
|
content: 'col*',
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'colgroup', getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return ['colgroup', node.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const ColNode = Node.create({
|
||||||
|
name: 'col',
|
||||||
|
group: 'block',
|
||||||
|
atom: true,
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'col', getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return ['col', node.attrs.htmlAttrs]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const CustomBold = Mark.create({
|
||||||
|
name: 'bold',
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'strong' }, { tag: 'b' }, { style: 'font-weight=bold' }, { style: 'font-weight=700' }]
|
||||||
|
},
|
||||||
|
renderHTML () {
|
||||||
|
return ['strong', 0]
|
||||||
|
},
|
||||||
|
addCommands () {
|
||||||
|
return {
|
||||||
|
toggleBold: () => ({ commands }) => commands.toggleMark(this.name)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addKeyboardShortcuts () {
|
||||||
|
return {
|
||||||
|
'Mod-b': () => this.editor.commands.toggleBold()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const CustomItalic = Mark.create({
|
||||||
|
name: 'italic',
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'em' }, { tag: 'i' }, { style: 'font-style=italic' }]
|
||||||
|
},
|
||||||
|
renderHTML () {
|
||||||
|
return ['em', 0]
|
||||||
|
},
|
||||||
|
addCommands () {
|
||||||
|
return {
|
||||||
|
toggleItalic: () => ({ commands }) => commands.toggleMark(this.name)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addKeyboardShortcuts () {
|
||||||
|
return {
|
||||||
|
'Mod-i': () => this.editor.commands.toggleItalic()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const CustomUnderline = Mark.create({
|
||||||
|
name: 'underline',
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'u' }, { style: 'text-decoration=underline' }]
|
||||||
|
},
|
||||||
|
renderHTML () {
|
||||||
|
return ['u', 0]
|
||||||
|
},
|
||||||
|
addCommands () {
|
||||||
|
return {
|
||||||
|
toggleUnderline: () => ({ commands }) => commands.toggleMark(this.name)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addKeyboardShortcuts () {
|
||||||
|
return {
|
||||||
|
'Mod-u': () => this.editor.commands.toggleUnderline()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const CustomStrike = Mark.create({
|
||||||
|
name: 'strike',
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 's' }, { tag: 'del' }, { tag: 'strike' }, { style: 'text-decoration=line-through' }]
|
||||||
|
},
|
||||||
|
renderHTML () {
|
||||||
|
return ['s', 0]
|
||||||
|
},
|
||||||
|
addCommands () {
|
||||||
|
return {
|
||||||
|
toggleStrike: () => ({ commands }) => commands.toggleMark(this.name)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addKeyboardShortcuts () {
|
||||||
|
return {
|
||||||
|
'Mod-Shift-s': () => this.editor.commands.toggleStrike()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const EmptySpanNode = Node.create({
|
||||||
|
name: 'emptySpan',
|
||||||
|
inline: true,
|
||||||
|
group: 'inline',
|
||||||
|
atom: true,
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{
|
||||||
|
tag: 'span',
|
||||||
|
priority: 60,
|
||||||
|
getAttrs (dom) {
|
||||||
|
if (dom.childNodes.length === 0 && dom.attributes.length > 0) {
|
||||||
|
return collectDomAttrs(dom)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return ['span', node.attrs.htmlAttrs]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const SpanMark = Mark.create({
|
||||||
|
name: 'span',
|
||||||
|
excludes: '',
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'span', getAttrs: collectSpanDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ mark }) {
|
||||||
|
return ['span', mark.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const LinkMark = Mark.create({
|
||||||
|
name: 'link',
|
||||||
|
excludes: '',
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'a', getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ mark }) {
|
||||||
|
return ['a', mark.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const SubscriptMark = Mark.create({
|
||||||
|
name: 'subscript',
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'sub', getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ mark }) {
|
||||||
|
return ['sub', mark.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const SuperscriptMark = Mark.create({
|
||||||
|
name: 'superscript',
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'sup', getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ mark }) {
|
||||||
|
return ['sup', mark.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const TabHandler = Extension.create({
|
||||||
|
name: 'tabHandler',
|
||||||
|
addKeyboardShortcuts () {
|
||||||
|
return {
|
||||||
|
Tab: () => {
|
||||||
|
this.editor.commands.insertContent('\t')
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const variableHighlightKey = new PluginKey('variableHighlight')
|
||||||
|
|
||||||
|
function buildDecorations (doc) {
|
||||||
|
const decorations = []
|
||||||
|
const regex = /\[\[[^\]]*\]\]/g
|
||||||
|
|
||||||
|
doc.descendants((node, pos) => {
|
||||||
|
if (!node.isText) return
|
||||||
|
|
||||||
|
let match
|
||||||
|
|
||||||
|
while ((match = regex.exec(node.text)) !== null) {
|
||||||
|
const from = pos + match.index
|
||||||
|
const to = from + match[0].length
|
||||||
|
|
||||||
|
decorations.push(Decoration.inline(from, to, { nodeName: 'dynamic-variable' }))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return DecorationSet.create(doc, decorations)
|
||||||
|
}
|
||||||
|
|
||||||
|
const VariableHighlight = Extension.create({
|
||||||
|
name: 'variableHighlight',
|
||||||
|
addProseMirrorPlugins () {
|
||||||
|
return [
|
||||||
|
new Plugin({
|
||||||
|
key: variableHighlightKey,
|
||||||
|
state: {
|
||||||
|
init (_, { doc }) {
|
||||||
|
return buildDecorations(doc)
|
||||||
|
},
|
||||||
|
apply (tr, oldSet) {
|
||||||
|
if (tr.docChanged) {
|
||||||
|
return buildDecorations(tr.doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
return oldSet
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
decorations (state) {
|
||||||
|
return this.getState(state)
|
||||||
|
},
|
||||||
|
handleTextInput (view, from, to, text) {
|
||||||
|
if (text !== '[') return false
|
||||||
|
|
||||||
|
const { state } = view
|
||||||
|
const charBefore = state.doc.textBetween(Math.max(from - 1, 0), from)
|
||||||
|
|
||||||
|
if (charBefore !== '[') return false
|
||||||
|
|
||||||
|
const tr = state.tr.insertText('[]]', from, to)
|
||||||
|
|
||||||
|
tr.setSelection(state.selection.constructor.create(tr.doc, from + 1))
|
||||||
|
|
||||||
|
view.dispatch(tr)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export function buildEditor ({ dynamicAreaProps, attachmentsIndex, renderHtmlForSaveRef, onFieldDrop, onFieldDestroy, editorOptions }) {
|
||||||
|
const FieldNode = Node.create({
|
||||||
|
name: 'fieldNode',
|
||||||
|
inline: true,
|
||||||
|
group: 'inline',
|
||||||
|
atom: true,
|
||||||
|
draggable: true,
|
||||||
|
addAttributes () {
|
||||||
|
return {
|
||||||
|
uuid: { default: null },
|
||||||
|
areaUuid: { default: null },
|
||||||
|
width: { default: '124px' },
|
||||||
|
height: { default: null },
|
||||||
|
verticalAlign: { default: 'text-bottom' },
|
||||||
|
display: { default: 'inline-flex' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{
|
||||||
|
tag: 'dynamic-field',
|
||||||
|
getAttrs (dom) {
|
||||||
|
return {
|
||||||
|
uuid: dom.getAttribute('uuid'),
|
||||||
|
areaUuid: dom.getAttribute('area-uuid'),
|
||||||
|
width: dom.style.width,
|
||||||
|
height: dom.style.height,
|
||||||
|
display: dom.style.display,
|
||||||
|
verticalAlign: dom.style.verticalAlign
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
const attrs = {
|
||||||
|
uuid: node.attrs.uuid,
|
||||||
|
'area-uuid': node.attrs.areaUuid,
|
||||||
|
style: `width: ${node.attrs.width}; height: ${node.attrs.height}; display: ${node.attrs.display}; vertical-align: ${node.attrs.verticalAlign};`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!renderHtmlForSaveRef.value) {
|
||||||
|
const fieldArea = dynamicAreaProps.findFieldArea(node.attrs.areaUuid)
|
||||||
|
|
||||||
|
if (fieldArea?.field && fieldArea?.area) {
|
||||||
|
const field = JSON.parse(JSON.stringify(fieldArea.field))
|
||||||
|
const area = JSON.parse(JSON.stringify(fieldArea.area))
|
||||||
|
|
||||||
|
delete field.areas
|
||||||
|
delete field.uuid
|
||||||
|
delete field.submitter_uuid
|
||||||
|
delete area.uuid
|
||||||
|
delete area.attachment_uuid
|
||||||
|
|
||||||
|
attrs['data-field'] = JSON.stringify(field)
|
||||||
|
attrs['data-area'] = JSON.stringify(area)
|
||||||
|
attrs['data-template-id'] = dynamicAreaProps.template.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['dynamic-field', attrs]
|
||||||
|
},
|
||||||
|
addNodeView () {
|
||||||
|
return ({ node, getPos, editor }) => {
|
||||||
|
const dom = document.createElement('span')
|
||||||
|
|
||||||
|
const nodeStyle = reactive({
|
||||||
|
width: node.attrs.width,
|
||||||
|
height: node.attrs.height,
|
||||||
|
verticalAlign: node.attrs.verticalAlign,
|
||||||
|
display: node.attrs.display
|
||||||
|
})
|
||||||
|
|
||||||
|
dom.dataset.areaUuid = node.attrs.areaUuid
|
||||||
|
|
||||||
|
const shadow = dom.attachShadow({ mode: 'open' })
|
||||||
|
|
||||||
|
shadow.adoptedStyleSheets = [dynamicStylesheet]
|
||||||
|
|
||||||
|
const app = createApp(DynamicArea, {
|
||||||
|
fieldUuid: node.attrs.uuid,
|
||||||
|
areaUuid: node.attrs.areaUuid,
|
||||||
|
nodeStyle,
|
||||||
|
getPos,
|
||||||
|
editor,
|
||||||
|
editable: editorOptions.editable,
|
||||||
|
...dynamicAreaProps
|
||||||
|
})
|
||||||
|
|
||||||
|
app.mount(shadow)
|
||||||
|
|
||||||
|
return {
|
||||||
|
dom,
|
||||||
|
update (updatedNode) {
|
||||||
|
if (updatedNode.attrs.areaUuid === node.attrs.areaUuid) {
|
||||||
|
nodeStyle.width = updatedNode.attrs.width
|
||||||
|
nodeStyle.height = updatedNode.attrs.height
|
||||||
|
nodeStyle.verticalAlign = updatedNode.attrs.verticalAlign
|
||||||
|
nodeStyle.display = updatedNode.attrs.display
|
||||||
|
}
|
||||||
|
},
|
||||||
|
destroy () {
|
||||||
|
onFieldDestroy(node)
|
||||||
|
|
||||||
|
app.unmount()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const FieldDropPlugin = Extension.create({
|
||||||
|
name: 'fieldDrop',
|
||||||
|
addProseMirrorPlugins () {
|
||||||
|
return [
|
||||||
|
new Plugin({
|
||||||
|
key: new PluginKey('fieldDrop'),
|
||||||
|
props: {
|
||||||
|
handleDrop: onFieldDrop
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const DynamicImageNode = ImageNode.extend({
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
const { loading, ...attrs } = node.attrs.htmlAttrs
|
||||||
|
|
||||||
|
return ['img', attrs]
|
||||||
|
},
|
||||||
|
addNodeView () {
|
||||||
|
return ({ node }) => {
|
||||||
|
const dom = document.createElement('img')
|
||||||
|
|
||||||
|
const attrs = { ...node.attrs.htmlAttrs }
|
||||||
|
|
||||||
|
const blobUuid = attrs.src?.startsWith('blob:') && attrs.src.slice(5)
|
||||||
|
|
||||||
|
if (blobUuid && attachmentsIndex[blobUuid]) {
|
||||||
|
attrs.src = attachmentsIndex[blobUuid]
|
||||||
|
}
|
||||||
|
|
||||||
|
dom.setAttribute('loading', 'lazy')
|
||||||
|
|
||||||
|
Object.entries(attrs).forEach(([k, v]) => dom.setAttribute(k, v))
|
||||||
|
|
||||||
|
return { dom }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return new Editor({
|
||||||
|
extensions: [
|
||||||
|
Document,
|
||||||
|
Text,
|
||||||
|
HardBreak,
|
||||||
|
History,
|
||||||
|
Gapcursor,
|
||||||
|
Dropcursor,
|
||||||
|
CustomBold,
|
||||||
|
CustomItalic,
|
||||||
|
CustomUnderline,
|
||||||
|
CustomStrike,
|
||||||
|
CustomParagraph,
|
||||||
|
CustomHeading,
|
||||||
|
SectionNode,
|
||||||
|
ArticleNode,
|
||||||
|
DivNode,
|
||||||
|
BlockquoteNode,
|
||||||
|
PreNode,
|
||||||
|
OrderedListNode,
|
||||||
|
BulletListNode,
|
||||||
|
ListItemNode,
|
||||||
|
TableNode,
|
||||||
|
TableHead,
|
||||||
|
TableBody,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableHeader,
|
||||||
|
ColGroupNode,
|
||||||
|
ColNode,
|
||||||
|
DynamicImageNode,
|
||||||
|
EmptySpanNode,
|
||||||
|
LinkMark,
|
||||||
|
SpanMark,
|
||||||
|
SubscriptMark,
|
||||||
|
SuperscriptMark,
|
||||||
|
VariableHighlight,
|
||||||
|
TabHandler,
|
||||||
|
FieldNode,
|
||||||
|
FieldDropPlugin
|
||||||
|
],
|
||||||
|
editorProps: {
|
||||||
|
attributes: {
|
||||||
|
style: 'outline: none'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parseOptions: {
|
||||||
|
preserveWhitespace: true
|
||||||
|
},
|
||||||
|
injectCSS: false,
|
||||||
|
...editorOptions
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="visible"
|
||||||
|
class="absolute z-10 flex items-center gap-0.5 px-1.5 py-1 bg-white border border-base-300 rounded-lg shadow select-none"
|
||||||
|
:style="{ top: (coords.top - 42) + 'px', left: coords.left + 'px', transform: 'translateX(-50%)' }"
|
||||||
|
@mousedown.prevent
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center w-7 h-7 border-none rounded cursor-pointer text-gray-700"
|
||||||
|
:class="isBold ? 'bg-base-200' : 'bg-transparent'"
|
||||||
|
title="Bold"
|
||||||
|
@click="toggleBold"
|
||||||
|
>
|
||||||
|
<IconBold
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center w-7 h-7 border-none rounded cursor-pointer text-gray-700"
|
||||||
|
:class="isItalic ? 'bg-base-200' : 'bg-transparent'"
|
||||||
|
title="Italic"
|
||||||
|
@click="toggleItalic"
|
||||||
|
>
|
||||||
|
<IconItalic
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center w-7 h-7 border-none rounded cursor-pointer text-gray-700"
|
||||||
|
:class="isUnderline ? 'bg-base-200' : 'bg-transparent'"
|
||||||
|
title="Underline"
|
||||||
|
@click="toggleUnderline"
|
||||||
|
>
|
||||||
|
<IconUnderline
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center w-7 h-7 border-none rounded cursor-pointer text-gray-700"
|
||||||
|
:class="isStrike ? 'bg-base-200' : 'bg-transparent'"
|
||||||
|
title="Strikethrough"
|
||||||
|
@click="toggleStrike"
|
||||||
|
>
|
||||||
|
<IconStrikethrough
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div class="w-px h-5 bg-base-300 mx-1" />
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center text-xs h-7 border-none rounded cursor-pointer text-gray-700 bg-transparent"
|
||||||
|
title="Wrap in variable"
|
||||||
|
@click="wrapVariable"
|
||||||
|
>
|
||||||
|
<IconBracketsContain
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
<span class="px-0.5">
|
||||||
|
Variable
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<div class="w-px h-5 bg-base-300 mx-1" />
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center text-xs h-7 border-none rounded cursor-pointer text-gray-700 bg-transparent"
|
||||||
|
title="Wrap in condition"
|
||||||
|
@click="wrapCondition"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.6"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="tabler-icon tabler-icon-brackets-contain"
|
||||||
|
><path d="M7 4h-4v16h4" /><path d="M17 4h4v16h-4" />
|
||||||
|
<text
|
||||||
|
x="12"
|
||||||
|
y="16.5"
|
||||||
|
text-anchor="middle"
|
||||||
|
fill="currentColor"
|
||||||
|
stroke="none"
|
||||||
|
font-size="14"
|
||||||
|
font-weight="600"
|
||||||
|
font-family="ui-sans-serif, system-ui, sans-serif"
|
||||||
|
>if</text>
|
||||||
|
</svg>
|
||||||
|
<span class="px-0.5">
|
||||||
|
Condition
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconBold, IconItalic, IconUnderline, IconStrikethrough, IconBracketsContain } from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DynamicMenu',
|
||||||
|
components: {
|
||||||
|
IconBold,
|
||||||
|
IconItalic,
|
||||||
|
IconUnderline,
|
||||||
|
IconStrikethrough,
|
||||||
|
IconBracketsContain
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
editor: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
coords: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['add-variable', 'add-condition'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isMouseDown: false,
|
||||||
|
isBold: this.editor.isActive('bold'),
|
||||||
|
isItalic: this.editor.isActive('italic'),
|
||||||
|
isUnderline: this.editor.isActive('underline'),
|
||||||
|
isStrike: this.editor.isActive('strike')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
visible () {
|
||||||
|
return !!this.coords && !this.isMouseDown
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.editor.view.dom.addEventListener('mousedown', this.onMouseDown)
|
||||||
|
|
||||||
|
document.addEventListener('mouseup', this.onMouseUp)
|
||||||
|
|
||||||
|
this.editor.on('transaction', this.onTransaction)
|
||||||
|
},
|
||||||
|
beforeUnmount () {
|
||||||
|
if (!this.editor.isDestroyed) {
|
||||||
|
this.editor.view.dom.removeEventListener('mousedown', this.onMouseDown)
|
||||||
|
this.editor.off('transaction', this.onTransaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.removeEventListener('mouseup', this.onMouseUp)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleBold () {
|
||||||
|
this.editor.chain().focus().toggleBold().run()
|
||||||
|
},
|
||||||
|
toggleItalic () {
|
||||||
|
this.editor.chain().focus().toggleItalic().run()
|
||||||
|
},
|
||||||
|
toggleUnderline () {
|
||||||
|
this.editor.chain().focus().toggleUnderline().run()
|
||||||
|
},
|
||||||
|
toggleStrike () {
|
||||||
|
this.editor.chain().focus().toggleStrike().run()
|
||||||
|
},
|
||||||
|
wrapVariable () {
|
||||||
|
const { from, to } = this.editor.state.selection
|
||||||
|
const replacement = '[[variable]]'
|
||||||
|
const varFrom = from + 2
|
||||||
|
const varTo = varFrom + 8
|
||||||
|
|
||||||
|
this.editor.chain().focus()
|
||||||
|
.insertContentAt({ from, to }, replacement)
|
||||||
|
.setTextSelection({ from: varFrom, to: varTo })
|
||||||
|
.run()
|
||||||
|
|
||||||
|
this.$emit('add-variable')
|
||||||
|
},
|
||||||
|
wrapCondition () {
|
||||||
|
const { from, to } = this.editor.state.selection
|
||||||
|
const endText = '[[end]]'
|
||||||
|
const ifText = '[[if:variable]]'
|
||||||
|
|
||||||
|
this.editor.chain().focus()
|
||||||
|
.insertContentAt(to, endText)
|
||||||
|
.insertContentAt(from, ifText)
|
||||||
|
.setTextSelection({ from: from + 5, to: from + 13 })
|
||||||
|
.run()
|
||||||
|
|
||||||
|
this.$emit('add-condition')
|
||||||
|
},
|
||||||
|
onMouseDown () {
|
||||||
|
this.isMouseDown = true
|
||||||
|
},
|
||||||
|
onMouseUp () {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isMouseDown = false
|
||||||
|
}, 1)
|
||||||
|
},
|
||||||
|
onTransaction () {
|
||||||
|
this.isBold = this.editor.isActive('bold')
|
||||||
|
this.isItalic = this.editor.isActive('italic')
|
||||||
|
this.isUnderline = this.editor.isActive('underline')
|
||||||
|
this.isStrike = this.editor.isActive('strike')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,901 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="relative bg-white select-none mb-4 before:border before:rounded before:top-0 before:bottom-0 before:left-0 before:right-0 before:absolute"
|
||||||
|
:class="{ 'cursor-crosshair': isDrawMode && editable }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="isDrawMode && editable && cursorHighlightCoords"
|
||||||
|
class="absolute pointer-events-none z-10 bg-black"
|
||||||
|
:style="{ width: '1px', height: cursorHighlightCoords.height + 'px', left: cursorHighlightCoords.x + 'px', top: cursorHighlightCoords.y + 'px' }"
|
||||||
|
/>
|
||||||
|
<div :style="{ zoom: containerWidth / sectionWidthPx }">
|
||||||
|
<section
|
||||||
|
:id="section.id"
|
||||||
|
ref="editorElement"
|
||||||
|
dir="auto"
|
||||||
|
:class="section.classList.value"
|
||||||
|
:style="section.style.cssText"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Teleport
|
||||||
|
v-if="editor"
|
||||||
|
:to="container"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="areaToolbarCoords && selectedField && selectedArea && !isAreaDrag"
|
||||||
|
class="absolute z-10"
|
||||||
|
:style="{ left: areaToolbarCoords.left + 'px', top: areaToolbarCoords.top + 'px' }"
|
||||||
|
>
|
||||||
|
<AreaTitle
|
||||||
|
:area="selectedArea"
|
||||||
|
:field="selectedField"
|
||||||
|
:editable="editable"
|
||||||
|
:template="template"
|
||||||
|
:selected-areas-ref="selectedAreasRef"
|
||||||
|
:get-field-type-index="getFieldTypeIndex"
|
||||||
|
@remove="onRemoveSelectedArea"
|
||||||
|
@change="onSelectedAreaChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DynamicMenu
|
||||||
|
v-if="editable"
|
||||||
|
v-show="!selectedAreasRef.value.length"
|
||||||
|
:editor="editor"
|
||||||
|
:coords="dynamicMenuCoords"
|
||||||
|
@add-variable="dynamicMenuCoords = null"
|
||||||
|
@add-condition="dynamicMenuCoords = null"
|
||||||
|
/>
|
||||||
|
<FieldContextMenu
|
||||||
|
v-if="contextMenu && contextMenuField"
|
||||||
|
:context-menu="contextMenu"
|
||||||
|
:field="contextMenuField"
|
||||||
|
:with-copy-to-all-pages="false"
|
||||||
|
@close="closeContextMenu"
|
||||||
|
@copy="onContextMenuCopy"
|
||||||
|
@delete="onContextMenuDelete"
|
||||||
|
@add-custom-field="$emit('add-custom-field', $event)"
|
||||||
|
@set-draw="$emit('set-draw', $event)"
|
||||||
|
@save="save"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { shallowRef } from 'vue'
|
||||||
|
import { DOMSerializer, Fragment } from '@tiptap/pm/model'
|
||||||
|
import { v4 } from 'uuid'
|
||||||
|
import FieldContextMenu from './field_context_menu.vue'
|
||||||
|
import AreaTitle from './area_title.vue'
|
||||||
|
import DynamicMenu from './dynamic_menu.vue'
|
||||||
|
import { buildEditor } from './dynamic_editor.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DynamicSection',
|
||||||
|
components: {
|
||||||
|
DynamicMenu,
|
||||||
|
FieldContextMenu,
|
||||||
|
AreaTitle
|
||||||
|
},
|
||||||
|
inject: ['template', 'save', 't', 'fieldsDragFieldRef', 'customDragFieldRef', 'selectedAreasRef', 'getFieldTypeIndex', 'fieldTypes', 'withPhone', 'withPayment', 'withVerification', 'withKba', 'backgroundColor'],
|
||||||
|
props: {
|
||||||
|
section: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
sectionRefs: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
containerWidth: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
attachmentsIndex: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
selectedSubmitter: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
dragField: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
drawField: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
drawFieldType: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
drawCustomField: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
renderHtmlForSaveRef: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
drawOption: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
attachmentUuid: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['update', 'draw', 'set-draw', 'add-custom-field'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isAreaDrag: false,
|
||||||
|
areaToolbarCoords: null,
|
||||||
|
dynamicMenuCoords: null,
|
||||||
|
contextMenu: null,
|
||||||
|
cursorHighlightCoords: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
defaultHeight () {
|
||||||
|
return CSS.supports('height', '1lh') ? '1lh' : '1em'
|
||||||
|
},
|
||||||
|
fieldAreaIndex () {
|
||||||
|
return (this.template.fields || []).reduce((acc, field) => {
|
||||||
|
field.areas?.forEach((area) => {
|
||||||
|
acc[area.uuid] = { area, field }
|
||||||
|
})
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
defaultSizes () {
|
||||||
|
return {
|
||||||
|
checkbox: { width: '18px', height: '18px' },
|
||||||
|
radio: { width: '18px', height: '18px' },
|
||||||
|
multiple: { width: '18px', height: '18px' },
|
||||||
|
signature: { width: '140px', height: '50px' },
|
||||||
|
initials: { width: '40px', height: '32px' },
|
||||||
|
stamp: { width: '150px', height: '80px' },
|
||||||
|
kba: { width: '150px', height: '80px' },
|
||||||
|
verification: { width: '150px', height: '80px' },
|
||||||
|
image: { width: '200px', height: '100px' },
|
||||||
|
date: { width: '100px', height: this.defaultHeight },
|
||||||
|
datenow: { width: '100px', height: this.defaultHeight },
|
||||||
|
text: { width: '120px', height: this.defaultHeight },
|
||||||
|
cells: { width: '120px', height: this.defaultHeight },
|
||||||
|
file: { width: '120px', height: this.defaultHeight },
|
||||||
|
payment: { width: '120px', height: this.defaultHeight },
|
||||||
|
number: { width: '80px', height: this.defaultHeight },
|
||||||
|
select: { width: '120px', height: this.defaultHeight },
|
||||||
|
phone: { width: '120px', height: this.defaultHeight }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
editorRef: () => shallowRef(),
|
||||||
|
editor () {
|
||||||
|
return this.editorRef.value
|
||||||
|
},
|
||||||
|
sectionWidthPx () {
|
||||||
|
const pt = parseFloat(this.section.style.width)
|
||||||
|
|
||||||
|
return pt * (96 / 72)
|
||||||
|
},
|
||||||
|
zoom () {
|
||||||
|
return this.containerWidth / this.sectionWidthPx
|
||||||
|
},
|
||||||
|
isDraggingField () {
|
||||||
|
return !!(this.fieldsDragFieldRef?.value || this.customDragFieldRef?.value || this.dragField)
|
||||||
|
},
|
||||||
|
isDrawMode () {
|
||||||
|
return !!(this.drawField || this.drawCustomField || this.drawFieldType)
|
||||||
|
},
|
||||||
|
selectedArea () {
|
||||||
|
return this.selectedAreasRef.value[0]
|
||||||
|
},
|
||||||
|
selectedField () {
|
||||||
|
if (this.selectedArea) {
|
||||||
|
return this.fieldAreaIndex[this.selectedArea.uuid]?.field
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contextMenuField () {
|
||||||
|
if (this.contextMenu?.areaUuid) {
|
||||||
|
return this.fieldAreaIndex[this.contextMenu.areaUuid].field
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
containerWidth () {
|
||||||
|
this.closeContextMenu()
|
||||||
|
|
||||||
|
if (this.dynamicMenuCoords && this.editor && !this.editor.state.selection.empty) {
|
||||||
|
this.$nextTick(() => this.setDynamicMenuCoords(this.editor))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.editorRef.value = buildEditor({
|
||||||
|
dynamicAreaProps: {
|
||||||
|
template: this.template,
|
||||||
|
t: this.t,
|
||||||
|
selectedAreasRef: this.selectedAreasRef,
|
||||||
|
getFieldTypeIndex: this.getFieldTypeIndex,
|
||||||
|
findFieldArea: (areaUuid) => this.fieldAreaIndex[areaUuid],
|
||||||
|
getZoom: () => this.zoom,
|
||||||
|
onAreaContextMenu: this.onAreaContextMenu,
|
||||||
|
onAreaResize: this.onAreaResize,
|
||||||
|
onAreaDragStart: this.onAreaDragStart
|
||||||
|
},
|
||||||
|
attachmentsIndex: this.attachmentsIndex,
|
||||||
|
onFieldDrop: this.onFieldDrop,
|
||||||
|
onFieldDestroy: this.onFieldDestroy,
|
||||||
|
renderHtmlForSaveRef: this.renderHtmlForSaveRef,
|
||||||
|
editorOptions: {
|
||||||
|
element: this.$refs.editorElement,
|
||||||
|
editable: this.editable,
|
||||||
|
content: this.section.innerHTML,
|
||||||
|
onUpdate: (event) => this.$emit('update', event),
|
||||||
|
onSelectionUpdate: this.onSelectionUpdate,
|
||||||
|
onBlur: () => { this.dynamicMenuCoords = null }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.editor.view.dom.addEventListener('paste', this.onEditorPaste, true)
|
||||||
|
this.editor.view.dom.addEventListener('pointerdown', this.onEditorPointerDown, true)
|
||||||
|
this.editor.view.dom.addEventListener('mousemove', this.onEditorMouseMove)
|
||||||
|
this.editor.view.dom.addEventListener('mouseleave', this.onEditorMouseLeave)
|
||||||
|
this.editor.view.dom.addEventListener('keydown', this.onEditorKeyDown)
|
||||||
|
},
|
||||||
|
beforeUnmount () {
|
||||||
|
if (this.editor) {
|
||||||
|
this.editor.view.dom.removeEventListener('paste', this.onEditorPaste, true)
|
||||||
|
this.editor.view.dom.removeEventListener('pointerdown', this.onEditorPointerDown, true)
|
||||||
|
this.editor.view.dom.removeEventListener('mousemove', this.onEditorMouseMove)
|
||||||
|
this.editor.view.dom.removeEventListener('mouseleave', this.onEditorMouseLeave)
|
||||||
|
this.editor.view.dom.removeEventListener('keydown', this.onEditorKeyDown)
|
||||||
|
this.editor.destroy()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
findAreaNodePos (areaUuid) {
|
||||||
|
const el = this.editor.view.dom.querySelector(`[data-area-uuid="${areaUuid}"]`)
|
||||||
|
|
||||||
|
return this.editor.view.posAtDOM(el, 0)
|
||||||
|
},
|
||||||
|
getFieldInsertIndex (pos) {
|
||||||
|
const view = this.editor.view
|
||||||
|
const fields = this.template.fields || []
|
||||||
|
|
||||||
|
if (!fields.length) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
let previousField = null
|
||||||
|
|
||||||
|
view.state.doc.nodesBetween(0, pos, (node, nodePos) => {
|
||||||
|
if (node.type.name !== 'fieldNode') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodePos + node.nodeSize <= pos) {
|
||||||
|
previousField = this.fieldAreaIndex[node.attrs.areaUuid]?.field || previousField
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!previousField) {
|
||||||
|
previousField = this.getPreviousSectionField()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!previousField) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousFieldIndex = fields.indexOf(previousField)
|
||||||
|
|
||||||
|
return previousFieldIndex === -1 ? fields.length : previousFieldIndex + 1
|
||||||
|
},
|
||||||
|
insertFieldInTemplate (field, index) {
|
||||||
|
const currentFieldIndex = this.template.fields.indexOf(field)
|
||||||
|
|
||||||
|
if (currentFieldIndex !== -1) {
|
||||||
|
return currentFieldIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
this.template.fields.splice(index, 0, field)
|
||||||
|
|
||||||
|
return index
|
||||||
|
},
|
||||||
|
getLastFieldInSection (sectionRef) {
|
||||||
|
let lastField = null
|
||||||
|
|
||||||
|
sectionRef.editor.state.doc.descendants((node) => {
|
||||||
|
if (node.type.name === 'fieldNode') {
|
||||||
|
lastField = this.fieldAreaIndex[node.attrs.areaUuid]?.field || lastField
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return lastField
|
||||||
|
},
|
||||||
|
getPreviousSectionField () {
|
||||||
|
const sectionIndex = this.sectionRefs.indexOf(this)
|
||||||
|
|
||||||
|
for (let index = sectionIndex - 1; index >= 0; index -= 1) {
|
||||||
|
const previousField = this.getLastFieldInSection(this.sectionRefs[index])
|
||||||
|
|
||||||
|
if (previousField) {
|
||||||
|
return previousField
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
removeArea (area) {
|
||||||
|
const { field } = this.fieldAreaIndex[area.uuid]
|
||||||
|
const areaIndex = field.areas.indexOf(area)
|
||||||
|
|
||||||
|
if (areaIndex !== -1) {
|
||||||
|
field.areas.splice(areaIndex, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.areas.length === 0) {
|
||||||
|
this.template.fields.splice(this.template.fields.indexOf(field), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pos = this.findAreaNodePos(area.uuid)
|
||||||
|
|
||||||
|
this.editor.chain().focus().deleteRange({ from: pos, to: pos + 1 }).run()
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
|
onSelectionUpdate ({ editor }) {
|
||||||
|
const { selection } = editor.state
|
||||||
|
|
||||||
|
if (selection.node?.type.name === 'fieldNode') {
|
||||||
|
const { areaUuid } = selection.node.attrs
|
||||||
|
|
||||||
|
const field = this.fieldAreaIndex[areaUuid]?.field
|
||||||
|
|
||||||
|
if (field) {
|
||||||
|
const area = field.areas.find((a) => a.uuid === areaUuid)
|
||||||
|
|
||||||
|
if (area) {
|
||||||
|
const dom = editor.view.nodeDOM(selection.from)
|
||||||
|
const areaEl = dom.shadowRoot.firstElementChild
|
||||||
|
|
||||||
|
if (areaEl) {
|
||||||
|
const rect = areaEl.getBoundingClientRect()
|
||||||
|
const containerRect = this.container.getBoundingClientRect()
|
||||||
|
|
||||||
|
this.areaToolbarCoords = {
|
||||||
|
left: rect.left - containerRect.left,
|
||||||
|
top: rect.top - containerRect.top
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectedAreasRef.value = [area]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.areaToolbarCoords = null
|
||||||
|
this.selectedAreasRef.value = []
|
||||||
|
|
||||||
|
if (editor.state.selection.empty) {
|
||||||
|
this.dynamicMenuCoords = null
|
||||||
|
} else {
|
||||||
|
this.setDynamicMenuCoords(editor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setDynamicMenuCoords (editor) {
|
||||||
|
const { from, to } = editor.state.selection
|
||||||
|
const view = editor.view
|
||||||
|
const start = view.coordsAtPos(from)
|
||||||
|
const end = view.coordsAtPos(to)
|
||||||
|
const containerRect = this.container.getBoundingClientRect()
|
||||||
|
const left = (start.left + end.right) / 2 - containerRect.left
|
||||||
|
|
||||||
|
this.dynamicMenuCoords = {
|
||||||
|
top: Math.min(start.top, end.top) - containerRect.top,
|
||||||
|
left: Math.max(80, Math.min(left, containerRect.width - 80))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFieldDestroy (node) {
|
||||||
|
this.selectedAreasRef.value = []
|
||||||
|
|
||||||
|
const { areaUuid } = node.attrs
|
||||||
|
|
||||||
|
let nodeExistsInDoc = false
|
||||||
|
|
||||||
|
this.editor.state.doc.descendants((docNode) => {
|
||||||
|
if (docNode.attrs.areaUuid === areaUuid) {
|
||||||
|
nodeExistsInDoc = true
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (nodeExistsInDoc) return
|
||||||
|
|
||||||
|
const fieldArea = this.fieldAreaIndex[areaUuid]
|
||||||
|
|
||||||
|
if (!fieldArea) return
|
||||||
|
|
||||||
|
const field = fieldArea.field
|
||||||
|
|
||||||
|
const areaIndex = field.areas.findIndex((a) => a.uuid === areaUuid)
|
||||||
|
|
||||||
|
if (areaIndex !== -1) {
|
||||||
|
field.areas.splice(areaIndex, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!field.areas?.length) {
|
||||||
|
this.template.fields.splice(this.template.fields.indexOf(field), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
|
onAreaResize (rect) {
|
||||||
|
const containerRect = this.container.getBoundingClientRect()
|
||||||
|
|
||||||
|
this.areaToolbarCoords = {
|
||||||
|
left: rect.left - containerRect.left,
|
||||||
|
top: rect.top - containerRect.top
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onAreaDragStart () {
|
||||||
|
this.isAreaDrag = true
|
||||||
|
},
|
||||||
|
onAreaContextMenu (area, e) {
|
||||||
|
this.contextMenu = {
|
||||||
|
x: e.clientX,
|
||||||
|
y: e.clientY,
|
||||||
|
areaUuid: area.uuid
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deselectArea () {
|
||||||
|
this.areaToolbarCoords = null
|
||||||
|
this.selectedAreasRef.value = []
|
||||||
|
},
|
||||||
|
closeContextMenu () {
|
||||||
|
this.contextMenu = null
|
||||||
|
},
|
||||||
|
onEditorMouseLeave () {
|
||||||
|
this.cursorHighlightCoords = null
|
||||||
|
},
|
||||||
|
onEditorKeyDown (event) {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
this.editor.chain().setNodeSelection(0).blur().run()
|
||||||
|
this.deselectArea()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onEditorMouseMove (event) {
|
||||||
|
if (!this.isDrawMode || !this.editable) {
|
||||||
|
this.cursorHighlightCoords = null
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const view = this.editor?.view
|
||||||
|
|
||||||
|
if (!view) return
|
||||||
|
|
||||||
|
const pos = view.posAtCoords({ left: event.clientX, top: event.clientY })
|
||||||
|
|
||||||
|
if (!pos) {
|
||||||
|
this.cursorHighlightCoords = null
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const coords = view.coordsAtPos(pos.pos)
|
||||||
|
const outerRect = this.$el.getBoundingClientRect()
|
||||||
|
const lineHeight = coords.bottom - coords.top
|
||||||
|
|
||||||
|
this.cursorHighlightCoords = {
|
||||||
|
x: coords.left - outerRect.left,
|
||||||
|
y: coords.top - outerRect.top,
|
||||||
|
height: lineHeight
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getFieldNode (areaUuid) {
|
||||||
|
const pos = this.findAreaNodePos(areaUuid)
|
||||||
|
|
||||||
|
return this.editor.state.doc.nodeAt(pos)
|
||||||
|
},
|
||||||
|
serializeFieldNodeHtml (areaUuid) {
|
||||||
|
const node = this.getFieldNode(areaUuid)
|
||||||
|
|
||||||
|
if (!node) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const serializer = DOMSerializer.fromSchema(this.editor.state.schema)
|
||||||
|
const container = document.createElement('div')
|
||||||
|
|
||||||
|
container.appendChild(serializer.serializeFragment(Fragment.from(node)))
|
||||||
|
|
||||||
|
return container.innerHTML
|
||||||
|
},
|
||||||
|
async writeHtmlToClipboard ({ html, text }, clipboardData = null) {
|
||||||
|
if (clipboardData) {
|
||||||
|
clipboardData.setData('text/html', html)
|
||||||
|
clipboardData.setData('text/plain', text || html)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navigator.clipboard?.write && window.ClipboardItem) {
|
||||||
|
await navigator.clipboard.write([
|
||||||
|
new ClipboardItem({
|
||||||
|
'text/html': new Blob([html], { type: 'text/html' }),
|
||||||
|
'text/plain': new Blob([text || html], { type: 'text/plain' })
|
||||||
|
})
|
||||||
|
])
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navigator.clipboard?.writeText) {
|
||||||
|
await navigator.clipboard.writeText(text || html)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
async copyFieldAreaToClipboard (areaUuid, clipboardData = null) {
|
||||||
|
const html = this.serializeFieldNodeHtml(areaUuid)
|
||||||
|
|
||||||
|
if (!html) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.writeHtmlToClipboard({ html, text: html }, clipboardData)
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to copy dynamic field:', e)
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onContextMenuCopy () {
|
||||||
|
await this.copyFieldAreaToClipboard(this.contextMenu.areaUuid)
|
||||||
|
|
||||||
|
this.closeContextMenu()
|
||||||
|
},
|
||||||
|
buildCopiedField (payload) {
|
||||||
|
const field = JSON.parse(JSON.stringify(payload.field))
|
||||||
|
const area = {
|
||||||
|
...JSON.parse(JSON.stringify(payload.area)),
|
||||||
|
uuid: v4(),
|
||||||
|
attachment_uuid: this.attachmentUuid
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.templateId !== this.template.id) {
|
||||||
|
delete field.conditions
|
||||||
|
|
||||||
|
if (field.preferences) {
|
||||||
|
delete field.preferences.formula
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newField = {
|
||||||
|
...field,
|
||||||
|
uuid: v4(),
|
||||||
|
submitter_uuid: this.selectedSubmitter.uuid,
|
||||||
|
areas: [area]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['radio', 'multiple'].includes(field.type) && field.options?.length) {
|
||||||
|
const oldOptionUuid = area.option_uuid
|
||||||
|
const optionsMap = {}
|
||||||
|
|
||||||
|
newField.options = field.options.map((opt) => {
|
||||||
|
const newUuid = v4()
|
||||||
|
optionsMap[opt.uuid] = newUuid
|
||||||
|
|
||||||
|
return { ...opt, uuid: newUuid }
|
||||||
|
})
|
||||||
|
|
||||||
|
area.option_uuid = optionsMap[oldOptionUuid] || newField.options[0]?.uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
return { field: newField, area }
|
||||||
|
},
|
||||||
|
onEditorPaste (event) {
|
||||||
|
const clipboardData = event.clipboardData
|
||||||
|
|
||||||
|
if (!clipboardData) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = clipboardData.getData('text/html')
|
||||||
|
const text = clipboardData.getData('text/plain')
|
||||||
|
const clipboardHtml = html || (text.includes('<dynamic-field') ? text : '')
|
||||||
|
|
||||||
|
if (!clipboardHtml || !clipboardHtml.includes('data-field=')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = document.createElement('div')
|
||||||
|
|
||||||
|
container.innerHTML = clipboardHtml
|
||||||
|
|
||||||
|
const fieldNodes = [...container.querySelectorAll('dynamic-field[data-field][data-area]')]
|
||||||
|
|
||||||
|
if (!fieldNodes.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
const { selection } = this.editor.state
|
||||||
|
const { from, to } = selection
|
||||||
|
let insertIndex = this.getFieldInsertIndex(selection.node?.type.name === 'fieldNode' ? to : from)
|
||||||
|
|
||||||
|
let lastArea = null
|
||||||
|
|
||||||
|
fieldNodes.forEach((fieldNode) => {
|
||||||
|
const fieldValue = fieldNode.dataset.field
|
||||||
|
const areaValue = fieldNode.dataset.area
|
||||||
|
const templateId = fieldNode.dataset.templateId
|
||||||
|
|
||||||
|
if (!fieldValue || !areaValue) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { field, area } = this.buildCopiedField({
|
||||||
|
field: JSON.parse(fieldValue),
|
||||||
|
area: JSON.parse(areaValue),
|
||||||
|
templateId: Number(templateId)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.insertFieldInTemplate(field, insertIndex)
|
||||||
|
insertIndex += 1
|
||||||
|
|
||||||
|
fieldNode.setAttribute('uuid', field.uuid)
|
||||||
|
fieldNode.setAttribute('area-uuid', area.uuid)
|
||||||
|
fieldNode.removeAttribute('data-field')
|
||||||
|
fieldNode.removeAttribute('data-area')
|
||||||
|
fieldNode.removeAttribute('data-template-id')
|
||||||
|
|
||||||
|
lastArea = area
|
||||||
|
})
|
||||||
|
|
||||||
|
this.editor.chain().focus().insertContentAt({ from, to }, container.innerHTML).run()
|
||||||
|
|
||||||
|
if (lastArea) {
|
||||||
|
this.editor.commands.setNodeSelection(this.findAreaNodePos(lastArea.uuid))
|
||||||
|
}
|
||||||
|
|
||||||
|
this.closeContextMenu()
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
|
onContextMenuDelete () {
|
||||||
|
const menu = this.contextMenu
|
||||||
|
const fieldArea = this.fieldAreaIndex[menu.areaUuid]
|
||||||
|
|
||||||
|
if (fieldArea) {
|
||||||
|
this.removeArea(fieldArea.area)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.closeContextMenu()
|
||||||
|
this.deselectArea()
|
||||||
|
},
|
||||||
|
onRemoveSelectedArea () {
|
||||||
|
this.removeArea(this.selectedArea)
|
||||||
|
|
||||||
|
this.deselectArea()
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
|
onSelectedAreaChange () {
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
|
onFieldDrop (view, event, _slice, moved) {
|
||||||
|
this.isAreaDrag = false
|
||||||
|
|
||||||
|
if (moved) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const draggedField = this.fieldsDragFieldRef?.value || this.customDragFieldRef?.value || this.dragField
|
||||||
|
|
||||||
|
if (!draggedField) return false
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
const pos = view.posAtCoords({ left: event.clientX, top: event.clientY })
|
||||||
|
|
||||||
|
if (!pos) return false
|
||||||
|
|
||||||
|
this.insertFieldAtRange({
|
||||||
|
sourceField: draggedField,
|
||||||
|
existingField: this.fieldsDragFieldRef?.value,
|
||||||
|
from: pos.pos
|
||||||
|
})
|
||||||
|
|
||||||
|
this.fieldsDragFieldRef.value = null
|
||||||
|
this.customDragFieldRef.value = null
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
onEditorPointerDown (event) {
|
||||||
|
if (!this.isDrawMode || !this.editable || this.isDraggingField) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.button === 2) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const view = this.editor?.view
|
||||||
|
|
||||||
|
if (!view) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const selection = view.state.selection
|
||||||
|
const isTextRangeSelection = !selection.empty && !selection.node
|
||||||
|
|
||||||
|
let from = selection.from
|
||||||
|
let to = selection.to
|
||||||
|
|
||||||
|
if (!isTextRangeSelection) {
|
||||||
|
const pos = view.posAtCoords({ left: event.clientX, top: event.clientY })
|
||||||
|
|
||||||
|
if (!pos) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
from = pos.pos
|
||||||
|
to = pos.pos
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceField = this.drawField || this.drawCustomField || { type: this.drawFieldType }
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
if (this.drawOption && this.drawField) {
|
||||||
|
const areaWithoutOption = this.drawField.areas?.find((a) => !a.option_uuid)
|
||||||
|
|
||||||
|
if (areaWithoutOption && !this.drawField.areas.find((a) => a.option_uuid === this.drawField.options[0].uuid)) {
|
||||||
|
areaWithoutOption.option_uuid = this.drawField.options[0].uuid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const inserted = this.insertFieldAtRange({
|
||||||
|
sourceField,
|
||||||
|
existingField: this.drawField,
|
||||||
|
optionUuid: this.drawOption?.uuid,
|
||||||
|
from,
|
||||||
|
to
|
||||||
|
})
|
||||||
|
|
||||||
|
if (inserted) {
|
||||||
|
this.$emit('draw', inserted)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
buildFieldArea ({ optionUuid = null } = {}) {
|
||||||
|
const area = {
|
||||||
|
uuid: v4(),
|
||||||
|
attachment_uuid: this.attachmentUuid
|
||||||
|
}
|
||||||
|
|
||||||
|
if (optionUuid) {
|
||||||
|
area.option_uuid = optionUuid
|
||||||
|
}
|
||||||
|
|
||||||
|
return area
|
||||||
|
},
|
||||||
|
buildNewField (sourceField, area) {
|
||||||
|
const fieldType = sourceField?.type || 'text'
|
||||||
|
const newField = {
|
||||||
|
name: sourceField?.name || '',
|
||||||
|
uuid: v4(),
|
||||||
|
required: fieldType !== 'checkbox',
|
||||||
|
submitter_uuid: this.selectedSubmitter.uuid,
|
||||||
|
type: fieldType,
|
||||||
|
areas: [area]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['select', 'multiple', 'radio'].includes(fieldType)) {
|
||||||
|
if (sourceField?.options?.length) {
|
||||||
|
newField.options = sourceField.options.map((opt) => ({
|
||||||
|
value: typeof opt === 'string' ? opt : opt.value,
|
||||||
|
uuid: v4()
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
newField.options = [{ value: '', uuid: v4() }, { value: '', uuid: v4() }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fieldType === 'datenow') {
|
||||||
|
newField.type = 'date'
|
||||||
|
newField.readonly = true
|
||||||
|
newField.default_value = '{{date}}'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['stamp', 'heading', 'strikethrough'].includes(fieldType)) {
|
||||||
|
newField.readonly = true
|
||||||
|
|
||||||
|
if (fieldType === 'strikethrough') {
|
||||||
|
newField.default_value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newField
|
||||||
|
},
|
||||||
|
insertFieldAtRange ({ sourceField, existingField = null, optionUuid = null, from, to = from }) {
|
||||||
|
if (!sourceField) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldType = sourceField.type || existingField?.type || 'text'
|
||||||
|
const dims = this.defaultSizes[fieldType]
|
||||||
|
const area = this.buildFieldArea({ optionUuid })
|
||||||
|
const insertIndex = this.getFieldInsertIndex(from)
|
||||||
|
|
||||||
|
let field = existingField
|
||||||
|
const view = this.editor.view
|
||||||
|
|
||||||
|
if (field) {
|
||||||
|
if (!this.template.fields.includes(field)) {
|
||||||
|
this.insertFieldInTemplate(field, insertIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
field.areas = field.areas || []
|
||||||
|
field.areas.push(area)
|
||||||
|
} else {
|
||||||
|
field = this.buildNewField(sourceField, area)
|
||||||
|
this.insertFieldInTemplate(field, insertIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeType = view.state.schema.nodes.fieldNode
|
||||||
|
const fieldNode = nodeType.create({
|
||||||
|
uuid: field.uuid,
|
||||||
|
areaUuid: area.uuid,
|
||||||
|
width: dims.width,
|
||||||
|
height: dims.height
|
||||||
|
})
|
||||||
|
|
||||||
|
const tr = from !== to
|
||||||
|
? view.state.tr.replaceWith(from, to, fieldNode)
|
||||||
|
: view.state.tr.insert(from, fieldNode)
|
||||||
|
|
||||||
|
view.dispatch(tr)
|
||||||
|
|
||||||
|
this.editor.chain().focus().setNodeSelection(from).run()
|
||||||
|
this.save()
|
||||||
|
|
||||||
|
return { area, field, pos: from }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
@config "../../../tailwind.dynamic.config.js";
|
||||||
|
|
||||||
|
@import "tailwindcss/components";
|
||||||
|
@import "tailwindcss/utilities";
|
||||||
|
|
||||||
|
*,
|
||||||
|
::before,
|
||||||
|
::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-width: 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
::before,
|
||||||
|
::after {
|
||||||
|
--tw-content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
:host {
|
||||||
|
all: initial;
|
||||||
|
}
|
||||||
@@ -0,0 +1,422 @@
|
|||||||
|
<template>
|
||||||
|
<div class="group">
|
||||||
|
<div class="flex items-center justify-between py-1.5 px-0.5">
|
||||||
|
<div class="flex items-center space-x-1 min-w-0">
|
||||||
|
<FieldType
|
||||||
|
:model-value="formType"
|
||||||
|
:editable="editable"
|
||||||
|
:button-width="18"
|
||||||
|
:menu-classes="'mt-1.5'"
|
||||||
|
:menu-style="{ backgroundColor: dropdownBgColor }"
|
||||||
|
@update:model-value="onTypeChange"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="truncate"
|
||||||
|
:title="path"
|
||||||
|
>{{ displayName }}</span>
|
||||||
|
<span
|
||||||
|
v-if="isArray"
|
||||||
|
class="text-xs bg-base-200 rounded px-1 flex-shrink-0"
|
||||||
|
>{{ t('list') }}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="editable"
|
||||||
|
class="flex items-center flex-shrink-0"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="dropdown dropdown-end"
|
||||||
|
@mouseenter="renderDropdown = true"
|
||||||
|
@touchstart="renderDropdown = true"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
tabindex="0"
|
||||||
|
:title="t('settings')"
|
||||||
|
class="cursor-pointer text-transparent group-hover:text-base-content"
|
||||||
|
>
|
||||||
|
<IconSettings
|
||||||
|
:width="18"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<ul
|
||||||
|
v-if="renderDropdown"
|
||||||
|
tabindex="0"
|
||||||
|
class="mt-1.5 dropdown-content menu menu-xs p-2 shadow rounded-box w-52 z-10"
|
||||||
|
:style="{ backgroundColor: dropdownBgColor }"
|
||||||
|
@click="closeDropdown"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
|
||||||
|
@change="onTypeChange($event.target.value)"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="varType in variableTypes"
|
||||||
|
:key="varType"
|
||||||
|
:value="varType"
|
||||||
|
:selected="varType === formType"
|
||||||
|
>{{ t(varType) }}</option>
|
||||||
|
</select>
|
||||||
|
<label
|
||||||
|
:style="{ backgroundColor: dropdownBgColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>{{ t('type') }}</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="formType === 'number'"
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
|
||||||
|
@change="[schema.format = $event.target.value, save()]"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="format in numberFormats"
|
||||||
|
:key="format"
|
||||||
|
:value="format"
|
||||||
|
:selected="format === schema.format || (format === 'none' && !schema.format)"
|
||||||
|
>{{ formatNumber(123456789.567, format) }}</option>
|
||||||
|
</select>
|
||||||
|
<label
|
||||||
|
:style="{ backgroundColor: dropdownBgColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>{{ t('format') }}</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="['text', 'number'].includes(formType)"
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="schema.default_value"
|
||||||
|
:type="formType === 'number' ? 'number' : 'text'"
|
||||||
|
:placeholder="t('default_value')"
|
||||||
|
dir="auto"
|
||||||
|
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0 bg-transparent"
|
||||||
|
@blur="save"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
v-if="schema.default_value"
|
||||||
|
:style="{ backgroundColor: dropdownBgColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>{{ t('default_value') }}</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="formType === 'date'"
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
:value="schema.format || 'MM/DD/YYYY'"
|
||||||
|
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
|
||||||
|
@change="[schema.format = $event.target.value, save()]"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="format in dateFormats"
|
||||||
|
:key="format"
|
||||||
|
:value="format"
|
||||||
|
>{{ formatDate(new Date(), format) }}</option>
|
||||||
|
</select>
|
||||||
|
<label
|
||||||
|
:style="{ backgroundColor: dropdownBgColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>{{ t('format') }}</label>
|
||||||
|
</div>
|
||||||
|
<li
|
||||||
|
v-if="formType === 'date'"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<label class="cursor-pointer py-1.5">
|
||||||
|
<input
|
||||||
|
:checked="schema.default_value === '{{date}}'"
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-xs"
|
||||||
|
@change="[schema.default_value = $event.target.checked ? '{{date}}' : undefined, save()]"
|
||||||
|
>
|
||||||
|
<span class="label-text">{{ t('current_date') }}</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<div
|
||||||
|
v-if="['radio', 'select'].includes(formType)"
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
dir="auto"
|
||||||
|
class="select select-bordered select-xs w-full max-w-xs h-7 !outline-0 font-normal bg-transparent"
|
||||||
|
@change="[schema.default_value = $event.target.value || undefined, save()]"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
value=""
|
||||||
|
:selected="!schema.default_value"
|
||||||
|
>{{ t('none') }}</option>
|
||||||
|
<option
|
||||||
|
v-for="opt in (schema.options || [])"
|
||||||
|
:key="opt"
|
||||||
|
:value="opt"
|
||||||
|
:selected="schema.default_value === opt"
|
||||||
|
>{{ opt }}</option>
|
||||||
|
</select>
|
||||||
|
<label
|
||||||
|
:style="{ backgroundColor: dropdownBgColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>{{ t('default_value') }}</label>
|
||||||
|
</div>
|
||||||
|
<li
|
||||||
|
v-if="formType === 'checkbox'"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<label class="cursor-pointer py-1.5">
|
||||||
|
<input
|
||||||
|
:checked="schema.default_value === true"
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-xs"
|
||||||
|
@change="[schema.default_value = $event.target.checked || undefined, save()]"
|
||||||
|
>
|
||||||
|
<span class="label-text">{{ t('checked') }}</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li @click.stop>
|
||||||
|
<label class="cursor-pointer py-1.5">
|
||||||
|
<input
|
||||||
|
:checked="schema.required !== false"
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-xs"
|
||||||
|
@change="[schema.required = $event.target.checked, save()]"
|
||||||
|
>
|
||||||
|
<span class="label-text">{{ t('required') }}</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="['radio', 'select'].includes(formType) && schema.options"
|
||||||
|
ref="options"
|
||||||
|
class="pl-2 pr-1 pb-1.5 space-y-1.5"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(option, index) in schema.options"
|
||||||
|
:key="index"
|
||||||
|
class="flex space-x-1.5 items-center"
|
||||||
|
>
|
||||||
|
<span class="text-sm w-3.5 select-none">{{ index + 1 }}.</span>
|
||||||
|
<input
|
||||||
|
:value="option"
|
||||||
|
class="w-full input input-primary input-xs text-sm bg-transparent"
|
||||||
|
type="text"
|
||||||
|
dir="auto"
|
||||||
|
:placeholder="`${t('option')} ${index + 1}`"
|
||||||
|
@blur="[schema.options.splice(index, 1, $event.target.value), save()]"
|
||||||
|
@keydown.enter="$event.target.value ? onOptionEnter(index, $event.target.value) : null"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="text-sm w-3.5"
|
||||||
|
tabindex="-1"
|
||||||
|
@click="[schema.options.splice(index, 1), save()]"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="text-center text-sm w-full pb-1"
|
||||||
|
@click="addOptionAndFocus((schema.options || []).length)"
|
||||||
|
>
|
||||||
|
+ {{ t('add_option') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import FieldType from './field_type'
|
||||||
|
import { IconSettings } from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DynamicVariable',
|
||||||
|
components: {
|
||||||
|
FieldType,
|
||||||
|
IconSettings
|
||||||
|
},
|
||||||
|
inject: ['t', 'save', 'backgroundColor'],
|
||||||
|
provide () {
|
||||||
|
return {
|
||||||
|
fieldTypes: ['text', 'number', 'date', 'checkbox', 'radio', 'select']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
path: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
groupKey: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isArray: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
renderDropdown: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
displayName () {
|
||||||
|
if (this.groupKey) {
|
||||||
|
const prefix = this.groupKey + (this.path.startsWith(this.groupKey + '[].') ? '[].' : '.')
|
||||||
|
|
||||||
|
return this.path.slice(prefix.length)
|
||||||
|
} else {
|
||||||
|
return this.path
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dropdownBgColor () {
|
||||||
|
return ['', null, 'transparent'].includes(this.backgroundColor) ? 'white' : this.backgroundColor
|
||||||
|
},
|
||||||
|
schemaTypeToFormType () {
|
||||||
|
return { string: 'text', number: 'number', boolean: 'checkbox', date: 'date' }
|
||||||
|
},
|
||||||
|
formType () {
|
||||||
|
return this.schema.form_type || this.schemaTypeToFormType[this.schema.type] || 'text'
|
||||||
|
},
|
||||||
|
variableTypes () {
|
||||||
|
return ['text', 'number', 'date', 'checkbox', 'radio', 'select']
|
||||||
|
},
|
||||||
|
formTypeToSchemaType () {
|
||||||
|
return { text: 'string', number: 'number', date: 'date', checkbox: 'boolean', radio: 'string', select: 'string' }
|
||||||
|
},
|
||||||
|
numberFormats () {
|
||||||
|
return [
|
||||||
|
'none',
|
||||||
|
'usd',
|
||||||
|
'eur',
|
||||||
|
'gbp',
|
||||||
|
'comma',
|
||||||
|
'dot',
|
||||||
|
'space'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
dateFormats () {
|
||||||
|
const formats = [
|
||||||
|
'MM/DD/YYYY',
|
||||||
|
'DD/MM/YYYY',
|
||||||
|
'YYYY-MM-DD',
|
||||||
|
'DD-MM-YYYY',
|
||||||
|
'DD.MM.YYYY',
|
||||||
|
'MMM D, YYYY',
|
||||||
|
'MMMM D, YYYY',
|
||||||
|
'D MMM YYYY',
|
||||||
|
'D MMMM YYYY'
|
||||||
|
]
|
||||||
|
|
||||||
|
if (Intl.DateTimeFormat().resolvedOptions().timeZone?.includes('Seoul') || navigator.language?.startsWith('ko')) {
|
||||||
|
formats.push('YYYY년 MM월 DD일')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.schema.format && !formats.includes(this.schema.format)) {
|
||||||
|
formats.unshift(this.schema.format)
|
||||||
|
}
|
||||||
|
|
||||||
|
return formats
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onTypeChange (newType) {
|
||||||
|
this.schema.type = this.formTypeToSchemaType[newType] || 'string'
|
||||||
|
this.schema.form_type = newType
|
||||||
|
|
||||||
|
if (['radio', 'select'].includes(newType)) {
|
||||||
|
if (!this.schema.options || !this.schema.options.length) {
|
||||||
|
this.schema.options = ['', '']
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delete this.schema.options
|
||||||
|
delete this.schema.default_value
|
||||||
|
delete this.schema.format
|
||||||
|
}
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
|
onOptionEnter (index, value) {
|
||||||
|
this.schema.options.splice(index, 1, value)
|
||||||
|
this.schema.options.splice(index + 1, 0, '')
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.options.querySelectorAll('input')[index + 1]?.focus()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
addOptionAndFocus (index) {
|
||||||
|
if (!this.schema.options) {
|
||||||
|
this.schema.options = []
|
||||||
|
}
|
||||||
|
|
||||||
|
this.schema.options.splice(index, 0, '')
|
||||||
|
this.save()
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.options.querySelectorAll('input')[index]?.focus()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
formatNumber (number, format) {
|
||||||
|
if (format === 'comma') {
|
||||||
|
return new Intl.NumberFormat('en-US').format(number)
|
||||||
|
} else if (format === 'usd') {
|
||||||
|
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
|
||||||
|
} else if (format === 'gbp') {
|
||||||
|
return new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
|
||||||
|
} else if (format === 'eur') {
|
||||||
|
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
|
||||||
|
} else if (format === 'dot') {
|
||||||
|
return new Intl.NumberFormat('de-DE').format(number)
|
||||||
|
} else if (format === 'space') {
|
||||||
|
return new Intl.NumberFormat('fr-FR').format(number)
|
||||||
|
} else {
|
||||||
|
return number
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formatDate (date, format) {
|
||||||
|
const monthFormats = { M: 'numeric', MM: '2-digit', MMM: 'short', MMMM: 'long' }
|
||||||
|
const dayFormats = { D: 'numeric', DD: '2-digit' }
|
||||||
|
const yearFormats = { YYYY: 'numeric', YY: '2-digit' }
|
||||||
|
|
||||||
|
const parts = new Intl.DateTimeFormat([], {
|
||||||
|
day: dayFormats[format.match(/D+/)],
|
||||||
|
month: monthFormats[format.match(/M+/)],
|
||||||
|
year: yearFormats[format.match(/Y+/)]
|
||||||
|
}).formatToParts(date)
|
||||||
|
|
||||||
|
return format
|
||||||
|
.replace(/D+/, parts.find((p) => p.type === 'day').value)
|
||||||
|
.replace(/M+/, parts.find((p) => p.type === 'month').value)
|
||||||
|
.replace(/Y+/, parts.find((p) => p.type === 'year').value)
|
||||||
|
},
|
||||||
|
closeDropdown () {
|
||||||
|
this.$el.getRootNode().activeElement.blur()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
v-if="!schemaEntries.length"
|
||||||
|
class="text-center py-4 px-2"
|
||||||
|
>
|
||||||
|
<p class="font-medium">
|
||||||
|
{{ t('no_variables') }}
|
||||||
|
</p>
|
||||||
|
<p class="text-sm mt-1">
|
||||||
|
{{ t('no_variables_description') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<template
|
||||||
|
v-for="([key, node], index) in schemaEntries"
|
||||||
|
:key="key"
|
||||||
|
>
|
||||||
|
<div v-if="isGroup(node)">
|
||||||
|
<hr
|
||||||
|
v-if="index > 0"
|
||||||
|
class="border-base-300"
|
||||||
|
>
|
||||||
|
<label class="peer flex items-center py-1.5 cursor-pointer select-none">
|
||||||
|
<span class="w-5 flex justify-center items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="hidden peer"
|
||||||
|
checked
|
||||||
|
>
|
||||||
|
<IconChevronDown
|
||||||
|
class="hidden peer-checked:block ml-0.5"
|
||||||
|
:width="14"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
<IconChevronRight
|
||||||
|
class="block peer-checked:hidden ml-0.5"
|
||||||
|
:width="14"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span class="ml-1">{{ key }}</span>
|
||||||
|
<span
|
||||||
|
v-if="node.type === 'array'"
|
||||||
|
class="text-xs bg-base-200 rounded px-1 ml-1"
|
||||||
|
>{{ t('list') }}</span>
|
||||||
|
</label>
|
||||||
|
<div class="hidden peer-has-[:checked]:block pl-5">
|
||||||
|
<template
|
||||||
|
v-for="[varNode, varPath] in nestedVariables(node, key)"
|
||||||
|
:key="varPath"
|
||||||
|
>
|
||||||
|
<hr class="border-base-300">
|
||||||
|
<DynamicVariable
|
||||||
|
:path="varPath"
|
||||||
|
:group-key="key"
|
||||||
|
:editable="editable"
|
||||||
|
:schema="varNode"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<hr
|
||||||
|
v-if="index > 0"
|
||||||
|
class="border-base-300"
|
||||||
|
>
|
||||||
|
<DynamicVariable
|
||||||
|
:path="key"
|
||||||
|
:editable="editable"
|
||||||
|
:schema="node.type === 'array' && node.items ? node.items : node"
|
||||||
|
:is-array="node.type === 'array'"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import DynamicVariable from './dynamic_variable'
|
||||||
|
import { IconChevronDown, IconChevronRight } from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DynamicVariables',
|
||||||
|
components: {
|
||||||
|
DynamicVariable,
|
||||||
|
IconChevronDown,
|
||||||
|
IconChevronRight
|
||||||
|
},
|
||||||
|
inject: ['t', 'template', 'save', 'backgroundColor'],
|
||||||
|
props: {
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
schemaEntries () {
|
||||||
|
return Object.entries(this.template.variables_schema || {}).filter(([, node]) => !node.disabled)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
isGroup (node) {
|
||||||
|
return (node.type === 'object' && node.properties) || (node.type === 'array' && node.items?.type === 'object' && node.items?.properties)
|
||||||
|
},
|
||||||
|
nestedVariables (node, groupKey) {
|
||||||
|
const properties = node.type === 'array' ? node.items?.properties : node.properties
|
||||||
|
|
||||||
|
if (!properties) return []
|
||||||
|
|
||||||
|
const prefix = node.type === 'array' ? `${groupKey}[]` : groupKey
|
||||||
|
|
||||||
|
return this.collectLeafVariables(properties, prefix)
|
||||||
|
},
|
||||||
|
collectLeafVariables (properties, prefix) {
|
||||||
|
return Object.entries(properties).reduce((result, [key, node]) => {
|
||||||
|
if (node.disabled) return result
|
||||||
|
|
||||||
|
const path = `${prefix}.${key}`
|
||||||
|
|
||||||
|
if (node.type === 'object' && node.properties) {
|
||||||
|
result.push(...this.collectLeafVariables(node.properties, path))
|
||||||
|
} else if (node.type === 'array' && node.items?.properties) {
|
||||||
|
result.push(...this.collectLeafVariables(node.items.properties, `${path}[]`))
|
||||||
|
} else {
|
||||||
|
result.push([node, path])
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,584 @@
|
|||||||
|
const KEYWORDS = ['if', 'else', 'for', 'end']
|
||||||
|
const TYPE_PRIORITY = { string: 3, number: 2, boolean: 1 }
|
||||||
|
const AND_OR_REGEXP = /\s+(AND|OR)\s+/i
|
||||||
|
const COMPARISON_OPERATORS_REGEXP = />=|<=|!=|==|>|<|=/
|
||||||
|
|
||||||
|
function buildTokens (elem, acc = []) {
|
||||||
|
if (elem.nodeType === Node.TEXT_NODE) {
|
||||||
|
if (elem.textContent) {
|
||||||
|
const text = elem.textContent
|
||||||
|
const re = /[[\]]/g
|
||||||
|
let match
|
||||||
|
let found = false
|
||||||
|
|
||||||
|
while ((match = re.exec(text)) !== null) {
|
||||||
|
found = true
|
||||||
|
|
||||||
|
acc.push({
|
||||||
|
elem,
|
||||||
|
value: match[0],
|
||||||
|
textLength: text.length,
|
||||||
|
index: match.index
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
acc.push({ elem, value: '', textLength: 0, index: 0 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const child of elem.childNodes) {
|
||||||
|
buildTokens(child, acc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokensPair (cur, nxt) {
|
||||||
|
if (cur.elem === nxt.elem) {
|
||||||
|
return cur.elem.textContent.slice(cur.index + 1, nxt.index).trim() === ''
|
||||||
|
} else {
|
||||||
|
return cur.elem.textContent.slice(cur.index + 1).trim() === '' &&
|
||||||
|
nxt.elem.textContent.slice(0, nxt.index).trim() === ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTags (tokens) {
|
||||||
|
const normalized = []
|
||||||
|
|
||||||
|
for (let i = 0; i < tokens.length - 1; i++) {
|
||||||
|
const cur = tokens[i]
|
||||||
|
const nxt = tokens[i + 1]
|
||||||
|
|
||||||
|
if (cur.value === '[' && nxt.value === '[' && tokensPair(cur, nxt)) {
|
||||||
|
normalized.push(['open', cur])
|
||||||
|
} else if (cur.value === ']' && nxt.value === ']' && tokensPair(cur, nxt)) {
|
||||||
|
normalized.push(['close', nxt])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags = []
|
||||||
|
|
||||||
|
for (let i = 0; i < normalized.length - 1; i++) {
|
||||||
|
const [curOp, openToken] = normalized[i]
|
||||||
|
const [nxtOp, closeToken] = normalized[i + 1]
|
||||||
|
|
||||||
|
if (curOp === 'open' && nxtOp === 'close') {
|
||||||
|
tags.push({ openToken, closeToken, value: '' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags
|
||||||
|
}
|
||||||
|
|
||||||
|
function findTextNodesInBranch (elements, toElem, acc) {
|
||||||
|
if (!elements || elements.length === 0) return acc
|
||||||
|
|
||||||
|
for (const elem of elements) {
|
||||||
|
if (elem.nodeType === Node.TEXT_NODE) {
|
||||||
|
acc.push(elem)
|
||||||
|
} else {
|
||||||
|
findTextNodesInBranch(Array.from(elem.childNodes), toElem, acc)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (acc.length > 0 && acc[acc.length - 1] === toElem) return acc
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
|
||||||
|
function findTextNodesBetween (fromElem, toElem, acc = []) {
|
||||||
|
if (fromElem === toElem) return [fromElem]
|
||||||
|
|
||||||
|
let currentElement = fromElem
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const parent = currentElement.parentNode
|
||||||
|
|
||||||
|
if (!parent) return acc
|
||||||
|
|
||||||
|
const children = Array.from(parent.childNodes)
|
||||||
|
const startIndex = children.indexOf(currentElement)
|
||||||
|
|
||||||
|
if (startIndex === -1) return acc
|
||||||
|
|
||||||
|
const elementsInBranch = children.slice(startIndex)
|
||||||
|
|
||||||
|
findTextNodesInBranch(elementsInBranch, toElem, acc)
|
||||||
|
|
||||||
|
if (acc.length > 0 && acc[acc.length - 1] === toElem) return acc
|
||||||
|
|
||||||
|
let p = elementsInBranch[0].parentNode
|
||||||
|
|
||||||
|
while (p && !p.nextSibling) {
|
||||||
|
p = p.parentNode
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!p || !p.nextSibling) return acc
|
||||||
|
|
||||||
|
currentElement = p.nextSibling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapTagValues (tags) {
|
||||||
|
for (const tag of tags) {
|
||||||
|
const textNodes = findTextNodesBetween(tag.openToken.elem, tag.closeToken.elem)
|
||||||
|
|
||||||
|
for (const elem of textNodes) {
|
||||||
|
let part
|
||||||
|
|
||||||
|
if (tag.openToken.elem === elem && tag.closeToken.elem === elem) {
|
||||||
|
part = elem.textContent.slice(tag.openToken.index, tag.closeToken.index + 1)
|
||||||
|
} else if (tag.openToken.elem === elem) {
|
||||||
|
part = elem.textContent.slice(tag.openToken.index)
|
||||||
|
} else if (tag.closeToken.elem === elem) {
|
||||||
|
part = elem.textContent.slice(0, tag.closeToken.index + 1)
|
||||||
|
} else {
|
||||||
|
part = elem.textContent
|
||||||
|
}
|
||||||
|
|
||||||
|
tag.value += part
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTagTypeName (tagString) {
|
||||||
|
const val = tagString.replace(/[[\]]/g, '').trim()
|
||||||
|
const parts = val.split(':').map((s) => s.trim())
|
||||||
|
|
||||||
|
if (parts.length === 2 && KEYWORDS.includes(parts[0])) {
|
||||||
|
return [parts[0], parts[1]]
|
||||||
|
} else if (KEYWORDS.includes(val)) {
|
||||||
|
return [val, null]
|
||||||
|
} else {
|
||||||
|
return ['var', val]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSimpleVariable (str) {
|
||||||
|
const s = str.trim()
|
||||||
|
|
||||||
|
return !AND_OR_REGEXP.test(s) &&
|
||||||
|
!COMPARISON_OPERATORS_REGEXP.test(s) &&
|
||||||
|
!s.includes('(') &&
|
||||||
|
!s.includes('!') &&
|
||||||
|
!s.includes('&&') &&
|
||||||
|
!s.includes('||') &&
|
||||||
|
!s.startsWith('"') &&
|
||||||
|
!s.startsWith("'") &&
|
||||||
|
!/^-?\d/.test(s) &&
|
||||||
|
!/^(true|false)$/i.test(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenizeCondition (str) {
|
||||||
|
const tokens = []
|
||||||
|
let pos = 0
|
||||||
|
|
||||||
|
str = str.trim()
|
||||||
|
|
||||||
|
while (pos < str.length) {
|
||||||
|
const rest = str.slice(pos)
|
||||||
|
let m
|
||||||
|
|
||||||
|
if ((m = rest.match(/^\s+/))) {
|
||||||
|
pos += m[0].length
|
||||||
|
} else if ((m = rest.match(/^(>=|<=|!=|==|>|<|=)/))) {
|
||||||
|
tokens.push({ type: 'operator', value: m[1] })
|
||||||
|
pos += m[1].length
|
||||||
|
} else if (rest[0] === '!') {
|
||||||
|
tokens.push({ type: 'not', value: '!' })
|
||||||
|
pos += 1
|
||||||
|
} else if (rest[0] === '(') {
|
||||||
|
tokens.push({ type: 'lparen', value: '(' })
|
||||||
|
pos += 1
|
||||||
|
} else if (rest[0] === ')') {
|
||||||
|
tokens.push({ type: 'rparen', value: ')' })
|
||||||
|
pos += 1
|
||||||
|
} else if (rest.startsWith('&&')) {
|
||||||
|
tokens.push({ type: 'and', value: 'AND' })
|
||||||
|
pos += 2
|
||||||
|
} else if ((m = rest.match(/^AND\b/i))) {
|
||||||
|
tokens.push({ type: 'and', value: 'AND' })
|
||||||
|
pos += 3
|
||||||
|
} else if (rest.startsWith('||')) {
|
||||||
|
tokens.push({ type: 'or', value: 'OR' })
|
||||||
|
pos += 2
|
||||||
|
} else if ((m = rest.match(/^OR\b/i))) {
|
||||||
|
tokens.push({ type: 'or', value: 'OR' })
|
||||||
|
pos += 2
|
||||||
|
} else if ((m = rest.match(/^"([^"]*)"/) || rest.match(/^'([^']*)'/))) {
|
||||||
|
tokens.push({ type: 'string', value: m[1] })
|
||||||
|
pos += m[0].length
|
||||||
|
} else if ((m = rest.match(/^(-?\d+\.?\d*)/))) {
|
||||||
|
tokens.push({ type: 'number', value: m[1].includes('.') ? parseFloat(m[1]) : parseInt(m[1], 10) })
|
||||||
|
pos += m[1].length
|
||||||
|
} else if ((m = rest.match(/^(true|false)\b/i))) {
|
||||||
|
tokens.push({ type: 'boolean', value: m[1].toLowerCase() === 'true' })
|
||||||
|
pos += m[1].length
|
||||||
|
} else if ((m = rest.match(/^([\p{L}_][\p{L}\p{N}_.]*)/u))) {
|
||||||
|
tokens.push({ type: 'variable', value: m[1] })
|
||||||
|
pos += m[1].length
|
||||||
|
} else {
|
||||||
|
pos += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseOrExpr (tokens, pos) {
|
||||||
|
let left, right
|
||||||
|
|
||||||
|
;[left, pos] = parseAndExpr(tokens, pos)
|
||||||
|
|
||||||
|
while (pos < tokens.length && tokens[pos].type === 'or') {
|
||||||
|
pos += 1
|
||||||
|
;[right, pos] = parseAndExpr(tokens, pos)
|
||||||
|
left = { type: 'or', left, right }
|
||||||
|
}
|
||||||
|
|
||||||
|
return [left, pos]
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAndExpr (tokens, pos) {
|
||||||
|
let left, right
|
||||||
|
|
||||||
|
;[left, pos] = parsePrimary(tokens, pos)
|
||||||
|
|
||||||
|
while (pos < tokens.length && tokens[pos].type === 'and') {
|
||||||
|
pos += 1
|
||||||
|
;[right, pos] = parsePrimary(tokens, pos)
|
||||||
|
left = { type: 'and', left, right }
|
||||||
|
}
|
||||||
|
|
||||||
|
return [left, pos]
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePrimary (tokens, pos) {
|
||||||
|
if (pos >= tokens.length) return [null, pos]
|
||||||
|
|
||||||
|
if (tokens[pos].type === 'not') {
|
||||||
|
const [child, p] = parsePrimary(tokens, pos + 1)
|
||||||
|
|
||||||
|
return [{ type: 'not', child }, p]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokens[pos].type === 'lparen') {
|
||||||
|
const [node, p] = parseOrExpr(tokens, pos + 1)
|
||||||
|
|
||||||
|
return [node, p < tokens.length && tokens[p].type === 'rparen' ? p + 1 : p]
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseComparisonOrPresence(tokens, pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseComparisonOrPresence (tokens, pos) {
|
||||||
|
if (pos >= tokens.length || tokens[pos].type !== 'variable') return [null, pos]
|
||||||
|
|
||||||
|
const variableName = tokens[pos].value
|
||||||
|
|
||||||
|
pos += 1
|
||||||
|
|
||||||
|
if (pos < tokens.length && tokens[pos].type === 'operator') {
|
||||||
|
let operator = tokens[pos].value
|
||||||
|
|
||||||
|
if (operator === '=') operator = '=='
|
||||||
|
|
||||||
|
pos += 1
|
||||||
|
|
||||||
|
if (pos < tokens.length && ['string', 'number', 'variable', 'boolean'].includes(tokens[pos].type)) {
|
||||||
|
const valueToken = tokens[pos]
|
||||||
|
|
||||||
|
return [{
|
||||||
|
type: 'comparison',
|
||||||
|
variableName,
|
||||||
|
operator,
|
||||||
|
value: valueToken.value,
|
||||||
|
valueIsVariable: valueToken.type === 'variable'
|
||||||
|
}, pos + 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [{ type: 'presence', variableName }, pos]
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCondition (conditionString) {
|
||||||
|
const stripped = conditionString.trim()
|
||||||
|
|
||||||
|
if (stripped.startsWith('!') && isSimpleVariable(stripped.slice(1))) {
|
||||||
|
return { type: 'not', child: { type: 'presence', variableName: stripped.slice(1) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSimpleVariable(stripped)) {
|
||||||
|
return { type: 'presence', variableName: stripped }
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = tokenizeCondition(stripped)
|
||||||
|
const [ast] = parseOrExpr(tokens, 0)
|
||||||
|
|
||||||
|
return ast
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractConditionVariables (node, acc = []) {
|
||||||
|
if (!node) return acc
|
||||||
|
|
||||||
|
switch (node.type) {
|
||||||
|
case 'or':
|
||||||
|
case 'and':
|
||||||
|
extractConditionVariables(node.left, acc)
|
||||||
|
extractConditionVariables(node.right, acc)
|
||||||
|
break
|
||||||
|
case 'not':
|
||||||
|
extractConditionVariables(node.child, acc)
|
||||||
|
break
|
||||||
|
case 'comparison':
|
||||||
|
acc.push({
|
||||||
|
name: node.variableName,
|
||||||
|
type: node.valueIsVariable ? null : (typeof node.value === 'boolean' ? 'boolean' : (typeof node.value === 'number' ? 'number' : 'string'))
|
||||||
|
})
|
||||||
|
|
||||||
|
if (node.valueIsVariable) {
|
||||||
|
acc.push({ name: node.value, type: null })
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
case 'presence':
|
||||||
|
acc.push({ name: node.variableName, type: 'boolean' })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
|
||||||
|
const IRREGULAR_PLURALS = {
|
||||||
|
people: 'person',
|
||||||
|
men: 'man',
|
||||||
|
women: 'woman',
|
||||||
|
children: 'child',
|
||||||
|
teeth: 'tooth',
|
||||||
|
feet: 'foot',
|
||||||
|
mice: 'mouse',
|
||||||
|
geese: 'goose',
|
||||||
|
oxen: 'ox',
|
||||||
|
criteria: 'criterion',
|
||||||
|
phenomena: 'phenomenon',
|
||||||
|
alumni: 'alumnus',
|
||||||
|
data: 'datum',
|
||||||
|
media: 'medium'
|
||||||
|
}
|
||||||
|
|
||||||
|
function singularize (word) {
|
||||||
|
if (IRREGULAR_PLURALS[word]) return IRREGULAR_PLURALS[word]
|
||||||
|
|
||||||
|
if (word.endsWith('ies')) return word.slice(0, -3) + 'y'
|
||||||
|
if (word.endsWith('ches') || word.endsWith('shes')) return word.slice(0, -2)
|
||||||
|
if (word.endsWith('ses') || word.endsWith('xes') || word.endsWith('zes')) return word.slice(0, -2)
|
||||||
|
if (word.endsWith('s') && !word.endsWith('ss')) return word.slice(0, -1)
|
||||||
|
|
||||||
|
return word
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOperators (tags) {
|
||||||
|
const operators = []
|
||||||
|
const stack = [{ children: operators, operator: null }]
|
||||||
|
|
||||||
|
for (const tag of tags) {
|
||||||
|
const [type, variableName] = parseTagTypeName(tag.value)
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'for':
|
||||||
|
case 'if': {
|
||||||
|
const operator = { type, variableName, tag, children: [] }
|
||||||
|
|
||||||
|
if (type === 'if') {
|
||||||
|
try {
|
||||||
|
operator.condition = parseCondition(variableName)
|
||||||
|
} catch (e) {
|
||||||
|
// ignore parse errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stack[stack.length - 1].children.push(operator)
|
||||||
|
stack.push({ children: operator.children, operator })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'else': {
|
||||||
|
const current = stack[stack.length - 1]
|
||||||
|
|
||||||
|
if (current.operator && current.operator.type === 'if') {
|
||||||
|
current.operator.elseTag = tag
|
||||||
|
current.operator.elseChildren = []
|
||||||
|
current.children = current.operator.elseChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'end': {
|
||||||
|
const popped = stack.pop()
|
||||||
|
|
||||||
|
if (popped.operator) {
|
||||||
|
popped.operator.endTag = tag
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'var':
|
||||||
|
stack[stack.length - 1].children.push({ type, variableName, tag })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return operators
|
||||||
|
}
|
||||||
|
|
||||||
|
function assignNestedSchema (propertiesHash, parentProperties, keyString, value) {
|
||||||
|
const keys = keyString.split('.')
|
||||||
|
const lastKey = keys.pop()
|
||||||
|
|
||||||
|
let currentLevel = null
|
||||||
|
|
||||||
|
if (keys.length > 0 && parentProperties[keys[0]]) {
|
||||||
|
currentLevel = keys.reduce((current, key) => {
|
||||||
|
if (!current[key]) current[key] = { type: 'object', properties: {} }
|
||||||
|
|
||||||
|
return current[key].properties
|
||||||
|
}, parentProperties)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentLevel) {
|
||||||
|
currentLevel = keys.reduce((current, key) => {
|
||||||
|
if (!current[key]) current[key] = { type: 'object', properties: {} }
|
||||||
|
|
||||||
|
return current[key].properties
|
||||||
|
}, propertiesHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentLevel[lastKey] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
function assignNestedSchemaWithPriority (propertiesHash, parentProperties, keyString, newType) {
|
||||||
|
const keys = keyString.split('.')
|
||||||
|
const lastKey = keys.pop()
|
||||||
|
|
||||||
|
let currentLevel = null
|
||||||
|
|
||||||
|
if (keys.length > 0 && parentProperties[keys[0]]) {
|
||||||
|
currentLevel = keys.reduce((current, key) => {
|
||||||
|
if (!current[key]) current[key] = { type: 'object', properties: {} }
|
||||||
|
|
||||||
|
return current[key].properties
|
||||||
|
}, parentProperties)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentLevel) {
|
||||||
|
currentLevel = keys.reduce((current, key) => {
|
||||||
|
if (!current[key]) current[key] = { type: 'object', properties: {} }
|
||||||
|
|
||||||
|
return current[key].properties
|
||||||
|
}, propertiesHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = currentLevel[lastKey]
|
||||||
|
|
||||||
|
if (existing && (TYPE_PRIORITY[newType] || 0) <= (TYPE_PRIORITY[existing.type] || 0)) return
|
||||||
|
|
||||||
|
currentLevel[lastKey] = { type: newType }
|
||||||
|
}
|
||||||
|
|
||||||
|
function processConditionVariables (condition, propertiesHash, parentProperties) {
|
||||||
|
const variables = extractConditionVariables(condition)
|
||||||
|
|
||||||
|
for (const varInfo of variables) {
|
||||||
|
assignNestedSchemaWithPriority(propertiesHash, parentProperties, varInfo.name, varInfo.type || 'boolean')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processOperators (operators, propertiesHash = {}, parentProperties = {}) {
|
||||||
|
if (!operators || operators.length === 0) return propertiesHash
|
||||||
|
|
||||||
|
for (const op of operators) {
|
||||||
|
switch (op.type) {
|
||||||
|
case 'var': {
|
||||||
|
if (!op.variableName.includes('.') && parentProperties[op.variableName]) {
|
||||||
|
const item = parentProperties[op.variableName]
|
||||||
|
|
||||||
|
if (item && item.type === 'object' && item.properties && Object.keys(item.properties).length === 0) {
|
||||||
|
delete item.properties
|
||||||
|
item.type = 'string'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assignNestedSchema(propertiesHash, parentProperties, op.variableName, { type: 'string' })
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'if':
|
||||||
|
if (op.condition) {
|
||||||
|
processConditionVariables(op.condition, propertiesHash, parentProperties)
|
||||||
|
}
|
||||||
|
|
||||||
|
processOperators(op.children, propertiesHash, parentProperties)
|
||||||
|
processOperators(op.elseChildren, propertiesHash, parentProperties)
|
||||||
|
break
|
||||||
|
case 'for': {
|
||||||
|
const parts = op.variableName.split('.')
|
||||||
|
const singularKey = singularize(parts[parts.length - 1])
|
||||||
|
|
||||||
|
let itemProperties = parentProperties[singularKey]?.items
|
||||||
|
itemProperties = itemProperties || propertiesHash[parts[0]]?.items
|
||||||
|
itemProperties = itemProperties || { type: 'object', properties: {} }
|
||||||
|
|
||||||
|
assignNestedSchema(propertiesHash, parentProperties, op.variableName, { type: 'array', items: itemProperties })
|
||||||
|
processOperators(op.children, propertiesHash, { ...parentProperties, [singularKey]: itemProperties })
|
||||||
|
|
||||||
|
if (itemProperties.type === 'object' && itemProperties.properties && Object.keys(itemProperties.properties).length === 0) {
|
||||||
|
delete itemProperties.properties
|
||||||
|
itemProperties.type = 'string'
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return propertiesHash
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeSchemaProperties (target, source) {
|
||||||
|
for (const key of Object.keys(source)) {
|
||||||
|
if (!target[key]) {
|
||||||
|
target[key] = source[key]
|
||||||
|
} else if (target[key].type === 'object' && source[key].type === 'object') {
|
||||||
|
if (!target[key].properties) target[key].properties = {}
|
||||||
|
if (source[key].properties) {
|
||||||
|
mergeSchemaProperties(target[key].properties, source[key].properties)
|
||||||
|
}
|
||||||
|
} else if (target[key].type === 'array' && source[key].type === 'array') {
|
||||||
|
if (source[key].items && source[key].items.properties) {
|
||||||
|
if (!target[key].items) {
|
||||||
|
target[key].items = source[key].items
|
||||||
|
} else if (target[key].items.properties) {
|
||||||
|
mergeSchemaProperties(target[key].items.properties, source[key].items.properties)
|
||||||
|
}
|
||||||
|
} else if (source[key].items && !target[key].items) {
|
||||||
|
target[key].items = source[key].items
|
||||||
|
}
|
||||||
|
} else if ((TYPE_PRIORITY[source[key].type] || 0) > (TYPE_PRIORITY[target[key].type] || 0)) {
|
||||||
|
target[key] = source[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return target
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildVariablesSchema (dom) {
|
||||||
|
const tokens = buildTokens(dom)
|
||||||
|
const tags = mapTagValues(buildTags(tokens))
|
||||||
|
const operators = buildOperators(tags)
|
||||||
|
|
||||||
|
return processOperators(operators)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { buildVariablesSchema, mergeSchemaProperties, buildOperators, buildTokens, buildTags, mapTagValues }
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="list-field group"
|
class="list-field group"
|
||||||
|
:class="`list-field-${field.type}`"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="border border-base-300 rounded relative group fields-list-item"
|
class="border border-base-300 rounded relative group fields-list-item"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
v-if="showRequired"
|
v-if="showRequired"
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
|
class="field-settings-required w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<label
|
<label
|
||||||
v-if="showReadOnly"
|
v-if="showReadOnly"
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
|
class="field-settings-read-only w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<label
|
<label
|
||||||
v-if="showPrefillable"
|
v-if="showPrefillable"
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
|
class="field-settings-prefillable w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<label
|
<label
|
||||||
v-if="showSetSigningDate"
|
v-if="showSetSigningDate"
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
|
class="field-settings-set-signing-date w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<label
|
<label
|
||||||
v-if="showWithLogo"
|
v-if="showWithLogo"
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
|
class="field-settings-with-logo w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<label
|
<label
|
||||||
v-if="showSignatureId"
|
v-if="showSignatureId"
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
|
class="field-settings-signature-id w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<label
|
<label
|
||||||
v-if="showChecked"
|
v-if="showChecked"
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
|
class="field-settings-checked w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -113,6 +113,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<ContextSubmenu
|
<ContextSubmenu
|
||||||
v-if="showVerificationMethod"
|
v-if="showVerificationMethod"
|
||||||
|
class="field-settings-verification-method"
|
||||||
:icon="IconId"
|
:icon="IconId"
|
||||||
:label="t('method')"
|
:label="t('method')"
|
||||||
:options="methodOptions"
|
:options="methodOptions"
|
||||||
@@ -125,6 +126,7 @@
|
|||||||
>
|
>
|
||||||
<ContextSubmenu
|
<ContextSubmenu
|
||||||
v-if="showFormatSubmenu"
|
v-if="showFormatSubmenu"
|
||||||
|
class="field-settings-format"
|
||||||
:icon="IconAdjustmentsHorizontal"
|
:icon="IconAdjustmentsHorizontal"
|
||||||
:label="t('format')"
|
:label="t('format')"
|
||||||
:options="formatOptions"
|
:options="formatOptions"
|
||||||
@@ -133,6 +135,7 @@
|
|||||||
/>
|
/>
|
||||||
<ContextSubmenu
|
<ContextSubmenu
|
||||||
v-if="showValidationSubmenu && field.type !== 'number'"
|
v-if="showValidationSubmenu && field.type !== 'number'"
|
||||||
|
class="field-settings-validation"
|
||||||
:icon="IconInputCheck"
|
:icon="IconInputCheck"
|
||||||
:label="t('validation')"
|
:label="t('validation')"
|
||||||
:options="validationMenuItems.map(k => ({ value: k, label: t(k) }))"
|
:options="validationMenuItems.map(k => ({ value: k, label: t(k) }))"
|
||||||
@@ -141,7 +144,7 @@
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="field.type === 'number'"
|
v-if="field.type === 'number'"
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
class="field-settings-validation w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
@click.stop="openNumberRangeModal"
|
@click.stop="openNumberRangeModal"
|
||||||
>
|
>
|
||||||
<IconInputCheck class="w-4 h-4" />
|
<IconInputCheck class="w-4 h-4" />
|
||||||
@@ -149,6 +152,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<ContextSubmenu
|
<ContextSubmenu
|
||||||
v-if="showPaymentSettings"
|
v-if="showPaymentSettings"
|
||||||
|
class="field-settings-currency"
|
||||||
:icon="IconCash"
|
:icon="IconCash"
|
||||||
:label="t('currency')"
|
:label="t('currency')"
|
||||||
:options="currencyOptions"
|
:options="currencyOptions"
|
||||||
@@ -157,6 +161,7 @@
|
|||||||
/>
|
/>
|
||||||
<ContextSubmenu
|
<ContextSubmenu
|
||||||
v-if="showPaymentSettings"
|
v-if="showPaymentSettings"
|
||||||
|
class="field-settings-price"
|
||||||
:icon="IconCoins"
|
:icon="IconCoins"
|
||||||
:label="t('price')"
|
:label="t('price')"
|
||||||
:options="priceTypeOptions"
|
:options="priceTypeOptions"
|
||||||
@@ -165,7 +170,7 @@
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="showFont"
|
v-if="showFont"
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
class="field-settings-font w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
@click.stop="openFontModal"
|
@click.stop="openFontModal"
|
||||||
>
|
>
|
||||||
<IconTypography class="w-4 h-4" />
|
<IconTypography class="w-4 h-4" />
|
||||||
@@ -173,7 +178,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="showDescription"
|
v-if="showDescription"
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
class="field-settings-description w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
@click.stop="openDescriptionModal"
|
@click.stop="openDescriptionModal"
|
||||||
>
|
>
|
||||||
<IconInfoCircle class="w-4 h-4" />
|
<IconInfoCircle class="w-4 h-4" />
|
||||||
@@ -181,7 +186,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="showCondition"
|
v-if="showCondition"
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
|
class="field-settings-condition w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
|
||||||
@click.stop="openConditionModal"
|
@click.stop="openConditionModal"
|
||||||
>
|
>
|
||||||
<span class="flex items-center space-x-2">
|
<span class="flex items-center space-x-2">
|
||||||
@@ -196,7 +201,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="showFormula"
|
v-if="showFormula"
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
class="field-settings-formula w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
@click.stop="openFormulaModal"
|
@click.stop="openFormulaModal"
|
||||||
>
|
>
|
||||||
<IconMathFunction class="w-4 h-4" />
|
<IconMathFunction class="w-4 h-4" />
|
||||||
@@ -207,7 +212,7 @@
|
|||||||
class="my-1 border-neutral-200"
|
class="my-1 border-neutral-200"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
|
class="field-settings-copy w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
|
||||||
@click.stop="$emit('copy')"
|
@click.stop="$emit('copy')"
|
||||||
>
|
>
|
||||||
<span class="flex items-center space-x-2">
|
<span class="flex items-center space-x-2">
|
||||||
@@ -217,7 +222,7 @@
|
|||||||
<span class="text-xs text-base-content/60 ml-4">{{ isMac ? '⌘C' : 'Ctrl+C' }}</span>
|
<span class="text-xs text-base-content/60 ml-4">{{ isMac ? '⌘C' : 'Ctrl+C' }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm text-red-600"
|
class="field-settings-remove w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm text-red-600"
|
||||||
@click.stop="$emit('delete')"
|
@click.stop="$emit('delete')"
|
||||||
>
|
>
|
||||||
<span class="flex items-center space-x-2">
|
<span class="flex items-center space-x-2">
|
||||||
@@ -228,12 +233,13 @@
|
|||||||
</button>
|
</button>
|
||||||
<ContextSubmenu
|
<ContextSubmenu
|
||||||
v-if="showMoreSubmenu"
|
v-if="showMoreSubmenu"
|
||||||
|
class="field-settings-more"
|
||||||
:icon="IconDots"
|
:icon="IconDots"
|
||||||
:label="t('more')"
|
:label="t('more')"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-if="showDrawNewArea"
|
v-if="showDrawNewArea"
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
|
class="field-settings-draw-new-area w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
|
||||||
@click="handleMoreSelect('draw_new_area')"
|
@click="handleMoreSelect('draw_new_area')"
|
||||||
>
|
>
|
||||||
<IconNewSection class="w-4 h-4" />
|
<IconNewSection class="w-4 h-4" />
|
||||||
@@ -241,7 +247,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="showCopyToAllPages"
|
v-if="showCopyToAllPages"
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
|
class="field-settings-copy-to-all-pages w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
|
||||||
@click="handleMoreSelect('copy_to_all_pages')"
|
@click="handleMoreSelect('copy_to_all_pages')"
|
||||||
>
|
>
|
||||||
<IconCopy class="w-4 h-4" />
|
<IconCopy class="w-4 h-4" />
|
||||||
@@ -249,7 +255,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="showSaveAsCustom"
|
v-if="showSaveAsCustom"
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
|
class="field-settings-save-as-custom-field w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
|
||||||
@click="handleMoreSelect('save_as_custom')"
|
@click="handleMoreSelect('save_as_custom')"
|
||||||
>
|
>
|
||||||
<IconForms class="w-4 h-4" />
|
<IconForms class="w-4 h-4" />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="field.type === 'verification'"
|
v-if="field.type === 'verification'"
|
||||||
class="py-1.5 px-1 relative"
|
class="field-settings-verification-method py-1.5 px-1 relative"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<select
|
<select
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="['select', 'radio'].includes(field.type) && !defaultField"
|
v-if="['select', 'radio'].includes(field.type) && !defaultField"
|
||||||
class="py-1.5 px-1 relative"
|
class="field-settings-default-value py-1.5 px-1 relative"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<select
|
<select
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="['text', 'number'].includes(field.type) && !defaultField"
|
v-if="['text', 'number'].includes(field.type) && !defaultField"
|
||||||
class="py-1.5 px-1 relative"
|
class="field-settings-default-value py-1.5 px-1 relative"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="['text', 'cells'].includes(field.type)"
|
v-if="['text', 'cells'].includes(field.type)"
|
||||||
class="py-1.5 px-1 relative"
|
class="field-settings-validation py-1.5 px-1 relative"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<select
|
<select
|
||||||
@@ -122,7 +122,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="['text', 'cells'].includes(field.type) && field.validation && lengthValidation"
|
v-if="['text', 'cells'].includes(field.type) && field.validation && lengthValidation"
|
||||||
class="py-1.5 px-1 relative flex space-x-1"
|
class="field-settings-length-validation py-1.5 px-1 relative flex space-x-1"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<div class="w-1/2 relative">
|
<div class="w-1/2 relative">
|
||||||
@@ -166,7 +166,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="field.type === 'number'"
|
v-if="field.type === 'number'"
|
||||||
class="py-1.5 px-1 relative flex space-x-1"
|
class="field-settings-number-range py-1.5 px-1 relative flex space-x-1"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<div class="w-1/2 relative">
|
<div class="w-1/2 relative">
|
||||||
@@ -210,7 +210,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="field.type === 'number'"
|
v-if="field.type === 'number'"
|
||||||
class="py-1.5 px-1 relative"
|
class="field-settings-number-format py-1.5 px-1 relative"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<select
|
<select
|
||||||
@@ -237,7 +237,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation"
|
v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation"
|
||||||
class="py-1.5 px-1 relative"
|
class="field-settings-custom-validation py-1.5 px-1 relative"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -259,7 +259,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation"
|
v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation"
|
||||||
class="py-1.5 px-1 relative"
|
class="field-settings-error-message py-1.5 px-1 relative"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -280,7 +280,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="field.type === 'date'"
|
v-if="field.type === 'date'"
|
||||||
class="py-1.5 px-1 relative"
|
class="field-settings-date-format py-1.5 px-1 relative"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<select
|
<select
|
||||||
@@ -307,7 +307,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="field.type === 'signature'"
|
v-if="field.type === 'signature'"
|
||||||
class="py-1.5 px-1 relative"
|
class="field-settings-signature-format py-1.5 px-1 relative"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<select
|
<select
|
||||||
@@ -340,6 +340,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<li
|
<li
|
||||||
v-if="[true, false].includes(withSignatureId) && field.type === 'signature'"
|
v-if="[true, false].includes(withSignatureId) && field.type === 'signature'"
|
||||||
|
class="field-settings-signature-id"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<label class="cursor-pointer py-1.5">
|
<label class="cursor-pointer py-1.5">
|
||||||
@@ -355,6 +356,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="withRequired && field.type !== 'phone' && field.type !== 'stamp' && field.type !== 'verification' && field.type !== 'strikethrough' && field.type !== 'heading'"
|
v-if="withRequired && field.type !== 'phone' && field.type !== 'stamp' && field.type !== 'verification' && field.type !== 'strikethrough' && field.type !== 'heading'"
|
||||||
|
class="field-settings-required"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<label class="cursor-pointer py-1.5">
|
<label class="cursor-pointer py-1.5">
|
||||||
@@ -370,6 +372,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="field.type == 'stamp'"
|
v-if="field.type == 'stamp'"
|
||||||
|
class="field-settings-with-logo"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<label class="cursor-pointer py-1.5">
|
<label class="cursor-pointer py-1.5">
|
||||||
@@ -384,6 +387,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="field.type == 'checkbox'"
|
v-if="field.type == 'checkbox'"
|
||||||
|
class="field-settings-checked"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<label class="cursor-pointer py-1.5">
|
<label class="cursor-pointer py-1.5">
|
||||||
@@ -398,6 +402,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="field.type == 'date'"
|
v-if="field.type == 'date'"
|
||||||
|
class="field-settings-set-signing-date"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<label class="cursor-pointer py-1.5">
|
<label class="cursor-pointer py-1.5">
|
||||||
@@ -412,6 +417,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="['text', 'number', 'radio', 'multiple', 'select'].includes(field.type)"
|
v-if="['text', 'number', 'radio', 'multiple', 'select'].includes(field.type)"
|
||||||
|
class="field-settings-read-only"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<label class="cursor-pointer py-1.5">
|
<label class="cursor-pointer py-1.5">
|
||||||
@@ -427,6 +433,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="withPrefillable && prefillableFieldTypes.includes(field['type'])"
|
v-if="withPrefillable && prefillableFieldTypes.includes(field['type'])"
|
||||||
|
class="field-settings-prefillable"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<label class="cursor-pointer py-1.5">
|
<label class="cursor-pointer py-1.5">
|
||||||
@@ -444,7 +451,10 @@
|
|||||||
v-if="field.type != 'stamp'"
|
v-if="field.type != 'stamp'"
|
||||||
class="pb-0.5 mt-0.5"
|
class="pb-0.5 mt-0.5"
|
||||||
>
|
>
|
||||||
<li v-if="['text', 'number', 'date', 'select', 'heading', 'cells'].includes(field.type)">
|
<li
|
||||||
|
v-if="['text', 'number', 'date', 'select', 'heading', 'cells'].includes(field.type)"
|
||||||
|
class="field-settings-font"
|
||||||
|
>
|
||||||
<label
|
<label
|
||||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||||
@click="$emit('click-font')"
|
@click="$emit('click-font')"
|
||||||
@@ -459,6 +469,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="field.type != 'stamp' && field.type != 'heading' && field.type != 'strikethrough'"
|
v-if="field.type != 'stamp' && field.type != 'heading' && field.type != 'strikethrough'"
|
||||||
|
class="field-settings-description"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||||
@@ -474,6 +485,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="withCondition && field.type != 'stamp' && field.type != 'heading'"
|
v-if="withCondition && field.type != 'stamp' && field.type != 'heading'"
|
||||||
|
class="field-settings-condition"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||||
@@ -487,7 +499,10 @@
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="field.type == 'number'">
|
<li
|
||||||
|
v-if="field.type == 'number'"
|
||||||
|
class="field-settings-formula"
|
||||||
|
>
|
||||||
<label
|
<label
|
||||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||||
@click="$emit('click-formula')"
|
@click="$emit('click-formula')"
|
||||||
@@ -501,13 +516,14 @@
|
|||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<hr
|
<hr
|
||||||
v-if="withCopyToAllPages || withAreas || withCustomFields"
|
v-if="(withCopyToAllPages && canCopyToAllPages) || withAreas || withCustomFields"
|
||||||
class="pb-0.5 mt-0.5"
|
class="pb-0.5 mt-0.5"
|
||||||
>
|
>
|
||||||
<template v-if="withAreas">
|
<template v-if="withAreas">
|
||||||
<li
|
<li
|
||||||
v-for="(area, index) in sortedAreas"
|
v-for="(area, index) in sortedAreas"
|
||||||
:key="index"
|
:key="index"
|
||||||
|
class="field-settings-area"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
@@ -527,7 +543,10 @@
|
|||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="!field.areas?.length || !['radio', 'multiple'].includes(field.type)">
|
<li
|
||||||
|
v-if="!field.areas?.length || !['radio', 'multiple'].includes(field.type)"
|
||||||
|
class="field-settings-draw-new-area"
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="text-sm py-1 px-2"
|
class="text-sm py-1 px-2"
|
||||||
@@ -541,7 +560,10 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
<li v-if="withCopyToAllPages && field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells', 'stamp'].includes(field.type)">
|
<li
|
||||||
|
v-if="withCopyToAllPages && canCopyToAllPages && field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells', 'stamp'].includes(field.type)"
|
||||||
|
class="field-settings-copy-to-all-pages"
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="text-sm py-1 px-2"
|
class="text-sm py-1 px-2"
|
||||||
@@ -554,7 +576,10 @@
|
|||||||
{{ t('copy_to_all_pages') }}
|
{{ t('copy_to_all_pages') }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="withCustomFields">
|
<li
|
||||||
|
v-if="withCustomFields"
|
||||||
|
class="field-settings-save-as-custom-field"
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="text-sm py-1 px-2"
|
class="text-sm py-1 px-2"
|
||||||
@@ -656,6 +681,15 @@ export default {
|
|||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
},
|
},
|
||||||
|
canCopyToAllPages () {
|
||||||
|
const firstArea = this.field.areas[0]
|
||||||
|
|
||||||
|
if (firstArea) {
|
||||||
|
return firstArea.page !== null && firstArea.page !== undefined
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
numberFormats () {
|
numberFormats () {
|
||||||
return [
|
return [
|
||||||
'none',
|
'none',
|
||||||
@@ -719,7 +753,7 @@ export default {
|
|||||||
return ['text', 'number', 'cells', 'date', 'checkbox', 'select', 'radio', 'phone']
|
return ['text', 'number', 'cells', 'date', 'checkbox', 'select', 'radio', 'phone']
|
||||||
},
|
},
|
||||||
sortedAreas () {
|
sortedAreas () {
|
||||||
return (this.field.areas || []).sort((a, b) => {
|
return (this.field.areas || []).filter((e) => e.page !== null && e.page !== undefined).sort((a, b) => {
|
||||||
return this.schemaAttachmentsIndexes[a.attachment_uuid] - this.schemaAttachmentsIndexes[b.attachment_uuid]
|
return this.schemaAttachmentsIndexes[a.attachment_uuid] - this.schemaAttachmentsIndexes[b.attachment_uuid]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,42 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="withStickySubmitters ? 'sticky top-0 z-[1]' : ''">
|
<div
|
||||||
|
:class="withStickySubmitters ? 'sticky top-0 z-[1]' : ''"
|
||||||
|
:style="withStickySubmitters ? { backgroundColor } : {}"
|
||||||
|
class="flex items-center gap-1"
|
||||||
|
>
|
||||||
<FieldSubmitter
|
<FieldSubmitter
|
||||||
:model-value="selectedSubmitter.uuid"
|
:model-value="selectedSubmitter.uuid"
|
||||||
class="roles-dropdown w-full rounded-lg roles-dropdown"
|
class="roles-dropdown w-full rounded-lg roles-dropdown"
|
||||||
:style="withStickySubmitters ? { backgroundColor } : {}"
|
|
||||||
:submitters="submitters"
|
:submitters="submitters"
|
||||||
:menu-style="{ overflow: 'auto', display: 'flex', flexDirection: 'row', maxHeight: 'calc(100vh - 120px)', backgroundColor: ['', null, 'transparent'].includes(backgroundColor) ? 'white' : backgroundColor }"
|
:menu-style="{ overflow: 'auto', display: 'flex', flexDirection: 'row', maxHeight: 'calc(100vh - 120px)', backgroundColor: ['', null, 'transparent'].includes(backgroundColor) ? 'white' : backgroundColor }"
|
||||||
:editable="editable && !defaultSubmitters.length"
|
:editable="editable && !defaultSubmitters.length"
|
||||||
@new-submitter="save"
|
@new-submitter="save"
|
||||||
@remove="removeSubmitter"
|
@remove="removeSubmitter"
|
||||||
@name-change="save"
|
@name-change="save"
|
||||||
@update:model-value="$emit('change-submitter', submitters.find((s) => s.uuid === $event))"
|
@update:model-value="[$emit('change-submitter', submitters.find((s) => s.uuid === $event)), isShowVariables = false]"
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
v-if="hasDynamicDocuments"
|
||||||
|
class="flex-shrink-0 rounded-md border hover:border-content flex items-center justify-center self-stretch"
|
||||||
|
:class="isShowVariables ? 'border-base-content bg-base-content text-base-100' : 'border-base-300'"
|
||||||
|
style="width: 44px"
|
||||||
|
:title="t('variables')"
|
||||||
|
@click.prevent="toggleVariables"
|
||||||
|
>
|
||||||
|
<IconBracketsContain
|
||||||
|
:width="22"
|
||||||
|
:height="22"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<DynamicVariables
|
||||||
|
v-if="isShowVariables"
|
||||||
|
:editable="editable"
|
||||||
|
class="mt-1"
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
|
v-if="!isShowVariables"
|
||||||
ref="fields"
|
ref="fields"
|
||||||
class="fields mt-2"
|
class="fields mt-2"
|
||||||
:class="{ 'mb-1': !withCustomFields || !customFields.length }"
|
:class="{ 'mb-1': !withCustomFields || !customFields.length }"
|
||||||
@@ -42,7 +65,7 @@
|
|||||||
@set-draw="$emit('set-draw', $event)"
|
@set-draw="$emit('set-draw', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="submitterDefaultFields.length && editable">
|
<div v-if="!isShowVariables && submitterDefaultFields.length && editable">
|
||||||
<hr class="mb-2">
|
<hr class="mb-2">
|
||||||
<template v-if="isShowFieldSearch">
|
<template v-if="isShowFieldSearch">
|
||||||
<input
|
<input
|
||||||
@@ -110,7 +133,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="editable && withCustomFields && (customFields.length || newCustomField)"
|
v-if="!isShowVariables && editable && withCustomFields && (customFields.length || newCustomField)"
|
||||||
class="tabs w-full mb-1.5"
|
class="tabs w-full mb-1.5"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@@ -127,7 +150,7 @@
|
|||||||
>{{ t('custom') }}</a>
|
>{{ t('custom') }}</a>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="showCustomTab && editable && (customFields.length || newCustomField)"
|
v-if="!isShowVariables && showCustomTab && editable && (customFields.length || newCustomField)"
|
||||||
ref="customFields"
|
ref="customFields"
|
||||||
class="custom-fields"
|
class="custom-fields"
|
||||||
@dragover.prevent="onCustomFieldDragover"
|
@dragover.prevent="onCustomFieldDragover"
|
||||||
@@ -195,7 +218,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="editable && !onlyDefinedFields && (!showCustomTab || (!customFields.length && !newCustomField))"
|
v-if="!isShowVariables && editable && !onlyDefinedFields && (!showCustomTab || (!customFields.length && !newCustomField))"
|
||||||
id="field-types-grid"
|
id="field-types-grid"
|
||||||
class="grid grid-cols-3 gap-1 pb-2 fields-grid"
|
class="grid grid-cols-3 gap-1 pb-2 fields-grid"
|
||||||
>
|
>
|
||||||
@@ -283,7 +306,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="fields.length < 4 && editable && withHelp && !showTourStartForm"
|
v-if="!isShowVariables && fields.length < 4 && editable && withHelp && !showTourStartForm"
|
||||||
class="text-xs p-2 border border-base-200 rounded"
|
class="text-xs p-2 border border-base-200 rounded"
|
||||||
>
|
>
|
||||||
<ul class="list-disc list-outside ml-3">
|
<ul class="list-disc list-outside ml-3">
|
||||||
@@ -299,7 +322,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="withFieldsDetection && editable && fields.length < 2"
|
v-if="!isShowVariables && withFieldsDetection && editable && fields.length < 2 && !template.schema.some((item) => item.dynamic)"
|
||||||
class="my-2"
|
class="my-2"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@@ -336,7 +359,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-show="fields.length < 4 && editable && withHelp && showTourStartForm"
|
v-show="!isShowVariables && fields.length < 4 && editable && withHelp && showTourStartForm"
|
||||||
class="rounded py-2 px-4 w-full border border-dashed border-base-300"
|
class="rounded py-2 px-4 w-full border border-dashed border-base-300"
|
||||||
>
|
>
|
||||||
<div class="text-center text-sm">
|
<div class="text-center text-sm">
|
||||||
@@ -359,7 +382,8 @@ import Field from './field'
|
|||||||
import CustomField from './custom_field'
|
import CustomField from './custom_field'
|
||||||
import FieldType from './field_type'
|
import FieldType from './field_type'
|
||||||
import FieldSubmitter from './field_submitter'
|
import FieldSubmitter from './field_submitter'
|
||||||
import { IconLock, IconCirclePlus, IconInnerShadowTop, IconSparkles } from '@tabler/icons-vue'
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
import { IconLock, IconCirclePlus, IconInnerShadowTop, IconSparkles, IconBracketsContain } from '@tabler/icons-vue'
|
||||||
import IconDrag from './icon_drag'
|
import IconDrag from './icon_drag'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
|
|
||||||
@@ -374,7 +398,9 @@ export default {
|
|||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
FieldSubmitter,
|
FieldSubmitter,
|
||||||
IconDrag,
|
IconDrag,
|
||||||
IconLock
|
IconLock,
|
||||||
|
IconBracketsContain,
|
||||||
|
DynamicVariables: defineAsyncComponent(() => import(/* webpackChunkName: "dynamic-editor" */ './dynamic_variables'))
|
||||||
},
|
},
|
||||||
inject: ['save', 'backgroundColor', 'withPhone', 'withVerification', 'withKba', 'withPayment', 't', 'fieldsDragFieldRef', 'customDragFieldRef', 'baseFetch', 'selectedAreasRef', 'getFieldTypeIndex'],
|
inject: ['save', 'backgroundColor', 'withPhone', 'withVerification', 'withKba', 'withPayment', 't', 'fieldsDragFieldRef', 'customDragFieldRef', 'baseFetch', 'selectedAreasRef', 'getFieldTypeIndex'],
|
||||||
props: {
|
props: {
|
||||||
@@ -475,7 +501,7 @@ export default {
|
|||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['add-field', 'set-draw', 'set-draw-type', 'set-draw-custom-field', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter', 'set-drag-placeholder', 'select-submitter'],
|
emits: ['add-field', 'set-draw', 'set-draw-type', 'set-draw-custom-field', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter', 'set-drag-placeholder', 'select-submitter', 'rebuild-variables-schema', 'remove-field', 'remove-submitter'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
fieldPagesLoaded: null,
|
fieldPagesLoaded: null,
|
||||||
@@ -483,12 +509,16 @@ export default {
|
|||||||
newCustomField: null,
|
newCustomField: null,
|
||||||
showCustomTab: false,
|
showCustomTab: false,
|
||||||
defaultFieldsSearch: '',
|
defaultFieldsSearch: '',
|
||||||
customFieldsSearch: ''
|
customFieldsSearch: '',
|
||||||
|
isShowVariables: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
fieldNames: FieldType.computed.fieldNames,
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
fieldIcons: FieldType.computed.fieldIcons,
|
fieldIcons: FieldType.computed.fieldIcons,
|
||||||
|
hasDynamicDocuments () {
|
||||||
|
return this.template.schema.some((item) => item.dynamic)
|
||||||
|
},
|
||||||
numberOfPages () {
|
numberOfPages () {
|
||||||
return this.template.documents.reduce((acc, doc) => {
|
return this.template.documents.reduce((acc, doc) => {
|
||||||
return acc + doc.metadata?.pdf?.number_of_pages || doc.preview_images.length
|
return acc + doc.metadata?.pdf?.number_of_pages || doc.preview_images.length
|
||||||
@@ -556,6 +586,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
toggleVariables () {
|
||||||
|
this.$emit('rebuild-variables-schema')
|
||||||
|
this.isShowVariables = !this.isShowVariables
|
||||||
|
},
|
||||||
onDragstart (event, field) {
|
onDragstart (event, field) {
|
||||||
this.removeDragOverlay(event)
|
this.removeDragOverlay(event)
|
||||||
|
|
||||||
@@ -582,6 +616,10 @@ export default {
|
|||||||
delete customField.prefillable
|
delete customField.prefillable
|
||||||
delete customField.conditions
|
delete customField.conditions
|
||||||
|
|
||||||
|
if (Array.isArray(customField.areas)) {
|
||||||
|
customField.areas = customField.areas.filter((area) => area.page !== null && area.page !== undefined)
|
||||||
|
}
|
||||||
|
|
||||||
customField.areas?.forEach((area) => {
|
customField.areas?.forEach((area) => {
|
||||||
delete area.attachment_uuid
|
delete area.attachment_uuid
|
||||||
delete area.page
|
delete area.page
|
||||||
@@ -767,6 +805,8 @@ export default {
|
|||||||
this.$emit('change-submitter', this.submitters[0])
|
this.$emit('change-submitter', this.submitters[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.$emit('remove-submitter', submitter)
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
removeField (field, save = true) {
|
removeField (field, save = true) {
|
||||||
@@ -792,6 +832,8 @@ export default {
|
|||||||
this.selectedAreasRef.value.splice(this.selectedAreasRef.value.indexOf(area), 1)
|
this.selectedAreasRef.value.splice(this.selectedAreasRef.value.indexOf(area), 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.$emit('remove-field', field)
|
||||||
|
|
||||||
if (save) {
|
if (save) {
|
||||||
this.save()
|
this.save()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export default {
|
|||||||
acceptFileTypes: {
|
acceptFileTypes: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'image/*, application/pdf, application/zip'
|
default: 'image/*, application/pdf, application/zip, application/json'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['add', 'replace', 'replace-and-clone', 'error'],
|
emits: ['add', 'replace', 'replace-and-clone', 'error'],
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ const en = {
|
|||||||
with_logo: 'With logo',
|
with_logo: 'With logo',
|
||||||
unchecked: 'Unchecked',
|
unchecked: 'Unchecked',
|
||||||
price: 'Price',
|
price: 'Price',
|
||||||
|
type: 'Type',
|
||||||
|
list: 'list',
|
||||||
|
no_variables: 'No variables yet',
|
||||||
|
no_variables_description: 'Add [[variable]] marks to your document to create dynamic content variables.',
|
||||||
type_value: 'Type value',
|
type_value: 'Type value',
|
||||||
equal: 'Equal',
|
equal: 'Equal',
|
||||||
not_equal: 'Not equal',
|
not_equal: 'Not equal',
|
||||||
@@ -73,6 +77,7 @@ const en = {
|
|||||||
up: 'Up',
|
up: 'Up',
|
||||||
down: 'Down',
|
down: 'Down',
|
||||||
checked: 'Checked',
|
checked: 'Checked',
|
||||||
|
current_date: 'Current date',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
any: 'Any',
|
any: 'Any',
|
||||||
@@ -104,6 +109,7 @@ const en = {
|
|||||||
option: 'Option',
|
option: 'Option',
|
||||||
options: 'Options',
|
options: 'Options',
|
||||||
condition: 'Condition',
|
condition: 'Condition',
|
||||||
|
make_dynamic: 'Make dynamic',
|
||||||
first_party: 'First Party',
|
first_party: 'First Party',
|
||||||
second_party: 'Second Party',
|
second_party: 'Second Party',
|
||||||
third_party: 'Third Party',
|
third_party: 'Third Party',
|
||||||
@@ -244,6 +250,10 @@ const es = {
|
|||||||
search_field: 'Campo de búsqueda',
|
search_field: 'Campo de búsqueda',
|
||||||
field_not_found: 'Campo no encontrado',
|
field_not_found: 'Campo no encontrado',
|
||||||
clear: 'Borrar',
|
clear: 'Borrar',
|
||||||
|
type: 'Tipo',
|
||||||
|
list: 'lista',
|
||||||
|
no_variables: 'Aún sin variables',
|
||||||
|
no_variables_description: 'Agregue marcas [[variable]] a su documento para crear variables de contenido dinámico.',
|
||||||
type_value: 'Escriba valor',
|
type_value: 'Escriba valor',
|
||||||
align: 'Alinear',
|
align: 'Alinear',
|
||||||
resize: 'Redimensionar',
|
resize: 'Redimensionar',
|
||||||
@@ -277,6 +287,7 @@ const es = {
|
|||||||
remove_condition: 'Eliminar condición',
|
remove_condition: 'Eliminar condición',
|
||||||
add_condition: 'Agregar condición',
|
add_condition: 'Agregar condición',
|
||||||
condition: 'Condición',
|
condition: 'Condición',
|
||||||
|
make_dynamic: 'Hacer dinámico',
|
||||||
formula: 'Fórmula',
|
formula: 'Fórmula',
|
||||||
edit: 'Editar',
|
edit: 'Editar',
|
||||||
settings: 'Configuración',
|
settings: 'Configuración',
|
||||||
@@ -286,6 +297,7 @@ const es = {
|
|||||||
are_you_sure_: '¿Estás seguro?',
|
are_you_sure_: '¿Estás seguro?',
|
||||||
sign_yourself: 'Firma tú mismo',
|
sign_yourself: 'Firma tú mismo',
|
||||||
checked: 'Seleccionado',
|
checked: 'Seleccionado',
|
||||||
|
current_date: 'Fecha actual',
|
||||||
send: 'Enviar',
|
send: 'Enviar',
|
||||||
remove: 'Eliminar',
|
remove: 'Eliminar',
|
||||||
save: 'Guardar',
|
save: 'Guardar',
|
||||||
@@ -475,6 +487,10 @@ const it = {
|
|||||||
with_logo: 'Con logo',
|
with_logo: 'Con logo',
|
||||||
unchecked: 'Non selezionato',
|
unchecked: 'Non selezionato',
|
||||||
price: 'Prezzo',
|
price: 'Prezzo',
|
||||||
|
type: 'Tipo',
|
||||||
|
list: 'lista',
|
||||||
|
no_variables: 'Ancora nessuna variabile',
|
||||||
|
no_variables_description: 'Aggiungi marcatori [[variable]] al documento per creare variabili di contenuto dinamico.',
|
||||||
type_value: 'Inserisci valore',
|
type_value: 'Inserisci valore',
|
||||||
equal: 'Uguale',
|
equal: 'Uguale',
|
||||||
not_equal: 'Non uguale',
|
not_equal: 'Non uguale',
|
||||||
@@ -499,6 +515,7 @@ const it = {
|
|||||||
up: 'Su',
|
up: 'Su',
|
||||||
down: 'Giù',
|
down: 'Giù',
|
||||||
checked: 'Selezionato',
|
checked: 'Selezionato',
|
||||||
|
current_date: 'Data corrente',
|
||||||
save: 'Salva',
|
save: 'Salva',
|
||||||
cancel: 'Annulla',
|
cancel: 'Annulla',
|
||||||
any: 'Qualsiasi',
|
any: 'Qualsiasi',
|
||||||
@@ -530,6 +547,7 @@ const it = {
|
|||||||
option: 'Opzione',
|
option: 'Opzione',
|
||||||
options: 'Opzioni',
|
options: 'Opzioni',
|
||||||
condition: 'Condizione',
|
condition: 'Condizione',
|
||||||
|
make_dynamic: 'Rendi dinamico',
|
||||||
first_party: 'Prima parte',
|
first_party: 'Prima parte',
|
||||||
second_party: 'Seconda parte',
|
second_party: 'Seconda parte',
|
||||||
third_party: 'Terza parte',
|
third_party: 'Terza parte',
|
||||||
@@ -670,6 +688,10 @@ const pt = {
|
|||||||
search_field: 'Campo de busca',
|
search_field: 'Campo de busca',
|
||||||
field_not_found: 'Campo não encontrado',
|
field_not_found: 'Campo não encontrado',
|
||||||
clear: 'Limpar',
|
clear: 'Limpar',
|
||||||
|
type: 'Tipo',
|
||||||
|
list: 'lista',
|
||||||
|
no_variables: 'Ainda sem variáveis',
|
||||||
|
no_variables_description: 'Adicione marcações [[variable]] ao documento para criar variáveis de conteúdo dinâmico.',
|
||||||
type_value: 'Digite valor',
|
type_value: 'Digite valor',
|
||||||
add_all_required_fields_to_continue: 'Adicione todos os campos obrigatórios para continuar',
|
add_all_required_fields_to_continue: 'Adicione todos os campos obrigatórios para continuar',
|
||||||
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'O PDF carregado contém campos. Manter ou removê-los?',
|
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'O PDF carregado contém campos. Manter ou removê-los?',
|
||||||
@@ -703,6 +725,7 @@ const pt = {
|
|||||||
select_value_: 'Selecionar valor...',
|
select_value_: 'Selecionar valor...',
|
||||||
remove_condition: 'Remover condição',
|
remove_condition: 'Remover condição',
|
||||||
condition: 'Condição',
|
condition: 'Condição',
|
||||||
|
make_dynamic: 'Tornar dinâmico',
|
||||||
formula: 'Fórmula',
|
formula: 'Fórmula',
|
||||||
edit: 'Editar',
|
edit: 'Editar',
|
||||||
settings: 'Configurações',
|
settings: 'Configurações',
|
||||||
@@ -712,6 +735,7 @@ const pt = {
|
|||||||
are_you_sure_: 'Tem certeza?',
|
are_you_sure_: 'Tem certeza?',
|
||||||
sign_yourself: 'Assine você mesmo',
|
sign_yourself: 'Assine você mesmo',
|
||||||
checked: 'Marcado',
|
checked: 'Marcado',
|
||||||
|
current_date: 'Data atual',
|
||||||
send: 'Enviar',
|
send: 'Enviar',
|
||||||
remove: 'Remover',
|
remove: 'Remover',
|
||||||
save: 'Salvar',
|
save: 'Salvar',
|
||||||
@@ -901,6 +925,10 @@ const fr = {
|
|||||||
with_logo: 'Avec logo',
|
with_logo: 'Avec logo',
|
||||||
unchecked: 'Décoché',
|
unchecked: 'Décoché',
|
||||||
price: 'Prix',
|
price: 'Prix',
|
||||||
|
type: 'Type',
|
||||||
|
list: 'liste',
|
||||||
|
no_variables: 'Pas encore de variables',
|
||||||
|
no_variables_description: 'Ajoutez des balises [[variable]] à votre document pour créer des variables de contenu dynamique.',
|
||||||
type_value: 'Saisir une valeur',
|
type_value: 'Saisir une valeur',
|
||||||
equal: 'Égal',
|
equal: 'Égal',
|
||||||
not_equal: 'Différent',
|
not_equal: 'Différent',
|
||||||
@@ -925,6 +953,7 @@ const fr = {
|
|||||||
up: 'Haut',
|
up: 'Haut',
|
||||||
down: 'Bas',
|
down: 'Bas',
|
||||||
checked: 'Coché',
|
checked: 'Coché',
|
||||||
|
current_date: 'Date du jour',
|
||||||
save: 'Enregistrer',
|
save: 'Enregistrer',
|
||||||
cancel: 'Annuler',
|
cancel: 'Annuler',
|
||||||
any: "N'importe lequel",
|
any: "N'importe lequel",
|
||||||
@@ -956,6 +985,7 @@ const fr = {
|
|||||||
option: 'Option',
|
option: 'Option',
|
||||||
options: 'Options',
|
options: 'Options',
|
||||||
condition: 'Condition',
|
condition: 'Condition',
|
||||||
|
make_dynamic: 'Rendre dynamique',
|
||||||
first_party: 'Première partie',
|
first_party: 'Première partie',
|
||||||
second_party: 'Deuxième partie',
|
second_party: 'Deuxième partie',
|
||||||
third_party: 'Troisième partie',
|
third_party: 'Troisième partie',
|
||||||
@@ -1114,6 +1144,10 @@ const de = {
|
|||||||
with_logo: 'Mit Logo',
|
with_logo: 'Mit Logo',
|
||||||
unchecked: 'Nicht markiert',
|
unchecked: 'Nicht markiert',
|
||||||
price: 'Preis',
|
price: 'Preis',
|
||||||
|
type: 'Typ',
|
||||||
|
list: 'Liste',
|
||||||
|
no_variables: 'Noch keine Variablen',
|
||||||
|
no_variables_description: 'Fügen Sie [[variable]]-Markierungen zu Ihrem Dokument hinzu, um dynamische Inhaltsvariablen zu erstellen.',
|
||||||
type_value: 'Wert eingeben',
|
type_value: 'Wert eingeben',
|
||||||
equal: 'Gleich',
|
equal: 'Gleich',
|
||||||
not_equal: 'Ungleich',
|
not_equal: 'Ungleich',
|
||||||
@@ -1138,6 +1172,7 @@ const de = {
|
|||||||
up: 'Nach oben',
|
up: 'Nach oben',
|
||||||
down: 'Nach unten',
|
down: 'Nach unten',
|
||||||
checked: 'Markiert',
|
checked: 'Markiert',
|
||||||
|
current_date: 'Aktuelles Datum',
|
||||||
save: 'Speichern',
|
save: 'Speichern',
|
||||||
cancel: 'Abbrechen',
|
cancel: 'Abbrechen',
|
||||||
any: 'Beliebig',
|
any: 'Beliebig',
|
||||||
@@ -1169,6 +1204,7 @@ const de = {
|
|||||||
option: 'Option',
|
option: 'Option',
|
||||||
options: 'Optionen',
|
options: 'Optionen',
|
||||||
condition: 'Bedingung',
|
condition: 'Bedingung',
|
||||||
|
make_dynamic: 'Dynamisch machen',
|
||||||
first_party: 'Erste Partei',
|
first_party: 'Erste Partei',
|
||||||
second_party: 'Zweite Partei',
|
second_party: 'Zweite Partei',
|
||||||
third_party: 'Dritte Partei',
|
third_party: 'Dritte Partei',
|
||||||
@@ -1327,6 +1363,10 @@ const nl = {
|
|||||||
with_logo: 'Met logo',
|
with_logo: 'Met logo',
|
||||||
unchecked: 'Niet aangevinkt',
|
unchecked: 'Niet aangevinkt',
|
||||||
price: 'Prijs',
|
price: 'Prijs',
|
||||||
|
type: 'Type',
|
||||||
|
list: 'lijst',
|
||||||
|
no_variables: 'Nog geen variabelen',
|
||||||
|
no_variables_description: 'Voeg [[variable]]-markeringen toe aan uw document om dynamische inhoudsvariabelen te maken.',
|
||||||
type_value: 'Typ waarde',
|
type_value: 'Typ waarde',
|
||||||
equal: 'Gelijk aan',
|
equal: 'Gelijk aan',
|
||||||
not_equal: 'Niet gelijk aan',
|
not_equal: 'Niet gelijk aan',
|
||||||
@@ -1351,6 +1391,7 @@ const nl = {
|
|||||||
up: 'Omhoog',
|
up: 'Omhoog',
|
||||||
down: 'Omlaag',
|
down: 'Omlaag',
|
||||||
checked: 'Aangevinkt',
|
checked: 'Aangevinkt',
|
||||||
|
current_date: 'Huidige datum',
|
||||||
save: 'Opslaan',
|
save: 'Opslaan',
|
||||||
cancel: 'Annuleren',
|
cancel: 'Annuleren',
|
||||||
any: 'Elke',
|
any: 'Elke',
|
||||||
@@ -1382,6 +1423,7 @@ const nl = {
|
|||||||
option: 'Optie',
|
option: 'Optie',
|
||||||
options: 'Opties',
|
options: 'Opties',
|
||||||
condition: 'Voorwaarde',
|
condition: 'Voorwaarde',
|
||||||
|
make_dynamic: 'Dynamisch maken',
|
||||||
first_party: 'Eerste partij',
|
first_party: 'Eerste partij',
|
||||||
second_party: 'Tweede partij',
|
second_party: 'Tweede partij',
|
||||||
third_party: 'Derde partij',
|
third_party: 'Derde partij',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<svg
|
<svg
|
||||||
height="40"
|
height="40"
|
||||||
width="40"
|
width="40"
|
||||||
style="color: #e0753f"
|
style="color: #e97a42"
|
||||||
viewBox="0 0 180 180"
|
viewBox="0 0 180 180"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="!('price_id' in field.preferences) && !('payment_link_id' in field.preferences)"
|
v-if="!('price_id' in field.preferences) && !('payment_link_id' in field.preferences)"
|
||||||
class="py-1.5 px-1 relative"
|
class="field-settings-currency py-1.5 px-1 relative"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<select
|
<select
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="py-1.5 px-1 relative"
|
class="field-settings-price py-1.5 px-1 relative"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="!isConnected || isOauthSuccess"
|
v-if="!isConnected || isOauthSuccess"
|
||||||
class="py-1.5 px-1 relative"
|
class="field-settings-stripe-connect py-1.5 px-1 relative"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -203,7 +203,7 @@
|
|||||||
>{{ t('learn_more') }}</a>
|
>{{ t('learn_more') }}</a>
|
||||||
</div>
|
</div>
|
||||||
<li
|
<li
|
||||||
class="mb-1"
|
class="field-settings-formula mb-1"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||||
@@ -218,7 +218,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<hr>
|
<hr>
|
||||||
<li>
|
<li class="field-settings-description">
|
||||||
<label
|
<label
|
||||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||||
@click="$emit('click-description')"
|
@click="$emit('click-description')"
|
||||||
@@ -233,7 +233,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="withCondition"
|
v-if="withCondition"
|
||||||
class="mt-1"
|
class="field-settings-condition mt-1"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||||
@@ -251,7 +251,10 @@
|
|||||||
v-if="withCustomFields"
|
v-if="withCustomFields"
|
||||||
class="pb-0.5 mt-0.5"
|
class="pb-0.5 mt-0.5"
|
||||||
>
|
>
|
||||||
<li v-if="withCustomFields">
|
<li
|
||||||
|
v-if="withCustomFields"
|
||||||
|
class="field-settings-save-as-custom-field"
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="text-sm py-1 px-2"
|
class="text-sm py-1 px-2"
|
||||||
|
|||||||
@@ -18,20 +18,18 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style="width: 26px"
|
style="width: 26px"
|
||||||
class="flex flex-col justify-between group-hover:opacity-100"
|
class="flex flex-col"
|
||||||
:class="{'opacity-0': !item.conditions?.length }"
|
|
||||||
>
|
>
|
||||||
<div>
|
<button
|
||||||
<button
|
v-if="item.conditions?.length"
|
||||||
class="btn border-gray-300 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors p-0 document-control-button"
|
class="btn border-gray-300 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors p-0 document-control-button"
|
||||||
@click.stop="isShowConditionsModal = true"
|
@click.stop="isShowConditionsModal = true"
|
||||||
>
|
>
|
||||||
<IconRouteAltLeft
|
<IconRouteAltLeft
|
||||||
:width="14"
|
:width="14"
|
||||||
:stroke-width="1.6"
|
:stroke-width="1.6"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="">
|
<div class="">
|
||||||
<ReplaceButton
|
<ReplaceButton
|
||||||
@@ -44,51 +42,105 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col justify-between opacity-0 group-hover:opacity-100"
|
class="flex flex-col justify-between"
|
||||||
>
|
>
|
||||||
<div>
|
<span
|
||||||
|
class="dropdown dropdown-end group-hover:opacity-100 has-[label:focus]:opacity-100"
|
||||||
|
:class="{ 'dropdown-open': isMakeDynamicLoading, 'opacity-0': !isMakeDynamicLoading }"
|
||||||
|
@mouseenter="renderDropdown = true"
|
||||||
|
@touchstart="renderDropdown = true"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
tabindex="0"
|
||||||
|
class="btn border-gray-300 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors document-control-button px-0"
|
||||||
|
style="width: 24px; height: 24px"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<IconDotsVertical
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<ul
|
||||||
|
v-if="renderDropdown"
|
||||||
|
tabindex="0"
|
||||||
|
class="mt-1.5 dropdown-content p-1 shadow-lg rounded-lg border border-neutral-200 z-50 bg-white"
|
||||||
|
style="min-width: 170px"
|
||||||
|
@click="closeDropdown"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
|
||||||
|
@click.stop="isShowConditionsModal = true; closeDropdown()"
|
||||||
|
>
|
||||||
|
<span class="flex items-center space-x-2">
|
||||||
|
<IconRouteAltLeft class="w-4 h-4" />
|
||||||
|
<span>{{ t('condition') }}</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="item.conditions?.length"
|
||||||
|
class="bg-neutral-200 rounded px-1 leading-3"
|
||||||
|
style="font-size: 9px;"
|
||||||
|
>{{ item.conditions.length }}</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li v-if="!item.dynamic">
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm whitespace-nowrap"
|
||||||
|
@click.stop="$emit('reorder', item); closeDropdown()"
|
||||||
|
>
|
||||||
|
<IconSortDescending2 class="w-4 h-4" />
|
||||||
|
<span>{{ t('reorder_fields') }}</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li v-if="withDynamicDocuments && !item.dynamic && document.metadata?.original_uuid">
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm whitespace-nowrap"
|
||||||
|
:disabled="isMakeDynamicLoading"
|
||||||
|
@click.stop="makeDynamic"
|
||||||
|
>
|
||||||
|
<IconInnerShadowTop
|
||||||
|
v-if="isMakeDynamicLoading"
|
||||||
|
class="w-4 h-4 animate-spin"
|
||||||
|
/>
|
||||||
|
<IconBolt
|
||||||
|
v-else
|
||||||
|
class="w-4 h-4"
|
||||||
|
/>
|
||||||
|
<span>{{ t('make_dynamic') }}</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<hr class="my-1 border-neutral-200">
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm text-red-600"
|
||||||
|
@click.stop="$emit('remove', item); closeDropdown()"
|
||||||
|
>
|
||||||
|
<IconTrashX class="w-4 h-4" />
|
||||||
|
<span>{{ t('remove') }}</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
v-if="withArrows"
|
||||||
|
class="flex flex-col space-y-1 opacity-0 group-hover:opacity-100"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
class="btn border-gray-300 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors document-control-button"
|
class="btn border-gray-300 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors document-control-button"
|
||||||
style="width: 24px; height: 24px"
|
style="width: 24px; height: 24px"
|
||||||
@click.stop="$emit('remove', item)"
|
@click.stop="$emit('up', item)"
|
||||||
>
|
>
|
||||||
×
|
↑
|
||||||
</button>
|
</button>
|
||||||
</div>
|
<button
|
||||||
<div
|
class="btn border-gray-300 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors document-control-button"
|
||||||
class="flex flex-col space-y-1"
|
style="width: 24px; height: 24px"
|
||||||
>
|
@click.stop="$emit('down', item)"
|
||||||
<span
|
|
||||||
:data-tip="t('reorder_fields')"
|
|
||||||
class="tooltip tooltip-left before:text-xs"
|
|
||||||
>
|
>
|
||||||
<button
|
↓
|
||||||
class="btn border-gray-300 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors p-0 document-control-button"
|
</button>
|
||||||
@click.stop="$emit('reorder', item)"
|
|
||||||
>
|
|
||||||
<IconSortDescending2
|
|
||||||
:width="18"
|
|
||||||
:height="18"
|
|
||||||
:stroke-width="1.6"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
<template v-if="withArrows">
|
|
||||||
<button
|
|
||||||
class="btn border-gray-300 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors document-control-button"
|
|
||||||
style="width: 24px; height: 24px"
|
|
||||||
@click.stop="$emit('up', item)"
|
|
||||||
>
|
|
||||||
↑
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn border-gray-300 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors document-control-button"
|
|
||||||
style="width: 24px; height: 24px"
|
|
||||||
@click.stop="$emit('down', item)"
|
|
||||||
>
|
|
||||||
↓
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -129,24 +181,29 @@
|
|||||||
<script>
|
<script>
|
||||||
import Contenteditable from './contenteditable'
|
import Contenteditable from './contenteditable'
|
||||||
import Upload from './upload'
|
import Upload from './upload'
|
||||||
import { IconRouteAltLeft, IconSortDescending2 } from '@tabler/icons-vue'
|
import { IconRouteAltLeft, IconSortDescending2, IconDotsVertical, IconTrashX, IconBolt, IconInnerShadowTop } from '@tabler/icons-vue'
|
||||||
import ConditionsModal from './conditions_modal'
|
import ConditionsModal from './conditions_modal'
|
||||||
import ReplaceButton from './replace'
|
import ReplaceButton from './replace'
|
||||||
import GoogleDriveDocumentSettings from './google_drive_document_settings'
|
import GoogleDriveDocumentSettings from './google_drive_document_settings'
|
||||||
import Field from './field'
|
import Field from './field'
|
||||||
import FieldType from './field_type'
|
import FieldType from './field_type'
|
||||||
|
import { v4 } from 'uuid'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DocumentPreview',
|
name: 'DocumentPreview',
|
||||||
components: {
|
components: {
|
||||||
Contenteditable,
|
Contenteditable,
|
||||||
|
IconInnerShadowTop,
|
||||||
IconRouteAltLeft,
|
IconRouteAltLeft,
|
||||||
ConditionsModal,
|
ConditionsModal,
|
||||||
ReplaceButton,
|
ReplaceButton,
|
||||||
GoogleDriveDocumentSettings,
|
GoogleDriveDocumentSettings,
|
||||||
IconSortDescending2
|
IconSortDescending2,
|
||||||
|
IconDotsVertical,
|
||||||
|
IconTrashX,
|
||||||
|
IconBolt
|
||||||
},
|
},
|
||||||
inject: ['t', 'getFieldTypeIndex'],
|
inject: ['t', 'getFieldTypeIndex', 'baseFetch'],
|
||||||
props: {
|
props: {
|
||||||
item: {
|
item: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -168,13 +225,22 @@ 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,
|
||||||
required: true,
|
required: true,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
dynamicDocuments: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
withDynamicDocuments: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
withArrows: {
|
withArrows: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -184,7 +250,9 @@ export default {
|
|||||||
emits: ['scroll-to', 'change', 'remove', 'up', 'down', 'replace', 'reorder'],
|
emits: ['scroll-to', 'change', 'remove', 'up', 'down', 'replace', 'reorder'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isShowConditionsModal: false
|
isShowConditionsModal: false,
|
||||||
|
isMakeDynamicLoading: false,
|
||||||
|
renderDropdown: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -200,6 +268,96 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
upload: Upload.methods.upload,
|
upload: Upload.methods.upload,
|
||||||
buildDefaultName: Field.methods.buildDefaultName,
|
buildDefaultName: Field.methods.buildDefaultName,
|
||||||
|
closeDropdown () {
|
||||||
|
this.$el.getRootNode().activeElement.blur()
|
||||||
|
},
|
||||||
|
makeDynamic () {
|
||||||
|
this.isMakeDynamicLoading = true
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
this.baseFetch(`/templates/${this.template.id}/dynamic_documents`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ uuid: this.document.uuid }),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
import(/* webpackChunkName: "dynamic-editor" */ './dynamic_document')
|
||||||
|
]).then(async ([resp, _]) => {
|
||||||
|
const dynamicDocument = await resp.json()
|
||||||
|
|
||||||
|
this.template.schema.find((item) => item.attachment_uuid === dynamicDocument.uuid).dynamic = true
|
||||||
|
|
||||||
|
this.removeFieldAreas()
|
||||||
|
|
||||||
|
if (dynamicDocument.fields?.length) {
|
||||||
|
this.addDynamicFields(dynamicDocument.fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dynamicDocument.uuid) {
|
||||||
|
delete dynamicDocument.fields
|
||||||
|
|
||||||
|
this.dynamicDocuments.push(dynamicDocument)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('change')
|
||||||
|
}).finally(() => {
|
||||||
|
this.isMakeDynamicLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
removeFieldAreas () {
|
||||||
|
this.template.fields.forEach((field) => {
|
||||||
|
if (field.areas?.length) {
|
||||||
|
field.areas = field.areas.filter((a) => a.attachment_uuid !== this.document.uuid)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.template.fields = this.template.fields.filter((field) => field.areas?.length)
|
||||||
|
},
|
||||||
|
addDynamicFields (fields) {
|
||||||
|
const submittersNameIndex = this.template.submitters.reduce((acc, submitter) => {
|
||||||
|
acc[submitter.name] = submitter
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
fields.forEach((field) => {
|
||||||
|
const roleName = field.role || this.template.submitters[0]?.name || this.t('first_party')
|
||||||
|
|
||||||
|
let submitter = submittersNameIndex[roleName]
|
||||||
|
|
||||||
|
if (!submitter) {
|
||||||
|
submitter = { name: roleName, uuid: v4() }
|
||||||
|
|
||||||
|
this.template.submitters.push(submitter)
|
||||||
|
|
||||||
|
submittersNameIndex[roleName] = submitter
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingField = this.template.fields.find((f) => {
|
||||||
|
return f.name && f.name === field.name && f.type === (field.type || 'text') && f.submitter_uuid === submitter.uuid
|
||||||
|
})
|
||||||
|
|
||||||
|
if (existingField) {
|
||||||
|
field.areas.forEach((area) => {
|
||||||
|
area.attachment_uuid = this.document.uuid
|
||||||
|
|
||||||
|
existingField.areas = existingField.areas || []
|
||||||
|
existingField.areas.push(area)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
field.submitter_uuid = submitter.uuid
|
||||||
|
|
||||||
|
delete field.role
|
||||||
|
|
||||||
|
field.areas.forEach((area) => {
|
||||||
|
area.attachment_uuid = this.document.uuid
|
||||||
|
})
|
||||||
|
|
||||||
|
this.template.fields.push(field)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
onUpdateName (value) {
|
onUpdateName (value) {
|
||||||
this.item.name = value
|
this.item.name = value
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,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'],
|
emits: ['success'],
|
||||||
|
|||||||
@@ -195,7 +195,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'],
|
emits: ['success', 'error'],
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class GenerateAttachmentPreviewJob
|
|
||||||
include Sidekiq::Job
|
|
||||||
|
|
||||||
InvalidFormat = Class.new(StandardError)
|
|
||||||
|
|
||||||
sidekiq_options queue: :images
|
|
||||||
|
|
||||||
def perform(params = {})
|
|
||||||
attachment = ActiveStorage::Attachment.find(params['attachment_id'])
|
|
||||||
|
|
||||||
if attachment.content_type == Templates::ProcessDocument::PDF_CONTENT_TYPE
|
|
||||||
Templates::ProcessDocument.generate_pdf_preview_images(attachment, attachment.download)
|
|
||||||
elsif attachment.image?
|
|
||||||
Templates::ProcessDocument.generate_preview_image(attachment, attachment.download)
|
|
||||||
else
|
|
||||||
raise InvalidFormat, attachment.id
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SendTemplateArchivedWebhookRequestJob
|
||||||
|
include Sidekiq::Job
|
||||||
|
|
||||||
|
sidekiq_options queue: :webhooks
|
||||||
|
|
||||||
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
|
def perform(params = {})
|
||||||
|
template = Template.find_by(id: params['template_id'])
|
||||||
|
|
||||||
|
return unless template
|
||||||
|
|
||||||
|
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
|
||||||
|
|
||||||
|
return unless webhook_url
|
||||||
|
|
||||||
|
attempt = params['attempt'].to_i
|
||||||
|
|
||||||
|
return if webhook_url.url.blank? || webhook_url.events.exclude?('template.archived')
|
||||||
|
|
||||||
|
resp = SendWebhookRequest.call(webhook_url, event_type: 'template.archived',
|
||||||
|
event_uuid: params['event_uuid'],
|
||||||
|
record: template,
|
||||||
|
attempt:,
|
||||||
|
data: template.as_json(only: %i[id archived_at]))
|
||||||
|
|
||||||
|
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||||
|
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
|
||||||
|
SendTemplateArchivedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||||
|
**params,
|
||||||
|
'attempt' => attempt + 1,
|
||||||
|
'last_status' => resp&.status.to_i
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -5,6 +5,8 @@ class ApplicationMailer < ActionMailer::Base
|
|||||||
layout 'mailer'
|
layout 'mailer'
|
||||||
|
|
||||||
register_interceptor ActionMailerConfigsInterceptor
|
register_interceptor ActionMailerConfigsInterceptor
|
||||||
|
register_interceptor HtmlToPlainTextInterceptor
|
||||||
|
register_preview_interceptor HtmlToPlainTextInterceptor
|
||||||
|
|
||||||
register_observer ActionMailerEventsObserver
|
register_observer ActionMailerEventsObserver
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
|
|
||||||
assign_message_metadata('submitter_invitation', @submitter)
|
assign_message_metadata('submitter_invitation', @submitter)
|
||||||
|
|
||||||
reply_to = build_submitter_reply_to(@submitter)
|
reply_to = build_submitter_reply_to(@submitter, email_config: @email_config)
|
||||||
|
|
||||||
maybe_set_custom_domain(@submitter)
|
maybe_set_custom_domain(@submitter)
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class AccountConfig < ApplicationRecord
|
|||||||
ALLOW_TYPED_SIGNATURE = 'allow_typed_signature'
|
ALLOW_TYPED_SIGNATURE = 'allow_typed_signature'
|
||||||
ALLOW_TO_RESUBMIT = 'allow_to_resubmit'
|
ALLOW_TO_RESUBMIT = 'allow_to_resubmit'
|
||||||
ALLOW_TO_DECLINE_KEY = 'allow_to_decline'
|
ALLOW_TO_DECLINE_KEY = 'allow_to_decline'
|
||||||
|
ALLOW_TO_DELEGATE_KEY = 'allow_to_delegate'
|
||||||
ALLOW_TO_PARTIAL_DOWNLOAD_KEY = 'allow_to_partial_download'
|
ALLOW_TO_PARTIAL_DOWNLOAD_KEY = 'allow_to_partial_download'
|
||||||
SUBMITTER_REMINDERS = 'submitter_reminders'
|
SUBMITTER_REMINDERS = 'submitter_reminders'
|
||||||
ENFORCE_SIGNING_ORDER_KEY = 'enforce_signing_order'
|
ENFORCE_SIGNING_ORDER_KEY = 'enforce_signing_order'
|
||||||
@@ -57,6 +58,7 @@ class AccountConfig < ApplicationRecord
|
|||||||
DOCUMENT_FILENAME_FORMAT_KEY = 'document_filename_format'
|
DOCUMENT_FILENAME_FORMAT_KEY = 'document_filename_format'
|
||||||
TEMPLATE_CUSTOM_FIELDS_KEY = 'template_custom_fields'
|
TEMPLATE_CUSTOM_FIELDS_KEY = 'template_custom_fields'
|
||||||
POLICY_LINKS_KEY = 'policy_links'
|
POLICY_LINKS_KEY = 'policy_links'
|
||||||
|
ENABLE_MCP_KEY = 'enable_mcp'
|
||||||
|
|
||||||
EMAIL_VARIABLES = {
|
EMAIL_VARIABLES = {
|
||||||
SUBMITTER_INVITATION_EMAIL_KEY => %w[template.name submitter.link account.name].freeze,
|
SUBMITTER_INVITATION_EMAIL_KEY => %w[template.name submitter.link account.name].freeze,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
# index_document_generation_events_on_submitter_id (submitter_id)
|
# index_document_generation_events_on_submitter_id (submitter_id)
|
||||||
# index_document_generation_events_on_submitter_id_and_event_name (submitter_id,event_name) UNIQUE WHERE ((event_name)::text = ANY ((ARRAY['start'::character varying, 'complete'::character varying])::text[]))
|
# index_document_generation_events_on_submitter_id_and_event_name (submitter_id,event_name) UNIQUE WHERE ((event_name)::text = ANY (ARRAY[('start'::character varying)::text, ('complete'::character varying)::text]))
|
||||||
#
|
#
|
||||||
# Foreign Keys
|
# Foreign Keys
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: dynamic_documents
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# body :text not null
|
||||||
|
# head :text
|
||||||
|
# sha1 :text not null
|
||||||
|
# uuid :uuid not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# template_id :bigint not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_dynamic_documents_on_template_id (template_id)
|
||||||
|
#
|
||||||
|
# Foreign Keys
|
||||||
|
#
|
||||||
|
# fk_rails_... (template_id => templates.id)
|
||||||
|
#
|
||||||
|
class DynamicDocument < ApplicationRecord
|
||||||
|
belongs_to :template
|
||||||
|
|
||||||
|
has_many_attached :attachments
|
||||||
|
|
||||||
|
has_many :versions, class_name: 'DynamicDocumentVersion', dependent: :destroy
|
||||||
|
|
||||||
|
attribute :fields, :json
|
||||||
|
|
||||||
|
before_validation :set_sha1
|
||||||
|
|
||||||
|
def set_sha1
|
||||||
|
self.sha1 = Digest::SHA1.hexdigest(body)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: dynamic_document_versions
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# areas :text not null
|
||||||
|
# sha1 :string not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# dynamic_document_id :bigint not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# idx_on_dynamic_document_id_sha1_3503adf557 (dynamic_document_id,sha1) UNIQUE
|
||||||
|
#
|
||||||
|
# Foreign Keys
|
||||||
|
#
|
||||||
|
# fk_rails_... (dynamic_document_id => dynamic_documents.id)
|
||||||
|
#
|
||||||
|
class DynamicDocumentVersion < ApplicationRecord
|
||||||
|
belongs_to :dynamic_document
|
||||||
|
|
||||||
|
has_one_attached :document
|
||||||
|
|
||||||
|
attribute :areas, :string, default: -> { [] }
|
||||||
|
|
||||||
|
serialize :areas, coder: JSON
|
||||||
|
end
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
#
|
#
|
||||||
# index_email_events_on_account_id_and_event_datetime (account_id,event_datetime)
|
# index_email_events_on_account_id_and_event_datetime (account_id,event_datetime)
|
||||||
# index_email_events_on_email (email)
|
# index_email_events_on_email (email)
|
||||||
# index_email_events_on_email_event_types (email) WHERE ((event_type)::text = ANY ((ARRAY['bounce'::character varying, 'soft_bounce'::character varying, 'permanent_bounce'::character varying, 'complaint'::character varying, 'soft_complaint'::character varying])::text[]))
|
# index_email_events_on_email_event_types (email) WHERE ((event_type)::text = ANY (ARRAY[('bounce'::character varying)::text, ('soft_bounce'::character varying)::text, ('permanent_bounce'::character varying)::text, ('complaint'::character varying)::text, ('soft_complaint'::character varying)::text]))
|
||||||
# index_email_events_on_emailable (emailable_type,emailable_id)
|
# index_email_events_on_emailable (emailable_type,emailable_id)
|
||||||
# index_email_events_on_message_id (message_id)
|
# index_email_events_on_message_id (message_id)
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
# index_lock_events_on_event_name_and_key (event_name,key) UNIQUE WHERE ((event_name)::text = ANY ((ARRAY['start'::character varying, 'complete'::character varying])::text[]))
|
# index_lock_events_on_event_name_and_key (event_name,key) UNIQUE WHERE ((event_name)::text = ANY (ARRAY[('start'::character varying)::text, ('complete'::character varying)::text]))
|
||||||
# index_lock_events_on_key (key)
|
# index_lock_events_on_key (key)
|
||||||
#
|
#
|
||||||
class LockEvent < ApplicationRecord
|
class LockEvent < ApplicationRecord
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: mcp_tokens
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# archived_at :datetime
|
||||||
|
# name :string not null
|
||||||
|
# sha256 :string not null
|
||||||
|
# token_prefix :string not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# user_id :bigint not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_mcp_tokens_on_sha256 (sha256) UNIQUE
|
||||||
|
# index_mcp_tokens_on_user_id (user_id)
|
||||||
|
#
|
||||||
|
# Foreign Keys
|
||||||
|
#
|
||||||
|
# fk_rails_... (user_id => users.id)
|
||||||
|
#
|
||||||
|
class McpToken < ApplicationRecord
|
||||||
|
TOKEN_LENGTH = 43
|
||||||
|
|
||||||
|
belongs_to :user
|
||||||
|
|
||||||
|
before_validation :set_sha256_and_token_prefix, on: :create
|
||||||
|
|
||||||
|
attribute :token, :string, default: -> { SecureRandom.base58(TOKEN_LENGTH) }
|
||||||
|
|
||||||
|
scope :active, -> { where(archived_at: nil) }
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_sha256_and_token_prefix
|
||||||
|
self.sha256 = Digest::SHA256.hexdigest(token)
|
||||||
|
self.token_prefix = token[0, 5]
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
# name :text
|
# name :text
|
||||||
# preferences :text not null
|
# preferences :text not null
|
||||||
# slug :string not null
|
# slug :string not null
|
||||||
# source :text not null
|
# source :string not null
|
||||||
# submitters_order :string not null
|
# submitters_order :string not null
|
||||||
# template_fields :text
|
# template_fields :text
|
||||||
# template_schema :text
|
# template_schema :text
|
||||||
@@ -75,6 +75,17 @@ class Submission < ApplicationRecord
|
|||||||
->(e) { where(uuid: (e.template_schema.presence || e.template.schema).pluck('attachment_uuid')) },
|
->(e) { where(uuid: (e.template_schema.presence || e.template.schema).pluck('attachment_uuid')) },
|
||||||
through: :template, source: :documents_attachments
|
through: :template, source: :documents_attachments
|
||||||
|
|
||||||
|
has_many :template_schema_static_documents,
|
||||||
|
->(e) { where(uuid: e.template_schema.reject { |s| s['dynamic'] }.pluck('attachment_uuid')) },
|
||||||
|
through: :template, source: :documents_attachments
|
||||||
|
|
||||||
|
has_many :template_schema_dynamic_document_versions,
|
||||||
|
->(e) { where(sha1: e.template_schema.select { |s| s['dynamic'] }.pluck('dynamic_document_sha1')) },
|
||||||
|
through: :template, source: :dynamic_document_versions
|
||||||
|
|
||||||
|
has_many :template_schema_dynamic_document_attachments,
|
||||||
|
through: :template_schema_dynamic_document_versions, source: :document_attachment
|
||||||
|
|
||||||
scope :active, -> { where(archived_at: nil) }
|
scope :active, -> { where(archived_at: nil) }
|
||||||
scope :archived, -> { where.not(archived_at: nil) }
|
scope :archived, -> { where.not(archived_at: nil) }
|
||||||
scope :pending, lambda {
|
scope :pending, lambda {
|
||||||
@@ -110,13 +121,51 @@ class Submission < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def schema_documents
|
def schema_documents
|
||||||
if template_id?
|
return documents_attachments unless template_id?
|
||||||
template_schema_documents
|
|
||||||
|
dynamic_count = template_schema&.count { |e| e['dynamic'] }.to_i
|
||||||
|
|
||||||
|
if variables_schema.blank?
|
||||||
|
if dynamic_count > 0
|
||||||
|
if dynamic_count == template_schema.size
|
||||||
|
template_schema_dynamic_document_attachments
|
||||||
|
else
|
||||||
|
template_schema_dynamic_and_static_document_attachments
|
||||||
|
end
|
||||||
|
else
|
||||||
|
template_schema_documents
|
||||||
|
end
|
||||||
|
elsif dynamic_count > 0 && dynamic_count != template_schema.size
|
||||||
|
template_schema_submission_dynamic_and_static_document_attachments
|
||||||
else
|
else
|
||||||
documents_attachments
|
documents_attachments
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def template_schema_submission_dynamic_and_static_document_attachments
|
||||||
|
@template_schema_submission_dynamic_and_static_document_attachments ||=
|
||||||
|
ActiveStorage::Attachment.where(
|
||||||
|
ActiveStorage::Attachment.arel_table[:id].in(
|
||||||
|
template_schema_static_documents.select(:id).arel.union(
|
||||||
|
:all,
|
||||||
|
documents_attachments.select(:id).arel
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def template_schema_dynamic_and_static_document_attachments
|
||||||
|
@template_schema_dynamic_and_static_document_attachments ||=
|
||||||
|
ActiveStorage::Attachment.where(
|
||||||
|
ActiveStorage::Attachment.arel_table[:id].in(
|
||||||
|
template_schema_static_documents.select(:id).arel.union(
|
||||||
|
:all,
|
||||||
|
template_schema_dynamic_document_attachments.select(:id).arel
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
def fields_uuid_index
|
def fields_uuid_index
|
||||||
@fields_uuid_index ||= (template_fields || template.fields).index_by { |f| f['uuid'] }
|
@fields_uuid_index ||= (template_fields || template.fields).index_by { |f| f['uuid'] }
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
# index_submission_events_on_created_at (created_at)
|
# index_submission_events_on_created_at (created_at)
|
||||||
# index_submission_events_on_submission_id (submission_id)
|
# index_submission_events_on_submission_id (submission_id)
|
||||||
# index_submission_events_on_submitter_id (submitter_id)
|
# index_submission_events_on_submitter_id (submitter_id)
|
||||||
# index_submissions_events_on_sms_event_types (account_id,created_at) WHERE ((event_type)::text = ANY ((ARRAY['send_sms'::character varying, 'send_2fa_sms'::character varying])::text[]))
|
# index_submissions_events_on_sms_event_types (account_id,created_at) WHERE ((event_type)::text = ANY (ARRAY[('send_sms'::character varying)::text, ('send_2fa_sms'::character varying)::text]))
|
||||||
#
|
#
|
||||||
# Foreign Keys
|
# Foreign Keys
|
||||||
#
|
#
|
||||||
@@ -64,6 +64,7 @@ class SubmissionEvent < ApplicationRecord
|
|||||||
invite_party: 'invite_party',
|
invite_party: 'invite_party',
|
||||||
complete_form: 'complete_form',
|
complete_form: 'complete_form',
|
||||||
decline_form: 'decline_form',
|
decline_form: 'decline_form',
|
||||||
|
delegate_form: 'delegate_form',
|
||||||
api_complete_form: 'api_complete_form'
|
api_complete_form: 'api_complete_form'
|
||||||
}, scope: false
|
}, scope: false
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ class Submitter < ApplicationRecord
|
|||||||
belongs_to :account
|
belongs_to :account
|
||||||
has_one :template, through: :submission
|
has_one :template, through: :submission
|
||||||
has_one :search_entry, as: :record, inverse_of: :record, dependent: :destroy if SearchEntry.table_exists?
|
has_one :search_entry, as: :record, inverse_of: :record, dependent: :destroy if SearchEntry.table_exists?
|
||||||
|
has_many :submitter_versions, dependent: :destroy
|
||||||
|
|
||||||
attribute :values, :string, default: -> { {} }
|
attribute :values, :string, default: -> { {} }
|
||||||
attribute :preferences, :string, default: -> { {} }
|
attribute :preferences, :string, default: -> { {} }
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: submitter_versions
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# email :string
|
||||||
|
# name :string
|
||||||
|
# phone :string
|
||||||
|
# slug :string not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# submitter_id :bigint not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_submitter_versions_on_slug (slug)
|
||||||
|
# index_submitter_versions_on_submitter_id (submitter_id)
|
||||||
|
#
|
||||||
|
# Foreign Keys
|
||||||
|
#
|
||||||
|
# fk_rails_... (submitter_id => submitters.id)
|
||||||
|
#
|
||||||
|
class SubmitterVersion < ApplicationRecord
|
||||||
|
belongs_to :submitter
|
||||||
|
end
|
||||||
@@ -70,6 +70,12 @@ class Template < ApplicationRecord
|
|||||||
has_many :submissions, dependent: :destroy
|
has_many :submissions, dependent: :destroy
|
||||||
has_many :template_sharings, dependent: :destroy
|
has_many :template_sharings, dependent: :destroy
|
||||||
has_many :template_accesses, dependent: :destroy
|
has_many :template_accesses, dependent: :destroy
|
||||||
|
has_many :dynamic_documents, dependent: :destroy
|
||||||
|
has_many :dynamic_document_versions, through: :dynamic_documents, source: :versions
|
||||||
|
|
||||||
|
has_many :schema_dynamic_documents, lambda { |e|
|
||||||
|
where(uuid: e.schema.select { |e| e['dynamic'] }.pluck('attachment_uuid'))
|
||||||
|
}, class_name: 'DynamicDocument', dependent: :destroy, inverse_of: :template
|
||||||
|
|
||||||
scope :active, -> { where(archived_at: nil) }
|
scope :active, -> { where(archived_at: nil) }
|
||||||
scope :archived, -> { where.not(archived_at: nil) }
|
scope :archived, -> { where.not(archived_at: nil) }
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ class User < ApplicationRecord
|
|||||||
belongs_to :account
|
belongs_to :account
|
||||||
has_one :access_token, dependent: :destroy
|
has_one :access_token, dependent: :destroy
|
||||||
has_many :access_tokens, dependent: :destroy
|
has_many :access_tokens, dependent: :destroy
|
||||||
|
has_many :mcp_tokens, dependent: :destroy
|
||||||
has_many :templates, dependent: :destroy, foreign_key: :author_id, inverse_of: :author
|
has_many :templates, dependent: :destroy, foreign_key: :author_id, inverse_of: :author
|
||||||
has_many :template_folders, dependent: :destroy, foreign_key: :author_id, inverse_of: :author
|
has_many :template_folders, dependent: :destroy, foreign_key: :author_id, inverse_of: :author
|
||||||
has_many :user_configs, dependent: :destroy
|
has_many :user_configs, dependent: :destroy
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class WebhookUrl < ApplicationRecord
|
|||||||
submission.archived
|
submission.archived
|
||||||
template.created
|
template.created
|
||||||
template.updated
|
template.updated
|
||||||
|
template.archived
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
|
|||||||
@@ -142,9 +142,38 @@
|
|||||||
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
|
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<submit-form data-on="change" class="flex">
|
<% if !Docuseal.multitenant? || can?(:manage, :disable_decline) %>
|
||||||
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false %>
|
<submit-form data-on="change" class="flex">
|
||||||
</submit-form>
|
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false %>
|
||||||
|
</submit-form>
|
||||||
|
<% else %>
|
||||||
|
<a href="<%= console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") %>" data-turbo="false" data-tip="<%= I18n.t('unlock_with_docuseal_pro') %>" data-on="change" class="flex tooltip">
|
||||||
|
<%= f.check_box :value, class: 'toggle pointer-events-none', checked: account_config.value != false, disabled: true %>
|
||||||
|
</a>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::ALLOW_TO_DELEGATE_KEY) %>
|
||||||
|
<% if can?(:manage, account_config) %>
|
||||||
|
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
|
||||||
|
<%= f.hidden_field :key %>
|
||||||
|
<div class="flex items-center justify-between gap-4 py-2.5">
|
||||||
|
<div class="flex items-center space-x-1">
|
||||||
|
<span class="text-left"><%= t('allow_to_delegate_documents') %></span>
|
||||||
|
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('allow_recipients_to_delegate_documents') %>">
|
||||||
|
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<% if !Docuseal.multitenant? || can?(:manage, :delegate_form) %>
|
||||||
|
<submit-form data-on="change" class="flex">
|
||||||
|
<%= f.check_box :value, class: 'toggle', checked: account_config.value == true %>
|
||||||
|
</submit-form>
|
||||||
|
<% else %>
|
||||||
|
<a href="<%= console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") %>" data-turbo="false" data-tip="<%= I18n.t('unlock_with_docuseal_pro') %>" class="flex tooltip">
|
||||||
|
<%= f.check_box :value, class: 'toggle pointer-events-none', checked: account_config.value == true, disabled: true %>
|
||||||
|
</a>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="<%= local_assigns[:class] %>"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0" /><path d="M6 21v-2a4 4 0 0 1 4 -4h3" /><path d="M16 22l5 -5" /><path d="M21 21.5v-4.5h-4.5" /></svg>
|
||||||
|
After Width: | Height: | Size: 416 B |
@@ -0,0 +1,112 @@
|
|||||||
|
<div class="flex-wrap space-y-4 md:flex md:flex-nowrap md:space-y-0 md:space-x-10">
|
||||||
|
<%= render 'shared/settings_nav' %>
|
||||||
|
<div class="md:flex-grow">
|
||||||
|
<div class="flex flex-col md:flex-row md:flex-wrap gap-2 md:justify-between md:items-end mb-4 min-h-12">
|
||||||
|
<h1 class="text-4xl font-bold">
|
||||||
|
<%= t('mcp_server') %>
|
||||||
|
</h1>
|
||||||
|
<div class="flex flex-col md:flex-row gap-y-2 gap-x-4 md:items-center">
|
||||||
|
<div class="tooltip">
|
||||||
|
<%= link_to new_settings_mcp_path, class: 'btn btn-primary btn-md gap-2 w-full md:w-fit', data: { turbo_frame: 'modal' } do %>
|
||||||
|
<%= svg_icon('plus', class: 'w-6 h-6') %>
|
||||||
|
<span><%= t('new_token') %></span>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% if @mcp_token.present? %>
|
||||||
|
<div class="space-y-4 mb-4">
|
||||||
|
<div class="card bg-base-200">
|
||||||
|
<div class="card-body p-6">
|
||||||
|
<label for="mcp_token" class="text-sm font-semibold">
|
||||||
|
<%= t('please_copy_the_token_below_now_as_it_wont_be_shown_again') %>:
|
||||||
|
</label>
|
||||||
|
<div class="flex w-full space-x-4">
|
||||||
|
<input id="mcp_token" type="text" value="<%= @mcp_token.token %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
|
||||||
|
<%= render 'shared/clipboard_copy', icon: 'copy', text: @mcp_token.token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<p class="text-2xl font-bold">
|
||||||
|
<%= t('instructions') %>
|
||||||
|
</p>
|
||||||
|
<div class="card bg-base-200">
|
||||||
|
<div class="card-body p-6">
|
||||||
|
<p class="text-2xl font-semibold"><%= t('connect_to_docuseal_mcp') %></p>
|
||||||
|
<p class="text-lg"><%= t('add_the_following_to_your_mcp_client_configuration') %>:</p>
|
||||||
|
<div class="mockup-code overflow-hidden">
|
||||||
|
<% text = JSON.pretty_generate({ mcpServers: { docuseal: { type: 'http', url: "#{root_url(Docuseal.default_url_options)}mcp", headers: { Authorization: "Bearer #{@mcp_token.token}" } } } }).strip %>
|
||||||
|
<span class="top-0 right-0 absolute">
|
||||||
|
<%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
||||||
|
</span>
|
||||||
|
<pre class="before:!m-0 pl-4 pb-4"><code class="overflow-hidden w-full"><%== HighlightCode.call(text, 'JSON', theme: 'base16.dark') %></code></pre>
|
||||||
|
</div>
|
||||||
|
<p class="text-lg"><%= t('works_with_claude_desktop_cursor_windsurf_vs_code_and_any_mcp_compatible_client') %></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="table w-full table-lg rounded-b-none overflow-hidden">
|
||||||
|
<thead class="bg-base-200">
|
||||||
|
<tr class="text-neutral uppercase">
|
||||||
|
<th>
|
||||||
|
<%= t('name') %>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<%= t('token') %>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<%= t('created_at') %>
|
||||||
|
</th>
|
||||||
|
<th class="text-right" width="1px">
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% @mcp_tokens.each do |mcp_token| %>
|
||||||
|
<tr scope="row">
|
||||||
|
<td>
|
||||||
|
<%= mcp_token.name %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<% if @mcp_token.present? && mcp_token.id == @mcp_token.id %>
|
||||||
|
<%= @mcp_token.token %>
|
||||||
|
<% else %>
|
||||||
|
<%= "#{mcp_token.token_prefix}#{'*' * 38}" %>
|
||||||
|
<% end %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<%= l(mcp_token.created_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>
|
||||||
|
</td>
|
||||||
|
<td class="flex items-center space-x-2 justify-end">
|
||||||
|
<%= button_to settings_mcp_path(mcp_token), method: :delete, class: 'btn btn-outline btn-error btn-xs', title: t('remove'), data: { turbo_confirm: t('are_you_sure_') } do %>
|
||||||
|
<%= t('remove') %>
|
||||||
|
<% end %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::ENABLE_MCP_KEY) %>
|
||||||
|
<% if can?(:manage, account_config) %>
|
||||||
|
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
|
||||||
|
<%= f.hidden_field :key %>
|
||||||
|
<div class="flex items-center gap-4 py-2.5">
|
||||||
|
<div class="flex items-center space-x-1">
|
||||||
|
<span class="text-left"><%= t('enable_mcp_server') %></span>
|
||||||
|
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('all_existing_mcp_connections_will_be_stopped_immediately_when_this_setting_is_disabled') %>">
|
||||||
|
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<submit-form data-on="change" class="flex">
|
||||||
|
<%= f.check_box :value, class: 'toggle', checked: account_config.value == true %>
|
||||||
|
</submit-form>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<%= render 'shared/turbo_modal', title: t('new_token') do %>
|
||||||
|
<%= form_for @mcp_token, url: settings_mcp_index_path, method: :post, html: { autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="w-full">
|
||||||
|
<%= f.label :name, t('name'), class: 'label' %>
|
||||||
|
<%= f.text_field :name, required: true, class: 'base-input w-full', dir: 'auto' %>
|
||||||
|
</div>
|
||||||
|
<div class="form-control pt-2">
|
||||||
|
<%= f.button button_title, class: 'base-button' %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<% account_config = AccountConfig.where(account: current_account, key: AccountConfig::FORM_WITH_CONFETTI_KEY).first_or_initialize(value: true) %>
|
<% account_config = AccountConfig.where(account: current_account, key: AccountConfig::FORM_WITH_CONFETTI_KEY).first_or_initialize(value: false) %>
|
||||||
<% if can?(:manage, account_config) %>
|
<% if can?(:manage, account_config) %>
|
||||||
<div class="px-1 mt-2">
|
<div class="px-1 mt-2">
|
||||||
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
|
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
<%= t('show_confetti_on_successful_completion') %>
|
<%= t('show_confetti_on_successful_completion') %>
|
||||||
</span>
|
</span>
|
||||||
<submit-form data-on="change" class="flex">
|
<submit-form data-on="change" class="flex">
|
||||||
<%= f.check_box :value, { class: 'toggle', checked: account_config.value != false }, '1', '0' %>
|
<%= f.check_box :value, { class: 'toggle', checked: account_config.value == true }, '1', '0' %>
|
||||||
</submit-form>
|
</submit-form>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,70 +1,76 @@
|
|||||||
<% link_tooltip_html = capture do %>
|
<% if value.to_s.start_with?('<html') %>
|
||||||
<div class="hidden absolute flex bg-white border border-base-300 rounded-xl shadow p-1 gap-1 items-center z-50" contenteditable="false">
|
<autoresize-textarea>
|
||||||
<input type="text" placeholder="<%= t('enter_a_url_or_variable_name') %>" class="rounded-lg border border-base-300 px-2 py-1 text-sm outline-none" style="field-sizing: content; min-width: 205px; max-width: 320px;" autocomplete="off">
|
<%= text_area_tag name, value, required: true, class: 'base-input w-full py-2 !rounded-2xl', dir: 'auto', style: 'max-height: 400px' %>
|
||||||
<button type="button" data-role="link-save" class="flex items-center px-1 w-6 h-6 rounded hover:bg-success/10 cursor-pointer">
|
</autoresize-textarea>
|
||||||
<%= svg_icon('check', class: 'w-4 h-4 text-success') %>
|
<% else %>
|
||||||
</button>
|
<markdown-editor>
|
||||||
<button type="button" data-role="link-remove" class="flex items-center px-1 w-6 h-6 rounded hover:bg-error/10 cursor-pointer">
|
<template data-target="markdown-editor.linkTooltipTemplate">
|
||||||
<%= svg_icon('x', class: 'w-4 h-4 text-error') %>
|
<div class="hidden absolute flex bg-white border border-base-300 rounded-xl shadow p-1 gap-1 items-center z-50" contenteditable="false">
|
||||||
</button>
|
<input type="text" placeholder="<%= t('enter_a_url_or_variable_name') %>" class="rounded-lg border border-base-300 px-2 py-1 text-sm outline-none" style="field-sizing: content; min-width: 205px; max-width: 320px;" autocomplete="off">
|
||||||
</div>
|
<button type="button" data-role="link-save" class="flex items-center px-1 w-6 h-6 rounded hover:bg-success/10 cursor-pointer">
|
||||||
<% end %>
|
<%= svg_icon('check', class: 'w-4 h-4 text-success') %>
|
||||||
<markdown-editor data-link-tooltip-html="<%= link_tooltip_html.squish %>">
|
</button>
|
||||||
<div class="border border-base-content/20 rounded-2xl bg-white">
|
<button type="button" data-role="link-remove" class="flex items-center px-1 w-6 h-6 rounded hover:bg-error/10 cursor-pointer">
|
||||||
<div class="flex items-center px-2 py-2 border-b" style="height: 42px;">
|
<%= svg_icon('x', class: 'w-4 h-4 text-error') %>
|
||||||
<div class="flex items-center gap-1">
|
</button>
|
||||||
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('bold') %> (Ctrl+B)">
|
|
||||||
<button type="button" data-action="click:markdown-editor#bold" data-target="markdown-editor.boldButton" aria-label="<%= t('bold') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
|
|
||||||
<%= svg_icon('bold', class: 'w-4 h-4') %>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('italic') %> (Ctrl+I)">
|
|
||||||
<button type="button" data-action="click:markdown-editor#italic" data-target="markdown-editor.italicButton" aria-label="<%= t('italic') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
|
|
||||||
<%= svg_icon('italic', class: 'w-4 h-4') %>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('underline') %> (Ctrl+U)">
|
|
||||||
<button type="button" data-action="click:markdown-editor#underline" data-target="markdown-editor.underlineButton" aria-label="<%= t('underline') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
|
|
||||||
<%= svg_icon('underline', class: 'w-4 h-4') %>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('link') %> (Ctrl+K)">
|
|
||||||
<button type="button" data-action="click:markdown-editor#linkSelection" data-target="markdown-editor.linkButton" aria-label="<%= t('link') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
|
|
||||||
<%= svg_icon('link', class: 'w-4 h-4') %>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mx-2 h-5 border-l border-base-content/20"></div>
|
</template>
|
||||||
<div class="flex items-center gap-1">
|
<div class="border border-base-content/20 rounded-2xl bg-white">
|
||||||
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('undo') %> (Ctrl+Z)">
|
<div class="flex items-center px-2 py-2 border-b" style="height: 42px;">
|
||||||
<button type="button" data-action="click:markdown-editor#undo" data-target="markdown-editor.undoButton" aria-label="<%= t('undo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
|
<div class="flex items-center gap-1">
|
||||||
<%= svg_icon('arrow_back_up', class: 'w-4 h-4') %>
|
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('bold') %> (Ctrl+B)">
|
||||||
</button>
|
<button type="button" data-action="click:markdown-editor#bold" data-target="markdown-editor.boldButton" aria-label="<%= t('bold') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
|
||||||
</div>
|
<%= svg_icon('bold', class: 'w-4 h-4') %>
|
||||||
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('redo') %> (Ctrl+Shift+Z)">
|
</button>
|
||||||
<button type="button" data-action="click:markdown-editor#redo" data-target="markdown-editor.redoButton" aria-label="<%= t('redo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
|
</div>
|
||||||
<%= svg_icon('arrow_forward_up', class: 'w-4 h-4') %>
|
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('italic') %> (Ctrl+I)">
|
||||||
</button>
|
<button type="button" data-action="click:markdown-editor#italic" data-target="markdown-editor.italicButton" aria-label="<%= t('italic') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
|
||||||
</div>
|
<%= svg_icon('italic', class: 'w-4 h-4') %>
|
||||||
</div>
|
</button>
|
||||||
<% if local_assigns[:variables]&.any? %>
|
</div>
|
||||||
<% variable_labels = { 'account.name' => t('variables.account_name'), 'submitter.link' => t('variables.submitter_link'), 'template.name' => t('variables.template_name'), 'submission.submitters' => t('variables.submission_submitters'), 'submission.link' => t('variables.submission_link'), 'documents.link' => t('variables.documents_link') } %>
|
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('underline') %> (Ctrl+U)">
|
||||||
<div class="dropdown dropdown-end ml-auto">
|
<button type="button" data-action="click:markdown-editor#underline" data-target="markdown-editor.underlineButton" aria-label="<%= t('underline') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
|
||||||
<label tabindex="0" class="flex items-center gap-1 text-sm px-2 py-1 rounded hover:bg-base-200 cursor-pointer">
|
<%= svg_icon('underline', class: 'w-4 h-4') %>
|
||||||
<%= t('add_variable') %>
|
</button>
|
||||||
<%= svg_icon('chevron_down', class: 'w-3.5 h-3.5') %>
|
</div>
|
||||||
</label>
|
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('link') %> (Ctrl+K)">
|
||||||
<div tabindex="0" class="dropdown-content right-0 top-full mt-1 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg z-50">
|
<button type="button" data-action="click:markdown-editor#linkSelection" data-target="markdown-editor.linkButton" aria-label="<%= t('link') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
|
||||||
<% local_assigns[:variables]&.each do |variable| %>
|
<%= svg_icon('link', class: 'w-4 h-4') %>
|
||||||
<button type="button" data-variable="<%= variable %>" data-action="click:markdown-editor#insertVariable" class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 text-left text-sm cursor-pointer whitespace-nowrap">
|
</button>
|
||||||
<%= variable_labels.fetch(variable, "{#{variable}}") %>
|
|
||||||
</button>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<div class="mx-2 h-5 border-l border-base-content/20"></div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('undo') %> (Ctrl+Z)">
|
||||||
|
<button type="button" data-action="click:markdown-editor#undo" data-target="markdown-editor.undoButton" aria-label="<%= t('undo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
|
||||||
|
<%= svg_icon('arrow_back_up', class: 'w-4 h-4') %>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('redo') %> (Ctrl+Shift+Z)">
|
||||||
|
<button type="button" data-action="click:markdown-editor#redo" data-target="markdown-editor.redoButton" aria-label="<%= t('redo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
|
||||||
|
<%= svg_icon('arrow_forward_up', class: 'w-4 h-4') %>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% if local_assigns[:variables]&.any? %>
|
||||||
|
<% variable_labels = { 'account.name' => t('variables.account_name'), 'submitter.link' => t('variables.submitter_link'), 'template.name' => t('variables.template_name'), 'submission.submitters' => t('variables.submission_submitters'), 'submission.link' => t('variables.submission_link'), 'documents.link' => t('variables.documents_link') } %>
|
||||||
|
<div class="dropdown dropdown-end ml-auto">
|
||||||
|
<label tabindex="0" class="flex items-center gap-1 text-sm px-2 py-1 rounded hover:bg-base-200 cursor-pointer">
|
||||||
|
<%= t('add_variable') %>
|
||||||
|
<%= svg_icon('chevron_down', class: 'w-3.5 h-3.5') %>
|
||||||
|
</label>
|
||||||
|
<div tabindex="0" class="dropdown-content right-0 top-full mt-1 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg z-50">
|
||||||
|
<% local_assigns[:variables]&.each do |variable| %>
|
||||||
|
<button type="button" data-variable="<%= variable %>" data-action="click:markdown-editor#insertVariable" class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 text-left text-sm cursor-pointer whitespace-nowrap">
|
||||||
|
<%= variable_labels.fetch(variable, "{#{variable}}") %>
|
||||||
|
</button>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<div data-target="markdown-editor.editorElement"></div>
|
||||||
</div>
|
</div>
|
||||||
<div data-target="markdown-editor.editorElement"></div>
|
<%= hidden_field_tag name, value, required: true, data: { target: 'markdown-editor.textarea' } %>
|
||||||
</div>
|
</markdown-editor>
|
||||||
<%= hidden_field_tag name, value, required: true, data: { target: 'markdown-editor.textarea' } %>
|
<% end %>
|
||||||
</markdown-editor>
|
|
||||||
@@ -8,12 +8,18 @@
|
|||||||
<div class="collapse-content">
|
<div class="collapse-content">
|
||||||
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
|
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
|
||||||
<%= f.hidden_field :key %>
|
<%= f.hidden_field :key %>
|
||||||
<%= f.fields_for :value, Struct.new(:subject, :body).new(*f.object.value.values_at('subject', 'body')) do |ff| %>
|
<%= f.fields_for :value, Struct.new(:subject, :body, :reply_to).new(*f.object.value.values_at('subject', 'body', 'reply_to')) do |ff| %>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= ff.label :subject, t('subject'), class: 'label' %>
|
<%= ff.label :subject, t('subject'), class: 'label' %>
|
||||||
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
|
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
|
||||||
</div>
|
</div>
|
||||||
<%= render 'personalization_settings/email_body_field', ff:, config: f.object %>
|
<%= render 'personalization_settings/email_body_field', ff:, config: f.object %>
|
||||||
|
<% if can?(:manage, :reply_to) || can?(:manage, :personalization_advanced) %>
|
||||||
|
<div class="form-control">
|
||||||
|
<%= ff.label :reply_to, t('reply_to'), class: 'label' %>
|
||||||
|
<%= ff.email_field :reply_to, class: 'base-input', dir: 'auto', placeholder: t(:email) %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="form-control pt-2">
|
<div class="form-control pt-2">
|
||||||
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
|
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<svg class="<%= local_assigns[:class] %>" height="<%= local_assigns.fetch(:height, '37') %>" width="<%= local_assigns.fetch(:width, '37') %>" style="color: #e0753f" viewBox="0 0 180 180" xmlns="http://www.w3.org/2000/svg">
|
<svg class="<%= local_assigns[:class] %>" height="<%= local_assigns.fetch(:height, '37') %>" width="<%= local_assigns.fetch(:width, '37') %>" style="color: #e97a42" viewBox="0 0 180 180" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill="currentColor" d="M 178.224 72.09 c -0.296 -1.463 -0.627 -2.919 -0.996 -4.364 -0.293 -1.151 -0.616 -2.293 -0.956 -3.433 -0.301 -1.008 -0.612 -2.014 -0.95 -3.012 -0.531 -1.578 -1.113 -3.142 -1.735 -4.694 -0.216 -0.54 -0.433 -1.082 -0.661 -1.618 -0.195 -0.462 -0.399 -0.917 -0.601 -1.375 -0.262 -0.591 -0.53 -1.177 -0.804 -1.762 -0.074 -0.159 -0.151 -0.315 -0.226 -0.474 -0.209 -0.441 -0.422 -0.881 -0.638 -1.318 -0.076 -0.154 -0.153 -0.306 -0.229 -0.459 -0.236 -0.471 -0.477 -0.939 -0.721 -1.406 -0.053 -0.101 -0.105 -0.201 -0.158 -0.302 -1.143 -2.16 -2.367 -4.269 -3.68 -6.322 -0.116 -0.181 -0.237 -0.359 -0.355 -0.539 -0.094 -0.144 -0.189 -0.288 -0.284 -0.432 -0.284 -0.431 -0.57 -0.861 -0.862 -1.287 -0.112 -0.164 -0.225 -0.326 -0.338 -0.489 -0.193 -0.279 -0.382 -0.56 -0.579 -0.836 -0.089 -0.125 -0.182 -0.249 -0.273 -0.374 -0.13 -0.182 -0.264 -0.362 -0.395 -0.542 -0.277 -0.38 -0.556 -0.76 -0.838 -1.135 -0.15 -0.199 -0.303 -0.395 -0.454 -0.593 -0.21 -0.274 -0.417 -0.552 -0.63 -0.823 -0.055 -0.069 -0.111 -0.136 -0.166 -0.205 -0.482 -0.61 -0.971 -1.216 -1.47 -1.814 -0.129 -0.155 -0.262 -0.306 -0.392 -0.461 -0.402 -0.476 -0.808 -0.95 -1.22 -1.417 -0.186 -0.212 -0.375 -0.422 -0.563 -0.631 -0.384 -0.428 -0.773 -0.854 -1.167 -1.276 -0.176 -0.189 -0.351 -0.379 -0.529 -0.567 -0.564 -0.595 -1.134 -1.186 -1.716 -1.768 -1.091 -1.091 -2.207 -2.15 -3.346 -3.178 -1.016 -0.919 -2.05 -1.815 -3.103 -2.684 -0.772 -0.636 -1.557 -1.255 -2.348 -1.864 -3.465 -2.67 -7.112 -5.075 -10.927 -7.209 -2.869 -1.604 -5.83 -3.06 -8.883 -4.351 -2.443 -1.033 -4.922 -1.948 -7.428 -2.756 -8.879 -2.863 -18.13 -4.318 -27.605 -4.318 -3.19 0 -6.354 0.169 -9.488 0.496 -4.036 0.421 -8.019 1.114 -11.94 2.073 -1.732 0.423 -3.452 0.892 -5.157 1.42 -2.856 0.883 -5.673 1.912 -8.447 3.085 -2.645 1.118 -5.222 2.357 -7.729 3.711 -2.574 1.39 -5.073 2.901 -7.494 4.533 -1.195 0.805 -2.37 1.64 -3.527 2.503 -1.156 0.864 -2.292 1.756 -3.408 2.676 -0.553 0.456 -1.1 0.919 -1.643 1.389 -1.649 1.427 -3.252 2.92 -4.806 4.473 -2.582 2.582 -4.991 5.299 -7.222 8.138 -0.892 1.135 -1.756 2.292 -2.59 3.467 -0.417 0.588 -0.827 1.18 -1.23 1.778 -0.403 0.597 -0.798 1.199 -1.186 1.806 -0.388 0.607 -0.769 1.218 -1.143 1.835 -2.241 3.697 -4.216 7.562 -5.916 11.582 -1.095 2.589 -2.059 5.217 -2.901 7.877 -0.153 0.482 -0.3 0.965 -0.444 1.449 -0.339 1.14 -0.663 2.282 -0.956 3.433 -0.369 1.446 -0.7 2.901 -0.996 4.364 -1.034 5.121 -1.618 10.343 -1.749 15.637 -0.018 0.757 -0.028 1.514 -0.028 2.274 0 1.123 0.02 2.244 0.062 3.361 0.285 7.82 1.568 15.475 3.825 22.879 0.044 0.147 0.088 0.295 0.133 0.441 0.877 2.823 1.894 5.608 3.054 8.35 0.85 2.009 1.769 3.98 2.755 5.912 0.539 1.057 1.105 2.099 1.685 3.132 4.013 7.142 8.98 13.698 14.846 19.564 7.713 7.713 16.611 13.878 26.477 18.352 0.705 0.32 1.415 0.632 2.131 0.935 2.081 0.88 4.185 1.679 6.313 2.396 9.217 3.106 18.85 4.677 28.719 4.677 8.031 0 15.902 -1.047 23.522 -3.107 0.633 -0.172 1.266 -0.35 1.895 -0.535 0.757 -0.222 1.509 -0.456 2.26 -0.698 0.717 -0.232 1.431 -0.474 2.145 -0.723 1.752 -0.616 3.49 -1.281 5.211 -2.009 0.755 -0.319 1.503 -0.651 2.247 -0.989 1.237 -0.563 2.459 -1.15 3.664 -1.766 0.644 -0.328 1.283 -0.665 1.917 -1.009 1.654 -0.896 3.274 -1.848 4.865 -2.844 5.736 -3.591 11.06 -7.827 15.912 -12.679 0.775 -0.775 1.534 -1.562 2.278 -2.36 5.204 -5.59 9.636 -11.754 13.246 -18.417 0.343 -0.634 0.68 -1.274 1.009 -1.917 0.482 -0.944 0.943 -1.9 1.392 -2.863 0.471 -1.007 0.928 -2.021 1.364 -3.049 1.22 -2.886 2.281 -5.82 3.187 -8.793 0.559 -1.833 1.056 -3.68 1.494 -5.542 0.108 -0.458 0.211 -0.916 0.312 -1.376 0.194 -0.883 0.373 -1.77 0.539 -2.659 1.02 -5.455 1.542 -11.02 1.542 -16.663 0 -6.074 -0.595 -12.058 -1.776 -17.911 z m -161.733 19.614 c -1.118 -56.662 44.604 -74.877 60.998 -67.647 2.187 0.965 4.732 2.431 7.042 2.96 5.295 1.213 13.432 -3.113 13.521 6.273 0.078 8.156 -3.389 13.108 -10.797 16.177 -7.539 3.124 -14.777 9.181 -19.95 15.493 -21.487 26.216 -31.231 68.556 -7.565 94.296 -13.679 -5.545 -42.418 -25.467 -43.248 -67.552 z m 91.109 72.619 c -0.053 0.008 -4.171 0.775 -4.171 0.775 0 0 -15.862 -22.957 -23.509 -21.719 11.291 16.04 12.649 22.625 12.649 22.625 -0.053 0.001 -0.107 0.001 -0.161 0.003 -51.831 2.131 -42.785 -64.026 -28.246 -86.502 -1.555 13.073 8.878 39.992 39.034 44.1 9.495 1.293 32.302 -3.275 41.015 -11.38 0.098 1.825 0.163 3.85 0.159 6.013 -0.046 23.538 -13.47 42.743 -36.77 46.085 z m 30.575 -15.708 c 9.647 -9.263 12.869 -27.779 9.103 -44.137 -4.608 -20.011 -28.861 -32.383 -40.744 -35.564 5.766 -8.089 27.908 -14.274 39.567 5.363 -5.172 -10.519 -13.556 -23.023 -1.732 -33.128 12.411 13.329 19.411 29.94 20.161 48.7 0.75 18.753 -6.64 41.768 -26.355 58.765 z" />
|
<path fill="currentColor" d="M 178.224 72.09 c -0.296 -1.463 -0.627 -2.919 -0.996 -4.364 -0.293 -1.151 -0.616 -2.293 -0.956 -3.433 -0.301 -1.008 -0.612 -2.014 -0.95 -3.012 -0.531 -1.578 -1.113 -3.142 -1.735 -4.694 -0.216 -0.54 -0.433 -1.082 -0.661 -1.618 -0.195 -0.462 -0.399 -0.917 -0.601 -1.375 -0.262 -0.591 -0.53 -1.177 -0.804 -1.762 -0.074 -0.159 -0.151 -0.315 -0.226 -0.474 -0.209 -0.441 -0.422 -0.881 -0.638 -1.318 -0.076 -0.154 -0.153 -0.306 -0.229 -0.459 -0.236 -0.471 -0.477 -0.939 -0.721 -1.406 -0.053 -0.101 -0.105 -0.201 -0.158 -0.302 -1.143 -2.16 -2.367 -4.269 -3.68 -6.322 -0.116 -0.181 -0.237 -0.359 -0.355 -0.539 -0.094 -0.144 -0.189 -0.288 -0.284 -0.432 -0.284 -0.431 -0.57 -0.861 -0.862 -1.287 -0.112 -0.164 -0.225 -0.326 -0.338 -0.489 -0.193 -0.279 -0.382 -0.56 -0.579 -0.836 -0.089 -0.125 -0.182 -0.249 -0.273 -0.374 -0.13 -0.182 -0.264 -0.362 -0.395 -0.542 -0.277 -0.38 -0.556 -0.76 -0.838 -1.135 -0.15 -0.199 -0.303 -0.395 -0.454 -0.593 -0.21 -0.274 -0.417 -0.552 -0.63 -0.823 -0.055 -0.069 -0.111 -0.136 -0.166 -0.205 -0.482 -0.61 -0.971 -1.216 -1.47 -1.814 -0.129 -0.155 -0.262 -0.306 -0.392 -0.461 -0.402 -0.476 -0.808 -0.95 -1.22 -1.417 -0.186 -0.212 -0.375 -0.422 -0.563 -0.631 -0.384 -0.428 -0.773 -0.854 -1.167 -1.276 -0.176 -0.189 -0.351 -0.379 -0.529 -0.567 -0.564 -0.595 -1.134 -1.186 -1.716 -1.768 -1.091 -1.091 -2.207 -2.15 -3.346 -3.178 -1.016 -0.919 -2.05 -1.815 -3.103 -2.684 -0.772 -0.636 -1.557 -1.255 -2.348 -1.864 -3.465 -2.67 -7.112 -5.075 -10.927 -7.209 -2.869 -1.604 -5.83 -3.06 -8.883 -4.351 -2.443 -1.033 -4.922 -1.948 -7.428 -2.756 -8.879 -2.863 -18.13 -4.318 -27.605 -4.318 -3.19 0 -6.354 0.169 -9.488 0.496 -4.036 0.421 -8.019 1.114 -11.94 2.073 -1.732 0.423 -3.452 0.892 -5.157 1.42 -2.856 0.883 -5.673 1.912 -8.447 3.085 -2.645 1.118 -5.222 2.357 -7.729 3.711 -2.574 1.39 -5.073 2.901 -7.494 4.533 -1.195 0.805 -2.37 1.64 -3.527 2.503 -1.156 0.864 -2.292 1.756 -3.408 2.676 -0.553 0.456 -1.1 0.919 -1.643 1.389 -1.649 1.427 -3.252 2.92 -4.806 4.473 -2.582 2.582 -4.991 5.299 -7.222 8.138 -0.892 1.135 -1.756 2.292 -2.59 3.467 -0.417 0.588 -0.827 1.18 -1.23 1.778 -0.403 0.597 -0.798 1.199 -1.186 1.806 -0.388 0.607 -0.769 1.218 -1.143 1.835 -2.241 3.697 -4.216 7.562 -5.916 11.582 -1.095 2.589 -2.059 5.217 -2.901 7.877 -0.153 0.482 -0.3 0.965 -0.444 1.449 -0.339 1.14 -0.663 2.282 -0.956 3.433 -0.369 1.446 -0.7 2.901 -0.996 4.364 -1.034 5.121 -1.618 10.343 -1.749 15.637 -0.018 0.757 -0.028 1.514 -0.028 2.274 0 1.123 0.02 2.244 0.062 3.361 0.285 7.82 1.568 15.475 3.825 22.879 0.044 0.147 0.088 0.295 0.133 0.441 0.877 2.823 1.894 5.608 3.054 8.35 0.85 2.009 1.769 3.98 2.755 5.912 0.539 1.057 1.105 2.099 1.685 3.132 4.013 7.142 8.98 13.698 14.846 19.564 7.713 7.713 16.611 13.878 26.477 18.352 0.705 0.32 1.415 0.632 2.131 0.935 2.081 0.88 4.185 1.679 6.313 2.396 9.217 3.106 18.85 4.677 28.719 4.677 8.031 0 15.902 -1.047 23.522 -3.107 0.633 -0.172 1.266 -0.35 1.895 -0.535 0.757 -0.222 1.509 -0.456 2.26 -0.698 0.717 -0.232 1.431 -0.474 2.145 -0.723 1.752 -0.616 3.49 -1.281 5.211 -2.009 0.755 -0.319 1.503 -0.651 2.247 -0.989 1.237 -0.563 2.459 -1.15 3.664 -1.766 0.644 -0.328 1.283 -0.665 1.917 -1.009 1.654 -0.896 3.274 -1.848 4.865 -2.844 5.736 -3.591 11.06 -7.827 15.912 -12.679 0.775 -0.775 1.534 -1.562 2.278 -2.36 5.204 -5.59 9.636 -11.754 13.246 -18.417 0.343 -0.634 0.68 -1.274 1.009 -1.917 0.482 -0.944 0.943 -1.9 1.392 -2.863 0.471 -1.007 0.928 -2.021 1.364 -3.049 1.22 -2.886 2.281 -5.82 3.187 -8.793 0.559 -1.833 1.056 -3.68 1.494 -5.542 0.108 -0.458 0.211 -0.916 0.312 -1.376 0.194 -0.883 0.373 -1.77 0.539 -2.659 1.02 -5.455 1.542 -11.02 1.542 -16.663 0 -6.074 -0.595 -12.058 -1.776 -17.911 z m -161.733 19.614 c -1.118 -56.662 44.604 -74.877 60.998 -67.647 2.187 0.965 4.732 2.431 7.042 2.96 5.295 1.213 13.432 -3.113 13.521 6.273 0.078 8.156 -3.389 13.108 -10.797 16.177 -7.539 3.124 -14.777 9.181 -19.95 15.493 -21.487 26.216 -31.231 68.556 -7.565 94.296 -13.679 -5.545 -42.418 -25.467 -43.248 -67.552 z m 91.109 72.619 c -0.053 0.008 -4.171 0.775 -4.171 0.775 0 0 -15.862 -22.957 -23.509 -21.719 11.291 16.04 12.649 22.625 12.649 22.625 -0.053 0.001 -0.107 0.001 -0.161 0.003 -51.831 2.131 -42.785 -64.026 -28.246 -86.502 -1.555 13.073 8.878 39.992 39.034 44.1 9.495 1.293 32.302 -3.275 41.015 -11.38 0.098 1.825 0.163 3.85 0.159 6.013 -0.046 23.538 -13.47 42.743 -36.77 46.085 z m 30.575 -15.708 c 9.647 -9.263 12.869 -27.779 9.103 -44.137 -4.608 -20.011 -28.861 -32.383 -40.744 -35.564 5.766 -8.089 27.908 -14.274 39.567 5.363 -5.172 -10.519 -13.556 -23.023 -1.732 -33.128 12.411 13.329 19.411 29.94 20.161 48.7 0.75 18.753 -6.64 41.768 -26.355 58.765 z" />
|
||||||
<circle fill="currentColor" cx="71.927" cy="32.004" r="2.829" />
|
<circle fill="currentColor" cx="71.927" cy="32.004" r="2.829" />
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
@@ -13,12 +13,12 @@
|
|||||||
<%= link_to t('account'), settings_account_path, class: 'text-base hover:bg-base-300' %>
|
<%= link_to t('account'), settings_account_path, class: 'text-base hover:bg-base-300' %>
|
||||||
</li>
|
</li>
|
||||||
<% unless Docuseal.multitenant? %>
|
<% unless Docuseal.multitenant? %>
|
||||||
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::EMAIL_SMTP_KEY, account: current_account)) && ENV['SMTP_ADDRESS'].blank? && true_user == current_user %>
|
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::EMAIL_SMTP_KEY, account: current_account)) && ENV['SMTP_ADDRESS'].blank? && true_user == current_user %>
|
||||||
<li>
|
<li>
|
||||||
<%= link_to t('email'), settings_email_index_path, class: 'text-base hover:bg-base-300' %>
|
<%= link_to t('email'), settings_email_index_path, class: 'text-base hover:bg-base-300' %>
|
||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::FILES_STORAGE_KEY, account: current_account)) && true_user == current_user %>
|
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::FILES_STORAGE_KEY, account: current_account)) && true_user == current_user && ENV['S3_ATTACHMENTS_BUCKET'].blank? && ENV['GCS_BUCKET'].blank? && ENV['AZURE_CONTAINER'].blank? %>
|
||||||
<li>
|
<li>
|
||||||
<%= link_to t('storage'), settings_storage_index_path, class: 'text-base hover:bg-base-300' %>
|
<%= link_to t('storage'), settings_storage_index_path, class: 'text-base hover:bg-base-300' %>
|
||||||
</li>
|
</li>
|
||||||
@@ -90,6 +90,11 @@
|
|||||||
<%= link_to 'SSO', settings_sso_index_path, class: 'text-base hover:bg-base-300' %>
|
<%= link_to 'SSO', settings_sso_index_path, class: 'text-base hover:bg-base-300' %>
|
||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% if !Docuseal.multitenant? && can?(:read, McpToken) && can?(:manage, :mcp) %>
|
||||||
|
<li>
|
||||||
|
<%= link_to 'MCP', settings_mcp_index_path, class: 'text-base hover:bg-base-300' %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
<%= render 'shared/settings_nav_extra2' %>
|
<%= render 'shared/settings_nav_extra2' %>
|
||||||
<% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %>
|
<% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %>
|
||||||
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'w-full' } do |f| %>
|
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'w-full' } do |f| %>
|
||||||
|
|||||||
@@ -20,10 +20,13 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
|
<% submitter_versions_index = @submission.submitters.preload(:submitter_versions).each_with_object({}) { |s, h| h[s.id] = s.submitter_versions.to_a.sort_by(&:created_at) } %>
|
||||||
<% @submission.submission_events.order(:event_timestamp).each do |event| %>
|
<% @submission.submission_events.order(:event_timestamp).each do |event| %>
|
||||||
<% submitter = @submission.submitters.find { |e| e.id == event.submitter_id } %>
|
<% submitter = @submission.submitters.find { |e| e.id == event.submitter_id } %>
|
||||||
<% bg_class = event_colors[submitters_uuids.index(submitter.uuid) % event_colors.length] %>
|
<% bg_class = event_colors[submitters_uuids.index(submitter.uuid) % event_colors.length] %>
|
||||||
<% submitter_name = event.event_type.include?('sms') || event.event_type.include?('phone') ? (event.data['phone'] || submitter.phone) : (submitter.name || submitter.email || submitter.phone) %>
|
<% versions = submitter_versions_index[submitter.id] || [] %>
|
||||||
|
<% active_version = versions.find { |v| v.created_at > event.event_timestamp } %>
|
||||||
|
<% submitter_name = event.event_type.include?('sms') || event.event_type.include?('phone') ? (event.data['phone'] || active_version&.phone || submitter.phone) : (active_version&.name || active_version&.email || active_version&.phone || submitter.name || submitter.email || submitter.phone) %>
|
||||||
<li class="ml-7">
|
<li class="ml-7">
|
||||||
<span class="absolute flex items-center justify-center w-7 h-7 rounded-full -start-3.5 ring-8 ring-base-100 text-base-content <%= bg_class %>">
|
<span class="absolute flex items-center justify-center w-7 h-7 rounded-full -start-3.5 ring-8 ring-base-100 text-base-content <%= bg_class %>">
|
||||||
<%= svg_icon(SubmissionEventsController::SUBMISSION_EVENT_ICONS.fetch(event.event_type, 'circle_dot'), class: 'w-4 h-4') %>
|
<%= svg_icon(SubmissionEventsController::SUBMISSION_EVENT_ICONS.fetch(event.event_type, 'circle_dot'), class: 'w-4 h-4') %>
|
||||||
@@ -43,6 +46,9 @@
|
|||||||
<%= t('submission_event_names.complete_verification_by_html', provider: event.data['method'], submitter_name:) %>
|
<%= t('submission_event_names.complete_verification_by_html', provider: event.data['method'], submitter_name:) %>
|
||||||
<% elsif event.event_type == 'invite_party' && (invited_submitter = @submission.submitters.find { |e| e.uuid == event.data['uuid'] }) && (name = @submission.template_submitters.find { |e| e['uuid'] == event.data['uuid'] }&.dig('name')) %>
|
<% elsif event.event_type == 'invite_party' && (invited_submitter = @submission.submitters.find { |e| e.uuid == event.data['uuid'] }) && (name = @submission.template_submitters.find { |e| e['uuid'] == event.data['uuid'] }&.dig('name')) %>
|
||||||
<%= t('submission_event_names.invite_party_by_html', invited_submitter_name: [invited_submitter.name || invited_submitter.email || invited_submitter.phone, name].join(' '), submitter_name:) %>
|
<%= t('submission_event_names.invite_party_by_html', invited_submitter_name: [invited_submitter.name || invited_submitter.email || invited_submitter.phone, name].join(' '), submitter_name:) %>
|
||||||
|
<% elsif event.event_type == 'delegate_form' %>
|
||||||
|
<% delegate_from = event.data['old_email'].presence || (versions.reverse.find { |v| v.created_at <= event.event_timestamp }&.then { |v| v.name || v.phone }) %>
|
||||||
|
<%= t('submission_event_names.delegate_form_by_html', from: delegate_from, to: event.data['email']) %>
|
||||||
<% elsif event.event_type.include?('send_') %>
|
<% elsif event.event_type.include?('send_') %>
|
||||||
<%= t("submission_event_names.#{event.event_type}_to_html", submitter_name:) %>
|
<%= t("submission_event_names.#{event.event_type}_to_html", submitter_name:) %>
|
||||||
<% elsif event.event_type.start_with?('bounce_') || event.event_type.start_with?('complaint_') %>
|
<% elsif event.event_type.start_with?('bounce_') || event.event_type.start_with?('complaint_') %>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
<% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %>
|
<% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %>
|
||||||
<dynamic-list class="space-y-4">
|
<dynamic-list class="space-y-4">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="card card-compact bg-base-300/40" data-targets="dynamic-list.items">
|
<div class="relative <%= 'p-4 rounded-xl bg-base-300/40' unless local_assigns[:variables_form] %>" data-targets="dynamic-list.items">
|
||||||
<div class="card-body">
|
<div>
|
||||||
<div class="absolute right-4 top-5">
|
<div class="absolute right-4 top-5">
|
||||||
<a href="#" data-action="click:dynamic-list#removeItem" class="<%= submitters.size == 1 ? 'right-2 top-1' : '-top-3' %> relative block w-6 h-6 rounded-lg text-neutral-700 text-center bg-base-300 p-1 hidden hover:bg-neutral hover:text-white">
|
<a href="#" data-action="click:dynamic-list#removeItem" class="<%= submitters.size == 1 ? 'right-2 top-1' : '-top-3' %> relative block w-6 h-6 rounded-lg text-neutral-700 text-center bg-base-300 p-1 hidden hover:bg-neutral hover:text-white">
|
||||||
<%= svg_icon('trash', class: 'w-4 h-4') %>
|
<%= svg_icon('trash', class: 'w-4 h-4') %>
|
||||||
@@ -86,13 +86,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% if params[:selfsign].blank? && local_assigns[:prefillable_fields].blank? && local_assigns[:recipient_form_fields].blank? %>
|
<% if params[:selfsign].blank? && local_assigns[:prefillable_fields].blank? && local_assigns[:recipient_form_fields].blank? && local_assigns[:variables_form].blank? %>
|
||||||
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem">
|
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem">
|
||||||
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
|
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
|
||||||
<span><%= t('add_new') %></span>
|
<span><%= t('add_new') %></span>
|
||||||
</a>
|
</a>
|
||||||
<% end %>
|
<% end %>
|
||||||
</dynamic-list>
|
</dynamic-list>
|
||||||
|
<%= local_assigns[:variables_form] %>
|
||||||
<div>
|
<div>
|
||||||
<%= render('submitters_order', f:, template:) if Accounts.can_send_emails?(current_account) %>
|
<%= render('submitters_order', f:, template:) if Accounts.can_send_emails?(current_account) %>
|
||||||
<%= render 'send_email', f:, template: %>
|
<%= render 'send_email', f:, template: %>
|
||||||
|
|||||||
@@ -1,6 +1,27 @@
|
|||||||
<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
|
<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
|
||||||
<% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %>
|
<% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %>
|
||||||
<% if submitters.size == 1 %>
|
<% if submitters.size == 1 && local_assigns[:variables_form] %>
|
||||||
|
<% item = submitters.first %>
|
||||||
|
<div class="grid gap-1">
|
||||||
|
<submitter-item class="grid md:grid-cols-2 gap-1">
|
||||||
|
<div class="form-control">
|
||||||
|
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
||||||
|
<submitters-autocomplete data-field="email">
|
||||||
|
<linked-input data-target-id="<%= "email_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
||||||
|
<%= tag.input type: 'email', multiple: true, name: 'submission[1][submitters][][email]', autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: t('email'), required: true, value: item['email'].presence || (params[:selfsign] || item['is_requester'] ? current_user.email : ''), id: "email_#{item['uuid']}" %>
|
||||||
|
</linked-input>
|
||||||
|
</submitters-autocomplete>
|
||||||
|
</div>
|
||||||
|
<div class="form-control flex">
|
||||||
|
<submitters-autocomplete data-field="name">
|
||||||
|
<linked-input data-target-id="<%= "email_name_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
||||||
|
<input type="text" name="submission[1][submitters][][name]" autocomplete="off" class="base-input !h-10 w-full" placeholder="<%= "#{t('name')} (#{t('optional')})" %>" value="<%= params[:selfsign] || item['is_requester'] ? current_user.full_name : '' %>" dir="auto" id="email_name_<%= item['uuid'] %>">
|
||||||
|
</linked-input>
|
||||||
|
</submitters-autocomplete>
|
||||||
|
</div>
|
||||||
|
</submitter-item>
|
||||||
|
</div>
|
||||||
|
<% elsif submitters.size == 1 %>
|
||||||
<submitter-item class="form-control">
|
<submitter-item class="form-control">
|
||||||
<emails-textarea data-bulk-enabled="<%= Docuseal.demo? || !Docuseal.multitenant? || can?(:manage, :bulk_send) %>" data-limit="<%= Docuseal.multitenant? ? (can?(:manage, :bulk_send) ? 40 : 1) : nil %>">
|
<emails-textarea data-bulk-enabled="<%= Docuseal.demo? || !Docuseal.multitenant? || can?(:manage, :bulk_send) %>" data-limit="<%= Docuseal.multitenant? ? (can?(:manage, :bulk_send) ? 40 : 1) : nil %>">
|
||||||
<submitters-autocomplete data-field="email" class="block relative">
|
<submitters-autocomplete data-field="email" class="block relative">
|
||||||
@@ -13,8 +34,8 @@
|
|||||||
<% else %>
|
<% else %>
|
||||||
<dynamic-list class="space-y-4">
|
<dynamic-list class="space-y-4">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="card card-compact bg-base-300/40" data-targets="dynamic-list.items">
|
<div class="relative <%= 'p-4 rounded-xl bg-base-300/40' unless local_assigns[:variables_form] %>" data-targets="dynamic-list.items">
|
||||||
<div class="card-body">
|
<div>
|
||||||
<div class="absolute right-4 top-5">
|
<div class="absolute right-4 top-5">
|
||||||
<a href="#" data-action="click:dynamic-list#removeItem" class="-top-3 relative block w-6 h-6 rounded-lg text-neutral-700 text-center bg-base-300 p-1 hidden hover:bg-neutral hover:text-white">
|
<a href="#" data-action="click:dynamic-list#removeItem" class="-top-3 relative block w-6 h-6 rounded-lg text-neutral-700 text-center bg-base-300 p-1 hidden hover:bg-neutral hover:text-white">
|
||||||
<%= svg_icon('trash', class: 'w-4 h-4') %>
|
<%= svg_icon('trash', class: 'w-4 h-4') %>
|
||||||
@@ -38,7 +59,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% if params[:selfsign].blank? %>
|
<% if params[:selfsign].blank? && local_assigns[:variables_form].blank? %>
|
||||||
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem">
|
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem">
|
||||||
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
|
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
|
||||||
<span><%= t('add_new') %></span>
|
<span><%= t('add_new') %></span>
|
||||||
@@ -46,6 +67,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</dynamic-list>
|
</dynamic-list>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<%= local_assigns[:variables_form] %>
|
||||||
<div>
|
<div>
|
||||||
<%= render('submitters_order', f:, template:) if Accounts.can_send_emails?(current_account) %>
|
<%= render('submitters_order', f:, template:) if Accounts.can_send_emails?(current_account) %>
|
||||||
<%= render 'send_email', f:, template: %>
|
<%= render 'send_email', f:, template: %>
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
<% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %>
|
<% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %>
|
||||||
<dynamic-list class="space-y-4">
|
<dynamic-list class="space-y-4">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="card card-compact bg-base-300/40" data-targets="dynamic-list.items">
|
<div class="relative <%= 'p-4 rounded-xl bg-base-300/40' unless local_assigns[:variables_form] %>" data-targets="dynamic-list.items">
|
||||||
<div class="card-body">
|
<div>
|
||||||
<div class="absolute right-4 top-5">
|
<div class="absolute right-4 top-5">
|
||||||
<a href="#" data-action="click:dynamic-list#removeItem" class="<%= submitters.size == 1 ? 'right-2 top-1' : '-top-3' %> relative block w-6 h-6 rounded-lg text-neutral-700 text-center bg-base-300 p-1 hidden hover:bg-neutral hover:text-white">
|
<a href="#" data-action="click:dynamic-list#removeItem" class="<%= submitters.size == 1 ? 'right-2 top-1' : '-top-3' %> relative block w-6 h-6 rounded-lg text-neutral-700 text-center bg-base-300 p-1 hidden hover:bg-neutral hover:text-white">
|
||||||
<%= svg_icon('trash', class: 'w-4 h-4') %>
|
<%= svg_icon('trash', class: 'w-4 h-4') %>
|
||||||
@@ -49,13 +49,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% if params[:selfsign].blank? %>
|
<% if params[:selfsign].blank? && local_assigns[:variables_form].blank? %>
|
||||||
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem">
|
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem">
|
||||||
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
|
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
|
||||||
<span><%= t('add_new') %></span>
|
<span><%= t('add_new') %></span>
|
||||||
</a>
|
</a>
|
||||||
<% end %>
|
<% end %>
|
||||||
</dynamic-list>
|
</dynamic-list>
|
||||||
|
<%= local_assigns[:variables_form] %>
|
||||||
<div>
|
<div>
|
||||||
<%= render('submitters_order', f:, template:) if Accounts.can_send_emails?(current_account) %>
|
<%= render('submitters_order', f:, template:) if Accounts.can_send_emails?(current_account) %>
|
||||||
<%= render 'send_sms', f:, checked: true %>
|
<%= render 'send_sms', f:, checked: true %>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<% if template.preferences['submitters_order'] == 'preserved' || template.submitters.any? { |s| s['order'] } %>
|
<% if template.preferences['submitters_order'] == 'preserved' || template.submitters.any? { |s| s['order'] } || (template.submitters.size > 1 && template.fields.any? { |f| f['type'] == 'verification' }) %>
|
||||||
<%= f.hidden_field :preserve_order, value: '1' %>
|
<%= f.hidden_field :preserve_order, value: '1' %>
|
||||||
<% elsif template.submitters.size > 1 %>
|
<% elsif template.submitters.size > 1 %>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
|
|||||||
Whitespace-only changes.
@@ -4,35 +4,41 @@
|
|||||||
<% default_tab = cookies.permanent[:add_recipients_tab].presence || 'email' %>
|
<% default_tab = cookies.permanent[:add_recipients_tab].presence || 'email' %>
|
||||||
<% recipient_form_fields = Accounts.load_recipient_form_fields(current_account) if prefillable_fields.blank? %>
|
<% recipient_form_fields = Accounts.load_recipient_form_fields(current_account) if prefillable_fields.blank? %>
|
||||||
<% only_detailed = require_phone_2fa || require_email_2fa || prefillable_fields.present? || recipient_form_fields.present? %>
|
<% only_detailed = require_phone_2fa || require_email_2fa || prefillable_fields.present? || recipient_form_fields.present? %>
|
||||||
|
<% with_list = @template.variables_schema.blank? %>
|
||||||
|
<% variables_form = render 'variables_form', schema: @template.variables_schema if @template.variables_schema.present? && @template.variables_schema.any? { |_, v| !v['disabled'] } %>
|
||||||
<%= render 'shared/turbo_modal_large', title: params[:selfsign] ? t('add_recipients') : t('add_new_recipients') do %>
|
<%= render 'shared/turbo_modal_large', title: params[:selfsign] ? t('add_recipients') : t('add_new_recipients') do %>
|
||||||
<% options = [only_detailed ? nil : [t('via_email'), 'email'], only_detailed ? nil : [t('via_phone'), 'phone'], [t('detailed'), 'detailed'], [t('upload_list'), 'list']].compact %>
|
<% options = [only_detailed ? nil : [t('via_email'), 'email'], only_detailed ? nil : [t('via_phone'), 'phone'], [t('detailed'), 'detailed'], with_list ? [t('upload_list'), 'list'] : nil].compact %>
|
||||||
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center px-2 mt-4 block">
|
<% if options.size > 1 %>
|
||||||
<div class="flex justify-center">
|
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center px-2 mt-4 block">
|
||||||
<% options.each_with_index do |(label, value), index| %>
|
<div class="flex justify-center">
|
||||||
<%= content_tag(value == 'list' ? 'span' : 'toggle-cookies', data: { value:, key: 'add_recipients_tab' }) do %>
|
<% options.each_with_index do |(label, value), index| %>
|
||||||
<%= radio_button_tag 'option', value, value == (only_detailed ? 'detailed' : default_tab), class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %>
|
<%= content_tag(value == 'list' ? 'span' : 'toggle-cookies', data: { value:, key: 'add_recipients_tab' }) do %>
|
||||||
<label for="option_<%= value %>" class="block bg-base-200 md:min-w-[112px] text-sm font-semibold whitespace-nowrap py-1.5 px-4 peer-checked:bg-base-300 <%= 'hidden sm:inline-block' if value == 'list' %> <%= 'rounded-l-3xl' if index.zero? %> <%= 'rounded-r-3xl sm:rounded-r-none' if value == 'detailed' %> <%= 'rounded-r-3xl' if index == options.size - 1 %>">
|
<%= radio_button_tag 'option', value, value == (only_detailed ? 'detailed' : default_tab), class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %>
|
||||||
<%= label %>
|
<label for="option_<%= value %>" class="block bg-base-200 md:min-w-[112px] text-sm font-semibold whitespace-nowrap py-1.5 px-4 peer-checked:bg-base-300 <%= 'hidden sm:inline-block' if value == 'list' %> <%= 'rounded-l-3xl' if index.zero? %> <%= 'rounded-r-3xl sm:rounded-r-none' if value == 'detailed' && index != options.size - 1 %> <%= 'rounded-r-3xl' if index == options.size - 1 %>">
|
||||||
</label>
|
<%= label %>
|
||||||
|
</label>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
</div>
|
||||||
</div>
|
</toggle-visible>
|
||||||
</toggle-visible>
|
<% end %>
|
||||||
<div class="px-5 mb-5 mt-4">
|
<div class="px-5 mb-5 mt-4">
|
||||||
<% unless only_detailed %>
|
<% unless only_detailed %>
|
||||||
<div id="email" class="<%= 'hidden' if default_tab != 'email' %>">
|
<div id="email" class="<%= 'hidden' if default_tab != 'email' %>">
|
||||||
<%= render 'email_form', template: @template %>
|
<%= render 'email_form', template: @template, variables_form: %>
|
||||||
</div>
|
</div>
|
||||||
<div id="phone" class="<%= 'hidden' if default_tab != 'phone' %>">
|
<div id="phone" class="<%= 'hidden' if default_tab != 'phone' %>">
|
||||||
<%= render 'phone_form', template: @template %>
|
<%= render 'phone_form', template: @template, variables_form: %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div id="detailed" class="<%= 'hidden' if !only_detailed && default_tab != 'detailed' %>">
|
<div id="detailed" class="<%= 'hidden' if !only_detailed && default_tab != 'detailed' %>">
|
||||||
<%= render 'detailed_form', template: @template, require_phone_2fa:, require_email_2fa:, prefillable_fields:, recipient_form_fields: %>
|
<%= render 'detailed_form', template: @template, require_phone_2fa:, require_email_2fa:, prefillable_fields:, recipient_form_fields:, variables_form: %>
|
||||||
</div>
|
|
||||||
<div id="list" class="hidden">
|
|
||||||
<%= render 'list_form', template: @template %>
|
|
||||||
</div>
|
</div>
|
||||||
|
<% if with_list %>
|
||||||
|
<div id="list" class="hidden">
|
||||||
|
<%= render 'list_form', template: @template %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
<%= render 'submissions/error' %>
|
<%= render 'submissions/error' %>
|
||||||
</div>
|
</div>
|
||||||
<%= content_for(:modal_extra) %>
|
<%= content_for(:modal_extra) %>
|
||||||
|
|||||||
@@ -104,7 +104,7 @@
|
|||||||
<% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %>
|
<% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %>
|
||||||
<% lazyload_metadata = document.preview_images.first&.metadata || Templates::ProcessDocument::US_LETTER_SIZE %>
|
<% lazyload_metadata = document.preview_images.first&.metadata || Templates::ProcessDocument::US_LETTER_SIZE %>
|
||||||
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
|
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
|
||||||
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_uuid, "#{index}.jpg")) %>
|
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_key, "#{index}.jpg")) %>
|
||||||
<page-container id="<%= "page-#{document.uuid}-#{index}" %>" class="block before:border before:absolute before:top-0 before:bottom-0 before:left-0 before:right-0 before:rounded relative mb-4" style="container-type: size; aspect-ratio: <%= width = page.metadata['width'] %> / <%= height = page.metadata['height'] %>">
|
<page-container id="<%= "page-#{document.uuid}-#{index}" %>" class="block before:border before:absolute before:top-0 before:bottom-0 before:left-0 before:right-0 before:rounded relative mb-4" style="container-type: size; aspect-ratio: <%= width = page.metadata['width'] %> / <%= height = page.metadata['height'] %>">
|
||||||
<img loading="lazy" src="<%= page.url %>" width="<%= width %>" class="rounded" height="<%= height %>">
|
<img loading="lazy" src="<%= page.url %>" width="<%= width %>" class="rounded" height="<%= height %>">
|
||||||
<div class="top-0 bottom-0 left-0 right-0 absolute">
|
<div class="top-0 bottom-0 left-0 right-0 absolute">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<% filter_params = params.permit(:q, *Submissions::Filter::ALLOWED_PARAMS) %>
|
<% filter_params = params.permit(:q, :archived, *Submissions::Filter::ALLOWED_PARAMS) %>
|
||||||
<%= render 'shared/turbo_modal', title: t('export'), close_after_submit: false do %>
|
<%= render 'shared/turbo_modal', title: t('export'), close_after_submit: false do %>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<%= button_to template_submissions_export_index_path(@template), params: { format: :xlsx, **filter_params }, method: :get, data: { turbo_frame: :_top } do %>
|
<%= button_to template_submissions_export_index_path(@template), params: { format: :xlsx, **filter_params }, method: :get, data: { turbo_frame: :_top } do %>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<%= form_for '', url: submit_form_delegate_index_path(submitter.slug), method: :post do |f| %>
|
||||||
|
<div class="form-control mt-2">
|
||||||
|
<%= f.label :email, t(:enter_the_email_address_of_the_person_you_want_to_delegate_to), class: 'label' %>
|
||||||
|
<%= f.email_field :email, required: true, class: 'base-input w-full', dir: 'auto', placeholder: t('email') %>
|
||||||
|
</div>
|
||||||
|
<toggle-submit dir="auto" class="form-control mt-4">
|
||||||
|
<%= f.button button_title(title: t(:delegate)), class: 'base-button' %>
|
||||||
|
</toggle-submit>
|
||||||
|
<% end %>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
|
||||||
|
<div class="space-y-6 mx-auto">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<%= render 'start_form/banner' %>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="mr-3">
|
||||||
|
<%= svg_icon('user_share', class: 'w-10 h-10') %>
|
||||||
|
</div>
|
||||||
|
<div dir="auto">
|
||||||
|
<p class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template.name %></p>
|
||||||
|
<p class="text-sm"><%= t('document_has_been_delegated_on_html', time: l(@submitter.submission_events.order(event_timestamp: :desc).find_by!(event_type: :delegate_form).event_timestamp, format: :long)) %></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<%= render 'shared/attribution', link_path: '/start', account: @submitter.account %>
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
<% schema = Submissions.filtered_conditions_schema(@submitter.submission, values:, include_submitter_uuid: @submitter.uuid) %>
|
<% schema = Submissions.filtered_conditions_schema(@submitter.submission, values:, include_submitter_uuid: @submitter.uuid) %>
|
||||||
<% font_scale = 1000.0 / PdfUtils::US_LETTER_W %>
|
<% font_scale = 1000.0 / PdfUtils::US_LETTER_W %>
|
||||||
<% decline_modal_checkbox_uuid = nil %>
|
<% decline_modal_checkbox_uuid = nil %>
|
||||||
|
<% delegate_modal_checkbox_uuid = nil %>
|
||||||
<div style="max-height: -webkit-fill-available;">
|
<div style="max-height: -webkit-fill-available;">
|
||||||
<div id="scrollbox">
|
<div id="scrollbox">
|
||||||
<div class="mx-auto block pb-72" style="max-width: 1000px">
|
<div class="mx-auto block pb-72" style="max-width: 1000px">
|
||||||
@@ -20,42 +21,85 @@
|
|||||||
<%= @submitter.submission.name || @submitter.submission.template.name %>
|
<%= @submitter.submission.name || @submitter.submission.template.name %>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-2" style="margin-left: 20px; flex-shrink: 0">
|
<div class="flex items-center space-x-2" style="margin-left: 20px; flex-shrink: 0">
|
||||||
<% if @form_configs[:with_decline] %>
|
<% if @form_configs[:with_delegate] %>
|
||||||
<label id="decline_button" for="<%= decline_modal_checkbox_uuid = SecureRandom.uuid %>" class="btn btn-sm !px-5"><%= t(:decline) %></label>
|
<label id="delegate_button" for="<%= delegate_modal_checkbox_uuid = SecureRandom.uuid %>" class="btn btn-sm !px-5"><%= t(:delegate) %></label>
|
||||||
<% end %>
|
<% if @form_configs[:with_decline] %>
|
||||||
<% if @form_configs[:with_partial_download] %>
|
<label id="decline_button" for="<%= decline_modal_checkbox_uuid = SecureRandom.uuid %>" class="btn btn-sm px-2" title="<%= t(:decline) %>">
|
||||||
<download-button data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm !px-4">
|
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||||
<span class="flex items-center justify-center" data-target="download-button.defaultButton">
|
</label>
|
||||||
<%= svg_icon('download', class: 'w-6 h-6 inline md:hidden') %>
|
<% end %>
|
||||||
<span class="hidden md:inline"><%= t('download') %></span>
|
<% if @form_configs[:with_partial_download] %>
|
||||||
</span>
|
<download-button data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm px-2" title="<%= t('download') %>">
|
||||||
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
|
<span data-target="download-button.defaultButton">
|
||||||
<%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
|
<%= svg_icon('download', class: 'w-5 h-5') %>
|
||||||
<span class="hidden md:inline"><%= t('downloading') %></span>
|
</span>
|
||||||
</span>
|
<span class="hidden" data-target="download-button.loadingButton">
|
||||||
</download-button>
|
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin') %>
|
||||||
|
</span>
|
||||||
|
</download-button>
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
<% if @form_configs[:with_decline] %>
|
||||||
|
<label id="decline_button" for="<%= decline_modal_checkbox_uuid = SecureRandom.uuid %>" class="btn btn-sm !px-5"><%= t(:decline) %></label>
|
||||||
|
<% end %>
|
||||||
|
<% if @form_configs[:with_partial_download] %>
|
||||||
|
<download-button data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm !px-4">
|
||||||
|
<span class="flex items-center justify-center" data-target="download-button.defaultButton">
|
||||||
|
<%= svg_icon('download', class: 'w-6 h-6 inline md:hidden') %>
|
||||||
|
<span class="hidden md:inline"><%= t('download') %></span>
|
||||||
|
</span>
|
||||||
|
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
|
||||||
|
<%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
|
||||||
|
<span class="hidden md:inline"><%= t('downloading') %></span>
|
||||||
|
</span>
|
||||||
|
</download-button>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<scroll-buttons class="fixed right-5 top-2 hidden md:flex gap-1 z-50 ease-in-out opacity-0 -translate-y-10">
|
<scroll-buttons class="fixed right-5 top-2 hidden md:flex gap-1 z-50 ease-in-out opacity-0 -translate-y-10">
|
||||||
<% if @form_configs[:with_decline] %>
|
<% if @form_configs[:with_delegate] %>
|
||||||
<label id="decline_button" for="<%= decline_modal_checkbox_uuid %>" class="btn btn-sm px-0">
|
<label id="delegate_button" for="<%= delegate_modal_checkbox_uuid %>" class="btn btn-sm px-0">
|
||||||
<span class="min-[1366px]:inline hidden px-3">
|
<span class="min-[1366px]:inline hidden px-3">
|
||||||
<%= t(:decline) %>
|
<%= t(:delegate) %>
|
||||||
</span>
|
</span>
|
||||||
<span class="inline min-[1366px]:hidden px-2">
|
<span class="inline min-[1366px]:hidden px-2">
|
||||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
<%= svg_icon('user_share', class: 'w-5 h-5') %>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
<% if @form_configs[:with_decline] %>
|
||||||
|
<label id="decline_button" for="<%= decline_modal_checkbox_uuid %>" class="btn btn-sm px-2">
|
||||||
|
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||||
|
</label>
|
||||||
|
<% end %>
|
||||||
|
<download-button data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm px-2">
|
||||||
|
<span data-target="download-button.defaultButton">
|
||||||
|
<%= svg_icon('download', class: 'w-5 h-5') %>
|
||||||
|
</span>
|
||||||
|
<span class="hidden" data-target="download-button.loadingButton">
|
||||||
|
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin') %>
|
||||||
|
</span>
|
||||||
|
</download-button>
|
||||||
|
<% else %>
|
||||||
|
<% if @form_configs[:with_decline] %>
|
||||||
|
<label id="decline_button" for="<%= decline_modal_checkbox_uuid %>" class="btn btn-sm px-0">
|
||||||
|
<span class="min-[1366px]:inline hidden px-3">
|
||||||
|
<%= t(:decline) %>
|
||||||
|
</span>
|
||||||
|
<span class="inline min-[1366px]:hidden px-2">
|
||||||
|
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<% end %>
|
||||||
|
<download-button data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm px-2">
|
||||||
|
<span data-target="download-button.defaultButton">
|
||||||
|
<%= svg_icon('download', class: 'w-5 h-5') %>
|
||||||
|
</span>
|
||||||
|
<span class="hidden" data-target="download-button.loadingButton">
|
||||||
|
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin') %>
|
||||||
|
</span>
|
||||||
|
</download-button>
|
||||||
<% end %>
|
<% end %>
|
||||||
<download-button data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm px-2">
|
|
||||||
<span data-target="download-button.defaultButton">
|
|
||||||
<%= svg_icon('download', class: 'w-5 h-5') %>
|
|
||||||
</span>
|
|
||||||
<span class="hidden" data-target="download-button.loadingButton">
|
|
||||||
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin') %>
|
|
||||||
</span>
|
|
||||||
</download-button>
|
|
||||||
</scroll-buttons>
|
</scroll-buttons>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% schema.each do |item| %>
|
<% schema.each do |item| %>
|
||||||
@@ -65,7 +109,7 @@
|
|||||||
<% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %>
|
<% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %>
|
||||||
<% lazyload_metadata = document.preview_images.last&.metadata || Templates::ProcessDocument::US_LETTER_SIZE %>
|
<% lazyload_metadata = document.preview_images.last&.metadata || Templates::ProcessDocument::US_LETTER_SIZE %>
|
||||||
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
|
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
|
||||||
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_uuid, "#{index}.jpg")) %>
|
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_key, "#{index}.jpg")) %>
|
||||||
<page-container class="block relative my-4 shadow-md" style="container-type: size; aspect-ratio: <%= width = page.metadata['width'] %> / <%= height = page.metadata['height'] %>">
|
<page-container class="block relative my-4 shadow-md" style="container-type: size; aspect-ratio: <%= width = page.metadata['width'] %> / <%= height = page.metadata['height'] %>">
|
||||||
<img loading="lazy" src="<%= page.url %>" width="<%= width %>" height="<%= height %>">
|
<img loading="lazy" src="<%= page.url %>" width="<%= width %>" height="<%= height %>">
|
||||||
<div id="page-<%= [document.uuid, index].join('-') %>" class="top-0 bottom-0 left-0 right-0 absolute">
|
<div id="page-<%= [document.uuid, index].join('-') %>" class="top-0 bottom-0 left-0 right-0 absolute">
|
||||||
@@ -110,4 +154,9 @@
|
|||||||
<%= render 'submit_form/decline_form', submitter: @submitter %>
|
<%= render 'submit_form/decline_form', submitter: @submitter %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% if @form_configs[:with_delegate] %>
|
||||||
|
<%= render 'shared/html_modal', title: t(:delegate), uuid: delegate_modal_checkbox_uuid do %>
|
||||||
|
<%= render 'submit_form/delegate_form', submitter: @submitter %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
<%= render 'scripts/autosize_field' %>
|
<%= render 'scripts/autosize_field' %>
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<input id="file_dropzone_input" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="image/*, application/pdf, application/zip<%= ", #{Templates::CreateAttachments::DOCUMENT_EXTENSIONS.join(', ')}" if Docuseal.advanced_formats? %>" multiple>
|
<input id="file_dropzone_input" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="image/*, application/pdf, application/zip, application/json<%= ", #{Templates::CreateAttachments::DOCUMENT_EXTENSIONS.join(', ')}" if Docuseal.advanced_formats? %>" multiple>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</file-dropzone>
|
</file-dropzone>
|
||||||
|
|||||||
@@ -49,9 +49,9 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= link_to template_share_link_path(template), class: 'absolute md:relative bottom-0 right-0 btn btn-xs md:btn-sm whitespace-nowrap btn-neutral text-white mt-1 px-2', data: { turbo_frame: :modal } do %>
|
<%= link_to template_share_link_path(template), class: "absolute md:relative bottom-0 right-0 btn btn-xs md:btn-sm whitespace-nowrap btn-neutral text-white mt-1 px-2", data: { turbo_frame: :modal } do %>
|
||||||
<span class="flex items-center justify-center space-x-2">
|
<span class="flex items-center justify-center space-x-2">
|
||||||
<%= svg_icon('link', class: 'w-4 h-4 md:w-6 md:h-6 text-white') %>
|
<%= svg_icon('link', class: 'w-4 h-4 md:w-6 md:h-6') %>
|
||||||
<span><%= t('link') %></span>
|
<span><%= t('link') %></span>
|
||||||
</span>
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<input type="hidden" name="form_id" value="<%= form_id %>">
|
<input type="hidden" name="form_id" value="<%= form_id %>">
|
||||||
<submit-form data-on="change" data-disable="true">
|
<submit-form data-on="change" data-disable="true">
|
||||||
<input id="upload_template" name="files[]" class="hidden" type="file" accept="image/*, application/pdf, application/zip<%= ", #{Templates::CreateAttachments::DOCUMENT_EXTENSIONS.join(', ')}" if Docuseal.advanced_formats? %>" multiple>
|
<input id="upload_template" name="files[]" class="hidden" type="file" accept="image/*, application/pdf, application/zip, application/json<%= ", #{Templates::CreateAttachments::DOCUMENT_EXTENSIONS.join(', ')}" if Docuseal.advanced_formats? %>" multiple>
|
||||||
</submit-form>
|
</submit-form>
|
||||||
<input hidden name="folder_name" value="<%= local_assigns[:folder_name] %>">
|
<input hidden name="folder_name" value="<%= local_assigns[:folder_name] %>">
|
||||||
<% end %>
|
<% end %>
|
||||||
Loaded 100 of 160 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user