mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 14:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cabfe45608 | ||
|
|
4ade6a5911 | ||
|
|
10d45c3e20 | ||
|
|
6fb247ec06 | ||
|
|
1546258529 | ||
|
|
7ec83b8af6 | ||
|
|
21d0a87c17 | ||
|
|
528f59c1bf | ||
|
|
d1535c4cd8 | ||
|
|
da44d51a3b | ||
|
|
73ce10b26a | ||
|
|
13874b8830 | ||
|
|
ac66809a05 | ||
|
|
f5229d5dec | ||
|
|
222291d11d | ||
|
|
28f47e1093 | ||
|
|
7784346a97 | ||
|
|
ed46af8418 | ||
|
|
095f25a6e9 | ||
|
|
3cdab30dc8 | ||
|
|
8e462047ba | ||
|
|
b7f8b65d9d | ||
|
|
da21c37254 | ||
|
|
a89aef4a89 | ||
|
|
dd6516fd11 | ||
|
|
344a3ce198 | ||
|
|
62f8283587 | ||
|
|
1f89accac3 | ||
|
|
9fcaef4cf7 | ||
|
|
d5738a0631 | ||
|
|
8bf7a1f95a | ||
|
|
04cf36891e | ||
|
|
b2d9948c30 | ||
|
|
a2c9ac1707 | ||
|
|
46a6bd0108 | ||
|
|
5f069e7a40 | ||
|
|
ff57e5c6ae | ||
|
|
f65b6e2d76 | ||
|
|
58fd180ae0 | ||
|
|
cd6503c4c3 | ||
|
|
e8b36c2b6d | ||
|
|
51743f1359 | ||
|
|
504c42646b | ||
|
|
c61e84d1b4 | ||
|
|
b8ab01c46c | ||
|
|
9558060bde | ||
|
|
0741c879f1 | ||
|
|
2e6dd2867e | ||
|
|
9f3fcccb61 | ||
|
|
ae08d7abad | ||
|
|
16808a7ec6 | ||
|
|
b1dfcd6283 | ||
|
|
369f4c0f08 | ||
|
|
827de03579 | ||
|
|
0c7f4e5b43 | ||
|
|
9c700a3fb6 | ||
|
|
89bf83febb | ||
|
|
d44b35cf0a | ||
|
|
891833e273 | ||
|
|
29d3a80d3f | ||
|
|
90fef58684 | ||
|
|
07841722ac | ||
|
|
9e4da5948b | ||
|
|
dc6e4313a1 | ||
|
|
a68cc0b689 | ||
|
|
d057fb0f67 | ||
|
|
7e8f045a29 | ||
|
|
5249d8d18d | ||
|
|
f0fafbadd4 | ||
|
|
8b51079dcc | ||
|
|
354bccd6e8 | ||
|
|
6806772346 | ||
|
|
60082655d4 | ||
|
|
7fe56941fd | ||
|
|
abd498dd33 | ||
|
|
99ca0136ed | ||
|
|
e52830c9b4 | ||
|
|
755decca27 | ||
|
|
41604008d1 | ||
|
|
5bddce8969 | ||
|
|
b25503f141 | ||
|
|
bdf1850448 | ||
|
|
230d3ccf69 | ||
|
|
e378025a2d | ||
|
|
04129ded90 | ||
|
|
a7891f89f8 | ||
|
|
37d4a8e834 | ||
|
|
528a1216f8 | ||
|
|
f2479bd259 | ||
|
|
135f0826d2 | ||
|
|
5710f0177b | ||
|
|
1a3a0528ba | ||
|
|
59793ff374 | ||
|
|
3c4ed42419 | ||
|
|
6a17b61550 | ||
|
|
76659497b7 | ||
|
|
ee50c957cb | ||
|
|
a9dd200919 | ||
|
|
1fa953104a | ||
|
|
50eb5b070e | ||
|
|
10fd624bec | ||
|
|
04ec2f8260 | ||
|
|
d828d79574 | ||
|
|
01dd3fefe5 | ||
|
|
45ae954c0c | ||
|
|
1304849b55 | ||
|
|
0875faa079 | ||
|
|
3aa15d6ea6 | ||
|
|
6557329e97 | ||
|
|
be27ce4161 | ||
|
|
a3391a970e | ||
|
|
744d45d2c5 | ||
|
|
310b16f54a | ||
|
|
6d3ad4a869 | ||
|
|
1ce2448820 | ||
|
|
07b4ada5be | ||
|
|
f70bf530ba | ||
|
|
a26aa0f411 | ||
|
|
584577c21f | ||
|
|
1503375eed | ||
|
|
2dec099f20 | ||
|
|
bf6eed4ea2 | ||
|
|
e0dc33cfb4 | ||
|
|
9c9dc27537 | ||
|
|
7305637b8c | ||
|
|
a6e22adf2e | ||
|
|
809c6b1aa8 | ||
|
|
1536ded2a6 | ||
|
|
76d54971ce | ||
|
|
6dec56198c | ||
|
|
d96d252df3 | ||
|
|
3fca76a716 | ||
|
|
a184fa11f1 | ||
|
|
e9c0b6a703 | ||
|
|
ec7df08496 | ||
|
|
6627d7eac5 | ||
|
|
1f8945d710 | ||
|
|
daaa289a5c | ||
|
|
81a6a48d70 | ||
|
|
b93c7cd261 | ||
|
|
570482e12d | ||
|
|
39407557f2 | ||
|
|
3b040d558d | ||
|
|
56ba3c2a52 | ||
|
|
c7afe33c72 | ||
|
|
82500c2d7d | ||
|
|
fb1c1f117b | ||
|
|
7c6a7513fd | ||
|
|
6d13119ee0 | ||
|
|
1e2c752937 | ||
|
|
e82faf8320 | ||
|
|
4adb1001b0 | ||
|
|
992a1b26c0 | ||
|
|
c07bc6687c | ||
|
|
eea44bda34 | ||
|
|
7cdf263da1 | ||
|
|
ee65a5693c | ||
|
|
0af6ccf35f | ||
|
|
888f1ec6df | ||
|
|
c95a8616ac | ||
|
|
5ea6289b7a | ||
|
|
d4a79ca5db | ||
|
|
70015ce1c4 | ||
|
|
6b85c28944 | ||
|
|
6c289cf273 | ||
|
|
a64bc3c618 | ||
|
|
46cf1e3067 | ||
|
|
565e1eb2bc | ||
|
|
e689687805 | ||
|
|
3c3b61fb47 | ||
|
|
1355d350c5 | ||
|
|
fda911e178 | ||
|
|
f995e1864c | ||
|
|
911e55ccc3 | ||
|
|
41fedfcc40 | ||
|
|
2f9fc95af3 | ||
|
|
11b0b16ca7 | ||
|
|
f8cb7ffdab | ||
|
|
8b59c0aaa0 | ||
|
|
bb2fb7a0c2 | ||
|
|
8f8b36617a | ||
|
|
ff53436fd4 | ||
|
|
cf09a4f733 | ||
|
|
2303c21cea | ||
|
|
89e797b95f | ||
|
|
20375c3a42 | ||
|
|
339ceda18d | ||
|
|
97ce32fd52 | ||
|
|
8c4cbd86f7 | ||
|
|
eaedaa96bb | ||
|
|
bd7ad5fce8 | ||
|
|
759dac1b70 | ||
|
|
38577c6235 | ||
|
|
b24fff0f55 | ||
|
|
9e0efeb092 | ||
|
|
41152f90fc | ||
|
|
4486f8040e | ||
|
|
a98caad968 | ||
|
|
ffb75238b4 | ||
|
|
cba3e74f3a | ||
|
|
8d3f0b5b98 | ||
|
|
a2598b30f7 | ||
|
|
8d11f5ddae | ||
|
|
6ffc88b4ed | ||
|
|
640b451727 | ||
|
|
db6e2eb7ca | ||
|
|
407816b576 | ||
|
|
486cc493e6 | ||
|
|
3eb0b8a89f | ||
|
|
c23eca0ade | ||
|
|
67de153016 | ||
|
|
ba2da69a67 | ||
|
|
111bef14e1 | ||
|
|
22d4c1bd10 | ||
|
|
e3dfb0ae83 | ||
|
|
8d7c3f0e26 | ||
|
|
b11775ebda | ||
|
|
608191183a | ||
|
|
2ba903e170 | ||
|
|
342b64d502 | ||
|
|
1a8f048aaa | ||
|
|
7309581b75 | ||
|
|
36d59c40a7 | ||
|
|
ed3c7c324b | ||
|
|
fd868360a3 | ||
|
|
8dc2ebfbe0 | ||
|
|
997711b34a | ||
|
|
ac01b7129d | ||
|
|
f4b7528c0d | ||
|
|
d7b9ae5df5 | ||
|
|
4db29126e9 | ||
|
|
4cfee2fe45 | ||
|
|
db5dbd6ac2 | ||
|
|
44db775b89 | ||
|
|
8e2e780a1a | ||
|
|
7aba2eb4da | ||
|
|
3e250bfb28 | ||
|
|
9c02ed66f5 | ||
|
|
f5ae8c60d3 | ||
|
|
e90ac391b2 | ||
|
|
0e5f949bcf | ||
|
|
b7c270bef8 | ||
|
|
5f16b506f5 | ||
|
|
176148fc71 | ||
|
|
861daaedf4 | ||
|
|
d8430f4e63 | ||
|
|
dd44864485 | ||
|
|
b3bb29f383 | ||
|
|
b4d137c0de | ||
|
|
62a969d8fe | ||
|
|
961f09e092 | ||
|
|
b2dfa83f18 | ||
|
|
37196ff89f | ||
|
|
b50d982497 | ||
|
|
4a484aca69 | ||
|
|
0bf37d571a | ||
|
|
d4e94ac2bc | ||
|
|
75bc789bb1 | ||
|
|
bc231d5bf1 | ||
|
|
ab9008b3be | ||
|
|
65491936c3 | ||
|
|
e589998840 | ||
|
|
0afba8d11e | ||
|
|
f622ed168f | ||
|
|
5a31df40d8 | ||
|
|
db3b80c96c | ||
|
|
bd28a761d0 | ||
|
|
4f2830a34d | ||
|
|
a5716cd518 | ||
|
|
d035525c7a | ||
|
|
13fa87c449 | ||
|
|
246ccfb049 | ||
|
|
cbffb1e5b8 | ||
|
|
c4b007411d | ||
|
|
04264fe937 | ||
|
|
0c37ad12b7 | ||
|
|
8c5298ed2e | ||
|
|
262118e047 | ||
|
|
e5b63ea2b0 | ||
|
|
746757d402 | ||
|
|
e6e640328b | ||
|
|
845782a69c | ||
|
|
fc6baa1b3b | ||
|
|
47822ecc15 | ||
|
|
3d0c7f1118 | ||
|
|
e9c8e4d325 | ||
|
|
39cc82ce0c | ||
|
|
451f83421a | ||
|
|
5073f2eefb | ||
|
|
231fff5508 | ||
|
|
f1d146eca3 | ||
|
|
b6635fcc4f | ||
|
|
fb5e13ee4c | ||
|
|
ba84741a64 | ||
|
|
65c275ac17 | ||
|
|
825322d489 | ||
|
|
739e2abdf8 | ||
|
|
1d2394e31e | ||
|
|
1b41af798d | ||
|
|
118f4a231b | ||
|
|
e48652f425 | ||
|
|
848f01edf8 | ||
|
|
43fbc42770 | ||
|
|
2c736a0eed | ||
|
|
33ca930055 | ||
|
|
3b396f8421 | ||
|
|
aa77e3a8d3 | ||
|
|
75316d8d87 | ||
|
|
a999109a5c | ||
|
|
bdd33c7d6b | ||
|
|
61c5ee22a0 | ||
|
|
12c5b909e0 | ||
|
|
871ef6dda6 | ||
|
|
40052a2d7c | ||
|
|
347be0137d | ||
|
|
fe6baba8bf | ||
|
|
ed8c313bd4 | ||
|
|
ca0acb34d6 | ||
|
|
34ea639c25 | ||
|
|
680ab9dbed | ||
|
|
9377766e52 | ||
|
|
7a2c37454e | ||
|
|
8f6f418f54 | ||
|
|
9b7745c565 | ||
|
|
a389e8cc38 | ||
|
|
2056301f70 | ||
|
|
93babe3660 | ||
|
|
4aef2d8eb0 | ||
|
|
c5ec2ed230 | ||
|
|
a156d5f6cc | ||
|
|
f84674cddd | ||
|
|
b7b62c197a | ||
|
|
f5350eac36 | ||
|
|
ee6228db15 | ||
|
|
3e434c0878 | ||
|
|
7c5372a8e2 | ||
|
|
6ab68cc36b | ||
|
|
ee2400a5c5 | ||
|
|
80be5d2cf5 | ||
|
|
76cf44a31b | ||
|
|
eb63113227 | ||
|
|
b7045fb3d9 | ||
|
|
11a5b5842d | ||
|
|
362994afd5 | ||
|
|
d7682f4ebd | ||
|
|
56d4fe7fa9 | ||
|
|
93a852ac14 | ||
|
|
964db0ab08 | ||
|
|
8547295da7 | ||
|
|
c75050fa46 | ||
|
|
65372c52aa | ||
|
|
12c767b7d3 | ||
|
|
2fc5e9cfb3 | ||
|
|
bccc78ee65 | ||
|
|
c0793f3ce7 | ||
|
|
a8dd8699a3 | ||
|
|
91bb25e8b6 | ||
|
|
c3f4480cdc | ||
|
|
33e226861e | ||
|
|
4f5acbec9d | ||
|
|
d2c9bbf6ab | ||
|
|
b5e57e1f2f | ||
|
|
c82c8b2272 | ||
|
|
43f1b598a7 | ||
|
|
5081e387d0 | ||
|
|
1e5f55c1d6 | ||
|
|
aa38773be3 | ||
|
|
96530bd809 | ||
|
|
127f95a9d0 | ||
|
|
cf31e12348 | ||
|
|
4adaa531b7 | ||
|
|
c7659563b8 | ||
|
|
7b5494468f | ||
|
|
d12c67fd54 | ||
|
|
e20eacb5ee | ||
|
|
98055f0762 | ||
|
|
a46c42c3a0 | ||
|
|
c3d1503361 | ||
|
|
65b47d27cd | ||
|
|
968c07f8be | ||
|
|
91814ca105 | ||
|
|
9128f24270 | ||
|
|
264897bbd5 | ||
|
|
67d59ff1ba | ||
|
|
81eee56e1e | ||
|
|
92ea8af786 | ||
|
|
df0616f77e | ||
|
|
a337dff85c | ||
|
|
48e349c1d2 | ||
|
|
df9fa0fec7 | ||
|
|
b3e72f0726 | ||
|
|
700ebc4727 | ||
|
|
745a18efca | ||
|
|
b4a8147ff0 | ||
|
|
0543c25054 | ||
|
|
b1a0afea66 | ||
|
|
43c41e2557 | ||
|
|
bc8eb33f05 | ||
|
|
d1cebc92f7 | ||
|
|
bf02cf4058 | ||
|
|
5212773d0a | ||
|
|
bf53204a41 | ||
|
|
092a19966c | ||
|
|
6060a2b798 | ||
|
|
ce12af68b2 | ||
|
|
c05f8d47aa | ||
|
|
f8be19c20e | ||
|
|
2fe32a2145 | ||
|
|
794ea4d0e8 | ||
|
|
307b0f5afd | ||
|
|
9011e3f4b4 | ||
|
|
0b45ce5c0c | ||
|
|
a8ab93a4cc | ||
|
|
d1768be636 | ||
|
|
b2cd7c82a7 | ||
|
|
b88d99afe0 | ||
|
|
4523d38551 | ||
|
|
c4ba892559 | ||
|
|
40f8093e00 | ||
|
|
4375459856 | ||
|
|
b3eb8a70bf | ||
|
|
6245a487b6 | ||
|
|
a89f4c1b10 | ||
|
|
7df729b8b5 | ||
|
|
f1db7c7e82 | ||
|
|
00af69cbf2 | ||
|
|
c6819a19bd | ||
|
|
f5cbcb3d24 | ||
|
|
6ca75c53ef | ||
|
|
e0ec443e10 | ||
|
|
9eed2fc2ef | ||
|
|
b9c6d3ff9f | ||
|
|
03c25a89b2 | ||
|
|
f5b4a0c5ab | ||
|
|
b10ed46ccc | ||
|
|
c55036a1b1 | ||
|
|
962a5dade5 | ||
|
|
369a09a6df | ||
|
|
e9a39e2571 | ||
|
|
1cf9cc18f1 | ||
|
|
1984d4ec8d | ||
|
|
fa99b4ebdc | ||
|
|
83c793a2a5 | ||
|
|
d078727070 | ||
|
|
0d8e2c5ff0 | ||
|
|
ea062d7494 | ||
|
|
d525e0597a | ||
|
|
964b5d4e74 | ||
|
|
abf17eb09f | ||
|
|
178809dad7 | ||
|
|
881d189715 | ||
|
|
48a8973910 | ||
|
|
ae891932c5 | ||
|
|
a8b5e00814 | ||
|
|
e98432d7e4 | ||
|
|
29ff58ed98 | ||
|
|
a621575ffc | ||
|
|
d7c7efc203 | ||
|
|
4f013ca927 | ||
|
|
d9823feef7 | ||
|
|
676d775544 | ||
|
|
840ad17258 | ||
|
|
ce0cd09b70 | ||
|
|
bb752fbc00 | ||
|
|
c71fb04404 | ||
|
|
e856cf8787 | ||
|
|
71aae65bc6 | ||
|
|
7a33444f9b | ||
|
|
e12ec9d986 | ||
|
|
94d1fb7dcb | ||
|
|
6e43a42274 | ||
|
|
0e0aa0259e | ||
|
|
75ab6d39ef | ||
|
|
6f84d84f96 | ||
|
|
cf46593e9b | ||
|
|
1808346348 | ||
|
|
ea59a68499 | ||
|
|
cba2d02990 | ||
|
|
e92fc0d3e1 | ||
|
|
f1eaa77092 | ||
|
|
5934bdec55 | ||
|
|
7a7d3c6ee3 | ||
|
|
cb2f58c04c | ||
|
|
a1c117f1ca |
No files matched your search
@@ -12,7 +12,7 @@ jobs:
|
|||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.4.2
|
ruby-version: 4.0.5
|
||||||
- name: Cache gems
|
- name: Cache gems
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.4.2
|
ruby-version: 4.0.5
|
||||||
- name: Cache gems
|
- name: Cache gems
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 20.9.0
|
node-version: 20.19.0
|
||||||
- name: Cache directory path
|
- name: Cache directory path
|
||||||
id: yarn-cache-dir-path
|
id: yarn-cache-dir-path
|
||||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.4.2
|
ruby-version: 4.0.5
|
||||||
- name: Cache gems
|
- name: Cache gems
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@@ -104,7 +104,7 @@ 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 libvips liblept5
|
||||||
- name: Run Brakeman
|
- name: Run Brakeman
|
||||||
run: bundle exec brakeman -q --exit-on-warn
|
run: bundle exec brakeman -q --exit-on-warn
|
||||||
|
|
||||||
@@ -132,11 +132,11 @@ jobs:
|
|||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.4.2
|
ruby-version: 4.0.5
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 20.9.0
|
node-version: 20.19.0
|
||||||
- name: Install Chrome
|
- name: Install Chrome
|
||||||
uses: browser-actions/setup-chrome@latest
|
uses: browser-actions/setup-chrome@latest
|
||||||
with:
|
with:
|
||||||
@@ -162,8 +162,8 @@ 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 -y libvips
|
sudo apt-get install -y libvips liblept5
|
||||||
wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz"
|
wget -O pdfium-linux.tgz "https://github.com/bblanchon/pdfium-binaries/releases/latest/download/pdfium-linux-$(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
|
sudo tar -xzf pdfium-linux.tgz --strip-components=1 -C /usr/lib lib/libpdfium.so
|
||||||
rm -f pdfium-linux.tgz
|
rm -f pdfium-linux.tgz
|
||||||
- name: Run
|
- name: Run
|
||||||
|
|||||||
@@ -1,27 +1,14 @@
|
|||||||
name: Build Docker Images
|
name: Build Docker Images
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
push:
|
||||||
inputs:
|
tags:
|
||||||
version:
|
- "*.*.*"
|
||||||
description: Version
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
image:
|
|
||||||
description: QEMU image
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
default: tonistiigi/binfmt:latest
|
|
||||||
os:
|
|
||||||
description: OS
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
default: ubuntu-24.04-arm
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ${{ inputs.os }}
|
runs-on: ubuntu-24.04-arm
|
||||||
timeout-minutes: 20
|
timeout-minutes: 30
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -34,18 +21,16 @@ jobs:
|
|||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: docuseal/docuseal
|
images: docuseal/docuseal
|
||||||
tags: latest,${{ inputs.version }}
|
tags: type=semver,pattern={{version}}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
with:
|
|
||||||
image: ${{ inputs.image }}
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Create .version file
|
- name: Create .version file
|
||||||
run: echo ${{ inputs.version }} > .version
|
run: echo ${{ github.ref_name }} > .version
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
|
|||||||
+29
-4
@@ -1,4 +1,4 @@
|
|||||||
require:
|
plugins:
|
||||||
- rubocop-performance
|
- rubocop-performance
|
||||||
- rubocop-rails
|
- rubocop-rails
|
||||||
- rubocop-rspec
|
- rubocop-rspec
|
||||||
@@ -10,7 +10,7 @@ AllCops:
|
|||||||
- node_modules/**/*
|
- node_modules/**/*
|
||||||
- bin/*
|
- bin/*
|
||||||
- vendor/**/*
|
- vendor/**/*
|
||||||
TargetRubyVersion: '3.3'
|
TargetRubyVersion: '4.0'
|
||||||
SuggestExtensions: false
|
SuggestExtensions: false
|
||||||
|
|
||||||
Metrics/BlockLength:
|
Metrics/BlockLength:
|
||||||
@@ -28,7 +28,7 @@ Lint/MissingSuper:
|
|||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Metrics/ParameterLists:
|
Metrics/ParameterLists:
|
||||||
Max: 10
|
Max: 12
|
||||||
|
|
||||||
Metrics/MethodLength:
|
Metrics/MethodLength:
|
||||||
Max: 30
|
Max: 30
|
||||||
@@ -48,6 +48,18 @@ Style/MultipleComparison:
|
|||||||
Style/NumericPredicate:
|
Style/NumericPredicate:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Style/MinMaxComparison:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/EmptyClassDefinition:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/OneClassPerFile:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Layout/MultilineMethodCallIndentation:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
Naming/PredicateMethod:
|
Naming/PredicateMethod:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
@@ -81,7 +93,7 @@ RSpec/AnyInstance:
|
|||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Metrics/BlockNesting:
|
Metrics/BlockNesting:
|
||||||
Max: 5
|
Max: 6
|
||||||
|
|
||||||
Rails/I18nLocaleTexts:
|
Rails/I18nLocaleTexts:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
@@ -97,3 +109,16 @@ Rails/ApplicationController:
|
|||||||
|
|
||||||
Rails/Output:
|
Rails/Output:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/StrongParametersExpect:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/RedirectBackOrTo:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/UnknownEnv:
|
||||||
|
Environments:
|
||||||
|
- development
|
||||||
|
- test
|
||||||
|
- production
|
||||||
|
- local
|
||||||
+43
-37
@@ -1,22 +1,23 @@
|
|||||||
FROM ruby:3.4.2-alpine AS download
|
FROM ruby:4.0.5-alpine AS download
|
||||||
|
|
||||||
WORKDIR /fonts
|
WORKDIR /fonts
|
||||||
|
|
||||||
RUN apk --no-cache add fontforge wget && \
|
RUN apk --no-cache add wget unzip && \
|
||||||
wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && \
|
wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && \
|
||||||
wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && \
|
wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && \
|
||||||
wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && \
|
wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && \
|
||||||
wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && \
|
wget https://raw.githubusercontent.com/impallari/DancingScript/master/OFL.txt && \
|
||||||
wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && \
|
wget https://raw.githubusercontent.com/notofonts/noto-fonts/refs/heads/main/LICENSE && \
|
||||||
wget https://github.com/impallari/DancingScript/raw/master/OFL.txt && \
|
wget -O /model.onnx "https://github.com/docusealco/fields-detection/releases/download/2.0.0/model_704_int8.onnx" && \
|
||||||
wget -O /model.onnx "https://github.com/docusealco/fields-detection/releases/download/1.0.0/model_704_int8.onnx" && \
|
wget -O pdfium-linux.zip "https://github.com/docusealco/pdfium-binaries/releases/download/20260613/pdfium-musl-$(uname -m).zip" && \
|
||||||
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" && \
|
case "$(uname -m)" in \
|
||||||
|
x86_64) echo "2c953ff72ee2dda07e7fc577e25841cc3d6464468a7c5adfaea574efcbc3b90b pdfium-linux.zip" ;; \
|
||||||
|
aarch64) echo "23bbe287d2753fdb05741c7660647eb0ef0d2e4da2ce0722bfa9d9d455bd64e2 pdfium-linux.zip" ;; \
|
||||||
|
esac | sha256sum -c - && \
|
||||||
mkdir -p /pdfium-linux && \
|
mkdir -p /pdfium-linux && \
|
||||||
tar -xzf pdfium-linux.tgz -C /pdfium-linux
|
unzip -q pdfium-linux.zip -d /pdfium-linux
|
||||||
|
|
||||||
RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")'
|
FROM ruby:4.0.5-alpine AS webpack
|
||||||
|
|
||||||
FROM ruby:3.4.2-alpine AS webpack
|
|
||||||
|
|
||||||
ENV RAILS_ENV=production
|
ENV RAILS_ENV=production
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
@@ -36,22 +37,25 @@ COPY ./config/shakapacker.yml ./config/shakapacker.yml
|
|||||||
COPY ./postcss.config.js ./postcss.config.js
|
COPY ./postcss.config.js ./postcss.config.js
|
||||||
COPY ./tailwind.config.js ./tailwind.config.js
|
COPY ./tailwind.config.js ./tailwind.config.js
|
||||||
COPY ./tailwind.form.config.js ./tailwind.form.config.js
|
COPY ./tailwind.form.config.js ./tailwind.form.config.js
|
||||||
|
COPY ./tailwind.dynamic.config.js ./tailwind.dynamic.config.js
|
||||||
COPY ./tailwind.application.config.js ./tailwind.application.config.js
|
COPY ./tailwind.application.config.js ./tailwind.application.config.js
|
||||||
COPY ./app/javascript ./app/javascript
|
COPY ./app/javascript ./app/javascript
|
||||||
COPY ./app/views ./app/views
|
COPY ./app/views ./app/views
|
||||||
|
|
||||||
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
|
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
|
||||||
|
|
||||||
FROM ruby:3.4.2-alpine AS app
|
FROM ruby:4.0.5-alpine AS app
|
||||||
|
|
||||||
ENV RAILS_ENV=production
|
ENV RAILS_ENV=production
|
||||||
ENV BUNDLE_WITHOUT="development:test"
|
ENV BUNDLE_WITHOUT="development:test"
|
||||||
ENV LD_PRELOAD=/lib/libgcompat.so.0
|
ENV OPENSSL_CONF=/etc/openssl_legacy.cnf
|
||||||
ENV OPENSSL_CONF=/app/openssl_legacy.cnf
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev yaml-dev redis libheif vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
RUN apk add --no-cache libpq vips redis onnxruntime leptonica && \
|
||||||
|
rm -f /usr/bin/onnx_test_runner /usr/bin/onnxruntime_test
|
||||||
|
|
||||||
|
RUN addgroup -g 2000 docuseal && adduser -u 2000 -G docuseal -s /bin/sh -D -h /home/docuseal docuseal
|
||||||
|
|
||||||
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
|
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
|
||||||
\n\
|
\n\
|
||||||
@@ -63,37 +67,39 @@ legacy = legacy_sect\n\
|
|||||||
activate = 1\n\
|
activate = 1\n\
|
||||||
\n\
|
\n\
|
||||||
[legacy_sect]\n\
|
[legacy_sect]\n\
|
||||||
activate = 1' >> /app/openssl_legacy.cnf
|
activate = 1' >> /etc/openssl_legacy.cnf
|
||||||
|
|
||||||
COPY ./Gemfile ./Gemfile.lock ./
|
COPY --chown=docuseal:docuseal ./Gemfile ./Gemfile.lock ./
|
||||||
|
|
||||||
RUN apk add --no-cache build-base && bundle install && apk del --no-cache build-base && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf && ln -sf /usr/lib/libonnxruntime.so.1 $(ruby -e "print Dir[Gem::Specification.find_by_name('onnxruntime').gem_dir + '/vendor/*.so'].first")
|
RUN apk add --no-cache build-base git libpq-dev yaml-dev && bundle install && apk del --no-cache build-base git libpq-dev yaml-dev && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales,resources/unicode_data/properties}'] + Dir['/usr/local/bundle/gems/*/{test,tests,examples,sample,misc,doc,docs}'] + Dir['/usr/local/bundle/gems/*/ext/**/*.{c,h,o,S}']" | xargs rm -rf && ln -sf /usr/lib/libonnxruntime.so.1 $(ruby -e "print Dir[Gem::Specification.find_by_name('onnxruntime').gem_dir + '/vendor/*.so'].first")
|
||||||
|
|
||||||
RUN echo 'https://dl-cdn.alpinelinux.org/alpine/edge/main' >> /etc/apk/repositories && echo 'https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache onnxruntime
|
COPY --chown=docuseal:docuseal ./bin ./bin
|
||||||
|
COPY --chown=docuseal:docuseal ./app ./app
|
||||||
|
COPY --chown=docuseal:docuseal ./config ./config
|
||||||
|
COPY --chown=docuseal:docuseal ./db/migrate ./db/migrate
|
||||||
|
COPY --chown=docuseal:docuseal ./log ./log
|
||||||
|
COPY --chown=docuseal:docuseal ./lib ./lib
|
||||||
|
COPY --chown=docuseal:docuseal ./public ./public
|
||||||
|
COPY --chown=docuseal:docuseal ./tmp ./tmp
|
||||||
|
COPY --chown=docuseal:docuseal LICENSE LICENSE_ADDITIONAL_TERMS README.md Rakefile config.ru .version ./
|
||||||
|
COPY --chown=docuseal:docuseal .version ./public/version
|
||||||
|
|
||||||
COPY ./bin ./bin
|
COPY --chown=docuseal:docuseal --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts/LICENSE /fonts/
|
||||||
COPY ./app ./app
|
|
||||||
COPY ./config ./config
|
|
||||||
COPY ./db/migrate ./db/migrate
|
|
||||||
COPY ./log ./log
|
|
||||||
COPY ./lib ./lib
|
|
||||||
COPY ./public ./public
|
|
||||||
COPY ./tmp ./tmp
|
|
||||||
COPY LICENSE README.md Rakefile config.ru .version ./
|
|
||||||
COPY .version ./public/version
|
|
||||||
|
|
||||||
COPY --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
|
||||||
COPY --from=download /fonts/FreeSans.ttf /usr/share/fonts/freefont
|
|
||||||
COPY --from=download /pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so
|
COPY --from=download /pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so
|
||||||
COPY --from=download /pdfium-linux/licenses/pdfium.txt /usr/lib/libpdfium-LICENSE.txt
|
COPY --from=download /pdfium-linux/licenses/ /usr/lib/libpdfium-licenses/
|
||||||
COPY --from=download /model.onnx /app/tmp/model.onnx
|
COPY --chown=docuseal:docuseal --from=download /model.onnx /app/tmp/model.onnx
|
||||||
COPY --from=webpack /app/public/packs ./public/packs
|
COPY --chown=docuseal:docuseal --from=webpack /app/public/packs ./public/packs
|
||||||
|
|
||||||
RUN ln -s /fonts /app/public/fonts
|
RUN mkdir -p /app/public/fonts && ln -s /fonts/DancingScript-Regular.otf /app/public/fonts/ && \
|
||||||
RUN bundle exec bootsnap precompile --gemfile app/ lib/
|
mkdir -p /usr/share/fonts/noto && ln -s /fonts/GoNotoKurrent-Regular.ttf /usr/share/fonts/noto/ && ln -s /fonts/GoNotoKurrent-Bold.ttf /usr/share/fonts/noto/ && fc-cache -f && \
|
||||||
|
bundle exec bootsnap precompile -j 1 --gemfile app/ lib/ && \
|
||||||
|
chown -R docuseal:docuseal /app/tmp/cache
|
||||||
|
|
||||||
WORKDIR /data/docuseal
|
WORKDIR /data/docuseal
|
||||||
|
ENV HOME=/home/docuseal
|
||||||
ENV WORKDIR=/data/docuseal
|
ENV WORKDIR=/data/docuseal
|
||||||
|
ENV VIPS_MAX_COORD=17000
|
||||||
|
ENV VIPS_BLOCK_UNTRUSTED=1
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["/app/bin/bundle", "exec", "puma", "-C", "/app/config/puma.rb", "--dir", "/app"]
|
CMD ["/app/bin/bundle", "exec", "puma", "-C", "/app/config/puma.rb", "--dir", "/app"]
|
||||||
@@ -2,16 +2,17 @@
|
|||||||
|
|
||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
ruby '3.4.2'
|
ruby '4.0.5'
|
||||||
|
|
||||||
gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic'
|
gem 'addressable'
|
||||||
|
gem 'arabic-letter-connector', require: false
|
||||||
gem 'aws-sdk-s3', require: false
|
gem 'aws-sdk-s3', require: false
|
||||||
gem 'aws-sdk-secretsmanager', require: false
|
gem 'aws-sdk-secretsmanager', require: false
|
||||||
gem 'azure-storage-blob', require: false
|
gem 'azure-blob', require: false
|
||||||
gem 'bootsnap', require: false
|
gem 'bootsnap', require: false
|
||||||
gem 'cancancan'
|
gem 'cancancan'
|
||||||
gem 'csv'
|
gem 'csv', require: false
|
||||||
gem 'csv-safe'
|
gem 'csv-safe', require: false
|
||||||
gem 'devise'
|
gem 'devise'
|
||||||
gem 'devise-two-factor'
|
gem 'devise-two-factor'
|
||||||
gem 'dotenv', require: false
|
gem 'dotenv', require: false
|
||||||
@@ -20,31 +21,27 @@ gem 'faraday'
|
|||||||
gem 'faraday-follow_redirects'
|
gem 'faraday-follow_redirects'
|
||||||
gem 'google-cloud-storage', require: false
|
gem 'google-cloud-storage', require: false
|
||||||
gem 'hexapdf'
|
gem 'hexapdf'
|
||||||
gem 'image_processing'
|
gem 'jwt', require: false
|
||||||
gem 'jwt'
|
|
||||||
gem 'lograge'
|
gem 'lograge'
|
||||||
gem 'mysql2', require: false
|
gem 'numo-narray-alt', require: false
|
||||||
gem 'numo-narray'
|
gem 'onnxruntime', require: false
|
||||||
gem 'oj'
|
|
||||||
gem 'onnxruntime'
|
|
||||||
gem 'pagy'
|
gem 'pagy'
|
||||||
gem 'pg', require: false
|
gem 'pg', require: false
|
||||||
gem 'premailer-rails'
|
|
||||||
gem 'pretender'
|
gem 'pretender'
|
||||||
gem 'puma', require: false
|
gem 'puma', require: false
|
||||||
gem 'rack'
|
gem 'rack'
|
||||||
gem 'rails'
|
gem 'rails'
|
||||||
gem 'rails_autolink'
|
|
||||||
gem 'rails-i18n'
|
gem 'rails-i18n'
|
||||||
gem 'rotp'
|
gem 'rotp'
|
||||||
gem 'rouge', require: false
|
gem 'rouge', require: false
|
||||||
gem 'rqrcode'
|
gem 'rqrcode', require: false
|
||||||
gem 'ruby-vips'
|
gem 'ruby-vips'
|
||||||
gem 'rubyXL'
|
gem 'rubyXL', require: false
|
||||||
gem 'shakapacker'
|
gem 'shakapacker'
|
||||||
gem 'sidekiq'
|
gem 'sidekiq'
|
||||||
gem 'sqlite3', require: false
|
gem 'sqlite3', require: false
|
||||||
gem 'strip_attributes'
|
gem 'strip_attributes'
|
||||||
|
gem 'trilogy', require: false
|
||||||
gem 'turbo-rails'
|
gem 'turbo-rails'
|
||||||
gem 'twitter_cldr', require: false
|
gem 'twitter_cldr', require: false
|
||||||
gem 'tzinfo-data'
|
gem 'tzinfo-data'
|
||||||
|
|||||||
+296
-327
@@ -1,29 +1,31 @@
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (8.0.2.1)
|
action_text-trix (2.1.18)
|
||||||
actionpack (= 8.0.2.1)
|
railties
|
||||||
activesupport (= 8.0.2.1)
|
actioncable (8.1.3)
|
||||||
|
actionpack (= 8.1.3)
|
||||||
|
activesupport (= 8.1.3)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
actionmailbox (8.0.2.1)
|
actionmailbox (8.1.3)
|
||||||
actionpack (= 8.0.2.1)
|
actionpack (= 8.1.3)
|
||||||
activejob (= 8.0.2.1)
|
activejob (= 8.1.3)
|
||||||
activerecord (= 8.0.2.1)
|
activerecord (= 8.1.3)
|
||||||
activestorage (= 8.0.2.1)
|
activestorage (= 8.1.3)
|
||||||
activesupport (= 8.0.2.1)
|
activesupport (= 8.1.3)
|
||||||
mail (>= 2.8.0)
|
mail (>= 2.8.0)
|
||||||
actionmailer (8.0.2.1)
|
actionmailer (8.1.3)
|
||||||
actionpack (= 8.0.2.1)
|
actionpack (= 8.1.3)
|
||||||
actionview (= 8.0.2.1)
|
actionview (= 8.1.3)
|
||||||
activejob (= 8.0.2.1)
|
activejob (= 8.1.3)
|
||||||
activesupport (= 8.0.2.1)
|
activesupport (= 8.1.3)
|
||||||
mail (>= 2.8.0)
|
mail (>= 2.8.0)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
actionpack (8.0.2.1)
|
actionpack (8.1.3)
|
||||||
actionview (= 8.0.2.1)
|
actionview (= 8.1.3)
|
||||||
activesupport (= 8.0.2.1)
|
activesupport (= 8.1.3)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
rack (>= 2.2.4)
|
rack (>= 2.2.4)
|
||||||
rack-session (>= 1.0.1)
|
rack-session (>= 1.0.1)
|
||||||
@@ -31,97 +33,97 @@ GEM
|
|||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.6)
|
rails-html-sanitizer (~> 1.6)
|
||||||
useragent (~> 0.16)
|
useragent (~> 0.16)
|
||||||
actiontext (8.0.2.1)
|
actiontext (8.1.3)
|
||||||
actionpack (= 8.0.2.1)
|
action_text-trix (~> 2.1.15)
|
||||||
activerecord (= 8.0.2.1)
|
actionpack (= 8.1.3)
|
||||||
activestorage (= 8.0.2.1)
|
activerecord (= 8.1.3)
|
||||||
activesupport (= 8.0.2.1)
|
activestorage (= 8.1.3)
|
||||||
|
activesupport (= 8.1.3)
|
||||||
globalid (>= 0.6.0)
|
globalid (>= 0.6.0)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (8.0.2.1)
|
actionview (8.1.3)
|
||||||
activesupport (= 8.0.2.1)
|
activesupport (= 8.1.3)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.11)
|
erubi (~> 1.11)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.6)
|
rails-html-sanitizer (~> 1.6)
|
||||||
activejob (8.0.2.1)
|
activejob (8.1.3)
|
||||||
activesupport (= 8.0.2.1)
|
activesupport (= 8.1.3)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (8.0.2.1)
|
activemodel (8.1.3)
|
||||||
activesupport (= 8.0.2.1)
|
activesupport (= 8.1.3)
|
||||||
activerecord (8.0.2.1)
|
activerecord (8.1.3)
|
||||||
activemodel (= 8.0.2.1)
|
activemodel (= 8.1.3)
|
||||||
activesupport (= 8.0.2.1)
|
activesupport (= 8.1.3)
|
||||||
timeout (>= 0.4.0)
|
timeout (>= 0.4.0)
|
||||||
activestorage (8.0.2.1)
|
activestorage (8.1.3)
|
||||||
actionpack (= 8.0.2.1)
|
actionpack (= 8.1.3)
|
||||||
activejob (= 8.0.2.1)
|
activejob (= 8.1.3)
|
||||||
activerecord (= 8.0.2.1)
|
activerecord (= 8.1.3)
|
||||||
activesupport (= 8.0.2.1)
|
activesupport (= 8.1.3)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
activesupport (8.0.2.1)
|
activesupport (8.1.3)
|
||||||
base64
|
base64
|
||||||
benchmark (>= 0.3)
|
|
||||||
bigdecimal
|
bigdecimal
|
||||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||||
connection_pool (>= 2.2.5)
|
connection_pool (>= 2.2.5)
|
||||||
drb
|
drb
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
|
json
|
||||||
logger (>= 1.4.2)
|
logger (>= 1.4.2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
securerandom (>= 0.3)
|
securerandom (>= 0.3)
|
||||||
tzinfo (~> 2.0, >= 2.0.5)
|
tzinfo (~> 2.0, >= 2.0.5)
|
||||||
uri (>= 0.13.1)
|
uri (>= 0.13.1)
|
||||||
addressable (2.8.7)
|
addressable (2.9.0)
|
||||||
public_suffix (>= 2.0.2, < 7.0)
|
public_suffix (>= 2.0.2, < 8.0)
|
||||||
annotaterb (4.14.0)
|
annotaterb (4.22.0)
|
||||||
|
activerecord (>= 6.0.0)
|
||||||
|
activesupport (>= 6.0.0)
|
||||||
arabic-letter-connector (0.1.1)
|
arabic-letter-connector (0.1.1)
|
||||||
ast (2.4.3)
|
ast (2.4.3)
|
||||||
aws-eventstream (1.3.0)
|
aws-eventstream (1.4.0)
|
||||||
aws-partitions (1.1027.0)
|
aws-partitions (1.1233.0)
|
||||||
aws-sdk-core (3.214.0)
|
aws-sdk-core (3.244.0)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.992.0)
|
aws-partitions (~> 1, >= 1.992.0)
|
||||||
aws-sigv4 (~> 1.9)
|
aws-sigv4 (~> 1.9)
|
||||||
|
base64
|
||||||
|
bigdecimal
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
aws-sdk-kms (1.96.0)
|
logger
|
||||||
aws-sdk-core (~> 3, >= 3.210.0)
|
aws-sdk-kms (1.123.0)
|
||||||
|
aws-sdk-core (~> 3, >= 3.244.0)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sdk-s3 (1.176.1)
|
aws-sdk-s3 (1.218.0)
|
||||||
aws-sdk-core (~> 3, >= 3.210.0)
|
aws-sdk-core (~> 3, >= 3.244.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sdk-secretsmanager (1.110.0)
|
aws-sdk-secretsmanager (1.129.0)
|
||||||
aws-sdk-core (~> 3, >= 3.210.0)
|
aws-sdk-core (~> 3, >= 3.244.0)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sigv4 (1.10.1)
|
aws-sigv4 (1.12.1)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
azure-storage-blob (2.0.3)
|
azure-blob (0.8.0)
|
||||||
azure-storage-common (~> 2.0)
|
cgi
|
||||||
nokogiri (~> 1, >= 1.10.8)
|
rexml
|
||||||
azure-storage-common (2.0.4)
|
|
||||||
faraday (~> 1.0)
|
|
||||||
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
|
|
||||||
net-http-persistent (~> 4.0)
|
|
||||||
nokogiri (~> 1, >= 1.10.8)
|
|
||||||
base64 (0.3.0)
|
base64 (0.3.0)
|
||||||
bcrypt (3.1.20)
|
bcrypt (3.1.22)
|
||||||
benchmark (0.4.1)
|
better_html (2.2.0)
|
||||||
better_html (2.1.1)
|
actionview (>= 7.0)
|
||||||
actionview (>= 6.0)
|
activesupport (>= 7.0)
|
||||||
activesupport (>= 6.0)
|
|
||||||
ast (~> 2.0)
|
ast (~> 2.0)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
parser (>= 2.4)
|
parser (>= 2.4)
|
||||||
smart_properties
|
smart_properties
|
||||||
bigdecimal (3.2.2)
|
bigdecimal (4.1.2)
|
||||||
bindex (0.8.1)
|
bindex (0.8.1)
|
||||||
bootsnap (1.18.4)
|
bootsnap (1.23.0)
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
brakeman (7.0.0)
|
brakeman (8.0.4)
|
||||||
racc
|
racc
|
||||||
builder (3.3.0)
|
builder (3.3.0)
|
||||||
bullet (8.0.0)
|
bullet (8.1.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
uniform_notifier (~> 1.11)
|
uniform_notifier (~> 1.11)
|
||||||
camertron-eprun (1.1.1)
|
camertron-eprun (1.1.1)
|
||||||
@@ -135,51 +137,50 @@ GEM
|
|||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
regexp_parser (>= 1.5, < 3.0)
|
regexp_parser (>= 1.5, < 3.0)
|
||||||
xpath (~> 3.2)
|
xpath (~> 3.2)
|
||||||
|
cgi (0.5.1)
|
||||||
childprocess (5.1.0)
|
childprocess (5.1.0)
|
||||||
logger (~> 1.5)
|
logger (~> 1.5)
|
||||||
chunky_png (1.4.0)
|
chunky_png (1.4.0)
|
||||||
cldr-plurals-runtime-rb (1.1.0)
|
cldr-plurals-runtime-rb (1.1.0)
|
||||||
cmdparse (3.0.7)
|
cmdparse (3.0.7)
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
concurrent-ruby (1.3.5)
|
concurrent-ruby (1.3.6)
|
||||||
connection_pool (2.5.3)
|
connection_pool (3.0.2)
|
||||||
crack (1.0.0)
|
crack (1.0.1)
|
||||||
bigdecimal
|
bigdecimal
|
||||||
rexml
|
rexml
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
css_parser (1.21.0)
|
csv (3.3.5)
|
||||||
addressable
|
|
||||||
csv (3.3.2)
|
|
||||||
csv-safe (3.3.1)
|
csv-safe (3.3.1)
|
||||||
csv (~> 3.0)
|
csv (~> 3.0)
|
||||||
cuprite (0.15.1)
|
cuprite (0.17)
|
||||||
capybara (~> 3.0)
|
capybara (~> 3.0)
|
||||||
ferrum (~> 0.15.0)
|
ferrum (~> 0.17.0)
|
||||||
date (3.4.1)
|
date (3.5.1)
|
||||||
debug (1.10.0)
|
debug (1.11.1)
|
||||||
irb (~> 1.10)
|
irb (~> 1.10)
|
||||||
reline (>= 0.3.8)
|
reline (>= 0.3.8)
|
||||||
declarative (0.0.20)
|
declarative (0.0.20)
|
||||||
devise (4.9.4)
|
devise (5.0.4)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
orm_adapter (~> 0.1)
|
orm_adapter (~> 0.1)
|
||||||
railties (>= 4.1.0)
|
railties (>= 7.0)
|
||||||
responders
|
responders
|
||||||
warden (~> 1.2.3)
|
warden (~> 1.2.3)
|
||||||
devise-two-factor (6.1.0)
|
devise-two-factor (6.4.0)
|
||||||
activesupport (>= 7.0, < 8.1)
|
activesupport (>= 7.2, < 8.2)
|
||||||
devise (~> 4.0)
|
devise (>= 4.0, < 6.0)
|
||||||
railties (>= 7.0, < 8.1)
|
railties (>= 7.2, < 8.2)
|
||||||
rotp (~> 6.0)
|
rotp (~> 6.0)
|
||||||
diff-lcs (1.5.1)
|
diff-lcs (1.6.2)
|
||||||
digest-crc (0.6.5)
|
digest-crc (0.7.0)
|
||||||
rake (>= 12.0.0, < 14.0.0)
|
rake (>= 12.0.0, < 14.0.0)
|
||||||
docile (1.4.1)
|
docile (1.4.1)
|
||||||
dotenv (3.1.7)
|
dotenv (3.2.0)
|
||||||
drb (2.2.3)
|
drb (2.2.3)
|
||||||
email_typo (0.2.3)
|
email_typo (0.2.3)
|
||||||
erb (5.0.2)
|
erb (6.0.4)
|
||||||
erb_lint (0.7.0)
|
erb_lint (0.9.0)
|
||||||
activesupport
|
activesupport
|
||||||
better_html (>= 2.0.1)
|
better_html (>= 2.0.1)
|
||||||
parser (>= 2.7.1.4)
|
parser (>= 2.7.1.4)
|
||||||
@@ -187,117 +188,97 @@ GEM
|
|||||||
rubocop (>= 1)
|
rubocop (>= 1)
|
||||||
smart_properties
|
smart_properties
|
||||||
erubi (1.13.1)
|
erubi (1.13.1)
|
||||||
factory_bot (6.5.0)
|
factory_bot (6.5.6)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 6.1.0)
|
||||||
factory_bot_rails (6.4.4)
|
factory_bot_rails (6.5.1)
|
||||||
factory_bot (~> 6.5)
|
factory_bot (~> 6.5)
|
||||||
railties (>= 5.0.0)
|
railties (>= 6.1.0)
|
||||||
faker (3.5.1)
|
faker (3.6.1)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
faraday (1.10.4)
|
faraday (2.14.2)
|
||||||
faraday-em_http (~> 1.0)
|
faraday-net_http (>= 2.0, < 3.5)
|
||||||
faraday-em_synchrony (~> 1.0)
|
json
|
||||||
faraday-excon (~> 1.1)
|
logger
|
||||||
faraday-httpclient (~> 1.0)
|
faraday-follow_redirects (0.5.0)
|
||||||
faraday-multipart (~> 1.0)
|
|
||||||
faraday-net_http (~> 1.0)
|
|
||||||
faraday-net_http_persistent (~> 1.0)
|
|
||||||
faraday-patron (~> 1.0)
|
|
||||||
faraday-rack (~> 1.0)
|
|
||||||
faraday-retry (~> 1.0)
|
|
||||||
ruby2_keywords (>= 0.0.4)
|
|
||||||
faraday-em_http (1.0.0)
|
|
||||||
faraday-em_synchrony (1.0.0)
|
|
||||||
faraday-excon (1.1.0)
|
|
||||||
faraday-follow_redirects (0.3.0)
|
|
||||||
faraday (>= 1, < 3)
|
faraday (>= 1, < 3)
|
||||||
faraday-httpclient (1.0.1)
|
faraday-net_http (3.4.2)
|
||||||
faraday-multipart (1.1.0)
|
net-http (~> 0.5)
|
||||||
multipart-post (~> 2.0)
|
ferrum (0.17.2)
|
||||||
faraday-net_http (1.0.2)
|
|
||||||
faraday-net_http_persistent (1.2.0)
|
|
||||||
faraday-patron (1.0.0)
|
|
||||||
faraday-rack (1.0.0)
|
|
||||||
faraday-retry (1.0.3)
|
|
||||||
faraday_middleware (1.2.1)
|
|
||||||
faraday (~> 1.0)
|
|
||||||
ferrum (0.15)
|
|
||||||
addressable (~> 2.5)
|
addressable (~> 2.5)
|
||||||
|
base64 (~> 0.2)
|
||||||
concurrent-ruby (~> 1.1)
|
concurrent-ruby (~> 1.1)
|
||||||
webrick (~> 1.7)
|
webrick (~> 1.7)
|
||||||
websocket-driver (~> 0.7)
|
websocket-driver (~> 0.7)
|
||||||
ffi (1.17.1)
|
ffi (1.17.4-aarch64-linux-gnu)
|
||||||
ffi (1.17.1-aarch64-linux-gnu)
|
ffi (1.17.4-aarch64-linux-musl)
|
||||||
ffi (1.17.1-aarch64-linux-musl)
|
ffi (1.17.4-arm64-darwin)
|
||||||
ffi (1.17.1-arm64-darwin)
|
ffi (1.17.4-x86_64-linux-gnu)
|
||||||
ffi (1.17.1-x86_64-linux-gnu)
|
ffi (1.17.4-x86_64-linux-musl)
|
||||||
ffi (1.17.1-x86_64-linux-musl)
|
foreman (0.90.0)
|
||||||
foreman (0.88.1)
|
thor (~> 1.4)
|
||||||
geom2d (0.4.1)
|
geom2d (0.4.1)
|
||||||
globalid (1.2.1)
|
globalid (1.3.0)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
google-apis-core (0.15.1)
|
google-apis-core (1.0.2)
|
||||||
addressable (~> 2.5, >= 2.5.1)
|
addressable (~> 2.8, >= 2.8.7)
|
||||||
googleauth (~> 1.9)
|
faraday (~> 2.13)
|
||||||
httpclient (>= 2.8.3, < 3.a)
|
faraday-follow_redirects (~> 0.3)
|
||||||
mini_mime (~> 1.0)
|
googleauth (~> 1.14)
|
||||||
mutex_m
|
mini_mime (~> 1.1)
|
||||||
representable (~> 3.0)
|
representable (~> 3.0)
|
||||||
retriable (>= 2.0, < 4.a)
|
retriable (~> 3.1)
|
||||||
google-apis-iamcredentials_v1 (0.22.0)
|
google-apis-iamcredentials_v1 (0.26.0)
|
||||||
google-apis-core (>= 0.15.0, < 2.a)
|
google-apis-core (>= 0.15.0, < 2.a)
|
||||||
google-apis-storage_v1 (0.49.0)
|
google-apis-storage_v1 (0.61.0)
|
||||||
google-apis-core (>= 0.15.0, < 2.a)
|
google-apis-core (>= 0.15.0, < 2.a)
|
||||||
google-cloud-core (1.7.1)
|
google-cloud-core (1.8.0)
|
||||||
google-cloud-env (>= 1.0, < 3.a)
|
google-cloud-env (>= 1.0, < 3.a)
|
||||||
google-cloud-errors (~> 1.0)
|
google-cloud-errors (~> 1.0)
|
||||||
google-cloud-env (2.2.1)
|
google-cloud-env (2.3.1)
|
||||||
|
base64 (~> 0.2)
|
||||||
faraday (>= 1.0, < 3.a)
|
faraday (>= 1.0, < 3.a)
|
||||||
google-cloud-errors (1.4.0)
|
google-cloud-errors (1.6.0)
|
||||||
google-cloud-storage (1.54.0)
|
google-cloud-storage (1.59.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
digest-crc (~> 0.4)
|
digest-crc (~> 0.4)
|
||||||
google-apis-core (~> 0.13)
|
google-apis-core (>= 0.18, < 2)
|
||||||
google-apis-iamcredentials_v1 (~> 0.18)
|
google-apis-iamcredentials_v1 (~> 0.18)
|
||||||
google-apis-storage_v1 (~> 0.38)
|
google-apis-storage_v1 (>= 0.42)
|
||||||
google-cloud-core (~> 1.6)
|
google-cloud-core (~> 1.6)
|
||||||
googleauth (~> 1.9)
|
googleauth (~> 1.9)
|
||||||
mini_mime (~> 1.0)
|
mini_mime (~> 1.0)
|
||||||
google-logging-utils (0.1.0)
|
google-logging-utils (0.2.0)
|
||||||
googleauth (1.12.2)
|
googleauth (1.16.2)
|
||||||
faraday (>= 1.0, < 3.a)
|
faraday (>= 1.0, < 3.a)
|
||||||
google-cloud-env (~> 2.2)
|
google-cloud-env (~> 2.2)
|
||||||
google-logging-utils (~> 0.1)
|
google-logging-utils (~> 0.1)
|
||||||
jwt (>= 1.4, < 3.0)
|
jwt (>= 1.4, < 4.0)
|
||||||
multi_json (~> 1.11)
|
multi_json (~> 1.11)
|
||||||
os (>= 0.9, < 2.0)
|
os (>= 0.9, < 2.0)
|
||||||
signet (>= 0.16, < 2.a)
|
signet (>= 0.16, < 2.a)
|
||||||
hashdiff (1.1.2)
|
hashdiff (1.2.1)
|
||||||
hexapdf (1.4.0)
|
hexapdf (1.7.0)
|
||||||
cmdparse (~> 3.0, >= 3.0.3)
|
cmdparse (~> 3.0, >= 3.0.3)
|
||||||
geom2d (~> 0.4, >= 0.4.1)
|
geom2d (~> 0.4, >= 0.4.1)
|
||||||
openssl (>= 2.2.1)
|
openssl (>= 2.2.1)
|
||||||
strscan (>= 3.1.2)
|
strscan (>= 3.1.2)
|
||||||
htmlentities (4.3.4)
|
i18n (1.14.8)
|
||||||
httpclient (2.8.3)
|
|
||||||
i18n (1.14.7)
|
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
image_processing (1.13.0)
|
io-console (0.8.2)
|
||||||
mini_magick (>= 4.9.5, < 5)
|
irb (1.18.0)
|
||||||
ruby-vips (>= 2.0.17, < 3)
|
|
||||||
io-console (0.8.1)
|
|
||||||
irb (1.15.2)
|
|
||||||
pp (>= 0.6.0)
|
pp (>= 0.6.0)
|
||||||
|
prism (>= 1.3.0)
|
||||||
rdoc (>= 4.0.0)
|
rdoc (>= 4.0.0)
|
||||||
reline (>= 0.4.2)
|
reline (>= 0.4.2)
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.15.0)
|
json (2.19.9)
|
||||||
jwt (2.9.3)
|
jwt (3.2.0)
|
||||||
base64
|
base64
|
||||||
language_server-protocol (3.17.0.5)
|
language_server-protocol (3.17.0.5)
|
||||||
launchy (3.0.1)
|
launchy (3.1.1)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
childprocess (~> 5.0)
|
childprocess (~> 5.0)
|
||||||
|
logger (~> 1.6)
|
||||||
letter_opener (1.10.0)
|
letter_opener (1.10.0)
|
||||||
launchy (>= 2.2, < 4)
|
launchy (>= 2.2, < 4)
|
||||||
letter_opener_web (3.0.0)
|
letter_opener_web (3.0.0)
|
||||||
@@ -312,29 +293,27 @@ GEM
|
|||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
railties (>= 4)
|
railties (>= 4)
|
||||||
request_store (~> 1.0)
|
request_store (~> 1.0)
|
||||||
loofah (2.24.1)
|
loofah (2.25.1)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
mail (2.8.1)
|
mail (2.9.0)
|
||||||
|
logger
|
||||||
mini_mime (>= 0.1.1)
|
mini_mime (>= 0.1.1)
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
marcel (1.0.4)
|
marcel (1.1.0)
|
||||||
matrix (0.4.2)
|
matrix (0.4.3)
|
||||||
method_source (1.1.0)
|
method_source (1.1.0)
|
||||||
mini_magick (4.13.2)
|
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.9)
|
minitest (6.0.6)
|
||||||
minitest (5.25.5)
|
drb (~> 2.0)
|
||||||
msgpack (1.7.5)
|
prism (~> 1.5)
|
||||||
multi_json (1.15.0)
|
msgpack (1.8.0)
|
||||||
multipart-post (2.4.1)
|
multi_json (1.19.1)
|
||||||
mutex_m (0.3.0)
|
net-http (0.9.1)
|
||||||
mysql2 (0.5.6)
|
uri (>= 0.11.1)
|
||||||
net-http-persistent (4.0.5)
|
net-imap (0.6.4.1)
|
||||||
connection_pool (~> 2.2)
|
|
||||||
net-imap (0.5.9)
|
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
net-pop (0.1.2)
|
net-pop (0.1.2)
|
||||||
@@ -343,124 +322,113 @@ GEM
|
|||||||
timeout
|
timeout
|
||||||
net-smtp (0.5.1)
|
net-smtp (0.5.1)
|
||||||
net-protocol
|
net-protocol
|
||||||
nio4r (2.7.4)
|
nio4r (2.7.5)
|
||||||
nokogiri (1.18.9)
|
nokogiri (1.19.3-aarch64-linux-gnu)
|
||||||
mini_portile2 (~> 2.8.2)
|
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.9-aarch64-linux-gnu)
|
nokogiri (1.19.3-aarch64-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.9-aarch64-linux-musl)
|
nokogiri (1.19.3-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.9-arm64-darwin)
|
nokogiri (1.19.3-x86_64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.9-x86_64-linux-gnu)
|
nokogiri (1.19.3-x86_64-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.9-x86_64-linux-musl)
|
numo-narray-alt (0.10.3)
|
||||||
racc (~> 1.4)
|
|
||||||
numo-narray (0.9.2.1)
|
|
||||||
oj (3.16.11)
|
|
||||||
bigdecimal (>= 3.0)
|
|
||||||
ostruct (>= 0.2)
|
|
||||||
onnxruntime (0.10.1)
|
|
||||||
ffi
|
|
||||||
onnxruntime (0.10.1-aarch64-linux)
|
onnxruntime (0.10.1-aarch64-linux)
|
||||||
ffi
|
ffi
|
||||||
onnxruntime (0.10.1-arm64-darwin)
|
onnxruntime (0.10.1-arm64-darwin)
|
||||||
ffi
|
ffi
|
||||||
onnxruntime (0.10.1-x86_64-linux)
|
onnxruntime (0.10.1-x86_64-linux)
|
||||||
ffi
|
ffi
|
||||||
openssl (3.3.0)
|
openssl (4.0.1)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
ostruct (0.6.3)
|
package_json (0.2.0)
|
||||||
package_json (0.1.0)
|
pagy (43.4.4)
|
||||||
pagy (9.3.3)
|
json
|
||||||
parallel (1.27.0)
|
uri
|
||||||
parser (3.3.9.0)
|
yaml
|
||||||
|
parallel (1.28.0)
|
||||||
|
parser (3.3.11.1)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
pg (1.5.9)
|
pg (1.6.3-aarch64-linux)
|
||||||
pp (0.6.2)
|
pg (1.6.3-aarch64-linux-musl)
|
||||||
|
pg (1.6.3-arm64-darwin)
|
||||||
|
pg (1.6.3-x86_64-linux)
|
||||||
|
pg (1.6.3-x86_64-linux-musl)
|
||||||
|
pp (0.6.3)
|
||||||
prettyprint
|
prettyprint
|
||||||
premailer (1.27.0)
|
pretender (1.0.0)
|
||||||
addressable
|
actionpack (>= 7.2)
|
||||||
css_parser (>= 1.19.0)
|
|
||||||
htmlentities (>= 4.0.0)
|
|
||||||
premailer-rails (1.12.0)
|
|
||||||
actionmailer (>= 3)
|
|
||||||
net-smtp
|
|
||||||
premailer (~> 1.7, >= 1.7.9)
|
|
||||||
pretender (0.5.0)
|
|
||||||
actionpack (>= 6.1)
|
|
||||||
prettyprint (0.2.0)
|
prettyprint (0.2.0)
|
||||||
prism (1.5.1)
|
prism (1.9.0)
|
||||||
pry (0.15.0)
|
pry (0.16.0)
|
||||||
coderay (~> 1.1)
|
coderay (~> 1.1)
|
||||||
method_source (~> 1.0)
|
method_source (~> 1.0)
|
||||||
|
reline (>= 0.6.0)
|
||||||
pry-rails (0.3.11)
|
pry-rails (0.3.11)
|
||||||
pry (>= 0.13.0)
|
pry (>= 0.13.0)
|
||||||
psych (5.2.6)
|
psych (5.3.1)
|
||||||
date
|
date
|
||||||
stringio
|
stringio
|
||||||
public_suffix (6.0.1)
|
public_suffix (7.0.5)
|
||||||
puma (6.5.0)
|
puma (8.0.2)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rack (3.2.3)
|
rack (3.2.6)
|
||||||
rack-proxy (0.7.7)
|
rack-proxy (0.7.7)
|
||||||
rack
|
rack
|
||||||
rack-session (2.1.1)
|
rack-session (2.1.2)
|
||||||
base64 (>= 0.1.0)
|
base64 (>= 0.1.0)
|
||||||
rack (>= 3.0.0)
|
rack (>= 3.0.0)
|
||||||
rack-test (2.2.0)
|
rack-test (2.2.0)
|
||||||
rack (>= 1.3)
|
rack (>= 1.3)
|
||||||
rackup (2.2.1)
|
rackup (2.3.1)
|
||||||
rack (>= 3)
|
rack (>= 3)
|
||||||
rails (8.0.2.1)
|
rails (8.1.3)
|
||||||
actioncable (= 8.0.2.1)
|
actioncable (= 8.1.3)
|
||||||
actionmailbox (= 8.0.2.1)
|
actionmailbox (= 8.1.3)
|
||||||
actionmailer (= 8.0.2.1)
|
actionmailer (= 8.1.3)
|
||||||
actionpack (= 8.0.2.1)
|
actionpack (= 8.1.3)
|
||||||
actiontext (= 8.0.2.1)
|
actiontext (= 8.1.3)
|
||||||
actionview (= 8.0.2.1)
|
actionview (= 8.1.3)
|
||||||
activejob (= 8.0.2.1)
|
activejob (= 8.1.3)
|
||||||
activemodel (= 8.0.2.1)
|
activemodel (= 8.1.3)
|
||||||
activerecord (= 8.0.2.1)
|
activerecord (= 8.1.3)
|
||||||
activestorage (= 8.0.2.1)
|
activestorage (= 8.1.3)
|
||||||
activesupport (= 8.0.2.1)
|
activesupport (= 8.1.3)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 8.0.2.1)
|
railties (= 8.1.3)
|
||||||
rails-dom-testing (2.3.0)
|
rails-dom-testing (2.3.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
minitest
|
minitest
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
rails-html-sanitizer (1.6.2)
|
rails-html-sanitizer (1.7.0)
|
||||||
loofah (~> 2.21)
|
loofah (~> 2.25)
|
||||||
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||||
rails-i18n (8.0.1)
|
rails-i18n (8.1.0)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
railties (>= 8.0.0, < 9)
|
railties (>= 8.0.0, < 9)
|
||||||
rails_autolink (1.1.8)
|
railties (8.1.3)
|
||||||
actionview (> 3.1)
|
actionpack (= 8.1.3)
|
||||||
activesupport (> 3.1)
|
activesupport (= 8.1.3)
|
||||||
railties (> 3.1)
|
|
||||||
railties (8.0.2.1)
|
|
||||||
actionpack (= 8.0.2.1)
|
|
||||||
activesupport (= 8.0.2.1)
|
|
||||||
irb (~> 1.13)
|
irb (~> 1.13)
|
||||||
rackup (>= 1.0.0)
|
rackup (>= 1.0.0)
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0, >= 1.2.2)
|
thor (~> 1.0, >= 1.2.2)
|
||||||
|
tsort (>= 0.2)
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.3.0)
|
rake (13.4.2)
|
||||||
rdoc (6.14.2)
|
rdoc (7.2.0)
|
||||||
erb
|
erb
|
||||||
psych (>= 4.0.0)
|
psych (>= 4.0.0)
|
||||||
redis-client (0.23.0)
|
tsort
|
||||||
|
redis-client (0.29.0)
|
||||||
connection_pool
|
connection_pool
|
||||||
regexp_parser (2.11.3)
|
regexp_parser (2.11.3)
|
||||||
reline (0.6.2)
|
reline (0.6.3)
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
representable (3.2.0)
|
representable (3.2.0)
|
||||||
declarative (< 0.1.0)
|
declarative (< 0.1.0)
|
||||||
@@ -468,35 +436,35 @@ GEM
|
|||||||
uber (< 0.2.0)
|
uber (< 0.2.0)
|
||||||
request_store (1.7.0)
|
request_store (1.7.0)
|
||||||
rack (>= 1.4)
|
rack (>= 1.4)
|
||||||
responders (3.1.1)
|
responders (3.2.0)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 7.0)
|
||||||
railties (>= 5.2)
|
railties (>= 7.0)
|
||||||
retriable (3.1.2)
|
retriable (3.4.1)
|
||||||
rexml (3.4.4)
|
rexml (3.4.4)
|
||||||
rotp (6.3.0)
|
rotp (6.3.0)
|
||||||
rouge (4.5.2)
|
rouge (4.7.0)
|
||||||
rqrcode (2.2.0)
|
rqrcode (3.2.0)
|
||||||
chunky_png (~> 1.0)
|
chunky_png (~> 1.0)
|
||||||
rqrcode_core (~> 1.0)
|
rqrcode_core (~> 2.0)
|
||||||
rqrcode_core (1.2.0)
|
rqrcode_core (2.1.0)
|
||||||
rspec-core (3.13.2)
|
rspec-core (3.13.6)
|
||||||
rspec-support (~> 3.13.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-expectations (3.13.3)
|
rspec-expectations (3.13.5)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.13.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-mocks (3.13.2)
|
rspec-mocks (3.13.8)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.13.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-rails (7.1.0)
|
rspec-rails (8.0.4)
|
||||||
actionpack (>= 7.0)
|
actionpack (>= 7.2)
|
||||||
activesupport (>= 7.0)
|
activesupport (>= 7.2)
|
||||||
railties (>= 7.0)
|
railties (>= 7.2)
|
||||||
rspec-core (~> 3.13)
|
rspec-core (>= 3.13.0, < 5.0.0)
|
||||||
rspec-expectations (~> 3.13)
|
rspec-expectations (>= 3.13.0, < 5.0.0)
|
||||||
rspec-mocks (~> 3.13)
|
rspec-mocks (>= 3.13.0, < 5.0.0)
|
||||||
rspec-support (~> 3.13)
|
rspec-support (>= 3.13.0, < 5.0.0)
|
||||||
rspec-support (3.13.2)
|
rspec-support (3.13.7)
|
||||||
rubocop (1.81.1)
|
rubocop (1.86.0)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (~> 3.17.0.2)
|
language_server-protocol (~> 3.17.0.2)
|
||||||
lint_roller (~> 1.1.0)
|
lint_roller (~> 1.1.0)
|
||||||
@@ -504,122 +472,127 @@ GEM
|
|||||||
parser (>= 3.3.0.2)
|
parser (>= 3.3.0.2)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 2.9.3, < 3.0)
|
regexp_parser (>= 2.9.3, < 3.0)
|
||||||
rubocop-ast (>= 1.47.1, < 2.0)
|
rubocop-ast (>= 1.49.0, < 2.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 2.4.0, < 4.0)
|
unicode-display_width (>= 2.4.0, < 4.0)
|
||||||
rubocop-ast (1.47.1)
|
rubocop-ast (1.49.1)
|
||||||
parser (>= 3.3.7.2)
|
parser (>= 3.3.7.2)
|
||||||
prism (~> 1.4)
|
prism (~> 1.7)
|
||||||
rubocop-performance (1.23.0)
|
rubocop-performance (1.26.1)
|
||||||
rubocop (>= 1.48.1, < 2.0)
|
lint_roller (~> 1.1)
|
||||||
rubocop-ast (>= 1.31.1, < 2.0)
|
rubocop (>= 1.75.0, < 2.0)
|
||||||
rubocop-rails (2.27.0)
|
rubocop-ast (>= 1.47.1, < 2.0)
|
||||||
|
rubocop-rails (2.34.3)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
|
lint_roller (~> 1.1)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.52.0, < 2.0)
|
rubocop (>= 1.75.0, < 2.0)
|
||||||
rubocop-ast (>= 1.31.1, < 2.0)
|
rubocop-ast (>= 1.44.0, < 2.0)
|
||||||
rubocop-rspec (3.3.0)
|
rubocop-rspec (3.9.0)
|
||||||
rubocop (~> 1.61)
|
lint_roller (~> 1.1)
|
||||||
|
rubocop (~> 1.81)
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
ruby-vips (2.2.2)
|
ruby-vips (2.3.0)
|
||||||
ffi (~> 1.12)
|
ffi (~> 1.12)
|
||||||
logger
|
logger
|
||||||
ruby2_keywords (0.0.5)
|
rubyXL (3.4.35)
|
||||||
rubyXL (3.4.33)
|
|
||||||
nokogiri (>= 1.10.8)
|
nokogiri (>= 1.10.8)
|
||||||
rubyzip (>= 1.3.0)
|
rubyzip (>= 3.2.2)
|
||||||
rubyzip (2.3.2)
|
rubyzip (3.2.2)
|
||||||
securerandom (0.4.1)
|
securerandom (0.4.1)
|
||||||
semantic_range (3.1.0)
|
semantic_range (3.1.1)
|
||||||
shakapacker (8.0.2)
|
shakapacker (9.7.0)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
package_json
|
package_json
|
||||||
rack-proxy (>= 0.6.1)
|
rack-proxy (>= 0.6.1)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
semantic_range (>= 2.3.0)
|
semantic_range (>= 2.3.0)
|
||||||
sidekiq (7.3.7)
|
sidekiq (8.1.6)
|
||||||
connection_pool (>= 2.3.0)
|
connection_pool (>= 3.0.0)
|
||||||
logger
|
json (>= 2.16.0)
|
||||||
rack (>= 2.2.4)
|
logger (>= 1.7.0)
|
||||||
redis-client (>= 0.22.2)
|
rack (>= 3.2.0)
|
||||||
signet (0.19.0)
|
redis-client (>= 0.29.0)
|
||||||
|
signet (0.21.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
faraday (>= 0.17.5, < 3.a)
|
faraday (>= 0.17.5, < 3.a)
|
||||||
jwt (>= 1.5, < 3.0)
|
jwt (>= 1.5, < 4.0)
|
||||||
multi_json (~> 1.10)
|
multi_json (~> 1.10)
|
||||||
simplecov (0.22.0)
|
simplecov (0.22.0)
|
||||||
docile (~> 1.1)
|
docile (~> 1.1)
|
||||||
simplecov-html (~> 0.11)
|
simplecov-html (~> 0.11)
|
||||||
simplecov_json_formatter (~> 0.1)
|
simplecov_json_formatter (~> 0.1)
|
||||||
simplecov-html (0.13.1)
|
simplecov-html (0.13.2)
|
||||||
simplecov_json_formatter (0.1.4)
|
simplecov_json_formatter (0.1.4)
|
||||||
smart_properties (1.17.0)
|
smart_properties (1.17.0)
|
||||||
sqlite3 (2.5.0)
|
sqlite3 (2.9.2-aarch64-linux-gnu)
|
||||||
mini_portile2 (~> 2.8.0)
|
sqlite3 (2.9.2-aarch64-linux-musl)
|
||||||
sqlite3 (2.5.0-aarch64-linux-gnu)
|
sqlite3 (2.9.2-arm64-darwin)
|
||||||
sqlite3 (2.5.0-aarch64-linux-musl)
|
sqlite3 (2.9.2-x86_64-linux-gnu)
|
||||||
sqlite3 (2.5.0-arm64-darwin)
|
sqlite3 (2.9.2-x86_64-linux-musl)
|
||||||
sqlite3 (2.5.0-x86_64-linux-gnu)
|
stringio (3.2.0)
|
||||||
sqlite3 (2.5.0-x86_64-linux-musl)
|
strip_attributes (2.0.1)
|
||||||
stringio (3.1.7)
|
|
||||||
strip_attributes (1.14.1)
|
|
||||||
activemodel (>= 3.0, < 9.0)
|
activemodel (>= 3.0, < 9.0)
|
||||||
strscan (3.1.5)
|
strscan (3.1.8)
|
||||||
thor (1.4.0)
|
thor (1.5.0)
|
||||||
timeout (0.4.3)
|
timeout (0.6.1)
|
||||||
trailblazer-option (0.1.2)
|
trailblazer-option (0.1.2)
|
||||||
turbo-rails (2.0.11)
|
trilogy (2.12.2)
|
||||||
actionpack (>= 6.0.0)
|
bigdecimal
|
||||||
railties (>= 6.0.0)
|
tsort (0.2.0)
|
||||||
twitter_cldr (6.12.1)
|
turbo-rails (2.0.23)
|
||||||
|
actionpack (>= 7.1.0)
|
||||||
|
railties (>= 7.1.0)
|
||||||
|
twitter_cldr (6.14.0)
|
||||||
|
base64
|
||||||
camertron-eprun
|
camertron-eprun
|
||||||
cldr-plurals-runtime-rb (~> 1.1)
|
cldr-plurals-runtime-rb (~> 1.1)
|
||||||
tzinfo
|
tzinfo
|
||||||
tzinfo (2.0.6)
|
tzinfo (2.0.6)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
tzinfo-data (1.2024.2)
|
tzinfo-data (1.2026.1)
|
||||||
tzinfo (>= 1.0.0)
|
tzinfo (>= 1.0.0)
|
||||||
uber (0.1.0)
|
uber (0.1.0)
|
||||||
unicode-display_width (3.2.0)
|
unicode-display_width (3.2.0)
|
||||||
unicode-emoji (~> 4.1)
|
unicode-emoji (~> 4.1)
|
||||||
unicode-emoji (4.1.0)
|
unicode-emoji (4.2.0)
|
||||||
uniform_notifier (1.16.0)
|
uniform_notifier (1.18.0)
|
||||||
uri (1.0.3)
|
uri (1.1.1)
|
||||||
useragent (0.16.11)
|
useragent (0.16.11)
|
||||||
warden (1.2.9)
|
warden (1.2.9)
|
||||||
rack (>= 2.0.9)
|
rack (>= 2.0.9)
|
||||||
web-console (4.2.1)
|
web-console (4.3.0)
|
||||||
actionview (>= 6.0.0)
|
actionview (>= 8.0.0)
|
||||||
activemodel (>= 6.0.0)
|
|
||||||
bindex (>= 0.4.0)
|
bindex (>= 0.4.0)
|
||||||
railties (>= 6.0.0)
|
railties (>= 8.0.0)
|
||||||
webmock (3.24.0)
|
webmock (3.26.2)
|
||||||
addressable (>= 2.8.0)
|
addressable (>= 2.8.0)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff (>= 0.4.0, < 2.0.0)
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
webrick (1.9.1)
|
webrick (1.9.2)
|
||||||
websocket-driver (0.8.0)
|
websocket-driver (0.8.0)
|
||||||
base64
|
base64
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
xpath (3.2.0)
|
xpath (3.2.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
zeitwerk (2.7.3)
|
yaml (0.4.0)
|
||||||
|
zeitwerk (2.7.5)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
aarch64-linux
|
aarch64-linux
|
||||||
aarch64-linux-musl
|
aarch64-linux-musl
|
||||||
arm64-darwin
|
arm64-darwin
|
||||||
ruby
|
|
||||||
x86_64-linux
|
x86_64-linux
|
||||||
x86_64-linux-musl
|
x86_64-linux-musl
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
|
addressable
|
||||||
annotaterb
|
annotaterb
|
||||||
arabic-letter-connector
|
arabic-letter-connector
|
||||||
aws-sdk-s3
|
aws-sdk-s3
|
||||||
aws-sdk-secretsmanager
|
aws-sdk-secretsmanager
|
||||||
azure-storage-blob
|
azure-blob
|
||||||
better_html
|
better_html
|
||||||
bootsnap
|
bootsnap
|
||||||
brakeman
|
brakeman
|
||||||
@@ -642,24 +615,19 @@ DEPENDENCIES
|
|||||||
foreman
|
foreman
|
||||||
google-cloud-storage
|
google-cloud-storage
|
||||||
hexapdf
|
hexapdf
|
||||||
image_processing
|
|
||||||
jwt
|
jwt
|
||||||
letter_opener_web
|
letter_opener_web
|
||||||
lograge
|
lograge
|
||||||
mysql2
|
numo-narray-alt
|
||||||
numo-narray
|
|
||||||
oj
|
|
||||||
onnxruntime
|
onnxruntime
|
||||||
pagy
|
pagy
|
||||||
pg
|
pg
|
||||||
premailer-rails
|
|
||||||
pretender
|
pretender
|
||||||
pry-rails
|
pry-rails
|
||||||
puma
|
puma
|
||||||
rack
|
rack
|
||||||
rails
|
rails
|
||||||
rails-i18n
|
rails-i18n
|
||||||
rails_autolink
|
|
||||||
rotp
|
rotp
|
||||||
rouge
|
rouge
|
||||||
rqrcode
|
rqrcode
|
||||||
@@ -675,6 +643,7 @@ DEPENDENCIES
|
|||||||
simplecov
|
simplecov
|
||||||
sqlite3
|
sqlite3
|
||||||
strip_attributes
|
strip_attributes
|
||||||
|
trilogy
|
||||||
turbo-rails
|
turbo-rails
|
||||||
twitter_cldr
|
twitter_cldr
|
||||||
tzinfo-data
|
tzinfo-data
|
||||||
@@ -682,7 +651,7 @@ DEPENDENCIES
|
|||||||
webmock
|
webmock
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
ruby 3.4.2p28
|
ruby 4.0.5
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.5.3
|
4.0.3
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Additional Terms
|
||||||
|
|
||||||
|
In accordance with Section 7(b) of the GNU Affero General Public License,
|
||||||
|
a covered work must retain the original DocuSeal attribution in interactive
|
||||||
|
user interfaces.
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<h1 align="center" style="border-bottom: none">
|
<h1 align="center" style="border-bottom: none">
|
||||||
<div>
|
<div>
|
||||||
<a href="https://www.docuseal.com">
|
<a href="https://www.docuseal.com">
|
||||||
<img alt="DocuSeal" src="https://github.com/docusealco/docuseal/assets/5418788/c12cd051-81cd-4402-bc3a-92f2cfdc1b06" width="80" />
|
<img alt="DocuSeal" src="https://github.com/user-attachments/assets/38b45682-ffa4-4919-abde-d2d422325c44" width="80" />
|
||||||
<br>
|
<br>
|
||||||
</a>
|
</a>
|
||||||
DocuSeal
|
DocuSeal
|
||||||
@@ -50,7 +50,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
- Company logo and white-label
|
- Company logo and white-label
|
||||||
- User roles
|
- User roles
|
||||||
- Automated reminders
|
- Automated reminders
|
||||||
- Invitation and identify verification via SMS
|
- Invitation and identity verification via SMS
|
||||||
- Conditional fields and formulas
|
- Conditional fields and formulas
|
||||||
- Bulk send with CSV, XLSX spreadsheet import
|
- Bulk send with CSV, XLSX spreadsheet import
|
||||||
- SSO / SAML
|
- SSO / SAML
|
||||||
@@ -64,7 +64,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
|
|
||||||
|Heroku|Railway|
|
|Heroku|Railway|
|
||||||
|:--:|:---:|
|
|:--:|:---:|
|
||||||
| [<img alt="Deploy on Heroku" src="https://www.herokucdn.com/deploy/button.svg" height="40">](https://heroku.com/deploy?template=https://github.com/docusealco/docuseal-heroku) | [<img alt="Deploy on Railway" src="https://railway.app/button.svg" height="40">](https://railway.app/template/IGoDnc?referralCode=ruU7JR)|
|
| [<img alt="Deploy on Heroku" src="https://www.herokucdn.com/deploy/button.svg" height="40">](https://heroku.com/deploy?template=https://github.com/docusealco/docuseal-heroku) | [<img alt="Deploy on Railway" src="https://railway.app/button.svg" height="40">](https://railway.com/deploy/IGoDnc?referralCode=ruU7JR)|
|
||||||
|**DigitalOcean**|**Render**|
|
|**DigitalOcean**|**Render**|
|
||||||
| [<img alt="Deploy on DigitalOcean" src="https://www.deploytodo.com/do-btn-blue.svg" height="40">](https://cloud.digitalocean.com/apps/new?repo=https://github.com/docusealco/docuseal-digitalocean/tree/master&refcode=421d50f53990) | [<img alt="Deploy to Render" src="https://render.com/images/deploy-to-render-button.svg" height="40">](https://render.com/deploy?repo=https://github.com/docusealco/docuseal-render)
|
| [<img alt="Deploy on DigitalOcean" src="https://www.deploytodo.com/do-btn-blue.svg" height="40">](https://cloud.digitalocean.com/apps/new?repo=https://github.com/docusealco/docuseal-digitalocean/tree/master&refcode=421d50f53990) | [<img alt="Deploy to Render" src="https://render.com/images/deploy-to-render-button.svg" height="40">](https://render.com/deploy?repo=https://github.com/docusealco/docuseal-render)
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
docker run --name docuseal -p 3000:3000 -v.:/data docuseal/docuseal
|
docker run --name docuseal -p 3000:3000 -v.:/data docuseal/docuseal
|
||||||
```
|
```
|
||||||
|
|
||||||
By default DocuSeal docker container uses an SQLite database to store data and configurations. Alternatively, it is possible use PostgreSQL or MySQL databases by specifying the `DATABASE_URL` env variable.
|
By default DocuSeal docker container uses an SQLite database to store data and configurations. Alternatively, it is possible to use PostgreSQL or MySQL databases by specifying the `DATABASE_URL` env variable.
|
||||||
|
|
||||||
#### Docker Compose
|
#### Docker Compose
|
||||||
|
|
||||||
@@ -97,8 +97,8 @@ At DocuSeal we have expertise and technologies to make documents creation, filli
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Distributed under the AGPLv3 License. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) for more information.
|
Distributed under the AGPLv3 License with Section 7(b) Additional Terms. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) and [LICENSE_ADDITIONAL_TERMS](https://github.com/docusealco/docuseal/blob/master/LICENSE_ADDITIONAL_TERMS) for more information.
|
||||||
Unless otherwise noted, all files © 2023 DocuSeal LLC.
|
Unless otherwise noted, all files © 2023-2026 DocuSeal LLC.
|
||||||
|
|
||||||
## Tools
|
## Tools
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class AccountConfigsController < ApplicationController
|
|||||||
AccountConfig::FORCE_MFA,
|
AccountConfig::FORCE_MFA,
|
||||||
AccountConfig::ALLOW_TO_RESUBMIT,
|
AccountConfig::ALLOW_TO_RESUBMIT,
|
||||||
AccountConfig::ALLOW_TO_DECLINE_KEY,
|
AccountConfig::ALLOW_TO_DECLINE_KEY,
|
||||||
|
AccountConfig::ALLOW_TO_DELEGATE_KEY,
|
||||||
AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
|
AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
|
||||||
AccountConfig::ESIGNING_PREFERENCE_KEY,
|
AccountConfig::ESIGNING_PREFERENCE_KEY,
|
||||||
AccountConfig::FORM_WITH_CONFETTI_KEY,
|
AccountConfig::FORM_WITH_CONFETTI_KEY,
|
||||||
@@ -23,7 +24,8 @@ class AccountConfigsController < ApplicationController
|
|||||||
AccountConfig::WITH_SIGNATURE_ID,
|
AccountConfig::WITH_SIGNATURE_ID,
|
||||||
AccountConfig::COMBINE_PDF_RESULT_KEY,
|
AccountConfig::COMBINE_PDF_RESULT_KEY,
|
||||||
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
|
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
|
||||||
AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY
|
AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY,
|
||||||
|
AccountConfig::ENABLE_MCP_KEY
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
InvalidKey = Class.new(StandardError)
|
InvalidKey = Class.new(StandardError)
|
||||||
@@ -35,7 +37,7 @@ class AccountConfigsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
raise InvalidKey unless ALLOWED_KEYS.include?(@account_config.key)
|
raise InvalidKey unless allowed_keys.include?(@account_config.key)
|
||||||
|
|
||||||
@account_config.destroy!
|
@account_config.destroy!
|
||||||
|
|
||||||
@@ -44,8 +46,12 @@ class AccountConfigsController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def allowed_keys
|
||||||
|
ALLOWED_KEYS
|
||||||
|
end
|
||||||
|
|
||||||
def load_account_config
|
def load_account_config
|
||||||
raise InvalidKey unless ALLOWED_KEYS.include?(account_config_params[:key])
|
raise InvalidKey unless allowed_keys.include?(account_config_params[:key])
|
||||||
|
|
||||||
@account_config =
|
@account_config =
|
||||||
AccountConfig.find_or_initialize_by(account: current_account, key: account_config_params[:key])
|
AccountConfig.find_or_initialize_by(account: current_account, key: account_config_params[:key])
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AccountCustomFieldsController < ApplicationController
|
||||||
|
before_action :load_account_config, only: :create
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize!(:create, Template)
|
||||||
|
|
||||||
|
@account_config.update!(account_config_params)
|
||||||
|
|
||||||
|
render json: @account_config.value
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_account_config
|
||||||
|
@account_config =
|
||||||
|
AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY)
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_config_params
|
||||||
|
params.permit(
|
||||||
|
value: [[:uuid, :name, :type,
|
||||||
|
:required, :readonly, :default_value,
|
||||||
|
:title, :description,
|
||||||
|
{ preferences: {},
|
||||||
|
default_value: [],
|
||||||
|
options: [%i[value uuid]],
|
||||||
|
validation: %i[message pattern min max step],
|
||||||
|
areas: [%i[x y w h cell_w option_uuid]] }]]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -53,7 +53,9 @@ class AccountsController < ApplicationController
|
|||||||
# rubocop:disable Layout/LineLength
|
# rubocop:disable Layout/LineLength
|
||||||
render turbo_stream: turbo_stream.replace(
|
render turbo_stream: turbo_stream.replace(
|
||||||
:account_delete_button,
|
:account_delete_button,
|
||||||
html: helpers.tag.p(I18n.t('your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account'))
|
html: helpers.tag.p(I18n.t('your_account_will_be_permanently_deleted_within_2_months_please_contact_us_if_you_want_to_keep_your_account')) +
|
||||||
|
helpers.tag.br +
|
||||||
|
helpers.tag.p(I18n.t('your_email_address_has_been_released_immediately_you_can_now_be_added_to_your_company_team_without_waiting_for_the_deletion_period_to_end'))
|
||||||
)
|
)
|
||||||
# rubocop:enable Layout/LineLength
|
# rubocop:enable Layout/LineLength
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ module Api
|
|||||||
|
|
||||||
before_action :set_cors_headers
|
before_action :set_cors_headers
|
||||||
before_action :set_noindex_headers
|
before_action :set_noindex_headers
|
||||||
|
before_action :set_security_headers
|
||||||
|
|
||||||
|
# rubocop:disable Metrics
|
||||||
def show
|
def show
|
||||||
blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
|
blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
|
||||||
|
|
||||||
@@ -21,6 +23,12 @@ module Api
|
|||||||
|
|
||||||
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
|
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
|
||||||
|
|
||||||
|
if Submitters::DANGEROUS_EXTENSIONS.include?(blob.filename.extension.to_s.downcase)
|
||||||
|
Rollbar.error('Dangerous extension') if defined?(Rollbar)
|
||||||
|
|
||||||
|
return head :unprocessable_content
|
||||||
|
end
|
||||||
|
|
||||||
attachment = blob.attachments.take
|
attachment = blob.attachments.take
|
||||||
|
|
||||||
@record = attachment.record
|
@record = attachment.record
|
||||||
@@ -33,12 +41,19 @@ module Api
|
|||||||
else
|
else
|
||||||
http_cache_forever public: true do
|
http_cache_forever public: true do
|
||||||
response.headers['Accept-Ranges'] = 'bytes'
|
response.headers['Accept-Ranges'] = 'bytes'
|
||||||
response.headers['Content-Length'] = blob.byte_size.to_s
|
|
||||||
|
|
||||||
send_blob_stream blob, disposition: params[:disposition]
|
if request.head?
|
||||||
|
response.headers['Content-Type'] = blob.content_type_for_serving
|
||||||
|
head :ok
|
||||||
|
else
|
||||||
|
send_blob_stream blob, disposition: params[:disposition]
|
||||||
|
end
|
||||||
|
|
||||||
|
response.headers['Content-Length'] = blob.byte_size.to_s
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
# rubocop:enable Metrics
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
@@ -57,8 +72,6 @@ module Api
|
|||||||
return if !require_ttl && !require_auth
|
return if !require_ttl && !require_auth
|
||||||
end
|
end
|
||||||
|
|
||||||
Rollbar.error('Blob unauthorized') if defined?(Rollbar)
|
|
||||||
|
|
||||||
raise CanCan::AccessDenied
|
raise CanCan::AccessDenied
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ module Api
|
|||||||
|
|
||||||
before_action :set_cors_headers
|
before_action :set_cors_headers
|
||||||
before_action :set_noindex_headers
|
before_action :set_noindex_headers
|
||||||
|
before_action :set_security_headers
|
||||||
|
|
||||||
# rubocop:disable Metrics
|
# rubocop:disable Metrics
|
||||||
def show
|
def show
|
||||||
@@ -18,6 +19,12 @@ module Api
|
|||||||
|
|
||||||
return head :not_found unless blob
|
return head :not_found unless blob
|
||||||
|
|
||||||
|
if Submitters::DANGEROUS_EXTENSIONS.include?(blob.filename.extension.to_s.downcase)
|
||||||
|
Rollbar.error('Dangerous extension') if defined?(Rollbar)
|
||||||
|
|
||||||
|
return head :unprocessable_content
|
||||||
|
end
|
||||||
|
|
||||||
is_permitted = blob.attachments.any? do |a|
|
is_permitted = blob.attachments.any? do |a|
|
||||||
(current_user && a.record.account.id == current_user.account_id) ||
|
(current_user && a.record.account.id == current_user.account_id) ||
|
||||||
a.record.account.account_configs.any? { |e| e.key == 'legacy_blob_proxy' } ||
|
a.record.account.account_configs.any? { |e| e.key == 'legacy_blob_proxy' } ||
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
module Api
|
module Api
|
||||||
class ApiBaseController < ActionController::API
|
class ApiBaseController < ActionController::API
|
||||||
include ActiveStorage::SetCurrent
|
include ActiveStorage::SetCurrent
|
||||||
include Pagy::Backend
|
include Pagy::Method
|
||||||
|
|
||||||
DEFAULT_LIMIT = 10
|
DEFAULT_LIMIT = 10
|
||||||
MAX_LIMIT = 100
|
MAX_LIMIT = 100
|
||||||
@@ -102,6 +102,10 @@ module Api
|
|||||||
headers['X-Robots-Tag'] = 'noindex'
|
headers['X-Robots-Tag'] = 'noindex'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_security_headers
|
||||||
|
response.headers['X-Content-Type-Options'] = 'nosniff'
|
||||||
|
end
|
||||||
|
|
||||||
def set_cors_headers
|
def set_cors_headers
|
||||||
headers['Access-Control-Allow-Origin'] = '*'
|
headers['Access-Control-Allow-Origin'] = '*'
|
||||||
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
|
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
|
||||||
|
|||||||
@@ -8,36 +8,52 @@ module Api
|
|||||||
COOKIE_STORE_LIMIT = 10
|
COOKIE_STORE_LIMIT = 10
|
||||||
|
|
||||||
def create
|
def create
|
||||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
|
unless can_upload?(@submitter)
|
||||||
|
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content
|
||||||
|
end
|
||||||
|
|
||||||
|
file = params[:file]
|
||||||
|
|
||||||
if params[:type].in?(%w[initials signature])
|
if params[:type].in?(%w[initials signature])
|
||||||
image = Vips::Image.new_from_file(params[:file].path)
|
image = ImageUtils.load_vips(file.read, content_type: file.content_type)
|
||||||
|
|
||||||
if ImageUtils.blank?(image)
|
if ImageUtils.blank?(image)
|
||||||
Rollbar.error("Empty signature: #{submitter.id}") if defined?(Rollbar)
|
Rollbar.error("Empty signature: #{@submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_content
|
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
if ImageUtils.error?(image)
|
if ImageUtils.error?(image)
|
||||||
Rollbar.error("Error signature: #{submitter.id}") if defined?(Rollbar)
|
Rollbar.error("Error signature: #{@submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
return render json: { error: "#{params[:type]} error, try to sign on another device" },
|
return render json: { error: "#{params[:type]} error, try to sign on another device" },
|
||||||
status: :unprocessable_content
|
status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
|
metadata = { analyzed: true, identified: true, width: image.width, height: image.height }
|
||||||
end
|
end
|
||||||
|
|
||||||
attachment = Submitters.create_attachment!(submitter, params)
|
attachment = Submitters.create_attachment!(@submitter, file, metadata:)
|
||||||
|
|
||||||
if params[:remember_signature] == 'true' && submitter.email.present?
|
if params[:remember_signature] == 'true' && @submitter.email.present?
|
||||||
cookies.encrypted[:signature_uuids] = build_new_cookie_signatures_json(submitter, attachment)
|
cookies.encrypted[:signature_uuids] = build_new_cookie_signatures_json(@submitter, attachment)
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
|
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
|
||||||
rescue Submitters::MaliciousFileExtension => e
|
rescue Submitters::MaliciousFileExtension => e
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
render json: { error: e.message }, status: :unprocessable_entity
|
render json: { error: e.message }, status: :unprocessable_content
|
||||||
|
end
|
||||||
|
|
||||||
|
def can_upload?(submitter)
|
||||||
|
!submitter.declined_at? &&
|
||||||
|
!submitter.completed_at? &&
|
||||||
|
!submitter.submission.archived_at? &&
|
||||||
|
!submitter.submission.expired? &&
|
||||||
|
!submitter.submission.template&.archived_at?
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_new_cookie_signatures_json(submitter, attachment)
|
def build_new_cookie_signatures_json(submitter, attachment)
|
||||||
|
|||||||
@@ -5,34 +5,17 @@ module Api
|
|||||||
load_and_authorize_resource :submission
|
load_and_authorize_resource :submission
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
is_merge = params[:merge] == 'true' &&
|
||||||
|
(@submission.schema_documents || @submission.template.schema_documents).size > 1
|
||||||
|
|
||||||
documents =
|
documents =
|
||||||
if @submission.submitters.all?(&:completed_at?)
|
if @submission.submitters.all?(&:completed_at?)
|
||||||
last_submitter = @submission.submitters.max_by(&:completed_at)
|
build_completed_documents(@submission, merge: is_merge)
|
||||||
|
|
||||||
if last_submitter.documents_attachments.blank?
|
|
||||||
last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(last_submitter)
|
|
||||||
end
|
|
||||||
|
|
||||||
last_submitter.documents_attachments
|
|
||||||
else
|
else
|
||||||
values_hash = Submissions::GeneratePreviewAttachments.build_values_hash(@submission)
|
build_preview_documents(@submission, merge: is_merge)
|
||||||
|
|
||||||
if @submission.preview_documents.present? &&
|
|
||||||
@submission.preview_documents.all? { |s| s.metadata['values_hash'] == values_hash }
|
|
||||||
@submission.preview_documents
|
|
||||||
else
|
|
||||||
ApplicationRecord.no_touching do
|
|
||||||
@submission.preview_documents.each(&:destroy)
|
|
||||||
end
|
|
||||||
|
|
||||||
Submissions::GeneratePreviewAttachments.call(@submission, values_hash:)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
ActiveRecord::Associations::Preloader.new(
|
ActiveRecord::Associations::Preloader.new(records: documents, associations: [:blob]).call
|
||||||
records: documents,
|
|
||||||
associations: [:blob]
|
|
||||||
).call
|
|
||||||
|
|
||||||
expires_at = Accounts.link_expires_at(current_account)
|
expires_at = Accounts.link_expires_at(current_account)
|
||||||
|
|
||||||
@@ -43,5 +26,50 @@ module Api
|
|||||||
end
|
end
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def build_completed_documents(submission, merge: false)
|
||||||
|
last_submitter = submission.submitters.max_by(&:completed_at)
|
||||||
|
|
||||||
|
if merge
|
||||||
|
if submission.merged_document_attachment.blank?
|
||||||
|
submission.merged_document_attachment =
|
||||||
|
Submissions::GenerateCombinedAttachment.call(last_submitter, with_audit: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
[submission.merged_document_attachment]
|
||||||
|
else
|
||||||
|
if last_submitter.documents_attachments.blank?
|
||||||
|
last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(last_submitter)
|
||||||
|
end
|
||||||
|
|
||||||
|
last_submitter.documents_attachments
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_preview_documents(submission, merge: false)
|
||||||
|
values_hash = Submissions::GeneratePreviewAttachments.build_values_hash(submission)
|
||||||
|
|
||||||
|
if merge
|
||||||
|
if submission.preview_merged_document_attachment.present? &&
|
||||||
|
submission.preview_merged_document_attachment.metadata['values_hash'] == values_hash
|
||||||
|
[submission.preview_merged_document_attachment]
|
||||||
|
else
|
||||||
|
ApplicationRecord.no_touching { submission.preview_merged_document_attachment&.destroy }
|
||||||
|
|
||||||
|
Submissions::GeneratePreviewAttachments.call(submission, values_hash:, merge: true)
|
||||||
|
end
|
||||||
|
elsif submission.preview_documents.present? &&
|
||||||
|
submission.preview_documents.all? { |s| s.metadata['values_hash'] == values_hash }
|
||||||
|
submission.preview_documents
|
||||||
|
else
|
||||||
|
ApplicationRecord.no_touching do
|
||||||
|
submission.preview_documents.each(&:destroy)
|
||||||
|
end
|
||||||
|
|
||||||
|
Submissions::GeneratePreviewAttachments.call(submission, values_hash:)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -54,6 +54,12 @@ module Api
|
|||||||
|
|
||||||
return render json: { error: 'Template not found' }, status: :unprocessable_content if @template.nil?
|
return render json: { error: 'Template not found' }, status: :unprocessable_content if @template.nil?
|
||||||
|
|
||||||
|
if @template.archived_at?
|
||||||
|
Rollbar.warning("Archived template submission: #{@template.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return render json: { error: 'Template has been archived' }, status: :unprocessable_content
|
||||||
|
end
|
||||||
|
|
||||||
if @template.fields.blank?
|
if @template.fields.blank?
|
||||||
Rollbar.warning("Template does not contain fields: #{@template.id}") if defined?(Rollbar)
|
Rollbar.warning("Template does not contain fields: #{@template.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
@@ -156,7 +162,7 @@ module Api
|
|||||||
params:)
|
params:)
|
||||||
else
|
else
|
||||||
submissions_attrs, attachments =
|
submissions_attrs, attachments =
|
||||||
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_params, template)
|
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_params, template, purpose: :api)
|
||||||
|
|
||||||
submissions = Submissions.create_from_submitters(
|
submissions = Submissions.create_from_submitters(
|
||||||
template:,
|
template:,
|
||||||
@@ -172,7 +178,10 @@ module Api
|
|||||||
Submissions::NormalizeParamUtils.save_default_value_attachments!(attachments, submitters)
|
Submissions::NormalizeParamUtils.save_default_value_attachments!(attachments, submitters)
|
||||||
|
|
||||||
submitters.each do |submitter|
|
submitters.each do |submitter|
|
||||||
SubmissionEvents.create_with_tracking_data(submitter, 'api_complete_form', request) if submitter.completed_at?
|
if submitter.completed_at?
|
||||||
|
Submitters::SubmitValues.maybe_invite_via_field(submitter, request)
|
||||||
|
SubmissionEvents.create_with_tracking_data(submitter, 'api_complete_form', request)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
submissions
|
submissions
|
||||||
@@ -182,13 +191,13 @@ 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,
|
||||||
:require_phone_2fa, :expire_at, :name,
|
:require_phone_2fa, :require_email_2fa, :expire_at, :name,
|
||||||
{
|
{
|
||||||
variables: {},
|
variables: {},
|
||||||
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, :order,
|
:require_phone_2fa, :require_email_2fa, :order, :index, :invite_by,
|
||||||
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
|
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
|
||||||
fields: [:name, :uuid, :default_value, :value, :title, :description,
|
fields: [:name, :uuid, :default_value, :value, :title, :description,
|
||||||
:readonly, :required, :validation_pattern, :invalid_message,
|
:readonly, :required, :validation_pattern, :invalid_message,
|
||||||
|
|||||||
@@ -34,18 +34,24 @@ module Api
|
|||||||
render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true, params:)
|
render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true, params:)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# rubocop:disable Metrics/MethodLength
|
||||||
def update
|
def update
|
||||||
if @submitter.completed_at?
|
if @submitter.completed_at?
|
||||||
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_content
|
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if @submitter.declined_at?
|
||||||
|
return render json: { error: 'Submitter has already declined the submission.' }, status: :unprocessable_content
|
||||||
|
end
|
||||||
|
|
||||||
submission = @submitter.submission
|
submission = @submitter.submission
|
||||||
role = submission.template_submitters.find { |e| e['uuid'] == @submitter.uuid }['name']
|
role = submission.template_submitters.find { |e| e['uuid'] == @submitter.uuid }['name']
|
||||||
|
|
||||||
normalized_params, new_attachments = Submissions::NormalizeParamUtils.normalize_submitter_params!(
|
normalized_params, new_attachments = Submissions::NormalizeParamUtils.normalize_submitter_params!(
|
||||||
submitter_params.merge(role:),
|
submitter_params.merge(role:),
|
||||||
@submitter.template || Template.new(submitters: submission.template_submitters, account: @submitter.account),
|
@submitter.template || Template.new(submitters: submission.template_submitters, account: @submitter.account),
|
||||||
for_submitter: @submitter
|
for_submitter: @submitter,
|
||||||
|
purpose: :api
|
||||||
)
|
)
|
||||||
|
|
||||||
Submissions::CreateFromSubmitters.maybe_set_template_fields(submission, [normalized_params],
|
Submissions::CreateFromSubmitters.maybe_set_template_fields(submission, [normalized_params],
|
||||||
@@ -60,7 +66,10 @@ module Api
|
|||||||
|
|
||||||
@submitter.submission.save!
|
@submitter.submission.save!
|
||||||
|
|
||||||
SubmissionEvents.create_with_tracking_data(@submitter, 'api_complete_form', request) if @submitter.completed_at?
|
if @submitter.completed_at?
|
||||||
|
Submitters::SubmitValues.maybe_invite_via_field(@submitter, request)
|
||||||
|
SubmissionEvents.create_with_tracking_data(@submitter, 'api_complete_form', request)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if @submitter.completed_at?
|
if @submitter.completed_at?
|
||||||
@@ -78,13 +87,14 @@ module Api
|
|||||||
|
|
||||||
render json: { error: e.message }, status: :unprocessable_content
|
render json: { error: e.message }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
# rubocop:enable Metrics/MethodLength
|
||||||
|
|
||||||
def submitter_params
|
def submitter_params
|
||||||
submitter_params = params.key?(:submitter) ? params.require(:submitter) : params
|
submitter_params = params.key?(:submitter) ? params.require(:submitter) : params
|
||||||
|
|
||||||
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, :require_phone_2fa,
|
:completed, :phone, :application_key, :external_id, :go_to_last, :require_phone_2fa, :require_email_2fa,
|
||||||
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
||||||
fields: [[:name, :uuid, :default_value, :value, :required,
|
fields: [[:name, :uuid, :default_value, :value, :required,
|
||||||
:readonly, :validation_pattern, :invalid_message,
|
:readonly, :validation_pattern, :invalid_message,
|
||||||
@@ -151,9 +161,7 @@ module Api
|
|||||||
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
|
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
|
||||||
end
|
end
|
||||||
|
|
||||||
submitter.values = submitter.values.transform_values do |v|
|
submitter.values = Submitters::SubmitValues.replace_current_date_placeholders(submitter)
|
||||||
v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
submitter
|
submitter
|
||||||
@@ -195,10 +203,15 @@ 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')
|
if submitter_preferences.key?('require_phone_2fa')
|
||||||
submitter.preferences['require_phone_2fa'] = submitter_preferences['require_phone_2fa']
|
submitter.preferences['require_phone_2fa'] = submitter_preferences['require_phone_2fa']
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if submitter_preferences.key?('require_email_2fa')
|
||||||
|
submitter.preferences['require_email_2fa'] = submitter_preferences['require_email_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
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ module Api
|
|||||||
|
|
||||||
ActiveRecord::Associations::Preloader.new(
|
ActiveRecord::Associations::Preloader.new(
|
||||||
records: [@template],
|
records: [@template],
|
||||||
associations: [schema_documents: :preview_images_attachments]
|
associations: [{ schema_documents: :preview_images_attachments }]
|
||||||
).call
|
).call
|
||||||
|
|
||||||
cloned_template = Templates::Clone.call(
|
cloned_template = Templates::Clone.call(
|
||||||
|
|||||||
@@ -9,20 +9,7 @@ module Api
|
|||||||
|
|
||||||
templates = paginate(templates.preload(:author, folder: :parent_folder))
|
templates = paginate(templates.preload(:author, folder: :parent_folder))
|
||||||
|
|
||||||
schema_documents =
|
schema_documents, dynamic_documents, preview_image_attachments = preload_relations(templates)
|
||||||
ActiveStorage::Attachment.where(record_id: templates.map(&:id),
|
|
||||||
record_type: 'Template',
|
|
||||||
name: :documents,
|
|
||||||
uuid: templates.flat_map { |t| t.schema.pluck('attachment_uuid') })
|
|
||||||
.preload(:blob)
|
|
||||||
|
|
||||||
preview_image_attachments =
|
|
||||||
ActiveStorage::Attachment.joins(:blob)
|
|
||||||
.where(blob: { filename: ['0.png', '0.jpg'] })
|
|
||||||
.where(record_id: schema_documents.map(&:id),
|
|
||||||
record_type: 'ActiveStorage::Attachment',
|
|
||||||
name: :preview_images)
|
|
||||||
.preload(:blob)
|
|
||||||
|
|
||||||
expires_at = Accounts.link_expires_at(current_account)
|
expires_at = Accounts.link_expires_at(current_account)
|
||||||
|
|
||||||
@@ -30,6 +17,7 @@ module Api
|
|||||||
data: templates.map do |t|
|
data: templates.map do |t|
|
||||||
Templates::SerializeForApi.call(t,
|
Templates::SerializeForApi.call(t,
|
||||||
schema_documents: schema_documents.select { |e| e.record_id == t.id },
|
schema_documents: schema_documents.select { |e| e.record_id == t.id },
|
||||||
|
dynamic_documents:,
|
||||||
preview_image_attachments:,
|
preview_image_attachments:,
|
||||||
expires_at:)
|
expires_at:)
|
||||||
end,
|
end,
|
||||||
@@ -69,6 +57,7 @@ module Api
|
|||||||
SearchEntries.enqueue_reindex(@template)
|
SearchEntries.enqueue_reindex(@template)
|
||||||
|
|
||||||
WebhookUrls.enqueue_events(@template, 'template.updated')
|
WebhookUrls.enqueue_events(@template, 'template.updated')
|
||||||
|
WebhookUrls.enqueue_events(@template, 'template.archived') if archived == true
|
||||||
|
|
||||||
render json: @template.as_json(only: %i[id updated_at])
|
render json: @template.as_json(only: %i[id updated_at])
|
||||||
end
|
end
|
||||||
@@ -78,6 +67,8 @@ module Api
|
|||||||
@template.destroy!
|
@template.destroy!
|
||||||
else
|
else
|
||||||
@template.update!(archived_at: Time.current)
|
@template.update!(archived_at: Time.current)
|
||||||
|
|
||||||
|
WebhookUrls.enqueue_events(@template, 'template.archived')
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: @template.as_json(only: %i[id archived_at])
|
render json: @template.as_json(only: %i[id archived_at])
|
||||||
@@ -85,6 +76,41 @@ module Api
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def preload_relations(templates)
|
||||||
|
schema_documents =
|
||||||
|
ActiveStorage::Attachment.where(record_id: templates.map(&:id),
|
||||||
|
record_type: 'Template',
|
||||||
|
name: :documents,
|
||||||
|
uuid: templates.flat_map { |t| t.schema.pluck('attachment_uuid') })
|
||||||
|
.preload(:blob)
|
||||||
|
|
||||||
|
dynamic_document_uuids =
|
||||||
|
templates.flat_map { |t| t.schema.select { |item| item['dynamic'] }.pluck('attachment_uuid') }
|
||||||
|
|
||||||
|
dynamic_documents =
|
||||||
|
if dynamic_document_uuids.present?
|
||||||
|
DynamicDocument.where(template: templates.map(&:id))
|
||||||
|
.where(uuid: dynamic_document_uuids)
|
||||||
|
.preload(current_version: { document_attachment: :blob })
|
||||||
|
.select(:id, :uuid, :template_id, :sha1, :created_at, :updated_at)
|
||||||
|
else
|
||||||
|
DynamicDocument.none
|
||||||
|
end
|
||||||
|
|
||||||
|
preview_attachment_ids =
|
||||||
|
schema_documents.map(&:id) + dynamic_documents.filter_map { |d| d.current_version&.document_attachment&.id }
|
||||||
|
|
||||||
|
preview_image_attachments =
|
||||||
|
ActiveStorage::Attachment.joins(:blob)
|
||||||
|
.where(blob: { filename: ['0.png', '0.jpg'] })
|
||||||
|
.where(record_id: preview_attachment_ids,
|
||||||
|
record_type: 'ActiveStorage::Attachment',
|
||||||
|
name: :preview_images)
|
||||||
|
.preload(:blob)
|
||||||
|
|
||||||
|
[schema_documents, dynamic_documents, preview_image_attachments]
|
||||||
|
end
|
||||||
|
|
||||||
def filter_templates(templates, params)
|
def filter_templates(templates, params)
|
||||||
templates = Templates.search(current_user, 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
|
||||||
@@ -107,7 +133,8 @@ module Api
|
|||||||
:external_id,
|
:external_id,
|
||||||
:shared_link,
|
:shared_link,
|
||||||
{
|
{
|
||||||
submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email order]],
|
submitters: [%i[name uuid is_requester invite_by_uuid invite_via_field_uuid
|
||||||
|
optional_invite_by_uuid linked_to_uuid email order]],
|
||||||
fields: [[:uuid, :submitter_uuid, :name, :type,
|
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||||
:required, :readonly, :default_value,
|
:required, :readonly, :default_value,
|
||||||
:title, :description, :prefillable,
|
:title, :description, :prefillable,
|
||||||
@@ -116,7 +143,7 @@ module Api
|
|||||||
conditions: [%i[field_uuid value action operation]],
|
conditions: [%i[field_uuid value action operation]],
|
||||||
options: [%i[value uuid]],
|
options: [%i[value uuid]],
|
||||||
validation: %i[message pattern min max step],
|
validation: %i[message pattern min max step],
|
||||||
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
|
areas: [%i[uuid x y w h cell_w attachment_uuid option_uuid page]] }]]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ class ApplicationController < ActionController::Base
|
|||||||
BROWSER_LOCALE_REGEXP = /\A\w{2}(?:-\w{2})?/
|
BROWSER_LOCALE_REGEXP = /\A\w{2}(?:-\w{2})?/
|
||||||
|
|
||||||
include ActiveStorage::SetCurrent
|
include ActiveStorage::SetCurrent
|
||||||
include Pagy::Backend
|
include Pagy::Method
|
||||||
|
|
||||||
check_authorization unless: :devise_controller?
|
check_authorization unless: :devise_controller?
|
||||||
|
|
||||||
@@ -17,12 +17,13 @@ class ApplicationController < ActionController::Base
|
|||||||
|
|
||||||
helper_method :button_title,
|
helper_method :button_title,
|
||||||
:current_account,
|
:current_account,
|
||||||
|
:true_ability,
|
||||||
:form_link_host,
|
:form_link_host,
|
||||||
:svg_icon
|
:svg_icon
|
||||||
|
|
||||||
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
|
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
|
||||||
|
|
||||||
rescue_from Pagy::OverflowError do
|
rescue_from Pagy::RangeError do
|
||||||
redirect_to request.path
|
redirect_to request.path
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -41,10 +42,6 @@ class ApplicationController < ActionController::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def default_url_options
|
def default_url_options
|
||||||
if request.domain == 'docuseal.com'
|
|
||||||
return { host: 'docuseal.com', protocol: ENV['FORCE_SSL'].present? ? 'https' : 'http' }
|
|
||||||
end
|
|
||||||
|
|
||||||
Docuseal.default_url_options
|
Docuseal.default_url_options
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -59,7 +56,7 @@ class ApplicationController < ActionController::Base
|
|||||||
|
|
||||||
def pagy_auto(collection, **keyword_args)
|
def pagy_auto(collection, **keyword_args)
|
||||||
if current_ability.can?(:manage, :countless)
|
if current_ability.can?(:manage, :countless)
|
||||||
pagy_countless(collection, **keyword_args)
|
pagy(:countless, collection, **keyword_args)
|
||||||
else
|
else
|
||||||
pagy(collection, **keyword_args)
|
pagy(collection, **keyword_args)
|
||||||
end
|
end
|
||||||
@@ -102,6 +99,10 @@ class ApplicationController < ActionController::Base
|
|||||||
current_user&.account
|
current_user&.account
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def true_ability
|
||||||
|
@true_ability ||= Ability.new(true_user)
|
||||||
|
end
|
||||||
|
|
||||||
def maybe_redirect_to_setup
|
def maybe_redirect_to_setup
|
||||||
redirect_to setup_index_path unless User.exists?
|
redirect_to setup_index_path unless User.exists?
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class ErrorsController < ActionController::Base
|
|||||||
render json: { status: error_status_code, error: }.compact, status: error_status_code
|
render json: { status: error_status_code, error: }.compact, status: error_status_code
|
||||||
end
|
end
|
||||||
|
|
||||||
f.html { render error_status_code.to_s, status: error_status_code }
|
f.any { render error_status_code.to_s, status: error_status_code }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ class ErrorsController < ActionController::Base
|
|||||||
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
|
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
|
||||||
headers['Access-Control-Allow-Headers'] = '*'
|
headers['Access-Control-Allow-Headers'] = '*'
|
||||||
headers['Access-Control-Max-Age'] = '1728000'
|
headers['Access-Control-Max-Age'] = '1728000'
|
||||||
headers['Access-Control-Allow-Credentials'] = true
|
headers['Access-Control-Allow-Credentials'] = 'true'
|
||||||
end
|
end
|
||||||
|
|
||||||
def error_status_code
|
def error_status_code
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
class EsignSettingsController < ApplicationController
|
class EsignSettingsController < ApplicationController
|
||||||
DEFAULT_CERT_NAME = 'DocuSeal Self-Host Autogenerated'
|
DEFAULT_CERT_NAME = 'DocuSeal Self-Host Autogenerated'
|
||||||
|
|
||||||
CertFormRecord = Struct.new(:name, :file, :password, keyword_init: true) do
|
CertFormRecord = Struct.new(:name, :file, :password) do
|
||||||
include ActiveModel::Validations
|
include ActiveModel::Validations
|
||||||
|
|
||||||
def to_key
|
def to_key
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class McpController < ActionController::API
|
||||||
|
before_action :authenticate_user!
|
||||||
|
before_action :verify_mcp_enabled!
|
||||||
|
|
||||||
|
before_action do
|
||||||
|
authorize!(:manage, :mcp)
|
||||||
|
end
|
||||||
|
|
||||||
|
def call
|
||||||
|
return head :ok if request.raw_post.blank?
|
||||||
|
|
||||||
|
body = JSON.parse(request.raw_post)
|
||||||
|
|
||||||
|
result = Mcp::HandleRequest.call(body, current_user, current_ability)
|
||||||
|
|
||||||
|
if result
|
||||||
|
render json: result
|
||||||
|
else
|
||||||
|
head :accepted
|
||||||
|
end
|
||||||
|
rescue CanCan::AccessDenied
|
||||||
|
render json: { jsonrpc: '2.0', id: nil, error: { code: -32_603, message: 'Forbidden' } }, status: :forbidden
|
||||||
|
rescue JSON::ParserError
|
||||||
|
render json: { jsonrpc: '2.0', id: nil, error: { code: -32_700, message: 'Parse error' } }, status: :bad_request
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def authenticate_user!
|
||||||
|
render json: { error: 'Not authenticated' }, status: :unauthorized unless current_user
|
||||||
|
end
|
||||||
|
|
||||||
|
def verify_mcp_enabled!
|
||||||
|
return if Docuseal.multitenant?
|
||||||
|
|
||||||
|
return if AccountConfig.exists?(account_id: current_user.account_id,
|
||||||
|
key: AccountConfig::ENABLE_MCP_KEY,
|
||||||
|
value: true)
|
||||||
|
|
||||||
|
render json: { error: 'MCP is disabled' }, status: :forbidden
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_user
|
||||||
|
@current_user ||= user_from_api_key
|
||||||
|
end
|
||||||
|
|
||||||
|
def user_from_api_key
|
||||||
|
token = request.headers['Authorization'].to_s[/\ABearer\s+(.+)\z/, 1]
|
||||||
|
|
||||||
|
return if token.blank?
|
||||||
|
|
||||||
|
sha256 = Digest::SHA256.hexdigest(token)
|
||||||
|
|
||||||
|
User.joins(:mcp_tokens).active.find_by(mcp_tokens: { sha256:, archived_at: nil })
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class McpSettingsController < ApplicationController
|
||||||
|
load_and_authorize_resource :mcp_token, parent: false
|
||||||
|
|
||||||
|
before_action do
|
||||||
|
authorize!(:manage, :mcp)
|
||||||
|
end
|
||||||
|
|
||||||
|
def index
|
||||||
|
@mcp_tokens = @mcp_tokens.active.order(id: :desc)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@mcp_token = current_user.mcp_tokens.new(mcp_token_params)
|
||||||
|
|
||||||
|
if @mcp_token.save
|
||||||
|
@mcp_tokens = [@mcp_token]
|
||||||
|
|
||||||
|
render :index, status: :created
|
||||||
|
else
|
||||||
|
render turbo_stream: turbo_stream.replace(:modal, template: 'mcp_settings/new'), status: :unprocessable_content
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@mcp_token.update!(archived_at: Time.current)
|
||||||
|
|
||||||
|
redirect_back fallback_location: settings_mcp_index_path, notice: I18n.t('mcp_token_has_been_removed')
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def mcp_token_params
|
||||||
|
params.require(:mcp_token).permit(:name)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -5,6 +5,8 @@ class PasswordsController < Devise::PasswordsController
|
|||||||
skip_before_action :require_no_authentication, only: %i[edit update]
|
skip_before_action :require_no_authentication, only: %i[edit update]
|
||||||
# rubocop:enable Rails/LexicallyScopedActionFilter
|
# rubocop:enable Rails/LexicallyScopedActionFilter
|
||||||
|
|
||||||
|
around_action :with_browser_locale
|
||||||
|
|
||||||
class Current < ActiveSupport::CurrentAttributes
|
class Current < ActiveSupport::CurrentAttributes
|
||||||
attribute :user
|
attribute :user
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,9 +6,17 @@ class PreviewDocumentPageController < ActionController::API
|
|||||||
FORMAT = Templates::ProcessDocument::FORMAT
|
FORMAT = Templates::ProcessDocument::FORMAT
|
||||||
|
|
||||||
def show
|
def show
|
||||||
attachment_uuid = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid], purpose: :attachment)
|
result_data =
|
||||||
|
ApplicationRecord.signed_id_verifier.verified(params[:signed_key], purpose: :attachment)
|
||||||
|
|
||||||
attachment = ActiveStorage::Attachment.find_by(uuid: attachment_uuid) if attachment_uuid
|
attachment =
|
||||||
|
if result_data.is_a?(Array) && result_data.compact_blank.size == 2
|
||||||
|
attachment_id, attachment_uuid = result_data
|
||||||
|
|
||||||
|
ActiveStorage::Attachment.find_by(id: attachment_id, uuid: attachment_uuid)
|
||||||
|
elsif result_data
|
||||||
|
ActiveStorage::Attachment.find_by(uuid: result_data)
|
||||||
|
end
|
||||||
|
|
||||||
return head :not_found unless attachment
|
return head :not_found unless attachment
|
||||||
|
|
||||||
@@ -17,7 +25,10 @@ class PreviewDocumentPageController < ActionController::API
|
|||||||
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"] })
|
||||||
|
|
||||||
return redirect_to preview_image.url, allow_other_host: true if preview_image
|
if preview_image
|
||||||
|
return redirect_to preview_image.url(time: ActiveStorage::Attachment.service_url_time),
|
||||||
|
allow_other_host: true
|
||||||
|
end
|
||||||
|
|
||||||
file_path =
|
file_path =
|
||||||
if attachment.service.name == :disk
|
if attachment.service.name == :disk
|
||||||
@@ -29,11 +40,11 @@ class PreviewDocumentPageController < ActionController::API
|
|||||||
preview_image =
|
preview_image =
|
||||||
Templates::ProcessDocument.generate_pdf_preview_from_file(attachment, file_path, params[:id].to_i)
|
Templates::ProcessDocument.generate_pdf_preview_from_file(attachment, file_path, params[:id].to_i)
|
||||||
|
|
||||||
redirect_to preview_image.url, allow_other_host: true
|
redirect_to preview_image.url(time: ActiveStorage::Attachment.service_url_time), allow_other_host: true
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_or_create_document_tempfile_path(attachment)
|
def find_or_create_document_tempfile_path(attachment)
|
||||||
file_path = "#{Dir.tmpdir}/#{attachment.uuid}"
|
file_path = "#{Dir.tmpdir}/attachment-#{Digest::SHA1.hexdigest("#{attachment.id}-#{attachment.uuid}")}"
|
||||||
|
|
||||||
File.open(file_path, File::RDWR | File::CREAT, 0o644) do |f|
|
File.open(file_path, File::RDWR | File::CREAT, 0o644) do |f|
|
||||||
f.flock(File::LOCK_EX)
|
f.flock(File::LOCK_EX)
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class RevealAccessTokenController < ApplicationController
|
class RevealAccessTokenController < ApplicationController
|
||||||
|
rate_limit to: 4, within: 1.minute, only: %i[create], by: -> { current_user.id }, with: lambda {
|
||||||
|
Rollbar.error('Rate limit api key') if defined?(Rollbar)
|
||||||
|
|
||||||
|
render turbo_stream: turbo_stream.replace(:modal, template: 'reveal_access_token/show',
|
||||||
|
locals: { error_message: I18n.t(:too_many_attempts) }),
|
||||||
|
status: :unprocessable_content
|
||||||
|
}
|
||||||
|
|
||||||
def show
|
def show
|
||||||
authorize!(:manage, current_user.access_token)
|
authorize!(:manage, current_user.access_token)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class SendSubmissionEmailController < ApplicationController
|
|||||||
template = Template.find_by!(slug: params[:template_slug])
|
template = Template.find_by!(slug: params[:template_slug])
|
||||||
|
|
||||||
@submitter =
|
@submitter =
|
||||||
Submitter.completed.where(submission: template.submissions).find_by!(email: params[:email].to_s.downcase)
|
Submitter.completed.where(submission: template.submissions).find_by(email: params[:email].to_s.downcase)
|
||||||
elsif params[:submission_slug]
|
elsif params[:submission_slug]
|
||||||
submission = Submission.find_by(slug: params[:submission_slug])
|
submission = Submission.find_by(slug: params[:submission_slug])
|
||||||
|
|
||||||
@@ -27,9 +27,11 @@ class SendSubmissionEmailController < ApplicationController
|
|||||||
@submitter = Submitter.completed.find_by!(slug: params[:submitter_slug])
|
@submitter = Submitter.completed.find_by!(slug: params[:submitter_slug])
|
||||||
end
|
end
|
||||||
|
|
||||||
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
if @submitter
|
||||||
|
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
||||||
|
|
||||||
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! if can_send?(@submitter)
|
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! if can_send?(@submitter)
|
||||||
|
end
|
||||||
|
|
||||||
respond_to do |f|
|
respond_to do |f|
|
||||||
f.html { render :success }
|
f.html { render :success }
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ class StartFormController < ApplicationController
|
|||||||
COOKIES_DEFAULTS = { httponly: true, secure: Rails.env.production? }.freeze
|
COOKIES_DEFAULTS = { httponly: true, secure: Rails.env.production? }.freeze
|
||||||
|
|
||||||
def show
|
def show
|
||||||
raise ActionController::RoutingError, I18n.t('not_found') if @template.preferences['require_phone_2fa']
|
if @template.preferences['require_phone_2fa'] || @template.preferences['require_email_2fa']
|
||||||
|
raise ActionController::RoutingError, I18n.t('not_found')
|
||||||
|
end
|
||||||
|
|
||||||
if @template.shared_link?
|
if @template.shared_link?
|
||||||
@submitter = @template.submissions.new(account_id: @template.account_id)
|
@submitter = @template.submissions.new(account_id: @template.account_id)
|
||||||
@@ -99,12 +101,23 @@ class StartFormController < ApplicationController
|
|||||||
def load_resubmit_submitter
|
def load_resubmit_submitter
|
||||||
@resubmit_submitter =
|
@resubmit_submitter =
|
||||||
if params[:resubmit].present? && !params[:resubmit].in?([true, 'true'])
|
if params[:resubmit].present? && !params[:resubmit].in?([true, 'true'])
|
||||||
Submitter.find_by(slug: params[:resubmit])
|
submitter = Submitter.find_by(slug: params[:resubmit])
|
||||||
|
|
||||||
|
submitter if submitter && can_resubmit?(submitter)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def can_resubmit?(submitter)
|
||||||
|
submitter.completed_at? && submitter.completed_at > 14.days.ago &&
|
||||||
|
%w[api embed mcp].exclude?(submitter.submission.source) &&
|
||||||
|
submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false
|
||||||
|
end
|
||||||
|
|
||||||
def authorize_start!
|
def authorize_start!
|
||||||
return redirect_to start_form_path(@template.slug) if @template.archived_at?
|
is_archived = @template.archived_at? || @template.account.archived_at?
|
||||||
|
|
||||||
|
return redirect_to submit_form_path(@resubmit_submitter.slug) if @resubmit_submitter && is_archived
|
||||||
|
return redirect_to start_form_path(@template.slug) if is_archived
|
||||||
|
|
||||||
return if @resubmit_submitter
|
return if @resubmit_submitter
|
||||||
return if @template.shared_link? || (current_user && current_ability.can?(:read, @template))
|
return if @template.shared_link? || (current_user && current_ability.can?(:read, @template))
|
||||||
@@ -170,6 +183,8 @@ class StartFormController < ApplicationController
|
|||||||
submitters: [submitter],
|
submitters: [submitter],
|
||||||
source: :link)
|
source: :link)
|
||||||
|
|
||||||
|
Submissions::CreateFromSubmitters.maybe_set_dynamic_documents(submitter.submission)
|
||||||
|
|
||||||
submitter.account_id = submitter.submission.account_id
|
submitter.account_id = submitter.submission.account_id
|
||||||
|
|
||||||
submitter
|
submitter
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ class SubmissionEventsController < ApplicationController
|
|||||||
'api_complete_form' => 'check',
|
'api_complete_form' => 'check',
|
||||||
'send_reminder_email' => 'mail_forward',
|
'send_reminder_email' => 'mail_forward',
|
||||||
'send_2fa_sms' => '2fa',
|
'send_2fa_sms' => '2fa',
|
||||||
|
'send_2fa_email' => '2fa',
|
||||||
'send_sms' => 'send',
|
'send_sms' => 'send',
|
||||||
'phone_verified' => 'phone_check',
|
'phone_verified' => 'phone_check',
|
||||||
'email_verified' => 'email_check',
|
'email_verified' => 'email_check',
|
||||||
'click_sms' => 'hand_click',
|
'click_sms' => 'hand_click',
|
||||||
'decline_form' => 'x',
|
'decline_form' => 'x',
|
||||||
|
'delegate_form' => 'user_share',
|
||||||
'start_verification' => 'player_play',
|
'start_verification' => 'player_play',
|
||||||
'complete_verification' => 'check',
|
'complete_verification' => 'check',
|
||||||
'invite_party' => 'user_plus'
|
'invite_party' => 'user_plus'
|
||||||
|
|||||||
@@ -12,13 +12,21 @@ class SubmissionsController < ApplicationController
|
|||||||
authorize!(:create, Submission)
|
authorize!(:create, Submission)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
FIELD_ICONS = {
|
||||||
|
'text' => 'text_size', 'signature' => 'writing_sign', 'date' => 'calendar_event',
|
||||||
|
'number' => 'square_number_1', 'image' => 'photo', 'initials' => 'letter_case_upper',
|
||||||
|
'file' => 'paperclip', 'select' => 'select', 'checkbox' => 'checkbox', 'radio' => 'circle_dot',
|
||||||
|
'stamp' => 'rubber_stamp', 'cells' => 'columns_3', 'multiple' => 'checks', 'phone' => 'phone_check',
|
||||||
|
'payment' => 'credit_card', 'verification' => 'id', 'kba' => 'user_scan'
|
||||||
|
}.freeze
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@submission = Submissions.preload_with_pages(@submission)
|
@submission = Submissions.preload_with_pages(@submission)
|
||||||
|
|
||||||
unless @submission.submitters.all?(&:completed_at?)
|
unless @submission.submitters.all?(&:completed_at?)
|
||||||
ActiveRecord::Associations::Preloader.new(
|
ActiveRecord::Associations::Preloader.new(
|
||||||
records: [@submission],
|
records: [@submission],
|
||||||
associations: [submitters: :start_form_submission_events]
|
associations: [{ submitters: :start_form_submission_events }]
|
||||||
).call
|
).call
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -30,6 +38,8 @@ class SubmissionsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
return redirect_to template_path(@template), alert: I18n.t('template_has_been_archived') if @template.archived_at?
|
||||||
|
|
||||||
save_template_message(@template, params) if params[:save_message] == '1'
|
save_template_message(@template, params) if params[:save_message] == '1'
|
||||||
|
|
||||||
[params.delete(:subject), params.delete(:body)] if params[:is_custom_message] != '1'
|
[params.delete(:subject), params.delete(:body)] if params[:is_custom_message] != '1'
|
||||||
@@ -43,17 +53,7 @@ class SubmissionsController < ApplicationController
|
|||||||
emails: params[:emails],
|
emails: params[:emails],
|
||||||
params: params.merge('send_completed_email' => true))
|
params: params.merge('send_completed_email' => true))
|
||||||
else
|
else
|
||||||
submissions_attrs = submissions_params[:submission].to_h.values
|
create_submissions(@template, submissions_params, params)
|
||||||
|
|
||||||
submissions_attrs, =
|
|
||||||
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_attrs, @template)
|
|
||||||
|
|
||||||
Submissions.create_from_submitters(template: @template,
|
|
||||||
user: current_user,
|
|
||||||
source: :invite,
|
|
||||||
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
|
|
||||||
submissions_attrs:,
|
|
||||||
params: params.merge('send_completed_email' => true))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
WebhookUrls.enqueue_events(submissions, 'submission.created')
|
WebhookUrls.enqueue_events(submissions, 'submission.created')
|
||||||
@@ -88,6 +88,21 @@ class SubmissionsController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def create_submissions(template, submissions_params, params)
|
||||||
|
submissions_attrs = submissions_params[:submission].to_h.values
|
||||||
|
|
||||||
|
submissions_attrs, _, new_fields =
|
||||||
|
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_attrs, template, add_fields: true)
|
||||||
|
|
||||||
|
Submissions.create_from_submitters(template: template,
|
||||||
|
user: current_user,
|
||||||
|
source: :invite,
|
||||||
|
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
|
||||||
|
submissions_attrs:,
|
||||||
|
new_fields:,
|
||||||
|
params: params.merge('send_completed_email' => true))
|
||||||
|
end
|
||||||
|
|
||||||
def save_template_message(template, params)
|
def save_template_message(template, params)
|
||||||
template.preferences['request_email_subject'] = params[:subject] if params[:subject].present?
|
template.preferences['request_email_subject'] = params[:subject] if params[:subject].present?
|
||||||
template.preferences['request_email_body'] = params[:body] if params[:body].present?
|
template.preferences['request_email_body'] = params[:body] if params[:body].present?
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class SubmissionsDebugController < ApplicationController
|
|||||||
def index
|
def index
|
||||||
@submitter = Submitter.preload({ attachments_attachments: :blob },
|
@submitter = Submitter.preload({ attachments_attachments: :blob },
|
||||||
submission: { template: { documents_attachments: :blob } })
|
submission: { template: { documents_attachments: :blob } })
|
||||||
.find_by(slug: params[:submitter_slug])
|
.find_by(slug: params[:submit_form_slug])
|
||||||
|
|
||||||
respond_to do |f|
|
respond_to do |f|
|
||||||
f.html do
|
f.html do
|
||||||
|
|||||||
@@ -1,40 +1,17 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class SubmissionsDownloadController < ApplicationController
|
class SubmissionsDownloadController < ApplicationController
|
||||||
skip_before_action :authenticate_user!
|
load_and_authorize_resource :submission
|
||||||
skip_authorization_check
|
|
||||||
|
|
||||||
TTL = 40.minutes
|
|
||||||
FILES_TTL = 5.minutes
|
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
last_submitter = @submission.submitters.where.not(completed_at: nil).order(:completed_at).last
|
||||||
|
|
||||||
signature_valid =
|
|
||||||
if @submitter&.slug == params[:submitter_slug]
|
|
||||||
true
|
|
||||||
else
|
|
||||||
@submitter = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
@submitter ||= Submitter.find_by!(slug: params[:submitter_slug])
|
|
||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(@submitter)
|
|
||||||
|
|
||||||
last_submitter = @submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
|
|
||||||
|
|
||||||
return head :not_found unless last_submitter
|
return head :not_found unless last_submitter
|
||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(last_submitter)
|
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||||
|
|
||||||
if last_submitter.completed_at < TTL.ago && !signature_valid && !current_user_submitter?(last_submitter)
|
|
||||||
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
|
||||||
|
|
||||||
return head :not_found
|
|
||||||
end
|
|
||||||
|
|
||||||
if params[:combined] == 'true'
|
if params[:combined] == 'true'
|
||||||
url = build_combined_url(@submitter)
|
url = Submitters.build_combined_url(last_submitter)
|
||||||
|
|
||||||
if url
|
if url
|
||||||
render json: [url]
|
render json: [url]
|
||||||
@@ -42,43 +19,7 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
head :not_found
|
head :not_found
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
render json: build_urls(last_submitter)
|
render json: Submitters.build_document_urls(last_submitter)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def current_user_submitter?(submitter)
|
|
||||||
current_user && current_user.account.submitters.exists?(id: submitter.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_urls(submitter)
|
|
||||||
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
|
|
||||||
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
|
|
||||||
|
|
||||||
Submitters.select_attachments_for_download(submitter).map do |attachment|
|
|
||||||
ActiveStorage::Blob.proxy_url(
|
|
||||||
attachment.blob,
|
|
||||||
expires_at: FILES_TTL.from_now.to_i,
|
|
||||||
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_combined_url(submitter)
|
|
||||||
return if submitter.submission.submitters.exists?(completed_at: nil)
|
|
||||||
return if submitter.submission.submitters.order(:completed_at).last != submitter
|
|
||||||
|
|
||||||
attachment = submitter.submission.combined_document_attachment
|
|
||||||
attachment ||= Submissions::EnsureCombinedGenerated.call(submitter)
|
|
||||||
|
|
||||||
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
|
|
||||||
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
|
|
||||||
|
|
||||||
ActiveStorage::Blob.proxy_url(
|
|
||||||
attachment.blob,
|
|
||||||
expires_at: FILES_TTL.from_now.to_i,
|
|
||||||
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
@@ -5,10 +5,11 @@ class SubmissionsExportController < ApplicationController
|
|||||||
load_and_authorize_resource :submission, through: :template, parent: false, only: :index
|
load_and_authorize_resource :submission, through: :template, parent: false, only: :index
|
||||||
|
|
||||||
def index
|
def index
|
||||||
submissions = @submissions.active
|
submissions = params[:archived] == 'true' ? @submissions.archived : @submissions.active
|
||||||
.preload(submitters: { documents_attachments: :blob,
|
|
||||||
attachments_attachments: :blob })
|
submissions = submissions.preload(submitters: { documents_attachments: :blob,
|
||||||
.order(id: :asc)
|
attachments_attachments: :blob })
|
||||||
|
.order(id: :asc)
|
||||||
|
|
||||||
submissions = Submissions.search(current_user, submissions, params[:q], search_values: true)
|
submissions = Submissions.search(current_user, submissions, params[:q], search_values: true)
|
||||||
submissions = Submissions::Filter.call(submissions, current_user, params)
|
submissions = Submissions::Filter.call(submissions, current_user, params)
|
||||||
|
|||||||
@@ -10,13 +10,15 @@ class SubmissionsPreviewController < ApplicationController
|
|||||||
TTL = 40.minutes
|
TTL = 40.minutes
|
||||||
|
|
||||||
def show
|
def show
|
||||||
submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
@sig_submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
||||||
|
|
||||||
signature_valid =
|
signature_valid =
|
||||||
if submitter && submitter.submission.slug == params[:slug]
|
if @sig_submitter && @sig_submitter.submission.slug == params[:slug]
|
||||||
@submission = submitter.submission
|
@submission = @sig_submitter.submission
|
||||||
|
|
||||||
true
|
true
|
||||||
|
else
|
||||||
|
@sig_submitter = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
@submission ||= Submission.find_by!(slug: params[:slug])
|
@submission ||= Submission.find_by!(slug: params[:slug])
|
||||||
@@ -36,7 +38,7 @@ class SubmissionsPreviewController < ApplicationController
|
|||||||
|
|
||||||
@submission = Submissions.preload_with_pages(@submission)
|
@submission = Submissions.preload_with_pages(@submission)
|
||||||
|
|
||||||
render 'submissions/show', layout: 'plain'
|
render 'submissions/show', layout: 'plain', locals: { is_preview: true }
|
||||||
end
|
end
|
||||||
|
|
||||||
def completed
|
def completed
|
||||||
@@ -53,8 +55,11 @@ class SubmissionsPreviewController < ApplicationController
|
|||||||
|
|
||||||
def use_signature?(submission)
|
def use_signature?(submission)
|
||||||
return false if current_user && can?(:read, submission)
|
return false if current_user && can?(:read, submission)
|
||||||
return true if submission.submitters.any? { |e| e.preferences['require_phone_2fa'] }
|
return true if submission.submitters.any? do |e|
|
||||||
|
e.preferences['require_phone_2fa'] || e.preferences['require_email_2fa']
|
||||||
|
end
|
||||||
return true if submission.template&.preferences&.dig('require_phone_2fa')
|
return true if submission.template&.preferences&.dig('require_phone_2fa')
|
||||||
|
return true if submission.template&.preferences&.dig('require_email_2fa')
|
||||||
|
|
||||||
!submission_valid_ttl?(submission)
|
!submission_valid_ttl?(submission)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmissionsPreviewDownloadController < ApplicationController
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
TTL = 40.minutes
|
||||||
|
|
||||||
|
def index
|
||||||
|
@submission = Submission.find_by!(slug: params[:submission_slug] || params[:submissions_preview_slug])
|
||||||
|
|
||||||
|
last_submitter = @submission.submitters.where.not(completed_at: nil).order(:completed_at).last
|
||||||
|
|
||||||
|
return head :not_found unless last_submitter
|
||||||
|
|
||||||
|
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||||
|
|
||||||
|
unless current_user_submission?(@submission)
|
||||||
|
if use_2fa?(@submission)
|
||||||
|
Rollbar.info("2FA download error: #{last_submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return head :not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
if last_submitter.completed_at < TTL.ago
|
||||||
|
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return head :not_found
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if params[:combined] == 'true'
|
||||||
|
respond_with_combined(last_submitter)
|
||||||
|
else
|
||||||
|
render json: Submitters.build_document_urls(last_submitter)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def respond_with_combined(submitter)
|
||||||
|
url = Submitters.build_combined_url(submitter)
|
||||||
|
|
||||||
|
if url
|
||||||
|
render json: [url]
|
||||||
|
else
|
||||||
|
head :not_found
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_user_submission?(submission)
|
||||||
|
current_user && current_ability.can?(:read, submission)
|
||||||
|
end
|
||||||
|
|
||||||
|
def use_2fa?(submission)
|
||||||
|
return true if submission.submitters.any? do |e|
|
||||||
|
e.preferences['require_phone_2fa'] || e.preferences['require_email_2fa']
|
||||||
|
end
|
||||||
|
return true if submission.template&.preferences&.dig('require_phone_2fa')
|
||||||
|
return true if submission.template&.preferences&.dig('require_email_2fa')
|
||||||
|
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmissionsResendEmailController < ApplicationController
|
||||||
|
load_and_authorize_resource :submission
|
||||||
|
|
||||||
|
before_action do
|
||||||
|
authorize!(:manage, :resend_all)
|
||||||
|
authorize!(:update, @submission)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
submitters = @submission.submitters.reject(&:completed_at?).select { |s| s.email.present? && !s.declined_at? }
|
||||||
|
|
||||||
|
if Docuseal.multitenant?
|
||||||
|
recent_submitter_ids =
|
||||||
|
SubmissionEvent.where(submitter_id: submitters.map(&:id),
|
||||||
|
event_type: 'send_email',
|
||||||
|
created_at: 10.hours.ago..Time.current).pluck(:submitter_id).to_set
|
||||||
|
|
||||||
|
submitters = submitters.reject { |s| recent_submitter_ids.include?(s.id) }
|
||||||
|
end
|
||||||
|
|
||||||
|
submitters.each do |submitter|
|
||||||
|
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => submitter.id)
|
||||||
|
|
||||||
|
submitter.sent_at ||= Time.current
|
||||||
|
submitter.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
notice =
|
||||||
|
if submitters.empty?
|
||||||
|
I18n.t('email_has_been_sent_already')
|
||||||
|
else
|
||||||
|
I18n.t('emails_have_been_sent_to_n_recipients', count: submitters.size)
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect_back(fallback_location: submission_path(@submission), notice:)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -4,6 +4,8 @@ class SubmissionsUnarchiveController < ApplicationController
|
|||||||
load_and_authorize_resource :submission
|
load_and_authorize_resource :submission
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
authorize!(:update, @submission)
|
||||||
|
|
||||||
@submission.update!(archived_at: nil)
|
@submission.update!(archived_at: nil)
|
||||||
|
|
||||||
redirect_to submission_path(@submission), notice: I18n.t('submission_has_been_unarchived')
|
redirect_to submission_path(@submission), notice: I18n.t('submission_has_been_unarchived')
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmitFormCompletedDownloadController < ApplicationController
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
TTL = 40.minutes
|
||||||
|
FILES_TTL = 5.minutes
|
||||||
|
|
||||||
|
def index
|
||||||
|
@submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
||||||
|
|
||||||
|
signature_valid =
|
||||||
|
if @submitter&.slug == submitter_slug
|
||||||
|
true
|
||||||
|
else
|
||||||
|
@submitter = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
@submitter ||= Submitter.find_by!(slug: submitter_slug)
|
||||||
|
|
||||||
|
Submissions::EnsureResultGenerated.call(@submitter)
|
||||||
|
|
||||||
|
last_submitter = @submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
|
||||||
|
|
||||||
|
return head :not_found unless last_submitter
|
||||||
|
|
||||||
|
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||||
|
|
||||||
|
if !signature_valid && !current_user_submitter?(last_submitter)
|
||||||
|
unless Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||||
|
Rollbar.info("2FA download error: #{last_submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return head :not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
if last_submitter.completed_at < TTL.ago
|
||||||
|
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return head :not_found
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if params[:combined] == 'true'
|
||||||
|
respond_with_combined(last_submitter)
|
||||||
|
else
|
||||||
|
render json: Submitters.build_document_urls(last_submitter)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def submitter_slug
|
||||||
|
params[:submit_form_slug] || params[:submitter_slug] || params[:submitter_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
def respond_with_combined(submitter)
|
||||||
|
url = Submitters.build_combined_url(submitter)
|
||||||
|
|
||||||
|
if url
|
||||||
|
render json: [url]
|
||||||
|
else
|
||||||
|
head :not_found
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_user_submitter?(submitter)
|
||||||
|
current_user && current_ability.can?(:read, submitter)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -3,19 +3,21 @@
|
|||||||
class SubmitFormController < ApplicationController
|
class SubmitFormController < ApplicationController
|
||||||
layout 'form'
|
layout 'form'
|
||||||
|
|
||||||
around_action :with_browser_locale, only: %i[show completed success]
|
around_action :with_browser_locale, only: %i[show completed success delegated]
|
||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
before_action :load_submitter, only: %i[show update completed]
|
before_action :load_submitter, only: %i[show update completed]
|
||||||
|
before_action :maybe_redirect_delegated, only: %i[show completed]
|
||||||
before_action :maybe_render_locked_page, only: :show
|
before_action :maybe_render_locked_page, only: :show
|
||||||
before_action :maybe_require_link_2fa, only: %i[show update]
|
before_action :maybe_require_link_2fa, only: %i[show]
|
||||||
|
|
||||||
CONFIG_KEYS = [].freeze
|
CONFIG_KEYS = [].freeze
|
||||||
|
|
||||||
def show
|
def show
|
||||||
submission = @submitter.submission
|
submission = @submitter.submission
|
||||||
|
|
||||||
|
return render :email_2fa unless Submitters::AuthorizedForForm.pass_email_2fa?(@submitter, request)
|
||||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||||
|
|
||||||
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
|
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
|
||||||
@@ -47,6 +49,11 @@ class SubmitFormController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
unless Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||||
|
return render json: { error: I18n.t('verification_required_refresh_the_page_and_pass_2fa') },
|
||||||
|
status: :unprocessable_content
|
||||||
|
end
|
||||||
|
|
||||||
if @submitter.completed_at?
|
if @submitter.completed_at?
|
||||||
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_content
|
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
@@ -72,22 +79,31 @@ class SubmitFormController < ApplicationController
|
|||||||
|
|
||||||
render json: { field_uuid: e.message }, status: :unprocessable_content
|
render json: { field_uuid: e.message }, status: :unprocessable_content
|
||||||
rescue Submitters::SubmitValues::ValidationError => e
|
rescue Submitters::SubmitValues::ValidationError => e
|
||||||
|
Rollbar.warning("Validation error #{@submitter.id}: #{e.message}") if defined?(Rollbar)
|
||||||
|
|
||||||
render json: { error: e.message }, status: :unprocessable_content
|
render json: { error: e.message }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
def completed
|
def completed
|
||||||
raise ActionController::RoutingError, I18n.t('not_found') if @submitter.account.archived_at?
|
raise ActionController::RoutingError, I18n.t('not_found') if @submitter.account.archived_at?
|
||||||
|
|
||||||
|
return if Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||||
|
|
||||||
|
redirect_to submit_form_path(params[:submit_form_slug])
|
||||||
end
|
end
|
||||||
|
|
||||||
def success; end
|
def success; end
|
||||||
|
|
||||||
|
def delegated
|
||||||
|
submitter_version = SubmitterVersion.find_by!(slug: params[:slug] || params[:submit_form_slug])
|
||||||
|
|
||||||
|
@submitter = submitter_version.submitter
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def maybe_require_link_2fa
|
def maybe_require_link_2fa
|
||||||
return if @submitter.submission.source != 'link'
|
return if Submitters::AuthorizedForForm.pass_link_2fa?(@submitter, current_user, request)
|
||||||
return unless @submitter.submission.template&.preferences&.dig('shared_link_2fa') == true
|
|
||||||
return if cookies.encrypted[:email_2fa_slug] == @submitter.slug
|
|
||||||
return if @submitter.email == current_user&.email && current_user&.account_id == @submitter.account_id
|
|
||||||
|
|
||||||
redirect_to start_form_path(@submitter.submission.template.slug)
|
redirect_to start_form_path(@submitter.submission.template.slug)
|
||||||
end
|
end
|
||||||
@@ -101,8 +117,18 @@ class SubmitFormController < ApplicationController
|
|||||||
render :declined if @submitter.declined_at?
|
render :declined if @submitter.declined_at?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def maybe_redirect_delegated
|
||||||
|
return if @submitter
|
||||||
|
|
||||||
|
submitter_version = SubmitterVersion.find_by!(slug: params[:slug] || params[:submit_form_slug])
|
||||||
|
|
||||||
|
submitter_version.submitter.submission_events.find_by!(event_type: :delegate_form)
|
||||||
|
|
||||||
|
redirect_to submit_form_delegated_path(submitter_version.slug)
|
||||||
|
end
|
||||||
|
|
||||||
def load_submitter
|
def load_submitter
|
||||||
@submitter = Submitter.find_by!(slug: params[:slug] || params[:submit_form_slug])
|
@submitter = Submitter.find_by(slug: params[:slug] || params[:submit_form_slug])
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_attachments_index(submission)
|
def build_attachments_index(submission)
|
||||||
|
|||||||
@@ -4,29 +4,43 @@ class SubmitFormDeclineController < ApplicationController
|
|||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
def create
|
before_action :load_submitter
|
||||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
|
||||||
|
|
||||||
return redirect_to submit_form_path(submitter.slug) if submitter.declined_at? ||
|
def create
|
||||||
submitter.completed_at? ||
|
return redirect_to submit_form_path(@submitter.slug) if declining_disabled? ||
|
||||||
submitter.submission.archived_at? ||
|
@submitter.declined_at? ||
|
||||||
submitter.submission.expired? ||
|
@submitter.completed_at? ||
|
||||||
submitter.submission.template&.archived_at?
|
@submitter.submission.archived_at? ||
|
||||||
|
@submitter.submission.expired? ||
|
||||||
|
@submitter.submission.template&.archived_at? ||
|
||||||
|
!Submitters::AuthorizedForForm.call(@submitter,
|
||||||
|
current_user,
|
||||||
|
request)
|
||||||
|
|
||||||
ApplicationRecord.transaction do
|
ApplicationRecord.transaction do
|
||||||
submitter.update!(declined_at: Time.current)
|
@submitter.update!(declined_at: Time.current)
|
||||||
|
|
||||||
SubmissionEvents.create_with_tracking_data(submitter, 'decline_form', request, { reason: params[:reason] })
|
SubmissionEvents.create_with_tracking_data(@submitter, 'decline_form', request, { reason: params[:reason] })
|
||||||
end
|
end
|
||||||
|
|
||||||
user = submitter.submission.created_by_user || submitter.template.author
|
user = @submitter.submission.created_by_user || @submitter.template.author
|
||||||
|
|
||||||
if user.user_configs.find_by(key: UserConfig::RECEIVE_DECLINED_EMAIL)&.value != false
|
if user.user_configs.find_by(key: UserConfig::RECEIVE_DECLINED_EMAIL)&.value != false
|
||||||
SubmitterMailer.declined_email(submitter, user).deliver_later!
|
SubmitterMailer.declined_email(@submitter, user).deliver_later!
|
||||||
end
|
end
|
||||||
|
|
||||||
WebhookUrls.enqueue_events(submitter, 'form.declined')
|
WebhookUrls.enqueue_events(@submitter, 'form.declined')
|
||||||
|
|
||||||
redirect_to submit_form_path(submitter.slug)
|
redirect_to submit_form_path(@submitter.slug)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def declining_disabled?
|
||||||
|
@submitter.account.account_configs.find_by(key: AccountConfig::ALLOW_TO_DECLINE_KEY)&.value == false
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_submitter
|
||||||
|
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmitFormDelegateController < ApplicationController
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
before_action :load_submitter
|
||||||
|
|
||||||
|
def create
|
||||||
|
return redirect_to submit_form_path(@submitter.slug) if @submitter.declined_at? ||
|
||||||
|
@submitter.completed_at? ||
|
||||||
|
@submitter.submission.archived_at? ||
|
||||||
|
@submitter.submission.expired? ||
|
||||||
|
@submitter.submission.template&.archived_at? ||
|
||||||
|
!Submitters::AuthorizedForForm.call(@submitter,
|
||||||
|
current_user,
|
||||||
|
request)
|
||||||
|
|
||||||
|
@submitter.account.account_configs.find_by!(key: AccountConfig::ALLOW_TO_DELEGATE_KEY, value: true)
|
||||||
|
|
||||||
|
email = Submissions.normalize_email(params[:email])
|
||||||
|
|
||||||
|
return redirect_to submit_form_path(@submitter.slug) if email.blank?
|
||||||
|
|
||||||
|
old_slug = @submitter.slug
|
||||||
|
|
||||||
|
ApplicationRecord.transaction do
|
||||||
|
@submitter.submitter_versions.create!(slug: old_slug, email: @submitter.email,
|
||||||
|
name: @submitter.name, phone: @submitter.phone)
|
||||||
|
|
||||||
|
SubmissionEvents.create_with_tracking_data(@submitter, 'delegate_form', request,
|
||||||
|
{ old_email: @submitter.email, email: })
|
||||||
|
|
||||||
|
@submitter.update!(email:, phone: nil, name: nil, slug: SecureRandom.base58(14))
|
||||||
|
end
|
||||||
|
|
||||||
|
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => @submitter.id)
|
||||||
|
|
||||||
|
redirect_to submit_form_delegated_path(old_slug)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_submitter
|
||||||
|
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -9,7 +9,7 @@ class SubmitFormDownloadController < ApplicationController
|
|||||||
def index
|
def index
|
||||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||||
|
|
||||||
return redirect_to submitter_download_index_path(@submitter.slug) if @submitter.completed_at?
|
return redirect_to submit_form_documents_path(@submitter.slug) if @submitter.completed_at?
|
||||||
|
|
||||||
return head :unprocessable_content if @submitter.declined_at? ||
|
return head :unprocessable_content if @submitter.declined_at? ||
|
||||||
@submitter.submission.archived_at? ||
|
@submitter.submission.archived_at? ||
|
||||||
@@ -17,7 +17,8 @@ class SubmitFormDownloadController < ApplicationController
|
|||||||
@submitter.submission.template&.archived_at? ||
|
@submitter.submission.template&.archived_at? ||
|
||||||
AccountConfig.exists?(account_id: @submitter.account_id,
|
AccountConfig.exists?(account_id: @submitter.account_id,
|
||||||
key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
|
key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
|
||||||
value: false)
|
value: false) ||
|
||||||
|
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||||
|
|
||||||
last_completed_submitter = @submitter.submission.submitters
|
last_completed_submitter = @submitter.submission.submitters
|
||||||
.where.not(id: @submitter.id)
|
.where.not(id: @submitter.id)
|
||||||
@@ -32,7 +33,7 @@ class SubmitFormDownloadController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
urls = attachments.map do |attachment|
|
urls = attachments.map do |attachment|
|
||||||
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
|
ActiveStorage::Blob.proxy_path(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: urls
|
render json: urls
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ class SubmitFormDrawSignatureController < ApplicationController
|
|||||||
|
|
||||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||||
|
|
||||||
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at?
|
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at? ||
|
||||||
|
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||||
return redirect_to submit_form_path(@submitter.slug)
|
return redirect_to submit_form_path(@submitter.slug)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmitFormEmail2fasController < ApplicationController
|
||||||
|
around_action :with_browser_locale
|
||||||
|
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
before_action :load_submitter
|
||||||
|
|
||||||
|
COOKIES_TTL = 12.hours
|
||||||
|
COOKIES_DEFAULTS = { httponly: true, secure: Rails.env.production? }.freeze
|
||||||
|
|
||||||
|
def create
|
||||||
|
RateLimit.call("verify-2fa-code-#{@submitter.id}", limit: 2, ttl: 45.seconds, enabled: true)
|
||||||
|
|
||||||
|
value = [@submitter.email.downcase.strip, @submitter.slug].join(':')
|
||||||
|
|
||||||
|
if EmailVerificationCodes.verify(params[:one_time_code].to_s.gsub(/\D/, ''), value)
|
||||||
|
SubmissionEvents.create_with_tracking_data(@submitter, 'email_verified', request, { email: @submitter.email })
|
||||||
|
|
||||||
|
cookies.encrypted[:email_2fa_slug] =
|
||||||
|
{ value: @submitter.slug, expires: COOKIES_TTL.from_now, **COOKIES_DEFAULTS }
|
||||||
|
|
||||||
|
redirect_to submit_form_path(@submitter.slug)
|
||||||
|
else
|
||||||
|
redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:invalid_code)
|
||||||
|
end
|
||||||
|
rescue RateLimit::LimitApproached
|
||||||
|
redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:too_many_attempts)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
if @submitter.submission_events.where(event_type: 'send_2fa_email').exists?(created_at: 15.seconds.ago..)
|
||||||
|
return redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:rate_limit_exceeded)
|
||||||
|
end
|
||||||
|
|
||||||
|
RateLimit.call("send-email-code-#{@submitter.id}", limit: 2, ttl: 45.seconds, enabled: true)
|
||||||
|
|
||||||
|
SendSubmitterVerificationEmailJob.perform_async('submitter_id' => @submitter.id, 'locale' => I18n.locale.to_s)
|
||||||
|
|
||||||
|
redir_params = params[:resend] ? { alert: I18n.t(:code_has_been_resent) } : {}
|
||||||
|
|
||||||
|
redirect_to submit_form_path(@submitter.slug, status: :sent), **redir_params
|
||||||
|
rescue RateLimit::LimitApproached
|
||||||
|
redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:too_many_attempts)
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_submitter
|
||||||
|
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -5,12 +5,12 @@ class SubmitFormInviteController < ApplicationController
|
|||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
def create
|
def create
|
||||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||||
|
|
||||||
return head :unprocessable_content unless can_invite?(submitter)
|
return head :unprocessable_content unless can_invite?(@submitter)
|
||||||
|
|
||||||
invite_submitters = filter_invite_submitters(submitter, 'invite_by_uuid')
|
invite_submitters = filter_invite_submitters(@submitter, 'invite_by_uuid')
|
||||||
optional_invite_submitters = filter_invite_submitters(submitter, 'optional_invite_by_uuid')
|
optional_invite_submitters = filter_invite_submitters(@submitter, 'optional_invite_by_uuid')
|
||||||
|
|
||||||
ApplicationRecord.transaction do
|
ApplicationRecord.transaction do
|
||||||
(invite_submitters + optional_invite_submitters).each do |item|
|
(invite_submitters + optional_invite_submitters).each do |item|
|
||||||
@@ -19,18 +19,20 @@ class SubmitFormInviteController < ApplicationController
|
|||||||
next unless attrs
|
next unless attrs
|
||||||
next if attrs[:email].blank?
|
next if attrs[:email].blank?
|
||||||
|
|
||||||
submitter.submission.submitters.create!(**attrs, account_id: submitter.account_id)
|
email = Submissions.normalize_email(attrs[:email])
|
||||||
|
|
||||||
SubmissionEvents.create_with_tracking_data(submitter, 'invite_party', request, { uuid: submitter.uuid })
|
@submitter.submission.submitters.create!(uuid: attrs[:uuid], email:, account_id: @submitter.account_id)
|
||||||
|
|
||||||
|
SubmissionEvents.create_with_tracking_data(@submitter, 'invite_party', request, { uuid: @submitter.uuid })
|
||||||
end
|
end
|
||||||
|
|
||||||
submitter.submission.update!(submitters_order: :preserved)
|
@submitter.submission.update!(submitters_order: :preserved)
|
||||||
end
|
end
|
||||||
|
|
||||||
submitter.submission.submitters.reload
|
@submitter.submission.submitters.reload
|
||||||
|
|
||||||
if invite_submitters.all? { |s| submitter.submission.submitters.any? { |e| e.uuid == s['uuid'] } }
|
if invite_submitters.all? { |s| @submitter.submission.submitters.any? { |e| e.uuid == s['uuid'] } }
|
||||||
Submitters::SubmitValues.call(submitter, ActionController::Parameters.new(completed: 'true'), request)
|
Submitters::SubmitValues.call(@submitter, ActionController::Parameters.new(completed: 'true'), request)
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
else
|
else
|
||||||
@@ -45,7 +47,8 @@ class SubmitFormInviteController < ApplicationController
|
|||||||
!submitter.completed_at? &&
|
!submitter.completed_at? &&
|
||||||
!submitter.submission.archived_at? &&
|
!submitter.submission.archived_at? &&
|
||||||
!submitter.submission.expired? &&
|
!submitter.submission.expired? &&
|
||||||
!submitter.submission.template&.archived_at?
|
!submitter.submission.template&.archived_at? &&
|
||||||
|
Submitters::AuthorizedForForm.call(submitter, current_user, request)
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_invite_submitters(submitter, key = 'invite_by_uuid')
|
def filter_invite_submitters(submitter, key = 'invite_by_uuid')
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmitFormMetadataController < ApplicationController
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
def index
|
||||||
|
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||||
|
|
||||||
|
return head :not_found if @submitter.declined_at? ||
|
||||||
|
@submitter.completed_at? ||
|
||||||
|
@submitter.submission.archived_at? ||
|
||||||
|
@submitter.submission.expired? ||
|
||||||
|
@submitter.submission.template&.archived_at? ||
|
||||||
|
@submitter.account.archived_at? ||
|
||||||
|
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||||
|
|
||||||
|
submission = @submitter.submission
|
||||||
|
values = submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) }
|
||||||
|
schema = Submissions.filtered_conditions_schema(submission, values:, include_submitter_uuid: @submitter.uuid)
|
||||||
|
|
||||||
|
documents = schema.filter_map do |item|
|
||||||
|
submission.schema_documents.find { |a| a.uuid == item['attachment_uuid'] }
|
||||||
|
end
|
||||||
|
|
||||||
|
ActiveRecord::Associations::Preloader.new(records: documents, associations: %i[blob record]).call
|
||||||
|
|
||||||
|
text_runs = documents.to_h do |document|
|
||||||
|
[
|
||||||
|
document.uuid,
|
||||||
|
DocumentMetadatas.find_or_create_for_document(document, account_id: document.record.account_id).text_runs
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: { text_runs: }
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -5,19 +5,21 @@ class SubmitFormValuesController < ApplicationController
|
|||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
def index
|
def index
|
||||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||||
|
|
||||||
return render json: {} if submitter.completed_at? || submitter.declined_at?
|
return render json: {} if @submitter.completed_at? ||
|
||||||
return render json: {} if submitter.submission.template&.archived_at? ||
|
@submitter.declined_at? ||
|
||||||
submitter.submission.archived_at? ||
|
@submitter.submission.template&.archived_at? ||
|
||||||
submitter.submission.expired?
|
@submitter.submission.archived_at? ||
|
||||||
|
@submitter.submission.expired? ||
|
||||||
|
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||||
|
|
||||||
value = submitter.values[params['field_uuid']]
|
value = @submitter.values[params['field_uuid']]
|
||||||
attachment = submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present?
|
attachment = @submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present?
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
value:,
|
value:,
|
||||||
attachment: attachment&.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
|
attachment: attachment&.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
|
||||||
}, head: :ok
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -7,25 +7,34 @@ class SubmittersAutocompleteController < ApplicationController
|
|||||||
LIMIT = 100
|
LIMIT = 100
|
||||||
|
|
||||||
def index
|
def index
|
||||||
submitters = search_submitters(@submitters)
|
field = SELECT_COLUMNS.find { |c| c == params[:field] }
|
||||||
|
|
||||||
|
submitters = search_submitters(@submitters, field)
|
||||||
|
|
||||||
arel_columns = SELECT_COLUMNS.map { |col| Submitter.arel_table[col] }
|
arel_columns = SELECT_COLUMNS.map { |col| Submitter.arel_table[col] }
|
||||||
values = submitters.limit(LIMIT).group(arel_columns).pluck(arel_columns)
|
|
||||||
|
values =
|
||||||
|
if field
|
||||||
|
max_ids = submitters.group(field).limit(LIMIT).select(Submitter.arel_table[:id].maximum)
|
||||||
|
|
||||||
|
submitters.where(id: max_ids).order(id: :desc).pluck(arel_columns)
|
||||||
|
else
|
||||||
|
submitters.limit(LIMIT).group(arel_columns).pluck(arel_columns)
|
||||||
|
end
|
||||||
|
|
||||||
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
|
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
|
||||||
attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present?
|
|
||||||
|
|
||||||
render json: attrs
|
render json: attrs
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def search_submitters(submitters)
|
def search_submitters(submitters, field)
|
||||||
if SELECT_COLUMNS.include?(params[:field])
|
if field
|
||||||
if Docuseal.fulltext_search?
|
if Docuseal.fulltext_search?
|
||||||
Submitters.fulltext_search_field(current_user, submitters, params[:q], params[:field])
|
Submitters.fulltext_search_field(current_user, submitters, params[:q], field)
|
||||||
else
|
else
|
||||||
column = Submitter.arel_table[params[:field].to_sym]
|
column = Submitter.arel_table[field.to_sym]
|
||||||
|
|
||||||
term = "#{params[:q].downcase}%"
|
term = "#{params[:q].downcase}%"
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class SubmittersController < ApplicationController
|
|||||||
def update
|
def update
|
||||||
submission = @submitter.submission
|
submission = @submitter.submission
|
||||||
|
|
||||||
if @submitter.submission_events.exists?(event_type: 'start_form') || submission.archived_at? || submission.expired?
|
unless submitter_editable?(submission)
|
||||||
return redirect_back fallback_location: submission_path(submission), alert: I18n.t('submitter_cannot_be_updated')
|
return redirect_back fallback_location: submission_path(submission), alert: I18n.t('submitter_cannot_be_updated')
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -48,6 +48,12 @@ class SubmittersController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def submitter_editable?(submission)
|
||||||
|
!@submitter.submission_events.exists?(event_type: 'start_form') &&
|
||||||
|
!@submitter.completed_at? && !@submitter.declined_at? &&
|
||||||
|
!submission.archived_at? && !submission.expired? && !submission.template&.archived_at?
|
||||||
|
end
|
||||||
|
|
||||||
def maybe_resend_email_sms(submitter, params)
|
def maybe_resend_email_sms(submitter, params)
|
||||||
if params[:send_email] == '1' && submitter.email.present?
|
if params[:send_email] == '1' && submitter.email.present?
|
||||||
is_sent_recently = Docuseal.multitenant? &&
|
is_sent_recently = Docuseal.multitenant? &&
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmittersDownloadController < ApplicationController
|
||||||
|
load_and_authorize_resource :submitter
|
||||||
|
|
||||||
|
def index
|
||||||
|
Submissions::EnsureResultGenerated.call(@submitter)
|
||||||
|
|
||||||
|
render json: Submitters.build_document_urls(@submitter)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -5,6 +5,8 @@ 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
|
||||||
|
return redirect_to submit_form_path(slug: @submitter.slug) if @submitter.completed_at.blank? ||
|
||||||
|
@submitter.completed_at < 1.month.ago
|
||||||
|
|
||||||
submission = @submitter.account.submissions.new(created_by_user: current_user,
|
submission = @submitter.account.submissions.new(created_by_user: current_user,
|
||||||
submitters_order: :preserved,
|
submitters_order: :preserved,
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class SubmittersSendEmailController < ApplicationController
|
class SubmittersSendEmailController < ApplicationController
|
||||||
load_and_authorize_resource :submitter, id_param: :submitter_slug, find_by: :slug
|
load_and_authorize_resource :submitter
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
authorize!(:update, @submitter)
|
||||||
|
|
||||||
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
|
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
|
||||||
event_type: 'send_email',
|
event_type: 'send_email',
|
||||||
created_at: 10.hours.ago..Time.current)
|
created_at: 10.hours.ago..Time.current)
|
||||||
|
|||||||
@@ -6,17 +6,19 @@ class TemplateDocumentsController < ApplicationController
|
|||||||
FILES_TTL = 5.minutes
|
FILES_TTL = 5.minutes
|
||||||
|
|
||||||
def index
|
def index
|
||||||
render json: @template.schema_documents.map { |d| ActiveStorage::Blob.proxy_url(d.blob, expires_at: FILES_TTL.from_now.to_i) }
|
render json: @template.schema_documents.map { |d| ActiveStorage::Blob.proxy_path(d.blob, expires_at: FILES_TTL.from_now.to_i) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
authorize!(:update, @template)
|
||||||
|
|
||||||
if params[:blobs].blank? && params[:files].blank?
|
if params[:blobs].blank? && params[:files].blank?
|
||||||
return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_content
|
return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
old_fields_hash = @template.fields.hash
|
old_fields_hash = @template.fields.hash
|
||||||
|
|
||||||
documents = Templates::CreateAttachments.call(@template, params, extract_fields: true)
|
documents, = Templates::CreateAttachments.call(@template, params, extract_fields: true)
|
||||||
|
|
||||||
schema = documents.map do |doc|
|
schema = documents.map do |doc|
|
||||||
{ attachment_uuid: doc.uuid, name: doc.filename.base }
|
{ attachment_uuid: doc.uuid, name: doc.filename.base }
|
||||||
@@ -27,7 +29,7 @@ class TemplateDocumentsController < ApplicationController
|
|||||||
fields: old_fields_hash == @template.fields.hash ? nil : @template.fields,
|
fields: old_fields_hash == @template.fields.hash ? nil : @template.fields,
|
||||||
submitters: old_fields_hash == @template.fields.hash ? nil : @template.submitters,
|
submitters: old_fields_hash == @template.fields.hash ? nil : @template.submitters,
|
||||||
documents: documents.as_json(
|
documents: documents.as_json(
|
||||||
methods: %i[metadata signed_uuid],
|
methods: %i[metadata signed_key],
|
||||||
include: {
|
include: {
|
||||||
preview_images: { methods: %i[url metadata filename] }
|
preview_images: { methods: %i[url metadata filename] }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplateDocumentsCropController < ApplicationController
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
before_action :load_attachment
|
||||||
|
|
||||||
|
rescue_from Leptonica::LeptonicaError do
|
||||||
|
render json: { error: I18n.t(:unable_to_save) }, status: :unprocessable_content
|
||||||
|
end
|
||||||
|
|
||||||
|
def index
|
||||||
|
render json: { corners: Leptonica.detect_document_corners(@attachment.download) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize!(:update, @template)
|
||||||
|
|
||||||
|
document = Templates::CreateDocumentCrop.call(@template, @attachment, crop_params)
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
document: document.as_json(
|
||||||
|
methods: %i[metadata signed_key],
|
||||||
|
include: {
|
||||||
|
preview_images: { methods: %i[url metadata filename] }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_attachment
|
||||||
|
@attachment = @template.documents_attachments.find_by!(uuid: params[:attachment_uuid])
|
||||||
|
end
|
||||||
|
|
||||||
|
def crop_params
|
||||||
|
params.permit(:scan, :rotate, :flip_h, :flip_v, corners: [%i[x y]])
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplateDocumentsModifyController < ApplicationController
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize!(:update, @template)
|
||||||
|
|
||||||
|
documents_layout =
|
||||||
|
params.require(:documents).map do |item|
|
||||||
|
item.permit(:attachment_uuid,
|
||||||
|
pages: [:attachment_uuid, :page, :rotate,
|
||||||
|
{ redact: [%i[x y w h color]], replaced_page: %i[attachment_uuid page] }]).to_h
|
||||||
|
end
|
||||||
|
|
||||||
|
Templates::ModifyDocuments.call(@template, documents_layout)
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
schema: @template.schema,
|
||||||
|
fields: @template.fields,
|
||||||
|
submitters: @template.submitters,
|
||||||
|
documents: @template.schema_documents.reload.preload(:blob, preview_images_attachments: :blob).as_json(
|
||||||
|
methods: %i[metadata signed_key],
|
||||||
|
include: {
|
||||||
|
preview_images: { methods: %i[url metadata filename] }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
rescue Templates::ModifyDocuments::InvalidLayout
|
||||||
|
render json: { error: I18n.t(:unable_to_save) }, status: :unprocessable_content
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplateDocumentsPageObjectsController < ApplicationController
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def index
|
||||||
|
attachment = @template.documents_attachments.find_by!(uuid: params[:attachment_uuid])
|
||||||
|
|
||||||
|
render json: Templates::ModifyDocuments.page_objects(attachment, params[:page].to_i)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -8,11 +8,14 @@ class TemplatesCloneAndReplaceController < ApplicationController
|
|||||||
|
|
||||||
ActiveRecord::Associations::Preloader.new(
|
ActiveRecord::Associations::Preloader.new(
|
||||||
records: [@template],
|
records: [@template],
|
||||||
associations: [schema_documents: :preview_images_attachments]
|
associations: [{ schema_documents: :preview_images_attachments }]
|
||||||
).call
|
).call
|
||||||
|
|
||||||
cloned_template = Templates::Clone.call(@template, author: current_user)
|
cloned_template = Templates::Clone.call(@template, author: current_user)
|
||||||
cloned_template.name = File.basename(params[:files].first.original_filename, '.*')
|
cloned_template.name = File.basename(params[:files].first.original_filename, '.*')
|
||||||
|
|
||||||
|
authorize!(:create, cloned_template)
|
||||||
|
|
||||||
cloned_template.save!
|
cloned_template.save!
|
||||||
|
|
||||||
documents = Templates::ReplaceAttachments.call(cloned_template, params, extract_fields: true)
|
documents = Templates::ReplaceAttachments.call(cloned_template, params, extract_fields: true)
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesCloneController < ApplicationController
|
||||||
|
load_and_authorize_resource :template, instance_name: :base_template
|
||||||
|
|
||||||
|
def new
|
||||||
|
authorize!(:create, Template)
|
||||||
|
|
||||||
|
@template = Template.new(name: "#{@base_template.name} (#{I18n.t('clone')})")
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
ActiveRecord::Associations::Preloader.new(
|
||||||
|
records: [@base_template],
|
||||||
|
associations: [{ schema_documents: :preview_images_attachments }]
|
||||||
|
).call
|
||||||
|
|
||||||
|
@template = Templates::Clone.call(@base_template, author: current_user,
|
||||||
|
name: params.dig(:template, :name),
|
||||||
|
folder_name: params[:folder_name])
|
||||||
|
|
||||||
|
authorize!(:create, @template)
|
||||||
|
|
||||||
|
if params[:account_id].present? && true_ability.can?(:manage, Account.find(params[:account_id]))
|
||||||
|
@template.account_id = params[:account_id]
|
||||||
|
@template.author = true_user if true_user.account_id == @template.account_id
|
||||||
|
@template.folder = @template.account.default_template_folder if @template.account_id != current_account.id
|
||||||
|
else
|
||||||
|
@template.account = current_account
|
||||||
|
end
|
||||||
|
|
||||||
|
Templates.maybe_assign_access(@template)
|
||||||
|
|
||||||
|
if @template.save
|
||||||
|
Templates::CloneAttachments.call(template: @template, original_template: @base_template)
|
||||||
|
|
||||||
|
SearchEntries.enqueue_reindex(@template)
|
||||||
|
|
||||||
|
WebhookUrls.enqueue_events(@template, 'template.created')
|
||||||
|
|
||||||
|
maybe_redirect_to_template(@template)
|
||||||
|
else
|
||||||
|
render turbo_stream: turbo_stream.replace(:modal, partial: 'templates_clone/form'), status: :unprocessable_content
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def maybe_redirect_to_template(template)
|
||||||
|
if template.account == current_account
|
||||||
|
redirect_to(edit_template_path(template))
|
||||||
|
else
|
||||||
|
redirect_back(fallback_location: root_path, notice: I18n.t('template_has_been_cloned'))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -3,8 +3,6 @@
|
|||||||
class TemplatesController < ApplicationController
|
class TemplatesController < ApplicationController
|
||||||
load_and_authorize_resource :template
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
before_action :load_base_template, only: %i[new create]
|
|
||||||
|
|
||||||
def show
|
def show
|
||||||
submissions = @template.submissions.accessible_by(current_ability)
|
submissions = @template.submissions.accessible_by(current_ability)
|
||||||
submissions = submissions.active if @template.archived_at.blank?
|
submissions = submissions.active if @template.archived_at.blank?
|
||||||
@@ -26,59 +24,27 @@ class TemplatesController < ApplicationController
|
|||||||
redirect_to root_path
|
redirect_to root_path
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new; end
|
||||||
@template.name = "#{@base_template.name} (#{I18n.t('clone')})" if @base_template
|
|
||||||
end
|
|
||||||
|
|
||||||
def edit
|
def edit
|
||||||
ActiveRecord::Associations::Preloader.new(
|
@template_data = Templates.serialize_for_builder(@template)
|
||||||
records: [@template],
|
|
||||||
associations: [schema_documents: [:blob, { preview_images_attachments: :blob }]]
|
|
||||||
).call
|
|
||||||
|
|
||||||
@template_data =
|
|
||||||
@template.as_json.merge(
|
|
||||||
documents: @template.schema_documents.as_json(
|
|
||||||
methods: %i[metadata signed_uuid],
|
|
||||||
include: { preview_images: { methods: %i[url metadata filename] } }
|
|
||||||
)
|
|
||||||
).to_json
|
|
||||||
|
|
||||||
render :edit, layout: 'plain'
|
render :edit, layout: 'plain'
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
if @base_template
|
@template.author = current_user
|
||||||
ActiveRecord::Associations::Preloader.new(
|
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
||||||
records: [@base_template],
|
@template.account = current_account
|
||||||
associations: [schema_documents: :preview_images_attachments]
|
|
||||||
).call
|
|
||||||
|
|
||||||
@template = Templates::Clone.call(@base_template, author: current_user,
|
|
||||||
name: params.dig(:template, :name),
|
|
||||||
folder_name: params[:folder_name])
|
|
||||||
else
|
|
||||||
@template.author = current_user
|
|
||||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
|
||||||
end
|
|
||||||
|
|
||||||
if params[:account_id].present? && authorized_clone_account_id?(params[:account_id])
|
|
||||||
@template.account_id = params[:account_id]
|
|
||||||
@template.folder = @template.account.default_template_folder if @template.account_id != current_account.id
|
|
||||||
else
|
|
||||||
@template.account = current_account
|
|
||||||
end
|
|
||||||
|
|
||||||
Templates.maybe_assign_access(@template)
|
Templates.maybe_assign_access(@template)
|
||||||
|
|
||||||
if @template.save
|
if @template.save
|
||||||
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
|
||||||
|
|
||||||
SearchEntries.enqueue_reindex(@template)
|
SearchEntries.enqueue_reindex(@template)
|
||||||
|
|
||||||
WebhookUrls.enqueue_events(@template, 'template.created')
|
WebhookUrls.enqueue_events(@template, 'template.created')
|
||||||
|
|
||||||
maybe_redirect_to_template(@template)
|
redirect_to(edit_template_path(@template))
|
||||||
else
|
else
|
||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_content
|
render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_content
|
||||||
end
|
end
|
||||||
@@ -107,6 +73,8 @@ class TemplatesController < ApplicationController
|
|||||||
else
|
else
|
||||||
@template.update!(archived_at: Time.current)
|
@template.update!(archived_at: Time.current)
|
||||||
|
|
||||||
|
WebhookUrls.enqueue_events(@template, 'template.archived')
|
||||||
|
|
||||||
I18n.t('template_has_been_archived')
|
I18n.t('template_has_been_archived')
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -118,9 +86,11 @@ class TemplatesController < ApplicationController
|
|||||||
def template_params
|
def template_params
|
||||||
params.require(:template).permit(
|
params.require(:template).permit(
|
||||||
:name,
|
:name,
|
||||||
{ schema: [[:attachment_uuid, :google_drive_file_id, :name,
|
{ schema: [[:attachment_uuid, :google_drive_file_id, :name, :dynamic,
|
||||||
{ conditions: [%i[field_uuid value action operation]] }]],
|
{ conditions: [%i[field_uuid value action operation]] }]],
|
||||||
submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid optional_invite_by_uuid email order]],
|
submitters: [%i[name uuid is_requester linked_to_uuid invite_via_field_uuid
|
||||||
|
invite_by_uuid optional_invite_by_uuid email order]],
|
||||||
|
variables_schema: {},
|
||||||
fields: [[:uuid, :submitter_uuid, :name, :type,
|
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||||
:required, :readonly, :default_value,
|
:required, :readonly, :default_value,
|
||||||
:title, :description, :prefillable,
|
:title, :description, :prefillable,
|
||||||
@@ -129,26 +99,7 @@ class TemplatesController < ApplicationController
|
|||||||
conditions: [%i[field_uuid value action operation]],
|
conditions: [%i[field_uuid value action operation]],
|
||||||
options: [%i[value uuid]],
|
options: [%i[value uuid]],
|
||||||
validation: %i[message pattern min max step],
|
validation: %i[message pattern min max step],
|
||||||
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
areas: [%i[uuid x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def authorized_clone_account_id?(account_id)
|
|
||||||
true_user.account_id.to_s == account_id.to_s ||
|
|
||||||
true_user.account.linked_accounts.accessible_by(current_ability).exists?(id: account_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def maybe_redirect_to_template(template)
|
|
||||||
if template.account == current_account
|
|
||||||
redirect_to(edit_template_path(@template))
|
|
||||||
else
|
|
||||||
redirect_back(fallback_location: root_path, notice: I18n.t('template_has_been_cloned'))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def load_base_template
|
|
||||||
return if params[:base_template_id].blank?
|
|
||||||
|
|
||||||
@base_template = Template.accessible_by(current_ability).find_by(id: params[:base_template_id])
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
@@ -9,32 +9,34 @@ class TemplatesDebugController < ApplicationController
|
|||||||
schema_uuids = @template.schema.index_by { |e| e['attachment_uuid'] }
|
schema_uuids = @template.schema.index_by { |e| e['attachment_uuid'] }
|
||||||
attachment = @template.documents.find { |a| schema_uuids[a.uuid] }
|
attachment = @template.documents.find { |a| schema_uuids[a.uuid] }
|
||||||
|
|
||||||
data = attachment.download
|
if attachment
|
||||||
|
data = attachment.download
|
||||||
|
|
||||||
unless attachment.image?
|
unless attachment.image?
|
||||||
pdf = HexaPDF::Document.new(io: StringIO.new(data))
|
pdf = HexaPDF::Document.new(io: StringIO.new(data))
|
||||||
|
|
||||||
fields = Templates::FindAcroFields.call(pdf, attachment, data)
|
fields = Templates::FindAcroFields.call(pdf, attachment, data)
|
||||||
|
end
|
||||||
|
|
||||||
|
# fields, = Templates::DetectFields.call(StringIO.new(data), attachment:) if fields.blank?
|
||||||
|
|
||||||
|
attachment.metadata['pdf'] ||= {}
|
||||||
|
attachment.metadata['pdf']['fields'] = fields
|
||||||
|
|
||||||
|
@template.update!(fields: Templates::ProcessDocument.normalize_attachment_fields(@template, [attachment]))
|
||||||
end
|
end
|
||||||
|
|
||||||
fields, = Templates::DetectFields.call(StringIO.new(data), attachment:) if fields.blank?
|
|
||||||
|
|
||||||
attachment.metadata['pdf'] ||= {}
|
|
||||||
attachment.metadata['pdf']['fields'] = fields
|
|
||||||
|
|
||||||
@template.update!(fields: Templates::ProcessDocument.normalize_attachment_fields(@template, [attachment]))
|
|
||||||
|
|
||||||
debug_file if DEBUG_FILE.present?
|
debug_file if DEBUG_FILE.present?
|
||||||
|
|
||||||
ActiveRecord::Associations::Preloader.new(
|
ActiveRecord::Associations::Preloader.new(
|
||||||
records: [@template],
|
records: [@template],
|
||||||
associations: [schema_documents: { preview_images_attachments: :blob }]
|
associations: [{ schema_documents: { preview_images_attachments: :blob } }]
|
||||||
).call
|
).call
|
||||||
|
|
||||||
@template_data =
|
@template_data =
|
||||||
@template.as_json.merge(
|
@template.as_json.merge(
|
||||||
documents: @template.schema_documents.as_json(
|
documents: @template.schema_documents.as_json(
|
||||||
methods: %i[metadata signed_uuid],
|
methods: %i[metadata signed_key],
|
||||||
include: { preview_images: { methods: %i[url metadata filename] } }
|
include: { preview_images: { methods: %i[url metadata filename] } }
|
||||||
)
|
)
|
||||||
).to_json
|
).to_json
|
||||||
@@ -58,9 +60,16 @@ class TemplatesDebugController < ApplicationController
|
|||||||
|
|
||||||
params = { files: [file] }
|
params = { files: [file] }
|
||||||
|
|
||||||
documents = Templates::CreateAttachments.call(@template, params)
|
documents, dynamic_documents = Templates::CreateAttachments.call(@template, params,
|
||||||
|
dynamic: DEBUG_FILE.ends_with?('.docx'))
|
||||||
|
|
||||||
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
|
schema = documents.map do |doc|
|
||||||
|
{
|
||||||
|
attachment_uuid: doc.uuid,
|
||||||
|
name: doc.filename.base,
|
||||||
|
dynamic: dynamic_documents.find { |e| e.uuid == doc.uuid }.present?
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
@template.update!(schema:)
|
@template.update!(schema:)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,11 +11,19 @@ class TemplatesDetectFieldsController < ApplicationController
|
|||||||
sse = SSE.new(response.stream)
|
sse = SSE.new(response.stream)
|
||||||
|
|
||||||
documents = @template.schema_documents.preload(:blob)
|
documents = @template.schema_documents.preload(:blob)
|
||||||
|
documents = documents.where(uuid: params[:attachment_uuid]) if params[:attachment_uuid].present?
|
||||||
|
|
||||||
|
page_number = params[:page].presence&.to_i
|
||||||
|
|
||||||
documents.each do |document|
|
documents.each do |document|
|
||||||
io = StringIO.new(document.download)
|
io =
|
||||||
|
if document.image?
|
||||||
|
StringIO.new(document.preview_images.joins(:blob).find_by(blob: { filename: ['0.png', '0.jpg'] }).download)
|
||||||
|
else
|
||||||
|
StringIO.new(document.download)
|
||||||
|
end
|
||||||
|
|
||||||
Templates::DetectFields.call(io, attachment: document) do |(attachment_uuid, page, fields)|
|
Templates::DetectFields.call(io, attachment: document, page_number:) do |(attachment_uuid, page, fields)|
|
||||||
sse.write({ attachment_uuid:, page:, fields: })
|
sse.write({ attachment_uuid:, page:, fields: })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ class TemplatesFoldersController < ApplicationController
|
|||||||
def edit; end
|
def edit; end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
authorize!(:update, @template)
|
||||||
|
|
||||||
name = [params[:parent_name], params[:name]].compact_blank.join(' / ')
|
name = [params[:parent_name], params[:name]].compact_blank.join(' / ')
|
||||||
|
|
||||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, name)
|
@template.folder = TemplateFolders.find_or_create_by_name(current_user, name)
|
||||||
|
|||||||
@@ -54,12 +54,12 @@ class TemplatesPreferencesController < ApplicationController
|
|||||||
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 validate_unique_submitters
|
completed_redirect_url validate_unique_submitters
|
||||||
require_all_submitters submitters_order require_phone_2fa
|
require_all_submitters submitters_order require_phone_2fa require_email_2fa
|
||||||
default_expire_at_duration shared_link_2fa default_expire_at request_email_enabled
|
default_expire_at_duration shared_link_2fa default_expire_at request_email_enabled
|
||||||
completed_notification_email_subject completed_notification_email_body
|
completed_notification_email_subject completed_notification_email_body
|
||||||
completed_notification_email_enabled completed_notification_email_attach_audit] +
|
completed_notification_email_enabled completed_notification_email_attach_audit] +
|
||||||
[completed_message: %i[title body],
|
[{ completed_message: %i[title body],
|
||||||
submitters: [%i[uuid request_email_subject request_email_body]], link_form_fields: []]
|
submitters: [%i[uuid request_email_subject request_email_body]], link_form_fields: [] }]
|
||||||
).tap do |attrs|
|
).tap do |attrs|
|
||||||
attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1'
|
attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1'
|
||||||
|
|
||||||
|
|||||||
@@ -4,18 +4,7 @@ class TemplatesPreviewController < ApplicationController
|
|||||||
load_and_authorize_resource :template
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
def show
|
def show
|
||||||
ActiveRecord::Associations::Preloader.new(
|
@template_data = Templates.serialize_for_builder(@template)
|
||||||
records: [@template],
|
|
||||||
associations: [schema_documents: { preview_images_attachments: :blob }]
|
|
||||||
).call
|
|
||||||
|
|
||||||
@template_data =
|
|
||||||
@template.as_json.merge(
|
|
||||||
documents: @template.schema_documents.as_json(
|
|
||||||
methods: %i[metadata signed_uuid],
|
|
||||||
include: { preview_images: { methods: %i[url metadata filename] } }
|
|
||||||
)
|
|
||||||
).to_json
|
|
||||||
|
|
||||||
render :show, layout: 'plain'
|
render :show, layout: 'plain'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class TemplatesRecipientsController < ApplicationController
|
|||||||
|
|
||||||
def submitters_params
|
def submitters_params
|
||||||
permit_params = { submitters: [%i[name uuid is_requester optional_invite_by_uuid
|
permit_params = { submitters: [%i[name uuid is_requester optional_invite_by_uuid
|
||||||
invite_by_uuid linked_to_uuid email option order]] }
|
invite_by_uuid invite_via_field_uuid linked_to_uuid email option order]] }
|
||||||
|
|
||||||
params.require(:template).permit(permit_params).fetch(:submitters, {}).values.filter_map do |s|
|
params.require(:template).permit(permit_params).fetch(:submitters, {}).values.filter_map do |s|
|
||||||
next if s[:uuid].blank?
|
next if s[:uuid].blank?
|
||||||
@@ -36,6 +36,7 @@ class TemplatesRecipientsController < ApplicationController
|
|||||||
s[:order] = s[:order].to_i if s[:order].present?
|
s[:order] = s[:order].to_i if s[:order].present?
|
||||||
s.delete(:invite_by_uuid) if s[:invite_by_uuid].blank?
|
s.delete(:invite_by_uuid) if s[:invite_by_uuid].blank?
|
||||||
s.delete(:optional_invite_by_uuid) if s[:optional_invite_by_uuid].blank?
|
s.delete(:optional_invite_by_uuid) if s[:optional_invite_by_uuid].blank?
|
||||||
|
s.delete(:invite_via_field_uuid) if s[:invite_via_field_uuid].blank?
|
||||||
|
|
||||||
normalize_option_value(s)
|
normalize_option_value(s)
|
||||||
end
|
end
|
||||||
@@ -53,6 +54,7 @@ class TemplatesRecipientsController < ApplicationController
|
|||||||
attrs.delete(:email)
|
attrs.delete(:email)
|
||||||
attrs.delete(:linked_to_uuid)
|
attrs.delete(:linked_to_uuid)
|
||||||
attrs.delete(:invite_by_uuid)
|
attrs.delete(:invite_by_uuid)
|
||||||
|
attrs.delete(:invite_via_field_uuid)
|
||||||
attrs.delete(:optional_invite_by_uuid)
|
attrs.delete(:optional_invite_by_uuid)
|
||||||
when /\Alinked_to_(.*)\z/
|
when /\Alinked_to_(.*)\z/
|
||||||
attrs[:linked_to_uuid] = ::Regexp.last_match(-1)
|
attrs[:linked_to_uuid] = ::Regexp.last_match(-1)
|
||||||
|
|||||||
@@ -4,8 +4,12 @@ class TemplatesRestoreController < ApplicationController
|
|||||||
load_and_authorize_resource :template
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
authorize!(:update, @template)
|
||||||
|
|
||||||
@template.update!(archived_at: nil)
|
@template.update!(archived_at: nil)
|
||||||
|
|
||||||
|
WebhookUrls.enqueue_events(@template, 'template.updated')
|
||||||
|
|
||||||
redirect_to template_path(@template), notice: I18n.t('template_has_been_unarchived')
|
redirect_to template_path(@template), notice: I18n.t('template_has_been_unarchived')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesShareLinkQrController < ApplicationController
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def show
|
||||||
|
return render :disabled, layout: 'plain' unless @template.shared_link?
|
||||||
|
|
||||||
|
shared_link_url = start_form_url(slug: @template.slug, host: form_link_host)
|
||||||
|
|
||||||
|
@qr_svg_code = RQRCode::QRCode.new(shared_link_url, level: :m).as_svg(viewbox: true)
|
||||||
|
|
||||||
|
@page_size =
|
||||||
|
if TimeUtils.timezone_abbr(current_account.timezone, Time.current.beginning_of_year).in?(TimeUtils::US_TIMEZONES)
|
||||||
|
'Letter'
|
||||||
|
else
|
||||||
|
'A4'
|
||||||
|
end
|
||||||
|
|
||||||
|
render :show, layout: false
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -5,14 +5,16 @@ class TemplatesUploadsController < ApplicationController
|
|||||||
|
|
||||||
layout 'plain'
|
layout 'plain'
|
||||||
|
|
||||||
def show; end
|
def show
|
||||||
|
redirect_to root_path if params[:url].blank?
|
||||||
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
url_params = create_file_params_from_url if params[:url].present?
|
url_params = create_file_params_from_url if params[:url].present?
|
||||||
|
|
||||||
save_template!(@template, url_params)
|
save_template!(@template, url_params)
|
||||||
|
|
||||||
documents = Templates::CreateAttachments.call(@template, url_params || params, extract_fields: true)
|
documents, = Templates::CreateAttachments.call(@template, url_params || params, extract_fields: true)
|
||||||
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
|
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
|
||||||
|
|
||||||
if @template.fields.blank?
|
if @template.fields.blank?
|
||||||
@@ -56,7 +58,7 @@ class TemplatesUploadsController < ApplicationController
|
|||||||
def create_file_params_from_url
|
def create_file_params_from_url
|
||||||
tempfile = Tempfile.new
|
tempfile = Tempfile.new
|
||||||
tempfile.binmode
|
tempfile.binmode
|
||||||
tempfile.write(DownloadUtils.call(params[:url]).body)
|
tempfile.write(DownloadUtils.call(params[:url], validate: true).body)
|
||||||
tempfile.rewind
|
tempfile.rewind
|
||||||
|
|
||||||
filename = URI.decode_www_form_component(params[:filename]) if params[:filename].present?
|
filename = URI.decode_www_form_component(params[:filename]) if params[:filename].present?
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesVersionsController < ApplicationController
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def index
|
||||||
|
versions = @template.template_versions.order(id: :desc).preload(:author)
|
||||||
|
|
||||||
|
render json: versions.as_json(TemplateVersions::SERIALIZE_PARAMS)
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
version = @template.template_versions.find(params[:id])
|
||||||
|
|
||||||
|
render json: TemplateVersions.serialize(version)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize!(:update, @template)
|
||||||
|
|
||||||
|
TemplateVersions.find_or_create_for(@template, author: current_user)
|
||||||
|
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
class TestingAccountsController < ApplicationController
|
class TestingAccountsController < ApplicationController
|
||||||
skip_authorization_check only: :destroy
|
skip_authorization_check only: :destroy
|
||||||
|
|
||||||
def show
|
def create
|
||||||
authorize!(:manage, current_account)
|
authorize!(:manage, current_account)
|
||||||
authorize!(:manage, current_user)
|
authorize!(:manage, current_user)
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class TimestampServerController < ApplicationController
|
|||||||
uri = Addressable::URI.parse(url)
|
uri = Addressable::URI.parse(url)
|
||||||
|
|
||||||
conn = Faraday.new(uri.origin) do |c|
|
conn = Faraday.new(uri.origin) do |c|
|
||||||
c.basic_auth(uri.user, uri.password) if uri.password.present?
|
c.request :authorization, :basic, uri.user, uri.password if uri.password.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
response = conn.post(uri.path, req.to_der,
|
response = conn.post(uri.path, req.to_der,
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ class UserInitialsController < ApplicationController
|
|||||||
|
|
||||||
return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_initials') if file.blank?
|
return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_initials') if file.blank?
|
||||||
|
|
||||||
|
extension = File.extname(file.original_filename).delete_prefix('.').downcase
|
||||||
|
|
||||||
|
if Submitters::DANGEROUS_EXTENSIONS.include?(extension)
|
||||||
|
raise Submitters::MaliciousFileExtension, "File type '.#{extension}' is not allowed."
|
||||||
|
end
|
||||||
|
|
||||||
blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
|
blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
|
||||||
filename: file.original_filename,
|
filename: file.original_filename,
|
||||||
content_type: file.content_type)
|
content_type: file.content_type)
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ class UserSignaturesController < ApplicationController
|
|||||||
|
|
||||||
return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_signature') if file.blank?
|
return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_signature') if file.blank?
|
||||||
|
|
||||||
|
extension = File.extname(file.original_filename).delete_prefix('.').downcase
|
||||||
|
|
||||||
|
if Submitters::DANGEROUS_EXTENSIONS.include?(extension)
|
||||||
|
raise Submitters::MaliciousFileExtension, "File type '.#{extension}' is not allowed."
|
||||||
|
end
|
||||||
|
|
||||||
blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
|
blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
|
||||||
filename: file.original_filename,
|
filename: file.original_filename,
|
||||||
content_type: file.content_type)
|
content_type: file.content_type)
|
||||||
|
|||||||
@@ -16,7 +16,19 @@ class UsersController < ApplicationController
|
|||||||
@users.active.where.not(role: 'integration')
|
@users.active.where.not(role: 'integration')
|
||||||
end
|
end
|
||||||
|
|
||||||
@pagy, @users = pagy(@users.preload(account: :account_accesses).where(account: current_account).order(id: :desc))
|
@users = @users.preload(account: :account_accesses).where(account: current_account).order(id: :desc)
|
||||||
|
|
||||||
|
respond_to do |format|
|
||||||
|
format.html do
|
||||||
|
@pagy, @users = pagy(@users)
|
||||||
|
end
|
||||||
|
|
||||||
|
if current_ability.can?(:manage, current_account)
|
||||||
|
format.csv do
|
||||||
|
send_data Users.generate_csv(@users), filename: "users-#{Time.current.iso8601}.csv", type: 'text/csv'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def new; end
|
def new; end
|
||||||
@@ -54,7 +66,8 @@ class UsersController < ApplicationController
|
|||||||
def update
|
def update
|
||||||
return redirect_to settings_users_path, notice: I18n.t('unable_to_update_user') if Docuseal.demo?
|
return redirect_to settings_users_path, notice: I18n.t('unable_to_update_user') if Docuseal.demo?
|
||||||
|
|
||||||
attrs = user_params.compact_blank.merge(user_params.slice(:archived_at))
|
attrs = user_params.compact_blank
|
||||||
|
attrs = attrs.merge(user_params.slice(:archived_at)) if current_ability.can?(:create, @user)
|
||||||
|
|
||||||
if params.dig(:user, :account_id).present?
|
if params.dig(:user, :account_id).present?
|
||||||
account = Account.accessible_by(current_ability).find(params.dig(:user, :account_id))
|
account = Account.accessible_by(current_ability).find(params.dig(:user, :account_id))
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class WebhookEventsController < ApplicationController
|
|||||||
Submissions::SerializeForApi.call(@webhook_event.record)
|
Submissions::SerializeForApi.call(@webhook_event.record)
|
||||||
when 'template.created', 'template.updated'
|
when 'template.created', 'template.updated'
|
||||||
Templates::SerializeForApi.call(@webhook_event.record)
|
Templates::SerializeForApi.call(@webhook_event.record)
|
||||||
when 'submission.archived'
|
when 'submission.archived', 'template.archived'
|
||||||
@webhook_event.record.as_json(only: %i[id archived_at])
|
@webhook_event.record.as_json(only: %i[id archived_at])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class WebhookHmacController < ApplicationController
|
||||||
|
load_and_authorize_resource :webhook_url, parent: false
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
end
|
||||||
@@ -15,7 +15,7 @@ class WebhookSettingsController < ApplicationController
|
|||||||
|
|
||||||
@webhook_events = @webhook_events.where(status: params[:status]) if %w[success error].include?(params[:status])
|
@webhook_events = @webhook_events.where(status: params[:status]) if %w[success error].include?(params[:status])
|
||||||
|
|
||||||
@pagy, @webhook_events = pagy_countless(@webhook_events.order(id: :desc))
|
@pagy, @webhook_events = pagy(:countless, @webhook_events.order(id: :desc))
|
||||||
|
|
||||||
render :show
|
render :show
|
||||||
end
|
end
|
||||||
@@ -26,15 +26,19 @@ class WebhookSettingsController < ApplicationController
|
|||||||
|
|
||||||
@webhook_events = @webhook_events.where(status: params[:status]) if %w[success error].include?(params[:status])
|
@webhook_events = @webhook_events.where(status: params[:status]) if %w[success error].include?(params[:status])
|
||||||
|
|
||||||
@pagy, @webhook_events = pagy_countless(@webhook_events.order(id: :desc))
|
@pagy, @webhook_events = pagy(:countless, @webhook_events.order(id: :desc))
|
||||||
end
|
end
|
||||||
|
|
||||||
def new; end
|
def new; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@webhook_url.save!
|
if @webhook_url.url.present?
|
||||||
|
@webhook_url.save!
|
||||||
|
|
||||||
redirect_to settings_webhooks_path, notice: I18n.t('webhook_url_has_been_saved')
|
redirect_to settings_webhooks_path, notice: I18n.t('webhook_url_has_been_saved')
|
||||||
|
else
|
||||||
|
redirect_back fallback_location: settings_webhooks_path
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import SubmittersAutocomplete from './elements/submitter_autocomplete'
|
|||||||
import FolderAutocomplete from './elements/folder_autocomplete'
|
import FolderAutocomplete from './elements/folder_autocomplete'
|
||||||
import SignatureForm from './elements/signature_form'
|
import SignatureForm from './elements/signature_form'
|
||||||
import SubmitForm from './elements/submit_form'
|
import SubmitForm from './elements/submit_form'
|
||||||
|
import ConvertUpload from './elements/convert_upload'
|
||||||
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 ToggleSubmit from './elements/toggle_submit'
|
import ToggleSubmit from './elements/toggle_submit'
|
||||||
@@ -40,18 +41,20 @@ import DashboardDropzone from './elements/dashboard_dropzone'
|
|||||||
import RequiredCheckboxGroup from './elements/required_checkbox_group'
|
import RequiredCheckboxGroup from './elements/required_checkbox_group'
|
||||||
import PageContainer from './elements/page_container'
|
import PageContainer from './elements/page_container'
|
||||||
import EmailEditor from './elements/email_editor'
|
import EmailEditor from './elements/email_editor'
|
||||||
|
import MarkdownEditor from './elements/markdown_editor'
|
||||||
import MountOnClick from './elements/mount_on_click'
|
import MountOnClick from './elements/mount_on_click'
|
||||||
import RemoveOnEvent from './elements/remove_on_event'
|
import RemoveOnEvent from './elements/remove_on_event'
|
||||||
import ScrollTo from './elements/scroll_to'
|
import ScrollTo from './elements/scroll_to'
|
||||||
import SetValue from './elements/set_value'
|
import SetValue from './elements/set_value'
|
||||||
import ReviewForm from './elements/review_form'
|
import ReviewForm from './elements/review_form'
|
||||||
import ShowOnValue from './elements/show_on_value'
|
import ShowOnValue from './elements/show_on_value'
|
||||||
import CustomValidation from './elements/custom_validation'
|
|
||||||
import ToggleClasses from './elements/toggle_classes'
|
import ToggleClasses from './elements/toggle_classes'
|
||||||
import AutosizeField from './elements/autosize_field'
|
import AutosizeField from './elements/autosize_field'
|
||||||
import GoogleDriveFilePicker from './elements/google_drive_file_picker'
|
import GoogleDriveFilePicker from './elements/google_drive_file_picker'
|
||||||
import OpenModal from './elements/open_modal'
|
import OpenModal from './elements/open_modal'
|
||||||
import BarChart from './elements/bar_chart'
|
import BarChart from './elements/bar_chart'
|
||||||
|
import FieldCondition from './elements/field_condition'
|
||||||
|
import ConfirmUpload from './elements/confirm_upload'
|
||||||
|
|
||||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||||
|
|
||||||
@@ -110,6 +113,7 @@ safeRegisterElement('submitters-autocomplete', SubmittersAutocomplete)
|
|||||||
safeRegisterElement('folder-autocomplete', FolderAutocomplete)
|
safeRegisterElement('folder-autocomplete', FolderAutocomplete)
|
||||||
safeRegisterElement('signature-form', SignatureForm)
|
safeRegisterElement('signature-form', SignatureForm)
|
||||||
safeRegisterElement('submit-form', SubmitForm)
|
safeRegisterElement('submit-form', SubmitForm)
|
||||||
|
safeRegisterElement('convert-upload', ConvertUpload)
|
||||||
safeRegisterElement('prompt-password', PromptPassword)
|
safeRegisterElement('prompt-password', PromptPassword)
|
||||||
safeRegisterElement('emails-textarea', EmailsTextarea)
|
safeRegisterElement('emails-textarea', EmailsTextarea)
|
||||||
safeRegisterElement('toggle-cookies', ToggleCookies)
|
safeRegisterElement('toggle-cookies', ToggleCookies)
|
||||||
@@ -130,18 +134,20 @@ safeRegisterElement('check-on-click', CheckOnClick)
|
|||||||
safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup)
|
safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup)
|
||||||
safeRegisterElement('page-container', PageContainer)
|
safeRegisterElement('page-container', PageContainer)
|
||||||
safeRegisterElement('email-editor', EmailEditor)
|
safeRegisterElement('email-editor', EmailEditor)
|
||||||
|
safeRegisterElement('markdown-editor', MarkdownEditor)
|
||||||
safeRegisterElement('mount-on-click', MountOnClick)
|
safeRegisterElement('mount-on-click', MountOnClick)
|
||||||
safeRegisterElement('remove-on-event', RemoveOnEvent)
|
safeRegisterElement('remove-on-event', RemoveOnEvent)
|
||||||
safeRegisterElement('scroll-to', ScrollTo)
|
safeRegisterElement('scroll-to', ScrollTo)
|
||||||
safeRegisterElement('set-value', SetValue)
|
safeRegisterElement('set-value', SetValue)
|
||||||
safeRegisterElement('review-form', ReviewForm)
|
safeRegisterElement('review-form', ReviewForm)
|
||||||
safeRegisterElement('show-on-value', ShowOnValue)
|
safeRegisterElement('show-on-value', ShowOnValue)
|
||||||
safeRegisterElement('custom-validation', CustomValidation)
|
|
||||||
safeRegisterElement('toggle-classes', ToggleClasses)
|
safeRegisterElement('toggle-classes', ToggleClasses)
|
||||||
safeRegisterElement('autosize-field', AutosizeField)
|
safeRegisterElement('autosize-field', AutosizeField)
|
||||||
safeRegisterElement('google-drive-file-picker', GoogleDriveFilePicker)
|
safeRegisterElement('google-drive-file-picker', GoogleDriveFilePicker)
|
||||||
safeRegisterElement('open-modal', OpenModal)
|
safeRegisterElement('open-modal', OpenModal)
|
||||||
safeRegisterElement('bar-chart', BarChart)
|
safeRegisterElement('bar-chart', BarChart)
|
||||||
|
safeRegisterElement('field-condition', FieldCondition)
|
||||||
|
safeRegisterElement('confirm-upload', ConfirmUpload)
|
||||||
|
|
||||||
safeRegisterElement('template-builder', class extends HTMLElement {
|
safeRegisterElement('template-builder', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
@@ -151,23 +157,35 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
|||||||
|
|
||||||
this.appElem.classList.add('md:h-screen')
|
this.appElem.classList.add('md:h-screen')
|
||||||
|
|
||||||
|
const template = reactive(JSON.parse(this.dataset.template))
|
||||||
|
|
||||||
this.app = createApp(TemplateBuilder, {
|
this.app = createApp(TemplateBuilder, {
|
||||||
template: reactive(JSON.parse(this.dataset.template)),
|
template,
|
||||||
|
customFields: reactive(JSON.parse(this.dataset.customFields || '[]')),
|
||||||
|
dynamicDocuments: reactive(JSON.parse(this.dataset.dynamicDocuments || '[]')),
|
||||||
backgroundColor: '#faf7f5',
|
backgroundColor: '#faf7f5',
|
||||||
locale: this.dataset.locale,
|
locale: this.dataset.locale,
|
||||||
withPhone: this.dataset.withPhone === 'true',
|
withPhone: this.dataset.withPhone === 'true',
|
||||||
|
withPrefillable: template.fields?.some((f) => f.prefillable),
|
||||||
withVerification: ['true', 'false'].includes(this.dataset.withVerification) ? this.dataset.withVerification === 'true' : null,
|
withVerification: ['true', 'false'].includes(this.dataset.withVerification) ? this.dataset.withVerification === 'true' : null,
|
||||||
|
withKba: ['true', 'false'].includes(this.dataset.withKba) ? this.dataset.withKba === 'true' : null,
|
||||||
withLogo: this.dataset.withLogo !== 'false',
|
withLogo: this.dataset.withLogo !== 'false',
|
||||||
withFieldsDetection: this.dataset.withFieldsDetection === 'true',
|
withFieldsDetection: this.dataset.withFieldsDetection === 'true',
|
||||||
|
withDetectExistingFields: this.dataset.withDetectExistingFields === 'true',
|
||||||
|
withRevisions: true,
|
||||||
|
withRevisionsMenu: this.dataset.withRevisionsMenu === 'true',
|
||||||
editable: this.dataset.editable !== 'false',
|
editable: this.dataset.editable !== 'false',
|
||||||
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
||||||
|
withCustomFields: true,
|
||||||
withPayment: this.dataset.withPayment === 'true',
|
withPayment: this.dataset.withPayment === 'true',
|
||||||
isPaymentConnected: this.dataset.isPaymentConnected === 'true',
|
isPaymentConnected: this.dataset.isPaymentConnected === 'true',
|
||||||
withFormula: this.dataset.withFormula === 'true',
|
withFormula: this.dataset.withFormula === 'true',
|
||||||
withSendButton: this.dataset.withSendButton !== 'false',
|
withSendButton: this.dataset.withSendButton !== 'false',
|
||||||
withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false',
|
withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false',
|
||||||
withConditions: this.dataset.withConditions === 'true',
|
withConditions: this.dataset.withConditions === 'true',
|
||||||
|
withDynamicDocuments: this.dataset.withDynamicDocuments === 'true',
|
||||||
withGoogleDrive: this.dataset.withGoogleDrive === 'true',
|
withGoogleDrive: this.dataset.withGoogleDrive === 'true',
|
||||||
|
pagePreviewFormat: this.dataset.pagePreviewFormat || '.jpg',
|
||||||
withReplaceAndCloneUpload: true,
|
withReplaceAndCloneUpload: true,
|
||||||
withDownload: true,
|
withDownload: true,
|
||||||
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
||||||
@@ -181,10 +199,35 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onSubmit = (e) => {
|
onSubmit = (e) => {
|
||||||
if (e.detail.success && e.detail?.formSubmission?.formElement?.id === 'submitters_form') {
|
if (e.detail.success) {
|
||||||
e.detail.fetchResponse.response.json().then((data) => {
|
if (e.detail?.formSubmission?.formElement?.id === 'submitters_form') {
|
||||||
this.component.template.submitters = data.submitters
|
e.detail.fetchResponse.response.json().then((data) => {
|
||||||
})
|
this.component.template.submitters = data.submitters
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.detail?.formSubmission?.formElement?.action?.endsWith('/prefillable_fields')) {
|
||||||
|
e.detail.fetchResponse.response.text().then((data) => {
|
||||||
|
const doc = new DOMParser().parseFromString(data, 'text/html')
|
||||||
|
const fragment = doc.querySelector('turbo-stream template').content
|
||||||
|
|
||||||
|
const prefillableUuidsIndex = {}
|
||||||
|
|
||||||
|
fragment.querySelectorAll('[name="field_uuid"]').forEach((field) => {
|
||||||
|
prefillableUuidsIndex[field.value] = true
|
||||||
|
})
|
||||||
|
|
||||||
|
this.component.template.fields.forEach((field) => {
|
||||||
|
if (prefillableUuidsIndex[field.uuid]) {
|
||||||
|
field.prefillable = true
|
||||||
|
field.readonly = true
|
||||||
|
} else if (field.prefillable) {
|
||||||
|
delete field.prefillable
|
||||||
|
delete field.readonly
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -155,3 +155,7 @@ button[disabled] .enabled, button.btn-disabled .enabled {
|
|||||||
.font-courier {
|
.font-courier {
|
||||||
font-family: "Courier New", Consolas, "Liberation Mono", monospace, ui-monospace, SFMono-Regular, Menlo, Monaco;
|
font-family: "Courier New", Consolas, "Liberation Mono", monospace, ui-monospace, SFMono-Regular, Menlo, Monaco;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
markdown-editor [contenteditable] p {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
+52
-6
@@ -4,15 +4,27 @@ import { isValidSignatureCanvas } from './submission_form/validate_signature'
|
|||||||
|
|
||||||
window.customElements.define('draw-signature', class extends HTMLElement {
|
window.customElements.define('draw-signature', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
const scale = 3
|
this.setCanvasSize()
|
||||||
|
|
||||||
this.canvas.width = this.canvas.parentNode.clientWidth * scale
|
|
||||||
this.canvas.height = this.canvas.parentNode.clientHeight * scale
|
|
||||||
|
|
||||||
this.canvas.getContext('2d').scale(scale, scale)
|
|
||||||
|
|
||||||
this.pad = new SignaturePad(this.canvas)
|
this.pad = new SignaturePad(this.canvas)
|
||||||
|
|
||||||
|
this.resizeObserver = new ResizeObserver(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (!this.canvas) return
|
||||||
|
if (!this.canvas.parentNode?.clientWidth) return
|
||||||
|
|
||||||
|
const { width, height } = this.canvas
|
||||||
|
|
||||||
|
this.setCanvasSize()
|
||||||
|
|
||||||
|
if (this.canvas.width !== width || this.canvas.height !== height) {
|
||||||
|
this.redrawCanvas(width, height)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.resizeObserver.observe(this.canvas.parentNode)
|
||||||
|
|
||||||
if (this.dataset.color) {
|
if (this.dataset.color) {
|
||||||
this.pad.penColor = this.dataset.color
|
this.pad.penColor = this.dataset.color
|
||||||
}
|
}
|
||||||
@@ -57,6 +69,40 @@ window.customElements.define('draw-signature', class extends HTMLElement {
|
|||||||
this.updateSubmitButtonVisibility()
|
this.updateSubmitButtonVisibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
if (this.resizeObserver) {
|
||||||
|
this.resizeObserver.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setCanvasSize () {
|
||||||
|
const scale = 3
|
||||||
|
|
||||||
|
const width = this.canvas.parentNode.clientWidth
|
||||||
|
const height = this.canvas.parentNode.clientHeight
|
||||||
|
|
||||||
|
if (this.canvas.width !== width * scale || this.canvas.height !== height * scale) {
|
||||||
|
this.canvas.width = width * scale
|
||||||
|
this.canvas.height = height * scale
|
||||||
|
|
||||||
|
this.canvas.getContext('2d').scale(scale, scale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
redrawCanvas (oldWidth, oldHeight) {
|
||||||
|
if (this.pad && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0 && this.canvas.width > 0 && this.canvas.height > 0) {
|
||||||
|
const sx = this.canvas.width / oldWidth
|
||||||
|
const sy = this.canvas.height / oldHeight
|
||||||
|
|
||||||
|
const scaledData = this.pad.toData().map((stroke) => ({
|
||||||
|
...stroke,
|
||||||
|
points: stroke.points.map((p) => ({ ...p, x: p.x * sx, y: p.y * sy }))
|
||||||
|
}))
|
||||||
|
|
||||||
|
this.pad.fromData(scaledData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateSubmitButtonVisibility () {
|
updateSubmitButtonVisibility () {
|
||||||
if (this.pad.isEmpty()) {
|
if (this.pad.isEmpty()) {
|
||||||
this.submitButton.style.display = 'none'
|
this.submitButton.style.display = 'none'
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { target, targetable } from '@github/catalyst/lib/targetable'
|
||||||
|
|
||||||
|
export default targetable(class extends HTMLElement {
|
||||||
|
static [target.static] = [
|
||||||
|
'prompt',
|
||||||
|
'processing',
|
||||||
|
'logo'
|
||||||
|
]
|
||||||
|
|
||||||
|
connectedCallback () {
|
||||||
|
this.form.addEventListener('submit', this.onSubmit)
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
this.form.removeEventListener('submit', this.onSubmit)
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit = () => {
|
||||||
|
this.prompt.classList.add('hidden')
|
||||||
|
this.processing.classList.remove('hidden')
|
||||||
|
this.logo.classList.add('animate-bounce')
|
||||||
|
}
|
||||||
|
|
||||||
|
get form () {
|
||||||
|
return this.querySelector('form')
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
export function convertImage (sourceFile, targetType, quality) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
|
||||||
|
reader.onload = function (event) {
|
||||||
|
const img = new Image()
|
||||||
|
|
||||||
|
img.onload = function () {
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
|
||||||
|
canvas.width = img.width
|
||||||
|
canvas.height = img.height
|
||||||
|
ctx.drawImage(img, 0, 0)
|
||||||
|
canvas.toBlob(function (blob) {
|
||||||
|
const ext = targetType === 'image/jpeg' ? '.jpg' : '.png'
|
||||||
|
const newFile = new File([blob], sourceFile.name.replace(/\.\w+$/, ext), { type: targetType })
|
||||||
|
resolve(newFile)
|
||||||
|
}, targetType, quality)
|
||||||
|
}
|
||||||
|
|
||||||
|
img.onerror = () => reject(new Error(`browser cannot decode ${sourceFile.type || sourceFile.name}`))
|
||||||
|
|
||||||
|
img.src = event.target.result
|
||||||
|
}
|
||||||
|
reader.onerror = reject
|
||||||
|
reader.readAsDataURL(sourceFile)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function convertImagesInInput (input) {
|
||||||
|
if (!input.files || input.files.length === 0) return
|
||||||
|
|
||||||
|
const dt = new DataTransfer()
|
||||||
|
let didConvert = false
|
||||||
|
|
||||||
|
for (const file of Array.from(input.files)) {
|
||||||
|
let converted = file
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (['image/bmp', 'image/vnd.microsoft.icon', 'image/svg+xml', 'image/gif'].includes(file.type)) {
|
||||||
|
converted = await convertImage(file, 'image/png')
|
||||||
|
didConvert = true
|
||||||
|
} else if (['image/heic', 'image/heif', 'image/heic-sequence', 'image/heif-sequence', 'image/avif', 'image/avif-sequence', 'image/webp'].includes(file.type)) {
|
||||||
|
converted = await convertImage(file, 'image/jpeg', 0.9)
|
||||||
|
didConvert = true
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert(e.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
dt.items.add(converted)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (didConvert) {
|
||||||
|
input.files = dt.files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
const input = this.querySelector('input[type="file"]')
|
||||||
|
const form = input.form
|
||||||
|
|
||||||
|
input.addEventListener('change', async () => {
|
||||||
|
await convertImagesInInput(input)
|
||||||
|
|
||||||
|
form.querySelector('[type="submit"]')?.setAttribute('disabled', true)
|
||||||
|
|
||||||
|
form.requestSubmit()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
export default class extends HTMLElement {
|
|
||||||
connectedCallback () {
|
|
||||||
const input = this.querySelector('input')
|
|
||||||
const invalidMessage = this.dataset.invalidMessage || ''
|
|
||||||
|
|
||||||
input.addEventListener('invalid', () => {
|
|
||||||
input.setCustomValidity(input.value ? invalidMessage : '')
|
|
||||||
})
|
|
||||||
|
|
||||||
input.addEventListener('input', () => {
|
|
||||||
input.setCustomValidity('')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { target, targets, targetable } from '@github/catalyst/lib/targetable'
|
import { target, targets, targetable } from '@github/catalyst/lib/targetable'
|
||||||
|
import { convertImagesInInput } from './convert_upload'
|
||||||
|
|
||||||
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">
|
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 stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
@@ -150,12 +151,16 @@ export default targetable(class extends HTMLElement {
|
|||||||
if (!this.isLoading) this.hideDraghover()
|
if (!this.isLoading) this.hideDraghover()
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadFiles (files, url) {
|
async uploadFiles (files, url) {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
|
|
||||||
this.form.action = url
|
this.form.action = url
|
||||||
|
|
||||||
this.form.querySelector('[type="file"]').files = files
|
const input = this.form.querySelector('[type="file"]')
|
||||||
|
|
||||||
|
input.files = files
|
||||||
|
|
||||||
|
await convertImagesInInput(input)
|
||||||
|
|
||||||
this.form.querySelector('[type="submit"]').click()
|
this.form.querySelector('[type="submit"]').click()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ export default targetable(class extends HTMLElement {
|
|||||||
|
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.addEventListener('click', () => this.downloadFiles())
|
this.addEventListener('click', () => this.downloadFiles())
|
||||||
|
this.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault()
|
||||||
|
this.downloadFiles()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleState () {
|
toggleState () {
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.targetId = this.dataset.targetId
|
||||||
|
this.fieldId = this.dataset.fieldId
|
||||||
|
this.action = (this.dataset.action || '').trim()
|
||||||
|
this.expectedValue = this.dataset.value
|
||||||
|
|
||||||
|
this.targetEl = document.getElementById(this.targetId)
|
||||||
|
this.sourceEl = document.getElementById(this.fieldId)
|
||||||
|
|
||||||
|
this.bindListeners()
|
||||||
|
|
||||||
|
this.evaluateAndApply()
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
this.unbindListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
bindListeners () {
|
||||||
|
this.eventsFor(this.sourceEl).forEach((ev) => {
|
||||||
|
this.sourceEl.addEventListener(ev, this.evaluateAndApply)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
unbindListeners () {
|
||||||
|
this.eventsFor(this.sourceEl).forEach((ev) => {
|
||||||
|
this.sourceEl.removeEventListener(ev, this.evaluateAndApply)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
eventsFor (el) {
|
||||||
|
if (!el) return []
|
||||||
|
|
||||||
|
const tag = el.tagName.toLowerCase()
|
||||||
|
|
||||||
|
if (tag === 'textarea') return ['input']
|
||||||
|
if (tag === 'input') return ['input', 'change']
|
||||||
|
|
||||||
|
return ['change']
|
||||||
|
}
|
||||||
|
|
||||||
|
evaluateAndApply = () => {
|
||||||
|
const fieldConditions = document.querySelectorAll(`field-condition[data-target-id="${this.targetId}"]`)
|
||||||
|
|
||||||
|
const result = [...fieldConditions].reduce((acc, cond) => {
|
||||||
|
if (cond.dataset.operation === 'or') {
|
||||||
|
acc.push(acc.pop() || cond.checkCondition())
|
||||||
|
} else {
|
||||||
|
acc.push(cond.checkCondition())
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
this.apply(!result.includes(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
checkCondition () {
|
||||||
|
const action = this.action
|
||||||
|
const actual = this.getSourceValue()
|
||||||
|
const expected = this.expectedValue
|
||||||
|
|
||||||
|
if (action === 'empty' || action === 'unchecked') return this.isEmpty(actual)
|
||||||
|
if (action === 'not_empty' || action === 'checked') return !this.isEmpty(actual)
|
||||||
|
|
||||||
|
if (['equal', 'not_equal', 'greater_than', 'less_than'].includes(action) && this.sourceEl?.getAttribute('type') === 'number') {
|
||||||
|
if (this.isEmpty(actual) || this.isEmpty(expected)) return false
|
||||||
|
|
||||||
|
const actualNumber = parseFloat(actual)
|
||||||
|
const expectedNumber = parseFloat(expected)
|
||||||
|
|
||||||
|
if (Number.isNaN(actualNumber) || Number.isNaN(expectedNumber)) return false
|
||||||
|
|
||||||
|
if (action === 'equal') return Math.abs(actualNumber - expectedNumber) < Number.EPSILON
|
||||||
|
if (action === 'not_equal') return Math.abs(actualNumber - expectedNumber) > Number.EPSILON
|
||||||
|
if (action === 'greater_than') return actualNumber > expectedNumber
|
||||||
|
if (action === 'less_than') return actualNumber < expectedNumber
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'equal') {
|
||||||
|
const list = Array.isArray(actual) ? actual : [actual]
|
||||||
|
return list.filter((v) => v !== null && v !== undefined).map(String).includes(String(expected))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'contains') return this.contains(actual, expected)
|
||||||
|
|
||||||
|
if (action === 'not_equal') {
|
||||||
|
const list = Array.isArray(actual) ? actual : [actual]
|
||||||
|
return !list.filter((v) => v !== null && v !== undefined).map(String).includes(String(expected))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'does_not_contain') return !this.contains(actual, expected)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
getSourceValue () {
|
||||||
|
const el = this.sourceEl
|
||||||
|
|
||||||
|
if (!el) return
|
||||||
|
|
||||||
|
const tag = el.tagName.toLowerCase()
|
||||||
|
const type = (el.getAttribute('type') || '').toLowerCase()
|
||||||
|
|
||||||
|
if (tag === 'select') return el.value
|
||||||
|
if (tag === 'textarea') return el.value
|
||||||
|
if (tag === 'input' && type === 'checkbox') return el.checked ? (el.value || '1') : null
|
||||||
|
if (tag === 'input') return el.value
|
||||||
|
|
||||||
|
return el.value ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
isEmpty (obj) {
|
||||||
|
if (obj == null) return true
|
||||||
|
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
return obj.length === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj === 'string') {
|
||||||
|
return obj.trim().length === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj === 'object') {
|
||||||
|
return Object.keys(obj).length === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj === false) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
contains (actual, expected) {
|
||||||
|
if (expected === null || expected === undefined) return false
|
||||||
|
|
||||||
|
const exp = String(expected)
|
||||||
|
|
||||||
|
if (Array.isArray(actual)) return actual.filter((v) => v !== null && v !== undefined).map(String).includes(exp)
|
||||||
|
|
||||||
|
if (typeof actual === 'string') return actual.includes(exp)
|
||||||
|
|
||||||
|
return actual !== null && actual !== undefined && String(actual) === exp
|
||||||
|
}
|
||||||
|
|
||||||
|
apply (passed) {
|
||||||
|
const controls = this.targetEl.matches('input, select, textarea, button')
|
||||||
|
? [this.targetEl]
|
||||||
|
: Array.from(this.targetEl.querySelectorAll('input, select, textarea, button'))
|
||||||
|
|
||||||
|
if (passed) {
|
||||||
|
this.targetEl.style.display = ''
|
||||||
|
this.targetEl.labels.forEach((label) => { label.style.display = '' })
|
||||||
|
|
||||||
|
controls.forEach((c) => (c.disabled = false))
|
||||||
|
} else {
|
||||||
|
this.targetEl.style.display = 'none'
|
||||||
|
this.targetEl.labels.forEach((label) => { label.style.display = 'none' })
|
||||||
|
|
||||||
|
controls.forEach((c) => (c.disabled = true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { actionable } from '@github/catalyst/lib/actionable'
|
import { actionable } from '@github/catalyst/lib/actionable'
|
||||||
import { target, targetable } from '@github/catalyst/lib/targetable'
|
import { target, targetable } from '@github/catalyst/lib/targetable'
|
||||||
|
import { convertImagesInInput } from './convert_upload'
|
||||||
|
|
||||||
export default actionable(targetable(class extends HTMLElement {
|
export default actionable(targetable(class extends HTMLElement {
|
||||||
static [target.static] = [
|
static [target.static] = [
|
||||||
@@ -38,17 +39,21 @@ export default actionable(targetable(class extends HTMLElement {
|
|||||||
this.classList.add('border-base-300', 'hover:bg-base-200/30')
|
this.classList.add('border-base-300', 'hover:bg-base-200/30')
|
||||||
}
|
}
|
||||||
|
|
||||||
onDrop (e) {
|
async onDrop (e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
this.input.files = e.dataTransfer.files
|
this.input.files = e.dataTransfer.files
|
||||||
|
|
||||||
this.uploadFiles(e.dataTransfer.files)
|
await convertImagesInInput(this.input)
|
||||||
|
|
||||||
|
this.uploadFiles(this.input.files)
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelectFiles (e) {
|
async onSelectFiles (e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
|
await convertImagesInInput(this.input)
|
||||||
|
|
||||||
this.uploadFiles(this.input.files)
|
this.uploadFiles(this.input.files)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,382 @@
|
|||||||
|
import { target, targetable } from '@github/catalyst/lib/targetable'
|
||||||
|
import { actionable } from '@github/catalyst/lib/actionable'
|
||||||
|
|
||||||
|
function loadTiptap () {
|
||||||
|
return Promise.all([
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/core'),
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-bold'),
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-italic'),
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-paragraph'),
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-text'),
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-hard-break'),
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-document'),
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-link'),
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-underline'),
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extensions'),
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/markdown'),
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/pm/state'),
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/pm/view')
|
||||||
|
]).then(([core, bold, italic, paragraph, text, hardBreak, document, link, underline, extensions, markdown, pmState, pmView]) => ({
|
||||||
|
Editor: core.Editor,
|
||||||
|
Extension: core.Extension,
|
||||||
|
Bold: bold.default || bold,
|
||||||
|
Italic: italic.default || italic,
|
||||||
|
Paragraph: paragraph.default || paragraph,
|
||||||
|
Text: text.default || text,
|
||||||
|
HardBreak: hardBreak.default || hardBreak,
|
||||||
|
Document: document.default || document,
|
||||||
|
Link: link.default || link,
|
||||||
|
Underline: underline.default || underline,
|
||||||
|
UndoRedo: extensions.UndoRedo,
|
||||||
|
Markdown: markdown.Markdown,
|
||||||
|
Plugin: pmState.Plugin,
|
||||||
|
Decoration: pmView.Decoration,
|
||||||
|
DecorationSet: pmView.DecorationSet
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
class LinkTooltip {
|
||||||
|
constructor (container, editor, templateEl) {
|
||||||
|
this.container = container
|
||||||
|
this.editor = editor
|
||||||
|
|
||||||
|
this.tooltip = templateEl.content.firstElementChild.cloneNode(true)
|
||||||
|
|
||||||
|
this.input = this.tooltip.querySelector('input')
|
||||||
|
this.saveButton = this.tooltip.querySelector('[data-role="link-save"]')
|
||||||
|
this.removeButton = this.tooltip.querySelector('[data-role="link-remove"]')
|
||||||
|
|
||||||
|
container.style.position = 'relative'
|
||||||
|
container.appendChild(this.tooltip)
|
||||||
|
}
|
||||||
|
|
||||||
|
isVisible () {
|
||||||
|
return !this.tooltip.classList.contains('hidden')
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizeUrl (url) {
|
||||||
|
if (!url) return url
|
||||||
|
if (/^{/i.test(url)) return url
|
||||||
|
if (/^https?:\/\//i.test(url)) return url
|
||||||
|
if (/^mailto:/i.test(url)) return url
|
||||||
|
|
||||||
|
return `https://${url}`
|
||||||
|
}
|
||||||
|
|
||||||
|
show (url, pos, { focus = false } = {}) {
|
||||||
|
this.input.value = url || ''
|
||||||
|
this.removeButton.classList.toggle('hidden', !url)
|
||||||
|
|
||||||
|
this.tooltip.classList.remove('hidden')
|
||||||
|
|
||||||
|
const coords = this.editor.view.coordsAtPos(pos)
|
||||||
|
const containerRect = this.container.getBoundingClientRect()
|
||||||
|
|
||||||
|
this.tooltip.style.left = `${coords.left - containerRect.left}px`
|
||||||
|
this.tooltip.style.top = `${coords.bottom - containerRect.top + 4}px`
|
||||||
|
|
||||||
|
if (focus) this.input.focus()
|
||||||
|
|
||||||
|
this.saveHandler = () => {
|
||||||
|
const inputUrl = this.input.value.trim()
|
||||||
|
|
||||||
|
if (inputUrl) {
|
||||||
|
this.editor.chain().focus().extendMarkRange('link').setLink({ href: this.normalizeUrl(inputUrl) }).run()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.removeHandler = () => {
|
||||||
|
this.editor.chain().focus().extendMarkRange('link').unsetLink().run()
|
||||||
|
|
||||||
|
this.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.keyHandler = (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault()
|
||||||
|
this.saveHandler()
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
e.preventDefault()
|
||||||
|
this.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveButton.addEventListener('click', this.saveHandler, { once: true })
|
||||||
|
this.removeButton.addEventListener('click', this.removeHandler, { once: true })
|
||||||
|
this.input.addEventListener('keydown', this.keyHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
hide () {
|
||||||
|
if (this.saveHandler) {
|
||||||
|
this.saveButton.removeEventListener('click', this.saveHandler)
|
||||||
|
this.saveHandler = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.removeHandler) {
|
||||||
|
this.removeButton.removeEventListener('click', this.removeHandler)
|
||||||
|
this.removeHandler = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.keyHandler) {
|
||||||
|
this.input.removeEventListener('keydown', this.keyHandler)
|
||||||
|
this.keyHandler = null
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tooltip.classList.add('hidden')
|
||||||
|
this.currentMark = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default actionable(targetable(class extends HTMLElement {
|
||||||
|
static [target.static] = [
|
||||||
|
'textarea',
|
||||||
|
'editorElement',
|
||||||
|
'boldButton',
|
||||||
|
'italicButton',
|
||||||
|
'underlineButton',
|
||||||
|
'linkButton',
|
||||||
|
'linkTooltipTemplate'
|
||||||
|
]
|
||||||
|
|
||||||
|
async connectedCallback () {
|
||||||
|
if (!this.textarea || !this.editorElement) return
|
||||||
|
|
||||||
|
this.textarea.style.display = 'none'
|
||||||
|
this.adjustShortcutsForPlatform()
|
||||||
|
|
||||||
|
const { Editor, Extension, Bold, Italic, Paragraph, Text, HardBreak, UndoRedo, Document, Link, Underline, Markdown, Plugin, Decoration, DecorationSet } = await loadTiptap()
|
||||||
|
|
||||||
|
const buildDecorations = (doc) => {
|
||||||
|
const decorations = []
|
||||||
|
const regex = /\{\{?[a-zA-Z0-9_.-]+\}\}?/g
|
||||||
|
|
||||||
|
doc.descendants((node, pos) => {
|
||||||
|
if (!node.isText) return
|
||||||
|
|
||||||
|
let match
|
||||||
|
|
||||||
|
while ((match = regex.exec(node.text)) !== null) {
|
||||||
|
decorations.push(
|
||||||
|
Decoration.inline(pos + match.index, pos + match.index + match[0].length, {
|
||||||
|
class: 'bg-amber-100 py-0.5 px-1 rounded'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return DecorationSet.create(doc, decorations)
|
||||||
|
}
|
||||||
|
|
||||||
|
const VariableHighlight = Extension.create({
|
||||||
|
name: 'variableHighlight',
|
||||||
|
addProseMirrorPlugins () {
|
||||||
|
return [new Plugin({
|
||||||
|
state: {
|
||||||
|
init (_, { doc }) {
|
||||||
|
return buildDecorations(doc)
|
||||||
|
},
|
||||||
|
apply (tr, oldSet) {
|
||||||
|
return tr.docChanged ? buildDecorations(tr.doc) : oldSet
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
decorations (state) {
|
||||||
|
return this.getState(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.editor = new Editor({
|
||||||
|
element: this.editorElement,
|
||||||
|
extensions: [
|
||||||
|
Markdown,
|
||||||
|
Document,
|
||||||
|
Paragraph,
|
||||||
|
Text,
|
||||||
|
Bold,
|
||||||
|
Italic,
|
||||||
|
HardBreak.extend({
|
||||||
|
addKeyboardShortcuts () {
|
||||||
|
return {
|
||||||
|
Enter: () => this.editor.commands.setHardBreak()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
UndoRedo,
|
||||||
|
Link.extend({
|
||||||
|
inclusive: true,
|
||||||
|
addKeyboardShortcuts: () => ({
|
||||||
|
'Mod-k': () => {
|
||||||
|
this.toggleLink()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}).configure({
|
||||||
|
openOnClick: false,
|
||||||
|
HTMLAttributes: {
|
||||||
|
class: 'link',
|
||||||
|
'data-turbo': 'false',
|
||||||
|
style: 'color: #2563eb; text-decoration: underline; cursor: text;'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Underline,
|
||||||
|
VariableHighlight
|
||||||
|
],
|
||||||
|
content: (this.textarea.value || '').trim().replace(/ *\n/g, '<br>'),
|
||||||
|
contentType: 'markdown',
|
||||||
|
editorProps: {
|
||||||
|
attributes: {
|
||||||
|
style: 'min-height: 220px',
|
||||||
|
dir: 'auto',
|
||||||
|
class: 'p-3 outline-none focus:outline-none'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onUpdate: ({ editor }) => {
|
||||||
|
this.textarea.value = editor.getMarkdown()
|
||||||
|
this.textarea.dispatchEvent(new Event('input', { bubbles: true }))
|
||||||
|
},
|
||||||
|
onSelectionUpdate: ({ editor }) => {
|
||||||
|
this.updateToolbarState()
|
||||||
|
this.handleLinkTooltip(editor)
|
||||||
|
},
|
||||||
|
onBlur: () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.linkTooltip.tooltip.contains(document.activeElement)) {
|
||||||
|
this.linkTooltip.hide()
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.linkTooltip = new LinkTooltip(this, this.editor, this.linkTooltipTemplate)
|
||||||
|
}
|
||||||
|
|
||||||
|
adjustShortcutsForPlatform () {
|
||||||
|
if ((navigator.userAgentData?.platform || navigator.platform)?.toLowerCase()?.includes('mac')) {
|
||||||
|
this.querySelectorAll('.tooltip[data-tip]').forEach(tooltip => {
|
||||||
|
const tip = tooltip.getAttribute('data-tip')
|
||||||
|
|
||||||
|
if (tip && tip.includes('Ctrl')) {
|
||||||
|
tooltip.setAttribute('data-tip', tip.replace(/Ctrl/g, '⌘'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bold (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.editor.chain().focus().toggleBold().run()
|
||||||
|
this.updateToolbarState()
|
||||||
|
}
|
||||||
|
|
||||||
|
italic (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.editor.chain().focus().toggleItalic().run()
|
||||||
|
this.updateToolbarState()
|
||||||
|
}
|
||||||
|
|
||||||
|
underline (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.editor.chain().focus().toggleUnderline().run()
|
||||||
|
this.updateToolbarState()
|
||||||
|
}
|
||||||
|
|
||||||
|
linkSelection (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.toggleLink()
|
||||||
|
this.updateToolbarState()
|
||||||
|
}
|
||||||
|
|
||||||
|
undo (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.editor.chain().focus().undo().run()
|
||||||
|
this.updateToolbarState()
|
||||||
|
}
|
||||||
|
|
||||||
|
redo (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.editor.chain().focus().redo().run()
|
||||||
|
this.updateToolbarState()
|
||||||
|
}
|
||||||
|
|
||||||
|
updateToolbarState () {
|
||||||
|
this.boldButton.classList.toggle('bg-base-200', this.editor.isActive('bold'))
|
||||||
|
this.italicButton.classList.toggle('bg-base-200', this.editor.isActive('italic'))
|
||||||
|
this.underlineButton.classList.toggle('bg-base-200', this.editor.isActive('underline'))
|
||||||
|
this.linkButton.classList.toggle('bg-base-200', this.editor.isActive('link'))
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLinkTooltip (editor) {
|
||||||
|
const { from } = editor.state.selection
|
||||||
|
const mark = editor.state.doc.resolve(from).marks().find(m => m.type.name === 'link')
|
||||||
|
|
||||||
|
if (!mark) {
|
||||||
|
if (this.linkTooltip.isVisible()) this.linkTooltip.hide()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.linkTooltip.isVisible() && this.linkTooltip.currentMark === mark) return
|
||||||
|
|
||||||
|
let linkStart = from
|
||||||
|
const start = editor.state.doc.resolve(from).start()
|
||||||
|
|
||||||
|
for (let i = from - 1; i >= start; i--) {
|
||||||
|
if (editor.state.doc.resolve(i).marks().some(m => m.eq(mark))) {
|
||||||
|
linkStart = i
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.linkTooltip.hide()
|
||||||
|
this.linkTooltip.show(mark.attrs.href, linkStart > start ? linkStart - 1 : linkStart)
|
||||||
|
this.linkTooltip.currentMark = mark
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleLink () {
|
||||||
|
if (this.editor.isActive('link')) {
|
||||||
|
this.linkTooltip.hide()
|
||||||
|
this.editor.chain().focus().extendMarkRange('link').unsetLink().run()
|
||||||
|
this.updateToolbarState()
|
||||||
|
} else {
|
||||||
|
const { from } = this.editor.state.selection
|
||||||
|
|
||||||
|
this.linkTooltip.hide()
|
||||||
|
this.linkTooltip.show(this.editor.getAttributes('link').href, from, { focus: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
insertVariable (e) {
|
||||||
|
const variable = e.target.closest('[data-variable]')?.dataset.variable
|
||||||
|
|
||||||
|
if (variable) {
|
||||||
|
const { from, to } = this.editor.state.selection
|
||||||
|
|
||||||
|
if (variable.includes('link') && from !== to) {
|
||||||
|
this.editor.chain().focus().setLink({ href: `{${variable}}` }).run()
|
||||||
|
} else {
|
||||||
|
this.editor.chain().focus().insertContent(`{${variable}}`).run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
this.linkTooltip.hide()
|
||||||
|
|
||||||
|
if (this.editor) {
|
||||||
|
this.editor.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
const dialog = document.getElementById(this.dataset.target)
|
||||||
|
|
||||||
|
this.querySelector('button').addEventListener('click', () => {
|
||||||
|
if (dialog) {
|
||||||
|
dialog.inert = false
|
||||||
|
dialog.showModal()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (dialog) {
|
||||||
|
dialog.addEventListener('close', () => {
|
||||||
|
dialog.inert = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,11 +47,13 @@ export default class extends HTMLElement {
|
|||||||
|
|
||||||
this.classList.remove('hidden', '-translate-y-10', 'opacity-0')
|
this.classList.remove('hidden', '-translate-y-10', 'opacity-0')
|
||||||
this.classList.add('translate-y-0', 'opacity-100')
|
this.classList.add('translate-y-0', 'opacity-100')
|
||||||
|
this.inert = false
|
||||||
}
|
}
|
||||||
|
|
||||||
hideButtons () {
|
hideButtons () {
|
||||||
this.classList.remove('translate-y-0', 'opacity-100')
|
this.classList.remove('translate-y-0', 'opacity-100')
|
||||||
this.classList.add('-translate-y-10', 'opacity-0')
|
this.classList.add('-translate-y-10', 'opacity-0')
|
||||||
|
this.inert = true
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (this.classList.contains('-translate-y-10')) {
|
if (this.classList.contains('-translate-y-10')) {
|
||||||
|
|||||||
@@ -5,17 +5,29 @@ export default targetable(class extends HTMLElement {
|
|||||||
static [target.static] = ['canvas', 'input', 'clear', 'button']
|
static [target.static] = ['canvas', 'input', 'clear', 'button']
|
||||||
|
|
||||||
async connectedCallback () {
|
async connectedCallback () {
|
||||||
const scale = 3
|
|
||||||
|
|
||||||
this.canvas.width = this.canvas.parentNode.clientWidth * scale
|
|
||||||
this.canvas.height = this.canvas.parentNode.clientHeight * scale
|
|
||||||
|
|
||||||
this.canvas.getContext('2d').scale(scale, scale)
|
|
||||||
|
|
||||||
const { default: SignaturePad } = await import('signature_pad')
|
const { default: SignaturePad } = await import('signature_pad')
|
||||||
|
|
||||||
|
this.setCanvasSize()
|
||||||
|
|
||||||
this.pad = new SignaturePad(this.canvas)
|
this.pad = new SignaturePad(this.canvas)
|
||||||
|
|
||||||
|
this.resizeObserver = new ResizeObserver(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (!this.canvas) return
|
||||||
|
if (!this.canvas.parentNode?.clientWidth) return
|
||||||
|
|
||||||
|
const { width, height } = this.canvas
|
||||||
|
|
||||||
|
this.setCanvasSize()
|
||||||
|
|
||||||
|
if (this.canvas.width !== width || this.canvas.height !== height) {
|
||||||
|
this.redrawCanvas(width, height)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.resizeObserver.observe(this.canvas.parentNode)
|
||||||
|
|
||||||
this.clear.addEventListener('click', (e) => {
|
this.clear.addEventListener('click', (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
@@ -47,4 +59,38 @@ export default targetable(class extends HTMLElement {
|
|||||||
|
|
||||||
this.closest('form').requestSubmit()
|
this.closest('form').requestSubmit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
if (this.resizeObserver) {
|
||||||
|
this.resizeObserver.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setCanvasSize () {
|
||||||
|
const scale = 3
|
||||||
|
|
||||||
|
const width = this.canvas.parentNode.clientWidth
|
||||||
|
const height = this.canvas.parentNode.clientWidth / 2.5
|
||||||
|
|
||||||
|
if (this.canvas.width !== width * scale || this.canvas.height !== height * scale) {
|
||||||
|
this.canvas.width = width * scale
|
||||||
|
this.canvas.height = height * scale
|
||||||
|
|
||||||
|
this.canvas.getContext('2d').scale(scale, scale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
redrawCanvas (oldWidth, oldHeight) {
|
||||||
|
if (this.pad && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0 && this.canvas.width > 0 && this.canvas.height > 0) {
|
||||||
|
const sx = this.canvas.width / oldWidth
|
||||||
|
const sy = this.canvas.height / oldHeight
|
||||||
|
|
||||||
|
const scaledData = this.pad.toData().map((stroke) => ({
|
||||||
|
...stroke,
|
||||||
|
points: stroke.points.map((p) => ({ ...p, x: p.x * sx, y: p.y * sy }))
|
||||||
|
}))
|
||||||
|
|
||||||
|
this.pad.fromData(scaledData)
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
@@ -1,12 +1,18 @@
|
|||||||
export default class extends HTMLElement {
|
export default class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.input.addEventListener('change', (event) => {
|
this.input.addEventListener('change', (event) => {
|
||||||
|
if (!this.target) return
|
||||||
|
|
||||||
|
const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value
|
||||||
|
const dataValue = this.dataset.value === 'false' ? false : this.dataset.value || true
|
||||||
|
|
||||||
if (this.dataset.attribute) {
|
if (this.dataset.attribute) {
|
||||||
this.target[this.dataset.attribute] = event.target.checked
|
this.target[this.dataset.attribute] = value === dataValue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.dataset.className) {
|
if (this.dataset.className) {
|
||||||
this.target.classList.toggle(this.dataset.className, event.target.value !== this.dataset.value)
|
this.target.classList.toggle(this.dataset.className, value !== dataValue)
|
||||||
|
|
||||||
if (this.dataset.className === 'hidden' && this.target.tagName === 'INPUT') {
|
if (this.dataset.className === 'hidden' && this.target.tagName === 'INPUT') {
|
||||||
this.target.disabled = event.target.value !== this.dataset.value
|
this.target.disabled = event.target.value !== this.dataset.value
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
export default class extends HTMLElement {
|
export default class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
const button = this.querySelector('a, button')
|
const button = this.querySelector('a, button, label')
|
||||||
|
|
||||||
|
const target = this.dataset.targetId ? document.getElementById(this.dataset.targetId) : button
|
||||||
|
|
||||||
button.addEventListener('click', () => {
|
button.addEventListener('click', () => {
|
||||||
this.dataset.classes.split(' ').forEach((cls) => {
|
this.dataset.classes.split(' ').forEach((cls) => {
|
||||||
button.classList.toggle(cls)
|
if (this.dataset.action === 'remove') {
|
||||||
|
target.classList.remove(cls)
|
||||||
|
} else if (this.dataset.action === 'add') {
|
||||||
|
target.classList.add(cls)
|
||||||
|
} else {
|
||||||
|
target.classList.toggle(cls)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,6 @@ export default class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get button () {
|
get button () {
|
||||||
return this.querySelector('button')
|
return this.querySelector('button, label')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ import FetchForm from './elements/fetch_form'
|
|||||||
import ScrollButtons from './elements/scroll_buttons'
|
import ScrollButtons from './elements/scroll_buttons'
|
||||||
import PageContainer from './elements/page_container'
|
import PageContainer from './elements/page_container'
|
||||||
import SubmitForm from './elements/submit_form'
|
import SubmitForm from './elements/submit_form'
|
||||||
|
import ModalButton from './elements/modal_button'
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ safeRegisterElement('fetch-form', FetchForm)
|
|||||||
safeRegisterElement('scroll-buttons', ScrollButtons)
|
safeRegisterElement('scroll-buttons', ScrollButtons)
|
||||||
safeRegisterElement('page-container', PageContainer)
|
safeRegisterElement('page-container', PageContainer)
|
||||||
safeRegisterElement('submit-form', SubmitForm)
|
safeRegisterElement('submit-form', SubmitForm)
|
||||||
|
safeRegisterElement('modal-button', ModalButton)
|
||||||
safeRegisterElement('submission-form', class extends HTMLElement {
|
safeRegisterElement('submission-form', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.appElem = document.createElement('div')
|
this.appElem = document.createElement('div')
|
||||||
@@ -31,12 +33,14 @@ safeRegisterElement('submission-form', class extends HTMLElement {
|
|||||||
isDemo: this.dataset.isDemo === 'true',
|
isDemo: this.dataset.isDemo === 'true',
|
||||||
attribution: this.dataset.attribution !== 'false',
|
attribution: this.dataset.attribution !== 'false',
|
||||||
scrollPadding: this.dataset.scrollPadding || '-80px',
|
scrollPadding: this.dataset.scrollPadding || '-80px',
|
||||||
|
signatureText: this.dataset.signatureText,
|
||||||
language: this.dataset.language,
|
language: this.dataset.language,
|
||||||
dryRun: this.dataset.dryRun === 'true',
|
dryRun: this.dataset.dryRun === 'true',
|
||||||
expand: ['true', 'false'].includes(this.dataset.expand) ? this.dataset.expand === 'true' : null,
|
expand: ['true', 'false'].includes(this.dataset.expand) ? this.dataset.expand === 'true' : null,
|
||||||
withSignatureId: this.dataset.withSignatureId === 'true',
|
withSignatureId: this.dataset.withSignatureId === 'true',
|
||||||
requireSigningReason: this.dataset.requireSigningReason === 'true',
|
requireSigningReason: this.dataset.requireSigningReason === 'true',
|
||||||
withConfetti: this.dataset.withConfetti !== 'false',
|
withConfetti: this.dataset.withConfetti === 'true',
|
||||||
|
withFieldLabels: this.dataset.withFieldLabels !== 'false',
|
||||||
withDisclosure: this.dataset.withDisclosure === 'true',
|
withDisclosure: this.dataset.withDisclosure === 'true',
|
||||||
reuseSignature: this.dataset.reuseSignature !== 'false',
|
reuseSignature: this.dataset.reuseSignature !== 'false',
|
||||||
withTypedSignature: this.dataset.withTypedSignature !== 'false',
|
withTypedSignature: this.dataset.withTypedSignature !== 'false',
|
||||||
@@ -48,7 +52,9 @@ safeRegisterElement('submission-form', class extends HTMLElement {
|
|||||||
completedMessage: JSON.parse(this.dataset.completedMessage || '{}'),
|
completedMessage: JSON.parse(this.dataset.completedMessage || '{}'),
|
||||||
completedRedirectUrl: this.dataset.completedRedirectUrl,
|
completedRedirectUrl: this.dataset.completedRedirectUrl,
|
||||||
attachments: reactive(JSON.parse(this.dataset.attachments)),
|
attachments: reactive(JSON.parse(this.dataset.attachments)),
|
||||||
fields: JSON.parse(this.dataset.fields)
|
fields: JSON.parse(this.dataset.fields),
|
||||||
|
completeButtonContainer: document.getElementById('complete_button_container'),
|
||||||
|
completeButtonScrollContainer: document.getElementById('complete_button_container_scroll')
|
||||||
})
|
})
|
||||||
|
|
||||||
this.app.mount(this.appElem)
|
this.app.mount(this.appElem)
|
||||||
|
|||||||
@@ -0,0 +1,434 @@
|
|||||||
|
<template>
|
||||||
|
<template
|
||||||
|
v-for="(pages, docUuid) in textRuns"
|
||||||
|
:key="docUuid"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-for="(_, pageIndex) in pages"
|
||||||
|
:key="pageIndex"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-for="(pageElem, i) in [findPageElement(docUuid, pageIndex)]"
|
||||||
|
:key="i"
|
||||||
|
>
|
||||||
|
<Teleport
|
||||||
|
v-if="pageElem"
|
||||||
|
:to="pageElem"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-for="(item, index) in sortedItemsForPage(docUuid, pageIndex)"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<template v-if="item.type === 'text_group'">
|
||||||
|
<span
|
||||||
|
class="absolute overflow-hidden text-transparent select-none pointer-events-none"
|
||||||
|
:style="{ left: item.x * 100 + '%', top: item.y * 100 + '%', width: item.w * 100 + '%', height: item.h * 100 + '%' }"
|
||||||
|
>{{ item.text }}</span>
|
||||||
|
<span
|
||||||
|
v-for="(run, runIndex) in item.items"
|
||||||
|
:key="runIndex"
|
||||||
|
aria-hidden="true"
|
||||||
|
class="absolute overflow-hidden text-transparent"
|
||||||
|
:style="{ left: run.x * 100 + '%', top: run.y * 100 + '%', width: run.w * 100 + '%', height: run.h * 100 + '%', fontSize: run.font_size ? (run.font_size / 10) + 'cqmin' : undefined, textAlign: 'justify', textAlignLast: 'justify', textJustify: 'inter-character' }"
|
||||||
|
>{{ run.text }}</span>
|
||||||
|
</template>
|
||||||
|
<FieldArea
|
||||||
|
v-else-if="item.type === 'field_area'"
|
||||||
|
:ref="setAreaRef"
|
||||||
|
v-model="values[item.field.uuid]"
|
||||||
|
:values="values"
|
||||||
|
:field="item.field"
|
||||||
|
:area="item.area"
|
||||||
|
:submittable="true"
|
||||||
|
:page-width="1400"
|
||||||
|
:page-height="(1400.0 / pageElem.offsetWidth) * pageElem.offsetHeight"
|
||||||
|
:field-index="item.fieldIndex"
|
||||||
|
:is-inline-size="isInlineSize"
|
||||||
|
:scroll-padding="scrollPadding"
|
||||||
|
:submitter="submitter"
|
||||||
|
:with-field-placeholder="withFieldPlaceholder"
|
||||||
|
:with-signature-id="withSignatureId"
|
||||||
|
:is-active="currentStep === item.step"
|
||||||
|
:with-label="withLabel && !withFieldPlaceholder && item.step.length < 2"
|
||||||
|
:is-value-set="item.step.some((f) => f.uuid in values)"
|
||||||
|
:attachments-index="attachmentsIndex"
|
||||||
|
@click="[$emit('focus-step', item.stepIndex), maybeScrollOnClick(item.field, item.area)]"
|
||||||
|
/>
|
||||||
|
<FieldArea
|
||||||
|
v-else-if="item.type === 'readonly_field_area'"
|
||||||
|
:model-value="readonlyConditionalFieldValues[item.field.uuid]"
|
||||||
|
:values="readonlyConditionalFieldValues"
|
||||||
|
:field="item.field"
|
||||||
|
:area="item.area"
|
||||||
|
:submittable="false"
|
||||||
|
:page-width="1400"
|
||||||
|
:page-height="(1400.0 / pageElem.offsetWidth) * pageElem.offsetHeight"
|
||||||
|
:field-index="item.fieldIndex"
|
||||||
|
:is-inline-size="isInlineSize"
|
||||||
|
:submitter="submitter"
|
||||||
|
:attachments-index="attachmentsIndex"
|
||||||
|
/>
|
||||||
|
<FieldArea
|
||||||
|
v-else-if="item.type === 'formula_area' && isMathLoaded"
|
||||||
|
:model-value="calculateFormula(item.field)"
|
||||||
|
:is-inline-size="isInlineSize"
|
||||||
|
:field="item.field"
|
||||||
|
:area="item.area"
|
||||||
|
:submittable="false"
|
||||||
|
:field-index="item.fieldIndex"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import FieldArea from './area'
|
||||||
|
import FormulaAreas from './formula_areas'
|
||||||
|
import FieldAreas from './areas'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'AccessibilityAreas',
|
||||||
|
components: {
|
||||||
|
FieldArea
|
||||||
|
},
|
||||||
|
inject: ['baseUrl', 't'],
|
||||||
|
props: {
|
||||||
|
submitterSlug: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
filledFieldsIndex: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
steps: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
readonlyConditionalFields: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
readonlyConditionalFieldValues: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
formulaFields: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
readonlyValues: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
submitter: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
currentStep: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
withFieldPlaceholder: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
withSignatureId: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
withLabel: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
scrollPadding: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '-80px'
|
||||||
|
},
|
||||||
|
scrollEl: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
attachmentsIndex: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
fetchOptions: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['focus-step'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isMathLoaded: false,
|
||||||
|
math: null,
|
||||||
|
textRuns: {},
|
||||||
|
areaRefs: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
fieldValuesIndex () {
|
||||||
|
return this.filledFieldsIndex || this.extractStaticValues()
|
||||||
|
},
|
||||||
|
isMobileContainer: FieldAreas.computed.isMobileContainer,
|
||||||
|
isInlineSize: FieldAreas.computed.isInlineSize,
|
||||||
|
fieldsUuidIndex () {
|
||||||
|
return this.formulaFields.reduce((acc, field) => {
|
||||||
|
acc[field.uuid] = field
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
fieldAreasIndex () {
|
||||||
|
const index = Object.create(null)
|
||||||
|
|
||||||
|
this.steps.forEach((step, stepIndex) => {
|
||||||
|
step.forEach((field, fieldIndex) => {
|
||||||
|
(field.areas || []).forEach((area) => {
|
||||||
|
index[area.attachment_uuid] ||= Object.create(null)
|
||||||
|
index[area.attachment_uuid][area.page] ||= []
|
||||||
|
index[area.attachment_uuid][area.page].push({
|
||||||
|
type: 'field_area',
|
||||||
|
field,
|
||||||
|
area,
|
||||||
|
step,
|
||||||
|
stepIndex,
|
||||||
|
fieldIndex,
|
||||||
|
x: area.x,
|
||||||
|
y: area.y,
|
||||||
|
w: area.w,
|
||||||
|
h: area.h
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return index
|
||||||
|
},
|
||||||
|
formulaAreasIndex () {
|
||||||
|
const index = Object.create(null)
|
||||||
|
|
||||||
|
this.formulaFields.forEach((field, fieldIndex) => {
|
||||||
|
(field.areas || []).forEach((area) => {
|
||||||
|
index[area.attachment_uuid] ||= Object.create(null)
|
||||||
|
index[area.attachment_uuid][area.page] ||= []
|
||||||
|
index[area.attachment_uuid][area.page].push({
|
||||||
|
type: 'formula_area',
|
||||||
|
field,
|
||||||
|
area,
|
||||||
|
fieldIndex,
|
||||||
|
x: area.x,
|
||||||
|
y: area.y,
|
||||||
|
w: area.w,
|
||||||
|
h: area.h
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return index
|
||||||
|
},
|
||||||
|
readonlyFieldAreasIndex () {
|
||||||
|
const index = Object.create(null)
|
||||||
|
|
||||||
|
this.readonlyConditionalFields.forEach((field, fieldIndex) => {
|
||||||
|
(field.areas || []).forEach((area) => {
|
||||||
|
index[area.attachment_uuid] ||= Object.create(null)
|
||||||
|
index[area.attachment_uuid][area.page] ||= []
|
||||||
|
index[area.attachment_uuid][area.page].push({
|
||||||
|
type: 'readonly_field_area',
|
||||||
|
field,
|
||||||
|
area,
|
||||||
|
fieldIndex,
|
||||||
|
x: area.x,
|
||||||
|
y: area.y,
|
||||||
|
w: area.w,
|
||||||
|
h: area.h
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeUpdate () {
|
||||||
|
this.areaRefs = []
|
||||||
|
},
|
||||||
|
async mounted () {
|
||||||
|
const [metadataResult] = await Promise.all([
|
||||||
|
fetch(this.baseUrl + `/s/${this.submitterSlug}/metadata`, {
|
||||||
|
...this.fetchOptions
|
||||||
|
}).then((r) => r.json()).catch(() => ({})),
|
||||||
|
this.loadMath()
|
||||||
|
])
|
||||||
|
|
||||||
|
this.textRuns = metadataResult.text_runs || {}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
normalizeFormula: FormulaAreas.methods.normalizeFormula,
|
||||||
|
calculateFormula: FormulaAreas.methods.calculateFormula,
|
||||||
|
scrollInContainer: FieldAreas.methods.scrollInContainer,
|
||||||
|
scrollIntoArea: FieldAreas.methods.scrollIntoArea,
|
||||||
|
scrollIntoField: FieldAreas.methods.scrollIntoField,
|
||||||
|
maybeScrollOnClick: FieldAreas.methods.maybeScrollOnClick,
|
||||||
|
setAreaRef (el) {
|
||||||
|
if (el) {
|
||||||
|
this.areaRefs.push(el)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadMath () {
|
||||||
|
if (this.formulaFields.length && !this.isMathLoaded) {
|
||||||
|
const { Calculator } = await import('./calculator')
|
||||||
|
|
||||||
|
this.math = new Calculator()
|
||||||
|
this.isMathLoaded = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extractStaticValues () {
|
||||||
|
const result = Object.create(null)
|
||||||
|
const root = this.$root.$el?.parentNode?.getRootNode() || document
|
||||||
|
const pageContainers = root.querySelectorAll('page-container')
|
||||||
|
|
||||||
|
pageContainers.forEach((container) => {
|
||||||
|
const overlay = container.querySelector('[id^="page-"]')
|
||||||
|
|
||||||
|
if (!overlay) return
|
||||||
|
|
||||||
|
const parts = overlay.id.split('-')
|
||||||
|
const pageIndex = parseInt(parts[parts.length - 1])
|
||||||
|
const docUuid = parts.slice(1, -1).join('-')
|
||||||
|
|
||||||
|
const fieldValues = overlay.querySelectorAll('field-value')
|
||||||
|
|
||||||
|
if (!fieldValues.length) return
|
||||||
|
|
||||||
|
result[docUuid] ||= Object.create(null)
|
||||||
|
result[docUuid][pageIndex] = []
|
||||||
|
|
||||||
|
fieldValues.forEach((el) => {
|
||||||
|
const style = el.style
|
||||||
|
const x = parseFloat(style.left) / 100
|
||||||
|
const y = parseFloat(style.top) / 100
|
||||||
|
const w = parseFloat(style.width) / 100
|
||||||
|
const h = parseFloat(style.height) / 100
|
||||||
|
const text = el.textContent.trim()
|
||||||
|
|
||||||
|
if (text) {
|
||||||
|
result[docUuid][pageIndex].push({ type: 'static_value', text, x, y, w, h })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
findPageElement (docUuid, pageIndex) {
|
||||||
|
return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${docUuid}-${pageIndex}`)
|
||||||
|
},
|
||||||
|
sortedItemsForPage (docUuid, pageIndex) {
|
||||||
|
const items = []
|
||||||
|
|
||||||
|
const pageTextRuns = this.textRuns[docUuid]?.[pageIndex] || []
|
||||||
|
|
||||||
|
pageTextRuns.forEach((run) => {
|
||||||
|
items.push({
|
||||||
|
type: 'text_run',
|
||||||
|
text: run.text,
|
||||||
|
x: run.x,
|
||||||
|
y: run.y,
|
||||||
|
w: run.w,
|
||||||
|
h: run.h,
|
||||||
|
font_size: run.font_size
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const fieldAreas = this.fieldAreasIndex[docUuid]?.[pageIndex] || []
|
||||||
|
items.push(...fieldAreas)
|
||||||
|
|
||||||
|
const readonlyFieldAreas = this.readonlyFieldAreasIndex[docUuid]?.[pageIndex] || []
|
||||||
|
items.push(...readonlyFieldAreas)
|
||||||
|
|
||||||
|
const formulaAreas = this.formulaAreasIndex[docUuid]?.[pageIndex] || []
|
||||||
|
items.push(...formulaAreas)
|
||||||
|
|
||||||
|
const pageFieldValues = this.fieldValuesIndex[docUuid]?.[pageIndex] || []
|
||||||
|
items.push(...pageFieldValues)
|
||||||
|
|
||||||
|
items.sort((a, b) => {
|
||||||
|
const aCenterY = a.y + a.h / 2
|
||||||
|
const bCenterY = b.y + b.h / 2
|
||||||
|
const lineThreshold = Math.min(a.h, b.h) / 2
|
||||||
|
|
||||||
|
if (Math.abs(aCenterY - bCenterY) < lineThreshold) {
|
||||||
|
return a.x - b.x
|
||||||
|
}
|
||||||
|
|
||||||
|
return aCenterY - bCenterY
|
||||||
|
})
|
||||||
|
|
||||||
|
const grouped = []
|
||||||
|
let currentGroup = null
|
||||||
|
|
||||||
|
const closeGroup = () => {
|
||||||
|
if (!currentGroup) return
|
||||||
|
|
||||||
|
const groupItems = currentGroup.items
|
||||||
|
const minX = Math.min(...groupItems.map((i) => i.x))
|
||||||
|
const minY = Math.min(...groupItems.map((i) => i.y))
|
||||||
|
const maxEndX = Math.max(...groupItems.map((i) => i.x + i.w))
|
||||||
|
const maxEndY = Math.max(...groupItems.map((i) => i.y + i.h))
|
||||||
|
|
||||||
|
currentGroup.x = minX
|
||||||
|
currentGroup.y = minY
|
||||||
|
currentGroup.w = maxEndX - minX
|
||||||
|
currentGroup.h = maxEndY - minY
|
||||||
|
currentGroup.text = groupItems.map((i) => i.text).join(' ').replace(/\s+/g, ' ').trim()
|
||||||
|
|
||||||
|
grouped.push(currentGroup)
|
||||||
|
currentGroup = null
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const isTextLike = item.type === 'text_run' || item.type === 'static_value'
|
||||||
|
|
||||||
|
if (!isTextLike) {
|
||||||
|
closeGroup()
|
||||||
|
grouped.push(item)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentGroup) {
|
||||||
|
currentGroup = { type: 'text_group', items: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
currentGroup.items.push(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
closeGroup()
|
||||||
|
|
||||||
|
return grouped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -42,10 +42,25 @@ export default {
|
|||||||
const areas = {}
|
const areas = {}
|
||||||
|
|
||||||
this.field.areas?.forEach((area) => {
|
this.field.areas?.forEach((area) => {
|
||||||
areas[area.attachment_uuid + area.page] ||= area
|
areas[area.attachment_uuid] ||= []
|
||||||
|
areas[area.attachment_uuid].push(area)
|
||||||
})
|
})
|
||||||
|
|
||||||
return Object.values(areas).slice(0, 6)
|
const sortedAreas = Object.values(areas).reduce((acc, group) => {
|
||||||
|
const seen = {}
|
||||||
|
const sortedGroup = [...group].sort((a, b) => a.page - b.page)
|
||||||
|
|
||||||
|
sortedGroup.forEach((area) => {
|
||||||
|
if (!seen[area.page]) {
|
||||||
|
seen[area.page] = true
|
||||||
|
acc.push(area)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return sortedAreas.slice(0, 6)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex absolute lg:text-base -outline-offset-1 field-area"
|
class="flex absolute lg:text-base -outline-offset-1 focus-visible:outline-blue-500 focus-visible:outline-2 focus-visible:outline field-area"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
:style="[computedStyle, fontStyle]"
|
:style="[computedStyle, fontStyle]"
|
||||||
: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 }"
|
: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 }"
|
||||||
|
:role="submittable && !isNativeInputField ? 'button' : undefined"
|
||||||
|
:tabindex="submittable && !isNativeInputField ? 0 : undefined"
|
||||||
|
:aria-label="submittable && !isNativeInputField ? fieldAreaLabel : undefined"
|
||||||
|
@keydown.enter.prevent="submittable && !isNativeInputField ? $el.click() : undefined"
|
||||||
|
@keydown.space.prevent="submittable && !isNativeInputField ? $el.click() : undefined"
|
||||||
>
|
>
|
||||||
<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"
|
||||||
@@ -18,6 +23,7 @@
|
|||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
class="max-h-10 text-base-content"
|
class="max-h-10 text-base-content"
|
||||||
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,6 +66,11 @@
|
|||||||
class="object-contain mx-auto"
|
class="object-contain mx-auto"
|
||||||
:src="stamp.url"
|
:src="stamp.url"
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
v-else-if="field.type === 'kba' && kba"
|
||||||
|
class="object-contain mx-auto"
|
||||||
|
:src="kba.url"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-else-if="field.type === 'signature' && signature"
|
v-else-if="field.type === 'signature' && signature"
|
||||||
class="flex justify-between h-full gap-1 overflow-hidden w-full"
|
class="flex justify-between h-full gap-1 overflow-hidden w-full"
|
||||||
@@ -124,6 +135,7 @@
|
|||||||
v-if="submittable"
|
v-if="submittable"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:value="false"
|
:value="false"
|
||||||
|
:aria-label="field.name || fieldNames[field.type]"
|
||||||
class="aspect-square base-checkbox"
|
class="aspect-square base-checkbox"
|
||||||
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
:checked="!!modelValue"
|
:checked="!!modelValue"
|
||||||
@@ -144,6 +156,8 @@
|
|||||||
v-if="submittable"
|
v-if="submittable"
|
||||||
type="radio"
|
type="radio"
|
||||||
:value="false"
|
:value="false"
|
||||||
|
:name="`radio-area-${field.uuid}`"
|
||||||
|
:aria-label="optionValue(option)"
|
||||||
class="aspect-square checked:checkbox checked:checkbox-xs"
|
class="aspect-square checked:checkbox checked:checkbox-xs"
|
||||||
:class="{ 'base-radio': !modelValue || modelValue !== optionValue(option), '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
:class="{ 'base-radio': !modelValue || modelValue !== optionValue(option), '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
:checked="!!modelValue && modelValue === optionValue(option)"
|
:checked="!!modelValue && modelValue === optionValue(option)"
|
||||||
@@ -164,6 +178,7 @@
|
|||||||
v-if="submittable"
|
v-if="submittable"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:value="false"
|
:value="false"
|
||||||
|
:aria-label="optionValue(option)"
|
||||||
class="aspect-square base-checkbox"
|
class="aspect-square base-checkbox"
|
||||||
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
:checked="!!modelValue && modelValue.includes(optionValue(option))"
|
:checked="!!modelValue && modelValue.includes(optionValue(option))"
|
||||||
@@ -177,8 +192,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="field.type === 'cells'"
|
v-else-if="field.type === 'cells'"
|
||||||
class="w-full flex items-center"
|
class="w-full flex"
|
||||||
:class="{ 'justify-end': field.preferences?.align === 'right', ...fontClasses }"
|
:class="{ 'justify-end': field.preferences?.align === 'right', ...alignClasses, ...fontClasses }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(char, index) in modelValue"
|
v-for="(char, index) in modelValue"
|
||||||
@@ -272,7 +287,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import MarkdownContent from './markdown_content'
|
import MarkdownContent from './markdown_content'
|
||||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconId } from '@tabler/icons-vue'
|
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconId, IconUserScan } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FieldArea',
|
name: 'FieldArea',
|
||||||
@@ -391,9 +406,20 @@ export default {
|
|||||||
stamp: this.t('stamp'),
|
stamp: this.t('stamp'),
|
||||||
payment: this.t('payment'),
|
payment: this.t('payment'),
|
||||||
phone: this.t('phone'),
|
phone: this.t('phone'),
|
||||||
verification: this.t('verify_id')
|
verification: this.t('verify_id'),
|
||||||
|
kba: this.t('kba')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
isNativeInputField () {
|
||||||
|
return ['checkbox', 'radio', 'multiple'].includes(this.field.type)
|
||||||
|
},
|
||||||
|
fieldAreaLabel () {
|
||||||
|
const name = this.field.name || this.fieldNames[this.field.type] || this.field.type
|
||||||
|
if (this.area.option_uuid && this.option) {
|
||||||
|
return `${name} - ${this.optionValue(this.option)}`
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
},
|
||||||
strikethroughWidth () {
|
strikethroughWidth () {
|
||||||
if (this.isInlineSize) {
|
if (this.isInlineSize) {
|
||||||
return '0.6cqmin'
|
return '0.6cqmin'
|
||||||
@@ -454,7 +480,8 @@ export default {
|
|||||||
multiple: IconChecks,
|
multiple: IconChecks,
|
||||||
phone: IconPhoneCheck,
|
phone: IconPhoneCheck,
|
||||||
payment: IconCreditCard,
|
payment: IconCreditCard,
|
||||||
verification: IconId
|
verification: IconId,
|
||||||
|
kba: IconUserScan
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
image () {
|
image () {
|
||||||
@@ -471,6 +498,13 @@ export default {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
kba () {
|
||||||
|
if (this.field.type === 'kba') {
|
||||||
|
return this.attachmentsIndex[this.modelValue]
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
signature () {
|
signature () {
|
||||||
if (this.field.type === 'signature') {
|
if (this.field.type === 'signature') {
|
||||||
return this.attachmentsIndex[this.modelValue]
|
return this.attachmentsIndex[this.modelValue]
|
||||||
@@ -493,7 +527,8 @@ export default {
|
|||||||
try {
|
try {
|
||||||
return this.formatDate(
|
return this.formatDate(
|
||||||
this.modelValue === '{{date}}' ? new Date() : new Date(this.modelValue),
|
this.modelValue === '{{date}}' ? new Date() : new Date(this.modelValue),
|
||||||
this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
|
this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'),
|
||||||
|
{ withTimePlaceholders: this.modelValue === '{{date}}', utc: this.modelValue !== '{{date}}' }
|
||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
return this.modelValue
|
return this.modelValue
|
||||||
@@ -560,7 +595,7 @@ export default {
|
|||||||
return style
|
return style
|
||||||
},
|
},
|
||||||
isNarrow () {
|
isNarrow () {
|
||||||
return this.area.h > 0 && (this.area.w / this.area.h) > 6
|
return this.area.h > 0 && ((this.area.w * this.pageWidth) / (this.area.h * this.pageHeight)) > 4.5
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -608,40 +643,63 @@ export default {
|
|||||||
return new Intl.NumberFormat('de-DE').format(number)
|
return new Intl.NumberFormat('de-DE').format(number)
|
||||||
} else if (format === 'space') {
|
} else if (format === 'space') {
|
||||||
return new Intl.NumberFormat('fr-FR').format(number)
|
return new Intl.NumberFormat('fr-FR').format(number)
|
||||||
|
} else if (format === 'percent') {
|
||||||
|
return `${number}%`
|
||||||
|
} else if (format === 'percent_space') {
|
||||||
|
return `${String(number).replace('.', ',')} %`
|
||||||
} else {
|
} else {
|
||||||
return number
|
return number
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
formatDate (date, format) {
|
formatDate (date, format, { withTimePlaceholders = false, utc = true } = {}) {
|
||||||
const monthFormats = {
|
const monthFormats = { M: 'numeric', MM: '2-digit', MMM: 'short', MMMM: 'long' }
|
||||||
M: 'numeric',
|
const dayFormats = { D: 'numeric', DD: '2-digit' }
|
||||||
MM: '2-digit',
|
const yearFormats = { YYYY: 'numeric', YYY: 'numeric', YY: '2-digit' }
|
||||||
MMM: 'short',
|
const hourFormats = { H: 'numeric', HH: '2-digit', h: 'numeric', hh: '2-digit' }
|
||||||
MMMM: 'long'
|
const minuteFormats = { m: 'numeric', mm: '2-digit' }
|
||||||
}
|
const secondFormats = { s: 'numeric', ss: '2-digit' }
|
||||||
|
|
||||||
const dayFormats = {
|
const hasTime = /[HhAasz]/.test(format)
|
||||||
D: 'numeric',
|
|
||||||
DD: '2-digit'
|
|
||||||
}
|
|
||||||
|
|
||||||
const yearFormats = {
|
const opts = {
|
||||||
YYYY: 'numeric',
|
|
||||||
YYY: 'numeric',
|
|
||||||
YY: '2-digit'
|
|
||||||
}
|
|
||||||
|
|
||||||
const parts = new Intl.DateTimeFormat([], {
|
|
||||||
day: dayFormats[format.match(/D+/)],
|
day: dayFormats[format.match(/D+/)],
|
||||||
month: monthFormats[format.match(/M+/)],
|
month: monthFormats[format.match(/M+/)],
|
||||||
year: yearFormats[format.match(/Y+/)],
|
year: yearFormats[format.match(/Y+/)]
|
||||||
timeZone: 'UTC'
|
}
|
||||||
}).formatToParts(date)
|
|
||||||
|
|
||||||
return format
|
if (format.match(/H+/)) { opts.hour = hourFormats[format.match(/H+/)[0]]; opts.hour12 = false }
|
||||||
.replace(/D+/, parts.find((p) => p.type === 'day').value)
|
if (format.match(/h+/)) { opts.hour = hourFormats[format.match(/h+/)[0]]; opts.hour12 = true }
|
||||||
.replace(/M+/, parts.find((p) => p.type === 'month').value)
|
if (/[Aa]/.test(format) && opts.hour12 === undefined) opts.hour12 = true
|
||||||
.replace(/Y+/, parts.find((p) => p.type === 'year').value)
|
if (format.match(/m+/)) opts.minute = minuteFormats[format.match(/m+/)[0]]
|
||||||
|
if (format.match(/s+/)) opts.second = secondFormats[format.match(/s+/)[0]]
|
||||||
|
if (/z/.test(format)) opts.timeZoneName = 'short'
|
||||||
|
if (!hasTime && utc) opts.timeZone = 'UTC'
|
||||||
|
|
||||||
|
const partTypes = {
|
||||||
|
M: 'month',
|
||||||
|
D: 'day',
|
||||||
|
Y: 'year',
|
||||||
|
H: 'hour',
|
||||||
|
h: 'hour',
|
||||||
|
m: 'minute',
|
||||||
|
s: 'second',
|
||||||
|
z: 'timeZoneName',
|
||||||
|
A: 'dayPeriod',
|
||||||
|
a: 'dayPeriod'
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = new Intl.DateTimeFormat([], opts).formatToParts(date)
|
||||||
|
|
||||||
|
return format.replace(/MMMM|MMM|MM|M|DD|D|YYYY|YYY|YY|HH|hh|H|h|mm|m|ss|s|A|a|z/g, (token) => {
|
||||||
|
if (withTimePlaceholders && /^(HH|hh|H|h|mm|m|ss|s|A|a)$/.test(token)) return '--'
|
||||||
|
|
||||||
|
const value = parts.find((p) => p.type === partTypes[token[0]])?.value
|
||||||
|
|
||||||
|
if (token === 'A') return (value || '').toUpperCase()
|
||||||
|
if (token === 'a') return (value || '').toLowerCase()
|
||||||
|
|
||||||
|
return value
|
||||||
|
})
|
||||||
},
|
},
|
||||||
updateMultipleSelectValue (value) {
|
updateMultipleSelectValue (value) {
|
||||||
if (this.modelValue?.includes(value)) {
|
if (this.modelValue?.includes(value)) {
|
||||||
|
|||||||
Loaded 100 of 493 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user