mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 22:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b94e6bf54a | ||
|
|
8f975c2898 | ||
|
|
d25c4fa766 | ||
|
|
43db15b350 | ||
|
|
09074e2ac0 | ||
|
|
04295f4540 | ||
|
|
11c48a495f | ||
|
|
942c961cd1 | ||
|
|
9193511d5f | ||
|
|
e0ddf97771 | ||
|
|
2bd249bd5a | ||
|
|
b72c99dbce | ||
|
|
fc0504f91e | ||
|
|
1da2196b0e | ||
|
|
4bdb249a2b | ||
|
|
cb38549480 | ||
|
|
ee9ee6ee32 | ||
|
|
748eaaa98d | ||
|
|
9e4ec11e33 | ||
|
|
93d2b76eae | ||
|
|
2c8942e888 | ||
|
|
4424840c81 | ||
|
|
68392b4e74 | ||
|
|
0b4e345ad6 | ||
|
|
1cea5f5a99 | ||
|
|
cdaadbab20 | ||
|
|
d1ec8c6837 | ||
|
|
cdffa66db1 | ||
|
|
fca21ab476 | ||
|
|
b1d2d92f54 | ||
|
|
10bb9112c7 | ||
|
|
58b337a3ee | ||
|
|
de52f2f5e5 | ||
|
|
f10e941529 | ||
|
|
78b1864d7d | ||
|
|
ab49af9bcb | ||
|
|
286b8d7067 | ||
|
|
21c087cba7 | ||
|
|
ddd881a52f | ||
|
|
12c69578af | ||
|
|
e96cb5eb8a | ||
|
|
c01cfe83be | ||
|
|
b92d1ee38c | ||
|
|
b90c67b184 | ||
|
|
edb29977dd | ||
|
|
e55c8f251d | ||
|
|
f1d6bbdc67 | ||
|
|
8da4c2fa10 | ||
|
|
e872fa2497 | ||
|
|
165bfef9b9 | ||
|
|
8a50c755d2 | ||
|
|
002bc315f6 | ||
|
|
e87522eeab | ||
|
|
377244f948 | ||
|
|
73423a6f44 | ||
|
|
d219e3cd7e | ||
|
|
09b0022195 | ||
|
|
8aac935641 | ||
|
|
16e5c13633 | ||
|
|
fff1831768 | ||
|
|
f91a318e94 | ||
|
|
ed06e17b23 | ||
|
|
63e1be7dfe | ||
|
|
16044612bd | ||
|
|
27d8880414 | ||
|
|
a0aae6a9de | ||
|
|
749043dfbb | ||
|
|
320fe02e35 | ||
|
|
6f4b9d4a4e | ||
|
|
a589cf9471 | ||
|
|
7fc7283191 | ||
|
|
af03560494 | ||
|
|
988d1b93a5 | ||
|
|
ab0278198a | ||
|
|
fbc649ab59 | ||
|
|
6b5adc2504 | ||
|
|
7c4307ef10 | ||
|
|
de744e2f6c | ||
|
|
c171597c7f | ||
|
|
3034c00bb3 | ||
|
|
9539c476c0 | ||
|
|
4552c72860 | ||
|
|
85f8a92721 | ||
|
|
049b9c78c2 | ||
|
|
acf06084bf | ||
|
|
61da477733 | ||
|
|
fdeec548a3 | ||
|
|
ff0d486831 | ||
|
|
d5bf54ee26 | ||
|
|
7616774613 | ||
|
|
27c4dc7eca | ||
|
|
4ae834d6f7 | ||
|
|
b71a66a082 | ||
|
|
80cc38542c | ||
|
|
185aecec42 | ||
|
|
8c092eba78 | ||
|
|
781ccc701d | ||
|
|
009e05584a | ||
|
|
f9d52e56a2 | ||
|
|
9e66f8412c | ||
|
|
181e42c4b3 | ||
|
|
15e60fc5e7 | ||
|
|
ee357e58ac | ||
|
|
0d9a709fa3 | ||
|
|
7eae09bdb0 | ||
|
|
e8e32e5d1f | ||
|
|
a3d54db4c6 | ||
|
|
9686ae6250 | ||
|
|
044195f07b | ||
|
|
35f159266a | ||
|
|
3b717ae3de | ||
|
|
2389c3fc40 | ||
|
|
92da790e77 | ||
|
|
14418b93b1 | ||
|
|
1289aff493 | ||
|
|
7568f563cb | ||
|
|
5ae56bd50c | ||
|
|
c2ded81959 | ||
|
|
f2c307ef63 | ||
|
|
af787887d8 | ||
|
|
54272d83eb | ||
|
|
3070fc2074 | ||
|
|
6a31d2c722 | ||
|
|
f26bfe9acc | ||
|
|
a729fd6234 | ||
|
|
c2051c156e | ||
|
|
3852870c09 | ||
|
|
a58253a6a1 | ||
|
|
30c98a970d | ||
|
|
8f2edef09e | ||
|
|
9e465ea47e | ||
|
|
cdda55d528 | ||
|
|
db302858c4 | ||
|
|
b81cc83027 | ||
|
|
766d653349 | ||
|
|
98c04eac15 | ||
|
|
c35dfea154 | ||
|
|
37a9a86a1d | ||
|
|
e9a759055b | ||
|
|
f6c061a57b | ||
|
|
91f3d8ed7e | ||
|
|
3b18502d3d | ||
|
|
df8e79683a | ||
|
|
7080e8ea33 | ||
|
|
c56f8cb38f | ||
|
|
1ceac968b0 | ||
|
|
938ee99cfc | ||
|
|
2367040e45 | ||
|
|
df19aabfba | ||
|
|
f7b5bcd52f | ||
|
|
a249b0a678 | ||
|
|
99bc46b40f | ||
|
|
c1e6f0970d | ||
|
|
e8c2316bb2 | ||
|
|
9866e5468c | ||
|
|
fc60df7974 | ||
|
|
08784d28c7 | ||
|
|
0440ea652f | ||
|
|
e466562391 | ||
|
|
b2c8667c53 | ||
|
|
d6893079f7 | ||
|
|
ac1d973605 | ||
|
|
1405fefc85 | ||
|
|
2a75055fb6 | ||
|
|
f84fdd51de | ||
|
|
b38073126e | ||
|
|
c6e681195d | ||
|
|
53d924be0b | ||
|
|
d6cf027d5d | ||
|
|
ec332bab88 | ||
|
|
8b3b90a7e2 | ||
|
|
9047e3bed3 | ||
|
|
72b7391245 | ||
|
|
2df05f0327 | ||
|
|
95f3eb0e5d | ||
|
|
ce8d964ce6 | ||
|
|
f0d5fc4e2d | ||
|
|
b4b17d847e | ||
|
|
fd537e7ac0 | ||
|
|
8c271104e1 | ||
|
|
51edd60d96 | ||
|
|
a265ca1716 | ||
|
|
a773f63a65 | ||
|
|
576c8a2e24 | ||
|
|
5829de9ee4 | ||
|
|
b202941b79 | ||
|
|
6f6fb74988 | ||
|
|
0137bdbe4c | ||
|
|
55d9511c01 | ||
|
|
929d29576d | ||
|
|
6f7128a51c | ||
|
|
fc5c4ff2fc | ||
|
|
a81595db2b | ||
|
|
a5615d500d | ||
|
|
da9266befc | ||
|
|
5b3cffeb73 | ||
|
|
8f7b8a5fdf | ||
|
|
6d93116993 | ||
|
|
a0f4916d3b | ||
|
|
ace897eaf9 | ||
|
|
b8667c5408 | ||
|
|
d87d66632e | ||
|
|
3015c2d156 | ||
|
|
52b9e9ee8e | ||
|
|
5bfad0271c | ||
|
|
58797891c7 | ||
|
|
897750ba1e | ||
|
|
99341e773f | ||
|
|
71319c1548 | ||
|
|
c0a42f6bfc | ||
|
|
d5abb46112 | ||
|
|
2508b5fc8a | ||
|
|
4bd1ef0da3 | ||
|
|
4efd748571 | ||
|
|
859892d1fd | ||
|
|
7adeaf93cd | ||
|
|
5dbe155ecb | ||
|
|
667d065bee | ||
|
|
bb613059de | ||
|
|
1b2dec4c2a | ||
|
|
2c2ef97c30 | ||
|
|
6df1d68237 | ||
|
|
20c9faedff | ||
|
|
d931a40ea9 | ||
|
|
1097b98b85 | ||
|
|
2e597fcf43 | ||
|
|
bb2fa6a261 | ||
|
|
1dc8369487 | ||
|
|
e6d60f1985 | ||
|
|
82e3cb8a4d | ||
|
|
aeec0cbf8e | ||
|
|
7a2d1fc6c5 | ||
|
|
68033ab7d2 | ||
|
|
7595c60ea8 | ||
|
|
d9318c04f8 | ||
|
|
2d76198290 | ||
|
|
02a7b6ea55 | ||
|
|
ab57076b8f | ||
|
|
82fba47c98 | ||
|
|
5f32e2a5af | ||
|
|
70012fbd7c | ||
|
|
668f6ca975 | ||
|
|
e78dd301ef | ||
|
|
f8ec3daf10 | ||
|
|
75c929639d | ||
|
|
e9c1242b8d | ||
|
|
e7eb1d4f12 | ||
|
|
abbd3e809a | ||
|
|
f430b4f5a6 | ||
|
|
e65234b4c8 | ||
|
|
fe04d22c94 | ||
|
|
44a7a5ebdc | ||
|
|
119ef0d0c7 | ||
|
|
cdb8eb7931 | ||
|
|
aef176c0cc | ||
|
|
0c21b55865 | ||
|
|
964b1444a5 | ||
|
|
48de343948 | ||
|
|
2640934c15 | ||
|
|
442dd4d6db | ||
|
|
3168feb3d6 | ||
|
|
fbae4933f6 | ||
|
|
78bb66c344 | ||
|
|
5af65ef74f | ||
|
|
4d354ae9ff | ||
|
|
1e57c1ace7 | ||
|
|
fefd138dfd | ||
|
|
16a77d3536 | ||
|
|
e03857bdb7 | ||
|
|
fc679bcd72 | ||
|
|
652b0da021 | ||
|
|
2d54b0db2f | ||
|
|
7a29894926 | ||
|
|
f9ae79dd5b | ||
|
|
cadb78ca35 | ||
|
|
36a6b7ff1d | ||
|
|
efee46f113 | ||
|
|
b6639738c3 | ||
|
|
aab07c5f49 | ||
|
|
33c0784bc6 | ||
|
|
6f5ebfc8a6 | ||
|
|
811091ff14 | ||
|
|
6666f39928 | ||
|
|
f67db9fe5f | ||
|
|
45f002803e | ||
|
|
8c19f90e88 | ||
|
|
a8f5d5cea7 | ||
|
|
6788071eec | ||
|
|
0c690b3033 | ||
|
|
dcb963b623 | ||
|
|
04e30dbdd6 | ||
|
|
ac6d5a7aed | ||
|
|
3166a31b67 | ||
|
|
fc3283eb3a | ||
|
|
d6a96d4aa3 | ||
|
|
cda349d71c | ||
|
|
87345a3b3d | ||
|
|
b791d0699c | ||
|
|
2cb15f3282 | ||
|
|
8b823ca0d0 | ||
|
|
f3f08797f7 | ||
|
|
cab9672d25 | ||
|
|
c8d3b1bace | ||
|
|
5ee51b2b57 | ||
|
|
525dc503a6 | ||
|
|
12cf28792b | ||
|
|
33466c1d07 | ||
|
|
226ae2172e | ||
|
|
8e2700b8b8 | ||
|
|
753cbd4d27 | ||
|
|
f16405ca70 | ||
|
|
23f052c400 | ||
|
|
90a0a03e15 | ||
|
|
2dba8ae366 | ||
|
|
5bb7ad571c | ||
|
|
0cdb7c2c4f | ||
|
|
11bdc522cd | ||
|
|
014835f90d | ||
|
|
05e66b60ee | ||
|
|
505b185cf2 | ||
|
|
50c32ac6ff | ||
|
|
878ab25fd4 | ||
|
|
1b31772ff6 | ||
|
|
d4940234b7 | ||
|
|
9bdc5d4863 | ||
|
|
a73fd7929c | ||
|
|
8269a61751 | ||
|
|
de072d1437 | ||
|
|
5b24793f2b | ||
|
|
660f7ab8f3 | ||
|
|
6e3fea5fa0 | ||
|
|
29ecbecbc2 | ||
|
|
ad3bbe9892 | ||
|
|
b570ebee69 | ||
|
|
207780fa3f | ||
|
|
8aa219433b | ||
|
|
cb1e9f2be4 | ||
|
|
202943f58d | ||
|
|
6dd15b2091 | ||
|
|
1c55e4f18b | ||
|
|
6cc36b1728 | ||
|
|
37751cd5c1 | ||
|
|
80d882cd20 | ||
|
|
8bef75e570 | ||
|
|
077eab7005 | ||
|
|
715c70d422 | ||
|
|
a3c23f9d53 | ||
|
|
c1e67a8af0 | ||
|
|
2fdc294293 | ||
|
|
1f49ba069e | ||
|
|
43ef0fc0d0 | ||
|
|
a1a920e536 | ||
|
|
dec33b4331 | ||
|
|
432f290633 | ||
|
|
cf98616a27 | ||
|
|
1a720cf1ad | ||
|
|
557c6c34f2 | ||
|
|
943c76505d | ||
|
|
66eb83e6a7 | ||
|
|
0844427357 | ||
|
|
72e9d9ea2d | ||
|
|
3db7d47746 | ||
|
|
95899336b8 | ||
|
|
f6691c110e | ||
|
|
749722a9df | ||
|
|
265b668cb4 | ||
|
|
0527e6a5c1 | ||
|
|
d989b61d19 | ||
|
|
c1f3e6ff13 | ||
|
|
3c0fff2d82 | ||
|
|
bd599c264f | ||
|
|
0274093c89 | ||
|
|
5845b5bdfd | ||
|
|
3dd0ba6b0b | ||
|
|
e79306f8a3 | ||
|
|
0171b0c2dd | ||
|
|
a963ef53d3 | ||
|
|
344f302c76 | ||
|
|
17ded282b2 | ||
|
|
2ce57df485 | ||
|
|
849289287e | ||
|
|
704fbc2393 | ||
|
|
168fc8bd03 | ||
|
|
c5598d9efe | ||
|
|
f02315726d | ||
|
|
e56dcb59c0 | ||
|
|
c74f350ea2 | ||
|
|
af9d015cf2 | ||
|
|
633e5ac5e4 | ||
|
|
2d0f323854 | ||
|
|
cfe5610ec9 | ||
|
|
55616ac321 | ||
|
|
8f2d1deb38 | ||
|
|
e2763ceb80 | ||
|
|
10462f3140 | ||
|
|
3aef92486b | ||
|
|
c1a9ee0bbb | ||
|
|
398fae82cb | ||
|
|
a16443f961 | ||
|
|
ad4e0e1d27 | ||
|
|
493a2108e9 | ||
|
|
c2c9bb64b1 | ||
|
|
bb1426c0af | ||
|
|
2505768e4d | ||
|
|
23a1b70534 | ||
|
|
b34616ecbc | ||
|
|
0111252f6f | ||
|
|
a561cc5f73 | ||
|
|
519e6f1f79 | ||
|
|
da871b5d3a | ||
|
|
ae3f939d23 | ||
|
|
fb6ede564e | ||
|
|
1c701d4fe8 | ||
|
|
c743cc689d | ||
|
|
8432bec3d6 | ||
|
|
8e72b0b0e4 | ||
|
|
19e4bbc01e | ||
|
|
56a9e412c8 | ||
|
|
4b75623b86 | ||
|
|
e1ea42a997 | ||
|
|
ee37710e57 | ||
|
|
ae99218789 | ||
|
|
9fe8214ca0 | ||
|
|
6ca44a5cc3 | ||
|
|
801abd197b | ||
|
|
970977e469 | ||
|
|
f83674b934 | ||
|
|
57c7dc1b5e |
No files matched your search
@@ -9,7 +9,7 @@
|
|||||||
"rules": {
|
"rules": {
|
||||||
"vue/no-deprecated-html-element-is": 0,
|
"vue/no-deprecated-html-element-is": 0,
|
||||||
"vue/no-mutating-props": 0,
|
"vue/no-mutating-props": 0,
|
||||||
"vue/no-v-html": 0
|
"vue/one-component-per-file": 0
|
||||||
},
|
},
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 2022,
|
"ecmaVersion": 2022,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.2.2
|
ruby-version: 3.3.3
|
||||||
- name: Cache gems
|
- name: Cache gems
|
||||||
uses: actions/cache@v1
|
uses: actions/cache@v1
|
||||||
with:
|
with:
|
||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.2.2
|
ruby-version: 3.3.3
|
||||||
- name: Cache gems
|
- name: Cache gems
|
||||||
uses: actions/cache@v1
|
uses: actions/cache@v1
|
||||||
with:
|
with:
|
||||||
@@ -100,7 +100,7 @@ jobs:
|
|||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.2.2
|
ruby-version: 3.3.3
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -36,3 +36,4 @@ yarn-debug.log*
|
|||||||
/attachments
|
/attachments
|
||||||
/docuseal
|
/docuseal
|
||||||
/ee
|
/ee
|
||||||
|
dump.rdb
|
||||||
+4
-3
@@ -10,7 +10,8 @@ AllCops:
|
|||||||
- node_modules/**/*
|
- node_modules/**/*
|
||||||
- bin/*
|
- bin/*
|
||||||
- vendor/**/*
|
- vendor/**/*
|
||||||
TargetRubyVersion: '3.2'
|
TargetRubyVersion: '3.3'
|
||||||
|
SuggestExtensions: false
|
||||||
|
|
||||||
Metrics/BlockLength:
|
Metrics/BlockLength:
|
||||||
Exclude:
|
Exclude:
|
||||||
@@ -44,7 +45,7 @@ Layout/LineLength:
|
|||||||
AllowedPatterns: ['\A\s*#']
|
AllowedPatterns: ['\A\s*#']
|
||||||
|
|
||||||
Metrics/AbcSize:
|
Metrics/AbcSize:
|
||||||
Max: 40
|
Max: 45
|
||||||
|
|
||||||
Metrics/ModuleLength:
|
Metrics/ModuleLength:
|
||||||
Max: 500
|
Max: 500
|
||||||
@@ -76,5 +77,5 @@ Rails/SkipsModelValidations:
|
|||||||
Rails/ApplicationController:
|
Rails/ApplicationController:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Capybara/ClickLinkOrButtonStyle:
|
Rails/Output:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
+28
-12
@@ -1,10 +1,12 @@
|
|||||||
FROM ruby:3.2.2-alpine3.18 as fonts
|
FROM ruby:3.3.3-alpine as fonts
|
||||||
|
|
||||||
WORKDIR /fonts
|
WORKDIR /fonts
|
||||||
|
|
||||||
RUN apk --no-cache add wget && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt
|
RUN apk --no-cache add fontforge wget && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/v2031/DancingScript-Regular.otf && wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt
|
||||||
|
|
||||||
FROM ruby:3.2.2-alpine3.18 as webpack
|
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.3.3-alpine as webpack
|
||||||
|
|
||||||
ENV RAILS_ENV=production
|
ENV RAILS_ENV=production
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
@@ -21,28 +23,41 @@ RUN yarn install --network-timeout 1000000
|
|||||||
COPY ./bin/shakapacker ./bin/shakapacker
|
COPY ./bin/shakapacker ./bin/shakapacker
|
||||||
COPY ./config/webpack ./config/webpack
|
COPY ./config/webpack ./config/webpack
|
||||||
COPY ./config/shakapacker.yml ./config/shakapacker.yml
|
COPY ./config/shakapacker.yml ./config/shakapacker.yml
|
||||||
COPY ./postcss.config.js ./postcss.config.js ./
|
COPY ./postcss.config.js ./postcss.config.js
|
||||||
COPY ./tailwind.config.js ./tailwind.config.js ./
|
COPY ./tailwind.config.js ./tailwind.config.js
|
||||||
COPY ./tailwind.form.config.js ./tailwind.form.config.js ./
|
COPY ./tailwind.form.config.js ./tailwind.form.config.js
|
||||||
COPY ./tailwind.application.config.js ./tailwind.application.config.js ./
|
COPY ./tailwind.application.config.js ./tailwind.application.config.js
|
||||||
COPY ./app/javascript ./app/javascript
|
COPY ./app/javascript ./app/javascript
|
||||||
COPY ./app/views ./app/views
|
COPY ./app/views ./app/views
|
||||||
|
|
||||||
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
|
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
|
||||||
|
|
||||||
FROM ruby:3.2.2-alpine3.18 as app
|
FROM ruby:3.3.3-alpine as app
|
||||||
|
|
||||||
ENV RAILS_ENV=production
|
ENV RAILS_ENV=production
|
||||||
ENV BUNDLE_WITHOUT="development:test"
|
ENV BUNDLE_WITHOUT="development:test"
|
||||||
ENV PIDFILE=/dev/null
|
ENV LD_PRELOAD=/lib/libgcompat.so.0
|
||||||
|
ENV OPENSSL_CONF=/app/openssl_legacy.cnf
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils vips-heif libc6-compat ttf-freefont && mkdir /fonts
|
RUN apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils redis vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
||||||
|
|
||||||
|
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
|
||||||
|
\n\
|
||||||
|
[provider_sect]\n\
|
||||||
|
default = default_sect\n\
|
||||||
|
legacy = legacy_sect\n\
|
||||||
|
\n\
|
||||||
|
[default_sect]\n\
|
||||||
|
activate = 1\n\
|
||||||
|
\n\
|
||||||
|
[legacy_sect]\n\
|
||||||
|
activate = 1' >> /app/openssl_legacy.cnf
|
||||||
|
|
||||||
COPY ./Gemfile ./Gemfile.lock ./
|
COPY ./Gemfile ./Gemfile.lock ./
|
||||||
|
|
||||||
RUN bundle update --bundler && bundle install && 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 && bundle install && apk del 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
|
||||||
|
|
||||||
COPY ./bin ./bin
|
COPY ./bin ./bin
|
||||||
COPY ./app ./app
|
COPY ./app ./app
|
||||||
@@ -55,6 +70,7 @@ COPY ./tmp ./tmp
|
|||||||
COPY LICENSE README.md Rakefile config.ru .version ./
|
COPY LICENSE README.md Rakefile config.ru .version ./
|
||||||
|
|
||||||
COPY --from=fonts /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
COPY --from=fonts /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
||||||
|
COPY --from=fonts /fonts/FreeSans.ttf /usr/share/fonts/freefont
|
||||||
COPY --from=webpack /app/public/packs ./public/packs
|
COPY --from=webpack /app/public/packs ./public/packs
|
||||||
|
|
||||||
RUN ln -s /fonts /app/public/fonts
|
RUN ln -s /fonts /app/public/fonts
|
||||||
@@ -64,4 +80,4 @@ WORKDIR /data/docuseal
|
|||||||
ENV WORKDIR=/data/docuseal
|
ENV WORKDIR=/data/docuseal
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["/app/bin/rails", "server"]
|
CMD ["/app/bin/bundle", "exec", "puma", "-C", "/app/config/puma.rb", "--dir", "/app"]
|
||||||
@@ -2,13 +2,14 @@
|
|||||||
|
|
||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
ruby '3.2.2'
|
ruby '3.3.3'
|
||||||
|
|
||||||
gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic'
|
gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic'
|
||||||
gem 'aws-sdk-s3', require: false
|
gem 'aws-sdk-s3', require: false
|
||||||
gem 'azure-storage-blob', require: false
|
gem 'azure-storage-blob', require: false
|
||||||
gem 'bootsnap', require: false
|
gem 'bootsnap', require: false
|
||||||
gem 'cancancan'
|
gem 'cancancan'
|
||||||
|
gem 'csv'
|
||||||
gem 'devise'
|
gem 'devise'
|
||||||
gem 'devise-two-factor'
|
gem 'devise-two-factor'
|
||||||
gem 'dotenv', require: false
|
gem 'dotenv', require: false
|
||||||
@@ -23,11 +24,10 @@ gem 'lograge'
|
|||||||
gem 'mysql2', require: false
|
gem 'mysql2', require: false
|
||||||
gem 'oj'
|
gem 'oj'
|
||||||
gem 'pagy'
|
gem 'pagy'
|
||||||
gem 'pdf-reader'
|
|
||||||
gem 'pg', require: false
|
gem 'pg', require: false
|
||||||
gem 'premailer-rails'
|
gem 'premailer-rails'
|
||||||
gem 'pretender'
|
gem 'pretender'
|
||||||
gem 'puma'
|
gem 'puma', require: false
|
||||||
gem 'rack'
|
gem 'rack'
|
||||||
gem 'rails'
|
gem 'rails'
|
||||||
gem 'rails_autolink'
|
gem 'rails_autolink'
|
||||||
@@ -37,8 +37,8 @@ gem 'rqrcode'
|
|||||||
gem 'ruby-vips'
|
gem 'ruby-vips'
|
||||||
gem 'rubyXL'
|
gem 'rubyXL'
|
||||||
gem 'shakapacker'
|
gem 'shakapacker'
|
||||||
gem 'sidekiq', require: ENV.key?('REDIS_URL')
|
gem 'sidekiq'
|
||||||
gem 'sqlite3', require: false
|
gem 'sqlite3', require: false, force_ruby_platform: true
|
||||||
gem 'strip_attributes'
|
gem 'strip_attributes'
|
||||||
gem 'turbo-rails'
|
gem 'turbo-rails'
|
||||||
gem 'twitter_cldr', require: false
|
gem 'twitter_cldr', require: false
|
||||||
|
|||||||
+186
-191
@@ -1,36 +1,35 @@
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
Ascii85 (1.1.0)
|
actioncable (7.1.3.2)
|
||||||
actioncable (7.1.2)
|
actionpack (= 7.1.3.2)
|
||||||
actionpack (= 7.1.2)
|
activesupport (= 7.1.3.2)
|
||||||
activesupport (= 7.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 (7.1.2)
|
actionmailbox (7.1.3.2)
|
||||||
actionpack (= 7.1.2)
|
actionpack (= 7.1.3.2)
|
||||||
activejob (= 7.1.2)
|
activejob (= 7.1.3.2)
|
||||||
activerecord (= 7.1.2)
|
activerecord (= 7.1.3.2)
|
||||||
activestorage (= 7.1.2)
|
activestorage (= 7.1.3.2)
|
||||||
activesupport (= 7.1.2)
|
activesupport (= 7.1.3.2)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
actionmailer (7.1.2)
|
actionmailer (7.1.3.2)
|
||||||
actionpack (= 7.1.2)
|
actionpack (= 7.1.3.2)
|
||||||
actionview (= 7.1.2)
|
actionview (= 7.1.3.2)
|
||||||
activejob (= 7.1.2)
|
activejob (= 7.1.3.2)
|
||||||
activesupport (= 7.1.2)
|
activesupport (= 7.1.3.2)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
actionpack (7.1.2)
|
actionpack (7.1.3.2)
|
||||||
actionview (= 7.1.2)
|
actionview (= 7.1.3.2)
|
||||||
activesupport (= 7.1.2)
|
activesupport (= 7.1.3.2)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
racc
|
racc
|
||||||
rack (>= 2.2.4)
|
rack (>= 2.2.4)
|
||||||
@@ -38,35 +37,35 @@ GEM
|
|||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.6)
|
rails-html-sanitizer (~> 1.6)
|
||||||
actiontext (7.1.2)
|
actiontext (7.1.3.2)
|
||||||
actionpack (= 7.1.2)
|
actionpack (= 7.1.3.2)
|
||||||
activerecord (= 7.1.2)
|
activerecord (= 7.1.3.2)
|
||||||
activestorage (= 7.1.2)
|
activestorage (= 7.1.3.2)
|
||||||
activesupport (= 7.1.2)
|
activesupport (= 7.1.3.2)
|
||||||
globalid (>= 0.6.0)
|
globalid (>= 0.6.0)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (7.1.2)
|
actionview (7.1.3.2)
|
||||||
activesupport (= 7.1.2)
|
activesupport (= 7.1.3.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 (7.1.2)
|
activejob (7.1.3.2)
|
||||||
activesupport (= 7.1.2)
|
activesupport (= 7.1.3.2)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (7.1.2)
|
activemodel (7.1.3.2)
|
||||||
activesupport (= 7.1.2)
|
activesupport (= 7.1.3.2)
|
||||||
activerecord (7.1.2)
|
activerecord (7.1.3.2)
|
||||||
activemodel (= 7.1.2)
|
activemodel (= 7.1.3.2)
|
||||||
activesupport (= 7.1.2)
|
activesupport (= 7.1.3.2)
|
||||||
timeout (>= 0.4.0)
|
timeout (>= 0.4.0)
|
||||||
activestorage (7.1.2)
|
activestorage (7.1.3.2)
|
||||||
actionpack (= 7.1.2)
|
actionpack (= 7.1.3.2)
|
||||||
activejob (= 7.1.2)
|
activejob (= 7.1.3.2)
|
||||||
activerecord (= 7.1.2)
|
activerecord (= 7.1.3.2)
|
||||||
activesupport (= 7.1.2)
|
activesupport (= 7.1.3.2)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
activesupport (7.1.2)
|
activesupport (7.1.3.2)
|
||||||
base64
|
base64
|
||||||
bigdecimal
|
bigdecimal
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
@@ -78,24 +77,23 @@ GEM
|
|||||||
tzinfo (~> 2.0)
|
tzinfo (~> 2.0)
|
||||||
addressable (2.8.6)
|
addressable (2.8.6)
|
||||||
public_suffix (>= 2.0.2, < 6.0)
|
public_suffix (>= 2.0.2, < 6.0)
|
||||||
afm (0.2.2)
|
|
||||||
annotate (3.2.0)
|
annotate (3.2.0)
|
||||||
activerecord (>= 3.2, < 8.0)
|
activerecord (>= 3.2, < 8.0)
|
||||||
rake (>= 10.4, < 14.0)
|
rake (>= 10.4, < 14.0)
|
||||||
arabic-letter-connector (0.1.1)
|
arabic-letter-connector (0.1.1)
|
||||||
ast (2.4.2)
|
ast (2.4.2)
|
||||||
aws-eventstream (1.3.0)
|
aws-eventstream (1.3.0)
|
||||||
aws-partitions (1.874.0)
|
aws-partitions (1.913.0)
|
||||||
aws-sdk-core (3.190.1)
|
aws-sdk-core (3.191.6)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.651.0)
|
aws-partitions (~> 1, >= 1.651.0)
|
||||||
aws-sigv4 (~> 1.8)
|
aws-sigv4 (~> 1.8)
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
aws-sdk-kms (1.75.0)
|
aws-sdk-kms (1.79.0)
|
||||||
aws-sdk-core (~> 3, >= 3.188.0)
|
aws-sdk-core (~> 3, >= 3.191.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.142.0)
|
aws-sdk-s3 (1.146.1)
|
||||||
aws-sdk-core (~> 3, >= 3.189.0)
|
aws-sdk-core (~> 3, >= 3.191.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.8)
|
aws-sigv4 (~> 1.8)
|
||||||
aws-sigv4 (1.8.0)
|
aws-sigv4 (1.8.0)
|
||||||
@@ -110,52 +108,55 @@ GEM
|
|||||||
nokogiri (~> 1, >= 1.10.8)
|
nokogiri (~> 1, >= 1.10.8)
|
||||||
base64 (0.2.0)
|
base64 (0.2.0)
|
||||||
bcrypt (3.1.20)
|
bcrypt (3.1.20)
|
||||||
better_html (2.0.2)
|
better_html (2.1.1)
|
||||||
actionview (>= 6.0)
|
actionview (>= 6.0)
|
||||||
activesupport (>= 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.5)
|
bigdecimal (3.1.8)
|
||||||
bindex (0.8.1)
|
bindex (0.8.1)
|
||||||
bootsnap (1.17.0)
|
bootsnap (1.18.3)
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
builder (3.2.4)
|
builder (3.2.4)
|
||||||
bullet (7.1.4)
|
bullet (7.1.6)
|
||||||
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)
|
||||||
cancancan (3.5.0)
|
cancancan (3.5.0)
|
||||||
capybara (3.39.2)
|
capybara (3.40.0)
|
||||||
addressable
|
addressable
|
||||||
matrix
|
matrix
|
||||||
mini_mime (>= 0.1.3)
|
mini_mime (>= 0.1.3)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.11)
|
||||||
rack (>= 1.6.0)
|
rack (>= 1.6.0)
|
||||||
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)
|
||||||
|
childprocess (5.0.0)
|
||||||
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.2.2)
|
concurrent-ruby (1.2.3)
|
||||||
connection_pool (2.4.1)
|
connection_pool (2.4.1)
|
||||||
crack (0.4.5)
|
crack (1.0.0)
|
||||||
|
bigdecimal
|
||||||
rexml
|
rexml
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
css_parser (1.16.0)
|
css_parser (1.17.1)
|
||||||
addressable
|
addressable
|
||||||
|
csv (3.3.0)
|
||||||
cuprite (0.15)
|
cuprite (0.15)
|
||||||
capybara (~> 3.0)
|
capybara (~> 3.0)
|
||||||
ferrum (~> 0.14.0)
|
ferrum (~> 0.14.0)
|
||||||
date (3.3.4)
|
date (3.3.4)
|
||||||
debug (1.9.1)
|
debug (1.9.2)
|
||||||
irb (~> 1.10)
|
irb (~> 1.10)
|
||||||
reline (>= 0.3.8)
|
reline (>= 0.3.8)
|
||||||
declarative (0.0.20)
|
declarative (0.0.20)
|
||||||
devise (4.9.3)
|
devise (4.9.4)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
orm_adapter (~> 0.1)
|
orm_adapter (~> 0.1)
|
||||||
railties (>= 4.1.0)
|
railties (>= 4.1.0)
|
||||||
@@ -166,13 +167,12 @@ GEM
|
|||||||
devise (~> 4.0)
|
devise (~> 4.0)
|
||||||
railties (~> 7.0)
|
railties (~> 7.0)
|
||||||
rotp (~> 6.0)
|
rotp (~> 6.0)
|
||||||
diff-lcs (1.5.0)
|
diff-lcs (1.5.1)
|
||||||
digest-crc (0.6.5)
|
digest-crc (0.6.5)
|
||||||
rake (>= 12.0.0, < 14.0.0)
|
rake (>= 12.0.0, < 14.0.0)
|
||||||
docile (1.4.0)
|
docile (1.4.0)
|
||||||
dotenv (2.8.1)
|
dotenv (3.1.0)
|
||||||
drb (2.2.0)
|
drb (2.2.1)
|
||||||
ruby2_keywords
|
|
||||||
email_typo (0.2.3)
|
email_typo (0.2.3)
|
||||||
erb_lint (0.5.0)
|
erb_lint (0.5.0)
|
||||||
activesupport
|
activesupport
|
||||||
@@ -182,12 +182,12 @@ GEM
|
|||||||
rubocop
|
rubocop
|
||||||
smart_properties
|
smart_properties
|
||||||
erubi (1.12.0)
|
erubi (1.12.0)
|
||||||
factory_bot (6.4.4)
|
factory_bot (6.4.6)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
factory_bot_rails (6.4.2)
|
factory_bot_rails (6.4.3)
|
||||||
factory_bot (~> 6.4)
|
factory_bot (~> 6.4)
|
||||||
railties (>= 5.0.0)
|
railties (>= 5.0.0)
|
||||||
faker (3.2.2)
|
faker (3.3.1)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
faraday (1.10.3)
|
faraday (1.10.3)
|
||||||
faraday-em_http (~> 1.0)
|
faraday-em_http (~> 1.0)
|
||||||
@@ -225,34 +225,34 @@ GEM
|
|||||||
geom2d (0.4.1)
|
geom2d (0.4.1)
|
||||||
globalid (1.2.1)
|
globalid (1.2.1)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
google-apis-core (0.11.2)
|
google-apis-core (0.14.1)
|
||||||
addressable (~> 2.5, >= 2.5.1)
|
addressable (~> 2.5, >= 2.5.1)
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
googleauth (~> 1.9)
|
||||||
httpclient (>= 2.8.1, < 3.a)
|
httpclient (>= 2.8.1, < 3.a)
|
||||||
mini_mime (~> 1.0)
|
mini_mime (~> 1.0)
|
||||||
representable (~> 3.0)
|
representable (~> 3.0)
|
||||||
retriable (>= 2.0, < 4.a)
|
retriable (>= 2.0, < 4.a)
|
||||||
rexml
|
rexml
|
||||||
webrick
|
google-apis-iamcredentials_v1 (0.20.0)
|
||||||
google-apis-iamcredentials_v1 (0.17.0)
|
google-apis-core (>= 0.14.0, < 2.a)
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
google-apis-storage_v1 (0.37.0)
|
||||||
google-apis-storage_v1 (0.29.0)
|
google-apis-core (>= 0.14.0, < 2.a)
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
google-cloud-core (1.7.0)
|
||||||
google-cloud-core (1.6.1)
|
|
||||||
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.1.0)
|
google-cloud-env (2.1.1)
|
||||||
faraday (>= 1.0, < 3.a)
|
faraday (>= 1.0, < 3.a)
|
||||||
google-cloud-errors (1.3.1)
|
google-cloud-errors (1.4.0)
|
||||||
google-cloud-storage (1.45.0)
|
google-cloud-storage (1.49.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
digest-crc (~> 0.4)
|
digest-crc (~> 0.4)
|
||||||
google-apis-iamcredentials_v1 (~> 0.1)
|
google-apis-core (~> 0.13)
|
||||||
google-apis-storage_v1 (~> 0.29.0)
|
google-apis-iamcredentials_v1 (~> 0.18)
|
||||||
|
google-apis-storage_v1 (~> 0.33)
|
||||||
google-cloud-core (~> 1.6)
|
google-cloud-core (~> 1.6)
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
googleauth (~> 1.9)
|
||||||
mini_mime (~> 1.0)
|
mini_mime (~> 1.0)
|
||||||
googleauth (1.9.1)
|
googleauth (1.11.0)
|
||||||
faraday (>= 1.0, < 3.a)
|
faraday (>= 1.0, < 3.a)
|
||||||
google-cloud-env (~> 2.1)
|
google-cloud-env (~> 2.1)
|
||||||
jwt (>= 1.4, < 3.0)
|
jwt (>= 1.4, < 3.0)
|
||||||
@@ -260,30 +260,31 @@ GEM
|
|||||||
os (>= 0.9, < 2.0)
|
os (>= 0.9, < 2.0)
|
||||||
signet (>= 0.16, < 2.a)
|
signet (>= 0.16, < 2.a)
|
||||||
hashdiff (1.1.0)
|
hashdiff (1.1.0)
|
||||||
hashery (2.1.2)
|
hexapdf (0.42.0)
|
||||||
hexapdf (0.37.1)
|
|
||||||
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)
|
htmlentities (4.3.4)
|
||||||
httpclient (2.8.3)
|
httpclient (2.8.3)
|
||||||
i18n (1.14.1)
|
i18n (1.14.5)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
image_processing (1.12.2)
|
image_processing (1.12.2)
|
||||||
mini_magick (>= 4.9.5, < 5)
|
mini_magick (>= 4.9.5, < 5)
|
||||||
ruby-vips (>= 2.0.17, < 3)
|
ruby-vips (>= 2.0.17, < 3)
|
||||||
io-console (0.7.1)
|
io-console (0.7.2)
|
||||||
irb (1.11.0)
|
irb (1.13.1)
|
||||||
rdoc
|
rdoc (>= 4.0.0)
|
||||||
reline (>= 0.3.8)
|
reline (>= 0.4.2)
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.7.1)
|
json (2.7.2)
|
||||||
jwt (2.7.1)
|
jwt (2.8.1)
|
||||||
|
base64
|
||||||
language_server-protocol (3.17.0.3)
|
language_server-protocol (3.17.0.3)
|
||||||
launchy (2.5.2)
|
launchy (3.0.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
letter_opener (1.8.1)
|
childprocess (~> 5.0)
|
||||||
launchy (>= 2.2, < 3)
|
letter_opener (1.10.0)
|
||||||
|
launchy (>= 2.2, < 4)
|
||||||
letter_opener_web (2.0.0)
|
letter_opener_web (2.0.0)
|
||||||
actionmailer (>= 5.2)
|
actionmailer (>= 5.2)
|
||||||
letter_opener (~> 1.7)
|
letter_opener (~> 1.7)
|
||||||
@@ -302,21 +303,21 @@ GEM
|
|||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
marcel (1.0.2)
|
marcel (1.0.4)
|
||||||
matrix (0.4.2)
|
matrix (0.4.2)
|
||||||
method_source (1.0.0)
|
method_source (1.1.0)
|
||||||
mini_magick (4.12.0)
|
mini_magick (4.12.0)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.5)
|
mini_portile2 (2.8.6)
|
||||||
minitest (5.20.0)
|
minitest (5.23.0)
|
||||||
msgpack (1.7.2)
|
msgpack (1.7.2)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multipart-post (2.3.0)
|
multipart-post (2.4.0)
|
||||||
mutex_m (0.2.0)
|
mutex_m (0.2.0)
|
||||||
mysql2 (0.5.5)
|
mysql2 (0.5.6)
|
||||||
net-http-persistent (4.0.2)
|
net-http-persistent (4.0.2)
|
||||||
connection_pool (~> 2.2)
|
connection_pool (~> 2.2)
|
||||||
net-imap (0.4.9)
|
net-imap (0.4.10)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
net-pop (0.1.2)
|
net-pop (0.1.2)
|
||||||
@@ -325,28 +326,27 @@ GEM
|
|||||||
timeout
|
timeout
|
||||||
net-smtp (0.4.0)
|
net-smtp (0.4.0)
|
||||||
net-protocol
|
net-protocol
|
||||||
nio4r (2.7.0)
|
nio4r (2.7.1)
|
||||||
nokogiri (1.15.5)
|
nokogiri (1.16.5)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.16.5-aarch64-linux)
|
||||||
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.16.5-x86_64-linux)
|
||||||
|
racc (~> 1.4)
|
||||||
oj (3.16.3)
|
oj (3.16.3)
|
||||||
bigdecimal (>= 3.0)
|
bigdecimal (>= 3.0)
|
||||||
openssl (3.2.0)
|
openssl (3.2.0)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
pagy (6.2.0)
|
package_json (0.1.0)
|
||||||
parallel (1.24.0)
|
pagy (8.1.2)
|
||||||
parser (3.2.2.4)
|
parallel (1.25.1)
|
||||||
|
parser (3.3.4.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
pdf-reader (2.12.0)
|
pg (1.5.6)
|
||||||
Ascii85 (~> 1.0)
|
premailer (1.23.0)
|
||||||
afm (~> 0.2.1)
|
|
||||||
hashery (~> 2.0)
|
|
||||||
ruby-rc4
|
|
||||||
ttfunk
|
|
||||||
pg (1.5.4)
|
|
||||||
premailer (1.21.0)
|
|
||||||
addressable
|
addressable
|
||||||
css_parser (>= 1.12.0)
|
css_parser (>= 1.12.0)
|
||||||
htmlentities (>= 4.0.0)
|
htmlentities (>= 4.0.0)
|
||||||
@@ -363,34 +363,34 @@ GEM
|
|||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
psych (5.1.2)
|
psych (5.1.2)
|
||||||
stringio
|
stringio
|
||||||
public_suffix (5.0.4)
|
public_suffix (5.0.5)
|
||||||
puma (6.4.0)
|
puma (6.4.2)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
racc (1.7.3)
|
racc (1.8.0)
|
||||||
rack (2.2.8)
|
rack (3.0.11)
|
||||||
rack-proxy (0.7.7)
|
rack-proxy (0.7.7)
|
||||||
rack
|
rack
|
||||||
rack-session (1.0.2)
|
rack-session (2.0.0)
|
||||||
rack (< 3)
|
rack (>= 3.0.0)
|
||||||
rack-test (2.1.0)
|
rack-test (2.1.0)
|
||||||
rack (>= 1.3)
|
rack (>= 1.3)
|
||||||
rackup (1.0.0)
|
rackup (2.1.0)
|
||||||
rack (< 3)
|
rack (>= 3)
|
||||||
webrick
|
webrick (~> 1.8)
|
||||||
rails (7.1.2)
|
rails (7.1.3.2)
|
||||||
actioncable (= 7.1.2)
|
actioncable (= 7.1.3.2)
|
||||||
actionmailbox (= 7.1.2)
|
actionmailbox (= 7.1.3.2)
|
||||||
actionmailer (= 7.1.2)
|
actionmailer (= 7.1.3.2)
|
||||||
actionpack (= 7.1.2)
|
actionpack (= 7.1.3.2)
|
||||||
actiontext (= 7.1.2)
|
actiontext (= 7.1.3.2)
|
||||||
actionview (= 7.1.2)
|
actionview (= 7.1.3.2)
|
||||||
activejob (= 7.1.2)
|
activejob (= 7.1.3.2)
|
||||||
activemodel (= 7.1.2)
|
activemodel (= 7.1.3.2)
|
||||||
activerecord (= 7.1.2)
|
activerecord (= 7.1.3.2)
|
||||||
activestorage (= 7.1.2)
|
activestorage (= 7.1.3.2)
|
||||||
activesupport (= 7.1.2)
|
activesupport (= 7.1.3.2)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 7.1.2)
|
railties (= 7.1.3.2)
|
||||||
rails-dom-testing (2.2.0)
|
rails-dom-testing (2.2.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
minitest
|
minitest
|
||||||
@@ -398,113 +398,108 @@ GEM
|
|||||||
rails-html-sanitizer (1.6.0)
|
rails-html-sanitizer (1.6.0)
|
||||||
loofah (~> 2.21)
|
loofah (~> 2.21)
|
||||||
nokogiri (~> 1.14)
|
nokogiri (~> 1.14)
|
||||||
rails-i18n (7.0.8)
|
rails-i18n (7.0.9)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
railties (>= 6.0.0, < 8)
|
railties (>= 6.0.0, < 8)
|
||||||
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 (7.1.2)
|
railties (7.1.3.2)
|
||||||
actionpack (= 7.1.2)
|
actionpack (= 7.1.3.2)
|
||||||
activesupport (= 7.1.2)
|
activesupport (= 7.1.3.2)
|
||||||
irb
|
irb
|
||||||
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)
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.1.0)
|
rake (13.2.1)
|
||||||
rdoc (6.6.2)
|
rdoc (6.6.3.1)
|
||||||
psych (>= 4.0.0)
|
psych (>= 4.0.0)
|
||||||
redis-client (0.19.1)
|
redis-client (0.22.0)
|
||||||
connection_pool
|
connection_pool
|
||||||
regexp_parser (2.8.3)
|
regexp_parser (2.9.2)
|
||||||
reline (0.4.1)
|
reline (0.5.7)
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
representable (3.2.0)
|
representable (3.2.0)
|
||||||
declarative (< 0.1.0)
|
declarative (< 0.1.0)
|
||||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||||
uber (< 0.2.0)
|
uber (< 0.2.0)
|
||||||
request_store (1.5.1)
|
request_store (1.6.0)
|
||||||
rack (>= 1.4)
|
rack (>= 1.4)
|
||||||
responders (3.1.1)
|
responders (3.1.1)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
retriable (3.1.2)
|
retriable (3.1.2)
|
||||||
rexml (3.2.6)
|
rexml (3.3.2)
|
||||||
|
strscan
|
||||||
rotp (6.3.0)
|
rotp (6.3.0)
|
||||||
rqrcode (2.2.0)
|
rqrcode (2.2.0)
|
||||||
chunky_png (~> 1.0)
|
chunky_png (~> 1.0)
|
||||||
rqrcode_core (~> 1.0)
|
rqrcode_core (~> 1.0)
|
||||||
rqrcode_core (1.2.0)
|
rqrcode_core (1.2.0)
|
||||||
rspec-core (3.12.2)
|
rspec-core (3.13.0)
|
||||||
rspec-support (~> 3.12.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-expectations (3.12.3)
|
rspec-expectations (3.13.0)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.12.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-mocks (3.12.6)
|
rspec-mocks (3.13.0)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.12.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-rails (6.1.0)
|
rspec-rails (6.1.2)
|
||||||
actionpack (>= 6.1)
|
actionpack (>= 6.1)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
railties (>= 6.1)
|
railties (>= 6.1)
|
||||||
rspec-core (~> 3.12)
|
rspec-core (~> 3.13)
|
||||||
rspec-expectations (~> 3.12)
|
rspec-expectations (~> 3.13)
|
||||||
rspec-mocks (~> 3.12)
|
rspec-mocks (~> 3.13)
|
||||||
rspec-support (~> 3.12)
|
rspec-support (~> 3.13)
|
||||||
rspec-support (3.12.1)
|
rspec-support (3.13.1)
|
||||||
rubocop (1.59.0)
|
rubocop (1.65.0)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (>= 3.17.0)
|
language_server-protocol (>= 3.17.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 3.2.2.4)
|
parser (>= 3.3.0.2)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 1.8, < 3.0)
|
regexp_parser (>= 2.4, < 3.0)
|
||||||
rexml (>= 3.2.5, < 4.0)
|
rexml (>= 3.2.5, < 4.0)
|
||||||
rubocop-ast (>= 1.30.0, < 2.0)
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 2.4.0, < 3.0)
|
unicode-display_width (>= 2.4.0, < 3.0)
|
||||||
rubocop-ast (1.30.0)
|
rubocop-ast (1.31.3)
|
||||||
parser (>= 3.2.1.0)
|
parser (>= 3.3.1.0)
|
||||||
rubocop-capybara (2.19.0)
|
rubocop-performance (1.21.0)
|
||||||
rubocop (~> 1.41)
|
|
||||||
rubocop-factory_bot (2.24.0)
|
|
||||||
rubocop (~> 1.33)
|
|
||||||
rubocop-performance (1.20.1)
|
|
||||||
rubocop (>= 1.48.1, < 2.0)
|
rubocop (>= 1.48.1, < 2.0)
|
||||||
rubocop-ast (>= 1.30.0, < 2.0)
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
rubocop-rails (2.23.1)
|
rubocop-rails (2.24.1)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.33.0, < 2.0)
|
rubocop (>= 1.33.0, < 2.0)
|
||||||
rubocop-ast (>= 1.30.0, < 2.0)
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
rubocop-rspec (2.25.0)
|
rubocop-rspec (3.0.3)
|
||||||
rubocop (~> 1.40)
|
rubocop (~> 1.61)
|
||||||
rubocop-capybara (~> 2.17)
|
|
||||||
rubocop-factory_bot (~> 2.22)
|
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
ruby-rc4 (0.1.5)
|
ruby-vips (2.2.1)
|
||||||
ruby-vips (2.2.0)
|
|
||||||
ffi (~> 1.12)
|
ffi (~> 1.12)
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
rubyXL (3.4.25)
|
rubyXL (3.4.27)
|
||||||
nokogiri (>= 1.10.8)
|
nokogiri (>= 1.10.8)
|
||||||
rubyzip (>= 1.3.0)
|
rubyzip (>= 1.3.0)
|
||||||
rubyzip (2.3.2)
|
rubyzip (2.3.2)
|
||||||
semantic_range (3.0.0)
|
semantic_range (3.0.0)
|
||||||
shakapacker (7.1.0)
|
shakapacker (8.0.0)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
|
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.2.0)
|
sidekiq (7.2.2)
|
||||||
concurrent-ruby (< 2)
|
concurrent-ruby (< 2)
|
||||||
connection_pool (>= 2.3.0)
|
connection_pool (>= 2.3.0)
|
||||||
rack (>= 2.2.4)
|
rack (>= 2.2.4)
|
||||||
redis-client (>= 0.14.0)
|
redis-client (>= 0.19.0)
|
||||||
signet (0.18.0)
|
signet (0.19.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, < 3.0)
|
||||||
@@ -516,17 +511,16 @@ GEM
|
|||||||
simplecov-html (0.12.3)
|
simplecov-html (0.12.3)
|
||||||
simplecov_json_formatter (0.1.4)
|
simplecov_json_formatter (0.1.4)
|
||||||
smart_properties (1.17.0)
|
smart_properties (1.17.0)
|
||||||
sqlite3 (1.7.0)
|
sqlite3 (1.7.3)
|
||||||
mini_portile2 (~> 2.8.0)
|
mini_portile2 (~> 2.8.0)
|
||||||
sqlite3 (1.7.0-arm64-darwin)
|
|
||||||
stringio (3.1.0)
|
stringio (3.1.0)
|
||||||
strip_attributes (1.13.0)
|
strip_attributes (1.13.0)
|
||||||
activemodel (>= 3.0, < 8.0)
|
activemodel (>= 3.0, < 8.0)
|
||||||
thor (1.3.0)
|
strscan (3.1.0)
|
||||||
|
thor (1.3.1)
|
||||||
timeout (0.4.1)
|
timeout (0.4.1)
|
||||||
trailblazer-option (0.1.2)
|
trailblazer-option (0.1.2)
|
||||||
ttfunk (1.7.0)
|
turbo-rails (2.0.5)
|
||||||
turbo-rails (1.5.0)
|
|
||||||
actionpack (>= 6.0.0)
|
actionpack (>= 6.0.0)
|
||||||
activejob (>= 6.0.0)
|
activejob (>= 6.0.0)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
@@ -536,7 +530,7 @@ GEM
|
|||||||
tzinfo
|
tzinfo
|
||||||
tzinfo (2.0.6)
|
tzinfo (2.0.6)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
tzinfo-data (1.2023.4)
|
tzinfo-data (1.2024.1)
|
||||||
tzinfo (>= 1.0.0)
|
tzinfo (>= 1.0.0)
|
||||||
uber (0.1.0)
|
uber (0.1.0)
|
||||||
unicode-display_width (2.5.0)
|
unicode-display_width (2.5.0)
|
||||||
@@ -548,7 +542,7 @@ 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.19.1)
|
webmock (3.23.0)
|
||||||
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)
|
||||||
@@ -558,11 +552,12 @@ GEM
|
|||||||
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.6.12)
|
zeitwerk (2.6.14)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
arm64-darwin-22
|
aarch64-linux
|
||||||
ruby
|
ruby
|
||||||
|
x86_64-linux
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
annotate
|
annotate
|
||||||
@@ -574,6 +569,7 @@ DEPENDENCIES
|
|||||||
bullet
|
bullet
|
||||||
cancancan
|
cancancan
|
||||||
capybara
|
capybara
|
||||||
|
csv
|
||||||
cuprite
|
cuprite
|
||||||
debug
|
debug
|
||||||
devise
|
devise
|
||||||
@@ -594,7 +590,6 @@ DEPENDENCIES
|
|||||||
mysql2
|
mysql2
|
||||||
oj
|
oj
|
||||||
pagy
|
pagy
|
||||||
pdf-reader
|
|
||||||
pg
|
pg
|
||||||
premailer-rails
|
premailer-rails
|
||||||
pretender
|
pretender
|
||||||
@@ -625,7 +620,7 @@ DEPENDENCIES
|
|||||||
webmock
|
webmock
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
ruby 3.2.2p53
|
ruby 3.3.3p89
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.5.3
|
2.5.3
|
||||||
@@ -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
|
||||||
- Signing available in 12 Languages
|
- Signing available in 13 languages
|
||||||
- API and Webhooks for integrations
|
- API and Webhooks for integrations
|
||||||
- Easy to deploy in minutes
|
- Easy to deploy in minutes
|
||||||
|
|
||||||
@@ -51,11 +51,13 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
- User roles
|
- User roles
|
||||||
- Automated reminders
|
- Automated reminders
|
||||||
- Invitation and identify verification via SMS
|
- Invitation and identify verification via SMS
|
||||||
|
- Conditional fields and formulas
|
||||||
|
- Bulk send with CSV, XLSX spreadsheet import
|
||||||
- SSO / SAML
|
- SSO / SAML
|
||||||
- Template creation with HTML API ([Guide](https://www.docuseal.co/guides/create-pdf-document-fillable-form-with-html-api))
|
- Template creation with HTML API ([Guide](https://www.docuseal.co/guides/create-pdf-document-fillable-form-with-html-api))
|
||||||
- Template creation with PDF or DOCX and field tags API ([Guide](https://www.docuseal.co/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form))
|
- Template creation with PDF or DOCX and field tags API ([Guide](https://www.docuseal.co/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form))
|
||||||
- Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue) or [JavaScript](https://www.docuseal.co/docs/embedded))
|
- Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded))
|
||||||
- Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue) or [JavaScript](https://www.docuseal.co/docs/embedded))
|
- Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded))
|
||||||
- [Learn more](https://www.docuseal.co/pricing)
|
- [Learn more](https://www.docuseal.co/pricing)
|
||||||
|
|
||||||
## Deploy
|
## Deploy
|
||||||
@@ -65,9 +67,6 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
| [<img alt="Deploy on Heroku" src="https://www.herokucdn.com/deploy/button.svg" height="40">](https://heroku.com/deploy?template=https://github.com/docusealco/docuseal-heroku) | [<img alt="Deploy on Railway" src="https://railway.app/button.svg" height="40">](https://railway.app/template/IGoDnc?referralCode=ruU7JR)|
|
| [<img alt="Deploy on Heroku" src="https://www.herokucdn.com/deploy/button.svg" height="40">](https://heroku.com/deploy?template=https://github.com/docusealco/docuseal-heroku) | [<img alt="Deploy on Railway" src="https://railway.app/button.svg" height="40">](https://railway.app/template/IGoDnc?referralCode=ruU7JR)|
|
||||||
|**DigitalOcean**|**Render**|
|
|**DigitalOcean**|**Render**|
|
||||||
| [<img alt="Deploy on DigitalOcean" src="https://www.deploytodo.com/do-btn-blue.svg" height="40">](https://cloud.digitalocean.com/apps/new?repo=https://github.com/docusealco/docuseal-digitalocean/tree/master&refcode=421d50f53990) | [<img alt="Deploy to Render" src="https://render.com/images/deploy-to-render-button.svg" height="40">](https://render.com/deploy?repo=https://github.com/docusealco/docuseal-render)
|
| [<img alt="Deploy on DigitalOcean" src="https://www.deploytodo.com/do-btn-blue.svg" height="40">](https://cloud.digitalocean.com/apps/new?repo=https://github.com/docusealco/docuseal-digitalocean/tree/master&refcode=421d50f53990) | [<img alt="Deploy to Render" src="https://render.com/images/deploy-to-render-button.svg" height="40">](https://render.com/deploy?repo=https://github.com/docusealco/docuseal-render)
|
||||||
|**Koyeb**|**Elestio**|
|
|
||||||
| [<img alt="Deploy on Koyeb" src="https://www.koyeb.com/static/images/deploy/button.svg" height="40">](https://app.koyeb.com/deploy?name=docuseal&type=docker&image=docker.io/docuseal/docuseal&env[PORT]=8000&env[DATABASE_URL]=CHANGE_ME&env[SECRET_KEY_BASE]=CHANGE_ME&ports=8000;http;/) | [<img alt="Deploy on Elestio" src="https://pub-da36157c854648669813f3f76c526c2b.r2.dev/deploy-on-elestio-black.png">](https://dash.elest.io/deploy?soft=DocuSeal&id=339) |
|
|
||||||
|
|
||||||
|
|
||||||
#### Docker
|
#### Docker
|
||||||
|
|
||||||
@@ -100,3 +99,9 @@ At DocuSeal we have expertise and technologies to make documents creation, filli
|
|||||||
|
|
||||||
Distributed under the AGPLv3 License. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) for more information.
|
Distributed under the AGPLv3 License. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) for more information.
|
||||||
Unless otherwise noted, all files © 2023 DocuSeal LLC.
|
Unless otherwise noted, all files © 2023 DocuSeal LLC.
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
- [Signature Maker](https://www.docuseal.co/online-signature)
|
||||||
|
- [Sign Document Online](https://www.docuseal.co/sign-documents-online)
|
||||||
|
- [Fill PDF Online](https://www.docuseal.co/fill-pdf)
|
||||||
@@ -3,3 +3,12 @@
|
|||||||
If you come across any security concern or vulnarability, please report the information via email to security@docuseal.co instead of opening a GitHub issue. We will promptly respond and will collaborate with you to validate the issue, and resolve it ASAP.
|
If you come across any security concern or vulnarability, please report the information via email to security@docuseal.co instead of opening a GitHub issue. We will promptly respond and will collaborate with you to validate the issue, and resolve it ASAP.
|
||||||
|
|
||||||
**We have a bug bounty program to reward security researchers.**
|
**We have a bug bounty program to reward security researchers.**
|
||||||
|
|
||||||
|
Out of scope vulnerabilities:
|
||||||
|
|
||||||
|
- CSRF
|
||||||
|
- DNSSEC, CAA, CSP headers
|
||||||
|
- DNS or email security related
|
||||||
|
- Rate Limiting
|
||||||
|
|
||||||
|
Note: We reserve the right to classify any reported vulnerability as out of scope for the bug bounty program.
|
||||||
@@ -8,9 +8,17 @@ class AccountConfigsController < ApplicationController
|
|||||||
AccountConfig::ALLOW_TYPED_SIGNATURE,
|
AccountConfig::ALLOW_TYPED_SIGNATURE,
|
||||||
AccountConfig::FORCE_MFA,
|
AccountConfig::FORCE_MFA,
|
||||||
AccountConfig::ALLOW_TO_RESUBMIT,
|
AccountConfig::ALLOW_TO_RESUBMIT,
|
||||||
AccountConfig::ESIGNING_PREFERENCE_KEY
|
AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
|
||||||
|
AccountConfig::ESIGNING_PREFERENCE_KEY,
|
||||||
|
AccountConfig::FORM_WITH_CONFETTI_KEY,
|
||||||
|
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
|
||||||
|
AccountConfig::FORCE_SSO_AUTH_KEY,
|
||||||
|
AccountConfig::FLATTEN_RESULT_PDF_KEY,
|
||||||
|
AccountConfig::WITH_SIGNATURE_ID
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
|
InvalidKey = Class.new(StandardError)
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@account_config.update!(account_config_params)
|
@account_config.update!(account_config_params)
|
||||||
|
|
||||||
@@ -20,7 +28,7 @@ class AccountConfigsController < ApplicationController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def load_account_config
|
def load_account_config
|
||||||
return head :not_found unless ALLOWED_KEYS.include?(account_config_params[:key])
|
raise InvalidKey unless ALLOWED_KEYS.include?(account_config_params[:key])
|
||||||
|
|
||||||
@account_config =
|
@account_config =
|
||||||
AccountConfig.find_or_initialize_by(account: current_account, key: account_config_params[:key])
|
AccountConfig.find_or_initialize_by(account: current_account, key: account_config_params[:key])
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ class AccountsController < ApplicationController
|
|||||||
'fr-FR' => 'French (France)',
|
'fr-FR' => 'French (France)',
|
||||||
'es-ES' => 'Spanish (Spain)',
|
'es-ES' => 'Spanish (Spain)',
|
||||||
'pt-PT' => 'Portuguese (Portugal)',
|
'pt-PT' => 'Portuguese (Portugal)',
|
||||||
'de-DE' => 'German (Germany)'
|
'de-DE' => 'German (Germany)',
|
||||||
|
'it-IT' => 'Italian (Italy)'
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
before_action :load_account
|
before_action :load_account
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ module Api
|
|||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
before_action :set_cors_headers
|
before_action :set_cors_headers
|
||||||
|
before_action :set_noindex_headers
|
||||||
|
|
||||||
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 != 'blob' || (exp && exp < Time.current.to_i)
|
if blob_uuid.blank? || (purp.present? && purp != 'blob') || (exp && exp < Time.current.to_i)
|
||||||
Rollbar.error('Blob not found') if defined?(Rollbar)
|
Rollbar.error('Blob not found') if defined?(Rollbar)
|
||||||
|
|
||||||
return head :not_found
|
return head :not_found
|
||||||
@@ -20,6 +21,8 @@ module Api
|
|||||||
|
|
||||||
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
|
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
|
||||||
|
|
||||||
|
authorization_check!(blob) if exp.blank?
|
||||||
|
|
||||||
if request.headers['Range'].present?
|
if request.headers['Range'].present?
|
||||||
send_blob_byte_range_data blob, request.headers['Range']
|
send_blob_byte_range_data blob, request.headers['Range']
|
||||||
else
|
else
|
||||||
@@ -31,5 +34,22 @@ module Api
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def authorization_check!(blob)
|
||||||
|
is_authorized =
|
||||||
|
blob.attachments.all? do |a|
|
||||||
|
a.name.in?(%w[logo preview_images]) ||
|
||||||
|
(current_user && a.record.account.id == current_user.account_id) ||
|
||||||
|
!a.record.account.account_configs.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value
|
||||||
|
end
|
||||||
|
|
||||||
|
return if is_authorized
|
||||||
|
|
||||||
|
Rollbar.error('Blob aunauthorized') if defined?(Rollbar)
|
||||||
|
|
||||||
|
raise CanCan::AccessDenied
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -7,6 +7,9 @@ module Api
|
|||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
|
before_action :set_cors_headers
|
||||||
|
before_action :set_noindex_headers
|
||||||
|
|
||||||
# rubocop:disable Metrics
|
# rubocop:disable Metrics
|
||||||
def show
|
def show
|
||||||
Rollbar.info('Blob legacy') if defined?(Rollbar)
|
Rollbar.info('Blob legacy') if defined?(Rollbar)
|
||||||
|
|||||||
@@ -19,10 +19,14 @@ module Api
|
|||||||
render json: { error: e.message }, status: :unprocessable_entity
|
render json: { error: e.message }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
|
rescue_from RateLimit::LimitApproached do |e|
|
||||||
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
|
render json: { error: 'Too many requests' }, status: :too_many_requests
|
||||||
|
end
|
||||||
|
|
||||||
if Rails.env.production?
|
if Rails.env.production?
|
||||||
rescue_from CanCan::AccessDenied do |e|
|
rescue_from CanCan::AccessDenied do |e|
|
||||||
Rollbar.warning(e) if defined?(Rollbar)
|
|
||||||
|
|
||||||
render json: { error: e.message }, status: :forbidden
|
render json: { error: e.message }, status: :forbidden
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -35,31 +39,42 @@ module Api
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def paginate(relation)
|
def paginate(relation, field: :id)
|
||||||
result = relation.order(id: :desc)
|
result = relation.order(field => :desc)
|
||||||
.limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min)
|
.limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min)
|
||||||
|
|
||||||
result = result.where(relation.arel_table[:id].lt(params[:after])) if params[:after].present?
|
if field == :id
|
||||||
result = result.where(relation.arel_table[:id].gt(params[:before])) if params[:before].present?
|
result = result.where(id: ...params[:after].to_i) if params[:after].present?
|
||||||
|
result = result.where(id: (params[:before].to_i + 1)...) if params[:before].present?
|
||||||
|
else
|
||||||
|
result = result.where(field => ...params[:after]) if params[:after].present?
|
||||||
|
result = result.where(field => (params[:before] + 1)...) if params[:before].present?
|
||||||
|
end
|
||||||
|
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
def authenticate_user!
|
def authenticate_user!
|
||||||
@current_user ||=
|
|
||||||
if request.headers['X-Auth-Token'].present?
|
|
||||||
sha256 = Digest::SHA256.hexdigest(request.headers['X-Auth-Token'])
|
|
||||||
|
|
||||||
User.joins(:access_token).active.find_by(access_token: { sha256: })
|
|
||||||
end
|
|
||||||
|
|
||||||
render json: { error: 'Not authenticated' }, status: :unauthorized unless current_user
|
render json: { error: 'Not authenticated' }, status: :unauthorized unless current_user
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def current_user
|
||||||
|
super || @current_user ||=
|
||||||
|
if request.headers['X-Auth-Token'].present?
|
||||||
|
sha256 = Digest::SHA256.hexdigest(request.headers['X-Auth-Token'])
|
||||||
|
|
||||||
|
User.joins(:access_token).active.find_by(access_token: { sha256: })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def current_account
|
def current_account
|
||||||
current_user&.account
|
current_user&.account
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_noindex_headers
|
||||||
|
headers['X-Robots-Tag'] = 'noindex'
|
||||||
|
end
|
||||||
|
|
||||||
def set_cors_headers
|
def set_cors_headers
|
||||||
headers['Access-Control-Allow-Origin'] = '*'
|
headers['Access-Control-Allow-Origin'] = '*'
|
||||||
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
|
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
|
||||||
|
|||||||
@@ -1,16 +1,37 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Api
|
module Api
|
||||||
class AttachmentsController < ApiBaseController
|
class AttachmentsController < ActionController::API
|
||||||
skip_before_action :authenticate_user!
|
include ActionController::Cookies
|
||||||
skip_authorization_check
|
include ActiveStorage::SetCurrent
|
||||||
|
|
||||||
|
COOKIE_STORE_LIMIT = 10
|
||||||
|
|
||||||
def create
|
def create
|
||||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
attachment = Submitters.create_attachment!(submitter, params)
|
attachment = Submitters.create_attachment!(submitter, params)
|
||||||
|
|
||||||
render json: attachment.as_json(only: %i[uuid], methods: %i[url filename content_type])
|
if params[:remember_signature] == 'true' && submitter.email.present?
|
||||||
|
cookies.encrypted[:signature_uuids] = build_new_cookie_signatures_json(submitter, attachment)
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_new_cookie_signatures_json(submitter, attachment)
|
||||||
|
values =
|
||||||
|
begin
|
||||||
|
JSON.parse(cookies.encrypted[:signature_uuids].presence || '{}')
|
||||||
|
rescue JSON::ParserError
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
|
||||||
|
values[submitter.email] = attachment.uuid
|
||||||
|
|
||||||
|
values = values.to_a.last(COOKIE_STORE_LIMIT).to_h if values.size > COOKIE_STORE_LIMIT
|
||||||
|
|
||||||
|
values.to_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api
|
||||||
|
class FormEventsController < ApiBaseController
|
||||||
|
load_and_authorize_resource :submitter, parent: false
|
||||||
|
|
||||||
|
def index
|
||||||
|
submitters = @submitters.where.not(completed_at: nil)
|
||||||
|
|
||||||
|
params[:after] = Time.zone.at(params[:after].to_i) if params[:after].present?
|
||||||
|
params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present?
|
||||||
|
|
||||||
|
submitters = paginate(
|
||||||
|
submitters.preload(template: :folder, submission: [:submitters, { audit_trail_attachment: :blob }],
|
||||||
|
documents_attachments: :blob, attachments_attachments: :blob),
|
||||||
|
field: :completed_at
|
||||||
|
)
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
data: submitters.map do |s|
|
||||||
|
{
|
||||||
|
event_type: 'form.completed',
|
||||||
|
timestamp: s.completed_at,
|
||||||
|
data: Submitters::SerializeForWebhook.call(s)
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
pagination: {
|
||||||
|
count: submitters.size,
|
||||||
|
next: submitters.last&.completed_at&.to_i,
|
||||||
|
prev: submitters.first&.completed_at&.to_i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -17,11 +17,12 @@ module Api
|
|||||||
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] })
|
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] })
|
||||||
end
|
end
|
||||||
|
|
||||||
submissions = paginate(submissions.preload(:created_by_user, :template, :submitters,
|
submissions = paginate(submissions.preload(:created_by_user, :submitters,
|
||||||
|
template: :folder,
|
||||||
audit_trail_attachment: :blob))
|
audit_trail_attachment: :blob))
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
data: submissions.as_json(serialize_params),
|
data: submissions.as_json(Submissions::SerializeForApi::SERIALIZE_PARAMS),
|
||||||
pagination: {
|
pagination: {
|
||||||
count: submissions.size,
|
count: submissions.size,
|
||||||
next: submissions.last&.id,
|
next: submissions.last&.id,
|
||||||
@@ -43,33 +44,11 @@ module Api
|
|||||||
@submission.audit_trail_attachment = Submissions::GenerateAuditTrail.call(@submission)
|
@submission.audit_trail_attachment = Submissions::GenerateAuditTrail.call(@submission)
|
||||||
end
|
end
|
||||||
|
|
||||||
serialized_submitters = submitters.map { |submitter| Submitters::SerializeForApi.call(submitter) }
|
render json: Submissions::SerializeForApi.call(@submission, submitters, params)
|
||||||
|
|
||||||
json = @submission.as_json(
|
|
||||||
serialize_params.deep_merge(
|
|
||||||
include: { submission_events: { only: %i[id submitter_id event_type event_timestamp] } }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if submitters.all?(&:completed_at?)
|
|
||||||
last_submitter = submitters.max_by(&:completed_at)
|
|
||||||
|
|
||||||
json[:documents] = serialized_submitters.find { |e| e['id'] == last_submitter.id }['documents']
|
|
||||||
json[:status] = 'completed'
|
|
||||||
json[:completed_at] = last_submitter.completed_at
|
|
||||||
else
|
|
||||||
json[:documents] = []
|
|
||||||
json[:status] = 'pending'
|
|
||||||
json[:completed_at] = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
json[:submitters] = serialized_submitters
|
|
||||||
|
|
||||||
render json:
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
Params::SubmissionCreateValidator.call(params, dry_run: !Docuseal.multitenant?)
|
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_entity if @template.nil?
|
||||||
|
|
||||||
@@ -84,15 +63,19 @@ module Api
|
|||||||
|
|
||||||
submissions = create_submissions(@template, params)
|
submissions = create_submissions(@template, params)
|
||||||
|
|
||||||
|
submissions.each do |submission|
|
||||||
|
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id })
|
||||||
|
end
|
||||||
|
|
||||||
Submissions.send_signature_requests(submissions)
|
Submissions.send_signature_requests(submissions)
|
||||||
|
|
||||||
submissions.each do |submission|
|
submissions.each do |submission|
|
||||||
if submission.submitters.all?(&:completed_at?) && submission.submitters.last
|
if submission.submitters.all?(&:completed_at?) && submission.submitters.last
|
||||||
ProcessSubmitterCompletionJob.perform_later(submission.submitters.last)
|
ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => submission.submitters.last.id })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: submissions.flat_map(&:submitters)
|
render json: build_create_json(submissions)
|
||||||
rescue Submitters::NormalizeValues::BaseError => e
|
rescue Submitters::NormalizeValues::BaseError => e
|
||||||
Rollbar.warning(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
@@ -100,13 +83,31 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@submission.update!(archived_at: Time.current)
|
if params[:permanently] == 'true'
|
||||||
|
@submission.destroy!
|
||||||
|
else
|
||||||
|
@submission.update!(archived_at: Time.current)
|
||||||
|
|
||||||
render json: @submission.as_json(only: %i[id], methods: %i[archived_at])
|
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: @submission.as_json(only: %i[id archived_at])
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def build_create_json(submissions)
|
||||||
|
json = submissions.flat_map do |submission|
|
||||||
|
submission.submitters.map do |s|
|
||||||
|
Submitters::SerializeForApi.call(s, with_documents: false, with_urls: true, params:)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
json = { submitters: json } if request.path.ends_with?('/init')
|
||||||
|
|
||||||
|
json
|
||||||
|
end
|
||||||
|
|
||||||
def create_submissions(template, params)
|
def create_submissions(template, params)
|
||||||
is_send_email = !params[:send_email].in?(['false', false])
|
is_send_email = !params[:send_email].in?(['false', false])
|
||||||
|
|
||||||
@@ -126,7 +127,6 @@ module Api
|
|||||||
template:,
|
template:,
|
||||||
user: current_user,
|
user: current_user,
|
||||||
source: :api,
|
source: :api,
|
||||||
mark_as_sent: is_send_email,
|
|
||||||
submitters_order: params[:submitters_order] || params[:order] || 'preserved',
|
submitters_order: params[:submitters_order] || params[:order] || 'preserved',
|
||||||
submissions_attrs:,
|
submissions_attrs:,
|
||||||
params:
|
params:
|
||||||
@@ -144,31 +144,17 @@ module Api
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def serialize_params
|
|
||||||
{
|
|
||||||
only: %i[id source submitters_order created_at updated_at archived_at],
|
|
||||||
methods: %i[audit_log_url],
|
|
||||||
include: {
|
|
||||||
submitters: { only: %i[id slug uuid name email phone
|
|
||||||
completed_at opened_at sent_at
|
|
||||||
created_at updated_at external_id metadata],
|
|
||||||
methods: %i[status application_key] },
|
|
||||||
template: { only: %i[id name created_at updated_at] },
|
|
||||||
created_by_user: { only: %i[id email first_name last_name] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def submissions_params
|
def submissions_params
|
||||||
permitted_attrs = [
|
permitted_attrs = [
|
||||||
:send_email, :send_sms, :bcc_completed, :completed_redirect_url,
|
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
|
||||||
{
|
{
|
||||||
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,
|
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
|
||||||
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
||||||
fields: [%i[name default_value title description
|
fields: [:name, :uuid, :default_value, :value, :title, :description,
|
||||||
readonly validation_pattern invalid_message]] }]]
|
:readonly, :redacted, :validation_pattern, :invalid_message,
|
||||||
|
{ default_value: [], value: [], preferences: {} }] }]]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ module Api
|
|||||||
|
|
||||||
SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request)
|
SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request)
|
||||||
|
|
||||||
SendFormViewedWebhookRequestJob.perform_later(submitter)
|
SendFormViewedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id })
|
||||||
|
|
||||||
render json: {}
|
render json: {}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Api
|
|
||||||
class SubmittersAutocompleteController < ApiBaseController
|
|
||||||
load_and_authorize_resource :submitter, parent: false
|
|
||||||
|
|
||||||
SELECT_COLUMNS = %w[email phone name].freeze
|
|
||||||
LIMIT = 100
|
|
||||||
|
|
||||||
def index
|
|
||||||
submitters = search_submitters(@submitters)
|
|
||||||
|
|
||||||
values = submitters.limit(LIMIT).group(SELECT_COLUMNS.join(', ')).pluck(SELECT_COLUMNS.join(', '))
|
|
||||||
|
|
||||||
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
|
|
||||||
attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present?
|
|
||||||
|
|
||||||
render json: attrs
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def search_submitters(submitters)
|
|
||||||
if SELECT_COLUMNS.include?(params[:field])
|
|
||||||
column = Submitter.arel_table[params[:field].to_sym]
|
|
||||||
|
|
||||||
term = "%#{params[:q].downcase}%"
|
|
||||||
|
|
||||||
submitters.where(column.lower.matches(term))
|
|
||||||
else
|
|
||||||
Submitters.search(submitters, params[:q])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -11,13 +11,21 @@ module Api
|
|||||||
submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present?
|
submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present?
|
||||||
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
|
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
|
||||||
|
|
||||||
|
if params[:template_id].present?
|
||||||
|
submitters = submitters.joins(:submission).where(submission: { template_id: params[:template_id] })
|
||||||
|
end
|
||||||
|
|
||||||
|
submitters = maybe_filder_by_completed_at(submitters, params)
|
||||||
|
|
||||||
submitters = paginate(
|
submitters = paginate(
|
||||||
submitters.preload(:template, :submission, :submission_events,
|
submitters.preload(:template, :submission, :submission_events,
|
||||||
documents_attachments: :blob, attachments_attachments: :blob)
|
documents_attachments: :blob, attachments_attachments: :blob)
|
||||||
)
|
)
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
data: submitters.map { |s| Submitters::SerializeForApi.call(s, with_template: true, with_events: true) },
|
data: submitters.map do |s|
|
||||||
|
Submitters::SerializeForApi.call(s, with_template: true, with_events: true, params:)
|
||||||
|
end,
|
||||||
pagination: {
|
pagination: {
|
||||||
count: submitters.size,
|
count: submitters.size,
|
||||||
next: submitters.last&.id,
|
next: submitters.last&.id,
|
||||||
@@ -29,7 +37,7 @@ module Api
|
|||||||
def show
|
def show
|
||||||
Submissions::EnsureResultGenerated.call(@submitter) if @submitter.completed_at?
|
Submissions::EnsureResultGenerated.call(@submitter) if @submitter.completed_at?
|
||||||
|
|
||||||
render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true)
|
render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true, params:)
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@@ -60,42 +68,97 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
if @submitter.completed_at?
|
if @submitter.completed_at?
|
||||||
ProcessSubmitterCompletionJob.perform_later(@submitter)
|
ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => @submitter.id })
|
||||||
elsif normalized_params[:send_email] || normalized_params[:send_sms]
|
elsif normalized_params[:send_email] || normalized_params[:send_sms]
|
||||||
Submitters.send_signature_requests([@submitter])
|
Submitters.send_signature_requests([@submitter])
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: Submitters::SerializeForApi.call(@submitter, with_template: false, with_events: false)
|
render json: Submitters::SerializeForApi.call(@submitter, with_template: false,
|
||||||
|
with_urls: true,
|
||||||
|
with_events: false,
|
||||||
|
params:)
|
||||||
end
|
end
|
||||||
|
|
||||||
def submitter_params
|
def submitter_params
|
||||||
submitter_params = params.key?(:submitter) ? params.require(:submitter) : params
|
submitter_params = params.key?(:submitter) ? params.require(:submitter) : params
|
||||||
|
|
||||||
submitter_params.permit(
|
submitter_params.permit(
|
||||||
:send_email, :send_sms, :uuid, :name, :email, :role,
|
:send_email, :send_sms, :reply_to, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||||
:completed, :phone, :application_key, :external_id,
|
:completed, :phone, :application_key, :external_id, :go_to_last,
|
||||||
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
||||||
fields: [%i[name default_value readonly validation_pattern invalid_message]] }
|
fields: [[:name, :uuid, :default_value, :value,
|
||||||
|
:readonly, :redacted, :validation_pattern, :invalid_message,
|
||||||
|
{ default_value: [], value: [], preferences: {} }]] }
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def maybe_filder_by_completed_at(submitters, params)
|
||||||
|
if params[:completed_after].present?
|
||||||
|
submitters = submitters.where(completed_at: Time.zone.parse(params[:completed_after])..)
|
||||||
|
end
|
||||||
|
|
||||||
|
if params[:completed_before].present?
|
||||||
|
submitters = submitters.where(completed_at: ..Time.zone.parse(params[:completed_before]))
|
||||||
|
end
|
||||||
|
|
||||||
|
submitters
|
||||||
|
end
|
||||||
|
|
||||||
def assign_submitter_attrs(submitter, attrs)
|
def assign_submitter_attrs(submitter, attrs)
|
||||||
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
|
values = attrs[:values]&.to_unsafe_h || {}
|
||||||
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '') if attrs.key?(:phone)
|
|
||||||
submitter.values = submitter.values.merge(attrs[:values].to_unsafe_h) if attrs[:values].present?
|
|
||||||
submitter.completed_at = attrs[:completed] ? Time.current : submitter.completed_at
|
|
||||||
submitter.external_id = attrs[:application_key] if attrs.key?(:application_key)
|
|
||||||
submitter.external_id = attrs[:external_id] if attrs.key?(:external_id)
|
|
||||||
submitter.metadata = attrs[:metadata] if attrs.key?(:metadata)
|
|
||||||
|
|
||||||
assign_submission_fields(submitter.submission)
|
assign_submission_fields(submitter.submission)
|
||||||
|
|
||||||
|
phone_field_uuid = submitter.submission.template_fields.find do |f|
|
||||||
|
values[f['uuid']].present? && f['type'] == 'phone'
|
||||||
|
end&.dig('uuid')
|
||||||
|
|
||||||
|
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
|
||||||
|
|
||||||
|
if attrs.key?(:phone)
|
||||||
|
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '')
|
||||||
|
elsif values[phone_field_uuid].present?
|
||||||
|
submitter.phone = values[phone_field_uuid].to_s.gsub(/[^0-9+]/, '')
|
||||||
|
end
|
||||||
|
|
||||||
|
values = values.except(phone_field_uuid)
|
||||||
|
|
||||||
|
submitter.values = submitter.values.merge(values) if values.present?
|
||||||
|
submitter.metadata = attrs[:metadata] if attrs.key?(:metadata)
|
||||||
|
|
||||||
|
maybe_assign_completed_attributes(submitter, attrs)
|
||||||
|
|
||||||
|
assign_external_id(submitter, attrs)
|
||||||
assign_preferences(submitter, attrs)
|
assign_preferences(submitter, attrs)
|
||||||
|
|
||||||
submitter
|
submitter
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def maybe_assign_completed_attributes(submitter, attrs)
|
||||||
|
submitter.completed_at = attrs[:completed] ? Time.current : submitter.completed_at
|
||||||
|
|
||||||
|
if attrs[:completed]
|
||||||
|
submitter.values = Submitters::SubmitValues.merge_default_values(submitter)
|
||||||
|
submitter.values = Submitters::SubmitValues.merge_formula_values(submitter)
|
||||||
|
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
|
||||||
|
|
||||||
|
submitter.values = submitter.values.transform_values do |v|
|
||||||
|
v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
submitter
|
||||||
|
end
|
||||||
|
|
||||||
|
def assign_external_id(submitter, attrs)
|
||||||
|
submitter.external_id = attrs[:application_key] if attrs.key?(:application_key)
|
||||||
|
submitter.external_id = attrs[:external_id] if attrs.key?(:external_id)
|
||||||
|
|
||||||
|
submitter
|
||||||
|
end
|
||||||
|
|
||||||
def assign_submission_fields(submission)
|
def assign_submission_fields(submission)
|
||||||
submission.template_fields ||= submission.template.fields
|
submission.template_fields ||= submission.template.fields
|
||||||
submission.template_schema ||= submission.template.schema
|
submission.template_schema ||= submission.template.schema
|
||||||
@@ -111,6 +174,14 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms')
|
submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms')
|
||||||
|
submitter.preferences['reply_to'] = submitter_preferences['reply_to'] if submitter_preferences.key?('reply_to')
|
||||||
|
if submitter_preferences.key?('go_to_last')
|
||||||
|
submitter.preferences['go_to_last'] = submitter_preferences['go_to_last']
|
||||||
|
end
|
||||||
|
|
||||||
|
if submitter_preferences.key?('completed_redirect_url')
|
||||||
|
submitter.preferences['completed_redirect_url'] = submitter_preferences['completed_redirect_url']
|
||||||
|
end
|
||||||
|
|
||||||
return unless submitter_preferences.key?('email_message_uuid')
|
return unless submitter_preferences.key?('email_message_uuid')
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Api
|
|
||||||
class TemplateFoldersAutocompleteController < ApiBaseController
|
|
||||||
load_and_authorize_resource :template_folder, parent: false
|
|
||||||
|
|
||||||
LIMIT = 100
|
|
||||||
|
|
||||||
def index
|
|
||||||
template_folders = @template_folders.joins(:templates).where(templates: { archived_at: nil }).distinct
|
|
||||||
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
|
|
||||||
|
|
||||||
render json: template_folders.as_json(only: %i[name archived_at])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -7,6 +7,11 @@ module Api
|
|||||||
def create
|
def create
|
||||||
authorize!(:manage, @template)
|
authorize!(:manage, @template)
|
||||||
|
|
||||||
|
ActiveRecord::Associations::Preloader.new(
|
||||||
|
records: [@template],
|
||||||
|
associations: [schema_documents: :preview_images_attachments]
|
||||||
|
).call
|
||||||
|
|
||||||
cloned_template = Templates::Clone.call(
|
cloned_template = Templates::Clone.call(
|
||||||
@template,
|
@template,
|
||||||
author: current_user,
|
author: current_user,
|
||||||
@@ -18,18 +23,11 @@ module Api
|
|||||||
cloned_template.source = :api
|
cloned_template.source = :api
|
||||||
cloned_template.save!
|
cloned_template.save!
|
||||||
|
|
||||||
Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
|
schema_documents = Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
|
||||||
|
|
||||||
render json: cloned_template.as_json(serialize_params)
|
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id)
|
||||||
end
|
|
||||||
|
|
||||||
private
|
render json: Templates::SerializeForApi.call(cloned_template, schema_documents)
|
||||||
|
|
||||||
def serialize_params
|
|
||||||
{
|
|
||||||
include: { author: { only: %i[id email first_name last_name] },
|
|
||||||
documents: { only: %i[id uuid], methods: %i[url filename] } }
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -49,6 +49,14 @@ module Api
|
|||||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, folder_name)
|
@template.folder = TemplateFolders.find_or_create_by_name(current_user, folder_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Array.wrap(params[:roles].presence || params.dig(:template, :roles).presence).each_with_index do |role, index|
|
||||||
|
if (item = @template.submitters[index])
|
||||||
|
item['name'] = role
|
||||||
|
else
|
||||||
|
@template.submitters << { 'name' => role, 'uuid' => SecureRandom.uuid }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
archived = params.key?(:archived) ? params[:archived] : params.dig(:template, :archived)
|
archived = params.key?(:archived) ? params[:archived] : params.dig(:template, :archived)
|
||||||
|
|
||||||
if archived.in?([true, false])
|
if archived.in?([true, false])
|
||||||
@@ -57,11 +65,13 @@ module Api
|
|||||||
|
|
||||||
@template.update!(template_params)
|
@template.update!(template_params)
|
||||||
|
|
||||||
|
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
|
||||||
|
|
||||||
render json: @template.as_json(only: %i[id updated_at])
|
render json: @template.as_json(only: %i[id updated_at])
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
if params[:permanently] == 'true' && !Docuseal.multitenant?
|
if params[:permanently] == 'true'
|
||||||
@template.destroy!
|
@template.destroy!
|
||||||
else
|
else
|
||||||
@template.update!(archived_at: Time.current)
|
@template.update!(archived_at: Time.current)
|
||||||
@@ -83,20 +93,25 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def template_params
|
def template_params
|
||||||
permit_params = [
|
permitted_params = [
|
||||||
:name,
|
:name,
|
||||||
{ schema: [%i[attachment_uuid name]],
|
{
|
||||||
submitters: [%i[name uuid]],
|
submitters: [%i[name uuid]],
|
||||||
fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value,
|
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||||
|
:required, :readonly, :default_value,
|
||||||
|
:title, :description,
|
||||||
{ preferences: {},
|
{ preferences: {},
|
||||||
|
conditions: [%i[field_uuid value action]],
|
||||||
options: [%i[value uuid]],
|
options: [%i[value uuid]],
|
||||||
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
validation: %i[message pattern],
|
||||||
|
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
if params.key?(:template)
|
if params.key?(:template)
|
||||||
params.require(:template).permit(*permit_params)
|
params.require(:template).permit(permitted_params)
|
||||||
else
|
else
|
||||||
params.permit(*permit_params)
|
params.permit(permitted_params)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Api
|
|
||||||
class TemplatesDocumentsController < ApiBaseController
|
|
||||||
load_and_authorize_resource :template
|
|
||||||
|
|
||||||
def create
|
|
||||||
return head :unprocessable_entity if params[:blobs].blank? && params[:files].blank?
|
|
||||||
|
|
||||||
documents = Templates::CreateAttachments.call(@template, params)
|
|
||||||
|
|
||||||
schema = documents.map do |doc|
|
|
||||||
{ attachment_uuid: doc.uuid, name: doc.filename.base }
|
|
||||||
end
|
|
||||||
|
|
||||||
render json: {
|
|
||||||
schema:,
|
|
||||||
documents: documents.as_json(
|
|
||||||
methods: %i[metadata signed_uuid],
|
|
||||||
include: {
|
|
||||||
preview_images: { methods: %i[url metadata filename] }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
rescue Templates::CreateAttachments::PdfEncrypted
|
|
||||||
render json: { error: 'PDF encrypted' }, status: :unprocessable_entity
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api
|
||||||
|
class ToolsController < ApiBaseController
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
def merge
|
||||||
|
files = params[:files] || []
|
||||||
|
|
||||||
|
return render json: { error: 'Files are required' }, status: :unprocessable_entity if files.blank?
|
||||||
|
return render json: { error: 'At least 2 files are required' }, status: :unprocessable_entity if files.size < 2
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
data: Base64.encode64(PdfUtils.merge(files.map { |base64| StringIO.new(Base64.decode64(base64)) }).string)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api
|
||||||
|
class UsersController < ApiBaseController
|
||||||
|
authorize_resource :current_user
|
||||||
|
|
||||||
|
def show
|
||||||
|
render json: current_user.as_json(only: %i[id first_name last_name email])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -22,6 +22,12 @@ class ApplicationController < ActionController::Base
|
|||||||
redirect_to request.path
|
redirect_to request.path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
rescue_from RateLimit::LimitApproached do |e|
|
||||||
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
|
redirect_to request.referer, alert: 'Too many requests', status: :too_many_requests
|
||||||
|
end
|
||||||
|
|
||||||
if Rails.env.production?
|
if Rails.env.production?
|
||||||
rescue_from CanCan::AccessDenied do |e|
|
rescue_from CanCan::AccessDenied do |e|
|
||||||
Rollbar.warning(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
@@ -46,7 +52,8 @@ class ApplicationController < ActionController::Base
|
|||||||
private
|
private
|
||||||
|
|
||||||
def with_browser_locale(&)
|
def with_browser_locale(&)
|
||||||
locale = request.env['HTTP_ACCEPT_LANGUAGE'].to_s[BROWSER_LOCALE_REGEXP].to_s
|
locale = params[:lang].presence
|
||||||
|
locale ||= request.env['HTTP_ACCEPT_LANGUAGE'].to_s[BROWSER_LOCALE_REGEXP].to_s
|
||||||
|
|
||||||
locale =
|
locale =
|
||||||
if locale.starts_with?('en-') && locale != 'en-US'
|
if locale.starts_with?('en-') && locale != 'en-US'
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ class ConsoleRedirectController < ApplicationController
|
|||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
if request.path == '/upgrade'
|
||||||
|
params[:redir] = Docuseal.multitenant? ? "#{Docuseal::CONSOLE_URL}/plans" : "#{Docuseal::CONSOLE_URL}/on_premise"
|
||||||
|
end
|
||||||
|
|
||||||
|
params[:redir] = "#{Docuseal::CONSOLE_URL}/manage" if request.path == '/manage'
|
||||||
|
|
||||||
return redirect_to(new_user_session_path({ redir: params[:redir] }.compact)) if true_user.blank?
|
return redirect_to(new_user_session_path({ redir: params[:redir] }.compact)) if true_user.blank?
|
||||||
|
|
||||||
auth = JsonWebToken.encode(uuid: true_user.uuid,
|
auth = JsonWebToken.encode(uuid: true_user.uuid,
|
||||||
|
|||||||
@@ -7,62 +7,18 @@ class DashboardController < ApplicationController
|
|||||||
before_action :maybe_render_landing
|
before_action :maybe_render_landing
|
||||||
before_action :maybe_redirect_mfa_setup
|
before_action :maybe_redirect_mfa_setup
|
||||||
|
|
||||||
load_and_authorize_resource :template_folder, parent: false
|
skip_authorization_check
|
||||||
load_and_authorize_resource :template, parent: false
|
|
||||||
|
|
||||||
SHOW_TEMPLATES_FOLDERS_THRESHOLD = 9
|
|
||||||
TEMPLATES_PER_PAGE = 12
|
|
||||||
FOLDERS_PER_PAGE = 18
|
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@template_folders = filter_template_folders(@template_folders)
|
if cookies.permanent[:dashboard_view] == 'submissions'
|
||||||
|
SubmissionsDashboardController.dispatch(:index, request, response)
|
||||||
@pagy, @template_folders = pagy(
|
|
||||||
@template_folders,
|
|
||||||
items: FOLDERS_PER_PAGE,
|
|
||||||
page: @template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1
|
|
||||||
)
|
|
||||||
|
|
||||||
if @pagy.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD
|
|
||||||
@templates = @templates.none
|
|
||||||
else
|
else
|
||||||
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
|
TemplatesDashboardController.dispatch(:index, request, response)
|
||||||
@templates = filter_templates(@templates)
|
|
||||||
|
|
||||||
items =
|
|
||||||
if @template_folders.size < 4
|
|
||||||
TEMPLATES_PER_PAGE
|
|
||||||
else
|
|
||||||
(@template_folders.size < 7 ? 9 : 6)
|
|
||||||
end
|
|
||||||
|
|
||||||
@pagy, @templates = pagy(@templates, items:)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def filter_template_folders(template_folders)
|
|
||||||
rel = template_folders.joins(:active_templates)
|
|
||||||
.order(id: :desc)
|
|
||||||
.distinct
|
|
||||||
|
|
||||||
TemplateFolders.search(rel, params[:q])
|
|
||||||
end
|
|
||||||
|
|
||||||
def filter_templates(templates)
|
|
||||||
rel = templates.active.preload(:author).order(id: :desc)
|
|
||||||
|
|
||||||
if params[:q].blank?
|
|
||||||
shared_template_ids =
|
|
||||||
TemplateSharing.where(account_id: [current_account.id, TemplateSharing::ALL_ID]).select(:template_id)
|
|
||||||
|
|
||||||
rel = rel.where(folder_id: current_account.default_template_folder.id).or(rel.where(id: shared_template_ids))
|
|
||||||
end
|
|
||||||
|
|
||||||
Templates.search(rel, params[:q])
|
|
||||||
end
|
|
||||||
|
|
||||||
def maybe_redirect_product_url
|
def maybe_redirect_product_url
|
||||||
return if !Docuseal.multitenant? || signed_in?
|
return if !Docuseal.multitenant? || signed_in?
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ class ErrorsController < ActionController::Base
|
|||||||
'/api/templates/docx'
|
'/api/templates/docx'
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
|
SAFE_ERROR_MESSAGE_CLASSES = [
|
||||||
|
ActionDispatch::Http::Parameters::ParseError,
|
||||||
|
JSON::ParserError
|
||||||
|
].freeze
|
||||||
|
|
||||||
def show
|
def show
|
||||||
if request.original_fullpath.in?(ENTERPRISE_PATHS) && error_status_code == 404
|
if request.original_fullpath.in?(ENTERPRISE_PATHS) && error_status_code == 404
|
||||||
return render json: { status: 404, message: ENTERPRISE_FEATURE_MESSAGE }, status: :not_found
|
return render json: { status: 404, message: ENTERPRISE_FEATURE_MESSAGE }, status: :not_found
|
||||||
@@ -22,7 +27,13 @@ class ErrorsController < ActionController::Base
|
|||||||
|
|
||||||
respond_to do |f|
|
respond_to do |f|
|
||||||
f.json do
|
f.json do
|
||||||
render json: { status: error_status_code }, status: error_status_code
|
set_cors_headers
|
||||||
|
|
||||||
|
exception = request.env['action_dispatch.exception']
|
||||||
|
|
||||||
|
error = exception.message if exception.class.in?(SAFE_ERROR_MESSAGE_CLASSES)
|
||||||
|
|
||||||
|
render json: { status: error_status_code, error: }.compact, status: error_status_code
|
||||||
end
|
end
|
||||||
|
|
||||||
f.html { render error_status_code.to_s, status: error_status_code }
|
f.html { render error_status_code.to_s, status: error_status_code }
|
||||||
@@ -31,6 +42,14 @@ class ErrorsController < ActionController::Base
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def set_cors_headers
|
||||||
|
headers['Access-Control-Allow-Origin'] = '*'
|
||||||
|
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
|
||||||
|
headers['Access-Control-Allow-Headers'] = '*'
|
||||||
|
headers['Access-Control-Max-Age'] = '1728000'
|
||||||
|
headers['Access-Control-Allow-Credentials'] = true
|
||||||
|
end
|
||||||
|
|
||||||
def error_status_code
|
def error_status_code
|
||||||
@error_status_code ||=
|
@error_status_code ||=
|
||||||
ActionDispatch::ExceptionWrapper.new(request.env,
|
ActionDispatch::ExceptionWrapper.new(request.env,
|
||||||
|
|||||||
@@ -57,8 +57,10 @@ class EsignSettingsController < ApplicationController
|
|||||||
save_new_cert!(@encrypted_config, @cert_record)
|
save_new_cert!(@encrypted_config, @cert_record)
|
||||||
|
|
||||||
redirect_to settings_esign_path, notice: 'Certificate has been successfully added!'
|
redirect_to settings_esign_path, notice: 'Certificate has been successfully added!'
|
||||||
rescue OpenSSL::PKCS12::PKCS12Error
|
rescue OpenSSL::PKCS12::PKCS12Error => e
|
||||||
@cert_record.errors.add(:password, "is invalid. Make sure you're uploading a valid .p12 file")
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
|
@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_entity
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -35,12 +35,12 @@ class NotificationsSettingsController < ApplicationController
|
|||||||
|
|
||||||
def load_reminder_config
|
def load_reminder_config
|
||||||
@reminder_config =
|
@reminder_config =
|
||||||
AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::SUBMITTER_REMAILERS)
|
AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::SUBMITTER_REMINDERS)
|
||||||
end
|
end
|
||||||
|
|
||||||
def email_config_params
|
def email_config_params
|
||||||
params.require(:account_config).permit!.tap do |attrs|
|
params.require(:account_config).permit!.tap do |attrs|
|
||||||
attrs[:key] = nil unless attrs[:key].in?([AccountConfig::BCC_EMAILS, AccountConfig::SUBMITTER_REMAILERS])
|
attrs[:key] = nil unless attrs[:key].in?([AccountConfig::BCC_EMAILS, AccountConfig::SUBMITTER_REMINDERS])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -5,6 +5,12 @@ class PasswordsController < Devise::PasswordsController
|
|||||||
attribute :user
|
attribute :user
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
super do |resource|
|
||||||
|
resource.errors.clear unless Docuseal.multitenant?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
super do |resource|
|
super do |resource|
|
||||||
Current.user = resource
|
Current.user = resource
|
||||||
|
|||||||
@@ -1,26 +1,58 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class PersonalizationSettingsController < ApplicationController
|
class PersonalizationSettingsController < ApplicationController
|
||||||
|
ALLOWED_KEYS = [
|
||||||
|
AccountConfig::FORM_COMPLETED_BUTTON_KEY,
|
||||||
|
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
|
||||||
|
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
|
||||||
|
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY,
|
||||||
|
AccountConfig::FORM_COMPLETED_MESSAGE_KEY
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
InvalidKey = Class.new(StandardError)
|
||||||
|
|
||||||
|
before_action :load_and_authorize_account_config, only: :create
|
||||||
|
|
||||||
def show
|
def show
|
||||||
authorize!(:read, AccountConfig)
|
authorize!(:read, AccountConfig)
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
account_config =
|
if @account_config.value.is_a?(Hash)
|
||||||
current_account.account_configs.find_or_initialize_by(key: account_config_params[:key])
|
@account_config.value = @account_config.value.reject do |_, v|
|
||||||
|
v.blank? && v != false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
authorize!(:create, account_config)
|
if @account_config.value != false && @account_config.value.blank?
|
||||||
|
@account_config.destroy!
|
||||||
account_config.update!(account_config_params)
|
else
|
||||||
|
@account_config.save!
|
||||||
|
end
|
||||||
|
|
||||||
redirect_back(fallback_location: settings_personalization_path, notice: 'Settings have been saved.')
|
redirect_back(fallback_location: settings_personalization_path, notice: 'Settings have been saved.')
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def load_and_authorize_account_config
|
||||||
|
@account_config =
|
||||||
|
current_account.account_configs.find_or_initialize_by(key: account_config_params[:key])
|
||||||
|
|
||||||
|
@account_config.assign_attributes(account_config_params)
|
||||||
|
|
||||||
|
authorize!(:create, @account_config)
|
||||||
|
|
||||||
|
raise InvalidKey unless ALLOWED_KEYS.include?(@account_config.key)
|
||||||
|
|
||||||
|
@account_config
|
||||||
|
end
|
||||||
|
|
||||||
def account_config_params
|
def account_config_params
|
||||||
attrs = params.require(:account_config).permit!
|
attrs = params.require(:account_config).permit!
|
||||||
|
|
||||||
|
return attrs if attrs[:value].is_a?(String)
|
||||||
|
|
||||||
attrs[:value]&.transform_values! do |value|
|
attrs[:value]&.transform_values! do |value|
|
||||||
if value.in?(%w[true false])
|
if value.in?(%w[true false])
|
||||||
value == 'true'
|
value == 'true'
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class RegistrationsController < Devise::RegistrationsController
|
|
||||||
prepend_before_action :require_no_authentication, only: [:show]
|
|
||||||
prepend_before_action :maybe_redirect_if_signed_in, only: [:show]
|
|
||||||
|
|
||||||
def show; end
|
|
||||||
|
|
||||||
def create
|
|
||||||
super
|
|
||||||
|
|
||||||
Accounts.create_default_template(resource.account) if resource.account.persisted?
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def after_sign_up_path_for(...)
|
|
||||||
if params[:redir].present?
|
|
||||||
return console_redirect_index_path(redir: params[:redir]) if params[:redir].starts_with?(Docuseal::CONSOLE_URL)
|
|
||||||
|
|
||||||
return params[:redir]
|
|
||||||
end
|
|
||||||
|
|
||||||
super
|
|
||||||
end
|
|
||||||
|
|
||||||
def maybe_redirect_if_signed_in
|
|
||||||
return unless signed_in?
|
|
||||||
return if params[:redir].blank?
|
|
||||||
|
|
||||||
redirect_to after_sign_up_path_for(current_user), allow_other_host: true
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_flash_message(key, kind, options = {})
|
|
||||||
return if key == :alert && kind == 'already_authenticated'
|
|
||||||
|
|
||||||
super
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_resource(_hash = {})
|
|
||||||
account = Account.new(account_params)
|
|
||||||
account.timezone = Accounts.normalize_timezone(account.timezone)
|
|
||||||
|
|
||||||
self.resource = account.users.new(user_params)
|
|
||||||
|
|
||||||
account.name ||= resource.full_name if params[:action] == 'create'
|
|
||||||
end
|
|
||||||
|
|
||||||
def user_params
|
|
||||||
return {} if params[:user].blank?
|
|
||||||
|
|
||||||
params.require(:user).permit(:first_name, :last_name, :email, :password).compact_blank.tap do |attrs|
|
|
||||||
attrs[:password] ||= SecureRandom.hex if params[:action] == 'create'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def account_params
|
|
||||||
return {} if params[:account].blank?
|
|
||||||
|
|
||||||
params.require(:account).permit(:name, :timezone).compact_blank
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -21,6 +21,8 @@ class SendSubmissionEmailController < ApplicationController
|
|||||||
Submitter.find_by!(slug: params[:submitter_slug])
|
Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
||||||
|
|
||||||
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later!
|
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later!
|
||||||
|
|
||||||
respond_to do |f|
|
respond_to do |f|
|
||||||
|
|||||||
@@ -3,10 +3,14 @@
|
|||||||
class SessionsController < Devise::SessionsController
|
class SessionsController < Devise::SessionsController
|
||||||
before_action :configure_permitted_parameters
|
before_action :configure_permitted_parameters
|
||||||
|
|
||||||
|
around_action :with_browser_locale
|
||||||
|
|
||||||
def create
|
def create
|
||||||
email = sign_in_params[:email].to_s.downcase
|
email = sign_in_params[:email].to_s.downcase
|
||||||
|
|
||||||
if Docuseal.multitenant? && !User.exists?(email:)
|
if Docuseal.multitenant? && !User.exists?(email:)
|
||||||
|
Rollbar.warning('Sign in new user') if defined?(Rollbar)
|
||||||
|
|
||||||
return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)),
|
return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)),
|
||||||
notice: 'Create a new account'
|
notice: 'Create a new account'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
class SsoSettingsController < ApplicationController
|
class SsoSettingsController < ApplicationController
|
||||||
before_action :load_encrypted_config
|
before_action :load_encrypted_config
|
||||||
authorize_resource :encrypted_config, only: :index
|
authorize_resource :encrypted_config, only: :index
|
||||||
authorize_resource :encrypted_config, parent: false, except: :index
|
|
||||||
|
|
||||||
def index; end
|
def index; end
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,13 @@ class StartFormController < ApplicationController
|
|||||||
|
|
||||||
assign_submission_attributes(@submitter, @template) if @submitter.new_record?
|
assign_submission_attributes(@submitter, @template) if @submitter.new_record?
|
||||||
|
|
||||||
|
is_new_record = @submitter.new_record?
|
||||||
|
|
||||||
if @submitter.save
|
if @submitter.save
|
||||||
|
if is_new_record
|
||||||
|
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => @submitter.submission.id })
|
||||||
|
end
|
||||||
|
|
||||||
redirect_to submit_form_path(@submitter.slug)
|
redirect_to submit_form_path(@submitter.slug)
|
||||||
else
|
else
|
||||||
render :show
|
render :show
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmissionsArchivedController < ApplicationController
|
||||||
|
load_and_authorize_resource :submission, parent: false
|
||||||
|
|
||||||
|
def index
|
||||||
|
@submissions = @submissions.joins(:template)
|
||||||
|
@submissions = @submissions.where.not(archived_at: nil)
|
||||||
|
.or(@submissions.where.not(templates: { archived_at: nil }))
|
||||||
|
.preload(:created_by_user, template: :author)
|
||||||
|
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
|
||||||
|
|
||||||
|
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -19,6 +19,8 @@ class SubmissionsController < ApplicationController
|
|||||||
def create
|
def create
|
||||||
authorize!(:create, Submission)
|
authorize!(:create, Submission)
|
||||||
|
|
||||||
|
save_template_message(@template, params) if params[:save_message] == '1'
|
||||||
|
|
||||||
if params[:is_custom_message] != '1'
|
if params[:is_custom_message] != '1'
|
||||||
params.delete(:subject)
|
params.delete(:subject)
|
||||||
params.delete(:body)
|
params.delete(:body)
|
||||||
@@ -37,24 +39,45 @@ class SubmissionsController < ApplicationController
|
|||||||
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',
|
||||||
mark_as_sent: params[:send_email] == '1',
|
|
||||||
submissions_attrs: submissions_params[:submission].to_h.values,
|
submissions_attrs: submissions_params[:submission].to_h.values,
|
||||||
params: params.merge('send_completed_email' => true))
|
params: params.merge('send_completed_email' => true))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
submissions.each do |submission|
|
||||||
|
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id })
|
||||||
|
end
|
||||||
|
|
||||||
Submissions.send_signature_requests(submissions)
|
Submissions.send_signature_requests(submissions)
|
||||||
|
|
||||||
redirect_to template_path(@template), notice: 'New recipients have been added'
|
redirect_to template_path(@template), notice: 'New recipients have been added'
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@submission.update!(archived_at: Time.current)
|
notice =
|
||||||
|
if params[:permanently].present?
|
||||||
|
@submission.destroy!
|
||||||
|
|
||||||
redirect_back(fallback_location: template_path(@submission.template), notice: 'Submission has been archived')
|
'Submission has been removed'
|
||||||
|
else
|
||||||
|
@submission.update!(archived_at: Time.current)
|
||||||
|
|
||||||
|
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id)
|
||||||
|
|
||||||
|
'Submission has been archived'
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect_back(fallback_location: template_path(@submission.template), notice:)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def save_template_message(template, params)
|
||||||
|
template.preferences['request_email_subject'] = params[:subject] if params[:subject].present?
|
||||||
|
template.preferences['request_email_body'] = params[:body] if params[:body].present?
|
||||||
|
|
||||||
|
template.save!
|
||||||
|
end
|
||||||
|
|
||||||
def submissions_params
|
def submissions_params
|
||||||
params.permit(submission: { submitters: [%i[uuid email phone name]] })
|
params.permit(submission: { submitters: [%i[uuid email phone name]] })
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmissionsDashboardController < ApplicationController
|
||||||
|
load_and_authorize_resource :submission, parent: false
|
||||||
|
|
||||||
|
def index
|
||||||
|
@submissions = @submissions.joins(:template)
|
||||||
|
|
||||||
|
@submissions = @submissions.where(archived_at: nil)
|
||||||
|
.where(templates: { archived_at: nil })
|
||||||
|
.preload(:created_by_user, template: :author)
|
||||||
|
|
||||||
|
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
|
||||||
|
|
||||||
|
@submissions = @submissions.pending if params[:status] == 'pending'
|
||||||
|
@submissions = @submissions.completed if params[:status] == 'completed'
|
||||||
|
|
||||||
|
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -16,13 +16,16 @@ class SubmissionsDebugController < ApplicationController
|
|||||||
render 'submit_form/show'
|
render 'submit_form/show'
|
||||||
end
|
end
|
||||||
f.pdf do
|
f.pdf do
|
||||||
if params[:audit]
|
result =
|
||||||
Submissions::GenerateAuditTrail.call(@submitter.submission)
|
if params[:audit]
|
||||||
else
|
Submissions::GenerateAuditTrail.call(@submitter.submission)
|
||||||
Submissions::GenerateResultAttachments.call(@submitter)
|
elsif params[:combined]
|
||||||
end
|
Submissions::GenerateCombinedAttachment.call(@submitter)
|
||||||
|
else
|
||||||
|
Submissions::GenerateResultAttachments.call(@submitter)
|
||||||
|
end
|
||||||
|
|
||||||
send_data ActiveStorage::Attachment.where(name: params[:audit] ? :audit_trail : :documents).last.download,
|
send_data Array.wrap(result).first.download,
|
||||||
filename: 'debug.pdf',
|
filename: 'debug.pdf',
|
||||||
disposition: 'inline',
|
disposition: 'inline',
|
||||||
type: 'application/pdf'
|
type: 'application/pdf'
|
||||||
|
|||||||
@@ -33,7 +33,17 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
return head :not_found
|
return head :not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: build_urls(last_submitter)
|
if params[:combined]
|
||||||
|
url = build_combined_url(submitter)
|
||||||
|
|
||||||
|
if url
|
||||||
|
render json: [url]
|
||||||
|
else
|
||||||
|
head :not_found
|
||||||
|
end
|
||||||
|
else
|
||||||
|
render json: build_urls(last_submitter)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -44,7 +54,17 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
|
|
||||||
def build_urls(submitter)
|
def build_urls(submitter)
|
||||||
Submitters.select_attachments_for_download(submitter).map do |attachment|
|
Submitters.select_attachments_for_download(submitter).map do |attachment|
|
||||||
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.minutes.from_now.to_i)
|
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def build_combined_url(submitter)
|
||||||
|
return if submitter.submission.submitters.exists?(completed_at: nil)
|
||||||
|
return if submitter.submission.submitters.order(:completed_at).last != submitter
|
||||||
|
|
||||||
|
attachment = submitter.submission.combined_document_attachment
|
||||||
|
attachment ||= Submissions::GenerateCombinedAttachment.call(submitter)
|
||||||
|
|
||||||
|
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -7,33 +7,37 @@ class SubmitFormController < ApplicationController
|
|||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
PRELOAD_ALL_PAGES_AMOUNT = 200
|
CONFIG_KEYS = [].freeze
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@submitter = Submitter.find_by!(slug: params[:slug])
|
@submitter = Submitter.find_by!(slug: params[:slug])
|
||||||
|
|
||||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||||
|
return render :archived if @submitter.submission.template.archived_at? || @submitter.submission.archived_at?
|
||||||
|
|
||||||
ActiveRecord::Associations::Preloader.new(
|
Submitters.preload_with_pages(@submitter)
|
||||||
records: [@submitter],
|
|
||||||
associations: [submission: [:template, { template_schema_documents: :blob }]]
|
|
||||||
).call
|
|
||||||
|
|
||||||
total_pages =
|
|
||||||
@submitter.submission.template_schema_documents.sum { |e| e.metadata.dig('pdf', 'number_of_pages').to_i }
|
|
||||||
|
|
||||||
if total_pages < PRELOAD_ALL_PAGES_AMOUNT
|
|
||||||
ActiveRecord::Associations::Preloader.new(
|
|
||||||
records: @submitter.submission.template_schema_documents,
|
|
||||||
associations: [:blob, { preview_images_attachments: :blob }]
|
|
||||||
).call
|
|
||||||
end
|
|
||||||
|
|
||||||
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
|
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
|
||||||
|
|
||||||
cookies[:submitter_sid] = @submitter.signed_id
|
@attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments)
|
||||||
|
.preload(:blob).index_by(&:uuid)
|
||||||
|
|
||||||
render(@submitter.submission.template.archived_at? || @submitter.submission.archived_at? ? :archived : :show)
|
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
|
||||||
|
|
||||||
|
return unless @form_configs[:prefill_signature]
|
||||||
|
|
||||||
|
if (user_signature = UserConfigs.load_signature(current_user))
|
||||||
|
@signature_attachment = ActiveStorage::Attachment.find_or_create_by!(
|
||||||
|
blob_id: user_signature.blob_id,
|
||||||
|
name: 'attachments',
|
||||||
|
record: @submitter
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
@signature_attachment ||=
|
||||||
|
Submitters::MaybeAssignDefaultBrowserSignature.call(@submitter, params, cookies, @attachments_index.values)
|
||||||
|
|
||||||
|
@attachments_index[@signature_attachment.uuid] = @signature_attachment if @signature_attachment
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@@ -44,8 +48,6 @@ class SubmitFormController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
if submitter.template.archived_at? || submitter.submission.archived_at?
|
if submitter.template.archived_at? || submitter.submission.archived_at?
|
||||||
Rollbar.info("Archived template: #{submitter.template.id}") if defined?(Rollbar)
|
|
||||||
|
|
||||||
return render json: { error: 'Form has been archived.' }, status: :unprocessable_entity
|
return render json: { error: 'Form has been archived.' }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -57,7 +59,7 @@ class SubmitFormController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def completed
|
def completed
|
||||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
@submitter = Submitter.completed.find_by!(slug: params[:submit_form_slug])
|
||||||
end
|
end
|
||||||
|
|
||||||
def success; end
|
def success; end
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmitFormDrawSignatureController < ApplicationController
|
||||||
|
layout false
|
||||||
|
|
||||||
|
around_action :with_browser_locale, only: %i[show]
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
def show
|
||||||
|
@submitter = Submitter.find_by!(slug: params[:slug])
|
||||||
|
|
||||||
|
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||||
|
|
||||||
|
if @submitter.submission.template.archived_at? || @submitter.submission.archived_at?
|
||||||
|
return redirect_to submit_form_path(@submitter.slug)
|
||||||
|
end
|
||||||
|
|
||||||
|
render :show
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmitFormValuesController < ApplicationController
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
def index
|
||||||
|
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||||
|
|
||||||
|
return render json: {} if submitter.completed_at?
|
||||||
|
return render json: {} if submitter.submission.template.archived_at? || submitter.submission.archived_at?
|
||||||
|
|
||||||
|
value = submitter.values[params['field_uuid']]
|
||||||
|
attachment = submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present?
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
value:,
|
||||||
|
attachment: attachment&.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
|
||||||
|
}, head: :ok
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmittersAutocompleteController < ApplicationController
|
||||||
|
load_and_authorize_resource :submitter, parent: false
|
||||||
|
|
||||||
|
SELECT_COLUMNS = %w[email phone name].freeze
|
||||||
|
LIMIT = 100
|
||||||
|
|
||||||
|
def index
|
||||||
|
submitters = search_submitters(@submitters)
|
||||||
|
|
||||||
|
values = submitters.limit(LIMIT).group(SELECT_COLUMNS.join(', ')).pluck(SELECT_COLUMNS.join(', '))
|
||||||
|
|
||||||
|
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
|
||||||
|
attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present?
|
||||||
|
|
||||||
|
render json: attrs
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def search_submitters(submitters)
|
||||||
|
if SELECT_COLUMNS.include?(params[:field])
|
||||||
|
column = Submitter.arel_table[params[:field].to_sym]
|
||||||
|
|
||||||
|
term = "#{params[:q].downcase}%"
|
||||||
|
|
||||||
|
submitters.where(column.matches(term))
|
||||||
|
else
|
||||||
|
Submitters.search(submitters, params[:q])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -7,11 +7,13 @@ class SubmittersSendEmailController < ApplicationController
|
|||||||
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
|
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
|
||||||
event_type: 'send_email',
|
event_type: 'send_email',
|
||||||
created_at: 24.hours.ago..Time.current)
|
created_at: 24.hours.ago..Time.current)
|
||||||
|
Rollbar.warning("Already sent: #{@submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
return redirect_back(fallback_location: submission_path(@submitter.submission),
|
return redirect_back(fallback_location: submission_path(@submitter.submission),
|
||||||
alert: 'Email has been sent already.')
|
alert: 'Email has been sent already.')
|
||||||
end
|
end
|
||||||
|
|
||||||
SendSubmitterInvitationEmailJob.perform_later('submitter_id' => @submitter.id)
|
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => @submitter.id)
|
||||||
|
|
||||||
@submitter.sent_at ||= Time.current
|
@submitter.sent_at ||= Time.current
|
||||||
@submitter.save!
|
@submitter.save!
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplateDocumentsController < ApplicationController
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def create
|
||||||
|
if params[:blobs].blank? && params[:files].blank?
|
||||||
|
return render json: { error: 'File is missing' }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
|
old_fields_hash = @template.fields.hash
|
||||||
|
|
||||||
|
documents = Templates::CreateAttachments.call(@template, params, extract_fields: true)
|
||||||
|
|
||||||
|
schema = documents.map do |doc|
|
||||||
|
{ attachment_uuid: doc.uuid, name: doc.filename.base }
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
schema:,
|
||||||
|
fields: old_fields_hash == @template.fields.hash ? nil : @template.fields,
|
||||||
|
submitters: old_fields_hash == @template.fields.hash ? nil : @template.submitters,
|
||||||
|
documents: documents.as_json(
|
||||||
|
methods: %i[metadata signed_uuid],
|
||||||
|
include: {
|
||||||
|
preview_images: { methods: %i[url metadata filename] }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
rescue Templates::CreateAttachments::PdfEncrypted
|
||||||
|
render json: { error: 'PDF encrypted' }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplateFoldersAutocompleteController < ApplicationController
|
||||||
|
load_and_authorize_resource :template_folder, parent: false
|
||||||
|
|
||||||
|
LIMIT = 100
|
||||||
|
|
||||||
|
def index
|
||||||
|
template_folders = @template_folders.joins(:templates).where(templates: { archived_at: nil }).distinct
|
||||||
|
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
|
||||||
|
|
||||||
|
render json: template_folders.as_json(only: %i[name archived_at])
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -6,7 +6,7 @@ class TemplatesArchivedSubmissionsController < ApplicationController
|
|||||||
|
|
||||||
def index
|
def index
|
||||||
@submissions = @submissions.where.not(archived_at: nil)
|
@submissions = @submissions.where.not(archived_at: nil)
|
||||||
@submissions = Submissions.search(@submissions, params[:q])
|
@submissions = Submissions.search(@submissions, params[:q], search_values: true)
|
||||||
|
|
||||||
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
|
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class TemplatesController < ApplicationController
|
|||||||
def show
|
def show
|
||||||
submissions = @template.submissions.accessible_by(current_ability)
|
submissions = @template.submissions.accessible_by(current_ability)
|
||||||
submissions = submissions.active if @template.archived_at.blank?
|
submissions = submissions.active if @template.archived_at.blank?
|
||||||
submissions = Submissions.search(submissions, params[:q])
|
submissions = Submissions.search(submissions, params[:q], search_values: true)
|
||||||
|
|
||||||
@base_submissions = submissions
|
@base_submissions = submissions
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ class TemplatesController < ApplicationController
|
|||||||
def edit
|
def edit
|
||||||
ActiveRecord::Associations::Preloader.new(
|
ActiveRecord::Associations::Preloader.new(
|
||||||
records: [@template],
|
records: [@template],
|
||||||
associations: [schema_documents: { preview_images_attachments: :blob }]
|
associations: [schema_documents: [:blob, { preview_images_attachments: :blob }]]
|
||||||
).call
|
).call
|
||||||
|
|
||||||
@template_data =
|
@template_data =
|
||||||
@@ -43,6 +43,11 @@ class TemplatesController < ApplicationController
|
|||||||
|
|
||||||
def create
|
def create
|
||||||
if @base_template
|
if @base_template
|
||||||
|
ActiveRecord::Associations::Preloader.new(
|
||||||
|
records: [@base_template],
|
||||||
|
associations: [schema_documents: :preview_images_attachments]
|
||||||
|
).call
|
||||||
|
|
||||||
@template = Templates::Clone.call(@base_template, author: current_user,
|
@template = Templates::Clone.call(@base_template, author: current_user,
|
||||||
name: params.dig(:template, :name),
|
name: params.dig(:template, :name),
|
||||||
folder_name: params[:folder_name])
|
folder_name: params[:folder_name])
|
||||||
@@ -53,7 +58,7 @@ class TemplatesController < ApplicationController
|
|||||||
|
|
||||||
if params[:account_id].present? && authorized_clone_account_id?(params[:account_id])
|
if params[:account_id].present? && authorized_clone_account_id?(params[:account_id])
|
||||||
@template.account_id = params[:account_id]
|
@template.account_id = params[:account_id]
|
||||||
@template.folder = @template.account.default_template_folder
|
@template.folder = @template.account.default_template_folder if @template.account_id != current_account.id
|
||||||
else
|
else
|
||||||
@template.account = current_account
|
@template.account = current_account
|
||||||
end
|
end
|
||||||
@@ -61,19 +66,27 @@ class TemplatesController < ApplicationController
|
|||||||
if @template.save
|
if @template.save
|
||||||
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
||||||
|
|
||||||
|
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
|
||||||
|
|
||||||
maybe_redirect_to_template(@template)
|
maybe_redirect_to_template(@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_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@template.update!(template_params)
|
||||||
|
|
||||||
|
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
|
||||||
|
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
notice =
|
notice =
|
||||||
if params[:permanently].present?
|
if params[:permanently].present?
|
||||||
@template.destroy!
|
@template.destroy!
|
||||||
|
|
||||||
Rollbar.info("Remove template: #{@template.id}") if defined?(Rollbar)
|
|
||||||
|
|
||||||
'Template has been removed.'
|
'Template has been removed.'
|
||||||
else
|
else
|
||||||
@template.update!(archived_at: Time.current)
|
@template.update!(archived_at: Time.current)
|
||||||
@@ -86,6 +99,22 @@ class TemplatesController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def template_params
|
||||||
|
params.require(:template).permit(
|
||||||
|
:name,
|
||||||
|
{ schema: [%i[attachment_uuid name]],
|
||||||
|
submitters: [%i[name uuid]],
|
||||||
|
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||||
|
:required, :readonly, :default_value,
|
||||||
|
:title, :description,
|
||||||
|
{ preferences: {},
|
||||||
|
conditions: [%i[field_uuid value action]],
|
||||||
|
options: [%i[value uuid]],
|
||||||
|
validation: %i[message pattern],
|
||||||
|
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
def authorized_clone_account_id?(account_id)
|
def authorized_clone_account_id?(account_id)
|
||||||
true_user.account_id.to_s == account_id.to_s || true_user.account.linked_accounts.exists?(id: account_id)
|
true_user.account_id.to_s == account_id.to_s || true_user.account.linked_accounts.exists?(id: account_id)
|
||||||
end
|
end
|
||||||
@@ -98,10 +127,6 @@ class TemplatesController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def template_params
|
|
||||||
params.require(:template).permit(:name)
|
|
||||||
end
|
|
||||||
|
|
||||||
def load_base_template
|
def load_base_template
|
||||||
return if params[:base_template_id].blank?
|
return if params[:base_template_id].blank?
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesDashboardController < ApplicationController
|
||||||
|
load_and_authorize_resource :template_folder, parent: false
|
||||||
|
load_and_authorize_resource :template, parent: false
|
||||||
|
|
||||||
|
SHOW_TEMPLATES_FOLDERS_THRESHOLD = 9
|
||||||
|
TEMPLATES_PER_PAGE = 12
|
||||||
|
FOLDERS_PER_PAGE = 18
|
||||||
|
|
||||||
|
def index
|
||||||
|
@template_folders = filter_template_folders(@template_folders)
|
||||||
|
|
||||||
|
@pagy, @template_folders = pagy(
|
||||||
|
@template_folders,
|
||||||
|
items: FOLDERS_PER_PAGE,
|
||||||
|
page: @template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if @pagy.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD
|
||||||
|
@templates = @templates.none
|
||||||
|
else
|
||||||
|
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
|
||||||
|
@templates = filter_templates(@templates)
|
||||||
|
|
||||||
|
items =
|
||||||
|
if @template_folders.size < 4
|
||||||
|
TEMPLATES_PER_PAGE
|
||||||
|
else
|
||||||
|
(@template_folders.size < 7 ? 9 : 6)
|
||||||
|
end
|
||||||
|
|
||||||
|
@pagy, @templates = pagy(@templates, items:)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def filter_template_folders(template_folders)
|
||||||
|
rel = template_folders.joins(:active_templates)
|
||||||
|
.order(id: :desc)
|
||||||
|
.distinct
|
||||||
|
|
||||||
|
TemplateFolders.search(rel, params[:q])
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_templates(templates)
|
||||||
|
rel = templates.active.preload(:author).order(id: :desc)
|
||||||
|
|
||||||
|
if params[:q].blank?
|
||||||
|
if Docuseal.multitenant? && !current_account.testing?
|
||||||
|
rel = rel.where(folder_id: current_account.default_template_folder.id)
|
||||||
|
else
|
||||||
|
shared_template_ids =
|
||||||
|
TemplateSharing.where(account_id: [current_account.id, TemplateSharing::ALL_ID]).select(:template_id)
|
||||||
|
|
||||||
|
rel = rel.where(folder_id: current_account.default_template_folder.id).or(rel.where(id: shared_template_ids))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Templates.search(rel, params[:q])
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesDebugController < ApplicationController
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def show
|
||||||
|
attachment = @template.documents.first
|
||||||
|
|
||||||
|
pdf = HexaPDF::Document.new(io: StringIO.new(attachment.download))
|
||||||
|
|
||||||
|
fields = Templates::FindAcroFields.call(pdf, attachment)
|
||||||
|
|
||||||
|
attachment.metadata['pdf'] ||= {}
|
||||||
|
attachment.metadata['pdf']['fields'] = fields
|
||||||
|
|
||||||
|
@template.update!(fields: Templates::ProcessDocument.normalize_attachment_fields(@template, [attachment]))
|
||||||
|
|
||||||
|
ActiveRecord::Associations::Preloader.new(
|
||||||
|
records: [@template],
|
||||||
|
associations: [schema_documents: { preview_images_attachments: :blob }]
|
||||||
|
).call
|
||||||
|
|
||||||
|
@template_data =
|
||||||
|
@template.as_json.merge(
|
||||||
|
documents: @template.schema_documents.as_json(
|
||||||
|
methods: %i[metadata signed_uuid],
|
||||||
|
include: { preview_images: { methods: %i[url metadata filename] } }
|
||||||
|
)
|
||||||
|
).to_json
|
||||||
|
|
||||||
|
render 'templates/edit', layout: 'plain'
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesFormPreviewController < ApplicationController
|
||||||
|
layout 'form'
|
||||||
|
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def show
|
||||||
|
@submitter = Submitter.new(uuid: params[:uuid] || @template.submitters.first['uuid'],
|
||||||
|
account: current_account,
|
||||||
|
submission: @template.submissions.new(template_submitters: @template.submitters,
|
||||||
|
account: current_account))
|
||||||
|
|
||||||
|
@submitter.submission.submitters = @template.submitters.map { |item| Submitter.new(uuid: item['uuid']) }
|
||||||
|
|
||||||
|
Submitters.preload_with_pages(@submitter)
|
||||||
|
|
||||||
|
@attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments)
|
||||||
|
.preload(:blob).index_by(&:uuid)
|
||||||
|
|
||||||
|
@form_configs = Submitters::FormConfigs.call(@submitter)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesPreferencesController < ApplicationController
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize!(:update, @template)
|
||||||
|
|
||||||
|
@template.preferences = @template.preferences.merge(template_params[:preferences])
|
||||||
|
@template.preferences = @template.preferences.reject { |_, v| v.is_a?(String) && v.blank? }
|
||||||
|
@template.save!
|
||||||
|
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def template_params
|
||||||
|
params.require(:template).permit(
|
||||||
|
preferences: %i[bcc_completed request_email_subject request_email_body
|
||||||
|
documents_copy_email_subject documents_copy_email_body
|
||||||
|
documents_copy_email_enabled documents_copy_email_attach_audit
|
||||||
|
completed_notification_email_attach_documents
|
||||||
|
completed_notification_email_subject completed_notification_email_body
|
||||||
|
completed_notification_email_enabled completed_notification_email_attach_audit]
|
||||||
|
).tap do |attrs|
|
||||||
|
attrs[:preferences] = attrs[:preferences].transform_values do |value|
|
||||||
|
if %w[true false].include?(value)
|
||||||
|
value == 'true'
|
||||||
|
else
|
||||||
|
value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -12,18 +12,27 @@ class TemplatesUploadsController < ApplicationController
|
|||||||
|
|
||||||
save_template!(@template, url_params)
|
save_template!(@template, url_params)
|
||||||
|
|
||||||
documents = Templates::CreateAttachments.call(@template, url_params || params)
|
documents = Templates::CreateAttachments.call(@template, url_params || params, extract_fields: true)
|
||||||
|
|
||||||
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
|
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
|
||||||
|
|
||||||
|
if @template.fields.blank?
|
||||||
|
@template.fields = Templates::ProcessDocument.normalize_attachment_fields(@template, documents)
|
||||||
|
|
||||||
|
schema.each { |item| item['pending_fields'] = true } if @template.fields.present?
|
||||||
|
end
|
||||||
|
|
||||||
@template.update!(schema:)
|
@template.update!(schema:)
|
||||||
|
|
||||||
|
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => @template.id)
|
||||||
|
|
||||||
redirect_to edit_template_path(@template)
|
redirect_to edit_template_path(@template)
|
||||||
rescue Templates::CreateAttachments::PdfEncrypted
|
rescue Templates::CreateAttachments::PdfEncrypted
|
||||||
render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password)
|
render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password)
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
|
raise if Rails.env.local?
|
||||||
|
|
||||||
redirect_to root_path, alert: 'Unable to upload file'
|
redirect_to root_path, alert: 'Unable to upload file'
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -49,7 +58,7 @@ class TemplatesUploadsController < ApplicationController
|
|||||||
file = ActionDispatch::Http::UploadedFile.new(
|
file = ActionDispatch::Http::UploadedFile.new(
|
||||||
tempfile:,
|
tempfile:,
|
||||||
filename: File.basename(
|
filename: File.basename(
|
||||||
URI.decode_www_form_component(params[:filename].presence || params[:url])
|
URI.decode_www_form_component(params[:filename].presence || params[:url]), '.*'
|
||||||
),
|
),
|
||||||
type: Marcel::MimeType.for(tempfile)
|
type: Marcel::MimeType.for(tempfile)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class TimestampServerController < ApplicationController
|
class TimestampServerController < ApplicationController
|
||||||
|
HASH_ALGORITHM = 'SHA256'
|
||||||
|
|
||||||
before_action :build_encrypted_config
|
before_action :build_encrypted_config
|
||||||
authorize_resource :encrypted_config
|
authorize_resource :encrypted_config
|
||||||
|
|
||||||
|
TimestampError = Class.new(StandardError)
|
||||||
|
|
||||||
def create
|
def create
|
||||||
return head :not_found if Docuseal.multitenant?
|
return head :not_found if Docuseal.multitenant?
|
||||||
|
|
||||||
@@ -14,25 +18,29 @@ class TimestampServerController < ApplicationController
|
|||||||
else
|
else
|
||||||
redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
|
redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
|
||||||
end
|
end
|
||||||
rescue HexaPDF::Error, SocketError, Submissions::TimestampHandler::TimestampError, OpenSSL::Timestamp::TimestampError
|
rescue SocketError, TimestampError, OpenSSL::Timestamp::TimestampError
|
||||||
redirect_back fallback_location: settings_notifications_path, alert: 'Invalid Timeserver'
|
redirect_back fallback_location: settings_notifications_path, alert: 'Invalid Timeserver'
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def test_timeserver_url(url)
|
def test_timeserver_url(url)
|
||||||
pdf = HexaPDF::Document.new
|
req = OpenSSL::Timestamp::Request.new
|
||||||
pdf.pages.add
|
req.algorithm = HASH_ALGORITHM
|
||||||
|
req.message_imprint = OpenSSL::Digest.digest(HASH_ALGORITHM, 'test')
|
||||||
|
|
||||||
pkcs = Accounts.load_signing_pkcs(current_account)
|
uri = Addressable::URI.parse(url)
|
||||||
|
|
||||||
pdf.sign(StringIO.new,
|
conn = Faraday.new(uri.origin) do |c|
|
||||||
reason: 'Test',
|
c.basic_auth(uri.user, uri.password) if uri.password.present?
|
||||||
certificate: pkcs.certificate,
|
end
|
||||||
key: pkcs.key,
|
|
||||||
signature_size: 10_000,
|
response = conn.post(uri.path, req.to_der,
|
||||||
certificate_chain: pkcs.ca_certs || [],
|
'content-type' => 'application/timestamp-query')
|
||||||
timestamp_handler: Submissions::TimestampHandler.new(tsa_url: url))
|
|
||||||
|
raise TimestampError if response.status != 200 || response.body.blank?
|
||||||
|
|
||||||
|
response
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_encrypted_config
|
def load_encrypted_config
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ class UserConfigsController < ApplicationController
|
|||||||
UserConfig::RECEIVE_COMPLETED_EMAIL
|
UserConfig::RECEIVE_COMPLETED_EMAIL
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
|
InvalidKey = Class.new(StandardError)
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@user_config.update!(user_config_params)
|
@user_config.update!(user_config_params)
|
||||||
|
|
||||||
@@ -17,7 +19,7 @@ class UserConfigsController < ApplicationController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def load_user_config
|
def load_user_config
|
||||||
return head :not_found unless ALLOWED_KEYS.include?(user_config_params[:key])
|
raise InvalidKey unless ALLOWED_KEYS.include?(user_config_params[:key])
|
||||||
|
|
||||||
@user_config =
|
@user_config =
|
||||||
UserConfig.find_or_initialize_by(user: current_user, key: user_config_params[:key])
|
UserConfig.find_or_initialize_by(user: current_user, key: user_config_params[:key])
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class UserInitialsController < ApplicationController
|
||||||
|
before_action :load_user_config
|
||||||
|
authorize_resource :user_config
|
||||||
|
|
||||||
|
def edit; end
|
||||||
|
|
||||||
|
def update
|
||||||
|
file = params[:file]
|
||||||
|
|
||||||
|
return redirect_to settings_profile_index_path, notice: 'Unable to save initials' if file.blank?
|
||||||
|
|
||||||
|
blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
|
||||||
|
filename: file.original_filename,
|
||||||
|
content_type: file.content_type)
|
||||||
|
|
||||||
|
attachment = ActiveStorage::Attachment.create!(
|
||||||
|
blob:,
|
||||||
|
name: 'initials',
|
||||||
|
record: current_user
|
||||||
|
)
|
||||||
|
|
||||||
|
if @user_config.update(value: attachment.uuid)
|
||||||
|
redirect_to settings_profile_index_path, notice: 'Initials has been saved'
|
||||||
|
else
|
||||||
|
redirect_to settings_profile_index_path, notice: 'Unable to save initials'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@user_config.destroy
|
||||||
|
|
||||||
|
redirect_to settings_profile_index_path, notice: 'Initials has been removed'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_user_config
|
||||||
|
@user_config =
|
||||||
|
UserConfig.find_or_initialize_by(user: current_user, key: UserConfig::INITIALS_KEY)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,13 +1,20 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class UsersController < ApplicationController
|
class UsersController < ApplicationController
|
||||||
load_and_authorize_resource :user, only: %i[index edit new update destroy]
|
load_and_authorize_resource :user, only: %i[index edit update destroy]
|
||||||
|
|
||||||
before_action :build_user, only: :create
|
before_action :build_user, only: %i[new create]
|
||||||
authorize_resource :user, only: :create
|
authorize_resource :user, only: %i[new create]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@pagy, @users = pagy(@users.active.order(id: :desc))
|
@users =
|
||||||
|
if params[:status] == 'archived'
|
||||||
|
@users.archived
|
||||||
|
else
|
||||||
|
@users.active
|
||||||
|
end
|
||||||
|
|
||||||
|
@pagy, @users = pagy(@users.where(account: current_account).order(id: :desc))
|
||||||
end
|
end
|
||||||
|
|
||||||
def new; end
|
def new; end
|
||||||
@@ -15,10 +22,20 @@ class UsersController < ApplicationController
|
|||||||
def edit; end
|
def edit; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
existing_user = User.accessible_by(current_ability).find_by(email: @user.email)
|
||||||
|
|
||||||
|
if existing_user
|
||||||
|
existing_user.archived_at = nil
|
||||||
|
existing_user.assign_attributes(user_params)
|
||||||
|
existing_user.account = current_account
|
||||||
|
|
||||||
|
@user = existing_user
|
||||||
|
end
|
||||||
|
|
||||||
if @user.save
|
if @user.save
|
||||||
UserMailer.invitation_email(@user).deliver_later!
|
UserMailer.invitation_email(@user).deliver_later!
|
||||||
|
|
||||||
redirect_to settings_users_path, notice: 'User has been invited'
|
redirect_back fallback_location: settings_users_path, notice: '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_entity
|
||||||
end
|
end
|
||||||
@@ -27,11 +44,19 @@ class UsersController < ApplicationController
|
|||||||
def update
|
def update
|
||||||
return redirect_to settings_users_path, notice: 'Unable to update user.' if Docuseal.demo?
|
return redirect_to settings_users_path, notice: 'Unable to update user.' if Docuseal.demo?
|
||||||
|
|
||||||
attrs = user_params.compact_blank
|
attrs = user_params.compact_blank.merge(user_params.slice(:archived_at))
|
||||||
attrs.delete(:role) if !role_valid?(attrs[:role]) || current_user == @user
|
attrs.delete(:role) if !role_valid?(attrs[:role]) || current_user == @user
|
||||||
|
|
||||||
|
if params.dig(:user, :account_id).present?
|
||||||
|
account = Account.accessible_by(current_ability).find(params[:user][:account_id])
|
||||||
|
|
||||||
|
authorize!(:manage, account)
|
||||||
|
|
||||||
|
@user.account = account
|
||||||
|
end
|
||||||
|
|
||||||
if @user.update(attrs)
|
if @user.update(attrs)
|
||||||
redirect_to settings_users_path, notice: 'User has been updated'
|
redirect_back fallback_location: settings_users_path, notice: 'User has been updated'
|
||||||
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_entity
|
||||||
end
|
end
|
||||||
@@ -44,7 +69,7 @@ class UsersController < ApplicationController
|
|||||||
|
|
||||||
@user.update!(archived_at: Time.current)
|
@user.update!(archived_at: Time.current)
|
||||||
|
|
||||||
redirect_to settings_users_path, notice: 'User has been removed'
|
redirect_back fallback_location: settings_users_path, notice: 'User has been removed'
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -65,6 +90,11 @@ class UsersController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def user_params
|
def user_params
|
||||||
params.require(:user).permit(:email, :first_name, :last_name, :password, :role)
|
if params.key?(:user)
|
||||||
|
params.require(:user).permit(:email, :first_name, :last_name, :password,
|
||||||
|
:role, :archived_at, :account_id)
|
||||||
|
else
|
||||||
|
{}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -9,11 +9,14 @@ class VerifyPdfSignatureController < ApplicationController
|
|||||||
HexaPDF::Document.new(io: file.open)
|
HexaPDF::Document.new(io: file.open)
|
||||||
end
|
end
|
||||||
|
|
||||||
cert_data = if Docuseal.multitenant?
|
cert_data =
|
||||||
Docuseal::CERTS
|
if Docuseal.multitenant?
|
||||||
else
|
value = EncryptedConfig.find_by(account: current_account, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
|
||||||
EncryptedConfig.find_by(key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
|
|
||||||
end
|
Docuseal::CERTS.merge(value)
|
||||||
|
else
|
||||||
|
EncryptedConfig.find_by(key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
|
||||||
|
end
|
||||||
|
|
||||||
default_pkcs = GenerateCertificate.load_pkcs(cert_data)
|
default_pkcs = GenerateCertificate.load_pkcs(cert_data)
|
||||||
|
|
||||||
@@ -24,7 +27,8 @@ class VerifyPdfSignatureController < ApplicationController
|
|||||||
trusted_certs = [default_pkcs.certificate,
|
trusted_certs = [default_pkcs.certificate,
|
||||||
*default_pkcs.ca_certs,
|
*default_pkcs.ca_certs,
|
||||||
*custom_certs.map(&:certificate),
|
*custom_certs.map(&:certificate),
|
||||||
*custom_certs.flat_map(&:ca_certs).compact]
|
*custom_certs.flat_map(&:ca_certs).compact,
|
||||||
|
*Docuseal.trusted_certs]
|
||||||
|
|
||||||
render turbo_stream: turbo_stream.replace('result', partial: 'result',
|
render turbo_stream: turbo_stream.replace('result', partial: 'result',
|
||||||
locals: { pdfs:, files: params[:files], trusted_certs: })
|
locals: { pdfs:, files: params[:files], trusted_certs: })
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class WebhookPreferencesController < ApplicationController
|
||||||
|
EVENTS = %w[
|
||||||
|
form.viewed
|
||||||
|
form.started
|
||||||
|
form.completed
|
||||||
|
template.created
|
||||||
|
template.updated
|
||||||
|
submission.created
|
||||||
|
submission.archived
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
before_action :load_account_config
|
||||||
|
authorize_resource :account_config, parent: false
|
||||||
|
|
||||||
|
def create
|
||||||
|
@account_config.value[account_config_params[:event]] = account_config_params[:value] == '1'
|
||||||
|
|
||||||
|
@account_config.save!
|
||||||
|
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_account_config
|
||||||
|
@account_config =
|
||||||
|
current_account.account_configs.find_or_initialize_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)
|
||||||
|
@account_config.value ||= {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_config_params
|
||||||
|
params.permit(:event, :value)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -17,7 +17,8 @@ class WebhookSettingsController < ApplicationController
|
|||||||
def update
|
def update
|
||||||
submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last
|
submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last
|
||||||
|
|
||||||
SendFormCompletedWebhookRequestJob.perform_later(submitter)
|
SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id,
|
||||||
|
'encrypted_config_id' => @encrypted_config.id })
|
||||||
|
|
||||||
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook request has been sent.')
|
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook request has been sent.')
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import { encodeMethodIntoRequestBody } from '@hotwired/turbo-rails/app/javascrip
|
|||||||
|
|
||||||
import { createApp, reactive } from 'vue'
|
import { createApp, reactive } from 'vue'
|
||||||
import TemplateBuilder from './template_builder/builder'
|
import TemplateBuilder from './template_builder/builder'
|
||||||
|
import ImportList from './template_builder/import_list'
|
||||||
|
|
||||||
import ToggleVisible from './elements/toggle_visible'
|
import ToggleVisible from './elements/toggle_visible'
|
||||||
|
import ToggleCookies from './elements/toggle_cookies'
|
||||||
import DisableHidden from './elements/disable_hidden'
|
import DisableHidden from './elements/disable_hidden'
|
||||||
import TurboModal from './elements/turbo_modal'
|
import TurboModal from './elements/turbo_modal'
|
||||||
import FileDropzone from './elements/file_dropzone'
|
import FileDropzone from './elements/file_dropzone'
|
||||||
@@ -20,6 +22,8 @@ import FolderAutocomplete from './elements/folder_autocomplete'
|
|||||||
import SignatureForm from './elements/signature_form'
|
import SignatureForm from './elements/signature_form'
|
||||||
import SubmitForm from './elements/submit_form'
|
import SubmitForm from './elements/submit_form'
|
||||||
import PromptPassword from './elements/prompt_password'
|
import PromptPassword from './elements/prompt_password'
|
||||||
|
import EmailsTextarea from './elements/emails_textarea'
|
||||||
|
import ToggleOnSubmit from './elements/toggle_on_submit'
|
||||||
|
|
||||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||||
|
|
||||||
@@ -37,23 +41,6 @@ document.addEventListener('keyup', (e) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
window.customElements.define('toggle-visible', ToggleVisible)
|
|
||||||
window.customElements.define('disable-hidden', DisableHidden)
|
|
||||||
window.customElements.define('turbo-modal', TurboModal)
|
|
||||||
window.customElements.define('file-dropzone', FileDropzone)
|
|
||||||
window.customElements.define('menu-active', MenuActive)
|
|
||||||
window.customElements.define('clipboard-copy', ClipboardCopy)
|
|
||||||
window.customElements.define('dynamic-list', DynamicList)
|
|
||||||
window.customElements.define('download-button', DownloadButton)
|
|
||||||
window.customElements.define('set-origin-url', SetOriginUrl)
|
|
||||||
window.customElements.define('set-timezone', SetTimezone)
|
|
||||||
window.customElements.define('autoresize-textarea', AutoresizeTextarea)
|
|
||||||
window.customElements.define('submitters-autocomplete', SubmittersAutocomplete)
|
|
||||||
window.customElements.define('folder-autocomplete', FolderAutocomplete)
|
|
||||||
window.customElements.define('signature-form', SignatureForm)
|
|
||||||
window.customElements.define('submit-form', SubmitForm)
|
|
||||||
window.customElements.define('prompt-password', PromptPassword)
|
|
||||||
|
|
||||||
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
|
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
|
||||||
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
|
||||||
@@ -77,7 +64,29 @@ document.addEventListener('turbo:submit-end', async (event) => {
|
|||||||
URL.revokeObjectURL(url)
|
URL.revokeObjectURL(url)
|
||||||
})
|
})
|
||||||
|
|
||||||
window.customElements.define('template-builder', class extends HTMLElement {
|
const safeRegisterElement = (name, element, options = {}) => !window.customElements.get(name) && window.customElements.define(name, element, options)
|
||||||
|
|
||||||
|
safeRegisterElement('toggle-visible', ToggleVisible)
|
||||||
|
safeRegisterElement('disable-hidden', DisableHidden)
|
||||||
|
safeRegisterElement('turbo-modal', TurboModal)
|
||||||
|
safeRegisterElement('file-dropzone', FileDropzone)
|
||||||
|
safeRegisterElement('menu-active', MenuActive)
|
||||||
|
safeRegisterElement('clipboard-copy', ClipboardCopy)
|
||||||
|
safeRegisterElement('dynamic-list', DynamicList)
|
||||||
|
safeRegisterElement('download-button', DownloadButton)
|
||||||
|
safeRegisterElement('set-origin-url', SetOriginUrl)
|
||||||
|
safeRegisterElement('set-timezone', SetTimezone)
|
||||||
|
safeRegisterElement('autoresize-textarea', AutoresizeTextarea)
|
||||||
|
safeRegisterElement('submitters-autocomplete', SubmittersAutocomplete)
|
||||||
|
safeRegisterElement('folder-autocomplete', FolderAutocomplete)
|
||||||
|
safeRegisterElement('signature-form', SignatureForm)
|
||||||
|
safeRegisterElement('submit-form', SubmitForm)
|
||||||
|
safeRegisterElement('prompt-password', PromptPassword)
|
||||||
|
safeRegisterElement('emails-textarea', EmailsTextarea)
|
||||||
|
safeRegisterElement('toggle-cookies', ToggleCookies)
|
||||||
|
safeRegisterElement('toggle-on-submit', ToggleOnSubmit)
|
||||||
|
|
||||||
|
safeRegisterElement('template-builder', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.appElem = document.createElement('div')
|
this.appElem = document.createElement('div')
|
||||||
|
|
||||||
@@ -89,11 +98,34 @@ window.customElements.define('template-builder', class extends HTMLElement {
|
|||||||
withPhone: this.dataset.withPhone === 'true',
|
withPhone: this.dataset.withPhone === 'true',
|
||||||
withLogo: this.dataset.withLogo !== 'false',
|
withLogo: this.dataset.withLogo !== 'false',
|
||||||
editable: this.dataset.editable !== 'false',
|
editable: this.dataset.editable !== 'false',
|
||||||
|
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
||||||
withPayment: this.dataset.withPayment === 'true',
|
withPayment: this.dataset.withPayment === 'true',
|
||||||
|
isPaymentConnected: this.dataset.isPaymentConnected === 'true',
|
||||||
withFormula: this.dataset.withFormula === 'true',
|
withFormula: this.dataset.withFormula === 'true',
|
||||||
|
withConditions: this.dataset.withConditions === 'true',
|
||||||
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
||||||
acceptFileTypes: this.dataset.acceptFileTypes,
|
acceptFileTypes: this.dataset.acceptFileTypes
|
||||||
isDirectUpload: this.dataset.isDirectUpload === 'true'
|
})
|
||||||
|
|
||||||
|
this.app.mount(this.appElem)
|
||||||
|
|
||||||
|
this.appendChild(this.appElem)
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
this.app?.unmount()
|
||||||
|
this.appElem?.remove()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
safeRegisterElement('import-list', class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.appElem = document.createElement('div')
|
||||||
|
|
||||||
|
this.app = createApp(ImportList, {
|
||||||
|
template: JSON.parse(this.dataset.template),
|
||||||
|
multitenant: this.dataset.multitenant === 'true',
|
||||||
|
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content
|
||||||
})
|
})
|
||||||
|
|
||||||
this.app.mount(this.appElem)
|
this.app.mount(this.appElem)
|
||||||
|
|||||||
@@ -83,6 +83,11 @@ button[disabled] .enabled {
|
|||||||
bottom: auto;
|
bottom: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tooltip-pre:before {
|
||||||
|
white-space: pre;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
.tooltip-bottom-end:after {
|
.tooltip-bottom-end:after {
|
||||||
transform: translateX(-25%);
|
transform: translateX(-25%);
|
||||||
border-color: transparent transparent var(--tooltip-color) transparent;
|
border-color: transparent transparent var(--tooltip-color) transparent;
|
||||||
@@ -125,3 +130,7 @@ button[disabled] .enabled {
|
|||||||
outline-offset: 3px;
|
outline-offset: 3px;
|
||||||
outline-color: hsl(var(--bc) / 0.2);
|
outline-color: hsl(var(--bc) / 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
select:required:invalid {
|
||||||
|
color: gray !important;
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import SignaturePad from 'signature_pad'
|
||||||
|
import { cropCanvasAndExportToPNG } from './submission_form/crop_canvas'
|
||||||
|
|
||||||
|
window.customElements.define('draw-signature', class extends HTMLElement {
|
||||||
|
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)
|
||||||
|
|
||||||
|
this.pad = new SignaturePad(this.canvas)
|
||||||
|
|
||||||
|
this.pad.addEventListener('endStroke', () => {
|
||||||
|
this.updateSubmitButtonVisibility()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.clearButton.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.clearSignaturePad()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.form.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.submitButton.disabled = true
|
||||||
|
|
||||||
|
this.submitImage().then((data) => {
|
||||||
|
this.valueInput.value = data.uuid
|
||||||
|
|
||||||
|
return fetch(this.form.action, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: new FormData(this.form)
|
||||||
|
}).then((response) => {
|
||||||
|
this.form.classList.add('hidden')
|
||||||
|
this.success.classList.remove('hidden')
|
||||||
|
|
||||||
|
return response
|
||||||
|
})
|
||||||
|
}).finally(() => {
|
||||||
|
this.submitButton.disabled = false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSignaturePad () {
|
||||||
|
this.pad.clear()
|
||||||
|
this.updateSubmitButtonVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSubmitButtonVisibility () {
|
||||||
|
if (this.pad.isEmpty()) {
|
||||||
|
this.submitButton.style.display = 'none'
|
||||||
|
this.placeholderButton.style.display = 'block'
|
||||||
|
} else {
|
||||||
|
this.submitButton.style.display = 'block'
|
||||||
|
this.placeholderButton.style.display = 'none'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async submitImage () {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
cropCanvasAndExportToPNG(this.canvas, { errorOnTooSmall: true }).then(async (blob) => {
|
||||||
|
const file = new File([blob], 'signature.png', { type: 'image/png' })
|
||||||
|
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
formData.append('file', file)
|
||||||
|
formData.append('submitter_slug', this.dataset.slug)
|
||||||
|
formData.append('name', 'attachments')
|
||||||
|
formData.append('remember_signature', 'true')
|
||||||
|
|
||||||
|
return fetch('/api/attachments', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
}).then((resp) => resp.json()).then((attachment) => {
|
||||||
|
return resolve(attachment)
|
||||||
|
})
|
||||||
|
}).catch((error) => {
|
||||||
|
return reject(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get submitButton () {
|
||||||
|
return this.querySelector('button[type="submit"]')
|
||||||
|
}
|
||||||
|
|
||||||
|
get clearButton () {
|
||||||
|
return this.querySelector('button[aria-label="Clear"]')
|
||||||
|
}
|
||||||
|
|
||||||
|
get placeholderButton () {
|
||||||
|
return this.querySelector('button[disabled]')
|
||||||
|
}
|
||||||
|
|
||||||
|
get canvas () {
|
||||||
|
return this.querySelector('canvas')
|
||||||
|
}
|
||||||
|
|
||||||
|
get valueInput () {
|
||||||
|
return this.querySelector('input[name^="values"]')
|
||||||
|
}
|
||||||
|
|
||||||
|
get form () {
|
||||||
|
return this.querySelector('form')
|
||||||
|
}
|
||||||
|
|
||||||
|
get success () {
|
||||||
|
return this.querySelector('#success')
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -7,7 +7,7 @@ export default class extends HTMLElement {
|
|||||||
|
|
||||||
resize () {
|
resize () {
|
||||||
if (this.textarea.clientHeight < this.textarea.scrollHeight) {
|
if (this.textarea.clientHeight < this.textarea.scrollHeight) {
|
||||||
this.textarea.style.height = `${this.textarea.scrollHeight}px`
|
this.textarea.style.height = `${this.textarea.scrollHeight + 5}px`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,13 +17,18 @@ export default targetable(class extends HTMLElement {
|
|||||||
|
|
||||||
this.toggleState()
|
this.toggleState()
|
||||||
|
|
||||||
fetch(this.dataset.src).then((response) => response.json()).then((urls) => {
|
fetch(this.dataset.src).then(async (response) => {
|
||||||
const isSafariIos = /iPhone|iPad|iPod/i.test(navigator.userAgent)
|
if (response.ok) {
|
||||||
|
const urls = await response.json()
|
||||||
|
const isSafariIos = /iPhone|iPad|iPod/i.test(navigator.userAgent)
|
||||||
|
|
||||||
if (isSafariIos && urls.length > 1) {
|
if (isSafariIos && urls.length > 1) {
|
||||||
this.downloadSafariIos(urls)
|
this.downloadSafariIos(urls)
|
||||||
|
} else {
|
||||||
|
this.downloadUrls(urls)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.downloadUrls(urls)
|
alert('Failed to download files')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -48,9 +53,9 @@ export default targetable(class extends HTMLElement {
|
|||||||
fileRequests.reduce(
|
fileRequests.reduce(
|
||||||
(prevPromise, request) => prevPromise.then(() => request()),
|
(prevPromise, request) => prevPromise.then(() => request()),
|
||||||
Promise.resolve()
|
Promise.resolve()
|
||||||
)
|
).finally(() => {
|
||||||
|
this.toggleState()
|
||||||
this.toggleState()
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadSafariIos (urls) {
|
downloadSafariIos (urls) {
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
const emailRegexp = /([^@;,<>\s]+@[^@;,<>\s]+)/g
|
||||||
|
|
||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
if (this.dataset.limit) {
|
||||||
|
this.textarea.addEventListener('input', () => {
|
||||||
|
const emails = this.textarea.value.match(emailRegexp) || []
|
||||||
|
|
||||||
|
this.updateCounter(emails.length)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCounter (count) {
|
||||||
|
let counter = document.getElementById('emails_counter')
|
||||||
|
let bulkMessage = document.getElementById('bulk_message')
|
||||||
|
|
||||||
|
if (count < 2) {
|
||||||
|
counter?.remove()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((count + 10) > this.dataset.limit) {
|
||||||
|
if (!counter) {
|
||||||
|
counter = document.createElement('span')
|
||||||
|
|
||||||
|
counter.id = 'emails_counter'
|
||||||
|
counter.classList.add('text-xs', 'right-0', 'absolute')
|
||||||
|
counter.style.bottom = '-15px'
|
||||||
|
|
||||||
|
this.textarea.parentNode.append(counter)
|
||||||
|
}
|
||||||
|
|
||||||
|
counter.innerText = `${count} / ${this.dataset.limit}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.dataset.bulkEnabled !== 'true') {
|
||||||
|
if (!bulkMessage) {
|
||||||
|
bulkMessage = document.createElement('span')
|
||||||
|
|
||||||
|
bulkMessage.id = 'bulk_message'
|
||||||
|
bulkMessage.classList.add('text-xs', 'left-0', 'absolute')
|
||||||
|
bulkMessage.style.bottom = '-15px'
|
||||||
|
|
||||||
|
this.textarea.parentNode.append(bulkMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
bulkMessage.innerHTML = '<a class="link" data-turbo="false" href="/upgrade">Upgrade</a> to bulk send multiple recipients'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get textarea () {
|
||||||
|
return this.querySelector('textarea')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,10 +9,6 @@ export default actionable(targetable(class extends HTMLElement {
|
|||||||
]
|
]
|
||||||
|
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
if (this.dataset.isDirectUpload === 'true') {
|
|
||||||
import('@rails/activestorage')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addEventListener('drop', this.onDrop)
|
this.addEventListener('drop', this.onDrop)
|
||||||
|
|
||||||
this.addEventListener('dragover', (e) => e.preventDefault())
|
this.addEventListener('dragover', (e) => e.preventDefault())
|
||||||
@@ -48,61 +44,11 @@ export default actionable(targetable(class extends HTMLElement {
|
|||||||
this.classList.toggle('opacity-50')
|
this.classList.toggle('opacity-50')
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadFiles (files) {
|
uploadFiles () {
|
||||||
this.toggleLoading()
|
this.toggleLoading()
|
||||||
|
|
||||||
if (this.dataset.isDirectUpload === 'true') {
|
if (this.dataset.submitOnUpload) {
|
||||||
const { DirectUpload } = await import('@rails/activestorage')
|
this.closest('form').querySelector('button[type="submit"]').click()
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
Array.from(files).map(async (file) => {
|
|
||||||
const upload = new DirectUpload(
|
|
||||||
file,
|
|
||||||
'/direct_uploads',
|
|
||||||
this.input
|
|
||||||
)
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
upload.create((error, blob) => {
|
|
||||||
if (error) {
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
return reject(error)
|
|
||||||
} else {
|
|
||||||
return resolve(blob)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error(error)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
).then((blobs) => {
|
|
||||||
if (this.dataset.submitOnUpload) {
|
|
||||||
this.querySelectorAll('[name="blob_signed_ids[]"]').forEach((e) => e.remove())
|
|
||||||
}
|
|
||||||
|
|
||||||
blobs.forEach((blob) => {
|
|
||||||
const input = document.createElement('input')
|
|
||||||
|
|
||||||
input.type = 'hidden'
|
|
||||||
input.name = 'blob_signed_ids[]'
|
|
||||||
input.value = blob.signed_id
|
|
||||||
|
|
||||||
this.append(input)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (this.dataset.submitOnUpload === 'true') {
|
|
||||||
this.closest('form').querySelector('button[type="submit"]').click()
|
|
||||||
}
|
|
||||||
}).finally(() => {
|
|
||||||
if (this.dataset.submitOnUpload !== 'true') {
|
|
||||||
this.toggleLoading()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
if (this.dataset.submitOnUpload) {
|
|
||||||
this.closest('form').querySelector('button[type="submit"]').click()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
@@ -20,7 +20,7 @@ export default class extends HTMLElement {
|
|||||||
fetch = (text, resolve) => {
|
fetch = (text, resolve) => {
|
||||||
const queryParams = new URLSearchParams({ q: text })
|
const queryParams = new URLSearchParams({ q: text })
|
||||||
|
|
||||||
fetch('/api/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()
|
||||||
|
|
||||||
resolve(items)
|
resolve(items)
|
||||||
|
|||||||
@@ -5,8 +5,12 @@ export default targetable(class extends HTMLElement {
|
|||||||
static [target.static] = ['canvas', 'input', 'clear', 'button']
|
static [target.static] = ['canvas', 'input', 'clear', 'button']
|
||||||
|
|
||||||
async connectedCallback () {
|
async connectedCallback () {
|
||||||
this.canvas.width = this.canvas.parentNode.parentNode.clientWidth
|
const scale = 3
|
||||||
this.canvas.height = this.canvas.parentNode.parentNode.clientWidth / 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')
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export default class extends HTMLElement {
|
|||||||
preventSubmit: 1,
|
preventSubmit: 1,
|
||||||
minLength: 1,
|
minLength: 1,
|
||||||
showOnFocus: true,
|
showOnFocus: true,
|
||||||
|
debounceWaitMs: 200,
|
||||||
onSelect: this.onSelect,
|
onSelect: this.onSelect,
|
||||||
render: this.render,
|
render: this.render,
|
||||||
fetch: this.fetch
|
fetch: this.fetch
|
||||||
@@ -27,6 +28,8 @@ export default class extends HTMLElement {
|
|||||||
|
|
||||||
if (textarea && item[field]) {
|
if (textarea && item[field]) {
|
||||||
textarea.value = textarea.value.replace(/[^;,\s]+$/, item[field] + ' ')
|
textarea.value = textarea.value.replace(/[^;,\s]+$/, item[field] + ' ')
|
||||||
|
|
||||||
|
textarea.dispatchEvent(new Event('input', { bubbles: true }))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -37,16 +40,16 @@ export default class extends HTMLElement {
|
|||||||
if (q) {
|
if (q) {
|
||||||
const queryParams = new URLSearchParams({ q, field: this.dataset.field })
|
const queryParams = new URLSearchParams({ q, field: this.dataset.field })
|
||||||
|
|
||||||
fetch('/api/submitters_autocomplete?' + queryParams).then(async (resp) => {
|
this.currentFetch ||= fetch('/submitters_autocomplete?' + queryParams)
|
||||||
|
|
||||||
|
this.currentFetch.then(async (resp) => {
|
||||||
const items = await resp.json()
|
const items = await resp.json()
|
||||||
|
|
||||||
if (q.length < 3) {
|
resolve(items)
|
||||||
resolve(items.filter((e) => e[this.dataset.field].startsWith(q)))
|
|
||||||
} else {
|
|
||||||
resolve(items)
|
|
||||||
}
|
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
resolve([])
|
resolve([])
|
||||||
|
}).finally(() => {
|
||||||
|
this.currentFetch = null
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
resolve([])
|
resolve([])
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.button.addEventListener('click', () => {
|
||||||
|
const expirationDate = new Date()
|
||||||
|
|
||||||
|
expirationDate.setFullYear(expirationDate.getFullYear() + 10)
|
||||||
|
|
||||||
|
const expires = expirationDate.toUTCString()
|
||||||
|
|
||||||
|
document.cookie = this.dataset.key + '=' + this.dataset.value + '; expires=' + expires + '; path=/'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get button () {
|
||||||
|
return this.querySelector('button')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
document.addEventListener('turbo:submit-end', this.onSubmitEnd)
|
||||||
|
|
||||||
|
this.form.addEventListener('submit', this.onSubmit)
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
document.removeEventListener('turbo:submit-end', this.onSubmitEnd)
|
||||||
|
|
||||||
|
this.form.removeEventListener('submit', this.onSubmit)
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit = () => {
|
||||||
|
this.element.classList.add('invisible')
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmitEnd = (event) => {
|
||||||
|
if (event.target === this.form) {
|
||||||
|
const resp = event.detail?.formSubmission?.result?.fetchResponse?.response
|
||||||
|
|
||||||
|
if (resp?.status / 100 === 2) {
|
||||||
|
this.element.classList.remove('invisible')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get element () {
|
||||||
|
return document.getElementById(this.dataset.elementId)
|
||||||
|
}
|
||||||
|
|
||||||
|
get form () {
|
||||||
|
return this.closest('form')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.button.disabled = false
|
||||||
|
|
||||||
|
this.form.addEventListener('submit', () => {
|
||||||
|
this.button.disabled = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
this.button.disabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
get button () {
|
||||||
|
return this.querySelector('[type="submit"]')
|
||||||
|
}
|
||||||
|
|
||||||
|
get form () {
|
||||||
|
return this.querySelector('form') || this.closest('form')
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
-4
@@ -2,23 +2,36 @@ import { createApp, reactive } from 'vue'
|
|||||||
|
|
||||||
import Form from './submission_form/form'
|
import Form from './submission_form/form'
|
||||||
import DownloadButton from './elements/download_button'
|
import DownloadButton from './elements/download_button'
|
||||||
|
import ToggleSubmit from './elements/toggle_submit'
|
||||||
|
|
||||||
window.customElements.define('download-button', DownloadButton)
|
const safeRegisterElement = (name, element, options = {}) => !window.customElements.get(name) && window.customElements.define(name, element, options)
|
||||||
window.customElements.define('submission-form', class extends HTMLElement {
|
|
||||||
|
safeRegisterElement('download-button', DownloadButton)
|
||||||
|
safeRegisterElement('toggle-submit', ToggleSubmit)
|
||||||
|
safeRegisterElement('submission-form', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.appElem = document.createElement('div')
|
this.appElem = document.createElement('div')
|
||||||
|
|
||||||
this.app = createApp(Form, {
|
this.app = createApp(Form, {
|
||||||
submitter: JSON.parse(this.dataset.submitter),
|
submitter: JSON.parse(this.dataset.submitter),
|
||||||
canSendEmail: this.dataset.canSendEmail === 'true',
|
canSendEmail: this.dataset.canSendEmail === 'true',
|
||||||
isDirectUpload: this.dataset.isDirectUpload === 'true',
|
previousSignatureValue: this.dataset.previousSignatureValue,
|
||||||
goToLast: this.dataset.goToLast === 'true',
|
goToLast: this.dataset.goToLast === 'true',
|
||||||
isDemo: this.dataset.isDemo === 'true',
|
isDemo: this.dataset.isDemo === 'true',
|
||||||
attribution: this.dataset.attribution !== 'false',
|
attribution: this.dataset.attribution !== 'false',
|
||||||
|
scrollPadding: this.dataset.scrollPadding || '-80px',
|
||||||
|
dryRun: this.dataset.dryRun === 'true',
|
||||||
|
expand: ['true', 'false'].includes(this.dataset.expand) ? this.dataset.expand === 'true' : null,
|
||||||
|
withSignatureId: this.dataset.withSignatureId === 'true',
|
||||||
withConfetti: this.dataset.withConfetti !== 'false',
|
withConfetti: this.dataset.withConfetti !== 'false',
|
||||||
|
withDisclosure: this.dataset.withDisclosure === 'true',
|
||||||
withTypedSignature: this.dataset.withTypedSignature !== 'false',
|
withTypedSignature: this.dataset.withTypedSignature !== 'false',
|
||||||
|
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
||||||
|
rememberSignature: this.dataset.rememberSignature === 'true',
|
||||||
values: reactive(JSON.parse(this.dataset.values)),
|
values: reactive(JSON.parse(this.dataset.values)),
|
||||||
completedButton: JSON.parse(this.dataset.completedButton),
|
completedButton: JSON.parse(this.dataset.completedButton || '{}'),
|
||||||
|
withQrButton: true,
|
||||||
|
completedMessage: JSON.parse(this.dataset.completedMessage || '{}'),
|
||||||
completedRedirectUrl: this.dataset.completedRedirectUrl,
|
completedRedirectUrl: this.dataset.completedRedirectUrl,
|
||||||
attachments: reactive(JSON.parse(this.dataset.attachments)),
|
attachments: reactive(JSON.parse(this.dataset.attachments)),
|
||||||
fields: JSON.parse(this.dataset.fields)
|
fields: JSON.parse(this.dataset.fields)
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import Rollbar from 'rollbar/dist/rollbar.umd'
|
||||||
|
|
||||||
|
const token = document.querySelector('meta[name="rollbar-token"]')?.getAttribute('content')
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
window.Rollbar ||= new Rollbar({
|
||||||
|
accessToken: token,
|
||||||
|
captureUncaught: true,
|
||||||
|
captureUnhandledRejections: true,
|
||||||
|
captureIp: false,
|
||||||
|
autoInstrument: false,
|
||||||
|
ignoredMessages: [
|
||||||
|
/Failed to fetch/i,
|
||||||
|
/NetworkError/i,
|
||||||
|
/Load failed/i,
|
||||||
|
/Clipboard write is not allowed/i
|
||||||
|
],
|
||||||
|
transform (payload) {
|
||||||
|
payload.body.telemetry = []
|
||||||
|
|
||||||
|
const string = JSON.stringify(payload)
|
||||||
|
|
||||||
|
return JSON.parse(string.replace(/(\/[des]\/)\w{6}/g, (_, m) => m))
|
||||||
|
},
|
||||||
|
payload: {
|
||||||
|
client: {
|
||||||
|
javascript: {
|
||||||
|
source_map_enabled: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
environment: 'production'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex absolute lg:text-base -outline-offset-1"
|
class="field-area flex absolute lg:text-base -outline-offset-1"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
:style="computedStyle"
|
:style="computedStyle"
|
||||||
:class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'outline-red-500 outline-dashed outline-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
|
:class="{ 'text-[1.6vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'field-area-active outline-red-500 outline-dashed outline-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="!isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
|
v-if="(!withFieldPlaceholder || !field.name || field.type === 'cells') && !isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
|
||||||
class="absolute top-0 bottom-0 right-0 left-0 items-center justify-center h-full w-full"
|
class="absolute top-0 bottom-0 right-0 left-0 items-center justify-center h-full w-full"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -41,7 +41,8 @@
|
|||||||
<div
|
<div
|
||||||
v-if="isActive"
|
v-if="isActive"
|
||||||
ref="scrollToElem"
|
ref="scrollToElem"
|
||||||
class="absolute -top-20"
|
class="absolute"
|
||||||
|
:style="{ top: scrollPadding }"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
v-if="field.type === 'image' && image"
|
v-if="field.type === 'image' && image"
|
||||||
@@ -53,18 +54,44 @@
|
|||||||
class="object-contain mx-auto"
|
class="object-contain mx-auto"
|
||||||
:src="stamp.url"
|
:src="stamp.url"
|
||||||
>
|
>
|
||||||
<img
|
<div
|
||||||
v-else-if="field.type === 'signature' && signature"
|
v-else-if="field.type === 'signature' && signature"
|
||||||
class="object-contain mx-auto"
|
class="flex flex-col justify-between h-full overflow-hidden"
|
||||||
:src="signature.url"
|
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
class="flex-grow flex overflow-hidden"
|
||||||
|
style="min-height: 50%"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="object-contain mx-auto"
|
||||||
|
:src="signature.url"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="withSignatureId"
|
||||||
|
class="w-full mt-1 text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem]"
|
||||||
|
>
|
||||||
|
<div class="truncate uppercase">
|
||||||
|
ID: {{ signature.uuid }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ t('reason') }}: {{ t('digitally_signed_by') }} {{ submitter.name }}
|
||||||
|
<template v-if="submitter.email">
|
||||||
|
<{{ submitter.email }}>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ new Date(signature.created_at).toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', timeZoneName: 'short' }) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<img
|
<img
|
||||||
v-else-if="field.type === 'initials' && initials"
|
v-else-if="field.type === 'initials' && initials"
|
||||||
class="object-contain mx-auto"
|
class="object-contain mx-auto"
|
||||||
:src="initials.url"
|
:src="initials.url"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-else-if="field.type === 'file' || field.type === 'payment'"
|
v-else-if="(field.type === 'file' || field.type === 'payment') && attachments.length"
|
||||||
class="px-0.5 flex flex-col justify-center"
|
class="px-0.5 flex flex-col justify-center"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@@ -74,7 +101,7 @@
|
|||||||
:href="attachment.url"
|
:href="attachment.url"
|
||||||
>
|
>
|
||||||
<IconPaperclip
|
<IconPaperclip
|
||||||
class="inline w-[1.5vw] h-[1.5vw] lg:w-4 lg:h-4"
|
class="inline w-[1.6vw] h-[1.6vw] lg:w-4 lg:h-4"
|
||||||
/>
|
/>
|
||||||
{{ attachment.filename }}
|
{{ attachment.filename }}
|
||||||
</a>
|
</a>
|
||||||
@@ -157,16 +184,26 @@
|
|||||||
ref="textContainer"
|
ref="textContainer"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="flex items-center px-0.5 w-full"
|
class="flex items-center px-0.5 w-full"
|
||||||
|
:class="alignClasses[field.preferences?.align]"
|
||||||
>
|
>
|
||||||
<span v-if="Array.isArray(modelValue)">
|
<span
|
||||||
|
v-if="field && field.name && withFieldPlaceholder && !modelValue && modelValue !== 0"
|
||||||
|
class="whitespace-pre-wrap text-gray-400"
|
||||||
|
:class="{ 'w-full': field.preferences?.align }"
|
||||||
|
>{{ field.name }}</span>
|
||||||
|
<span v-else-if="Array.isArray(modelValue)">
|
||||||
{{ modelValue.join(', ') }}
|
{{ modelValue.join(', ') }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="field.type === 'date'">
|
<span v-else-if="field.type === 'date'">
|
||||||
{{ formattedDate }}
|
{{ formattedDate }}
|
||||||
</span>
|
</span>
|
||||||
|
<span v-else-if="field.type === 'number'">
|
||||||
|
{{ formatNumber(modelValue, field.preferences?.format) }}
|
||||||
|
</span>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
class="whitespace-pre-wrap"
|
class="whitespace-pre-wrap"
|
||||||
|
:class="{ 'w-full': field.preferences?.align }"
|
||||||
>{{ modelValue }}</span>
|
>{{ modelValue }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -187,11 +224,30 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
submitter: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
withSignatureId: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
isValueSet: {
|
isValueSet: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
scrollPadding: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '-80px'
|
||||||
|
},
|
||||||
|
withFieldPlaceholder: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
submittable: {
|
submittable: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -253,6 +309,13 @@ export default {
|
|||||||
phone: this.t('phone')
|
phone: this.t('phone')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
alignClasses () {
|
||||||
|
return {
|
||||||
|
center: 'text-center',
|
||||||
|
left: 'text-left',
|
||||||
|
right: 'text-right'
|
||||||
|
}
|
||||||
|
},
|
||||||
option () {
|
option () {
|
||||||
return this.field.options.find((o) => o.uuid === this.area.option_uuid)
|
return this.field.options.find((o) => o.uuid === this.area.option_uuid)
|
||||||
},
|
},
|
||||||
@@ -328,27 +391,35 @@ export default {
|
|||||||
computedStyle () {
|
computedStyle () {
|
||||||
const { x, y, w, h } = this.area
|
const { x, y, w, h } = this.area
|
||||||
|
|
||||||
return {
|
const style = {
|
||||||
top: y * 100 + '%',
|
top: y * 100 + '%',
|
||||||
left: x * 100 + '%',
|
left: x * 100 + '%',
|
||||||
width: w * 100 + '%',
|
width: w * 100 + '%',
|
||||||
height: h * 100 + '%'
|
height: h * 100 + '%'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.field.preferences?.font_size) {
|
||||||
|
style.fontSize = this.field.preferences.font_size + 'pt'
|
||||||
|
}
|
||||||
|
|
||||||
|
return style
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
modelValue () {
|
modelValue () {
|
||||||
if (this.field.type === 'text' && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > this.modelValue.length)) {
|
this.$nextTick(() => {
|
||||||
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? this.modelValue.length : 0
|
if (['date', 'text', 'number'].includes(this.field.type) && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.modelValue}`.length)) {
|
||||||
}
|
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? `${this.modelValue || (this.withFieldPlaceholder ? this.field.name : '')}`.length : 0
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
if (this.field.type === 'text' && this.$refs.textContainer) {
|
this.$nextTick(() => {
|
||||||
this.$nextTick(() => {
|
if (['date', 'text', 'number'].includes(this.field.type) && this.$refs.textContainer) {
|
||||||
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? this.modelValue.length : 0
|
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? `${this.modelValue || (this.withFieldPlaceholder ? this.field.name : '')}`.length : 0
|
||||||
})
|
}
|
||||||
}
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
optionValue (option) {
|
optionValue (option) {
|
||||||
@@ -362,6 +433,17 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
formatNumber (number, format) {
|
||||||
|
if (format === 'comma') {
|
||||||
|
return new Intl.NumberFormat('en-US').format(number)
|
||||||
|
} else if (format === 'dot') {
|
||||||
|
return new Intl.NumberFormat('de-DE').format(number)
|
||||||
|
} else if (format === 'space') {
|
||||||
|
return new Intl.NumberFormat('fr-FR').format(number)
|
||||||
|
} else {
|
||||||
|
return number
|
||||||
|
}
|
||||||
|
},
|
||||||
formatDate (date, format) {
|
formatDate (date, format) {
|
||||||
const monthFormats = {
|
const monthFormats = {
|
||||||
M: 'numeric',
|
M: 'numeric',
|
||||||
@@ -377,6 +459,7 @@ export default {
|
|||||||
|
|
||||||
const yearFormats = {
|
const yearFormats = {
|
||||||
YYYY: 'numeric',
|
YYYY: 'numeric',
|
||||||
|
YYY: 'numeric',
|
||||||
YY: '2-digit'
|
YY: '2-digit'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,12 @@
|
|||||||
:area="area"
|
:area="area"
|
||||||
:submittable="true"
|
:submittable="true"
|
||||||
:field-index="fieldIndex"
|
:field-index="fieldIndex"
|
||||||
|
:scroll-padding="scrollPadding"
|
||||||
|
:submitter="submitter"
|
||||||
|
:with-field-placeholder="withFieldPlaceholder"
|
||||||
|
:with-signature-id="withSignatureId"
|
||||||
:is-active="currentStep === step"
|
:is-active="currentStep === step"
|
||||||
:with-label="withLabel"
|
:with-label="withLabel && !withFieldPlaceholder"
|
||||||
:is-value-set="step.some((f) => f.uuid in values)"
|
:is-value-set="step.some((f) => f.uuid in values)"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
@click="$emit('focus-step', step)"
|
@click="$emit('focus-step', step)"
|
||||||
@@ -48,11 +52,30 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
withSignatureId: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
submitter: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
values: {
|
values: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
},
|
},
|
||||||
|
withFieldPlaceholder: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
scrollPadding: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '-80px'
|
||||||
|
},
|
||||||
attachmentsIndex: {
|
attachmentsIndex: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -63,6 +86,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
scrollEl: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
currentStep: {
|
currentStep: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -97,7 +125,12 @@ export default {
|
|||||||
if (container.style.overflow === 'hidden') {
|
if (container.style.overflow === 'hidden') {
|
||||||
this.scrollInContainer(areaRef.$el)
|
this.scrollInContainer(areaRef.$el)
|
||||||
} else {
|
} else {
|
||||||
areaRef.$refs.scrollToElem.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
const targetRect = areaRef.$refs.scrollToElem.getBoundingClientRect()
|
||||||
|
const root = this.$root.$el?.parentNode?.classList?.contains('ds') ? this.$root.$el : document.body
|
||||||
|
const rootRect = root.getBoundingClientRect()
|
||||||
|
const scrollEl = this.scrollEl || window
|
||||||
|
|
||||||
|
scrollEl.scrollTo({ top: targetRect.top - rootRect.top, behavior: 'smooth' })
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -116,7 +149,7 @@ export default {
|
|||||||
|
|
||||||
const targetTopRelativeToBox = targetRect.top - boxRect.top
|
const targetTopRelativeToBox = targetRect.top - boxRect.top
|
||||||
|
|
||||||
scrollbox.scrollTop = targetTopRelativeToBox - container.offsetHeight + formContainer.offsetHeight + target.offsetHeight + padding
|
scrollbox.scrollTo({ top: targetTopRelativeToBox - container.offsetHeight + formContainer.offsetHeight + target.offsetHeight + padding, behavior: 'smooth' })
|
||||||
},
|
},
|
||||||
setAreaRef (el) {
|
setAreaRef (el) {
|
||||||
if (el) {
|
if (el) {
|
||||||
|
|||||||
@@ -41,11 +41,18 @@
|
|||||||
:name="`values[${field.uuid}][]`"
|
:name="`values[${field.uuid}][]`"
|
||||||
>
|
>
|
||||||
</template>
|
</template>
|
||||||
|
<div
|
||||||
|
v-if="field.description && !modelValue.length"
|
||||||
|
dir="auto"
|
||||||
|
class="mb-3 px-1"
|
||||||
|
>
|
||||||
|
<MarkdownContent :string="field.description" />
|
||||||
|
</div>
|
||||||
<FileDropzone
|
<FileDropzone
|
||||||
:message="`${t('upload')} ${field.name || t('files')}${field.required ? '' : ` (${t('optional')})`}`"
|
:message="`${t('upload')} ${field.name || t('files')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
:is-direct-upload="isDirectUpload"
|
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
|
:dry-run="dryRun"
|
||||||
@upload="onUpload"
|
@upload="onUpload"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,12 +60,14 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import FileDropzone from './dropzone'
|
import FileDropzone from './dropzone'
|
||||||
|
import MarkdownContent from './markdown_content'
|
||||||
import { IconPaperclip, IconTrashX } from '@tabler/icons-vue'
|
import { IconPaperclip, IconTrashX } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AttachmentStep',
|
name: 'AttachmentStep',
|
||||||
components: {
|
components: {
|
||||||
FileDropzone,
|
FileDropzone,
|
||||||
|
MarkdownContent,
|
||||||
IconPaperclip,
|
IconPaperclip,
|
||||||
IconTrashX
|
IconTrashX
|
||||||
},
|
},
|
||||||
@@ -72,16 +81,16 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
dryRun: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
attachmentsIndex: {
|
attachmentsIndex: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
},
|
},
|
||||||
isDirectUpload: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -3,20 +3,29 @@
|
|||||||
class="mx-auto max-w-md flex flex-col"
|
class="mx-auto max-w-md flex flex-col"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
>
|
>
|
||||||
<p class="font-medium text-2xl flex items-center space-x-1.5 mx-auto">
|
<div class="font-medium text-2xl flex items-center space-x-1.5 mx-auto">
|
||||||
<IconCircleCheck
|
<IconCircleCheck
|
||||||
class="inline text-green-600"
|
class="inline text-green-600"
|
||||||
:width="30"
|
:width="30"
|
||||||
:height="30"
|
:height="30"
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
{{ t('form_has_been_completed') }}
|
{{ completedMessage.title || t('form_has_been_completed') }}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="completedMessage.body"
|
||||||
|
class="mt-2"
|
||||||
|
>
|
||||||
|
<MarkdownContent
|
||||||
|
:string="completedMessage.body"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<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="completedButton.url"
|
:href="sanitizeHref(completedButton.url)"
|
||||||
|
rel="noopener noreferrer nofollow"
|
||||||
class="white-button flex items-center w-full"
|
class="white-button flex items-center w-full"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
@@ -91,10 +100,12 @@
|
|||||||
|
|
||||||
<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'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FormCompleted',
|
name: 'FormCompleted',
|
||||||
components: {
|
components: {
|
||||||
|
MarkdownContent,
|
||||||
IconCircleCheck,
|
IconCircleCheck,
|
||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
IconBrandGithub,
|
IconBrandGithub,
|
||||||
@@ -142,6 +153,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
|
},
|
||||||
|
completedMessage: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
@@ -181,13 +197,18 @@ export default {
|
|||||||
download () {
|
download () {
|
||||||
this.isDownloading = true
|
this.isDownloading = true
|
||||||
|
|
||||||
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`).then((response) => response.json()).then((urls) => {
|
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`).then(async (response) => {
|
||||||
const isSafariIos = /iPhone|iPad|iPod/i.test(navigator.userAgent)
|
if (response.ok) {
|
||||||
|
const urls = await response.json()
|
||||||
|
const isSafariIos = /iPhone|iPad|iPod/i.test(navigator.userAgent)
|
||||||
|
|
||||||
if (isSafariIos && urls.length > 1) {
|
if (isSafariIos && urls.length > 1) {
|
||||||
this.downloadSafariIos(urls)
|
this.downloadSafariIos(urls)
|
||||||
|
} else {
|
||||||
|
this.downloadUrls(urls)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.downloadUrls(urls)
|
alert('Failed to download files')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -211,9 +232,14 @@ export default {
|
|||||||
fileRequests.reduce(
|
fileRequests.reduce(
|
||||||
(prevPromise, request) => prevPromise.then(() => request()),
|
(prevPromise, request) => prevPromise.then(() => request()),
|
||||||
Promise.resolve()
|
Promise.resolve()
|
||||||
)
|
).finally(() => {
|
||||||
|
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) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
function cropCanvasAndExportToPNG (canvas) {
|
function cropCanvasAndExportToPNG (canvas, { errorOnTooSmall } = { errorOnTooSmall: false }) {
|
||||||
const ctx = canvas.getContext('2d')
|
const ctx = canvas.getContext('2d')
|
||||||
|
|
||||||
const width = canvas.width
|
const width = canvas.width
|
||||||
@@ -33,6 +33,10 @@ function cropCanvasAndExportToPNG (canvas) {
|
|||||||
croppedCanvas.height = croppedHeight
|
croppedCanvas.height = croppedHeight
|
||||||
const croppedCtx = croppedCanvas.getContext('2d')
|
const croppedCtx = croppedCanvas.getContext('2d')
|
||||||
|
|
||||||
|
if (errorOnTooSmall && (croppedWidth < 20 || croppedHeight < 20)) {
|
||||||
|
return Promise.reject(new Error('Image too small'))
|
||||||
|
}
|
||||||
|
|
||||||
croppedCtx.drawImage(canvas, leftmost, topmost, croppedWidth, croppedHeight, 0, 0, croppedWidth, croppedHeight)
|
croppedCtx.drawImage(canvas, leftmost, topmost, croppedWidth, croppedHeight, 0, 0, croppedWidth, croppedHeight)
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|||||||
@@ -1,20 +1,39 @@
|
|||||||
<template>
|
<template>
|
||||||
<div dir="auto">
|
<div dir="auto">
|
||||||
<div class="flex justify-between items-center w-full mb-2">
|
<div
|
||||||
|
class="flex justify-between items-center w-full"
|
||||||
|
:class="{ 'mb-2': !field.description }"
|
||||||
|
>
|
||||||
<label
|
<label
|
||||||
:for="field.uuid"
|
:for="field.uuid"
|
||||||
class="label text-2xl"
|
class="label text-2xl"
|
||||||
>{{ field.name && showFieldNames ? field.name : t('date') }}
|
>
|
||||||
<template v-if="!field.required">({{ t('optional') }})</template>
|
<MarkdownContent
|
||||||
|
v-if="field.title"
|
||||||
|
:string="field.title"
|
||||||
|
/>
|
||||||
|
<template v-else>
|
||||||
|
{{ field.name && showFieldNames ? field.name : t('date') }}
|
||||||
|
<template v-if="!field.required">
|
||||||
|
({{ t('optional') }})
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline btn-sm !normal-case font-normal"
|
class="btn btn-outline btn-sm !normal-case font-normal"
|
||||||
@click.prevent="setCurrentDate"
|
@click.prevent="[setCurrentDate(), $emit('focus')]"
|
||||||
>
|
>
|
||||||
<IconCalendarCheck :width="16" />
|
<IconCalendarCheck :width="16" />
|
||||||
{{ t('set_today') }}
|
{{ t('set_today') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="field.description"
|
||||||
|
class="mb-3 px-1"
|
||||||
|
dir="auto"
|
||||||
|
>
|
||||||
|
<MarkdownContent :string="field.description" />
|
||||||
|
</div>
|
||||||
<AppearsOn :field="field" />
|
<AppearsOn :field="field" />
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<input
|
<input
|
||||||
@@ -33,11 +52,13 @@
|
|||||||
<script>
|
<script>
|
||||||
import { IconCalendarCheck } from '@tabler/icons-vue'
|
import { IconCalendarCheck } from '@tabler/icons-vue'
|
||||||
import AppearsOn from './appears_on'
|
import AppearsOn from './appears_on'
|
||||||
|
import MarkdownContent from './markdown_content'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DateStep',
|
name: 'DateStep',
|
||||||
components: {
|
components: {
|
||||||
IconCalendarCheck,
|
IconCalendarCheck,
|
||||||
|
MarkdownContent,
|
||||||
AppearsOn
|
AppearsOn
|
||||||
},
|
},
|
||||||
inject: ['t'],
|
inject: ['t'],
|
||||||
|
|||||||
@@ -65,16 +65,16 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
dryRun: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
accept: {
|
accept: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: '*/*'
|
default: '*/*'
|
||||||
},
|
},
|
||||||
isDirectUpload: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
multiple: {
|
multiple: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -92,11 +92,6 @@ export default {
|
|||||||
return 'el' + Math.random().toString(32).split('.')[1]
|
return 'el' + Math.random().toString(32).split('.')[1]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
|
||||||
if (this.isDirectUpload) {
|
|
||||||
import('@rails/activestorage')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
onDropFiles (e) {
|
onDropFiles (e) {
|
||||||
this.uploadFiles(e.dataTransfer.files)
|
this.uploadFiles(e.dataTransfer.files)
|
||||||
@@ -113,55 +108,28 @@ export default {
|
|||||||
async uploadFiles (files) {
|
async uploadFiles (files) {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
|
|
||||||
if (this.isDirectUpload) {
|
return await Promise.all(
|
||||||
const { DirectUpload } = await import('@rails/activestorage')
|
Array.from(files).map(async (file) => {
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
const blobs = await Promise.all(
|
if (this.dryRun) {
|
||||||
Array.from(files).map(async (file) => {
|
return new Promise((resolve) => {
|
||||||
const upload = new DirectUpload(
|
const reader = new FileReader()
|
||||||
file,
|
|
||||||
'/direct_uploads',
|
|
||||||
this.$refs.input
|
|
||||||
)
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
reader.readAsDataURL(file)
|
||||||
upload.create((error, blob) => {
|
|
||||||
if (error) {
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
return reject(error)
|
reader.onloadend = () => {
|
||||||
} else {
|
resolve({
|
||||||
return resolve(blob)
|
url: reader.result,
|
||||||
}
|
uuid: Math.random().toString(),
|
||||||
})
|
filename: file.name
|
||||||
}).catch((error) => {
|
})
|
||||||
console.error(error)
|
}
|
||||||
})
|
})
|
||||||
})
|
} else {
|
||||||
)
|
if (file.type === 'image/bmp') {
|
||||||
|
file = await this.convertBmpToPng(file)
|
||||||
return await Promise.all(
|
}
|
||||||
blobs.map((blob) => {
|
|
||||||
return fetch(this.baseUrl + '/api/attachments', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
name: 'attachments',
|
|
||||||
blob_signed_id: blob.signed_id,
|
|
||||||
submitter_slug: this.submitterSlug
|
|
||||||
}),
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
}).then(resp => resp.json()).then((data) => {
|
|
||||||
return data
|
|
||||||
})
|
|
||||||
})).then((result) => {
|
|
||||||
this.$emit('upload', result)
|
|
||||||
}).finally(() => {
|
|
||||||
this.isLoading = false
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
return await Promise.all(
|
|
||||||
Array.from(files).map((file) => {
|
|
||||||
const formData = new FormData()
|
|
||||||
|
|
||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
formData.append('submitter_slug', this.submitterSlug)
|
formData.append('submitter_slug', this.submitterSlug)
|
||||||
@@ -173,12 +141,38 @@ export default {
|
|||||||
}).then(resp => resp.json()).then((data) => {
|
}).then(resp => resp.json()).then((data) => {
|
||||||
return data
|
return data
|
||||||
})
|
})
|
||||||
})).then((result) => {
|
}
|
||||||
this.$emit('upload', result)
|
})).then((result) => {
|
||||||
}).finally(() => {
|
this.$emit('upload', result)
|
||||||
this.isLoading = false
|
}).finally(() => {
|
||||||
})
|
this.isLoading = false
|
||||||
}
|
})
|
||||||
|
},
|
||||||
|
convertBmpToPng (bmpFile) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
|
||||||
|
reader.onload = function (event) {
|
||||||
|
const img = new Image()
|
||||||
|
|
||||||
|
img.onload = function () {
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
|
||||||
|
canvas.width = img.width
|
||||||
|
canvas.height = img.height
|
||||||
|
ctx.drawImage(img, 0, 0)
|
||||||
|
canvas.toBlob(function (blob) {
|
||||||
|
const newFile = new File([blob], bmpFile.name.replace(/\.\w+$/, '.png'), { type: 'image/png' })
|
||||||
|
resolve(newFile)
|
||||||
|
}, 'image/png')
|
||||||
|
}
|
||||||
|
|
||||||
|
img.src = event.target.result
|
||||||
|
}
|
||||||
|
reader.onerror = reject
|
||||||
|
reader.readAsDataURL(bmpFile)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,15 @@
|
|||||||
ref="areas"
|
ref="areas"
|
||||||
:steps="stepFields"
|
:steps="stepFields"
|
||||||
:values="values"
|
:values="values"
|
||||||
|
:with-field-placeholder="withFieldPlaceholder"
|
||||||
|
:submitter="submitter"
|
||||||
|
:scroll-el="scrollEl"
|
||||||
|
:with-signature-id="withSignatureId"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
:with-label="!isAnonymousChecboxes && showFieldNames"
|
:with-label="!isAnonymousChecboxes && showFieldNames"
|
||||||
:current-step="currentStepFields"
|
:current-step="currentStepFields"
|
||||||
@focus-step="[saveStep(), goToStep($event, false, true), currentField.type !== 'checkbox' ? isFormVisible = true : '']"
|
:scroll-padding="scrollPadding"
|
||||||
|
@focus-step="[saveStep(), currentField.type !== 'checkbox' ? isFormVisible = true : '', goToStep($event, false, true)]"
|
||||||
/>
|
/>
|
||||||
<FormulaFieldAreas
|
<FormulaFieldAreas
|
||||||
v-if="formulaFields.length"
|
v-if="formulaFields.length"
|
||||||
@@ -16,10 +21,20 @@
|
|||||||
<button
|
<button
|
||||||
v-if="!isFormVisible"
|
v-if="!isFormVisible"
|
||||||
id="expand_form_button"
|
id="expand_form_button"
|
||||||
class="btn btn-neutral flex text-white absolute rounded-none border-x-0 md:border md:rounded-full bottom-0 w-full md:mb-4 text-base"
|
class="btn btn-neutral flex text-white absolute bottom-0 w-full mb-3"
|
||||||
|
style="width: 96%; margin-left: 2%"
|
||||||
@click.prevent="[isFormVisible = true, scrollIntoField(currentField)]"
|
@click.prevent="[isFormVisible = true, scrollIntoField(currentField)]"
|
||||||
>
|
>
|
||||||
{{ t('submit_form') }}
|
<template v-if="['initials', 'signature'].includes(currentField.type)">
|
||||||
|
<IconWritingSign stroke-width="1.5" />
|
||||||
|
{{ t('sign_now') }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="alwaysMinimize">
|
||||||
|
{{ t('next') }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ t('submit_form') }}
|
||||||
|
</template>
|
||||||
<IconArrowsDiagonal
|
<IconArrowsDiagonal
|
||||||
class="absolute right-0 mr-4"
|
class="absolute right-0 mr-4"
|
||||||
:width="20"
|
:width="20"
|
||||||
@@ -95,17 +110,32 @@
|
|||||||
/>
|
/>
|
||||||
<div v-else-if="currentField.type === 'select'">
|
<div v-else-if="currentField.type === 'select'">
|
||||||
<label
|
<label
|
||||||
v-if="showFieldNames && currentField.name"
|
v-if="showFieldNames && (currentField.name || currentField.title)"
|
||||||
:for="currentField.uuid"
|
:for="currentField.uuid"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="label text-2xl mb-2"
|
class="label text-2xl"
|
||||||
>{{ currentField.name }}
|
:class="{ 'mb-2': !currentField.description }"
|
||||||
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
>
|
||||||
|
<MarkdownContent
|
||||||
|
v-if="currentField.title"
|
||||||
|
:string="currentField.title"
|
||||||
|
/>
|
||||||
|
<template v-else>
|
||||||
|
{{ currentField.name }}
|
||||||
|
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
||||||
|
</template>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="py-1"
|
class="py-1"
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
v-if="currentField.description"
|
||||||
|
dir="auto"
|
||||||
|
class="mb-3 px-1"
|
||||||
|
>
|
||||||
|
<MarkdownContent :string="currentField.description" />
|
||||||
|
</div>
|
||||||
<AppearsOn :field="currentField" />
|
<AppearsOn :field="currentField" />
|
||||||
<select
|
<select
|
||||||
:id="currentField.uuid"
|
:id="currentField.uuid"
|
||||||
@@ -134,13 +164,28 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-else-if="currentField.type === 'radio'">
|
<div v-else-if="currentField.type === 'radio'">
|
||||||
<label
|
<label
|
||||||
v-if="showFieldNames && currentField.name"
|
v-if="showFieldNames && (currentField.name || currentField.title)"
|
||||||
:for="currentField.uuid"
|
:for="currentField.uuid"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="label text-2xl mb-2"
|
class="label text-2xl"
|
||||||
>{{ currentField.name }}
|
:class="{ 'mb-2': !currentField.description }"
|
||||||
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
>
|
||||||
|
<MarkdownContent
|
||||||
|
v-if="currentField.title"
|
||||||
|
:string="currentField.title"
|
||||||
|
/>
|
||||||
|
<template v-else>
|
||||||
|
{{ currentField.name }}
|
||||||
|
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
||||||
|
</template>
|
||||||
</label>
|
</label>
|
||||||
|
<div
|
||||||
|
v-if="currentField.description"
|
||||||
|
dir="auto"
|
||||||
|
class="mb-3 px-1"
|
||||||
|
>
|
||||||
|
<MarkdownContent :string="currentField.description" />
|
||||||
|
</div>
|
||||||
<div class="flex w-full max-h-44 overflow-y-auto">
|
<div class="flex w-full max-h-44 overflow-y-auto">
|
||||||
<div
|
<div
|
||||||
v-if="!showFieldNames || (currentField.options.every((e) => !e.value) && currentField.options.length > 4)"
|
v-if="!showFieldNames || (currentField.options.every((e) => !e.value) && currentField.options.length > 4)"
|
||||||
@@ -172,6 +217,7 @@
|
|||||||
:name="`values[${currentField.uuid}]`"
|
:name="`values[${currentField.uuid}]`"
|
||||||
:value="option.value || `${t('option')} ${index + 1}`"
|
:value="option.value || `${t('option')} ${index + 1}`"
|
||||||
:required="currentField.required"
|
:required="currentField.required"
|
||||||
|
@click="scrollIntoField(currentField)"
|
||||||
>
|
>
|
||||||
<span class="text-xl">
|
<span class="text-xl">
|
||||||
{{ option.value || `${t('option')} ${index + 1}` }}
|
{{ option.value || `${t('option')} ${index + 1}` }}
|
||||||
@@ -191,66 +237,77 @@
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-else-if="currentField.type === 'checkbox'"
|
v-else-if="currentField.type === 'checkbox'"
|
||||||
class="flex w-full max-h-44 overflow-y-auto"
|
|
||||||
>
|
>
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
name="cast_boolean"
|
|
||||||
value="true"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="space-y-3.5 mx-auto"
|
v-if="currentField.description"
|
||||||
|
dir="auto"
|
||||||
|
class="mb-3 px-1"
|
||||||
>
|
>
|
||||||
<template v-if="isAnonymousChecboxes || !showFieldNames">
|
<MarkdownContent :string="currentField.description" />
|
||||||
<span class="text-xl">
|
</div>
|
||||||
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? t('submit') : t('next') }}</span>.
|
<div
|
||||||
</span>
|
class="flex w-full max-h-44 overflow-y-auto"
|
||||||
<input
|
>
|
||||||
v-for="field in currentStepFields"
|
<input
|
||||||
:key="field.uuid"
|
type="hidden"
|
||||||
type="hidden"
|
name="cast_boolean"
|
||||||
:name="`values[${field.uuid}]`"
|
value="true"
|
||||||
:value="!!values[field.uuid]"
|
>
|
||||||
>
|
<div
|
||||||
</template>
|
class="space-y-3.5 mx-auto"
|
||||||
<template v-else>
|
>
|
||||||
<div
|
<template v-if="isAnonymousChecboxes || !showFieldNames">
|
||||||
v-for="(field, index) in currentStepFields"
|
<span class="text-xl">
|
||||||
:key="field.uuid"
|
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? t('submit') : t('next') }}</span>.
|
||||||
>
|
</span>
|
||||||
<label
|
<input
|
||||||
:for="field.uuid"
|
v-for="field in currentStepFields"
|
||||||
class="flex items-center space-x-3"
|
:key="field.uuid"
|
||||||
|
type="hidden"
|
||||||
|
:name="`values[${field.uuid}]`"
|
||||||
|
:value="!!values[field.uuid]"
|
||||||
>
|
>
|
||||||
<input
|
</template>
|
||||||
type="hidden"
|
<template v-else>
|
||||||
:name="`values[${field.uuid}]`"
|
<div
|
||||||
:value="!!values[field.uuid]"
|
v-for="(field, index) in currentStepFields"
|
||||||
|
:key="field.uuid"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
:for="field.uuid"
|
||||||
|
class="flex items-center space-x-3"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
:id="field.uuid"
|
type="hidden"
|
||||||
type="checkbox"
|
:name="`values[${field.uuid}]`"
|
||||||
class="base-checkbox !h-7 !w-7"
|
:value="!!values[field.uuid]"
|
||||||
:oninvalid="`this.setCustomValidity('${t('please_check_the_box_to_continue')}')`"
|
>
|
||||||
:onchange="`this.setCustomValidity(validity.valueMissing ? '${t('please_check_the_box_to_continue')}' : '');`"
|
<input
|
||||||
:required="field.required"
|
:id="field.uuid"
|
||||||
:checked="!!values[field.uuid]"
|
type="checkbox"
|
||||||
@click="[scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
|
class="base-checkbox !h-7 !w-7"
|
||||||
>
|
:oninvalid="`this.setCustomValidity('${t('please_check_the_box_to_continue')}')`"
|
||||||
<span
|
:onchange="`this.setCustomValidity(validity.valueMissing ? '${t('please_check_the_box_to_continue')}' : '');`"
|
||||||
v-if="field.title"
|
:required="field.required"
|
||||||
class="text-xl"
|
:checked="!!values[field.uuid]"
|
||||||
v-html="field.title"
|
@click="[scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
|
||||||
/>
|
>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-if="field.title"
|
||||||
class="text-xl"
|
class="text-xl"
|
||||||
>
|
>
|
||||||
{{ field.name || field.type + ' ' + (index + 1) }}
|
<MarkdownContent :string="field.title" />
|
||||||
</span>
|
</span>
|
||||||
</label>
|
<span
|
||||||
</div>
|
v-else
|
||||||
</template>
|
class="text-xl"
|
||||||
|
>
|
||||||
|
{{ field.name || field.type + ' ' + (index + 1) }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ImageStep
|
<ImageStep
|
||||||
@@ -258,7 +315,7 @@
|
|||||||
:key="currentField.uuid"
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
:is-direct-upload="isDirectUpload"
|
:dry-run="dryRun"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
:show-field-names="showFieldNames"
|
:show-field-names="showFieldNames"
|
||||||
@@ -270,11 +327,15 @@
|
|||||||
:key="currentField.uuid"
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
:previous-value="previousSignatureValueFor(currentField)"
|
:previous-value="previousSignatureValueFor(currentField) || previousSignatureValue"
|
||||||
:is-direct-upload="isDirectUpload"
|
|
||||||
:with-typed-signature="withTypedSignature"
|
:with-typed-signature="withTypedSignature"
|
||||||
|
:remember-signature="rememberSignature"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
:submitter-slug="submitterSlug"
|
:button-text="buttonText"
|
||||||
|
:dry-run="dryRun"
|
||||||
|
:with-disclosure="withDisclosure"
|
||||||
|
:with-qr-button="withQrButton"
|
||||||
|
:submitter="submitter"
|
||||||
:show-field-names="showFieldNames"
|
:show-field-names="showFieldNames"
|
||||||
@attached="attachments.push($event)"
|
@attached="attachments.push($event)"
|
||||||
@start="scrollIntoField(currentField)"
|
@start="scrollIntoField(currentField)"
|
||||||
@@ -286,8 +347,8 @@
|
|||||||
:key="currentField.uuid"
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
|
:dry-run="dryRun"
|
||||||
:previous-value="previousInitialsValue"
|
:previous-value="previousInitialsValue"
|
||||||
:is-direct-upload="isDirectUpload"
|
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
:show-field-names="showFieldNames"
|
:show-field-names="showFieldNames"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
@@ -300,7 +361,7 @@
|
|||||||
v-else-if="currentField.type === 'file'"
|
v-else-if="currentField.type === 'file'"
|
||||||
:key="currentField.uuid"
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:is-direct-upload="isDirectUpload"
|
:dry-run="dryRun"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
@@ -312,7 +373,9 @@
|
|||||||
:key="currentField.uuid"
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
|
:locale="language?.toLowerCase() || browserLanguage"
|
||||||
:show-field-names="showFieldNames"
|
:show-field-names="showFieldNames"
|
||||||
|
:verified-value="phoneVerifiedValues[currentField.uuid]"
|
||||||
:default-value="submitter.phone"
|
:default-value="submitter.phone"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
@focus="scrollIntoField(currentField)"
|
@focus="scrollIntoField(currentField)"
|
||||||
@@ -332,7 +395,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="currentField.type !== 'payment' || submittedValues[currentField.uuid]"
|
v-if="currentField.type !== 'payment' || submittedValues[currentField.uuid]"
|
||||||
class="mt-6 md:mt-8"
|
:class="currentField.type === 'signature' ? 'mt-2' : 'mt-6 md:mt-8'"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
id="submit_form_button"
|
id="submit_form_button"
|
||||||
@@ -346,11 +409,8 @@
|
|||||||
v-if="isSubmitting"
|
v-if="isSubmitting"
|
||||||
class="mr-1 animate-spin"
|
class="mr-1 animate-spin"
|
||||||
/>
|
/>
|
||||||
<span v-if="stepFields.length === currentStep + 1">
|
<span>
|
||||||
{{ t('submit') }}
|
{{ buttonText }}
|
||||||
</span>
|
|
||||||
<span v-else>
|
|
||||||
{{ t('next') }}
|
|
||||||
</span><span
|
</span><span
|
||||||
v-if="isSubmitting"
|
v-if="isSubmitting"
|
||||||
class="w-6 flex justify-start mr-1"
|
class="w-6 flex justify-start mr-1"
|
||||||
@@ -370,8 +430,9 @@
|
|||||||
:is-demo="isDemo"
|
:is-demo="isDemo"
|
||||||
:attribution="attribution"
|
:attribution="attribution"
|
||||||
:completed-button="completedRedirectUrl ? {} : completedButton"
|
:completed-button="completedRedirectUrl ? {} : completedButton"
|
||||||
|
:completed-message="completedRedirectUrl ? {} : completedMessage"
|
||||||
:with-send-copy-button="withSendCopyButton && !completedRedirectUrl"
|
:with-send-copy-button="withSendCopyButton && !completedRedirectUrl"
|
||||||
:with-download-button="withDownloadButton && !completedRedirectUrl"
|
:with-download-button="withDownloadButton && !completedRedirectUrl && !dryRun"
|
||||||
:with-confetti="withConfetti"
|
:with-confetti="withConfetti"
|
||||||
:can-send-email="canSendEmail && !!submitter.email"
|
:can-send-email="canSendEmail && !!submitter.email"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
@@ -412,18 +473,23 @@ import PaymentStep from './payment_step'
|
|||||||
import TextStep from './text_step'
|
import TextStep from './text_step'
|
||||||
import NumberStep from './number_step'
|
import NumberStep from './number_step'
|
||||||
import DateStep from './date_step'
|
import DateStep from './date_step'
|
||||||
|
import MarkdownContent from './markdown_content'
|
||||||
import FormCompleted from './completed'
|
import FormCompleted from './completed'
|
||||||
import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
import { IconInnerShadowTop, IconArrowsDiagonal, IconWritingSign, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||||
import AppearsOn from './appears_on'
|
import AppearsOn from './appears_on'
|
||||||
import i18n from './i18n'
|
import i18n from './i18n'
|
||||||
|
|
||||||
const isEmpty = (obj) => {
|
const isEmpty = (obj) => {
|
||||||
if (obj == null) return true
|
if (obj == null) return true
|
||||||
|
|
||||||
if (Array.isArray(obj) || typeof obj === 'string') {
|
if (Array.isArray(obj)) {
|
||||||
return obj.length === 0
|
return obj.length === 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof obj === 'string') {
|
||||||
|
return obj.trim().length === 0
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof obj === 'object') {
|
if (typeof obj === 'object') {
|
||||||
return Object.keys(obj).length === 0
|
return Object.keys(obj).length === 0
|
||||||
}
|
}
|
||||||
@@ -442,6 +508,7 @@ export default {
|
|||||||
ImageStep,
|
ImageStep,
|
||||||
SignatureStep,
|
SignatureStep,
|
||||||
AppearsOn,
|
AppearsOn,
|
||||||
|
IconWritingSign,
|
||||||
AttachmentStep,
|
AttachmentStep,
|
||||||
InitialsStep,
|
InitialsStep,
|
||||||
MultiSelectStep,
|
MultiSelectStep,
|
||||||
@@ -452,6 +519,7 @@ export default {
|
|||||||
NumberStep,
|
NumberStep,
|
||||||
FormulaFieldAreas,
|
FormulaFieldAreas,
|
||||||
PhoneStep,
|
PhoneStep,
|
||||||
|
MarkdownContent,
|
||||||
PaymentStep,
|
PaymentStep,
|
||||||
IconArrowsDiagonalMinimize2,
|
IconArrowsDiagonalMinimize2,
|
||||||
FormCompleted
|
FormCompleted
|
||||||
@@ -469,6 +537,16 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
withSignatureId: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
scrollPadding: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '-80px'
|
||||||
|
},
|
||||||
canSendEmail: {
|
canSendEmail: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -479,6 +557,16 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
withFieldPlaceholder: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
scrollEl: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
onComplete: {
|
onComplete: {
|
||||||
type: Function,
|
type: Function,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -487,6 +575,26 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
expand: {
|
expand: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
rememberSignature: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
minimize: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
withDisclosure: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
reuseSignature: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
@@ -496,11 +604,21 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
autoscrollFields: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
showFieldNames: {
|
showFieldNames: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
withQrButton: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
withTypedSignature: {
|
withTypedSignature: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -526,10 +644,10 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
isDirectUpload: {
|
previousSignatureValue: {
|
||||||
type: Boolean,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: ''
|
||||||
},
|
},
|
||||||
allowToSkip: {
|
allowToSkip: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -546,6 +664,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
dryRun: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
attribution: {
|
attribution: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -561,6 +684,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
},
|
},
|
||||||
|
authenticityToken: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
i18n: {
|
i18n: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -585,22 +713,43 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
|
},
|
||||||
|
completedMessage: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isCompleted: false,
|
isCompleted: false,
|
||||||
isFormVisible: this.expand,
|
isFormVisible: this.expand !== false,
|
||||||
showFillAllRequiredFields: false,
|
showFillAllRequiredFields: false,
|
||||||
currentStep: 0,
|
currentStep: 0,
|
||||||
|
enableScrollIntoField: true,
|
||||||
|
phoneVerifiedValues: {},
|
||||||
|
orientation: screen?.orientation?.type,
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
submittedValues: {},
|
submittedValues: {},
|
||||||
recalculateButtonDisabledKey: ''
|
recalculateButtonDisabledKey: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isMobile () {
|
||||||
|
return /android|iphone|ipad/i.test(navigator.userAgent)
|
||||||
|
},
|
||||||
|
buttonText () {
|
||||||
|
if (this.alwaysMinimize || this.stepFields.length === this.currentStep + 1) {
|
||||||
|
return this.t('submit')
|
||||||
|
} else {
|
||||||
|
return this.t('next')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
alwaysMinimize () {
|
||||||
|
return this.minimize || (this.orientation?.includes('landscape') && this.isMobile && parseInt(window.innerHeight) < 550)
|
||||||
|
},
|
||||||
currentStepFields () {
|
currentStepFields () {
|
||||||
return this.stepFields[this.currentStep]
|
return this.stepFields[this.currentStep] || []
|
||||||
},
|
},
|
||||||
browserLanguage () {
|
browserLanguage () {
|
||||||
return (navigator.language || navigator.userLanguage || 'en').split('-')[0]
|
return (navigator.language || navigator.userLanguage || 'en').split('-')[0]
|
||||||
@@ -608,16 +757,24 @@ export default {
|
|||||||
queryParams () {
|
queryParams () {
|
||||||
return new URLSearchParams(window.location.search)
|
return new URLSearchParams(window.location.search)
|
||||||
},
|
},
|
||||||
authenticityToken () {
|
|
||||||
return document.querySelector('meta[name="csrf-token"]')?.content
|
|
||||||
},
|
|
||||||
submitterSlug () {
|
submitterSlug () {
|
||||||
return this.submitter.slug
|
return this.submitter.slug
|
||||||
},
|
},
|
||||||
previousInitialsValue () {
|
fieldsUuidIndex () {
|
||||||
const initialsField = [...this.fields].reverse().find((field) => field.type === 'initials' && !!this.values[field.uuid])
|
return this.fields.reduce((acc, f) => {
|
||||||
|
acc[f.uuid] = f
|
||||||
|
|
||||||
return this.values[initialsField?.uuid]
|
return acc
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
previousInitialsValue () {
|
||||||
|
if (this.reuseSignature !== false) {
|
||||||
|
const initialsField = [...this.fields].reverse().find((field) => field.type === 'initials' && !!this.values[field.uuid])
|
||||||
|
|
||||||
|
return this.values[initialsField?.uuid]
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
},
|
},
|
||||||
isAnonymousChecboxes () {
|
isAnonymousChecboxes () {
|
||||||
return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name && !e.required) && this.currentStepFields.length > 4
|
return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name && !e.required) && this.currentStepFields.length > 4
|
||||||
@@ -639,10 +796,12 @@ export default {
|
|||||||
return this.fields.filter((f) => !f.readonly).reduce((acc, f) => {
|
return this.fields.filter((f) => !f.readonly).reduce((acc, f) => {
|
||||||
const prevStep = acc[acc.length - 1]
|
const prevStep = acc[acc.length - 1]
|
||||||
|
|
||||||
if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox') {
|
if (this.checkFieldConditions(f)) {
|
||||||
prevStep.push(f)
|
if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox' && !f.description) {
|
||||||
} else {
|
prevStep.push(f)
|
||||||
acc.push([f])
|
} else {
|
||||||
|
acc.push([f])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc
|
return acc
|
||||||
@@ -665,11 +824,21 @@ export default {
|
|||||||
watch: {
|
watch: {
|
||||||
expand (value) {
|
expand (value) {
|
||||||
this.isFormVisible = value
|
this.isFormVisible = value
|
||||||
|
},
|
||||||
|
currentStepFields (value) {
|
||||||
|
if (isEmpty(value) && this.currentStep > 0) {
|
||||||
|
this.currentStep -= 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
beforeUnmount () {
|
||||||
|
screen?.orientation?.removeEventListener('change', this.onOrientationChange)
|
||||||
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.submittedValues = JSON.parse(JSON.stringify(this.values))
|
this.submittedValues = JSON.parse(JSON.stringify(this.values))
|
||||||
|
|
||||||
|
screen?.orientation.addEventListener('change', this.onOrientationChange)
|
||||||
|
|
||||||
this.fields.forEach((field) => {
|
this.fields.forEach((field) => {
|
||||||
if (field.default_value && !field.readonly) {
|
if (field.default_value && !field.readonly) {
|
||||||
this.values[field.uuid] ||= field.default_value
|
this.values[field.uuid] ||= field.default_value
|
||||||
@@ -699,6 +868,14 @@ export default {
|
|||||||
this.currentStep = Math.min(...indexesList)
|
this.currentStep = Math.min(...indexesList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (document.body?.clientWidth >= 768 && this.expand !== true && ['signature', 'initials', 'file', 'image'].includes(this.currentField?.type)) {
|
||||||
|
this.isFormVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.alwaysMinimize) {
|
||||||
|
this.isFormVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
|
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
const root = this.$root.$el.parentNode.getRootNode()
|
const root = this.$root.$el.parentNode.getRootNode()
|
||||||
@@ -716,18 +893,57 @@ export default {
|
|||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.recalculateButtonDisabledKey = Math.random()
|
this.recalculateButtonDisabledKey = Math.random()
|
||||||
|
|
||||||
Promise.all([
|
if (!this.dryRun) {
|
||||||
this.maybeTrackEmailClick(),
|
Promise.all([
|
||||||
this.maybeTrackSmsClick()
|
this.maybeTrackEmailClick(),
|
||||||
]).finally(() => {
|
this.maybeTrackSmsClick()
|
||||||
this.trackViewForm()
|
]).finally(() => {
|
||||||
})
|
this.trackViewForm()
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
t (key) {
|
t (key) {
|
||||||
return this.i18n[key] || i18n[this.language?.toLowerCase()]?.[key] || i18n[this.browserLanguage]?.[key] || i18n.en[key] || key
|
return this.i18n[key] || i18n[this.language?.toLowerCase()]?.[key] || i18n[this.browserLanguage]?.[key] || i18n.en[key] || key
|
||||||
},
|
},
|
||||||
|
onOrientationChange (event) {
|
||||||
|
this.orientation = event.target.type
|
||||||
|
},
|
||||||
|
checkFieldConditions (field) {
|
||||||
|
if (field.conditions?.length) {
|
||||||
|
return field.conditions.reduce((acc, c) => {
|
||||||
|
if (['empty', 'unchecked'].includes(c.action)) {
|
||||||
|
return acc && isEmpty(this.values[c.field_uuid])
|
||||||
|
} else if (['not_empty', 'checked'].includes(c.action)) {
|
||||||
|
return acc && !isEmpty(this.values[c.field_uuid])
|
||||||
|
} else if (['equal', 'contains'].includes(c.action)) {
|
||||||
|
const field = this.fieldsUuidIndex[c.field_uuid]
|
||||||
|
const option = field.options.find((o) => o.uuid === c.value)
|
||||||
|
const values = [this.values[c.field_uuid]].flat()
|
||||||
|
|
||||||
|
return acc && values.includes(this.optionValue(option, field.options.indexOf(option)))
|
||||||
|
} else if (['not_equal', 'does_not_contain'].includes(c.action)) {
|
||||||
|
const field = this.fieldsUuidIndex[c.field_uuid]
|
||||||
|
const option = field.options.find((o) => o.uuid === c.value)
|
||||||
|
const values = [this.values[c.field_uuid]].flat()
|
||||||
|
|
||||||
|
return acc && !values.includes(this.optionValue(option, field.options.indexOf(option)))
|
||||||
|
} else {
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
}, true)
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
optionValue (option, index) {
|
||||||
|
if (option.value) {
|
||||||
|
return option.value
|
||||||
|
} else {
|
||||||
|
return `${this.t('option')} ${index + 1}`
|
||||||
|
}
|
||||||
|
},
|
||||||
maybeTrackEmailClick () {
|
maybeTrackEmailClick () {
|
||||||
const { queryParams } = this
|
const { queryParams } = this
|
||||||
|
|
||||||
@@ -788,11 +1004,15 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
previousSignatureValueFor (field) {
|
previousSignatureValueFor (field) {
|
||||||
const signatureField = [...this.fields].reverse().find((f) =>
|
if (this.reuseSignature !== false) {
|
||||||
f.type === 'signature' && field.preferences?.format === f.preferences?.format && !!this.values[f.uuid]
|
const signatureField = [...this.fields].reverse().find((f) =>
|
||||||
)
|
f.type === 'signature' && field.preferences?.format === f.preferences?.format && !!this.values[f.uuid]
|
||||||
|
)
|
||||||
|
|
||||||
return this.values[signatureField?.uuid]
|
return this.values[signatureField?.uuid]
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
},
|
},
|
||||||
goToStep (step, scrollToArea = false, clickUpload = false) {
|
goToStep (step, scrollToArea = false, clickUpload = false) {
|
||||||
this.currentStep = this.stepFields.indexOf(step)
|
this.currentStep = this.stepFields.indexOf(step)
|
||||||
@@ -801,21 +1021,32 @@ export default {
|
|||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.recalculateButtonDisabledKey = Math.random()
|
this.recalculateButtonDisabledKey = Math.random()
|
||||||
|
|
||||||
if (scrollToArea) {
|
if (!this.isCompleted) {
|
||||||
this.scrollIntoField(step[0])
|
if (scrollToArea) {
|
||||||
}
|
this.scrollIntoField(step[0])
|
||||||
|
}
|
||||||
|
|
||||||
this.$refs.form.querySelector('input[type="date"], input[type="number"], input[type="text"], select')?.focus()
|
this.enableScrollIntoField = false
|
||||||
|
this.$refs.form.querySelector('input[type="date"], input[type="number"], input[type="text"], select')?.focus()
|
||||||
|
this.enableScrollIntoField = true
|
||||||
|
|
||||||
if (clickUpload && !this.values[this.currentField.uuid] && ['file', 'image'].includes(this.currentField.type)) {
|
if (clickUpload && !this.values[this.currentField.uuid] && ['file', 'image'].includes(this.currentField.type)) {
|
||||||
this.$refs.form.querySelector('input[type="file"]')?.click()
|
this.$refs.form.querySelector('input[type="file"]')?.click()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
saveStep (formData) {
|
saveStep (formData) {
|
||||||
const currentFieldUuids = this.currentStepFields.map((f) => f.uuid)
|
const currentFieldUuids = this.currentStepFields.map((f) => f.uuid)
|
||||||
|
const currentFieldType = this.currentField.type
|
||||||
|
|
||||||
if (this.isCompleted) {
|
if (this.dryRun) {
|
||||||
|
currentFieldUuids.forEach((fieldUuid) => {
|
||||||
|
this.submittedValues[fieldUuid] = this.values[fieldUuid]
|
||||||
|
})
|
||||||
|
|
||||||
|
return Promise.resolve({})
|
||||||
|
} else if (this.isCompleted) {
|
||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
} else {
|
} else {
|
||||||
return fetch(this.baseUrl + this.submitPath, {
|
return fetch(this.baseUrl + this.submitPath, {
|
||||||
@@ -825,6 +1056,10 @@ export default {
|
|||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
currentFieldUuids.forEach((fieldUuid) => {
|
currentFieldUuids.forEach((fieldUuid) => {
|
||||||
this.submittedValues[fieldUuid] = this.values[fieldUuid]
|
this.submittedValues[fieldUuid] = this.values[fieldUuid]
|
||||||
|
|
||||||
|
if (currentFieldType === 'phone') {
|
||||||
|
this.phoneVerifiedValues[fieldUuid] = this.values[fieldUuid]
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -833,7 +1068,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
scrollIntoField (field) {
|
scrollIntoField (field) {
|
||||||
return this.$refs.areas.scrollIntoField(field)
|
if (this.enableScrollIntoField) {
|
||||||
|
return this.$refs.areas.scrollIntoField(field)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
scrollIntoArea (area) {
|
scrollIntoArea (area) {
|
||||||
return this.$refs.areas.scrollIntoArea(area)
|
return this.$refs.areas.scrollIntoArea(area)
|
||||||
@@ -863,7 +1100,15 @@ export default {
|
|||||||
formData.append('completed', 'true')
|
formData.append('completed', 'true')
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.saveStep(formData).then(async (response) => {
|
let saveStepRequest
|
||||||
|
|
||||||
|
if (!isLastStep && this.phoneVerifiedValues[this.currentField.uuid] && this.phoneVerifiedValues[this.currentField.uuid] === this.values[this.currentField.uuid]) {
|
||||||
|
saveStepRequest = Promise.resolve({})
|
||||||
|
} else {
|
||||||
|
saveStepRequest = this.saveStep(formData)
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveStepRequest.then(async (response) => {
|
||||||
if (response.status === 422 || response.status === 500) {
|
if (response.status === 422 || response.status === 500) {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
||||||
@@ -879,7 +1124,11 @@ export default {
|
|||||||
const nextStep = (isLastStep && emptyRequiredField) || this.stepFields[this.currentStep + 1]
|
const nextStep = (isLastStep && emptyRequiredField) || this.stepFields[this.currentStep + 1]
|
||||||
|
|
||||||
if (nextStep) {
|
if (nextStep) {
|
||||||
this.goToStep(nextStep, true)
|
if (this.alwaysMinimize) {
|
||||||
|
this.isFormVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
this.goToStep(nextStep, this.autoscrollFields)
|
||||||
|
|
||||||
if (emptyRequiredField === nextStep) {
|
if (emptyRequiredField === nextStep) {
|
||||||
this.showFillAllRequiredFields = true
|
this.showFillAllRequiredFields = true
|
||||||
@@ -903,7 +1152,11 @@ export default {
|
|||||||
this.isSubmitting = false
|
this.isSubmitting = false
|
||||||
})
|
})
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.log(error)
|
if (error?.message === 'Image too small') {
|
||||||
|
alert('Signature is too small - please redraw.')
|
||||||
|
} else {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.isSubmitting = false
|
this.isSubmitting = false
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
const en = {
|
const en = {
|
||||||
text: 'Text',
|
text: 'Text',
|
||||||
|
by_clicking_you_agree_to_the: 'By clicking "{button}", you agree to the',
|
||||||
|
electronic_signature_disclosure: 'Electronic Signature Disclosure',
|
||||||
|
esignature_disclosure: 'eSignature Disclosure',
|
||||||
signature: 'Signature',
|
signature: 'Signature',
|
||||||
initials: 'Initials',
|
initials: 'Initials',
|
||||||
|
drawn_signature_on_a_touchscreen_device: 'Drawn signature on a touchscreen device',
|
||||||
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scan the QR code with the camera app to open the form on mobile and draw your signature',
|
||||||
date: 'Date',
|
date: 'Date',
|
||||||
number: 'Number',
|
number: 'Number',
|
||||||
|
already_paid: 'Already paid',
|
||||||
image: 'Image',
|
image: 'Image',
|
||||||
|
pay: 'Pay',
|
||||||
take_photo: 'Take photo',
|
take_photo: 'Take photo',
|
||||||
|
number_phone_is_invalid: '{number} phone is invalid',
|
||||||
file: 'File',
|
file: 'File',
|
||||||
|
digitally_signed_by: 'Digitally signed by',
|
||||||
|
reason: 'Reason',
|
||||||
select: 'Select',
|
select: 'Select',
|
||||||
checkbox: 'Checkbox',
|
checkbox: 'Checkbox',
|
||||||
multiple: 'Multiple',
|
multiple: 'Multiple',
|
||||||
@@ -17,13 +27,14 @@ const en = {
|
|||||||
payment: 'Payment',
|
payment: 'Payment',
|
||||||
phone: 'Phone',
|
phone: 'Phone',
|
||||||
submit_form: 'Submit Form',
|
submit_form: 'Submit Form',
|
||||||
|
sign_now: 'Sign Now',
|
||||||
type_here_: 'Type here...',
|
type_here_: 'Type here...',
|
||||||
optional: 'optional',
|
optional: 'optional',
|
||||||
option: 'Option',
|
option: 'Option',
|
||||||
appears_on: 'Appears on',
|
appears_on: 'Appears on',
|
||||||
page: 'Page',
|
page: 'Page',
|
||||||
select_your_option: 'Select your option',
|
select_your_option: 'Select your option',
|
||||||
complete_hightlighted_checkboxes_and_click: 'Complete hightlighted checkboxes and click',
|
complete_hightlighted_checkboxes_and_click: 'Complete highlighted checkboxes and click',
|
||||||
submit: 'submit',
|
submit: 'submit',
|
||||||
next: 'next',
|
next: 'next',
|
||||||
click_to_upload: 'Click to upload',
|
click_to_upload: 'Click to upload',
|
||||||
@@ -41,7 +52,7 @@ const en = {
|
|||||||
please_check_the_box_to_continue: 'Please check the box to continue',
|
please_check_the_box_to_continue: 'Please check the box to continue',
|
||||||
open_source_documents_software: 'open source documents software',
|
open_source_documents_software: 'open source documents software',
|
||||||
verified_phone_number: 'Verify Phone Number',
|
verified_phone_number: 'Verify Phone Number',
|
||||||
use_international_format: 'Use internatioanl format: +1xxx',
|
use_international_format: 'Use international format: +1xxx',
|
||||||
six_digits_code: '6-digit code',
|
six_digits_code: '6-digit code',
|
||||||
change_phone_number: 'Change phone number',
|
change_phone_number: 'Change phone number',
|
||||||
sending: 'Sending...',
|
sending: 'Sending...',
|
||||||
@@ -64,9 +75,18 @@ const en = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
|
drawn_signature_on_a_touchscreen_device: 'Firma dibujada en un dispositivo con pantalla táctil',
|
||||||
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Escanea el código QR con la aplicación de la cámara para abrir el formulario en el móvil y dibujar tu firma',
|
||||||
|
by_clicking_you_agree_to_the: 'Al hacer clic en "{button}", usted acepta el',
|
||||||
|
electronic_signature_disclosure: 'Divulgación de Firma Electrónica',
|
||||||
|
esignature_disclosure: 'Divulgación de eFirma',
|
||||||
|
already_paid: 'Ya pagado',
|
||||||
minimize: 'Minimizar',
|
minimize: 'Minimizar',
|
||||||
text: 'Texto',
|
text: 'Texto',
|
||||||
signature: 'Firma',
|
signature: 'Firma',
|
||||||
|
pay: 'Pagar',
|
||||||
|
digitally_signed_by: 'Firmado digitalmente por',
|
||||||
|
reason: 'Razón',
|
||||||
initials: 'Iniciales',
|
initials: 'Iniciales',
|
||||||
date: 'Fecha',
|
date: 'Fecha',
|
||||||
number: 'Número',
|
number: 'Número',
|
||||||
@@ -74,6 +94,7 @@ const es = {
|
|||||||
file: 'Archivo',
|
file: 'Archivo',
|
||||||
select: 'Seleccionar',
|
select: 'Seleccionar',
|
||||||
checkbox: 'Casilla',
|
checkbox: 'Casilla',
|
||||||
|
number_phone_is_invalid: '{number} teléfono es inválido',
|
||||||
multiple: 'Múltiple',
|
multiple: 'Múltiple',
|
||||||
radio: 'Radio',
|
radio: 'Radio',
|
||||||
cells: 'Celdas',
|
cells: 'Celdas',
|
||||||
@@ -82,6 +103,7 @@ const es = {
|
|||||||
phone: 'Teléfono',
|
phone: 'Teléfono',
|
||||||
take_photo: 'Tomar foto',
|
take_photo: 'Tomar foto',
|
||||||
submit_form: 'Enviar Formulario',
|
submit_form: 'Enviar Formulario',
|
||||||
|
sign_now: 'Firmar ahora',
|
||||||
type_here_: 'Escribe aquí...',
|
type_here_: 'Escribe aquí...',
|
||||||
optional: 'opcional',
|
optional: 'opcional',
|
||||||
appears_on: 'Aparece en',
|
appears_on: 'Aparece en',
|
||||||
@@ -128,13 +150,22 @@ const es = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
|
drawn_signature_on_a_touchscreen_device: 'Firma disegnata su un dispositivo con schermo tattile',
|
||||||
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scansiona il codice QR con l\'app della fotocamera per aprire il modulo sul cellulare e disegnare la tua firma',
|
||||||
|
by_clicking_you_agree_to_the: 'Cliccando su "{button}", accetti il',
|
||||||
|
electronic_signature_disclosure: 'Divulgazione della Firma Elettronica',
|
||||||
|
esignature_disclosure: 'Divulgazione della eFirma',
|
||||||
minimize: 'Minimizza',
|
minimize: 'Minimizza',
|
||||||
|
already_paid: 'Pagato',
|
||||||
text: 'Testo',
|
text: 'Testo',
|
||||||
signature: 'Firma',
|
signature: 'Firma',
|
||||||
|
digitally_signed_by: 'Firmato digitalmente da',
|
||||||
|
reason: 'Motivo',
|
||||||
initials: 'Iniziali',
|
initials: 'Iniziali',
|
||||||
date: 'Data',
|
date: 'Data',
|
||||||
number: 'Numero',
|
number: 'Numero',
|
||||||
image: 'Immagine',
|
image: 'Immagine',
|
||||||
|
pay: 'Pagamento',
|
||||||
file: 'File',
|
file: 'File',
|
||||||
select: 'Seleziona',
|
select: 'Seleziona',
|
||||||
checkbox: 'Casella di controllo',
|
checkbox: 'Casella di controllo',
|
||||||
@@ -143,8 +174,10 @@ const it = {
|
|||||||
cells: 'Celle',
|
cells: 'Celle',
|
||||||
stamp: 'Timbro',
|
stamp: 'Timbro',
|
||||||
payment: 'Pagamento',
|
payment: 'Pagamento',
|
||||||
|
number_phone_is_invalid: '{number} telefono non è valido',
|
||||||
phone: 'Telefono',
|
phone: 'Telefono',
|
||||||
submit_form: 'Invia Modulo',
|
submit_form: 'Invia Modulo',
|
||||||
|
sign_now: 'Firma ora',
|
||||||
type_here_: 'Digita qui...',
|
type_here_: 'Digita qui...',
|
||||||
optional: 'opzionale',
|
optional: 'opzionale',
|
||||||
appears_on: 'Compare su',
|
appears_on: 'Compare su',
|
||||||
@@ -192,13 +225,22 @@ const it = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
|
drawn_signature_on_a_touchscreen_device: 'Gezeichnete Unterschrift auf einem Touchscreen-Gerät',
|
||||||
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scannen Sie den QR-Code mit der Kamera-App, um das Formular auf dem Handy zu öffnen und Ihre Unterschrift zu zeichnen',
|
||||||
|
by_clicking_you_agree_to_the: 'Durch Klicken auf "{button}" stimmen Sie zu, dass Sie die',
|
||||||
|
electronic_signature_disclosure: 'Elektronische Unterschriftenoffenlegung',
|
||||||
|
esignature_disclosure: 'eSignatur Offenlegung',
|
||||||
|
already_paid: 'Bereits bezahlt',
|
||||||
minimize: 'Minimieren',
|
minimize: 'Minimieren',
|
||||||
text: 'Text',
|
text: 'Text',
|
||||||
signature: 'Unterschrift',
|
signature: 'Unterschrift',
|
||||||
|
digitally_signed_by: 'Digital unterschrieben von',
|
||||||
|
reason: 'Grund',
|
||||||
initials: 'Initialen',
|
initials: 'Initialen',
|
||||||
date: 'Datum',
|
date: 'Datum',
|
||||||
number: 'Nummer',
|
number: 'Nummer',
|
||||||
image: 'Bild',
|
image: 'Bild',
|
||||||
|
pay: 'Bezahlen',
|
||||||
file: 'Datei',
|
file: 'Datei',
|
||||||
select: 'Auswählen',
|
select: 'Auswählen',
|
||||||
checkbox: 'Checkbox',
|
checkbox: 'Checkbox',
|
||||||
@@ -207,8 +249,10 @@ const de = {
|
|||||||
cells: 'Zellen',
|
cells: 'Zellen',
|
||||||
stamp: 'Stempel',
|
stamp: 'Stempel',
|
||||||
payment: 'Zahlung',
|
payment: 'Zahlung',
|
||||||
|
number_phone_is_invalid: '{number} Telefonnummer ist ungültig',
|
||||||
phone: 'Telefon',
|
phone: 'Telefon',
|
||||||
submit_form: 'Formular absenden',
|
submit_form: 'Formular absenden',
|
||||||
|
sign_now: 'Jetzt unterschreiben',
|
||||||
type_here_: 'Hier eingeben...',
|
type_here_: 'Hier eingeben...',
|
||||||
optional: 'optional',
|
optional: 'optional',
|
||||||
appears_on: 'Erscheint auf',
|
appears_on: 'Erscheint auf',
|
||||||
@@ -256,10 +300,19 @@ const de = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
|
drawn_signature_on_a_touchscreen_device: 'Signature dessinée sur un appareil à écran tactile',
|
||||||
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scannez le code QR avec l\'application de l\'appareil photo pour ouvrir le formulaire sur mobile et dessiner votre signature',
|
||||||
|
by_clicking_you_agree_to_the: 'En cliquant sur "{button}", vous acceptez la',
|
||||||
|
electronic_signature_disclosure: 'Divulgation de Signature Électronique',
|
||||||
|
esignature_disclosure: 'Divulgation de la eSignature',
|
||||||
minimize: 'Réduire',
|
minimize: 'Réduire',
|
||||||
text: 'Texte',
|
text: 'Texte',
|
||||||
|
already_paid: 'Déjà payé',
|
||||||
signature: 'Signature',
|
signature: 'Signature',
|
||||||
initials: 'Initiales',
|
initials: 'Initiales',
|
||||||
|
digitally_signed_by: 'Signé numériquement par',
|
||||||
|
reason: 'Raison',
|
||||||
|
pay: 'Payer',
|
||||||
date: 'Date',
|
date: 'Date',
|
||||||
number: 'Numéro',
|
number: 'Numéro',
|
||||||
image: 'Image',
|
image: 'Image',
|
||||||
@@ -271,8 +324,10 @@ const fr = {
|
|||||||
cells: 'Cellules',
|
cells: 'Cellules',
|
||||||
stamp: 'Tampon',
|
stamp: 'Tampon',
|
||||||
payment: 'Paiement',
|
payment: 'Paiement',
|
||||||
|
number_phone_is_invalid: '{number} téléphone est invalide',
|
||||||
phone: 'Téléphone',
|
phone: 'Téléphone',
|
||||||
submit_form: 'Envoyer le Formulaire',
|
submit_form: 'Envoyer le Formulaire',
|
||||||
|
sign_now: 'Signer maintenant',
|
||||||
type_here_: 'Tapez ici...',
|
type_here_: 'Tapez ici...',
|
||||||
optional: 'facultatif',
|
optional: 'facultatif',
|
||||||
appears_on: 'Apparaît sur',
|
appears_on: 'Apparaît sur',
|
||||||
@@ -320,14 +375,23 @@ const fr = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pl = {
|
const pl = {
|
||||||
|
drawn_signature_on_a_touchscreen_device: 'Podpis odręczny na urządzeniu z ekranem dotykowym',
|
||||||
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Zeskanuj kod QR za pomocą aplikacji aparatu, aby otworzyć formularz na telefonie i narysować swój podpis',
|
||||||
|
by_clicking_you_agree_to_the: 'Klikając na "{button}", zgadzasz się na',
|
||||||
|
electronic_signature_disclosure: 'Ujawnienie Elektronicznej Sygnatury',
|
||||||
|
esignature_disclosure: 'Ujawnienie ePodpisu',
|
||||||
minimize: 'Zminimalizuj',
|
minimize: 'Zminimalizuj',
|
||||||
text: 'Tekst',
|
text: 'Tekst',
|
||||||
|
already_paid: 'Już zapłacono',
|
||||||
|
digitally_signed_by: 'Podpisano cyfrowo przez',
|
||||||
|
reason: 'Powód',
|
||||||
signature: 'Podpis',
|
signature: 'Podpis',
|
||||||
initials: 'Inicjały',
|
initials: 'Inicjały',
|
||||||
date: 'Data',
|
date: 'Data',
|
||||||
number: 'Numer',
|
number: 'Numer',
|
||||||
image: 'Obraz',
|
image: 'Obraz',
|
||||||
file: 'Plik',
|
file: 'Plik',
|
||||||
|
pay: 'Zapłać',
|
||||||
select: 'Wybierz',
|
select: 'Wybierz',
|
||||||
checkbox: 'Pole wyboru',
|
checkbox: 'Pole wyboru',
|
||||||
multiple: 'Wielokrotne',
|
multiple: 'Wielokrotne',
|
||||||
@@ -335,8 +399,10 @@ const pl = {
|
|||||||
cells: 'Komórki',
|
cells: 'Komórki',
|
||||||
stamp: 'Pieczątka',
|
stamp: 'Pieczątka',
|
||||||
payment: 'Płatność',
|
payment: 'Płatność',
|
||||||
|
number_phone_is_invalid: '{number} telefon jest nieprawidłowy',
|
||||||
phone: 'Telefon',
|
phone: 'Telefon',
|
||||||
submit_form: 'Wyślij Formularz',
|
submit_form: 'Wyślij Formularz',
|
||||||
|
sign_now: 'Podpisz teraz',
|
||||||
type_here_: 'Wpisz tutaj...',
|
type_here_: 'Wpisz tutaj...',
|
||||||
optional: 'opcjonalny',
|
optional: 'opcjonalny',
|
||||||
appears_on: 'Pojawia się na',
|
appears_on: 'Pojawia się na',
|
||||||
@@ -384,13 +450,22 @@ const pl = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const uk = {
|
const uk = {
|
||||||
|
drawn_signature_on_a_touchscreen_device: 'Підпис на сенсорному пристрої',
|
||||||
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Скануйте QR-код за допомогою програми камери, щоб відкрити форму на мобільному пристрої та намалювати свій підпис',
|
||||||
|
by_clicking_you_agree_to_the: 'Натиснувши на "{button}", ви погоджуєтеся з',
|
||||||
|
electronic_signature_disclosure: 'Розголошення Електронного Підпису',
|
||||||
|
esignature_disclosure: 'Розголошення еПідпису',
|
||||||
minimize: 'Зменшити',
|
minimize: 'Зменшити',
|
||||||
text: 'Текст',
|
text: 'Текст',
|
||||||
|
already_paid: 'Вже оплачено',
|
||||||
signature: 'Підпис',
|
signature: 'Підпис',
|
||||||
|
digitally_signed_by: 'Підписано в електронному вигляді',
|
||||||
|
reason: 'Причина',
|
||||||
initials: 'Ініціали',
|
initials: 'Ініціали',
|
||||||
date: 'Дата',
|
date: 'Дата',
|
||||||
number: 'Число',
|
number: 'Число',
|
||||||
image: 'Зображення',
|
image: 'Зображення',
|
||||||
|
pay: 'Оплатити',
|
||||||
file: 'Файл',
|
file: 'Файл',
|
||||||
select: 'Вибрати',
|
select: 'Вибрати',
|
||||||
checkbox: 'Прапорець',
|
checkbox: 'Прапорець',
|
||||||
@@ -399,8 +474,10 @@ const uk = {
|
|||||||
cells: 'Клітинки',
|
cells: 'Клітинки',
|
||||||
stamp: 'Штамп',
|
stamp: 'Штамп',
|
||||||
payment: 'Платіж',
|
payment: 'Платіж',
|
||||||
|
number_phone_is_invalid: '{number} телефон недійсний',
|
||||||
phone: 'Телефон',
|
phone: 'Телефон',
|
||||||
submit_form: 'Надіслати Форму',
|
submit_form: 'Надіслати Форму',
|
||||||
|
sign_now: 'Підписати зараз',
|
||||||
type_here_: 'Введіть тут',
|
type_here_: 'Введіть тут',
|
||||||
optional: 'необов’язково',
|
optional: 'необов’язково',
|
||||||
appears_on: "З'являється на",
|
appears_on: "З'являється на",
|
||||||
@@ -448,13 +525,22 @@ const uk = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cs = {
|
const cs = {
|
||||||
|
drawn_signature_on_a_touchscreen_device: 'Namalovaný podpis na dotykovém zařízení',
|
||||||
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Naskenujte QR kód pomocí aplikace fotoaparátu, abyste otevřeli formulář na mobilním zařízení a nakreslili svůj podpis',
|
||||||
|
by_clicking_you_agree_to_the: 'Kliknutím na "{button}" souhlasíte s',
|
||||||
|
electronic_signature_disclosure: 'Zveřejněním Elektronického Podpisu',
|
||||||
|
esignature_disclosure: 'Zveřejnění ePodpisu',
|
||||||
|
already_paid: 'Už zaplaceno',
|
||||||
minimize: 'Minimalizovat',
|
minimize: 'Minimalizovat',
|
||||||
text: 'Text',
|
text: 'Text',
|
||||||
signature: 'Podpis',
|
signature: 'Podpis',
|
||||||
initials: 'Iniciály',
|
initials: 'Iniciály',
|
||||||
date: 'Datum',
|
date: 'Datum',
|
||||||
number: 'Číslo',
|
number: 'Číslo',
|
||||||
|
digitally_signed_by: 'Digitálně podepsáno',
|
||||||
|
reason: 'Důvod',
|
||||||
image: 'Obrázek',
|
image: 'Obrázek',
|
||||||
|
pay: 'Zaplatit',
|
||||||
file: 'Soubor',
|
file: 'Soubor',
|
||||||
select: 'Vybrat',
|
select: 'Vybrat',
|
||||||
checkbox: 'Zaškrtávací políčko',
|
checkbox: 'Zaškrtávací políčko',
|
||||||
@@ -464,7 +550,9 @@ const cs = {
|
|||||||
stamp: 'Razítko',
|
stamp: 'Razítko',
|
||||||
payment: 'Platba',
|
payment: 'Platba',
|
||||||
phone: 'Telefon',
|
phone: 'Telefon',
|
||||||
|
number_phone_is_invalid: '{number} telefon je neplatný',
|
||||||
submit_form: 'Odeslat formulář',
|
submit_form: 'Odeslat formulář',
|
||||||
|
sign_now: 'Podepsat nyní',
|
||||||
type_here_: 'Zadejte zde',
|
type_here_: 'Zadejte zde',
|
||||||
optional: 'volitelné',
|
optional: 'volitelné',
|
||||||
appears_on: 'Zobrazuje se na',
|
appears_on: 'Zobrazuje se na',
|
||||||
@@ -512,13 +600,22 @@ const cs = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
|
drawn_signature_on_a_touchscreen_device: 'Assinatura desenhada em um dispositivo com tela sensível ao toque',
|
||||||
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Escaneie o código QR com o aplicativo da câmera para abrir o formulário no celular e desenhar sua assinatura',
|
||||||
|
by_clicking_you_agree_to_the: 'Ao clicar em "{button}", você concorda com o',
|
||||||
|
electronic_signature_disclosure: 'Divulgação de Assinatura Eletrônica',
|
||||||
|
esignature_disclosure: 'Divulgação da eAssinatura',
|
||||||
|
already_paid: 'Já pago',
|
||||||
minimize: 'Minimizar',
|
minimize: 'Minimizar',
|
||||||
text: 'Texto',
|
text: 'Texto',
|
||||||
signature: 'Assinatura',
|
signature: 'Assinatura',
|
||||||
initials: 'Iniciais',
|
initials: 'Iniciais',
|
||||||
|
digitally_signed_by: 'Assinado digitalmente por',
|
||||||
|
reason: 'Razão',
|
||||||
date: 'Data',
|
date: 'Data',
|
||||||
number: 'Número',
|
number: 'Número',
|
||||||
image: 'Imagem',
|
image: 'Imagem',
|
||||||
|
pay: 'Pagar',
|
||||||
file: 'Arquivo',
|
file: 'Arquivo',
|
||||||
select: 'Selecionar',
|
select: 'Selecionar',
|
||||||
checkbox: 'Caixa',
|
checkbox: 'Caixa',
|
||||||
@@ -527,8 +624,10 @@ const pt = {
|
|||||||
cells: 'Células',
|
cells: 'Células',
|
||||||
stamp: 'Carimbo',
|
stamp: 'Carimbo',
|
||||||
payment: 'Pagamento',
|
payment: 'Pagamento',
|
||||||
|
number_phone_is_invalid: '{number} telefone é inválido',
|
||||||
phone: 'Telefone',
|
phone: 'Telefone',
|
||||||
submit_form: 'Enviar Formulário',
|
submit_form: 'Enviar Formulário',
|
||||||
|
sign_now: 'Assinar agora',
|
||||||
type_here_: 'Digite aqui',
|
type_here_: 'Digite aqui',
|
||||||
optional: 'opcional',
|
optional: 'opcional',
|
||||||
appears_on: 'Aparece em',
|
appears_on: 'Aparece em',
|
||||||
@@ -576,14 +675,23 @@ const pt = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const he = {
|
const he = {
|
||||||
|
drawn_signature_on_a_touchscreen_device: 'חתימה שנוצרה במכשיר עם מסך מגע',
|
||||||
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'סרוק את קוד ה-QR באמצעות אפליקציית המצלמה כדי לפתוח את הטופס במובייל ולצייר את החתימה שלך',
|
||||||
|
by_clicking_you_agree_to_the: 'על ידי לחיצה על "{button}", אתה מסכים ל',
|
||||||
|
electronic_signature_disclosure: 'חשיפת חתימה אלקטרונית',
|
||||||
|
esignature_disclosure: 'חשיפת ה-eחתימה',
|
||||||
minimize: 'לקטן',
|
minimize: 'לקטן',
|
||||||
text: 'טקסט',
|
text: 'טקסט',
|
||||||
|
already_paid: 'כבר שולם',
|
||||||
signature: 'חתימה',
|
signature: 'חתימה',
|
||||||
initials: 'ראשי תיבות',
|
initials: 'ראשי תיבות',
|
||||||
date: 'תאריך',
|
date: 'תאריך',
|
||||||
|
digitally_signed_by: 'חתום דיגיטלית על ידי',
|
||||||
|
reason: 'סיבה',
|
||||||
number: 'מספר',
|
number: 'מספר',
|
||||||
image: 'תמונה',
|
image: 'תמונה',
|
||||||
file: 'קובץ',
|
file: 'קובץ',
|
||||||
|
pay: 'תשלום',
|
||||||
select: 'בחר',
|
select: 'בחר',
|
||||||
checkbox: 'תיק בחירה',
|
checkbox: 'תיק בחירה',
|
||||||
multiple: 'רב ערכים',
|
multiple: 'רב ערכים',
|
||||||
@@ -592,7 +700,9 @@ const he = {
|
|||||||
stamp: 'חותמת',
|
stamp: 'חותמת',
|
||||||
payment: 'תשלום',
|
payment: 'תשלום',
|
||||||
phone: 'טלפון',
|
phone: 'טלפון',
|
||||||
|
number_phone_is_invalid: '{number} טלפון אינו חוקי',
|
||||||
submit_form: 'שלח טופס',
|
submit_form: 'שלח טופס',
|
||||||
|
sign_now: 'חתום כעת',
|
||||||
type_here_: 'הקלד כאן',
|
type_here_: 'הקלד כאן',
|
||||||
optional: 'אופציונלי',
|
optional: 'אופציונלי',
|
||||||
option: 'אפשרות',
|
option: 'אפשרות',
|
||||||
@@ -641,9 +751,17 @@ const he = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nl = {
|
const nl = {
|
||||||
|
drawn_signature_on_a_touchscreen_device: 'Getekende handtekening op een apparaat met een touchscreen',
|
||||||
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scan de QR-code met de camera-app om het formulier op mobiel te openen en uw handtekening te tekenen',
|
||||||
|
by_clicking_you_agree_to_the: 'Door op "{button}" te klikken, gaat u akkoord met de',
|
||||||
|
electronic_signature_disclosure: 'Openbaarmaking van Elektronische Handtekening',
|
||||||
|
esignature_disclosure: 'Openbaarmaking van eHandtekening',
|
||||||
minimize: 'Minimaliseren',
|
minimize: 'Minimaliseren',
|
||||||
text: 'Tekst',
|
text: 'Tekst',
|
||||||
|
already_paid: 'Al betaald',
|
||||||
signature: 'Handtekening',
|
signature: 'Handtekening',
|
||||||
|
digitally_signed_by: 'Digitaal ondertekend door',
|
||||||
|
reason: 'Reden',
|
||||||
initials: 'Initialen',
|
initials: 'Initialen',
|
||||||
date: 'Datum',
|
date: 'Datum',
|
||||||
number: 'Nummer',
|
number: 'Nummer',
|
||||||
@@ -653,12 +771,15 @@ const nl = {
|
|||||||
select: 'Selecteer',
|
select: 'Selecteer',
|
||||||
checkbox: 'Aankruisvakje',
|
checkbox: 'Aankruisvakje',
|
||||||
multiple: 'Meerdere',
|
multiple: 'Meerdere',
|
||||||
|
pay: 'Betaal',
|
||||||
radio: 'Radio',
|
radio: 'Radio',
|
||||||
cells: 'Cellen',
|
cells: 'Cellen',
|
||||||
stamp: 'Stempel',
|
stamp: 'Stempel',
|
||||||
payment: 'Betaling',
|
payment: 'Betaling',
|
||||||
phone: 'Telefoon',
|
phone: 'Telefoon',
|
||||||
|
number_phone_is_invalid: '{number} telefoon is ongeldig',
|
||||||
submit_form: 'Formulier verzenden',
|
submit_form: 'Formulier verzenden',
|
||||||
|
sign_now: 'Nu ondertekenen',
|
||||||
type_here_: 'Typ hier...',
|
type_here_: 'Typ hier...',
|
||||||
optional: 'Optioneel',
|
optional: 'Optioneel',
|
||||||
option: 'Optie',
|
option: 'Optie',
|
||||||
@@ -706,13 +827,21 @@ const nl = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ar = {
|
const ar = {
|
||||||
|
drawn_signature_on_a_touchscreen_device: 'توقيع مرسوم على جهاز بشاشة تعمل باللمس',
|
||||||
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'امسح رمز الاستجابة السريعة باستخدام تطبيق الكاميرا لفتح النموذج على الهاتف المحمول ورسم توقيعك',
|
||||||
|
by_clicking_you_agree_to_the: 'بالنقر فوق "{button}"، أنت توافق على',
|
||||||
|
electronic_signature_disclosure: 'كشف التوقيع الإلكتروني',
|
||||||
|
esignature_disclosure: 'كشف التوقيع الإلكتروني',
|
||||||
text: 'نص',
|
text: 'نص',
|
||||||
signature: 'توقيع',
|
signature: 'توقيع',
|
||||||
initials: 'الاختصارات',
|
initials: 'الاختصارات',
|
||||||
date: 'تاريخ',
|
date: 'تاريخ',
|
||||||
number: 'رقم',
|
number: 'رقم',
|
||||||
|
digitally_signed_by: 'تم التوقيع رقميًا بواسطة',
|
||||||
|
reason: 'السبب',
|
||||||
image: 'صورة',
|
image: 'صورة',
|
||||||
take_photo: 'التقاط صورة',
|
take_photo: 'التقاط صورة',
|
||||||
|
pay: 'دفع',
|
||||||
file: 'ملف',
|
file: 'ملف',
|
||||||
select: 'اختيار',
|
select: 'اختيار',
|
||||||
checkbox: 'خانة اختيار',
|
checkbox: 'خانة اختيار',
|
||||||
@@ -723,7 +852,9 @@ const ar = {
|
|||||||
minimize: 'تصغير',
|
minimize: 'تصغير',
|
||||||
payment: 'الدفع',
|
payment: 'الدفع',
|
||||||
phone: 'هاتف',
|
phone: 'هاتف',
|
||||||
|
number_phone_is_invalid: '{number} هاتف غير صالح',
|
||||||
submit_form: 'إرسال النموذج',
|
submit_form: 'إرسال النموذج',
|
||||||
|
sign_now: 'وقع الآن',
|
||||||
type_here_: 'اكتب هنا',
|
type_here_: 'اكتب هنا',
|
||||||
optional: 'اختياري',
|
optional: 'اختياري',
|
||||||
option: 'خيار',
|
option: 'خيار',
|
||||||
@@ -770,6 +901,82 @@ const ar = {
|
|||||||
files: 'الملفات'
|
files: 'الملفات'
|
||||||
}
|
}
|
||||||
|
|
||||||
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar }
|
const ko = {
|
||||||
|
drawn_signature_on_a_touchscreen_device: '터치스크린 장치에서 그린 서명',
|
||||||
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: '카메라 앱으로 QR 코드를 스캔하여 모바일에서 양식을 열고 서명을 그리세요',
|
||||||
|
by_clicking_you_agree_to_the: '"{button}"를 클릭함으로써, 다음에 동의하게 됩니다',
|
||||||
|
electronic_signature_disclosure: '전자 서명 공개',
|
||||||
|
esignature_disclosure: '전자 서명 공개',
|
||||||
|
already_paid: '이미 지불됨',
|
||||||
|
text: '텍스트',
|
||||||
|
signature: '서명',
|
||||||
|
initials: '이니셜',
|
||||||
|
date: '날짜',
|
||||||
|
digitally_signed_by: '디지털 서명이 완료되었습니다',
|
||||||
|
reason: '이유',
|
||||||
|
number: '숫자',
|
||||||
|
pay: '급여',
|
||||||
|
image: '이미지',
|
||||||
|
take_photo: '사진 찍기',
|
||||||
|
file: '파일',
|
||||||
|
select: '선택',
|
||||||
|
checkbox: '체크박스',
|
||||||
|
multiple: '다중',
|
||||||
|
radio: '라디오',
|
||||||
|
cells: '셀',
|
||||||
|
stamp: '스탬프',
|
||||||
|
minimize: '최소화',
|
||||||
|
payment: '결제',
|
||||||
|
phone: '전화',
|
||||||
|
number_phone_is_invalid: '{number} 전화번호가 유효하지 않습니다',
|
||||||
|
submit_form: '양식 제출',
|
||||||
|
sign_now: '지금 서명',
|
||||||
|
type_here_: '여기에 입력...',
|
||||||
|
optional: '선택 사항',
|
||||||
|
option: '옵션',
|
||||||
|
appears_on: '다음에 나타납니다',
|
||||||
|
page: '페이지',
|
||||||
|
select_your_option: '옵션 선택',
|
||||||
|
complete_hightlighted_checkboxes_and_click: '강조된 체크박스를 완료하고 클릭',
|
||||||
|
submit: '제출',
|
||||||
|
next: '다음',
|
||||||
|
click_to_upload: '업로드하려면 클릭',
|
||||||
|
or_drag_and_drop_files: '또는 파일을 드래그하여 놓으십시오',
|
||||||
|
send_copy_via_email: '이메일로 사본 보내기',
|
||||||
|
download: '다운로드',
|
||||||
|
clear: '지우기',
|
||||||
|
redraw: '다시 그리기',
|
||||||
|
draw_initials: '이니셜 그리기',
|
||||||
|
type_signature_here: '여기에 서명 입력',
|
||||||
|
type_initial_here: '여기에 이니셜 입력',
|
||||||
|
form_has_been_completed: '양식이 완료되었습니다!',
|
||||||
|
create_a_free_account: '무료 계정 생성',
|
||||||
|
signed_with: '다음으로 서명:',
|
||||||
|
please_check_the_box_to_continue: '계속하려면 확인란을 선택하십시오',
|
||||||
|
open_source_documents_software: '오픈 소스 문서 소프트웨어',
|
||||||
|
verified_phone_number: '전화번호 확인됨',
|
||||||
|
use_international_format: '국제 포맷 사용: +1xxx',
|
||||||
|
six_digits_code: '6자리 코드',
|
||||||
|
change_phone_number: '전화번호 변경',
|
||||||
|
sending: '전송 중...',
|
||||||
|
resend_code: '코드 다시 보내기',
|
||||||
|
verification_code_has_been_resent: '인증 코드가 SMS로 다시 전송되었습니다',
|
||||||
|
please_fill_all_required_fields: '필수 필드를 모두 입력하십시오',
|
||||||
|
set_today: '오늘 날짜로 설정',
|
||||||
|
toggle_multiline_text: '다중 텍스트 전환',
|
||||||
|
draw_signature: '서명 그리기',
|
||||||
|
type_initial: '이니셜 입력',
|
||||||
|
draw: '그리기',
|
||||||
|
type: '입력',
|
||||||
|
type_text: '텍스트 입력',
|
||||||
|
email_has_been_sent: '이메일이 전송되었습니다',
|
||||||
|
processing: '처리 중',
|
||||||
|
pay_with_strip: '스트라이프로 결제',
|
||||||
|
reupload: '다시 업로드',
|
||||||
|
upload: '업로드',
|
||||||
|
files: '파일'
|
||||||
|
}
|
||||||
|
|
||||||
|
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar, ko }
|
||||||
|
|
||||||
export default i18n
|
export default i18n
|
||||||
@@ -24,13 +24,21 @@
|
|||||||
:name="`values[${field.uuid}]`"
|
:name="`values[${field.uuid}]`"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div
|
||||||
|
v-if="!modelValue"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="field.description"
|
||||||
|
dir="auto"
|
||||||
|
class="mb-3 px-1"
|
||||||
|
>
|
||||||
|
<MarkdownContent :string="field.description" />
|
||||||
|
</div>
|
||||||
<FileDropzone
|
<FileDropzone
|
||||||
v-if="!modelValue"
|
|
||||||
:message="`${t('upload')} ${field.name || t('image')}${field.required ? '' : ` (${t('optional')})`}`"
|
:message="`${t('upload')} ${field.name || t('image')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
|
:dry-run="dryRun"
|
||||||
:accept="'image/*'"
|
:accept="'image/*'"
|
||||||
:is-direct-upload="isDirectUpload"
|
|
||||||
@upload="onImageUpload"
|
@upload="onImageUpload"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -39,12 +47,14 @@
|
|||||||
<script>
|
<script>
|
||||||
import FileDropzone from './dropzone'
|
import FileDropzone from './dropzone'
|
||||||
import { IconReload } from '@tabler/icons-vue'
|
import { IconReload } from '@tabler/icons-vue'
|
||||||
|
import MarkdownContent from './markdown_content'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ImageStep',
|
name: 'ImageStep',
|
||||||
components: {
|
components: {
|
||||||
FileDropzone,
|
FileDropzone,
|
||||||
IconReload
|
IconReload,
|
||||||
|
MarkdownContent
|
||||||
},
|
},
|
||||||
inject: ['t'],
|
inject: ['t'],
|
||||||
props: {
|
props: {
|
||||||
@@ -52,16 +62,16 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
isDirectUpload: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
showFieldNames: {
|
showFieldNames: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
dryRun: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
submitterSlug: {
|
submitterSlug: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -1,9 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div dir="auto">
|
<div dir="auto">
|
||||||
<div class="flex justify-between items-center w-full mb-2">
|
<div
|
||||||
|
class="flex justify-between items-center w-full"
|
||||||
|
:class="{ 'mb-2': !field.description }"
|
||||||
|
>
|
||||||
<label
|
<label
|
||||||
class="label text-2xl"
|
class="label text-2xl"
|
||||||
>{{ showFieldNames && field.name ? field.name : t('initials') }}</label>
|
>
|
||||||
|
<MarkdownContent
|
||||||
|
v-if="field.title"
|
||||||
|
:string="field.title"
|
||||||
|
/>
|
||||||
|
<template v-else>
|
||||||
|
{{ showFieldNames && field.name ? field.name : t('initials') }}
|
||||||
|
</template>
|
||||||
|
</label>
|
||||||
<div class="space-x-2 flex">
|
<div class="space-x-2 flex">
|
||||||
<span
|
<span
|
||||||
v-if="isDrawInitials"
|
v-if="isDrawInitials"
|
||||||
@@ -70,6 +81,13 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="field.description"
|
||||||
|
dir="auto"
|
||||||
|
class="mb-3 px-1"
|
||||||
|
>
|
||||||
|
<MarkdownContent :string="field.description" />
|
||||||
|
</div>
|
||||||
<AppearsOn :field="field" />
|
<AppearsOn :field="field" />
|
||||||
<input
|
<input
|
||||||
:value="modelValue || computedPreviousValue"
|
:value="modelValue || computedPreviousValue"
|
||||||
@@ -105,6 +123,7 @@ import { cropCanvasAndExportToPNG } from './crop_canvas'
|
|||||||
import { IconReload, IconTextSize, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
import { IconReload, IconTextSize, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||||
import SignaturePad from 'signature_pad'
|
import SignaturePad from 'signature_pad'
|
||||||
import AppearsOn from './appears_on'
|
import AppearsOn from './appears_on'
|
||||||
|
import MarkdownContent from './markdown_content'
|
||||||
|
|
||||||
const scale = 3
|
const scale = 3
|
||||||
|
|
||||||
@@ -115,6 +134,7 @@ export default {
|
|||||||
IconReload,
|
IconReload,
|
||||||
IconTextSize,
|
IconTextSize,
|
||||||
IconSignature,
|
IconSignature,
|
||||||
|
MarkdownContent,
|
||||||
IconArrowsDiagonalMinimize2
|
IconArrowsDiagonalMinimize2
|
||||||
},
|
},
|
||||||
inject: ['baseUrl', 't'],
|
inject: ['baseUrl', 't'],
|
||||||
@@ -123,6 +143,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
dryRun: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
submitterSlug: {
|
submitterSlug: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
@@ -132,11 +157,6 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
isDirectUpload: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
attachmentsIndex: {
|
attachmentsIndex: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -174,18 +194,12 @@ export default {
|
|||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (this.$refs.canvas) {
|
if (this.$refs.canvas) {
|
||||||
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
|
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
|
||||||
this.$refs.canvas.height = (this.$refs.canvas.parentNode.clientWidth / 3) * scale
|
this.$refs.canvas.height = (this.$refs.canvas.parentNode.clientWidth / 4.5) * scale
|
||||||
|
|
||||||
this.$refs.canvas.getContext('2d').scale(scale, scale)
|
this.$refs.canvas.getContext('2d').scale(scale, scale)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$refs.textInput?.focus()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (this.isDirectUpload) {
|
|
||||||
import('@rails/activestorage')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.$refs.canvas) {
|
if (this.$refs.canvas) {
|
||||||
this.pad = new SignaturePad(this.$refs.canvas)
|
this.pad = new SignaturePad(this.$refs.canvas)
|
||||||
|
|
||||||
@@ -199,7 +213,7 @@ export default {
|
|||||||
entries.forEach(entry => {
|
entries.forEach(entry => {
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
|
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
|
||||||
this.$refs.canvas.height = (this.$refs.canvas.parentNode.clientWidth / 3) * scale
|
this.$refs.canvas.height = (this.$refs.canvas.parentNode.clientWidth / 4.5) * scale
|
||||||
|
|
||||||
this.$refs.canvas.getContext('2d').scale(scale, scale)
|
this.$refs.canvas.getContext('2d').scale(scale, scale)
|
||||||
|
|
||||||
@@ -237,13 +251,28 @@ export default {
|
|||||||
const context = canvas.getContext('2d')
|
const context = canvas.getContext('2d')
|
||||||
|
|
||||||
const fontFamily = 'Arial'
|
const fontFamily = 'Arial'
|
||||||
const fontSize = '44px'
|
const initialFontSize = 50
|
||||||
const fontStyle = 'italic'
|
const fontStyle = 'italic'
|
||||||
const fontWeight = ''
|
const fontWeight = ''
|
||||||
|
|
||||||
context.font = fontStyle + ' ' + fontWeight + ' ' + fontSize + ' ' + fontFamily
|
const setFontSize = (size) => {
|
||||||
context.textAlign = 'center'
|
context.font = `${fontStyle} ${fontWeight} ${size}px ${fontFamily}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const adjustFontSizeToFit = (text, maxWidth, initialSize) => {
|
||||||
|
let size = initialSize
|
||||||
|
|
||||||
|
setFontSize(size)
|
||||||
|
|
||||||
|
while (context.measureText(text).width > maxWidth && size > 1) {
|
||||||
|
size -= 1
|
||||||
|
setFontSize(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
adjustFontSizeToFit(e.target.value, canvas.width / scale, initialFontSize)
|
||||||
|
|
||||||
|
context.textAlign = 'center'
|
||||||
context.clearRect(0, 0, canvas.width / scale, canvas.height / scale)
|
context.clearRect(0, 0, canvas.width / scale, canvas.height / scale)
|
||||||
context.fillText(e.target.value, canvas.width / 2 / scale, canvas.height / 2 / scale + 11)
|
context.fillText(e.target.value, canvas.width / 2 / scale, canvas.height / 2 / scale + 11)
|
||||||
},
|
},
|
||||||
@@ -273,28 +302,19 @@ export default {
|
|||||||
cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => {
|
cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => {
|
||||||
const file = new File([blob], 'initials.png', { type: 'image/png' })
|
const file = new File([blob], 'initials.png', { type: 'image/png' })
|
||||||
|
|
||||||
if (this.isDirectUpload) {
|
if (this.dryRun) {
|
||||||
const { DirectUpload } = await import('@rails/activestorage')
|
const reader = new FileReader()
|
||||||
|
|
||||||
new DirectUpload(
|
reader.readAsDataURL(file)
|
||||||
file,
|
|
||||||
'/direct_uploads'
|
|
||||||
).create((_error, data) => {
|
|
||||||
fetch(this.baseUrl + '/api/attachments', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
submitter_slug: this.submitterSlug,
|
|
||||||
blob_signed_id: data.signed_id,
|
|
||||||
name: 'attachments'
|
|
||||||
}),
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
}).then((resp) => resp.json()).then((attachment) => {
|
|
||||||
this.$emit('update:model-value', attachment.uuid)
|
|
||||||
this.$emit('attached', attachment)
|
|
||||||
|
|
||||||
return resolve(attachment)
|
reader.onloadend = () => {
|
||||||
})
|
const attachment = { url: reader.result, uuid: Math.random().toString() }
|
||||||
})
|
|
||||||
|
this.$emit('attached', attachment)
|
||||||
|
this.$emit('update:model-value', attachment.uuid)
|
||||||
|
|
||||||
|
resolve(attachment)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<span>
|
||||||
|
<template
|
||||||
|
v-for="(item, index) in items"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
v-if="item.startsWith('<a') && item.endsWith('</a>')"
|
||||||
|
:href="sanitizeHref(extractAttr(item, 'href'))"
|
||||||
|
rel="noopener noreferrer nofollow"
|
||||||
|
:class="extractAttr(item, 'class') || 'link'"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{{ extractText(item) }}
|
||||||
|
</a>
|
||||||
|
<b
|
||||||
|
v-else-if="item.startsWith('<b>') || item.startsWith('<strong>')"
|
||||||
|
>
|
||||||
|
{{ extractText(item) }}
|
||||||
|
</b>
|
||||||
|
<i
|
||||||
|
v-else-if="item.startsWith('<i>') || item.startsWith('<em>')"
|
||||||
|
>
|
||||||
|
{{ extractText(item) }}
|
||||||
|
</i>
|
||||||
|
<br
|
||||||
|
v-else-if="item === '<br>' || item === '\n'"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-else
|
||||||
|
>
|
||||||
|
{{ item }}
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import snarkdown from 'snarkdown'
|
||||||
|
|
||||||
|
const htmlSplitRegexp = /(<a.+?<\/a>|<i>.+?<\/i>|<b>.+?<\/b>|<em>.+?<\/em>|<strong>.+?<\/strong>|<br>)/
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'MarkdownContent',
|
||||||
|
props: {
|
||||||
|
string: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
items () {
|
||||||
|
return snarkdown(this.string.replace(/\n/g, '<br>')).split(htmlSplitRegexp)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
sanitizeHref (href) {
|
||||||
|
if (href && href.trim().match(/^((?:https?:\/\/)|\/)/)) {
|
||||||
|
return href.replace(/javascript:/g, '')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extractAttr (text, attr) {
|
||||||
|
if (text.includes(attr)) {
|
||||||
|
return text.split(attr).pop().split('"')[1]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extractText (text) {
|
||||||
|
if (text) {
|
||||||
|
return text.match(/>(.+?)</)?.[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,10 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<label
|
<label
|
||||||
v-if="showFieldNames && field.name"
|
v-if="showFieldNames && (field.name || field.title)"
|
||||||
:for="field.uuid"
|
:for="field.uuid"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="label text-2xl mb-2"
|
class="label text-2xl"
|
||||||
>{{ field.name }}</label>
|
:class="{ 'mb-2': !field.description }"
|
||||||
|
><MarkdownContent
|
||||||
|
v-if="field.title"
|
||||||
|
:string="field.title"
|
||||||
|
/>
|
||||||
|
<template v-else>{{ field.name }}</template>
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
v-if="field.description"
|
||||||
|
dir="auto"
|
||||||
|
class="mb-3 px-1"
|
||||||
|
>
|
||||||
|
<MarkdownContent :string="field.description" />
|
||||||
|
</div>
|
||||||
<div class="flex w-full max-h-44 overflow-y-auto">
|
<div class="flex w-full max-h-44 overflow-y-auto">
|
||||||
<input
|
<input
|
||||||
v-if="modelValue.length === 0"
|
v-if="modelValue.length === 0"
|
||||||
@@ -32,6 +45,7 @@
|
|||||||
<label
|
<label
|
||||||
:for="option.uuid"
|
:for="option.uuid"
|
||||||
class="flex items-center space-x-3"
|
class="flex items-center space-x-3"
|
||||||
|
@click="scrollIntoField(field)"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
:id="option.uuid"
|
:id="option.uuid"
|
||||||
@@ -53,8 +67,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import MarkdownContent from './markdown_content'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MultiSelectStep',
|
name: 'MultiSelectStep',
|
||||||
|
components: {
|
||||||
|
MarkdownContent
|
||||||
|
},
|
||||||
inject: ['t', 'scrollIntoField'],
|
inject: ['t', 'scrollIntoField'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<label
|
<label
|
||||||
v-if="showFieldNames && field.name"
|
v-if="showFieldNames && (field.name || field.title)"
|
||||||
:for="field.uuid"
|
:for="field.uuid"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="label text-2xl"
|
class="label text-2xl"
|
||||||
:class="{ 'mb-2': !field.description }"
|
:class="{ 'mb-2': !field.description }"
|
||||||
><template v-if="field.title"><span v-html="field.title" /></template>
|
><MarkdownContent
|
||||||
|
v-if="field.title"
|
||||||
|
:string="field.title"
|
||||||
|
/>
|
||||||
<template v-else>{{ field.name }}</template>
|
<template v-else>{{ field.name }}</template>
|
||||||
<template v-if="!field.required">({{ t('optional') }})</template>
|
<template v-if="!field.required">
|
||||||
|
({{ t('optional') }})
|
||||||
|
</template>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
@@ -15,9 +20,11 @@
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="field.description"
|
v-if="field.description"
|
||||||
class="mb-3 px-1 text-lg"
|
dir="auto"
|
||||||
v-html="field.description"
|
class="mb-3 px-1"
|
||||||
/>
|
>
|
||||||
|
<MarkdownContent :string="field.description" />
|
||||||
|
</div>
|
||||||
<AppearsOn :field="field" />
|
<AppearsOn :field="field" />
|
||||||
<div class="items-center flex">
|
<div class="items-center flex">
|
||||||
<input
|
<input
|
||||||
@@ -41,11 +48,13 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import AppearsOn from './appears_on'
|
import AppearsOn from './appears_on'
|
||||||
|
import MarkdownContent from './markdown_content'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TextStep',
|
name: 'TextStep',
|
||||||
components: {
|
components: {
|
||||||
AppearsOn
|
AppearsOn,
|
||||||
|
MarkdownContent
|
||||||
},
|
},
|
||||||
inject: ['t'],
|
inject: ['t'],
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -3,8 +3,20 @@
|
|||||||
v-if="!modelValue && !sessionId"
|
v-if="!modelValue && !sessionId"
|
||||||
:for="field.uuid"
|
:for="field.uuid"
|
||||||
class="label text-2xl mb-2"
|
class="label text-2xl mb-2"
|
||||||
>{{ field.name || defaultName }}
|
>
|
||||||
|
<MarkdownContent
|
||||||
|
v-if="field.title"
|
||||||
|
:string="field.title"
|
||||||
|
/>
|
||||||
|
<template v-else>{{ field.name || defaultName }}</template>
|
||||||
</label>
|
</label>
|
||||||
|
<div
|
||||||
|
v-if="field.description"
|
||||||
|
dir="auto"
|
||||||
|
class="mb-4 px-1"
|
||||||
|
>
|
||||||
|
<MarkdownContent :string="field.description" />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -17,7 +29,7 @@
|
|||||||
v-if="modelValue && !sessionId"
|
v-if="modelValue && !sessionId"
|
||||||
class=" text-2xl mb-2"
|
class=" text-2xl mb-2"
|
||||||
>
|
>
|
||||||
Already paid
|
{{ t('already_paid') }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<button
|
<button
|
||||||
@@ -39,7 +51,7 @@
|
|||||||
class="btn bg-[#7B73FF] text-white hover:bg-[#0A2540] text-lg w-full"
|
class="btn bg-[#7B73FF] text-white hover:bg-[#0A2540] text-lg w-full"
|
||||||
:class="{ disabled: isCreatingCheckout }"
|
:class="{ disabled: isCreatingCheckout }"
|
||||||
:disabled="isCreatingCheckout"
|
:disabled="isCreatingCheckout"
|
||||||
@click.prevent="startCheckout"
|
@click.prevent="postCheckout"
|
||||||
>
|
>
|
||||||
<IconInnerShadowTop
|
<IconInnerShadowTop
|
||||||
v-if="isCreatingCheckout"
|
v-if="isCreatingCheckout"
|
||||||
@@ -60,11 +72,13 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconBrandStripe, IconInnerShadowTop, IconLoader } from '@tabler/icons-vue'
|
import { IconBrandStripe, IconInnerShadowTop, IconLoader } from '@tabler/icons-vue'
|
||||||
|
import MarkdownContent from './markdown_content'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PaymentStep',
|
name: 'PaymentStep',
|
||||||
components: {
|
components: {
|
||||||
IconBrandStripe,
|
IconBrandStripe,
|
||||||
|
MarkdownContent,
|
||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
IconLoader
|
IconLoader
|
||||||
},
|
},
|
||||||
@@ -105,18 +119,22 @@ export default {
|
|||||||
currency
|
currency
|
||||||
}).format(price)
|
}).format(price)
|
||||||
|
|
||||||
return `Pay ${formattedPrice}`
|
return this.t('pay') + ' ' + formattedPrice
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
if (this.sessionId) {
|
if (this.sessionId) {
|
||||||
this.$emit('submit')
|
this.$emit('submit')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.sessionId) {
|
||||||
|
this.postCheckout({ checkStatus: true })
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async submit () {
|
async submit () {
|
||||||
if (this.sessionId) {
|
if (this.sessionId) {
|
||||||
return fetch((this.baseUrl || '/embed').replace('/embed', '/api/stripe_payments/' + this.sessionId), {
|
return fetch(this.baseUrl + '/api/stripe_payments/' + this.sessionId, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
submitter_slug: this.submitterSlug
|
submitter_slug: this.submitterSlug
|
||||||
@@ -144,14 +162,16 @@ export default {
|
|||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
startCheckout () {
|
postCheckout ({ checkStatus } = {}) {
|
||||||
this.isCreatingCheckout = true
|
this.isCreatingCheckout = true
|
||||||
|
|
||||||
fetch((this.baseUrl || '/embed').replace('/embed', '/api/stripe_payments'), {
|
fetch(this.baseUrl + '/api/stripe_payments', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
submitter_slug: this.submitterSlug,
|
submitter_slug: this.submitterSlug,
|
||||||
field_uuid: this.field.uuid
|
field_uuid: this.field.uuid,
|
||||||
|
check_status: checkStatus,
|
||||||
|
referer: document.location.href
|
||||||
}),
|
}),
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
}).then(async (resp) => {
|
}).then(async (resp) => {
|
||||||
|
|||||||
@@ -2,10 +2,27 @@
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
:for="isCodeSent ? 'one_time_code' : field.uuid"
|
:for="isCodeSent ? 'one_time_code' : field.uuid"
|
||||||
class="label text-2xl mb-2"
|
class="label text-2xl"
|
||||||
>{{ showFieldNames && field.name ? field.name : t('verified_phone_number') }}
|
:class="{ 'mb-2': !field.description }"
|
||||||
<template v-if="!field.required">({{ t('optional') }})</template>
|
>
|
||||||
|
<MarkdownContent
|
||||||
|
v-if="field.title"
|
||||||
|
:string="field.title"
|
||||||
|
/>
|
||||||
|
<template v-else>
|
||||||
|
{{ showFieldNames && field.name ? field.name : t('verified_phone_number') }}
|
||||||
|
<template v-if="!field.required">
|
||||||
|
({{ t('optional') }})
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
</label>
|
</label>
|
||||||
|
<div
|
||||||
|
v-if="field.description"
|
||||||
|
dir="auto"
|
||||||
|
class="mb-3 px-1"
|
||||||
|
>
|
||||||
|
<MarkdownContent :string="field.description" />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
@@ -53,14 +70,13 @@
|
|||||||
class="base-input !text-2xl w-full"
|
class="base-input !text-2xl w-full"
|
||||||
autocomplete="tel"
|
autocomplete="tel"
|
||||||
pattern="^\+[0-9\s\-]+$"
|
pattern="^\+[0-9\s\-]+$"
|
||||||
:oninvalid="`this.value ? this.setCustomValidity('${t('use_international_format')}...') : ''`"
|
|
||||||
oninput="this.setCustomValidity('')"
|
|
||||||
type="tel"
|
type="tel"
|
||||||
inputmode="tel"
|
inputmode="tel"
|
||||||
:required="field.required"
|
:required="field.required"
|
||||||
placeholder="+1 234 567-8900"
|
placeholder="+1 234 567-8900"
|
||||||
:name="`values[${field.uuid}]`"
|
:name="`values[${field.uuid}]`"
|
||||||
@input="$emit('update:model-value', $event.target.value)"
|
@invalid="$event.target.value ? $event.target.setCustomValidity(`${t('use_international_format')}...`) : ''"
|
||||||
|
@input="[$event.target.setCustomValidity(''), $emit('update:model-value', $event.target.value)]"
|
||||||
@focus="$emit('focus')"
|
@focus="$emit('focus')"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,6 +84,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import MarkdownContent from './markdown_content'
|
||||||
|
|
||||||
function throttle (func, delay) {
|
function throttle (func, delay) {
|
||||||
let lastCallTime = 0
|
let lastCallTime = 0
|
||||||
|
|
||||||
@@ -83,16 +101,29 @@ function throttle (func, delay) {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PhoneStep',
|
name: 'PhoneStep',
|
||||||
|
components: {
|
||||||
|
MarkdownContent
|
||||||
|
},
|
||||||
inject: ['t', 'baseUrl'],
|
inject: ['t', 'baseUrl'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
verifiedValue: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
submitterSlug: {
|
submitterSlug: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
locale: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'en'
|
||||||
|
},
|
||||||
showFieldNames: {
|
showFieldNames: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -139,24 +170,37 @@ export default {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
submitter_slug: this.submitterSlug,
|
submitter_slug: this.submitterSlug,
|
||||||
|
locale: this.locale,
|
||||||
phone: this.$refs.phone.value
|
phone: this.$refs.phone.value
|
||||||
}),
|
}),
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
}).then(async (resp) => {
|
||||||
|
if (resp.status === 422) {
|
||||||
|
const data = await resp.json()
|
||||||
|
|
||||||
|
alert(this.t('number_phone_is_invalid').replace('{number}', this.$refs.phone.value))
|
||||||
|
|
||||||
|
return Promise.reject(new Error(data.error))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
async submit () {
|
async submit () {
|
||||||
|
if (this.verifiedValue && this.verifiedValue === this.modelValue) {
|
||||||
|
return Promise.resolve({})
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.$refs.phone.value.toString().startsWith('+')) {
|
if (!this.$refs.phone.value.toString().startsWith('+')) {
|
||||||
alert(this.t('use_international_format'))
|
alert(this.t('use_international_format'))
|
||||||
|
|
||||||
return Promise.reject(new Error('phone invalid'))
|
return Promise.reject(new Error('phone invalid'))
|
||||||
} else if (!this.isCodeSent) {
|
} else if (!this.isCodeSent) {
|
||||||
this.sendVerificationCode()
|
return this.sendVerificationCode().then(() => {
|
||||||
|
this.$emit('update:model-value', this.$refs.phone.value)
|
||||||
|
|
||||||
this.$emit('update:model-value', this.$refs.phone.value)
|
this.isCodeSent = true
|
||||||
|
|
||||||
this.isCodeSent = true
|
return Promise.reject(new Error('verify with code'))
|
||||||
|
})
|
||||||
return Promise.reject(new Error('verify with code'))
|
|
||||||
} else {
|
} else {
|
||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<div dir="auto">
|
<div
|
||||||
<div class="flex justify-between items-center w-full mb-2">
|
dir="auto"
|
||||||
|
class="relative"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex justify-between items-center w-full"
|
||||||
|
:class="{ 'mb-2': !field.description }"
|
||||||
|
>
|
||||||
<label
|
<label
|
||||||
class="label text-2xl"
|
class="label text-2xl"
|
||||||
>{{ showFieldNames && field.name ? field.name : t('signature') }}</label>
|
>
|
||||||
|
<MarkdownContent
|
||||||
|
v-if="field.title"
|
||||||
|
:string="field.title"
|
||||||
|
/>
|
||||||
|
<template v-else>
|
||||||
|
{{ showFieldNames && field.name ? field.name : t('signature') }}
|
||||||
|
</template>
|
||||||
|
</label>
|
||||||
<div class="space-x-2 flex">
|
<div class="space-x-2 flex">
|
||||||
<span
|
<span
|
||||||
v-if="isTextSignature && field.preferences?.format !== 'typed'"
|
v-if="isTextSignature && field.preferences?.format !== 'typed'"
|
||||||
@@ -14,7 +28,7 @@
|
|||||||
id="type_text_button"
|
id="type_text_button"
|
||||||
href="#"
|
href="#"
|
||||||
class="btn btn-outline btn-sm font-medium"
|
class="btn btn-outline btn-sm font-medium"
|
||||||
@click.prevent="toggleTextInput"
|
@click.prevent="[toggleTextInput(), hideQr()]"
|
||||||
>
|
>
|
||||||
<IconSignature :width="16" />
|
<IconSignature :width="16" />
|
||||||
<span class="hidden sm:inline">
|
<span class="hidden sm:inline">
|
||||||
@@ -30,8 +44,8 @@
|
|||||||
<a
|
<a
|
||||||
id="type_text_button"
|
id="type_text_button"
|
||||||
href="#"
|
href="#"
|
||||||
class="btn btn-outline btn-sm font-medium"
|
class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap"
|
||||||
@click.prevent="toggleTextInput"
|
@click.prevent="[toggleTextInput(), hideQr()]"
|
||||||
>
|
>
|
||||||
<IconTextSize :width="16" />
|
<IconTextSize :width="16" />
|
||||||
<span class="hidden sm:inline">
|
<span class="hidden sm:inline">
|
||||||
@@ -40,12 +54,12 @@
|
|||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="field.preferences?.format !== 'typed'"
|
v-if="field.preferences?.format !== 'typed' && field.preferences?.format !== 'drawn'"
|
||||||
class="tooltip"
|
class="tooltip"
|
||||||
:data-tip="t('take_photo')"
|
:data-tip="t('take_photo')"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="btn btn-outline btn-sm font-medium"
|
class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap"
|
||||||
>
|
>
|
||||||
<IconCamera :width="16" />
|
<IconCamera :width="16" />
|
||||||
<input
|
<input
|
||||||
@@ -69,15 +83,23 @@
|
|||||||
<IconReload :width="16" />
|
<IconReload :width="16" />
|
||||||
{{ t('redraw') }}
|
{{ t('redraw') }}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<span
|
||||||
v-else
|
v-if="withQrButton && !modelValue && !computedPreviousValue && field.preferences?.format !== 'typed'"
|
||||||
href="#"
|
class=" tooltip"
|
||||||
class="btn btn-outline btn-sm font-medium"
|
:data-tip="t('drawn_signature_on_a_touchscreen_device')"
|
||||||
@click.prevent="clear"
|
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<a
|
||||||
{{ t('clear') }}
|
href="#"
|
||||||
</a>
|
class="btn btn-sm btn-neutral font-medium hidden md:flex"
|
||||||
|
:class="{ 'btn-outline': !isShowQr, 'text-white': isShowQr }"
|
||||||
|
@click.prevent="isShowQr ? hideQr() : [isTextSignature = false, showQr()]"
|
||||||
|
>
|
||||||
|
<IconQrcode
|
||||||
|
:width="19"
|
||||||
|
:height="19"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
:title="t('minimize')"
|
:title="t('minimize')"
|
||||||
@@ -91,6 +113,13 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="field.description"
|
||||||
|
dir="auto"
|
||||||
|
class="mb-3 px-1"
|
||||||
|
>
|
||||||
|
<MarkdownContent :string="field.description" />
|
||||||
|
</div>
|
||||||
<AppearsOn :field="field" />
|
<AppearsOn :field="field" />
|
||||||
<input
|
<input
|
||||||
:value="modelValue || computedPreviousValue"
|
:value="modelValue || computedPreviousValue"
|
||||||
@@ -100,14 +129,61 @@
|
|||||||
<img
|
<img
|
||||||
v-if="modelValue || computedPreviousValue"
|
v-if="modelValue || computedPreviousValue"
|
||||||
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
|
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
|
||||||
class="mx-auto bg-white border border-base-300 rounded max-h-72"
|
class="mx-auto bg-white border border-base-300 rounded max-h-44"
|
||||||
>
|
>
|
||||||
<canvas
|
<div class="relative">
|
||||||
v-show="!modelValue && !computedPreviousValue"
|
<div
|
||||||
ref="canvas"
|
v-if="!modelValue && !computedPreviousValue && !isShowQr && !isTextSignature && isSignatureStarted"
|
||||||
style="padding: 1px; 0"
|
class="absolute top-0.5 right-0.5"
|
||||||
class="bg-white border border-base-300 rounded-2xl w-full"
|
>
|
||||||
/>
|
<a
|
||||||
|
href="#"
|
||||||
|
class="btn btn-ghost font-medium btn-xs md:btn-sm"
|
||||||
|
@click.prevent="[clear(), hideQr()]"
|
||||||
|
>
|
||||||
|
<IconReload :width="16" />
|
||||||
|
{{ t('clear') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<canvas
|
||||||
|
v-show="!modelValue && !computedPreviousValue"
|
||||||
|
ref="canvas"
|
||||||
|
style="padding: 1px; 0"
|
||||||
|
class="bg-white border border-base-300 rounded-2xl w-full"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="isShowQr"
|
||||||
|
class="top-0 bottom-0 right-0 left-0 absolute bg-white rounded-2xl m-0.5"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="isShowQr"
|
||||||
|
class="top-0 bottom-0 right-0 left-0 absolute bg-base-content/10 rounded-2xl"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="absolute top-1.5 right-1.5 tooltip"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="btn btn-sm btn-circle btn-normal btn-outline"
|
||||||
|
@click.prevent="hideQr"
|
||||||
|
>
|
||||||
|
<IconX />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-center w-full h-full p-4">
|
||||||
|
<div
|
||||||
|
class="bg-white p-4 rounded-xl h-full"
|
||||||
|
>
|
||||||
|
<canvas
|
||||||
|
ref="qrCanvas"
|
||||||
|
class="h-full"
|
||||||
|
width="132"
|
||||||
|
height="132"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
v-if="isTextSignature"
|
v-if="isTextSignature"
|
||||||
id="signature_text_input"
|
id="signature_text_input"
|
||||||
@@ -118,14 +194,43 @@
|
|||||||
type="text"
|
type="text"
|
||||||
@input="updateWrittenSignature"
|
@input="updateWrittenSignature"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
v-if="isShowQr"
|
||||||
|
dir="auto"
|
||||||
|
class="text-base-content/60 text-xs text-center w-full mt-1"
|
||||||
|
>
|
||||||
|
{{ t('scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature') }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="withDisclosure"
|
||||||
|
dir="auto"
|
||||||
|
class="text-base-content/60 text-xs text-center w-full mt-1"
|
||||||
|
>
|
||||||
|
{{ t('by_clicking_you_agree_to_the').replace('{button}', buttonText.charAt(0).toUpperCase() + buttonText.slice(1)) }} <a
|
||||||
|
href="https://www.docuseal.co/esign-disclosure"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<span class="inline md:hidden">
|
||||||
|
{{ t('esignature_disclosure') }}
|
||||||
|
</span>
|
||||||
|
<span class="hidden md:inline">
|
||||||
|
{{ t('electronic_signature_disclosure') }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="mt-5 md:mt-7"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2, IconQrcode, IconX } from '@tabler/icons-vue'
|
||||||
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
||||||
import SignaturePad from 'signature_pad'
|
import SignaturePad from 'signature_pad'
|
||||||
import AppearsOn from './appears_on'
|
import AppearsOn from './appears_on'
|
||||||
|
import MarkdownContent from './markdown_content'
|
||||||
|
|
||||||
let isFontLoaded = false
|
let isFontLoaded = false
|
||||||
|
|
||||||
@@ -137,6 +242,9 @@ export default {
|
|||||||
AppearsOn,
|
AppearsOn,
|
||||||
IconReload,
|
IconReload,
|
||||||
IconCamera,
|
IconCamera,
|
||||||
|
IconQrcode,
|
||||||
|
MarkdownContent,
|
||||||
|
IconX,
|
||||||
IconTextSize,
|
IconTextSize,
|
||||||
IconSignature,
|
IconSignature,
|
||||||
IconArrowsDiagonalMinimize2
|
IconArrowsDiagonalMinimize2
|
||||||
@@ -147,8 +255,8 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
submitterSlug: {
|
submitter: {
|
||||||
type: String,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
showFieldNames: {
|
showFieldNames: {
|
||||||
@@ -156,16 +264,36 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
isDirectUpload: {
|
dryRun: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
withDisclosure: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
withQrButton: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'Submit'
|
||||||
|
},
|
||||||
withTypedSignature: {
|
withTypedSignature: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
rememberSignature: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
attachmentsIndex: {
|
attachmentsIndex: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -186,12 +314,16 @@ export default {
|
|||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isSignatureStarted: !!this.previousValue,
|
isSignatureStarted: !!this.previousValue,
|
||||||
|
isShowQr: false,
|
||||||
isUsePreviousValue: true,
|
isUsePreviousValue: true,
|
||||||
isTextSignature: this.field.preferences?.format === 'typed',
|
isTextSignature: this.field.preferences?.format === 'typed',
|
||||||
uploadImageInputKey: Math.random().toString()
|
uploadImageInputKey: Math.random().toString()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
submitterSlug () {
|
||||||
|
return this.submitter.slug
|
||||||
|
},
|
||||||
computedPreviousValue () {
|
computedPreviousValue () {
|
||||||
if (this.isUsePreviousValue) {
|
if (this.isUsePreviousValue) {
|
||||||
return this.previousValue
|
return this.previousValue
|
||||||
@@ -210,14 +342,10 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (this.isDirectUpload) {
|
|
||||||
import('@rails/activestorage')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.$refs.canvas) {
|
if (this.$refs.canvas) {
|
||||||
this.pad = new SignaturePad(this.$refs.canvas)
|
this.pad = new SignaturePad(this.$refs.canvas)
|
||||||
|
|
||||||
this.pad.addEventListener('beginStroke', () => {
|
this.pad.addEventListener('endStroke', () => {
|
||||||
this.isSignatureStarted = true
|
this.isSignatureStarted = true
|
||||||
|
|
||||||
this.$emit('start')
|
this.$emit('start')
|
||||||
@@ -241,6 +369,7 @@ export default {
|
|||||||
},
|
},
|
||||||
beforeUnmount () {
|
beforeUnmount () {
|
||||||
this.intersectionObserver?.disconnect()
|
this.intersectionObserver?.disconnect()
|
||||||
|
this.stopCheckSignature()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
remove () {
|
remove () {
|
||||||
@@ -262,6 +391,55 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
showQr () {
|
||||||
|
this.isShowQr = true
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
import('qr-creator').then(({ default: Qr }) => {
|
||||||
|
if (this.$refs.qrCanvas) {
|
||||||
|
Qr.render({
|
||||||
|
text: `${document.location.origin}/p/${this.submitterSlug}?f=${this.field.uuid.split('-')[0]}`,
|
||||||
|
radius: 0.0,
|
||||||
|
ecLevel: 'H',
|
||||||
|
background: null,
|
||||||
|
size: 132
|
||||||
|
}, this.$refs.qrCanvas)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.startCheckSignature()
|
||||||
|
},
|
||||||
|
hideQr () {
|
||||||
|
this.isShowQr = false
|
||||||
|
|
||||||
|
this.stopCheckSignature()
|
||||||
|
},
|
||||||
|
startCheckSignature () {
|
||||||
|
const after = JSON.stringify(new Date())
|
||||||
|
|
||||||
|
this.checkSignatureInterval = setInterval(() => {
|
||||||
|
this.checkSignature({ after })
|
||||||
|
}, 2000)
|
||||||
|
},
|
||||||
|
stopCheckSignature () {
|
||||||
|
if (this.checkSignatureInterval) {
|
||||||
|
clearInterval(this.checkSignatureInterval)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
checkSignature (params = {}) {
|
||||||
|
return fetch(this.baseUrl + '/s/' + this.submitterSlug + '/values?field_uuid=' + this.field.uuid + '&after=' + params.after, {
|
||||||
|
method: 'GET'
|
||||||
|
}).then(async (resp) => {
|
||||||
|
const { attachment } = await resp.json()
|
||||||
|
|
||||||
|
if (attachment?.uuid) {
|
||||||
|
this.$emit('attached', attachment)
|
||||||
|
this.$emit('update:model-value', attachment.uuid)
|
||||||
|
this.hideQr()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
clear () {
|
clear () {
|
||||||
this.pad.clear()
|
this.pad.clear()
|
||||||
|
|
||||||
@@ -269,6 +447,7 @@ export default {
|
|||||||
|
|
||||||
if (this.$refs.textInput) {
|
if (this.$refs.textInput) {
|
||||||
this.$refs.textInput.value = ''
|
this.$refs.textInput.value = ''
|
||||||
|
this.$refs.textInput.focus()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateWrittenSignature (e) {
|
updateWrittenSignature (e) {
|
||||||
@@ -278,13 +457,28 @@ export default {
|
|||||||
const context = canvas.getContext('2d')
|
const context = canvas.getContext('2d')
|
||||||
|
|
||||||
const fontFamily = 'Dancing Script'
|
const fontFamily = 'Dancing Script'
|
||||||
const fontSize = '38px'
|
const initialFontSize = 44
|
||||||
const fontStyle = 'italic'
|
const fontStyle = 'italic'
|
||||||
const fontWeight = ''
|
const fontWeight = ''
|
||||||
|
|
||||||
context.font = fontStyle + ' ' + fontWeight + ' ' + fontSize + ' ' + fontFamily
|
const setFontSize = (size) => {
|
||||||
context.textAlign = 'center'
|
context.font = `${fontStyle} ${fontWeight} ${size}px ${fontFamily}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const adjustFontSizeToFit = (text, maxWidth, initialSize) => {
|
||||||
|
let size = initialSize
|
||||||
|
|
||||||
|
setFontSize(size)
|
||||||
|
|
||||||
|
while (context.measureText(text).width > maxWidth && size > 1) {
|
||||||
|
size -= 1
|
||||||
|
setFontSize(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
adjustFontSizeToFit(e.target.value, canvas.width / scale, initialFontSize)
|
||||||
|
|
||||||
|
context.textAlign = 'center'
|
||||||
context.clearRect(0, 0, canvas.width / scale, canvas.height / scale)
|
context.clearRect(0, 0, canvas.width / scale, canvas.height / scale)
|
||||||
context.fillText(e.target.value, canvas.width / 2 / scale, canvas.height / 2 / scale + 11)
|
context.fillText(e.target.value, canvas.width / 2 / scale, canvas.height / 2 / scale + 11)
|
||||||
},
|
},
|
||||||
@@ -357,6 +551,27 @@ export default {
|
|||||||
this.uploadImageInputKey = Math.random().toString()
|
this.uploadImageInputKey = Math.random().toString()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
maybeSetSignedUuid (signedUuid) {
|
||||||
|
try {
|
||||||
|
if (window.localStorage && signedUuid && this.rememberSignature) {
|
||||||
|
const values = window.localStorage.getItem('signed_signature_uuids')
|
||||||
|
|
||||||
|
let data
|
||||||
|
|
||||||
|
if (values) {
|
||||||
|
data = JSON.parse(values)
|
||||||
|
} else {
|
||||||
|
data = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
data[this.submitter.email] = signedUuid
|
||||||
|
|
||||||
|
window.localStorage.setItem('signed_signature_uuids', JSON.stringify(data))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
},
|
||||||
async submit () {
|
async submit () {
|
||||||
if (this.modelValue || this.computedPreviousValue) {
|
if (this.modelValue || this.computedPreviousValue) {
|
||||||
if (this.computedPreviousValue) {
|
if (this.computedPreviousValue) {
|
||||||
@@ -366,38 +581,30 @@ export default {
|
|||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve, reject) => {
|
||||||
cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => {
|
cropCanvasAndExportToPNG(this.$refs.canvas, { errorOnTooSmall: true }).then(async (blob) => {
|
||||||
const file = new File([blob], 'signature.png', { type: 'image/png' })
|
const file = new File([blob], 'signature.png', { type: 'image/png' })
|
||||||
|
|
||||||
if (this.isDirectUpload) {
|
if (this.dryRun) {
|
||||||
const { DirectUpload } = await import('@rails/activestorage')
|
const reader = new FileReader()
|
||||||
|
|
||||||
new DirectUpload(
|
reader.readAsDataURL(file)
|
||||||
file,
|
|
||||||
'/direct_uploads'
|
|
||||||
).create((_error, data) => {
|
|
||||||
fetch(this.baseUrl + '/api/attachments', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
submitter_slug: this.submitterSlug,
|
|
||||||
blob_signed_id: data.signed_id,
|
|
||||||
name: 'attachments'
|
|
||||||
}),
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
}).then((resp) => resp.json()).then((attachment) => {
|
|
||||||
this.$emit('update:model-value', attachment.uuid)
|
|
||||||
this.$emit('attached', attachment)
|
|
||||||
|
|
||||||
return resolve(attachment)
|
reader.onloadend = () => {
|
||||||
})
|
const attachment = { url: reader.result, uuid: Math.random().toString() }
|
||||||
})
|
|
||||||
|
this.$emit('attached', attachment)
|
||||||
|
this.$emit('update:model-value', attachment.uuid)
|
||||||
|
|
||||||
|
resolve(attachment)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
|
|
||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
formData.append('submitter_slug', this.submitterSlug)
|
formData.append('submitter_slug', this.submitterSlug)
|
||||||
formData.append('name', 'attachments')
|
formData.append('name', 'attachments')
|
||||||
|
formData.append('remember_signature', this.rememberSignature)
|
||||||
|
|
||||||
return fetch(this.baseUrl + '/api/attachments', {
|
return fetch(this.baseUrl + '/api/attachments', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -406,9 +613,17 @@ export default {
|
|||||||
this.$emit('attached', attachment)
|
this.$emit('attached', attachment)
|
||||||
this.$emit('update:model-value', attachment.uuid)
|
this.$emit('update:model-value', attachment.uuid)
|
||||||
|
|
||||||
|
this.maybeSetSignedUuid(attachment.signed_uuid)
|
||||||
|
|
||||||
return resolve(attachment)
|
return resolve(attachment)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
if (error.message === 'Image too small' && this.field.required === false) {
|
||||||
|
return resolve({})
|
||||||
|
} else {
|
||||||
|
return reject(error)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<label
|
<label
|
||||||
v-if="showFieldNames && field.name"
|
v-if="showFieldNames && (field.name || field.title)"
|
||||||
:for="field.uuid"
|
:for="field.uuid"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="label text-2xl"
|
class="label text-2xl"
|
||||||
:class="{ 'mb-2': !field.description }"
|
:class="{ 'mb-2': !field.description }"
|
||||||
><template v-if="field.title"><span v-html="field.title" /></template>
|
><MarkdownContent
|
||||||
|
v-if="field.title"
|
||||||
|
:string="field.title"
|
||||||
|
/>
|
||||||
<template v-else>{{ field.name }}</template>
|
<template v-else>{{ field.name }}</template>
|
||||||
<template v-if="!field.required">({{ t('optional') }})</template>
|
<template v-if="!field.required">
|
||||||
|
({{ t('optional') }})
|
||||||
|
</template>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
@@ -15,9 +20,11 @@
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="field.description"
|
v-if="field.description"
|
||||||
class="mb-3 px-1 text-lg"
|
dir="auto"
|
||||||
v-html="field.description"
|
class="mb-3 px-1"
|
||||||
/>
|
>
|
||||||
|
<MarkdownContent :string="field.description" />
|
||||||
|
</div>
|
||||||
<AppearsOn :field="field" />
|
<AppearsOn :field="field" />
|
||||||
<div class="items-center flex">
|
<div class="items-center flex">
|
||||||
<input
|
<input
|
||||||
@@ -30,11 +37,11 @@
|
|||||||
:class="{ '!pr-11 -mr-10': !field.validation?.pattern }"
|
:class="{ '!pr-11 -mr-10': !field.validation?.pattern }"
|
||||||
:required="field.required"
|
:required="field.required"
|
||||||
:pattern="field.validation?.pattern"
|
:pattern="field.validation?.pattern"
|
||||||
:oninvalid="field.validation?.message ? `this.setCustomValidity(${JSON.stringify(field.validation.message)})` : ''"
|
|
||||||
:oninput="field.validation?.message ? `this.setCustomValidity('')` : ''"
|
|
||||||
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
|
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||||
type="text"
|
type="text"
|
||||||
:name="`values[${field.uuid}]`"
|
:name="`values[${field.uuid}]`"
|
||||||
|
@invalid="field.validation?.message ? $event.target.setCustomValidity(field.validation.message) : ''"
|
||||||
|
@input="field.validation?.message ? $event.target.setCustomValidity('') : ''"
|
||||||
@focus="$emit('focus')"
|
@focus="$emit('focus')"
|
||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
@@ -69,11 +76,13 @@
|
|||||||
<script>
|
<script>
|
||||||
import { IconAlignBoxLeftTop } from '@tabler/icons-vue'
|
import { IconAlignBoxLeftTop } from '@tabler/icons-vue'
|
||||||
import AppearsOn from './appears_on'
|
import AppearsOn from './appears_on'
|
||||||
|
import MarkdownContent from './markdown_content'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TextStep',
|
name: 'TextStep',
|
||||||
components: {
|
components: {
|
||||||
IconAlignBoxLeftTop,
|
IconAlignBoxLeftTop,
|
||||||
|
MarkdownContent,
|
||||||
AppearsOn
|
AppearsOn
|
||||||
},
|
},
|
||||||
inject: ['t'],
|
inject: ['t'],
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
class="absolute overflow-visible group"
|
class="absolute overflow-visible group"
|
||||||
:style="positionStyle"
|
:style="positionStyle"
|
||||||
@pointerdown.stop
|
@pointerdown.stop
|
||||||
@mousedown.stop="startDrag"
|
@mousedown="startMouseMove"
|
||||||
@touchstart="startTouchDrag"
|
@touchstart="startTouchDrag"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="isSelected || isDraw"
|
v-if="isSelected || isDraw"
|
||||||
class="top-0 bottom-0 right-0 left-0 absolute border border-1.5 pointer-events-none"
|
class="top-0 bottom-0 right-0 left-0 absolute border border-1.5 pointer-events-none"
|
||||||
:class="borderColors[submitterIndex]"
|
:class="field.type === 'heading' ? '' : borderColors[submitterIndex]"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="field.type === 'cells' && (isSelected || isDraw)"
|
v-if="field.type === 'cells' && (isSelected || isDraw)"
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
v-for="(cellW, index) in cells"
|
v-for="(cellW, index) in cells"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="absolute top-0 bottom-0 border-r"
|
class="absolute top-0 bottom-0 border-r"
|
||||||
:class="borderColors[submitterIndex]"
|
:class="field.type === 'heading' ? '' : borderColors[submitterIndex]"
|
||||||
:style="{ left: (cellW / area.w * 100) + '%' }"
|
:style="{ left: (cellW / area.w * 100) + '%' }"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -31,18 +31,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="field?.type"
|
v-if="field?.type && (isSelected || isNameFocus)"
|
||||||
class="absolute bg-white rounded-t border overflow-visible whitespace-nowrap group-hover:flex group-hover:z-10"
|
class="absolute bg-white rounded-t border overflow-visible whitespace-nowrap flex z-10"
|
||||||
:class="{ 'flex z-10': isNameFocus || isSelected, invisible: !isNameFocus && !isSelected }"
|
|
||||||
style="top: -25px; height: 25px"
|
style="top: -25px; height: 25px"
|
||||||
@mousedown.stop
|
@mousedown.stop
|
||||||
@pointerdown.stop
|
@pointerdown.stop
|
||||||
>
|
>
|
||||||
<FieldSubmitter
|
<FieldSubmitter
|
||||||
|
v-if="field.type != 'heading'"
|
||||||
v-model="field.submitter_uuid"
|
v-model="field.submitter_uuid"
|
||||||
class="border-r"
|
class="border-r"
|
||||||
:compact="true"
|
:compact="true"
|
||||||
:editable="editable && !defaultField"
|
:editable="editable && (!defaultField || defaultField.role !== submitter?.name)"
|
||||||
:allow-add-new="!defaultSubmitters.length"
|
:allow-add-new="!defaultSubmitters.length"
|
||||||
:menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px] mt-[1px]'"
|
:menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px] mt-[1px]'"
|
||||||
:submitters="template.submitters"
|
:submitters="template.submitters"
|
||||||
@@ -61,19 +61,21 @@
|
|||||||
<span
|
<span
|
||||||
v-if="field.type !== 'checkbox' || field.name"
|
v-if="field.type !== 'checkbox' || field.name"
|
||||||
ref="name"
|
ref="name"
|
||||||
:contenteditable="editable && !defaultField"
|
:contenteditable="editable && !defaultField && field.type !== 'heading'"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="pr-1 cursor-text outline-none block"
|
class="pr-1 cursor-text outline-none block"
|
||||||
style="min-width: 2px"
|
style="min-width: 2px"
|
||||||
|
@paste.prevent="onPaste"
|
||||||
@keydown.enter.prevent="onNameEnter"
|
@keydown.enter.prevent="onNameEnter"
|
||||||
@focus="onNameFocus"
|
@focus="onNameFocus"
|
||||||
@blur="onNameBlur"
|
@blur="onNameBlur"
|
||||||
>{{ optionIndexText }} {{ field.name || defaultName }}</span>
|
>{{ optionIndexText }} {{ (defaultField ? (field.title || field.name) : field.name) || defaultName }}</span>
|
||||||
<div
|
<div
|
||||||
v-if="isNameFocus && !['checkbox', 'phone'].includes(field.type)"
|
v-if="isSettingsFocus || (isValueInput && field.type !== 'heading') || (isNameFocus && !['checkbox', 'phone'].includes(field.type))"
|
||||||
class="flex items-center ml-1.5"
|
class="flex items-center ml-1.5"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
v-if="!isValueInput"
|
||||||
:id="`required-checkbox-${field.uuid}`"
|
:id="`required-checkbox-${field.uuid}`"
|
||||||
v-model="field.required"
|
v-model="field.required"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -81,11 +83,69 @@
|
|||||||
@mousedown.prevent
|
@mousedown.prevent
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
|
v-if="!isValueInput"
|
||||||
:for="`required-checkbox-${field.uuid}`"
|
:for="`required-checkbox-${field.uuid}`"
|
||||||
class="label text-xs"
|
class="label text-xs"
|
||||||
@click.prevent="field.required = !field.required"
|
@click.prevent="field.required = !field.required"
|
||||||
@mousedown.prevent
|
@mousedown.prevent
|
||||||
>{{ t('required') }}</label>
|
>{{ t('required') }}</label>
|
||||||
|
<input
|
||||||
|
v-if="isValueInput"
|
||||||
|
:id="`readonly-checkbox-${field.uuid}`"
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox checkbox-xs no-animation rounded"
|
||||||
|
:checked="!(field.readonly ?? true)"
|
||||||
|
@change="field.readonly = !(field.readonly ?? true)"
|
||||||
|
@mousedown.prevent
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
v-if="isValueInput"
|
||||||
|
:for="`readonly-checkbox-${field.uuid}`"
|
||||||
|
class="label text-xs"
|
||||||
|
@click.prevent="field.readonly = !(field.readonly ?? true)"
|
||||||
|
@mousedown.prevent
|
||||||
|
>{{ t('editable') }}</label>
|
||||||
|
<span
|
||||||
|
v-if="field.type !== 'payment' && !isValueInput"
|
||||||
|
class="dropdown dropdown-end"
|
||||||
|
@mouseenter="renderDropdown = true"
|
||||||
|
@touchstart="renderDropdown = true"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
ref="settingsButton"
|
||||||
|
tabindex="0"
|
||||||
|
:title="t('settings')"
|
||||||
|
class="cursor-pointer flex items-center"
|
||||||
|
style="height: 25px"
|
||||||
|
@focus="isSettingsFocus = true"
|
||||||
|
@blur="maybeBlurSettings"
|
||||||
|
>
|
||||||
|
<IconDotsVertical class="w-5 h-5" />
|
||||||
|
</label>
|
||||||
|
<ul
|
||||||
|
v-if="renderDropdown"
|
||||||
|
ref="settingsDropdown"
|
||||||
|
tabindex="0"
|
||||||
|
class="dropdown-content menu menu-xs px-2 pb-2 pt-1 shadow rounded-box w-52 z-10 rounded-t-none"
|
||||||
|
:style="{ backgroundColor: 'white' }"
|
||||||
|
@dragstart.prevent.stop
|
||||||
|
@click="closeDropdown"
|
||||||
|
@focusout="maybeBlurSettings"
|
||||||
|
>
|
||||||
|
<FieldSettings
|
||||||
|
:field="field"
|
||||||
|
:default-field="defaultField"
|
||||||
|
:editable="editable"
|
||||||
|
:background-color="'white'"
|
||||||
|
:with-required="false"
|
||||||
|
:with-areas="false"
|
||||||
|
@click-formula="isShowFormulaModal = true"
|
||||||
|
@click-description="isShowDescriptionModal = true"
|
||||||
|
@click-condition="isShowConditionsModal = true"
|
||||||
|
@scroll-to="[selectedAreaRef.value = $event, $emit('scroll-to', $event)]"
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
v-else-if="editable"
|
v-else-if="editable"
|
||||||
@@ -97,23 +157,25 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
ref="touchValueTarget"
|
||||||
class="flex items-center h-full w-full"
|
class="flex items-center h-full w-full"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
:class="[bgColors[submitterIndex], field?.default_value ? '' : 'justify-center']"
|
:class="[isValueInput ? 'bg-opacity-50' : 'bg-opacity-80', field.type === 'heading' ? 'bg-gray-50' : bgColors[submitterIndex], isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? (alignClasses[field.preferences?.align] || '') : 'justify-center']"
|
||||||
|
@click="focusValueInput"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-if="field"
|
v-if="field"
|
||||||
class="flex justify-center items-center space-x-1 h-full"
|
class="flex justify-center items-center space-x-1"
|
||||||
:class="{'w-full h-full': field.type == 'checkbox'}"
|
:class="{ 'w-full': ['cells', 'checkbox'].includes(field.type), 'h-full': !isValueInput }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="field?.default_value"
|
v-if="isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas && field.type !== 'checkbox')"
|
||||||
:class="{ 'w-full h-full': field.type == 'checkbox', 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars }"
|
:class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type), 'text-[1.6vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref="textContainer"
|
ref="textContainer"
|
||||||
class="flex items-center px-0.5"
|
class="flex items-center px-0.5"
|
||||||
:class="{'w-full h-full': field.type == 'checkbox'}"
|
:class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type) }"
|
||||||
>
|
>
|
||||||
<IconCheck
|
<IconCheck
|
||||||
v-if="field.type == 'checkbox'"
|
v-if="field.type == 'checkbox'"
|
||||||
@@ -121,8 +183,37 @@
|
|||||||
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else-if="field.type === 'number' && !isValueInput && (field.default_value || field.default_value == 0)"
|
||||||
class="whitespace-pre-wrap"
|
class="whitespace-pre-wrap"
|
||||||
|
>{{ formatNumber(field.default_value, field.preferences?.format) }}</span>
|
||||||
|
<span
|
||||||
|
v-else-if="field.default_value === '{{date}}'"
|
||||||
|
>
|
||||||
|
{{ t('signing_date') }}
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
v-else-if="field.type === 'cells' && field.default_value"
|
||||||
|
class="w-full flex items-center"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(char, index) in field.default_value"
|
||||||
|
:key="index"
|
||||||
|
class="text-center flex-none"
|
||||||
|
:style="{ width: (area.cell_w / area.w * 100) + '%' }"
|
||||||
|
>
|
||||||
|
{{ char }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
ref="defaultValue"
|
||||||
|
:contenteditable="isValueInput"
|
||||||
|
class="whitespace-pre-wrap outline-none empty:before:content-[attr(placeholder)] before:text-gray-400"
|
||||||
|
:class="{ 'cursor-text': isValueInput }"
|
||||||
|
:placeholder="withFieldPlaceholder && !isValueInput ? field.name || defaultName : t('type_value')"
|
||||||
|
@blur="onDefaultValueBlur"
|
||||||
|
@paste.prevent="onPaste"
|
||||||
|
@keydown.enter="onDefaultValueEnter"
|
||||||
>{{ field.default_value }}</span>
|
>{{ field.default_value }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -136,8 +227,12 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
v-if="!isValueInput"
|
||||||
ref="touchTarget"
|
ref="touchTarget"
|
||||||
class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer"
|
class="absolute top-0 bottom-0 right-0 left-0"
|
||||||
|
:class="isDragged ? 'cursor-grab' : 'cursor-pointer'"
|
||||||
|
@dblclick="maybeToggleDefaultValue"
|
||||||
|
@click="maybeToggleCheckboxValue"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
v-if="field?.type && editable"
|
v-if="field?.type && editable"
|
||||||
@@ -145,6 +240,38 @@
|
|||||||
@mousedown.stop="startResize"
|
@mousedown.stop="startResize"
|
||||||
@touchstart="startTouchResize"
|
@touchstart="startTouchResize"
|
||||||
/>
|
/>
|
||||||
|
<Teleport
|
||||||
|
v-if="isShowFormulaModal"
|
||||||
|
:to="modalContainerEl"
|
||||||
|
>
|
||||||
|
<FormulaModal
|
||||||
|
:field="field"
|
||||||
|
:editable="editable && !defaultField"
|
||||||
|
:build-default-name="buildDefaultName"
|
||||||
|
@close="isShowFormulaModal = false"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
<Teleport
|
||||||
|
v-if="isShowConditionsModal"
|
||||||
|
:to="modalContainerEl"
|
||||||
|
>
|
||||||
|
<ConditionsModal
|
||||||
|
:field="field"
|
||||||
|
:build-default-name="buildDefaultName"
|
||||||
|
@close="isShowConditionsModal = false"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
<Teleport
|
||||||
|
v-if="isShowDescriptionModal"
|
||||||
|
:to="modalContainerEl"
|
||||||
|
>
|
||||||
|
<DescriptionModal
|
||||||
|
:field="field"
|
||||||
|
:editable="editable && !defaultField"
|
||||||
|
:build-default-name="buildDefaultName"
|
||||||
|
@close="isShowDescriptionModal = false"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -152,7 +279,11 @@
|
|||||||
import FieldSubmitter from './field_submitter'
|
import FieldSubmitter from './field_submitter'
|
||||||
import FieldType from './field_type'
|
import FieldType from './field_type'
|
||||||
import Field from './field'
|
import Field from './field'
|
||||||
import { IconX, IconCheck } from '@tabler/icons-vue'
|
import FieldSettings from './field_settings'
|
||||||
|
import FormulaModal from './formula_modal'
|
||||||
|
import ConditionsModal from './conditions_modal'
|
||||||
|
import DescriptionModal from './description_modal'
|
||||||
|
import { IconX, IconCheck, IconDotsVertical } from '@tabler/icons-vue'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -160,6 +291,11 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
FieldType,
|
FieldType,
|
||||||
IconCheck,
|
IconCheck,
|
||||||
|
FieldSettings,
|
||||||
|
FormulaModal,
|
||||||
|
IconDotsVertical,
|
||||||
|
DescriptionModal,
|
||||||
|
ConditionsModal,
|
||||||
FieldSubmitter,
|
FieldSubmitter,
|
||||||
IconX
|
IconX
|
||||||
},
|
},
|
||||||
@@ -169,6 +305,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
inputMode: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
isDraw: {
|
isDraw: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -179,6 +320,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
withFieldPlaceholder: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
defaultSubmitters: {
|
defaultSubmitters: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -195,11 +341,18 @@ export default {
|
|||||||
default: null
|
default: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove'],
|
emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
isShowFormulaModal: false,
|
||||||
|
isShowConditionsModal: false,
|
||||||
|
isContenteditable: false,
|
||||||
|
isSettingsFocus: false,
|
||||||
|
isShowDescriptionModal: false,
|
||||||
isResize: false,
|
isResize: false,
|
||||||
isDragged: false,
|
isDragged: false,
|
||||||
|
isMoved: false,
|
||||||
|
renderDropdown: false,
|
||||||
isNameFocus: false,
|
isNameFocus: false,
|
||||||
textOverflowChars: 0,
|
textOverflowChars: 0,
|
||||||
dragFrom: { x: 0, y: 0 }
|
dragFrom: { x: 0, y: 0 }
|
||||||
@@ -208,9 +361,29 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
fieldNames: FieldType.computed.fieldNames,
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
fieldIcons: FieldType.computed.fieldIcons,
|
fieldIcons: FieldType.computed.fieldIcons,
|
||||||
|
isDefaultValuePresent () {
|
||||||
|
if (this.field?.type === 'radio' && this.field?.areas?.length > 1) {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return this.field?.default_value || this.field?.default_value === 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isValueInput () {
|
||||||
|
return (this.field.type === 'heading' && this.isSelected) || this.isContenteditable || (this.inputMode && ['text', 'number', 'date'].includes(this.field.type))
|
||||||
|
},
|
||||||
|
modalContainerEl () {
|
||||||
|
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
||||||
|
},
|
||||||
defaultName () {
|
defaultName () {
|
||||||
return this.buildDefaultName(this.field, this.template.fields)
|
return this.buildDefaultName(this.field, this.template.fields)
|
||||||
},
|
},
|
||||||
|
alignClasses () {
|
||||||
|
return {
|
||||||
|
center: 'justify-center',
|
||||||
|
left: 'justify-start',
|
||||||
|
right: 'justify-end'
|
||||||
|
}
|
||||||
|
},
|
||||||
optionIndexText () {
|
optionIndexText () {
|
||||||
if (this.area.option_uuid && this.field.options) {
|
if (this.area.option_uuid && this.field.options) {
|
||||||
return `${this.field.options.findIndex((o) => o.uuid === this.area.option_uuid) + 1}.`
|
return `${this.field.options.findIndex((o) => o.uuid === this.area.option_uuid) + 1}.`
|
||||||
@@ -263,26 +436,26 @@ export default {
|
|||||||
},
|
},
|
||||||
bgColors () {
|
bgColors () {
|
||||||
return [
|
return [
|
||||||
'bg-red-100/80',
|
'bg-red-100',
|
||||||
'bg-sky-100/80',
|
'bg-sky-100',
|
||||||
'bg-emerald-100/80',
|
'bg-emerald-100',
|
||||||
'bg-yellow-100/80',
|
'bg-yellow-100',
|
||||||
'bg-purple-100/80',
|
'bg-purple-100',
|
||||||
'bg-pink-100/80',
|
'bg-pink-100',
|
||||||
'bg-cyan-100/80',
|
'bg-cyan-100',
|
||||||
'bg-orange-100/80',
|
'bg-orange-100',
|
||||||
'bg-lime-100/80',
|
'bg-lime-100',
|
||||||
'bg-indigo-100/80',
|
'bg-indigo-100',
|
||||||
'bg-red-100/80',
|
'bg-red-100',
|
||||||
'bg-sky-100/80',
|
'bg-sky-100',
|
||||||
'bg-emerald-100/80',
|
'bg-emerald-100',
|
||||||
'bg-yellow-100/80',
|
'bg-yellow-100',
|
||||||
'bg-purple-100/80',
|
'bg-purple-100',
|
||||||
'bg-pink-100/80',
|
'bg-pink-100',
|
||||||
'bg-cyan-100/80',
|
'bg-cyan-100',
|
||||||
'bg-orange-100/80',
|
'bg-orange-100',
|
||||||
'bg-lime-100/80',
|
'bg-lime-100',
|
||||||
'bg-indigo-100/80'
|
'bg-indigo-100'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
isSelected () {
|
isSelected () {
|
||||||
@@ -301,20 +474,78 @@ export default {
|
|||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'field.default_value' () {
|
'field.default_value' () {
|
||||||
if (this.field.type === 'text' && this.field.default_value && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > this.field.default_value.length)) {
|
|
||||||
this.textOverflowChars = this.$el.clientHeight < this.$refs.textContainer.clientHeight ? this.field.default_value.length : 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted () {
|
|
||||||
if (this.field.type === 'text' && this.field.default_value && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > this.field.default_value)) {
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.textOverflowChars = this.$el.clientHeight < this.$refs.textContainer.clientHeight ? this.field.default_value.length : 0
|
if (['date', 'text', 'number'].includes(this.field.type) && this.field.default_value && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.field.default_value}`.length)) {
|
||||||
|
this.textOverflowChars = this.$el.clientHeight < this.$refs.textContainer.clientHeight ? `${this.field.default_value}`.length : 0
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mounted () {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (['date', 'text', 'number'].includes(this.field.type) && this.field.default_value && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.field.default_value}`.length)) {
|
||||||
|
this.textOverflowChars = this.$el.clientHeight < this.$refs.textContainer.clientHeight ? `${this.field.default_value}`.length : 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
buildDefaultName: Field.methods.buildDefaultName,
|
buildDefaultName: Field.methods.buildDefaultName,
|
||||||
|
closeDropdown () {
|
||||||
|
document.activeElement.blur()
|
||||||
|
},
|
||||||
|
maybeToggleDefaultValue () {
|
||||||
|
if (['text', 'number'].includes(this.field.type)) {
|
||||||
|
this.isContenteditable = true
|
||||||
|
|
||||||
|
this.$nextTick(() => this.focusValueInput())
|
||||||
|
} else if (this.field.type === 'checkbox') {
|
||||||
|
this.field.readonly = !this.field.readonly
|
||||||
|
this.field.default_value === true ? delete this.field.default_value : this.field.default_value = true
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
} else if (this.field.type === 'date') {
|
||||||
|
this.field.readonly = !this.field.readonly
|
||||||
|
this.field.default_value === '{{date}}' ? delete this.field.default_value : this.field.default_value = '{{date}}'
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
maybeToggleCheckboxValue () {
|
||||||
|
if (this.inputMode && this.field.type === 'checkbox') {
|
||||||
|
this.field.readonly = !this.field.readonly
|
||||||
|
this.field.default_value === true ? delete this.field.default_value : this.field.default_value = true
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
focusValueInput (e) {
|
||||||
|
if (this.$refs.defaultValue !== document.activeElement) {
|
||||||
|
this.$refs.defaultValue.focus()
|
||||||
|
|
||||||
|
if (this.$refs.defaultValue.innerText.length && this.$refs.defaultValue !== e?.target) {
|
||||||
|
window.getSelection().collapse(
|
||||||
|
this.$refs.defaultValue.firstChild,
|
||||||
|
this.$refs.defaultValue.innerText.length
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formatNumber (number, format) {
|
||||||
|
if (format === 'comma') {
|
||||||
|
return new Intl.NumberFormat('en-US').format(number)
|
||||||
|
} else if (format === 'dot') {
|
||||||
|
return new Intl.NumberFormat('de-DE').format(number)
|
||||||
|
} else if (format === 'space') {
|
||||||
|
return new Intl.NumberFormat('fr-FR').format(number)
|
||||||
|
} else {
|
||||||
|
return number
|
||||||
|
}
|
||||||
|
},
|
||||||
|
maybeBlurSettings (e) {
|
||||||
|
if (!e.relatedTarget || !this.$refs.settingsDropdown.contains(e.relatedTarget)) {
|
||||||
|
this.isSettingsFocus = false
|
||||||
|
}
|
||||||
|
},
|
||||||
onNameFocus (e) {
|
onNameFocus (e) {
|
||||||
this.selectedAreaRef.value = this.area
|
this.selectedAreaRef.value = this.area
|
||||||
|
|
||||||
@@ -341,9 +572,20 @@ export default {
|
|||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
|
onPaste (e) {
|
||||||
|
const text = (e.clipboardData || window.clipboardData).getData('text/plain')
|
||||||
|
|
||||||
|
const selection = this.$el.getRootNode().getSelection()
|
||||||
|
|
||||||
|
if (selection.rangeCount) {
|
||||||
|
selection.deleteFromDocument()
|
||||||
|
selection.getRangeAt(0).insertNode(document.createTextNode(text))
|
||||||
|
selection.collapseToEnd()
|
||||||
|
}
|
||||||
|
},
|
||||||
onResizeCell (e) {
|
onResizeCell (e) {
|
||||||
if (e.target.id === 'mask') {
|
if (e.target.id === 'mask') {
|
||||||
const positionX = e.layerX / (e.target.clientWidth - 1)
|
const positionX = e.offsetX / (e.target.clientWidth - 1)
|
||||||
|
|
||||||
if (positionX > this.area.x) {
|
if (positionX > this.area.x) {
|
||||||
this.area.cell_w = positionX - this.area.x
|
this.area.cell_w = positionX - this.area.x
|
||||||
@@ -357,6 +599,10 @@ export default {
|
|||||||
delete this.field.options
|
delete this.field.options
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (['heading'].includes(this.field.type)) {
|
||||||
|
this.field.readonly = true
|
||||||
|
}
|
||||||
|
|
||||||
if (['select', 'multiple', 'radio'].includes(this.field.type)) {
|
if (['select', 'multiple', 'radio'].includes(this.field.type)) {
|
||||||
this.field.options ||= [{ value: '', uuid: v4() }]
|
this.field.options ||= [{ value: '', uuid: v4() }]
|
||||||
}
|
}
|
||||||
@@ -370,6 +616,10 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
onNameBlur (e) {
|
onNameBlur (e) {
|
||||||
|
if (e.relatedTarget === this.$refs.settingsButton) {
|
||||||
|
this.isSettingsFocus = true
|
||||||
|
}
|
||||||
|
|
||||||
const text = this.$refs.name.innerText.trim()
|
const text = this.$refs.name.innerText.trim()
|
||||||
|
|
||||||
this.isNameFocus = false
|
this.isNameFocus = false
|
||||||
@@ -384,45 +634,65 @@ export default {
|
|||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
|
onDefaultValueBlur (e) {
|
||||||
|
const text = this.$refs.defaultValue.innerText.trim()
|
||||||
|
|
||||||
|
this.isContenteditable = false
|
||||||
|
|
||||||
|
if (text) {
|
||||||
|
if (this.field.type === 'number') {
|
||||||
|
const number = parseFloat(text)
|
||||||
|
|
||||||
|
if (number || number === 0) {
|
||||||
|
this.field.default_value = parseFloat(text)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.field.default_value = text
|
||||||
|
}
|
||||||
|
|
||||||
|
if (![true, false].includes(this.field.readonly)) {
|
||||||
|
this.field.readonly = true
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$refs.defaultValue.innerText = text
|
||||||
|
} else {
|
||||||
|
delete this.field.readonly
|
||||||
|
delete this.field.default_value
|
||||||
|
this.$refs.defaultValue.innerText = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
|
onDefaultValueEnter (e) {
|
||||||
|
if (this.field.type !== 'heading') {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.$refs.defaultValue.blur()
|
||||||
|
}
|
||||||
|
},
|
||||||
onNameEnter (e) {
|
onNameEnter (e) {
|
||||||
this.$refs.name.blur()
|
this.$refs.name.blur()
|
||||||
},
|
},
|
||||||
resize (e) {
|
resize (e) {
|
||||||
if (e.target.id === 'mask') {
|
if (e.target.id === 'mask') {
|
||||||
this.area.w = e.layerX / e.target.clientWidth - this.area.x
|
this.area.w = e.offsetX / e.target.clientWidth - this.area.x
|
||||||
this.area.h = e.layerY / e.target.clientHeight - this.area.y
|
this.area.h = e.offsetY / e.target.clientHeight - this.area.y
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
drag (e) {
|
drag (e) {
|
||||||
if (e.target.id === 'mask') {
|
if (e.target.id === 'mask') {
|
||||||
this.isDragged = true
|
this.isDragged = true
|
||||||
|
|
||||||
this.area.x = (e.layerX - this.dragFrom.x) / e.target.clientWidth
|
this.area.x = (e.offsetX - this.dragFrom.x) / e.target.clientWidth
|
||||||
this.area.y = (e.layerY - this.dragFrom.y) / e.target.clientHeight
|
this.area.y = (e.offsetY - this.dragFrom.y) / e.target.clientHeight
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
startDrag (e) {
|
|
||||||
this.selectedAreaRef.value = this.area
|
|
||||||
|
|
||||||
if (!this.editable) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const rect = e.target.getBoundingClientRect()
|
|
||||||
|
|
||||||
this.dragFrom = { x: e.clientX - rect.left, y: e.clientY - rect.top }
|
|
||||||
|
|
||||||
this.$el.getRootNode().addEventListener('mousemove', this.drag)
|
|
||||||
this.$el.getRootNode().addEventListener('mouseup', this.stopDrag)
|
|
||||||
|
|
||||||
this.$emit('start-drag')
|
|
||||||
},
|
|
||||||
startTouchDrag (e) {
|
startTouchDrag (e) {
|
||||||
if (e.target !== this.$refs.touchTarget) {
|
if (e.target !== this.$refs.touchTarget && e.target !== this.$refs.touchValueTarget) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$refs?.name?.blur()
|
document.activeElement?.blur()
|
||||||
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
@@ -440,11 +710,15 @@ export default {
|
|||||||
this.$emit('start-drag')
|
this.$emit('start-drag')
|
||||||
},
|
},
|
||||||
touchDrag (e) {
|
touchDrag (e) {
|
||||||
|
if (!this.editable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const page = this.$parent.$refs.mask.previousSibling
|
const page = this.$parent.$refs.mask.previousSibling
|
||||||
const rect = page.getBoundingClientRect()
|
const rect = page.getBoundingClientRect()
|
||||||
|
|
||||||
this.area.x = (this.dragFrom.x + e.touches[0].clientX - rect.left) / rect.width
|
this.area.x = Math.min(Math.max((this.dragFrom.x + e.touches[0].clientX - rect.left) / rect.width, 0), 1 - this.area.w)
|
||||||
this.area.y = (this.dragFrom.y + e.touches[0].clientY - rect.top) / rect.height
|
this.area.y = Math.min(Math.max((this.dragFrom.y + e.touches[0].clientY - rect.top) / rect.height, 0), 1 - this.area.h)
|
||||||
},
|
},
|
||||||
stopTouchDrag () {
|
stopTouchDrag () {
|
||||||
this.$el.getRootNode().removeEventListener('touchmove', this.touchDrag)
|
this.$el.getRootNode().removeEventListener('touchmove', this.touchDrag)
|
||||||
@@ -458,6 +732,60 @@ export default {
|
|||||||
|
|
||||||
this.$emit('stop-drag')
|
this.$emit('stop-drag')
|
||||||
},
|
},
|
||||||
|
startMouseMove (e) {
|
||||||
|
if (e.target !== this.$refs.touchTarget && e.target !== this.$refs.touchValueTarget) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.activeElement !== this.$refs.defaultValue) {
|
||||||
|
document.activeElement?.blur()
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.isDragged = true
|
||||||
|
|
||||||
|
const rect = e.target.getBoundingClientRect()
|
||||||
|
|
||||||
|
this.selectedAreaRef.value = this.area
|
||||||
|
|
||||||
|
if (this.field.type === 'heading') {
|
||||||
|
this.$nextTick(() => this.focusValueInput())
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dragFrom = { x: rect.left - e.clientX, y: rect.top - e.clientY }
|
||||||
|
|
||||||
|
this.$el.getRootNode().addEventListener('mousemove', this.mouseMove)
|
||||||
|
this.$el.getRootNode().addEventListener('mouseup', this.stopMouseMove)
|
||||||
|
|
||||||
|
this.$emit('start-drag')
|
||||||
|
},
|
||||||
|
mouseMove (e) {
|
||||||
|
if (!this.editable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isMoved = true
|
||||||
|
|
||||||
|
const page = this.$parent.$refs.mask.previousSibling
|
||||||
|
const rect = page.getBoundingClientRect()
|
||||||
|
|
||||||
|
this.area.x = Math.min(Math.max((this.dragFrom.x + e.clientX - rect.left) / rect.width, 0), 1 - this.area.w)
|
||||||
|
this.area.y = Math.min(Math.max((this.dragFrom.y + e.clientY - rect.top) / rect.height, 0), 1 - this.area.h)
|
||||||
|
},
|
||||||
|
stopMouseMove (e) {
|
||||||
|
this.$el.getRootNode().removeEventListener('mousemove', this.mouseMove)
|
||||||
|
this.$el.getRootNode().removeEventListener('mouseup', this.stopMouseMove)
|
||||||
|
|
||||||
|
if (this.isMoved) {
|
||||||
|
this.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isDragged = false
|
||||||
|
this.isMoved = false
|
||||||
|
|
||||||
|
this.$emit('stop-drag')
|
||||||
|
},
|
||||||
stopDrag () {
|
stopDrag () {
|
||||||
this.$el.getRootNode().removeEventListener('mousemove', this.drag)
|
this.$el.getRootNode().removeEventListener('mousemove', this.drag)
|
||||||
this.$el.getRootNode().removeEventListener('mouseup', this.stopDrag)
|
this.$el.getRootNode().removeEventListener('mouseup', this.stopDrag)
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
Loaded 100 of 334 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user