mirror of
https://github.com/docusealco/docuseal
synced 2026-11-05 14:42:33 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13a1835c00 | ||
|
|
c35407d7a9 | ||
|
|
e55ccc0b76 | ||
|
|
b8d00870b4 | ||
|
|
5413fbc248 | ||
|
|
2d2eb7ce4d | ||
|
|
ed2ddbf74e | ||
|
|
b99e5d1262 | ||
|
|
87f6878eda | ||
|
|
99b815eb4b | ||
|
|
e40a29b9ab | ||
|
|
151890bb72 | ||
|
|
78edd87cc1 | ||
|
|
88c99a2b35 | ||
|
|
282b8cfa73 | ||
|
|
e9888c7466 | ||
|
|
0647befb3a | ||
|
|
85aa3d0451 | ||
|
|
6d2b3bb654 | ||
|
|
b394854e9d | ||
|
|
2e586e930e | ||
|
|
8f8f69f0bc | ||
|
|
04663d55eb | ||
|
|
93d0b935e2 | ||
|
|
829b2b2855 | ||
|
|
cf6de444f4 | ||
|
|
126f5d58a2 | ||
|
|
09b93067f4 | ||
|
|
f14458f238 | ||
|
|
445139f05c | ||
|
|
e5e4965f05 | ||
|
|
1c8f7b0316 | ||
|
|
9fe3656652 | ||
|
|
17ae46bd61 | ||
|
|
a036483938 | ||
|
|
3c65b0e4ba | ||
|
|
ced5ab67ab | ||
|
|
28c4177128 | ||
|
|
5d86a08b5f | ||
|
|
a69fb81429 | ||
|
|
e6ac3380d4 | ||
|
|
3a535e7112 | ||
|
|
7464f9cfad | ||
|
|
58e24ef758 | ||
|
|
c1d809f989 | ||
|
|
80b24f0e2a | ||
|
|
2e253904a3 | ||
|
|
a7f381caf0 | ||
|
|
c6a7555f54 | ||
|
|
0772880bbc | ||
|
|
73339d8cd9 | ||
|
|
25504e8cb1 | ||
|
|
63a874528a | ||
|
|
c1c99621ff | ||
|
|
5c168f4a29 | ||
|
|
bef825c4e0 | ||
|
|
a990d22831 | ||
|
|
1687a372ae | ||
|
|
9b267d4a07 | ||
|
|
2189c2e9fe | ||
|
|
1e43fe8969 | ||
|
|
f77a716658 | ||
|
|
a70ac571e3 | ||
|
|
4f5e72f983 | ||
|
|
8f47ec6d4e | ||
|
|
9e1bbf2944 | ||
|
|
aa7d6e9aaa | ||
|
|
57c8be33ce | ||
|
|
ed7109bfc6 | ||
|
|
9e2590489b | ||
|
|
d793f00b37 | ||
|
|
5896c9a2aa | ||
|
|
78b0ce9828 | ||
|
|
e187644716 | ||
|
|
a847e7aa9c | ||
|
|
0905e7d215 | ||
|
|
91147d2f48 | ||
|
|
acce6195b0 | ||
|
|
ce9ab691ec | ||
|
|
4190544edc | ||
|
|
db8e5efbd5 | ||
|
|
a5c6f4e40c | ||
|
|
7537d05d1f | ||
|
|
44dcc7f312 | ||
|
|
218c22da21 | ||
|
|
a8433e9bd3 | ||
|
|
800101f747 | ||
|
|
f637ee907c | ||
|
|
6a9b123cdc | ||
|
|
0a2f8dfab3 | ||
|
|
3dc00ed7fc | ||
|
|
af160b3f3f | ||
|
|
1a9e4026d9 | ||
|
|
5ef32bcf9e | ||
|
|
2d6f4c521e | ||
|
|
439f95d87e | ||
|
|
ca7b9671f2 | ||
|
|
20ec614d3a | ||
|
|
887ebbbcf6 | ||
|
|
4225295295 | ||
|
|
89367bc905 | ||
|
|
4bacaa26e1 | ||
|
|
0f9fcf3fdc | ||
|
|
437f61dc26 | ||
|
|
e55542a000 | ||
|
|
e004ac2d40 | ||
|
|
92bfedd01d | ||
|
|
e7fd1e4006 | ||
|
|
56bba60d3f | ||
|
|
6b6a917f60 | ||
|
|
df7b3f9dfe | ||
|
|
1b21b0c65b | ||
|
|
258133d65c | ||
|
|
8f33c8d8b6 | ||
|
|
e11f4bddbe | ||
|
|
d071e8c429 | ||
|
|
79aa034183 | ||
|
|
f5dad673de | ||
|
|
7e8375aee8 | ||
|
|
1999611b26 | ||
|
|
188c3a8c84 | ||
|
|
d35538e2ba | ||
|
|
a86f896694 | ||
|
|
35eeeba0c3 | ||
|
|
e3e5c237d4 | ||
|
|
9e89cb92a9 | ||
|
|
827850646d | ||
|
|
eafae4cda2 | ||
|
|
e012b7f5ac | ||
|
|
da9e7c6366 | ||
|
|
20cc5d1e2e | ||
|
|
ae2cedbd70 | ||
|
|
1f46f74400 | ||
|
|
930445258d | ||
|
|
be533e492e | ||
|
|
66758d45bc | ||
|
|
6f31bd7af8 | ||
|
|
f4d2fa4c28 | ||
|
|
5d3369d556 | ||
|
|
5ea55c733f | ||
|
|
547bc9aa94 | ||
|
|
53ca45a67a | ||
|
|
ef28b80589 | ||
|
|
05fa75f9a2 | ||
|
|
87b57279a4 | ||
|
|
f09732df90 | ||
|
|
a588c2f4a4 | ||
|
|
960d36f628 | ||
|
|
456dbae1cd | ||
|
|
0fc1c0d1ed | ||
|
|
714324ea99 | ||
|
|
5a29ed22db | ||
|
|
061580594c | ||
|
|
8723ce7f7a | ||
|
|
522df7f476 | ||
|
|
026637c5ab | ||
|
|
fde993a579 | ||
|
|
a960ea268a | ||
|
|
d59776e8f5 | ||
|
|
27e5c2c81e | ||
|
|
0609a70544 | ||
|
|
3ed15e383b | ||
|
|
d29be7c2cf | ||
|
|
79f72c92b3 | ||
|
|
7e6331d1ad | ||
|
|
0986dfba73 | ||
|
|
7cf0b5aaf7 | ||
|
|
ae5f87f8fb | ||
|
|
8259ebdb6c | ||
|
|
a4ca0abba1 | ||
|
|
ca009252ac | ||
|
|
9678cd1286 | ||
|
|
8cb0a7a020 | ||
|
|
d3cc2e1ca8 | ||
|
|
08ac60c7e6 | ||
|
|
49afb2e87c | ||
|
|
be621339c3 | ||
|
|
5f5b49a1ba | ||
|
|
c0dd4b85ed | ||
|
|
95f7cd5220 | ||
|
|
f0753de99e | ||
|
|
38ecda82fa | ||
|
|
073859a4b7 | ||
|
|
44972fcc39 | ||
|
|
1232dfb248 | ||
|
|
5de334522c | ||
|
|
7a6b4acea6 | ||
|
|
198a599a2b | ||
|
|
d36f40cafd | ||
|
|
9abdb68bc5 | ||
|
|
359f695eb5 | ||
|
|
2b9e5a86b6 | ||
|
|
c07d76e2a4 | ||
|
|
f5b29642ee | ||
|
|
3f6cdcd15b | ||
|
|
bde8097288 | ||
|
|
617d75a4ed | ||
|
|
aa1874cdb4 | ||
|
|
22b2e7475a | ||
|
|
ad3367dae0 | ||
|
|
47c090e1f1 | ||
|
|
678997fb36 | ||
|
|
24f195c7e5 | ||
|
|
5741bb4db2 | ||
|
|
1d169668f0 | ||
|
|
ef3d5bec7e | ||
|
|
d578ca3cc9 | ||
|
|
5432978c0d | ||
|
|
de1af49348 | ||
|
|
2f0bcb4a80 | ||
|
|
11f27c8cf0 |
No files matched your search
@@ -1,6 +1,11 @@
|
|||||||
---
|
---
|
||||||
EnableDefaultLinters: true
|
EnableDefaultLinters: true
|
||||||
linters:
|
linters:
|
||||||
|
AllowedScriptType:
|
||||||
|
enabled: true
|
||||||
|
allowed_types:
|
||||||
|
- text/javascript
|
||||||
|
- application/json
|
||||||
ErbSafety:
|
ErbSafety:
|
||||||
enabled: false
|
enabled: false
|
||||||
HardCodedString:
|
HardCodedString:
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ jobs:
|
|||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 4.0.5
|
ruby-version: 4.0.9
|
||||||
- name: Cache gems
|
- name: Cache gems
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 4.0.5
|
ruby-version: 4.0.9
|
||||||
- name: Cache gems
|
- name: Cache gems
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 4.0.5
|
ruby-version: 4.0.9
|
||||||
- name: Cache gems
|
- name: Cache gems
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@@ -113,7 +113,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
container:
|
container:
|
||||||
image: ruby:4.0.5-alpine
|
image: ruby:4.0.9-alpine
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
@@ -152,11 +152,7 @@ jobs:
|
|||||||
bundle config path vendor/bundle
|
bundle config path vendor/bundle
|
||||||
bundle install --jobs 4 --retry 4
|
bundle install --jobs 4 --retry 4
|
||||||
yarn install
|
yarn install
|
||||||
wget -O pdfium-linux.zip "https://github.com/docusealco/pdfium-binaries/releases/download/20260813/pdfium-musl-$(uname -m).zip"
|
wget -O pdfium-linux.zip "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-musl-$(uname -m).zip"
|
||||||
case "$(uname -m)" in
|
|
||||||
x86_64) echo "c5c7dde243ecb66ab0819c8193515ef38ad53549fe260f3c2dfd93ea56eda2e7 pdfium-linux.zip" ;;
|
|
||||||
aarch64) echo "64c4483449b1b4dccc696ad0c5c96e0b7f74dcc57b4f23c676b7a70671b0bbb5 pdfium-linux.zip" ;;
|
|
||||||
esac | sha256sum -c -
|
|
||||||
unzip -q pdfium-linux.zip -d /tmp/pdfium-linux
|
unzip -q pdfium-linux.zip -d /tmp/pdfium-linux
|
||||||
cp /tmp/pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so
|
cp /tmp/pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so
|
||||||
rm -rf pdfium-linux.zip /tmp/pdfium-linux
|
rm -rf pdfium-linux.zip /tmp/pdfium-linux
|
||||||
|
|||||||
+11
-8
@@ -1,4 +1,4 @@
|
|||||||
FROM ruby:4.0.5-alpine AS download
|
FROM ruby:4.0.9-alpine AS download
|
||||||
|
|
||||||
WORKDIR /fonts
|
WORKDIR /fonts
|
||||||
|
|
||||||
@@ -9,15 +9,15 @@ RUN apk --no-cache add wget unzip && \
|
|||||||
wget https://raw.githubusercontent.com/impallari/DancingScript/master/OFL.txt && \
|
wget https://raw.githubusercontent.com/impallari/DancingScript/master/OFL.txt && \
|
||||||
wget https://raw.githubusercontent.com/notofonts/noto-fonts/refs/heads/main/LICENSE && \
|
wget https://raw.githubusercontent.com/notofonts/noto-fonts/refs/heads/main/LICENSE && \
|
||||||
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.zip "https://github.com/docusealco/pdfium-binaries/releases/download/20260813/pdfium-musl-$(uname -m).zip" && \
|
wget -O pdfium-linux.zip "https://github.com/docusealco/pdfium-binaries/releases/download/20260920/pdfium-musl-$(uname -m).zip" && \
|
||||||
case "$(uname -m)" in \
|
case "$(uname -m)" in \
|
||||||
x86_64) echo "c5c7dde243ecb66ab0819c8193515ef38ad53549fe260f3c2dfd93ea56eda2e7 pdfium-linux.zip" ;; \
|
x86_64) echo "bbca8a648dbd1ba81f9c6c223f21dc394cf1ad381c6c17d60c6dcc4a7d3a2ae0 pdfium-linux.zip" ;; \
|
||||||
aarch64) echo "64c4483449b1b4dccc696ad0c5c96e0b7f74dcc57b4f23c676b7a70671b0bbb5 pdfium-linux.zip" ;; \
|
aarch64) echo "73b72ed79b9bfdd494dee7e863482c24971a8d4b11f6cd1289aa170d31304a1f pdfium-linux.zip" ;; \
|
||||||
esac | sha256sum -c - && \
|
esac | sha256sum -c - && \
|
||||||
mkdir -p /pdfium-linux && \
|
mkdir -p /pdfium-linux && \
|
||||||
unzip -q pdfium-linux.zip -d /pdfium-linux
|
unzip -q pdfium-linux.zip -d /pdfium-linux
|
||||||
|
|
||||||
FROM ruby:4.0.5-alpine AS webpack
|
FROM ruby:4.0.9-alpine AS webpack
|
||||||
|
|
||||||
ENV RAILS_ENV=production
|
ENV RAILS_ENV=production
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
@@ -44,7 +44,7 @@ COPY ./app/views ./app/views
|
|||||||
|
|
||||||
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
|
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
|
||||||
|
|
||||||
FROM ruby:4.0.5-alpine AS app
|
FROM ruby:4.0.9-alpine AS app
|
||||||
|
|
||||||
ENV RAILS_ENV=production
|
ENV RAILS_ENV=production
|
||||||
ENV BUNDLE_WITHOUT="development:test"
|
ENV BUNDLE_WITHOUT="development:test"
|
||||||
@@ -52,7 +52,8 @@ ENV OPENSSL_CONF=/etc/openssl_legacy.cnf
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add --no-cache libpq vips redis onnxruntime leptonica && \
|
RUN apk upgrade --no-cache ca-certificates-bundle && \
|
||||||
|
apk add --no-cache libpq vips redis onnxruntime leptonica jemalloc font-liberation font-carlito && \
|
||||||
rm -f /usr/bin/onnx_test_runner /usr/bin/onnxruntime_test
|
rm -f /usr/bin/onnx_test_runner /usr/bin/onnxruntime_test
|
||||||
|
|
||||||
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
|
||||||
@@ -91,7 +92,7 @@ COPY --chown=docuseal:docuseal --from=download /model.onnx /app/tmp/model.onnx
|
|||||||
COPY --chown=docuseal:docuseal --from=webpack /app/public/packs ./public/packs
|
COPY --chown=docuseal:docuseal --from=webpack /app/public/packs ./public/packs
|
||||||
|
|
||||||
RUN mkdir -p /app/public/fonts && ln -s /fonts/DancingScript-Regular.otf /app/public/fonts/ && \
|
RUN mkdir -p /app/public/fonts && ln -s /fonts/DancingScript-Regular.otf /app/public/fonts/ && \
|
||||||
mkdir -p /usr/share/fonts/noto && ln -s /fonts/GoNotoKurrent-Regular.ttf /usr/share/fonts/noto/ && ln -s /fonts/GoNotoKurrent-Bold.ttf /usr/share/fonts/noto/ && fc-cache -f && \
|
mkdir -p /usr/share/fonts/noto/bold && ln -s /fonts/GoNotoKurrent-Regular.ttf /usr/share/fonts/noto/ && ln -s /fonts/GoNotoKurrent-Bold.ttf /usr/share/fonts/noto/bold/ && fc-cache -f && \
|
||||||
bundle exec bootsnap precompile -j 1 --gemfile app/ lib/ && \
|
bundle exec bootsnap precompile -j 1 --gemfile app/ lib/ && \
|
||||||
chown -R docuseal:docuseal /app/tmp/cache
|
chown -R docuseal:docuseal /app/tmp/cache
|
||||||
|
|
||||||
@@ -100,6 +101,8 @@ ENV HOME=/home/docuseal
|
|||||||
ENV WORKDIR=/data/docuseal
|
ENV WORKDIR=/data/docuseal
|
||||||
ENV VIPS_MAX_COORD=17000
|
ENV VIPS_MAX_COORD=17000
|
||||||
ENV VIPS_BLOCK_UNTRUSTED=1
|
ENV VIPS_BLOCK_UNTRUSTED=1
|
||||||
|
# ENV LD_PRELOAD=/usr/lib/libjemalloc.so.2
|
||||||
|
ENV MALLOC_CONF=dirty_decay_ms:1000,narenas:2,background_thread:true
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["/app/bin/bundle", "exec", "puma", "-C", "/app/config/puma.rb", "--dir", "/app"]
|
CMD ["/app/bin/bundle", "exec", "puma", "-C", "/app/config/puma.rb", "--dir", "/app"]
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
ruby '4.0.5'
|
ruby '4.0.9'
|
||||||
|
|
||||||
gem 'addressable'
|
gem 'addressable'
|
||||||
gem 'arabic-letter-connector', require: false
|
gem 'arabic-letter-connector', require: false
|
||||||
@@ -45,6 +45,7 @@ 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'
|
||||||
|
gem 'wordcraft', require: false, group: :ce
|
||||||
gem 'xlsxtream', require: false
|
gem 'xlsxtream', require: false
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
|
|||||||
+18
-7
@@ -497,7 +497,7 @@ GEM
|
|||||||
ruby-vips (2.3.0)
|
ruby-vips (2.3.0)
|
||||||
ffi (~> 1.12)
|
ffi (~> 1.12)
|
||||||
logger
|
logger
|
||||||
rubyzip (3.2.2)
|
rubyzip (3.7.0)
|
||||||
securerandom (0.4.1)
|
securerandom (0.4.1)
|
||||||
semantic_range (3.1.1)
|
semantic_range (3.1.1)
|
||||||
shakapacker (9.7.0)
|
shakapacker (9.7.0)
|
||||||
@@ -524,11 +524,11 @@ GEM
|
|||||||
simplecov-html (0.13.2)
|
simplecov-html (0.13.2)
|
||||||
simplecov_json_formatter (0.1.4)
|
simplecov_json_formatter (0.1.4)
|
||||||
smart_properties (1.17.0)
|
smart_properties (1.17.0)
|
||||||
sqlite3 (2.9.5-aarch64-linux-gnu)
|
sqlite3 (2.9.6-aarch64-linux-gnu)
|
||||||
sqlite3 (2.9.5-aarch64-linux-musl)
|
sqlite3 (2.9.6-aarch64-linux-musl)
|
||||||
sqlite3 (2.9.5-arm64-darwin)
|
sqlite3 (2.9.6-arm64-darwin)
|
||||||
sqlite3 (2.9.5-x86_64-linux-gnu)
|
sqlite3 (2.9.6-x86_64-linux-gnu)
|
||||||
sqlite3 (2.9.5-x86_64-linux-musl)
|
sqlite3 (2.9.6-x86_64-linux-musl)
|
||||||
strip_attributes (2.0.1)
|
strip_attributes (2.0.1)
|
||||||
activemodel (>= 3.0, < 9.0)
|
activemodel (>= 3.0, < 9.0)
|
||||||
strscan (3.1.8)
|
strscan (3.1.8)
|
||||||
@@ -572,6 +572,16 @@ GEM
|
|||||||
base64
|
base64
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
|
wordcraft (0.11.0-aarch64-linux-gnu)
|
||||||
|
ffi (>= 1.15)
|
||||||
|
wordcraft (0.11.0-aarch64-linux-musl)
|
||||||
|
ffi (>= 1.15)
|
||||||
|
wordcraft (0.11.0-arm64-darwin)
|
||||||
|
ffi (>= 1.15)
|
||||||
|
wordcraft (0.11.0-x86_64-linux-gnu)
|
||||||
|
ffi (>= 1.15)
|
||||||
|
wordcraft (0.11.0-x86_64-linux-musl)
|
||||||
|
ffi (>= 1.15)
|
||||||
xlsxtream (3.1.0)
|
xlsxtream (3.1.0)
|
||||||
zip_kit (>= 6.2, < 7)
|
zip_kit (>= 6.2, < 7)
|
||||||
xpath (3.2.0)
|
xpath (3.2.0)
|
||||||
@@ -650,10 +660,11 @@ DEPENDENCIES
|
|||||||
tzinfo-data
|
tzinfo-data
|
||||||
web-console
|
web-console
|
||||||
webmock
|
webmock
|
||||||
|
wordcraft
|
||||||
xlsxtream
|
xlsxtream
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
ruby 4.0.5
|
ruby 4.0.9
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
4.0.3
|
4.0.3
|
||||||
@@ -40,7 +40,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
- Files storage on disk or AWS S3, Google Storage, Azure Cloud
|
- Files storage on disk or AWS S3, Google Storage, Azure Cloud
|
||||||
- Automatic PDF eSignature
|
- Automatic PDF eSignature
|
||||||
- PDF signature verification
|
- PDF signature verification
|
||||||
- Users management
|
- User management
|
||||||
- Mobile-optimized
|
- Mobile-optimized
|
||||||
- 7 UI languages with signing available in 14 languages
|
- 7 UI languages with signing available in 14 languages
|
||||||
- API and Webhooks for integrations
|
- API and Webhooks for integrations
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ module Api
|
|||||||
|
|
||||||
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
|
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
|
||||||
|
|
||||||
if Submitters::DANGEROUS_EXTENSIONS.include?(blob.filename.extension.to_s.downcase)
|
if FilenameUtils.dangerous_extension(blob.filename)
|
||||||
Rollbar.error('Dangerous extension') if defined?(Rollbar)
|
Rollbar.error('Dangerous extension') if defined?(Rollbar)
|
||||||
|
|
||||||
return head :unprocessable_content
|
return head :unprocessable_content
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ module Api
|
|||||||
|
|
||||||
return head :not_found unless blob
|
return head :not_found unless blob
|
||||||
|
|
||||||
if Submitters::DANGEROUS_EXTENSIONS.include?(blob.filename.extension.to_s.downcase)
|
if FilenameUtils.dangerous_extension(blob.filename)
|
||||||
Rollbar.error('Dangerous extension') if defined?(Rollbar)
|
Rollbar.error('Dangerous extension') if defined?(Rollbar)
|
||||||
|
|
||||||
return head :unprocessable_content
|
return head :unprocessable_content
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Api
|
|
||||||
class AttachmentsController < ActionController::API
|
|
||||||
include ActionController::Cookies
|
|
||||||
include ActiveStorage::SetCurrent
|
|
||||||
|
|
||||||
COOKIE_STORE_LIMIT = 10
|
|
||||||
|
|
||||||
def create
|
|
||||||
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
|
||||||
|
|
||||||
unless can_upload?(@submitter)
|
|
||||||
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content
|
|
||||||
end
|
|
||||||
|
|
||||||
file = params[:file]
|
|
||||||
|
|
||||||
if params[:type].in?(%w[initials signature])
|
|
||||||
image = ImageUtils.load_vips(file.read, content_type: file.content_type)
|
|
||||||
|
|
||||||
if ImageUtils.blank?(image)
|
|
||||||
Rollbar.error("Empty signature: #{@submitter.id}") if defined?(Rollbar)
|
|
||||||
|
|
||||||
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_content
|
|
||||||
end
|
|
||||||
|
|
||||||
if ImageUtils.error?(image)
|
|
||||||
Rollbar.error("Error signature: #{@submitter.id}") if defined?(Rollbar)
|
|
||||||
|
|
||||||
return render json: { error: "#{params[:type]} error, try to sign on another device" },
|
|
||||||
status: :unprocessable_content
|
|
||||||
end
|
|
||||||
|
|
||||||
metadata = { analyzed: true, identified: true, width: image.width, height: image.height }
|
|
||||||
end
|
|
||||||
|
|
||||||
attachment = Submitters.create_attachment!(@submitter, file, metadata:)
|
|
||||||
|
|
||||||
if params[:remember_signature] == 'true' && @submitter.email.present?
|
|
||||||
cookies.encrypted[:signature_uuids] = build_new_cookie_signatures_json(@submitter, attachment)
|
|
||||||
end
|
|
||||||
|
|
||||||
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
|
|
||||||
rescue Submitters::MaliciousFileExtension => e
|
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
|
||||||
|
|
||||||
render json: { error: e.message }, status: :unprocessable_content
|
|
||||||
end
|
|
||||||
|
|
||||||
def can_upload?(submitter)
|
|
||||||
!submitter.declined_at? &&
|
|
||||||
!submitter.completed_at? &&
|
|
||||||
!submitter.submission.archived_at? &&
|
|
||||||
!submitter.submission.expired? &&
|
|
||||||
!submitter.submission.template&.archived_at?
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_new_cookie_signatures_json(submitter, attachment)
|
|
||||||
values =
|
|
||||||
begin
|
|
||||||
JSON.parse(cookies.encrypted[:signature_uuids].presence || '{}')
|
|
||||||
rescue JSON::ParserError
|
|
||||||
{}
|
|
||||||
end
|
|
||||||
|
|
||||||
values[submitter.email] = attachment.uuid
|
|
||||||
|
|
||||||
values = values.to_a.last(COOKIE_STORE_LIMIT).to_h if values.size > COOKIE_STORE_LIMIT
|
|
||||||
|
|
||||||
values.to_json
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -98,7 +98,7 @@ module Api
|
|||||||
|
|
||||||
render json: build_create_json(submissions)
|
render json: build_create_json(submissions)
|
||||||
rescue Submitters::NormalizeValues::BaseError, Submissions::CreateFromSubmitters::BaseError,
|
rescue Submitters::NormalizeValues::BaseError, Submissions::CreateFromSubmitters::BaseError,
|
||||||
DownloadUtils::UnableToDownload => e
|
Submitters::SubmitValues::ValidationError, DownloadUtils::UnableToDownload => e
|
||||||
Rollbar.warning(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
render json: { error: e.message }, status: :unprocessable_content
|
render json: { error: e.message }, status: :unprocessable_content
|
||||||
@@ -173,7 +173,6 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def filter_submissions(submissions, params)
|
def filter_submissions(submissions, params)
|
||||||
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
|
|
||||||
submissions = submissions.where(slug: params[:slug]) if params[:slug].present?
|
submissions = submissions.where(slug: params[:slug]) if params[:slug].present?
|
||||||
|
|
||||||
if params[:template_folder].present?
|
if params[:template_folder].present?
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Api
|
|
||||||
class SubmitterEmailClicksController < ApiBaseController
|
|
||||||
skip_before_action :authenticate_user!
|
|
||||||
skip_authorization_check
|
|
||||||
|
|
||||||
def create
|
|
||||||
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
|
||||||
|
|
||||||
if params[:t] == SubmissionEvents.build_tracking_param(@submitter, 'click_email')
|
|
||||||
SubmissionEvents.create_with_tracking_data(@submitter, 'click_email', request)
|
|
||||||
end
|
|
||||||
|
|
||||||
render json: {}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Api
|
module Api
|
||||||
class SubmitterFormViewsController < ApiBaseController
|
class SubmitterFormViewsController < ActionController::API
|
||||||
skip_before_action :authenticate_user!
|
|
||||||
skip_authorization_check
|
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,8 @@ module Api
|
|||||||
|
|
||||||
render json: Submitters::SerializeForApi.call(@submitter, with_template: false, with_urls: true,
|
render json: Submitters::SerializeForApi.call(@submitter, with_template: false, with_urls: true,
|
||||||
with_events: false, params:)
|
with_events: false, params:)
|
||||||
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
|
rescue Submitters::NormalizeValues::BaseError, Submitters::SubmitValues::ValidationError,
|
||||||
|
DownloadUtils::UnableToDownload => e
|
||||||
Rollbar.warning(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
render json: { error: e.message }, status: :unprocessable_content
|
render json: { error: e.message }, status: :unprocessable_content
|
||||||
@@ -178,10 +179,7 @@ module Api
|
|||||||
submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present?
|
submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present?
|
||||||
submitters = submitters.where(slug: params[:slug]) if params[:slug].present?
|
submitters = submitters.where(slug: params[:slug]) if params[:slug].present?
|
||||||
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
|
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
|
||||||
|
submitters = submitters.where(template_id: params[:template_id]) if params[:template_id].present?
|
||||||
if params[:template_id].present?
|
|
||||||
submitters = submitters.joins(:submission).where(submissions: { template_id: params[:template_id] })
|
|
||||||
end
|
|
||||||
|
|
||||||
maybe_filter_by_completed_at(submitters, params)
|
maybe_filter_by_completed_at(submitters, params)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -13,13 +13,17 @@ class ApplicationController < ActionController::Base
|
|||||||
before_action :maybe_redirect_to_setup, unless: :signed_in?
|
before_action :maybe_redirect_to_setup, unless: :signed_in?
|
||||||
before_action :authenticate_user!, unless: :devise_controller?
|
before_action :authenticate_user!, unless: :devise_controller?
|
||||||
|
|
||||||
before_action :set_csp, if: -> { request.get? && !request.headers['HTTP_X_TURBO'] }
|
before_action :set_csp, if: lambda {
|
||||||
|
request.get? && !request.headers['HTTP_X_TURBO_REQUEST_ID'] && !turbo_infinite_scroll?
|
||||||
|
}
|
||||||
|
|
||||||
helper_method :button_title,
|
helper_method :button_title,
|
||||||
:current_account,
|
:current_account,
|
||||||
:true_ability,
|
:true_ability,
|
||||||
:form_link_host,
|
:form_link_host,
|
||||||
:svg_icon
|
:svg_icon,
|
||||||
|
:turbo_native_ios?,
|
||||||
|
:turbo_native_android?
|
||||||
|
|
||||||
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
|
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
|
||||||
|
|
||||||
@@ -64,6 +68,28 @@ class ApplicationController < ActionController::Base
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def turbo_infinite_scroll?
|
||||||
|
request.headers['HTTP_X_TURBO_INFINITE_SCROLL'].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def turbo_native_app?
|
||||||
|
return @turbo_native_app if defined?(@turbo_native_app)
|
||||||
|
|
||||||
|
@turbo_native_app = request.user_agent.to_s.include?('Hotwire Native')
|
||||||
|
end
|
||||||
|
|
||||||
|
def turbo_native_ios?
|
||||||
|
return @turbo_native_ios if defined?(@turbo_native_ios)
|
||||||
|
|
||||||
|
@turbo_native_ios = request.user_agent.to_s.include?('Hotwire Native iOS')
|
||||||
|
end
|
||||||
|
|
||||||
|
def turbo_native_android?
|
||||||
|
return @turbo_native_android if defined?(@turbo_native_android)
|
||||||
|
|
||||||
|
@turbo_native_android = request.user_agent.to_s.include?('Hotwire Native Android')
|
||||||
|
end
|
||||||
|
|
||||||
def with_locale(&)
|
def with_locale(&)
|
||||||
return yield unless current_account
|
return yield unless current_account
|
||||||
|
|
||||||
@@ -92,7 +118,12 @@ class ApplicationController < ActionController::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def sign_in_for_demo
|
def sign_in_for_demo
|
||||||
sign_in(User.active.order('random()').take) unless signed_in?
|
return if signed_in?
|
||||||
|
|
||||||
|
account = Account.create!(name: "Demo #{SecureRandom.hex(3)}")
|
||||||
|
|
||||||
|
sign_in(account.users.create!(first_name: 'Demo', last_name: 'User',
|
||||||
|
email: "#{SecureRandom.hex}@docuseal.com", password: SecureRandom.hex))
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_account
|
def current_account
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class EnquiriesController < ApplicationController
|
|
||||||
skip_before_action :authenticate_user!
|
|
||||||
skip_authorization_check
|
|
||||||
|
|
||||||
def create
|
|
||||||
if params[:talk_to_sales] == 'on'
|
|
||||||
Faraday.post(Docuseal::ENQUIRIES_URL,
|
|
||||||
enquiry_params.merge(type: :talk_to_sales).to_json,
|
|
||||||
'Content-Type' => 'application/json')
|
|
||||||
end
|
|
||||||
|
|
||||||
head :ok
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def enquiry_params
|
|
||||||
params.require(:user).permit(:email)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -46,6 +46,8 @@ module Mcp
|
|||||||
|
|
||||||
authorize!(:create, @template)
|
authorize!(:create, @template)
|
||||||
|
|
||||||
|
Templates.maybe_assign_access(@template)
|
||||||
|
|
||||||
if mcp_params['url'].present?
|
if mcp_params['url'].present?
|
||||||
tempfile = Tempfile.new
|
tempfile = Tempfile.new
|
||||||
tempfile.binmode
|
tempfile.binmode
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class MfaSetupController < ApplicationController
|
|||||||
def destroy
|
def destroy
|
||||||
RateLimit.call("mfa-setup-otp-#{current_user.id}", limit: 5, ttl: 5.minutes, enabled: true)
|
RateLimit.call("mfa-setup-otp-#{current_user.id}", limit: 5, ttl: 5.minutes, enabled: true)
|
||||||
|
|
||||||
if current_user.validate_and_consume_otp!(params[:otp_attempt])
|
if current_user.valid_for_authentication? { current_user.validate_and_consume_otp!(params[:otp_attempt]) }
|
||||||
current_user.update!(otp_required_for_login: false, otp_secret: nil)
|
current_user.update!(otp_required_for_login: false, otp_secret: nil)
|
||||||
|
|
||||||
redirect_to settings_profile_index_path, notice: I18n.t('2fa_has_been_removed')
|
redirect_to settings_profile_index_path, notice: I18n.t('2fa_has_been_removed')
|
||||||
|
|||||||
@@ -26,8 +26,9 @@ class PreviewDocumentPageController < ActionController::API
|
|||||||
|
|
||||||
@template = attachment.record
|
@template = attachment.record
|
||||||
|
|
||||||
preview_image = attachment.preview_images.joins(:blob)
|
preview_image =
|
||||||
.find_by(blob: { filename: ["#{params[:id]}.png", "#{params[:id]}.jpg"] })
|
attachment.preview_images.joins(:blob)
|
||||||
|
.find_by(blob: { filename: ["#{params[:id].to_i}.png", "#{params[:id].to_i}.jpg"] })
|
||||||
|
|
||||||
if preview_image
|
if preview_image
|
||||||
return redirect_to preview_image.url(time: ActiveStorage::Attachment.service_url_time),
|
return redirect_to preview_image.url(time: ActiveStorage::Attachment.service_url_time),
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SearchController < ApplicationController
|
||||||
|
load_and_authorize_resource :template_folder, parent: false
|
||||||
|
load_and_authorize_resource :template, parent: false
|
||||||
|
load_and_authorize_resource :submission, parent: false
|
||||||
|
load_and_authorize_resource :template, instance_name: :filter_template, id_param: :template_id, parent: true
|
||||||
|
|
||||||
|
FOLDERS_LIMIT = 4
|
||||||
|
LIMIT = 12
|
||||||
|
|
||||||
|
def index
|
||||||
|
@query = params[:q].to_s.squish
|
||||||
|
|
||||||
|
return render_infinite_scroll(@template_folders, @templates, @submissions, @query) if turbo_infinite_scroll?
|
||||||
|
|
||||||
|
if @query.blank?
|
||||||
|
@template_folders = @templates = @submissions = []
|
||||||
|
elsif params[:scope] == 'folders'
|
||||||
|
@folders_pagy, @template_folders =
|
||||||
|
pagy(:countless, filter_folders(@template_folders, @templates, @query), limit: LIMIT)
|
||||||
|
|
||||||
|
@templates = @submissions = []
|
||||||
|
else
|
||||||
|
folders = filter_folders(@template_folders, @templates, @query)
|
||||||
|
|
||||||
|
@pagy, @template_folders, @templates, @submissions =
|
||||||
|
if params[:scope] == 'submissions'
|
||||||
|
load_submissions_page(folders, @templates, @submissions, @query)
|
||||||
|
else
|
||||||
|
load_templates_page(folders, @templates, @submissions, @query)
|
||||||
|
end
|
||||||
|
|
||||||
|
if @templates.blank? && @submissions.blank? && params[:template_id].blank?
|
||||||
|
@folders_pagy, @template_folders = pagy(:countless, folders, limit: LIMIT)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_submissions_page(folders, templates, submissions, query)
|
||||||
|
pagy, submissions = pagy(:countless, filter_submissions(submissions, query), limit: LIMIT)
|
||||||
|
|
||||||
|
return [pagy, [], [], submissions] if pagy.next || params[:template_id].present?
|
||||||
|
|
||||||
|
[pagy, folders.limit(FOLDERS_LIMIT).to_a, filter_templates(templates, query).limit(LIMIT).to_a, submissions]
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_templates_page(folders, templates, submissions, query)
|
||||||
|
pagy, templates = pagy(:countless, filter_templates(templates, query), limit: LIMIT)
|
||||||
|
|
||||||
|
submissions = pagy.next ? [] : filter_submissions(submissions, query).limit(LIMIT).to_a
|
||||||
|
|
||||||
|
[pagy, folders.limit(FOLDERS_LIMIT).to_a, templates, submissions]
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_infinite_scroll(folders, templates, submissions, query)
|
||||||
|
target, partial, collection =
|
||||||
|
case params[:scope]
|
||||||
|
when 'folders'
|
||||||
|
['search_folders', 'search/folder', filter_folders(folders, templates, query)]
|
||||||
|
when 'submissions'
|
||||||
|
['search_submissions', 'search/submission', filter_submissions(submissions, query)]
|
||||||
|
else
|
||||||
|
['search_templates', 'search/template', filter_templates(templates, query)]
|
||||||
|
end
|
||||||
|
|
||||||
|
pagy, records = pagy(:countless, collection, limit: LIMIT)
|
||||||
|
|
||||||
|
render turbo_stream: [
|
||||||
|
turbo_stream.append(target, partial:, collection: records),
|
||||||
|
turbo_stream.replace('infinite_scroll', partial: 'shared/infinite_scroll', locals: { pagy: })
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_folders(folders, templates, query)
|
||||||
|
return folders.none if params[:archived] == 'true'
|
||||||
|
|
||||||
|
folders = folders.where.not(id: current_account.default_template_folder.id)
|
||||||
|
|
||||||
|
folders = folders.where(parent_folder_id: filter_folder_ids(params[:folder])) if params[:folder].present?
|
||||||
|
folders = folders.where(author_id: filter_author_id(params[:author])) if params[:author].present?
|
||||||
|
folders = TemplateFolders.filter_active_folders(folders, templates)
|
||||||
|
folders = TemplateFolders.search(folders, query)
|
||||||
|
|
||||||
|
folders.preload(:parent_folder).order(:name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_templates(templates, query)
|
||||||
|
templates = params[:archived] == 'true' ? templates.where.not(archived_at: nil) : templates.active
|
||||||
|
templates = templates.select_for_list.preload(:author, :template_accesses, folder: :parent_folder)
|
||||||
|
|
||||||
|
templates = templates.where(folder_id: filter_folder_ids(params[:folder])) if params[:folder].present?
|
||||||
|
templates = templates.where(author_id: filter_author_id(params[:author])) if params[:author].present?
|
||||||
|
|
||||||
|
Templates.search(current_user, templates, query).order(id: :desc)
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_folder_ids(folder_name)
|
||||||
|
TemplateFolders.filter_by_full_name(current_account.template_folders, folder_name)
|
||||||
|
.preload(:subfolders)
|
||||||
|
.flat_map { |folder| [folder.id, *folder.subfolders.map(&:id)] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_author_id(email)
|
||||||
|
current_account.users.find_by(email:)&.id || -1
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_submissions(submissions, query)
|
||||||
|
submissions = submissions.left_joins(:template)
|
||||||
|
|
||||||
|
submissions =
|
||||||
|
if params[:archived] == 'true'
|
||||||
|
submissions.where.not(archived_at: nil).or(submissions.where.not(templates: { archived_at: nil }))
|
||||||
|
else
|
||||||
|
submissions.where(archived_at: nil).where(templates: { archived_at: nil })
|
||||||
|
end
|
||||||
|
|
||||||
|
submissions = Submissions.search(current_user, submissions, query, search_template: true)
|
||||||
|
submissions = Submissions::Filter.call(submissions, current_user, params)
|
||||||
|
|
||||||
|
submissions =
|
||||||
|
if params[:status] == 'completed' || params[:completed_at_from].present? || params[:completed_at_to].present?
|
||||||
|
submissions.order(completed_at: :desc)
|
||||||
|
else
|
||||||
|
submissions.order(id: :desc)
|
||||||
|
end
|
||||||
|
|
||||||
|
submissions.select_for_list.preload(:submitters, :template)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -3,6 +3,10 @@
|
|||||||
class SessionsController < Devise::SessionsController
|
class SessionsController < Devise::SessionsController
|
||||||
before_action :configure_permitted_parameters
|
before_action :configure_permitted_parameters
|
||||||
|
|
||||||
|
skip_before_action :verify_authenticity_token, if: -> { request.xhr? && request.format.json? }
|
||||||
|
|
||||||
|
respond_to :json, only: :create
|
||||||
|
|
||||||
around_action :with_browser_locale
|
around_action :with_browser_locale
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@@ -11,12 +15,26 @@ class SessionsController < Devise::SessionsController
|
|||||||
if Docuseal.multitenant? && !User.exists?(email:)
|
if Docuseal.multitenant? && !User.exists?(email:)
|
||||||
Rollbar.warning('Sign in new user') if defined?(Rollbar)
|
Rollbar.warning('Sign in new user') if defined?(Rollbar)
|
||||||
|
|
||||||
return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)),
|
respond_to do |format|
|
||||||
notice: I18n.t('create_a_new_account')
|
format.html do
|
||||||
|
redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)),
|
||||||
|
notice: t('create_a_new_account')
|
||||||
|
end
|
||||||
|
format.json do
|
||||||
|
render json: { sign_up: true, notice: t('create_a_new_account') }, status: :unprocessable_content
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if User.exists?(email:, otp_required_for_login: true) && sign_in_params[:otp_attempt].blank?
|
if User.exists?(email:, otp_required_for_login: true) && sign_in_params[:otp_attempt].blank?
|
||||||
return render :otp, locals: { resource: User.new(sign_in_params) }, status: :unprocessable_content
|
respond_to do |format|
|
||||||
|
format.html { render :otp, locals: { resource: User.new(sign_in_params) }, status: :unprocessable_content }
|
||||||
|
format.json { render json: { otp_required: true, email_otp: false }, status: :unprocessable_content }
|
||||||
|
end
|
||||||
|
|
||||||
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
super
|
super
|
||||||
|
|||||||
@@ -29,5 +29,17 @@ class SubmissionsArchivedController < ApplicationController
|
|||||||
|
|
||||||
ActiveRecord::Associations::Preloader.new(records: @submissions.filter_map(&:template),
|
ActiveRecord::Associations::Preloader.new(records: @submissions.filter_map(&:template),
|
||||||
associations: :author).call
|
associations: :author).call
|
||||||
|
|
||||||
|
render_infinite_scroll(@pagy, @submissions) if turbo_infinite_scroll?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def render_infinite_scroll(pagy, submissions)
|
||||||
|
render turbo_stream: [
|
||||||
|
turbo_stream.before('infinite_scroll', partial: 'templates/submission', collection: submissions,
|
||||||
|
locals: { with_template: true, archived: true }),
|
||||||
|
turbo_stream.replace('infinite_scroll', partial: 'shared/infinite_scroll', locals: { pagy: })
|
||||||
|
]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -18,6 +18,16 @@ class SubmissionsController < ApplicationController
|
|||||||
'payment' => 'credit_card', 'verification' => 'id', 'kba' => 'user_scan'
|
'payment' => 'credit_card', 'verification' => 'id', 'kba' => 'user_scan'
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
|
STATUS_BADGES = {
|
||||||
|
'awaiting' => 'badge-info', 'sent' => 'badge-info', 'completed' => 'badge-success',
|
||||||
|
'opened' => 'badge-warning', 'declined' => 'badge-error'
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
STATUS_ICONS = {
|
||||||
|
'awaiting' => 'clock', 'sent' => 'send', 'completed' => 'circle_check',
|
||||||
|
'opened' => 'mail_opened', 'declined' => 'x_circle'
|
||||||
|
}.freeze
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@submission = Submissions.preload_with_pages(@submission)
|
@submission = Submissions.preload_with_pages(@submission)
|
||||||
|
|
||||||
@@ -56,6 +66,8 @@ class SubmissionsController < ApplicationController
|
|||||||
create_submissions(@template, submissions_params, params)
|
create_submissions(@template, submissions_params, params)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
raise Submissions::CreateFromSubmitters::BaseError, 'Invalid email address' if submissions.blank?
|
||||||
|
|
||||||
WebhookUrls.enqueue_events(submissions, 'submission.created')
|
WebhookUrls.enqueue_events(submissions, 'submission.created')
|
||||||
|
|
||||||
Submissions.send_signature_requests(submissions)
|
Submissions.send_signature_requests(submissions)
|
||||||
@@ -83,7 +95,14 @@ class SubmissionsController < ApplicationController
|
|||||||
I18n.t('submission_has_been_archived')
|
I18n.t('submission_has_been_archived')
|
||||||
end
|
end
|
||||||
|
|
||||||
redirect_back(fallback_location: @submission.template_id ? template_path(@submission.template) : root_path, notice:)
|
if turbo_native_app? && request.format.turbo_stream?
|
||||||
|
flash.now[:notice] = notice
|
||||||
|
|
||||||
|
render turbo_stream: [turbo_stream.remove(@submission), turbo_stream.append_all('body', partial: 'shared/flash')]
|
||||||
|
else
|
||||||
|
redirect_back(fallback_location: @submission.template_id ? template_path(@submission.template) : root_path,
|
||||||
|
notice:)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -30,5 +30,17 @@ class SubmissionsDashboardController < ApplicationController
|
|||||||
|
|
||||||
ActiveRecord::Associations::Preloader.new(records: @submissions.filter_map(&:template),
|
ActiveRecord::Associations::Preloader.new(records: @submissions.filter_map(&:template),
|
||||||
associations: :author).call
|
associations: :author).call
|
||||||
|
|
||||||
|
render_infinite_scroll(@pagy, @submissions) if turbo_infinite_scroll?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def render_infinite_scroll(pagy, submissions)
|
||||||
|
render turbo_stream: [
|
||||||
|
turbo_stream.before('infinite_scroll', partial: 'templates/submission', collection: submissions,
|
||||||
|
locals: { with_template: true }),
|
||||||
|
turbo_stream.replace('infinite_scroll', partial: 'shared/infinite_scroll', locals: { pagy: })
|
||||||
|
]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmitFormClickEmailController < ActionController::API
|
||||||
|
def create
|
||||||
|
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||||
|
|
||||||
|
if params[:t].present? && params[:t] == SubmissionEvents.build_tracking_param(@submitter, 'click_email')
|
||||||
|
SubmissionEvents.create_with_tracking_data(@submitter, 'click_email', request)
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: {}
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -29,7 +29,9 @@ class SubmitFormCompletedDownloadController < ApplicationController
|
|||||||
Submissions::EnsureResultGenerated.call(last_submitter)
|
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||||
|
|
||||||
if !signature_valid && !current_user_submitter?(last_submitter)
|
if !signature_valid && !current_user_submitter?(last_submitter)
|
||||||
return head :not_found unless Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
unless Submitters::AuthorizedForForm.call(@submitter, current_user, request, with_order: true)
|
||||||
|
return head :not_found
|
||||||
|
end
|
||||||
|
|
||||||
if last_submitter.completed_at < TTL.ago
|
if last_submitter.completed_at < TTL.ago
|
||||||
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
||||||
|
|||||||
@@ -31,24 +31,22 @@ class SubmitFormController < ApplicationController
|
|||||||
|
|
||||||
Submissions.preload_with_pages(submission)
|
Submissions.preload_with_pages(submission)
|
||||||
|
|
||||||
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
|
|
||||||
|
|
||||||
@attachments_index = build_attachments_index(submission)
|
@attachments_index = build_attachments_index(submission)
|
||||||
|
|
||||||
|
if current_user && current_user.email == @submitter.email
|
||||||
|
@current_user_data = current_user.as_json(only: %i[first_name last_name], methods: %i[full_name])
|
||||||
|
|
||||||
|
initials = UserConfigs.load_initials(current_user)
|
||||||
|
|
||||||
|
@prefill_initials = Submitters.build_prefill_attachment_data(@submitter, initials) if initials
|
||||||
|
end
|
||||||
|
|
||||||
return unless @form_configs[:prefill_signature]
|
return unless @form_configs[:prefill_signature]
|
||||||
|
|
||||||
if (user_signature = UserConfigs.load_signature(current_user))
|
signature = UserConfigs.load_signature(current_user) ||
|
||||||
@signature_attachment = ActiveStorage::Attachment.find_or_create_by!(
|
Submitters::FindRememberedSignature.call(@submitter, params, cookies)
|
||||||
blob_id: user_signature.blob_id,
|
|
||||||
name: 'attachments',
|
|
||||||
record: @submitter
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
@signature_attachment ||=
|
@prefill_signature = Submitters.build_prefill_attachment_data(@submitter, signature) if signature
|
||||||
Submitters::MaybeAssignDefaultBrowserSignature.call(@submitter, params, cookies, @attachments_index.values)
|
|
||||||
|
|
||||||
@attachments_index[@signature_attachment.uuid] = @signature_attachment if @signature_attachment
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ class SubmitFormDelegateController < ApplicationController
|
|||||||
@submitter.viewer? ||
|
@submitter.viewer? ||
|
||||||
!Submitters::AuthorizedForForm.call(@submitter,
|
!Submitters::AuthorizedForForm.call(@submitter,
|
||||||
current_user,
|
current_user,
|
||||||
request)
|
request,
|
||||||
|
with_order: true)
|
||||||
|
|
||||||
@submitter.account.account_configs.find_by!(key: AccountConfig::ALLOW_TO_DELEGATE_KEY, value: true)
|
@submitter.account.account_configs.find_by!(key: AccountConfig::ALLOW_TO_DELEGATE_KEY, value: true)
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ class SubmitFormDownloadController < ApplicationController
|
|||||||
AccountConfig.exists?(account_id: @submitter.account_id,
|
AccountConfig.exists?(account_id: @submitter.account_id,
|
||||||
key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
|
key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
|
||||||
value: false) ||
|
value: false) ||
|
||||||
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
!Submitters::AuthorizedForForm.call(@submitter, current_user, request,
|
||||||
|
with_order: true)
|
||||||
|
|
||||||
last_completed_submitter = @submitter.submission.submitters
|
last_completed_submitter = @submitter.submission.submitters
|
||||||
.where.not(id: @submitter.id)
|
.where.not(id: @submitter.id)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class SubmitFormDrawSignatureController < ApplicationController
|
|||||||
return redirect_to submit_form_path(@submitter.slug) if @submitter.viewer?
|
return redirect_to submit_form_path(@submitter.slug) if @submitter.viewer?
|
||||||
|
|
||||||
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at? ||
|
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at? ||
|
||||||
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
!Submitters::AuthorizedForForm.call(@submitter, current_user, request, with_order: true)
|
||||||
return redirect_to submit_form_path(@submitter.slug)
|
return redirect_to submit_form_path(@submitter.slug)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ class SubmitFormEmail2fasController < ApplicationController
|
|||||||
def create
|
def create
|
||||||
RateLimit.call("verify-2fa-code-#{@submitter.id}", limit: 2, ttl: 45.seconds, enabled: true)
|
RateLimit.call("verify-2fa-code-#{@submitter.id}", limit: 2, ttl: 45.seconds, enabled: true)
|
||||||
|
|
||||||
value = [@submitter.email.downcase.strip, @submitter.slug].join(':')
|
value = [@submitter.email.downcase.squish, @submitter.slug].join(':')
|
||||||
|
|
||||||
if EmailVerificationCodes.verify(params[:one_time_code].to_s.gsub(/\D/, ''), value)
|
if OtpChallenges.verify(params[:one_time_code].to_s.gsub(/\D/, ''), value, purpose: 'form_email_2fa')
|
||||||
SubmissionEvents.create_with_tracking_data(@submitter, 'email_verified', request, { email: @submitter.email })
|
SubmissionEvents.create_with_tracking_data(@submitter, 'email_verified', request, { email: @submitter.email })
|
||||||
|
|
||||||
cookies.encrypted[:email_2fa_slug] =
|
cookies.encrypted[:email_2fa_slug] =
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ class SubmitFormMetadataController < ApplicationController
|
|||||||
submission.schema_documents.find { |a| a.uuid == item['attachment_uuid'] }
|
submission.schema_documents.find { |a| a.uuid == item['attachment_uuid'] }
|
||||||
end
|
end
|
||||||
|
|
||||||
ActiveRecord::Associations::Preloader.new(records: documents, associations: %i[blob record]).call
|
ActiveRecord::Associations::Preloader.new(records: documents, associations: :blob).call
|
||||||
|
|
||||||
text_runs = documents.to_h do |document|
|
text_runs = documents.to_h do |document|
|
||||||
[
|
[
|
||||||
document.uuid,
|
document.uuid,
|
||||||
DocumentMetadatas.find_or_create_for_document(document, account_id: document.record.account_id).text_runs
|
DocumentMetadatas.find_or_create_for_document(document, account_id: submission.account_id).text_runs
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -39,6 +39,6 @@ class SubmitFormMetadataController < ApplicationController
|
|||||||
!submitter.submission.expired? &&
|
!submitter.submission.expired? &&
|
||||||
!submitter.submission.template&.archived_at? &&
|
!submitter.submission.template&.archived_at? &&
|
||||||
!submitter.account.archived_at? &&
|
!submitter.account.archived_at? &&
|
||||||
Submitters::AuthorizedForForm.call(submitter, current_user, request)
|
Submitters::AuthorizedForForm.call(submitter, current_user, request, with_order: true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmitFormPrefillAttachmentsController < ActionController::API
|
||||||
|
include ActiveStorage::SetCurrent
|
||||||
|
|
||||||
|
def create
|
||||||
|
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||||
|
|
||||||
|
unless can_upload?(@submitter)
|
||||||
|
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content
|
||||||
|
end
|
||||||
|
|
||||||
|
uuid, slug = ApplicationRecord.signed_id_verifier.verify(params[:prefill_token], purpose: :prefill_attachment)
|
||||||
|
|
||||||
|
raise ActiveRecord::RecordNotFound if slug != @submitter.slug
|
||||||
|
|
||||||
|
source_attachment = ActiveStorage::Attachment.find_by!(uuid:)
|
||||||
|
|
||||||
|
attachment = @submitter.attachments_attachments.find_or_create_by!(blob_id: source_attachment.blob_id)
|
||||||
|
|
||||||
|
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def can_upload?(submitter)
|
||||||
|
!submitter.declined_at? &&
|
||||||
|
!submitter.completed_at? &&
|
||||||
|
!submitter.submission.archived_at? &&
|
||||||
|
!submitter.submission.expired? &&
|
||||||
|
!submitter.submission.template&.archived_at?
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmitFormUploadController < ActionController::API
|
||||||
|
include ActionController::Cookies
|
||||||
|
include ActiveStorage::SetCurrent
|
||||||
|
|
||||||
|
before_action :load_submitter
|
||||||
|
before_action :authorize_upload!
|
||||||
|
|
||||||
|
COOKIE_STORE_LIMIT = 10
|
||||||
|
|
||||||
|
def create
|
||||||
|
file = params[:file]
|
||||||
|
|
||||||
|
if params[:type].in?(%w[initials signature])
|
||||||
|
image = ImageUtils.load_vips(file.read, content_type: file.content_type)
|
||||||
|
|
||||||
|
if ImageUtils.blank?(image)
|
||||||
|
Rollbar.error("Empty signature: #{@submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_content
|
||||||
|
end
|
||||||
|
|
||||||
|
if ImageUtils.error?(image)
|
||||||
|
Rollbar.error("Error signature: #{@submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return render json: { error: "#{params[:type]} error, try to sign on another device" },
|
||||||
|
status: :unprocessable_content
|
||||||
|
end
|
||||||
|
|
||||||
|
metadata = { analyzed: true, identified: true, width: image.width, height: image.height }
|
||||||
|
end
|
||||||
|
|
||||||
|
attachment = Submitters.create_attachment!(@submitter, file, metadata:)
|
||||||
|
|
||||||
|
if params[:remember_signature] == 'true' && @submitter.email.present?
|
||||||
|
cookies.permanent.encrypted[:signature_uuids] = build_new_cookie_signatures_json(@submitter, attachment)
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
|
||||||
|
rescue Submitters::MaliciousFileExtension => e
|
||||||
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
|
render json: { error: e.message }, status: :unprocessable_content
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_submitter
|
||||||
|
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||||
|
end
|
||||||
|
|
||||||
|
def authorize_upload!
|
||||||
|
unless can_upload?(@submitter)
|
||||||
|
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content
|
||||||
|
end
|
||||||
|
|
||||||
|
return if Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||||
|
|
||||||
|
render json: { error: I18n.t('verification_required_refresh_the_page_and_pass_2fa') },
|
||||||
|
status: :unprocessable_content
|
||||||
|
end
|
||||||
|
|
||||||
|
def can_upload?(submitter)
|
||||||
|
!submitter.declined_at? &&
|
||||||
|
!submitter.completed_at? &&
|
||||||
|
!submitter.submission.archived_at? &&
|
||||||
|
!submitter.submission.expired? &&
|
||||||
|
!submitter.submission.template&.archived_at?
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_new_cookie_signatures_json(submitter, attachment)
|
||||||
|
values =
|
||||||
|
begin
|
||||||
|
JSON.parse(cookies.encrypted[:signature_uuids].presence || '{}')
|
||||||
|
rescue JSON::ParserError
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
|
||||||
|
values[submitter.email] = attachment.uuid
|
||||||
|
|
||||||
|
values = values.to_a.last(COOKIE_STORE_LIMIT).to_h if values.size > COOKIE_STORE_LIMIT
|
||||||
|
|
||||||
|
values.to_json
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmitFormViewController < ActionController::API
|
||||||
|
def create
|
||||||
|
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||||
|
|
||||||
|
return head :forbidden if Submitter.signed_id_verifier.verified(params[:v], purpose: :view_form) != @submitter.slug
|
||||||
|
|
||||||
|
@submitter.opened_at = Time.current
|
||||||
|
@submitter.save
|
||||||
|
|
||||||
|
SubmissionEvents.create_with_tracking_data(@submitter, 'view_form', request)
|
||||||
|
|
||||||
|
WebhookUrls.enqueue_events(@submitter, 'form.viewed')
|
||||||
|
|
||||||
|
render json: {}
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -81,9 +81,26 @@ class SubmittersController < ApplicationController
|
|||||||
|
|
||||||
submitter.name = attrs[:name] if attrs.key?(:name)
|
submitter.name = attrs[:name] if attrs.key?(:name)
|
||||||
|
|
||||||
|
if submitter_changed?(submitter)
|
||||||
|
submitter.submitter_versions.new(slug: submitter.slug, email: submitter.email_was,
|
||||||
|
name: submitter.name_was, phone: submitter.phone_was)
|
||||||
|
|
||||||
|
submitter.slug = SecureRandom.base58(14)
|
||||||
|
submitter.values = Submitters.fetch_values_for_delegate(submitter)
|
||||||
|
end
|
||||||
|
|
||||||
submitter
|
submitter
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def submitter_changed?(submitter)
|
||||||
|
if submitter.email_was.blank? && submitter.phone_was.blank?
|
||||||
|
submitter.name_was.present? && submitter.name_changed?
|
||||||
|
else
|
||||||
|
(submitter.email_was.present? && submitter.email_changed?) ||
|
||||||
|
(submitter.phone_was.present? && submitter.phone_changed?)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def submitter_params
|
def submitter_params
|
||||||
params.require(:submitter).permit(:email, :name, :phone).transform_values(&:strip)
|
params.require(:submitter).permit(:email, :name, :phone).transform_values(&:strip)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,7 +6,17 @@ class TemplateDocumentsController < ApplicationController
|
|||||||
FILES_TTL = 5.minutes
|
FILES_TTL = 5.minutes
|
||||||
|
|
||||||
def index
|
def index
|
||||||
render json: @template.schema_documents.map { |d| ActiveStorage::Blob.proxy_path(d.blob, expires_at: FILES_TTL.from_now.to_i) }
|
documents = @template.schema.map do |item|
|
||||||
|
if item['dynamic']
|
||||||
|
dynamic_document = @template.schema_dynamic_documents.find { |e| e.uuid == item['attachment_uuid'] }
|
||||||
|
|
||||||
|
DynamicDocuments::EnsureVersionGenerated.call(dynamic_document).document_attachment
|
||||||
|
else
|
||||||
|
@template.schema_documents.find { |e| e.uuid == item['attachment_uuid'] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: documents.map { |d| ActiveStorage::Blob.proxy_path(d.blob, expires_at: FILES_TTL.from_now.to_i) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
|||||||
@@ -13,10 +13,7 @@ class TemplateFoldersController < ApplicationController
|
|||||||
.where(folder: [@template_folder, *(params[:q].present? ? @template_folder.subfolders : [])])
|
.where(folder: [@template_folder, *(params[:q].present? ? @template_folder.subfolders : [])])
|
||||||
.preload(:author, :template_accesses)
|
.preload(:author, :template_accesses)
|
||||||
|
|
||||||
@template_folders =
|
@template_folders = filter_folders(@template_folder)
|
||||||
@template_folder.subfolders.where(id: Template.accessible_by(current_ability).active.select(:folder_id))
|
|
||||||
|
|
||||||
@template_folders = TemplateFolders.search(@template_folders, params[:q])
|
|
||||||
@template_folders = TemplateFolders.sort(@template_folders, current_user, selected_order)
|
@template_folders = TemplateFolders.sort(@template_folders, current_user, selected_order)
|
||||||
|
|
||||||
if @templates.exists?
|
if @templates.exists?
|
||||||
@@ -40,6 +37,8 @@ class TemplateFoldersController < ApplicationController
|
|||||||
|
|
||||||
@templates = @templates.none
|
@templates = @templates.none
|
||||||
end
|
end
|
||||||
|
|
||||||
|
render_infinite_scroll(@pagy, @templates, @template_folders) if turbo_infinite_scroll?
|
||||||
end
|
end
|
||||||
|
|
||||||
def edit; end
|
def edit; end
|
||||||
@@ -55,6 +54,27 @@ class TemplateFoldersController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def filter_folders(template_folder)
|
||||||
|
template_folders =
|
||||||
|
template_folder.subfolders.where(id: Template.accessible_by(current_ability).active.select(:folder_id))
|
||||||
|
|
||||||
|
TemplateFolders.search(template_folders, params[:q])
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_infinite_scroll(pagy, templates, template_folders)
|
||||||
|
render_params =
|
||||||
|
if templates.present?
|
||||||
|
{ partial: 'templates/template', collection: templates }
|
||||||
|
else
|
||||||
|
{ partial: 'template_folders/folder', collection: template_folders, as: :folder }
|
||||||
|
end
|
||||||
|
|
||||||
|
render turbo_stream: [
|
||||||
|
turbo_stream.before('infinite_scroll', **render_params),
|
||||||
|
turbo_stream.replace('infinite_scroll', partial: 'shared/infinite_scroll', locals: { pagy: })
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
def selected_order
|
def selected_order
|
||||||
@selected_order ||=
|
@selected_order ||=
|
||||||
if can?(:manage, :countless)
|
if can?(:manage, :countless)
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ class TemplateSharingsTestingController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
testing_account = Accounts.find_or_create_testing_user(true_user.account).account
|
account = can?(:manage, :tenants_test) ? current_account : true_user.account
|
||||||
|
|
||||||
|
testing_account = Accounts.find_or_create_testing_user(account).account
|
||||||
|
|
||||||
if params[:value] == '1'
|
if params[:value] == '1'
|
||||||
TemplateSharing.create!(ability: :manage, account: testing_account, template: @template)
|
TemplateSharing.create!(ability: :manage, account: testing_account, template: @template)
|
||||||
|
|||||||
@@ -12,13 +12,23 @@ class TemplatesArchivedController < ApplicationController
|
|||||||
|
|
||||||
@pagy, @templates = pagy_auto(@templates.select_for_list, limit: 12)
|
@pagy, @templates = pagy_auto(@templates.select_for_list, limit: 12)
|
||||||
|
|
||||||
return unless params[:q].present? && @templates.blank?
|
if params[:q].present? && @templates.blank?
|
||||||
|
|
||||||
@related_submissions_pagy, @related_submissions = load_related_submissions
|
@related_submissions_pagy, @related_submissions = load_related_submissions
|
||||||
end
|
end
|
||||||
|
|
||||||
|
render_infinite_scroll(@pagy, @templates) if turbo_infinite_scroll?
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def render_infinite_scroll(pagy, templates)
|
||||||
|
render turbo_stream: [
|
||||||
|
turbo_stream.before('infinite_scroll', partial: 'templates/template', collection: templates,
|
||||||
|
locals: { with_folder: true }),
|
||||||
|
turbo_stream.replace('infinite_scroll', partial: 'shared/infinite_scroll', locals: { pagy: })
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
def load_related_submissions
|
def load_related_submissions
|
||||||
related_submissions =
|
related_submissions =
|
||||||
Submission.accessible_by(current_ability)
|
Submission.accessible_by(current_ability)
|
||||||
|
|||||||
@@ -16,7 +16,19 @@ class TemplatesArchivedSubmissionsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
@pagy, @submissions = pagy_auto(@submissions.select_for_list.preload(submitters: :start_form_submission_events))
|
@pagy, @submissions = pagy_auto(@submissions.select_for_list.preload(submitters: :start_form_submission_events))
|
||||||
|
|
||||||
|
render_infinite_scroll(@pagy, @submissions, @template) if turbo_infinite_scroll?
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
redirect_to root_path
|
redirect_to root_path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def render_infinite_scroll(pagy, submissions, template)
|
||||||
|
render turbo_stream: [
|
||||||
|
turbo_stream.before('infinite_scroll', partial: 'templates/submission', collection: submissions,
|
||||||
|
locals: { template:, archived: true }),
|
||||||
|
turbo_stream.replace('infinite_scroll', partial: 'shared/infinite_scroll', locals: { pagy: })
|
||||||
|
]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -21,6 +21,8 @@ class TemplatesController < ApplicationController
|
|||||||
|
|
||||||
@pagy, @submissions =
|
@pagy, @submissions =
|
||||||
pagy_auto(submissions.select_for_list.preload(:template_accesses, submitters: :start_form_submission_events))
|
pagy_auto(submissions.select_for_list.preload(:template_accesses, submitters: :start_form_submission_events))
|
||||||
|
|
||||||
|
render_infinite_scroll(@pagy, @submissions, @template) if turbo_infinite_scroll?
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
redirect_to root_path
|
redirect_to root_path
|
||||||
end
|
end
|
||||||
@@ -81,11 +83,23 @@ class TemplatesController < ApplicationController
|
|||||||
I18n.t('template_has_been_archived')
|
I18n.t('template_has_been_archived')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if @template.destroyed? && turbo_native_app?
|
||||||
|
redirect_to templates_archived_index_path, notice:
|
||||||
|
else
|
||||||
redirect_back(fallback_location: root_path, notice:)
|
redirect_back(fallback_location: root_path, notice:)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def render_infinite_scroll(pagy, submissions, template)
|
||||||
|
render turbo_stream: [
|
||||||
|
turbo_stream.before('infinite_scroll', partial: 'templates/submission', collection: submissions,
|
||||||
|
locals: { template:, archived: template.archived_at? }),
|
||||||
|
turbo_stream.replace('infinite_scroll', partial: 'shared/infinite_scroll', locals: { pagy: })
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
def template_params
|
def template_params
|
||||||
params.require(:template).permit(
|
params.require(:template).permit(
|
||||||
:name,
|
:name,
|
||||||
|
|||||||
@@ -43,10 +43,26 @@ class TemplatesDashboardController < ApplicationController
|
|||||||
@related_submissions_pagy, @related_submissions = load_related_submissions
|
@related_submissions_pagy, @related_submissions = load_related_submissions
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
render_infinite_scroll(@pagy, @templates, @template_folders) if turbo_infinite_scroll?
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def render_infinite_scroll(pagy, templates, template_folders)
|
||||||
|
render_params =
|
||||||
|
if templates.present?
|
||||||
|
{ partial: 'templates/template', collection: templates }
|
||||||
|
else
|
||||||
|
{ partial: 'template_folders/folder', collection: template_folders, as: :folder }
|
||||||
|
end
|
||||||
|
|
||||||
|
render turbo_stream: [
|
||||||
|
turbo_stream.before('infinite_scroll', **render_params),
|
||||||
|
turbo_stream.replace('infinite_scroll', partial: 'shared/infinite_scroll', locals: { pagy: })
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
def load_templates(templates, folders_count, show_shared_inline: false)
|
def load_templates(templates, folders_count, show_shared_inline: false)
|
||||||
templates = templates.preload(:author, :template_accesses)
|
templates = templates.preload(:author, :template_accesses)
|
||||||
|
|
||||||
@@ -94,7 +110,7 @@ class TemplatesDashboardController < ApplicationController
|
|||||||
|
|
||||||
pinned_count = (show_default_in_grid ? 1 : 0) + (show_shared_in_grid ? 1 : 0)
|
pinned_count = (show_default_in_grid ? 1 : 0) + (show_shared_in_grid ? 1 : 0)
|
||||||
|
|
||||||
pagy = Pagy::Offset.new(count: folders_count + pinned_count,
|
pagy, = pagy(template_folders, count: folders_count + pinned_count,
|
||||||
page: show_inline_folders ? 1 : [params[:page].to_s.to_i, 1].max,
|
page: show_inline_folders ? 1 : [params[:page].to_s.to_i, 1].max,
|
||||||
limit: FOLDERS_PER_PAGE,
|
limit: FOLDERS_PER_PAGE,
|
||||||
raise_range_error: true)
|
raise_range_error: true)
|
||||||
@@ -103,7 +119,7 @@ class TemplatesDashboardController < ApplicationController
|
|||||||
show_shared_folder = show_shared_in_grid && pagy.page == 1
|
show_shared_folder = show_shared_in_grid && pagy.page == 1
|
||||||
|
|
||||||
folder_offset = pagy.page == 1 ? 0 : pagy.offset - pinned_count
|
folder_offset = pagy.page == 1 ? 0 : pagy.offset - pinned_count
|
||||||
folder_limit = pagy.page == 1 ? FOLDERS_PER_PAGE - pinned_count : FOLDERS_PER_PAGE
|
folder_limit = pagy.page == 1 ? pagy.limit - pinned_count : pagy.limit
|
||||||
|
|
||||||
template_folders = template_folders.offset(folder_offset).limit(folder_limit)
|
template_folders = template_folders.offset(folder_offset).limit(folder_limit)
|
||||||
|
|
||||||
|
|||||||
@@ -19,13 +19,22 @@ class TemplatesSharedController < ApplicationController
|
|||||||
|
|
||||||
@pagy, @templates = pagy_auto(@templates.select_for_list, limit: 12)
|
@pagy, @templates = pagy_auto(@templates.select_for_list, limit: 12)
|
||||||
|
|
||||||
return unless params[:q].present? && @templates.blank?
|
if params[:q].present? && @templates.blank?
|
||||||
|
|
||||||
@related_submissions_pagy, @related_submissions = load_related_submissions(is_archived: @is_archived)
|
@related_submissions_pagy, @related_submissions = load_related_submissions(is_archived: @is_archived)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
render_infinite_scroll(@pagy, @templates) if turbo_infinite_scroll?
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def render_infinite_scroll(pagy, templates)
|
||||||
|
render turbo_stream: [
|
||||||
|
turbo_stream.before('infinite_scroll', partial: 'templates/template', collection: templates),
|
||||||
|
turbo_stream.replace('infinite_scroll', partial: 'shared/infinite_scroll', locals: { pagy: })
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
def load_related_submissions(is_archived:)
|
def load_related_submissions(is_archived:)
|
||||||
shared_templates = Templates.shared(current_user)
|
shared_templates = Templates.shared(current_user)
|
||||||
shared_templates = is_archived ? shared_templates.archived : shared_templates.active
|
shared_templates = is_archived ? shared_templates.archived : shared_templates.active
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
class TestingApiSettingsController < ApplicationController
|
class TestingApiSettingsController < ApplicationController
|
||||||
def index
|
def index
|
||||||
|
raise CanCan::AccessDenied unless current_account.testing?
|
||||||
|
|
||||||
authorize!(:manage, current_user.access_token)
|
authorize!(:manage, current_user.access_token)
|
||||||
|
|
||||||
@webhook_url = current_account.webhook_urls.first_or_initialize
|
@webhook_url = current_account.webhook_urls.first_or_initialize
|
||||||
|
|||||||
@@ -11,11 +11,9 @@ class UserInitialsController < ApplicationController
|
|||||||
|
|
||||||
return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_initials') if file.blank?
|
return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_initials') if file.blank?
|
||||||
|
|
||||||
extension = File.extname(file.original_filename).delete_prefix('.').downcase
|
extension = FilenameUtils.dangerous_extension(file.original_filename)
|
||||||
|
|
||||||
if Submitters::DANGEROUS_EXTENSIONS.include?(extension)
|
raise Submitters::MaliciousFileExtension, "File type '.#{extension}' is not allowed." if extension
|
||||||
raise Submitters::MaliciousFileExtension, "File type '.#{extension}' is not allowed."
|
|
||||||
end
|
|
||||||
|
|
||||||
blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
|
blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
|
||||||
filename: file.original_filename,
|
filename: file.original_filename,
|
||||||
|
|||||||
@@ -11,11 +11,9 @@ class UserSignaturesController < ApplicationController
|
|||||||
|
|
||||||
return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_signature') if file.blank?
|
return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_signature') if file.blank?
|
||||||
|
|
||||||
extension = File.extname(file.original_filename).delete_prefix('.').downcase
|
extension = FilenameUtils.dangerous_extension(file.original_filename)
|
||||||
|
|
||||||
if Submitters::DANGEROUS_EXTENSIONS.include?(extension)
|
raise Submitters::MaliciousFileExtension, "File type '.#{extension}' is not allowed." if extension
|
||||||
raise Submitters::MaliciousFileExtension, "File type '.#{extension}' is not allowed."
|
|
||||||
end
|
|
||||||
|
|
||||||
blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
|
blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
|
||||||
filename: file.original_filename,
|
filename: file.original_filename,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class UsersController < ApplicationController
|
|||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html do
|
format.html do
|
||||||
@pagy, @users = pagy(@users)
|
@pagy, @users = pagy(@users, limit: turbo_native_app? ? 1_000 : Pagy::OPTIONS[:limit])
|
||||||
end
|
end
|
||||||
|
|
||||||
if current_ability.can?(:manage, current_account)
|
if current_ability.can?(:manage, current_account)
|
||||||
|
|||||||
+179
-21
@@ -1,5 +1,5 @@
|
|||||||
import '@hotwired/turbo'
|
import '@hotwired/turbo-rails'
|
||||||
import { encodeMethodIntoRequestBody } from '@hotwired/turbo-rails/app/javascript/turbo/fetch_requests'
|
import './native'
|
||||||
|
|
||||||
import { createApp, reactive } from 'vue'
|
import { createApp, reactive } from 'vue'
|
||||||
import TemplateBuilder from './template_builder/builder'
|
import TemplateBuilder from './template_builder/builder'
|
||||||
@@ -9,6 +9,15 @@ import ToggleVisible from './elements/toggle_visible'
|
|||||||
import ToggleCookies from './elements/toggle_cookies'
|
import ToggleCookies from './elements/toggle_cookies'
|
||||||
import DisableHidden from './elements/disable_hidden'
|
import DisableHidden from './elements/disable_hidden'
|
||||||
import TurboModal from './elements/turbo_modal'
|
import TurboModal from './elements/turbo_modal'
|
||||||
|
import NativeAction from './elements/native_action'
|
||||||
|
import NativeMenu from './elements/native_menu'
|
||||||
|
import NativeSearch from './elements/native_search'
|
||||||
|
import NativeModal from './elements/native_modal'
|
||||||
|
import NativeEvent from './elements/native_event'
|
||||||
|
import NativeUpload from './elements/native_upload'
|
||||||
|
import FormPrompt from './elements/form_prompt'
|
||||||
|
import InfiniteScroll from './elements/infinite_scroll'
|
||||||
|
import ModalButton from './elements/modal_button'
|
||||||
import FileDropzone from './elements/file_dropzone'
|
import FileDropzone from './elements/file_dropzone'
|
||||||
import MenuActive from './elements/menu_active'
|
import MenuActive from './elements/menu_active'
|
||||||
import ClipboardCopy from './elements/clipboard_copy'
|
import ClipboardCopy from './elements/clipboard_copy'
|
||||||
@@ -26,6 +35,9 @@ import PromptPassword from './elements/prompt_password'
|
|||||||
import EmailsTextarea from './elements/emails_textarea'
|
import EmailsTextarea from './elements/emails_textarea'
|
||||||
import ToggleSubmit from './elements/toggle_submit'
|
import ToggleSubmit from './elements/toggle_submit'
|
||||||
import ToggleOnSubmit from './elements/toggle_on_submit'
|
import ToggleOnSubmit from './elements/toggle_on_submit'
|
||||||
|
import ToggleOnChecked from './elements/toggle_on_checked'
|
||||||
|
import ToggleSendTitle from './elements/toggle_send_title'
|
||||||
|
import AppendTemplate from './elements/append_template'
|
||||||
import CheckOnClick from './elements/check_on_click'
|
import CheckOnClick from './elements/check_on_click'
|
||||||
import PasswordInput from './elements/password_input'
|
import PasswordInput from './elements/password_input'
|
||||||
import SearchInput from './elements/search_input'
|
import SearchInput from './elements/search_input'
|
||||||
@@ -59,10 +71,8 @@ import ConfirmUpload from './elements/confirm_upload'
|
|||||||
import ScrollFade from './elements/scroll_fade'
|
import ScrollFade from './elements/scroll_fade'
|
||||||
import OpenModalMobile from './elements/open_modal_mobile'
|
import OpenModalMobile from './elements/open_modal_mobile'
|
||||||
import HistoryBack from './elements/history_back'
|
import HistoryBack from './elements/history_back'
|
||||||
|
import DatePlaceholder from './elements/date_placeholder'
|
||||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
import FlashMessage from './elements/flash_message'
|
||||||
|
|
||||||
TurboInstantClick.start()
|
|
||||||
|
|
||||||
document.addEventListener('turbo:before-cache', () => {
|
document.addEventListener('turbo:before-cache', () => {
|
||||||
window.flash?.remove()
|
window.flash?.remove()
|
||||||
@@ -74,10 +84,18 @@ document.addEventListener('keyup', (e) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
|
document.addEventListener('turbo:morph', () => {
|
||||||
document.addEventListener('turbo:before-fetch-request', (event) => {
|
if (document.activeElement?.closest('.dropdown')) {
|
||||||
event.detail.fetchOptions.headers['X-Turbo'] = 'true'
|
document.activeElement.blur()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
document.addEventListener('turbo:before-fetch-response', (event) => {
|
||||||
|
if (event.detail.fetchResponse.header('content-disposition')?.includes('attachment')) {
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
document.addEventListener('turbo:submit-end', async (event) => {
|
document.addEventListener('turbo:submit-end', async (event) => {
|
||||||
const resp = event.detail?.formSubmission?.result?.fetchResponse?.response
|
const resp = event.detail?.formSubmission?.result?.fetchResponse?.response
|
||||||
|
|
||||||
@@ -85,11 +103,23 @@ document.addEventListener('turbo:submit-end', async (event) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filename = decodeURIComponent(resp.headers.get('content-disposition').split('"')[1])
|
||||||
|
const download = window.webkit?.messageHandlers?.download
|
||||||
|
|
||||||
|
if (download) {
|
||||||
|
const reader = new FileReader()
|
||||||
|
|
||||||
|
reader.onload = () => download.postMessage({ name: filename, data: reader.result.split(',')[1] })
|
||||||
|
reader.readAsDataURL(await resp.blob())
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const url = URL.createObjectURL(await resp.blob())
|
const url = URL.createObjectURL(await resp.blob())
|
||||||
const link = document.createElement('a')
|
const link = document.createElement('a')
|
||||||
|
|
||||||
link.href = url
|
link.href = url
|
||||||
link.setAttribute('download', decodeURIComponent(resp.headers.get('content-disposition').split('"')[1]))
|
link.setAttribute('download', filename)
|
||||||
|
|
||||||
document.body.appendChild(link)
|
document.body.appendChild(link)
|
||||||
|
|
||||||
@@ -105,6 +135,15 @@ const safeRegisterElement = (name, element, options = {}) => !window.customEleme
|
|||||||
safeRegisterElement('toggle-visible', ToggleVisible)
|
safeRegisterElement('toggle-visible', ToggleVisible)
|
||||||
safeRegisterElement('disable-hidden', DisableHidden)
|
safeRegisterElement('disable-hidden', DisableHidden)
|
||||||
safeRegisterElement('turbo-modal', TurboModal)
|
safeRegisterElement('turbo-modal', TurboModal)
|
||||||
|
safeRegisterElement('native-action', NativeAction)
|
||||||
|
safeRegisterElement('native-menu', NativeMenu)
|
||||||
|
safeRegisterElement('native-search', NativeSearch)
|
||||||
|
safeRegisterElement('native-modal', NativeModal)
|
||||||
|
safeRegisterElement('native-event', NativeEvent)
|
||||||
|
safeRegisterElement('native-upload', NativeUpload)
|
||||||
|
safeRegisterElement('form-prompt', FormPrompt)
|
||||||
|
safeRegisterElement('infinite-scroll', InfiniteScroll)
|
||||||
|
safeRegisterElement('modal-button', ModalButton)
|
||||||
safeRegisterElement('file-dropzone', FileDropzone)
|
safeRegisterElement('file-dropzone', FileDropzone)
|
||||||
safeRegisterElement('menu-active', MenuActive)
|
safeRegisterElement('menu-active', MenuActive)
|
||||||
safeRegisterElement('clipboard-copy', ClipboardCopy)
|
safeRegisterElement('clipboard-copy', ClipboardCopy)
|
||||||
@@ -123,6 +162,9 @@ safeRegisterElement('emails-textarea', EmailsTextarea)
|
|||||||
safeRegisterElement('toggle-cookies', ToggleCookies)
|
safeRegisterElement('toggle-cookies', ToggleCookies)
|
||||||
safeRegisterElement('toggle-submit', ToggleSubmit)
|
safeRegisterElement('toggle-submit', ToggleSubmit)
|
||||||
safeRegisterElement('toggle-on-submit', ToggleOnSubmit)
|
safeRegisterElement('toggle-on-submit', ToggleOnSubmit)
|
||||||
|
safeRegisterElement('toggle-on-checked', ToggleOnChecked)
|
||||||
|
safeRegisterElement('toggle-send-title', ToggleSendTitle)
|
||||||
|
safeRegisterElement('append-template', AppendTemplate)
|
||||||
safeRegisterElement('password-input', PasswordInput)
|
safeRegisterElement('password-input', PasswordInput)
|
||||||
safeRegisterElement('search-input', SearchInput)
|
safeRegisterElement('search-input', SearchInput)
|
||||||
safeRegisterElement('toggle-attribute', ToggleAttribute)
|
safeRegisterElement('toggle-attribute', ToggleAttribute)
|
||||||
@@ -156,22 +198,37 @@ safeRegisterElement('confirm-upload', ConfirmUpload)
|
|||||||
safeRegisterElement('scroll-fade', ScrollFade)
|
safeRegisterElement('scroll-fade', ScrollFade)
|
||||||
safeRegisterElement('open-modal-mobile', OpenModalMobile)
|
safeRegisterElement('open-modal-mobile', OpenModalMobile)
|
||||||
safeRegisterElement('history-back', HistoryBack)
|
safeRegisterElement('history-back', HistoryBack)
|
||||||
|
safeRegisterElement('date-placeholder', DatePlaceholder)
|
||||||
|
safeRegisterElement('flash-message', FlashMessage)
|
||||||
|
|
||||||
safeRegisterElement('template-builder', class extends HTMLElement {
|
safeRegisterElement('template-builder', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
document.addEventListener('turbo:submit-end', this.onSubmit)
|
document.addEventListener('turbo:submit-end', this.onSubmit)
|
||||||
|
document.addEventListener('template-builder:update', this.onSheetSubmit)
|
||||||
|
document.addEventListener('template-builder:sync', this.onSync)
|
||||||
|
document.addEventListener('template-builder:scroll-to', this.onScrollTo)
|
||||||
|
document.addEventListener('template-builder:select-field', this.onSelectField)
|
||||||
|
document.addEventListener('template-builder:select-submitter', this.onSelectSubmitter)
|
||||||
|
document.addEventListener('template-builder:sync-custom-fields', this.onSyncCustomFields)
|
||||||
|
document.addEventListener('template-builder:draw-field', this.onDrawField)
|
||||||
|
document.addEventListener('native:builder-view', this.onBuilderView)
|
||||||
document.addEventListener('turbo:before-cache', this.onBeforeCache)
|
document.addEventListener('turbo:before-cache', this.onBeforeCache)
|
||||||
|
|
||||||
this.appElem = document.createElement('div')
|
this.appElem = document.createElement('div')
|
||||||
|
|
||||||
this.appElem.classList.add('md:h-screen')
|
this.appElem.classList.add('md:h-screen')
|
||||||
|
|
||||||
|
if (this.dataset.nativeView) {
|
||||||
|
this.appElem.classList.add('min-w-0')
|
||||||
|
}
|
||||||
|
|
||||||
const template = reactive(JSON.parse(this.dataset.template))
|
const template = reactive(JSON.parse(this.dataset.template))
|
||||||
|
|
||||||
this.app = createApp(TemplateBuilder, {
|
this.app = createApp(TemplateBuilder, {
|
||||||
template,
|
template,
|
||||||
customFields: reactive(JSON.parse(this.dataset.customFields || '[]')),
|
customFields: reactive(JSON.parse(this.dataset.customFields || '[]')),
|
||||||
dateFormats: JSON.parse(this.dataset.dateFormats || '[]'),
|
dateFormats: JSON.parse(this.dataset.dateFormats || '[]'),
|
||||||
|
autosave: this.dataset.autosave !== 'false',
|
||||||
dynamicDocuments: reactive(JSON.parse(this.dataset.dynamicDocuments || '[]')),
|
dynamicDocuments: reactive(JSON.parse(this.dataset.dynamicDocuments || '[]')),
|
||||||
backgroundColor: '#faf7f5',
|
backgroundColor: '#faf7f5',
|
||||||
locale: this.dataset.locale,
|
locale: this.dataset.locale,
|
||||||
@@ -203,7 +260,9 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
|||||||
withDownload: true,
|
withDownload: true,
|
||||||
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
||||||
acceptFileTypes: this.dataset.acceptFileTypes,
|
acceptFileTypes: this.dataset.acceptFileTypes,
|
||||||
showTourStartForm: this.dataset.showTourStartForm === 'true'
|
showTourStartForm: this.dataset.showTourStartForm === 'true',
|
||||||
|
nativePlatform: this.dataset.nativePlatform,
|
||||||
|
nativeView: this.dataset.nativeView
|
||||||
})
|
})
|
||||||
|
|
||||||
this.component = this.app.mount(this.appElem)
|
this.component = this.app.mount(this.appElem)
|
||||||
@@ -211,17 +270,89 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
|||||||
this.appendChild(this.appElem)
|
this.appendChild(this.appElem)
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit = (e) => {
|
onSubmit = async (e) => {
|
||||||
if (e.detail.success) {
|
const form = e.detail.formSubmission?.formElement
|
||||||
if (e.detail?.formSubmission?.formElement?.id === 'submitters_form') {
|
|
||||||
e.detail.fetchResponse.response.json().then((data) => {
|
if (!e.detail.success || !form) return
|
||||||
this.component.template.submitters = data.submitters
|
|
||||||
})
|
const data = {}
|
||||||
|
|
||||||
|
new FormData(form).forEach((value, key) => { data[key] = value })
|
||||||
|
|
||||||
|
this.applySubmission(form.id, form.action, await e.detail.fetchResponse.responseText, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.detail?.formSubmission?.formElement?.action?.endsWith('/prefillable_fields')) {
|
onSheetSubmit = (e) => {
|
||||||
e.detail.fetchResponse.response.text().then((data) => {
|
this.applySubmission(e.detail.form, e.detail.action, e.detail.body, {})
|
||||||
const doc = new DOMParser().parseFromString(data, 'text/html')
|
this.component.syncNative()
|
||||||
|
}
|
||||||
|
|
||||||
|
onSync = (e) => {
|
||||||
|
const { schema, documents, fields, submitters, pendingFieldAttachmentUuids } = e.detail
|
||||||
|
const selectedAreas = this.component.selectedAreasRef.value.map((area) => {
|
||||||
|
const field = this.component.template.fields.find((f) => f.areas?.includes(area))
|
||||||
|
|
||||||
|
return field && { uuid: field.uuid, index: field.areas.indexOf(area) }
|
||||||
|
})
|
||||||
|
const selectedSubmitterUuid = this.component.selectedSubmitter?.uuid
|
||||||
|
|
||||||
|
Object.assign(this.component.template, { schema, documents, fields, submitters })
|
||||||
|
|
||||||
|
this.component.selectedAreasRef.value = selectedAreas.map((item) => item && fields.find((f) => f.uuid === item.uuid)?.areas?.[item.index]).filter(Boolean)
|
||||||
|
this.component.selectedSubmitter = submitters.find((s) => s.uuid === selectedSubmitterUuid) || submitters[0]
|
||||||
|
this.component.pendingFieldAttachmentUuids = pendingFieldAttachmentUuids
|
||||||
|
}
|
||||||
|
|
||||||
|
onScrollTo = (e) => {
|
||||||
|
this.component.scrollIntoDocument(e.detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
onSyncCustomFields = (e) => {
|
||||||
|
this.component.customFields.splice(0, this.component.customFields.length, ...e.detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectSubmitter = (e) => {
|
||||||
|
const submitter = this.component.template.submitters.find((s) => s.uuid === e.detail.uuid)
|
||||||
|
|
||||||
|
if (submitter) this.component.selectedSubmitter = submitter
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectField = (e) => {
|
||||||
|
const { uuid, attachment_uuid: attachmentUuid, page, x, y } = e.detail
|
||||||
|
const field = this.component.template.fields.find((f) => f.uuid === uuid)
|
||||||
|
const areas = field?.areas || []
|
||||||
|
const area = areas.find((a) => a.attachment_uuid === attachmentUuid && a.page === page && a.x === x && a.y === y) || areas[0]
|
||||||
|
|
||||||
|
if (area) this.component.scrollToArea(area)
|
||||||
|
}
|
||||||
|
|
||||||
|
onDrawField = (e) => {
|
||||||
|
const { uuid, type, option, custom_field_uuid: customFieldUuid } = e.detail
|
||||||
|
|
||||||
|
if (customFieldUuid) {
|
||||||
|
const field = this.component.customFields.find((f) => f.uuid === customFieldUuid)
|
||||||
|
|
||||||
|
if (field) {
|
||||||
|
this.component.drawCustomField = field
|
||||||
|
this.component.showDrawField = true
|
||||||
|
}
|
||||||
|
} else if (uuid) {
|
||||||
|
const field = this.component.template.fields.find((f) => f.uuid === uuid)
|
||||||
|
|
||||||
|
if (field) {
|
||||||
|
this.component.drawField = field
|
||||||
|
this.component.drawOption = option || null
|
||||||
|
}
|
||||||
|
} else if (type) {
|
||||||
|
this.component.startFieldDraw({ type })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applySubmission (formId, action, body, data) {
|
||||||
|
if (formId === 'submitters_form') {
|
||||||
|
this.component.template.submitters = JSON.parse(body).submitters
|
||||||
|
} else if (action.endsWith('/prefillable_fields')) {
|
||||||
|
const doc = new DOMParser().parseFromString(body, 'text/html')
|
||||||
const fragment = doc.querySelector('turbo-stream template').content
|
const fragment = doc.querySelector('turbo-stream template').content
|
||||||
|
|
||||||
const prefillableUuidsIndex = {}
|
const prefillableUuidsIndex = {}
|
||||||
@@ -239,9 +370,28 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
|||||||
delete field.readonly
|
delete field.readonly
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
} else if (data['template[name]']) {
|
||||||
|
this.component.template.name = data['template[name]']
|
||||||
|
|
||||||
|
document.title = data['template[name]']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onBuilderView = (event) => {
|
||||||
|
const root = document.documentElement.cloneNode(true)
|
||||||
|
const builder = root.querySelector('template-builder')
|
||||||
|
const style = document.createElement('style')
|
||||||
|
|
||||||
|
builder.innerHTML = ''
|
||||||
|
builder.setAttribute('data-native-view', event.detail.view)
|
||||||
|
|
||||||
|
root.querySelector('meta[name="viewport"]')?.setAttribute('content', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no')
|
||||||
|
|
||||||
|
style.textContent = 'html { overflow-x: hidden; overscroll-behavior-x: none; } html, body { max-width: 100%; } body { -webkit-user-select: none; user-select: none; } input, textarea, [contenteditable] { -webkit-user-select: text; user-select: text; }'
|
||||||
|
|
||||||
|
root.querySelector('head').appendChild(style)
|
||||||
|
|
||||||
|
event.detail.result = '<!DOCTYPE html>' + root.outerHTML
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeCache = () => {
|
onBeforeCache = () => {
|
||||||
@@ -251,6 +401,14 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
|||||||
|
|
||||||
disconnectedCallback () {
|
disconnectedCallback () {
|
||||||
document.removeEventListener('turbo:submit-end', this.onSubmit)
|
document.removeEventListener('turbo:submit-end', this.onSubmit)
|
||||||
|
document.removeEventListener('template-builder:update', this.onSheetSubmit)
|
||||||
|
document.removeEventListener('template-builder:sync', this.onSync)
|
||||||
|
document.removeEventListener('template-builder:scroll-to', this.onScrollTo)
|
||||||
|
document.removeEventListener('template-builder:select-field', this.onSelectField)
|
||||||
|
document.removeEventListener('template-builder:select-submitter', this.onSelectSubmitter)
|
||||||
|
document.removeEventListener('template-builder:sync-custom-fields', this.onSyncCustomFields)
|
||||||
|
document.removeEventListener('template-builder:draw-field', this.onDrawField)
|
||||||
|
document.removeEventListener('native:builder-view', this.onBuilderView)
|
||||||
document.removeEventListener('turbo:before-cache', this.onBeforeCache)
|
document.removeEventListener('turbo:before-cache', this.onBeforeCache)
|
||||||
|
|
||||||
this.app?.unmount()
|
this.app?.unmount()
|
||||||
|
|||||||
@@ -166,3 +166,71 @@ button[disabled] .enabled, button.btn-disabled .enabled {
|
|||||||
markdown-editor [contenteditable] p {
|
markdown-editor [contenteditable] p {
|
||||||
margin-bottom: 18px;
|
margin-bottom: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
native-action {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.table-stack,
|
||||||
|
.table-stack tbody,
|
||||||
|
.table-stack tr,
|
||||||
|
.table-stack td {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-stack,
|
||||||
|
div:has(> .table-stack) {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-stack thead {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-stack tr {
|
||||||
|
@apply bg-base-200 rounded-2xl py-3 mb-3 border-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-stack td {
|
||||||
|
@apply px-4 py-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-stack td[data-label] {
|
||||||
|
@apply flow-root text-right;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-stack td[data-label]::before {
|
||||||
|
content: attr(data-label);
|
||||||
|
@apply float-left mr-4 text-sm leading-6 text-base-content/60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-stack td:first-child {
|
||||||
|
@apply font-semibold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-stack td:last-child {
|
||||||
|
@apply grid grid-cols-2 gap-2 pt-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-stack td:last-child:has(> .dropdown) {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr)) auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-stack td:last-child:not(:has(*)) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-stack td:last-child > * {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-stack td:last-child > :last-child:nth-child(odd):not(.dropdown) {
|
||||||
|
@apply col-span-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-stack td:last-child .btn {
|
||||||
|
@apply btn-sm w-full;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -126,11 +126,10 @@ window.customElements.define('draw-signature', class extends HTMLElement {
|
|||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
|
|
||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
formData.append('submitter_slug', this.dataset.slug)
|
|
||||||
formData.append('name', 'attachments')
|
formData.append('name', 'attachments')
|
||||||
formData.append('remember_signature', 'true')
|
formData.append('remember_signature', 'true')
|
||||||
|
|
||||||
return fetch('/api/attachments', {
|
return fetch(`/s/${this.dataset.slug}/upload`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
}).then(resp => resp.json())
|
}).then(resp => resp.json())
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.addEventListener('click', this.onClick)
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
this.removeEventListener('click', this.onClick)
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const target = document.getElementById(this.dataset.targetId)
|
||||||
|
const item = this.querySelector('template').content.firstElementChild.cloneNode(true)
|
||||||
|
|
||||||
|
item.querySelector('a').addEventListener('click', (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
item.remove()
|
||||||
|
|
||||||
|
target.classList.toggle('hidden', !target.childElementCount)
|
||||||
|
})
|
||||||
|
|
||||||
|
target.append(item)
|
||||||
|
target.classList.remove('hidden')
|
||||||
|
|
||||||
|
item.querySelector('input').focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
export default class extends HTMLElement {
|
export default class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
|
this.id = `autoresize-textarea-${Math.random().toString(32).split('.')[1]}`
|
||||||
|
|
||||||
this.resize()
|
this.resize()
|
||||||
|
|
||||||
this.textarea.addEventListener('input', () => this.resize())
|
this.textarea.addEventListener('input', () => this.resize())
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
export default class extends HTMLElement {
|
export default class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
|
this.id = `bar-chart-${Math.random().toString(32).split('.')[1]}`
|
||||||
|
|
||||||
this.chartLabels = JSON.parse(this.dataset.labels || '[]')
|
this.chartLabels = JSON.parse(this.dataset.labels || '[]')
|
||||||
this.chartDatasets = JSON.parse(this.dataset.datasets || '[]')
|
this.chartDatasets = JSON.parse(this.dataset.datasets || '[]')
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,18 @@ export default class extends HTMLElement {
|
|||||||
|
|
||||||
this.addEventListener('click', (e) => {
|
this.addEventListener('click', (e) => {
|
||||||
const text = this.dataset.text || this.innerText.trim()
|
const text = this.dataset.text || this.innerText.trim()
|
||||||
|
const download = window.webkit?.messageHandlers?.download
|
||||||
|
|
||||||
|
if (download && /^https?:\/\//.test(text)) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
return download.postMessage({ url: text })
|
||||||
|
}
|
||||||
|
|
||||||
if (navigator.clipboard) {
|
if (navigator.clipboard) {
|
||||||
navigator.clipboard.writeText(text)
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
window.webkit?.messageHandlers?.flash?.postMessage({ style: 'notice', message: this.dataset.copied })
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
if (e.target.tagName !== 'INPUT') {
|
if (e.target.tagName !== 'INPUT') {
|
||||||
alert(`Clipboard not available. Make sure you're using https://\nCopy text: ${text}`)
|
alert(`Clipboard not available. Make sure you're using https://\nCopy text: ${text}`)
|
||||||
|
|||||||
@@ -59,10 +59,10 @@ export async function convertImagesInInput (input) {
|
|||||||
|
|
||||||
export default class extends HTMLElement {
|
export default class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
const input = this.querySelector('input[type="file"]')
|
this.addEventListener('change', async (e) => {
|
||||||
|
const input = e.target
|
||||||
const form = input.form
|
const form = input.form
|
||||||
|
|
||||||
input.addEventListener('change', async () => {
|
|
||||||
await convertImagesInInput(input)
|
await convertImagesInInput(input)
|
||||||
|
|
||||||
form.querySelector('[type="submit"]')?.setAttribute('disabled', true)
|
form.querySelector('[type="submit"]')?.setAttribute('disabled', true)
|
||||||
|
|||||||
@@ -26,17 +26,41 @@ export default targetable(class extends HTMLElement {
|
|||||||
|
|
||||||
window.addEventListener('dragleave', this.onWindowDragleave)
|
window.addEventListener('dragleave', this.onWindowDragleave)
|
||||||
|
|
||||||
this.fileDropzone?.addEventListener('drop', this.onDropFile)
|
this.addEventListener('drop', this.onDrop)
|
||||||
this.folderDropzone?.addEventListener('drop', this.onDropNewFolder)
|
this.addEventListener('dragstart', this.onDragstart)
|
||||||
|
this.addEventListener('dragover', this.onDragover)
|
||||||
|
this.addEventListener('dragleave', this.onDragleave)
|
||||||
|
}
|
||||||
|
|
||||||
this.folderCards.forEach((el) => el.addEventListener('drop', (e) => this.onDropFolder(e, el)))
|
dropTarget (e) {
|
||||||
this.templateCards.forEach((el) => el.addEventListener('drop', this.onDropTemplate))
|
return e.target.closest([
|
||||||
this.templateCards.forEach((el) => el.addEventListener('dragstart', this.onTemplateDragStart))
|
'[data-target="dashboard-dropzone.fileDropzone"]',
|
||||||
|
'[data-target="dashboard-dropzone.folderDropzone"]',
|
||||||
|
'[data-targets="dashboard-dropzone.folderCards"]',
|
||||||
|
'[data-targets="dashboard-dropzone.templateCards"]'
|
||||||
|
].join(', '))
|
||||||
|
}
|
||||||
|
|
||||||
return [this.fileDropzone, this.folderDropzone, ...this.folderCards, ...this.templateCards].forEach((el) => {
|
onDrop = (e) => {
|
||||||
el?.addEventListener('dragover', this.onDragover)
|
const el = this.dropTarget(e)
|
||||||
el?.addEventListener('dragleave', this.onDragleave)
|
|
||||||
})
|
if (!el) return
|
||||||
|
|
||||||
|
if (el === this.fileDropzone) {
|
||||||
|
this.onDropFile(e)
|
||||||
|
} else if (el === this.folderDropzone) {
|
||||||
|
this.onDropNewFolder(e)
|
||||||
|
} else if (el.dataset.targets === 'dashboard-dropzone.folderCards') {
|
||||||
|
this.onDropFolder(e, el)
|
||||||
|
} else {
|
||||||
|
this.onDropTemplate(e, el)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDragstart = (e) => {
|
||||||
|
if (e.target.closest('[data-targets="dashboard-dropzone.templateCards"]')) {
|
||||||
|
this.onTemplateDragStart(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback () {
|
disconnectedCallback () {
|
||||||
@@ -122,13 +146,13 @@ export default targetable(class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
window.Turbo.cache.clear()
|
window.Turbo.cache.clear()
|
||||||
window.Turbo.visit(location.href)
|
window.Turbo.visit(location.href, { action: 'replace' })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onDropTemplate = (e) => {
|
onDropTemplate = (e, el) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
if (e.dataTransfer.files.length) {
|
if (e.dataTransfer.files.length) {
|
||||||
@@ -136,10 +160,10 @@ export default targetable(class extends HTMLElement {
|
|||||||
loading.classList.add('bottom-5', 'left-0', 'flex', 'justify-center', 'w-full', 'absolute')
|
loading.classList.add('bottom-5', 'left-0', 'flex', 'justify-center', 'w-full', 'absolute')
|
||||||
loading.innerHTML = loadingIconHtml
|
loading.innerHTML = loadingIconHtml
|
||||||
|
|
||||||
e.target.appendChild(loading)
|
el.appendChild(loading)
|
||||||
e.target.classList.add('opacity-50')
|
el.classList.add('opacity-50')
|
||||||
|
|
||||||
const id = e.target.href.split('/').pop()
|
const id = el.href.split('/').pop()
|
||||||
|
|
||||||
this.uploadFiles(e.dataTransfer.files, `/templates/${id}/clone_and_replace`)
|
this.uploadFiles(e.dataTransfer.files, `/templates/${id}/clone_and_replace`)
|
||||||
}
|
}
|
||||||
@@ -173,16 +197,20 @@ export default targetable(class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onDragover (e) {
|
onDragover = (e) => {
|
||||||
if (e.dataTransfer?.types?.includes('Files') || this.dataset.targets !== 'dashboard-dropzone.templateCards') {
|
const el = this.dropTarget(e)
|
||||||
this.style.backgroundColor = '#F7F3F0'
|
|
||||||
|
|
||||||
if (this.classList.contains('before:border-base-300')) {
|
if (!el) return
|
||||||
this.classList.remove('before:border-base-300')
|
|
||||||
this.classList.add('before:border-base-content/30')
|
if (e.dataTransfer?.types?.includes('Files') || el.dataset.targets !== 'dashboard-dropzone.templateCards') {
|
||||||
} else if (this.classList.contains('border-base-300')) {
|
el.style.backgroundColor = '#F7F3F0'
|
||||||
this.classList.remove('border-base-300')
|
|
||||||
this.classList.add('border-base-content/30')
|
if (el.classList.contains('before:border-base-300')) {
|
||||||
|
el.classList.remove('before:border-base-300')
|
||||||
|
el.classList.add('before:border-base-content/30')
|
||||||
|
} else if (el.classList.contains('border-base-300')) {
|
||||||
|
el.classList.remove('border-base-300')
|
||||||
|
el.classList.add('border-base-content/30')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,15 +233,19 @@ export default targetable(class extends HTMLElement {
|
|||||||
a.remove()
|
a.remove()
|
||||||
}
|
}
|
||||||
|
|
||||||
onDragleave () {
|
onDragleave = (e) => {
|
||||||
this.style.backgroundColor = null
|
const el = this.dropTarget(e)
|
||||||
|
|
||||||
if (this.classList.contains('before:border-base-content/30')) {
|
if (!el) return
|
||||||
this.classList.remove('before:border-base-content/30')
|
|
||||||
this.classList.add('before:border-base-300')
|
el.style.backgroundColor = null
|
||||||
} else if (this.classList.contains('border-base-content/30')) {
|
|
||||||
this.classList.remove('border-base-content/30')
|
if (el.classList.contains('before:border-base-content/30')) {
|
||||||
this.classList.add('border-base-300')
|
el.classList.remove('before:border-base-content/30')
|
||||||
|
el.classList.add('before:border-base-300')
|
||||||
|
} else if (el.classList.contains('border-base-content/30')) {
|
||||||
|
el.classList.remove('border-base-content/30')
|
||||||
|
el.classList.add('border-base-300')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
const input = this.querySelector('input')
|
||||||
|
const placeholder = this.querySelector('span')
|
||||||
|
|
||||||
|
input.addEventListener('change', () => {
|
||||||
|
placeholder.classList.toggle('hidden', !!input.value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,18 @@ export default class extends HTMLElement {
|
|||||||
static observedAttributes = ['class']
|
static observedAttributes = ['class']
|
||||||
|
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
|
this.addEventListener('turbo:morph-element', this.onMorph)
|
||||||
|
|
||||||
|
this.trigger()
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
this.removeEventListener('turbo:morph-element', this.onMorph)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMorph = (event) => {
|
||||||
|
if (event.target !== this) return
|
||||||
|
|
||||||
this.trigger()
|
this.trigger()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,16 @@ export default targetable(class extends HTMLElement {
|
|||||||
downloadFiles () {
|
downloadFiles () {
|
||||||
if (!this.dataset.src) return
|
if (!this.dataset.src) return
|
||||||
|
|
||||||
|
const download = window.webkit?.messageHandlers?.download
|
||||||
|
|
||||||
|
if (download) {
|
||||||
|
this.toggleState()
|
||||||
|
|
||||||
|
document.addEventListener('native:download-end', () => this.toggleState(), { once: true })
|
||||||
|
|
||||||
|
return download.postMessage({ src: this.dataset.src })
|
||||||
|
}
|
||||||
|
|
||||||
this.toggleState()
|
this.toggleState()
|
||||||
|
|
||||||
fetch(this.dataset.src).then(async (response) => {
|
fetch(this.dataset.src).then(async (response) => {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { actionable } from '@github/catalyst/lib/actionable'
|
|||||||
import { targets, targetable } from '@github/catalyst/lib/targetable'
|
import { targets, targetable } from '@github/catalyst/lib/targetable'
|
||||||
|
|
||||||
export default actionable(targetable(class extends HTMLElement {
|
export default actionable(targetable(class extends HTMLElement {
|
||||||
static [targets.static] = ['items']
|
static [targets.static] = ['items', 'groups']
|
||||||
|
|
||||||
addItem (e) {
|
addItem (e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -24,11 +24,28 @@ export default actionable(targetable(class extends HTMLElement {
|
|||||||
duplicateItem.querySelectorAll('a.hidden').forEach((button) => button.classList.toggle('hidden'))
|
duplicateItem.querySelectorAll('a.hidden').forEach((button) => button.classList.toggle('hidden'))
|
||||||
|
|
||||||
originalItem.parentNode.append(duplicateItem)
|
originalItem.parentNode.append(duplicateItem)
|
||||||
|
|
||||||
|
this.toggleMultipleClasses()
|
||||||
|
|
||||||
|
duplicateItem.querySelector("input:not([type='hidden'])")?.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
removeItem (e) {
|
removeItem (e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
this.items.find((item) => item.contains(e.target))?.remove()
|
this.items.find((item) => item.contains(e.target))?.remove()
|
||||||
|
|
||||||
|
this.toggleMultipleClasses()
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleMultipleClasses () {
|
||||||
|
const isMultiple = this.items.length > 1
|
||||||
|
const elements = [...this.items, ...this.groups]
|
||||||
|
|
||||||
|
elements.forEach((element) => {
|
||||||
|
const classes = element.dataset.multipleClasses || this.dataset.multipleClasses
|
||||||
|
|
||||||
|
classes?.split(' ').forEach((cls) => element.classList.toggle(cls, isMultiple))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
@@ -39,6 +39,8 @@ export default targetable(class extends HTMLElement {
|
|||||||
]
|
]
|
||||||
|
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
|
this.id = `email-editor-${Math.random().toString(32).split('.')[1]}`
|
||||||
|
|
||||||
this.mount()
|
this.mount()
|
||||||
|
|
||||||
if (this.input.value) {
|
if (this.input.value) {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export default class extends HTMLElement {
|
|||||||
counter.innerText = `${count} / ${this.dataset.limit}`
|
counter.innerText = `${count} / ${this.dataset.limit}`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.dataset.bulkEnabled !== 'true') {
|
if (this.dataset.bulkEnabled !== 'true' && !/Hotwire Native iOS/.test(navigator.userAgent)) {
|
||||||
if (!bulkMessage) {
|
if (!bulkMessage) {
|
||||||
bulkMessage = document.createElement('span')
|
bulkMessage = document.createElement('span')
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,15 @@ export default actionable(targetable(class extends HTMLElement {
|
|||||||
this.addEventListener('dragover', (e) => e.preventDefault())
|
this.addEventListener('dragover', (e) => e.preventDefault())
|
||||||
this.addEventListener('drop', this.onDrop)
|
this.addEventListener('drop', this.onDrop)
|
||||||
document.addEventListener('turbo:submit-end', this.toggleLoading)
|
document.addEventListener('turbo:submit-end', this.toggleLoading)
|
||||||
|
|
||||||
|
if (this.area) {
|
||||||
|
this.initArea()
|
||||||
|
} else {
|
||||||
|
queueMicrotask(() => this.initArea())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initArea () {
|
||||||
this.area?.addEventListener('dragover', this.onDragover)
|
this.area?.addEventListener('dragover', this.onDragover)
|
||||||
this.area?.addEventListener('dragleave', this.onDragleave)
|
this.area?.addEventListener('dragleave', this.onDragleave)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.addEventListener('turbo:morph-element', this.onMorph)
|
||||||
|
|
||||||
|
this.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
this.removeEventListener('turbo:morph-element', this.onMorph)
|
||||||
|
|
||||||
|
clearTimeout(this.closeTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMorph = (event) => {
|
||||||
|
if (event.target !== this) return
|
||||||
|
|
||||||
|
this.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
show () {
|
||||||
|
clearTimeout(this.closeTimeout)
|
||||||
|
|
||||||
|
this.hideAnimation?.cancel()
|
||||||
|
|
||||||
|
this.animate([
|
||||||
|
{ transform: 'translateY(-100%)', opacity: 0 },
|
||||||
|
{ transform: 'translateY(0)', opacity: 1 }
|
||||||
|
], { duration: 200, easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)' })
|
||||||
|
|
||||||
|
this.closeTimeout = setTimeout(this.close, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
close = () => {
|
||||||
|
clearTimeout(this.closeTimeout)
|
||||||
|
|
||||||
|
this.hideAnimation = this.animate([
|
||||||
|
{ transform: 'translateY(0)', opacity: 1 },
|
||||||
|
{ transform: 'translateY(-100%)', opacity: 0 }
|
||||||
|
], { duration: 150, easing: 'ease-in', fill: 'forwards' })
|
||||||
|
|
||||||
|
this.hideAnimation.onfinish = () => this.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.addEventListener('click', this.prompt)
|
||||||
|
document.addEventListener('turbo:submit-end', this.onSubmitEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
this.removeEventListener('click', this.prompt)
|
||||||
|
document.removeEventListener('turbo:submit-end', this.onSubmitEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
get form () {
|
||||||
|
return this.querySelector('form')
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt = () => {
|
||||||
|
const label = this.form.querySelector('label')
|
||||||
|
const input = this.form.querySelector(`#${CSS.escape(label.htmlFor)}`)
|
||||||
|
const value = window.prompt(label.textContent.trim(), input.value)?.trim()
|
||||||
|
|
||||||
|
if (!value || value === input.value) return
|
||||||
|
|
||||||
|
input.value = value
|
||||||
|
|
||||||
|
this.form.requestSubmit()
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmitEnd = (e) => {
|
||||||
|
if (e.detail.success && e.detail.formSubmission?.formElement === this.form && this.dataset.refresh !== 'false') {
|
||||||
|
window.Turbo.visit(window.location.href, { action: 'replace' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -353,7 +353,7 @@ function buildExtensions ({ Node, Mark, Extension, Plugin, Decoration, Decoratio
|
|||||||
|
|
||||||
const buildDecorations = (doc) => {
|
const buildDecorations = (doc) => {
|
||||||
const decorations = []
|
const decorations = []
|
||||||
const regex = /\{\{?[a-zA-Z0-9_.-]+\}\}?/g
|
const regex = /\{\{?[^{}\n]+\}\}?/g
|
||||||
|
|
||||||
doc.descendants((node, pos) => {
|
doc.descendants((node, pos) => {
|
||||||
if (!node.isText) return
|
if (!node.isText) return
|
||||||
@@ -458,6 +458,8 @@ export default actionable(targetable(class extends HTMLElement {
|
|||||||
async connectedCallback () {
|
async connectedCallback () {
|
||||||
if (!this.textarea || !this.editorElement) return
|
if (!this.textarea || !this.editorElement) return
|
||||||
|
|
||||||
|
this.id = `html-editor-${Math.random().toString(32).split('.')[1]}`
|
||||||
|
|
||||||
this.textarea.style.display = 'none'
|
this.textarea.style.display = 'none'
|
||||||
this.adjustShortcutsForPlatform()
|
this.adjustShortcutsForPlatform()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { renderStreamMessage, session } from '@hotwired/turbo'
|
||||||
|
|
||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
if (this.dataset.limit) this.replaceLimitParam()
|
||||||
|
|
||||||
|
if (!this.dataset.src) {
|
||||||
|
this.remove()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.observer = new IntersectionObserver((entries) => {
|
||||||
|
if (entries.some((entry) => entry.isIntersecting)) {
|
||||||
|
this.load()
|
||||||
|
}
|
||||||
|
}, { rootMargin: '400px' })
|
||||||
|
|
||||||
|
this.observer.observe(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
this.observer?.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceLimitParam () {
|
||||||
|
const url = new URL(window.location.href)
|
||||||
|
|
||||||
|
url.searchParams.set('limit', this.dataset.limit)
|
||||||
|
|
||||||
|
session.history.replace(url, session.history.restorationIdentifier)
|
||||||
|
session.view.lastRenderedLocation = url
|
||||||
|
}
|
||||||
|
|
||||||
|
load () {
|
||||||
|
if (this.loading || !this.dataset.src) return
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
fetch(this.dataset.src, {
|
||||||
|
headers: { 'X-Turbo-Infinite-Scroll': 'true' }
|
||||||
|
}).then(async (response) => {
|
||||||
|
if (!response.ok) return
|
||||||
|
|
||||||
|
delete this.dataset.src
|
||||||
|
|
||||||
|
renderStreamMessage(await response.text())
|
||||||
|
}).finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -143,6 +143,8 @@ export default actionable(targetable(class extends HTMLElement {
|
|||||||
async connectedCallback () {
|
async connectedCallback () {
|
||||||
if (!this.textarea || !this.editorElement) return
|
if (!this.textarea || !this.editorElement) return
|
||||||
|
|
||||||
|
this.id = `markdown-editor-${Math.random().toString(32).split('.')[1]}`
|
||||||
|
|
||||||
this.textarea.style.display = 'none'
|
this.textarea.style.display = 'none'
|
||||||
this.adjustShortcutsForPlatform()
|
this.adjustShortcutsForPlatform()
|
||||||
|
|
||||||
@@ -150,7 +152,7 @@ export default actionable(targetable(class extends HTMLElement {
|
|||||||
|
|
||||||
const buildDecorations = (doc) => {
|
const buildDecorations = (doc) => {
|
||||||
const decorations = []
|
const decorations = []
|
||||||
const regex = /\{\{?[a-zA-Z0-9_.-]+\}\}?/g
|
const regex = /\{\{?[^{}\n]+\}\}?/g
|
||||||
|
|
||||||
doc.descendants((node, pos) => {
|
doc.descendants((node, pos) => {
|
||||||
if (!node.isText) return
|
if (!node.isText) return
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
export default class extends HTMLElement {
|
export default class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
const maskedToken = this.input.value
|
|
||||||
|
|
||||||
this.input.addEventListener('focus', () => {
|
this.input.addEventListener('focus', () => {
|
||||||
|
this.maskedToken = this.input.value
|
||||||
this.input.value = this.dataset.token
|
this.input.value = this.dataset.token
|
||||||
this.input.select()
|
this.input.select()
|
||||||
})
|
})
|
||||||
|
|
||||||
this.input.addEventListener('focusout', () => {
|
this.input.addEventListener('focusout', () => {
|
||||||
this.input.value = maskedToken
|
this.input.value = this.maskedToken
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
export default class extends HTMLElement {
|
export default class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
|
this.id = `menu-active-${Math.random().toString(32).split('.')[1]}`
|
||||||
|
|
||||||
this.querySelectorAll('a').forEach((link) => {
|
this.querySelectorAll('a').forEach((link) => {
|
||||||
if (document.location.pathname.startsWith(link.pathname) && !link.getAttribute('href').startsWith('http')) {
|
if (document.location.pathname.startsWith(link.pathname) && !link.getAttribute('href').startsWith('http')) {
|
||||||
link.classList.add('bg-base-300')
|
link.classList.add('bg-base-300')
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { nativeIcon } from '../native'
|
||||||
|
|
||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
const bridge = window.webkit?.messageHandlers?.native
|
||||||
|
|
||||||
|
if (!bridge) return
|
||||||
|
|
||||||
|
const target = this.querySelector('a, button, [role="button"]') || this.firstElementChild
|
||||||
|
|
||||||
|
if (!target && !this.dataset.native) return
|
||||||
|
|
||||||
|
this.nativeId ||= `native-action-${Math.random().toString(32).split('.')[1]}`
|
||||||
|
this.id = this.nativeId
|
||||||
|
|
||||||
|
if (target) target.dataset.nativeId = this.nativeId
|
||||||
|
|
||||||
|
bridge.postMessage({
|
||||||
|
type: 'action',
|
||||||
|
op: 'add',
|
||||||
|
id: this.nativeId,
|
||||||
|
title: this.dataset.label || target?.textContent.trim(),
|
||||||
|
icon: nativeIcon(this.dataset.icon),
|
||||||
|
placement: this.dataset.placement || 'menu',
|
||||||
|
side: this.dataset.side,
|
||||||
|
view: this.dataset.view,
|
||||||
|
section: this.dataset.section,
|
||||||
|
menu: this.dataset.menu,
|
||||||
|
menuIcon: nativeIcon(this.dataset.menuIcon),
|
||||||
|
native: this.dataset.native,
|
||||||
|
url: this.dataset.url,
|
||||||
|
accept: this.dataset.accept,
|
||||||
|
destructive: this.dataset.destructive === 'true',
|
||||||
|
selected: this.dataset.selected === 'true',
|
||||||
|
haptic: this.dataset.haptic === 'true'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
if (this.nativeId) {
|
||||||
|
window.webkit?.messageHandlers?.native?.postMessage({ type: 'action', op: 'remove', id: this.nativeId })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
if (this.dataset.on === 'submit') {
|
||||||
|
document.addEventListener('turbo:submit-end', this.onSubmitEnd)
|
||||||
|
this.addEventListener('turbo:before-fetch-response', this.onBeforeFetchResponse)
|
||||||
|
} else {
|
||||||
|
this.addEventListener('click', this.dispatch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
document.removeEventListener('turbo:submit-end', this.onSubmitEnd)
|
||||||
|
this.removeEventListener('turbo:before-fetch-response', this.onBeforeFetchResponse)
|
||||||
|
this.removeEventListener('click', this.dispatch)
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeFetchResponse = (e) => {
|
||||||
|
const response = e.detail.fetchResponse
|
||||||
|
|
||||||
|
if (window.webkit?.messageHandlers?.modal && response.succeeded && !response.redirected && !response.isHTML) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmitEnd = async (e) => {
|
||||||
|
const form = e.detail.formSubmission?.formElement
|
||||||
|
|
||||||
|
if (!e.detail.success || !form || !this.contains(form)) return
|
||||||
|
|
||||||
|
this.post(JSON.stringify({ form: form.id, action: form.action, body: await e.detail.fetchResponse.responseText }))
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch = async () => {
|
||||||
|
if (this.dataset.loading) return
|
||||||
|
|
||||||
|
let detail = this.dataset.detail || 'null'
|
||||||
|
|
||||||
|
if (this.dataset.src) {
|
||||||
|
this.dataset.loading = 'true'
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(this.dataset.src, { headers: { Accept: 'application/json' } })
|
||||||
|
|
||||||
|
if (!response.ok) return
|
||||||
|
|
||||||
|
detail = await response.text()
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
} finally {
|
||||||
|
delete this.dataset.loading
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.post(detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
post (detail) {
|
||||||
|
window.webkit?.messageHandlers?.modal?.postMessage({ action: 'dispatch', name: this.dataset.name, detail, dismiss: this.dataset.dismiss || 'true' })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { nativeIcon } from '../native'
|
||||||
|
|
||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
if (window.webkit?.messageHandlers?.native) {
|
||||||
|
this.addEventListener('click', this.onClick)
|
||||||
|
} else {
|
||||||
|
document.addEventListener('pointerdown', this.onPointerDown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
this.removeEventListener('click', this.onClick)
|
||||||
|
document.removeEventListener('pointerdown', this.onPointerDown)
|
||||||
|
}
|
||||||
|
|
||||||
|
onPointerDown = (event) => {
|
||||||
|
if (this.contains(document.activeElement) && !this.contains(event.target)) {
|
||||||
|
document.activeElement.blur()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick = (event) => {
|
||||||
|
if (event.target.closest('native-menu-action')) return
|
||||||
|
|
||||||
|
const rect = this.firstElementChild.getBoundingClientRect()
|
||||||
|
|
||||||
|
const items = [...this.querySelectorAll('native-menu-action')].map((action) => {
|
||||||
|
const target = action.querySelector('a, button, [role="button"]') || action.firstElementChild
|
||||||
|
|
||||||
|
target.dataset.nativeId ||= `native-menu-${Math.random().toString(32).split('.')[1]}`
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: target.dataset.nativeId,
|
||||||
|
title: action.dataset.label || target.textContent.trim(),
|
||||||
|
icon: nativeIcon(action.dataset.icon),
|
||||||
|
destructive: action.dataset.destructive === 'true',
|
||||||
|
selected: action.dataset.selected === 'true',
|
||||||
|
haptic: action.dataset.haptic === 'true'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
window.webkit.messageHandlers.native.postMessage({
|
||||||
|
type: 'menu',
|
||||||
|
rect: { x: rect.left + window.scrollX, y: rect.top + window.scrollY, width: rect.width, height: rect.height },
|
||||||
|
items
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
document.addEventListener('turbo:submit-end', this.onSubmit)
|
||||||
|
this.addEventListener('click', this.onClick)
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
document.removeEventListener('turbo:submit-end', this.onSubmit)
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit = async (e) => {
|
||||||
|
if (!e.detail.success) return
|
||||||
|
if (e.detail.fetchResponse?.redirected && e.detail.fetchResponse.location.href !== window.location.href) return
|
||||||
|
if (e.detail.formSubmission?.formElement?.method === 'get') return
|
||||||
|
if (this.dataset.closeAfterSubmit === 'false') return
|
||||||
|
if (e.detail.formSubmission?.formElement?.dataset?.closeOnSubmit === 'false') return
|
||||||
|
if (e.detail.formSubmission?.formElement?.closest('native-event[data-on="submit"]')) return
|
||||||
|
|
||||||
|
const flash = await this.flashFrom(e.detail.fetchResponse)
|
||||||
|
|
||||||
|
window.webkit?.messageHandlers?.modal?.postMessage({ action: 'submitted', ...flash })
|
||||||
|
}
|
||||||
|
|
||||||
|
async flashFrom (fetchResponse) {
|
||||||
|
const html = await fetchResponse?.responseHTML.catch(() => null)
|
||||||
|
const flash = html && new DOMParser().parseFromString(html, 'text/html').getElementById('native_flash')
|
||||||
|
|
||||||
|
if (flash?.dataset.alert) return { alert: flash.dataset.alert }
|
||||||
|
if (flash?.dataset.notice) return { notice: flash.dataset.notice }
|
||||||
|
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (document.documentElement.scrollHeight > window.innerHeight) {
|
||||||
|
window.webkit?.messageHandlers?.modal?.postMessage({ action: 'expand' })
|
||||||
|
}
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
document.addEventListener('native:search', this.onSearch)
|
||||||
|
document.addEventListener('turbo:before-cache', this.onBeforeCache)
|
||||||
|
this.addEventListener('touchstart', this.onTouchStart, { passive: true })
|
||||||
|
this.addEventListener('scroll', this.onScroll, { capture: true, passive: true })
|
||||||
|
|
||||||
|
const scroller = this.querySelector('[data-scroll-top]')
|
||||||
|
|
||||||
|
if (scroller) scroller.scrollTop = scroller.dataset.scrollTop
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
document.removeEventListener('native:search', this.onSearch)
|
||||||
|
document.removeEventListener('turbo:before-cache', this.onBeforeCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeCache = () => {
|
||||||
|
if (this.scroller) this.scroller.dataset.scrollTop = this.scroller.scrollTop
|
||||||
|
}
|
||||||
|
|
||||||
|
onTouchStart = () => {
|
||||||
|
this.isTouching = true
|
||||||
|
}
|
||||||
|
|
||||||
|
onScroll = (event) => {
|
||||||
|
this.scroller = event.target
|
||||||
|
|
||||||
|
if (!this.isTouching) return
|
||||||
|
|
||||||
|
this.isTouching = false
|
||||||
|
|
||||||
|
window.webkit?.messageHandlers?.native?.postMessage({ type: 'dismiss-keyboard' })
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearch = (event) => {
|
||||||
|
const query = (event.detail?.q || '').trim()
|
||||||
|
|
||||||
|
if (query === (this.dataset.q || '')) return
|
||||||
|
|
||||||
|
this.dataset.q = query
|
||||||
|
|
||||||
|
const url = new URL(this.frame.src || this.dataset.src, window.location.href)
|
||||||
|
|
||||||
|
url.searchParams.delete('page')
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
url.searchParams.set('q', query)
|
||||||
|
} else {
|
||||||
|
url.searchParams.delete('q')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.frame.src = url.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
get frame () {
|
||||||
|
return this.querySelector('turbo-frame')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { convertImagesInInput } from './convert_upload'
|
||||||
|
|
||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.files = []
|
||||||
|
|
||||||
|
document.addEventListener('turbo:submit-end', this.onSubmitEnd)
|
||||||
|
document.addEventListener('native:upload-start', this.onUploadStart)
|
||||||
|
document.addEventListener('native:upload-chunk', this.onUploadChunk)
|
||||||
|
document.addEventListener('native:upload-submit', this.onUploadSubmit)
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
document.removeEventListener('turbo:submit-end', this.onSubmitEnd)
|
||||||
|
document.removeEventListener('native:upload-start', this.onUploadStart)
|
||||||
|
document.removeEventListener('native:upload-chunk', this.onUploadChunk)
|
||||||
|
document.removeEventListener('native:upload-submit', this.onUploadSubmit)
|
||||||
|
}
|
||||||
|
|
||||||
|
get form () {
|
||||||
|
return this.querySelector('form')
|
||||||
|
}
|
||||||
|
|
||||||
|
onUploadStart = (event) => {
|
||||||
|
this.files = []
|
||||||
|
|
||||||
|
event.detail.result = true
|
||||||
|
}
|
||||||
|
|
||||||
|
onUploadChunk = ({ detail: { index, name, type, chunk } }) => {
|
||||||
|
const file = this.files[index] ||= { name, type, chunks: [] }
|
||||||
|
|
||||||
|
file.chunks.push(Uint8Array.from(atob(chunk), (char) => char.charCodeAt(0)))
|
||||||
|
}
|
||||||
|
|
||||||
|
onUploadSubmit = (event) => {
|
||||||
|
event.detail.result = this.submit(event.detail.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit (url) {
|
||||||
|
const dataTransfer = new DataTransfer()
|
||||||
|
|
||||||
|
this.files.forEach(({ name, type, chunks }) => dataTransfer.items.add(new File(chunks, name, { type })))
|
||||||
|
this.files = []
|
||||||
|
|
||||||
|
const input = this.form.querySelector('[type="file"]')
|
||||||
|
|
||||||
|
input.files = dataTransfer.files
|
||||||
|
|
||||||
|
await convertImagesInInput(input)
|
||||||
|
|
||||||
|
this.form.action = url
|
||||||
|
this.form.requestSubmit()
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmitEnd = (e) => {
|
||||||
|
if (e.detail.formSubmission?.formElement !== this.form) return
|
||||||
|
|
||||||
|
window.webkit?.messageHandlers?.native?.postMessage({ type: 'upload', success: e.detail.success })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,17 @@
|
|||||||
export default class extends HTMLElement {
|
export default class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
|
this.addEventListener('pointerdown', this.onPointerdown)
|
||||||
|
}
|
||||||
|
|
||||||
|
onPointerdown = (event) => {
|
||||||
if (this.dataset.disabled === 'true') return
|
if (this.dataset.disabled === 'true') return
|
||||||
|
if (/Hotwire Native/.test(navigator.userAgent)) return
|
||||||
if (!this.isMobile()) return
|
if (!this.isMobile()) return
|
||||||
if (window.innerWidth >= 768) return
|
if (window.innerWidth >= 768) return
|
||||||
|
|
||||||
this.querySelectorAll('a[data-turbo-frame]').forEach((link) => {
|
const link = event.target.closest('a[data-turbo-frame]')
|
||||||
link.dataset.turboFrame = '_top'
|
|
||||||
})
|
if (link) link.dataset.turboFrame = '_top'
|
||||||
}
|
}
|
||||||
|
|
||||||
isMobile () {
|
isMobile () {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
export default class extends HTMLElement {
|
export default class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
|
this.id = `scroll-fade-${Math.random().toString(32).split('.')[1]}`
|
||||||
|
|
||||||
this.addEventListener('scroll', this.updateFade, { passive: true })
|
this.addEventListener('scroll', this.updateFade, { passive: true })
|
||||||
window.addEventListener('resize', this.updateFade)
|
window.addEventListener('resize', this.updateFade)
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,27 @@
|
|||||||
export default class extends HTMLElement {
|
export default class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.button.addEventListener('click', (event) => {
|
this.addEventListener('click', this.onClick)
|
||||||
|
|
||||||
|
document.addEventListener('turbo:before-cache', this.onBeforeCache)
|
||||||
|
|
||||||
|
if (this.input?.value) {
|
||||||
|
this.input.focus()
|
||||||
|
this.input.setSelectionRange(this.input.value.length, this.input.value.length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
document.removeEventListener('turbo:before-cache', this.onBeforeCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick = (event) => {
|
||||||
|
if (!event.target.closest('button')) return
|
||||||
|
|
||||||
if (!this.input.value && document.activeElement !== this.input) {
|
if (!this.input.value && document.activeElement !== this.input) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
this.input.focus()
|
this.input.focus()
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
document.addEventListener('turbo:before-cache', this.onBeforeCache)
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectedCallback () {
|
|
||||||
document.removeEventListener('turbo:before-cache', this.onBeforeCache)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeCache = () => {
|
onBeforeCache = () => {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
export default class extends HTMLElement {
|
export default class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
|
this.id = `set-timezone-${Math.random().toString(32).split('.')[1]}`
|
||||||
|
|
||||||
if (this.dataset.inputId) {
|
if (this.dataset.inputId) {
|
||||||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ export default targetable(class extends HTMLElement {
|
|||||||
static [target.static] = ['canvas', 'input', 'clear', 'button']
|
static [target.static] = ['canvas', 'input', 'clear', 'button']
|
||||||
|
|
||||||
async connectedCallback () {
|
async connectedCallback () {
|
||||||
|
this.lockSwipe(true)
|
||||||
|
|
||||||
const { default: SignaturePad } = await import('signature_pad')
|
const { default: SignaturePad } = await import('signature_pad')
|
||||||
|
|
||||||
this.setCanvasSize()
|
this.setCanvasSize()
|
||||||
@@ -61,11 +63,17 @@ export default targetable(class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback () {
|
disconnectedCallback () {
|
||||||
|
this.lockSwipe(false)
|
||||||
|
|
||||||
if (this.resizeObserver) {
|
if (this.resizeObserver) {
|
||||||
this.resizeObserver.disconnect()
|
this.resizeObserver.disconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lockSwipe (locked) {
|
||||||
|
window.webkit?.messageHandlers?.native?.postMessage({ type: 'swipe', op: locked ? 'lock' : 'unlock' })
|
||||||
|
}
|
||||||
|
|
||||||
setCanvasSize () {
|
setCanvasSize () {
|
||||||
const scale = 3
|
const scale = 3
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
export default class extends HTMLElement {
|
export default class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
|
if (this.lastElementChild) {
|
||||||
|
this.init()
|
||||||
|
} else {
|
||||||
|
queueMicrotask(() => this.init())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
if (this.interval) {
|
||||||
|
clearInterval(this.interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init () {
|
||||||
const form = this.querySelector('form') || (this.querySelector('input, button, select') || this.lastElementChild).form
|
const form = this.querySelector('form') || (this.querySelector('input, button, select') || this.lastElementChild).form
|
||||||
|
|
||||||
if (this.dataset.interval) {
|
if (this.dataset.interval) {
|
||||||
@@ -24,10 +38,4 @@ export default class extends HTMLElement {
|
|||||||
form.requestSubmit()
|
form.requestSubmit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback () {
|
|
||||||
if (this.interval) {
|
|
||||||
clearInterval(this.interval)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
export default class extends HTMLElement {
|
export default class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
|
this.addEventListener('click', this.onClick)
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick = (event) => {
|
||||||
const button = this.querySelector('a, button, label')
|
const button = this.querySelector('a, button, label')
|
||||||
|
|
||||||
|
if (!button?.contains(event.target)) return
|
||||||
|
|
||||||
const target = this.dataset.targetId ? document.getElementById(this.dataset.targetId) : button
|
const target = this.dataset.targetId ? document.getElementById(this.dataset.targetId) : button
|
||||||
|
|
||||||
button.addEventListener('click', () => {
|
|
||||||
this.dataset.classes.split(' ').forEach((cls) => {
|
this.dataset.classes.split(' ').forEach((cls) => {
|
||||||
if (this.dataset.action === 'remove') {
|
if (this.dataset.action === 'remove') {
|
||||||
target.classList.remove(cls)
|
target.classList.remove(cls)
|
||||||
@@ -14,6 +19,5 @@ export default class extends HTMLElement {
|
|||||||
target.classList.toggle(cls)
|
target.classList.toggle(cls)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.form.addEventListener('change', this.toggle)
|
||||||
|
|
||||||
|
this.toggle()
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
this.form.removeEventListener('change', this.toggle)
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle = () => {
|
||||||
|
const selector = this.dataset.names.split(' ').map((name) => `[name="${name}"]:checked:enabled`).join(', ')
|
||||||
|
const isChecked = !!this.form.querySelector(selector)
|
||||||
|
|
||||||
|
this.firstElementChild.classList.toggle('hidden', !isChecked)
|
||||||
|
this.lastElementChild.classList.toggle('hidden', isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
get form () {
|
||||||
|
return this.closest('form')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,29 +1,23 @@
|
|||||||
export default class extends HTMLElement {
|
export default class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
document.addEventListener('turbo:submit-end', this.onSubmitEnd)
|
|
||||||
|
|
||||||
this.form.addEventListener('submit', this.onSubmit)
|
this.form.addEventListener('submit', this.onSubmit)
|
||||||
|
this.form.addEventListener('turbo:before-fetch-response', this.onFetchResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback () {
|
disconnectedCallback () {
|
||||||
document.removeEventListener('turbo:submit-end', this.onSubmitEnd)
|
|
||||||
|
|
||||||
this.form.removeEventListener('submit', this.onSubmit)
|
this.form.removeEventListener('submit', this.onSubmit)
|
||||||
|
this.form.removeEventListener('turbo:before-fetch-response', this.onFetchResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit = () => {
|
onSubmit = () => {
|
||||||
this.element.classList.add('invisible')
|
this.element.classList.add('invisible')
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmitEnd = (event) => {
|
onFetchResponse = (event) => {
|
||||||
if (event.target === this.form) {
|
if (event.detail.fetchResponse.succeeded) {
|
||||||
const resp = event.detail?.formSubmission?.result?.fetchResponse?.response
|
|
||||||
|
|
||||||
if (resp?.status / 100 === 2) {
|
|
||||||
this.element.classList.remove('invisible')
|
this.element.classList.remove('invisible')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
get element () {
|
get element () {
|
||||||
return document.getElementById(this.dataset.elementId)
|
return document.getElementById(this.dataset.elementId)
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.events.forEach((event) => this.form.addEventListener(event, this.toggle))
|
||||||
|
|
||||||
|
this.toggle()
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
this.events.forEach((event) => this.form.removeEventListener(event, this.toggle))
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle = (event) => {
|
||||||
|
if (event?.type === 'input' && event.target.matches(this.fieldSelector('name'))) return
|
||||||
|
|
||||||
|
if (event?.type === 'focusin' && !this.isKeepSendUsed && this.isFocusFromName(event)) {
|
||||||
|
this.isKeepSendUsed = true
|
||||||
|
this.keepSendInput = event.target
|
||||||
|
} else if (event?.type === 'focusout' && event.target === this.keepSendInput) {
|
||||||
|
this.keepSendInput = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNameOnly = (field) => this.isFilled('name') && !this.isFilled(field) && !this.keepSendInput?.matches(this.fieldSelector(field))
|
||||||
|
|
||||||
|
const isSend = (this.isChecked('send_email') && !isNameOnly('email')) ||
|
||||||
|
(this.isChecked('send_sms') && !isNameOnly('phone'))
|
||||||
|
|
||||||
|
this.firstElementChild.classList.toggle('hidden', !isSend)
|
||||||
|
this.lastElementChild.classList.toggle('hidden', isSend)
|
||||||
|
}
|
||||||
|
|
||||||
|
isChecked (name) {
|
||||||
|
return !!this.form.querySelector(`[name="${name}"]:checked:enabled`)
|
||||||
|
}
|
||||||
|
|
||||||
|
isFilled (field) {
|
||||||
|
return [...this.form.querySelectorAll(this.fieldSelector(field))].some((input) => input.value.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
isFocusFromName ({ target, relatedTarget }) {
|
||||||
|
return !!relatedTarget?.matches(this.fieldSelector('name')) && !!relatedTarget.value.trim() &&
|
||||||
|
['email', 'phone'].some((field) => target.matches(this.fieldSelector(field)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldSelector (field) {
|
||||||
|
return `[name$="[][${field}]"]`
|
||||||
|
}
|
||||||
|
|
||||||
|
get events () {
|
||||||
|
return ['change', 'input', 'linked-input.update', 'focusin', 'focusout']
|
||||||
|
}
|
||||||
|
|
||||||
|
get form () {
|
||||||
|
return this.closest('form')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import './native'
|
||||||
|
|
||||||
import { createApp, reactive } from 'vue'
|
import { createApp, reactive } from 'vue'
|
||||||
|
|
||||||
import Form from './submission_form/form'
|
import Form from './submission_form/form'
|
||||||
@@ -8,6 +10,7 @@ import ScrollButtons from './elements/scroll_buttons'
|
|||||||
import PageContainer from './elements/page_container'
|
import PageContainer from './elements/page_container'
|
||||||
import SubmitForm from './elements/submit_form'
|
import SubmitForm from './elements/submit_form'
|
||||||
import ModalButton from './elements/modal_button'
|
import ModalButton from './elements/modal_button'
|
||||||
|
import NativeAction from './elements/native_action'
|
||||||
|
|
||||||
const safeRegisterElement = (name, element, options = {}) => !window.customElements.get(name) && window.customElements.define(name, element, options)
|
const safeRegisterElement = (name, element, options = {}) => !window.customElements.get(name) && window.customElements.define(name, element, options)
|
||||||
|
|
||||||
@@ -18,6 +21,7 @@ safeRegisterElement('scroll-buttons', ScrollButtons)
|
|||||||
safeRegisterElement('page-container', PageContainer)
|
safeRegisterElement('page-container', PageContainer)
|
||||||
safeRegisterElement('submit-form', SubmitForm)
|
safeRegisterElement('submit-form', SubmitForm)
|
||||||
safeRegisterElement('modal-button', ModalButton)
|
safeRegisterElement('modal-button', ModalButton)
|
||||||
|
safeRegisterElement('native-action', NativeAction)
|
||||||
safeRegisterElement('submission-form', class extends HTMLElement {
|
safeRegisterElement('submission-form', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.appElem = document.createElement('div')
|
this.appElem = document.createElement('div')
|
||||||
@@ -28,7 +32,9 @@ safeRegisterElement('submission-form', class extends HTMLElement {
|
|||||||
optionalInviteSubmitters: JSON.parse(this.dataset.optionalInviteSubmitters),
|
optionalInviteSubmitters: JSON.parse(this.dataset.optionalInviteSubmitters),
|
||||||
schema: JSON.parse(this.dataset.schema),
|
schema: JSON.parse(this.dataset.schema),
|
||||||
canSendEmail: this.dataset.canSendEmail === 'true',
|
canSendEmail: this.dataset.canSendEmail === 'true',
|
||||||
previousSignatureValue: this.dataset.previousSignatureValue,
|
prefillSignature: this.dataset.prefillSignature ? JSON.parse(this.dataset.prefillSignature) : null,
|
||||||
|
prefillInitials: this.dataset.prefillInitials ? JSON.parse(this.dataset.prefillInitials) : null,
|
||||||
|
currentUser: this.dataset.currentUser ? JSON.parse(this.dataset.currentUser) : null,
|
||||||
goToLast: this.dataset.goToLast === 'true',
|
goToLast: this.dataset.goToLast === 'true',
|
||||||
isDemo: this.dataset.isDemo === 'true',
|
isDemo: this.dataset.isDemo === 'true',
|
||||||
attribution: this.dataset.attribution !== 'false',
|
attribution: this.dataset.attribution !== 'false',
|
||||||
@@ -37,10 +43,12 @@ safeRegisterElement('submission-form', class extends HTMLElement {
|
|||||||
language: this.dataset.language,
|
language: this.dataset.language,
|
||||||
paymentProvider: this.dataset.paymentProvider,
|
paymentProvider: this.dataset.paymentProvider,
|
||||||
dryRun: this.dataset.dryRun === 'true',
|
dryRun: this.dataset.dryRun === 'true',
|
||||||
|
viewToken: this.dataset.viewToken,
|
||||||
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 === 'true',
|
withConfetti: this.dataset.withConfetti === 'true',
|
||||||
|
nativePlatform: this.dataset.nativePlatform,
|
||||||
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',
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
const requestCache = new Map()
|
|
||||||
const cacheTtl = 10 * 1000
|
|
||||||
|
|
||||||
function isPreloadable (linkElement) {
|
|
||||||
const href = linkElement.getAttribute('href')
|
|
||||||
|
|
||||||
if (!href || href === '#' || linkElement.dataset.turbo === 'false' || linkElement.dataset.prefetch === 'false') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (linkElement.origin !== document.location.origin) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!['http:', 'https:'].includes(linkElement.protocol)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (linkElement.pathname + linkElement.search === document.location.pathname + document.location.search) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (linkElement.dataset.turboMethod && linkElement.dataset.turboMethod !== 'get') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
function mouseoverListener (event) {
|
|
||||||
let linkElement
|
|
||||||
|
|
||||||
if (event.target.tagName === 'A') {
|
|
||||||
linkElement = event.target
|
|
||||||
} else {
|
|
||||||
linkElement = event.target.closest('a')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!linkElement) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isPreloadable(linkElement)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = linkElement.getAttribute('href')
|
|
||||||
const loc = new URL(url, location.protocol + '//' + location.host)
|
|
||||||
const absoluteUrl = loc.toString()
|
|
||||||
|
|
||||||
const cached = requestCache.get(absoluteUrl)
|
|
||||||
|
|
||||||
if (cached && cached.ttl > new Date()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestOptions = {
|
|
||||||
credentials: 'same-origin',
|
|
||||||
headers: { Accept: 'text/html, application/xhtml+xml', 'VND.PREFETCH': 'true', 'X-Turbo': 'true' },
|
|
||||||
method: 'GET',
|
|
||||||
redirect: 'follow'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (linkElement.dataset.turboFrame && linkElement.dataset.turboFrame !== '_top') {
|
|
||||||
requestOptions.headers['Turbo-Frame'] = linkElement.dataset.turboFrame
|
|
||||||
} else if (linkElement.dataset.turboFrame !== '_top') {
|
|
||||||
const turboFrame = linkElement.closest('turbo-frame')
|
|
||||||
|
|
||||||
if (turboFrame) {
|
|
||||||
requestOptions.headers['Turbo-Frame'] = turboFrame.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
requestCache.set(absoluteUrl, { request: fetch(absoluteUrl, requestOptions), ttl: new Date(new Date().getTime() + cacheTtl) })
|
|
||||||
}
|
|
||||||
|
|
||||||
function turboBeforeFetchRequest (event) {
|
|
||||||
if (event.target.tagName !== 'FORM' && event.detail.fetchOptions.method === 'GET') {
|
|
||||||
const cached = requestCache.get(event.detail.url.toString())
|
|
||||||
|
|
||||||
if (cached && cached.ttl > new Date()) {
|
|
||||||
event.detail.response = cached.request
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
requestCache.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
function start () {
|
|
||||||
if (!window.turboInstantClickEnabled) {
|
|
||||||
document.addEventListener('turbo:before-fetch-request', turboBeforeFetchRequest)
|
|
||||||
document.addEventListener('mouseover', mouseoverListener, { capture: true, passive: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
window.turboInstantClickEnabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
export { start }
|
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
const ICONS = {
|
||||||
|
ios: {
|
||||||
|
link: 'link',
|
||||||
|
archive: 'archivebox',
|
||||||
|
folder: 'folder',
|
||||||
|
trash: 'trash',
|
||||||
|
rotate: 'arrow.counterclockwise',
|
||||||
|
copy: 'doc.on.doc',
|
||||||
|
pencil: 'pencil',
|
||||||
|
logs: 'list.bullet.rectangle',
|
||||||
|
external_link: 'arrow.up.right.square',
|
||||||
|
download: 'arrow.down.circle',
|
||||||
|
mail_forward: 'paperplane',
|
||||||
|
send: 'paperplane',
|
||||||
|
writing: 'signature',
|
||||||
|
adjustments_horizontal: 'slider.horizontal.3',
|
||||||
|
file_text: 'doc.text',
|
||||||
|
files: 'doc.on.doc',
|
||||||
|
plus: 'plus',
|
||||||
|
eye: 'eye',
|
||||||
|
check: 'checkmark',
|
||||||
|
code: 'chevron.left.forwardslash.chevron.right',
|
||||||
|
history: 'clock.arrow.circlepath',
|
||||||
|
user: 'person',
|
||||||
|
users: 'person.2',
|
||||||
|
users_plus: 'person.badge.plus',
|
||||||
|
user_plus: 'person.badge.plus',
|
||||||
|
eye_plus: 'eye',
|
||||||
|
mail_plus: 'envelope',
|
||||||
|
photo: 'photo',
|
||||||
|
settings: 'gearshape',
|
||||||
|
logout: 'rectangle.portrait.and.arrow.right',
|
||||||
|
upload: 'arrow.up.doc',
|
||||||
|
scan: 'doc.viewfinder',
|
||||||
|
pencil_plus: 'square.and.pencil',
|
||||||
|
arrow_sort: 'arrow.up.arrow.down',
|
||||||
|
sort_descending_numbers: 'calendar',
|
||||||
|
sort_descending_small_big: 'clock.arrow.circlepath',
|
||||||
|
sort_ascending_letters: 'textformat.abc',
|
||||||
|
calendar: 'calendar',
|
||||||
|
calendar_check: 'calendar.badge.checkmark',
|
||||||
|
info_circle: 'info.circle',
|
||||||
|
list: 'list.bullet',
|
||||||
|
clock: 'clock',
|
||||||
|
circle_check: 'checkmark.circle'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const platform = navigator.userAgent.includes('Hotwire Native Android') ? 'android' : 'ios'
|
||||||
|
|
||||||
|
export function nativeIcon (name) {
|
||||||
|
return ICONS[platform]?.[name] || name
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlers = window.webkit?.messageHandlers
|
||||||
|
const isNativeLoaded = window.isNativeLoaded
|
||||||
|
|
||||||
|
if (handlers?.native && window === window.top && !isNativeLoaded) {
|
||||||
|
window.isNativeLoaded = true
|
||||||
|
|
||||||
|
handlers.native.postMessage({ type: 'reset' })
|
||||||
|
|
||||||
|
const reportPathConfiguration = () => {
|
||||||
|
const element = document.getElementById('native_path_configuration')
|
||||||
|
|
||||||
|
if (!element) return
|
||||||
|
|
||||||
|
const config = JSON.parse(element.textContent)
|
||||||
|
|
||||||
|
config.rules.forEach(({ properties }) => {
|
||||||
|
if (properties.native_actions) properties.native_actions = properties.native_actions.map(nativeIcon)
|
||||||
|
})
|
||||||
|
|
||||||
|
handlers.native.postMessage({ type: 'path-configuration', json: JSON.stringify(config) })
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('turbo:load', reportPathConfiguration)
|
||||||
|
|
||||||
|
document.addEventListener('turbo:before-cache', () => {
|
||||||
|
document.getElementById('native_path_configuration')?.remove()
|
||||||
|
})
|
||||||
|
|
||||||
|
reportPathConfiguration()
|
||||||
|
|
||||||
|
const reportFlash = (element) => {
|
||||||
|
if (!element) return
|
||||||
|
|
||||||
|
if (element.dataset.notice) handlers.flash.postMessage({ style: 'notice', message: element.dataset.notice })
|
||||||
|
if (element.dataset.alert) handlers.flash.postMessage({ style: 'alert', message: element.dataset.alert })
|
||||||
|
|
||||||
|
element.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
const reportPageFlash = () => reportFlash(document.getElementById('native_flash'))
|
||||||
|
|
||||||
|
new MutationObserver(reportPageFlash).observe(document.documentElement, { childList: true, subtree: true })
|
||||||
|
|
||||||
|
document.addEventListener('turbo:load', reportPageFlash)
|
||||||
|
|
||||||
|
document.addEventListener('native:flash', (event) => {
|
||||||
|
reportFlash(new DOMParser().parseFromString(event.detail.html, 'text/html').getElementById('native_flash'))
|
||||||
|
})
|
||||||
|
|
||||||
|
reportPageFlash()
|
||||||
|
|
||||||
|
document.addEventListener('click', (event) => {
|
||||||
|
const link = event.target.closest('a[href]')
|
||||||
|
|
||||||
|
if (!link) return
|
||||||
|
|
||||||
|
const action = link.closest('native-action, native-menu-action')
|
||||||
|
const title = link.dataset.nativeTitle || link.getAttribute('aria-label') || link.closest('[data-native-title]')?.dataset.nativeTitle || action?.dataset.label || action?.textContent.replace(/\s+/g, ' ').trim()
|
||||||
|
|
||||||
|
if (!title) return
|
||||||
|
|
||||||
|
const actions = link.closest('[data-native-actions]')?.dataset.nativeActions
|
||||||
|
|
||||||
|
handlers.native.postMessage({ type: 'title', url: link.href, title, actions: actions ? actions.split(',').map(nativeIcon) : null })
|
||||||
|
}, true)
|
||||||
|
|
||||||
|
document.addEventListener('click', (event) => {
|
||||||
|
if (document.querySelector('native-modal')) return
|
||||||
|
|
||||||
|
const link = event.target.closest('a[data-turbo-frame="modal"], a[data-turbo-frame="drawer"]')
|
||||||
|
|
||||||
|
if (!link?.href) return
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
handlers.modal.postMessage({ action: 'open', url: link.href, modal: link.closest('[data-native-modal]')?.dataset.nativeModal || '' })
|
||||||
|
}, true)
|
||||||
|
|
||||||
|
let isTouchPrefetch = false
|
||||||
|
|
||||||
|
document.addEventListener('touchstart', (event) => {
|
||||||
|
isTouchPrefetch = true
|
||||||
|
event.target.closest('a[href]')?.dispatchEvent(new MouseEvent('mouseenter'))
|
||||||
|
isTouchPrefetch = false
|
||||||
|
}, { passive: true })
|
||||||
|
|
||||||
|
document.addEventListener('turbo:before-prefetch', (event) => {
|
||||||
|
if (!isTouchPrefetch || event.target.closest('a[data-turbo-frame="modal"], a[data-turbo-frame="drawer"]')) {
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener('native:action', (event) => {
|
||||||
|
document.querySelector(`[data-native-id="${event.detail.id}"]`)?.click()
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener('native:title-bottom', (event) => {
|
||||||
|
const rect = document.querySelector('h1, h2')?.getBoundingClientRect()
|
||||||
|
|
||||||
|
event.detail.result = rect?.height ? rect.bottom - document.documentElement.getBoundingClientRect().top : null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handlers?.native && window.Turbo && !isNativeLoaded) {
|
||||||
|
const staleSnapshots = new Set()
|
||||||
|
let isRestoring = false
|
||||||
|
|
||||||
|
window.Turbo.session.view.clearSnapshotCache = function () {
|
||||||
|
this.snapshotCache.keys.forEach((key) => staleSnapshots.add(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
window.Turbo.session.navigator.linkPrefetchingIsEnabledForLocation = () => !document.querySelector('native-modal')
|
||||||
|
|
||||||
|
document.addEventListener('turbo:submit-end', (e) => {
|
||||||
|
if (e.detail.success && !e.detail.formSubmission.isSafe) {
|
||||||
|
handlers.native.postMessage({ type: 'stale' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener('native:pull-to-refresh', (event) => {
|
||||||
|
const { session } = window.Turbo
|
||||||
|
const url = new URL(document.baseURI)
|
||||||
|
|
||||||
|
url.searchParams.delete('limit')
|
||||||
|
|
||||||
|
session.history.replace(url, session.history.restorationIdentifier)
|
||||||
|
session.view.lastRenderedLocation = url
|
||||||
|
|
||||||
|
event.detail.result = new Promise((resolve) => {
|
||||||
|
document.addEventListener('turbo:render', () => resolve(true), { once: true })
|
||||||
|
document.addEventListener('turbo:fetch-request-error', () => resolve(true), { once: true })
|
||||||
|
|
||||||
|
session.refresh(document.baseURI)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener('native:refresh', () => {
|
||||||
|
window.Turbo.session.refresh(document.baseURI)
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener('native:turbo-stream', (e) => {
|
||||||
|
if (!e.detail.body?.trimStart().startsWith('<turbo-stream')) return
|
||||||
|
|
||||||
|
window.Turbo.renderStreamMessage(e.detail.body)
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener('turbo:visit', (e) => {
|
||||||
|
isRestoring = e.detail.action === 'restore'
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener('turbo:load', () => {
|
||||||
|
if (staleSnapshots.delete(window.location.href.split('#')[0]) && isRestoring && window.location.pathname !== '/search') {
|
||||||
|
setTimeout(() => window.Turbo.session.refresh(document.baseURI))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -125,6 +125,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
fieldsUuidIndex: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
values: {
|
values: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -195,7 +200,7 @@ export default {
|
|||||||
},
|
},
|
||||||
isMobileContainer: FieldAreas.computed.isMobileContainer,
|
isMobileContainer: FieldAreas.computed.isMobileContainer,
|
||||||
isInlineSize: FieldAreas.computed.isInlineSize,
|
isInlineSize: FieldAreas.computed.isInlineSize,
|
||||||
fieldsUuidIndex () {
|
formulaFieldsUuidIndex () {
|
||||||
return this.formulaFields.reduce((acc, field) => {
|
return this.formulaFields.reduce((acc, field) => {
|
||||||
acc[field.uuid] = field
|
acc[field.uuid] = field
|
||||||
|
|
||||||
@@ -289,6 +294,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
normalizeFormula: FormulaAreas.methods.normalizeFormula,
|
normalizeFormula: FormulaAreas.methods.normalizeFormula,
|
||||||
calculateFormula: FormulaAreas.methods.calculateFormula,
|
calculateFormula: FormulaAreas.methods.calculateFormula,
|
||||||
|
dateFormulaValue: FormulaAreas.methods.dateFormulaValue,
|
||||||
evalTextFormula: FormulaAreas.methods.evalTextFormula,
|
evalTextFormula: FormulaAreas.methods.evalTextFormula,
|
||||||
scrollInContainer: FieldAreas.methods.scrollInContainer,
|
scrollInContainer: FieldAreas.methods.scrollInContainer,
|
||||||
scrollIntoArea: FieldAreas.methods.scrollIntoArea,
|
scrollIntoArea: FieldAreas.methods.scrollIntoArea,
|
||||||
|
|||||||
@@ -633,6 +633,32 @@ defineFunction('contains', {
|
|||||||
return String(v[0]).indexOf(String(v[1])) !== -1
|
return String(v[0]).indexOf(String(v[1])) !== -1
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
defineFunction('today', {
|
||||||
|
min: 0,
|
||||||
|
max: 0,
|
||||||
|
call: () => {
|
||||||
|
const now = new Date()
|
||||||
|
return Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()) / 86400000
|
||||||
|
}
|
||||||
|
})
|
||||||
|
defineFunction('datedif', {
|
||||||
|
min: 3,
|
||||||
|
max: 3,
|
||||||
|
call: (v) => {
|
||||||
|
if (v[0] == null || v[1] == null) return null
|
||||||
|
const start = toNumber(v[0])
|
||||||
|
const end = toNumber(v[1])
|
||||||
|
const unit = String(v[2]).toLowerCase()
|
||||||
|
if (unit === 'd') return end - start
|
||||||
|
const from = new Date(Math.min(start, end) * 86400000)
|
||||||
|
const to = new Date(Math.max(start, end) * 86400000)
|
||||||
|
const months = (to.getUTCFullYear() - from.getUTCFullYear()) * 12 + to.getUTCMonth() - from.getUTCMonth() - (to.getUTCDate() < from.getUTCDate() ? 1 : 0)
|
||||||
|
const sign = end < start ? -1 : 1
|
||||||
|
if (unit === 'm') return sign * months
|
||||||
|
if (unit === 'y') return sign * Math.floor(months / 12)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const OP_CLASSES = {
|
const OP_CLASSES = {
|
||||||
add: Addition,
|
add: Addition,
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="!isWebView && withDownloadButton"
|
v-if="(!isWebView || isNativeApp) && withDownloadButton"
|
||||||
class="base-button flex items-center space-x-1 w-full completed-form-download-button"
|
class="base-button flex items-center space-x-1 w-full completed-form-download-button"
|
||||||
:disabled="isDownloading"
|
:disabled="isDownloading"
|
||||||
@click.prevent="download"
|
@click.prevent="download"
|
||||||
@@ -186,6 +186,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
|
},
|
||||||
|
nativePlatform: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
@@ -195,6 +200,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isNativeApp () {
|
||||||
|
return !!this.nativePlatform && !!window.webkit?.messageHandlers?.download
|
||||||
|
},
|
||||||
isWebView () {
|
isWebView () {
|
||||||
return /webview|wv|ip((?!.*Safari)|(?=.*like Safari))/i.test(window.navigator.userAgent)
|
return /webview|wv|ip((?!.*Safari)|(?=.*like Safari))/i.test(window.navigator.userAgent)
|
||||||
}
|
}
|
||||||
@@ -230,6 +238,12 @@ export default {
|
|||||||
download () {
|
download () {
|
||||||
this.isDownloading = true
|
this.isDownloading = true
|
||||||
|
|
||||||
|
if (this.isNativeApp) {
|
||||||
|
document.addEventListener('native:download-end', () => { this.isDownloading = false }, { once: true })
|
||||||
|
|
||||||
|
return window.webkit.messageHandlers.download.postMessage({ src: this.baseUrl + `/s/${this.submitterSlug}/documents` })
|
||||||
|
}
|
||||||
|
|
||||||
fetch(this.baseUrl + `/s/${this.submitterSlug}/documents`, {
|
fetch(this.baseUrl + `/s/${this.submitterSlug}/documents`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
...this.fetchOptions
|
...this.fetchOptions
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export default {
|
|||||||
IconCloudUpload,
|
IconCloudUpload,
|
||||||
IconInnerShadowTop
|
IconInnerShadowTop
|
||||||
},
|
},
|
||||||
inject: ['baseUrl', 't'],
|
inject: ['baseUrl', 'fetchOptions', 't'],
|
||||||
props: {
|
props: {
|
||||||
message: {
|
message: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -197,9 +197,20 @@ export default {
|
|||||||
formData.append('submitter_slug', this.submitterSlug)
|
formData.append('submitter_slug', this.submitterSlug)
|
||||||
formData.append('name', 'attachments')
|
formData.append('name', 'attachments')
|
||||||
|
|
||||||
|
return fetch(this.baseUrl + `/s/${this.submitterSlug}/upload`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { ...this.fetchOptions.headers },
|
||||||
|
body: formData
|
||||||
|
}).then((resp) => {
|
||||||
|
if (resp.status === 404) {
|
||||||
return fetch(this.baseUrl + '/api/attachments', {
|
return fetch(this.baseUrl + '/api/attachments', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
headers: { ...this.fetchOptions.headers },
|
||||||
body: formData
|
body: formData
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return resp
|
||||||
|
}
|
||||||
}).then(async (resp) => {
|
}).then(async (resp) => {
|
||||||
const data = await resp.json()
|
const data = await resp.json()
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
:readonly-conditional-fields="readonlyConditionalFields"
|
:readonly-conditional-fields="readonlyConditionalFields"
|
||||||
:readonly-conditional-field-values="readonlyConditionalFieldValues"
|
:readonly-conditional-field-values="readonlyConditionalFieldValues"
|
||||||
:formula-fields="formulaFields"
|
:formula-fields="formulaFields"
|
||||||
|
:fields-uuid-index="fieldsUuidIndex"
|
||||||
:values="values"
|
:values="values"
|
||||||
:readonly-values="readonlyFieldValues"
|
:readonly-values="readonlyFieldValues"
|
||||||
:submitter="submitter"
|
:submitter="submitter"
|
||||||
@@ -71,6 +72,7 @@
|
|||||||
<FormulaFieldAreas
|
<FormulaFieldAreas
|
||||||
v-if="!withAccessibilityAreas && !isAccessibilityMode && formulaFields.length"
|
v-if="!withAccessibilityAreas && !isAccessibilityMode && formulaFields.length"
|
||||||
:fields="formulaFields"
|
:fields="formulaFields"
|
||||||
|
:fields-uuid-index="fieldsUuidIndex"
|
||||||
:readonly-values="readonlyFieldValues"
|
:readonly-values="readonlyFieldValues"
|
||||||
:values="values"
|
:values="values"
|
||||||
/>
|
/>
|
||||||
@@ -489,8 +491,8 @@
|
|||||||
:reason="values[currentField.preferences?.reason_field_uuid]"
|
:reason="values[currentField.preferences?.reason_field_uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
:values="values"
|
:values="values"
|
||||||
:previous-value="previousSignatureValueFor(currentField) || previousSignatureValue"
|
:previous-value="previousSignatureValueFor(currentField)"
|
||||||
:touch-attachment-uuid="previousSignatureValue"
|
:prefill-data="prefillSignature"
|
||||||
:with-typed-signature="withTypedSignature"
|
:with-typed-signature="withTypedSignature"
|
||||||
:remember-signature="rememberSignature"
|
:remember-signature="rememberSignature"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
@@ -503,8 +505,8 @@
|
|||||||
:with-qr-button="withQrButton"
|
:with-qr-button="withQrButton"
|
||||||
:submitter="submitter"
|
:submitter="submitter"
|
||||||
:show-field-names="showFieldNames"
|
:show-field-names="showFieldNames"
|
||||||
|
:native-platform="nativePlatform"
|
||||||
@update:reason="values[currentField.preferences?.reason_field_uuid] = $event"
|
@update:reason="values[currentField.preferences?.reason_field_uuid] = $event"
|
||||||
@touch-attachment="attachmentsIndex[previousSignatureValue] ? attachmentsIndex[previousSignatureValue].created_at = new Date() : null"
|
|
||||||
@attached="attachments.push($event)"
|
@attached="attachments.push($event)"
|
||||||
@start="scrollIntoField(currentField)"
|
@start="scrollIntoField(currentField)"
|
||||||
@minimize="minimizeForm"
|
@minimize="minimizeForm"
|
||||||
@@ -518,9 +520,11 @@
|
|||||||
:dry-run="dryRun"
|
:dry-run="dryRun"
|
||||||
:submitter="submitter"
|
:submitter="submitter"
|
||||||
:previous-value="previousInitialsValue"
|
:previous-value="previousInitialsValue"
|
||||||
|
:prefill-data="prefillInitials"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
:show-field-names="showFieldNames"
|
:show-field-names="showFieldNames"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
|
:native-platform="nativePlatform"
|
||||||
@attached="attachments.push($event)"
|
@attached="attachments.push($event)"
|
||||||
@start="scrollIntoField(currentField)"
|
@start="scrollIntoField(currentField)"
|
||||||
@focus="scrollIntoField(currentField)"
|
@focus="scrollIntoField(currentField)"
|
||||||
@@ -558,6 +562,7 @@
|
|||||||
:field="currentField"
|
:field="currentField"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
:fields="formulaFields"
|
:fields="formulaFields"
|
||||||
|
:fields-uuid-index="fieldsUuidIndex"
|
||||||
:values="values"
|
:values="values"
|
||||||
:readonly-values="readonlyFieldValues"
|
:readonly-values="readonlyFieldValues"
|
||||||
:fetch-options="fetchOptions"
|
:fetch-options="fetchOptions"
|
||||||
@@ -652,6 +657,7 @@
|
|||||||
:with-confetti="withConfetti"
|
:with-confetti="withConfetti"
|
||||||
:can-send-email="canSendEmail && !!submitter.email"
|
:can-send-email="canSendEmail && !!submitter.email"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
|
:native-platform="nativePlatform"
|
||||||
/>
|
/>
|
||||||
<nav
|
<nav
|
||||||
v-if="stepFields.length < 80"
|
v-if="stepFields.length < 80"
|
||||||
@@ -772,6 +778,7 @@ export default {
|
|||||||
provide () {
|
provide () {
|
||||||
return {
|
return {
|
||||||
baseUrl: this.baseUrl,
|
baseUrl: this.baseUrl,
|
||||||
|
fetchOptions: this.fetchOptions,
|
||||||
scrollIntoArea: this.scrollIntoArea,
|
scrollIntoArea: this.scrollIntoArea,
|
||||||
scrollIntoField: this.scrollIntoField,
|
scrollIntoField: this.scrollIntoField,
|
||||||
t: this.t
|
t: this.t
|
||||||
@@ -909,6 +916,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
nativePlatform: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
autoscrollFields: {
|
autoscrollFields: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -969,10 +981,20 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
previousSignatureValue: {
|
prefillSignature: {
|
||||||
type: String,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
default: ''
|
default: null
|
||||||
|
},
|
||||||
|
prefillInitials: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
currentUser: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
},
|
},
|
||||||
allowToSkip: {
|
allowToSkip: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -994,6 +1016,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
viewToken: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
attribution: {
|
attribution: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -1329,6 +1356,30 @@ export default {
|
|||||||
|
|
||||||
screen?.orientation?.addEventListener('change', this.onOrientationChange)
|
screen?.orientation?.addEventListener('change', this.onOrientationChange)
|
||||||
|
|
||||||
|
if (this.currentUser) {
|
||||||
|
this.fields.forEach((field) => {
|
||||||
|
if (field.readonly || field.type !== 'text') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldName = (field.name || '').toLowerCase()
|
||||||
|
|
||||||
|
let value
|
||||||
|
|
||||||
|
if (fieldName === 'full name' || fieldName === 'legal name') {
|
||||||
|
value = this.currentUser.full_name
|
||||||
|
} else if (fieldName === 'first name') {
|
||||||
|
value = this.currentUser.first_name
|
||||||
|
} else if (fieldName === 'last name') {
|
||||||
|
value = this.currentUser.last_name
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
this.values[field.uuid] ??= value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
this.fields.forEach((field) => {
|
this.fields.forEach((field) => {
|
||||||
if (field.default_value && !field.readonly) {
|
if (field.default_value && !field.readonly) {
|
||||||
this.values[field.uuid] ||= field.default_value
|
this.values[field.uuid] ||= field.default_value
|
||||||
@@ -1523,15 +1574,10 @@ export default {
|
|||||||
const newUrl = [window.location.pathname, queryParams.toString()].filter(Boolean).join('?')
|
const newUrl = [window.location.pathname, queryParams.toString()].filter(Boolean).join('?')
|
||||||
window.history.replaceState({}, document.title, newUrl)
|
window.history.replaceState({}, document.title, newUrl)
|
||||||
|
|
||||||
return fetch(this.baseUrl + '/api/submitter_email_clicks', {
|
return fetch(this.baseUrl + `/s/${this.submitterSlug}/click_email`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers },
|
||||||
'Content-Type': 'application/json'
|
body: JSON.stringify({ t })
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
t,
|
|
||||||
submitter_slug: this.submitterSlug
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
@@ -1547,29 +1593,28 @@ export default {
|
|||||||
const newUrl = [window.location.pathname, queryParams.toString()].filter(Boolean).join('?')
|
const newUrl = [window.location.pathname, queryParams.toString()].filter(Boolean).join('?')
|
||||||
window.history.replaceState({}, document.title, newUrl)
|
window.history.replaceState({}, document.title, newUrl)
|
||||||
|
|
||||||
return fetch(this.baseUrl + '/api/submitter_sms_clicks', {
|
return fetch(this.baseUrl + `/s/${this.submitterSlug}/click_sms`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers },
|
||||||
'Content-Type': 'application/json'
|
body: JSON.stringify({ c })
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
c,
|
|
||||||
submitter_slug: this.submitterSlug
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
trackViewForm () {
|
trackViewForm () {
|
||||||
fetch(this.baseUrl + '/api/submitter_form_views', {
|
fetch(this.baseUrl + `/s/${this.submitterSlug}/view`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
'Content-Type': 'application/json'
|
body: JSON.stringify({ v: this.viewToken })
|
||||||
},
|
}).then((resp) => {
|
||||||
body: JSON.stringify({
|
if (resp.status === 404) {
|
||||||
submitter_slug: this.submitterSlug
|
return fetch(this.baseUrl + '/api/submitter_form_views', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ submitter_slug: this.submitterSlug })
|
||||||
})
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
previousSignatureValueFor (field) {
|
previousSignatureValueFor (field) {
|
||||||
@@ -1767,6 +1812,10 @@ export default {
|
|||||||
this.isCompleted = true
|
this.isCompleted = true
|
||||||
this.isFormVisible = true
|
this.isFormVisible = true
|
||||||
|
|
||||||
|
if (this.nativePlatform) {
|
||||||
|
window.webkit?.messageHandlers?.native?.postMessage({ type: 'stale' })
|
||||||
|
}
|
||||||
|
|
||||||
if (resp?.text) {
|
if (resp?.text) {
|
||||||
const respData = await resp.text()
|
const respData = await resp.text()
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
|
},
|
||||||
|
fieldsUuidIndex: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
@@ -59,7 +64,7 @@ export default {
|
|||||||
isInlineSize () {
|
isInlineSize () {
|
||||||
return CSS.supports('container-type: size')
|
return CSS.supports('container-type: size')
|
||||||
},
|
},
|
||||||
fieldsUuidIndex () {
|
formulaFieldsUuidIndex () {
|
||||||
return this.fields.reduce((acc, field) => {
|
return this.fields.reduce((acc, field) => {
|
||||||
acc[field.uuid] = field
|
acc[field.uuid] = field
|
||||||
|
|
||||||
@@ -82,8 +87,8 @@ export default {
|
|||||||
if (depth > 10) return formula
|
if (depth > 10) return formula
|
||||||
|
|
||||||
return formula.replace(/{{(.*?)}}/g, (match, uuid) => {
|
return formula.replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||||
if (this.fieldsUuidIndex[uuid]) {
|
if (this.formulaFieldsUuidIndex[uuid]) {
|
||||||
return `(${this.normalizeFormula(this.fieldsUuidIndex[uuid].preferences.formula, depth + 1)})`
|
return `(${this.normalizeFormula(this.formulaFieldsUuidIndex[uuid].preferences.formula, depth + 1)})`
|
||||||
} else {
|
} else {
|
||||||
return match
|
return match
|
||||||
}
|
}
|
||||||
@@ -91,16 +96,36 @@ export default {
|
|||||||
},
|
},
|
||||||
calculateFormula (field) {
|
calculateFormula (field) {
|
||||||
const transformedFormula = this.normalizeFormula(field.preferences.formula).replace(/{{(.*?)}}/g, (match, uuid) => {
|
const transformedFormula = this.normalizeFormula(field.preferences.formula).replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||||
return this.readonlyValues[uuid] || this.values[uuid] || 0.0
|
const value = this.readonlyValues[uuid] || this.values[uuid]
|
||||||
|
|
||||||
|
if (this.fieldsUuidIndex[uuid]?.type === 'date') {
|
||||||
|
return this.dateFormulaValue(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value || 0.0
|
||||||
})
|
})
|
||||||
|
|
||||||
return this.math.evaluate(transformedFormula.toLowerCase())
|
const value = this.math.evaluate(transformedFormula.toLowerCase())
|
||||||
|
|
||||||
|
if (field.type === 'date') {
|
||||||
|
return Number.isFinite(value) ? new Date(Math.floor(value) * 86400000).toISOString().slice(0, 10) : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
},
|
||||||
|
dateFormulaValue (value) {
|
||||||
|
if (!value) return 'null'
|
||||||
|
if (value === '{{date}}') return 'today()'
|
||||||
|
|
||||||
|
const [year, month, day = 1] = value.slice(0, 10).split('-').map(Number)
|
||||||
|
|
||||||
|
return Date.UTC(year, month - 1, day) / 86400000
|
||||||
},
|
},
|
||||||
evalTextFormula (field, depth = 0) {
|
evalTextFormula (field, depth = 0) {
|
||||||
if (depth > 10) return ''
|
if (depth > 10) return ''
|
||||||
|
|
||||||
return field.preferences.formula.replace(/{{(.*?)}}/g, (match, uuid) => {
|
return field.preferences.formula.replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||||
const formulaField = this.fieldsUuidIndex[uuid]
|
const formulaField = this.formulaFieldsUuidIndex[uuid]
|
||||||
|
|
||||||
if (formulaField?.preferences?.formula) {
|
if (formulaField?.preferences?.formula) {
|
||||||
if (formulaField.type === 'text') {
|
if (formulaField.type === 'text') {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const en = {
|
|||||||
must_be_valid_email: 'Must be a valid email address',
|
must_be_valid_email: 'Must be a valid email address',
|
||||||
must_be_valid_url: 'Must be a valid URL (starting with http:// or https://)',
|
must_be_valid_url: 'Must be a valid URL (starting with http:// or https://)',
|
||||||
must_be_valid_zip: 'Must be a valid ZIP code (XXXXX or XXXXX-XXXX)',
|
must_be_valid_zip: 'Must be a valid ZIP code (XXXXX or XXXXX-XXXX)',
|
||||||
|
must_be_valid_iban: 'Must be a valid IBAN',
|
||||||
must_contain_numbers_only: 'Must contain numbers only',
|
must_contain_numbers_only: 'Must contain numbers only',
|
||||||
must_contain_letters_only: 'Must contain letters only',
|
must_contain_letters_only: 'Must contain letters only',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.',
|
||||||
@@ -132,6 +133,7 @@ const es = {
|
|||||||
must_be_valid_email: 'Debe ser una dirección de correo electrónico válida',
|
must_be_valid_email: 'Debe ser una dirección de correo electrónico válida',
|
||||||
must_be_valid_url: 'Debe ser una URL válida (que comience con http:// o https://)',
|
must_be_valid_url: 'Debe ser una URL válida (que comience con http:// o https://)',
|
||||||
must_be_valid_zip: 'Debe ser un ZIP válido (XXXXX o XXXXX-XXXX)',
|
must_be_valid_zip: 'Debe ser un ZIP válido (XXXXX o XXXXX-XXXX)',
|
||||||
|
must_be_valid_iban: 'Debe ser un IBAN válido',
|
||||||
must_contain_numbers_only: 'Debe contener solo números',
|
must_contain_numbers_only: 'Debe contener solo números',
|
||||||
must_contain_letters_only: 'Debe contener solo letras',
|
must_contain_letters_only: 'Debe contener solo letras',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete todos los campos requeridos para continuar con la verificación de identidad.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete todos los campos requeridos para continuar con la verificación de identidad.',
|
||||||
@@ -251,6 +253,7 @@ const it = {
|
|||||||
must_be_valid_email: 'Deve essere un indirizzo email valido',
|
must_be_valid_email: 'Deve essere un indirizzo email valido',
|
||||||
must_be_valid_url: 'Deve essere un URL valido (che inizia con http:// o https://)',
|
must_be_valid_url: 'Deve essere un URL valido (che inizia con http:// o https://)',
|
||||||
must_be_valid_zip: 'Deve essere uno ZIP valido (XXXXX o XXXXX-XXXX)',
|
must_be_valid_zip: 'Deve essere uno ZIP valido (XXXXX o XXXXX-XXXX)',
|
||||||
|
must_be_valid_iban: 'Deve essere un IBAN valido',
|
||||||
must_contain_numbers_only: 'Deve contenere solo numeri',
|
must_contain_numbers_only: 'Deve contenere solo numeri',
|
||||||
must_contain_letters_only: 'Deve contenere solo lettere',
|
must_contain_letters_only: 'Deve contenere solo lettere',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: "Compila tutti i campi obbligatori per procedere con la verifica dell'identità.",
|
complete_all_required_fields_to_proceed_with_identity_verification: "Compila tutti i campi obbligatori per procedere con la verifica dell'identità.",
|
||||||
@@ -370,6 +373,7 @@ const de = {
|
|||||||
must_be_valid_email: 'Muss eine gültige E-Mail-Adresse sein',
|
must_be_valid_email: 'Muss eine gültige E-Mail-Adresse sein',
|
||||||
must_be_valid_url: 'Muss eine gültige URL sein (beginnend mit http:// oder https://)',
|
must_be_valid_url: 'Muss eine gültige URL sein (beginnend mit http:// oder https://)',
|
||||||
must_be_valid_zip: 'Muss eine gültige ZIP sein (XXXXX oder XXXXX-XXXX)',
|
must_be_valid_zip: 'Muss eine gültige ZIP sein (XXXXX oder XXXXX-XXXX)',
|
||||||
|
must_be_valid_iban: 'Muss eine gültige IBAN sein',
|
||||||
must_contain_numbers_only: 'Darf nur Zahlen enthalten',
|
must_contain_numbers_only: 'Darf nur Zahlen enthalten',
|
||||||
must_contain_letters_only: 'Darf nur Buchstaben enthalten',
|
must_contain_letters_only: 'Darf nur Buchstaben enthalten',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Füllen Sie alle Pflichtfelder aus, um mit der Identitätsprüfung fortzufahren.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Füllen Sie alle Pflichtfelder aus, um mit der Identitätsprüfung fortzufahren.',
|
||||||
@@ -489,6 +493,7 @@ const fr = {
|
|||||||
must_be_valid_email: 'Doit être une adresse e-mail valide',
|
must_be_valid_email: 'Doit être une adresse e-mail valide',
|
||||||
must_be_valid_url: 'Doit être une URL valide (commençant par http:// ou https://)',
|
must_be_valid_url: 'Doit être une URL valide (commençant par http:// ou https://)',
|
||||||
must_be_valid_zip: 'Doit être un ZIP valide (XXXXX ou XXXXX-XXXX)',
|
must_be_valid_zip: 'Doit être un ZIP valide (XXXXX ou XXXXX-XXXX)',
|
||||||
|
must_be_valid_iban: 'Doit être un IBAN valide',
|
||||||
must_contain_numbers_only: 'Doit contenir uniquement des chiffres',
|
must_contain_numbers_only: 'Doit contenir uniquement des chiffres',
|
||||||
must_contain_letters_only: 'Doit contenir uniquement des lettres',
|
must_contain_letters_only: 'Doit contenir uniquement des lettres',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour poursuivre la vérification d'identité.",
|
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour poursuivre la vérification d'identité.",
|
||||||
@@ -608,6 +613,7 @@ const pl = {
|
|||||||
must_be_valid_email: 'Musi być prawidłowym adresem e-mail',
|
must_be_valid_email: 'Musi być prawidłowym adresem e-mail',
|
||||||
must_be_valid_url: 'Musi być prawidłowym adresem URL (zaczynającym się od http:// lub https://)',
|
must_be_valid_url: 'Musi być prawidłowym adresem URL (zaczynającym się od http:// lub https://)',
|
||||||
must_be_valid_zip: 'Musi być prawidłowym ZIP (XXXXX lub XXXXX-XXXX)',
|
must_be_valid_zip: 'Musi być prawidłowym ZIP (XXXXX lub XXXXX-XXXX)',
|
||||||
|
must_be_valid_iban: 'Musi być prawidłowym numerem IBAN',
|
||||||
must_contain_numbers_only: 'Może zawierać tylko cyfry',
|
must_contain_numbers_only: 'Może zawierać tylko cyfry',
|
||||||
must_contain_letters_only: 'Może zawierać tylko litery',
|
must_contain_letters_only: 'Może zawierać tylko litery',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Uzupełnij wszystkie wymagane pola, aby kontynuować weryfikację tożsamości.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Uzupełnij wszystkie wymagane pola, aby kontynuować weryfikację tożsamości.',
|
||||||
@@ -727,6 +733,7 @@ const uk = {
|
|||||||
must_be_valid_email: 'Має бути дійсною адресою електронної пошти',
|
must_be_valid_email: 'Має бути дійсною адресою електронної пошти',
|
||||||
must_be_valid_url: 'Має бути дійсним URL (починаючи з http:// або https://)',
|
must_be_valid_url: 'Має бути дійсним URL (починаючи з http:// або https://)',
|
||||||
must_be_valid_zip: 'Має бути дійсним ZIP (XXXXX або XXXXX-XXXX)',
|
must_be_valid_zip: 'Має бути дійсним ZIP (XXXXX або XXXXX-XXXX)',
|
||||||
|
must_be_valid_iban: 'Має бути дійсним IBAN',
|
||||||
must_contain_numbers_only: 'Має містити лише цифри',
|
must_contain_numbers_only: 'Має містити лише цифри',
|
||||||
must_contain_letters_only: 'Має містити лише літери',
|
must_contain_letters_only: 'Має містити лише літери',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: "Заповніть всі обов'язкові поля, щоб продовжити перевірку особи.",
|
complete_all_required_fields_to_proceed_with_identity_verification: "Заповніть всі обов'язкові поля, щоб продовжити перевірку особи.",
|
||||||
@@ -846,6 +853,7 @@ const cs = {
|
|||||||
must_be_valid_email: 'Musí být platná e-mailová adresa',
|
must_be_valid_email: 'Musí být platná e-mailová adresa',
|
||||||
must_be_valid_url: 'Musí být platná URL (začínající http:// nebo https://)',
|
must_be_valid_url: 'Musí být platná URL (začínající http:// nebo https://)',
|
||||||
must_be_valid_zip: 'Musí být platný ZIP (XXXXX nebo XXXXX-XXXX)',
|
must_be_valid_zip: 'Musí být platný ZIP (XXXXX nebo XXXXX-XXXX)',
|
||||||
|
must_be_valid_iban: 'Musí být platný IBAN',
|
||||||
must_contain_numbers_only: 'Musí obsahovat pouze čísla',
|
must_contain_numbers_only: 'Musí obsahovat pouze čísla',
|
||||||
must_contain_letters_only: 'Musí obsahovat pouze písmena',
|
must_contain_letters_only: 'Musí obsahovat pouze písmena',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Vyplňte všechna povinná pole, abyste mohli pokračovat v ověření identity.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Vyplňte všechna povinná pole, abyste mohli pokračovat v ověření identity.',
|
||||||
@@ -965,6 +973,7 @@ const pt = {
|
|||||||
must_be_valid_email: 'Deve ser um endereço de e-mail válido',
|
must_be_valid_email: 'Deve ser um endereço de e-mail válido',
|
||||||
must_be_valid_url: 'Deve ser uma URL válida (começando com http:// ou https://)',
|
must_be_valid_url: 'Deve ser uma URL válida (começando com http:// ou https://)',
|
||||||
must_be_valid_zip: 'Deve ser um ZIP válido (XXXXX ou XXXXX-XXXX)',
|
must_be_valid_zip: 'Deve ser um ZIP válido (XXXXX ou XXXXX-XXXX)',
|
||||||
|
must_be_valid_iban: 'Deve ser um IBAN válido',
|
||||||
must_contain_numbers_only: 'Deve conter apenas números',
|
must_contain_numbers_only: 'Deve conter apenas números',
|
||||||
must_contain_letters_only: 'Deve conter apenas letras',
|
must_contain_letters_only: 'Deve conter apenas letras',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Preencha todos os campos obrigatórios para prosseguir com a verificação de identidade.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Preencha todos os campos obrigatórios para prosseguir com a verificação de identidade.',
|
||||||
@@ -1084,6 +1093,7 @@ const he = {
|
|||||||
must_be_valid_email: 'חייבת להיות כתובת אימייל חוקית',
|
must_be_valid_email: 'חייבת להיות כתובת אימייל חוקית',
|
||||||
must_be_valid_url: 'חייבת להיות כתובת URL חוקית (המתחילה ב-http:// או https://)',
|
must_be_valid_url: 'חייבת להיות כתובת URL חוקית (המתחילה ב-http:// או https://)',
|
||||||
must_be_valid_zip: 'חייב להיות ZIP חוקי (XXXXX או XXXXX-XXXX)',
|
must_be_valid_zip: 'חייב להיות ZIP חוקי (XXXXX או XXXXX-XXXX)',
|
||||||
|
must_be_valid_iban: 'חייב להיות IBAN חוקי',
|
||||||
must_contain_numbers_only: 'חייב להכיל מספרים בלבד',
|
must_contain_numbers_only: 'חייב להכיל מספרים בלבד',
|
||||||
must_contain_letters_only: 'חייב להכיל אותיות בלבד',
|
must_contain_letters_only: 'חייב להכיל אותיות בלבד',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'מלא את כל השדות הנדרשים כדי להמשיך עם אימות זהות.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'מלא את כל השדות הנדרשים כדי להמשיך עם אימות זהות.',
|
||||||
@@ -1203,6 +1213,7 @@ const nl = {
|
|||||||
must_be_valid_email: 'Moet een geldig e-mailadres zijn',
|
must_be_valid_email: 'Moet een geldig e-mailadres zijn',
|
||||||
must_be_valid_url: 'Moet een geldige URL zijn (beginnend met http:// of https://)',
|
must_be_valid_url: 'Moet een geldige URL zijn (beginnend met http:// of https://)',
|
||||||
must_be_valid_zip: 'Moet een geldige ZIP zijn (XXXXX of XXXXX-XXXX)',
|
must_be_valid_zip: 'Moet een geldige ZIP zijn (XXXXX of XXXXX-XXXX)',
|
||||||
|
must_be_valid_iban: 'Moet een geldig IBAN zijn',
|
||||||
must_contain_numbers_only: 'Mag alleen cijfers bevatten',
|
must_contain_numbers_only: 'Mag alleen cijfers bevatten',
|
||||||
must_contain_letters_only: 'Mag alleen letters bevatten',
|
must_contain_letters_only: 'Mag alleen letters bevatten',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Vul alle verplichte velden in om door te gaan met de identiteitsverificatie.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Vul alle verplichte velden in om door te gaan met de identiteitsverificatie.',
|
||||||
@@ -1322,6 +1333,7 @@ const ar = {
|
|||||||
must_be_valid_email: 'يجب أن يكون عنوان بريد إلكتروني صالحًا',
|
must_be_valid_email: 'يجب أن يكون عنوان بريد إلكتروني صالحًا',
|
||||||
must_be_valid_url: 'يجب أن يكون عنوان URL صالحًا (يبدأ بـ http:// أو https://)',
|
must_be_valid_url: 'يجب أن يكون عنوان URL صالحًا (يبدأ بـ http:// أو https://)',
|
||||||
must_be_valid_zip: 'يجب أن يكون ZIP صالحًا (XXXXX أو XXXXX-XXXX)',
|
must_be_valid_zip: 'يجب أن يكون ZIP صالحًا (XXXXX أو XXXXX-XXXX)',
|
||||||
|
must_be_valid_iban: 'يجب أن يكون IBAN صالحًا',
|
||||||
must_contain_numbers_only: 'يجب أن يحتوي على أرقام فقط',
|
must_contain_numbers_only: 'يجب أن يحتوي على أرقام فقط',
|
||||||
must_contain_letters_only: 'يجب أن يحتوي على أحرف فقط',
|
must_contain_letters_only: 'يجب أن يحتوي على أحرف فقط',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'أكمل جميع الحقول المطلوبة للمتابعة في التحقق من الهوية.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'أكمل جميع الحقول المطلوبة للمتابعة في التحقق من الهوية.',
|
||||||
@@ -1441,6 +1453,7 @@ const ko = {
|
|||||||
must_be_valid_email: '유효한 이메일 주소여야 합니다',
|
must_be_valid_email: '유효한 이메일 주소여야 합니다',
|
||||||
must_be_valid_url: '유효한 URL이어야 합니다 (http:// 또는 https://로 시작)',
|
must_be_valid_url: '유효한 URL이어야 합니다 (http:// 또는 https://로 시작)',
|
||||||
must_be_valid_zip: '유효한 ZIP이어야 합니다 (XXXXX 또는 XXXXX-XXXX)',
|
must_be_valid_zip: '유효한 ZIP이어야 합니다 (XXXXX 또는 XXXXX-XXXX)',
|
||||||
|
must_be_valid_iban: '유효한 IBAN이어야 합니다',
|
||||||
must_contain_numbers_only: '숫자만 포함해야 합니다',
|
must_contain_numbers_only: '숫자만 포함해야 합니다',
|
||||||
must_contain_letters_only: '문자만 포함해야 합니다',
|
must_contain_letters_only: '문자만 포함해야 합니다',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: '신원 확인을 진행하려면 모든 필수 필드를 작성하십시오.',
|
complete_all_required_fields_to_proceed_with_identity_verification: '신원 확인을 진행하려면 모든 필수 필드를 작성하십시오.',
|
||||||
@@ -1560,6 +1573,7 @@ const ja = {
|
|||||||
must_be_valid_email: '有効なメールアドレスである必要があります',
|
must_be_valid_email: '有効なメールアドレスである必要があります',
|
||||||
must_be_valid_url: '有効なURLである必要があります (http://またはhttps://で始まる)',
|
must_be_valid_url: '有効なURLである必要があります (http://またはhttps://で始まる)',
|
||||||
must_be_valid_zip: '有効なZIPである必要があります (XXXXX または XXXXX-XXXX)',
|
must_be_valid_zip: '有効なZIPである必要があります (XXXXX または XXXXX-XXXX)',
|
||||||
|
must_be_valid_iban: '有効なIBANである必要があります',
|
||||||
must_contain_numbers_only: '数字のみを含む必要があります',
|
must_contain_numbers_only: '数字のみを含む必要があります',
|
||||||
must_contain_letters_only: '文字のみを含む必要があります',
|
must_contain_letters_only: '文字のみを含む必要があります',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: '本人確認を進めるには、すべての必須項目を入力してください。',
|
complete_all_required_fields_to_proceed_with_identity_verification: '本人確認を進めるには、すべての必須項目を入力してください。',
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
v-if="modelValue || computedPreviousValue"
|
v-if="modelValue || computedPreviousValue || computedPrefillData"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn font-medium btn-outline btn-sm clear-canvas-button"
|
class="btn font-medium btn-outline btn-sm clear-canvas-button"
|
||||||
@click="remove"
|
@click="remove"
|
||||||
@@ -139,8 +139,8 @@
|
|||||||
:name="`values[${field.uuid}]`"
|
:name="`values[${field.uuid}]`"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="modelValue || computedPreviousValue"
|
v-if="modelValue || computedPreviousValue || computedPrefillData"
|
||||||
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
|
:src="previousValueUrl"
|
||||||
:alt="field.name || t('initials')"
|
:alt="field.name || t('initials')"
|
||||||
class="mx-auto bg-white border border-base-300 rounded max-h-44"
|
class="mx-auto bg-white border border-base-300 rounded max-h-44"
|
||||||
>
|
>
|
||||||
@@ -150,20 +150,23 @@
|
|||||||
class="absolute top-0 right-0 left-0 bottom-0"
|
class="absolute top-0 right-0 left-0 bottom-0"
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
|
v-if="!isDrawInitials && !modelValue && !computedPreviousValue && !computedPrefillData"
|
||||||
for="initials_text_input"
|
for="initials_text_input"
|
||||||
class="absolute top-0 right-0 left-0 bottom-0"
|
class="absolute top-0 right-0 left-0 bottom-0"
|
||||||
/>
|
/>
|
||||||
<canvas
|
<canvas
|
||||||
v-show="!modelValue && !computedPreviousValue"
|
v-show="!modelValue && !computedPreviousValue && !computedPrefillData"
|
||||||
ref="canvas"
|
ref="canvas"
|
||||||
role="img"
|
role="img"
|
||||||
:aria-label="t('initials_drawing_area')"
|
:aria-label="t('initials_drawing_area')"
|
||||||
class="bg-white border border-base-300 rounded-2xl w-full draw-canvas"
|
class="bg-white border border-base-300 rounded-2xl w-full draw-canvas"
|
||||||
|
@touchstart.passive="lockSwipe"
|
||||||
|
@touchend.passive="unlockSwipe"
|
||||||
|
@touchcancel.passive="unlockSwipe"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
|
v-if="!isDrawInitials && !modelValue && !computedPreviousValue && !computedPrefillData"
|
||||||
id="initials_text_input"
|
id="initials_text_input"
|
||||||
ref="textInput"
|
ref="textInput"
|
||||||
class="base-input !text-2xl w-full mt-6 text-center"
|
class="base-input !text-2xl w-full mt-6 text-center"
|
||||||
@@ -199,7 +202,7 @@ export default {
|
|||||||
MarkdownContent,
|
MarkdownContent,
|
||||||
IconArrowsDiagonalMinimize2
|
IconArrowsDiagonalMinimize2
|
||||||
},
|
},
|
||||||
inject: ['baseUrl', 't'],
|
inject: ['baseUrl', 'fetchOptions', 't'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -233,6 +236,16 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
prefillData: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
nativePlatform: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -255,10 +268,24 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
computedPrefillData () {
|
||||||
|
if (this.isUsePreviousValue && this.field.required === true && !this.previousValue && !this.modelValue) {
|
||||||
|
return this.prefillData
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
previousValueUrl () {
|
||||||
|
if (this.computedPrefillData) {
|
||||||
|
return this.computedPrefillData.url
|
||||||
|
} else {
|
||||||
|
return this.attachmentsIndex[this.modelValue || this.computedPreviousValue].url
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
this.isInitialsStarted = !!this.computedPreviousValue
|
this.isInitialsStarted = !!(this.computedPreviousValue || this.computedPrefillData)
|
||||||
},
|
},
|
||||||
async mounted () {
|
async mounted () {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@@ -311,6 +338,8 @@ export default {
|
|||||||
this.intersectionObserver?.disconnect()
|
this.intersectionObserver?.disconnect()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
lockSwipe: SignatureStep.methods.lockSwipe,
|
||||||
|
unlockSwipe: SignatureStep.methods.unlockSwipe,
|
||||||
drawOnCanvas: SignatureStep.methods.drawOnCanvas,
|
drawOnCanvas: SignatureStep.methods.drawOnCanvas,
|
||||||
drawImage (event) {
|
drawImage (event) {
|
||||||
this.remove()
|
this.remove()
|
||||||
@@ -397,7 +426,48 @@ export default {
|
|||||||
this.updateWrittenInitials({ target: this.$refs.textInput })
|
this.updateWrittenInitials({ target: this.$refs.textInput })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
submitPrefillAttachment () {
|
||||||
|
if (this.dryRun) {
|
||||||
|
const attachment = { uuid: Math.random().toString(), url: this.prefillData.url }
|
||||||
|
|
||||||
|
this.$emit('attached', attachment)
|
||||||
|
this.$emit('update:model-value', attachment.uuid)
|
||||||
|
|
||||||
|
return Promise.resolve(attachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
formData.append('prefill_token', this.prefillData.token)
|
||||||
|
|
||||||
|
return fetch(this.baseUrl + `/s/${this.submitterSlug}/prefill_attachments`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { ...this.fetchOptions.headers },
|
||||||
|
body: formData
|
||||||
|
}).then(async (resp) => {
|
||||||
|
if (!resp.ok) {
|
||||||
|
const data = await resp.json()
|
||||||
|
|
||||||
|
return Promise.reject(new Error(data.error))
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachment = await resp.json()
|
||||||
|
|
||||||
|
this.$emit('attached', attachment)
|
||||||
|
this.$emit('update:model-value', attachment.uuid)
|
||||||
|
|
||||||
|
return attachment
|
||||||
|
}).catch((error) => {
|
||||||
|
this.remove()
|
||||||
|
|
||||||
|
return Promise.reject(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
async submit () {
|
async submit () {
|
||||||
|
if (this.computedPrefillData) {
|
||||||
|
return this.submitPrefillAttachment()
|
||||||
|
}
|
||||||
|
|
||||||
if (this.modelValue || this.computedPreviousValue) {
|
if (this.modelValue || this.computedPreviousValue) {
|
||||||
if (this.computedPreviousValue) {
|
if (this.computedPreviousValue) {
|
||||||
this.$emit('update:model-value', this.computedPreviousValue)
|
this.$emit('update:model-value', this.computedPreviousValue)
|
||||||
@@ -431,9 +501,20 @@ export default {
|
|||||||
formData.append('name', 'attachments')
|
formData.append('name', 'attachments')
|
||||||
formData.append('type', 'initials')
|
formData.append('type', 'initials')
|
||||||
|
|
||||||
|
return fetch(this.baseUrl + `/s/${this.submitterSlug}/upload`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { ...this.fetchOptions.headers },
|
||||||
|
body: formData
|
||||||
|
}).then((resp) => {
|
||||||
|
if (resp.status === 404) {
|
||||||
return fetch(this.baseUrl + '/api/attachments', {
|
return fetch(this.baseUrl + '/api/attachments', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
headers: { ...this.fetchOptions.headers },
|
||||||
body: formData
|
body: formData
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return resp
|
||||||
|
}
|
||||||
}).then(async (resp) => {
|
}).then(async (resp) => {
|
||||||
if (resp.status === 422 || resp.status === 500) {
|
if (resp.status === 422 || resp.status === 500) {
|
||||||
const data = await resp.json()
|
const data = await resp.json()
|
||||||
|
|||||||
@@ -3,11 +3,19 @@
|
|||||||
v-if="!error"
|
v-if="!error"
|
||||||
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
|
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
|
||||||
>
|
>
|
||||||
|
<span class="flex items-end">
|
||||||
<MarkdownContent
|
<MarkdownContent
|
||||||
v-if="field.title"
|
v-if="field.title"
|
||||||
:string="field.title"
|
:string="field.title"
|
||||||
/>
|
/>
|
||||||
<template v-else>{{ field.name || 'Knowledge Based Authentication' }}</template>
|
<template v-else>{{ field.name || 'Knowledge Based Authentication' }}</template>
|
||||||
|
<span
|
||||||
|
class="tooltip tooltip-bottom ml-2 mb-1"
|
||||||
|
data-tip="To verify your identity, the details you enter are checked against third-party records, which may include credit bureau data, and used to generate questions based on those records. Only these identity details are shared with the identity verification provider; this document and your tax return information are not shared with it or with any credit reporting company. If the check is recorded on your credit report, it appears as a soft inquiry, which does not affect your credit score and is not shown to lenders. This check does not give the sender of this document or the IRS access to your credit report."
|
||||||
|
>
|
||||||
|
<IconInfoCircle class="w-4 h-4 text-neutral-500" />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="questions"
|
v-if="questions"
|
||||||
class="float-right font-normal text-neutral-500 mt-1 whitespace-nowrap text-right text-sm"
|
class="float-right font-normal text-neutral-500 mt-1 whitespace-nowrap text-right text-sm"
|
||||||
@@ -282,13 +290,14 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import MarkdownContent from './markdown_content'
|
import MarkdownContent from './markdown_content'
|
||||||
import { IconInnerShadowTop } from '@tabler/icons-vue'
|
import { IconInnerShadowTop, IconInfoCircle } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'KbaStep',
|
name: 'KbaStep',
|
||||||
components: {
|
components: {
|
||||||
MarkdownContent,
|
MarkdownContent,
|
||||||
IconInnerShadowTop
|
IconInnerShadowTop,
|
||||||
|
IconInfoCircle
|
||||||
},
|
},
|
||||||
inject: ['baseUrl', 't'],
|
inject: ['baseUrl', 't'],
|
||||||
props: {
|
props: {
|
||||||
@@ -483,7 +492,7 @@ export default {
|
|||||||
this.clearCountdown()
|
this.clearCountdown()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = { ...this.form, submitter_slug: this.submitterSlug }
|
const payload = { ...this.form }
|
||||||
|
|
||||||
if (payload.dob) {
|
if (payload.dob) {
|
||||||
payload.dob = payload.dob.replace(/-/g, '')
|
payload.dob = payload.dob.replace(/-/g, '')
|
||||||
@@ -497,7 +506,7 @@ export default {
|
|||||||
payload.phone = payload.phone.replace(/^\+1/, '')
|
payload.phone = payload.phone.replace(/^\+1/, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
const resp = await fetch(this.baseUrl + '/api/kba', {
|
const resp = await fetch(this.baseUrl + `/s/${this.submitterSlug}/kba`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
...this.fetchOptions,
|
...this.fetchOptions,
|
||||||
@@ -548,13 +557,12 @@ export default {
|
|||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(this.baseUrl + `/api/kba/${this.field.uuid}`, {
|
const resp = await fetch(this.baseUrl + `/s/${this.submitterSlug}/kba/${this.field.uuid}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
token: this.token,
|
token: this.token,
|
||||||
answers: formattedAnswers,
|
answers: formattedAnswers,
|
||||||
reference: this.reference,
|
reference: this.reference
|
||||||
submitter_slug: this.submitterSlug
|
|
||||||
}),
|
}),
|
||||||
...this.fetchOptions,
|
...this.fetchOptions,
|
||||||
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
|
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
|
||||||
|
|||||||
@@ -104,6 +104,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
},
|
},
|
||||||
|
fieldsUuidIndex: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
values: {
|
values: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
@@ -137,7 +142,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
fieldsUuidIndex () {
|
formulaFieldsUuidIndex () {
|
||||||
return this.fields.reduce((acc, field) => {
|
return this.fields.reduce((acc, field) => {
|
||||||
acc[field.uuid] = field
|
acc[field.uuid] = field
|
||||||
|
|
||||||
@@ -151,7 +156,7 @@ export default {
|
|||||||
return this.provider === 'paypal'
|
return this.provider === 'paypal'
|
||||||
},
|
},
|
||||||
paymentEndpoint () {
|
paymentEndpoint () {
|
||||||
return this.isPaypal ? '/api/paypal_payments' : '/api/stripe_payments'
|
return `/s/${this.submitterSlug}/${this.isPaypal ? 'paypal_payments' : 'stripe_payments'}`
|
||||||
},
|
},
|
||||||
sessionId () {
|
sessionId () {
|
||||||
return this.queryParams.get('stripe_session_id') || this.queryParams.get('paypal_order_id') || (this.isPaypal && this.queryParams.get('token'))
|
return this.queryParams.get('stripe_session_id') || this.queryParams.get('paypal_order_id') || (this.isPaypal && this.queryParams.get('token'))
|
||||||
@@ -216,17 +221,31 @@ export default {
|
|||||||
},
|
},
|
||||||
calculateFormula () {
|
calculateFormula () {
|
||||||
const transformedFormula = this.normalizeFormula(this.field.preferences.formula).replace(/{{(.*?)}}/g, (match, uuid) => {
|
const transformedFormula = this.normalizeFormula(this.field.preferences.formula).replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||||
return this.readonlyValues[uuid] || this.values[uuid] || 0.0
|
const value = this.readonlyValues[uuid] || this.values[uuid]
|
||||||
|
|
||||||
|
if (this.fieldsUuidIndex[uuid]?.type === 'date') {
|
||||||
|
return this.dateFormulaValue(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value || 0.0
|
||||||
})
|
})
|
||||||
|
|
||||||
return this.math.evaluate(transformedFormula.toLowerCase())
|
return this.math.evaluate(transformedFormula.toLowerCase())
|
||||||
},
|
},
|
||||||
|
dateFormulaValue (value) {
|
||||||
|
if (!value) return 'null'
|
||||||
|
if (value === '{{date}}') return 'today()'
|
||||||
|
|
||||||
|
const [year, month, day = 1] = value.slice(0, 10).split('-').map(Number)
|
||||||
|
|
||||||
|
return Date.UTC(year, month - 1, day) / 86400000
|
||||||
|
},
|
||||||
normalizeFormula (formula, depth = 0) {
|
normalizeFormula (formula, depth = 0) {
|
||||||
if (depth > 10) return formula
|
if (depth > 10) return formula
|
||||||
|
|
||||||
return formula.replace(/{{(.*?)}}/g, (match, uuid) => {
|
return formula.replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||||
if (this.fieldsUuidIndex[uuid]) {
|
if (this.formulaFieldsUuidIndex[uuid]) {
|
||||||
return `(${this.normalizeFormula(this.fieldsUuidIndex[uuid].preferences.formula, depth + 1)})`
|
return `(${this.normalizeFormula(this.formulaFieldsUuidIndex[uuid].preferences.formula, depth + 1)})`
|
||||||
} else {
|
} else {
|
||||||
return match
|
return match
|
||||||
}
|
}
|
||||||
@@ -236,9 +255,6 @@ export default {
|
|||||||
if (this.sessionId) {
|
if (this.sessionId) {
|
||||||
return fetch(this.baseUrl + this.paymentEndpoint + '/' + this.sessionId, {
|
return fetch(this.baseUrl + this.paymentEndpoint + '/' + this.sessionId, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({
|
|
||||||
submitter_slug: this.submitterSlug
|
|
||||||
}),
|
|
||||||
...this.fetchOptions,
|
...this.fetchOptions,
|
||||||
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
|
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
|
||||||
}).then(async (resp) => {
|
}).then(async (resp) => {
|
||||||
@@ -275,7 +291,6 @@ export default {
|
|||||||
fetch(this.baseUrl + this.paymentEndpoint, {
|
fetch(this.baseUrl + this.paymentEndpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
submitter_slug: this.submitterSlug,
|
|
||||||
field_uuid: this.field.uuid,
|
field_uuid: this.field.uuid,
|
||||||
check_status: checkStatus,
|
check_status: checkStatus,
|
||||||
referer: this.normalizedPaymentUrl()
|
referer: this.normalizedPaymentUrl()
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ export default {
|
|||||||
}, 1000)
|
}, 1000)
|
||||||
},
|
},
|
||||||
sendVerificationCode () {
|
sendVerificationCode () {
|
||||||
return fetch(this.baseUrl + '/api/send_phone_verification_code', {
|
const options = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
submitter_slug: this.submitterSlug,
|
submitter_slug: this.submitterSlug,
|
||||||
@@ -293,6 +293,14 @@ export default {
|
|||||||
phone: this.fullInternationalPhoneValue
|
phone: this.fullInternationalPhoneValue
|
||||||
}),
|
}),
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(this.baseUrl + `/s/${this.submitterSlug}/send_phone_verification_code`, options).then((resp) => {
|
||||||
|
if (resp.status === 404) {
|
||||||
|
return fetch(this.baseUrl + '/api/send_phone_verification_code', options)
|
||||||
|
} else {
|
||||||
|
return resp
|
||||||
|
}
|
||||||
}).then(async (resp) => {
|
}).then(async (resp) => {
|
||||||
if ([422, 429].includes(resp.status)) {
|
if ([422, 429].includes(resp.status)) {
|
||||||
const data = await resp.json()
|
const data = await resp.json()
|
||||||
|
|||||||
Loaded 100 of 312 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user