mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 14:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
154d706be1 | ||
|
|
aaae257c75 | ||
|
|
ec944a2283 | ||
|
|
69d63ecffe | ||
|
|
06a1e2992d | ||
|
|
ec9293bac0 | ||
|
|
8bf727c43c | ||
|
|
708b8afdf5 | ||
|
|
6f7b9b1ebf | ||
|
|
4f48c4fd9f | ||
|
|
9dac90e6cc | ||
|
|
0741a6f485 | ||
|
|
1f338dfd2b | ||
|
|
0310c26edd | ||
|
|
e43570d4c1 | ||
|
|
d7142c2935 | ||
|
|
fa6dc04ef1 | ||
|
|
c5ec7a4b37 | ||
|
|
bd3d643318 | ||
|
|
1ae635963c | ||
|
|
a61c3e798d | ||
|
|
931addedbe | ||
|
|
9aa4a547cf | ||
|
|
82a7df5e3f | ||
|
|
0ff87383d0 | ||
|
|
ea3dec443f | ||
|
|
d7a895b98b | ||
|
|
b90b7b7845 | ||
|
|
3d05d75f2d | ||
|
|
e701881377 | ||
|
|
4615e3e48f | ||
|
|
988a5361a6 | ||
|
|
1b60b42428 | ||
|
|
7b74487214 | ||
|
|
91d98ed267 | ||
|
|
24fbb7de06 | ||
|
|
91df5b2407 | ||
|
|
eef2d9d599 | ||
|
|
8ad70b7e74 | ||
|
|
8b775103c7 | ||
|
|
8c2475aac1 | ||
|
|
d3236ae4a1 | ||
|
|
8fd9af3a6a | ||
|
|
4f94858901 | ||
|
|
d0ac4ad222 | ||
|
|
10e4476f4a | ||
|
|
74411d883a | ||
|
|
18ee2d5316 | ||
|
|
b4f80422eb | ||
|
|
7fe9ab7601 | ||
|
|
13ad614e6b | ||
|
|
7b1b4bcf2c | ||
|
|
a6715d4034 | ||
|
|
254995c6c6 | ||
|
|
0e33e23d1f | ||
|
|
57503eefd2 | ||
|
|
cd477c880c | ||
|
|
bf2f5b24dc | ||
|
|
7eba4865bc | ||
|
|
64cd24a24d | ||
|
|
de5acd0949 | ||
|
|
6ebbc3edac | ||
|
|
2020f6c136 | ||
|
|
ee4acd5a82 | ||
|
|
2e487ac936 | ||
|
|
dee5fc7c07 | ||
|
|
e130584fec | ||
|
|
ade5c4832f | ||
|
|
33b14d5278 | ||
|
|
48e73c2c81 | ||
|
|
894d98e92b | ||
|
|
14c84eff93 | ||
|
|
01d982b093 | ||
|
|
35852ef6ab | ||
|
|
47b235dc5f | ||
|
|
0e11d2a581 | ||
|
|
e57e11f206 | ||
|
|
51f639065c | ||
|
|
6c1d4b466f | ||
|
|
b940c597dc | ||
|
|
1ce67bcb39 | ||
|
|
5050a67d08 | ||
|
|
76321cef3c | ||
|
|
8f3a203994 | ||
|
|
ea24562861 | ||
|
|
4a888d048e | ||
|
|
36331bb993 | ||
|
|
6f0e7c0ca8 | ||
|
|
2491b7b7e3 | ||
|
|
7965c17f46 | ||
|
|
5519459519 | ||
|
|
d8bf2cc101 | ||
|
|
2aece98d67 | ||
|
|
f602497255 | ||
|
|
11db68cdc7 | ||
|
|
bed98f4344 | ||
|
|
055f7e2bc1 | ||
|
|
dae3b4b6b5 | ||
|
|
f2888a08dc | ||
|
|
95627c5963 | ||
|
|
4bb3721db0 | ||
|
|
b12111383b | ||
|
|
263c0ff939 | ||
|
|
efc8ba6b0a | ||
|
|
4c530b3f88 | ||
|
|
f6358c9d47 | ||
|
|
b3baea2f6f | ||
|
|
c0594a8b74 | ||
|
|
1403401b03 | ||
|
|
700184f776 | ||
|
|
b6a1a3ee4d | ||
|
|
8cbf5802b5 | ||
|
|
ab9706afef | ||
|
|
f07f9d8cf9 | ||
|
|
591caecb92 | ||
|
|
ef4c2b7213 | ||
|
|
18999255a9 | ||
|
|
0a2a1de6b9 | ||
|
|
1b857b6eb3 | ||
|
|
e5e6b11c95 | ||
|
|
3ec3f58d55 | ||
|
|
3bfce862ec | ||
|
|
c01b67f7ec | ||
|
|
ea0efe9314 | ||
|
|
28b583691f | ||
|
|
33811945be | ||
|
|
9d1860f038 | ||
|
|
f03166b4ea | ||
|
|
c59d948d41 | ||
|
|
57d63dd2f2 | ||
|
|
c88715daec | ||
|
|
0375dad7c9 | ||
|
|
de3f83db96 | ||
|
|
a7bb96464c | ||
|
|
1d48fe9621 | ||
|
|
9544bb3d76 | ||
|
|
4e94664008 | ||
|
|
e41fe5fbca | ||
|
|
24d713dec6 | ||
|
|
f464e1c46a | ||
|
|
85d52d1f28 | ||
|
|
e77b5fa2c9 | ||
|
|
c91b4a765b | ||
|
|
2a031c14c8 | ||
|
|
f7dfc0b2ed | ||
|
|
3b894f5281 | ||
|
|
9645332e48 | ||
|
|
5670390b37 | ||
|
|
260bd6d5d5 | ||
|
|
fa0733f480 | ||
|
|
8b9678a434 | ||
|
|
43669bf095 | ||
|
|
2ef8df3f0c | ||
|
|
1c9d15aa7e | ||
|
|
9fcd8955d3 | ||
|
|
5e44ba49b8 | ||
|
|
ac1f90e711 | ||
|
|
c6eae66127 | ||
|
|
08aa902e07 | ||
|
|
1690337699 | ||
|
|
f78cf8cc6e | ||
|
|
343118d00d | ||
|
|
a3bd37c9a2 | ||
|
|
8bc13a5900 | ||
|
|
21138c1d02 | ||
|
|
38674a9fc9 | ||
|
|
a7ed0b86a4 | ||
|
|
91a96f06c8 | ||
|
|
23631b75f2 | ||
|
|
0fb9c2bbf6 | ||
|
|
14fa8fc8e7 | ||
|
|
047c192988 | ||
|
|
bfa244c8fa | ||
|
|
019222ae40 | ||
|
|
8a10852fe2 | ||
|
|
01232aed5a | ||
|
|
82f3ff4824 | ||
|
|
7be3f5a931 | ||
|
|
9d013a7384 | ||
|
|
0c71d7bff8 | ||
|
|
fdd030652e | ||
|
|
f2bf1461f0 | ||
|
|
fc6f796f30 | ||
|
|
5f44806287 | ||
|
|
318b65af18 | ||
|
|
91b182b04a | ||
|
|
15b886d4ff | ||
|
|
0122a237e3 | ||
|
|
2dca7d8a8e | ||
|
|
87265e2f22 | ||
|
|
3166defa1a | ||
|
|
93ba17f42e | ||
|
|
751be1dae5 | ||
|
|
3f8d2831e2 | ||
|
|
4d8942803e | ||
|
|
46d9bb2c87 | ||
|
|
34433c0575 | ||
|
|
544306eac4 | ||
|
|
0eeebfd3cf | ||
|
|
050224a10d | ||
|
|
7b6fc7d06f | ||
|
|
7fb148c85c | ||
|
|
dffa59fecd | ||
|
|
24fe2a1192 | ||
|
|
97b8ac4444 | ||
|
|
e34a763dd9 | ||
|
|
298b1d3e1e | ||
|
|
d9978519cc | ||
|
|
b4a68cf79e | ||
|
|
5663e60f92 | ||
|
|
19e8835809 | ||
|
|
330fae927f | ||
|
|
c5a5212947 | ||
|
|
21d81e38bf | ||
|
|
5b1a4ebffd | ||
|
|
5f5dabd0e5 | ||
|
|
ff46993de6 | ||
|
|
7676db2196 | ||
|
|
3ef0d28ffe | ||
|
|
231177f057 | ||
|
|
e5f7739952 | ||
|
|
4a8e2a82e5 | ||
|
|
6efe8027f1 | ||
|
|
96bb4eb6e5 | ||
|
|
38e68814cf | ||
|
|
786c80bee7 | ||
|
|
98dcce299e | ||
|
|
cbff242a5d | ||
|
|
1876b3efba | ||
|
|
51091f05f0 | ||
|
|
d76eee2ae4 | ||
|
|
d7bfa96d57 | ||
|
|
be60608211 | ||
|
|
4e5abb3c09 | ||
|
|
7fee82ded2 | ||
|
|
e1cd62ff40 | ||
|
|
b498291b62 | ||
|
|
def3009846 | ||
|
|
9d43fd9aa5 | ||
|
|
22a21fb037 | ||
|
|
2a9c375273 | ||
|
|
847e70ec1b | ||
|
|
7964d6e0f0 | ||
|
|
d0259791bf | ||
|
|
a9dcaeccbc | ||
|
|
501b91a603 | ||
|
|
2b5f772feb | ||
|
|
7cada0af8b | ||
|
|
67be785e69 | ||
|
|
d1cfc64adc | ||
|
|
4e5157d1ee | ||
|
|
5e27cc36c6 | ||
|
|
6e81a3be3f | ||
|
|
858b2f33fd | ||
|
|
6adcc87b2b | ||
|
|
e1d5a5b6a6 | ||
|
|
f090e79002 | ||
|
|
56b32e35a2 | ||
|
|
b40c27cdbf | ||
|
|
c4505e67f0 |
No files matched your search
@@ -6,6 +6,7 @@ jobs:
|
|||||||
rubocop:
|
rubocop:
|
||||||
name: Rubocop
|
name: Rubocop
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
@@ -30,6 +31,7 @@ jobs:
|
|||||||
erblint:
|
erblint:
|
||||||
name: Erblint
|
name: Erblint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
@@ -54,6 +56,7 @@ jobs:
|
|||||||
eslint:
|
eslint:
|
||||||
name: ESLint
|
name: ESLint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
@@ -80,6 +83,7 @@ jobs:
|
|||||||
brakeman:
|
brakeman:
|
||||||
name: Brakeman
|
name: Brakeman
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
@@ -107,6 +111,7 @@ jobs:
|
|||||||
rspec:
|
rspec:
|
||||||
name: RSpec
|
name: RSpec
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
@@ -157,7 +162,10 @@ jobs:
|
|||||||
bundle install --jobs 4 --retry 4
|
bundle install --jobs 4 --retry 4
|
||||||
yarn install
|
yarn install
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install libvips
|
sudo apt-get install -y libvips
|
||||||
|
wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz"
|
||||||
|
sudo tar -xzf pdfium-linux.tgz --strip-components=1 -C /usr/lib lib/libpdfium.so
|
||||||
|
rm -f pdfium-linux.tgz
|
||||||
- name: Run
|
- name: Run
|
||||||
env:
|
env:
|
||||||
RAILS_ENV: test
|
RAILS_ENV: test
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-24.04-arm
|
runs-on: ubuntu-24.04-arm
|
||||||
|
timeout-minutes: 20
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
|
|||||||
+2
-2
@@ -63,10 +63,10 @@ RSpec/MultipleExpectations:
|
|||||||
Max: 25
|
Max: 25
|
||||||
|
|
||||||
RSpec/ExampleLength:
|
RSpec/ExampleLength:
|
||||||
Max: 50
|
Max: 500
|
||||||
|
|
||||||
RSpec/MultipleMemoizedHelpers:
|
RSpec/MultipleMemoizedHelpers:
|
||||||
Max: 9
|
Max: 15
|
||||||
|
|
||||||
Metrics/BlockNesting:
|
Metrics/BlockNesting:
|
||||||
Max: 5
|
Max: 5
|
||||||
|
|||||||
+16
-5
@@ -1,8 +1,17 @@
|
|||||||
FROM ruby:3.4.2-alpine AS fonts
|
FROM ruby:3.4.2-alpine AS download
|
||||||
|
|
||||||
WORKDIR /fonts
|
WORKDIR /fonts
|
||||||
|
|
||||||
RUN apk --no-cache add fontforge wget && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt
|
RUN apk --no-cache add fontforge wget && \
|
||||||
|
wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && \
|
||||||
|
wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && \
|
||||||
|
wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && \
|
||||||
|
wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && \
|
||||||
|
wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && \
|
||||||
|
wget https://github.com/impallari/DancingScript/raw/master/OFL.txt && \
|
||||||
|
wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" && \
|
||||||
|
mkdir -p /pdfium-linux && \
|
||||||
|
tar -xzf pdfium-linux.tgz -C /pdfium-linux
|
||||||
|
|
||||||
RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")'
|
RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")'
|
||||||
|
|
||||||
@@ -41,7 +50,7 @@ ENV OPENSSL_CONF=/app/openssl_legacy.cnf
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils redis libheif@edge vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev@edge redis libheif@edge vips-heif@edge gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
||||||
|
|
||||||
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
|
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
|
||||||
\n\
|
\n\
|
||||||
@@ -70,8 +79,10 @@ COPY ./tmp ./tmp
|
|||||||
COPY LICENSE README.md Rakefile config.ru .version ./
|
COPY LICENSE README.md Rakefile config.ru .version ./
|
||||||
COPY .version ./public/version
|
COPY .version ./public/version
|
||||||
|
|
||||||
COPY --from=fonts /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
COPY --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
||||||
COPY --from=fonts /fonts/FreeSans.ttf /usr/share/fonts/freefont
|
COPY --from=download /fonts/FreeSans.ttf /usr/share/fonts/freefont
|
||||||
|
COPY --from=download /pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so
|
||||||
|
COPY --from=download /pdfium-linux/licenses/pdfium.txt /usr/lib/libpdfium-LICENSE.txt
|
||||||
COPY --from=webpack /app/public/packs ./public/packs
|
COPY --from=webpack /app/public/packs ./public/packs
|
||||||
|
|
||||||
RUN ln -s /fonts /app/public/fonts
|
RUN ln -s /fonts /app/public/fonts
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ gem 'rails'
|
|||||||
gem 'rails_autolink'
|
gem 'rails_autolink'
|
||||||
gem 'rails-i18n'
|
gem 'rails-i18n'
|
||||||
gem 'rotp'
|
gem 'rotp'
|
||||||
|
gem 'rouge', require: false
|
||||||
gem 'rqrcode'
|
gem 'rqrcode'
|
||||||
gem 'ruby-vips'
|
gem 'ruby-vips'
|
||||||
gem 'rubyXL'
|
gem 'rubyXL'
|
||||||
|
|||||||
+9
-7
@@ -338,18 +338,18 @@ GEM
|
|||||||
net-smtp (0.5.0)
|
net-smtp (0.5.0)
|
||||||
net-protocol
|
net-protocol
|
||||||
nio4r (2.7.4)
|
nio4r (2.7.4)
|
||||||
nokogiri (1.18.5)
|
nokogiri (1.18.8)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.5-aarch64-linux-gnu)
|
nokogiri (1.18.8-aarch64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.5-aarch64-linux-musl)
|
nokogiri (1.18.8-aarch64-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.5-arm64-darwin)
|
nokogiri (1.18.8-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.5-x86_64-linux-gnu)
|
nokogiri (1.18.8-x86_64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.5-x86_64-linux-musl)
|
nokogiri (1.18.8-x86_64-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
oj (3.16.8)
|
oj (3.16.8)
|
||||||
bigdecimal (>= 3.0)
|
bigdecimal (>= 3.0)
|
||||||
@@ -387,7 +387,7 @@ GEM
|
|||||||
puma (6.5.0)
|
puma (6.5.0)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rack (3.1.12)
|
rack (3.1.16)
|
||||||
rack-proxy (0.7.7)
|
rack-proxy (0.7.7)
|
||||||
rack
|
rack
|
||||||
rack-session (2.0.0)
|
rack-session (2.0.0)
|
||||||
@@ -453,6 +453,7 @@ GEM
|
|||||||
retriable (3.1.2)
|
retriable (3.1.2)
|
||||||
rexml (3.4.0)
|
rexml (3.4.0)
|
||||||
rotp (6.3.0)
|
rotp (6.3.0)
|
||||||
|
rouge (4.5.2)
|
||||||
rqrcode (2.2.0)
|
rqrcode (2.2.0)
|
||||||
chunky_png (~> 1.0)
|
chunky_png (~> 1.0)
|
||||||
rqrcode_core (~> 1.0)
|
rqrcode_core (~> 1.0)
|
||||||
@@ -632,6 +633,7 @@ DEPENDENCIES
|
|||||||
rails-i18n
|
rails-i18n
|
||||||
rails_autolink
|
rails_autolink
|
||||||
rotp
|
rotp
|
||||||
|
rouge
|
||||||
rqrcode
|
rqrcode
|
||||||
rspec-rails
|
rspec-rails
|
||||||
rubocop
|
rubocop
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
- PDF signature verification
|
- PDF signature verification
|
||||||
- Users management
|
- Users management
|
||||||
- Mobile-optimized
|
- Mobile-optimized
|
||||||
- 6 UI languages with signing available in 13 languages
|
- 6 UI languages with signing available in 14 languages
|
||||||
- API and Webhooks for integrations
|
- API and Webhooks for integrations
|
||||||
- Easy to deploy in minutes
|
- Easy to deploy in minutes
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class AccountConfigsController < ApplicationController
|
class AccountConfigsController < ApplicationController
|
||||||
before_action :load_account_config
|
before_action :load_account_config, only: :create
|
||||||
authorize_resource :account_config
|
authorize_resource :account_config, only: :create
|
||||||
|
|
||||||
|
load_and_authorize_resource :account_config, only: :destroy
|
||||||
|
|
||||||
ALLOWED_KEYS = [
|
ALLOWED_KEYS = [
|
||||||
AccountConfig::ALLOW_TYPED_SIGNATURE,
|
AccountConfig::ALLOW_TYPED_SIGNATURE,
|
||||||
@@ -30,6 +32,14 @@ class AccountConfigsController < ApplicationController
|
|||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
raise InvalidKey unless ALLOWED_KEYS.include?(@account_config.key)
|
||||||
|
|
||||||
|
@account_config.destroy!
|
||||||
|
|
||||||
|
redirect_back(fallback_location: root_path)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def load_account_config
|
def load_account_config
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class AccountsController < ApplicationController
|
|||||||
def destroy
|
def destroy
|
||||||
authorize!(:manage, current_account)
|
authorize!(:manage, current_account)
|
||||||
|
|
||||||
true_user.update!(locked_at: Time.current)
|
true_user.update!(locked_at: Time.current, email: true_user.email.sub('@', '+removed@'))
|
||||||
|
|
||||||
# rubocop:disable Layout/LineLength
|
# rubocop:disable Layout/LineLength
|
||||||
render turbo_stream: turbo_stream.replace(
|
render turbo_stream: turbo_stream.replace(
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ module Api
|
|||||||
|
|
||||||
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
|
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
|
||||||
|
|
||||||
authorization_check!(blob) if exp.blank?
|
attachment = blob.attachments.take
|
||||||
|
|
||||||
|
@record = attachment.record
|
||||||
|
|
||||||
|
authorization_check!(attachment) if exp.blank?
|
||||||
|
|
||||||
if request.headers['Range'].present?
|
if request.headers['Range'].present?
|
||||||
send_blob_byte_range_data blob, request.headers['Range']
|
send_blob_byte_range_data blob, request.headers['Range']
|
||||||
@@ -37,9 +41,7 @@ module Api
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def authorization_check!(blob)
|
def authorization_check!(attachment)
|
||||||
attachment = blob.attachments.take
|
|
||||||
|
|
||||||
is_authorized = attachment.name.in?(%w[logo preview_images]) ||
|
is_authorized = attachment.name.in?(%w[logo preview_images]) ||
|
||||||
(current_user && attachment.record.account.id == current_user.account_id) ||
|
(current_user && attachment.record.account.id == current_user.account_id) ||
|
||||||
(current_user && !Docuseal.multitenant? && current_user.role == 'superadmin') ||
|
(current_user && !Docuseal.multitenant? && current_user.role == 'superadmin') ||
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ module Api
|
|||||||
def create
|
def create
|
||||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
|
if params[:type].in?(%w[initials signature]) && ImageUtils.blank?(Vips::Image.new_from_file(params[:file].path))
|
||||||
|
Rollbar.error("Empty signature: #{submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
attachment = Submitters.create_attachment!(submitter, params)
|
attachment = Submitters.create_attachment!(submitter, params)
|
||||||
|
|
||||||
if params[:remember_signature] == 'true' && submitter.email.present?
|
if params[:remember_signature] == 'true' && submitter.email.present?
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ module Api
|
|||||||
params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present?
|
params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present?
|
||||||
|
|
||||||
submitters = paginate(
|
submitters = paginate(
|
||||||
submitters.preload(template: :folder, submission: [:submitters, { audit_trail_attachment: :blob,
|
submitters.preload(template: { folder: :parent_folder },
|
||||||
combined_document_attachment: :blob }],
|
submission: [:submitters, { audit_trail_attachment: :blob,
|
||||||
|
combined_document_attachment: :blob }],
|
||||||
documents_attachments: :blob, attachments_attachments: :blob),
|
documents_attachments: :blob, attachments_attachments: :blob),
|
||||||
field: :completed_at
|
field: :completed_at
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,21 +10,11 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def index
|
def index
|
||||||
submissions = Submissions.search(@submissions, params[:q])
|
submissions = Submissions.search(current_user, @submissions, params[:q])
|
||||||
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
|
submissions = filter_submissions(submissions, params)
|
||||||
|
|
||||||
if params[:template_folder].present?
|
|
||||||
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] })
|
|
||||||
end
|
|
||||||
|
|
||||||
if params.key?(:archived)
|
|
||||||
submissions = params[:archived].in?(['true', true]) ? submissions.archived : submissions.active
|
|
||||||
end
|
|
||||||
|
|
||||||
submissions = Submissions::Filter.call(submissions, current_user, params)
|
|
||||||
|
|
||||||
submissions = paginate(submissions.preload(:created_by_user, :submitters,
|
submissions = paginate(submissions.preload(:created_by_user, :submitters,
|
||||||
template: :folder,
|
template: { folder: :parent_folder },
|
||||||
combined_document_attachment: :blob,
|
combined_document_attachment: :blob,
|
||||||
audit_trail_attachment: :blob))
|
audit_trail_attachment: :blob))
|
||||||
|
|
||||||
@@ -73,12 +63,7 @@ module Api
|
|||||||
|
|
||||||
submissions = create_submissions(@template, params)
|
submissions = create_submissions(@template, params)
|
||||||
|
|
||||||
WebhookUrls.for_account_id(@template.account_id, 'submission.created').each do |webhook_url|
|
WebhookUrls.enqueue_events(submissions, 'submission.created')
|
||||||
submissions.each do |submission|
|
|
||||||
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submission.id,
|
|
||||||
'webhook_url_id' => webhook_url.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Submissions.send_signature_requests(submissions)
|
Submissions.send_signature_requests(submissions)
|
||||||
|
|
||||||
@@ -90,6 +75,8 @@ module Api
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
SearchEntries.enqueue_reindex(submissions)
|
||||||
|
|
||||||
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
|
DownloadUtils::UnableToDownload => e
|
||||||
@@ -104,10 +91,7 @@ module Api
|
|||||||
else
|
else
|
||||||
@submission.update!(archived_at: Time.current)
|
@submission.update!(archived_at: Time.current)
|
||||||
|
|
||||||
WebhookUrls.for_account_id(@submission.account_id, 'submission.archived').each do |webhook_url|
|
WebhookUrls.enqueue_events(@submission, 'submission.archived')
|
||||||
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id,
|
|
||||||
'webhook_url_id' => webhook_url.id)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: @submission.as_json(only: %i[id archived_at])
|
render json: @submission.as_json(only: %i[id archived_at])
|
||||||
@@ -115,6 +99,24 @@ module Api
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
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?
|
||||||
|
|
||||||
|
if params[:template_folder].present?
|
||||||
|
folders =
|
||||||
|
TemplateFolders.filter_by_full_name(TemplateFolder.accessible_by(current_ability), params[:template_folder])
|
||||||
|
|
||||||
|
submissions = submissions.joins(:template).where(template: { folder_id: folders.pluck(:id) })
|
||||||
|
end
|
||||||
|
|
||||||
|
if params.key?(:archived)
|
||||||
|
submissions = params[:archived].in?(['true', true]) ? submissions.archived : submissions.active
|
||||||
|
end
|
||||||
|
|
||||||
|
Submissions::Filter.call(submissions, current_user, params)
|
||||||
|
end
|
||||||
|
|
||||||
def build_create_json(submissions)
|
def build_create_json(submissions)
|
||||||
json = submissions.flat_map do |submission|
|
json = submissions.flat_map do |submission|
|
||||||
submission.submitters.map do |s|
|
submission.submitters.map do |s|
|
||||||
@@ -178,14 +180,15 @@ module Api
|
|||||||
def submissions_params
|
def submissions_params
|
||||||
permitted_attrs = [
|
permitted_attrs = [
|
||||||
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
|
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
|
||||||
:expire_at,
|
:require_phone_2fa, :expire_at, :name,
|
||||||
{
|
{
|
||||||
message: %i[subject body],
|
message: %i[subject body],
|
||||||
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||||
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
|
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
|
||||||
|
:require_phone_2fa,
|
||||||
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
|
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
|
||||||
fields: [:name, :uuid, :default_value, :value, :title, :description,
|
fields: [:name, :uuid, :default_value, :value, :title, :description,
|
||||||
:readonly, :validation_pattern, :invalid_message,
|
:readonly, :required, :validation_pattern, :invalid_message,
|
||||||
{ default_value: [], value: [], preferences: {} }] }]]
|
{ default_value: [], value: [], preferences: {} }] }]]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ module Api
|
|||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
def create
|
def create
|
||||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
if params[:t] == SubmissionEvents.build_tracking_param(submitter, 'click_email')
|
if params[:t] == SubmissionEvents.build_tracking_param(@submitter, 'click_email')
|
||||||
SubmissionEvents.create_with_tracking_data(submitter, 'click_email', request)
|
SubmissionEvents.create_with_tracking_data(@submitter, 'click_email', request)
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: {}
|
render json: {}
|
||||||
|
|||||||
@@ -6,17 +6,14 @@ module Api
|
|||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
def create
|
def create
|
||||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
submitter.opened_at = Time.current
|
@submitter.opened_at = Time.current
|
||||||
submitter.save
|
@submitter.save
|
||||||
|
|
||||||
SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request)
|
SubmissionEvents.create_with_tracking_data(@submitter, 'view_form', request)
|
||||||
|
|
||||||
WebhookUrls.for_account_id(submitter.account_id, 'form.viewed').each do |webhook_url|
|
WebhookUrls.enqueue_events(@submitter, 'form.viewed')
|
||||||
SendFormViewedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
|
|
||||||
'webhook_url_id' => webhook_url.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
render json: {}
|
render json: {}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,17 +5,9 @@ module Api
|
|||||||
load_and_authorize_resource :submitter
|
load_and_authorize_resource :submitter
|
||||||
|
|
||||||
def index
|
def index
|
||||||
submitters = Submitters.search(@submitters, params[:q])
|
submitters = Submitters.search(current_user, @submitters, params[:q])
|
||||||
|
|
||||||
submitters = submitters.where(external_id: params[:application_key]) if params[:application_key].present?
|
submitters = filter_submitters(submitters, params)
|
||||||
submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present?
|
|
||||||
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
|
|
||||||
|
|
||||||
if params[:template_id].present?
|
|
||||||
submitters = submitters.joins(:submission).where(submission: { template_id: params[:template_id] })
|
|
||||||
end
|
|
||||||
|
|
||||||
submitters = maybe_filder_by_completed_at(submitters, params)
|
|
||||||
|
|
||||||
submitters = paginate(
|
submitters = paginate(
|
||||||
submitters.preload(:template, :submission, :submission_events,
|
submitters.preload(:template, :submission, :submission_events,
|
||||||
@@ -45,14 +37,16 @@ module Api
|
|||||||
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_entity
|
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
role = @submitter.submission.template_submitters.find { |e| e['uuid'] == @submitter.uuid }['name']
|
submission = @submitter.submission
|
||||||
|
role = submission.template_submitters.find { |e| e['uuid'] == @submitter.uuid }['name']
|
||||||
|
|
||||||
normalized_params, new_attachments =
|
normalized_params, new_attachments = Submissions::NormalizeParamUtils.normalize_submitter_params!(
|
||||||
Submissions::NormalizeParamUtils.normalize_submitter_params!(submitter_params.merge(role:), @submitter.template,
|
submitter_params.merge(role:),
|
||||||
for_submitter: @submitter)
|
@submitter.template || Template.new(submitters: submission.template_submitters, account: @submitter.account),
|
||||||
|
for_submitter: @submitter
|
||||||
|
)
|
||||||
|
|
||||||
Submissions::CreateFromSubmitters.maybe_set_template_fields(@submitter.submission,
|
Submissions::CreateFromSubmitters.maybe_set_template_fields(submission, [normalized_params],
|
||||||
[normalized_params],
|
|
||||||
default_submitter_uuid: @submitter.uuid)
|
default_submitter_uuid: @submitter.uuid)
|
||||||
|
|
||||||
assign_submitter_attrs(@submitter, normalized_params)
|
assign_submitter_attrs(@submitter, normalized_params)
|
||||||
@@ -73,10 +67,10 @@ module Api
|
|||||||
Submitters.send_signature_requests([@submitter])
|
Submitters.send_signature_requests([@submitter])
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: Submitters::SerializeForApi.call(@submitter, with_template: false,
|
SearchEntries.enqueue_reindex(@submitter)
|
||||||
with_urls: true,
|
|
||||||
with_events: false,
|
render json: Submitters::SerializeForApi.call(@submitter, with_template: false, with_urls: true,
|
||||||
params:)
|
with_events: false, params:)
|
||||||
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
|
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
|
||||||
Rollbar.warning(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
@@ -88,9 +82,9 @@ module Api
|
|||||||
|
|
||||||
submitter_params.permit(
|
submitter_params.permit(
|
||||||
:send_email, :send_sms, :reply_to, :completed_redirect_url, :uuid, :name, :email, :role,
|
:send_email, :send_sms, :reply_to, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||||
:completed, :phone, :application_key, :external_id, :go_to_last,
|
:completed, :phone, :application_key, :external_id, :go_to_last, :require_phone_2fa,
|
||||||
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
||||||
fields: [[:name, :uuid, :default_value, :value,
|
fields: [[:name, :uuid, :default_value, :value, :required,
|
||||||
:readonly, :validation_pattern, :invalid_message,
|
:readonly, :validation_pattern, :invalid_message,
|
||||||
{ default_value: [], value: [], preferences: {} }]] }
|
{ default_value: [], value: [], preferences: {} }]] }
|
||||||
)
|
)
|
||||||
@@ -163,6 +157,19 @@ module Api
|
|||||||
submitter
|
submitter
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def filter_submitters(submitters, params)
|
||||||
|
submitters = submitters.where(external_id: params[:application_key]) if params[:application_key].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(submission_id: params[:submission_id]) if params[:submission_id].present?
|
||||||
|
|
||||||
|
if params[:template_id].present?
|
||||||
|
submitters = submitters.joins(:submission).where(submissions: { template_id: params[:template_id] })
|
||||||
|
end
|
||||||
|
|
||||||
|
maybe_filder_by_completed_at(submitters, params)
|
||||||
|
end
|
||||||
|
|
||||||
def assign_external_id(submitter, attrs)
|
def assign_external_id(submitter, attrs)
|
||||||
submitter.external_id = attrs[:application_key] if attrs.key?(:application_key)
|
submitter.external_id = attrs[:application_key] if attrs.key?(:application_key)
|
||||||
submitter.external_id = attrs[:external_id] if attrs.key?(:external_id)
|
submitter.external_id = attrs[:external_id] if attrs.key?(:external_id)
|
||||||
@@ -186,6 +193,10 @@ module Api
|
|||||||
|
|
||||||
submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms')
|
submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms')
|
||||||
submitter.preferences['reply_to'] = submitter_preferences['reply_to'] if submitter_preferences.key?('reply_to')
|
submitter.preferences['reply_to'] = submitter_preferences['reply_to'] if submitter_preferences.key?('reply_to')
|
||||||
|
if submitter_preferences.key?('require_phone_2fa')
|
||||||
|
submitter.preferences['require_phone_2fa'] = submitter_preferences['require_phone_2fa']
|
||||||
|
end
|
||||||
|
|
||||||
if submitter_preferences.key?('go_to_last')
|
if submitter_preferences.key?('go_to_last')
|
||||||
submitter.preferences['go_to_last'] = submitter_preferences['go_to_last']
|
submitter.preferences['go_to_last'] = submitter_preferences['go_to_last']
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ module Api
|
|||||||
load_and_authorize_resource :template
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
def create
|
def create
|
||||||
authorize!(:manage, @template)
|
authorize!(:create, @template)
|
||||||
|
|
||||||
ActiveRecord::Associations::Preloader.new(
|
ActiveRecord::Associations::Preloader.new(
|
||||||
records: [@template],
|
records: [@template],
|
||||||
@@ -21,16 +21,16 @@ module Api
|
|||||||
)
|
)
|
||||||
|
|
||||||
cloned_template.source = :api
|
cloned_template.source = :api
|
||||||
cloned_template.save!
|
|
||||||
|
|
||||||
schema_documents = Templates::CloneAttachments.call(template: cloned_template,
|
schema_documents = Templates::CloneAttachments.call(template: cloned_template,
|
||||||
original_template: @template,
|
original_template: @template,
|
||||||
documents: params[:documents])
|
documents: params[:documents])
|
||||||
|
|
||||||
WebhookUrls.for_account_id(cloned_template.account_id, 'template.created').each do |webhook_url|
|
cloned_template.save!
|
||||||
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id,
|
|
||||||
'webhook_url_id' => webhook_url.id)
|
WebhookUrls.enqueue_events(cloned_template, 'template.created')
|
||||||
end
|
|
||||||
|
SearchEntries.enqueue_reindex(cloned_template)
|
||||||
|
|
||||||
render json: Templates::SerializeForApi.call(cloned_template, schema_documents)
|
render json: Templates::SerializeForApi.call(cloned_template, schema_documents)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ module Api
|
|||||||
def index
|
def index
|
||||||
templates = filter_templates(@templates, params)
|
templates = filter_templates(@templates, params)
|
||||||
|
|
||||||
templates = paginate(templates.preload(:author, :folder))
|
templates = paginate(templates.preload(:author, folder: :parent_folder))
|
||||||
|
|
||||||
schema_documents =
|
schema_documents =
|
||||||
ActiveStorage::Attachment.where(record_id: templates.map(&:id),
|
ActiveStorage::Attachment.where(record_id: templates.map(&:id),
|
||||||
@@ -65,10 +65,9 @@ module Api
|
|||||||
|
|
||||||
@template.update!(template_params)
|
@template.update!(template_params)
|
||||||
|
|
||||||
WebhookUrls.for_account_id(@template.account_id, 'template.updated').each do |webhook_url|
|
SearchEntries.enqueue_reindex(@template)
|
||||||
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id,
|
|
||||||
'webhook_url_id' => webhook_url.id)
|
WebhookUrls.enqueue_events(@template, 'template.updated')
|
||||||
end
|
|
||||||
|
|
||||||
render json: @template.as_json(only: %i[id updated_at])
|
render json: @template.as_json(only: %i[id updated_at])
|
||||||
end
|
end
|
||||||
@@ -86,11 +85,17 @@ module Api
|
|||||||
private
|
private
|
||||||
|
|
||||||
def filter_templates(templates, params)
|
def filter_templates(templates, params)
|
||||||
templates = Templates.search(templates, params[:q])
|
templates = Templates.search(current_user, templates, params[:q])
|
||||||
templates = params[:archived].in?(['true', true]) ? templates.archived : templates.active
|
templates = params[:archived].in?(['true', true]) ? templates.archived : templates.active
|
||||||
templates = templates.where(external_id: params[:application_key]) if params[:application_key].present?
|
templates = templates.where(external_id: params[:application_key]) if params[:application_key].present?
|
||||||
templates = templates.where(external_id: params[:external_id]) if params[:external_id].present?
|
templates = templates.where(external_id: params[:external_id]) if params[:external_id].present?
|
||||||
templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present?
|
templates = templates.where(slug: params[:slug]) if params[:slug].present?
|
||||||
|
|
||||||
|
if params[:folder].present?
|
||||||
|
folders = TemplateFolders.filter_by_full_name(TemplateFolder.accessible_by(current_ability), params[:folder])
|
||||||
|
|
||||||
|
templates = templates.where(folder_id: folders.pluck(:id))
|
||||||
|
end
|
||||||
|
|
||||||
templates
|
templates
|
||||||
end
|
end
|
||||||
@@ -99,6 +104,7 @@ module Api
|
|||||||
permitted_params = [
|
permitted_params = [
|
||||||
:name,
|
:name,
|
||||||
:external_id,
|
:external_id,
|
||||||
|
:shared_link,
|
||||||
{
|
{
|
||||||
submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email]],
|
submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email]],
|
||||||
fields: [[:uuid, :submitter_uuid, :name, :type,
|
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||||
|
|||||||
@@ -55,6 +55,14 @@ class ApplicationController < ActionController::Base
|
|||||||
request.session[:impersonated_user_id] = user.uuid
|
request.session[:impersonated_user_id] = user.uuid
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def pagy_auto(collection, **keyword_args)
|
||||||
|
if current_ability.can?(:manage, :countless)
|
||||||
|
pagy_countless(collection, **keyword_args)
|
||||||
|
else
|
||||||
|
pagy(collection, **keyword_args)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def with_locale(&)
|
def with_locale(&)
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ class EmailSmtpSettingsController < ApplicationController
|
|||||||
|
|
||||||
def create
|
def create
|
||||||
if @encrypted_config.update(email_configs)
|
if @encrypted_config.update(email_configs)
|
||||||
SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email'] || current_user.email).deliver_now!
|
unless Docuseal.multitenant?
|
||||||
|
SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email'] || current_user.email).deliver_now!
|
||||||
|
end
|
||||||
|
|
||||||
redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved')
|
redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved')
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -5,8 +5,12 @@ class ErrorsController < ActionController::Base
|
|||||||
'This feature is available in Pro Edition: https://www.docuseal.com/pricing'
|
'This feature is available in Pro Edition: https://www.docuseal.com/pricing'
|
||||||
|
|
||||||
ENTERPRISE_PATHS = [
|
ENTERPRISE_PATHS = [
|
||||||
|
'/submissions/html',
|
||||||
|
'/api/submissions/html',
|
||||||
'/templates/html',
|
'/templates/html',
|
||||||
'/api/templates/html',
|
'/api/templates/html',
|
||||||
|
'/submissions/pdf',
|
||||||
|
'/api/submissions/pdf',
|
||||||
'/templates/pdf',
|
'/templates/pdf',
|
||||||
'/api/templates/pdf',
|
'/api/templates/pdf',
|
||||||
'/templates/doc',
|
'/templates/doc',
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ class PreviewDocumentPageController < ActionController::API
|
|||||||
|
|
||||||
return head :not_found unless attachment
|
return head :not_found unless attachment
|
||||||
|
|
||||||
|
@template = attachment.record
|
||||||
|
|
||||||
preview_image = attachment.preview_images.joins(:blob)
|
preview_image = attachment.preview_images.joins(:blob)
|
||||||
.find_by(blob: { filename: ["#{params[:id]}.png", "#{params[:id]}.jpg"] })
|
.find_by(blob: { filename: ["#{params[:id]}.png", "#{params[:id]}.jpg"] })
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SearchEntriesReindexController < ApplicationController
|
||||||
|
def create
|
||||||
|
authorize!(:manage, EncryptedConfig)
|
||||||
|
|
||||||
|
ReindexAllSearchEntriesJob.perform_async
|
||||||
|
|
||||||
|
AccountConfig.find_or_initialize_by(account_id: Account.minimum(:id), key: :fulltext_search)
|
||||||
|
.update!(value: true)
|
||||||
|
|
||||||
|
Docuseal.instance_variable_set(:@fulltext_search, nil)
|
||||||
|
|
||||||
|
redirect_back(fallback_location: settings_account_path,
|
||||||
|
notice: "Started building search index. Visit #{root_url}jobs/busy to check progress.")
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -10,27 +10,37 @@ class SendSubmissionEmailController < ApplicationController
|
|||||||
SEND_DURATION = 30.minutes
|
SEND_DURATION = 30.minutes
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@submitter =
|
if params[:template_slug]
|
||||||
if params[:template_slug]
|
template = Template.find_by!(slug: params[:template_slug])
|
||||||
Submitter.joins(submission: :template).find_by!(email: params[:email].to_s.downcase,
|
|
||||||
template: { slug: params[:template_slug] })
|
@submitter =
|
||||||
elsif params[:submission_slug]
|
Submitter.completed.where(submission: template.submissions).find_by!(email: params[:email].to_s.downcase)
|
||||||
Submitter.joins(:submission).find_by!(email: params[:email].to_s.downcase,
|
elsif params[:submission_slug]
|
||||||
submission: { slug: params[:submission_slug] })
|
submission = Submission.find_by(slug: params[:submission_slug])
|
||||||
else
|
|
||||||
Submitter.find_by!(slug: params[:submitter_slug])
|
if submission
|
||||||
|
@submitter = Submitter.completed.find_by(submission: submission, email: params[:email].to_s.downcase)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
return redirect_to submissions_preview_completed_path(params[:submission_slug], status: :error) unless @submitter
|
||||||
|
else
|
||||||
|
@submitter = Submitter.completed.find_by!(slug: params[:submitter_slug])
|
||||||
|
end
|
||||||
|
|
||||||
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
||||||
|
|
||||||
unless EmailEvent.exists?(tag: :submitter_documents_copy, email: @submitter.email, emailable: @submitter,
|
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! unless already_sent?(@submitter)
|
||||||
event_type: :send, created_at: SEND_DURATION.ago..Time.current)
|
|
||||||
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later!
|
|
||||||
end
|
|
||||||
|
|
||||||
respond_to do |f|
|
respond_to do |f|
|
||||||
f.html { render :success }
|
f.html { render :success }
|
||||||
f.json { head :ok }
|
f.json { head :ok }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def already_sent?(submitter)
|
||||||
|
EmailEvent.exists?(tag: :submitter_documents_copy, email: submitter.email, emailable: submitter,
|
||||||
|
event_type: :send, created_at: SEND_DURATION.ago..Time.current)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -34,6 +34,7 @@ class SetupController < ApplicationController
|
|||||||
{ key: EncryptedConfig::ESIGN_CERTS_KEY, value: GenerateCertificate.call.transform_values(&:to_pem) }
|
{ key: EncryptedConfig::ESIGN_CERTS_KEY, value: GenerateCertificate.call.transform_values(&:to_pem) }
|
||||||
]
|
]
|
||||||
@account.encrypted_configs.create!(encrypted_configs)
|
@account.encrypted_configs.create!(encrypted_configs)
|
||||||
|
@account.account_configs.create!(key: :fulltext_search, value: true) if SearchEntry.table_exists?
|
||||||
|
|
||||||
Docuseal.refresh_default_url_options!
|
Docuseal.refresh_default_url_options!
|
||||||
|
|
||||||
|
|||||||
@@ -6,28 +6,43 @@ class StartFormController < ApplicationController
|
|||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
around_action :with_browser_locale, only: %i[show completed]
|
around_action :with_browser_locale, only: %i[show update completed]
|
||||||
before_action :maybe_redirect_com, only: %i[show completed]
|
before_action :maybe_redirect_com, only: %i[show completed]
|
||||||
|
before_action :load_resubmit_submitter, only: :update
|
||||||
before_action :load_template
|
before_action :load_template
|
||||||
|
before_action :authorize_start!, only: :update
|
||||||
|
|
||||||
|
COOKIES_TTL = 12.hours
|
||||||
|
COOKIES_DEFAULTS = { httponly: true, secure: Rails.env.production? }.freeze
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@submitter = @template.submissions.new(account_id: @template.account_id)
|
raise ActionController::RoutingError, I18n.t('not_found') if @template.preferences['require_phone_2fa']
|
||||||
.submitters.new(uuid: (filter_undefined_submitters(@template).first ||
|
|
||||||
@template.submitters.first)['uuid'])
|
if @template.shared_link?
|
||||||
|
@submitter = @template.submissions.new(account_id: @template.account_id)
|
||||||
|
.submitters.new(account_id: @template.account_id,
|
||||||
|
uuid: (filter_undefined_submitters(@template).first ||
|
||||||
|
@template.submitters.first)['uuid'])
|
||||||
|
render :email_verification if params[:email_verification]
|
||||||
|
else
|
||||||
|
Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return render :private if current_user && current_ability.can?(:read, @template)
|
||||||
|
|
||||||
|
raise ActionController::RoutingError, I18n.t('not_found')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
return redirect_to start_form_path(@template.slug) if @template.archived_at?
|
|
||||||
|
|
||||||
@submitter = find_or_initialize_submitter(@template, submitter_params)
|
@submitter = find_or_initialize_submitter(@template, submitter_params)
|
||||||
|
|
||||||
if @submitter.completed_at?
|
if @submitter.completed_at?
|
||||||
redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email])
|
redirect_to start_form_completed_path(@template.slug, submitter_params.compact_blank)
|
||||||
else
|
else
|
||||||
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record?
|
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record?
|
||||||
@error_message = multiple_submitters_error_message
|
@error_message = multiple_submitters_error_message
|
||||||
|
|
||||||
return render :show
|
return render :show, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
if (is_new_record = @submitter.new_record?)
|
if (is_new_record = @submitter.new_record?)
|
||||||
@@ -38,55 +53,112 @@ class StartFormController < ApplicationController
|
|||||||
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
|
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
|
||||||
end
|
end
|
||||||
|
|
||||||
if @submitter.save
|
if @template.preferences['shared_link_2fa'] == true
|
||||||
if is_new_record
|
handle_require_2fa(@submitter, is_new_record:)
|
||||||
WebhookUrls.for_account_id(@submitter.account_id, 'submission.created').each do |webhook_url|
|
elsif @submitter.errors.blank? && @submitter.save
|
||||||
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => @submitter.submission_id,
|
enqueue_new_submitter_jobs(@submitter) if is_new_record
|
||||||
'webhook_url_id' => webhook_url.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
redirect_to submit_form_path(@submitter.slug)
|
redirect_to submit_form_path(@submitter.slug)
|
||||||
else
|
else
|
||||||
render :show
|
render :show, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def completed
|
def completed
|
||||||
|
return redirect_to start_form_path(@template.slug) if !@template.shared_link? || @template.archived_at?
|
||||||
|
|
||||||
|
submitter_params = params.permit(:name, :email, :phone).tap do |attrs|
|
||||||
|
attrs[:email] = Submissions.normalize_email(attrs[:email])
|
||||||
|
end
|
||||||
|
|
||||||
|
required_fields = @template.preferences.fetch('link_form_fields', ['email'])
|
||||||
|
|
||||||
|
required_params = required_fields.index_with { |key| submitter_params[key] }
|
||||||
|
|
||||||
|
raise ActionController::RoutingError, I18n.t('not_found') if required_params.any? { |_, v| v.blank? } ||
|
||||||
|
required_params.except('name').compact_blank.blank?
|
||||||
|
|
||||||
@submitter = Submitter.where(submission: @template.submissions)
|
@submitter = Submitter.where(submission: @template.submissions)
|
||||||
.where.not(completed_at: nil)
|
.where.not(completed_at: nil)
|
||||||
.find_by!(email: params[:email])
|
.find_by!(required_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def enqueue_new_submitter_jobs(submitter)
|
||||||
|
WebhookUrls.enqueue_events(submitter.submission, 'submission.created')
|
||||||
|
|
||||||
|
SearchEntries.enqueue_reindex(submitter)
|
||||||
|
|
||||||
|
return unless submitter.submission.expire_at?
|
||||||
|
|
||||||
|
ProcessSubmissionExpiredJob.perform_at(submitter.submission.expire_at, 'submission_id' => submitter.submission_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_resubmit_submitter
|
||||||
|
@resubmit_submitter =
|
||||||
|
if params[:resubmit].present? && !params[:resubmit].in?([true, 'true'])
|
||||||
|
Submitter.find_by(slug: params[:resubmit])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def authorize_start!
|
||||||
|
return redirect_to start_form_path(@template.slug) if @template.archived_at?
|
||||||
|
|
||||||
|
return if @resubmit_submitter
|
||||||
|
return if @template.shared_link? || (current_user && current_ability.can?(:read, @template))
|
||||||
|
|
||||||
|
Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
redirect_to start_form_path(@template.slug)
|
||||||
|
end
|
||||||
|
|
||||||
def find_or_initialize_submitter(template, submitter_params)
|
def find_or_initialize_submitter(template, submitter_params)
|
||||||
Submitter.where(submission: template.submissions.where(expire_at: Time.current..)
|
required_fields = template.preferences.fetch('link_form_fields', ['email'])
|
||||||
.or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
|
|
||||||
.order(id: :desc)
|
required_params = required_fields.index_with { |key| submitter_params[key] }
|
||||||
.where(declined_at: nil)
|
|
||||||
.where(external_id: nil)
|
find_params = required_params.except('name')
|
||||||
.where(ip: [nil, request.remote_ip])
|
|
||||||
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
|
submitter = Submitter.new if find_params.compact_blank.blank?
|
||||||
.find_or_initialize_by(email: submitter_params[:email], **submitter_params.compact_blank)
|
|
||||||
|
submitter ||=
|
||||||
|
Submitter
|
||||||
|
.where(submission: template.submissions.where(expire_at: Time.current..)
|
||||||
|
.or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
|
||||||
|
.order(id: :desc)
|
||||||
|
.where(declined_at: nil)
|
||||||
|
.where(external_id: nil)
|
||||||
|
.where(template.preferences['shared_link_2fa'] == true ? {} : { ip: [nil, request.remote_ip] })
|
||||||
|
.then { |rel| params[:resubmit].present? || params[:selfsign].present? ? rel.where(completed_at: nil) : rel }
|
||||||
|
.find_or_initialize_by(find_params)
|
||||||
|
|
||||||
|
submitter.name = required_params['name'] if submitter.new_record?
|
||||||
|
|
||||||
|
unless @resubmit_submitter
|
||||||
|
required_params.each do |key, value|
|
||||||
|
submitter.errors.add(key.to_sym, :blank) if value.blank?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
submitter
|
||||||
end
|
end
|
||||||
|
|
||||||
def assign_submission_attributes(submitter, template)
|
def assign_submission_attributes(submitter, template)
|
||||||
resubmit_submitter =
|
|
||||||
(Submitter.where(submission: template.submissions).find_by(slug: params[:resubmit]) if params[:resubmit].present?)
|
|
||||||
|
|
||||||
submitter.assign_attributes(
|
submitter.assign_attributes(
|
||||||
uuid: (filter_undefined_submitters(template).first || @template.submitters.first)['uuid'],
|
uuid: (filter_undefined_submitters(template).first || @template.submitters.first)['uuid'],
|
||||||
ip: request.remote_ip,
|
ip: request.remote_ip,
|
||||||
ua: request.user_agent,
|
ua: request.user_agent,
|
||||||
values: resubmit_submitter&.preferences&.fetch('default_values', nil) || {},
|
values: @resubmit_submitter&.preferences&.fetch('default_values', nil) || {},
|
||||||
preferences: resubmit_submitter&.preferences.presence || { 'send_email' => true },
|
preferences: @resubmit_submitter&.preferences.presence || { 'send_email' => true },
|
||||||
metadata: resubmit_submitter&.metadata.presence || {}
|
metadata: @resubmit_submitter&.metadata.presence || {}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
submitter.assign_attributes(@resubmit_submitter.slice(:name, :email, :phone)) if @resubmit_submitter
|
||||||
|
|
||||||
if submitter.values.present?
|
if submitter.values.present?
|
||||||
resubmit_submitter.attachments.each do |attachment|
|
@resubmit_submitter.attachments.each do |attachment|
|
||||||
submitter.attachments << attachment.dup if submitter.values.value?(attachment.uuid)
|
submitter.attachments << attachment.dup if submitter.values.value?(attachment.uuid)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -94,6 +166,7 @@ class StartFormController < ApplicationController
|
|||||||
submitter.submission ||= Submission.new(template:,
|
submitter.submission ||= Submission.new(template:,
|
||||||
account_id: template.account_id,
|
account_id: template.account_id,
|
||||||
template_submitters: template.submitters,
|
template_submitters: template.submitters,
|
||||||
|
expire_at: Templates.build_default_expire_at(template),
|
||||||
submitters: [submitter],
|
submitters: [submitter],
|
||||||
source: :link)
|
source: :link)
|
||||||
|
|
||||||
@@ -103,19 +176,25 @@ class StartFormController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def filter_undefined_submitters(template)
|
def filter_undefined_submitters(template)
|
||||||
Templates.filter_undefined_submitters(template)
|
Templates.filter_undefined_submitters(template.submitters)
|
||||||
end
|
end
|
||||||
|
|
||||||
def submitter_params
|
def submitter_params
|
||||||
|
return { 'email' => current_user.email, 'name' => current_user.full_name } if params[:selfsign]
|
||||||
|
return @resubmit_submitter.slice(:name, :phone, :email) if @resubmit_submitter.present?
|
||||||
|
|
||||||
params.require(:submitter).permit(:email, :phone, :name).tap do |attrs|
|
params.require(:submitter).permit(:email, :phone, :name).tap do |attrs|
|
||||||
attrs[:email] = Submissions.normalize_email(attrs[:email])
|
attrs[:email] = Submissions.normalize_email(attrs[:email])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_template
|
def load_template
|
||||||
slug = params[:slug] || params[:start_form_slug]
|
@template =
|
||||||
|
if @resubmit_submitter
|
||||||
@template = Template.find_by!(slug:)
|
@resubmit_submitter.template
|
||||||
|
else
|
||||||
|
Template.find_by!(slug: params[:slug] || params[:start_form_slug])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def multiple_submitters_error_message
|
def multiple_submitters_error_message
|
||||||
@@ -125,4 +204,39 @@ class StartFormController < ApplicationController
|
|||||||
I18n.t('not_found')
|
I18n.t('not_found')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_require_2fa(submitter, is_new_record:)
|
||||||
|
return render :show, status: :unprocessable_entity if submitter.errors.present?
|
||||||
|
|
||||||
|
is_otp_verified = Submitters.verify_link_otp!(params[:one_time_code], submitter)
|
||||||
|
|
||||||
|
if cookies.encrypted[:email_2fa_slug] == submitter.slug || is_otp_verified
|
||||||
|
if submitter.save
|
||||||
|
enqueue_new_submitter_jobs(submitter) if is_new_record
|
||||||
|
|
||||||
|
if is_otp_verified
|
||||||
|
SubmissionEvents.create_with_tracking_data(submitter, 'email_verified', request)
|
||||||
|
|
||||||
|
cookies.encrypted[:email_2fa_slug] =
|
||||||
|
{ value: submitter.slug, expires: COOKIES_TTL.from_now, **COOKIES_DEFAULTS }
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect_to submit_form_path(submitter.slug)
|
||||||
|
else
|
||||||
|
render :show, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
else
|
||||||
|
Submitters.send_shared_link_email_verification_code(submitter, request:)
|
||||||
|
|
||||||
|
render :email_verification
|
||||||
|
end
|
||||||
|
rescue Submitters::UnableToSendCode, Submitters::InvalidOtp => e
|
||||||
|
redirect_to start_form_path(submitter.submission.template.slug,
|
||||||
|
params: submitter_params.merge(email_verification: true)),
|
||||||
|
alert: e.message
|
||||||
|
rescue RateLimit::LimitApproached
|
||||||
|
redirect_to start_form_path(submitter.submission.template.slug,
|
||||||
|
params: submitter_params.merge(email_verification: true)),
|
||||||
|
alert: I18n.t(:too_many_attempts)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class StartFormEmail2faSendController < ApplicationController
|
||||||
|
around_action :with_browser_locale
|
||||||
|
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
def create
|
||||||
|
@template = Template.find_by!(slug: params[:slug])
|
||||||
|
|
||||||
|
@submitter = @template.submissions.new(account_id: @template.account_id)
|
||||||
|
.submitters.new(**submitter_params, account_id: @template.account_id)
|
||||||
|
|
||||||
|
Submitters.send_shared_link_email_verification_code(@submitter, request:)
|
||||||
|
|
||||||
|
redir_params = { notice: I18n.t(:code_has_been_resent) } if params[:resend]
|
||||||
|
|
||||||
|
redirect_to start_form_path(@template.slug, params: submitter_params.merge(email_verification: true)),
|
||||||
|
**redir_params
|
||||||
|
rescue Submitters::UnableToSendCode => e
|
||||||
|
redirect_to start_form_path(@template.slug, params: submitter_params.merge(email_verification: true)),
|
||||||
|
alert: e.message
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def submitter_params
|
||||||
|
params.require(:submitter).permit(:name, :email, :phone)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -12,6 +12,7 @@ class SubmissionEventsController < ApplicationController
|
|||||||
'send_2fa_sms' => '2fa',
|
'send_2fa_sms' => '2fa',
|
||||||
'send_sms' => 'send',
|
'send_sms' => 'send',
|
||||||
'phone_verified' => 'phone_check',
|
'phone_verified' => 'phone_check',
|
||||||
|
'email_verified' => 'email_check',
|
||||||
'click_sms' => 'hand_click',
|
'click_sms' => 'hand_click',
|
||||||
'decline_form' => 'x',
|
'decline_form' => 'x',
|
||||||
'start_verification' => 'player_play',
|
'start_verification' => 'player_play',
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ class SubmissionsArchivedController < ApplicationController
|
|||||||
load_and_authorize_resource :submission, parent: false
|
load_and_authorize_resource :submission, parent: false
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@submissions = @submissions.joins(:template)
|
@submissions = @submissions.left_joins(:template)
|
||||||
@submissions = @submissions.where.not(archived_at: nil)
|
@submissions = @submissions.where.not(archived_at: nil)
|
||||||
.or(@submissions.where.not(templates: { archived_at: nil }))
|
.or(@submissions.where.not(templates: { archived_at: nil }))
|
||||||
.preload(:created_by_user, template: :author)
|
.preload(:template_accesses, :created_by_user, template: :author)
|
||||||
|
|
||||||
@submissions = @submissions.preload(:template_accesses) unless current_user.role.in?(%w[admin superadmin])
|
@submissions = Submissions.search(current_user, @submissions, params[:q], search_template: true)
|
||||||
|
|
||||||
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
|
|
||||||
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
||||||
|
|
||||||
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
|
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
|
||||||
@@ -20,6 +18,6 @@ class SubmissionsArchivedController < ApplicationController
|
|||||||
@submissions.order(id: :desc)
|
@submissions.order(id: :desc)
|
||||||
end
|
end
|
||||||
|
|
||||||
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events))
|
@pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -8,6 +8,10 @@ class SubmissionsController < ApplicationController
|
|||||||
|
|
||||||
prepend_before_action :maybe_redirect_com, only: %i[show]
|
prepend_before_action :maybe_redirect_com, only: %i[show]
|
||||||
|
|
||||||
|
before_action only: :create do
|
||||||
|
authorize!(:create, Submission)
|
||||||
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@submission = Submissions.preload_with_pages(@submission)
|
@submission = Submissions.preload_with_pages(@submission)
|
||||||
|
|
||||||
@@ -26,8 +30,6 @@ class SubmissionsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
authorize!(:create, Submission)
|
|
||||||
|
|
||||||
save_template_message(@template, params) if params[:save_message] == '1'
|
save_template_message(@template, params) if params[:save_message] == '1'
|
||||||
|
|
||||||
if params[:is_custom_message] != '1'
|
if params[:is_custom_message] != '1'
|
||||||
@@ -52,11 +54,18 @@ class SubmissionsController < ApplicationController
|
|||||||
params: params.merge('send_completed_email' => true))
|
params: params.merge('send_completed_email' => true))
|
||||||
end
|
end
|
||||||
|
|
||||||
enqueue_submission_created_webhooks(@template, submissions)
|
WebhookUrls.enqueue_events(submissions, 'submission.created')
|
||||||
|
|
||||||
Submissions.send_signature_requests(submissions)
|
Submissions.send_signature_requests(submissions)
|
||||||
|
|
||||||
|
SearchEntries.enqueue_reindex(submissions)
|
||||||
|
|
||||||
redirect_to template_path(@template), notice: I18n.t('new_recipients_have_been_added')
|
redirect_to template_path(@template), notice: I18n.t('new_recipients_have_been_added')
|
||||||
|
rescue Submissions::CreateFromSubmitters::BaseError => e
|
||||||
|
render turbo_stream: turbo_stream.replace(:submitters_error,
|
||||||
|
partial: 'submissions/error',
|
||||||
|
locals: { error: e.message }),
|
||||||
|
status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@@ -68,15 +77,12 @@ class SubmissionsController < ApplicationController
|
|||||||
else
|
else
|
||||||
@submission.update!(archived_at: Time.current)
|
@submission.update!(archived_at: Time.current)
|
||||||
|
|
||||||
WebhookUrls.for_account_id(@submission.account_id, 'submission.archived').each do |webhook_url|
|
WebhookUrls.enqueue_events(@submission, 'submission.archived')
|
||||||
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id,
|
|
||||||
'webhook_url_id' => webhook_url.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
I18n.t('submission_has_been_archived')
|
I18n.t('submission_has_been_archived')
|
||||||
end
|
end
|
||||||
|
|
||||||
redirect_back(fallback_location: template_path(@submission.template), notice:)
|
redirect_back(fallback_location: @submission.template_id ? template_path(@submission.template) : root_path, notice:)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -88,15 +94,6 @@ class SubmissionsController < ApplicationController
|
|||||||
template.save!
|
template.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
def enqueue_submission_created_webhooks(template, submissions)
|
|
||||||
WebhookUrls.for_account_id(template.account_id, 'submission.created').each do |webhook_url|
|
|
||||||
submissions.each do |submission|
|
|
||||||
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submission.id,
|
|
||||||
'webhook_url_id' => webhook_url.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def submissions_params
|
def submissions_params
|
||||||
params.permit(submission: { submitters: [%i[uuid email phone name]] })
|
params.permit(submission: { submitters: [%i[uuid email phone name]] })
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,15 +4,13 @@ class SubmissionsDashboardController < ApplicationController
|
|||||||
load_and_authorize_resource :submission, parent: false
|
load_and_authorize_resource :submission, parent: false
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@submissions = @submissions.joins(:template)
|
@submissions = @submissions.left_joins(:template)
|
||||||
|
|
||||||
@submissions = @submissions.where(archived_at: nil)
|
@submissions = @submissions.where(archived_at: nil)
|
||||||
.where(templates: { archived_at: nil })
|
.where(templates: { archived_at: nil })
|
||||||
.preload(:created_by_user, template: :author)
|
.preload(:template_accesses, :created_by_user, template: :author)
|
||||||
|
|
||||||
@submissions = @submissions.preload(:template_accesses) unless current_user.role.in?(%w[admin superadmin])
|
@submissions = Submissions.search(current_user, @submissions, params[:q], search_template: true)
|
||||||
|
|
||||||
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
|
|
||||||
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
||||||
|
|
||||||
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
|
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
|
||||||
@@ -21,6 +19,6 @@ class SubmissionsDashboardController < ApplicationController
|
|||||||
@submissions.order(id: :desc)
|
@submissions.order(id: :desc)
|
||||||
end
|
end
|
||||||
|
|
||||||
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events))
|
@pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -8,20 +8,20 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
FILES_TTL = 5.minutes
|
FILES_TTL = 5.minutes
|
||||||
|
|
||||||
def index
|
def index
|
||||||
submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
@submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
||||||
|
|
||||||
signature_valid =
|
signature_valid =
|
||||||
if submitter&.slug == params[:submitter_slug]
|
if @submitter&.slug == params[:submitter_slug]
|
||||||
true
|
true
|
||||||
else
|
else
|
||||||
submitter = nil
|
@submitter = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
submitter ||= Submitter.find_by!(slug: params[:submitter_slug])
|
@submitter ||= Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(submitter)
|
Submissions::EnsureResultGenerated.call(@submitter)
|
||||||
|
|
||||||
last_submitter = submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
|
last_submitter = @submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
|
||||||
|
|
||||||
return head :not_found unless last_submitter
|
return head :not_found unless last_submitter
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
if params[:combined] == 'true'
|
if params[:combined] == 'true'
|
||||||
url = build_combined_url(submitter)
|
url = build_combined_url(@submitter)
|
||||||
|
|
||||||
if url
|
if url
|
||||||
render json: [url]
|
render json: [url]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class SubmissionsPreviewController < ApplicationController
|
class SubmissionsPreviewController < ApplicationController
|
||||||
|
around_action :with_browser_locale
|
||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ class SubmissionsPreviewController < ApplicationController
|
|||||||
raise ActionController::RoutingError, I18n.t('not_found')
|
raise ActionController::RoutingError, I18n.t('not_found')
|
||||||
end
|
end
|
||||||
|
|
||||||
if !submission_valid_ttl?(@submission) && !signature_valid
|
if use_signature?(@submission) && !signature_valid
|
||||||
Rollbar.info("TTL: #{@submission.id}") if defined?(Rollbar)
|
Rollbar.info("TTL: #{@submission.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
return redirect_to submissions_preview_completed_path(@submission.slug)
|
return redirect_to submissions_preview_completed_path(@submission.slug)
|
||||||
@@ -47,9 +48,15 @@ class SubmissionsPreviewController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def submission_valid_ttl?(submission)
|
def use_signature?(submission)
|
||||||
return true if current_user && current_user.account.submissions.exists?(id: submission.id)
|
return false if current_user && can?(:read, submission)
|
||||||
|
return true if submission.submitters.any? { |e| e.preferences['require_phone_2fa'] }
|
||||||
|
return true if submission.template&.preferences&.dig('require_phone_2fa')
|
||||||
|
|
||||||
|
!submission_valid_ttl?(submission)
|
||||||
|
end
|
||||||
|
|
||||||
|
def submission_valid_ttl?(submission)
|
||||||
last_submitter = submission.submitters.select(&:completed_at?).max_by(&:completed_at)
|
last_submitter = submission.submitters.select(&:completed_at?).max_by(&:completed_at)
|
||||||
|
|
||||||
last_submitter && last_submitter.completed_at > TTL.ago
|
last_submitter && last_submitter.completed_at > TTL.ago
|
||||||
|
|||||||
@@ -7,27 +7,24 @@ class SubmitFormController < ApplicationController
|
|||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
|
before_action :load_submitter, only: %i[show update completed]
|
||||||
|
before_action :maybe_render_locked_page, only: :show
|
||||||
|
before_action :maybe_require_link_2fa, only: %i[show update]
|
||||||
|
|
||||||
CONFIG_KEYS = [].freeze
|
CONFIG_KEYS = [].freeze
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@submitter = Submitter.find_by!(slug: params[:slug])
|
|
||||||
|
|
||||||
submission = @submitter.submission
|
submission = @submitter.submission
|
||||||
|
|
||||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||||
return render :archived if submission.template.archived_at? ||
|
|
||||||
submission.archived_at? ||
|
|
||||||
@submitter.account.archived_at?
|
|
||||||
return render :expired if submission.expired?
|
|
||||||
return render :declined if @submitter.declined_at?
|
|
||||||
|
|
||||||
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
|
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
|
||||||
|
|
||||||
return render :awaiting if (@form_configs[:enforce_signing_order] ||
|
return render :awaiting if (@form_configs[:enforce_signing_order] ||
|
||||||
submission.template.preferences['submitters_order'] == 'preserved') &&
|
submission.template&.preferences&.dig('submitters_order') == 'preserved') &&
|
||||||
!Submitters.current_submitter_order?(@submitter)
|
!Submitters.current_submitter_order?(@submitter)
|
||||||
|
|
||||||
Submitters.preload_with_pages(@submitter)
|
Submissions.preload_with_pages(submission)
|
||||||
|
|
||||||
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
|
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
|
||||||
|
|
||||||
@@ -50,44 +47,62 @@ class SubmitFormController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
submitter = Submitter.find_by!(slug: params[:slug])
|
if @submitter.completed_at?
|
||||||
|
|
||||||
if submitter.completed_at?
|
|
||||||
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_entity
|
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
if submitter.template.archived_at? || submitter.submission.archived_at?
|
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at?
|
||||||
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_entity
|
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
if submitter.submission.expired?
|
if @submitter.submission.expired?
|
||||||
return render json: { error: I18n.t('form_has_been_expired') }, status: :unprocessable_entity
|
return render json: { error: I18n.t('form_has_been_expired') }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
if submitter.declined_at?
|
if @submitter.declined_at?
|
||||||
return render json: { error: I18n.t('form_has_been_declined') },
|
return render json: { error: I18n.t('form_has_been_declined') },
|
||||||
status: :unprocessable_entity
|
status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
Submitters::SubmitValues.call(submitter, params, request)
|
Submitters::SubmitValues.call(@submitter, params, request)
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
rescue Submitters::SubmitValues::RequiredFieldError => e
|
rescue Submitters::SubmitValues::RequiredFieldError => e
|
||||||
Rollbar.warning("Required field #{submitter.id}: #{e.message}") if defined?(Rollbar)
|
Rollbar.warning("Required field #{@submitter.id}: #{e.message}") if defined?(Rollbar)
|
||||||
|
|
||||||
render json: { field_uuid: e.message }, status: :unprocessable_entity
|
render json: { field_uuid: e.message }, status: :unprocessable_entity
|
||||||
rescue Submitters::SubmitValues::ValidationError => e
|
rescue Submitters::SubmitValues::ValidationError => e
|
||||||
render json: { error: e.message }, status: :unprocessable_entity
|
render json: { error: e.message }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
def completed
|
def completed; end
|
||||||
@submitter = Submitter.completed.find_by!(slug: params[:submit_form_slug])
|
|
||||||
end
|
|
||||||
|
|
||||||
def success; end
|
def success; end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def maybe_require_link_2fa
|
||||||
|
return if @submitter.submission.source != 'link'
|
||||||
|
return unless @submitter.submission.template&.preferences&.dig('shared_link_2fa') == true
|
||||||
|
return if cookies.encrypted[:email_2fa_slug] == @submitter.slug
|
||||||
|
return if @submitter.email == current_user&.email && current_user&.account_id == @submitter.account_id
|
||||||
|
|
||||||
|
redirect_to start_form_path(@submitter.submission.template.slug)
|
||||||
|
end
|
||||||
|
|
||||||
|
def maybe_render_locked_page
|
||||||
|
return render :archived if @submitter.submission.template&.archived_at? ||
|
||||||
|
@submitter.submission.archived_at? ||
|
||||||
|
@submitter.account.archived_at?
|
||||||
|
return render :expired if @submitter.submission.expired?
|
||||||
|
|
||||||
|
render :declined if @submitter.declined_at?
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_submitter
|
||||||
|
@submitter = Submitter.find_by!(slug: params[:slug] || params[:submit_form_slug])
|
||||||
|
end
|
||||||
|
|
||||||
def build_attachments_index(submission)
|
def build_attachments_index(submission)
|
||||||
ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments)
|
ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments)
|
||||||
.preload(:blob).index_by(&:uuid)
|
.preload(:blob).index_by(&:uuid)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class SubmitFormDeclineController < ApplicationController
|
|||||||
submitter.completed_at? ||
|
submitter.completed_at? ||
|
||||||
submitter.submission.archived_at? ||
|
submitter.submission.archived_at? ||
|
||||||
submitter.submission.expired? ||
|
submitter.submission.expired? ||
|
||||||
submitter.submission.template.archived_at?
|
submitter.submission.template&.archived_at?
|
||||||
|
|
||||||
ApplicationRecord.transaction do
|
ApplicationRecord.transaction do
|
||||||
submitter.update!(declined_at: Time.current)
|
submitter.update!(declined_at: Time.current)
|
||||||
@@ -25,10 +25,7 @@ class SubmitFormDeclineController < ApplicationController
|
|||||||
SubmitterMailer.declined_email(submitter, user).deliver_later!
|
SubmitterMailer.declined_email(submitter, user).deliver_later!
|
||||||
end
|
end
|
||||||
|
|
||||||
WebhookUrls.for_account_id(submitter.account_id, 'form.declined').each do |webhook_url|
|
WebhookUrls.enqueue_events(submitter, 'form.declined')
|
||||||
SendFormDeclinedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
|
|
||||||
'webhook_url_id' => webhook_url.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
redirect_to submit_form_path(submitter.slug)
|
redirect_to submit_form_path(submitter.slug)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,25 +7,28 @@ class SubmitFormDownloadController < ApplicationController
|
|||||||
FILES_TTL = 5.minutes
|
FILES_TTL = 5.minutes
|
||||||
|
|
||||||
def index
|
def index
|
||||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||||
|
|
||||||
return redirect_to submitter_download_index_path(submitter.slug) if submitter.completed_at?
|
return redirect_to submitter_download_index_path(@submitter.slug) if @submitter.completed_at?
|
||||||
|
|
||||||
return head :unprocessable_entity if submitter.declined_at? ||
|
return head :unprocessable_entity if @submitter.declined_at? ||
|
||||||
submitter.submission.archived_at? ||
|
@submitter.submission.archived_at? ||
|
||||||
submitter.submission.expired? ||
|
@submitter.submission.expired? ||
|
||||||
submitter.submission.template.archived_at?
|
@submitter.submission.template&.archived_at? ||
|
||||||
|
AccountConfig.exists?(account_id: @submitter.account_id,
|
||||||
|
key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
|
||||||
|
value: false)
|
||||||
|
|
||||||
last_completed_submitter = submitter.submission.submitters
|
last_completed_submitter = @submitter.submission.submitters
|
||||||
.where.not(id: submitter.id)
|
.where.not(id: @submitter.id)
|
||||||
.where.not(completed_at: nil)
|
.where.not(completed_at: nil)
|
||||||
.max_by(&:completed_at)
|
.max_by(&:completed_at)
|
||||||
|
|
||||||
attachments =
|
attachments =
|
||||||
if last_completed_submitter
|
if last_completed_submitter
|
||||||
Submitters.select_attachments_for_download(last_completed_submitter)
|
Submitters.select_attachments_for_download(last_completed_submitter)
|
||||||
else
|
else
|
||||||
submitter.submission.template.schema_documents.preload(:blob)
|
@submitter.submission.schema_documents.preload(:blob)
|
||||||
end
|
end
|
||||||
|
|
||||||
urls = attachments.map do |attachment|
|
urls = attachments.map do |attachment|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class SubmitFormDrawSignatureController < ApplicationController
|
|||||||
|
|
||||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||||
|
|
||||||
if @submitter.submission.template.archived_at? || @submitter.submission.archived_at?
|
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at?
|
||||||
return redirect_to submit_form_path(@submitter.slug)
|
return redirect_to submit_form_path(@submitter.slug)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class SubmitFormInviteController < ApplicationController
|
|||||||
!submitter.completed_at? &&
|
!submitter.completed_at? &&
|
||||||
!submitter.submission.archived_at? &&
|
!submitter.submission.archived_at? &&
|
||||||
!submitter.submission.expired? &&
|
!submitter.submission.expired? &&
|
||||||
!submitter.submission.template.archived_at?
|
!submitter.submission.template&.archived_at?
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_invite_submitters(submitter, key = 'invite_by_uuid')
|
def filter_invite_submitters(submitter, key = 'invite_by_uuid')
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class SubmitFormValuesController < ApplicationController
|
|||||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||||
|
|
||||||
return render json: {} if submitter.completed_at? || submitter.declined_at?
|
return render json: {} if submitter.completed_at? || submitter.declined_at?
|
||||||
return render json: {} if submitter.submission.template.archived_at? ||
|
return render json: {} if submitter.submission.template&.archived_at? ||
|
||||||
submitter.submission.archived_at? ||
|
submitter.submission.archived_at? ||
|
||||||
submitter.submission.expired?
|
submitter.submission.expired?
|
||||||
|
|
||||||
|
|||||||
@@ -22,13 +22,17 @@ class SubmittersAutocompleteController < ApplicationController
|
|||||||
|
|
||||||
def search_submitters(submitters)
|
def search_submitters(submitters)
|
||||||
if SELECT_COLUMNS.include?(params[:field])
|
if SELECT_COLUMNS.include?(params[:field])
|
||||||
column = Submitter.arel_table[params[:field].to_sym]
|
if Docuseal.fulltext_search?
|
||||||
|
Submitters.fulltext_search_field(current_user, submitters, params[:q], params[:field])
|
||||||
|
else
|
||||||
|
column = Submitter.arel_table[params[:field].to_sym]
|
||||||
|
|
||||||
term = "#{params[:q].downcase}%"
|
term = "#{params[:q].downcase}%"
|
||||||
|
|
||||||
submitters.where(column.matches(term))
|
submitters.where(column.matches(term))
|
||||||
|
end
|
||||||
else
|
else
|
||||||
Submitters.search(submitters, params[:q])
|
Submitters.search(current_user, submitters, params[:q])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -38,6 +38,8 @@ class SubmittersController < ApplicationController
|
|||||||
if @submitter.save
|
if @submitter.save
|
||||||
maybe_resend_email_sms(@submitter, params)
|
maybe_resend_email_sms(@submitter, params)
|
||||||
|
|
||||||
|
SearchEntries.enqueue_reindex(@submitter)
|
||||||
|
|
||||||
redirect_back fallback_location: submission_path(submission), notice: I18n.t('changes_have_been_saved')
|
redirect_back fallback_location: submission_path(submission), notice: I18n.t('changes_have_been_saved')
|
||||||
else
|
else
|
||||||
redirect_back fallback_location: submission_path(submission), alert: I18n.t('unable_to_save')
|
redirect_back fallback_location: submission_path(submission), alert: I18n.t('unable_to_save')
|
||||||
|
|||||||
@@ -6,13 +6,15 @@ class SubmittersResubmitController < ApplicationController
|
|||||||
def update
|
def update
|
||||||
return redirect_to submit_form_path(slug: @submitter.slug) if @submitter.email != current_user.email
|
return redirect_to submit_form_path(slug: @submitter.slug) if @submitter.email != current_user.email
|
||||||
|
|
||||||
submission = @submitter.template.submissions.new(created_by_user: current_user,
|
submission = @submitter.account.submissions.new(created_by_user: current_user,
|
||||||
submitters_order: :preserved,
|
submitters_order: :preserved,
|
||||||
**@submitter.submission.slice(:template_fields,
|
**@submitter.submission.slice(:template_fields,
|
||||||
:account_id,
|
:account_id,
|
||||||
:template_schema,
|
:name,
|
||||||
:template_submitters,
|
:template_id,
|
||||||
:preferences))
|
:template_schema,
|
||||||
|
:template_submitters,
|
||||||
|
:preferences))
|
||||||
|
|
||||||
@submitter.submission.submitters.each do |submitter|
|
@submitter.submission.submitters.each do |submitter|
|
||||||
new_submitter = submission.submitters.new(submitter.slice(:uuid, :email, :phone, :name,
|
new_submitter = submission.submitters.new(submitter.slice(:uuid, :email, :phone, :name,
|
||||||
@@ -27,6 +29,10 @@ class SubmittersResubmitController < ApplicationController
|
|||||||
|
|
||||||
submission.save!
|
submission.save!
|
||||||
|
|
||||||
|
@submitter.submission.documents_attachments.each do |attachment|
|
||||||
|
submission.documents_attachments.create!(uuid: attachment.uuid, blob_id: attachment.blob_id)
|
||||||
|
end
|
||||||
|
|
||||||
redirect_to submit_form_path(slug: @new_submitter.slug)
|
redirect_to submit_form_path(slug: @new_submitter.slug)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,31 @@
|
|||||||
class TemplateFoldersAutocompleteController < ApplicationController
|
class TemplateFoldersAutocompleteController < ApplicationController
|
||||||
load_and_authorize_resource :template_folder, parent: false
|
load_and_authorize_resource :template_folder, parent: false
|
||||||
|
|
||||||
LIMIT = 100
|
LIMIT = 30
|
||||||
|
|
||||||
def index
|
def index
|
||||||
template_folders = @template_folders.joins(:templates).where(templates: { archived_at: nil }).distinct
|
parent_name, name =
|
||||||
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
|
if params[:parent_name].present?
|
||||||
|
[params[:parent_name], params[:q]]
|
||||||
|
else
|
||||||
|
params[:q].to_s.split(' /', 2).map(&:squish)
|
||||||
|
end
|
||||||
|
|
||||||
render json: template_folders.as_json(only: %i[name archived_at])
|
if name
|
||||||
|
parent_folder = @template_folders.find_by(name: parent_name, parent_folder_id: nil)
|
||||||
|
else
|
||||||
|
name = parent_name
|
||||||
|
end
|
||||||
|
|
||||||
|
template_folders = TemplateFolders.filter_active_folders(@template_folders.where(parent_folder:),
|
||||||
|
Template.accessible_by(current_ability))
|
||||||
|
|
||||||
|
name = name.to_s.downcase
|
||||||
|
|
||||||
|
template_folders = TemplateFolders.search(template_folders, name).order(id: :desc).limit(LIMIT)
|
||||||
|
|
||||||
|
render json: template_folders.preload(:parent_folder)
|
||||||
|
.sort_by { |e| e.name.downcase.index(name) || Float::MAX }
|
||||||
|
.as_json(only: %i[name archived_at], methods: %i[full_name])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -3,12 +3,41 @@
|
|||||||
class TemplateFoldersController < ApplicationController
|
class TemplateFoldersController < ApplicationController
|
||||||
load_and_authorize_resource :template_folder
|
load_and_authorize_resource :template_folder
|
||||||
|
|
||||||
def show
|
helper_method :selected_order
|
||||||
@templates = @template_folder.templates.active.accessible_by(current_ability)
|
|
||||||
.preload(:author, :template_accesses).order(id: :desc)
|
|
||||||
@templates = Templates.search(@templates, params[:q])
|
|
||||||
|
|
||||||
@pagy, @templates = pagy(@templates, limit: 12)
|
TEMPLATES_PER_PAGE = 12
|
||||||
|
FOLDERS_PER_PAGE = 18
|
||||||
|
|
||||||
|
def show
|
||||||
|
@templates = Template.active.accessible_by(current_ability)
|
||||||
|
.where(folder: [@template_folder, *(params[:q].present? ? @template_folder.subfolders : [])])
|
||||||
|
.preload(:author, :template_accesses)
|
||||||
|
|
||||||
|
@template_folders =
|
||||||
|
@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)
|
||||||
|
|
||||||
|
if @templates.exists?
|
||||||
|
@templates = Templates.search(current_user, @templates, params[:q])
|
||||||
|
@templates = Templates::Order.call(@templates, current_user, selected_order)
|
||||||
|
|
||||||
|
limit =
|
||||||
|
if @template_folders.size < 4
|
||||||
|
TEMPLATES_PER_PAGE
|
||||||
|
else
|
||||||
|
(@template_folders.size < 7 ? 9 : 6)
|
||||||
|
end
|
||||||
|
|
||||||
|
@pagy, @templates = pagy_auto(@templates, limit:)
|
||||||
|
|
||||||
|
load_related_submissions if params[:q].present? && @templates.blank?
|
||||||
|
else
|
||||||
|
@pagy, @template_folders = pagy(@template_folders, limit: FOLDERS_PER_PAGE)
|
||||||
|
|
||||||
|
@templates = @templates.none
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def edit; end
|
def edit; end
|
||||||
@@ -24,7 +53,34 @@ class TemplateFoldersController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def selected_order
|
||||||
|
@selected_order ||=
|
||||||
|
if cookies.permanent[:dashboard_templates_order].blank? ||
|
||||||
|
(cookies.permanent[:dashboard_templates_order] == 'used_at' && can?(:manage, :countless))
|
||||||
|
'created_at'
|
||||||
|
else
|
||||||
|
cookies.permanent[:dashboard_templates_order]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def template_folder_params
|
def template_folder_params
|
||||||
params.require(:template_folder).permit(:name)
|
params.require(:template_folder).permit(:name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def load_related_submissions
|
||||||
|
@related_submissions =
|
||||||
|
Submission.accessible_by(current_ability)
|
||||||
|
.where(archived_at: nil)
|
||||||
|
.where(template_id: current_account.templates.active
|
||||||
|
.where(folder: [@template_folder, *@template_folder.subfolders])
|
||||||
|
.select(:id))
|
||||||
|
.preload(:template_accesses, :created_by_user,
|
||||||
|
template: :author,
|
||||||
|
submitters: :start_form_submission_events)
|
||||||
|
|
||||||
|
@related_submissions = Submissions.search(current_user, @related_submissions, params[:q])
|
||||||
|
.order(id: :desc)
|
||||||
|
|
||||||
|
@related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -4,9 +4,27 @@ class TemplatesArchivedController < ApplicationController
|
|||||||
load_and_authorize_resource :template, parent: false
|
load_and_authorize_resource :template, parent: false
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@templates = @templates.where.not(archived_at: nil).preload(:author, :folder, :template_accesses).order(id: :desc)
|
@templates = @templates.where.not(archived_at: nil)
|
||||||
@templates = Templates.search(@templates, params[:q])
|
.preload(:author, :template_accesses, folder: :parent_folder)
|
||||||
|
.order(id: :desc)
|
||||||
|
|
||||||
@pagy, @templates = pagy(@templates, limit: 12)
|
@templates = Templates.search(current_user, @templates, params[:q])
|
||||||
|
|
||||||
|
@pagy, @templates = pagy_auto(@templates, limit: 12)
|
||||||
|
|
||||||
|
return unless params[:q].present? && @templates.blank?
|
||||||
|
|
||||||
|
@related_submissions =
|
||||||
|
Submission.accessible_by(current_ability)
|
||||||
|
.joins(:template)
|
||||||
|
.where.not(templates: { archived_at: nil })
|
||||||
|
.preload(:template_accesses, :created_by_user,
|
||||||
|
template: :author,
|
||||||
|
submitters: :start_form_submission_events)
|
||||||
|
|
||||||
|
@related_submissions = Submissions.search(current_user, @related_submissions, params[:q])
|
||||||
|
.order(id: :desc)
|
||||||
|
|
||||||
|
@related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -6,7 +6,7 @@ class TemplatesArchivedSubmissionsController < ApplicationController
|
|||||||
|
|
||||||
def index
|
def index
|
||||||
@submissions = @submissions.where.not(archived_at: nil)
|
@submissions = @submissions.where.not(archived_at: nil)
|
||||||
@submissions = Submissions.search(@submissions, params[:q], search_values: true)
|
@submissions = Submissions.search(current_user, @submissions, params[:q], search_values: true)
|
||||||
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
||||||
|
|
||||||
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
|
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
|
||||||
@@ -15,7 +15,7 @@ class TemplatesArchivedSubmissionsController < ApplicationController
|
|||||||
@submissions.order(id: :desc)
|
@submissions.order(id: :desc)
|
||||||
end
|
end
|
||||||
|
|
||||||
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events))
|
@pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events))
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
redirect_to root_path
|
redirect_to root_path
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesCloneAndReplaceController < ApplicationController
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def create
|
||||||
|
return head :unprocessable_entity if params[:files].blank?
|
||||||
|
|
||||||
|
ActiveRecord::Associations::Preloader.new(
|
||||||
|
records: [@template],
|
||||||
|
associations: [schema_documents: :preview_images_attachments]
|
||||||
|
).call
|
||||||
|
|
||||||
|
cloned_template = Templates::Clone.call(@template, author: current_user)
|
||||||
|
cloned_template.name = File.basename(params[:files].first.original_filename, '.*')
|
||||||
|
cloned_template.save!
|
||||||
|
|
||||||
|
documents = Templates::ReplaceAttachments.call(cloned_template, params, extract_fields: true)
|
||||||
|
|
||||||
|
cloned_template.save!
|
||||||
|
|
||||||
|
Templates::CloneAttachments.call(template: cloned_template, original_template: @template,
|
||||||
|
excluded_attachment_uuids: documents.map(&:uuid))
|
||||||
|
|
||||||
|
SearchEntries.enqueue_reindex(cloned_template)
|
||||||
|
|
||||||
|
respond_to do |f|
|
||||||
|
f.html { redirect_to edit_template_path(cloned_template) }
|
||||||
|
f.json { render json: { id: cloned_template.id } }
|
||||||
|
end
|
||||||
|
rescue Templates::CreateAttachments::PdfEncrypted
|
||||||
|
respond_to do |f|
|
||||||
|
f.html { render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password) }
|
||||||
|
f.json { render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_entity }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -8,7 +8,7 @@ class TemplatesController < ApplicationController
|
|||||||
def show
|
def show
|
||||||
submissions = @template.submissions.accessible_by(current_ability)
|
submissions = @template.submissions.accessible_by(current_ability)
|
||||||
submissions = submissions.active if @template.archived_at.blank?
|
submissions = submissions.active if @template.archived_at.blank?
|
||||||
submissions = Submissions.search(submissions, params[:q], search_values: true)
|
submissions = Submissions.search(current_user, submissions, params[:q], search_values: true)
|
||||||
submissions = Submissions::Filter.call(submissions, current_user, params.except(:status))
|
submissions = Submissions::Filter.call(submissions, current_user, params.except(:status))
|
||||||
|
|
||||||
@base_submissions = submissions
|
@base_submissions = submissions
|
||||||
@@ -21,9 +21,7 @@ class TemplatesController < ApplicationController
|
|||||||
submissions.order(id: :desc)
|
submissions.order(id: :desc)
|
||||||
end
|
end
|
||||||
|
|
||||||
submissions = submissions.preload(:template_accesses) unless current_user.role.in?(%w[admin superadmin])
|
@pagy, @submissions = pagy_auto(submissions.preload(:template_accesses, submitters: :start_form_submission_events))
|
||||||
|
|
||||||
@pagy, @submissions = pagy(submissions.preload(submitters: :start_form_submission_events))
|
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
redirect_to root_path
|
redirect_to root_path
|
||||||
end
|
end
|
||||||
@@ -74,7 +72,9 @@ class TemplatesController < ApplicationController
|
|||||||
if @template.save
|
if @template.save
|
||||||
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
||||||
|
|
||||||
enqueue_template_created_webhooks(@template)
|
SearchEntries.enqueue_reindex(@template)
|
||||||
|
|
||||||
|
WebhookUrls.enqueue_events(@template, 'template.created')
|
||||||
|
|
||||||
maybe_redirect_to_template(@template)
|
maybe_redirect_to_template(@template)
|
||||||
else
|
else
|
||||||
@@ -83,9 +83,15 @@ class TemplatesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@template.update!(template_params)
|
@template.assign_attributes(template_params)
|
||||||
|
|
||||||
enqueue_template_updated_webhooks(@template)
|
is_name_changed = @template.name_changed?
|
||||||
|
|
||||||
|
@template.save!
|
||||||
|
|
||||||
|
SearchEntries.enqueue_reindex(@template) if is_name_changed
|
||||||
|
|
||||||
|
WebhookUrls.enqueue_events(@template, 'template.updated')
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
@@ -136,20 +142,6 @@ class TemplatesController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def enqueue_template_created_webhooks(template)
|
|
||||||
WebhookUrls.for_account_id(template.account_id, 'template.created').each do |webhook_url|
|
|
||||||
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => template.id,
|
|
||||||
'webhook_url_id' => webhook_url.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def enqueue_template_updated_webhooks(template)
|
|
||||||
WebhookUrls.for_account_id(template.account_id, 'template.updated').each do |webhook_url|
|
|
||||||
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => template.id,
|
|
||||||
'webhook_url_id' => webhook_url.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def load_base_template
|
def load_base_template
|
||||||
return if params[:base_template_id].blank?
|
return if params[:base_template_id].blank?
|
||||||
|
|
||||||
|
|||||||
@@ -8,14 +8,18 @@ class TemplatesDashboardController < ApplicationController
|
|||||||
TEMPLATES_PER_PAGE = 12
|
TEMPLATES_PER_PAGE = 12
|
||||||
FOLDERS_PER_PAGE = 18
|
FOLDERS_PER_PAGE = 18
|
||||||
|
|
||||||
|
helper_method :selected_order
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@template_folders = @template_folders.where(id: @templates.active.select(:folder_id)).order(id: :desc)
|
@template_folders =
|
||||||
|
TemplateFolders.filter_active_folders(@template_folders.where(parent_folder_id: nil), @templates)
|
||||||
|
|
||||||
@template_folders = TemplateFolders.search(@template_folders, params[:q])
|
@template_folders = TemplateFolders.search(@template_folders, params[:q])
|
||||||
|
@template_folders = TemplateFolders.sort(@template_folders, current_user, selected_order)
|
||||||
|
|
||||||
@pagy, @template_folders = pagy(
|
@pagy, @template_folders = pagy(
|
||||||
@template_folders,
|
@template_folders,
|
||||||
items: FOLDERS_PER_PAGE,
|
limit: FOLDERS_PER_PAGE,
|
||||||
page: @template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1
|
page: @template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,7 +27,8 @@ class TemplatesDashboardController < ApplicationController
|
|||||||
@templates = @templates.none
|
@templates = @templates.none
|
||||||
else
|
else
|
||||||
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
|
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
|
||||||
@templates = filter_templates(@templates)
|
@templates = filter_templates(@templates).preload(:author, :template_accesses)
|
||||||
|
@templates = Templates::Order.call(@templates, current_user, selected_order)
|
||||||
|
|
||||||
limit =
|
limit =
|
||||||
if @template_folders.size < 4
|
if @template_folders.size < 4
|
||||||
@@ -32,26 +37,60 @@ class TemplatesDashboardController < ApplicationController
|
|||||||
(@template_folders.size < 7 ? 9 : 6)
|
(@template_folders.size < 7 ? 9 : 6)
|
||||||
end
|
end
|
||||||
|
|
||||||
@pagy, @templates = pagy(@templates, limit:)
|
@pagy, @templates = pagy_auto(@templates, limit:)
|
||||||
|
|
||||||
|
load_related_submissions if params[:q].present? && @templates.blank?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def filter_templates(templates)
|
def filter_templates(templates)
|
||||||
rel = templates.active.preload(:author, :template_accesses).order(id: :desc)
|
rel = templates.active
|
||||||
|
|
||||||
if params[:q].blank?
|
if params[:q].blank?
|
||||||
if Docuseal.multitenant? && !current_account.testing?
|
if Docuseal.multitenant? ? current_account.testing? : current_account.linked_account_account
|
||||||
rel = rel.where(folder_id: current_account.default_template_folder.id)
|
shared_account_ids = [current_user.account_id]
|
||||||
else
|
shared_account_ids << TemplateSharing::ALL_ID if !Docuseal.multitenant? && !current_account.testing?
|
||||||
shared_template_ids =
|
|
||||||
TemplateSharing.where(account_id: [current_account.id, TemplateSharing::ALL_ID]).select(:template_id)
|
|
||||||
|
|
||||||
rel = rel.where(folder_id: current_account.default_template_folder.id).or(rel.where(id: shared_template_ids))
|
shared_template_ids = TemplateSharing.where(account_id: shared_account_ids).select(:template_id)
|
||||||
|
|
||||||
|
rel = Template.where(
|
||||||
|
Template.arel_table[:id].in(
|
||||||
|
rel.where(folder_id: current_account.default_template_folder.id).select(:id).arel
|
||||||
|
.union(:all, shared_template_ids.arel)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else
|
||||||
|
rel = rel.where(folder_id: current_account.default_template_folder.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Templates.search(rel, params[:q])
|
Templates.search(current_user, rel, params[:q])
|
||||||
|
end
|
||||||
|
|
||||||
|
def selected_order
|
||||||
|
@selected_order ||=
|
||||||
|
if cookies.permanent[:dashboard_templates_order].blank? ||
|
||||||
|
(cookies.permanent[:dashboard_templates_order] == 'used_at' && can?(:manage, :countless))
|
||||||
|
'created_at'
|
||||||
|
else
|
||||||
|
cookies.permanent[:dashboard_templates_order]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_related_submissions
|
||||||
|
@related_submissions = Submission.accessible_by(current_ability)
|
||||||
|
.left_joins(:template)
|
||||||
|
.where(archived_at: nil)
|
||||||
|
.where(templates: { archived_at: nil })
|
||||||
|
.preload(:template_accesses, :created_by_user,
|
||||||
|
template: :author,
|
||||||
|
submitters: :start_form_submission_events)
|
||||||
|
|
||||||
|
@related_submissions = Submissions.search(current_user, @related_submissions, params[:q])
|
||||||
|
.order(id: :desc)
|
||||||
|
|
||||||
|
@related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -6,7 +6,9 @@ class TemplatesFoldersController < ApplicationController
|
|||||||
def edit; end
|
def edit; end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:name])
|
name = [params[:parent_name], params[:name]].compact_blank.join(' / ')
|
||||||
|
|
||||||
|
@template.folder = TemplateFolders.find_or_create_by_name(current_user, name)
|
||||||
|
|
||||||
if @template.save
|
if @template.save
|
||||||
redirect_back(fallback_location: template_path(@template), notice: I18n.t('document_template_has_been_moved'))
|
redirect_back(fallback_location: template_path(@template), notice: I18n.t('document_template_has_been_moved'))
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class TemplatesFormPreviewController < ApplicationController
|
|||||||
|
|
||||||
@submitter.submission.submitters = @template.submitters.map { |item| Submitter.new(uuid: item['uuid']) }
|
@submitter.submission.submitters = @template.submitters.map { |item| Submitter.new(uuid: item['uuid']) }
|
||||||
|
|
||||||
Submitters.preload_with_pages(@submitter)
|
Submissions.preload_with_pages(@submitter.submission)
|
||||||
|
|
||||||
@attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments)
|
@attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments)
|
||||||
.preload(:blob).index_by(&:uuid)
|
.preload(:blob).index_by(&:uuid)
|
||||||
|
|||||||
@@ -24,14 +24,22 @@ class TemplatesPreferencesController < ApplicationController
|
|||||||
documents_copy_email_enabled documents_copy_email_attach_audit
|
documents_copy_email_enabled documents_copy_email_attach_audit
|
||||||
documents_copy_email_attach_documents documents_copy_email_reply_to
|
documents_copy_email_attach_documents documents_copy_email_reply_to
|
||||||
completed_notification_email_attach_documents
|
completed_notification_email_attach_documents
|
||||||
completed_redirect_url
|
completed_redirect_url validate_unique_submitters
|
||||||
submitters_order
|
require_all_submitters submitters_order require_phone_2fa
|
||||||
|
default_expire_at_duration shared_link_2fa
|
||||||
|
default_expire_at
|
||||||
completed_notification_email_subject completed_notification_email_body
|
completed_notification_email_subject completed_notification_email_body
|
||||||
completed_notification_email_enabled completed_notification_email_attach_audit] +
|
completed_notification_email_enabled completed_notification_email_attach_audit] +
|
||||||
[completed_message: %i[title body],
|
[completed_message: %i[title body],
|
||||||
submitters: [%i[uuid request_email_subject request_email_body]]]
|
submitters: [%i[uuid request_email_subject request_email_body]], link_form_fields: []]
|
||||||
).tap do |attrs|
|
).tap do |attrs|
|
||||||
attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1'
|
attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1'
|
||||||
|
|
||||||
|
if (default_expire_at = attrs.dig(:preferences, :default_expire_at).presence)
|
||||||
|
attrs[:preferences][:default_expire_at] =
|
||||||
|
(ActiveSupport::TimeZone[current_account.timezone] || Time.zone).parse(default_expire_at).utc
|
||||||
|
end
|
||||||
|
|
||||||
attrs[:preferences] = attrs[:preferences].transform_values do |value|
|
attrs[:preferences] = attrs[:preferences].transform_values do |value|
|
||||||
if %w[true false].include?(value)
|
if %w[true false].include?(value)
|
||||||
value == 'true'
|
value == 'true'
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesShareLinkController < ApplicationController
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize!(:update, @template)
|
||||||
|
|
||||||
|
@template.update!(template_params)
|
||||||
|
|
||||||
|
if params[:redir].present?
|
||||||
|
redirect_to params[:redir]
|
||||||
|
else
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def template_params
|
||||||
|
params.require(:template).permit(:shared_link)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -23,7 +23,9 @@ class TemplatesUploadsController < ApplicationController
|
|||||||
|
|
||||||
@template.update!(schema:)
|
@template.update!(schema:)
|
||||||
|
|
||||||
enqueue_template_created_webhooks(@template)
|
WebhookUrls.enqueue_events(@template, 'template.created')
|
||||||
|
|
||||||
|
SearchEntries.enqueue_reindex(@template)
|
||||||
|
|
||||||
redirect_to edit_template_path(@template)
|
redirect_to edit_template_path(@template)
|
||||||
rescue Templates::CreateAttachments::PdfEncrypted
|
rescue Templates::CreateAttachments::PdfEncrypted
|
||||||
@@ -66,11 +68,4 @@ class TemplatesUploadsController < ApplicationController
|
|||||||
|
|
||||||
{ files: [file] }
|
{ files: [file] }
|
||||||
end
|
end
|
||||||
|
|
||||||
def enqueue_template_created_webhooks(template)
|
|
||||||
WebhookUrls.for_account_id(template.account_id, 'template.created').each do |webhook_url|
|
|
||||||
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => template.id,
|
|
||||||
'webhook_url_id' => webhook_url.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
@@ -16,7 +16,7 @@ class UsersController < ApplicationController
|
|||||||
@users.active.where.not(role: 'integration')
|
@users.active.where.not(role: 'integration')
|
||||||
end
|
end
|
||||||
|
|
||||||
@pagy, @users = pagy(@users.where(account: current_account).order(id: :desc))
|
@pagy, @users = pagy(@users.preload(account: :account_accesses).where(account: current_account).order(id: :desc))
|
||||||
end
|
end
|
||||||
|
|
||||||
def new; end
|
def new; end
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class WebhookEventsController < ApplicationController
|
||||||
|
load_and_authorize_resource :webhook_url, parent: false, id_param: :webhook_id
|
||||||
|
before_action :load_webhook_event
|
||||||
|
|
||||||
|
def show
|
||||||
|
return unless current_ability.can?(:read, @webhook_event.record)
|
||||||
|
|
||||||
|
@data =
|
||||||
|
case @webhook_event.event_type
|
||||||
|
when 'form.started', 'form.completed', 'form.declined', 'form.viewed'
|
||||||
|
Submitters::SerializeForWebhook.call(@webhook_event.record)
|
||||||
|
when 'submission.created', 'submission.completed', 'submission.expired'
|
||||||
|
Submissions::SerializeForApi.call(@webhook_event.record)
|
||||||
|
when 'template.created', 'template.updated'
|
||||||
|
Templates::SerializeForApi.call(@webhook_event.record)
|
||||||
|
when 'submission.archived'
|
||||||
|
@webhook_event.record.as_json(only: %i[id archived_at])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def resend
|
||||||
|
id_key = WebhookUrls::EVENT_TYPE_ID_KEYS.fetch(@webhook_event.event_type.split('.').first)
|
||||||
|
|
||||||
|
last_attempt_id = @webhook_event.webhook_attempts.maximum(:id)
|
||||||
|
|
||||||
|
WebhookUrls::EVENT_TYPE_TO_JOB_CLASS[@webhook_event.event_type].perform_async(
|
||||||
|
id_key => @webhook_event.record_id,
|
||||||
|
'webhook_url_id' => @webhook_event.webhook_url_id,
|
||||||
|
'event_uuid' => @webhook_event.uuid,
|
||||||
|
'attempt' => SendWebhookRequest::MANUAL_ATTEMPT,
|
||||||
|
'last_status' => 0
|
||||||
|
)
|
||||||
|
|
||||||
|
render turbo_stream: [
|
||||||
|
turbo_stream.after(
|
||||||
|
params[:button_id],
|
||||||
|
helpers.tag.submit_form(
|
||||||
|
helpers.button_to('', refresh_settings_webhook_event_path(@webhook_url.id, @webhook_event.uuid),
|
||||||
|
params: { last_attempt_id: }),
|
||||||
|
class: 'hidden', data: { interval: 3_000 }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def refresh
|
||||||
|
return head :ok if @webhook_event.webhook_attempts.maximum(:id) == params[:last_attempt_id].to_i
|
||||||
|
|
||||||
|
render turbo_stream: [
|
||||||
|
turbo_stream.replace(helpers.dom_id(@webhook_event),
|
||||||
|
partial: 'event_row',
|
||||||
|
locals: { with_status: true, webhook_url: @webhook_url, webhook_event: @webhook_event }),
|
||||||
|
turbo_stream.replace(helpers.dom_id(@webhook_event, :drawer_events),
|
||||||
|
partial: 'drawer_events',
|
||||||
|
locals: { webhook_url: @webhook_url, webhook_event: @webhook_event })
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_webhook_event
|
||||||
|
@webhook_event = @webhook_url.webhook_events.find_by!(uuid: params[:id])
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -8,10 +8,26 @@ class WebhookSettingsController < ApplicationController
|
|||||||
@webhook_urls = @webhook_urls.order(id: :desc)
|
@webhook_urls = @webhook_urls.order(id: :desc)
|
||||||
@webhook_url = @webhook_urls.first_or_initialize
|
@webhook_url = @webhook_urls.first_or_initialize
|
||||||
|
|
||||||
render @webhook_urls.size > 1 ? 'index' : 'show'
|
if @webhook_urls.size > 1
|
||||||
|
render :index
|
||||||
|
else
|
||||||
|
@webhook_events = @webhook_url.webhook_events
|
||||||
|
|
||||||
|
@webhook_events = @webhook_events.where(status: params[:status]) if %w[success error].include?(params[:status])
|
||||||
|
|
||||||
|
@pagy, @webhook_events = pagy_countless(@webhook_events.order(id: :desc))
|
||||||
|
|
||||||
|
render :show
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def show; end
|
def show
|
||||||
|
@webhook_events = @webhook_url.webhook_events
|
||||||
|
|
||||||
|
@webhook_events = @webhook_events.where(status: params[:status]) if %w[success error].include?(params[:status])
|
||||||
|
|
||||||
|
@pagy, @webhook_events = pagy_countless(@webhook_events.order(id: :desc))
|
||||||
|
end
|
||||||
|
|
||||||
def new; end
|
def new; end
|
||||||
|
|
||||||
@@ -37,13 +53,18 @@ class WebhookSettingsController < ApplicationController
|
|||||||
def resend
|
def resend
|
||||||
submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last
|
submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last
|
||||||
|
|
||||||
|
authorize!(:read, submitter)
|
||||||
|
|
||||||
if submitter.blank? || @webhook_url.blank?
|
if submitter.blank? || @webhook_url.blank?
|
||||||
return redirect_back(fallback_location: settings_webhooks_path,
|
return redirect_back(fallback_location: settings_webhooks_path,
|
||||||
alert: I18n.t('unable_to_resend_webhook_request'))
|
alert: I18n.t('unable_to_resend_webhook_request'))
|
||||||
end
|
end
|
||||||
|
|
||||||
SendFormCompletedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
|
SendTestWebhookRequestJob.perform_async(
|
||||||
'webhook_url_id' => @webhook_url.id)
|
'submitter_id' => submitter.id,
|
||||||
|
'event_uuid' => SecureRandom.uuid,
|
||||||
|
'webhook_url_id' => @webhook_url.id
|
||||||
|
)
|
||||||
|
|
||||||
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_request_has_been_sent'))
|
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_request_has_been_sent'))
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import SubmitForm from './elements/submit_form'
|
|||||||
import PromptPassword from './elements/prompt_password'
|
import PromptPassword from './elements/prompt_password'
|
||||||
import EmailsTextarea from './elements/emails_textarea'
|
import EmailsTextarea from './elements/emails_textarea'
|
||||||
import ToggleOnSubmit from './elements/toggle_on_submit'
|
import ToggleOnSubmit from './elements/toggle_on_submit'
|
||||||
|
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'
|
||||||
import ToggleAttribute from './elements/toggle_attribute'
|
import ToggleAttribute from './elements/toggle_attribute'
|
||||||
@@ -33,6 +34,9 @@ import MaskedInput from './elements/masked_input'
|
|||||||
import SetDateButton from './elements/set_date_button'
|
import SetDateButton from './elements/set_date_button'
|
||||||
import IndeterminateCheckbox from './elements/indeterminate_checkbox'
|
import IndeterminateCheckbox from './elements/indeterminate_checkbox'
|
||||||
import AppTour from './elements/app_tour'
|
import AppTour from './elements/app_tour'
|
||||||
|
import DashboardDropzone from './elements/dashboard_dropzone'
|
||||||
|
import RequiredCheckboxGroup from './elements/required_checkbox_group'
|
||||||
|
import PageContainer from './elements/page_container'
|
||||||
|
|
||||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||||
|
|
||||||
@@ -101,6 +105,10 @@ safeRegisterElement('masked-input', MaskedInput)
|
|||||||
safeRegisterElement('set-date-button', SetDateButton)
|
safeRegisterElement('set-date-button', SetDateButton)
|
||||||
safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox)
|
safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox)
|
||||||
safeRegisterElement('app-tour', AppTour)
|
safeRegisterElement('app-tour', AppTour)
|
||||||
|
safeRegisterElement('dashboard-dropzone', DashboardDropzone)
|
||||||
|
safeRegisterElement('check-on-click', CheckOnClick)
|
||||||
|
safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup)
|
||||||
|
safeRegisterElement('page-container', PageContainer)
|
||||||
|
|
||||||
safeRegisterElement('template-builder', class extends HTMLElement {
|
safeRegisterElement('template-builder', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
@@ -125,6 +133,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
|||||||
withSendButton: this.dataset.withSendButton !== 'false',
|
withSendButton: this.dataset.withSendButton !== 'false',
|
||||||
withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false',
|
withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false',
|
||||||
withConditions: this.dataset.withConditions === 'true',
|
withConditions: this.dataset.withConditions === 'true',
|
||||||
|
withReplaceAndCloneUpload: 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'
|
||||||
|
|||||||
@@ -147,3 +147,11 @@ button[disabled] .enabled {
|
|||||||
outline-offset: 3px;
|
outline-offset: 3px;
|
||||||
outline-color: hsl(var(--bc) / 0.2);
|
outline-color: hsl(var(--bc) / 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.font-times {
|
||||||
|
font-family: "Times New Roman", Times, ui-serif, serif, Cambria, Georgia;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-courier {
|
||||||
|
font-family: "Courier New", Consolas, "Liberation Mono", monospace, ui-monospace, SFMono-Regular, Menlo, Monaco;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.addEventListener('click', () => {
|
||||||
|
if (this.element && !this.element.disabled && !this.element.checked) {
|
||||||
|
this.element.checked = true
|
||||||
|
this.element.dispatchEvent(new Event('change', { bubbles: true }))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get element () {
|
||||||
|
return document.getElementById(this.dataset.elementId)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,6 @@ export default class extends HTMLElement {
|
|||||||
this.clearChecked()
|
this.clearChecked()
|
||||||
|
|
||||||
this.addEventListener('click', (e) => {
|
this.addEventListener('click', (e) => {
|
||||||
e.stopPropagation()
|
|
||||||
|
|
||||||
const text = this.dataset.text || this.innerText.trim()
|
const text = this.dataset.text || this.innerText.trim()
|
||||||
|
|
||||||
if (navigator.clipboard) {
|
if (navigator.clipboard) {
|
||||||
|
|||||||
@@ -0,0 +1,248 @@
|
|||||||
|
import { target, targets, targetable } from '@github/catalyst/lib/targetable'
|
||||||
|
|
||||||
|
const loadingIconHtml = `<svg xmlns="http://www.w3.org/2000/svg" class="animate-spin" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M12 3a9 9 0 1 0 9 9" />
|
||||||
|
</svg>`
|
||||||
|
|
||||||
|
export default targetable(class extends HTMLElement {
|
||||||
|
static [targets.static] = [
|
||||||
|
'hiddenOnDrag',
|
||||||
|
'folderCards',
|
||||||
|
'templateCards'
|
||||||
|
]
|
||||||
|
|
||||||
|
static [target.static] = [
|
||||||
|
'form',
|
||||||
|
'fileDropzone',
|
||||||
|
'folderDropzone',
|
||||||
|
'fileDropzoneLoading'
|
||||||
|
]
|
||||||
|
|
||||||
|
connectedCallback () {
|
||||||
|
document.addEventListener('drop', this.onWindowDragdrop)
|
||||||
|
document.addEventListener('dragover', this.onWindowDropover)
|
||||||
|
|
||||||
|
window.addEventListener('dragleave', this.onWindowDragleave)
|
||||||
|
|
||||||
|
this.fileDropzone?.addEventListener('drop', this.onDropFile)
|
||||||
|
this.folderDropzone?.addEventListener('drop', this.onDropNewFolder)
|
||||||
|
|
||||||
|
this.folderCards.forEach((el) => el.addEventListener('drop', (e) => this.onDropFolder(e, el)))
|
||||||
|
this.templateCards.forEach((el) => el.addEventListener('drop', this.onDropTemplate))
|
||||||
|
this.templateCards.forEach((el) => el.addEventListener('dragstart', this.onTemplateDragStart))
|
||||||
|
|
||||||
|
return [this.fileDropzone, this.folderDropzone, ...this.folderCards, ...this.templateCards].forEach((el) => {
|
||||||
|
el?.addEventListener('dragover', this.onDragover)
|
||||||
|
el?.addEventListener('dragleave', this.onDragleave)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
document.removeEventListener('drop', this.onWindowDragdrop)
|
||||||
|
document.removeEventListener('dragover', this.onWindowDropover)
|
||||||
|
|
||||||
|
window.removeEventListener('dragleave', this.onWindowDragleave)
|
||||||
|
}
|
||||||
|
|
||||||
|
onTemplateDragStart = (e) => {
|
||||||
|
const id = e.target.href.split('/').pop()
|
||||||
|
|
||||||
|
this.folderCards.forEach((el) => el.classList.remove('bg-base-200', 'before:hidden'))
|
||||||
|
this.folderDropzone?.classList?.remove('hidden')
|
||||||
|
window.flash?.remove()
|
||||||
|
|
||||||
|
e.dataTransfer.effectAllowed = 'move'
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
e.dataTransfer.setData('template_id', id)
|
||||||
|
|
||||||
|
const dragPreview = e.target.cloneNode(true)
|
||||||
|
const rect = e.target.getBoundingClientRect()
|
||||||
|
|
||||||
|
const height = e.target.children[0].getBoundingClientRect().height + 50
|
||||||
|
|
||||||
|
dragPreview.children[1].remove()
|
||||||
|
dragPreview.style.width = `${rect.width}px`
|
||||||
|
dragPreview.style.height = `${height}px`
|
||||||
|
dragPreview.style.position = 'absolute'
|
||||||
|
dragPreview.style.top = '-1000px'
|
||||||
|
dragPreview.style.pointerEvents = 'none'
|
||||||
|
dragPreview.style.opacity = '0.9'
|
||||||
|
|
||||||
|
document.body.appendChild(dragPreview)
|
||||||
|
|
||||||
|
e.dataTransfer.setDragImage(dragPreview, rect.width / 2, height / 2)
|
||||||
|
|
||||||
|
setTimeout(() => document.body.removeChild(dragPreview), 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDropFile = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.fileDropzoneLoading.classList.remove('hidden')
|
||||||
|
this.fileDropzoneLoading.previousElementSibling.classList.add('hidden')
|
||||||
|
this.fileDropzoneLoading.classList.add('opacity-50')
|
||||||
|
|
||||||
|
this.uploadFiles(e.dataTransfer.files, '/templates_upload')
|
||||||
|
}
|
||||||
|
|
||||||
|
onDropFolder = (e, el) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const templateId = e.dataTransfer.getData('template_id')
|
||||||
|
|
||||||
|
if (e.dataTransfer.files.length || templateId) {
|
||||||
|
const loading = document.createElement('div')
|
||||||
|
const svg = el.querySelector('svg')
|
||||||
|
|
||||||
|
loading.innerHTML = loadingIconHtml
|
||||||
|
loading.children[0].classList.add(...svg.classList)
|
||||||
|
|
||||||
|
el.replaceChild(loading.children[0], svg)
|
||||||
|
el.classList.add('opacity-50')
|
||||||
|
|
||||||
|
if (e.dataTransfer.files.length) {
|
||||||
|
const params = new URLSearchParams({ folder_name: el.innerText.trim() }).toString()
|
||||||
|
|
||||||
|
this.uploadFiles(e.dataTransfer.files, `/templates_upload?${params}`)
|
||||||
|
} else {
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
formData.append('name', el.dataset.fullName)
|
||||||
|
|
||||||
|
fetch(`/templates/${templateId}/folder`, {
|
||||||
|
method: 'PUT',
|
||||||
|
redirect: 'manual',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
window.Turbo.cache.clear()
|
||||||
|
window.Turbo.visit(location.href)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDropTemplate = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (e.dataTransfer.files.length) {
|
||||||
|
const loading = document.createElement('div')
|
||||||
|
loading.classList.add('bottom-5', 'left-0', 'flex', 'justify-center', 'w-full', 'absolute')
|
||||||
|
loading.innerHTML = loadingIconHtml
|
||||||
|
|
||||||
|
e.target.appendChild(loading)
|
||||||
|
e.target.classList.add('opacity-50')
|
||||||
|
|
||||||
|
const id = e.target.href.split('/').pop()
|
||||||
|
|
||||||
|
this.uploadFiles(e.dataTransfer.files, `/templates/${id}/clone_and_replace`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onWindowDragdrop = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (!this.isLoading) this.hideDraghover()
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadFiles (files, url) {
|
||||||
|
this.isLoading = true
|
||||||
|
|
||||||
|
this.form.action = url
|
||||||
|
|
||||||
|
this.form.querySelector('[type="file"]').files = files
|
||||||
|
|
||||||
|
this.form.querySelector('[type="submit"]').click()
|
||||||
|
}
|
||||||
|
|
||||||
|
onWindowDropover = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (e.dataTransfer?.types?.includes('Files')) {
|
||||||
|
this.showDraghover()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDragover (e) {
|
||||||
|
if (e.dataTransfer?.types?.includes('Files') || this.dataset.targets !== 'dashboard-dropzone.templateCards') {
|
||||||
|
this.style.backgroundColor = '#F7F3F0'
|
||||||
|
|
||||||
|
if (this.classList.contains('before:border-base-300')) {
|
||||||
|
this.classList.remove('before:border-base-300')
|
||||||
|
this.classList.add('before:border-base-content/30')
|
||||||
|
} else if (this.classList.contains('border-base-300')) {
|
||||||
|
this.classList.remove('border-base-300')
|
||||||
|
this.classList.add('border-base-content/30')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDropNewFolder (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const templateId = e.dataTransfer.getData('template_id')
|
||||||
|
|
||||||
|
const a = document.createElement('a')
|
||||||
|
|
||||||
|
a.href = `/templates/${templateId}/folder/edit?autocomplete=false`
|
||||||
|
a.dataset.turboFrame = 'modal'
|
||||||
|
a.classList.add('hidden')
|
||||||
|
|
||||||
|
document.body.append(a)
|
||||||
|
|
||||||
|
a.click()
|
||||||
|
|
||||||
|
a.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
onDragleave () {
|
||||||
|
this.style.backgroundColor = null
|
||||||
|
|
||||||
|
if (this.classList.contains('before:border-base-content/30')) {
|
||||||
|
this.classList.remove('before:border-base-content/30')
|
||||||
|
this.classList.add('before:border-base-300')
|
||||||
|
} else if (this.classList.contains('border-base-content/30')) {
|
||||||
|
this.classList.remove('border-base-content/30')
|
||||||
|
this.classList.add('border-base-300')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onWindowDragleave = (e) => {
|
||||||
|
if (e.clientX <= 0 || e.clientY <= 0 || e.clientX >= window.innerWidth || e.clientY >= window.innerHeight) {
|
||||||
|
this.hideDraghover()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showDraghover = () => {
|
||||||
|
if (this.isDrag) return
|
||||||
|
|
||||||
|
this.isDrag = true
|
||||||
|
|
||||||
|
window.flash?.remove()
|
||||||
|
this.fileDropzone?.classList?.remove('hidden')
|
||||||
|
|
||||||
|
this.hiddenOnDrag.forEach((el) => { el.style.display = 'none' })
|
||||||
|
|
||||||
|
return [...this.folderCards, ...this.templateCards].forEach((el) => {
|
||||||
|
el.classList.remove('bg-base-200', 'before:hidden')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
hideDraghover = () => {
|
||||||
|
this.isDrag = false
|
||||||
|
|
||||||
|
this.fileDropzone?.classList?.add('hidden')
|
||||||
|
this.folderDropzone?.classList?.add('hidden')
|
||||||
|
|
||||||
|
this.hiddenOnDrag.forEach((el) => { el.style.display = null })
|
||||||
|
|
||||||
|
return [...this.folderCards, ...this.templateCards].forEach((el) => {
|
||||||
|
el.classList.add('bg-base-200', 'before:hidden')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.form.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.submit()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.dataset.onload === 'true') {
|
||||||
|
this.form.querySelector('button').click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
submit () {
|
||||||
|
fetch(this.form.action, {
|
||||||
|
method: this.form.method,
|
||||||
|
body: new FormData(this.form)
|
||||||
|
}).then(async (resp) => {
|
||||||
|
if (!resp.ok) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(await resp.text())
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
alert(data.error)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get form () {
|
||||||
|
return this.querySelector('form')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,19 +5,37 @@ export default actionable(targetable(class extends HTMLElement {
|
|||||||
static [target.static] = [
|
static [target.static] = [
|
||||||
'loading',
|
'loading',
|
||||||
'icon',
|
'icon',
|
||||||
'input'
|
'input',
|
||||||
|
'area'
|
||||||
]
|
]
|
||||||
|
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.addEventListener('drop', this.onDrop)
|
|
||||||
|
|
||||||
this.addEventListener('dragover', (e) => e.preventDefault())
|
this.addEventListener('dragover', (e) => e.preventDefault())
|
||||||
|
this.addEventListener('drop', this.onDrop)
|
||||||
document.addEventListener('turbo:submit-end', this.toggleLoading)
|
document.addEventListener('turbo:submit-end', this.toggleLoading)
|
||||||
|
this.area?.addEventListener('dragover', this.onDragover)
|
||||||
|
this.area?.addEventListener('dragleave', this.onDragleave)
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback () {
|
disconnectedCallback () {
|
||||||
|
this.removeEventListener('drop', this.onDrop)
|
||||||
document.removeEventListener('turbo:submit-end', this.toggleLoading)
|
document.removeEventListener('turbo:submit-end', this.toggleLoading)
|
||||||
|
this.area?.removeEventListener('dragover', this.onDragover)
|
||||||
|
this.area?.removeEventListener('dragleave', this.onDragleave)
|
||||||
|
}
|
||||||
|
|
||||||
|
onDragover (e) {
|
||||||
|
if (e.dataTransfer?.types?.includes('Files')) {
|
||||||
|
this.style.backgroundColor = '#F7F3F0'
|
||||||
|
this.classList.remove('border-base-300', 'hover:bg-base-200/30')
|
||||||
|
this.classList.add('border-base-content/30')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDragleave () {
|
||||||
|
this.style.backgroundColor = null
|
||||||
|
this.classList.remove('border-base-content/30')
|
||||||
|
this.classList.add('border-base-300', 'hover:bg-base-200/30')
|
||||||
}
|
}
|
||||||
|
|
||||||
onDrop (e) {
|
onDrop (e) {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import autocomplete from 'autocompleter'
|
|||||||
|
|
||||||
export default class extends HTMLElement {
|
export default class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
|
if (this.dataset.enabled === 'false') return
|
||||||
|
|
||||||
autocomplete({
|
autocomplete({
|
||||||
input: this.input,
|
input: this.input,
|
||||||
preventSubmit: this.dataset.submitOnSelect === 'true' ? 0 : 1,
|
preventSubmit: this.dataset.submitOnSelect === 'true' ? 0 : 1,
|
||||||
@@ -14,12 +16,16 @@ export default class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onSelect = (item) => {
|
onSelect = (item) => {
|
||||||
this.input.value = item.name
|
this.input.value = this.dataset.parentName ? item.name : item.full_name
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch = (text, resolve) => {
|
fetch = (text, resolve) => {
|
||||||
const queryParams = new URLSearchParams({ q: text })
|
const queryParams = new URLSearchParams({ q: text })
|
||||||
|
|
||||||
|
if (this.dataset.parentName) {
|
||||||
|
queryParams.append('parent_name', this.dataset.parentName)
|
||||||
|
}
|
||||||
|
|
||||||
fetch('/template_folders_autocomplete?' + queryParams).then(async (resp) => {
|
fetch('/template_folders_autocomplete?' + queryParams).then(async (resp) => {
|
||||||
const items = await resp.json()
|
const items = await resp.json()
|
||||||
|
|
||||||
@@ -34,7 +40,7 @@ export default class extends HTMLElement {
|
|||||||
|
|
||||||
div.setAttribute('dir', 'auto')
|
div.setAttribute('dir', 'auto')
|
||||||
|
|
||||||
div.textContent = item.name
|
div.textContent = this.dataset.parentName ? item.name : item.full_name
|
||||||
|
|
||||||
return div
|
return div
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
const image = this.querySelector('img')
|
||||||
|
|
||||||
|
image.addEventListener('load', (e) => {
|
||||||
|
image.setAttribute('width', e.target.naturalWidth)
|
||||||
|
image.setAttribute('height', e.target.naturalHeight)
|
||||||
|
|
||||||
|
this.style.aspectRatio = `${e.target.naturalWidth} / ${e.target.naturalHeight}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
|
||||||
|
checkbox.addEventListener('change', this.handleChange)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange = () => {
|
||||||
|
if (this.checkedCount !== 0) {
|
||||||
|
this.closest('form')?.requestSubmit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get checkedCount () {
|
||||||
|
return this.querySelectorAll('input[type="checkbox"]:checked').length
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.header = document.querySelector('#signing_form_header')
|
||||||
|
|
||||||
|
window.addEventListener('scroll', this.onScroll)
|
||||||
|
window.addEventListener('resize', this.onResize)
|
||||||
|
|
||||||
|
if (!this.isNarrow() && this.isHeaderNotVisible()) {
|
||||||
|
this.showButtons({ animate: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
window.removeEventListener('scroll', this.onScroll)
|
||||||
|
window.removeEventListener('resize', this.onResize)
|
||||||
|
}
|
||||||
|
|
||||||
|
onResize = () => {
|
||||||
|
if (this.isNarrow()) {
|
||||||
|
this.hideButtons(true)
|
||||||
|
} else if (this.isHeaderNotVisible()) {
|
||||||
|
this.showButtons()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isNarrow () {
|
||||||
|
return window.innerWidth < 1230
|
||||||
|
}
|
||||||
|
|
||||||
|
onScroll = () => {
|
||||||
|
if (this.isHeaderNotVisible() && !this.isNarrow()) {
|
||||||
|
this.showButtons()
|
||||||
|
} else {
|
||||||
|
this.hideButtons()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isHeaderNotVisible () {
|
||||||
|
const rect = this.header.getBoundingClientRect()
|
||||||
|
return rect.bottom <= 0 || rect.top >= window.innerHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
showButtons ({ animate } = { animate: true }) {
|
||||||
|
if (animate) {
|
||||||
|
this.classList.add('transition-transform', 'duration-300')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.classList.remove('hidden', '-translate-y-10', 'opacity-0')
|
||||||
|
this.classList.add('translate-y-0', 'opacity-100')
|
||||||
|
}
|
||||||
|
|
||||||
|
hideButtons () {
|
||||||
|
this.classList.remove('translate-y-0', 'opacity-100')
|
||||||
|
this.classList.add('-translate-y-10', 'opacity-0')
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.classList.contains('-translate-y-10')) {
|
||||||
|
this.classList.add('hidden')
|
||||||
|
}
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,17 @@
|
|||||||
export default class extends HTMLElement {
|
export default class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.querySelector('form').requestSubmit()
|
if (this.dataset.interval) {
|
||||||
|
this.interval = setInterval(() => {
|
||||||
|
this.querySelector('form').requestSubmit()
|
||||||
|
}, parseInt(this.dataset.interval))
|
||||||
|
} else {
|
||||||
|
this.querySelector('form').requestSubmit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
if (this.interval) {
|
||||||
|
clearInterval(this.interval)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,9 @@ export default class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fetch = (text, resolve) => {
|
fetch = (text, resolve) => {
|
||||||
const q = text.split(/[;,\s]+/).pop().trim()
|
const q = this.dataset.field === 'email'
|
||||||
|
? text.split(/[;,\s]+/).pop().trim()
|
||||||
|
: text
|
||||||
|
|
||||||
if (q) {
|
if (q) {
|
||||||
const queryParams = new URLSearchParams({ q, field: this.dataset.field })
|
const queryParams = new URLSearchParams({ q, field: this.dataset.field })
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export default actionable(class extends HTMLElement {
|
|||||||
const elementIds = JSON.parse(this.dataset.elementIds)
|
const elementIds = JSON.parse(this.dataset.elementIds)
|
||||||
|
|
||||||
elementIds.forEach((elementId) => {
|
elementIds.forEach((elementId) => {
|
||||||
document.getElementById(elementId).classList.toggle('hidden', event.target.value !== elementId)
|
document.getElementById(elementId).classList.toggle('hidden', (event.target.dataset.toggleId || event.target.value) !== elementId)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -3,11 +3,17 @@ import { createApp, reactive } from 'vue'
|
|||||||
import Form from './submission_form/form'
|
import Form from './submission_form/form'
|
||||||
import DownloadButton from './elements/download_button'
|
import DownloadButton from './elements/download_button'
|
||||||
import ToggleSubmit from './elements/toggle_submit'
|
import ToggleSubmit from './elements/toggle_submit'
|
||||||
|
import FetchForm from './elements/fetch_form'
|
||||||
|
import ScrollButtons from './elements/scroll_buttons'
|
||||||
|
import PageContainer from './elements/page_container'
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
safeRegisterElement('download-button', DownloadButton)
|
safeRegisterElement('download-button', DownloadButton)
|
||||||
safeRegisterElement('toggle-submit', ToggleSubmit)
|
safeRegisterElement('toggle-submit', ToggleSubmit)
|
||||||
|
safeRegisterElement('fetch-form', FetchForm)
|
||||||
|
safeRegisterElement('scroll-buttons', ScrollButtons)
|
||||||
|
safeRegisterElement('page-container', PageContainer)
|
||||||
safeRegisterElement('submission-form', class extends HTMLElement {
|
safeRegisterElement('submission-form', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.appElem = document.createElement('div')
|
this.appElem = document.createElement('div')
|
||||||
|
|||||||
@@ -70,3 +70,11 @@ button[disabled] .enabled {
|
|||||||
.base-radio {
|
.base-radio {
|
||||||
@apply radio bg-white radio-sm;
|
@apply radio bg-white radio-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.font-times {
|
||||||
|
font-family: "Times New Roman", Times, ui-serif, serif, Cambria, Georgia;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-courier {
|
||||||
|
font-family: "Courier New", Consolas, "Liberation Mono", monospace, ui-monospace, SFMono-Regular, Menlo, Monaco;
|
||||||
|
}
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
<div
|
<div
|
||||||
class="flex absolute lg:text-base -outline-offset-1 field-area"
|
class="flex absolute lg:text-base -outline-offset-1 field-area"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
:style="computedStyle"
|
:style="[computedStyle, fontStyle]"
|
||||||
:class="{ 'text-[1.6vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'outline-red-500 outline-dashed outline-2 z-10 field-area-active': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
|
:class="{ 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'outline-red-500 outline-dashed outline-2 z-10 field-area-active': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="(!withFieldPlaceholder || !field.name || field.type === 'cells') && !isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
|
v-if="(!withFieldPlaceholder || !field.name || field.type === 'cells') && !isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="isActive && withLabel && (!area.option_uuid || !option.value)"
|
v-if="isActive && withLabel && (!area.option_uuid || !option.value)"
|
||||||
class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap pointer-events-none"
|
class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap pointer-events-none field-area-active-label"
|
||||||
>
|
>
|
||||||
<template v-if="area.option_uuid && !option.value">
|
<template v-if="area.option_uuid && !option.value">
|
||||||
{{ optionValue(option) }}
|
{{ optionValue(option) }}
|
||||||
@@ -55,10 +55,12 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-else-if="field.type === 'signature' && signature"
|
v-else-if="field.type === 'signature' && signature"
|
||||||
class="flex flex-col justify-between h-full overflow-hidden"
|
class="flex justify-between h-full gap-1 overflow-hidden w-full"
|
||||||
|
:class="isNarrow && (isShowSignatureId || field.preferences?.reason_field_uuid) ? 'flex-row' : 'flex-col'"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex-grow flex overflow-hidden"
|
class="flex overflow-hidden"
|
||||||
|
:class="isNarrow && (isShowSignatureId || field.preferences?.reason_field_uuid) ? 'w-1/2' : 'flex-grow'"
|
||||||
style="min-height: 50%"
|
style="min-height: 50%"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -67,8 +69,9 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="withSignatureId"
|
v-if="isShowSignatureId || field.preferences?.reason_field_uuid"
|
||||||
class="w-full mt-1 text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem]"
|
class="text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem]"
|
||||||
|
:class="isNarrow ? 'w-1/2' : 'w-full'"
|
||||||
>
|
>
|
||||||
<div class="truncate uppercase">
|
<div class="truncate uppercase">
|
||||||
ID: {{ signature.uuid }}
|
ID: {{ signature.uuid }}
|
||||||
@@ -183,7 +186,7 @@
|
|||||||
v-else
|
v-else
|
||||||
ref="textContainer"
|
ref="textContainer"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="flex items-center px-0.5 w-full"
|
class="flex px-0.5 w-full"
|
||||||
:class="{ ...alignClasses, ...fontClasses }"
|
:class="{ ...alignClasses, ...fontClasses }"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -233,6 +236,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
isInlineSize: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
submitter: {
|
submitter: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -325,15 +333,25 @@ export default {
|
|||||||
verification: this.t('verify_id')
|
verification: this.t('verify_id')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
isShowSignatureId () {
|
||||||
|
if ([true, false].includes(this.field.preferences?.with_signature_id)) {
|
||||||
|
return this.field.preferences.with_signature_id
|
||||||
|
} else {
|
||||||
|
return this.withSignatureId
|
||||||
|
}
|
||||||
|
},
|
||||||
alignClasses () {
|
alignClasses () {
|
||||||
if (!this.field.preferences) {
|
if (!this.field.preferences) {
|
||||||
return {}
|
return { 'items-center': true }
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'text-center': this.field.preferences.align === 'center',
|
'text-center': this.field.preferences.align === 'center',
|
||||||
'text-left': this.field.preferences.align === 'left',
|
'text-left': this.field.preferences.align === 'left',
|
||||||
'text-right': this.field.preferences.align === 'right'
|
'text-right': this.field.preferences.align === 'right',
|
||||||
|
'items-center': !this.field.preferences.valign || this.field.preferences.valign === 'center',
|
||||||
|
'items-start': this.field.preferences.valign === 'top',
|
||||||
|
'items-end': this.field.preferences.valign === 'bottom'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fontClasses () {
|
fontClasses () {
|
||||||
@@ -342,8 +360,8 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'font-mono': this.field.preferences.font === 'Courier',
|
'font-courier': this.field.preferences.font === 'Courier',
|
||||||
'font-serif': this.field.preferences.font === 'Times',
|
'font-times': this.field.preferences.font === 'Times',
|
||||||
'font-bold': ['bold_italic', 'bold'].includes(this.field.preferences.font_type),
|
'font-bold': ['bold_italic', 'bold'].includes(this.field.preferences.font_type),
|
||||||
italic: ['bold_italic', 'italic'].includes(this.field.preferences.font_type)
|
italic: ['bold_italic', 'italic'].includes(this.field.preferences.font_type)
|
||||||
}
|
}
|
||||||
@@ -421,6 +439,34 @@ export default {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
fontStyle () {
|
||||||
|
let fontSize = ''
|
||||||
|
|
||||||
|
if (this.isInlineSize) {
|
||||||
|
if (this.textOverflowChars) {
|
||||||
|
fontSize = `${this.fontSizePx / 1.5 / 10}cqmin`
|
||||||
|
} else {
|
||||||
|
fontSize = `${this.fontSizePx / 10}cqmin`
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.textOverflowChars) {
|
||||||
|
fontSize = `clamp(1pt, ${this.fontSizePx / 1.5 / 10}vw, ${this.fontSizePx / 1.5}px)`
|
||||||
|
} else {
|
||||||
|
fontSize = `clamp(1pt, ${this.fontSizePx / 10}vw, ${this.fontSizePx}px)`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { fontSize, lineHeight: `calc(${fontSize} * ${this.lineHeight})` }
|
||||||
|
},
|
||||||
|
fontSizePx () {
|
||||||
|
return parseInt(this.field?.preferences?.font_size || 11) * this.fontScale
|
||||||
|
},
|
||||||
|
lineHeight () {
|
||||||
|
return 1.3
|
||||||
|
},
|
||||||
|
fontScale () {
|
||||||
|
return 1000 / 612.0
|
||||||
|
},
|
||||||
computedStyle () {
|
computedStyle () {
|
||||||
const { x, y, w, h } = this.area
|
const { x, y, w, h } = this.area
|
||||||
|
|
||||||
@@ -431,23 +477,21 @@ export default {
|
|||||||
height: h * 100 + '%'
|
height: h * 100 + '%'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.field.preferences?.font_size) {
|
|
||||||
style.fontSize = `clamp(4pt, 1.6vw, ${parseInt(this.field.preferences.font_size) * 1.23}pt)`
|
|
||||||
style.lineHeight = `clamp(6pt, 2.0vw, ${parseInt(this.field.preferences.font_size) * 1.23 + 3}pt)`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.field.preferences?.color) {
|
if (this.field.preferences?.color) {
|
||||||
style.color = this.field.preferences.color
|
style.color = this.field.preferences.color
|
||||||
}
|
}
|
||||||
|
|
||||||
return style
|
return style
|
||||||
|
},
|
||||||
|
isNarrow () {
|
||||||
|
return this.area.h > 0 && (this.area.w / this.area.h) > 6
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
modelValue () {
|
modelValue () {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (['date', 'text', 'number'].includes(this.field.type) && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.modelValue}`.length)) {
|
if (['date', 'text', 'number'].includes(this.field.type) && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.modelValue}`.length)) {
|
||||||
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? `${this.modelValue || (this.withFieldPlaceholder ? this.field.name : '')}`.length : 0
|
this.textOverflowChars = this.$refs.textContainer.scrollHeight > (this.$refs.textContainer.clientHeight + 1) ? `${this.modelValue || (this.withFieldPlaceholder ? this.field.name : '')}`.length : 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -455,7 +499,7 @@ export default {
|
|||||||
mounted () {
|
mounted () {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (['date', 'text', 'number'].includes(this.field.type) && this.$refs.textContainer) {
|
if (['date', 'text', 'number'].includes(this.field.type) && this.$refs.textContainer) {
|
||||||
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? `${this.modelValue || (this.withFieldPlaceholder ? this.field.name : '')}`.length : 0
|
this.textOverflowChars = this.$refs.textContainer.scrollHeight > (this.$refs.textContainer.clientHeight + 1) ? `${this.modelValue || (this.withFieldPlaceholder ? this.field.name : '')}`.length : 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
:area="area"
|
:area="area"
|
||||||
:submittable="submittable"
|
:submittable="submittable"
|
||||||
:field-index="fieldIndex"
|
:field-index="fieldIndex"
|
||||||
|
:is-inline-size="isInlineSize"
|
||||||
:scroll-padding="scrollPadding"
|
:scroll-padding="scrollPadding"
|
||||||
:submitter="submitter"
|
:submitter="submitter"
|
||||||
:with-field-placeholder="withFieldPlaceholder"
|
:with-field-placeholder="withFieldPlaceholder"
|
||||||
@@ -110,6 +111,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isInlineSize () {
|
||||||
|
return CSS.supports('container-type: size')
|
||||||
|
},
|
||||||
isMobileContainer () {
|
isMobileContainer () {
|
||||||
const root = this.$root.$el.parentNode.getRootNode()
|
const root = this.$root.$el.parentNode.getRootNode()
|
||||||
const container = root.body || root.querySelector('div')
|
const container = root.body || root.querySelector('div')
|
||||||
|
|||||||
@@ -193,9 +193,9 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.decline_button) {
|
document.querySelectorAll('#decline_button').forEach((button) => {
|
||||||
window.decline_button.setAttribute('disabled', 'true')
|
button.setAttribute('disabled', 'true')
|
||||||
}
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
sendCopyToEmail () {
|
sendCopyToEmail () {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
function cropCanvasAndExportToPNG (canvas) {
|
function cropCanvasAndExportToPNG (canvas, { errorOnTooSmall } = { errorOnTooSmall: false }) {
|
||||||
const ctx = canvas.getContext('2d')
|
const ctx = canvas.getContext('2d')
|
||||||
|
|
||||||
const width = canvas.width
|
const width = canvas.width
|
||||||
@@ -33,6 +33,10 @@ function cropCanvasAndExportToPNG (canvas) {
|
|||||||
croppedCanvas.height = croppedHeight
|
croppedCanvas.height = croppedHeight
|
||||||
const croppedCtx = croppedCanvas.getContext('2d')
|
const croppedCtx = croppedCanvas.getContext('2d')
|
||||||
|
|
||||||
|
if (errorOnTooSmall && (croppedWidth < 20 || croppedHeight < 20)) {
|
||||||
|
return Promise.reject(new Error('Image too small'))
|
||||||
|
}
|
||||||
|
|
||||||
croppedCtx.drawImage(canvas, leftmost, topmost, croppedWidth, croppedHeight, 0, 0, croppedWidth, croppedHeight)
|
croppedCtx.drawImage(canvas, leftmost, topmost, croppedWidth, croppedHeight, 0, 0, croppedWidth, croppedHeight)
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|||||||
@@ -95,7 +95,19 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onDropFiles (e) {
|
onDropFiles (e) {
|
||||||
this.uploadFiles(e.dataTransfer.files)
|
const files = Array.from(e.dataTransfer.files).filter((f) => {
|
||||||
|
if (this.accept === 'image/*') {
|
||||||
|
return f.type.startsWith('image')
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.accept === 'image/*' && !files.length) {
|
||||||
|
alert(this.t('please_upload_an_image_file'))
|
||||||
|
} else {
|
||||||
|
this.uploadFiles(files)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onSelectFiles (e) {
|
onSelectFiles (e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
:with-label="!isAnonymousChecboxes && showFieldNames"
|
:with-label="!isAnonymousChecboxes && showFieldNames"
|
||||||
:current-step="currentStepFields"
|
:current-step="currentStepFields"
|
||||||
:scroll-padding="scrollPadding"
|
:scroll-padding="scrollPadding"
|
||||||
@focus-step="[saveStep(), goToStep($event, false, true), currentField.type !== 'checkbox' ? isFormVisible = true : '']"
|
@focus-step="[saveStep(), currentField.type !== 'checkbox' ? isFormVisible = true : '', goToStep($event, false, true)]"
|
||||||
/>
|
/>
|
||||||
<FieldAreas
|
<FieldAreas
|
||||||
:steps="readonlyConditionalFields.map((e) => [e])"
|
:steps="readonlyConditionalFields.map((e) => [e])"
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
<button
|
<button
|
||||||
v-if="!isFormVisible"
|
v-if="!isFormVisible"
|
||||||
id="expand_form_button"
|
id="expand_form_button"
|
||||||
class="btn btn-neutral flex text-white absolute bottom-0 w-full mb-3 expand-form-button"
|
class="btn btn-neutral flex text-white absolute bottom-0 w-full mb-3 expand-form-button text-base"
|
||||||
style="width: 96%; margin-left: 2%"
|
style="width: 96%; margin-left: 2%"
|
||||||
@click.prevent="[isFormVisible = true, scrollIntoField(currentField)]"
|
@click.prevent="[isFormVisible = true, scrollIntoField(currentField)]"
|
||||||
>
|
>
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
<div
|
<div
|
||||||
v-show="isFormVisible"
|
v-show="isFormVisible"
|
||||||
id="form_container"
|
id="form_container"
|
||||||
class="shadow-md bg-base-100 absolute bottom-0 w-full border-base-200 border p-4 rounded form-container"
|
class="shadow-md bg-base-100 absolute bottom-0 w-full border-base-200 border p-4 rounded form-container overflow-hidden"
|
||||||
:class="{ 'md:bottom-4': isBreakpointMd }"
|
:class="{ 'md:bottom-4': isBreakpointMd }"
|
||||||
:style="{ backgroundColor: backgroundColor }"
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
>
|
>
|
||||||
@@ -1133,7 +1133,7 @@ export default {
|
|||||||
parent.style.overflow = 'hidden'
|
parent.style.overflow = 'hidden'
|
||||||
|
|
||||||
scrollbox.classList.add('h-full', 'overflow-y-auto')
|
scrollbox.classList.add('h-full', 'overflow-y-auto')
|
||||||
scrollbox.parentNode.classList.add('h-screen', 'overflow-y-auto')
|
scrollbox.parentNode.classList.add('h-screen', 'h-[100dvh]', 'overflow-y-auto')
|
||||||
scrollbox.parentNode.style.maxHeight = '-webkit-fill-available'
|
scrollbox.parentNode.style.maxHeight = '-webkit-fill-available'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1390,6 +1390,7 @@ export default {
|
|||||||
|
|
||||||
if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length && !this.optionalInviteSubmitters.length) {
|
if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length && !this.optionalInviteSubmitters.length) {
|
||||||
formData.append('completed', 'true')
|
formData.append('completed', 'true')
|
||||||
|
formData.append('timezone', Intl.DateTimeFormat().resolvedOptions().timeZone)
|
||||||
}
|
}
|
||||||
|
|
||||||
let saveStepRequest
|
let saveStepRequest
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
<FieldArea
|
<FieldArea
|
||||||
v-if="isMathLoaded"
|
v-if="isMathLoaded"
|
||||||
:model-value="calculateFormula(field)"
|
:model-value="calculateFormula(field)"
|
||||||
|
:is-inline-size="isInlineSize"
|
||||||
:field="field"
|
:field="field"
|
||||||
:area="area"
|
:area="area"
|
||||||
:submittable="false"
|
:submittable="false"
|
||||||
@@ -54,6 +55,11 @@ export default {
|
|||||||
isMathLoaded: false
|
isMathLoaded: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
isInlineSize () {
|
||||||
|
return CSS.supports('container-type: size')
|
||||||
|
}
|
||||||
|
},
|
||||||
async mounted () {
|
async mounted () {
|
||||||
const {
|
const {
|
||||||
create,
|
create,
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
const en = {
|
const en = {
|
||||||
|
please_upload_an_image_file: 'Please upload an image file',
|
||||||
|
must_be_characters_length: 'Must be {number} characters length',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.',
|
||||||
verify_id: 'Verify ID',
|
verify_id: 'Verify ID',
|
||||||
identity_verification: 'Identity verification',
|
identity_verification: 'Identity verification',
|
||||||
@@ -98,6 +100,8 @@ const en = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
|
please_upload_an_image_file: 'Por favor, sube un archivo de imagen',
|
||||||
|
must_be_characters_length: 'Debe tener {number} caracteres de longitud',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete todos los campos requeridos para continuar con la verificación de identidad.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete todos los campos requeridos para continuar con la verificación de identidad.',
|
||||||
verify_id: 'Verificar ID',
|
verify_id: 'Verificar ID',
|
||||||
identity_verification: 'Verificación de identidad',
|
identity_verification: 'Verificación de identidad',
|
||||||
@@ -148,6 +152,7 @@ const es = {
|
|||||||
sign_now: 'Firmar ahora',
|
sign_now: 'Firmar ahora',
|
||||||
type_here_: 'Escribe aquí...',
|
type_here_: 'Escribe aquí...',
|
||||||
optional: 'opcional',
|
optional: 'opcional',
|
||||||
|
option: 'Opción',
|
||||||
appears_on: 'Aparece en',
|
appears_on: 'Aparece en',
|
||||||
page: 'Página',
|
page: 'Página',
|
||||||
select_your_option: 'Selecciona tu opción',
|
select_your_option: 'Selecciona tu opción',
|
||||||
@@ -196,6 +201,8 @@ const es = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
|
please_upload_an_image_file: 'Per favore carica un file immagine',
|
||||||
|
must_be_characters_length: 'Deve essere lungo {number} caratteri',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: "Compila tutti i campi obbligatori per procedere con la verifica dell'identità.",
|
complete_all_required_fields_to_proceed_with_identity_verification: "Compila tutti i campi obbligatori per procedere con la verifica dell'identità.",
|
||||||
verify_id: 'Verifica ID',
|
verify_id: 'Verifica ID',
|
||||||
identity_verification: "Verifica dell'identità",
|
identity_verification: "Verifica dell'identità",
|
||||||
@@ -245,6 +252,7 @@ const it = {
|
|||||||
sign_now: 'Firma ora',
|
sign_now: 'Firma ora',
|
||||||
type_here_: 'Digita qui...',
|
type_here_: 'Digita qui...',
|
||||||
optional: 'opzionale',
|
optional: 'opzionale',
|
||||||
|
option: 'Opzione',
|
||||||
appears_on: 'Compare su',
|
appears_on: 'Compare su',
|
||||||
page: 'Pagina',
|
page: 'Pagina',
|
||||||
take_photo: 'Scattare una foto',
|
take_photo: 'Scattare una foto',
|
||||||
@@ -265,7 +273,7 @@ const it = {
|
|||||||
document_has_been_signed: 'Il documento è stato firmato!',
|
document_has_been_signed: 'Il documento è stato firmato!',
|
||||||
documents_have_been_signed: 'I documenti sono stati firmati!',
|
documents_have_been_signed: 'I documenti sono stati firmati!',
|
||||||
create_a_free_account: 'Crea un Account Gratuito',
|
create_a_free_account: 'Crea un Account Gratuito',
|
||||||
powered_by: 'Desarrollado por',
|
powered_by: 'Fornito da',
|
||||||
please_check_the_box_to_continue: 'Si prega di spuntare la casella per continuare.',
|
please_check_the_box_to_continue: 'Si prega di spuntare la casella per continuare.',
|
||||||
open_source_documents_software: 'software di documenti open source',
|
open_source_documents_software: 'software di documenti open source',
|
||||||
verified_phone_number: 'Verifica numero di telefono',
|
verified_phone_number: 'Verifica numero di telefono',
|
||||||
@@ -294,6 +302,8 @@ const it = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
|
please_upload_an_image_file: 'Bitte laden Sie eine Bilddatei hoch',
|
||||||
|
must_be_characters_length: 'Muss {number} Zeichen lang sein',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Vervollständigen Sie alle erforderlichen Felder, um mit der Identitätsverifizierung fortzufahren.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Vervollständigen Sie alle erforderlichen Felder, um mit der Identitätsverifizierung fortzufahren.',
|
||||||
verify_id: 'ID überprüfen',
|
verify_id: 'ID überprüfen',
|
||||||
identity_verification: 'Identitätsüberprüfung',
|
identity_verification: 'Identitätsüberprüfung',
|
||||||
@@ -343,6 +353,7 @@ const de = {
|
|||||||
sign_now: 'Jetzt unterschreiben',
|
sign_now: 'Jetzt unterschreiben',
|
||||||
type_here_: 'Hier eingeben...',
|
type_here_: 'Hier eingeben...',
|
||||||
optional: 'optional',
|
optional: 'optional',
|
||||||
|
option: 'Option',
|
||||||
appears_on: 'Erscheint auf',
|
appears_on: 'Erscheint auf',
|
||||||
page: 'Seite',
|
page: 'Seite',
|
||||||
take_photo: 'Foto aufnehmen',
|
take_photo: 'Foto aufnehmen',
|
||||||
@@ -392,9 +403,11 @@ const de = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
|
please_upload_an_image_file: 'Veuillez télécharger un fichier image',
|
||||||
|
must_be_characters_length: 'Doit contenir {number} caractères',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour continuer la vérification de l'identité.",
|
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour continuer la vérification de l'identité.",
|
||||||
verif_id: "Vérification de l'ID",
|
verify_id: "Vérification de l'ID",
|
||||||
verif_identite: "Vérification de l'identité",
|
identity_verification: "Vérification de l'identité",
|
||||||
complete: 'Terminer',
|
complete: 'Terminer',
|
||||||
fill_all_required_fields_to_complete: 'Veuillez remplir tous les champs obligatoires pour compléter',
|
fill_all_required_fields_to_complete: 'Veuillez remplir tous les champs obligatoires pour compléter',
|
||||||
sign_and_complete: 'Signer et Terminer',
|
sign_and_complete: 'Signer et Terminer',
|
||||||
@@ -441,6 +454,7 @@ const fr = {
|
|||||||
sign_now: 'Signer maintenant',
|
sign_now: 'Signer maintenant',
|
||||||
type_here_: 'Tapez ici...',
|
type_here_: 'Tapez ici...',
|
||||||
optional: 'facultatif',
|
optional: 'facultatif',
|
||||||
|
option: 'Option',
|
||||||
appears_on: 'Apparaît sur',
|
appears_on: 'Apparaît sur',
|
||||||
page: 'Page',
|
page: 'Page',
|
||||||
take_photo: 'Prendre une photo',
|
take_photo: 'Prendre une photo',
|
||||||
@@ -490,6 +504,8 @@ const fr = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pl = {
|
const pl = {
|
||||||
|
please_upload_an_image_file: 'Proszę przesłać plik obrazu',
|
||||||
|
must_be_characters_length: 'Musi mieć długość {number} znaków',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Uzupełnij wszystkie wymagane pola, aby kontynuować weryfikację tożsamości.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Uzupełnij wszystkie wymagane pola, aby kontynuować weryfikację tożsamości.',
|
||||||
verify_id: 'Zweryfikuj ID',
|
verify_id: 'Zweryfikuj ID',
|
||||||
identity_verification: 'Weryfikacja tożsamości',
|
identity_verification: 'Weryfikacja tożsamości',
|
||||||
@@ -539,6 +555,7 @@ const pl = {
|
|||||||
sign_now: 'Podpisz teraz',
|
sign_now: 'Podpisz teraz',
|
||||||
type_here_: 'Wpisz tutaj...',
|
type_here_: 'Wpisz tutaj...',
|
||||||
optional: 'opcjonalny',
|
optional: 'opcjonalny',
|
||||||
|
option: 'Opcja',
|
||||||
appears_on: 'Pojawia się na',
|
appears_on: 'Pojawia się na',
|
||||||
page: 'Strona',
|
page: 'Strona',
|
||||||
select_your_option: 'Wybierz swoją opcję',
|
select_your_option: 'Wybierz swoją opcję',
|
||||||
@@ -583,10 +600,13 @@ const pl = {
|
|||||||
reupload: 'Ponowne przesłanie',
|
reupload: 'Ponowne przesłanie',
|
||||||
upload: 'Przesyłanie',
|
upload: 'Przesyłanie',
|
||||||
files: 'Pliki',
|
files: 'Pliki',
|
||||||
signature_is_too_small_or_simple_please_redraw: 'Podpis jest zbyt mały lub zbyt prosty. Proszę narysować go ponownie.'
|
signature_is_too_small_or_simple_please_redraw: 'Podpis jest zbyt mały lub zbyt prosty. Proszę narysować go ponownie.',
|
||||||
|
wait_countdown_seconds: 'Poczekaj {countdown} sekund'
|
||||||
}
|
}
|
||||||
|
|
||||||
const uk = {
|
const uk = {
|
||||||
|
please_upload_an_image_file: 'Будь ласка, завантажте файл зображення',
|
||||||
|
must_be_characters_length: 'Має містити {number} символів',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: "Заповніть всі обов'язкові поля, щоб продовжити перевірку особи.",
|
complete_all_required_fields_to_proceed_with_identity_verification: "Заповніть всі обов'язкові поля, щоб продовжити перевірку особи.",
|
||||||
verify_id: 'Підтвердження ідентичності',
|
verify_id: 'Підтвердження ідентичності',
|
||||||
identity_verification: 'Ідентифікація особи',
|
identity_verification: 'Ідентифікація особи',
|
||||||
@@ -636,6 +656,7 @@ const uk = {
|
|||||||
sign_now: 'Підписати зараз',
|
sign_now: 'Підписати зараз',
|
||||||
type_here_: 'Введіть тут',
|
type_here_: 'Введіть тут',
|
||||||
optional: 'необов’язково',
|
optional: 'необов’язково',
|
||||||
|
option: 'Опція',
|
||||||
appears_on: "З'являється на",
|
appears_on: "З'являється на",
|
||||||
page: 'Сторінка',
|
page: 'Сторінка',
|
||||||
take_photo: 'Зробити фото',
|
take_photo: 'Зробити фото',
|
||||||
@@ -658,7 +679,7 @@ const uk = {
|
|||||||
create_a_free_account: 'Створити безкоштовний обліковий запис',
|
create_a_free_account: 'Створити безкоштовний обліковий запис',
|
||||||
powered_by: 'Працює на базі',
|
powered_by: 'Працює на базі',
|
||||||
please_check_the_box_to_continue: 'Будь ласка, позначте прапорець, щоб продовжити.',
|
please_check_the_box_to_continue: 'Будь ласка, позначте прапорець, щоб продовжити.',
|
||||||
open_source_documents_software: 'відкритий програмний засіб для документів',
|
open_source_documents_software: 'відкрите програмне забезпечення для документів',
|
||||||
verified_phone_number: 'Підтвердіть номер телефону',
|
verified_phone_number: 'Підтвердіть номер телефону',
|
||||||
use_international_format: 'Використовуйте міжнародний формат: +1xxx',
|
use_international_format: 'Використовуйте міжнародний формат: +1xxx',
|
||||||
six_digits_code: '6-значний код',
|
six_digits_code: '6-значний код',
|
||||||
@@ -685,6 +706,8 @@ const uk = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cs = {
|
const cs = {
|
||||||
|
please_upload_an_image_file: 'Nahrajte prosím obrázkový soubor',
|
||||||
|
must_be_characters_length: 'Musí mít délku {number} znaků',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Vyplňte všechna povinná pole, abyste mohli pokračovat v ověření identity.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Vyplňte všechna povinná pole, abyste mohli pokračovat v ověření identity.',
|
||||||
verify_id: 'Ověřit ID',
|
verify_id: 'Ověřit ID',
|
||||||
identity_verification: 'Ověření identity',
|
identity_verification: 'Ověření identity',
|
||||||
@@ -734,6 +757,7 @@ const cs = {
|
|||||||
sign_now: 'Podepsat nyní',
|
sign_now: 'Podepsat nyní',
|
||||||
type_here_: 'Zadejte zde',
|
type_here_: 'Zadejte zde',
|
||||||
optional: 'volitelné',
|
optional: 'volitelné',
|
||||||
|
option: 'Možnost',
|
||||||
appears_on: 'Zobrazuje se na',
|
appears_on: 'Zobrazuje se na',
|
||||||
page: 'Stránka',
|
page: 'Stránka',
|
||||||
select_your_option: 'Vyberte svou volbu',
|
select_your_option: 'Vyberte svou volbu',
|
||||||
@@ -783,6 +807,8 @@ const cs = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
|
please_upload_an_image_file: 'Por favor, envie um arquivo de imagem',
|
||||||
|
must_be_characters_length: 'Deve ter {number} caracteres',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Preencha todos os campos obrigatórios para prosseguir com a verificação de identidade.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Preencha todos os campos obrigatórios para prosseguir com a verificação de identidade.',
|
||||||
verify_id: 'Verificar ID',
|
verify_id: 'Verificar ID',
|
||||||
identity_verification: 'Verificação de identidade',
|
identity_verification: 'Verificação de identidade',
|
||||||
@@ -832,6 +858,7 @@ const pt = {
|
|||||||
sign_now: 'Assinar agora',
|
sign_now: 'Assinar agora',
|
||||||
type_here_: 'Digite aqui',
|
type_here_: 'Digite aqui',
|
||||||
optional: 'opcional',
|
optional: 'opcional',
|
||||||
|
option: 'Opção',
|
||||||
appears_on: 'Aparece em',
|
appears_on: 'Aparece em',
|
||||||
page: 'Página',
|
page: 'Página',
|
||||||
take_photo: 'Tirar foto',
|
take_photo: 'Tirar foto',
|
||||||
@@ -881,6 +908,8 @@ const pt = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const he = {
|
const he = {
|
||||||
|
please_upload_an_image_file: 'אנא העלה קובץ תמונה',
|
||||||
|
must_be_characters_length: 'חייב להיות באורך של {number} תווים',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'מלא את כל השדות הנדרשים כדי להמשיך עם אימות זהות.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'מלא את כל השדות הנדרשים כדי להמשיך עם אימות זהות.',
|
||||||
verify_id: 'אמת מזהה',
|
verify_id: 'אמת מזהה',
|
||||||
identity_verification: 'אימות זהות',
|
identity_verification: 'אימות זהות',
|
||||||
@@ -980,6 +1009,8 @@ const he = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nl = {
|
const nl = {
|
||||||
|
please_upload_an_image_file: 'Upload alstublieft een afbeeldingsbestand',
|
||||||
|
must_be_characters_length: 'Moet {number} tekens lang zijn',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Vul alle verplichte velden in om door te gaan met de identiteitsverificatie.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Vul alle verplichte velden in om door te gaan met de identiteitsverificatie.',
|
||||||
verify_id: 'Verifiëren ID',
|
verify_id: 'Verifiëren ID',
|
||||||
identity_verification: 'Identiteitsverificatie',
|
identity_verification: 'Identiteitsverificatie',
|
||||||
@@ -1079,6 +1110,8 @@ const nl = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ar = {
|
const ar = {
|
||||||
|
please_upload_an_image_file: 'يرجى تحميل ملف صورة',
|
||||||
|
must_be_characters_length: 'يجب أن يكون الطول {number} حرفًا',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'أكمل جميع الحقول المطلوبة للمتابعة في التحقق من الهوية.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'أكمل جميع الحقول المطلوبة للمتابعة في التحقق من الهوية.',
|
||||||
verify_id: 'تحقق من الهوية',
|
verify_id: 'تحقق من الهوية',
|
||||||
identity_verification: 'التحقق من الهوية',
|
identity_verification: 'التحقق من الهوية',
|
||||||
@@ -1097,6 +1130,7 @@ const ar = {
|
|||||||
select_a_reason: 'اختر سببًا',
|
select_a_reason: 'اختر سببًا',
|
||||||
value_is_invalid: 'القيمة غير صالحة',
|
value_is_invalid: 'القيمة غير صالحة',
|
||||||
verification_code_is_invalid: 'رمز التحقق غير صالح',
|
verification_code_is_invalid: 'رمز التحقق غير صالح',
|
||||||
|
already_paid: 'تم الدفع بالفعل',
|
||||||
drawn_signature_on_a_touchscreen_device: 'توقيع مرسوم على جهاز بشاشة تعمل باللمس',
|
drawn_signature_on_a_touchscreen_device: 'توقيع مرسوم على جهاز بشاشة تعمل باللمس',
|
||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'امسح رمز الاستجابة السريعة باستخدام تطبيق الكاميرا لفتح النموذج على الهاتف المحمول ورسم توقيعك',
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'امسح رمز الاستجابة السريعة باستخدام تطبيق الكاميرا لفتح النموذج على الهاتف المحمول ورسم توقيعك',
|
||||||
by_clicking_you_agree_to_the: 'بالنقر فوق "{button}"، أنت توافق على',
|
by_clicking_you_agree_to_the: 'بالنقر فوق "{button}"، أنت توافق على',
|
||||||
@@ -1177,6 +1211,8 @@ const ar = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ko = {
|
const ko = {
|
||||||
|
please_upload_an_image_file: '이미지 파일을 업로드해 주세요',
|
||||||
|
must_be_characters_length: '{number}자여야 합니다',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: '신원 확인을 진행하려면 모든 필수 필드를 작성하십시오.',
|
complete_all_required_fields_to_proceed_with_identity_verification: '신원 확인을 진행하려면 모든 필수 필드를 작성하십시오.',
|
||||||
verify_id: '아이디 확인',
|
verify_id: '아이디 확인',
|
||||||
identity_verification: '신원 확인',
|
identity_verification: '신원 확인',
|
||||||
@@ -1198,6 +1234,8 @@ const ko = {
|
|||||||
by_clicking_you_agree_to_the: '"{button}"를 클릭함으로써, 다음에 동의하게 됩니다',
|
by_clicking_you_agree_to_the: '"{button}"를 클릭함으로써, 다음에 동의하게 됩니다',
|
||||||
electronic_signature_disclosure: '전자 서명 공개',
|
electronic_signature_disclosure: '전자 서명 공개',
|
||||||
esignature_disclosure: '전자 서명 공개',
|
esignature_disclosure: '전자 서명 공개',
|
||||||
|
value_is_invalid: '값이 올바르지 않습니다',
|
||||||
|
verification_code_is_invalid: '인증 코드가 올바르지 않습니다',
|
||||||
already_paid: '이미 지불됨',
|
already_paid: '이미 지불됨',
|
||||||
text: '텍스트',
|
text: '텍스트',
|
||||||
signature: '서명',
|
signature: '서명',
|
||||||
@@ -1273,6 +1311,107 @@ const ko = {
|
|||||||
wait_countdown_seconds: '{countdown}초 기다리세요'
|
wait_countdown_seconds: '{countdown}초 기다리세요'
|
||||||
}
|
}
|
||||||
|
|
||||||
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar, ko }
|
const ja = {
|
||||||
|
please_upload_an_image_file: '画像ファイルをアップロードしてください',
|
||||||
|
must_be_characters_length: '{number}文字でなければなりません',
|
||||||
|
complete_all_required_fields_to_proceed_with_identity_verification: '本人確認を進めるには、すべての必須項目を入力してください。',
|
||||||
|
verify_id: '本人確認',
|
||||||
|
identity_verification: '本人確認',
|
||||||
|
complete: '完了',
|
||||||
|
fill_all_required_fields_to_complete: '完了するにはすべての必須項目を入力してください',
|
||||||
|
sign_and_complete: '署名して完了',
|
||||||
|
text: 'テキスト',
|
||||||
|
by_clicking_you_agree_to_the: '"{button}" をクリックすることで、次に同意したことになります:',
|
||||||
|
electronic_signature_disclosure: '電子署名に関する開示',
|
||||||
|
esignature_disclosure: '電子署名開示',
|
||||||
|
signature: '署名',
|
||||||
|
initials: 'イニシャル',
|
||||||
|
drawn_signature_on_a_touchscreen_device: 'タッチスクリーンデバイスで描かれた署名',
|
||||||
|
approved: '承認済み',
|
||||||
|
reviewed: '確認済み',
|
||||||
|
other: 'その他',
|
||||||
|
authored_by_me: '自分が作成',
|
||||||
|
invite: '招待',
|
||||||
|
email: 'メール',
|
||||||
|
approved_by: '承認者',
|
||||||
|
reviewed_by: '確認者',
|
||||||
|
authored_by: '作成者',
|
||||||
|
select_a_reason: '理由を選択',
|
||||||
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'QRコードをカメラアプリで読み取り、モバイルでフォームを開いて署名を描いてください',
|
||||||
|
date: '日付',
|
||||||
|
number: '数値',
|
||||||
|
value_is_invalid: '無効な値です',
|
||||||
|
verification_code_is_invalid: '認証コードが無効です',
|
||||||
|
already_paid: 'すでに支払い済み',
|
||||||
|
image: '画像',
|
||||||
|
pay: '支払う',
|
||||||
|
take_photo: '写真を撮る',
|
||||||
|
number_phone_is_invalid: '{number} の電話番号は無効です',
|
||||||
|
file: 'ファイル',
|
||||||
|
digitally_signed_by: '電子署名者:',
|
||||||
|
reason: '理由',
|
||||||
|
select: '選択',
|
||||||
|
checkbox: 'チェックボックス',
|
||||||
|
multiple: '複数選択',
|
||||||
|
radio: 'ラジオボタン',
|
||||||
|
cells: 'セル',
|
||||||
|
stamp: 'スタンプ',
|
||||||
|
minimize: '最小化',
|
||||||
|
payment: '支払い',
|
||||||
|
phone: '電話',
|
||||||
|
start_now: '今すぐ開始',
|
||||||
|
continue: '続行',
|
||||||
|
sign_now: '今すぐ署名',
|
||||||
|
type_here_: 'ここに入力...',
|
||||||
|
optional: '任意',
|
||||||
|
option: 'オプション',
|
||||||
|
appears_on: '表示ページ',
|
||||||
|
page: 'ページ',
|
||||||
|
select_your_option: 'オプションを選択',
|
||||||
|
complete_hightlighted_checkboxes_and_click: 'ハイライトされたチェックボックスを完了し、クリックしてください',
|
||||||
|
submit: '送信',
|
||||||
|
next: '次へ',
|
||||||
|
click_to_upload: 'クリックしてアップロード',
|
||||||
|
or_drag_and_drop_files: 'またはファイルをドラッグ&ドロップ',
|
||||||
|
send_copy_via_email: 'メールでコピーを送信',
|
||||||
|
download: 'ダウンロード',
|
||||||
|
clear: 'クリア',
|
||||||
|
redraw: '描き直す',
|
||||||
|
draw_initials: 'イニシャルを描く',
|
||||||
|
type_signature_here: 'ここに署名を入力',
|
||||||
|
type_initial_here: 'ここにイニシャルを入力',
|
||||||
|
form_has_been_completed: 'フォームが完了しました!',
|
||||||
|
document_has_been_signed: 'ドキュメントが署名されました!',
|
||||||
|
documents_have_been_signed: 'ドキュメントがすべて署名されました!',
|
||||||
|
create_a_free_account: '無料アカウントを作成',
|
||||||
|
powered_by: '提供元:',
|
||||||
|
please_check_the_box_to_continue: '続行するにはボックスにチェックを入れてください。',
|
||||||
|
open_source_documents_software: 'オープンソースのドキュメントソフトウェア',
|
||||||
|
verified_phone_number: '電話番号を認証',
|
||||||
|
use_international_format: '国際形式を使用してください:+1xxx',
|
||||||
|
six_digits_code: '6桁のコード',
|
||||||
|
change_phone_number: '電話番号を変更',
|
||||||
|
sending: '送信中...',
|
||||||
|
resend_code: 'コードを再送信',
|
||||||
|
verification_code_has_been_resent: '認証コードがSMSで再送信されました',
|
||||||
|
please_fill_all_required_fields: 'すべての必須項目を入力してください',
|
||||||
|
set_today: '今日の日付を設定',
|
||||||
|
toggle_multiline_text: '複数行テキスト切り替え',
|
||||||
|
draw_signature: '署名を描く',
|
||||||
|
type_initial: 'イニシャルを入力',
|
||||||
|
draw: '描く',
|
||||||
|
type: '入力',
|
||||||
|
type_text: 'テキストを入力',
|
||||||
|
email_has_been_sent: 'メールが送信されました',
|
||||||
|
processing: '処理中',
|
||||||
|
pay_with_strip: 'Stripeで支払う',
|
||||||
|
reupload: '再アップロード',
|
||||||
|
upload: 'アップロード',
|
||||||
|
files: 'ファイル',
|
||||||
|
signature_is_too_small_or_simple_please_redraw: '署名が小さすぎるか単純すぎます。もう一度描いてください。',
|
||||||
|
wait_countdown_seconds: '{countdown} 秒お待ちください'
|
||||||
|
}
|
||||||
|
|
||||||
|
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar, ko, ja }
|
||||||
|
|
||||||
export default i18n
|
export default i18n
|
||||||
@@ -203,7 +203,7 @@ export default {
|
|||||||
emits: ['attached', 'update:model-value', 'start', 'minimize', 'focus'],
|
emits: ['attached', 'update:model-value', 'start', 'minimize', 'focus'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isInitialsStarted: !!this.previousValue,
|
isInitialsStarted: false,
|
||||||
isUsePreviousValue: true,
|
isUsePreviousValue: true,
|
||||||
isDrawInitials: false,
|
isDrawInitials: false,
|
||||||
uploadImageInputKey: Math.random().toString()
|
uploadImageInputKey: Math.random().toString()
|
||||||
@@ -211,13 +211,16 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
computedPreviousValue () {
|
computedPreviousValue () {
|
||||||
if (this.isUsePreviousValue) {
|
if (this.isUsePreviousValue && this.field.required === true) {
|
||||||
return this.previousValue
|
return this.previousValue
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created () {
|
||||||
|
this.isInitialsStarted = !!this.computedPreviousValue
|
||||||
|
},
|
||||||
async mounted () {
|
async mounted () {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (this.$refs.canvas) {
|
if (this.$refs.canvas) {
|
||||||
@@ -339,8 +342,8 @@ export default {
|
|||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve, reject) => {
|
||||||
cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => {
|
cropCanvasAndExportToPNG(this.$refs.canvas, { errorOnTooSmall: true }).then(async (blob) => {
|
||||||
const file = new File([blob], 'initials.png', { type: 'image/png' })
|
const file = new File([blob], 'initials.png', { type: 'image/png' })
|
||||||
|
|
||||||
if (this.dryRun) {
|
if (this.dryRun) {
|
||||||
@@ -362,17 +365,34 @@ export default {
|
|||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
formData.append('submitter_slug', this.submitterSlug)
|
formData.append('submitter_slug', this.submitterSlug)
|
||||||
formData.append('name', 'attachments')
|
formData.append('name', 'attachments')
|
||||||
|
formData.append('type', 'initials')
|
||||||
|
|
||||||
return fetch(this.baseUrl + '/api/attachments', {
|
return fetch(this.baseUrl + '/api/attachments', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
}).then((resp) => resp.json()).then((attachment) => {
|
}).then(async (resp) => {
|
||||||
|
if (resp.status === 422 || resp.status === 500) {
|
||||||
|
const data = await resp.json()
|
||||||
|
|
||||||
|
return Promise.reject(new Error(data.error))
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachment = await resp.json()
|
||||||
|
|
||||||
this.$emit('attached', attachment)
|
this.$emit('attached', attachment)
|
||||||
this.$emit('update:model-value', attachment.uuid)
|
this.$emit('update:model-value', attachment.uuid)
|
||||||
|
|
||||||
return resolve(attachment)
|
return resolve(attachment)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
if (this.field.required === true) {
|
||||||
|
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
|
||||||
|
|
||||||
|
return reject(error)
|
||||||
|
} else {
|
||||||
|
return resolve({})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,7 +133,7 @@
|
|||||||
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"
|
||||||
>
|
>
|
||||||
<FileDropzone
|
<FileDropzone
|
||||||
v-if="format === 'upload' && !modelValue"
|
v-if="format === 'upload' && !modelValue && !computedPreviousValue"
|
||||||
:message="`${t('upload')} ${field.name || t('signature')}`"
|
:message="`${t('upload')} ${field.name || t('signature')}`"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
:dry-run="dryRun"
|
:dry-run="dryRun"
|
||||||
@@ -201,7 +201,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
v-if="isTextSignature"
|
v-if="isTextSignature && !modelValue && !computedPreviousValue"
|
||||||
id="signature_text_input"
|
id="signature_text_input"
|
||||||
ref="textInput"
|
ref="textInput"
|
||||||
class="base-input !text-2xl w-full mt-6"
|
class="base-input !text-2xl w-full mt-6"
|
||||||
@@ -391,7 +391,7 @@ export default {
|
|||||||
emits: ['attached', 'update:model-value', 'start', 'minimize', 'update:reason'],
|
emits: ['attached', 'update:model-value', 'start', 'minimize', 'update:reason'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isSignatureStarted: !!this.previousValue,
|
isSignatureStarted: false,
|
||||||
isShowQr: false,
|
isShowQr: false,
|
||||||
isOtherReason: false,
|
isOtherReason: false,
|
||||||
isUsePreviousValue: true,
|
isUsePreviousValue: true,
|
||||||
@@ -414,7 +414,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computedPreviousValue () {
|
computedPreviousValue () {
|
||||||
if (this.isUsePreviousValue) {
|
if (this.isUsePreviousValue && this.field.required === true) {
|
||||||
return this.previousValue
|
return this.previousValue
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
@@ -422,6 +422,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
|
this.isSignatureStarted = !!this.computedPreviousValue
|
||||||
|
|
||||||
if (this.requireSigningReason) {
|
if (this.requireSigningReason) {
|
||||||
this.field.preferences ||= {}
|
this.field.preferences ||= {}
|
||||||
this.field.preferences.reason_field_uuid ||= v4()
|
this.field.preferences.reason_field_uuid ||= v4()
|
||||||
@@ -683,13 +685,17 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.isSignatureStarted && this.pad.toData().length > 0 && !isValidSignatureCanvas(this.pad.toData())) {
|
if (this.isSignatureStarted && this.pad.toData().length > 0 && !isValidSignatureCanvas(this.pad.toData())) {
|
||||||
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
|
if (this.field.required === true || this.pad.toData().length > 0) {
|
||||||
|
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
|
||||||
|
|
||||||
return Promise.reject(new Error('Image too small or simple'))
|
return Promise.reject(new Error('Image too small or simple'))
|
||||||
|
} else {
|
||||||
|
Promise.resolve({})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve, reject) => {
|
||||||
cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => {
|
cropCanvasAndExportToPNG(this.$refs.canvas, { errorOnTooSmall: true }).then(async (blob) => {
|
||||||
const file = new File([blob], 'signature.png', { type: 'image/png' })
|
const file = new File([blob], 'signature.png', { type: 'image/png' })
|
||||||
|
|
||||||
if (this.dryRun) {
|
if (this.dryRun) {
|
||||||
@@ -712,11 +718,20 @@ export default {
|
|||||||
formData.append('submitter_slug', this.submitterSlug)
|
formData.append('submitter_slug', this.submitterSlug)
|
||||||
formData.append('name', 'attachments')
|
formData.append('name', 'attachments')
|
||||||
formData.append('remember_signature', this.rememberSignature)
|
formData.append('remember_signature', this.rememberSignature)
|
||||||
|
formData.append('type', 'signature')
|
||||||
|
|
||||||
return fetch(this.baseUrl + '/api/attachments', {
|
return fetch(this.baseUrl + '/api/attachments', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
}).then((resp) => resp.json()).then((attachment) => {
|
}).then(async (resp) => {
|
||||||
|
if (resp.status === 422 || resp.status === 500) {
|
||||||
|
const data = await resp.json()
|
||||||
|
|
||||||
|
return Promise.reject(new Error(data.error))
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachment = await resp.json()
|
||||||
|
|
||||||
this.$emit('attached', attachment)
|
this.$emit('attached', attachment)
|
||||||
this.$emit('update:model-value', attachment.uuid)
|
this.$emit('update:model-value', attachment.uuid)
|
||||||
|
|
||||||
@@ -725,6 +740,14 @@ export default {
|
|||||||
return resolve(attachment)
|
return resolve(attachment)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
if (this.field.required === true) {
|
||||||
|
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
|
||||||
|
|
||||||
|
return reject(error)
|
||||||
|
} else {
|
||||||
|
return resolve({})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,8 +43,8 @@
|
|||||||
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
|
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||||
type="text"
|
type="text"
|
||||||
:name="`values[${field.uuid}]`"
|
:name="`values[${field.uuid}]`"
|
||||||
@invalid="field.validation?.message ? $event.target.setCustomValidity(field.validation.message) : ''"
|
@invalid="validationMessage ? $event.target.setCustomValidity(validationMessage) : ''"
|
||||||
@input="field.validation?.message ? $event.target.setCustomValidity('') : ''"
|
@input="validationMessage ? $event.target.setCustomValidity('') : ''"
|
||||||
@focus="$emit('focus')"
|
@focus="$emit('focus')"
|
||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
@@ -127,6 +127,28 @@ export default {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
lengthValidation () {
|
||||||
|
if (this.field.validation?.pattern) {
|
||||||
|
return this.field.validation.pattern.match(/^\.{(?<min>\d+),(?<max>\d+)?}$/)?.groups
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
validationMessage () {
|
||||||
|
if (this.field.validation?.message) {
|
||||||
|
return this.field.validation.message
|
||||||
|
} else if (this.lengthValidation) {
|
||||||
|
const number =
|
||||||
|
[this.lengthValidation.min, this.lengthValidation.max]
|
||||||
|
.filter(Boolean)
|
||||||
|
.filter((v, i, a) => a.indexOf(v) === i)
|
||||||
|
.join('-')
|
||||||
|
|
||||||
|
return this.t('must_be_characters_length').replace('{number}', number)
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
},
|
||||||
text: {
|
text: {
|
||||||
set (value) {
|
set (value) {
|
||||||
this.$emit('update:model-value', value)
|
this.$emit('update:model-value', value)
|
||||||
@@ -151,8 +173,10 @@ export default {
|
|||||||
resizeTextarea () {
|
resizeTextarea () {
|
||||||
const textarea = this.$refs.textarea
|
const textarea = this.$refs.textarea
|
||||||
|
|
||||||
textarea.style.height = 'auto'
|
if (textarea) {
|
||||||
textarea.style.height = Math.min(250, textarea.scrollHeight) + 'px'
|
textarea.style.height = 'auto'
|
||||||
|
textarea.style.height = Math.min(250, textarea.scrollHeight) + 'px'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
toggleTextArea () {
|
toggleTextArea () {
|
||||||
this.isTextArea = true
|
this.isTextArea = true
|
||||||
|
|||||||
@@ -149,7 +149,8 @@ export default {
|
|||||||
return fetch(this.baseUrl + `/api/identity_verification/${this.field.uuid}`, {
|
return fetch(this.baseUrl + `/api/identity_verification/${this.field.uuid}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
submitter_slug: this.submitterSlug
|
submitter_slug: this.submitterSlug,
|
||||||
|
redirect_url: document.location.href
|
||||||
}),
|
}),
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
}).then(async (resp) => {
|
}).then(async (resp) => {
|
||||||
|
|||||||
@@ -140,6 +140,7 @@
|
|||||||
:background-color="'white'"
|
:background-color="'white'"
|
||||||
:with-required="false"
|
:with-required="false"
|
||||||
:with-areas="false"
|
:with-areas="false"
|
||||||
|
:with-signature-id="withSignatureId"
|
||||||
@click-formula="isShowFormulaModal = true"
|
@click-formula="isShowFormulaModal = true"
|
||||||
@click-font="isShowFontModal = true"
|
@click-font="isShowFontModal = true"
|
||||||
@click-description="isShowDescriptionModal = true"
|
@click-description="isShowDescriptionModal = true"
|
||||||
@@ -160,19 +161,20 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
ref="touchValueTarget"
|
ref="touchValueTarget"
|
||||||
class="flex items-center h-full w-full field-area"
|
class="flex h-full w-full field-area"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
:class="[isValueInput ? 'bg-opacity-50' : 'bg-opacity-80', field.type === 'heading' ? 'bg-gray-50' : bgColors[submitterIndex % bgColors.length], isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? fontClasses : 'justify-center']"
|
:class="[isValueInput ? 'bg-opacity-50' : 'bg-opacity-80', field.type === 'heading' ? 'bg-gray-50' : bgColors[submitterIndex % bgColors.length], isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? fontClasses : 'justify-center items-center']"
|
||||||
@click="focusValueInput"
|
@click="focusValueInput"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-if="field"
|
v-if="field"
|
||||||
class="flex justify-center items-center space-x-1"
|
class="flex justify-center items-center space-x-1"
|
||||||
:class="{ 'w-full': ['cells', 'checkbox'].includes(field.type), 'h-full': !isValueInput }"
|
:class="{ 'w-full': ['cells', 'checkbox'].includes(field.type), 'h-full': !isValueInput && !isDefaultValuePresent }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas && field.type !== 'checkbox')"
|
v-if="isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas && field.type !== 'checkbox')"
|
||||||
:class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type), 'text-[1.6vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars }"
|
:class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type) }"
|
||||||
|
:style="fontStyle"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref="textContainer"
|
ref="textContainer"
|
||||||
@@ -315,7 +317,7 @@ export default {
|
|||||||
FieldSubmitter,
|
FieldSubmitter,
|
||||||
IconX
|
IconX
|
||||||
},
|
},
|
||||||
inject: ['template', 'selectedAreaRef', 'save', 't'],
|
inject: ['template', 'selectedAreaRef', 'save', 't', 'isInlineSize'],
|
||||||
props: {
|
props: {
|
||||||
area: {
|
area: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -346,6 +348,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
withSignatureId: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
defaultSubmitters: {
|
defaultSubmitters: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -385,6 +392,34 @@ export default {
|
|||||||
fieldNames: FieldType.computed.fieldNames,
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
fieldLabels: FieldType.computed.fieldLabels,
|
fieldLabels: FieldType.computed.fieldLabels,
|
||||||
fieldIcons: FieldType.computed.fieldIcons,
|
fieldIcons: FieldType.computed.fieldIcons,
|
||||||
|
fontStyle () {
|
||||||
|
let fontSize = ''
|
||||||
|
|
||||||
|
if (this.isInlineSize) {
|
||||||
|
if (this.textOverflowChars) {
|
||||||
|
fontSize = `${this.fontSizePx / 1.5 / 10}cqmin`
|
||||||
|
} else {
|
||||||
|
fontSize = `${this.fontSizePx / 10}cqmin`
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.textOverflowChars) {
|
||||||
|
fontSize = `clamp(1pt, ${this.fontSizePx / 1.5 / 10}vw, ${this.fontSizePx / 1.5}px)`
|
||||||
|
} else {
|
||||||
|
fontSize = `clamp(1pt, ${this.fontSizePx / 10}vw, ${this.fontSizePx}px)`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { fontSize, lineHeight: `calc(${fontSize} * ${this.lineHeight})` }
|
||||||
|
},
|
||||||
|
fontSizePx () {
|
||||||
|
return parseInt(this.field?.preferences?.font_size || 11) * this.fontScale
|
||||||
|
},
|
||||||
|
lineHeight () {
|
||||||
|
return 1.3
|
||||||
|
},
|
||||||
|
fontScale () {
|
||||||
|
return 1040 / 612.0
|
||||||
|
},
|
||||||
isDefaultValuePresent () {
|
isDefaultValuePresent () {
|
||||||
if (this.field?.type === 'radio' && this.field?.areas?.length > 1) {
|
if (this.field?.type === 'radio' && this.field?.areas?.length > 1) {
|
||||||
return false
|
return false
|
||||||
@@ -403,15 +438,18 @@ export default {
|
|||||||
},
|
},
|
||||||
fontClasses () {
|
fontClasses () {
|
||||||
if (!this.field.preferences) {
|
if (!this.field.preferences) {
|
||||||
return {}
|
return { 'items-center': true }
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
'items-center': !this.field.preferences.valign || this.field.preferences.valign === 'center',
|
||||||
|
'items-start': this.field.preferences.valign === 'top',
|
||||||
|
'items-end': this.field.preferences.valign === 'bottom',
|
||||||
'justify-center': this.field.preferences.align === 'center',
|
'justify-center': this.field.preferences.align === 'center',
|
||||||
'justify-start': this.field.preferences.align === 'left',
|
'justify-start': this.field.preferences.align === 'left',
|
||||||
'justify-end': this.field.preferences.align === 'right',
|
'justify-end': this.field.preferences.align === 'right',
|
||||||
'font-mono': this.field.preferences.font === 'Courier',
|
'font-courier': this.field.preferences.font === 'Courier',
|
||||||
'font-serif': this.field.preferences.font === 'Times',
|
'font-times': this.field.preferences.font === 'Times',
|
||||||
'font-bold': ['bold_italic', 'bold'].includes(this.field.preferences.font_type),
|
'font-bold': ['bold_italic', 'bold'].includes(this.field.preferences.font_type),
|
||||||
italic: ['bold_italic', 'italic'].includes(this.field.preferences.font_type)
|
italic: ['bold_italic', 'italic'].includes(this.field.preferences.font_type)
|
||||||
}
|
}
|
||||||
@@ -488,7 +526,7 @@ export default {
|
|||||||
'field.default_value' () {
|
'field.default_value' () {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (['date', 'text', 'number'].includes(this.field.type) && this.field.default_value && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.field.default_value}`.length)) {
|
if (['date', 'text', 'number'].includes(this.field.type) && this.field.default_value && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.field.default_value}`.length)) {
|
||||||
this.textOverflowChars = this.$el.clientHeight < this.$refs.textContainer.clientHeight ? `${this.field.default_value}`.length : 0
|
this.textOverflowChars = (this.$el.clientHeight + 1) < this.$refs.textContainer.clientHeight ? `${this.field.default_value}`.length : 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -496,7 +534,7 @@ export default {
|
|||||||
mounted () {
|
mounted () {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (['date', 'text', 'number'].includes(this.field.type) && this.field.default_value && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.field.default_value}`.length)) {
|
if (['date', 'text', 'number'].includes(this.field.type) && this.field.default_value && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.field.default_value}`.length)) {
|
||||||
this.textOverflowChars = this.$el.clientHeight < this.$refs.textContainer.clientHeight ? `${this.field.default_value}`.length : 0
|
this.textOverflowChars = (this.$el.clientHeight + 1) < this.$refs.textContainer.clientHeight ? `${this.field.default_value}`.length : 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,19 @@
|
|||||||
class="mx-auto pl-3 h-full"
|
class="mx-auto pl-3 h-full"
|
||||||
:class="isMobile ? 'pl-4' : 'md:pl-4'"
|
:class="isMobile ? 'pl-4' : 'md:pl-4'"
|
||||||
@dragover="onDragover"
|
@dragover="onDragover"
|
||||||
|
@drop="isDragFile = false"
|
||||||
>
|
>
|
||||||
|
<HoverDropzone
|
||||||
|
v-if="sortedDocuments.length && withUploadButton && editable"
|
||||||
|
:is-dragging="isDragFile"
|
||||||
|
:template-id="template.id"
|
||||||
|
:accept-file-types="acceptFileTypes"
|
||||||
|
:with-replace-and-clone="withReplaceAndCloneUpload"
|
||||||
|
@add="[updateFromUpload($event), isDragFile = false]"
|
||||||
|
@replace="[onDocumentsReplace($event), isDragFile = false]"
|
||||||
|
@replace-and-clone="onDocumentsReplaceAndTemplateClone($event)"
|
||||||
|
@error="[onUploadFailed($event), isDragFile = false]"
|
||||||
|
/>
|
||||||
<DragPlaceholder
|
<DragPlaceholder
|
||||||
ref="dragPlaceholder"
|
ref="dragPlaceholder"
|
||||||
:field="fieldsDragFieldRef.value || toRaw(dragField)"
|
:field="fieldsDragFieldRef.value || toRaw(dragField)"
|
||||||
@@ -67,13 +79,52 @@
|
|||||||
name="buttons"
|
name="buttons"
|
||||||
/>
|
/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
<form
|
||||||
|
v-if="withSignYourselfButton && template.submitters.length < 2"
|
||||||
|
target="_blank"
|
||||||
|
data-turbo="false"
|
||||||
|
class="inline"
|
||||||
|
method="post"
|
||||||
|
:action="`/d/${template.slug}`"
|
||||||
|
@submit="maybeShowErrorTemplateAlert"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="_method"
|
||||||
|
value="put"
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="authenticity_token"
|
||||||
|
:value="authenticityToken"
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="selfsign"
|
||||||
|
value="true"
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary btn-ghost text-base hidden md:flex"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<IconWritingSign
|
||||||
|
width="22"
|
||||||
|
class="inline"
|
||||||
|
/>
|
||||||
|
<span class="hidden md:inline">
|
||||||
|
{{ t('sign_yourself') }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
<a
|
<a
|
||||||
v-if="withSignYourselfButton"
|
v-else-if="withSignYourselfButton"
|
||||||
id="sign_yourself_button"
|
id="sign_yourself_button"
|
||||||
:href="template.submitters.length > 1 ? `/templates/${template.id}/submissions/new?selfsign=true` : `/d/${template.slug}`"
|
:href="`/templates/${template.id}/submissions/new?selfsign=true`"
|
||||||
class="btn btn-primary btn-ghost text-base hidden md:flex"
|
class="btn btn-primary btn-ghost text-base hidden md:flex"
|
||||||
:target="template.submitters.length > 1 ? '' : '_blank'"
|
data-turbo-frame="modal"
|
||||||
:data-turbo-frame="template.submitters.length > 1 ? 'modal' : ''"
|
|
||||||
@click="maybeShowErrorTemplateAlert"
|
@click="maybeShowErrorTemplateAlert"
|
||||||
>
|
>
|
||||||
<IconWritingSign
|
<IconWritingSign
|
||||||
@@ -279,6 +330,7 @@
|
|||||||
:input-mode="inputMode"
|
:input-mode="inputMode"
|
||||||
:default-fields="[...defaultRequiredFields, ...defaultFields]"
|
:default-fields="[...defaultRequiredFields, ...defaultFields]"
|
||||||
:allow-draw="!onlyDefinedFields || drawField"
|
:allow-draw="!onlyDefinedFields || drawField"
|
||||||
|
:with-signature-id="withSignatureId"
|
||||||
:data-document-uuid="document.uuid"
|
:data-document-uuid="document.uuid"
|
||||||
:default-submitters="defaultSubmitters"
|
:default-submitters="defaultSubmitters"
|
||||||
:drag-field-placeholder="fieldsDragFieldRef.value || dragField"
|
:drag-field-placeholder="fieldsDragFieldRef.value || dragField"
|
||||||
@@ -385,6 +437,7 @@
|
|||||||
:default-required-fields="defaultRequiredFields"
|
:default-required-fields="defaultRequiredFields"
|
||||||
:field-types="fieldTypes"
|
:field-types="fieldTypes"
|
||||||
:with-sticky-submitters="withStickySubmitters"
|
:with-sticky-submitters="withStickySubmitters"
|
||||||
|
:with-signature-id="withSignatureId"
|
||||||
:only-defined-fields="onlyDefinedFields"
|
:only-defined-fields="onlyDefinedFields"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
:show-tour-start-form="showTourStartForm"
|
:show-tour-start-form="showTourStartForm"
|
||||||
@@ -433,6 +486,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import Upload from './upload'
|
import Upload from './upload'
|
||||||
import Dropzone from './dropzone'
|
import Dropzone from './dropzone'
|
||||||
|
import HoverDropzone from './hover_dropzone'
|
||||||
import DragPlaceholder from './drag_placeholder'
|
import DragPlaceholder from './drag_placeholder'
|
||||||
import Fields from './fields'
|
import Fields from './fields'
|
||||||
import MobileDrawField from './mobile_draw_field'
|
import MobileDrawField from './mobile_draw_field'
|
||||||
@@ -462,6 +516,7 @@ export default {
|
|||||||
MobileFields,
|
MobileFields,
|
||||||
Logo,
|
Logo,
|
||||||
Dropzone,
|
Dropzone,
|
||||||
|
HoverDropzone,
|
||||||
DocumentPreview,
|
DocumentPreview,
|
||||||
DocumentControls,
|
DocumentControls,
|
||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
@@ -488,7 +543,9 @@ export default {
|
|||||||
withPayment: this.withPayment,
|
withPayment: this.withPayment,
|
||||||
isPaymentConnected: this.isPaymentConnected,
|
isPaymentConnected: this.isPaymentConnected,
|
||||||
withFormula: this.withFormula,
|
withFormula: this.withFormula,
|
||||||
|
withSignatureId: this.withSignatureId,
|
||||||
withConditions: this.withConditions,
|
withConditions: this.withConditions,
|
||||||
|
isInlineSize: this.isInlineSize,
|
||||||
defaultDrawFieldType: this.defaultDrawFieldType,
|
defaultDrawFieldType: this.defaultDrawFieldType,
|
||||||
selectedAreaRef: computed(() => this.selectedAreaRef),
|
selectedAreaRef: computed(() => this.selectedAreaRef),
|
||||||
fieldsDragFieldRef: computed(() => this.fieldsDragFieldRef)
|
fieldsDragFieldRef: computed(() => this.fieldsDragFieldRef)
|
||||||
@@ -509,6 +566,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
withSignatureId: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
backgroundColor: {
|
backgroundColor: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -665,6 +727,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
withReplaceAndCloneUpload: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
withPhone: {
|
withPhone: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -724,7 +791,8 @@ export default {
|
|||||||
copiedArea: null,
|
copiedArea: null,
|
||||||
drawFieldType: null,
|
drawFieldType: null,
|
||||||
drawOption: null,
|
drawOption: null,
|
||||||
dragField: null
|
dragField: null,
|
||||||
|
isDragFile: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -734,6 +802,9 @@ export default {
|
|||||||
language () {
|
language () {
|
||||||
return this.locale.split('-')[0].toLowerCase()
|
return this.locale.split('-')[0].toLowerCase()
|
||||||
},
|
},
|
||||||
|
isInlineSize () {
|
||||||
|
return CSS.supports('container-type: size')
|
||||||
|
},
|
||||||
isMobile () {
|
isMobile () {
|
||||||
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
|
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
|
||||||
|
|
||||||
@@ -836,6 +907,7 @@ export default {
|
|||||||
window.addEventListener('keydown', this.onKeyDown)
|
window.addEventListener('keydown', this.onKeyDown)
|
||||||
|
|
||||||
window.addEventListener('resize', this.onWindowResize)
|
window.addEventListener('resize', this.onWindowResize)
|
||||||
|
window.addEventListener('dragleave', this.onWindowDragLeave)
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (document.location.search?.includes('stripe_connect_success')) {
|
if (document.location.search?.includes('stripe_connect_success')) {
|
||||||
@@ -854,6 +926,7 @@ export default {
|
|||||||
window.removeEventListener('keydown', this.onKeyDown)
|
window.removeEventListener('keydown', this.onKeyDown)
|
||||||
|
|
||||||
window.removeEventListener('resize', this.onWindowResize)
|
window.removeEventListener('resize', this.onWindowResize)
|
||||||
|
window.removeEventListener('dragleave', this.onWindowDragLeave)
|
||||||
},
|
},
|
||||||
beforeUpdate () {
|
beforeUpdate () {
|
||||||
this.documentRefs = []
|
this.documentRefs = []
|
||||||
@@ -868,6 +941,13 @@ export default {
|
|||||||
|
|
||||||
ref.x = e.clientX - ref.offsetX
|
ref.x = e.clientX - ref.offsetX
|
||||||
ref.y = e.clientY - ref.offsetY
|
ref.y = e.clientY - ref.offsetY
|
||||||
|
} else if (e.dataTransfer?.types?.includes('Files')) {
|
||||||
|
this.isDragFile = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onWindowDragLeave (event) {
|
||||||
|
if (event.clientX <= 0 || event.clientY <= 0 || event.clientX >= window.innerWidth || event.clientY >= window.innerHeight) {
|
||||||
|
this.isDragFile = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
reorderFields (item) {
|
reorderFields (item) {
|
||||||
@@ -984,12 +1064,23 @@ export default {
|
|||||||
field.readonly = true
|
field.readonly = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === 'datenow') {
|
||||||
|
field.type = 'date'
|
||||||
|
field.readonly = true
|
||||||
|
field.default_value = '{{date}}'
|
||||||
|
}
|
||||||
|
|
||||||
if (type === 'date') {
|
if (type === 'date') {
|
||||||
field.preferences = {
|
field.preferences = {
|
||||||
format: this.defaultDateFormat
|
format: this.defaultDateFormat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === 'signature' && [true, false].includes(this.withSignatureId)) {
|
||||||
|
field.preferences ||= {}
|
||||||
|
field.preferences.with_signature_id = this.withSignatureId
|
||||||
|
}
|
||||||
|
|
||||||
this.template.fields.push(field)
|
this.template.fields.push(field)
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
@@ -1250,7 +1341,7 @@ export default {
|
|||||||
area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth)
|
area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDraw (area) {
|
onDraw ({ area, isTooSmall }) {
|
||||||
if (this.drawField) {
|
if (this.drawField) {
|
||||||
if (this.drawOption) {
|
if (this.drawOption) {
|
||||||
const areaWithoutOption = this.drawField.areas?.find((a) => !a.option_uuid)
|
const areaWithoutOption = this.drawField.areas?.find((a) => !a.option_uuid)
|
||||||
@@ -1345,7 +1436,7 @@ export default {
|
|||||||
area.y -= area.h / 2
|
area.y -= area.h / 2
|
||||||
}
|
}
|
||||||
|
|
||||||
if (area.w) {
|
if (area.w && (type !== 'checkbox' || this.drawFieldType || !isTooSmall)) {
|
||||||
this.addField(type, area)
|
this.addField(type, area)
|
||||||
|
|
||||||
this.selectedAreaRef.value = area
|
this.selectedAreaRef.value = area
|
||||||
@@ -1382,6 +1473,12 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (field.type === 'datenow') {
|
||||||
|
field.type = 'date'
|
||||||
|
field.readonly = true
|
||||||
|
field.default_value = '{{date}}'
|
||||||
|
}
|
||||||
|
|
||||||
if (['stamp', 'heading'].includes(field.type)) {
|
if (['stamp', 'heading'].includes(field.type)) {
|
||||||
field.readonly = true
|
field.readonly = true
|
||||||
}
|
}
|
||||||
@@ -1390,6 +1487,11 @@ export default {
|
|||||||
field.preferences ||= {}
|
field.preferences ||= {}
|
||||||
field.preferences.format ||= this.defaultDateFormat
|
field.preferences.format ||= this.defaultDateFormat
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (field.type === 'signature' && [true, false].includes(this.withSignatureId)) {
|
||||||
|
field.preferences ||= {}
|
||||||
|
field.preferences.with_signature_id = this.withSignatureId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fieldArea = {
|
const fieldArea = {
|
||||||
@@ -1529,6 +1631,9 @@ export default {
|
|||||||
})
|
})
|
||||||
}, 'image/png')
|
}, 'image/png')
|
||||||
},
|
},
|
||||||
|
onUploadFailed (error) {
|
||||||
|
if (error) alert(error)
|
||||||
|
},
|
||||||
updateFromUpload (data) {
|
updateFromUpload (data) {
|
||||||
this.template.schema.push(...data.schema)
|
this.template.schema.push(...data.schema)
|
||||||
this.template.documents.push(...data.documents)
|
this.template.documents.push(...data.documents)
|
||||||
@@ -1649,6 +1754,29 @@ export default {
|
|||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
|
onDocumentsReplace (data) {
|
||||||
|
data.schema.forEach((schemaItem, index) => {
|
||||||
|
const existingSchemaItem = this.template.schema[index]
|
||||||
|
|
||||||
|
if (this.template.schema[index]) {
|
||||||
|
this.onDocumentReplace({
|
||||||
|
replaceSchemaItem: existingSchemaItem,
|
||||||
|
schema: [schemaItem],
|
||||||
|
documents: [data.documents.find((doc) => doc.uuid === schemaItem.attachment_uuid)]
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.updateFromUpload({
|
||||||
|
schema: [schemaItem],
|
||||||
|
documents: [data.documents.find((doc) => doc.uuid === schemaItem.attachment_uuid)],
|
||||||
|
fields: data.fields,
|
||||||
|
submitters: data.submitters
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onDocumentsReplaceAndTemplateClone (template) {
|
||||||
|
window.Turbo.visit(`/templates/${template.id}/edit`)
|
||||||
|
},
|
||||||
moveDocument (item, direction) {
|
moveDocument (item, direction) {
|
||||||
const currentIndex = this.template.schema.indexOf(item)
|
const currentIndex = this.template.schema.indexOf(item)
|
||||||
|
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ export default {
|
|||||||
fields () {
|
fields () {
|
||||||
if (this.item.submitter_uuid) {
|
if (this.item.submitter_uuid) {
|
||||||
return this.template.fields.reduce((acc, f) => {
|
return this.template.fields.reduce((acc, f) => {
|
||||||
if (f !== this.item) {
|
if (f !== this.item && (!f.conditions?.length || !f.conditions.find((c) => c.field_uuid === this.item.uuid))) {
|
||||||
acc.push(f)
|
acc.push(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
:data-page="index"
|
:data-page="index"
|
||||||
:areas="areasIndex[index]"
|
:areas="areasIndex[index]"
|
||||||
:allow-draw="allowDraw"
|
:allow-draw="allowDraw"
|
||||||
|
:with-signature-id="withSignatureId"
|
||||||
:is-drag="isDrag"
|
:is-drag="isDrag"
|
||||||
:with-field-placeholder="withFieldPlaceholder"
|
:with-field-placeholder="withFieldPlaceholder"
|
||||||
:default-fields="defaultFields"
|
:default-fields="defaultFields"
|
||||||
@@ -23,12 +24,13 @@
|
|||||||
@drop-field="$emit('drop-field', {...$event, attachment_uuid: document.uuid })"
|
@drop-field="$emit('drop-field', {...$event, attachment_uuid: document.uuid })"
|
||||||
@remove-area="$emit('remove-area', $event)"
|
@remove-area="$emit('remove-area', $event)"
|
||||||
@scroll-to="scrollToArea"
|
@scroll-to="scrollToArea"
|
||||||
@draw="$emit('draw', {...$event, attachment_uuid: document.uuid })"
|
@draw="$emit('draw', { area: {...$event.area, attachment_uuid: document.uuid }, isTooSmall: $event.isTooSmall })"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import Page from './page'
|
import Page from './page'
|
||||||
|
import { reactive } from 'vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TemplateDocument',
|
name: 'TemplateDocument',
|
||||||
@@ -65,6 +67,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
withSignatureId: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
drawFieldType: {
|
drawFieldType: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -123,14 +130,14 @@ export default {
|
|||||||
return this.document.metadata?.pdf?.number_of_pages || this.document.preview_images.length
|
return this.document.metadata?.pdf?.number_of_pages || this.document.preview_images.length
|
||||||
},
|
},
|
||||||
sortedPreviewImages () {
|
sortedPreviewImages () {
|
||||||
const lazyloadMetadata = this.document.preview_images[this.document.preview_images.length - 1].metadata
|
const lazyloadMetadata = this.document.preview_images[this.document.preview_images.length - 1]?.metadata || { width: 1400, height: 1812 }
|
||||||
|
|
||||||
return [...Array(this.numberOfPages).keys()].map((i) => {
|
return [...Array(this.numberOfPages).keys()].map((i) => {
|
||||||
return this.previewImagesIndex[i] || {
|
return this.previewImagesIndex[i] || reactive({
|
||||||
metadata: lazyloadMetadata,
|
metadata: { ...lazyloadMetadata },
|
||||||
id: Math.random().toString(),
|
id: Math.random().toString(),
|
||||||
url: this.basePreviewUrl + `/preview/${this.document.signed_uuid || this.document.uuid}/${i}.jpg`
|
url: this.basePreviewUrl + `/preview/${this.document.signed_uuid || this.document.uuid}/${i}.jpg`
|
||||||
}
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
previewImagesIndex () {
|
previewImagesIndex () {
|
||||||
|
|||||||
@@ -2,34 +2,42 @@
|
|||||||
<div
|
<div
|
||||||
class="flex h-60 w-full"
|
class="flex h-60 w-full"
|
||||||
@dragover.prevent
|
@dragover.prevent
|
||||||
|
@dragenter="isDragEntering = true"
|
||||||
|
@dragleave="isDragEntering = false"
|
||||||
@drop.prevent="onDropFiles"
|
@drop.prevent="onDropFiles"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
id="document_dropzone"
|
id="document_dropzone"
|
||||||
class="w-full relative hover:bg-base-200/30 rounded-md border border-2 border-base-content/10 border-dashed"
|
class="w-full relative rounded-md border-2 border-base-content/10 border-dashed"
|
||||||
:for="inputId"
|
:for="inputId"
|
||||||
:class="{ 'opacity-50': isLoading || isProcessing }"
|
:class="{ 'opacity-50': isLoading, 'hover:bg-base-200/50': withHoverClass && !isDragEntering, 'bg-base-200/50 border-base-content/30': isDragEntering }"
|
||||||
>
|
>
|
||||||
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
|
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center pointer-events-none">
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
<IconInnerShadowTop
|
<IconInnerShadowTop
|
||||||
v-if="isLoading || isProcessing"
|
v-if="isLoading"
|
||||||
class="animate-spin"
|
class="animate-spin"
|
||||||
:width="40"
|
:width="40"
|
||||||
:height="40"
|
:height="40"
|
||||||
/>
|
/>
|
||||||
<IconCloudUpload
|
<component
|
||||||
|
:is="icon"
|
||||||
v-else
|
v-else
|
||||||
|
class="stroke-[1.5px]"
|
||||||
:width="40"
|
:width="40"
|
||||||
:height="40"
|
:height="40"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="message"
|
v-if="message"
|
||||||
class="font-medium text-lg mb-1"
|
class="text-lg mb-1"
|
||||||
|
:class="{ 'mt-1': !withDescription, 'font-medium': withDescription }"
|
||||||
>
|
>
|
||||||
{{ message }}
|
{{ message }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm">
|
<div
|
||||||
|
v-if="withDescription"
|
||||||
|
class="text-sm"
|
||||||
|
>
|
||||||
<span class="font-medium">{{ t('click_to_upload') }}</span> {{ t('or_drag_and_drop_files') }}
|
<span class="font-medium">{{ t('click_to_upload') }}</span> {{ t('or_drag_and_drop_files') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,13 +62,16 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Upload from './upload'
|
import Upload from './upload'
|
||||||
import { IconCloudUpload, IconInnerShadowTop } from '@tabler/icons-vue'
|
import { IconCloudUpload, IconFilePlus, IconFileSymlink, IconFiles, IconInnerShadowTop } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FileDropzone',
|
name: 'FileDropzone',
|
||||||
components: {
|
components: {
|
||||||
|
IconFilePlus,
|
||||||
IconCloudUpload,
|
IconCloudUpload,
|
||||||
IconInnerShadowTop
|
IconInnerShadowTop,
|
||||||
|
IconFileSymlink,
|
||||||
|
IconFiles
|
||||||
},
|
},
|
||||||
inject: ['baseFetch', 't'],
|
inject: ['baseFetch', 't'],
|
||||||
props: {
|
props: {
|
||||||
@@ -68,35 +79,70 @@ export default {
|
|||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
withHoverClass: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'IconCloudUpload'
|
||||||
|
},
|
||||||
|
cloneTemplateOnUpload: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
withDescription: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
acceptFileTypes: {
|
acceptFileTypes: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'image/*, application/pdf'
|
default: 'image/*, application/pdf'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['success'],
|
emits: ['success', 'error', 'loading'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isProcessing: false
|
isDragEntering: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
inputId () {
|
inputId () {
|
||||||
return 'el' + Math.random().toString(32).split('.')[1]
|
return 'el' + Math.random().toString(32).split('.')[1]
|
||||||
},
|
},
|
||||||
|
uploadUrl () {
|
||||||
|
if (this.cloneTemplateOnUpload) {
|
||||||
|
return `/templates/${this.templateId}/clone_and_replace`
|
||||||
|
} else {
|
||||||
|
return `/templates/${this.templateId}/documents`
|
||||||
|
}
|
||||||
|
},
|
||||||
message () {
|
message () {
|
||||||
if (this.isLoading) {
|
if (this.isLoading) {
|
||||||
return this.t('uploading')
|
return this.t('uploading')
|
||||||
} else if (this.isProcessing) {
|
|
||||||
return this.t('processing_')
|
|
||||||
} else if (this.acceptFileTypes === 'image/*, application/pdf') {
|
} else if (this.acceptFileTypes === 'image/*, application/pdf') {
|
||||||
return this.t('add_pdf_documents_or_images')
|
return this.title || this.t('add_pdf_documents_or_images')
|
||||||
} else {
|
} else {
|
||||||
return this.t('add_documents_or_images')
|
return this.title || this.t('add_documents_or_images')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
isLoading (value) {
|
||||||
|
this.$emit('loading', value)
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
upload: Upload.methods.upload,
|
upload: Upload.methods.upload,
|
||||||
onDropFiles (e) {
|
onDropFiles (e) {
|
||||||
|
|||||||
@@ -125,6 +125,7 @@
|
|||||||
:field="field"
|
:field="field"
|
||||||
:default-field="defaultField"
|
:default-field="defaultField"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
|
:with-signature-id="withSignatureId"
|
||||||
:background-color="dropdownBgColor"
|
:background-color="dropdownBgColor"
|
||||||
@click-formula="isShowFormulaModal = true"
|
@click-formula="isShowFormulaModal = true"
|
||||||
@click-font="isShowFontModal = true"
|
@click-font="isShowFontModal = true"
|
||||||
@@ -302,6 +303,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
withSignatureId: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
withOptions: {
|
withOptions: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -153,13 +153,13 @@
|
|||||||
<option
|
<option
|
||||||
v-for="(key, value) in validations"
|
v-for="(key, value) in validations"
|
||||||
:key="key"
|
:key="key"
|
||||||
:selected="field.validation?.pattern ? value === field.validation.pattern : value === 'none'"
|
:selected="lengthValidation ? key == 'length' : (field.validation?.pattern ? value === field.validation.pattern : key === 'none')"
|
||||||
:value="value"
|
:value="key"
|
||||||
>
|
>
|
||||||
{{ t(key) }}
|
{{ t(key) }}
|
||||||
</option>
|
</option>
|
||||||
<option
|
<option
|
||||||
:selected="field.validation && !validations[field.validation.pattern]"
|
:selected="field.validation && !validations[field.validation.pattern] && !lengthValidation"
|
||||||
:value="validations[field.validation?.pattern] || !field.validation?.pattern ? 'custom' : field.validation?.pattern"
|
:value="validations[field.validation?.pattern] || !field.validation?.pattern ? 'custom' : field.validation?.pattern"
|
||||||
>
|
>
|
||||||
{{ t('custom') }}
|
{{ t('custom') }}
|
||||||
@@ -174,7 +174,51 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern]"
|
v-if="['text', 'cells'].includes(field.type) && field.validation && lengthValidation"
|
||||||
|
class="py-1.5 px-1 relative flex space-x-1"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<div class="w-1/2 relative">
|
||||||
|
<input
|
||||||
|
:placeholder="t('min')"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
:value="lengthValidation.min"
|
||||||
|
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
|
||||||
|
@input="field.validation.pattern = `.{${$event.target.value || 0},${lengthValidation.max || ''}}`"
|
||||||
|
@blur="save"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
v-if="lengthValidation.min"
|
||||||
|
:style="{ backgroundColor }"
|
||||||
|
class="absolute -top-2.5 left-1.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>
|
||||||
|
{{ t('min') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="w-1/2 relative">
|
||||||
|
<input
|
||||||
|
:placeholder="t('max')"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
|
||||||
|
:value="lengthValidation.max"
|
||||||
|
@input="field.validation.pattern = `.{${lengthValidation.min},${$event.target.value || ''}}`"
|
||||||
|
@blur="save"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
v-if="lengthValidation.max"
|
||||||
|
:style="{ backgroundColor }"
|
||||||
|
class="absolute -top-2.5 left-1.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>
|
||||||
|
{{ t('max') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation"
|
||||||
class="py-1.5 px-1 relative"
|
class="py-1.5 px-1 relative"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
@@ -195,6 +239,27 @@
|
|||||||
{{ t('regexp_validation') }}
|
{{ t('regexp_validation') }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation"
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="field.validation.message"
|
||||||
|
:placeholder="t('error_message')"
|
||||||
|
dir="auto"
|
||||||
|
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0 bg-transparent"
|
||||||
|
@blur="save"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
v-if="field.validation.message"
|
||||||
|
:style="{ backgroundColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>
|
||||||
|
{{ t('error_message') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="field.type === 'date'"
|
v-if="field.type === 'date'"
|
||||||
class="py-1.5 px-1 relative"
|
class="py-1.5 px-1 relative"
|
||||||
@@ -255,6 +320,21 @@
|
|||||||
{{ t('format') }}
|
{{ t('format') }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<li
|
||||||
|
v-if="[true, false].includes(withSignatureId) && field.type === 'signature'"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<label class="cursor-pointer py-1.5">
|
||||||
|
<input
|
||||||
|
:checked="field.preferences?.with_signature_id"
|
||||||
|
type="checkbox"
|
||||||
|
:disabled="!editable || (defaultField && [true, false].includes(defaultField.required))"
|
||||||
|
class="toggle toggle-xs"
|
||||||
|
@change="[field.preferences ||= {}, field.preferences.with_signature_id = $event.target.checked, save()]"
|
||||||
|
>
|
||||||
|
<span class="label-text">{{ t('signature_id') }}</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="withRequired && field.type !== 'phone' && field.type !== 'stamp' && field.type !== 'verification'"
|
v-if="withRequired && field.type !== 'phone' && field.type !== 'stamp' && field.type !== 'verification'"
|
||||||
@click.stop
|
@click.stop
|
||||||
@@ -330,7 +410,7 @@
|
|||||||
v-if="field.type != 'stamp'"
|
v-if="field.type != 'stamp'"
|
||||||
class="pb-0.5 mt-0.5"
|
class="pb-0.5 mt-0.5"
|
||||||
>
|
>
|
||||||
<li v-if="['text', 'number', 'date'].includes(field.type)">
|
<li v-if="['text', 'number', 'date', 'select'].includes(field.type)">
|
||||||
<label
|
<label
|
||||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||||
@click="$emit('click-font')"
|
@click="$emit('click-font')"
|
||||||
@@ -424,7 +504,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
<li v-if="field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells'].includes(field.type)">
|
<li v-if="field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells', 'stamp'].includes(field.type)">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="text-sm py-1 px-2"
|
class="text-sm py-1 px-2"
|
||||||
@@ -460,6 +540,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
withSignatureId: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
backgroundColor: {
|
backgroundColor: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -489,6 +574,7 @@ export default {
|
|||||||
emits: ['set-draw', 'scroll-to', 'click-formula', 'click-description', 'click-condition', 'click-font', 'remove-area'],
|
emits: ['set-draw', 'scroll-to', 'click-formula', 'click-description', 'click-condition', 'click-font', 'remove-area'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
selectedValidation: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -533,8 +619,16 @@ export default {
|
|||||||
|
|
||||||
return formats
|
return formats
|
||||||
},
|
},
|
||||||
|
lengthValidation () {
|
||||||
|
if (this.field.validation?.pattern && this.selectedValidation !== 'custom') {
|
||||||
|
return this.field.validation.pattern.match(/^\.{(?<min>\d+),(?<max>\d+)?}$/)?.groups
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
validations () {
|
validations () {
|
||||||
return {
|
return {
|
||||||
|
'.{0,}': 'length',
|
||||||
'^[0-9]{3}-[0-9]{2}-[0-9]{4}$': 'ssn',
|
'^[0-9]{3}-[0-9]{2}-[0-9]{4}$': 'ssn',
|
||||||
'^[0-9]{2}-[0-9]{7}$': 'ein',
|
'^[0-9]{2}-[0-9]{7}$': 'ein',
|
||||||
'^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$': 'email',
|
'^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$': 'email',
|
||||||
@@ -553,13 +647,21 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
onChangeValidation (event) {
|
onChangeValidation (event) {
|
||||||
if (event.target.value === 'custom') {
|
if (event.target.value === 'custom') {
|
||||||
this.field.validation = { pattern: '' }
|
this.selectedValidation = 'custom'
|
||||||
|
|
||||||
|
this.field.validation = { pattern: '', message: '' }
|
||||||
|
|
||||||
this.$nextTick(() => this.$refs.validationCustom.focus())
|
this.$nextTick(() => this.$refs.validationCustom.focus())
|
||||||
} else if (event.target.value) {
|
} else if (event.target.value) {
|
||||||
this.field.validation ||= {}
|
this.field.validation ||= {}
|
||||||
this.field.validation.pattern = event.target.value
|
this.field.validation.pattern =
|
||||||
|
Object.keys(this.validations).find(key => this.validations[key] === event.target.value)
|
||||||
|
|
||||||
|
this.selectedValidation = event.target.value
|
||||||
|
delete this.field.validation.message
|
||||||
} else {
|
} else {
|
||||||
|
this.selectedValidation = ''
|
||||||
|
|
||||||
delete this.field.validation
|
delete this.field.validation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
v-for="(icon, type) in fieldIconsSorted"
|
v-for="(icon, type) in fieldIconsSorted"
|
||||||
:key="type"
|
:key="type"
|
||||||
>
|
>
|
||||||
<li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification')">
|
<li v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification'))">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="text-sm py-1 px-2"
|
class="text-sm py-1 px-2"
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading, IconId } from '@tabler/icons-vue'
|
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading, IconId, IconCalendarCheck } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FiledTypeDropdown',
|
name: 'FiledTypeDropdown',
|
||||||
@@ -101,6 +101,7 @@ export default {
|
|||||||
signature: this.t('signature'),
|
signature: this.t('signature'),
|
||||||
initials: this.t('initials'),
|
initials: this.t('initials'),
|
||||||
date: this.t('date'),
|
date: this.t('date'),
|
||||||
|
datenow: this.t('date_signed'),
|
||||||
number: this.t('number'),
|
number: this.t('number'),
|
||||||
image: this.t('image'),
|
image: this.t('image'),
|
||||||
file: this.t('file'),
|
file: this.t('file'),
|
||||||
@@ -142,6 +143,7 @@ export default {
|
|||||||
signature: IconWritingSign,
|
signature: IconWritingSign,
|
||||||
initials: IconLetterCaseUpper,
|
initials: IconLetterCaseUpper,
|
||||||
date: IconCalendarEvent,
|
date: IconCalendarEvent,
|
||||||
|
datenow: IconCalendarCheck,
|
||||||
number: IconSquareNumber1,
|
number: IconSquareNumber1,
|
||||||
image: IconPhoto,
|
image: IconPhoto,
|
||||||
checkbox: IconCheckbox,
|
checkbox: IconCheckbox,
|
||||||
@@ -164,7 +166,7 @@ export default {
|
|||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
} else {
|
} else {
|
||||||
return Object.fromEntries(Object.entries(this.fieldIcons).filter(([key]) => key !== 'heading'))
|
return Object.fromEntries(Object.entries(this.fieldIcons).filter(([key]) => key !== 'heading' && key !== 'datenow'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
:field="field"
|
:field="field"
|
||||||
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
|
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
|
:with-signature-id="withSignatureId"
|
||||||
:default-field="defaultFieldsIndex[field.name]"
|
:default-field="defaultFieldsIndex[field.name]"
|
||||||
:draggable="editable"
|
:draggable="editable"
|
||||||
@dragstart="[fieldsDragFieldRef.value = field, removeDragOverlay($event), setDragPlaceholder($event)]"
|
@dragstart="[fieldsDragFieldRef.value = field, removeDragOverlay($event), setDragPlaceholder($event)]"
|
||||||
@@ -112,7 +113,7 @@
|
|||||||
:key="type"
|
:key="type"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification')"
|
v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification'))"
|
||||||
:id="`${type}_type_field_button`"
|
:id="`${type}_type_field_button`"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
class="field-type-button group flex items-center justify-center border border-dashed w-full rounded relative fields-grid-item"
|
class="field-type-button group flex items-center justify-center border border-dashed w-full rounded relative fields-grid-item"
|
||||||
@@ -168,7 +169,7 @@
|
|||||||
:data-tip="t('obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more')"
|
:data-tip="t('obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more')"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="https://www.docuseal.com/contact"
|
href="https://www.docuseal.com/qualified-electronic-signature"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative fields-grid-item"
|
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative fields-grid-item"
|
||||||
:style="{ backgroundColor }"
|
:style="{ backgroundColor }"
|
||||||
@@ -253,6 +254,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
withSignatureId: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
template: {
|
template: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
@@ -347,7 +353,7 @@ export default {
|
|||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
} else {
|
} else {
|
||||||
return Object.fromEntries(Object.entries(this.fieldIcons).filter(([key]) => key !== 'heading'))
|
return Object.fromEntries(Object.entries(this.fieldIcons).filter(([key]) => key !== 'heading' && key !== 'datenow'))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
submitterFields () {
|
submitterFields () {
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
<span class="relative">
|
<span class="relative">
|
||||||
<select
|
<select
|
||||||
class="select input-bordered bg-white select-sm text-center pl-2"
|
class="select input-bordered bg-white select-sm text-center pl-2"
|
||||||
style="font-size: 16px; width: 86px; text-align-last: center;"
|
style="font-size: 16px; line-height: 12px; width: 86px; text-align-last: center;"
|
||||||
@change="$event.target.value ? preferences.font_size = parseInt($event.target.value) : delete preferences.font_size"
|
@change="$event.target.value ? preferences.font_size = parseInt($event.target.value) : delete preferences.font_size"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
@@ -77,12 +77,12 @@
|
|||||||
</select>
|
</select>
|
||||||
<span
|
<span
|
||||||
class="border-l pl-1.5 absolute bg-white bottom-0 pointer-events-none text-sm h-5"
|
class="border-l pl-1.5 absolute bg-white bottom-0 pointer-events-none text-sm h-5"
|
||||||
style="right: 13px; top: 7px"
|
style="right: 13px; top: 6px"
|
||||||
>
|
>
|
||||||
pt
|
pt
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span class="flex">
|
||||||
<div
|
<div
|
||||||
class="join"
|
class="join"
|
||||||
style="height: 32px"
|
style="height: 32px"
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span class="flex">
|
||||||
<div
|
<div
|
||||||
class="join"
|
class="join"
|
||||||
style="height: 32px"
|
style="height: 32px"
|
||||||
@@ -114,6 +114,32 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
<span class="flex">
|
||||||
|
<div class="dropdown modal-field-font-dropdown">
|
||||||
|
<label
|
||||||
|
tabindex="0"
|
||||||
|
class="cursor-pointer flex bg-white border input-bordered rounded-md h-8 items-center justify-center px-1"
|
||||||
|
style="-webkit-appearance: none; -moz-appearance: none;"
|
||||||
|
>
|
||||||
|
<component :is="valigns.find((v) => v.value === (preferences.valign || 'center'))?.icon" />
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
tabindex="0"
|
||||||
|
class="dropdown-content p-0 mt-1 block z-10 menu shadow bg-white border border-base-300 rounded-md"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(valign, index) in valigns"
|
||||||
|
:key="index"
|
||||||
|
:value="valign.value"
|
||||||
|
:class="{ 'bg-base-300': preferences.valign == valign.value }"
|
||||||
|
class="hover:bg-base-300 px-2 py-1.5 cursor-pointer"
|
||||||
|
@click="[valign.value ? preferences.valign = valign.value : delete preferences.valign, closeDropdown()]"
|
||||||
|
>
|
||||||
|
<component :is="valign.icon" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<select
|
<select
|
||||||
class="input input-bordered bg-white input-sm text-lg rounded-md"
|
class="input input-bordered bg-white input-sm text-lg rounded-md"
|
||||||
@@ -134,10 +160,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<div
|
<div
|
||||||
class="flex items-center border border-base-content/20 rounded-xl bg-white px-4 h-16 modal-field-font-preview"
|
class="flex border border-base-content/20 rounded-xl bg-white px-4 h-16 modal-field-font-preview"
|
||||||
:style="{
|
:style="{
|
||||||
color: preferences.color || 'black',
|
color: preferences.color || 'black',
|
||||||
fontSize: (preferences.font_size || 12) + 'pt',
|
fontSize: (preferences.font_size || 11) + 'pt',
|
||||||
}"
|
}"
|
||||||
:class="textClasses"
|
:class="textClasses"
|
||||||
>
|
>
|
||||||
@@ -163,7 +189,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconChevronDown, IconBold, IconItalic, IconAlignLeft, IconAlignRight, IconAlignCenter } from '@tabler/icons-vue'
|
import { IconChevronDown, IconBold, IconItalic, IconAlignLeft, IconAlignRight, IconAlignCenter, IconAlignBoxCenterTop, IconAlignBoxCenterBottom, IconAlignBoxCenterMiddle } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FontModal',
|
name: 'FontModal',
|
||||||
@@ -196,8 +222,8 @@ export default {
|
|||||||
fonts () {
|
fonts () {
|
||||||
return [
|
return [
|
||||||
{ value: null, label: 'Default' },
|
{ value: null, label: 'Default' },
|
||||||
{ value: 'Times', label: 'Times', class: 'font-serif' },
|
{ value: 'Times', label: 'Times', class: 'font-times' },
|
||||||
{ value: 'Courier', label: 'Courier', class: 'font-mono' }
|
{ value: 'Courier', label: 'Courier', class: 'font-courier' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
types () {
|
types () {
|
||||||
@@ -213,6 +239,13 @@ export default {
|
|||||||
{ icon: IconAlignRight, value: 'right' }
|
{ icon: IconAlignRight, value: 'right' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
valigns () {
|
||||||
|
return [
|
||||||
|
{ icon: IconAlignBoxCenterTop, value: 'top' },
|
||||||
|
{ icon: IconAlignBoxCenterMiddle, value: 'center' },
|
||||||
|
{ icon: IconAlignBoxCenterBottom, value: 'bottom' }
|
||||||
|
]
|
||||||
|
},
|
||||||
sizes () {
|
sizes () {
|
||||||
return [...Array(23).keys()].map(i => i + 6)
|
return [...Array(23).keys()].map(i => i + 6)
|
||||||
},
|
},
|
||||||
@@ -225,17 +258,20 @@ export default {
|
|||||||
},
|
},
|
||||||
textClasses () {
|
textClasses () {
|
||||||
return {
|
return {
|
||||||
'font-mono': this.preferences.font === 'Courier',
|
'font-courier': this.preferences.font === 'Courier',
|
||||||
'font-serif': this.preferences.font === 'Times',
|
'font-times': this.preferences.font === 'Times',
|
||||||
'justify-center': this.preferences.align === 'center',
|
'justify-center': this.preferences.align === 'center',
|
||||||
'justify-start': this.preferences.align === 'left',
|
'justify-start': this.preferences.align === 'left',
|
||||||
'justify-end': this.preferences.align === 'right',
|
'justify-end': this.preferences.align === 'right',
|
||||||
|
'items-center': !this.preferences.valign || this.preferences.valign === 'center',
|
||||||
|
'items-start': this.preferences.valign === 'top',
|
||||||
|
'items-end': this.preferences.valign === 'bottom',
|
||||||
'font-bold': ['bold_italic', 'bold'].includes(this.preferences.font_type),
|
'font-bold': ['bold_italic', 'bold'].includes(this.preferences.font_type),
|
||||||
italic: ['bold_italic', 'italic'].includes(this.preferences.font_type)
|
italic: ['bold_italic', 'italic'].includes(this.preferences.font_type)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
keys () {
|
keys () {
|
||||||
return ['font_type', 'font_size', 'color', 'align', 'font']
|
return ['font_type', 'font_size', 'color', 'align', 'valign', 'font']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
fields () {
|
fields () {
|
||||||
return this.template.fields.reduce((acc, f) => {
|
return this.template.fields.reduce((acc, f) => {
|
||||||
if (f !== this.field && f.submitter_uuid === this.field.submitter_uuid && ['number'].includes(f.type) && !f.preferences?.formula) {
|
if (f !== this.field && ['number'].includes(f.type) && (!f.preferences?.formula || f.submitter_uuid !== this.field.submitter_uuid)) {
|
||||||
acc.push(f)
|
acc.push(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="isDragging || isLoading"
|
||||||
|
class="modal modal-open"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-2 p-4 items-center bg-base-100 h-full max-h-[85vh] max-w-6xl rounded-2xl w-full">
|
||||||
|
<Dropzone
|
||||||
|
class="flex-1 h-full"
|
||||||
|
icon="IconFilePlus"
|
||||||
|
:template-id="templateId"
|
||||||
|
:accept-file-types="acceptFileTypes"
|
||||||
|
:with-hover-class="false"
|
||||||
|
:with-description="false"
|
||||||
|
:title="t('add_a_new_document')"
|
||||||
|
type="add_files"
|
||||||
|
@loading="isLoading = $event"
|
||||||
|
@success="$emit('add', $event)"
|
||||||
|
@error="$emit('error', $event)"
|
||||||
|
/>
|
||||||
|
<div class="flex-1 flex gap-2 w-full">
|
||||||
|
<Dropzone
|
||||||
|
class="flex-1 h-full"
|
||||||
|
icon="IconFileSymlink"
|
||||||
|
:template-id="templateId"
|
||||||
|
:accept-file-types="acceptFileTypes"
|
||||||
|
:with-hover-class="false"
|
||||||
|
:with-description="false"
|
||||||
|
:title="t('replace_existing_document')"
|
||||||
|
@loading="isLoading = $event"
|
||||||
|
@success="$emit('replace', $event)"
|
||||||
|
@error="$emit('error', $event)"
|
||||||
|
/>
|
||||||
|
<Dropzone
|
||||||
|
v-if="withReplaceAndClone"
|
||||||
|
class="flex-1 h-full"
|
||||||
|
icon="IconFiles"
|
||||||
|
:template-id="templateId"
|
||||||
|
:accept-file-types="acceptFileTypes"
|
||||||
|
:with-hover-class="false"
|
||||||
|
:with-description="false"
|
||||||
|
:clone-template-on-upload="true"
|
||||||
|
:title="t('clone_and_replace_documents')"
|
||||||
|
@loading="isLoading = $event"
|
||||||
|
@success="$emit('replace-and-clone', $event)"
|
||||||
|
@error="$emit('error', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Dropzone from './dropzone'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HoverDropzone',
|
||||||
|
components: {
|
||||||
|
Dropzone
|
||||||
|
},
|
||||||
|
inject: ['t'],
|
||||||
|
props: {
|
||||||
|
isDragging: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
templateId: {
|
||||||
|
type: [Number, String],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
withReplaceAndClone: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
acceptFileTypes: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'image/*, application/pdf'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['add', 'replace', 'replace-and-clone', 'error'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isLoading: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
const en = {
|
const en = {
|
||||||
|
signature_id: 'Signature ID',
|
||||||
|
error_message: 'Error message',
|
||||||
|
length: 'Length',
|
||||||
|
min: 'Min',
|
||||||
|
max: 'Max',
|
||||||
font: 'Font',
|
font: 'Font',
|
||||||
party: 'Party',
|
party: 'Party',
|
||||||
|
date_signed: 'Date Signed',
|
||||||
method: 'Method',
|
method: 'Method',
|
||||||
reorder_fields: 'Reorder fields',
|
reorder_fields: 'Reorder fields',
|
||||||
verify_id: 'Verify ID',
|
verify_id: 'Verify ID',
|
||||||
@@ -63,6 +69,9 @@ const en = {
|
|||||||
processing_: 'Processing...',
|
processing_: 'Processing...',
|
||||||
add_pdf_documents_or_images: 'Add PDF documents or images',
|
add_pdf_documents_or_images: 'Add PDF documents or images',
|
||||||
add_documents_or_images: 'Add documents or images',
|
add_documents_or_images: 'Add documents or images',
|
||||||
|
add_a_new_document: 'Add a new document',
|
||||||
|
replace_existing_document: 'Replace existing document',
|
||||||
|
clone_and_replace_documents: 'Clone and replace documents',
|
||||||
required: 'Required',
|
required: 'Required',
|
||||||
default_value: 'Default value',
|
default_value: 'Default value',
|
||||||
format: 'Format',
|
format: 'Format',
|
||||||
@@ -164,6 +173,12 @@ const en = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
|
signature_id: 'ID de Firma',
|
||||||
|
error_message: 'Mensaje de error',
|
||||||
|
length: 'Longitud',
|
||||||
|
min: 'Mín',
|
||||||
|
max: 'Máx',
|
||||||
|
date_signed: 'Fecha actual',
|
||||||
fuente: 'Fuente',
|
fuente: 'Fuente',
|
||||||
party: 'Parte',
|
party: 'Parte',
|
||||||
method: 'Método',
|
method: 'Método',
|
||||||
@@ -225,6 +240,9 @@ const es = {
|
|||||||
processing_: 'Procesando...',
|
processing_: 'Procesando...',
|
||||||
add_pdf_documents_or_images: 'Agregar documentos PDF o imágenes',
|
add_pdf_documents_or_images: 'Agregar documentos PDF o imágenes',
|
||||||
add_documents_or_images: 'Agregar documentos o imágenes',
|
add_documents_or_images: 'Agregar documentos o imágenes',
|
||||||
|
add_a_new_document: 'Agregar un nuevo documento',
|
||||||
|
replace_existing_document: 'Reemplazar documento existente',
|
||||||
|
clone_and_replace_documents: 'Clonar y reemplazar documentos',
|
||||||
required: 'Requerido',
|
required: 'Requerido',
|
||||||
default_value: 'Valor predeterminado',
|
default_value: 'Valor predeterminado',
|
||||||
format: 'Formato',
|
format: 'Formato',
|
||||||
@@ -329,6 +347,12 @@ const es = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
|
signature_id: 'ID firma',
|
||||||
|
error_message: 'Messaggio di errore',
|
||||||
|
length: 'Lunghezza',
|
||||||
|
min: 'Min',
|
||||||
|
max: 'Max',
|
||||||
|
date_signed: 'Data attuale',
|
||||||
font: 'Carattere',
|
font: 'Carattere',
|
||||||
party: 'Parte',
|
party: 'Parte',
|
||||||
method: 'Metodo',
|
method: 'Metodo',
|
||||||
@@ -393,6 +417,9 @@ const it = {
|
|||||||
processing_: 'Elaborazione...',
|
processing_: 'Elaborazione...',
|
||||||
add_pdf_documents_or_images: 'Aggiungi documenti PDF o immagini',
|
add_pdf_documents_or_images: 'Aggiungi documenti PDF o immagini',
|
||||||
add_documents_or_images: 'Aggiungi documenti o immagini',
|
add_documents_or_images: 'Aggiungi documenti o immagini',
|
||||||
|
add_a_new_document: 'Aggiungi un nuovo documento',
|
||||||
|
replace_existing_document: 'Sostituisci documento esistente',
|
||||||
|
clone_and_replace_documents: 'Clona e sostituisci documenti',
|
||||||
required: 'Obbligatorio',
|
required: 'Obbligatorio',
|
||||||
default_value: 'Valore predefinito',
|
default_value: 'Valore predefinito',
|
||||||
format: 'Formato',
|
format: 'Formato',
|
||||||
@@ -494,6 +521,12 @@ const it = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
|
signature_id: 'ID da Assinatura',
|
||||||
|
error_message: 'Mensagem de erro',
|
||||||
|
length: 'Comprimento',
|
||||||
|
min: 'Mín',
|
||||||
|
max: 'Máx',
|
||||||
|
date_signed: 'Data atual',
|
||||||
fonte: 'Fonte',
|
fonte: 'Fonte',
|
||||||
party: 'Parte',
|
party: 'Parte',
|
||||||
method: 'Método',
|
method: 'Método',
|
||||||
@@ -555,6 +588,9 @@ const pt = {
|
|||||||
processing_: 'Processando...',
|
processing_: 'Processando...',
|
||||||
add_pdf_documents_or_images: 'Adicionar documentos PDF ou imagens',
|
add_pdf_documents_or_images: 'Adicionar documentos PDF ou imagens',
|
||||||
add_documents_or_images: 'Adicionar documentos ou imagens',
|
add_documents_or_images: 'Adicionar documentos ou imagens',
|
||||||
|
add_a_new_document: 'Ajouter un nouveau document',
|
||||||
|
replace_existing_document: 'Substituir documento existente',
|
||||||
|
clone_and_replace_documents: 'Clonar e substituir documentos',
|
||||||
required: 'Obrigatório',
|
required: 'Obrigatório',
|
||||||
default_value: 'Valor padrão',
|
default_value: 'Valor padrão',
|
||||||
format: 'Formato',
|
format: 'Formato',
|
||||||
@@ -659,6 +695,12 @@ const pt = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
|
signature_id: 'ID de signature',
|
||||||
|
error_message: 'Message d\'erreur',
|
||||||
|
length: 'Longueur',
|
||||||
|
min: 'Min',
|
||||||
|
max: 'Max',
|
||||||
|
date_signed: 'Date actuelle',
|
||||||
font: 'Police',
|
font: 'Police',
|
||||||
party: 'Partie',
|
party: 'Partie',
|
||||||
method: 'Méthode',
|
method: 'Méthode',
|
||||||
@@ -719,6 +761,9 @@ const fr = {
|
|||||||
processing_: 'Traitement en cours...',
|
processing_: 'Traitement en cours...',
|
||||||
add_pdf_documents_or_images: 'Ajoutez des documents PDF ou des images',
|
add_pdf_documents_or_images: 'Ajoutez des documents PDF ou des images',
|
||||||
add_documents_or_images: 'Ajoutez des documents ou des images',
|
add_documents_or_images: 'Ajoutez des documents ou des images',
|
||||||
|
add_a_new_document: 'Adicionar um novo documento',
|
||||||
|
replace_existing_document: 'Remplacer le document existant',
|
||||||
|
clone_and_replace_documents: 'Cloner et remplacer les documents',
|
||||||
required: 'Requis',
|
required: 'Requis',
|
||||||
default_value: 'Valeur par défaut',
|
default_value: 'Valeur par défaut',
|
||||||
format: 'Format',
|
format: 'Format',
|
||||||
@@ -824,6 +869,12 @@ const fr = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
|
signature_id: 'Signatur-ID',
|
||||||
|
error_message: 'Fehlermeldung',
|
||||||
|
length: 'Länge',
|
||||||
|
min: 'Min',
|
||||||
|
max: 'Max',
|
||||||
|
date_now: 'Akt. Datum',
|
||||||
font: 'Schriftart',
|
font: 'Schriftart',
|
||||||
party: 'Partei',
|
party: 'Partei',
|
||||||
method: 'Verfahren',
|
method: 'Verfahren',
|
||||||
@@ -884,6 +935,9 @@ const de = {
|
|||||||
processing_: 'Verarbeitung...',
|
processing_: 'Verarbeitung...',
|
||||||
add_pdf_documents_or_images: 'PDF-Dokumente oder Bilder hinzufügen',
|
add_pdf_documents_or_images: 'PDF-Dokumente oder Bilder hinzufügen',
|
||||||
add_documents_or_images: 'Dokumente oder Bilder hinzufügen',
|
add_documents_or_images: 'Dokumente oder Bilder hinzufügen',
|
||||||
|
add_a_new_document: 'Neues Dokument hinzufügen',
|
||||||
|
replace_existing_document: 'Vorhandenes Dokument ersetzen',
|
||||||
|
clone_and_replace_documents: 'Dokumente klonen und ersetzen',
|
||||||
required: 'Erforderlich',
|
required: 'Erforderlich',
|
||||||
default_value: 'Standardwert',
|
default_value: 'Standardwert',
|
||||||
format: 'Format',
|
format: 'Format',
|
||||||
|
|||||||
@@ -406,7 +406,7 @@ export default {
|
|||||||
this.mappings.every((m) => m.column_index !== index)
|
this.mappings.every((m) => m.column_index !== index)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (columnIndex !== -1) {
|
if (columnIndex !== -1 && this.rows.some((row) => row[columnIndex])) {
|
||||||
this.mappings.push({ uuid: v4(), field_name: field.name, column_index: columnIndex, submitter_uuid: submitter.uuid })
|
this.mappings.push({ uuid: v4(), field_name: field.name, column_index: columnIndex, submitter_uuid: submitter.uuid })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Loaded 100 of 367 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user