mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 14:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
1aacd98460 | ||
|
|
840162c9ca | ||
|
|
3519b0e009 | ||
|
|
53fae56c51 | ||
|
|
f4552dbaa8 | ||
|
|
dfab4c331c | ||
|
|
d2a0937038 | ||
|
|
fa1ef44b22 | ||
|
|
c1a5c91299 | ||
|
|
e6280e8f5a | ||
|
|
fd2ba57325 | ||
|
|
dc178ef03f | ||
|
|
68936e10c5 | ||
|
|
41f16e7d39 | ||
|
|
453ebbf90a | ||
|
|
7ca290c080 | ||
|
|
dd5b6a6aa6 | ||
|
|
bf2ebb995b | ||
|
|
3e929758fb | ||
|
|
4a53116586 | ||
|
|
2d1981acfa | ||
|
|
c610df269f | ||
|
|
96dac635f5 | ||
|
|
abd579103d | ||
|
|
7ed27bb413 | ||
|
|
180c542a9f | ||
|
|
89613113de | ||
|
|
3102900776 | ||
|
|
1a68053f4e | ||
|
|
5d4a032763 | ||
|
|
7481d27822 | ||
|
|
22afd01fd2 | ||
|
|
fd9eb09dd6 | ||
|
|
c53ed7f984 | ||
|
|
1d87e210be | ||
|
|
5fec952b8c | ||
|
|
c4fbcc103a | ||
|
|
9333a1ba45 | ||
|
|
de5cd92dba | ||
|
|
83e32bec9f | ||
|
|
4603efc04e | ||
|
|
6c73c82a27 | ||
|
|
8e71d9f9e9 | ||
|
|
80f60a5d88 | ||
|
|
2c33ec382d | ||
|
|
884865cffc | ||
|
|
9537fe8c91 | ||
|
|
c8fe36a2b6 | ||
|
|
7ec3282b56 | ||
|
|
ab8c41a520 | ||
|
|
4c1c70ff04 | ||
|
|
d7a6e80bb1 | ||
|
|
f87ef670d1 | ||
|
|
daba2505bc | ||
|
|
71528eda45 | ||
|
|
269e2beeaa | ||
|
|
72dd2c6bc5 | ||
|
|
9758f7a15f | ||
|
|
2521eb5a4a | ||
|
|
bf6afc5e61 | ||
|
|
291d869843 | ||
|
|
cdfecb2a8f | ||
|
|
c0df8e0ec3 | ||
|
|
a21f7d1653 | ||
|
|
79e2514416 | ||
|
|
80a7385183 | ||
|
|
1c8a7b6a7c | ||
|
|
b46ced2b6f | ||
|
|
5ae479644e | ||
|
|
cb66eb1293 | ||
|
|
b4d5a2cad4 | ||
|
|
32691b0c0d | ||
|
|
9163f744a3 | ||
|
|
23ecce51a3 | ||
|
|
747ffa8cf1 | ||
|
|
4e230899ed | ||
|
|
aff9f753c7 | ||
|
|
1127fac500 | ||
|
|
7707e1cd2b | ||
|
|
30274f7f1a | ||
|
|
a5da6cc781 | ||
|
|
201e4010c5 | ||
|
|
b9bbdbffae | ||
|
|
9e8e1996e8 | ||
|
|
b99235397a | ||
|
|
c0fd06daf3 | ||
|
|
90536294df | ||
|
|
f8e9787675 | ||
|
|
bf9b785946 | ||
|
|
dab3a6f031 | ||
|
|
88466193c5 | ||
|
|
c2f7601dad | ||
|
|
189f7b2d7b | ||
|
|
5d87160ee9 | ||
|
|
0cdcdb8898 | ||
|
|
c20619b5c8 | ||
|
|
93f84abce4 | ||
|
|
3c6a32caba | ||
|
|
72c08e5fdb | ||
|
|
022d80b5ce | ||
|
|
f48da51802 | ||
|
|
709d04b33f | ||
|
|
3a251dd063 | ||
|
|
9cc92b59b3 | ||
|
|
e0469b10a7 | ||
|
|
39eb67b162 | ||
|
|
8d9bea3b0f | ||
|
|
7015d8dde8 | ||
|
|
6b54072cb5 | ||
|
|
b898708306 | ||
|
|
1b1cf36839 | ||
|
|
18b89edc19 | ||
|
|
70c7ef4247 | ||
|
|
a8196709be | ||
|
|
a9d249ccf0 | ||
|
|
cd83d918e5 | ||
|
|
b64a84a362 | ||
|
|
f4426a8ee0 | ||
|
|
df8c44d2ce | ||
|
|
65ce3d4822 | ||
|
|
e5b1d94579 | ||
|
|
0b4754ca24 | ||
|
|
b3dc8d55be | ||
|
|
e5ab7667b9 | ||
|
|
2a4e6435f1 | ||
|
|
804431c44a | ||
|
|
f3b26bf7a0 | ||
|
|
aa27d90017 | ||
|
|
0fa8cf2dd0 | ||
|
|
634b5c37ba | ||
|
|
6e16f81fc6 | ||
|
|
ffc4024f70 | ||
|
|
a6981185b2 | ||
|
|
0361fabb66 | ||
|
|
04e7f101be | ||
|
|
86d680cfdf | ||
|
|
7bc2152057 | ||
|
|
74ed9a5040 | ||
|
|
30115fc0dc | ||
|
|
8e8e5e0e29 | ||
|
|
b58ce1b571 | ||
|
|
df5c4e4210 | ||
|
|
ba2e0999f1 | ||
|
|
a1f5dd08c7 | ||
|
|
c1123fef63 | ||
|
|
4dc6149530 | ||
|
|
1245ff2cce | ||
|
|
284204fd78 | ||
|
|
f7be74eb73 | ||
|
|
aeea619059 | ||
|
|
ea2b7f20e7 | ||
|
|
f40f1d25de | ||
|
|
c04bb2d7cf | ||
|
|
4c1ccd65bf | ||
|
|
54e064b0fc | ||
|
|
9a8b72883c | ||
|
|
5d75ee2e47 | ||
|
|
7357fa4871 | ||
|
|
f6850b5427 | ||
|
|
1e9a181e60 | ||
|
|
7224a1ccec | ||
|
|
9b935a8180 | ||
|
|
c4a693846e | ||
|
|
02c7cdcd97 | ||
|
|
36bee92e8e | ||
|
|
6542804f44 | ||
|
|
d805fd614c | ||
|
|
21299dc65f | ||
|
|
0df0946c2a | ||
|
|
1bb38d50a1 | ||
|
|
242987cb41 | ||
|
|
5456abde54 | ||
|
|
4a07427440 | ||
|
|
cf60dae2ff | ||
|
|
659997c6c1 | ||
|
|
389a47a152 | ||
|
|
b6b81ca487 | ||
|
|
719c1786f1 | ||
|
|
4685bac4b3 | ||
|
|
a05275cc08 | ||
|
|
1abf1b1658 | ||
|
|
8893dc5698 | ||
|
|
f8467b2da1 | ||
|
|
f39b8b000e | ||
|
|
961036fa31 | ||
|
|
8b9056894e | ||
|
|
7f979e9396 | ||
|
|
8db1192d0e | ||
|
|
3bd515951a | ||
|
|
2bd5b9676b | ||
|
|
993698ec01 | ||
|
|
fcdc44ddbe | ||
|
|
0cb6b0ae12 | ||
|
|
0388927c5b | ||
|
|
2c8f5d2129 | ||
|
|
2bffbf83c6 | ||
|
|
ec01529014 | ||
|
|
ea1abf5662 | ||
|
|
fcd5263af1 | ||
|
|
1fa8616dbc | ||
|
|
f627277b45 | ||
|
|
809c3ea270 | ||
|
|
9caa9d79d4 | ||
|
|
a8f1c1c323 | ||
|
|
59fbea5c5f | ||
|
|
d3273d879a | ||
|
|
ea227e82c9 | ||
|
|
676b330aa3 | ||
|
|
8d3295e125 | ||
|
|
8cb74e0bcc | ||
|
|
26749a4dbc | ||
|
|
14915d37b4 | ||
|
|
16d6d58efb | ||
|
|
b2b97a313a | ||
|
|
2974e6a64f | ||
|
|
dae34d0aa5 | ||
|
|
e9d0671184 | ||
|
|
897e33bf6a | ||
|
|
f37e93a409 | ||
|
|
561ac63c93 | ||
|
|
149e4be07a | ||
|
|
eb98dc9e06 | ||
|
|
ce7d965358 | ||
|
|
bb73024859 | ||
|
|
631a442e2a | ||
|
|
20a082ae8e | ||
|
|
154d706be1 | ||
|
|
aaae257c75 | ||
|
|
ec944a2283 | ||
|
|
69d63ecffe | ||
|
|
06a1e2992d | ||
|
|
ec9293bac0 | ||
|
|
8bf727c43c | ||
|
|
708b8afdf5 | ||
|
|
6f7b9b1ebf | ||
|
|
4f48c4fd9f | ||
|
|
9dac90e6cc |
No files matched your search
@@ -12,7 +12,7 @@ jobs:
|
|||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.4.2
|
ruby-version: 4.0.1
|
||||||
- name: Cache gems
|
- name: Cache gems
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.4.2
|
ruby-version: 4.0.1
|
||||||
- name: Cache gems
|
- name: Cache gems
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 20.9.0
|
node-version: 20.19.0
|
||||||
- name: Cache directory path
|
- name: Cache directory path
|
||||||
id: yarn-cache-dir-path
|
id: yarn-cache-dir-path
|
||||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.4.2
|
ruby-version: 4.0.1
|
||||||
- name: Cache gems
|
- name: Cache gems
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@@ -132,11 +132,11 @@ jobs:
|
|||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.4.2
|
ruby-version: 4.0.1
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 20.9.0
|
node-version: 20.19.0
|
||||||
- name: Install Chrome
|
- name: Install Chrome
|
||||||
uses: browser-actions/setup-chrome@latest
|
uses: browser-actions/setup-chrome@latest
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-24.04-arm
|
runs-on: ubuntu-24.04-arm
|
||||||
timeout-minutes: 20
|
timeout-minutes: 30
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -16,15 +16,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
-
|
- name: Docker meta
|
||||||
name: Docker meta
|
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: |
|
images: docuseal/docuseal
|
||||||
docuseal/docuseal
|
tags: type=semver,pattern={{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
|
||||||
|
|
||||||
|
|||||||
@@ -37,3 +37,4 @@ yarn-debug.log*
|
|||||||
/docuseal
|
/docuseal
|
||||||
/ee
|
/ee
|
||||||
dump.rdb
|
dump.rdb
|
||||||
|
*.onnx
|
||||||
+23
-2
@@ -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:
|
||||||
@@ -42,6 +42,18 @@ Metrics/CyclomaticComplexity:
|
|||||||
Metrics/PerceivedComplexity:
|
Metrics/PerceivedComplexity:
|
||||||
Max: 15
|
Max: 15
|
||||||
|
|
||||||
|
Style/MultipleComparison:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/NumericPredicate:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/MinMaxComparison:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Naming/PredicateMethod:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
Layout/LineLength:
|
Layout/LineLength:
|
||||||
AllowedPatterns: ['\A\s*#']
|
AllowedPatterns: ['\A\s*#']
|
||||||
|
|
||||||
@@ -68,6 +80,9 @@ RSpec/ExampleLength:
|
|||||||
RSpec/MultipleMemoizedHelpers:
|
RSpec/MultipleMemoizedHelpers:
|
||||||
Max: 15
|
Max: 15
|
||||||
|
|
||||||
|
RSpec/AnyInstance:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
Metrics/BlockNesting:
|
Metrics/BlockNesting:
|
||||||
Max: 5
|
Max: 5
|
||||||
|
|
||||||
@@ -85,3 +100,9 @@ Rails/ApplicationController:
|
|||||||
|
|
||||||
Rails/Output:
|
Rails/Output:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/StrongParametersExpect:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/RedirectBackOrTo:
|
||||||
|
Enabled: false
|
||||||
+28
-22
@@ -1,4 +1,4 @@
|
|||||||
FROM ruby:3.4.2-alpine AS download
|
FROM ruby:4.0.1-alpine AS download
|
||||||
|
|
||||||
WORKDIR /fonts
|
WORKDIR /fonts
|
||||||
|
|
||||||
@@ -9,13 +9,14 @@ RUN apk --no-cache add fontforge wget && \
|
|||||||
wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && \
|
wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && \
|
||||||
wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && \
|
wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && \
|
||||||
wget https://github.com/impallari/DancingScript/raw/master/OFL.txt && \
|
wget https://github.com/impallari/DancingScript/raw/master/OFL.txt && \
|
||||||
|
wget -O /model.onnx "https://github.com/docusealco/fields-detection/releases/download/2.0.0/model_704_int8.onnx" && \
|
||||||
wget -O 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/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" && \
|
||||||
mkdir -p /pdfium-linux && \
|
mkdir -p /pdfium-linux && \
|
||||||
tar -xzf pdfium-linux.tgz -C /pdfium-linux
|
tar -xzf pdfium-linux.tgz -C /pdfium-linux
|
||||||
|
|
||||||
RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")'
|
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:3.4.2-alpine AS webpack
|
FROM ruby:4.0.1-alpine AS webpack
|
||||||
|
|
||||||
ENV RAILS_ENV=production
|
ENV RAILS_ENV=production
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
@@ -41,16 +42,18 @@ COPY ./app/views ./app/views
|
|||||||
|
|
||||||
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
|
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
|
||||||
|
|
||||||
FROM ruby:3.4.2-alpine AS app
|
FROM ruby:4.0.1-alpine AS app
|
||||||
|
|
||||||
ENV RAILS_ENV=production
|
ENV RAILS_ENV=production
|
||||||
ENV BUNDLE_WITHOUT="development:test"
|
ENV BUNDLE_WITHOUT="development:test"
|
||||||
ENV LD_PRELOAD=/lib/libgcompat.so.0
|
ENV LD_PRELOAD=/lib/libgcompat.so.0
|
||||||
ENV OPENSSL_CONF=/app/openssl_legacy.cnf
|
ENV OPENSSL_CONF=/etc/openssl_legacy.cnf
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev@edge redis libheif@edge vips-heif@edge gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
RUN apk add --no-cache sqlite-dev libpq-dev vips-dev yaml-dev redis libheif vips-heif gcompat ttf-freefont onnxruntime && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
||||||
|
|
||||||
|
RUN 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\
|
||||||
@@ -62,33 +65,36 @@ 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
|
RUN apk add --no-cache build-base git && bundle install && apk del --no-cache build-base git && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf && ln -sf /usr/lib/libonnxruntime.so.1 $(ruby -e "print Dir[Gem::Specification.find_by_name('onnxruntime').gem_dir + '/vendor/*.so'].first")
|
||||||
|
|
||||||
COPY ./bin ./bin
|
COPY --chown=docuseal:docuseal ./bin ./bin
|
||||||
COPY ./app ./app
|
COPY --chown=docuseal:docuseal ./app ./app
|
||||||
COPY ./config ./config
|
COPY --chown=docuseal:docuseal ./config ./config
|
||||||
COPY ./db/migrate ./db/migrate
|
COPY --chown=docuseal:docuseal ./db/migrate ./db/migrate
|
||||||
COPY ./log ./log
|
COPY --chown=docuseal:docuseal ./log ./log
|
||||||
COPY ./lib ./lib
|
COPY --chown=docuseal:docuseal ./lib ./lib
|
||||||
COPY ./public ./public
|
COPY --chown=docuseal:docuseal ./public ./public
|
||||||
COPY ./tmp ./tmp
|
COPY --chown=docuseal:docuseal ./tmp ./tmp
|
||||||
COPY LICENSE README.md Rakefile config.ru .version ./
|
COPY --chown=docuseal:docuseal LICENSE README.md Rakefile config.ru .version ./
|
||||||
COPY .version ./public/version
|
COPY --chown=docuseal:docuseal .version ./public/version
|
||||||
|
|
||||||
COPY --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
COPY --chown=docuseal:docuseal --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
||||||
COPY --from=download /fonts/FreeSans.ttf /usr/share/fonts/freefont
|
COPY --from=download /fonts/FreeSans.ttf /usr/share/fonts/freefont
|
||||||
COPY --from=download /pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so
|
COPY --from=download /pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so
|
||||||
COPY --from=download /pdfium-linux/licenses/pdfium.txt /usr/lib/libpdfium-LICENSE.txt
|
COPY --from=download /pdfium-linux/licenses/pdfium.txt /usr/lib/libpdfium-LICENSE.txt
|
||||||
COPY --from=webpack /app/public/packs ./public/packs
|
COPY --chown=docuseal:docuseal --from=download /model.onnx /app/tmp/model.onnx
|
||||||
|
COPY --chown=docuseal:docuseal --from=webpack /app/public/packs ./public/packs
|
||||||
|
|
||||||
RUN ln -s /fonts /app/public/fonts
|
RUN ln -s /fonts /app/public/fonts && \
|
||||||
RUN bundle exec bootsnap precompile --gemfile app/ lib/
|
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
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|||||||
@@ -2,15 +2,16 @@
|
|||||||
|
|
||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
ruby '3.4.2'
|
ruby '4.0.1'
|
||||||
|
|
||||||
gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic'
|
gem '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', require: false
|
||||||
gem 'devise'
|
gem 'devise'
|
||||||
gem 'devise-two-factor'
|
gem 'devise-two-factor'
|
||||||
gem 'dotenv', require: false
|
gem 'dotenv', require: false
|
||||||
@@ -20,10 +21,11 @@ gem 'faraday-follow_redirects'
|
|||||||
gem 'google-cloud-storage', require: false
|
gem 'google-cloud-storage', require: false
|
||||||
gem 'hexapdf'
|
gem 'hexapdf'
|
||||||
gem 'image_processing'
|
gem 'image_processing'
|
||||||
gem 'jwt'
|
gem 'jwt', require: false
|
||||||
gem 'lograge'
|
gem 'lograge'
|
||||||
gem 'mysql2', require: false
|
gem 'numo-narray-alt', require: false
|
||||||
gem 'oj'
|
gem 'oj'
|
||||||
|
gem 'onnxruntime', require: false
|
||||||
gem 'pagy'
|
gem 'pagy'
|
||||||
gem 'pg', require: false
|
gem 'pg', require: false
|
||||||
gem 'premailer-rails'
|
gem 'premailer-rails'
|
||||||
@@ -35,13 +37,14 @@ 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', github: 'trilogy-libraries/trilogy', glob: 'contrib/ruby/*.gemspec', require: false
|
||||||
gem 'turbo-rails'
|
gem 'turbo-rails'
|
||||||
gem 'twitter_cldr', require: false
|
gem 'twitter_cldr', require: false
|
||||||
gem 'tzinfo-data'
|
gem 'tzinfo-data'
|
||||||
|
|||||||
+337
-305
@@ -1,29 +1,39 @@
|
|||||||
|
GIT
|
||||||
|
remote: https://github.com/trilogy-libraries/trilogy.git
|
||||||
|
revision: 3963d490459df7a2b5bedb42424c3285f25eab22
|
||||||
|
glob: contrib/ruby/*.gemspec
|
||||||
|
specs:
|
||||||
|
trilogy (2.10.0)
|
||||||
|
bigdecimal
|
||||||
|
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (8.0.1)
|
action_text-trix (2.1.16)
|
||||||
actionpack (= 8.0.1)
|
railties
|
||||||
activesupport (= 8.0.1)
|
actioncable (8.1.2)
|
||||||
|
actionpack (= 8.1.2)
|
||||||
|
activesupport (= 8.1.2)
|
||||||
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.1)
|
actionmailbox (8.1.2)
|
||||||
actionpack (= 8.0.1)
|
actionpack (= 8.1.2)
|
||||||
activejob (= 8.0.1)
|
activejob (= 8.1.2)
|
||||||
activerecord (= 8.0.1)
|
activerecord (= 8.1.2)
|
||||||
activestorage (= 8.0.1)
|
activestorage (= 8.1.2)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.1.2)
|
||||||
mail (>= 2.8.0)
|
mail (>= 2.8.0)
|
||||||
actionmailer (8.0.1)
|
actionmailer (8.1.2)
|
||||||
actionpack (= 8.0.1)
|
actionpack (= 8.1.2)
|
||||||
actionview (= 8.0.1)
|
actionview (= 8.1.2)
|
||||||
activejob (= 8.0.1)
|
activejob (= 8.1.2)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.1.2)
|
||||||
mail (>= 2.8.0)
|
mail (>= 2.8.0)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
actionpack (8.0.1)
|
actionpack (8.1.2)
|
||||||
actionview (= 8.0.1)
|
actionview (= 8.1.2)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.1.2)
|
||||||
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 +41,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.1)
|
actiontext (8.1.2)
|
||||||
actionpack (= 8.0.1)
|
action_text-trix (~> 2.1.15)
|
||||||
activerecord (= 8.0.1)
|
actionpack (= 8.1.2)
|
||||||
activestorage (= 8.0.1)
|
activerecord (= 8.1.2)
|
||||||
activesupport (= 8.0.1)
|
activestorage (= 8.1.2)
|
||||||
|
activesupport (= 8.1.2)
|
||||||
globalid (>= 0.6.0)
|
globalid (>= 0.6.0)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (8.0.1)
|
actionview (8.1.2)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.1.2)
|
||||||
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.1)
|
activejob (8.1.2)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.1.2)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (8.0.1)
|
activemodel (8.1.2)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.1.2)
|
||||||
activerecord (8.0.1)
|
activerecord (8.1.2)
|
||||||
activemodel (= 8.0.1)
|
activemodel (= 8.1.2)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.1.2)
|
||||||
timeout (>= 0.4.0)
|
timeout (>= 0.4.0)
|
||||||
activestorage (8.0.1)
|
activestorage (8.1.2)
|
||||||
actionpack (= 8.0.1)
|
actionpack (= 8.1.2)
|
||||||
activejob (= 8.0.1)
|
activejob (= 8.1.2)
|
||||||
activerecord (= 8.0.1)
|
activerecord (= 8.1.2)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.1.2)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
activesupport (8.0.1)
|
activesupport (8.1.2)
|
||||||
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.8.8)
|
||||||
public_suffix (>= 2.0.2, < 7.0)
|
public_suffix (>= 2.0.2, < 8.0)
|
||||||
annotaterb (4.14.0)
|
annotaterb (4.20.0)
|
||||||
|
activerecord (>= 6.0.0)
|
||||||
|
activesupport (>= 6.0.0)
|
||||||
arabic-letter-connector (0.1.1)
|
arabic-letter-connector (0.1.1)
|
||||||
ast (2.4.2)
|
ast (2.4.3)
|
||||||
aws-eventstream (1.3.0)
|
aws-eventstream (1.4.0)
|
||||||
aws-partitions (1.1027.0)
|
aws-partitions (1.1209.0)
|
||||||
aws-sdk-core (3.214.0)
|
aws-sdk-core (3.241.4)
|
||||||
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.121.0)
|
||||||
|
aws-sdk-core (~> 3, >= 3.241.4)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sdk-s3 (1.176.1)
|
aws-sdk-s3 (1.212.0)
|
||||||
aws-sdk-core (~> 3, >= 3.210.0)
|
aws-sdk-core (~> 3, >= 3.241.4)
|
||||||
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.128.0)
|
||||||
aws-sdk-core (~> 3, >= 3.210.0)
|
aws-sdk-core (~> 3, >= 3.241.4)
|
||||||
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)
|
base64 (0.3.0)
|
||||||
faraday (~> 1.0)
|
bcrypt (3.1.21)
|
||||||
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
|
better_html (2.2.0)
|
||||||
net-http-persistent (~> 4.0)
|
actionview (>= 7.0)
|
||||||
nokogiri (~> 1, >= 1.10.8)
|
activesupport (>= 7.0)
|
||||||
base64 (0.2.0)
|
|
||||||
bcrypt (3.1.20)
|
|
||||||
benchmark (0.4.0)
|
|
||||||
better_html (2.1.1)
|
|
||||||
actionview (>= 6.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.1.8)
|
bigdecimal (4.0.1)
|
||||||
bindex (0.8.1)
|
bindex (0.8.1)
|
||||||
bootsnap (1.18.4)
|
bootsnap (1.21.1)
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
brakeman (7.0.0)
|
brakeman (7.1.2)
|
||||||
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,26 +145,29 @@ 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.4)
|
concurrent-ruby (1.3.6)
|
||||||
connection_pool (2.4.1)
|
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)
|
css_parser (1.21.1)
|
||||||
addressable
|
addressable
|
||||||
csv (3.3.2)
|
csv (3.3.5)
|
||||||
cuprite (0.15.1)
|
csv-safe (3.3.1)
|
||||||
|
csv (~> 3.0)
|
||||||
|
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)
|
||||||
@@ -164,19 +177,20 @@ GEM
|
|||||||
railties (>= 4.1.0)
|
railties (>= 4.1.0)
|
||||||
responders
|
responders
|
||||||
warden (~> 1.2.3)
|
warden (~> 1.2.3)
|
||||||
devise-two-factor (6.1.0)
|
devise-two-factor (6.3.1)
|
||||||
activesupport (>= 7.0, < 8.1)
|
activesupport (>= 7.0, < 8.2)
|
||||||
devise (~> 4.0)
|
devise (>= 4.0, < 5.0)
|
||||||
railties (>= 7.0, < 8.1)
|
railties (>= 7.0, < 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.1)
|
drb (2.2.3)
|
||||||
email_typo (0.2.3)
|
email_typo (0.2.3)
|
||||||
erb_lint (0.7.0)
|
erb (6.0.1)
|
||||||
|
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)
|
||||||
@@ -184,115 +198,101 @@ 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.0)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
faraday (1.10.4)
|
faraday (2.14.1)
|
||||||
faraday-em_http (~> 1.0)
|
faraday-net_http (>= 2.0, < 3.5)
|
||||||
faraday-em_synchrony (~> 1.0)
|
json
|
||||||
faraday-excon (~> 1.1)
|
logger
|
||||||
faraday-httpclient (~> 1.0)
|
faraday-follow_redirects (0.5.0)
|
||||||
faraday-multipart (~> 1.0)
|
|
||||||
faraday-net_http (~> 1.0)
|
|
||||||
faraday-net_http_persistent (~> 1.0)
|
|
||||||
faraday-patron (~> 1.0)
|
|
||||||
faraday-rack (~> 1.0)
|
|
||||||
faraday-retry (~> 1.0)
|
|
||||||
ruby2_keywords (>= 0.0.4)
|
|
||||||
faraday-em_http (1.0.0)
|
|
||||||
faraday-em_synchrony (1.0.0)
|
|
||||||
faraday-excon (1.1.0)
|
|
||||||
faraday-follow_redirects (0.3.0)
|
|
||||||
faraday (>= 1, < 3)
|
faraday (>= 1, < 3)
|
||||||
faraday-httpclient (1.0.1)
|
faraday-net_http (3.4.2)
|
||||||
faraday-multipart (1.1.0)
|
net-http (~> 0.5)
|
||||||
multipart-post (~> 2.0)
|
ferrum (0.17.1)
|
||||||
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.3)
|
||||||
ffi (1.17.1-aarch64-linux-gnu)
|
ffi (1.17.3-aarch64-linux-gnu)
|
||||||
ffi (1.17.1-aarch64-linux-musl)
|
ffi (1.17.3-aarch64-linux-musl)
|
||||||
ffi (1.17.1-arm64-darwin)
|
ffi (1.17.3-arm64-darwin)
|
||||||
ffi (1.17.1-x86_64-linux-gnu)
|
ffi (1.17.3-x86_64-linux-gnu)
|
||||||
ffi (1.17.1-x86_64-linux-musl)
|
ffi (1.17.3-x86_64-linux-musl)
|
||||||
foreman (0.88.1)
|
foreman (0.90.0)
|
||||||
|
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.59.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.5.0)
|
||||||
google-cloud-storage (1.54.0)
|
google-cloud-storage (1.58.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.1)
|
||||||
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.0.3)
|
hexapdf (1.5.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)
|
||||||
htmlentities (4.3.4)
|
strscan (>= 3.1.2)
|
||||||
httpclient (2.8.3)
|
htmlentities (4.4.2)
|
||||||
i18n (1.14.6)
|
i18n (1.14.8)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
image_processing (1.13.0)
|
image_processing (1.14.0)
|
||||||
mini_magick (>= 4.9.5, < 5)
|
mini_magick (>= 4.9.5, < 6)
|
||||||
ruby-vips (>= 2.0.17, < 3)
|
ruby-vips (>= 2.0.17, < 3)
|
||||||
io-console (0.8.0)
|
io-console (0.8.2)
|
||||||
irb (1.14.3)
|
irb (1.16.0)
|
||||||
|
pp (>= 0.6.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.9.1)
|
json (2.18.1)
|
||||||
jwt (2.9.3)
|
jwt (3.1.2)
|
||||||
base64
|
base64
|
||||||
language_server-protocol (3.17.0.3)
|
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)
|
||||||
@@ -300,71 +300,86 @@ GEM
|
|||||||
letter_opener (~> 1.9)
|
letter_opener (~> 1.9)
|
||||||
railties (>= 6.1)
|
railties (>= 6.1)
|
||||||
rexml
|
rexml
|
||||||
logger (1.6.4)
|
lint_roller (1.1.0)
|
||||||
|
logger (1.7.0)
|
||||||
lograge (0.14.0)
|
lograge (0.14.0)
|
||||||
actionpack (>= 4)
|
actionpack (>= 4)
|
||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
railties (>= 4)
|
railties (>= 4)
|
||||||
request_store (~> 1.0)
|
request_store (~> 1.0)
|
||||||
loofah (2.23.1)
|
loofah (2.25.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
mail (2.8.1)
|
mail (2.9.0)
|
||||||
|
logger
|
||||||
mini_mime (>= 0.1.1)
|
mini_mime (>= 0.1.1)
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
marcel (1.0.4)
|
marcel (1.1.0)
|
||||||
matrix (0.4.2)
|
matrix (0.4.3)
|
||||||
method_source (1.1.0)
|
method_source (1.1.0)
|
||||||
mini_magick (4.13.2)
|
mini_magick (5.3.1)
|
||||||
|
logger
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.8)
|
minitest (6.0.1)
|
||||||
minitest (5.25.4)
|
prism (~> 1.5)
|
||||||
msgpack (1.7.5)
|
msgpack (1.8.0)
|
||||||
multi_json (1.15.0)
|
multi_json (1.19.1)
|
||||||
multipart-post (2.4.1)
|
net-http (0.9.1)
|
||||||
mutex_m (0.3.0)
|
uri (>= 0.11.1)
|
||||||
mysql2 (0.5.6)
|
net-imap (0.6.2)
|
||||||
net-http-persistent (4.0.5)
|
|
||||||
connection_pool (~> 2.2)
|
|
||||||
net-imap (0.5.6)
|
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
net-pop (0.1.2)
|
net-pop (0.1.2)
|
||||||
net-protocol
|
net-protocol
|
||||||
net-protocol (0.2.2)
|
net-protocol (0.2.2)
|
||||||
timeout
|
timeout
|
||||||
net-smtp (0.5.0)
|
net-smtp (0.5.1)
|
||||||
net-protocol
|
net-protocol
|
||||||
nio4r (2.7.4)
|
nio4r (2.7.5)
|
||||||
nokogiri (1.18.8)
|
nokogiri (1.19.0-aarch64-linux-gnu)
|
||||||
mini_portile2 (~> 2.8.2)
|
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.8-aarch64-linux-gnu)
|
nokogiri (1.19.0-aarch64-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.8-aarch64-linux-musl)
|
nokogiri (1.19.0-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.8-arm64-darwin)
|
nokogiri (1.19.0-x86_64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.8-x86_64-linux-gnu)
|
nokogiri (1.19.0-x86_64-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.8-x86_64-linux-musl)
|
numo-narray-alt (0.9.13)
|
||||||
racc (~> 1.4)
|
oj (3.16.13)
|
||||||
oj (3.16.8)
|
|
||||||
bigdecimal (>= 3.0)
|
bigdecimal (>= 3.0)
|
||||||
ostruct (>= 0.2)
|
ostruct (>= 0.2)
|
||||||
openssl (3.3.0)
|
onnxruntime (0.10.1)
|
||||||
|
ffi
|
||||||
|
onnxruntime (0.10.1-aarch64-linux)
|
||||||
|
ffi
|
||||||
|
onnxruntime (0.10.1-arm64-darwin)
|
||||||
|
ffi
|
||||||
|
onnxruntime (0.10.1-x86_64-linux)
|
||||||
|
ffi
|
||||||
|
openssl (4.0.0)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
ostruct (0.6.1)
|
ostruct (0.6.3)
|
||||||
package_json (0.1.0)
|
package_json (0.2.0)
|
||||||
pagy (9.3.3)
|
pagy (43.2.8)
|
||||||
parallel (1.26.3)
|
json
|
||||||
parser (3.3.6.0)
|
yaml
|
||||||
|
parallel (1.27.0)
|
||||||
|
parser (3.3.10.1)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
pg (1.5.9)
|
pg (1.6.3)
|
||||||
|
pg (1.6.3-aarch64-linux)
|
||||||
|
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
|
||||||
premailer (1.27.0)
|
premailer (1.27.0)
|
||||||
addressable
|
addressable
|
||||||
css_parser (>= 1.19.0)
|
css_parser (>= 1.19.0)
|
||||||
@@ -373,73 +388,80 @@ GEM
|
|||||||
actionmailer (>= 3)
|
actionmailer (>= 3)
|
||||||
net-smtp
|
net-smtp
|
||||||
premailer (~> 1.7, >= 1.7.9)
|
premailer (~> 1.7, >= 1.7.9)
|
||||||
pretender (0.5.0)
|
pretender (0.6.0)
|
||||||
actionpack (>= 6.1)
|
actionpack (>= 7.1)
|
||||||
pry (0.15.0)
|
prettyprint (0.2.0)
|
||||||
|
prism (1.8.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.2)
|
psych (5.3.1)
|
||||||
date
|
date
|
||||||
stringio
|
stringio
|
||||||
public_suffix (6.0.1)
|
public_suffix (7.0.2)
|
||||||
puma (6.5.0)
|
puma (7.2.0)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rack (3.1.16)
|
rack (3.2.4)
|
||||||
rack-proxy (0.7.7)
|
rack-proxy (0.7.7)
|
||||||
rack
|
rack
|
||||||
rack-session (2.0.0)
|
rack-session (2.1.1)
|
||||||
|
base64 (>= 0.1.0)
|
||||||
rack (>= 3.0.0)
|
rack (>= 3.0.0)
|
||||||
rack-test (2.1.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.1)
|
rails (8.1.2)
|
||||||
actioncable (= 8.0.1)
|
actioncable (= 8.1.2)
|
||||||
actionmailbox (= 8.0.1)
|
actionmailbox (= 8.1.2)
|
||||||
actionmailer (= 8.0.1)
|
actionmailer (= 8.1.2)
|
||||||
actionpack (= 8.0.1)
|
actionpack (= 8.1.2)
|
||||||
actiontext (= 8.0.1)
|
actiontext (= 8.1.2)
|
||||||
actionview (= 8.0.1)
|
actionview (= 8.1.2)
|
||||||
activejob (= 8.0.1)
|
activejob (= 8.1.2)
|
||||||
activemodel (= 8.0.1)
|
activemodel (= 8.1.2)
|
||||||
activerecord (= 8.0.1)
|
activerecord (= 8.1.2)
|
||||||
activestorage (= 8.0.1)
|
activestorage (= 8.1.2)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.1.2)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 8.0.1)
|
railties (= 8.1.2)
|
||||||
rails-dom-testing (2.2.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.6.2)
|
||||||
loofah (~> 2.21)
|
loofah (~> 2.21)
|
||||||
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)
|
rails_autolink (1.1.8)
|
||||||
actionview (> 3.1)
|
actionview (> 3.1)
|
||||||
activesupport (> 3.1)
|
activesupport (> 3.1)
|
||||||
railties (> 3.1)
|
railties (> 3.1)
|
||||||
railties (8.0.1)
|
railties (8.1.2)
|
||||||
actionpack (= 8.0.1)
|
actionpack (= 8.1.2)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.1.2)
|
||||||
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.2.1)
|
rake (13.3.1)
|
||||||
rdoc (6.10.0)
|
rdoc (7.1.0)
|
||||||
|
erb
|
||||||
psych (>= 4.0.0)
|
psych (>= 4.0.0)
|
||||||
redis-client (0.23.0)
|
tsort
|
||||||
|
redis-client (0.26.4)
|
||||||
connection_pool
|
connection_pool
|
||||||
regexp_parser (2.9.3)
|
regexp_parser (2.11.3)
|
||||||
reline (0.6.0)
|
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)
|
||||||
@@ -447,120 +469,126 @@ 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)
|
|
||||||
railties (>= 5.2)
|
|
||||||
retriable (3.1.2)
|
|
||||||
rexml (3.4.0)
|
|
||||||
rotp (6.3.0)
|
|
||||||
rouge (4.5.2)
|
|
||||||
rqrcode (2.2.0)
|
|
||||||
chunky_png (~> 1.0)
|
|
||||||
rqrcode_core (~> 1.0)
|
|
||||||
rqrcode_core (1.2.0)
|
|
||||||
rspec-core (3.13.2)
|
|
||||||
rspec-support (~> 3.13.0)
|
|
||||||
rspec-expectations (3.13.3)
|
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
|
||||||
rspec-support (~> 3.13.0)
|
|
||||||
rspec-mocks (3.13.2)
|
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
|
||||||
rspec-support (~> 3.13.0)
|
|
||||||
rspec-rails (7.1.0)
|
|
||||||
actionpack (>= 7.0)
|
actionpack (>= 7.0)
|
||||||
activesupport (>= 7.0)
|
|
||||||
railties (>= 7.0)
|
railties (>= 7.0)
|
||||||
|
retriable (3.1.2)
|
||||||
|
rexml (3.4.4)
|
||||||
|
rotp (6.3.0)
|
||||||
|
rouge (4.7.0)
|
||||||
|
rqrcode (3.2.0)
|
||||||
|
chunky_png (~> 1.0)
|
||||||
|
rqrcode_core (~> 2.0)
|
||||||
|
rqrcode_core (2.1.0)
|
||||||
|
rspec-core (3.13.6)
|
||||||
|
rspec-support (~> 3.13.0)
|
||||||
|
rspec-expectations (3.13.5)
|
||||||
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
|
rspec-support (~> 3.13.0)
|
||||||
|
rspec-mocks (3.13.7)
|
||||||
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
|
rspec-support (~> 3.13.0)
|
||||||
|
rspec-rails (8.0.2)
|
||||||
|
actionpack (>= 7.2)
|
||||||
|
activesupport (>= 7.2)
|
||||||
|
railties (>= 7.2)
|
||||||
rspec-core (~> 3.13)
|
rspec-core (~> 3.13)
|
||||||
rspec-expectations (~> 3.13)
|
rspec-expectations (~> 3.13)
|
||||||
rspec-mocks (~> 3.13)
|
rspec-mocks (~> 3.13)
|
||||||
rspec-support (~> 3.13)
|
rspec-support (~> 3.13)
|
||||||
rspec-support (3.13.2)
|
rspec-support (3.13.6)
|
||||||
rubocop (1.69.2)
|
rubocop (1.82.1)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (>= 3.17.0)
|
language_server-protocol (~> 3.17.0.2)
|
||||||
|
lint_roller (~> 1.1.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
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.36.2, < 2.0)
|
rubocop-ast (>= 1.48.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.37.0)
|
rubocop-ast (1.49.0)
|
||||||
parser (>= 3.3.1.0)
|
parser (>= 3.3.7.2)
|
||||||
rubocop-performance (1.23.0)
|
prism (~> 1.7)
|
||||||
rubocop (>= 1.48.1, < 2.0)
|
rubocop-performance (1.26.1)
|
||||||
rubocop-ast (>= 1.31.1, < 2.0)
|
lint_roller (~> 1.1)
|
||||||
rubocop-rails (2.27.0)
|
rubocop (>= 1.75.0, < 2.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.0)
|
||||||
shakapacker (8.0.2)
|
shakapacker (9.5.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.0)
|
||||||
connection_pool (>= 2.3.0)
|
connection_pool (>= 3.0.0)
|
||||||
logger
|
json (>= 2.16.0)
|
||||||
rack (>= 2.2.4)
|
logger (>= 1.7.0)
|
||||||
redis-client (>= 0.22.2)
|
rack (>= 3.2.0)
|
||||||
signet (0.19.0)
|
redis-client (>= 0.26.0)
|
||||||
|
signet (0.21.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
faraday (>= 0.17.5, < 3.a)
|
faraday (>= 0.17.5, < 3.a)
|
||||||
jwt (>= 1.5, < 3.0)
|
jwt (>= 1.5, < 4.0)
|
||||||
multi_json (~> 1.10)
|
multi_json (~> 1.10)
|
||||||
simplecov (0.22.0)
|
simplecov (0.22.0)
|
||||||
docile (~> 1.1)
|
docile (~> 1.1)
|
||||||
simplecov-html (~> 0.11)
|
simplecov-html (~> 0.11)
|
||||||
simplecov_json_formatter (~> 0.1)
|
simplecov_json_formatter (~> 0.1)
|
||||||
simplecov-html (0.13.1)
|
simplecov-html (0.13.2)
|
||||||
simplecov_json_formatter (0.1.4)
|
simplecov_json_formatter (0.1.4)
|
||||||
smart_properties (1.17.0)
|
smart_properties (1.17.0)
|
||||||
sqlite3 (2.5.0)
|
sqlite3 (2.9.0-aarch64-linux-gnu)
|
||||||
mini_portile2 (~> 2.8.0)
|
sqlite3 (2.9.0-aarch64-linux-musl)
|
||||||
sqlite3 (2.5.0-aarch64-linux-gnu)
|
sqlite3 (2.9.0-arm64-darwin)
|
||||||
sqlite3 (2.5.0-aarch64-linux-musl)
|
sqlite3 (2.9.0-x86_64-linux-gnu)
|
||||||
sqlite3 (2.5.0-arm64-darwin)
|
sqlite3 (2.9.0-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.2)
|
|
||||||
strip_attributes (1.14.1)
|
|
||||||
activemodel (>= 3.0, < 9.0)
|
activemodel (>= 3.0, < 9.0)
|
||||||
thor (1.3.2)
|
strscan (3.1.7)
|
||||||
timeout (0.4.3)
|
thor (1.5.0)
|
||||||
|
timeout (0.6.0)
|
||||||
trailblazer-option (0.1.2)
|
trailblazer-option (0.1.2)
|
||||||
turbo-rails (2.0.11)
|
tsort (0.2.0)
|
||||||
actionpack (>= 6.0.0)
|
turbo-rails (2.0.21)
|
||||||
railties (>= 6.0.0)
|
actionpack (>= 7.1.0)
|
||||||
twitter_cldr (6.12.1)
|
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.2025.3)
|
||||||
tzinfo (>= 1.0.0)
|
tzinfo (>= 1.0.0)
|
||||||
uber (0.1.0)
|
uber (0.1.0)
|
||||||
unicode-display_width (3.1.2)
|
unicode-display_width (3.2.0)
|
||||||
unicode-emoji (~> 4.0, >= 4.0.4)
|
unicode-emoji (~> 4.1)
|
||||||
unicode-emoji (4.0.4)
|
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)
|
||||||
@@ -569,23 +597,24 @@ GEM
|
|||||||
activemodel (>= 6.0.0)
|
activemodel (>= 6.0.0)
|
||||||
bindex (>= 0.4.0)
|
bindex (>= 0.4.0)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
webmock (3.24.0)
|
webmock (3.26.1)
|
||||||
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.7.6)
|
websocket-driver (0.8.0)
|
||||||
|
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.1)
|
yaml (0.4.0)
|
||||||
|
zeitwerk (2.7.4)
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -594,7 +623,7 @@ DEPENDENCIES
|
|||||||
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
|
||||||
@@ -602,6 +631,7 @@ DEPENDENCIES
|
|||||||
cancancan
|
cancancan
|
||||||
capybara
|
capybara
|
||||||
csv
|
csv
|
||||||
|
csv-safe
|
||||||
cuprite
|
cuprite
|
||||||
debug
|
debug
|
||||||
devise
|
devise
|
||||||
@@ -620,8 +650,9 @@ DEPENDENCIES
|
|||||||
jwt
|
jwt
|
||||||
letter_opener_web
|
letter_opener_web
|
||||||
lograge
|
lograge
|
||||||
mysql2
|
numo-narray-alt
|
||||||
oj
|
oj
|
||||||
|
onnxruntime
|
||||||
pagy
|
pagy
|
||||||
pg
|
pg
|
||||||
premailer-rails
|
premailer-rails
|
||||||
@@ -647,6 +678,7 @@ DEPENDENCIES
|
|||||||
simplecov
|
simplecov
|
||||||
sqlite3
|
sqlite3
|
||||||
strip_attributes
|
strip_attributes
|
||||||
|
trilogy!
|
||||||
turbo-rails
|
turbo-rails
|
||||||
twitter_cldr
|
twitter_cldr
|
||||||
tzinfo-data
|
tzinfo-data
|
||||||
@@ -654,7 +686,7 @@ DEPENDENCIES
|
|||||||
webmock
|
webmock
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
ruby 3.4.2p28
|
ruby 4.0.1
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.5.3
|
4.0.3
|
||||||
@@ -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
|
||||||
@@ -42,7 +42,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
- PDF signature verification
|
- PDF signature verification
|
||||||
- Users management
|
- Users management
|
||||||
- Mobile-optimized
|
- Mobile-optimized
|
||||||
- 6 UI languages with signing available in 14 languages
|
- 7 UI languages with signing available in 14 languages
|
||||||
- API and Webhooks for integrations
|
- API and Webhooks for integrations
|
||||||
- Easy to deploy in minutes
|
- Easy to deploy in minutes
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,11 @@ class AccountConfigsController < ApplicationController
|
|||||||
AccountConfig::ESIGNING_PREFERENCE_KEY,
|
AccountConfig::ESIGNING_PREFERENCE_KEY,
|
||||||
AccountConfig::FORM_WITH_CONFETTI_KEY,
|
AccountConfig::FORM_WITH_CONFETTI_KEY,
|
||||||
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
|
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
|
||||||
|
AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY,
|
||||||
AccountConfig::FORCE_SSO_AUTH_KEY,
|
AccountConfig::FORCE_SSO_AUTH_KEY,
|
||||||
AccountConfig::FLATTEN_RESULT_PDF_KEY,
|
AccountConfig::FLATTEN_RESULT_PDF_KEY,
|
||||||
AccountConfig::ENFORCE_SIGNING_ORDER_KEY,
|
AccountConfig::ENFORCE_SIGNING_ORDER_KEY,
|
||||||
|
AccountConfig::WITH_FILE_LINKS_KEY,
|
||||||
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,
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -8,7 +8,8 @@ class AccountsController < ApplicationController
|
|||||||
'es-ES' => 'Español',
|
'es-ES' => 'Español',
|
||||||
'pt-PT' => 'Português',
|
'pt-PT' => 'Português',
|
||||||
'de-DE' => 'Deutsch',
|
'de-DE' => 'Deutsch',
|
||||||
'it-IT' => 'Italiano'
|
'it-IT' => 'Italiano',
|
||||||
|
'nl-NL' => 'Nederlands'
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
before_action :load_account
|
before_action :load_account
|
||||||
@@ -27,7 +28,7 @@ class AccountsController < ApplicationController
|
|||||||
unless URI.parse(@encrypted_config.value.to_s).class.in?([URI::HTTP, URI::HTTPS])
|
unless URI.parse(@encrypted_config.value.to_s).class.in?([URI::HTTP, URI::HTTPS])
|
||||||
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url'))
|
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url'))
|
||||||
|
|
||||||
return render :show, status: :unprocessable_entity
|
return render :show, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
@encrypted_config.save!
|
@encrypted_config.save!
|
||||||
@@ -39,13 +40,15 @@ class AccountsController < ApplicationController
|
|||||||
redirect_to settings_account_path, notice: I18n.t('account_information_has_been_updated')
|
redirect_to settings_account_path, notice: I18n.t('account_information_has_been_updated')
|
||||||
end
|
end
|
||||||
rescue ActiveRecord::RecordInvalid
|
rescue ActiveRecord::RecordInvalid
|
||||||
render :show, status: :unprocessable_entity
|
render :show, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
authorize!(:manage, current_account)
|
authorize!(:manage, current_account)
|
||||||
|
|
||||||
|
true_user.skip_reconfirmation!
|
||||||
true_user.update!(locked_at: Time.current, email: true_user.email.sub('@', '+removed@'))
|
true_user.update!(locked_at: Time.current, email: true_user.email.sub('@', '+removed@'))
|
||||||
|
true_user.account.update!(archived_at: Time.current)
|
||||||
|
|
||||||
# rubocop:disable Layout/LineLength
|
# rubocop:disable Layout/LineLength
|
||||||
render turbo_stream: turbo_stream.replace(
|
render turbo_stream: turbo_stream.replace(
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ module Api
|
|||||||
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])
|
||||||
|
|
||||||
if blob_uuid.blank? || (purp.present? && purp != 'blob') || (exp && exp < Time.current.to_i)
|
if blob_uuid.blank? || purp != 'blob'
|
||||||
Rollbar.error('Blob not found') if defined?(Rollbar)
|
Rollbar.error('Blob not found') if defined?(Rollbar)
|
||||||
|
|
||||||
return head :not_found
|
return head :not_found
|
||||||
@@ -24,8 +24,9 @@ module Api
|
|||||||
attachment = blob.attachments.take
|
attachment = blob.attachments.take
|
||||||
|
|
||||||
@record = attachment.record
|
@record = attachment.record
|
||||||
|
@record = @record.record if @record.is_a?(ActiveStorage::Attachment)
|
||||||
|
|
||||||
authorization_check!(attachment) if exp.blank?
|
authorization_check!(attachment, @record, exp)
|
||||||
|
|
||||||
if request.headers['Range'].present?
|
if request.headers['Range'].present?
|
||||||
send_blob_byte_range_data blob, request.headers['Range']
|
send_blob_byte_range_data blob, request.headers['Range']
|
||||||
@@ -41,16 +42,22 @@ module Api
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def authorization_check!(attachment)
|
def authorization_check!(attachment, record, exp)
|
||||||
is_authorized = attachment.name.in?(%w[logo preview_images]) ||
|
return if attachment.name == 'logo'
|
||||||
(current_user && attachment.record.account.id == current_user.account_id) ||
|
return if exp.to_i >= Time.current.to_i
|
||||||
(current_user && !Docuseal.multitenant? && current_user.role == 'superadmin') ||
|
return if current_user && current_ability.can?(:read, record)
|
||||||
!attachment.record.account.account_configs
|
|
||||||
.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value
|
|
||||||
|
|
||||||
return if is_authorized
|
if exp.blank?
|
||||||
|
configs = record.account.account_configs.where(key: [AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
|
||||||
|
AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY])
|
||||||
|
|
||||||
Rollbar.error('Blob aunauthorized') if defined?(Rollbar)
|
require_auth = configs.any? { |c| c.key == AccountConfig::DOWNLOAD_LINKS_AUTH_KEY && c.value }
|
||||||
|
require_ttl = configs.none? { |c| c.key == AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY && c.value == false }
|
||||||
|
|
||||||
|
return if !require_ttl && !require_auth
|
||||||
|
end
|
||||||
|
|
||||||
|
Rollbar.error('Blob unauthorized') if defined?(Rollbar)
|
||||||
|
|
||||||
raise CanCan::AccessDenied
|
raise CanCan::AccessDenied
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
module Api
|
module Api
|
||||||
class ApiBaseController < ActionController::API
|
class ApiBaseController < ActionController::API
|
||||||
include ActiveStorage::SetCurrent
|
include ActiveStorage::SetCurrent
|
||||||
include Pagy::Backend
|
include Pagy::Method
|
||||||
|
|
||||||
DEFAULT_LIMIT = 10
|
DEFAULT_LIMIT = 10
|
||||||
MAX_LIMIT = 100
|
MAX_LIMIT = 100
|
||||||
@@ -16,7 +16,7 @@ module Api
|
|||||||
check_authorization
|
check_authorization
|
||||||
|
|
||||||
rescue_from Params::BaseValidator::InvalidParameterError do |e|
|
rescue_from Params::BaseValidator::InvalidParameterError do |e|
|
||||||
render json: { error: e.message }, status: :unprocessable_entity
|
render json: { error: e.message }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
rescue_from RateLimit::LimitApproached do |e|
|
rescue_from RateLimit::LimitApproached do |e|
|
||||||
@@ -33,7 +33,7 @@ module Api
|
|||||||
rescue_from JSON::ParserError do |e|
|
rescue_from JSON::ParserError do |e|
|
||||||
Rollbar.warning(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
render json: { error: "JSON parse error: #{e.message}" }, status: :unprocessable_entity
|
render json: { error: "JSON parse error: #{e.message}" }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,21 @@ module Api
|
|||||||
def create
|
def create
|
||||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
if params[:type].in?(%w[initials signature]) && ImageUtils.blank?(Vips::Image.new_from_file(params[:file].path))
|
if params[:type].in?(%w[initials signature])
|
||||||
Rollbar.error("Empty signature: #{submitter.id}") if defined?(Rollbar)
|
image = Vips::Image.new_from_file(params[:file].path)
|
||||||
|
|
||||||
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_entity
|
if ImageUtils.blank?(image)
|
||||||
|
Rollbar.error("Empty signature: #{submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_content
|
||||||
|
end
|
||||||
|
|
||||||
|
if ImageUtils.error?(image)
|
||||||
|
Rollbar.error("Error signature: #{submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return render json: { error: "#{params[:type]} error, try to sign on another device" },
|
||||||
|
status: :unprocessable_content
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
attachment = Submitters.create_attachment!(submitter, params)
|
attachment = Submitters.create_attachment!(submitter, params)
|
||||||
@@ -23,6 +34,10 @@ module Api
|
|||||||
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
|
||||||
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
|
render json: { error: e.message }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_new_cookie_signatures_json(submitter, attachment)
|
def build_new_cookie_signatures_json(submitter, attachment)
|
||||||
|
|||||||
@@ -11,18 +11,21 @@ module Api
|
|||||||
params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present?
|
params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present?
|
||||||
|
|
||||||
submitters = paginate(
|
submitters = paginate(
|
||||||
submitters.preload(template: :folder, submission: [:submitters, { audit_trail_attachment: :blob,
|
submitters.preload(template: { folder: :parent_folder },
|
||||||
combined_document_attachment: :blob }],
|
submission: [:submitters, { audit_trail_attachment: :blob,
|
||||||
|
combined_document_attachment: :blob }],
|
||||||
documents_attachments: :blob, attachments_attachments: :blob),
|
documents_attachments: :blob, attachments_attachments: :blob),
|
||||||
field: :completed_at
|
field: :completed_at
|
||||||
)
|
)
|
||||||
|
|
||||||
|
expires_at = Accounts.link_expires_at(current_account)
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
data: submitters.map do |s|
|
data: submitters.map do |s|
|
||||||
{
|
{
|
||||||
event_type: 'form.completed',
|
event_type: 'form.completed',
|
||||||
timestamp: s.completed_at,
|
timestamp: s.completed_at,
|
||||||
data: Submitters::SerializeForWebhook.call(s)
|
data: Submitters::SerializeForWebhook.call(s, expires_at:)
|
||||||
}
|
}
|
||||||
end,
|
end,
|
||||||
pagination: {
|
pagination: {
|
||||||
|
|||||||
@@ -5,41 +5,71 @@ 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]
|
expires_at = Accounts.link_expires_at(current_account)
|
||||||
).call
|
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
id: @submission.id,
|
id: @submission.id,
|
||||||
documents: documents.map do |attachment|
|
documents: documents.map do |attachment|
|
||||||
{ name: attachment.filename.base, url: ActiveStorage::Blob.proxy_url(attachment.blob) }
|
{ name: attachment.filename.base, url: ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) }
|
||||||
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
|
||||||
@@ -14,16 +14,19 @@ module Api
|
|||||||
:created_by_user, :submission_events,
|
:created_by_user, :submission_events,
|
||||||
template: :folder,
|
template: :folder,
|
||||||
submitters: { documents_attachments: :blob, attachments_attachments: :blob },
|
submitters: { documents_attachments: :blob, attachments_attachments: :blob },
|
||||||
audit_trail_attachment: :blob
|
audit_trail_attachment: :blob,
|
||||||
|
combined_document_attachment: :blob
|
||||||
),
|
),
|
||||||
field: :completed_at)
|
field: :completed_at)
|
||||||
|
|
||||||
|
expires_at = Accounts.link_expires_at(current_account)
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
data: submissions.map do |s|
|
data: submissions.map do |s|
|
||||||
{
|
{
|
||||||
event_type: 'submission.completed',
|
event_type: 'submission.completed',
|
||||||
timestamp: s.completed_at,
|
timestamp: s.completed_at,
|
||||||
data: Submissions::SerializeForApi.call(s, s.submitters)
|
data: Submissions::SerializeForApi.call(s, s.submitters, expires_at:)
|
||||||
}
|
}
|
||||||
end,
|
end,
|
||||||
pagination: {
|
pagination: {
|
||||||
|
|||||||
@@ -14,14 +14,16 @@ module Api
|
|||||||
submissions = filter_submissions(submissions, params)
|
submissions = filter_submissions(submissions, params)
|
||||||
|
|
||||||
submissions = paginate(submissions.preload(:created_by_user, :submitters,
|
submissions = paginate(submissions.preload(:created_by_user, :submitters,
|
||||||
template: :folder,
|
template: { folder: :parent_folder },
|
||||||
combined_document_attachment: :blob,
|
combined_document_attachment: :blob,
|
||||||
audit_trail_attachment: :blob))
|
audit_trail_attachment: :blob))
|
||||||
|
|
||||||
|
expires_at = Accounts.link_expires_at(current_account)
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
data: submissions.map do |s|
|
data: submissions.map do |s|
|
||||||
Submissions::SerializeForApi.call(s, s.submitters, params,
|
Submissions::SerializeForApi.call(s, s.submitters, params,
|
||||||
with_events: false, with_documents: false, with_values: false)
|
with_events: false, with_documents: false, with_values: false, expires_at:)
|
||||||
end,
|
end,
|
||||||
pagination: {
|
pagination: {
|
||||||
count: submissions.size,
|
count: submissions.size,
|
||||||
@@ -41,7 +43,7 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
if @submission.audit_trail_attachment.blank? && submitters.all?(&:completed_at?)
|
if @submission.audit_trail_attachment.blank? && submitters.all?(&:completed_at?)
|
||||||
@submission.audit_trail_attachment = Submissions::GenerateAuditTrail.call(@submission)
|
@submission.audit_trail_attachment = Submissions::EnsureAuditGenerated.call(@submission)
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: Submissions::SerializeForApi.call(@submission, submitters, params)
|
render json: Submissions::SerializeForApi.call(@submission, submitters, params)
|
||||||
@@ -50,12 +52,12 @@ module Api
|
|||||||
def create
|
def create
|
||||||
Params::SubmissionCreateValidator.call(params)
|
Params::SubmissionCreateValidator.call(params)
|
||||||
|
|
||||||
return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil?
|
return render json: { error: 'Template not found' }, status: :unprocessable_content if @template.nil?
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
return render json: { error: 'Template does not contain fields' }, status: :unprocessable_entity
|
return render json: { error: 'Template does not contain fields' }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
params[:send_email] = true unless params.key?(:send_email)
|
params[:send_email] = true unless params.key?(:send_email)
|
||||||
@@ -82,7 +84,7 @@ module Api
|
|||||||
DownloadUtils::UnableToDownload => e
|
DownloadUtils::UnableToDownload => e
|
||||||
Rollbar.warning(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
render json: { error: e.message }, status: :unprocessable_entity
|
render json: { error: e.message }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@@ -104,9 +106,10 @@ module Api
|
|||||||
submissions = submissions.where(slug: params[:slug]) if params[:slug].present?
|
submissions = submissions.where(slug: params[:slug]) if params[:slug].present?
|
||||||
|
|
||||||
if params[:template_folder].present?
|
if params[:template_folder].present?
|
||||||
folder_ids = TemplateFolder.accessible_by(current_ability).where(name: params[:template_folder]).pluck(:id)
|
folders =
|
||||||
|
TemplateFolders.filter_by_full_name(TemplateFolder.accessible_by(current_ability), params[:template_folder])
|
||||||
|
|
||||||
submissions = submissions.joins(:template).where(template: { folder_id: folder_ids })
|
submissions = submissions.joins(:template).where(template: { folder_id: folders.pluck(:id) })
|
||||||
end
|
end
|
||||||
|
|
||||||
if params.key?(:archived)
|
if params.key?(:archived)
|
||||||
@@ -144,7 +147,7 @@ module Api
|
|||||||
is_send_email = !params[:send_email].in?(['false', false])
|
is_send_email = !params[:send_email].in?(['false', false])
|
||||||
|
|
||||||
if (emails = (params[:emails] || params[:email]).presence) &&
|
if (emails = (params[:emails] || params[:email]).presence) &&
|
||||||
(params[:submission].blank? && params[:submitters].blank?)
|
params[:submission].blank? && params[:submitters].blank?
|
||||||
Submissions.create_from_emails(template:,
|
Submissions.create_from_emails(template:,
|
||||||
user: current_user,
|
user: current_user,
|
||||||
source: :api,
|
source: :api,
|
||||||
@@ -179,16 +182,17 @@ 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: {},
|
||||||
message: %i[subject body],
|
message: %i[subject body],
|
||||||
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||||
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
|
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
|
||||||
:require_phone_2fa,
|
:require_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,
|
||||||
{ default_value: [], value: [], preferences: {} }] }]]
|
{ default_value: [], value: [], preferences: {}, validation: {} }] }]]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ module Api
|
|||||||
documents_attachments: :blob, attachments_attachments: :blob)
|
documents_attachments: :blob, attachments_attachments: :blob)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
expires_at = Accounts.link_expires_at(current_account)
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
data: submitters.map do |s|
|
data: submitters.map do |s|
|
||||||
Submitters::SerializeForApi.call(s, with_template: true, with_events: true, params:)
|
Submitters::SerializeForApi.call(s, with_template: true, with_events: true, params:, expires_at:)
|
||||||
end,
|
end,
|
||||||
pagination: {
|
pagination: {
|
||||||
count: submitters.size,
|
count: submitters.size,
|
||||||
@@ -34,7 +36,7 @@ module Api
|
|||||||
|
|
||||||
def update
|
def update
|
||||||
if @submitter.completed_at?
|
if @submitter.completed_at?
|
||||||
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_entity
|
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
submission = @submitter.submission
|
submission = @submitter.submission
|
||||||
@@ -74,7 +76,7 @@ module Api
|
|||||||
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
|
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
|
||||||
Rollbar.warning(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
render json: { error: e.message }, status: :unprocessable_entity
|
render json: { error: e.message }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
def submitter_params
|
def submitter_params
|
||||||
@@ -82,7 +84,7 @@ module Api
|
|||||||
|
|
||||||
submitter_params.permit(
|
submitter_params.permit(
|
||||||
:send_email, :send_sms, :reply_to, :completed_redirect_url, :uuid, :name, :email, :role,
|
:send_email, :send_sms, :reply_to, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||||
:completed, :phone, :application_key, :external_id, :go_to_last, :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,
|
||||||
|
|||||||
@@ -26,13 +26,15 @@ module Api
|
|||||||
original_template: @template,
|
original_template: @template,
|
||||||
documents: params[:documents])
|
documents: params[:documents])
|
||||||
|
|
||||||
|
Templates.maybe_assign_access(cloned_template)
|
||||||
|
|
||||||
cloned_template.save!
|
cloned_template.save!
|
||||||
|
|
||||||
WebhookUrls.enqueue_events(cloned_template, 'template.created')
|
WebhookUrls.enqueue_events(cloned_template, 'template.created')
|
||||||
|
|
||||||
SearchEntries.enqueue_reindex(cloned_template)
|
SearchEntries.enqueue_reindex(cloned_template)
|
||||||
|
|
||||||
render json: Templates::SerializeForApi.call(cloned_template, schema_documents)
|
render json: Templates::SerializeForApi.call(cloned_template, schema_documents:)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -7,7 +7,7 @@ module Api
|
|||||||
def index
|
def index
|
||||||
templates = filter_templates(@templates, params)
|
templates = filter_templates(@templates, params)
|
||||||
|
|
||||||
templates = paginate(templates.preload(:author, :folder))
|
templates = paginate(templates.preload(:author, folder: :parent_folder))
|
||||||
|
|
||||||
schema_documents =
|
schema_documents =
|
||||||
ActiveStorage::Attachment.where(record_id: templates.map(&:id),
|
ActiveStorage::Attachment.where(record_id: templates.map(&:id),
|
||||||
@@ -24,13 +24,14 @@ module Api
|
|||||||
name: :preview_images)
|
name: :preview_images)
|
||||||
.preload(:blob)
|
.preload(:blob)
|
||||||
|
|
||||||
|
expires_at = Accounts.link_expires_at(current_account)
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
data: templates.map do |t|
|
data: templates.map do |t|
|
||||||
Templates::SerializeForApi.call(
|
Templates::SerializeForApi.call(t,
|
||||||
t,
|
schema_documents: schema_documents.select { |e| e.record_id == t.id },
|
||||||
schema_documents.select { |e| e.record_id == t.id },
|
preview_image_attachments:,
|
||||||
preview_image_attachments
|
expires_at:)
|
||||||
)
|
|
||||||
end,
|
end,
|
||||||
pagination: {
|
pagination: {
|
||||||
count: templates.size,
|
count: templates.size,
|
||||||
@@ -92,9 +93,9 @@ module Api
|
|||||||
templates = templates.where(slug: params[:slug]) if params[:slug].present?
|
templates = templates.where(slug: params[:slug]) if params[:slug].present?
|
||||||
|
|
||||||
if params[:folder].present?
|
if params[:folder].present?
|
||||||
folder_ids = TemplateFolder.accessible_by(current_ability).where(name: params[:folder]).pluck(:id)
|
folders = TemplateFolders.filter_by_full_name(TemplateFolder.accessible_by(current_ability), params[:folder])
|
||||||
|
|
||||||
templates = templates.where(folder_id: folder_ids)
|
templates = templates.where(folder_id: folders.pluck(:id))
|
||||||
end
|
end
|
||||||
|
|
||||||
templates
|
templates
|
||||||
@@ -106,14 +107,15 @@ 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]],
|
submitters: [%i[name uuid is_requester invite_by_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,
|
:title, :description, :prefillable,
|
||||||
{ preferences: {},
|
{ preferences: {},
|
||||||
|
default_value: [],
|
||||||
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],
|
validation: %i[message pattern min max step],
|
||||||
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
|
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ module Api
|
|||||||
def merge
|
def merge
|
||||||
files = params[:files] || []
|
files = params[:files] || []
|
||||||
|
|
||||||
return render json: { error: 'Files are required' }, status: :unprocessable_entity if files.blank?
|
return render json: { error: 'Files are required' }, status: :unprocessable_content if files.blank?
|
||||||
return render json: { error: 'At least 2 files are required' }, status: :unprocessable_entity if files.size < 2
|
return render json: { error: 'At least 2 files are required' }, status: :unprocessable_content if files.size < 2
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
data: Base64.encode64(PdfUtils.merge(files.map { |base64| StringIO.new(Base64.decode64(base64)) }).string)
|
data: Base64.encode64(PdfUtils.merge(files.map { |base64| StringIO.new(Base64.decode64(base64)) }).string)
|
||||||
@@ -35,7 +35,7 @@ module Api
|
|||||||
end
|
end
|
||||||
}
|
}
|
||||||
rescue HexaPDF::MalformedPDFError
|
rescue HexaPDF::MalformedPDFError
|
||||||
render json: { error: 'Malformed PDF' }, status: :unprocessable_entity
|
render json: { error: 'Malformed PDF' }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -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?
|
||||||
|
|
||||||
@@ -13,14 +13,17 @@ class ApplicationController < ActionController::Base
|
|||||||
before_action :maybe_redirect_to_setup, unless: :signed_in?
|
before_action :maybe_redirect_to_setup, unless: :signed_in?
|
||||||
before_action :authenticate_user!, unless: :devise_controller?
|
before_action :authenticate_user!, unless: :devise_controller?
|
||||||
|
|
||||||
|
before_action :set_csp, if: -> { request.get? && !request.headers['HTTP_X_TURBO'] }
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -57,7 +60,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
|
||||||
@@ -100,6 +103,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
|
||||||
@@ -123,4 +130,21 @@ class ApplicationController < ActionController::Base
|
|||||||
|
|
||||||
redirect_to request.url.gsub('.co/', '.com/'), allow_other_host: true, status: :moved_permanently
|
redirect_to request.url.gsub('.co/', '.com/'), allow_other_host: true, status: :moved_permanently
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_csp
|
||||||
|
request.content_security_policy = current_content_security_policy.tap do |policy|
|
||||||
|
policy.default_src :self
|
||||||
|
policy.script_src :self
|
||||||
|
policy.style_src :self, :unsafe_inline
|
||||||
|
policy.img_src :self, :https, :http, :blob, :data
|
||||||
|
policy.font_src :self, :https, :http, :blob, :data
|
||||||
|
policy.manifest_src :self
|
||||||
|
policy.media_src :self
|
||||||
|
policy.frame_src :self
|
||||||
|
policy.worker_src :self, :blob
|
||||||
|
policy.connect_src :self
|
||||||
|
|
||||||
|
policy.directives['connect-src'] << 'ws:' if Rails.env.development?
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -17,8 +17,10 @@ class ConsoleRedirectController < ApplicationController
|
|||||||
scope: :console,
|
scope: :console,
|
||||||
exp: 1.minute.from_now.to_i)
|
exp: 1.minute.from_now.to_i)
|
||||||
|
|
||||||
path = Addressable::URI.parse(params[:redir]).path if params[:redir].to_s.starts_with?(Docuseal::CONSOLE_URL)
|
redir_uri = Addressable::URI.parse(params[:redir])
|
||||||
|
path = redir_uri.path if params[:redir].to_s.starts_with?(Docuseal::CONSOLE_URL)
|
||||||
|
|
||||||
redirect_to("#{Docuseal::CONSOLE_URL}#{path}?#{{ auth: }.to_query}", allow_other_host: true)
|
redirect_to "#{Docuseal::CONSOLE_URL}#{path}?#{{ **redir_uri&.query_values, 'auth' => auth }.to_query}",
|
||||||
|
allow_other_host: true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -15,12 +15,12 @@ class EmailSmtpSettingsController < ApplicationController
|
|||||||
|
|
||||||
redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved')
|
redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved')
|
||||||
else
|
else
|
||||||
render :index, status: :unprocessable_entity
|
render :index, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
flash[:alert] = e.message
|
flash[:alert] = e.message
|
||||||
|
|
||||||
render :index, status: :unprocessable_entity
|
render :index, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class EsignSettingsController < ApplicationController
|
|||||||
@cert_record.errors.add(:name, I18n.t('already_exists'))
|
@cert_record.errors.add(:name, I18n.t('already_exists'))
|
||||||
|
|
||||||
return render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'),
|
return render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'),
|
||||||
status: :unprocessable_entity
|
status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
save_new_cert!(@encrypted_config, @cert_record)
|
save_new_cert!(@encrypted_config, @cert_record)
|
||||||
@@ -64,7 +64,7 @@ class EsignSettingsController < ApplicationController
|
|||||||
|
|
||||||
@cert_record.errors.add(:password, e.message)
|
@cert_record.errors.add(:password, e.message)
|
||||||
|
|
||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), status: :unprocessable_entity
|
render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class InvitationsController < Devise::PasswordsController
|
class InvitationsController < Devise::PasswordsController
|
||||||
|
def update
|
||||||
|
super do |resource|
|
||||||
|
resource.confirmed_at ||= Time.current if resource.errors.empty?
|
||||||
|
|
||||||
|
PasswordsController::Current.user = resource
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -24,7 +24,7 @@ class MfaSetupController < ApplicationController
|
|||||||
|
|
||||||
@error_message = I18n.t('code_is_invalid')
|
@error_message = I18n.t('code_is_invalid')
|
||||||
|
|
||||||
render turbo_stream: turbo_stream.replace(:mfa_form, partial: 'mfa_setup/form'), status: :unprocessable_entity
|
render turbo_stream: turbo_stream.replace(:mfa_form, partial: 'mfa_setup/form'), status: :unprocessable_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ class MfaSetupController < ApplicationController
|
|||||||
else
|
else
|
||||||
@error_message = I18n.t('code_is_invalid')
|
@error_message = I18n.t('code_is_invalid')
|
||||||
|
|
||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_entity
|
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class PasswordsController < Devise::PasswordsController
|
class PasswordsController < Devise::PasswordsController
|
||||||
|
# rubocop:disable Rails/LexicallyScopedActionFilter
|
||||||
|
skip_before_action :require_no_authentication, only: %i[edit update]
|
||||||
|
# rubocop:enable Rails/LexicallyScopedActionFilter
|
||||||
|
|
||||||
|
around_action :with_browser_locale
|
||||||
|
|
||||||
class Current < ActiveSupport::CurrentAttributes
|
class Current < ActiveSupport::CurrentAttributes
|
||||||
attribute :user
|
attribute :user
|
||||||
end
|
end
|
||||||
@@ -16,4 +22,10 @@ class PasswordsController < Devise::PasswordsController
|
|||||||
Current.user = resource
|
Current.user = resource
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def after_resetting_password_path_for(_)
|
||||||
|
new_session_path(resource_name)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -4,6 +4,7 @@ class PersonalizationSettingsController < ApplicationController
|
|||||||
ALLOWED_KEYS = [
|
ALLOWED_KEYS = [
|
||||||
AccountConfig::FORM_COMPLETED_BUTTON_KEY,
|
AccountConfig::FORM_COMPLETED_BUTTON_KEY,
|
||||||
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
|
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
|
||||||
|
AccountConfig::SUBMITTER_INVITATION_REMINDER_EMAIL_KEY,
|
||||||
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
|
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
|
||||||
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY,
|
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY,
|
||||||
AccountConfig::FORM_COMPLETED_MESSAGE_KEY,
|
AccountConfig::FORM_COMPLETED_MESSAGE_KEY,
|
||||||
|
|||||||
@@ -9,18 +9,25 @@ class ProfileController < ApplicationController
|
|||||||
|
|
||||||
def update_contact
|
def update_contact
|
||||||
if current_user.update(contact_params)
|
if current_user.update(contact_params)
|
||||||
redirect_to settings_profile_index_path, notice: I18n.t('contact_information_has_been_update')
|
if current_user.try(:pending_reconfirmation?) && current_user.previous_changes.key?(:unconfirmed_email)
|
||||||
|
SendConfirmationInstructionsJob.perform_async('user_id' => current_user.id)
|
||||||
|
|
||||||
|
redirect_to settings_profile_index_path,
|
||||||
|
notice: I18n.t('a_confirmation_email_has_been_sent_to_the_new_email_address')
|
||||||
|
else
|
||||||
|
redirect_to settings_profile_index_path, notice: I18n.t('contact_information_has_been_update')
|
||||||
|
end
|
||||||
else
|
else
|
||||||
render :index, status: :unprocessable_entity
|
render :index, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_password
|
def update_password
|
||||||
if current_user.update(password_params)
|
if current_user.update_with_password(password_params)
|
||||||
bypass_sign_in(current_user)
|
bypass_sign_in(current_user)
|
||||||
redirect_to settings_profile_index_path, notice: I18n.t('password_has_been_changed')
|
redirect_to settings_profile_index_path, notice: I18n.t('password_has_been_changed')
|
||||||
else
|
else
|
||||||
render :index, status: :unprocessable_entity
|
render :index, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -31,6 +38,6 @@ class ProfileController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def password_params
|
def password_params
|
||||||
params.require(:user).permit(:password, :password_confirmation)
|
params.require(:user).permit(:password, :password_confirmation, :current_password)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class RevealAccessTokenController < ApplicationController
|
||||||
|
def show
|
||||||
|
authorize!(:manage, current_user.access_token)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize!(:manage, current_user.access_token)
|
||||||
|
|
||||||
|
if current_user.valid_password?(params[:password])
|
||||||
|
render turbo_stream: turbo_stream.replace(:access_token_container,
|
||||||
|
partial: 'reveal_access_token/access_token',
|
||||||
|
locals: { token: current_user.access_token.token })
|
||||||
|
else
|
||||||
|
render turbo_stream: turbo_stream.replace(:modal, template: 'reveal_access_token/show',
|
||||||
|
locals: { error_message: I18n.t('wrong_password') }),
|
||||||
|
status: :unprocessable_content
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -29,7 +29,7 @@ class SendSubmissionEmailController < ApplicationController
|
|||||||
|
|
||||||
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
||||||
|
|
||||||
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! unless already_sent?(@submitter)
|
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! if can_send?(@submitter)
|
||||||
|
|
||||||
respond_to do |f|
|
respond_to do |f|
|
||||||
f.html { render :success }
|
f.html { render :success }
|
||||||
@@ -39,8 +39,11 @@ class SendSubmissionEmailController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def already_sent?(submitter)
|
def can_send?(submitter)
|
||||||
EmailEvent.exists?(tag: :submitter_documents_copy, email: submitter.email, emailable: submitter,
|
return false if submitter.account.archived_at?
|
||||||
event_type: :send, created_at: SEND_DURATION.ago..Time.current)
|
return false if EmailEvent.exists?(tag: :submitter_documents_copy, email: submitter.email, emailable: submitter,
|
||||||
|
event_type: :send, created_at: SEND_DURATION.ago..Time.current)
|
||||||
|
|
||||||
|
true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -16,7 +16,7 @@ class SessionsController < Devise::SessionsController
|
|||||||
end
|
end
|
||||||
|
|
||||||
if User.exists?(email:, otp_required_for_login: true) && sign_in_params[:otp_attempt].blank?
|
if User.exists?(email:, otp_required_for_login: true) && sign_in_params[:otp_attempt].blank?
|
||||||
return render :otp, locals: { resource: User.new(sign_in_params) }, status: :unprocessable_entity
|
return render :otp, locals: { resource: User.new(sign_in_params) }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
super
|
super
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ class SetupController < ApplicationController
|
|||||||
unless URI.parse(encrypted_config_params[:value].to_s).class.in?([URI::HTTP, URI::HTTPS])
|
unless URI.parse(encrypted_config_params[:value].to_s).class.in?([URI::HTTP, URI::HTTPS])
|
||||||
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url'))
|
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url'))
|
||||||
|
|
||||||
return render :index, status: :unprocessable_entity
|
return render :index, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
return render :index, status: :unprocessable_entity unless @account.valid?
|
return render :index, status: :unprocessable_content unless @account.valid?
|
||||||
|
|
||||||
if @user.save
|
if @user.save
|
||||||
encrypted_configs = [
|
encrypted_configs = [
|
||||||
@@ -42,7 +42,7 @@ class SetupController < ApplicationController
|
|||||||
|
|
||||||
redirect_to newsletter_path
|
redirect_to newsletter_path
|
||||||
else
|
else
|
||||||
render :index, status: :unprocessable_entity
|
render :index, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -42,7 +44,7 @@ class StartFormController < ApplicationController
|
|||||||
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record?
|
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record?
|
||||||
@error_message = multiple_submitters_error_message
|
@error_message = multiple_submitters_error_message
|
||||||
|
|
||||||
return render :show, status: :unprocessable_entity
|
return render :show, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
if (is_new_record = @submitter.new_record?)
|
if (is_new_record = @submitter.new_record?)
|
||||||
@@ -60,7 +62,7 @@ class StartFormController < ApplicationController
|
|||||||
|
|
||||||
redirect_to submit_form_path(@submitter.slug)
|
redirect_to submit_form_path(@submitter.slug)
|
||||||
else
|
else
|
||||||
render :show, status: :unprocessable_entity
|
render :show, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -81,7 +83,7 @@ class StartFormController < ApplicationController
|
|||||||
|
|
||||||
@submitter = Submitter.where(submission: @template.submissions)
|
@submitter = Submitter.where(submission: @template.submissions)
|
||||||
.where.not(completed_at: nil)
|
.where.not(completed_at: nil)
|
||||||
.find_by!(required_params)
|
.find_by!(required_params.except('name'))
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -206,7 +208,7 @@ class StartFormController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def handle_require_2fa(submitter, is_new_record:)
|
def handle_require_2fa(submitter, is_new_record:)
|
||||||
return render :show, status: :unprocessable_entity if submitter.errors.present?
|
return render :show, status: :unprocessable_content if submitter.errors.present?
|
||||||
|
|
||||||
is_otp_verified = Submitters.verify_link_otp!(params[:one_time_code], submitter)
|
is_otp_verified = Submitters.verify_link_otp!(params[:one_time_code], submitter)
|
||||||
|
|
||||||
@@ -223,7 +225,7 @@ class StartFormController < ApplicationController
|
|||||||
|
|
||||||
redirect_to submit_form_path(submitter.slug)
|
redirect_to submit_form_path(submitter.slug)
|
||||||
else
|
else
|
||||||
render :show, status: :unprocessable_entity
|
render :show, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
Submitters.send_shared_link_email_verification_code(submitter, request:)
|
Submitters.send_shared_link_email_verification_code(submitter, request:)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class StorageSettingsController < ApplicationController
|
|||||||
|
|
||||||
redirect_to settings_storage_index_path, notice: I18n.t('changes_have_been_saved')
|
redirect_to settings_storage_index_path, notice: I18n.t('changes_have_been_saved')
|
||||||
else
|
else
|
||||||
render :index, status: :unprocessable_entity
|
render :index, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ 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',
|
||||||
|
|||||||
@@ -12,6 +12,14 @@ 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)
|
||||||
|
|
||||||
@@ -32,10 +40,7 @@ class SubmissionsController < ApplicationController
|
|||||||
def create
|
def create
|
||||||
save_template_message(@template, params) if params[:save_message] == '1'
|
save_template_message(@template, params) if params[:save_message] == '1'
|
||||||
|
|
||||||
if params[:is_custom_message] != '1'
|
[params.delete(:subject), params.delete(:body)] if params[:is_custom_message] != '1'
|
||||||
params.delete(:subject)
|
|
||||||
params.delete(:body)
|
|
||||||
end
|
|
||||||
|
|
||||||
submissions =
|
submissions =
|
||||||
if params[:emails].present?
|
if params[:emails].present?
|
||||||
@@ -46,11 +51,17 @@ 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
|
||||||
|
|
||||||
|
submissions_attrs, _, new_fields =
|
||||||
|
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_attrs, @template, add_fields: true)
|
||||||
|
|
||||||
Submissions.create_from_submitters(template: @template,
|
Submissions.create_from_submitters(template: @template,
|
||||||
user: current_user,
|
user: current_user,
|
||||||
source: :invite,
|
source: :invite,
|
||||||
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
|
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
|
||||||
submissions_attrs: submissions_params[:submission].to_h.values,
|
submissions_attrs:,
|
||||||
|
new_fields:,
|
||||||
params: params.merge('send_completed_email' => true))
|
params: params.merge('send_completed_email' => true))
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -62,10 +73,9 @@ class SubmissionsController < ApplicationController
|
|||||||
|
|
||||||
redirect_to template_path(@template), notice: I18n.t('new_recipients_have_been_added')
|
redirect_to template_path(@template), notice: I18n.t('new_recipients_have_been_added')
|
||||||
rescue Submissions::CreateFromSubmitters::BaseError => e
|
rescue Submissions::CreateFromSubmitters::BaseError => e
|
||||||
render turbo_stream: turbo_stream.replace(:submitters_error,
|
render turbo_stream: turbo_stream.replace(:submitters_error, partial: 'submissions/error',
|
||||||
partial: 'submissions/error',
|
locals: { error: e.message }),
|
||||||
locals: { error: e.message }),
|
status: :unprocessable_content
|
||||||
status: :unprocessable_entity
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@@ -95,7 +105,7 @@ class SubmissionsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def submissions_params
|
def submissions_params
|
||||||
params.permit(submission: { submitters: [%i[uuid email phone name]] })
|
params.permit(submission: { submitters: [:uuid, :email, :phone, :name, { values: {} }] })
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_template
|
def load_template
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
return if submitter.submission.submitters.order(:completed_at).last != submitter
|
return if submitter.submission.submitters.order(:completed_at).last != submitter
|
||||||
|
|
||||||
attachment = submitter.submission.combined_document_attachment
|
attachment = submitter.submission.combined_document_attachment
|
||||||
attachment ||= Submissions::GenerateCombinedAttachment.call(submitter)
|
attachment ||= Submissions::EnsureCombinedGenerated.call(submitter)
|
||||||
|
|
||||||
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
|
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
|
||||||
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
|
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
|
||||||
|
|||||||
@@ -10,11 +10,16 @@ class SubmissionsExportController < ApplicationController
|
|||||||
attachments_attachments: :blob })
|
attachments_attachments: :blob })
|
||||||
.order(id: :asc)
|
.order(id: :asc)
|
||||||
|
|
||||||
|
submissions = Submissions.search(current_user, submissions, params[:q], search_values: true)
|
||||||
|
submissions = Submissions::Filter.call(submissions, current_user, params)
|
||||||
|
|
||||||
|
expires_at = Accounts.link_expires_at(current_account)
|
||||||
|
|
||||||
if params[:format] == 'csv'
|
if params[:format] == 'csv'
|
||||||
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format]),
|
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format], expires_at:),
|
||||||
filename: "#{@template.name}.csv"
|
filename: "#{@template.name}.csv"
|
||||||
elsif params[:format] == 'xlsx'
|
elsif params[:format] == 'xlsx'
|
||||||
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format]),
|
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format], expires_at:),
|
||||||
filename: "#{@template.name}.xlsx"
|
filename: "#{@template.name}.xlsx"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
class SubmissionsFiltersController < ApplicationController
|
class SubmissionsFiltersController < ApplicationController
|
||||||
ALLOWED_NAMES = %w[
|
ALLOWED_NAMES = %w[
|
||||||
author
|
author
|
||||||
|
folder
|
||||||
completed_at
|
completed_at
|
||||||
status
|
status
|
||||||
created_at
|
created_at
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ class SubmissionsPreviewController < ApplicationController
|
|||||||
|
|
||||||
def completed
|
def completed
|
||||||
@submission = Submission.find_by!(slug: params[:submissions_preview_slug])
|
@submission = Submission.find_by!(slug: params[:submissions_preview_slug])
|
||||||
|
|
||||||
|
raise ActionController::RoutingError, I18n.t('not_found') if @submission.account.archived_at?
|
||||||
|
|
||||||
@template = @submission.template
|
@template = @submission.template
|
||||||
|
|
||||||
render :completed, layout: 'form'
|
render :completed, layout: 'form'
|
||||||
@@ -50,8 +53,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
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class SubmitFormController < ApplicationController
|
|||||||
submission = @submitter.submission
|
submission = @submitter.submission
|
||||||
|
|
||||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||||
|
return render :email_2fa if require_email_2fa?(@submitter)
|
||||||
|
|
||||||
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
|
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
|
||||||
|
|
||||||
@@ -47,21 +48,26 @@ class SubmitFormController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
if require_email_2fa?(@submitter)
|
||||||
|
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_entity
|
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at?
|
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at?
|
||||||
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_entity
|
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
if @submitter.submission.expired?
|
if @submitter.submission.expired?
|
||||||
return render json: { error: I18n.t('form_has_been_expired') }, status: :unprocessable_entity
|
return render json: { error: I18n.t('form_has_been_expired') }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
if @submitter.declined_at?
|
if @submitter.declined_at?
|
||||||
return render json: { error: I18n.t('form_has_been_declined') },
|
return render json: { error: I18n.t('form_has_been_declined') },
|
||||||
status: :unprocessable_entity
|
status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
Submitters::SubmitValues.call(@submitter, params, request)
|
Submitters::SubmitValues.call(@submitter, params, request)
|
||||||
@@ -70,12 +76,16 @@ class SubmitFormController < ApplicationController
|
|||||||
rescue Submitters::SubmitValues::RequiredFieldError => e
|
rescue Submitters::SubmitValues::RequiredFieldError => e
|
||||||
Rollbar.warning("Required field #{@submitter.id}: #{e.message}") if defined?(Rollbar)
|
Rollbar.warning("Required field #{@submitter.id}: #{e.message}") if defined?(Rollbar)
|
||||||
|
|
||||||
render json: { field_uuid: e.message }, status: :unprocessable_entity
|
render json: { field_uuid: e.message }, status: :unprocessable_content
|
||||||
rescue Submitters::SubmitValues::ValidationError => e
|
rescue Submitters::SubmitValues::ValidationError => e
|
||||||
render json: { error: e.message }, status: :unprocessable_entity
|
render json: { error: e.message }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
def completed; end
|
def completed
|
||||||
|
raise ActionController::RoutingError, I18n.t('not_found') if @submitter.account.archived_at?
|
||||||
|
|
||||||
|
redirect_to submit_form_path(params[:submit_form_slug]) if require_email_2fa?(@submitter)
|
||||||
|
end
|
||||||
|
|
||||||
def success; end
|
def success; end
|
||||||
|
|
||||||
@@ -107,4 +117,12 @@ class SubmitFormController < ApplicationController
|
|||||||
ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments)
|
ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments)
|
||||||
.preload(:blob).index_by(&:uuid)
|
.preload(:blob).index_by(&:uuid)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def require_email_2fa?(submitter)
|
||||||
|
return false if submitter.submission.template&.preferences&.dig('require_email_2fa') != true &&
|
||||||
|
submitter.preferences['require_email_2fa'] != true
|
||||||
|
return false if cookies.encrypted[:email_2fa_slug] == submitter.slug
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -11,13 +11,13 @@ class SubmitFormDownloadController < ApplicationController
|
|||||||
|
|
||||||
return redirect_to submitter_download_index_path(@submitter.slug) if @submitter.completed_at?
|
return redirect_to submitter_download_index_path(@submitter.slug) if @submitter.completed_at?
|
||||||
|
|
||||||
return head :unprocessable_entity if @submitter.declined_at? ||
|
return head :unprocessable_content if @submitter.declined_at? ||
|
||||||
@submitter.submission.archived_at? ||
|
@submitter.submission.archived_at? ||
|
||||||
@submitter.submission.expired? ||
|
@submitter.submission.expired? ||
|
||||||
@submitter.submission.template&.archived_at? ||
|
@submitter.submission.template&.archived_at? ||
|
||||||
AccountConfig.exists?(account_id: @submitter.account_id,
|
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)
|
||||||
|
|
||||||
last_completed_submitter = @submitter.submission.submitters
|
last_completed_submitter = @submitter.submission.submitters
|
||||||
.where.not(id: @submitter.id)
|
.where.not(id: @submitter.id)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -7,7 +7,7 @@ class SubmitFormInviteController < ApplicationController
|
|||||||
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_entity 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')
|
||||||
@@ -34,7 +34,7 @@ class SubmitFormInviteController < ApplicationController
|
|||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
else
|
else
|
||||||
head :unprocessable_entity
|
head :unprocessable_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,15 @@
|
|||||||
class TemplateDocumentsController < ApplicationController
|
class TemplateDocumentsController < ApplicationController
|
||||||
load_and_authorize_resource :template
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
FILES_TTL = 5.minutes
|
||||||
|
|
||||||
|
def index
|
||||||
|
render json: @template.schema_documents.map { |d| ActiveStorage::Blob.proxy_url(d.blob, expires_at: FILES_TTL.from_now.to_i) }
|
||||||
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
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_entity
|
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
|
||||||
@@ -28,6 +34,6 @@ class TemplateDocumentsController < ApplicationController
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
rescue Templates::CreateAttachments::PdfEncrypted
|
rescue Templates::CreateAttachments::PdfEncrypted
|
||||||
render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_entity
|
render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -3,14 +3,31 @@
|
|||||||
class TemplateFoldersAutocompleteController < ApplicationController
|
class TemplateFoldersAutocompleteController < ApplicationController
|
||||||
load_and_authorize_resource :template_folder, parent: false
|
load_and_authorize_resource :template_folder, parent: false
|
||||||
|
|
||||||
LIMIT = 100
|
LIMIT = 30
|
||||||
|
|
||||||
def index
|
def index
|
||||||
templates_query = Template.accessible_by(current_ability).where(archived_at: nil)
|
parent_name, name =
|
||||||
|
if params[:parent_name].present?
|
||||||
|
[params[:parent_name], params[:q]]
|
||||||
|
else
|
||||||
|
params[:q].to_s.split(' /', 2).map(&:squish)
|
||||||
|
end
|
||||||
|
|
||||||
template_folders = @template_folders.where(id: templates_query.select(:folder_id))
|
if name
|
||||||
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
|
parent_folder = @template_folders.find_by(name: parent_name, parent_folder_id: nil)
|
||||||
|
else
|
||||||
|
name = parent_name
|
||||||
|
end
|
||||||
|
|
||||||
render json: template_folders.as_json(only: %i[name archived_at])
|
template_folders = TemplateFolders.filter_active_folders(@template_folders.where(parent_folder:),
|
||||||
|
Template.accessible_by(current_ability))
|
||||||
|
|
||||||
|
name = name.to_s.downcase
|
||||||
|
|
||||||
|
template_folders = TemplateFolders.search(template_folders, name).order(id: :desc).limit(LIMIT)
|
||||||
|
|
||||||
|
render json: template_folders.preload(:parent_folder)
|
||||||
|
.sort_by { |e| e.name.downcase.index(name) || Float::MAX }
|
||||||
|
.as_json(only: %i[name archived_at], methods: %i[full_name])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -5,13 +5,39 @@ class TemplateFoldersController < ApplicationController
|
|||||||
|
|
||||||
helper_method :selected_order
|
helper_method :selected_order
|
||||||
|
|
||||||
def show
|
TEMPLATES_PER_PAGE = 12
|
||||||
@templates = @template_folder.templates.active.accessible_by(current_ability)
|
FOLDERS_PER_PAGE = 18
|
||||||
.preload(:author, :template_accesses)
|
|
||||||
@templates = Templates.search(current_user, @templates, params[:q])
|
|
||||||
@templates = Templates::Order.call(@templates, current_user, selected_order)
|
|
||||||
|
|
||||||
@pagy, @templates = pagy_auto(@templates, limit: 12)
|
def show
|
||||||
|
@templates = Template.active.accessible_by(current_ability)
|
||||||
|
.where(folder: [@template_folder, *(params[:q].present? ? @template_folder.subfolders : [])])
|
||||||
|
.preload(:author, :template_accesses)
|
||||||
|
|
||||||
|
@template_folders =
|
||||||
|
@template_folder.subfolders.where(id: Template.accessible_by(current_ability).active.select(:folder_id))
|
||||||
|
|
||||||
|
@template_folders = TemplateFolders.search(@template_folders, params[:q])
|
||||||
|
@template_folders = TemplateFolders.sort(@template_folders, current_user, selected_order)
|
||||||
|
|
||||||
|
if @templates.exists?
|
||||||
|
@templates = Templates.search(current_user, @templates, params[:q])
|
||||||
|
@templates = Templates::Order.call(@templates, current_user, selected_order)
|
||||||
|
|
||||||
|
limit =
|
||||||
|
if @template_folders.size < 4
|
||||||
|
TEMPLATES_PER_PAGE
|
||||||
|
else
|
||||||
|
(@template_folders.size < 7 ? 9 : 6)
|
||||||
|
end
|
||||||
|
|
||||||
|
@pagy, @templates = pagy_auto(@templates, limit:)
|
||||||
|
|
||||||
|
load_related_submissions if params[:q].present? && @templates.blank?
|
||||||
|
else
|
||||||
|
@pagy, @template_folders = pagy(@template_folders, limit: FOLDERS_PER_PAGE)
|
||||||
|
|
||||||
|
@templates = @templates.none
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def edit; end
|
def edit; end
|
||||||
@@ -40,4 +66,21 @@ class TemplateFoldersController < ApplicationController
|
|||||||
def template_folder_params
|
def template_folder_params
|
||||||
params.require(:template_folder).permit(:name)
|
params.require(:template_folder).permit(:name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def load_related_submissions
|
||||||
|
@related_submissions =
|
||||||
|
Submission.accessible_by(current_ability)
|
||||||
|
.where(archived_at: nil)
|
||||||
|
.where(template_id: current_account.templates.active
|
||||||
|
.where(folder: [@template_folder, *@template_folder.subfolders])
|
||||||
|
.select(:id))
|
||||||
|
.preload(:template_accesses, :created_by_user,
|
||||||
|
template: :author,
|
||||||
|
submitters: :start_form_submission_events)
|
||||||
|
|
||||||
|
@related_submissions = Submissions.search(current_user, @related_submissions, params[:q])
|
||||||
|
.order(id: :desc)
|
||||||
|
|
||||||
|
@related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -4,9 +4,27 @@ class TemplatesArchivedController < ApplicationController
|
|||||||
load_and_authorize_resource :template, parent: false
|
load_and_authorize_resource :template, parent: false
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@templates = @templates.where.not(archived_at: nil).preload(:author, :folder, :template_accesses).order(id: :desc)
|
@templates = @templates.where.not(archived_at: nil)
|
||||||
|
.preload(:author, :template_accesses, folder: :parent_folder)
|
||||||
|
.order(id: :desc)
|
||||||
|
|
||||||
@templates = Templates.search(current_user, @templates, params[:q])
|
@templates = Templates.search(current_user, @templates, params[:q])
|
||||||
|
|
||||||
@pagy, @templates = pagy_auto(@templates, limit: 12)
|
@pagy, @templates = pagy_auto(@templates, limit: 12)
|
||||||
|
|
||||||
|
return unless params[:q].present? && @templates.blank?
|
||||||
|
|
||||||
|
@related_submissions =
|
||||||
|
Submission.accessible_by(current_ability)
|
||||||
|
.joins(:template)
|
||||||
|
.where.not(templates: { archived_at: nil })
|
||||||
|
.preload(:template_accesses, :created_by_user,
|
||||||
|
template: :author,
|
||||||
|
submitters: :start_form_submission_events)
|
||||||
|
|
||||||
|
@related_submissions = Submissions.search(current_user, @related_submissions, params[:q])
|
||||||
|
.order(id: :desc)
|
||||||
|
|
||||||
|
@related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -4,7 +4,7 @@ class TemplatesCloneAndReplaceController < ApplicationController
|
|||||||
load_and_authorize_resource :template
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
def create
|
def create
|
||||||
return head :unprocessable_entity if params[:files].blank?
|
return head :unprocessable_content if params[:files].blank?
|
||||||
|
|
||||||
ActiveRecord::Associations::Preloader.new(
|
ActiveRecord::Associations::Preloader.new(
|
||||||
records: [@template],
|
records: [@template],
|
||||||
@@ -17,6 +17,8 @@ class TemplatesCloneAndReplaceController < ApplicationController
|
|||||||
|
|
||||||
documents = Templates::ReplaceAttachments.call(cloned_template, params, extract_fields: true)
|
documents = Templates::ReplaceAttachments.call(cloned_template, params, extract_fields: true)
|
||||||
|
|
||||||
|
Templates.maybe_assign_access(cloned_template)
|
||||||
|
|
||||||
cloned_template.save!
|
cloned_template.save!
|
||||||
|
|
||||||
Templates::CloneAttachments.call(template: cloned_template, original_template: @template,
|
Templates::CloneAttachments.call(template: cloned_template, original_template: @template,
|
||||||
@@ -31,7 +33,7 @@ class TemplatesCloneAndReplaceController < ApplicationController
|
|||||||
rescue Templates::CreateAttachments::PdfEncrypted
|
rescue Templates::CreateAttachments::PdfEncrypted
|
||||||
respond_to do |f|
|
respond_to do |f|
|
||||||
f.html { render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password) }
|
f.html { render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password) }
|
||||||
f.json { render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_entity }
|
f.json { render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_content }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -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.authorize!(: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,9 +24,7 @@ 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(
|
ActiveRecord::Associations::Preloader.new(
|
||||||
@@ -48,37 +44,20 @@ class TemplatesController < ApplicationController
|
|||||||
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,
|
Templates.maybe_assign_access(@template)
|
||||||
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
|
|
||||||
|
|
||||||
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_entity
|
render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -116,35 +95,18 @@ class TemplatesController < ApplicationController
|
|||||||
def template_params
|
def template_params
|
||||||
params.require(:template).permit(
|
params.require(:template).permit(
|
||||||
:name,
|
:name,
|
||||||
{ schema: [[:attachment_uuid, :name, { conditions: [%i[field_uuid value action operation]] }]],
|
{ schema: [[:attachment_uuid, :google_drive_file_id, :name,
|
||||||
submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid optional_invite_by_uuid email]],
|
{ 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]],
|
||||||
fields: [[:uuid, :submitter_uuid, :name, :type,
|
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||||
:required, :readonly, :default_value,
|
:required, :readonly, :default_value,
|
||||||
:title, :description,
|
:title, :description, :prefillable,
|
||||||
{ preferences: {},
|
{ preferences: {},
|
||||||
|
default_value: [],
|
||||||
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],
|
validation: %i[message pattern min max step],
|
||||||
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
areas: [%i[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
|
||||||
@@ -11,10 +11,11 @@ class TemplatesDashboardController < ApplicationController
|
|||||||
helper_method :selected_order
|
helper_method :selected_order
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@template_folders = @template_folders.where(id: @templates.active.select(:folder_id))
|
@template_folders =
|
||||||
|
TemplateFolders.filter_active_folders(@template_folders.where(parent_folder_id: nil), @templates)
|
||||||
|
|
||||||
@template_folders = TemplateFolders.search(@template_folders, params[:q])
|
@template_folders = TemplateFolders.search(@template_folders, params[:q])
|
||||||
@template_folders = sort_template_folders(@template_folders, current_user, selected_order)
|
@template_folders = TemplateFolders.sort(@template_folders, current_user, selected_order)
|
||||||
|
|
||||||
@pagy, @template_folders = pagy(
|
@pagy, @template_folders = pagy(
|
||||||
@template_folders,
|
@template_folders,
|
||||||
@@ -68,40 +69,6 @@ class TemplatesDashboardController < ApplicationController
|
|||||||
Templates.search(current_user, rel, params[:q])
|
Templates.search(current_user, rel, params[:q])
|
||||||
end
|
end
|
||||||
|
|
||||||
def sort_template_folders(template_folders, current_user, order)
|
|
||||||
case order
|
|
||||||
when 'used_at'
|
|
||||||
subquery =
|
|
||||||
Template.left_joins(:submissions)
|
|
||||||
.group(:folder_id)
|
|
||||||
.where(account_id: current_user.account_id)
|
|
||||||
.select(
|
|
||||||
:folder_id,
|
|
||||||
Template.arel_table[:updated_at].maximum.as('updated_at_max'),
|
|
||||||
Submission.arel_table[:created_at].maximum.as('submission_created_at_max')
|
|
||||||
)
|
|
||||||
|
|
||||||
template_folders = template_folders.joins(
|
|
||||||
Template.arel_table
|
|
||||||
.join(subquery.arel.as('templates'), Arel::Nodes::OuterJoin)
|
|
||||||
.on(TemplateFolder.arel_table[:id].eq(Template.arel_table[:folder_id]))
|
|
||||||
.join_sources
|
|
||||||
)
|
|
||||||
|
|
||||||
template_folders.order(
|
|
||||||
Arel::Nodes::Case.new
|
|
||||||
.when(Template.arel_table[:submission_created_at_max].gt(Template.arel_table[:updated_at_max]))
|
|
||||||
.then(Template.arel_table[:submission_created_at_max])
|
|
||||||
.else(Template.arel_table[:updated_at_max])
|
|
||||||
.desc
|
|
||||||
)
|
|
||||||
when 'name'
|
|
||||||
template_folders.order(name: :asc)
|
|
||||||
else
|
|
||||||
template_folders.order(id: :desc)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def selected_order
|
def selected_order
|
||||||
@selected_order ||=
|
@selected_order ||=
|
||||||
if cookies.permanent[:dashboard_templates_order].blank? ||
|
if cookies.permanent[:dashboard_templates_order].blank? ||
|
||||||
|
|||||||
@@ -3,19 +3,29 @@
|
|||||||
class TemplatesDebugController < ApplicationController
|
class TemplatesDebugController < ApplicationController
|
||||||
load_and_authorize_resource :template
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
DEBUG_FILE = ''
|
||||||
|
|
||||||
def show
|
def show
|
||||||
attachment = @template.documents.first
|
schema_uuids = @template.schema.index_by { |e| e['attachment_uuid'] }
|
||||||
|
attachment = @template.documents.find { |a| schema_uuids[a.uuid] }
|
||||||
|
|
||||||
data = attachment.download
|
data = attachment.download
|
||||||
pdf = HexaPDF::Document.new(io: StringIO.new(data))
|
|
||||||
|
|
||||||
fields = Templates::FindAcroFields.call(pdf, attachment, data)
|
unless attachment.image?
|
||||||
|
pdf = HexaPDF::Document.new(io: StringIO.new(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'] ||= {}
|
||||||
attachment.metadata['pdf']['fields'] = fields
|
attachment.metadata['pdf']['fields'] = fields
|
||||||
|
|
||||||
@template.update!(fields: Templates::ProcessDocument.normalize_attachment_fields(@template, [attachment]))
|
@template.update!(fields: Templates::ProcessDocument.normalize_attachment_fields(@template, [attachment]))
|
||||||
|
|
||||||
|
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 }]
|
||||||
@@ -31,4 +41,27 @@ class TemplatesDebugController < ApplicationController
|
|||||||
|
|
||||||
render 'templates/edit', layout: 'plain'
|
render 'templates/edit', layout: 'plain'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def debug_file
|
||||||
|
tempfile = Tempfile.new
|
||||||
|
tempfile.binmode
|
||||||
|
tempfile.write(File.read(DEBUG_FILE))
|
||||||
|
tempfile.rewind
|
||||||
|
|
||||||
|
filename = File.basename(DEBUG_FILE)
|
||||||
|
|
||||||
|
file = ActionDispatch::Http::UploadedFile.new(
|
||||||
|
tempfile:,
|
||||||
|
filename:,
|
||||||
|
type: Marcel::MimeType.for(tempfile)
|
||||||
|
)
|
||||||
|
|
||||||
|
params = { files: [file] }
|
||||||
|
|
||||||
|
documents = Templates::CreateAttachments.call(@template, params)
|
||||||
|
|
||||||
|
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
|
||||||
|
|
||||||
|
@template.update!(schema:)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesDetectFieldsController < ApplicationController
|
||||||
|
include ActionController::Live
|
||||||
|
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def create
|
||||||
|
response.headers['Content-Type'] = 'text/event-stream'
|
||||||
|
|
||||||
|
sse = SSE.new(response.stream)
|
||||||
|
|
||||||
|
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|
|
||||||
|
io = StringIO.new(document.download)
|
||||||
|
|
||||||
|
Templates::DetectFields.call(io, attachment: document, page_number:) do |(attachment_uuid, page, fields)|
|
||||||
|
sse.write({ attachment_uuid:, page:, fields: })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
sse.write({ completed: true })
|
||||||
|
ensure
|
||||||
|
response.stream.close
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -6,7 +6,9 @@ class TemplatesFoldersController < ApplicationController
|
|||||||
def edit; end
|
def edit; end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:name])
|
name = [params[:parent_name], params[:name]].compact_blank.join(' / ')
|
||||||
|
|
||||||
|
@template.folder = TemplateFolders.find_or_create_by_name(current_user, name)
|
||||||
|
|
||||||
if @template.save
|
if @template.save
|
||||||
redirect_back(fallback_location: template_path(@template), notice: I18n.t('document_template_has_been_moved'))
|
redirect_back(fallback_location: template_path(@template), notice: I18n.t('document_template_has_been_moved'))
|
||||||
|
|||||||
@@ -3,6 +3,15 @@
|
|||||||
class TemplatesPreferencesController < ApplicationController
|
class TemplatesPreferencesController < ApplicationController
|
||||||
load_and_authorize_resource :template
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
RESETTABLE_PREFERENCE_KEYS = {
|
||||||
|
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY => %w[request_email_subject request_email_body submitters],
|
||||||
|
AccountConfig::SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => %w[invitation_reminder_email_subject
|
||||||
|
invitation_reminder_email_body],
|
||||||
|
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => %w[documents_copy_email_subject documents_copy_email_body],
|
||||||
|
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY => %w[completed_notification_email_subject
|
||||||
|
completed_notification_email_body]
|
||||||
|
}.freeze
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@@ -15,19 +24,38 @@ class TemplatesPreferencesController < ApplicationController
|
|||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
authorize!(:update, @template)
|
||||||
|
|
||||||
|
config_key = params[:config_key]
|
||||||
|
preferences_to_delete = RESETTABLE_PREFERENCE_KEYS[config_key]
|
||||||
|
|
||||||
|
return head :ok if preferences_to_delete.blank?
|
||||||
|
|
||||||
|
preferences_to_delete.each do |key|
|
||||||
|
@template.preferences.delete(key)
|
||||||
|
end
|
||||||
|
|
||||||
|
@template.save!
|
||||||
|
|
||||||
|
render turbo_stream: turbo_stream.replace("#{config_key}_form",
|
||||||
|
partial: "templates_preferences/#{config_key}_form"),
|
||||||
|
status: :ok
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def template_params
|
def template_params
|
||||||
params.require(:template).permit(
|
params.require(:template).permit(
|
||||||
preferences: %i[bcc_completed request_email_subject request_email_body
|
preferences: %i[bcc_completed request_email_subject request_email_body
|
||||||
|
invitation_reminder_email_subject invitation_reminder_email_body
|
||||||
documents_copy_email_subject documents_copy_email_body
|
documents_copy_email_subject documents_copy_email_body
|
||||||
documents_copy_email_enabled documents_copy_email_attach_audit
|
documents_copy_email_enabled documents_copy_email_attach_audit
|
||||||
documents_copy_email_attach_documents documents_copy_email_reply_to
|
documents_copy_email_attach_documents documents_copy_email_reply_to
|
||||||
completed_notification_email_attach_documents
|
completed_notification_email_attach_documents
|
||||||
completed_redirect_url 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_duration shared_link_2fa default_expire_at request_email_enabled
|
||||||
default_expire_at
|
|
||||||
completed_notification_email_subject completed_notification_email_body
|
completed_notification_email_subject completed_notification_email_body
|
||||||
completed_notification_email_enabled completed_notification_email_attach_audit] +
|
completed_notification_email_enabled completed_notification_email_attach_audit] +
|
||||||
[completed_message: %i[title body],
|
[completed_message: %i[title body],
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesPrefillableFieldsController < ApplicationController
|
||||||
|
PREFILLABLE_FIELD_TYPES = %w[text number cells date checkbox select radio phone].freeze
|
||||||
|
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize!(:update, @template)
|
||||||
|
|
||||||
|
field = @template.fields.find { |f| f['uuid'] == params[:field_uuid] }
|
||||||
|
|
||||||
|
if params[:prefillable] == 'false'
|
||||||
|
field.delete('prefillable')
|
||||||
|
field.delete('readonly')
|
||||||
|
elsif params[:prefillable] == 'true'
|
||||||
|
field['prefillable'] = true
|
||||||
|
field['readonly'] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
@template.save!
|
||||||
|
|
||||||
|
render turbo_stream: turbo_stream.replace(:prefillable_fields_list, partial: 'list',
|
||||||
|
locals: { template: @template })
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -9,6 +9,10 @@ class TemplatesRecipientsController < ApplicationController
|
|||||||
@template.submitters =
|
@template.submitters =
|
||||||
submitters_params.map { |s| s.reject { |_, v| v.is_a?(String) && v.blank? } }
|
submitters_params.map { |s| s.reject { |_, v| v.is_a?(String) && v.blank? } }
|
||||||
|
|
||||||
|
if @template.submitters.each_with_index.all? { |s, index| s['order'] == index }
|
||||||
|
@template.submitters.each { |s| s.delete('order') }
|
||||||
|
end
|
||||||
|
|
||||||
@template.save!
|
@template.save!
|
||||||
|
|
||||||
render json: { submitters: @template.submitters }
|
render json: { submitters: @template.submitters }
|
||||||
@@ -18,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]] }
|
invite_by_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?
|
||||||
@@ -29,6 +33,7 @@ class TemplatesRecipientsController < ApplicationController
|
|||||||
s.delete(:is_requester)
|
s.delete(:is_requester)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
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?
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ class TemplatesUploadsController < ApplicationController
|
|||||||
template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
||||||
template.name = File.basename((url_params || params)[:files].first.original_filename, '.*')
|
template.name = File.basename((url_params || params)[:files].first.original_filename, '.*')
|
||||||
|
|
||||||
|
Templates.maybe_assign_access(template)
|
||||||
|
|
||||||
template.save!
|
template.save!
|
||||||
|
|
||||||
template
|
template
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -24,12 +24,22 @@ class UsersController < ApplicationController
|
|||||||
def edit; end
|
def edit; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
if User.accessible_by(current_ability).exists?(email: @user.email)
|
existing_user = User.accessible_by(current_ability).find_by(email: @user.email)
|
||||||
@user.errors.add(:email, I18n.t('already_exists'))
|
|
||||||
|
|
||||||
return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity
|
if existing_user
|
||||||
|
if existing_user.archived_at? &&
|
||||||
|
current_ability.can?(:manage, existing_user) && current_ability.can?(:manage, @user.account)
|
||||||
|
existing_user.assign_attributes(@user.slice(:first_name, :last_name, :role, :account_id))
|
||||||
|
existing_user.archived_at = nil
|
||||||
|
@user = existing_user
|
||||||
|
else
|
||||||
|
@user.errors.add(:email, I18n.t('already_exists'))
|
||||||
|
|
||||||
|
return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_content
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@user.password = SecureRandom.hex if @user.password.blank?
|
||||||
@user.role = User::ADMIN_ROLE unless role_valid?(@user.role)
|
@user.role = User::ADMIN_ROLE unless role_valid?(@user.role)
|
||||||
|
|
||||||
if @user.save
|
if @user.save
|
||||||
@@ -37,14 +47,15 @@ class UsersController < ApplicationController
|
|||||||
|
|
||||||
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_invited')
|
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_invited')
|
||||||
else
|
else
|
||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity
|
render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
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))
|
||||||
@@ -54,10 +65,17 @@ class UsersController < ApplicationController
|
|||||||
@user.account = account
|
@user.account = account
|
||||||
end
|
end
|
||||||
|
|
||||||
if @user.update(attrs.except(current_user == @user ? :role : nil))
|
if @user.update(attrs.except(*(current_user == @user ? %i[password otp_required_for_login role] : %i[password])))
|
||||||
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_updated')
|
if @user.try(:pending_reconfirmation?) && @user.previous_changes.key?(:unconfirmed_email)
|
||||||
|
SendConfirmationInstructionsJob.perform_async('user_id' => @user.id)
|
||||||
|
|
||||||
|
redirect_back fallback_location: settings_users_path,
|
||||||
|
notice: I18n.t('a_confirmation_email_has_been_sent_to_the_new_email_address')
|
||||||
|
else
|
||||||
|
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_updated')
|
||||||
|
end
|
||||||
else
|
else
|
||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity
|
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -83,7 +101,7 @@ class UsersController < ApplicationController
|
|||||||
|
|
||||||
def user_params
|
def user_params
|
||||||
if params.key?(:user)
|
if params.key?(:user)
|
||||||
permitted_params = %i[email first_name last_name password archived_at]
|
permitted_params = %i[email first_name last_name password archived_at otp_required_for_login]
|
||||||
|
|
||||||
permitted_params << :role if role_valid?(params.dig(:user, :role))
|
permitted_params << :role if role_valid?(params.dig(:user, :role))
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class UsersSendResetPasswordController < ApplicationController
|
||||||
|
load_and_authorize_resource :user
|
||||||
|
|
||||||
|
LIMIT_DURATION = 10.minutes
|
||||||
|
|
||||||
|
def update
|
||||||
|
authorize!(:manage, @user)
|
||||||
|
|
||||||
|
if @user.reset_password_sent_at && @user.reset_password_sent_at > LIMIT_DURATION.ago
|
||||||
|
redirect_back fallback_location: settings_users_path, notice: I18n.t('email_has_been_sent_already')
|
||||||
|
else
|
||||||
|
@user.send_reset_password_instructions
|
||||||
|
|
||||||
|
redirect_back fallback_location: settings_users_path,
|
||||||
|
notice: I18n.t('an_email_with_password_reset_instructions_has_been_sent')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -15,7 +15,7 @@ class WebhookSettingsController < ApplicationController
|
|||||||
|
|
||||||
@webhook_events = @webhook_events.where(status: params[:status]) if %w[success error].include?(params[:status])
|
@webhook_events = @webhook_events.where(status: params[:status]) if %w[success error].include?(params[:status])
|
||||||
|
|
||||||
@pagy, @webhook_events = pagy_countless(@webhook_events.order(id: :desc))
|
@pagy, @webhook_events = pagy(:countless, @webhook_events.order(id: :desc))
|
||||||
|
|
||||||
render :show
|
render :show
|
||||||
end
|
end
|
||||||
@@ -26,7 +26,7 @@ class WebhookSettingsController < ApplicationController
|
|||||||
|
|
||||||
@webhook_events = @webhook_events.where(status: params[:status]) if %w[success error].include?(params[:status])
|
@webhook_events = @webhook_events.where(status: params[:status]) if %w[success error].include?(params[:status])
|
||||||
|
|
||||||
@pagy, @webhook_events = pagy_countless(@webhook_events.order(id: :desc))
|
@pagy, @webhook_events = pagy(:countless, @webhook_events.order(id: :desc))
|
||||||
end
|
end
|
||||||
|
|
||||||
def new; end
|
def new; end
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import SignatureForm from './elements/signature_form'
|
|||||||
import SubmitForm from './elements/submit_form'
|
import SubmitForm from './elements/submit_form'
|
||||||
import PromptPassword from './elements/prompt_password'
|
import PromptPassword from './elements/prompt_password'
|
||||||
import EmailsTextarea from './elements/emails_textarea'
|
import EmailsTextarea from './elements/emails_textarea'
|
||||||
|
import ToggleSubmit from './elements/toggle_submit'
|
||||||
import ToggleOnSubmit from './elements/toggle_on_submit'
|
import ToggleOnSubmit from './elements/toggle_on_submit'
|
||||||
import CheckOnClick from './elements/check_on_click'
|
import CheckOnClick from './elements/check_on_click'
|
||||||
import PasswordInput from './elements/password_input'
|
import PasswordInput from './elements/password_input'
|
||||||
@@ -34,9 +35,24 @@ import MaskedInput from './elements/masked_input'
|
|||||||
import SetDateButton from './elements/set_date_button'
|
import SetDateButton from './elements/set_date_button'
|
||||||
import IndeterminateCheckbox from './elements/indeterminate_checkbox'
|
import IndeterminateCheckbox from './elements/indeterminate_checkbox'
|
||||||
import AppTour from './elements/app_tour'
|
import AppTour from './elements/app_tour'
|
||||||
|
import AppTourStart from './elements/app_tour_start'
|
||||||
import DashboardDropzone from './elements/dashboard_dropzone'
|
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 MountOnClick from './elements/mount_on_click'
|
||||||
|
import RemoveOnEvent from './elements/remove_on_event'
|
||||||
|
import ScrollTo from './elements/scroll_to'
|
||||||
|
import SetValue from './elements/set_value'
|
||||||
|
import ReviewForm from './elements/review_form'
|
||||||
|
import ShowOnValue from './elements/show_on_value'
|
||||||
|
import CustomValidation from './elements/custom_validation'
|
||||||
|
import ToggleClasses from './elements/toggle_classes'
|
||||||
|
import AutosizeField from './elements/autosize_field'
|
||||||
|
import GoogleDriveFilePicker from './elements/google_drive_file_picker'
|
||||||
|
import OpenModal from './elements/open_modal'
|
||||||
|
import BarChart from './elements/bar_chart'
|
||||||
|
import FieldCondition from './elements/field_condition'
|
||||||
|
|
||||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||||
|
|
||||||
@@ -53,6 +69,9 @@ document.addEventListener('keyup', (e) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
|
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
|
||||||
|
document.addEventListener('turbo:before-fetch-request', (event) => {
|
||||||
|
event.detail.fetchOptions.headers['X-Turbo'] = 'true'
|
||||||
|
})
|
||||||
document.addEventListener('turbo:submit-end', async (event) => {
|
document.addEventListener('turbo:submit-end', async (event) => {
|
||||||
const resp = event.detail?.formSubmission?.result?.fetchResponse?.response
|
const resp = event.detail?.formSubmission?.result?.fetchResponse?.response
|
||||||
|
|
||||||
@@ -95,6 +114,7 @@ safeRegisterElement('submit-form', SubmitForm)
|
|||||||
safeRegisterElement('prompt-password', PromptPassword)
|
safeRegisterElement('prompt-password', PromptPassword)
|
||||||
safeRegisterElement('emails-textarea', EmailsTextarea)
|
safeRegisterElement('emails-textarea', EmailsTextarea)
|
||||||
safeRegisterElement('toggle-cookies', ToggleCookies)
|
safeRegisterElement('toggle-cookies', ToggleCookies)
|
||||||
|
safeRegisterElement('toggle-submit', ToggleSubmit)
|
||||||
safeRegisterElement('toggle-on-submit', ToggleOnSubmit)
|
safeRegisterElement('toggle-on-submit', ToggleOnSubmit)
|
||||||
safeRegisterElement('password-input', PasswordInput)
|
safeRegisterElement('password-input', PasswordInput)
|
||||||
safeRegisterElement('search-input', SearchInput)
|
safeRegisterElement('search-input', SearchInput)
|
||||||
@@ -105,10 +125,25 @@ safeRegisterElement('masked-input', MaskedInput)
|
|||||||
safeRegisterElement('set-date-button', SetDateButton)
|
safeRegisterElement('set-date-button', SetDateButton)
|
||||||
safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox)
|
safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox)
|
||||||
safeRegisterElement('app-tour', AppTour)
|
safeRegisterElement('app-tour', AppTour)
|
||||||
|
safeRegisterElement('app-tour-start', AppTourStart)
|
||||||
safeRegisterElement('dashboard-dropzone', DashboardDropzone)
|
safeRegisterElement('dashboard-dropzone', DashboardDropzone)
|
||||||
safeRegisterElement('check-on-click', CheckOnClick)
|
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('mount-on-click', MountOnClick)
|
||||||
|
safeRegisterElement('remove-on-event', RemoveOnEvent)
|
||||||
|
safeRegisterElement('scroll-to', ScrollTo)
|
||||||
|
safeRegisterElement('set-value', SetValue)
|
||||||
|
safeRegisterElement('review-form', ReviewForm)
|
||||||
|
safeRegisterElement('show-on-value', ShowOnValue)
|
||||||
|
safeRegisterElement('custom-validation', CustomValidation)
|
||||||
|
safeRegisterElement('toggle-classes', ToggleClasses)
|
||||||
|
safeRegisterElement('autosize-field', AutosizeField)
|
||||||
|
safeRegisterElement('google-drive-file-picker', GoogleDriveFilePicker)
|
||||||
|
safeRegisterElement('open-modal', OpenModal)
|
||||||
|
safeRegisterElement('bar-chart', BarChart)
|
||||||
|
safeRegisterElement('field-condition', FieldCondition)
|
||||||
|
|
||||||
safeRegisterElement('template-builder', class extends HTMLElement {
|
safeRegisterElement('template-builder', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
@@ -120,20 +155,26 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
|||||||
|
|
||||||
this.app = createApp(TemplateBuilder, {
|
this.app = createApp(TemplateBuilder, {
|
||||||
template: reactive(JSON.parse(this.dataset.template)),
|
template: reactive(JSON.parse(this.dataset.template)),
|
||||||
|
customFields: reactive(JSON.parse(this.dataset.customFields || '[]')),
|
||||||
backgroundColor: '#faf7f5',
|
backgroundColor: '#faf7f5',
|
||||||
locale: this.dataset.locale,
|
locale: this.dataset.locale,
|
||||||
withPhone: this.dataset.withPhone === 'true',
|
withPhone: this.dataset.withPhone === 'true',
|
||||||
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',
|
||||||
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',
|
||||||
|
withGoogleDrive: this.dataset.withGoogleDrive === 'true',
|
||||||
withReplaceAndCloneUpload: true,
|
withReplaceAndCloneUpload: true,
|
||||||
|
withDownload: true,
|
||||||
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
||||||
acceptFileTypes: this.dataset.acceptFileTypes,
|
acceptFileTypes: this.dataset.acceptFileTypes,
|
||||||
showTourStartForm: this.dataset.showTourStartForm === 'true'
|
showTourStartForm: this.dataset.showTourStartForm === 'true'
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ button .disabled {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
button[disabled] .disabled {
|
button[disabled] .disabled, button.btn-disabled .disabled {
|
||||||
display: initial;
|
display: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ button .enabled {
|
|||||||
display: initial;
|
display: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
button[disabled] .enabled {
|
button[disabled] .enabled, button.btn-disabled .enabled {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+51
-6
@@ -4,15 +4,26 @@ import { isValidSignatureCanvas } from './submission_form/validate_signature'
|
|||||||
|
|
||||||
window.customElements.define('draw-signature', class extends HTMLElement {
|
window.customElements.define('draw-signature', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
const scale = 3
|
this.setCanvasSize()
|
||||||
|
|
||||||
this.canvas.width = this.canvas.parentNode.clientWidth * scale
|
|
||||||
this.canvas.height = this.canvas.parentNode.clientHeight * scale
|
|
||||||
|
|
||||||
this.canvas.getContext('2d').scale(scale, scale)
|
|
||||||
|
|
||||||
this.pad = new SignaturePad(this.canvas)
|
this.pad = new SignaturePad(this.canvas)
|
||||||
|
|
||||||
|
this.resizeObserver = new ResizeObserver(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (!this.canvas) return
|
||||||
|
|
||||||
|
const { width, height } = this.canvas
|
||||||
|
|
||||||
|
this.setCanvasSize()
|
||||||
|
|
||||||
|
if (this.canvas.width !== width || this.canvas.height !== height) {
|
||||||
|
this.redrawCanvas(width, height)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.resizeObserver.observe(this.canvas.parentNode)
|
||||||
|
|
||||||
if (this.dataset.color) {
|
if (this.dataset.color) {
|
||||||
this.pad.penColor = this.dataset.color
|
this.pad.penColor = this.dataset.color
|
||||||
}
|
}
|
||||||
@@ -57,6 +68,40 @@ window.customElements.define('draw-signature', class extends HTMLElement {
|
|||||||
this.updateSubmitButtonVisibility()
|
this.updateSubmitButtonVisibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
if (this.resizeObserver) {
|
||||||
|
this.resizeObserver.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setCanvasSize () {
|
||||||
|
const scale = 3
|
||||||
|
|
||||||
|
const width = this.canvas.parentNode.clientWidth
|
||||||
|
const height = this.canvas.parentNode.clientHeight
|
||||||
|
|
||||||
|
if (this.canvas.width !== width * scale || this.canvas.height !== height * scale) {
|
||||||
|
this.canvas.width = width * scale
|
||||||
|
this.canvas.height = height * scale
|
||||||
|
|
||||||
|
this.canvas.getContext('2d').scale(scale, scale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
redrawCanvas (oldWidth, oldHeight) {
|
||||||
|
if (this.pad && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0) {
|
||||||
|
const sx = this.canvas.width / oldWidth
|
||||||
|
const sy = this.canvas.height / oldHeight
|
||||||
|
|
||||||
|
const scaledData = this.pad.toData().map((stroke) => ({
|
||||||
|
...stroke,
|
||||||
|
points: stroke.points.map((p) => ({ ...p, x: p.x * sx, y: p.y * sy }))
|
||||||
|
}))
|
||||||
|
|
||||||
|
this.pad.fromData(scaledData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateSubmitButtonVisibility () {
|
updateSubmitButtonVisibility () {
|
||||||
if (this.pad.isEmpty()) {
|
if (this.pad.isEmpty()) {
|
||||||
this.submitButton.style.display = 'none'
|
this.submitButton.style.display = 'none'
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.querySelector('form').addEventListener('submit', () => {
|
||||||
|
window.app_tour.start()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
const originalFontValue = this.field.style.fontSize
|
||||||
|
|
||||||
|
if (this.field.scrollHeight > this.field.clientHeight) {
|
||||||
|
this.field.style.fontSize = `calc(${originalFontValue} / 1.5)`
|
||||||
|
this.field.style.lineHeight = `calc(${this.field.style.fontSize} * 1.3)`
|
||||||
|
|
||||||
|
if (this.field.scrollHeight > this.field.clientHeight) {
|
||||||
|
this.field.style.fontSize = `calc(${originalFontValue} / 2.0)`
|
||||||
|
this.field.style.lineHeight = `calc(${this.field.style.fontSize} * 1.3)`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.field.classList.remove('hidden')
|
||||||
|
}
|
||||||
|
|
||||||
|
get field () {
|
||||||
|
return this.closest('field-value')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.chartLabels = JSON.parse(this.dataset.labels || '[]')
|
||||||
|
this.chartDatasets = JSON.parse(this.dataset.datasets || '[]')
|
||||||
|
|
||||||
|
this.initChart()
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
if (this.chartInstance) {
|
||||||
|
this.chartInstance.destroy()
|
||||||
|
this.chartInstance = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async initChart () {
|
||||||
|
const { default: Chart } = await import(/* webpackChunkName: "chartjs" */ 'chart.js/auto')
|
||||||
|
|
||||||
|
const canvas = this.querySelector('canvas')
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
|
||||||
|
this.chartInstance = new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: this.chartLabels,
|
||||||
|
datasets: this.chartDatasets
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: true,
|
||||||
|
animation: false,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
grace: '20%',
|
||||||
|
ticks: {
|
||||||
|
precision: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
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('')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ export default targetable(class extends HTMLElement {
|
|||||||
static [target.static] = [
|
static [target.static] = [
|
||||||
'form',
|
'form',
|
||||||
'fileDropzone',
|
'fileDropzone',
|
||||||
|
'folderDropzone',
|
||||||
'fileDropzoneLoading'
|
'fileDropzoneLoading'
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -25,12 +26,13 @@ export default targetable(class extends HTMLElement {
|
|||||||
window.addEventListener('dragleave', this.onWindowDragleave)
|
window.addEventListener('dragleave', this.onWindowDragleave)
|
||||||
|
|
||||||
this.fileDropzone?.addEventListener('drop', this.onDropFile)
|
this.fileDropzone?.addEventListener('drop', this.onDropFile)
|
||||||
|
this.folderDropzone?.addEventListener('drop', this.onDropNewFolder)
|
||||||
|
|
||||||
this.folderCards.forEach((el) => el.addEventListener('drop', (e) => this.onDropFolder(e, el)))
|
this.folderCards.forEach((el) => el.addEventListener('drop', (e) => this.onDropFolder(e, el)))
|
||||||
this.templateCards.forEach((el) => el.addEventListener('drop', this.onDropTemplate))
|
this.templateCards.forEach((el) => el.addEventListener('drop', this.onDropTemplate))
|
||||||
this.templateCards.forEach((el) => el.addEventListener('dragstart', this.onTemplateDragStart))
|
this.templateCards.forEach((el) => el.addEventListener('dragstart', this.onTemplateDragStart))
|
||||||
|
|
||||||
return [this.fileDropzone, ...this.folderCards, ...this.templateCards].forEach((el) => {
|
return [this.fileDropzone, this.folderDropzone, ...this.folderCards, ...this.templateCards].forEach((el) => {
|
||||||
el?.addEventListener('dragover', this.onDragover)
|
el?.addEventListener('dragover', this.onDragover)
|
||||||
el?.addEventListener('dragleave', this.onDragleave)
|
el?.addEventListener('dragleave', this.onDragleave)
|
||||||
})
|
})
|
||||||
@@ -46,6 +48,10 @@ export default targetable(class extends HTMLElement {
|
|||||||
onTemplateDragStart = (e) => {
|
onTemplateDragStart = (e) => {
|
||||||
const id = e.target.href.split('/').pop()
|
const id = e.target.href.split('/').pop()
|
||||||
|
|
||||||
|
this.folderCards.forEach((el) => el.classList.remove('bg-base-200', 'before:hidden'))
|
||||||
|
this.folderDropzone?.classList?.remove('hidden')
|
||||||
|
window.flash?.remove()
|
||||||
|
|
||||||
e.dataTransfer.effectAllowed = 'move'
|
e.dataTransfer.effectAllowed = 'move'
|
||||||
|
|
||||||
if (id) {
|
if (id) {
|
||||||
@@ -104,7 +110,7 @@ export default targetable(class extends HTMLElement {
|
|||||||
} else {
|
} else {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
|
|
||||||
formData.append('name', el.innerText.trim())
|
formData.append('name', el.dataset.fullName)
|
||||||
|
|
||||||
fetch(`/templates/${templateId}/folder`, {
|
fetch(`/templates/${templateId}/folder`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
@@ -176,6 +182,24 @@ export default targetable(class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onDropNewFolder (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const templateId = e.dataTransfer.getData('template_id')
|
||||||
|
|
||||||
|
const a = document.createElement('a')
|
||||||
|
|
||||||
|
a.href = `/templates/${templateId}/folder/edit?autocomplete=false`
|
||||||
|
a.dataset.turboFrame = 'modal'
|
||||||
|
a.classList.add('hidden')
|
||||||
|
|
||||||
|
document.body.append(a)
|
||||||
|
|
||||||
|
a.click()
|
||||||
|
|
||||||
|
a.remove()
|
||||||
|
}
|
||||||
|
|
||||||
onDragleave () {
|
onDragleave () {
|
||||||
this.style.backgroundColor = null
|
this.style.backgroundColor = null
|
||||||
|
|
||||||
@@ -199,6 +223,7 @@ export default targetable(class extends HTMLElement {
|
|||||||
|
|
||||||
this.isDrag = true
|
this.isDrag = true
|
||||||
|
|
||||||
|
window.flash?.remove()
|
||||||
this.fileDropzone?.classList?.remove('hidden')
|
this.fileDropzone?.classList?.remove('hidden')
|
||||||
|
|
||||||
this.hiddenOnDrag.forEach((el) => { el.style.display = 'none' })
|
this.hiddenOnDrag.forEach((el) => { el.style.display = 'none' })
|
||||||
@@ -212,6 +237,7 @@ export default targetable(class extends HTMLElement {
|
|||||||
this.isDrag = false
|
this.isDrag = false
|
||||||
|
|
||||||
this.fileDropzone?.classList?.add('hidden')
|
this.fileDropzone?.classList?.add('hidden')
|
||||||
|
this.folderDropzone?.classList?.add('hidden')
|
||||||
|
|
||||||
this.hiddenOnDrag.forEach((el) => { el.style.display = null })
|
this.hiddenOnDrag.forEach((el) => { el.style.display = null })
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import { target, targetable } from '@github/catalyst/lib/targetable'
|
||||||
|
|
||||||
|
let loaderPromise = null
|
||||||
|
|
||||||
|
function loadCodeMirror () {
|
||||||
|
if (!loaderPromise) {
|
||||||
|
loaderPromise = Promise.all([
|
||||||
|
import(/* webpackChunkName: "email-editor" */ '@codemirror/view'),
|
||||||
|
import(/* webpackChunkName: "email-editor" */ '@codemirror/commands'),
|
||||||
|
import(/* webpackChunkName: "email-editor" */ '@codemirror/language'),
|
||||||
|
import(/* webpackChunkName: "email-editor" */ '@codemirror/lang-html'),
|
||||||
|
import(/* webpackChunkName: "email-editor" */ '@specious/htmlflow')
|
||||||
|
]).then(([view, commands, language, html, htmlflow]) => {
|
||||||
|
return {
|
||||||
|
minimalSetup: [
|
||||||
|
commands.history(),
|
||||||
|
language.syntaxHighlighting(language.defaultHighlightStyle, { fallback: true }),
|
||||||
|
view.keymap.of([...commands.defaultKeymap, ...commands.historyKeymap])
|
||||||
|
],
|
||||||
|
EditorView: view.EditorView,
|
||||||
|
html: html.html,
|
||||||
|
htmlflow: htmlflow.default || htmlflow
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return loaderPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
export default targetable(class extends HTMLElement {
|
||||||
|
static [target.static] = [
|
||||||
|
'codeViewTab',
|
||||||
|
'previewViewTab',
|
||||||
|
'editorContainer',
|
||||||
|
'previewIframe'
|
||||||
|
]
|
||||||
|
|
||||||
|
connectedCallback () {
|
||||||
|
this.mount()
|
||||||
|
|
||||||
|
if (this.input.value) {
|
||||||
|
this.showPreviewView()
|
||||||
|
} else {
|
||||||
|
this.showCodeView()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.previewViewTab.addEventListener('click', this.showPreviewView)
|
||||||
|
this.codeViewTab.addEventListener('click', this.showCodeView)
|
||||||
|
}
|
||||||
|
|
||||||
|
showCodeView = () => {
|
||||||
|
this.editorView.dispatch({
|
||||||
|
changes: { from: 0, to: this.editorView.state.doc.length, insert: this.input.value }
|
||||||
|
})
|
||||||
|
|
||||||
|
this.previewViewTab.classList.remove('tab-active', 'tab-bordered')
|
||||||
|
this.previewViewTab.classList.add('pb-[3px]')
|
||||||
|
this.codeViewTab.classList.remove('pb-[3px]')
|
||||||
|
this.codeViewTab.classList.add('tab-active', 'tab-bordered')
|
||||||
|
this.editorContainer.classList.remove('hidden')
|
||||||
|
this.previewIframe.classList.add('hidden')
|
||||||
|
}
|
||||||
|
|
||||||
|
showPreviewView = () => {
|
||||||
|
this.previewIframe.srcdoc = this.input.value
|
||||||
|
|
||||||
|
this.codeViewTab.classList.remove('tab-active', 'tab-bordered')
|
||||||
|
this.codeViewTab.classList.add('pb-[3px]')
|
||||||
|
this.previewViewTab.classList.remove('pb-[3px]')
|
||||||
|
this.previewViewTab.classList.add('tab-active', 'tab-bordered')
|
||||||
|
this.editorContainer.classList.add('hidden')
|
||||||
|
this.previewIframe.classList.remove('hidden')
|
||||||
|
}
|
||||||
|
|
||||||
|
async mount () {
|
||||||
|
this.input = this.querySelector('input[type="hidden"]')
|
||||||
|
this.input.style.display = 'none'
|
||||||
|
|
||||||
|
const { EditorView, minimalSetup, html, htmlflow } = await loadCodeMirror()
|
||||||
|
|
||||||
|
this.editorView = new EditorView({
|
||||||
|
doc: this.input.value,
|
||||||
|
parent: this.editorContainer,
|
||||||
|
extensions: [
|
||||||
|
html(),
|
||||||
|
minimalSetup,
|
||||||
|
EditorView.lineWrapping,
|
||||||
|
EditorView.updateListener.of(update => {
|
||||||
|
if (update.docChanged) this.input.value = update.state.doc.toString()
|
||||||
|
}),
|
||||||
|
EditorView.theme({
|
||||||
|
'&': {
|
||||||
|
backgroundColor: 'white',
|
||||||
|
color: 'black',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontFamily: 'monospace'
|
||||||
|
},
|
||||||
|
'&.cm-focused': {
|
||||||
|
outline: 'none'
|
||||||
|
},
|
||||||
|
'&.cm-editor': {
|
||||||
|
borderRadius: '0.375rem',
|
||||||
|
border: 'none'
|
||||||
|
},
|
||||||
|
'.cm-gutters': {
|
||||||
|
display: 'none'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
this.previewIframe.srcdoc = this.editorView.state.doc.toString()
|
||||||
|
|
||||||
|
this.previewIframe.onload = () => {
|
||||||
|
const previewIframeDoc = this.previewIframe.contentDocument
|
||||||
|
|
||||||
|
if (previewIframeDoc.body) {
|
||||||
|
previewIframeDoc.body.contentEditable = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentDocument = this.previewIframe.contentDocument || this.previewIframe.contentWindow.document
|
||||||
|
|
||||||
|
contentDocument.body.addEventListener('input', async () => {
|
||||||
|
const html = contentDocument.documentElement.outerHTML.replace(' contenteditable="true"', '')
|
||||||
|
const prettifiedHtml = await htmlflow(html)
|
||||||
|
|
||||||
|
this.input.value = prettifiedHtml
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ import autocomplete from 'autocompleter'
|
|||||||
|
|
||||||
export default class extends HTMLElement {
|
export default class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
|
if (this.dataset.enabled === 'false') return
|
||||||
|
|
||||||
autocomplete({
|
autocomplete({
|
||||||
input: this.input,
|
input: this.input,
|
||||||
preventSubmit: this.dataset.submitOnSelect === 'true' ? 0 : 1,
|
preventSubmit: this.dataset.submitOnSelect === 'true' ? 0 : 1,
|
||||||
@@ -14,12 +16,16 @@ export default class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onSelect = (item) => {
|
onSelect = (item) => {
|
||||||
this.input.value = item.name
|
this.input.value = this.dataset.parentName ? item.name : item.full_name
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch = (text, resolve) => {
|
fetch = (text, resolve) => {
|
||||||
const queryParams = new URLSearchParams({ q: text })
|
const queryParams = new URLSearchParams({ q: text })
|
||||||
|
|
||||||
|
if (this.dataset.parentName) {
|
||||||
|
queryParams.append('parent_name', this.dataset.parentName)
|
||||||
|
}
|
||||||
|
|
||||||
fetch('/template_folders_autocomplete?' + queryParams).then(async (resp) => {
|
fetch('/template_folders_autocomplete?' + queryParams).then(async (resp) => {
|
||||||
const items = await resp.json()
|
const items = await resp.json()
|
||||||
|
|
||||||
@@ -34,7 +40,7 @@ export default class extends HTMLElement {
|
|||||||
|
|
||||||
div.setAttribute('dir', 'auto')
|
div.setAttribute('dir', 'auto')
|
||||||
|
|
||||||
div.textContent = item.name
|
div.textContent = this.dataset.parentName ? item.name : item.full_name
|
||||||
|
|
||||||
return div
|
return div
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
const iframeTemplate = this.querySelector('template')
|
||||||
|
|
||||||
|
this.observer = new IntersectionObserver((entries) => {
|
||||||
|
if (entries.some(e => e.isIntersecting)) {
|
||||||
|
iframeTemplate.parentElement.prepend(iframeTemplate.content)
|
||||||
|
|
||||||
|
this.observer.disconnect()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.observer.observe(this)
|
||||||
|
|
||||||
|
window.addEventListener('message', this.messageHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
messageHandler = (event) => {
|
||||||
|
if (event.data.type === 'google-drive-files-picked') {
|
||||||
|
this.form.querySelectorAll('input[name="google_drive_file_ids[]"]').forEach(el => el.remove())
|
||||||
|
|
||||||
|
const files = event.data.files || []
|
||||||
|
|
||||||
|
files.forEach((file) => {
|
||||||
|
const input = document.createElement('input')
|
||||||
|
input.type = 'hidden'
|
||||||
|
input.name = 'google_drive_file_ids[]'
|
||||||
|
input.value = file.id
|
||||||
|
this.form.appendChild(input)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.form.querySelector('button[type="submit"]').click()
|
||||||
|
this.loader.classList.remove('hidden')
|
||||||
|
} else if (event.data.type === 'google-drive-picker-loaded') {
|
||||||
|
this.loader.classList.add('hidden')
|
||||||
|
this.form.classList.remove('hidden')
|
||||||
|
} else if (event.data.type === 'google-drive-picker-request-oauth') {
|
||||||
|
document.getElementById(this.dataset.oauthButtonId).classList.remove('hidden')
|
||||||
|
this.classList.add('hidden')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
this.observer?.unobserve(this)
|
||||||
|
window.removeEventListener('message', this.messageHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
get form () {
|
||||||
|
return this.querySelector('form')
|
||||||
|
}
|
||||||
|
|
||||||
|
get loader () {
|
||||||
|
return document.getElementById('google_drive_loader')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.addEventListener('click', () => {
|
||||||
|
document.body.append(this.template.content)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get template () {
|
||||||
|
return document.getElementById(this.dataset.templateId)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
const src = this.getAttribute('src')
|
||||||
|
const link = document.createElement('a')
|
||||||
|
|
||||||
|
link.href = src
|
||||||
|
link.setAttribute('data-turbo-frame', 'modal')
|
||||||
|
link.style.display = 'none'
|
||||||
|
|
||||||
|
this.appendChild(link)
|
||||||
|
|
||||||
|
link.click()
|
||||||
|
|
||||||
|
window.history.replaceState({}, document.title, window.location.pathname)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
export default class extends HTMLElement {
|
export default class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.image.addEventListener('load', (e) => {
|
const image = this.querySelector('img')
|
||||||
this.image.setAttribute('width', e.target.naturalWidth)
|
|
||||||
this.image.setAttribute('height', e.target.naturalHeight)
|
image.addEventListener('load', (e) => {
|
||||||
|
image.setAttribute('width', e.target.naturalWidth)
|
||||||
|
image.setAttribute('height', e.target.naturalHeight)
|
||||||
|
|
||||||
this.style.aspectRatio = `${e.target.naturalWidth} / ${e.target.naturalHeight}`
|
this.style.aspectRatio = `${e.target.naturalWidth} / ${e.target.naturalHeight}`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
get image () {
|
|
||||||
return this.querySelector('img')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
const eventType = this.dataset.on || 'click'
|
||||||
|
const selector = document.getElementById(this.dataset.selectorId) || this
|
||||||
|
const eventElement = eventType === 'submit' ? this.querySelector('form') : this
|
||||||
|
|
||||||
|
eventElement.addEventListener(eventType, (event) => {
|
||||||
|
if (eventType === 'click') {
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
selector.remove()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.querySelectorAll('input[type="radio"]').forEach(radio => {
|
||||||
|
radio.addEventListener('change', (event) => {
|
||||||
|
const rating = parseInt(event.target.value)
|
||||||
|
|
||||||
|
if (rating === 10) {
|
||||||
|
window.review_comment.value = ''
|
||||||
|
window.review_comment.classList.add('hidden')
|
||||||
|
window.review_submit.classList.add('hidden')
|
||||||
|
event.target.form.submit()
|
||||||
|
} else {
|
||||||
|
window.review_comment.classList.remove('hidden')
|
||||||
|
window.review_submit.classList.remove('hidden')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.selector = document.getElementById(this.dataset.selectorId)
|
||||||
|
|
||||||
|
this.addEventListener('click', () => {
|
||||||
|
this.selector.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||||
|
history.replaceState(null, null, `#${this.dataset.selectorId}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,14 @@ export default class extends HTMLElement {
|
|||||||
this.input.classList.remove('w-60')
|
this.input.classList.remove('w-60')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.button.addEventListener('click', (event) => {
|
||||||
|
if (!this.input.value && document.activeElement !== this.input) {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
this.input.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
get input () {
|
get input () {
|
||||||
@@ -22,4 +30,8 @@ export default class extends HTMLElement {
|
|||||||
get title () {
|
get title () {
|
||||||
return document.querySelector(this.dataset.title)
|
return document.querySelector(this.dataset.title)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get button () {
|
||||||
|
return this.querySelector('button')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
const input = this.dataset.inputId ? document.getElementById(this.dataset.inputId) : this.querySelector('input')
|
||||||
|
|
||||||
|
this.firstElementChild.addEventListener(this.dataset.on || 'click', () => {
|
||||||
|
if (this.dataset.emptyOnly !== 'true' || !input.value) {
|
||||||
|
input.value = this.dataset.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.addEventListener('change', (event) => {
|
||||||
|
const targetValue = this.dataset.value
|
||||||
|
const selectorId = this.dataset.selectorId
|
||||||
|
const targetElement = document.getElementById(selectorId)
|
||||||
|
|
||||||
|
if (event.target.value === targetValue) {
|
||||||
|
targetElement.classList.remove('hidden')
|
||||||
|
} else {
|
||||||
|
targetElement.classList.add('hidden')
|
||||||
|
targetElement.value = ''
|
||||||
|
event.target.form.requestSubmit()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,17 +5,28 @@ export default targetable(class extends HTMLElement {
|
|||||||
static [target.static] = ['canvas', 'input', 'clear', 'button']
|
static [target.static] = ['canvas', 'input', 'clear', 'button']
|
||||||
|
|
||||||
async connectedCallback () {
|
async connectedCallback () {
|
||||||
const scale = 3
|
|
||||||
|
|
||||||
this.canvas.width = this.canvas.parentNode.clientWidth * scale
|
|
||||||
this.canvas.height = this.canvas.parentNode.clientHeight * scale
|
|
||||||
|
|
||||||
this.canvas.getContext('2d').scale(scale, scale)
|
|
||||||
|
|
||||||
const { default: SignaturePad } = await import('signature_pad')
|
const { default: SignaturePad } = await import('signature_pad')
|
||||||
|
|
||||||
|
this.setCanvasSize()
|
||||||
|
|
||||||
this.pad = new SignaturePad(this.canvas)
|
this.pad = new SignaturePad(this.canvas)
|
||||||
|
|
||||||
|
this.resizeObserver = new ResizeObserver(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (!this.canvas) return
|
||||||
|
|
||||||
|
const { width, height } = this.canvas
|
||||||
|
|
||||||
|
this.setCanvasSize()
|
||||||
|
|
||||||
|
if (this.canvas.width !== width || this.canvas.height !== height) {
|
||||||
|
this.redrawCanvas(width, height)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.resizeObserver.observe(this.canvas.parentNode)
|
||||||
|
|
||||||
this.clear.addEventListener('click', (e) => {
|
this.clear.addEventListener('click', (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
@@ -47,4 +58,38 @@ export default targetable(class extends HTMLElement {
|
|||||||
|
|
||||||
this.closest('form').requestSubmit()
|
this.closest('form').requestSubmit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
if (this.resizeObserver) {
|
||||||
|
this.resizeObserver.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setCanvasSize () {
|
||||||
|
const scale = 3
|
||||||
|
|
||||||
|
const width = this.canvas.parentNode.clientWidth
|
||||||
|
const height = this.canvas.parentNode.clientWidth / 2.5
|
||||||
|
|
||||||
|
if (this.canvas.width !== width * scale || this.canvas.height !== height * scale) {
|
||||||
|
this.canvas.width = width * scale
|
||||||
|
this.canvas.height = height * scale
|
||||||
|
|
||||||
|
this.canvas.getContext('2d').scale(scale, scale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
redrawCanvas (oldWidth, oldHeight) {
|
||||||
|
if (this.pad && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0) {
|
||||||
|
const sx = this.canvas.width / oldWidth
|
||||||
|
const sy = this.canvas.height / oldHeight
|
||||||
|
|
||||||
|
const scaledData = this.pad.toData().map((stroke) => ({
|
||||||
|
...stroke,
|
||||||
|
points: stroke.points.map((p) => ({ ...p, x: p.x * sx, y: p.y * sy }))
|
||||||
|
}))
|
||||||
|
|
||||||
|
this.pad.fromData(scaledData)
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
@@ -1,11 +1,27 @@
|
|||||||
export default class extends HTMLElement {
|
export default class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
|
const form = this.querySelector('form') || (this.querySelector('input, button, select') || this.lastElementChild).form
|
||||||
|
|
||||||
if (this.dataset.interval) {
|
if (this.dataset.interval) {
|
||||||
this.interval = setInterval(() => {
|
this.interval = setInterval(() => {
|
||||||
this.querySelector('form').requestSubmit()
|
form.requestSubmit()
|
||||||
}, parseInt(this.dataset.interval))
|
}, parseInt(this.dataset.interval))
|
||||||
|
} else if (this.dataset.on) {
|
||||||
|
this.lastElementChild.addEventListener(this.dataset.on, (event) => {
|
||||||
|
if (this.dataset.disable === 'true') {
|
||||||
|
form.querySelector('[type="submit"]')?.setAttribute('disabled', true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.dataset.submitIfValue === 'true') {
|
||||||
|
if (event.target.value) {
|
||||||
|
form.requestSubmit()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
form.requestSubmit()
|
||||||
|
}
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
this.querySelector('form').requestSubmit()
|
form.requestSubmit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
const button = this.querySelector('a, button')
|
||||||
|
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
this.dataset.classes.split(' ').forEach((cls) => {
|
||||||
|
button.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')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,8 +4,18 @@ export default actionable(class extends HTMLElement {
|
|||||||
trigger (event) {
|
trigger (event) {
|
||||||
const elementIds = JSON.parse(this.dataset.elementIds)
|
const elementIds = JSON.parse(this.dataset.elementIds)
|
||||||
|
|
||||||
elementIds.forEach((elementId) => {
|
if (event.target.type === 'checkbox') {
|
||||||
document.getElementById(elementId).classList.toggle('hidden', (event.target.dataset.toggleId || event.target.value) !== elementId)
|
elementIds.forEach((elementId) => {
|
||||||
})
|
document.getElementById(elementId)?.classList.toggle('hidden')
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
elementIds.forEach((elementId) => {
|
||||||
|
document.getElementById(elementId).classList.toggle('hidden', (event.target.dataset.toggleId || event.target.value) !== elementId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.dataset.focusId) {
|
||||||
|
document.getElementById(this.dataset.focusId)?.focus()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -6,6 +6,7 @@ import ToggleSubmit from './elements/toggle_submit'
|
|||||||
import FetchForm from './elements/fetch_form'
|
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'
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ safeRegisterElement('toggle-submit', ToggleSubmit)
|
|||||||
safeRegisterElement('fetch-form', FetchForm)
|
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('submission-form', class extends HTMLElement {
|
safeRegisterElement('submission-form', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.appElem = document.createElement('div')
|
this.appElem = document.createElement('div')
|
||||||
@@ -35,6 +37,7 @@ safeRegisterElement('submission-form', class extends HTMLElement {
|
|||||||
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 !== 'false',
|
||||||
|
withFieldLabels: this.dataset.withFieldLabels !== 'false',
|
||||||
withDisclosure: this.dataset.withDisclosure === 'true',
|
withDisclosure: this.dataset.withDisclosure === 'true',
|
||||||
reuseSignature: this.dataset.reuseSignature !== 'false',
|
reuseSignature: this.dataset.reuseSignature !== 'false',
|
||||||
withTypedSignature: this.dataset.withTypedSignature !== 'false',
|
withTypedSignature: this.dataset.withTypedSignature !== 'false',
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ button .disabled {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
button[disabled] .disabled {
|
button[disabled] .disabled, button.btn-disabled .disabled {
|
||||||
display: initial;
|
display: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ button .enabled {
|
|||||||
display: initial;
|
display: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
button[disabled] .enabled {
|
button[disabled] .enabled, button.btn-disabled .enabled {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ function mouseoverListener (event) {
|
|||||||
|
|
||||||
const requestOptions = {
|
const requestOptions = {
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
headers: { Accept: 'text/html, application/xhtml+xml', 'VND.PREFETCH': 'true' },
|
headers: { Accept: 'text/html, application/xhtml+xml', 'VND.PREFETCH': 'true', 'X-Turbo': 'true' },
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
redirect: 'follow'
|
redirect: 'follow'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,8 +29,15 @@
|
|||||||
{{ optionValue(option) }}
|
{{ optionValue(option) }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ field.title || field.name || fieldNames[field.type] }}
|
<MarkdownContent
|
||||||
<template v-if="field.type === 'checkbox' && !field.name">
|
v-if="field.title"
|
||||||
|
:text-only="true"
|
||||||
|
:string="field.title"
|
||||||
|
/>
|
||||||
|
<template v-else>
|
||||||
|
{{ field.name || fieldNames[field.type] }}
|
||||||
|
</template>
|
||||||
|
<template v-if="field.type === 'checkbox' && !field.name && !field.title">
|
||||||
{{ fieldIndex + 1 }}
|
{{ fieldIndex + 1 }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="!field.required && field.type !== 'checkbox'">
|
<template v-else-if="!field.required && field.type !== 'checkbox'">
|
||||||
@@ -53,14 +60,19 @@
|
|||||||
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"
|
||||||
:class="isNarrow && (withSignatureId || field.preferences?.reason_field_uuid) ? 'flex-row' : 'flex-col'"
|
:class="isNarrow && (isShowSignatureId || field.preferences?.reason_field_uuid) ? 'flex-row' : 'flex-col'"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex overflow-hidden"
|
class="flex overflow-hidden"
|
||||||
:class="isNarrow && (withSignatureId || field.preferences?.reason_field_uuid) ? 'w-1/2' : 'flex-grow'"
|
:class="isNarrow && (isShowSignatureId || field.preferences?.reason_field_uuid) ? 'w-1/2' : 'flex-grow'"
|
||||||
style="min-height: 50%"
|
style="min-height: 50%"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -69,7 +81,7 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="withSignatureId || field.preferences?.reason_field_uuid"
|
v-if="isShowSignatureId || field.preferences?.reason_field_uuid"
|
||||||
class="text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem]"
|
class="text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem]"
|
||||||
:class="isNarrow ? 'w-1/2' : 'w-full'"
|
:class="isNarrow ? 'w-1/2' : 'w-full'"
|
||||||
>
|
>
|
||||||
@@ -170,8 +182,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="field.type === 'cells'"
|
v-else-if="field.type === 'cells'"
|
||||||
class="w-full flex items-center"
|
class="w-full flex"
|
||||||
:class="{ 'justify-end': field.preferences?.align === 'right', ...fontClasses }"
|
:class="{ 'justify-end': field.preferences?.align === 'right', ...alignClasses, ...fontClasses }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(char, index) in modelValue"
|
v-for="(char, index) in modelValue"
|
||||||
@@ -212,6 +224,48 @@
|
|||||||
>
|
>
|
||||||
{{ formatNumber(modelValue, field.preferences?.format) }}
|
{{ formatNumber(modelValue, field.preferences?.format) }}
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="field.type === 'strikethrough'"
|
||||||
|
class="w-full h-full flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
v-if="(((1000.0 / pageWidth) * pageHeight) * area.h) < 40.0"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
>
|
||||||
|
<line
|
||||||
|
x1="0"
|
||||||
|
y1="50%"
|
||||||
|
x2="100%"
|
||||||
|
y2="50%"
|
||||||
|
:stroke="field.preferences?.color || 'red'"
|
||||||
|
:stroke-width="strikethroughWidth"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
:style="{ overflow: 'visible', width: `calc(100% - ${strikethroughWidth})`, height: `calc(100% - ${strikethroughWidth})` }"
|
||||||
|
>
|
||||||
|
<line
|
||||||
|
x1="0"
|
||||||
|
y1="0"
|
||||||
|
x2="100%"
|
||||||
|
y2="100%"
|
||||||
|
:stroke="field.preferences?.color || 'red'"
|
||||||
|
:stroke-width="strikethroughWidth"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="100%"
|
||||||
|
y1="0"
|
||||||
|
x2="0"
|
||||||
|
y2="100%"
|
||||||
|
:stroke="field.preferences?.color || 'red'"
|
||||||
|
:stroke-width="strikethroughWidth"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
class="whitespace-pre-wrap"
|
class="whitespace-pre-wrap"
|
||||||
@@ -222,12 +276,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconId } from '@tabler/icons-vue'
|
import MarkdownContent from './markdown_content'
|
||||||
|
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',
|
||||||
components: {
|
components: {
|
||||||
IconPaperclip,
|
IconPaperclip,
|
||||||
|
MarkdownContent,
|
||||||
IconCheck
|
IconCheck
|
||||||
},
|
},
|
||||||
inject: ['t'],
|
inject: ['t'],
|
||||||
@@ -251,6 +307,16 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
pageWidth: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
pageHeight: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
isValueSet: {
|
isValueSet: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -330,7 +396,22 @@ 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')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
strikethroughWidth () {
|
||||||
|
if (this.isInlineSize) {
|
||||||
|
return '0.6cqmin'
|
||||||
|
} else {
|
||||||
|
return 'clamp(0px, 0.5vw, 6px)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isShowSignatureId () {
|
||||||
|
if ([true, false].includes(this.field.preferences?.with_signature_id)) {
|
||||||
|
return this.field.preferences.with_signature_id
|
||||||
|
} else {
|
||||||
|
return this.withSignatureId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
alignClasses () {
|
alignClasses () {
|
||||||
@@ -379,7 +460,8 @@ export default {
|
|||||||
multiple: IconChecks,
|
multiple: IconChecks,
|
||||||
phone: IconPhoneCheck,
|
phone: IconPhoneCheck,
|
||||||
payment: IconCreditCard,
|
payment: IconCreditCard,
|
||||||
verification: IconId
|
verification: IconId,
|
||||||
|
kba: IconUserScan
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
image () {
|
image () {
|
||||||
@@ -396,6 +478,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]
|
||||||
@@ -415,10 +504,14 @@ export default {
|
|||||||
},
|
},
|
||||||
formattedDate () {
|
formattedDate () {
|
||||||
if (this.field.type === 'date' && this.modelValue) {
|
if (this.field.type === 'date' && this.modelValue) {
|
||||||
return this.formatDate(
|
try {
|
||||||
this.modelValue === '{{date}}' ? new Date() : new Date(this.modelValue),
|
return this.formatDate(
|
||||||
this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
|
this.modelValue === '{{date}}' ? new Date() : new Date(this.modelValue),
|
||||||
)
|
this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
return this.modelValue
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@@ -460,9 +553,6 @@ export default {
|
|||||||
fontScale () {
|
fontScale () {
|
||||||
return 1000 / 612.0
|
return 1000 / 612.0
|
||||||
},
|
},
|
||||||
ladscapeScale () {
|
|
||||||
return 8.5 / 11.0
|
|
||||||
},
|
|
||||||
computedStyle () {
|
computedStyle () {
|
||||||
const { x, y, w, h } = this.area
|
const { x, y, w, h } = this.area
|
||||||
|
|
||||||
@@ -477,10 +567,14 @@ export default {
|
|||||||
style.color = this.field.preferences.color
|
style.color = this.field.preferences.color
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.field.preferences?.background) {
|
||||||
|
style.background = this.field.preferences.background
|
||||||
|
}
|
||||||
|
|
||||||
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: {
|
||||||
|
|||||||
@@ -11,30 +11,37 @@
|
|||||||
v-for="(area, areaIndex) in field.areas"
|
v-for="(area, areaIndex) in field.areas"
|
||||||
:key="areaIndex"
|
:key="areaIndex"
|
||||||
>
|
>
|
||||||
<Teleport
|
<template
|
||||||
v-if="findPageElementForArea(area)"
|
v-for="(pageElem, index) in [findPageElementForArea(area)]"
|
||||||
:to="findPageElementForArea(area)"
|
:key="index"
|
||||||
>
|
>
|
||||||
<FieldArea
|
<Teleport
|
||||||
:ref="setAreaRef"
|
v-if="pageElem"
|
||||||
v-model="values[field.uuid]"
|
:to="pageElem"
|
||||||
:values="values"
|
>
|
||||||
:field="field"
|
<FieldArea
|
||||||
:area="area"
|
:ref="setAreaRef"
|
||||||
:submittable="submittable"
|
v-model="values[field.uuid]"
|
||||||
:field-index="fieldIndex"
|
:values="values"
|
||||||
:is-inline-size="isInlineSize"
|
:field="field"
|
||||||
:scroll-padding="scrollPadding"
|
:area="area"
|
||||||
:submitter="submitter"
|
:submittable="submittable"
|
||||||
:with-field-placeholder="withFieldPlaceholder"
|
:page-width="1400"
|
||||||
:with-signature-id="withSignatureId"
|
:page-height="(1400.0 / pageElem.offsetWidth) * pageElem.offsetHeight"
|
||||||
:is-active="currentStep === step"
|
:field-index="fieldIndex"
|
||||||
:with-label="withLabel && !withFieldPlaceholder && step.length < 2"
|
:is-inline-size="isInlineSize"
|
||||||
:is-value-set="step.some((f) => f.uuid in values)"
|
:scroll-padding="scrollPadding"
|
||||||
:attachments-index="attachmentsIndex"
|
:submitter="submitter"
|
||||||
@click="[$emit('focus-step', stepIndex), maybeScrollOnClick(field, area)]"
|
:with-field-placeholder="withFieldPlaceholder"
|
||||||
/>
|
:with-signature-id="withSignatureId"
|
||||||
</Teleport>
|
:is-active="currentStep === step"
|
||||||
|
:with-label="withLabel && !withFieldPlaceholder && step.length < 2"
|
||||||
|
:is-value-set="step.some((f) => f.uuid in values)"
|
||||||
|
:attachments-index="attachmentsIndex"
|
||||||
|
@click="[$emit('focus-step', stepIndex), maybeScrollOnClick(field, area)]"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
<div class="space-y-3 mt-5">
|
<div class="space-y-3 mt-5">
|
||||||
<a
|
<a
|
||||||
v-if="completedButton.url"
|
v-if="completedButton.url"
|
||||||
:href="sanitizeHref(completedButton.url)"
|
:href="sanitizeUrl(completedButton.url)"
|
||||||
rel="noopener noreferrer nofollow"
|
rel="noopener noreferrer nofollow"
|
||||||
class="white-button flex items-center w-full completed-form-completed-button"
|
class="white-button flex items-center w-full completed-form-completed-button"
|
||||||
>
|
>
|
||||||
@@ -102,6 +102,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop, IconLogin } from '@tabler/icons-vue'
|
import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop, IconLogin } from '@tabler/icons-vue'
|
||||||
import MarkdownContent from './markdown_content'
|
import MarkdownContent from './markdown_content'
|
||||||
|
import { sanitizeUrl } from '@braintree/sanitize-url'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FormCompleted',
|
name: 'FormCompleted',
|
||||||
@@ -198,6 +199,7 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
sanitizeUrl,
|
||||||
sendCopyToEmail () {
|
sendCopyToEmail () {
|
||||||
this.isSendingCopy = true
|
this.isSendingCopy = true
|
||||||
|
|
||||||
@@ -252,11 +254,6 @@ export default {
|
|||||||
this.isDownloading = false
|
this.isDownloading = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
sanitizeHref (href) {
|
|
||||||
if (href && href.trim().match(/^((?:https?:\/\/)|\/)/)) {
|
|
||||||
return href.replace(/javascript:/g, '')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
downloadSafariIos (urls) {
|
downloadSafariIos (urls) {
|
||||||
const fileRequests = urls.map((url) => {
|
const fileRequests = urls.map((url) => {
|
||||||
return fetch(url).then(async (resp) => {
|
return fetch(url).then(async (resp) => {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
|
v-if="withToday"
|
||||||
class="btn btn-outline btn-sm !normal-case font-normal set-current-date-button"
|
class="btn btn-outline btn-sm !normal-case font-normal set-current-date-button"
|
||||||
@click.prevent="[setCurrentDate(), $emit('focus')]"
|
@click.prevent="[setCurrentDate(), $emit('focus')]"
|
||||||
>
|
>
|
||||||
@@ -41,11 +42,13 @@
|
|||||||
<MarkdownContent :string="field.description" />
|
<MarkdownContent :string="field.description" />
|
||||||
</div>
|
</div>
|
||||||
<AppearsOn :field="field" />
|
<AppearsOn :field="field" />
|
||||||
<div class="text-center">
|
<div class="text-center flex">
|
||||||
<input
|
<input
|
||||||
:id="field.uuid"
|
:id="field.uuid"
|
||||||
ref="input"
|
ref="input"
|
||||||
v-model="value"
|
v-model="value"
|
||||||
|
:min="validationMin"
|
||||||
|
:max="validationMax"
|
||||||
class="base-input !text-2xl text-center w-full"
|
class="base-input !text-2xl text-center w-full"
|
||||||
:required="field.required"
|
:required="field.required"
|
||||||
type="date"
|
type="date"
|
||||||
@@ -89,6 +92,44 @@ export default {
|
|||||||
},
|
},
|
||||||
emits: ['update:model-value', 'focus', 'submit'],
|
emits: ['update:model-value', 'focus', 'submit'],
|
||||||
computed: {
|
computed: {
|
||||||
|
dateNowString () {
|
||||||
|
const today = new Date()
|
||||||
|
|
||||||
|
const yyyy = today.getFullYear()
|
||||||
|
const mm = String(today.getMonth() + 1).padStart(2, '0')
|
||||||
|
const dd = String(today.getDate()).padStart(2, '0')
|
||||||
|
|
||||||
|
return `${yyyy}-${mm}-${dd}`
|
||||||
|
},
|
||||||
|
validationMin () {
|
||||||
|
if (this.field.validation?.min) {
|
||||||
|
return ['{{date}}', '{date}'].includes(this.field.validation.min) ? this.dateNowString : this.field.validation.min
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
validationMax () {
|
||||||
|
if (this.field.validation?.max) {
|
||||||
|
return ['{{date}}', '{date}'].includes(this.field.validation.max) ? this.dateNowString : this.field.validation.max
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
withToday () {
|
||||||
|
const todayDate = new Date().setHours(0, 0, 0, 0)
|
||||||
|
|
||||||
|
if (this.validationMin) {
|
||||||
|
if (new Date(this.validationMin).setHours(0, 0, 0, 0) <= todayDate) {
|
||||||
|
return this.validationMax ? (new Date(this.validationMax).setHours(0, 0, 0, 0) >= todayDate) : true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else if (this.validationMax) {
|
||||||
|
return new Date(this.validationMax).setHours(0, 0, 0, 0) >= todayDate
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
value: {
|
value: {
|
||||||
set (value) {
|
set (value) {
|
||||||
this.$emit('update:model-value', value)
|
this.$emit('update:model-value', value)
|
||||||
|
|||||||
@@ -112,11 +112,23 @@ export default {
|
|||||||
onSelectFiles (e) {
|
onSelectFiles (e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
this.uploadFiles(this.$refs.input.files).then(() => {
|
const files = Array.from(this.$refs.input.files).filter((f) => {
|
||||||
if (this.$refs.input) {
|
if (this.accept === 'image/*') {
|
||||||
this.$refs.input.value = ''
|
return f.type.startsWith('image')
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (this.accept === 'image/*' && !files.length) {
|
||||||
|
alert(this.t('please_upload_an_image_file'))
|
||||||
|
} else {
|
||||||
|
this.uploadFiles(files).then(() => {
|
||||||
|
if (this.$refs.input) {
|
||||||
|
this.$refs.input.value = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async uploadFiles (files) {
|
async uploadFiles (files) {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
@@ -151,12 +163,20 @@ export default {
|
|||||||
return fetch(this.baseUrl + '/api/attachments', {
|
return fetch(this.baseUrl + '/api/attachments', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
}).then(resp => resp.json()).then((data) => {
|
}).then(async (resp) => {
|
||||||
return data
|
const data = await resp.json()
|
||||||
|
|
||||||
|
if (resp.status === 422) {
|
||||||
|
alert(data.error)
|
||||||
|
} else {
|
||||||
|
return data
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})).then((result) => {
|
})).then((result) => {
|
||||||
this.$emit('upload', result)
|
if (result && result[0]) {
|
||||||
|
this.$emit('upload', result)
|
||||||
|
}
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
})
|
})
|
||||||
|
|||||||
Loaded 100 of 425 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user