mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 14:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44f3910266 | ||
|
|
35c6798a80 | ||
|
|
0995c7ee63 | ||
|
|
0a25f78329 | ||
|
|
bb8fffc491 | ||
|
|
6f6c51fb32 | ||
|
|
39d14c15d6 | ||
|
|
17de28965b | ||
|
|
ae7865e508 | ||
|
|
dc8e8cc5c9 | ||
|
|
5208470c38 | ||
|
|
e7ef7b552f | ||
|
|
a478112b07 | ||
|
|
7d0f32e1f9 | ||
|
|
d35d8bead3 | ||
|
|
ab9cc52bcc | ||
|
|
7dc342a6f9 | ||
|
|
80a3ef9193 | ||
|
|
bb723d20cb | ||
|
|
0c24a2016c | ||
|
|
2124965996 | ||
|
|
6f1e104067 | ||
|
|
b20f40b5a3 | ||
|
|
1093ca5fc4 | ||
|
|
0d99c55d3f | ||
|
|
86dc1a88ed | ||
|
|
968c20d8ca | ||
|
|
1b2d978449 | ||
|
|
30582b2d75 | ||
|
|
38a8037a4d | ||
|
|
67de45f787 | ||
|
|
f358f24abb | ||
|
|
fd4e15003a | ||
|
|
81df249d5c | ||
|
|
ba60997992 | ||
|
|
8961294a29 | ||
|
|
bc2e0a000a | ||
|
|
1a693fba2d | ||
|
|
05b7acf6aa | ||
|
|
de13d8d397 | ||
|
|
6d7d8ae87c | ||
|
|
cfb1bdcd52 | ||
|
|
48d41833f1 | ||
|
|
01535f1afc | ||
|
|
fe845bfa36 | ||
|
|
b94dbc75b7 | ||
|
|
bdf40640d0 | ||
|
|
496f6c706b | ||
|
|
79f97d5555 | ||
|
|
82f679c84c | ||
|
|
9b789889fa | ||
|
|
dd8dd118da | ||
|
|
8222a4b6a4 | ||
|
|
31f8057bfa | ||
|
|
1d8e0417bd | ||
|
|
8100ca46c2 | ||
|
|
94d745cf1c | ||
|
|
a92bd0ad0f | ||
|
|
94a13ffca2 | ||
|
|
64c8707957 | ||
|
|
d421a73988 | ||
|
|
f7eef1f8d6 | ||
|
|
08c780d309 | ||
|
|
baaf1f510f | ||
|
|
8898dcc054 | ||
|
|
33521c8aeb | ||
|
|
9db03bd7b9 | ||
|
|
bd600c1fba | ||
|
|
82b18b08f1 | ||
|
|
0aefe616b3 | ||
|
|
59a66bbd16 | ||
|
|
155c708e4f | ||
|
|
5502c2ab1b | ||
|
|
e89b42de40 | ||
|
|
2562c4f432 | ||
|
|
17323074ca | ||
|
|
8a4c5c5e09 | ||
|
|
3033faff0d | ||
|
|
0e6aab0e8d | ||
|
|
75e3dd839a | ||
|
|
830e6eb88d | ||
|
|
3c8fae78d1 | ||
|
|
441c1d7044 | ||
|
|
203f4633f7 | ||
|
|
e558f4d9fb | ||
|
|
990e4020e9 | ||
|
|
1ad3e45519 | ||
|
|
442ced8381 | ||
|
|
ba46202a4d | ||
|
|
1c77a1ee90 | ||
|
|
ea74b59535 | ||
|
|
0e9343d9e2 | ||
|
|
af4cf7257f | ||
|
|
c8d4ee58f8 | ||
|
|
65e57c22e5 | ||
|
|
7351ae0a77 | ||
|
|
f1e0d5d137 | ||
|
|
8a1b71688c | ||
|
|
8ef1c309e0 | ||
|
|
a32dcc0dc5 | ||
|
|
16a2fde4b7 | ||
|
|
7856f7a134 | ||
|
|
c545924fe9 | ||
|
|
b1510bb88c | ||
|
|
31d2024ce5 | ||
|
|
23c49f8706 | ||
|
|
62eb0cda6b | ||
|
|
9f48c6e625 | ||
|
|
83ce401cf2 | ||
|
|
541b025ad0 | ||
|
|
5fd018df46 | ||
|
|
2822b0086a | ||
|
|
e27606f443 | ||
|
|
6c95cd5945 | ||
|
|
d723146060 | ||
|
|
3b03e61ae3 | ||
|
|
172513b96c | ||
|
|
68be42a48d | ||
|
|
0370e363bc | ||
|
|
ced88476cc | ||
|
|
c5d71505ef | ||
|
|
489f9859be | ||
|
|
a87ca65839 | ||
|
|
fa78d0976d | ||
|
|
4aaab2563b | ||
|
|
e073cf2c27 | ||
|
|
173d406b1f | ||
|
|
3bcbad6f04 | ||
|
|
e67854fe56 | ||
|
|
7510e0cb6b | ||
|
|
30c913b54f | ||
|
|
2cda9cd736 | ||
|
|
342d29f272 | ||
|
|
13f76c9944 | ||
|
|
ce4e55f387 | ||
|
|
712107ba5a | ||
|
|
1aa867893e | ||
|
|
4f6142f9c0 | ||
|
|
c303349666 | ||
|
|
e85df170f2 | ||
|
|
01333e5b2e | ||
|
|
514987138a | ||
|
|
0814c6d065 | ||
|
|
542a443fb8 | ||
|
|
f2c6bb1c3d | ||
|
|
b85c9cfdb7 | ||
|
|
e07b50c22d | ||
|
|
4dde9890e0 | ||
|
|
66d1ae2f64 | ||
|
|
d130835902 | ||
|
|
52f66368a3 | ||
|
|
89d0810625 | ||
|
|
06d3d1202b | ||
|
|
0698aad8e1 | ||
|
|
3575a25935 | ||
|
|
cdd034e72d | ||
|
|
26f7d5cf0d | ||
|
|
3c69430b94 | ||
|
|
c0162bccc8 | ||
|
|
1adb800f94 | ||
|
|
333a5d6b7b | ||
|
|
9aa68fe955 | ||
|
|
46af87d89a | ||
|
|
1066131143 | ||
|
|
a25022532e | ||
|
|
5162f9a9dc | ||
|
|
cc5694de43 | ||
|
|
f734ba280d | ||
|
|
500d0da563 | ||
|
|
6626a2b602 | ||
|
|
07e66a6e53 | ||
|
|
deef9c7d3b | ||
|
|
2ec8954dc8 | ||
|
|
c1af803e5b | ||
|
|
10abdbeba4 | ||
|
|
1dbde7a6d8 | ||
|
|
a27eb18670 | ||
|
|
44a49a4533 | ||
|
|
fc11078cae | ||
|
|
a42ae0b39e | ||
|
|
3163cdc761 | ||
|
|
5439c63ce7 | ||
|
|
b78287242b | ||
|
|
b869ae38b3 | ||
|
|
4a320e9ba6 | ||
|
|
0025928a35 | ||
|
|
c227a1988a | ||
|
|
3ade45f541 | ||
|
|
17da8bde80 | ||
|
|
59d9a59769 | ||
|
|
7210badde9 | ||
|
|
21659a73f3 | ||
|
|
c0e49ebeea | ||
|
|
2d24eac4a3 | ||
|
|
14c3c8181b | ||
|
|
036f6049a2 | ||
|
|
236e4cb35f | ||
|
|
ee77cb0982 | ||
|
|
d474ad05e5 | ||
|
|
dbf1fa37c5 | ||
|
|
7444bb8b68 | ||
|
|
f7132445a1 | ||
|
|
0127dd738e | ||
|
|
df561a6211 | ||
|
|
50d3404f3b | ||
|
|
607fc8d338 | ||
|
|
8c366dfc38 | ||
|
|
5ed08fa9ab | ||
|
|
ddb557ebef | ||
|
|
b48615df3f | ||
|
|
86c7b92675 | ||
|
|
041f362cc4 | ||
|
|
65f491325e | ||
|
|
57aef5f5e5 | ||
|
|
e704e6fa04 | ||
|
|
4c99bbb58a | ||
|
|
c0647eef2c | ||
|
|
c619b9be93 | ||
|
|
abab3f9688 | ||
|
|
7b15111e8f | ||
|
|
2c7f38c503 | ||
|
|
ed80df1372 | ||
|
|
3614d1796c | ||
|
|
ce39494f84 | ||
|
|
c639a3f733 | ||
|
|
74bef133b3 | ||
|
|
33142966dc | ||
|
|
42ec8d5fc9 | ||
|
|
01d752e811 | ||
|
|
99b33d4165 | ||
|
|
11a90d797a | ||
|
|
9da7463657 | ||
|
|
b6d350ebbd | ||
|
|
9e87a0373f | ||
|
|
aee55165c5 | ||
|
|
9ddbcae153 | ||
|
|
10cd3aa72a | ||
|
|
6684a27e2f | ||
|
|
b6f7a7dd15 | ||
|
|
309e43e18e | ||
|
|
d5b8640a0e | ||
|
|
49b9064d9f | ||
|
|
698031cfc0 | ||
|
|
6dc21e66e1 | ||
|
|
60b32fec94 | ||
|
|
c621166b45 | ||
|
|
8e2da83f9e | ||
|
|
a39ba5b275 | ||
|
|
edf466a6de | ||
|
|
3b8bb8c5bb | ||
|
|
79f1061353 | ||
|
|
745f2160c5 | ||
|
|
2f0c79444b | ||
|
|
69a3782903 | ||
|
|
3dd7c47558 | ||
|
|
761bb07ee6 | ||
|
|
92d7bc65f5 | ||
|
|
79e158be43 | ||
|
|
41ee2dba6a | ||
|
|
8e1354175f | ||
|
|
2474adffc2 | ||
|
|
b8fd5a77d4 | ||
|
|
18bb57aa99 | ||
|
|
17ff193e34 | ||
|
|
20ac63e524 | ||
|
|
518bbe3d1c | ||
|
|
fa5e3e7163 | ||
|
|
2b8280168e | ||
|
|
82854b8ce8 | ||
|
|
276b4f4a5f | ||
|
|
b73a730390 | ||
|
|
2e98885f41 | ||
|
|
bfece574b9 |
No files matched your search
@@ -30,6 +30,9 @@ jobs:
|
|||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Create .version file
|
||||||
|
run: echo ${{ github.ref_name }} > .version
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -67,8 +67,14 @@ RSpec/MultipleMemoizedHelpers:
|
|||||||
Rails/I18nLocaleTexts:
|
Rails/I18nLocaleTexts:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/FindEach:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
Rails/SkipsModelValidations:
|
Rails/SkipsModelValidations:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Rails/ApplicationController:
|
Rails/ApplicationController:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Capybara/ClickLinkOrButtonStyle:
|
||||||
|
Enabled: false
|
||||||
+14
-4
@@ -1,4 +1,12 @@
|
|||||||
FROM ruby:3.2.2-alpine as webpack
|
FROM ruby:3.2.2-alpine3.18 as fonts
|
||||||
|
|
||||||
|
WORKDIR /fonts
|
||||||
|
|
||||||
|
RUN apk --no-cache add fontforge wget ttf-liberation && cp /usr/share/fonts/liberation/LiberationSans-Regular.ttf /usr/share/fonts/liberation/LiberationSans-Bold.ttf . && wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansArabic/hinted/ttf/NotoSansArabic-Regular.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt
|
||||||
|
|
||||||
|
RUN fontforge -lang=py -c 'font1 = fontforge.open("LiberationSans-Regular.ttf"); font2 = fontforge.open("NotoSansArabic-Regular.ttf"); font1.mergeFonts(font2); font1.generate("LiberationSans-Regular.ttf")'
|
||||||
|
|
||||||
|
FROM ruby:3.2.2-alpine3.18 as webpack
|
||||||
|
|
||||||
ENV RAILS_ENV=production
|
ENV RAILS_ENV=production
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
@@ -24,14 +32,14 @@ COPY ./app/views ./app/views
|
|||||||
|
|
||||||
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
|
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
|
||||||
|
|
||||||
FROM ruby:3.2.2-alpine as app
|
FROM ruby:3.2.2-alpine3.18 as app
|
||||||
|
|
||||||
ENV RAILS_ENV=production
|
ENV RAILS_ENV=production
|
||||||
ENV BUNDLE_WITHOUT="development:test"
|
ENV BUNDLE_WITHOUT="development:test"
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler vips-heif libc6-compat ttf-freefont ttf-liberation && cp /usr/share/fonts/liberation/LiberationSans-Regular.ttf /usr/share/fonts/liberation/LiberationSans-Bold.ttf / && apk del ttf-liberation
|
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
|
||||||
|
|
||||||
COPY ./Gemfile ./Gemfile.lock ./
|
COPY ./Gemfile ./Gemfile.lock ./
|
||||||
|
|
||||||
@@ -45,10 +53,12 @@ COPY ./log ./log
|
|||||||
COPY ./lib ./lib
|
COPY ./lib ./lib
|
||||||
COPY ./public ./public
|
COPY ./public ./public
|
||||||
COPY ./tmp ./tmp
|
COPY ./tmp ./tmp
|
||||||
COPY LICENSE README.md Rakefile config.ru ./
|
COPY LICENSE README.md Rakefile config.ru .version ./
|
||||||
|
|
||||||
|
COPY --from=fonts /fonts/LiberationSans-Regular.ttf /fonts/LiberationSans-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
||||||
COPY --from=webpack /app/public/packs ./public/packs
|
COPY --from=webpack /app/public/packs ./public/packs
|
||||||
|
|
||||||
|
RUN ln -s /fonts /app/public/fonts
|
||||||
RUN bundle exec bootsnap precompile --gemfile app/ lib/
|
RUN bundle exec bootsnap precompile --gemfile app/ lib/
|
||||||
|
|
||||||
WORKDIR /data/docuseal
|
WORKDIR /data/docuseal
|
||||||
|
|||||||
@@ -21,13 +21,12 @@ gem 'jwt'
|
|||||||
gem 'lograge'
|
gem 'lograge'
|
||||||
gem 'mysql2', require: false
|
gem 'mysql2', require: false
|
||||||
gem 'oj'
|
gem 'oj'
|
||||||
gem 'omniauth-google-oauth2'
|
|
||||||
gem 'omniauth-rails_csrf_protection'
|
|
||||||
gem 'pagy'
|
gem 'pagy'
|
||||||
gem 'pdf-reader'
|
gem 'pdf-reader'
|
||||||
gem 'pg', require: false
|
gem 'pg', require: false
|
||||||
gem 'premailer-rails'
|
gem 'premailer-rails'
|
||||||
gem 'puma'
|
gem 'puma'
|
||||||
|
gem 'rack'
|
||||||
gem 'rails'
|
gem 'rails'
|
||||||
gem 'rails_autolink'
|
gem 'rails_autolink'
|
||||||
gem 'rails-i18n'
|
gem 'rails-i18n'
|
||||||
|
|||||||
+197
-205
@@ -2,93 +2,102 @@ GEM
|
|||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
Ascii85 (1.1.0)
|
Ascii85 (1.1.0)
|
||||||
actioncable (7.0.7)
|
actioncable (7.1.2)
|
||||||
actionpack (= 7.0.7)
|
actionpack (= 7.1.2)
|
||||||
activesupport (= 7.0.7)
|
activesupport (= 7.1.2)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailbox (7.0.7)
|
zeitwerk (~> 2.6)
|
||||||
actionpack (= 7.0.7)
|
actionmailbox (7.1.2)
|
||||||
activejob (= 7.0.7)
|
actionpack (= 7.1.2)
|
||||||
activerecord (= 7.0.7)
|
activejob (= 7.1.2)
|
||||||
activestorage (= 7.0.7)
|
activerecord (= 7.1.2)
|
||||||
activesupport (= 7.0.7)
|
activestorage (= 7.1.2)
|
||||||
|
activesupport (= 7.1.2)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
actionmailer (7.0.7)
|
actionmailer (7.1.2)
|
||||||
actionpack (= 7.0.7)
|
actionpack (= 7.1.2)
|
||||||
actionview (= 7.0.7)
|
actionview (= 7.1.2)
|
||||||
activejob (= 7.0.7)
|
activejob (= 7.1.2)
|
||||||
activesupport (= 7.0.7)
|
activesupport (= 7.1.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.0)
|
rails-dom-testing (~> 2.2)
|
||||||
actionpack (7.0.7)
|
actionpack (7.1.2)
|
||||||
actionview (= 7.0.7)
|
actionview (= 7.1.2)
|
||||||
activesupport (= 7.0.7)
|
activesupport (= 7.1.2)
|
||||||
rack (~> 2.0, >= 2.2.4)
|
nokogiri (>= 1.8.5)
|
||||||
|
racc
|
||||||
|
rack (>= 2.2.4)
|
||||||
|
rack-session (>= 1.0.1)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
rails-html-sanitizer (~> 1.6)
|
||||||
actiontext (7.0.7)
|
actiontext (7.1.2)
|
||||||
actionpack (= 7.0.7)
|
actionpack (= 7.1.2)
|
||||||
activerecord (= 7.0.7)
|
activerecord (= 7.1.2)
|
||||||
activestorage (= 7.0.7)
|
activestorage (= 7.1.2)
|
||||||
activesupport (= 7.0.7)
|
activesupport (= 7.1.2)
|
||||||
globalid (>= 0.6.0)
|
globalid (>= 0.6.0)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (7.0.7)
|
actionview (7.1.2)
|
||||||
activesupport (= 7.0.7)
|
activesupport (= 7.1.2)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.11)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
rails-html-sanitizer (~> 1.6)
|
||||||
activejob (7.0.7)
|
activejob (7.1.2)
|
||||||
activesupport (= 7.0.7)
|
activesupport (= 7.1.2)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (7.0.7)
|
activemodel (7.1.2)
|
||||||
activesupport (= 7.0.7)
|
activesupport (= 7.1.2)
|
||||||
activerecord (7.0.7)
|
activerecord (7.1.2)
|
||||||
activemodel (= 7.0.7)
|
activemodel (= 7.1.2)
|
||||||
activesupport (= 7.0.7)
|
activesupport (= 7.1.2)
|
||||||
activestorage (7.0.7)
|
timeout (>= 0.4.0)
|
||||||
actionpack (= 7.0.7)
|
activestorage (7.1.2)
|
||||||
activejob (= 7.0.7)
|
actionpack (= 7.1.2)
|
||||||
activerecord (= 7.0.7)
|
activejob (= 7.1.2)
|
||||||
activesupport (= 7.0.7)
|
activerecord (= 7.1.2)
|
||||||
|
activesupport (= 7.1.2)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
mini_mime (>= 1.1.0)
|
activesupport (7.1.2)
|
||||||
activesupport (7.0.7)
|
base64
|
||||||
|
bigdecimal
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
|
connection_pool (>= 2.2.5)
|
||||||
|
drb
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
|
mutex_m
|
||||||
tzinfo (~> 2.0)
|
tzinfo (~> 2.0)
|
||||||
addressable (2.8.5)
|
addressable (2.8.6)
|
||||||
public_suffix (>= 2.0.2, < 6.0)
|
public_suffix (>= 2.0.2, < 6.0)
|
||||||
afm (0.2.2)
|
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)
|
||||||
ast (2.4.2)
|
ast (2.4.2)
|
||||||
aws-eventstream (1.2.0)
|
aws-eventstream (1.3.0)
|
||||||
aws-partitions (1.807.0)
|
aws-partitions (1.874.0)
|
||||||
aws-sdk-core (3.180.3)
|
aws-sdk-core (3.190.1)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.651.0)
|
aws-partitions (~> 1, >= 1.651.0)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.8)
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
aws-sdk-kms (1.71.0)
|
aws-sdk-kms (1.75.0)
|
||||||
aws-sdk-core (~> 3, >= 3.177.0)
|
aws-sdk-core (~> 3, >= 3.188.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.132.1)
|
aws-sdk-s3 (1.142.0)
|
||||||
aws-sdk-core (~> 3, >= 3.179.0)
|
aws-sdk-core (~> 3, >= 3.189.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.6)
|
aws-sigv4 (~> 1.8)
|
||||||
aws-sigv4 (1.6.0)
|
aws-sigv4 (1.8.0)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
azure-storage-blob (2.0.3)
|
azure-storage-blob (2.0.3)
|
||||||
azure-storage-common (~> 2.0)
|
azure-storage-common (~> 2.0)
|
||||||
@@ -98,8 +107,8 @@ GEM
|
|||||||
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
|
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
|
||||||
net-http-persistent (~> 4.0)
|
net-http-persistent (~> 4.0)
|
||||||
nokogiri (~> 1, >= 1.10.8)
|
nokogiri (~> 1, >= 1.10.8)
|
||||||
base64 (0.1.1)
|
base64 (0.2.0)
|
||||||
bcrypt (3.1.19)
|
bcrypt (3.1.20)
|
||||||
better_html (2.0.2)
|
better_html (2.0.2)
|
||||||
actionview (>= 6.0)
|
actionview (>= 6.0)
|
||||||
activesupport (>= 6.0)
|
activesupport (>= 6.0)
|
||||||
@@ -107,11 +116,12 @@ GEM
|
|||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
parser (>= 2.4)
|
parser (>= 2.4)
|
||||||
smart_properties
|
smart_properties
|
||||||
|
bigdecimal (3.1.5)
|
||||||
bindex (0.8.1)
|
bindex (0.8.1)
|
||||||
bootsnap (1.16.0)
|
bootsnap (1.17.0)
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
builder (3.2.4)
|
builder (3.2.4)
|
||||||
bullet (7.0.7)
|
bullet (7.1.4)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
uniform_notifier (~> 1.11)
|
uniform_notifier (~> 1.11)
|
||||||
cancancan (3.5.0)
|
cancancan (3.5.0)
|
||||||
@@ -132,17 +142,17 @@ GEM
|
|||||||
crack (0.4.5)
|
crack (0.4.5)
|
||||||
rexml
|
rexml
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
css_parser (1.14.0)
|
css_parser (1.16.0)
|
||||||
addressable
|
addressable
|
||||||
cuprite (0.14.3)
|
cuprite (0.15)
|
||||||
capybara (~> 3.0)
|
capybara (~> 3.0)
|
||||||
ferrum (~> 0.13.0)
|
ferrum (~> 0.14.0)
|
||||||
date (3.3.3)
|
date (3.3.4)
|
||||||
debug (1.8.0)
|
debug (1.9.1)
|
||||||
irb (>= 1.5.0)
|
irb (~> 1.10)
|
||||||
reline (>= 0.3.1)
|
reline (>= 0.3.8)
|
||||||
declarative (0.0.20)
|
declarative (0.0.20)
|
||||||
devise (4.9.2)
|
devise (4.9.3)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
orm_adapter (~> 0.1)
|
orm_adapter (~> 0.1)
|
||||||
railties (>= 4.1.0)
|
railties (>= 4.1.0)
|
||||||
@@ -158,8 +168,10 @@ GEM
|
|||||||
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 (2.8.1)
|
||||||
|
drb (2.2.0)
|
||||||
|
ruby2_keywords
|
||||||
email_typo (0.2.3)
|
email_typo (0.2.3)
|
||||||
erb_lint (0.4.0)
|
erb_lint (0.5.0)
|
||||||
activesupport
|
activesupport
|
||||||
better_html (>= 2.0.1)
|
better_html (>= 2.0.1)
|
||||||
parser (>= 2.7.1.4)
|
parser (>= 2.7.1.4)
|
||||||
@@ -167,12 +179,12 @@ GEM
|
|||||||
rubocop
|
rubocop
|
||||||
smart_properties
|
smart_properties
|
||||||
erubi (1.12.0)
|
erubi (1.12.0)
|
||||||
factory_bot (6.2.1)
|
factory_bot (6.4.4)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
factory_bot_rails (6.2.0)
|
factory_bot_rails (6.4.2)
|
||||||
factory_bot (~> 6.2.0)
|
factory_bot (~> 6.4)
|
||||||
railties (>= 5.0.0)
|
railties (>= 5.0.0)
|
||||||
faker (3.2.1)
|
faker (3.2.2)
|
||||||
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)
|
||||||
@@ -201,16 +213,16 @@ GEM
|
|||||||
faraday-retry (1.0.3)
|
faraday-retry (1.0.3)
|
||||||
faraday_middleware (1.2.0)
|
faraday_middleware (1.2.0)
|
||||||
faraday (~> 1.0)
|
faraday (~> 1.0)
|
||||||
ferrum (0.13)
|
ferrum (0.14)
|
||||||
addressable (~> 2.5)
|
addressable (~> 2.5)
|
||||||
concurrent-ruby (~> 1.1)
|
concurrent-ruby (~> 1.1)
|
||||||
webrick (~> 1.7)
|
webrick (~> 1.7)
|
||||||
websocket-driver (>= 0.6, < 0.8)
|
websocket-driver (>= 0.6, < 0.8)
|
||||||
ffi (1.15.5)
|
ffi (1.16.3)
|
||||||
geom2d (0.4.1)
|
geom2d (0.4.1)
|
||||||
globalid (1.1.0)
|
globalid (1.2.1)
|
||||||
activesupport (>= 5.0)
|
activesupport (>= 6.1)
|
||||||
google-apis-core (0.11.1)
|
google-apis-core (0.11.2)
|
||||||
addressable (~> 2.5, >= 2.5.1)
|
addressable (~> 2.5, >= 2.5.1)
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
httpclient (>= 2.8.1, < 3.a)
|
httpclient (>= 2.8.1, < 3.a)
|
||||||
@@ -221,33 +233,32 @@ GEM
|
|||||||
webrick
|
webrick
|
||||||
google-apis-iamcredentials_v1 (0.17.0)
|
google-apis-iamcredentials_v1 (0.17.0)
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
google-apis-storage_v1 (0.19.0)
|
google-apis-storage_v1 (0.29.0)
|
||||||
google-apis-core (>= 0.9.0, < 2.a)
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
google-cloud-core (1.6.0)
|
google-cloud-core (1.6.1)
|
||||||
google-cloud-env (~> 1.0)
|
google-cloud-env (>= 1.0, < 3.a)
|
||||||
google-cloud-errors (~> 1.0)
|
google-cloud-errors (~> 1.0)
|
||||||
google-cloud-env (1.6.0)
|
google-cloud-env (2.1.0)
|
||||||
faraday (>= 0.17.3, < 3.0)
|
faraday (>= 1.0, < 3.a)
|
||||||
google-cloud-errors (1.3.1)
|
google-cloud-errors (1.3.1)
|
||||||
google-cloud-storage (1.44.0)
|
google-cloud-storage (1.45.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
digest-crc (~> 0.4)
|
digest-crc (~> 0.4)
|
||||||
google-apis-iamcredentials_v1 (~> 0.1)
|
google-apis-iamcredentials_v1 (~> 0.1)
|
||||||
google-apis-storage_v1 (~> 0.19.0)
|
google-apis-storage_v1 (~> 0.29.0)
|
||||||
google-cloud-core (~> 1.6)
|
google-cloud-core (~> 1.6)
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
mini_mime (~> 1.0)
|
mini_mime (~> 1.0)
|
||||||
googleauth (1.7.0)
|
googleauth (1.9.1)
|
||||||
faraday (>= 0.17.3, < 3.a)
|
faraday (>= 1.0, < 3.a)
|
||||||
|
google-cloud-env (~> 2.1)
|
||||||
jwt (>= 1.4, < 3.0)
|
jwt (>= 1.4, < 3.0)
|
||||||
memoist (~> 0.16)
|
|
||||||
multi_json (~> 1.11)
|
multi_json (~> 1.11)
|
||||||
os (>= 0.9, < 2.0)
|
os (>= 0.9, < 2.0)
|
||||||
signet (>= 0.16, < 2.a)
|
signet (>= 0.16, < 2.a)
|
||||||
hashdiff (1.0.1)
|
hashdiff (1.1.0)
|
||||||
hashery (2.1.2)
|
hashery (2.1.2)
|
||||||
hashie (5.0.0)
|
hexapdf (0.34.1)
|
||||||
hexapdf (0.33.0)
|
|
||||||
cmdparse (~> 3.0, >= 3.0.3)
|
cmdparse (~> 3.0, >= 3.0.3)
|
||||||
geom2d (~> 0.4, >= 0.4.1)
|
geom2d (~> 0.4, >= 0.4.1)
|
||||||
openssl (>= 2.2.1)
|
openssl (>= 2.2.1)
|
||||||
@@ -258,11 +269,12 @@ GEM
|
|||||||
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.6.0)
|
io-console (0.7.1)
|
||||||
irb (1.7.4)
|
irb (1.11.0)
|
||||||
reline (>= 0.3.6)
|
rdoc
|
||||||
|
reline (>= 0.3.8)
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.6.3)
|
json (2.7.1)
|
||||||
jwt (2.7.1)
|
jwt (2.7.1)
|
||||||
language_server-protocol (3.17.0.3)
|
language_server-protocol (3.17.0.3)
|
||||||
launchy (2.5.2)
|
launchy (2.5.2)
|
||||||
@@ -274,12 +286,12 @@ GEM
|
|||||||
letter_opener (~> 1.7)
|
letter_opener (~> 1.7)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
rexml
|
rexml
|
||||||
lograge (0.13.0)
|
lograge (0.14.0)
|
||||||
actionpack (>= 4)
|
actionpack (>= 4)
|
||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
railties (>= 4)
|
railties (>= 4)
|
||||||
request_store (~> 1.0)
|
request_store (~> 1.0)
|
||||||
loofah (2.21.3)
|
loofah (2.22.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
mail (2.8.1)
|
mail (2.8.1)
|
||||||
@@ -289,72 +301,48 @@ GEM
|
|||||||
net-smtp
|
net-smtp
|
||||||
marcel (1.0.2)
|
marcel (1.0.2)
|
||||||
matrix (0.4.2)
|
matrix (0.4.2)
|
||||||
memoist (0.16.2)
|
|
||||||
method_source (1.0.0)
|
method_source (1.0.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.4)
|
mini_portile2 (2.8.5)
|
||||||
minitest (5.19.0)
|
minitest (5.20.0)
|
||||||
msgpack (1.7.2)
|
msgpack (1.7.2)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multi_xml (0.6.0)
|
|
||||||
multipart-post (2.3.0)
|
multipart-post (2.3.0)
|
||||||
|
mutex_m (0.2.0)
|
||||||
mysql2 (0.5.5)
|
mysql2 (0.5.5)
|
||||||
net-http-persistent (4.0.2)
|
net-http-persistent (4.0.2)
|
||||||
connection_pool (~> 2.2)
|
connection_pool (~> 2.2)
|
||||||
net-imap (0.3.7)
|
net-imap (0.4.9)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
net-pop (0.1.2)
|
net-pop (0.1.2)
|
||||||
net-protocol
|
net-protocol
|
||||||
net-protocol (0.2.1)
|
net-protocol (0.2.2)
|
||||||
timeout
|
timeout
|
||||||
net-smtp (0.3.3)
|
net-smtp (0.4.0)
|
||||||
net-protocol
|
net-protocol
|
||||||
nio4r (2.5.9)
|
nio4r (2.7.0)
|
||||||
nokogiri (1.15.4)
|
nokogiri (1.15.5)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.15.4-arm64-darwin)
|
oj (3.16.3)
|
||||||
racc (~> 1.4)
|
bigdecimal (>= 3.0)
|
||||||
oauth2 (2.0.9)
|
openssl (3.2.0)
|
||||||
faraday (>= 0.17.3, < 3.0)
|
|
||||||
jwt (>= 1.0, < 3.0)
|
|
||||||
multi_xml (~> 0.5)
|
|
||||||
rack (>= 1.2, < 4)
|
|
||||||
snaky_hash (~> 2.0)
|
|
||||||
version_gem (~> 1.1)
|
|
||||||
oj (3.16.0)
|
|
||||||
omniauth (2.1.1)
|
|
||||||
hashie (>= 3.4.6)
|
|
||||||
rack (>= 2.2.3)
|
|
||||||
rack-protection
|
|
||||||
omniauth-google-oauth2 (1.1.1)
|
|
||||||
jwt (>= 2.0)
|
|
||||||
oauth2 (~> 2.0.6)
|
|
||||||
omniauth (~> 2.0)
|
|
||||||
omniauth-oauth2 (~> 1.8.0)
|
|
||||||
omniauth-oauth2 (1.8.0)
|
|
||||||
oauth2 (>= 1.4, < 3)
|
|
||||||
omniauth (~> 2.0)
|
|
||||||
omniauth-rails_csrf_protection (1.0.1)
|
|
||||||
actionpack (>= 4.2)
|
|
||||||
omniauth (~> 2.0)
|
|
||||||
openssl (3.1.0)
|
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
pagy (6.0.4)
|
pagy (6.2.0)
|
||||||
parallel (1.23.0)
|
parallel (1.24.0)
|
||||||
parser (3.2.2.3)
|
parser (3.2.2.4)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
pdf-reader (2.11.0)
|
pdf-reader (2.12.0)
|
||||||
Ascii85 (~> 1.0)
|
Ascii85 (~> 1.0)
|
||||||
afm (~> 0.2.1)
|
afm (~> 0.2.1)
|
||||||
hashery (~> 2.0)
|
hashery (~> 2.0)
|
||||||
ruby-rc4
|
ruby-rc4
|
||||||
ttfunk
|
ttfunk
|
||||||
pg (1.5.3)
|
pg (1.5.4)
|
||||||
premailer (1.21.0)
|
premailer (1.21.0)
|
||||||
addressable
|
addressable
|
||||||
css_parser (>= 1.12.0)
|
css_parser (>= 1.12.0)
|
||||||
@@ -368,31 +356,36 @@ GEM
|
|||||||
method_source (~> 1.0)
|
method_source (~> 1.0)
|
||||||
pry-rails (0.3.9)
|
pry-rails (0.3.9)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
public_suffix (5.0.3)
|
psych (5.1.2)
|
||||||
puma (6.3.1)
|
stringio
|
||||||
|
public_suffix (5.0.4)
|
||||||
|
puma (6.4.0)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
racc (1.7.1)
|
racc (1.7.3)
|
||||||
rack (2.2.8)
|
rack (2.2.8)
|
||||||
rack-protection (3.1.0)
|
rack-proxy (0.7.7)
|
||||||
rack (~> 2.2, >= 2.2.4)
|
|
||||||
rack-proxy (0.7.6)
|
|
||||||
rack
|
rack
|
||||||
|
rack-session (1.0.2)
|
||||||
|
rack (< 3)
|
||||||
rack-test (2.1.0)
|
rack-test (2.1.0)
|
||||||
rack (>= 1.3)
|
rack (>= 1.3)
|
||||||
rails (7.0.7)
|
rackup (1.0.0)
|
||||||
actioncable (= 7.0.7)
|
rack (< 3)
|
||||||
actionmailbox (= 7.0.7)
|
webrick
|
||||||
actionmailer (= 7.0.7)
|
rails (7.1.2)
|
||||||
actionpack (= 7.0.7)
|
actioncable (= 7.1.2)
|
||||||
actiontext (= 7.0.7)
|
actionmailbox (= 7.1.2)
|
||||||
actionview (= 7.0.7)
|
actionmailer (= 7.1.2)
|
||||||
activejob (= 7.0.7)
|
actionpack (= 7.1.2)
|
||||||
activemodel (= 7.0.7)
|
actiontext (= 7.1.2)
|
||||||
activerecord (= 7.0.7)
|
actionview (= 7.1.2)
|
||||||
activestorage (= 7.0.7)
|
activejob (= 7.1.2)
|
||||||
activesupport (= 7.0.7)
|
activemodel (= 7.1.2)
|
||||||
|
activerecord (= 7.1.2)
|
||||||
|
activestorage (= 7.1.2)
|
||||||
|
activesupport (= 7.1.2)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 7.0.7)
|
railties (= 7.1.2)
|
||||||
rails-dom-testing (2.2.0)
|
rails-dom-testing (2.2.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
minitest
|
minitest
|
||||||
@@ -407,19 +400,22 @@ GEM
|
|||||||
actionview (> 3.1)
|
actionview (> 3.1)
|
||||||
activesupport (> 3.1)
|
activesupport (> 3.1)
|
||||||
railties (> 3.1)
|
railties (> 3.1)
|
||||||
railties (7.0.7)
|
railties (7.1.2)
|
||||||
actionpack (= 7.0.7)
|
actionpack (= 7.1.2)
|
||||||
activesupport (= 7.0.7)
|
activesupport (= 7.1.2)
|
||||||
method_source
|
irb
|
||||||
|
rackup (>= 1.0.0)
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0, >= 1.2.2)
|
||||||
zeitwerk (~> 2.5)
|
zeitwerk (~> 2.6)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.0.6)
|
rake (13.1.0)
|
||||||
redis-client (0.16.0)
|
rdoc (6.6.2)
|
||||||
|
psych (>= 4.0.0)
|
||||||
|
redis-client (0.19.1)
|
||||||
connection_pool
|
connection_pool
|
||||||
regexp_parser (2.8.1)
|
regexp_parser (2.8.3)
|
||||||
reline (0.3.8)
|
reline (0.4.1)
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
representable (3.2.0)
|
representable (3.2.0)
|
||||||
declarative (< 0.1.0)
|
declarative (< 0.1.0)
|
||||||
@@ -427,12 +423,12 @@ GEM
|
|||||||
uber (< 0.2.0)
|
uber (< 0.2.0)
|
||||||
request_store (1.5.1)
|
request_store (1.5.1)
|
||||||
rack (>= 1.4)
|
rack (>= 1.4)
|
||||||
responders (3.1.0)
|
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.2.6)
|
||||||
rollbar (3.4.0)
|
rollbar (3.4.2)
|
||||||
rotp (6.3.0)
|
rotp (6.3.0)
|
||||||
rqrcode (2.2.0)
|
rqrcode (2.2.0)
|
||||||
chunky_png (~> 1.0)
|
chunky_png (~> 1.0)
|
||||||
@@ -446,7 +442,7 @@ GEM
|
|||||||
rspec-mocks (3.12.6)
|
rspec-mocks (3.12.6)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.12.0)
|
rspec-support (~> 3.12.0)
|
||||||
rspec-rails (6.0.3)
|
rspec-rails (6.1.0)
|
||||||
actionpack (>= 6.1)
|
actionpack (>= 6.1)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
railties (>= 6.1)
|
railties (>= 6.1)
|
||||||
@@ -455,38 +451,38 @@ GEM
|
|||||||
rspec-mocks (~> 3.12)
|
rspec-mocks (~> 3.12)
|
||||||
rspec-support (~> 3.12)
|
rspec-support (~> 3.12)
|
||||||
rspec-support (3.12.1)
|
rspec-support (3.12.1)
|
||||||
rubocop (1.56.1)
|
rubocop (1.59.0)
|
||||||
base64 (~> 0.1.1)
|
|
||||||
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.3)
|
parser (>= 3.2.2.4)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 1.8, < 3.0)
|
regexp_parser (>= 1.8, < 3.0)
|
||||||
rexml (>= 3.2.5, < 4.0)
|
rexml (>= 3.2.5, < 4.0)
|
||||||
rubocop-ast (>= 1.28.1, < 2.0)
|
rubocop-ast (>= 1.30.0, < 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.29.0)
|
rubocop-ast (1.30.0)
|
||||||
parser (>= 3.2.1.0)
|
parser (>= 3.2.1.0)
|
||||||
rubocop-capybara (2.18.0)
|
rubocop-capybara (2.19.0)
|
||||||
rubocop (~> 1.41)
|
rubocop (~> 1.41)
|
||||||
rubocop-factory_bot (2.23.1)
|
rubocop-factory_bot (2.24.0)
|
||||||
rubocop (~> 1.33)
|
rubocop (~> 1.33)
|
||||||
rubocop-performance (1.19.0)
|
rubocop-performance (1.20.1)
|
||||||
rubocop (>= 1.7.0, < 2.0)
|
rubocop (>= 1.48.1, < 2.0)
|
||||||
rubocop-ast (>= 0.4.0)
|
rubocop-ast (>= 1.30.0, < 2.0)
|
||||||
rubocop-rails (2.20.2)
|
rubocop-rails (2.23.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-rspec (2.23.2)
|
rubocop-ast (>= 1.30.0, < 2.0)
|
||||||
rubocop (~> 1.33)
|
rubocop-rspec (2.25.0)
|
||||||
|
rubocop (~> 1.40)
|
||||||
rubocop-capybara (~> 2.17)
|
rubocop-capybara (~> 2.17)
|
||||||
rubocop-factory_bot (~> 2.22)
|
rubocop-factory_bot (~> 2.22)
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
ruby-rc4 (0.1.5)
|
ruby-rc4 (0.1.5)
|
||||||
ruby-vips (2.1.4)
|
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.25)
|
||||||
@@ -494,17 +490,17 @@ GEM
|
|||||||
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.0.3)
|
shakapacker (7.1.0)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
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.1.2)
|
sidekiq (7.2.0)
|
||||||
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.14.0)
|
||||||
signet (0.17.0)
|
signet (0.18.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,38 +512,35 @@ 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)
|
||||||
snaky_hash (2.0.1)
|
sqlite3 (1.7.0)
|
||||||
hashie
|
|
||||||
version_gem (~> 1.1, >= 1.1.1)
|
|
||||||
sqlite3 (1.6.3)
|
|
||||||
mini_portile2 (~> 2.8.0)
|
mini_portile2 (~> 2.8.0)
|
||||||
sqlite3 (1.6.3-arm64-darwin)
|
sqlite3 (1.7.0-arm64-darwin)
|
||||||
|
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.2.2)
|
thor (1.3.0)
|
||||||
timeout (0.4.0)
|
timeout (0.4.1)
|
||||||
trailblazer-option (0.1.2)
|
trailblazer-option (0.1.2)
|
||||||
ttfunk (1.7.0)
|
ttfunk (1.7.0)
|
||||||
turbo-rails (1.4.0)
|
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)
|
||||||
tzinfo (2.0.6)
|
tzinfo (2.0.6)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
tzinfo-data (1.2023.3)
|
tzinfo-data (1.2023.4)
|
||||||
tzinfo (>= 1.0.0)
|
tzinfo (>= 1.0.0)
|
||||||
uber (0.1.0)
|
uber (0.1.0)
|
||||||
unicode-display_width (2.4.2)
|
unicode-display_width (2.5.0)
|
||||||
uniform_notifier (1.16.0)
|
uniform_notifier (1.16.0)
|
||||||
version_gem (1.1.3)
|
|
||||||
warden (1.2.9)
|
warden (1.2.9)
|
||||||
rack (>= 2.0.9)
|
rack (>= 2.0.9)
|
||||||
web-console (4.2.0)
|
web-console (4.2.1)
|
||||||
actionview (>= 6.0.0)
|
actionview (>= 6.0.0)
|
||||||
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.18.1)
|
webmock (3.19.1)
|
||||||
addressable (>= 2.8.0)
|
addressable (>= 2.8.0)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff (>= 0.4.0, < 2.0.0)
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
@@ -557,7 +550,7 @@ 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.11)
|
zeitwerk (2.6.12)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
arm64-darwin-22
|
arm64-darwin-22
|
||||||
@@ -591,14 +584,13 @@ DEPENDENCIES
|
|||||||
lograge
|
lograge
|
||||||
mysql2
|
mysql2
|
||||||
oj
|
oj
|
||||||
omniauth-google-oauth2
|
|
||||||
omniauth-rails_csrf_protection
|
|
||||||
pagy
|
pagy
|
||||||
pdf-reader
|
pdf-reader
|
||||||
pg
|
pg
|
||||||
premailer-rails
|
premailer-rails
|
||||||
pry-rails
|
pry-rails
|
||||||
puma
|
puma
|
||||||
|
rack
|
||||||
rails
|
rails
|
||||||
rails-i18n
|
rails-i18n
|
||||||
rails_autolink
|
rails_autolink
|
||||||
@@ -626,4 +618,4 @@ RUBY VERSION
|
|||||||
ruby 3.2.2p53
|
ruby 3.2.2p53
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.4.10
|
2.5.3
|
||||||
@@ -34,10 +34,10 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
- [x] PDF form fields builder (WYSIWYG)
|
- [x] PDF form fields builder (WYSIWYG)
|
||||||
- [x] 10 field types available (Signature, Date, File, Checkbox etc.)
|
- [x] 11 field types available (Signature, Date, File, Checkbox etc.)
|
||||||
- [x] Multiple submitters per document
|
- [x] Multiple submitters per document
|
||||||
- [x] Automated emails via SMTP
|
- [x] Automated emails via SMTP
|
||||||
- [x] Files storage on AWS S3, Google Storage, or Azure
|
- [x] Files storage on disk or AWS S3, Google Storage, Azure Cloud
|
||||||
- [x] Automatic PDF eSignature
|
- [x] Automatic PDF eSignature
|
||||||
- [x] PDF signature verification
|
- [x] PDF signature verification
|
||||||
- [x] Users management
|
- [x] Users management
|
||||||
@@ -76,14 +76,14 @@ Run the app under a custom domain over https using docker compose (make sure you
|
|||||||
HOST=your-domain-name.com docker-compose up
|
HOST=your-domain-name.com docker-compose up
|
||||||
```
|
```
|
||||||
|
|
||||||
## For Companies
|
## For Businesses
|
||||||
### Integrate seamless document signing into your web or mobile apps with DocuSeal!
|
### Integrate seamless document signing into your web or mobile apps with DocuSeal!
|
||||||
|
|
||||||
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
|
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
|
||||||
|
|
||||||
[](https://cal.com/docuseal)
|
[Book a Meeting](https://calendly.com/kriti-docuseal/30min)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Distributed under the AGPLv3 License. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) for more information.
|
Distributed under the AGPLv3 License. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) for more information.
|
||||||
Unless otherwise noted, all files © 2023 Oleksandr Turchyn.
|
Unless otherwise noted, all files © 2023 DocuSeal LLC.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AccountConfigsController < ApplicationController
|
||||||
|
before_action :load_account_config
|
||||||
|
authorize_resource :account_config
|
||||||
|
|
||||||
|
ALLOWED_KEYS = [
|
||||||
|
AccountConfig::ALLOW_TYPED_SIGNATURE,
|
||||||
|
AccountConfig::FORCE_MFA,
|
||||||
|
AccountConfig::ESIGNING_PREFERENCE_KEY
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
def create
|
||||||
|
@account_config.update!(account_config_params)
|
||||||
|
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_account_config
|
||||||
|
return head :not_found unless ALLOWED_KEYS.include?(account_config_params[:key])
|
||||||
|
|
||||||
|
@account_config =
|
||||||
|
AccountConfig.find_or_initialize_by(account: current_account, key: account_config_params[:key])
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_config_params
|
||||||
|
params.required(:account_config).permit!.tap do |attrs|
|
||||||
|
attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -4,6 +4,7 @@ class AccountsController < ApplicationController
|
|||||||
LOCALE_OPTIONS = {
|
LOCALE_OPTIONS = {
|
||||||
'en-US' => 'English (United States)',
|
'en-US' => 'English (United States)',
|
||||||
'en-GB' => 'English (United Kingdom)',
|
'en-GB' => 'English (United Kingdom)',
|
||||||
|
'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)'
|
||||||
|
|||||||
@@ -19,20 +19,37 @@ module Api
|
|||||||
|
|
||||||
render json: { error: e.message }, status: :forbidden
|
render json: { error: e.message }, status: :forbidden
|
||||||
end
|
end
|
||||||
|
|
||||||
|
rescue_from JSON::ParserError do |e|
|
||||||
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
|
render json: { error: "JSON parse error: #{e.message}" }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def paginate(relation)
|
def paginate(relation)
|
||||||
result = relation.order(id: :desc)
|
result = relation.order(id: :desc)
|
||||||
.limit([params[:limit] || DEFAULT_LIMIT, MAX_LIMIT].min)
|
.limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min)
|
||||||
|
|
||||||
result = result.where('id < ?', params[:after]) if params[:after].present?
|
result = result.where(relation.arel_table[:id].lt(params[:after])) if params[:after].present?
|
||||||
result = result.where('id > ?', params[:before]) if params[:before].present?
|
result = result.where(relation.arel_table[:id].gt(params[:before])) if params[:before].present?
|
||||||
|
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
|
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).find_by(access_token: { sha256: })
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: { error: 'Not authenticated' }, status: :unauthorized unless current_user
|
||||||
|
end
|
||||||
|
|
||||||
def current_account
|
def current_account
|
||||||
current_user&.account
|
current_user&.account
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
module Api
|
module Api
|
||||||
class SubmissionsController < ApiBaseController
|
class SubmissionsController < ApiBaseController
|
||||||
load_and_authorize_resource :template, only: :create
|
load_and_authorize_resource :template, only: :create
|
||||||
load_and_authorize_resource :submission, only: %i[show index]
|
load_and_authorize_resource :submission, only: %i[show index destroy]
|
||||||
|
|
||||||
before_action only: :create do
|
before_action only: :create do
|
||||||
authorize!(:create, Submission)
|
authorize!(:create, Submission)
|
||||||
@@ -13,7 +13,12 @@ module Api
|
|||||||
submissions = Submissions.search(@submissions, params[:q])
|
submissions = Submissions.search(@submissions, params[:q])
|
||||||
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
|
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
|
||||||
|
|
||||||
submissions = paginate(submissions.preload(:created_by_user, :template, :submitters))
|
if params[:template_folder].present?
|
||||||
|
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] })
|
||||||
|
end
|
||||||
|
|
||||||
|
submissions = paginate(submissions.preload(:created_by_user, :template, :submitters,
|
||||||
|
audit_trail_attachment: :blob))
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
data: submissions.as_json(serialize_params),
|
data: submissions.as_json(serialize_params),
|
||||||
@@ -26,73 +31,95 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
serialized_subbmitters =
|
submitters = @submission.submitters.preload(documents_attachments: :blob, attachments_attachments: :blob)
|
||||||
@submission.submitters.preload(documents_attachments: :blob, attachments_attachments: :blob).map do |submitter|
|
|
||||||
Submissions::EnsureResultGenerated.call(submitter) if submitter.completed_at?
|
|
||||||
|
|
||||||
Submitters::SerializeForApi.call(submitter)
|
serialized_submitters = submitters.map do |submitter|
|
||||||
end
|
Submissions::EnsureResultGenerated.call(submitter) if submitter.completed_at?
|
||||||
|
|
||||||
|
Submitters::SerializeForApi.call(submitter)
|
||||||
|
end
|
||||||
|
|
||||||
json = @submission.as_json(
|
json = @submission.as_json(
|
||||||
serialize_params.deep_merge(
|
serialize_params.deep_merge(
|
||||||
include: {
|
include: { submission_events: { only: %i[id submitter_id event_type event_timestamp] } }
|
||||||
submission_events: {
|
|
||||||
only: %i[id submitter_id event_type event_timestamp]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
json[:submitters] = serialized_subbmitters
|
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:
|
render json:
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
is_send_email = !params[:send_email].in?(['false', false])
|
return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil?
|
||||||
|
|
||||||
submissions =
|
params[:send_email] = true unless params.key?(:send_email)
|
||||||
if (emails = (params[:emails] || params[:email]).presence) && params[:submission].blank?
|
params[:send_sms] = false unless params.key?(:send_sms)
|
||||||
Submissions.create_from_emails(template: @template,
|
|
||||||
user: current_user,
|
|
||||||
source: :api,
|
|
||||||
mark_as_sent: is_send_email,
|
|
||||||
emails:)
|
|
||||||
else
|
|
||||||
submissions_attrs, attachments = normalize_submissions_params!(submissions_params[:submission], @template)
|
|
||||||
|
|
||||||
Submissions.create_from_submitters(
|
submissions = create_submissions(@template, params)
|
||||||
template: @template,
|
|
||||||
user: current_user,
|
|
||||||
source: :api,
|
|
||||||
mark_as_sent: is_send_email,
|
|
||||||
submitters_order: params[:submitters_order] || 'preserved',
|
|
||||||
submissions_attrs:
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
Submissions.send_signature_requests(submissions, send_email: is_send_email)
|
Submissions.send_signature_requests(submissions)
|
||||||
|
|
||||||
submitters = submissions.flat_map(&:submitters)
|
render json: submissions.flat_map(&:submitters)
|
||||||
|
|
||||||
save_default_value_attachments!(attachments, submitters)
|
|
||||||
|
|
||||||
render json: submitters
|
|
||||||
rescue Submitters::NormalizeValues::UnknownFieldName, Submitters::NormalizeValues::UnknownSubmitterName => e
|
rescue Submitters::NormalizeValues::UnknownFieldName, Submitters::NormalizeValues::UnknownSubmitterName => e
|
||||||
render json: { error: e.message }, status: :unprocessable_entity
|
render json: { error: e.message }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@submission.update!(deleted_at: Time.current)
|
@submission.update!(archived_at: Time.current)
|
||||||
|
|
||||||
render json: @submission.as_json(only: %i[id deleted_at])
|
render json: @submission.as_json(only: %i[id], methods: %i[archived_at])
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def create_submissions(template, params)
|
||||||
|
is_send_email = !params[:send_email].in?(['false', false])
|
||||||
|
|
||||||
|
if (emails = (params[:emails] || params[:email]).presence) && params[:submission].blank?
|
||||||
|
Submissions.create_from_emails(template:,
|
||||||
|
user: current_user,
|
||||||
|
source: :api,
|
||||||
|
mark_as_sent: is_send_email,
|
||||||
|
emails:,
|
||||||
|
params:)
|
||||||
|
else
|
||||||
|
submissions_attrs, attachments =
|
||||||
|
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_params, template)
|
||||||
|
|
||||||
|
submissions = Submissions.create_from_submitters(
|
||||||
|
template:,
|
||||||
|
user: current_user,
|
||||||
|
source: :api,
|
||||||
|
mark_as_sent: is_send_email,
|
||||||
|
submitters_order: params[:submitters_order] || params[:order] || 'preserved',
|
||||||
|
submissions_attrs:,
|
||||||
|
params:
|
||||||
|
)
|
||||||
|
|
||||||
|
Submissions::NormalizeParamUtils.save_default_value_attachments!(attachments,
|
||||||
|
submissions.flat_map(&:submitters))
|
||||||
|
|
||||||
|
submissions
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def serialize_params
|
def serialize_params
|
||||||
{
|
{
|
||||||
only: %i[id source submitters_order created_at updated_at],
|
only: %i[id source submitters_order created_at updated_at archived_at],
|
||||||
|
methods: %i[audit_log_url],
|
||||||
include: {
|
include: {
|
||||||
submitters: { only: %i[id slug uuid name email phone
|
submitters: { only: %i[id slug uuid name email phone
|
||||||
completed_at opened_at sent_at
|
completed_at opened_at sent_at
|
||||||
@@ -105,48 +132,19 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def submissions_params
|
def submissions_params
|
||||||
params.permit(submission: [{
|
key = params.key?(:submission) ? :submission : :submissions
|
||||||
submitters: [[:uuid, :name, :email, :role, :completed, :phone, { values: {} }]]
|
|
||||||
}])
|
|
||||||
end
|
|
||||||
|
|
||||||
def normalize_submissions_params!(submissions_params, template)
|
params.permit(
|
||||||
attachments = []
|
key => [
|
||||||
|
[:send_email, :send_sms, {
|
||||||
Array.wrap(submissions_params).each do |submission|
|
message: %i[subject body],
|
||||||
submission[:submitters].each_with_index do |submitter, index|
|
submitters: [[:send_email, :send_sms, :uuid, :name, :email, :role,
|
||||||
next if submitter[:values].blank?
|
:completed, :phone, :application_key,
|
||||||
|
{ values: {}, readonly_fields: [], message: %i[subject body],
|
||||||
values, new_attachments =
|
fields: [%i[name default_value readonly validation_pattern invalid_message]] }]]
|
||||||
Submitters::NormalizeValues.call(template,
|
}]
|
||||||
submitter[:values],
|
]
|
||||||
submitter[:role] || template.submitters[index]['name'])
|
).fetch(key, [])
|
||||||
|
|
||||||
attachments.push(*new_attachments)
|
|
||||||
|
|
||||||
submitter[:values] = values
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
[submissions_params, attachments]
|
|
||||||
end
|
|
||||||
|
|
||||||
def save_default_value_attachments!(attachments, submitters)
|
|
||||||
return if attachments.blank?
|
|
||||||
|
|
||||||
attachments_index = attachments.index_by(&:uuid)
|
|
||||||
|
|
||||||
submitters.each do |submitter|
|
|
||||||
submitter.values.to_a.each do |_, value|
|
|
||||||
attachment = attachments_index[value]
|
|
||||||
|
|
||||||
next unless attachment
|
|
||||||
|
|
||||||
attachment.record = submitter
|
|
||||||
|
|
||||||
attachment.save!
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -4,10 +4,113 @@ module Api
|
|||||||
class SubmittersController < ApiBaseController
|
class SubmittersController < ApiBaseController
|
||||||
load_and_authorize_resource :submitter
|
load_and_authorize_resource :submitter
|
||||||
|
|
||||||
|
def index
|
||||||
|
submitters = Submitters.search(@submitters, params[:q])
|
||||||
|
|
||||||
|
submitters = submitters.where(application_key: params[:application_key]) if params[:application_key].present?
|
||||||
|
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
|
||||||
|
|
||||||
|
submitters = paginate(
|
||||||
|
submitters.preload(:template, :submission, :submission_events,
|
||||||
|
documents_attachments: :blob, attachments_attachments: :blob)
|
||||||
|
)
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
data: submitters.map { |s| Submitters::SerializeForApi.call(s, with_template: true, with_events: true) },
|
||||||
|
pagination: {
|
||||||
|
count: submitters.size,
|
||||||
|
next: submitters.last&.id,
|
||||||
|
prev: submitters.first&.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
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)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
if @submitter.completed_at?
|
||||||
|
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
|
role = @submitter.submission.template_submitters.find { |e| e['uuid'] == @submitter.uuid }['name']
|
||||||
|
|
||||||
|
normalized_params, new_attachments =
|
||||||
|
Submissions::NormalizeParamUtils.normalize_submitter_params!(submitter_params.merge(role:), @submitter.template,
|
||||||
|
for_submitter: @submitter)
|
||||||
|
|
||||||
|
Submissions::CreateFromSubmitters.maybe_set_template_fields(@submitter.submission,
|
||||||
|
[normalized_params],
|
||||||
|
submitter_uuid: @submitter.uuid)
|
||||||
|
|
||||||
|
assign_submitter_attrs(@submitter, normalized_params)
|
||||||
|
|
||||||
|
ApplicationRecord.transaction do
|
||||||
|
Submissions::NormalizeParamUtils.save_default_value_attachments!(new_attachments, [@submitter])
|
||||||
|
|
||||||
|
@submitter.save!
|
||||||
|
|
||||||
|
@submitter.submission.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
if @submitter.completed_at?
|
||||||
|
ProcessSubmitterCompletionJob.perform_later(@submitter)
|
||||||
|
elsif normalized_params[:send_email] || normalized_params[:send_sms]
|
||||||
|
Submitters.send_signature_requests([@submitter])
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: Submitters::SerializeForApi.call(@submitter, with_template: false, with_events: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
def submitter_params
|
||||||
|
submitter_params = params.key?(:submitter) ? params.require(:submitter) : params
|
||||||
|
|
||||||
|
submitter_params.permit(
|
||||||
|
:send_email, :send_sms, :uuid, :name, :email, :role, :completed, :phone, :application_key,
|
||||||
|
{ values: {}, readonly_fields: [], message: %i[subject body],
|
||||||
|
fields: [%i[name default_value readonly validation_pattern invalid_message]] }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def assign_submitter_attrs(submitter, attrs)
|
||||||
|
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
|
||||||
|
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.application_key = attrs[:application_key] if attrs.key?(:application_key)
|
||||||
|
|
||||||
|
assign_submission_fields(submitter.submission)
|
||||||
|
assign_preferences(submitter, attrs)
|
||||||
|
|
||||||
|
submitter
|
||||||
|
end
|
||||||
|
|
||||||
|
def assign_submission_fields(submission)
|
||||||
|
submission.template_fields ||= submission.template.fields
|
||||||
|
submission.template_schema ||= submission.template.schema
|
||||||
|
end
|
||||||
|
|
||||||
|
def assign_preferences(submitter, attrs)
|
||||||
|
submitter_preferences = Submitters.normalize_preferences(submitter.account, current_user, attrs)
|
||||||
|
|
||||||
|
submitter.preferences['default_values'] = attrs[:values].to_unsafe_h if attrs[:values].present?
|
||||||
|
|
||||||
|
if submitter_preferences.key?('send_email')
|
||||||
|
submitter.preferences['send_email'] = submitter_preferences['send_email']
|
||||||
|
end
|
||||||
|
|
||||||
|
submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms')
|
||||||
|
|
||||||
|
return unless submitter_preferences.key?('email_message_uuid')
|
||||||
|
|
||||||
|
submitter.preferences['email_message_uuid'] = submitter_preferences['email_message_uuid']
|
||||||
|
|
||||||
|
submitter
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -7,10 +7,10 @@ module Api
|
|||||||
LIMIT = 100
|
LIMIT = 100
|
||||||
|
|
||||||
def index
|
def index
|
||||||
template_folders = @template_folders.joins(:templates).where(templates: { deleted_at: nil }).distinct
|
template_folders = @template_folders.joins(:templates).where(templates: { archived_at: nil }).distinct
|
||||||
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
|
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
|
||||||
|
|
||||||
render json: template_folders.as_json(only: %i[name deleted_at])
|
render json: template_folders.as_json(only: %i[name archived_at])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api
|
||||||
|
class TemplatesCloneController < ApiBaseController
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize!(:manage, @template)
|
||||||
|
|
||||||
|
cloned_template = Templates::Clone.call(@template,
|
||||||
|
author: current_user,
|
||||||
|
name: params[:name],
|
||||||
|
application_key: params[:application_key],
|
||||||
|
folder_name: params[:folder_name])
|
||||||
|
|
||||||
|
cloned_template.source = :api
|
||||||
|
cloned_template.save!
|
||||||
|
|
||||||
|
Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
|
||||||
|
|
||||||
|
render json: cloned_template.as_json(serialize_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
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
|
||||||
@@ -8,6 +8,8 @@ module Api
|
|||||||
templates = Templates.search(@templates, params[:q])
|
templates = Templates.search(@templates, params[:q])
|
||||||
|
|
||||||
templates = params[:archived] ? templates.archived : templates.active
|
templates = params[:archived] ? templates.archived : templates.active
|
||||||
|
templates = templates.where(application_key: params[:application_key]) if params[:application_key].present?
|
||||||
|
templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present?
|
||||||
|
|
||||||
templates = paginate(templates.preload(:author, documents_attachments: :blob))
|
templates = paginate(templates.preload(:author, documents_attachments: :blob))
|
||||||
|
|
||||||
@@ -36,9 +38,9 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@template.update!(deleted_at: Time.current)
|
@template.update!(archived_at: Time.current)
|
||||||
|
|
||||||
render json: @template.as_json(only: %i[id deleted_at])
|
render json: @template.as_json(only: %i[id archived_at])
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -51,11 +53,15 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def template_params
|
def template_params
|
||||||
params.require(:template).permit(:name,
|
params.require(:template).permit(
|
||||||
schema: [%i[attachment_uuid name]],
|
:name,
|
||||||
submitters: [%i[name uuid]],
|
schema: [%i[attachment_uuid name]],
|
||||||
fields: [[:uuid, :submitter_uuid, :name, :type, :required,
|
submitters: [%i[name uuid]],
|
||||||
{ options: [], areas: [%i[x y w h cell_w attachment_uuid page]] }]])
|
fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value,
|
||||||
|
{ preferences: {},
|
||||||
|
options: [%i[value uuid]],
|
||||||
|
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -16,6 +16,7 @@ module Api
|
|||||||
render json: {
|
render json: {
|
||||||
schema:,
|
schema:,
|
||||||
documents: documents.as_json(
|
documents: documents.as_json(
|
||||||
|
methods: [:metadata],
|
||||||
include: {
|
include: {
|
||||||
preview_images: { methods: %i[url metadata filename] }
|
preview_images: { methods: %i[url metadata filename] }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,14 @@ class ApiSettingsController < ApplicationController
|
|||||||
def index
|
def index
|
||||||
authorize!(:read, current_user.access_token)
|
authorize!(:read, current_user.access_token)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize!(:manage, current_user.access_token)
|
||||||
|
|
||||||
|
current_user.access_token.token = SecureRandom.base58(AccessToken::TOKEN_LENGTH)
|
||||||
|
|
||||||
|
current_user.access_token.save!
|
||||||
|
|
||||||
|
redirect_back(fallback_location: settings_api_index_path, notice: 'API token as been updated.')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -33,7 +33,7 @@ class ApplicationController < ActionController::Base
|
|||||||
private
|
private
|
||||||
|
|
||||||
def sign_in_for_demo
|
def sign_in_for_demo
|
||||||
sign_in(User.order('random()').take) unless signed_in?
|
sign_in(User.active.order('random()').take) unless signed_in?
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_account
|
def current_account
|
||||||
@@ -44,8 +44,9 @@ class ApplicationController < ActionController::Base
|
|||||||
redirect_to setup_index_path unless User.exists?
|
redirect_to setup_index_path unless User.exists?
|
||||||
end
|
end
|
||||||
|
|
||||||
def button_title(title: 'Submit', disabled_with: 'Submitting', icon: nil, icon_disabled: nil)
|
def button_title(title: 'Submit', disabled_with: 'Submitting', title_class: '', icon: nil, icon_disabled: nil)
|
||||||
render_to_string(partial: 'shared/button_title', locals: { title:, disabled_with:, icon:, icon_disabled: })
|
render_to_string(partial: 'shared/button_title',
|
||||||
|
locals: { title:, disabled_with:, title_class:, icon:, icon_disabled: })
|
||||||
end
|
end
|
||||||
|
|
||||||
def svg_icon(icon_name, class: '')
|
def svg_icon(icon_name, class: '')
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ class DashboardController < ApplicationController
|
|||||||
|
|
||||||
before_action :maybe_redirect_product_url
|
before_action :maybe_redirect_product_url
|
||||||
before_action :maybe_render_landing
|
before_action :maybe_render_landing
|
||||||
|
before_action :maybe_redirect_mfa_setup
|
||||||
|
|
||||||
load_and_authorize_resource :template_folder, parent: false
|
load_and_authorize_resource :template_folder, parent: false
|
||||||
load_and_authorize_resource :template, parent: false
|
load_and_authorize_resource :template, parent: false
|
||||||
@@ -62,6 +63,17 @@ class DashboardController < ApplicationController
|
|||||||
redirect_to Docuseal::PRODUCT_URL, allow_other_host: true
|
redirect_to Docuseal::PRODUCT_URL, allow_other_host: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def maybe_redirect_mfa_setup
|
||||||
|
return unless signed_in?
|
||||||
|
return if current_user.otp_required_for_login
|
||||||
|
|
||||||
|
return if !current_user.otp_required_for_login && !AccountConfig.exists?(value: true,
|
||||||
|
account_id: current_user.account_id,
|
||||||
|
key: AccountConfig::FORCE_MFA)
|
||||||
|
|
||||||
|
redirect_to mfa_setup_path, notice: 'Setup 2FA to continue'
|
||||||
|
end
|
||||||
|
|
||||||
def maybe_render_landing
|
def maybe_render_landing
|
||||||
return if signed_in?
|
return if signed_in?
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class EnquiriesController < ApplicationController
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
def create
|
||||||
|
if params[:talk_to_sales] == 'on'
|
||||||
|
Faraday.post(Docuseal::ENQUIRIES_URL,
|
||||||
|
enquiry_params.merge(type: :talk_to_sales).to_json,
|
||||||
|
'Content-Type' => 'application/json')
|
||||||
|
end
|
||||||
|
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def enquiry_params
|
||||||
|
params.require(:user).permit(:email)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ErrorsController < ActionController::Base
|
||||||
|
ENTERPRISE_FEATURE_MESSAGE =
|
||||||
|
'This feature is available in Pro Edition: https://www.docuseal.co/pricing'
|
||||||
|
|
||||||
|
ENTERPRISE_PATHS = [
|
||||||
|
'/templates/html',
|
||||||
|
'/api/templates/html',
|
||||||
|
'/templates/pdf',
|
||||||
|
'/api/templates/pdf',
|
||||||
|
'/templates/doc',
|
||||||
|
'/api/templates/doc',
|
||||||
|
'/templates/docx',
|
||||||
|
'/api/templates/docx'
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
def show
|
||||||
|
if request.original_fullpath.in?(ENTERPRISE_PATHS) && error_status_code == 404
|
||||||
|
return render json: { status: 404, message: ENTERPRISE_FEATURE_MESSAGE }, status: :not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
respond_to do |f|
|
||||||
|
f.json do
|
||||||
|
render json: { status: error_status_code }, status: error_status_code
|
||||||
|
end
|
||||||
|
|
||||||
|
f.html { render error_status_code.to_s, status: error_status_code }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def error_status_code
|
||||||
|
@error_status_code ||=
|
||||||
|
ActionDispatch::ExceptionWrapper.new(request.env,
|
||||||
|
request.env['action_dispatch.exception']).status_code
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -5,13 +5,11 @@ class MfaSetupController < ApplicationController
|
|||||||
authorize!(:update, current_user)
|
authorize!(:update, current_user)
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
before_action :set_provision_url, only: %i[show new]
|
||||||
current_user.otp_secret ||= User.generate_otp_secret
|
|
||||||
|
|
||||||
current_user.save!
|
def show; end
|
||||||
|
|
||||||
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal::PRODUCT_NAME)
|
def new; end
|
||||||
end
|
|
||||||
|
|
||||||
def edit; end
|
def edit; end
|
||||||
|
|
||||||
@@ -22,11 +20,11 @@ class MfaSetupController < ApplicationController
|
|||||||
|
|
||||||
redirect_to settings_profile_index_path, notice: '2FA has been configured'
|
redirect_to settings_profile_index_path, notice: '2FA has been configured'
|
||||||
else
|
else
|
||||||
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal::PRODUCT_NAME)
|
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name)
|
||||||
|
|
||||||
@error_message = 'Code is invalid'
|
@error_message = 'Code is invalid'
|
||||||
|
|
||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/new'), status: :unprocessable_entity
|
render turbo_stream: turbo_stream.replace(:mfa_form, partial: 'mfa_setup/form'), status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -41,4 +39,16 @@ class MfaSetupController < ApplicationController
|
|||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_entity
|
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_provision_url
|
||||||
|
return redirect_to root_path, alert: '2FA has been set up already' if current_user.otp_required_for_login
|
||||||
|
|
||||||
|
current_user.otp_secret ||= User.generate_otp_secret
|
||||||
|
|
||||||
|
current_user.save!
|
||||||
|
|
||||||
|
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -12,7 +12,7 @@ class NotificationsSettingsController < ApplicationController
|
|||||||
def index; end
|
def index; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
if @account_config.save
|
if @account_config.value.present? ? @account_config.save : @account_config.delete
|
||||||
redirect_back fallback_location: settings_notifications_path, notice: 'Changes have been saved'
|
redirect_back fallback_location: settings_notifications_path, notice: 'Changes have been saved'
|
||||||
else
|
else
|
||||||
redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
|
redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|
||||||
def google_oauth2
|
|
||||||
@user = Users.from_omniauth(request.env['omniauth.auth'])
|
|
||||||
|
|
||||||
if @user.persisted?
|
|
||||||
flash[:notice] = I18n.t('devise.omniauth_callbacks.success', kind: 'Google')
|
|
||||||
|
|
||||||
sign_in_and_redirect @user, event: :authentication
|
|
||||||
else
|
|
||||||
redirect_to new_registration_path(oauth_callback: true, user: @user.slice(:email, :first_name, :last_name)),
|
|
||||||
notice: 'Please complete registration with Google auth'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class PasswordsController < Devise::PasswordsController
|
||||||
|
class Current < ActiveSupport::CurrentAttributes
|
||||||
|
attribute :user
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
super do |resource|
|
||||||
|
Current.user = resource
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class PreviewDocumentPageController < ActionController::API
|
||||||
|
include ActiveStorage::SetCurrent
|
||||||
|
|
||||||
|
FORMAT = Templates::ProcessDocument::FORMAT
|
||||||
|
|
||||||
|
def show
|
||||||
|
attachment = ActiveStorage::Attachment.find_by(uuid: params[:attachment_uuid])
|
||||||
|
|
||||||
|
return head :not_found unless attachment
|
||||||
|
|
||||||
|
preview_image = attachment.preview_images.joins(:blob).find_by(blob: { filename: "#{params[:id]}#{FORMAT}" })
|
||||||
|
|
||||||
|
return redirect_to preview_image.url, allow_other_host: true if preview_image
|
||||||
|
|
||||||
|
file_path =
|
||||||
|
if attachment.service.name == :disk
|
||||||
|
ActiveStorage::Blob.service.path_for(attachment.key)
|
||||||
|
else
|
||||||
|
find_or_create_document_tempfile_path(attachment)
|
||||||
|
end
|
||||||
|
|
||||||
|
io = Templates::ProcessDocument.generate_pdf_preview_from_file(attachment, file_path, params[:id].to_i)
|
||||||
|
|
||||||
|
render plain: io.tap(&:rewind).read, content_type: 'image/jpeg'
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_or_create_document_tempfile_path(attachment)
|
||||||
|
file_path = "#{Dir.tmpdir}/#{attachment.uuid}"
|
||||||
|
|
||||||
|
File.open(file_path, File::RDWR | File::CREAT, 0o644) do |f|
|
||||||
|
f.flock(File::LOCK_EX)
|
||||||
|
|
||||||
|
# rubocop:disable Style/ZeroLengthPredicate
|
||||||
|
if f.size.zero?
|
||||||
|
f.binmode
|
||||||
|
|
||||||
|
f.write(attachment.download)
|
||||||
|
end
|
||||||
|
# rubocop:enable Style/ZeroLengthPredicate
|
||||||
|
end
|
||||||
|
|
||||||
|
file_path
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -31,10 +31,10 @@ class RegistrationsController < Devise::RegistrationsController
|
|||||||
redirect_to after_sign_up_path_for(current_user), allow_other_host: true
|
redirect_to after_sign_up_path_for(current_user), allow_other_host: true
|
||||||
end
|
end
|
||||||
|
|
||||||
def require_no_authentication
|
def set_flash_message(key, kind, options = {})
|
||||||
super
|
return if key == :alert && kind == 'already_authenticated'
|
||||||
|
|
||||||
flash.clear
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_resource(_hash = {})
|
def build_resource(_hash = {})
|
||||||
@@ -43,7 +43,7 @@ class RegistrationsController < Devise::RegistrationsController
|
|||||||
|
|
||||||
self.resource = account.users.new(user_params)
|
self.resource = account.users.new(user_params)
|
||||||
|
|
||||||
account.name ||= "#{resource.full_name}'s Company" if params[:action] == 'create'
|
account.name ||= resource.full_name if params[:action] == 'create'
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_params
|
def user_params
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ class SessionsController < Devise::SessionsController
|
|||||||
devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt])
|
devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt])
|
||||||
end
|
end
|
||||||
|
|
||||||
def require_no_authentication
|
def set_flash_message(key, kind, options = {})
|
||||||
super
|
return if key == :alert && kind == 'already_authenticated'
|
||||||
|
|
||||||
flash.clear
|
super
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SsoSettingsController < ApplicationController
|
||||||
|
before_action :load_encrypted_config
|
||||||
|
authorize_resource :encrypted_config, only: :index
|
||||||
|
authorize_resource :encrypted_config, parent: false, except: :index
|
||||||
|
|
||||||
|
def index; end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_encrypted_config
|
||||||
|
@encrypted_config =
|
||||||
|
EncryptedConfig.find_or_initialize_by(account: current_account, key: 'saml_configs')
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -13,21 +13,21 @@ class StartFormController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@submitter = Submitter.where(submission: @template.submissions.where(deleted_at: nil))
|
@submitter = Submitter.where(submission: @template.submissions.where(archived_at: nil))
|
||||||
.find_or_initialize_by(email: submitter_params[:email])
|
.order(id: :desc)
|
||||||
|
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
|
||||||
|
.find_or_initialize_by(**submitter_params.compact_blank)
|
||||||
|
|
||||||
if @submitter.completed_at?
|
if @submitter.completed_at?
|
||||||
redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email])
|
redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email])
|
||||||
else
|
else
|
||||||
@submitter.assign_attributes(
|
if @template.submitters.to_a.size > 1 && @submitter.new_record?
|
||||||
uuid: @template.submitters.first['uuid'],
|
@error_message = 'Not found'
|
||||||
ip: request.remote_ip,
|
|
||||||
ua: request.user_agent
|
|
||||||
)
|
|
||||||
|
|
||||||
@submitter.submission ||= Submission.new(template: @template,
|
return render :show
|
||||||
template_submitters: @template.submitters,
|
end
|
||||||
source: :link)
|
|
||||||
|
assign_submission_attributes(@submitter, @template) if @submitter.new_record?
|
||||||
|
|
||||||
if @submitter.save
|
if @submitter.save
|
||||||
redirect_to submit_form_path(@submitter.slug)
|
redirect_to submit_form_path(@submitter.slug)
|
||||||
@@ -45,8 +45,35 @@ class StartFormController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def assign_submission_attributes(submitter, template)
|
||||||
|
resubmit_submitter =
|
||||||
|
if params[:resubmit].present?
|
||||||
|
Submitter.where(submission: @template.submissions).find_by(slug: params[:resubmit])
|
||||||
|
end
|
||||||
|
|
||||||
|
submitter.assign_attributes(
|
||||||
|
uuid: template.submitters.first['uuid'],
|
||||||
|
ip: request.remote_ip,
|
||||||
|
ua: request.user_agent,
|
||||||
|
values: resubmit_submitter&.preferences&.fetch('default_values', nil) || {},
|
||||||
|
preferences: resubmit_submitter&.preferences.presence || { 'send_email' => true }
|
||||||
|
)
|
||||||
|
|
||||||
|
if submitter.values.present?
|
||||||
|
resubmit_submitter.attachments.each do |attachment|
|
||||||
|
submitter.attachments << attachment.dup if submitter.values.value?(attachment.uuid)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
submitter.submission ||= Submission.new(template:,
|
||||||
|
template_submitters: template.submitters,
|
||||||
|
source: :link)
|
||||||
|
|
||||||
|
submitter
|
||||||
|
end
|
||||||
|
|
||||||
def submitter_params
|
def submitter_params
|
||||||
params.require(:submitter).permit(:email).tap do |attrs|
|
params.require(:submitter).permit(:email, :phone, :name).tap do |attrs|
|
||||||
attrs[:email] = Submissions.normalize_email(attrs[:email])
|
attrs[:email] = Submissions.normalize_email(attrs[:email])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,12 +6,24 @@ class SubmissionsController < ApplicationController
|
|||||||
|
|
||||||
load_and_authorize_resource :submission, only: %i[show destroy]
|
load_and_authorize_resource :submission, only: %i[show destroy]
|
||||||
|
|
||||||
|
PRELOAD_ALL_PAGES_AMOUNT = 200
|
||||||
|
|
||||||
def show
|
def show
|
||||||
ActiveRecord::Associations::Preloader.new(
|
ActiveRecord::Associations::Preloader.new(
|
||||||
records: [@submission],
|
records: [@submission],
|
||||||
associations: [:template, { template_schema_documents: [:blob, { preview_images_attachments: :blob }] }]
|
associations: [:template, { template_schema_documents: :blob }]
|
||||||
).call
|
).call
|
||||||
|
|
||||||
|
total_pages =
|
||||||
|
@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: @submission.template_schema_documents,
|
||||||
|
associations: [:blob, { preview_images_attachments: :blob }]
|
||||||
|
).call
|
||||||
|
end
|
||||||
|
|
||||||
render :show, layout: 'plain'
|
render :show, layout: 'plain'
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -22,29 +34,36 @@ class SubmissionsController < ApplicationController
|
|||||||
def create
|
def create
|
||||||
authorize!(:create, Submission)
|
authorize!(:create, Submission)
|
||||||
|
|
||||||
|
if params[:is_custom_message] != '1'
|
||||||
|
params.delete(:subject)
|
||||||
|
params.delete(:body)
|
||||||
|
end
|
||||||
|
|
||||||
submissions =
|
submissions =
|
||||||
if params[:emails].present?
|
if params[:emails].present?
|
||||||
Submissions.create_from_emails(template: @template,
|
Submissions.create_from_emails(template: @template,
|
||||||
user: current_user,
|
user: current_user,
|
||||||
source: :invite,
|
source: :invite,
|
||||||
mark_as_sent: params[:send_email] == '1',
|
mark_as_sent: params[:send_email] == '1',
|
||||||
emails: params[:emails])
|
emails: params[:emails],
|
||||||
|
params: params.merge('send_completed_email' => true))
|
||||||
else
|
else
|
||||||
Submissions.create_from_submitters(template: @template,
|
Submissions.create_from_submitters(template: @template,
|
||||||
user: current_user,
|
user: current_user,
|
||||||
source: :invite,
|
source: :invite,
|
||||||
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
|
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
|
||||||
mark_as_sent: params[:send_email] == '1',
|
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))
|
||||||
end
|
end
|
||||||
|
|
||||||
Submissions.send_signature_requests(submissions, params)
|
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!(deleted_at: Time.current)
|
@submission.update!(archived_at: Time.current)
|
||||||
|
|
||||||
redirect_back(fallback_location: template_path(@submission.template), notice: 'Submission has been archived')
|
redirect_back(fallback_location: template_path(@submission.template), notice: 'Submission has been archived')
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmissionsPreviewController < ApplicationController
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
PRELOAD_ALL_PAGES_AMOUNT = 200
|
||||||
|
|
||||||
|
def show
|
||||||
|
@submission = Submission.find_by!(slug: params[:slug])
|
||||||
|
|
||||||
|
ActiveRecord::Associations::Preloader.new(
|
||||||
|
records: [@submission],
|
||||||
|
associations: [:template, { template_schema_documents: :blob }]
|
||||||
|
).call
|
||||||
|
|
||||||
|
total_pages =
|
||||||
|
@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: @submission.template_schema_documents,
|
||||||
|
associations: [:blob, { preview_images_attachments: :blob }]
|
||||||
|
).call
|
||||||
|
end
|
||||||
|
|
||||||
|
render 'submissions/show', layout: 'plain'
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -6,23 +6,48 @@ class SubmitFormController < ApplicationController
|
|||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
|
PRELOAD_ALL_PAGES_AMOUNT = 200
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@submitter =
|
@submitter = Submitter.find_by!(slug: params[:slug])
|
||||||
Submitter.preload(submission: [
|
|
||||||
:template, { template_schema_documents: [:blob, { preview_images_attachments: :blob }] }
|
|
||||||
])
|
|
||||||
.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?
|
||||||
|
|
||||||
|
ActiveRecord::Associations::Preloader.new(
|
||||||
|
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
|
cookies[:submitter_sid] = @submitter.signed_id
|
||||||
|
|
||||||
|
render @submitter.submission.template.archived_at? ? :archived : :show
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
submitter = Submitter.find_by!(slug: params[:slug])
|
submitter = Submitter.find_by!(slug: params[:slug])
|
||||||
|
|
||||||
|
if submitter.completed_at?
|
||||||
|
return render json: { error: 'Form has been completed already.' }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
end
|
||||||
|
|
||||||
Submitters::SubmitValues.call(submitter, params, request)
|
Submitters::SubmitValues.call(submitter, params, request)
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
|
|||||||
@@ -4,6 +4,13 @@ class SubmittersSendEmailController < ApplicationController
|
|||||||
load_and_authorize_resource :submitter, id_param: :submitter_slug, find_by: :slug
|
load_and_authorize_resource :submitter, id_param: :submitter_slug, find_by: :slug
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
|
||||||
|
event_type: 'send_email',
|
||||||
|
created_at: 24.hours.ago..Time.current)
|
||||||
|
return redirect_back(fallback_location: submission_path(@submitter.submission),
|
||||||
|
alert: 'Email has been sent already.')
|
||||||
|
end
|
||||||
|
|
||||||
SubmitterMailer.invitation_email(@submitter).deliver_later!
|
SubmitterMailer.invitation_email(@submitter).deliver_later!
|
||||||
|
|
||||||
SubmissionEvent.create!(submitter: @submitter, event_type: 'send_email')
|
SubmissionEvent.create!(submitter: @submitter, event_type: 'send_email')
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ class TemplatesArchivedController < ApplicationController
|
|||||||
load_and_authorize_resource :template, parent: false
|
load_and_authorize_resource :template, parent: false
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@templates = @templates.where.not(deleted_at: nil).preload(:author, :folder).order(id: :desc)
|
@templates = @templates.where.not(archived_at: nil).preload(:author, :folder).order(id: :desc)
|
||||||
@templates = Templates.search(@templates, params[:q])
|
@templates = Templates.search(@templates, params[:q])
|
||||||
|
|
||||||
@pagy, @templates = pagy(@templates, items: 12)
|
@pagy, @templates = pagy(@templates, items: 12)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ class TemplatesArchivedSubmissionsController < ApplicationController
|
|||||||
load_and_authorize_resource :submission, through: :template, parent: false
|
load_and_authorize_resource :submission, through: :template, parent: false
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@submissions = @submissions.where.not(deleted_at: nil)
|
@submissions = @submissions.where.not(archived_at: nil)
|
||||||
@submissions = Submissions.search(@submissions, params[:q])
|
@submissions = Submissions.search(@submissions, params[:q])
|
||||||
|
|
||||||
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
|
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
|
||||||
|
|||||||
@@ -7,9 +7,14 @@ class TemplatesController < ApplicationController
|
|||||||
|
|
||||||
def show
|
def show
|
||||||
submissions = @template.submissions
|
submissions = @template.submissions
|
||||||
submissions = submissions.active if @template.deleted_at.blank?
|
submissions = submissions.active if @template.archived_at.blank?
|
||||||
submissions = Submissions.search(submissions, params[:q])
|
submissions = Submissions.search(submissions, params[:q])
|
||||||
|
|
||||||
|
@base_submissions = submissions
|
||||||
|
|
||||||
|
submissions = submissions.pending if params[:status] == 'pending'
|
||||||
|
submissions = submissions.completed if params[:status] == 'completed'
|
||||||
|
|
||||||
@pagy, @submissions = pagy(submissions.preload(:submitters).order(id: :desc))
|
@pagy, @submissions = pagy(submissions.preload(:submitters).order(id: :desc))
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
redirect_to root_path
|
redirect_to root_path
|
||||||
@@ -25,14 +30,27 @@ class TemplatesController < ApplicationController
|
|||||||
associations: [schema_documents: { preview_images_attachments: :blob }]
|
associations: [schema_documents: { preview_images_attachments: :blob }]
|
||||||
).call
|
).call
|
||||||
|
|
||||||
|
@template_data =
|
||||||
|
@template.as_json.merge(
|
||||||
|
documents: @template.schema_documents.as_json(
|
||||||
|
methods: [:metadata],
|
||||||
|
include: { preview_images: { methods: %i[url metadata filename] } }
|
||||||
|
)
|
||||||
|
).to_json
|
||||||
|
|
||||||
render :edit, layout: 'plain'
|
render :edit, layout: 'plain'
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@template.account = current_account
|
if @base_template
|
||||||
@template.author = current_user
|
@template = Templates::Clone.call(@base_template, author: current_user,
|
||||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
name: params.dig(:template, :name),
|
||||||
@template.assign_attributes(@base_template.slice(:fields, :schema, :submitters)) if @base_template
|
folder_name: params[:folder_name])
|
||||||
|
else
|
||||||
|
@template.account = current_account
|
||||||
|
@template.author = current_user
|
||||||
|
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
||||||
|
end
|
||||||
|
|
||||||
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
|
||||||
@@ -44,9 +62,20 @@ class TemplatesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@template.update!(deleted_at: Time.current)
|
notice =
|
||||||
|
if params[:permanently].present?
|
||||||
|
@template.destroy!
|
||||||
|
|
||||||
redirect_back(fallback_location: root_path, notice: 'Template has been archived.')
|
Rollbar.info("Remove template: #{@template.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
'Template has been removed.'
|
||||||
|
else
|
||||||
|
@template.update!(archived_at: Time.current)
|
||||||
|
|
||||||
|
'Template has been archived.'
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect_back(fallback_location: root_path, notice:)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ class TemplatesRestoreController < ApplicationController
|
|||||||
load_and_authorize_resource :template
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@template.update!(deleted_at: nil)
|
@template.update!(archived_at: nil)
|
||||||
|
|
||||||
redirect_to template_path(@template), notice: 'Template has been unarchived'
|
redirect_to template_path(@template), notice: 'Template has been unarchived'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,20 +3,55 @@
|
|||||||
class TemplatesUploadsController < ApplicationController
|
class TemplatesUploadsController < ApplicationController
|
||||||
load_and_authorize_resource :template, parent: false
|
load_and_authorize_resource :template, parent: false
|
||||||
|
|
||||||
|
layout 'plain'
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
url_params = create_file_params_from_url if params[:url].present?
|
||||||
|
|
||||||
@template.account = current_account
|
@template.account = current_account
|
||||||
@template.author = current_user
|
@template.author = current_user
|
||||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
||||||
@template.name = File.basename(params[:files].first.original_filename, '.*')
|
@template.name = File.basename((url_params || params)[:files].first.original_filename, '.*')
|
||||||
|
|
||||||
@template.save!
|
@template.save!
|
||||||
|
|
||||||
documents = Templates::CreateAttachments.call(@template, params)
|
documents = Templates::CreateAttachments.call(@template, url_params || params)
|
||||||
|
|
||||||
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
|
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
|
||||||
|
|
||||||
@template.update!(schema:)
|
@template.update!(schema:)
|
||||||
|
|
||||||
redirect_to edit_template_path(@template)
|
redirect_to edit_template_path(@template)
|
||||||
|
rescue StandardError => e
|
||||||
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
|
redirect_to root_path, alert: 'Unable to upload file'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def create_file_params_from_url
|
||||||
|
tempfile = Tempfile.new
|
||||||
|
tempfile.binmode
|
||||||
|
tempfile.write(conn.get(Addressable::URI.parse(params[:url]).display_uri.to_s).body)
|
||||||
|
tempfile.rewind
|
||||||
|
|
||||||
|
file = ActionDispatch::Http::UploadedFile.new(
|
||||||
|
tempfile:,
|
||||||
|
filename: File.basename(
|
||||||
|
URI.decode_www_form_component(params[:filename].presence || params[:url])
|
||||||
|
),
|
||||||
|
type: Marcel::MimeType.for(tempfile)
|
||||||
|
)
|
||||||
|
|
||||||
|
{ files: [file] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def conn
|
||||||
|
Faraday.new do |faraday|
|
||||||
|
faraday.response :follow_redirects
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TimestampServerController < ApplicationController
|
||||||
|
before_action :build_encrypted_config
|
||||||
|
authorize_resource :encrypted_config
|
||||||
|
|
||||||
|
def create
|
||||||
|
return head :not_found if Docuseal.multitenant?
|
||||||
|
|
||||||
|
test_timeserver_url(@encrypted_config.value) if @encrypted_config.value.present?
|
||||||
|
|
||||||
|
if @encrypted_config.value.present? ? @encrypted_config.save : @encrypted_config.delete
|
||||||
|
redirect_back fallback_location: settings_notifications_path, notice: 'Changes have been saved'
|
||||||
|
else
|
||||||
|
redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
|
||||||
|
end
|
||||||
|
rescue HexaPDF::Error, SocketError, Submissions::TimestampHandler::TimestampError, OpenSSL::Timestamp::TimestampError
|
||||||
|
redirect_back fallback_location: settings_notifications_path, alert: 'Invalid Timeserver'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def test_timeserver_url(url)
|
||||||
|
pdf = HexaPDF::Document.new
|
||||||
|
pdf.pages.add
|
||||||
|
|
||||||
|
pkcs = Accounts.load_signing_pkcs(current_account)
|
||||||
|
|
||||||
|
pdf.sign(StringIO.new,
|
||||||
|
reason: 'Test',
|
||||||
|
certificate: pkcs.certificate,
|
||||||
|
key: pkcs.key,
|
||||||
|
signature_size: 10_000,
|
||||||
|
certificate_chain: pkcs.ca_certs || [],
|
||||||
|
timestamp_handler: Submissions::TimestampHandler.new(tsa_url: url))
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_encrypted_config
|
||||||
|
@encrypted_config
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_encrypted_config
|
||||||
|
@encrypted_config =
|
||||||
|
EncryptedConfig.find_or_initialize_by(account: current_account,
|
||||||
|
key: EncryptedConfig::TIMESTAMP_SERVER_URL_KEY)
|
||||||
|
|
||||||
|
@encrypted_config.assign_attributes(encrypted_config_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def encrypted_config_params
|
||||||
|
params.require(:encrypted_config).permit(:value)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -28,6 +28,12 @@ class UserSignaturesController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@user_config.destroy
|
||||||
|
|
||||||
|
redirect_to settings_profile_index_path, notice: 'Signature has been removed'
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def load_user_config
|
def load_user_config
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ 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?
|
||||||
|
|
||||||
if @user.update(user_params.compact_blank.except(current_user == @user ? :role : nil))
|
attrs = user_params.compact_blank
|
||||||
|
attrs.delete(:role) if !role_valid?(attrs[:role]) || current_user == @user
|
||||||
|
|
||||||
|
if @user.update(attrs)
|
||||||
redirect_to settings_users_path, notice: 'User has been updated'
|
redirect_to 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
|
||||||
@@ -39,17 +42,21 @@ class UsersController < ApplicationController
|
|||||||
return redirect_to settings_users_path, notice: 'Unable to remove user'
|
return redirect_to settings_users_path, notice: 'Unable to remove user'
|
||||||
end
|
end
|
||||||
|
|
||||||
@user.update!(deleted_at: Time.current)
|
@user.update!(archived_at: Time.current)
|
||||||
|
|
||||||
redirect_to settings_users_path, notice: 'User has been removed'
|
redirect_to settings_users_path, notice: 'User has been removed'
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def role_valid?(role)
|
||||||
|
User::ROLES.include?(role)
|
||||||
|
end
|
||||||
|
|
||||||
def build_user
|
def build_user
|
||||||
@user = current_account.users.find_by(email: user_params[:email])&.tap do |user|
|
@user = current_account.users.find_by(email: user_params[:email])&.tap do |user|
|
||||||
user.assign_attributes(user_params)
|
user.assign_attributes(user_params)
|
||||||
user.deleted_at = nil
|
user.archived_at = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
@user ||= current_account.users.new(user_params)
|
@user ||= current_account.users.new(user_params)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class VerifyPdfSignatureController < ApplicationController
|
|||||||
cert_data = if Docuseal.multitenant?
|
cert_data = if Docuseal.multitenant?
|
||||||
Docuseal::CERTS
|
Docuseal::CERTS
|
||||||
else
|
else
|
||||||
EncryptedConfig.find_by(account: current_account, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
|
EncryptedConfig.find_by(key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
|
||||||
end
|
end
|
||||||
|
|
||||||
default_pkcs = GenerateCertificate.load_pkcs(cert_data)
|
default_pkcs = GenerateCertificate.load_pkcs(cert_data)
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ class WebhookSettingsController < ApplicationController
|
|||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@encrypted_config.update!(encrypted_config_params)
|
@encrypted_config.assign_attributes(encrypted_config_params)
|
||||||
|
|
||||||
|
@encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete
|
||||||
|
|
||||||
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook URL has been saved.')
|
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook URL has been saved.')
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import AutoresizeTextarea from './elements/autoresize_textarea'
|
|||||||
import SubmittersAutocomplete from './elements/submitter_autocomplete'
|
import SubmittersAutocomplete from './elements/submitter_autocomplete'
|
||||||
import FolderAutocomplete from './elements/folder_autocomplete'
|
import FolderAutocomplete from './elements/folder_autocomplete'
|
||||||
import SignatureForm from './elements/signature_form'
|
import SignatureForm from './elements/signature_form'
|
||||||
|
import SubmitForm from './elements/submit_form'
|
||||||
|
|
||||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||||
|
|
||||||
@@ -49,6 +50,7 @@ window.customElements.define('autoresize-textarea', AutoresizeTextarea)
|
|||||||
window.customElements.define('submitters-autocomplete', SubmittersAutocomplete)
|
window.customElements.define('submitters-autocomplete', SubmittersAutocomplete)
|
||||||
window.customElements.define('folder-autocomplete', FolderAutocomplete)
|
window.customElements.define('folder-autocomplete', FolderAutocomplete)
|
||||||
window.customElements.define('signature-form', SignatureForm)
|
window.customElements.define('signature-form', SignatureForm)
|
||||||
|
window.customElements.define('submit-form', SubmitForm)
|
||||||
|
|
||||||
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) => {
|
||||||
@@ -76,12 +78,15 @@ document.addEventListener('turbo:submit-end', async (event) => {
|
|||||||
window.customElements.define('template-builder', class extends HTMLElement {
|
window.customElements.define('template-builder', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.appElem = document.createElement('div')
|
this.appElem = document.createElement('div')
|
||||||
this.appElem.classList.add('max-h-screen')
|
|
||||||
|
this.appElem.classList.add('md:h-screen')
|
||||||
|
|
||||||
this.app = createApp(TemplateBuilder, {
|
this.app = createApp(TemplateBuilder, {
|
||||||
template: reactive(JSON.parse(this.dataset.template)),
|
template: reactive(JSON.parse(this.dataset.template)),
|
||||||
backgroundColor: '#faf7f5',
|
backgroundColor: '#faf7f5',
|
||||||
withPhone: this.dataset.withPhone === 'true',
|
withPhone: this.dataset.withPhone === 'true',
|
||||||
|
withLogo: this.dataset.withLogo !== 'false',
|
||||||
|
withPayment: this.dataset.withPayment === 'true',
|
||||||
acceptFileTypes: this.dataset.acceptFileTypes,
|
acceptFileTypes: this.dataset.acceptFileTypes,
|
||||||
isDirectUpload: this.dataset.isDirectUpload === 'true'
|
isDirectUpload: this.dataset.isDirectUpload === 'true'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -75,16 +75,16 @@ button[disabled] .enabled {
|
|||||||
@apply select base-input w-full font-normal;
|
@apply select base-input w-full font-normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-bottom-end:before {
|
.tooltip-bottom-start:before {
|
||||||
transform: translateX(-95%);
|
transform: translateX(-30%);
|
||||||
top: var(--tooltip-offset);
|
top: var(--tooltip-offset);
|
||||||
left: 100%;
|
left: 100%;
|
||||||
right: auto;
|
right: auto;
|
||||||
bottom: auto;
|
bottom: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-bottom-end:after {
|
.tooltip-bottom-start:after {
|
||||||
transform: translateX(-25%);
|
transform: translateX(-75%);
|
||||||
border-color: transparent transparent var(--tooltip-color) transparent;
|
border-color: transparent transparent var(--tooltip-color) transparent;
|
||||||
top: var(--tooltip-tail-offset);
|
top: var(--tooltip-tail-offset);
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
|||||||
@@ -18,27 +18,64 @@ 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((response) => response.json()).then((urls) => {
|
||||||
const fileRequests = urls.map((url) => {
|
const isSafariIos = /iPhone|iPad|iPod/i.test(navigator.userAgent)
|
||||||
return () => {
|
|
||||||
return fetch(url).then(async (resp) => {
|
|
||||||
const blobUrl = URL.createObjectURL(await resp.blob())
|
|
||||||
const link = document.createElement('a')
|
|
||||||
|
|
||||||
link.href = blobUrl
|
if (isSafariIos && urls.length > 1) {
|
||||||
link.setAttribute('download', decodeURI(url.split('/').pop()))
|
this.downloadSafariIos(urls)
|
||||||
|
} else {
|
||||||
|
this.downloadUrls(urls)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
link.click()
|
downloadUrls (urls) {
|
||||||
|
const fileRequests = urls.map((url) => {
|
||||||
|
return () => {
|
||||||
|
return fetch(url).then(async (resp) => {
|
||||||
|
const blobUrl = URL.createObjectURL(await resp.blob())
|
||||||
|
const link = document.createElement('a')
|
||||||
|
|
||||||
URL.revokeObjectURL(url)
|
link.href = blobUrl
|
||||||
})
|
link.setAttribute('download', decodeURI(url.split('/').pop()))
|
||||||
}
|
|
||||||
|
link.click()
|
||||||
|
|
||||||
|
URL.revokeObjectURL(blobUrl)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
fileRequests.reduce(
|
||||||
|
(prevPromise, request) => prevPromise.then(() => request()),
|
||||||
|
Promise.resolve()
|
||||||
|
)
|
||||||
|
|
||||||
|
this.toggleState()
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadSafariIos (urls) {
|
||||||
|
const fileRequests = urls.map((url) => {
|
||||||
|
return fetch(url).then(async (resp) => {
|
||||||
|
const blob = await resp.blob()
|
||||||
|
const blobUrl = URL.createObjectURL(blob.slice(0, blob.size, 'application/octet-stream'))
|
||||||
|
const link = document.createElement('a')
|
||||||
|
|
||||||
|
link.href = blobUrl
|
||||||
|
link.setAttribute('download', decodeURI(url.split('/').pop()))
|
||||||
|
|
||||||
|
return link
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
fileRequests.reduce(
|
Promise.all(fileRequests).then((links) => {
|
||||||
(prevPromise, request) => prevPromise.then(() => request()),
|
links.forEach((link, index) => {
|
||||||
Promise.resolve()
|
setTimeout(() => {
|
||||||
)
|
link.click()
|
||||||
|
|
||||||
|
URL.revokeObjectURL(link.href)
|
||||||
|
}, index * 50)
|
||||||
|
})
|
||||||
|
}).finally(() => {
|
||||||
this.toggleState()
|
this.toggleState()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ export default class extends HTMLElement {
|
|||||||
render = (item) => {
|
render = (item) => {
|
||||||
const div = document.createElement('div')
|
const div = document.createElement('div')
|
||||||
|
|
||||||
|
div.setAttribute('dir', 'auto')
|
||||||
|
|
||||||
div.textContent = item.name
|
div.textContent = item.name
|
||||||
|
|
||||||
return div
|
return div
|
||||||
|
|||||||
@@ -1,7 +1,21 @@
|
|||||||
export default class extends HTMLElement {
|
export default class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
if (this.dataset.inputId) {
|
if (this.dataset.inputId) {
|
||||||
document.getElementById(this.dataset.inputId).value = Intl.DateTimeFormat().resolvedOptions().timeZone
|
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||||
|
|
||||||
|
if (this.dataset.params === 'true') {
|
||||||
|
const params = new URLSearchParams(this.input.value)
|
||||||
|
|
||||||
|
params.set('timezone', timezone)
|
||||||
|
|
||||||
|
this.input.value = params.toString()
|
||||||
|
} else {
|
||||||
|
this.input.value = timezone
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get input () {
|
||||||
|
return document.getElementById(this.dataset.inputId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.querySelector('form').requestSubmit()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,6 +56,8 @@ export default class extends HTMLElement {
|
|||||||
render = (item) => {
|
render = (item) => {
|
||||||
const div = document.createElement('div')
|
const div = document.createElement('div')
|
||||||
|
|
||||||
|
div.setAttribute('dir', 'auto')
|
||||||
|
|
||||||
div.textContent = item[this.dataset.field]
|
div.textContent = item[this.dataset.field]
|
||||||
|
|
||||||
return div
|
return div
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ window.customElements.define('submission-form', class extends HTMLElement {
|
|||||||
|
|
||||||
this.app = createApp(Form, {
|
this.app = createApp(Form, {
|
||||||
submitter: JSON.parse(this.dataset.submitter),
|
submitter: JSON.parse(this.dataset.submitter),
|
||||||
authenticityToken: this.dataset.authenticityToken,
|
|
||||||
canSendEmail: this.dataset.canSendEmail === 'true',
|
canSendEmail: this.dataset.canSendEmail === 'true',
|
||||||
isDirectUpload: this.dataset.isDirectUpload === 'true',
|
isDirectUpload: this.dataset.isDirectUpload === 'true',
|
||||||
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',
|
||||||
withConfetti: true,
|
withConfetti: true,
|
||||||
|
withTypedSignature: this.dataset.withTypedSignature !== 'false',
|
||||||
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),
|
||||||
attachments: reactive(JSON.parse(this.dataset.attachments)),
|
attachments: reactive(JSON.parse(this.dataset.attachments)),
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ select:required:invalid {
|
|||||||
@apply border-base-content/20;
|
@apply border-base-content/20;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.base-textarea {
|
||||||
|
@apply textarea textarea-bordered bg-white rounded-3xl;
|
||||||
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
@apply no-animation;
|
@apply no-animation;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="uniqueAreas.length > 1"
|
||||||
|
class="mb-2"
|
||||||
|
>
|
||||||
|
<div class="flex space-x-1 text-sm ml-1 -mt-3">
|
||||||
|
<span>
|
||||||
|
{{ t('appears_on') }}:
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
v-for="area in uniqueAreas"
|
||||||
|
:key="area.uuid"
|
||||||
|
href="#"
|
||||||
|
class="link pr-0.5"
|
||||||
|
@click.prevent="scrollIntoArea(area)"
|
||||||
|
>
|
||||||
|
{{ t('page') }} <template v-if="isMultipleDocs">{{ attachmentUuids.indexOf(area.attachment_uuid) + 1 }}-</template>{{ area.page + 1 }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'AppearsOn',
|
||||||
|
inject: ['t', 'scrollIntoArea'],
|
||||||
|
props: {
|
||||||
|
field: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isMultipleDocs () {
|
||||||
|
return this.attachmentUuids.length > 1
|
||||||
|
},
|
||||||
|
attachmentUuids () {
|
||||||
|
return [...new Set(this.uniqueAreas.map((e) => e.attachment_uuid))]
|
||||||
|
},
|
||||||
|
uniqueAreas () {
|
||||||
|
const areas = {}
|
||||||
|
|
||||||
|
this.field.areas.forEach((area) => {
|
||||||
|
areas[area.attachment_uuid + area.page] ||= area
|
||||||
|
})
|
||||||
|
|
||||||
|
return Object.values(areas).slice(0, 6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex absolute text-[1.5vw] lg:text-base"
|
class="flex absolute lg:text-base"
|
||||||
|
dir="auto"
|
||||||
:style="computedStyle"
|
:style="computedStyle"
|
||||||
:class="{ 'cursor-default': !submittable, 'bg-red-100 border cursor-pointer ': submittable, 'border-red-100': !isActive && submittable, 'bg-opacity-70': !isActive && !isValueSet && submittable, 'border-red-500 border-dashed z-10': isActive && submittable, 'bg-opacity-30': (isActive || isValueSet) && submittable }"
|
:class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'bg-red-100 border cursor-pointer ': submittable, 'border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'border-red-500 border-dashed border-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="!isActive && !isValueSet && field.type !== 'checkbox' && submittable"
|
v-if="!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
|
||||||
@@ -21,15 +22,20 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="isActive && withLabel"
|
v-if="isActive && withLabel && (!area.option_uuid || !option.value)"
|
||||||
class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap"
|
class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap pointer-events-none"
|
||||||
>
|
>
|
||||||
{{ field.name || fieldNames[field.type] }}
|
<template v-if="area.option_uuid && !option.value">
|
||||||
<template v-if="field.type === 'checkbox' && !field.name">
|
{{ optionValue(option) }}
|
||||||
{{ fieldIndex + 1 }}
|
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="!field.required && field.type !== 'checkbox'">
|
<template v-else>
|
||||||
(optional)
|
{{ field.name || fieldNames[field.type] }}
|
||||||
|
<template v-if="field.type === 'checkbox' && !field.name">
|
||||||
|
{{ fieldIndex + 1 }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="!field.required && field.type !== 'checkbox'">
|
||||||
|
(optional)
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -42,6 +48,11 @@
|
|||||||
class="object-contain mx-auto"
|
class="object-contain mx-auto"
|
||||||
:src="image.url"
|
:src="image.url"
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
v-else-if="field.type === 'stamp' && stamp"
|
||||||
|
class="object-contain mx-auto"
|
||||||
|
:src="stamp.url"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
v-else-if="field.type === 'signature' && signature"
|
v-else-if="field.type === 'signature' && signature"
|
||||||
class="object-contain mx-auto"
|
class="object-contain mx-auto"
|
||||||
@@ -53,7 +64,7 @@
|
|||||||
:src="initials.url"
|
:src="initials.url"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-else-if="field.type === 'file'"
|
v-else-if="field.type === 'file' || field.type === 'payment'"
|
||||||
class="px-0.5 flex flex-col justify-center"
|
class="px-0.5 flex flex-col justify-center"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@@ -87,6 +98,44 @@
|
|||||||
: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 }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="field.type === 'radio' && area.option_uuid"
|
||||||
|
class="w-full p-[0.2vw] flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-if="submittable"
|
||||||
|
type="radio"
|
||||||
|
:value="false"
|
||||||
|
class="aspect-square base-radio"
|
||||||
|
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
|
:checked="!!modelValue && modelValue === optionValue(option)"
|
||||||
|
@click="$emit('update:model-value', optionValue(option))"
|
||||||
|
>
|
||||||
|
<IconCheck
|
||||||
|
v-else-if="!!modelValue && modelValue === optionValue(option)"
|
||||||
|
class="aspect-square"
|
||||||
|
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="field.type === 'multiple' && area.option_uuid"
|
||||||
|
class="w-full p-[0.2vw] flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-if="submittable"
|
||||||
|
type="checkbox"
|
||||||
|
:value="false"
|
||||||
|
class="aspect-square base-checkbox"
|
||||||
|
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
|
:checked="!!modelValue && modelValue.includes(optionValue(option))"
|
||||||
|
@change="updateMultipleSelectValue(optionValue(option))"
|
||||||
|
>
|
||||||
|
<IconCheck
|
||||||
|
v-else-if="!!modelValue && modelValue.includes(optionValue(option))"
|
||||||
|
class="aspect-square"
|
||||||
|
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="field.type === 'cells'"
|
v-else-if="field.type === 'cells'"
|
||||||
class="w-full flex items-center"
|
class="w-full flex items-center"
|
||||||
@@ -102,7 +151,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="flex items-center px-0.5"
|
ref="textContainer"
|
||||||
|
dir="auto"
|
||||||
|
class="flex items-center px-0.5 w-full"
|
||||||
>
|
>
|
||||||
<span v-if="Array.isArray(modelValue)">
|
<span v-if="Array.isArray(modelValue)">
|
||||||
{{ modelValue.join(', ') }}
|
{{ modelValue.join(', ') }}
|
||||||
@@ -110,15 +161,16 @@
|
|||||||
<span v-else-if="field.type === 'date'">
|
<span v-else-if="field.type === 'date'">
|
||||||
{{ formattedDate }}
|
{{ formattedDate }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span
|
||||||
{{ modelValue }}
|
v-else
|
||||||
</span>
|
class="whitespace-pre-wrap"
|
||||||
|
>{{ modelValue }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper } from '@tabler/icons-vue'
|
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FieldArea',
|
name: 'FieldArea',
|
||||||
@@ -126,6 +178,7 @@ export default {
|
|||||||
IconPaperclip,
|
IconPaperclip,
|
||||||
IconCheck
|
IconCheck
|
||||||
},
|
},
|
||||||
|
inject: ['t'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -172,6 +225,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['update:model-value'],
|
emits: ['update:model-value'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
textOverflowChars: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
fieldNames () {
|
fieldNames () {
|
||||||
return {
|
return {
|
||||||
@@ -180,15 +238,20 @@ export default {
|
|||||||
signature: 'Signature',
|
signature: 'Signature',
|
||||||
date: 'Date',
|
date: 'Date',
|
||||||
image: 'Image',
|
image: 'Image',
|
||||||
|
stamp: 'Stamp',
|
||||||
initials: 'Initials',
|
initials: 'Initials',
|
||||||
file: 'File',
|
file: 'File',
|
||||||
select: 'Select',
|
select: 'Select',
|
||||||
checkbox: 'Checkbox',
|
checkbox: 'Checkbox',
|
||||||
radio: 'Radio',
|
radio: 'Radio',
|
||||||
multiple: 'Multiple Select',
|
multiple: 'Multiple Select',
|
||||||
phone: 'Phone'
|
phone: 'Phone',
|
||||||
|
payment: 'Payment'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
option () {
|
||||||
|
return this.field.options.find((o) => o.uuid === this.area.option_uuid)
|
||||||
|
},
|
||||||
fieldIcons () {
|
fieldIcons () {
|
||||||
return {
|
return {
|
||||||
text: IconTextSize,
|
text: IconTextSize,
|
||||||
@@ -200,9 +263,11 @@ export default {
|
|||||||
select: IconSelect,
|
select: IconSelect,
|
||||||
checkbox: IconCheckbox,
|
checkbox: IconCheckbox,
|
||||||
radio: IconCircleDot,
|
radio: IconCircleDot,
|
||||||
|
stamp: IconRubberStamp,
|
||||||
cells: IconColumns3,
|
cells: IconColumns3,
|
||||||
multiple: IconChecks,
|
multiple: IconChecks,
|
||||||
phone: IconPhoneCheck
|
phone: IconPhoneCheck,
|
||||||
|
payment: IconCreditCard
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
image () {
|
image () {
|
||||||
@@ -212,6 +277,13 @@ export default {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
stamp () {
|
||||||
|
if (this.field.type === 'stamp') {
|
||||||
|
return this.attachmentsIndex[this.modelValue]
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
signature () {
|
signature () {
|
||||||
if (this.field.type === 'signature') {
|
if (this.field.type === 'signature') {
|
||||||
return this.attachmentsIndex[this.modelValue]
|
return this.attachmentsIndex[this.modelValue]
|
||||||
@@ -226,9 +298,15 @@ export default {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
locale () {
|
||||||
|
return Intl.DateTimeFormat().resolvedOptions()?.locale
|
||||||
|
},
|
||||||
formattedDate () {
|
formattedDate () {
|
||||||
if (this.field.type === 'date' && this.modelValue) {
|
if (this.field.type === 'date' && this.modelValue) {
|
||||||
return new Intl.DateTimeFormat([], { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' }).format(new Date(this.modelValue))
|
return this.formatDate(
|
||||||
|
new Date(this.modelValue),
|
||||||
|
this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@@ -236,6 +314,8 @@ export default {
|
|||||||
attachments () {
|
attachments () {
|
||||||
if (this.field.type === 'file') {
|
if (this.field.type === 'file') {
|
||||||
return (this.modelValue || []).map((uuid) => this.attachmentsIndex[uuid])
|
return (this.modelValue || []).map((uuid) => this.attachmentsIndex[uuid])
|
||||||
|
} else if (this.field.type === 'payment') {
|
||||||
|
return [this.attachmentsIndex[this.modelValue]].filter(Boolean)
|
||||||
} else {
|
} else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@@ -250,6 +330,78 @@ export default {
|
|||||||
height: h * 100 + '%'
|
height: h * 100 + '%'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
modelValue () {
|
||||||
|
if (this.field.type === 'text' && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > this.modelValue.length)) {
|
||||||
|
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? this.modelValue.length : 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
if (this.field.type === 'text' && this.$refs.textContainer) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? this.modelValue.length : 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
optionValue (option) {
|
||||||
|
if (option) {
|
||||||
|
if (option.value) {
|
||||||
|
return option.value
|
||||||
|
} else {
|
||||||
|
const index = this.field.options.indexOf(option)
|
||||||
|
|
||||||
|
return `${this.t('option')} ${index + 1}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formatDate (date, format) {
|
||||||
|
const monthFormats = {
|
||||||
|
M: 'numeric',
|
||||||
|
MM: '2-digit',
|
||||||
|
MMM: 'short',
|
||||||
|
MMMM: 'long'
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayFormats = {
|
||||||
|
D: 'numeric',
|
||||||
|
DD: '2-digit'
|
||||||
|
}
|
||||||
|
|
||||||
|
const yearFormats = {
|
||||||
|
YYYY: 'numeric',
|
||||||
|
YY: '2-digit'
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = new Intl.DateTimeFormat([], {
|
||||||
|
day: dayFormats[format.match(/D+/)],
|
||||||
|
month: monthFormats[format.match(/M+/)],
|
||||||
|
year: yearFormats[format.match(/Y+/)],
|
||||||
|
timeZone: 'UTC'
|
||||||
|
}).formatToParts(date)
|
||||||
|
|
||||||
|
return format
|
||||||
|
.replace(/D+/, parts.find((p) => p.type === 'day').value)
|
||||||
|
.replace(/M+/, parts.find((p) => p.type === 'month').value)
|
||||||
|
.replace(/Y+/, parts.find((p) => p.type === 'year').value)
|
||||||
|
},
|
||||||
|
updateMultipleSelectValue (value) {
|
||||||
|
if (this.modelValue?.includes(value)) {
|
||||||
|
const newValue = [...this.modelValue]
|
||||||
|
|
||||||
|
newValue.splice(newValue.indexOf(value), 1)
|
||||||
|
|
||||||
|
this.$emit('update:model-value', newValue)
|
||||||
|
} else {
|
||||||
|
const newValue = this.modelValue ? [...this.modelValue] : []
|
||||||
|
|
||||||
|
newValue.push(value)
|
||||||
|
|
||||||
|
this.$emit('update:model-value', newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
:key="areaIndex"
|
:key="areaIndex"
|
||||||
>
|
>
|
||||||
<Teleport
|
<Teleport
|
||||||
|
v-if="findPageElementForArea(area)"
|
||||||
:to="findPageElementForArea(area)"
|
:to="findPageElementForArea(area)"
|
||||||
>
|
>
|
||||||
<FieldArea
|
<FieldArea
|
||||||
@@ -82,22 +83,25 @@ export default {
|
|||||||
return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${area.attachment_uuid}-${area.page}`)
|
return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${area.attachment_uuid}-${area.page}`)
|
||||||
},
|
},
|
||||||
scrollIntoField (field) {
|
scrollIntoField (field) {
|
||||||
this.areaRefs.find((area) => {
|
if (field?.areas) {
|
||||||
if (area.field === field) {
|
this.scrollIntoArea(field.areas[0])
|
||||||
const root = this.$root.$el.parentNode.getRootNode()
|
}
|
||||||
const container = root.body || root.querySelector('div')
|
},
|
||||||
|
scrollIntoArea (area) {
|
||||||
|
const areaRef = this.areaRefs.find((a) => a.area === area)
|
||||||
|
|
||||||
if (container.style.overflow === 'hidden') {
|
if (areaRef) {
|
||||||
this.scrollInContainer(area.$el)
|
const root = this.$root.$el.parentNode.getRootNode()
|
||||||
} else {
|
const container = root.body || root.querySelector('div')
|
||||||
area.$refs.scrollToElem.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
if (container.style.overflow === 'hidden') {
|
||||||
|
this.scrollInContainer(areaRef.$el)
|
||||||
} else {
|
} else {
|
||||||
return null
|
areaRef.$refs.scrollToElem.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
scrollInContainer (target) {
|
scrollInContainer (target) {
|
||||||
const root = this.$root.$el.parentNode.getRootNode()
|
const root = this.$root.$el.parentNode.getRootNode()
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div v-if="modelValue.length">
|
<div v-if="value.length">
|
||||||
<div
|
<div
|
||||||
v-for="(val, index) in modelValue"
|
v-for="(val, index) in value"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="flex mb-2"
|
class="flex mb-2"
|
||||||
>
|
>
|
||||||
@@ -26,10 +26,7 @@
|
|||||||
{{ attachmentsIndex[val].filename }}
|
{{ attachmentsIndex[val].filename }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button @click.prevent="removeAttachment(val)">
|
||||||
v-if="modelValue"
|
|
||||||
@click.prevent="removeAttachment(val)"
|
|
||||||
>
|
|
||||||
<IconTrashX
|
<IconTrashX
|
||||||
:width="18"
|
:width="18"
|
||||||
:heigh="19"
|
:heigh="19"
|
||||||
@@ -91,18 +88,28 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['attached', 'update:model-value'],
|
emits: ['attached', 'update:model-value'],
|
||||||
|
computed: {
|
||||||
|
value: {
|
||||||
|
set (value) {
|
||||||
|
this.$emit('update:model-value', this.modelValue || [])
|
||||||
|
},
|
||||||
|
get () {
|
||||||
|
return this.modelValue || []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
removeAttachment (uuid) {
|
removeAttachment (uuid) {
|
||||||
this.modelValue.splice(this.modelValue.indexOf(uuid), 1)
|
this.value.splice(this.value.indexOf(uuid), 1)
|
||||||
|
|
||||||
this.$emit('update:model-value', this.modelValue)
|
this.$emit('update:model-value', this.value)
|
||||||
},
|
},
|
||||||
onUpload (attachments) {
|
onUpload (attachments) {
|
||||||
attachments.forEach((attachment) => {
|
attachments.forEach((attachment) => {
|
||||||
this.$emit('attached', attachment)
|
this.$emit('attached', attachment)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.$emit('update:model-value', [...this.modelValue, ...attachments.map(a => a.uuid)])
|
this.$emit('update:model-value', [...this.value, ...attachments.map(a => a.uuid)])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,17 @@
|
|||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<div class="space-y-3 mt-5">
|
<div class="space-y-3 mt-5">
|
||||||
|
<a
|
||||||
|
v-if="completedButton.url"
|
||||||
|
:href="completedButton.url"
|
||||||
|
class="white-button flex items-center w-full"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{{ completedButton.title || 'Back to Website' }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
<button
|
<button
|
||||||
v-if="canSendEmail && !isDemo"
|
v-if="canSendEmail && !isDemo && withSendCopyButton"
|
||||||
class="white-button !h-auto flex items-center space-x-1 w-full"
|
class="white-button !h-auto flex items-center space-x-1 w-full"
|
||||||
:disabled="isSendingCopy"
|
:disabled="isSendingCopy"
|
||||||
@click.prevent="sendCopyToEmail"
|
@click.prevent="sendCopyToEmail"
|
||||||
@@ -27,7 +36,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="!isWebView"
|
v-if="!isWebView && withDownloadButton"
|
||||||
class="base-button flex items-center space-x-1 w-full"
|
class="base-button flex items-center space-x-1 w-full"
|
||||||
:disabled="isDownloading"
|
:disabled="isDownloading"
|
||||||
@click.prevent="download"
|
@click.prevent="download"
|
||||||
@@ -52,15 +61,6 @@
|
|||||||
Star on Github
|
Star on Github
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<a
|
|
||||||
v-if="completedButton.url"
|
|
||||||
:href="completedButton.url"
|
|
||||||
class="white-button flex items-center space-x-1 w-full"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
{{ completedButton.title || 'Back to Website' }}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
<a
|
<a
|
||||||
v-if="isDemo"
|
v-if="isDemo"
|
||||||
href="https://docuseal.co/sign_up"
|
href="https://docuseal.co/sign_up"
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
>
|
>
|
||||||
{{ t('signed_with') }}
|
{{ t('signed_with') }}
|
||||||
<a
|
<a
|
||||||
href="https://www.docuseal.co"
|
href="https://www.docuseal.co/start"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="underline"
|
class="underline"
|
||||||
>DocuSeal</a> - {{ t('open_source_documents_software') }}
|
>DocuSeal</a> - {{ t('open_source_documents_software') }}
|
||||||
@@ -115,6 +115,16 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
withDownloadButton: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
withSendCopyButton: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
withConfetti: {
|
withConfetti: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -169,27 +179,62 @@ export default {
|
|||||||
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((response) => response.json()).then((urls) => {
|
||||||
const fileRequests = urls.map((url) => {
|
const isSafariIos = /iPhone|iPad|iPod/i.test(navigator.userAgent)
|
||||||
return () => {
|
|
||||||
return fetch(url).then(async (resp) => {
|
|
||||||
const blobUrl = URL.createObjectURL(await resp.blob())
|
|
||||||
const link = document.createElement('a')
|
|
||||||
|
|
||||||
link.href = blobUrl
|
if (isSafariIos && urls.length > 1) {
|
||||||
link.setAttribute('download', decodeURI(url.split('/').pop()))
|
this.downloadSafariIos(urls)
|
||||||
|
} else {
|
||||||
|
this.downloadUrls(urls)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
downloadUrls (urls) {
|
||||||
|
const fileRequests = urls.map((url) => {
|
||||||
|
return () => {
|
||||||
|
return fetch(url).then(async (resp) => {
|
||||||
|
const blobUrl = URL.createObjectURL(await resp.blob())
|
||||||
|
const link = document.createElement('a')
|
||||||
|
|
||||||
link.click()
|
link.href = blobUrl
|
||||||
|
link.setAttribute('download', decodeURI(url.split('/').pop()))
|
||||||
|
|
||||||
URL.revokeObjectURL(url)
|
link.click()
|
||||||
})
|
|
||||||
}
|
URL.revokeObjectURL(blobUrl)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
fileRequests.reduce(
|
||||||
|
(prevPromise, request) => prevPromise.then(() => request()),
|
||||||
|
Promise.resolve()
|
||||||
|
)
|
||||||
|
|
||||||
|
this.isDownloading = false
|
||||||
|
},
|
||||||
|
downloadSafariIos (urls) {
|
||||||
|
const fileRequests = urls.map((url) => {
|
||||||
|
return fetch(url).then(async (resp) => {
|
||||||
|
const blob = await resp.blob()
|
||||||
|
const blobUrl = URL.createObjectURL(blob.slice(0, blob.size, 'application/octet-stream'))
|
||||||
|
const link = document.createElement('a')
|
||||||
|
|
||||||
|
link.href = blobUrl
|
||||||
|
link.setAttribute('download', decodeURI(url.split('/').pop()))
|
||||||
|
|
||||||
|
return link
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
fileRequests.reduce(
|
Promise.all(fileRequests).then((links) => {
|
||||||
(prevPromise, request) => prevPromise.then(() => request()),
|
links.forEach((link, index) => {
|
||||||
Promise.resolve()
|
setTimeout(() => {
|
||||||
)
|
link.click()
|
||||||
|
|
||||||
|
URL.revokeObjectURL(link.href)
|
||||||
|
}, index * 50)
|
||||||
|
})
|
||||||
|
}).finally(() => {
|
||||||
this.isDownloading = false
|
this.isDownloading = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<template>
|
||||||
|
<div dir="auto">
|
||||||
|
<div class="flex justify-between items-center w-full mb-2">
|
||||||
|
<label
|
||||||
|
:for="field.uuid"
|
||||||
|
class="label text-2xl"
|
||||||
|
>{{ field.name || t('date') }}
|
||||||
|
<template v-if="!field.required">({{ t('optional') }})</template>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline btn-sm !normal-case font-normal"
|
||||||
|
@click.prevent="setCurrentDate"
|
||||||
|
>
|
||||||
|
<IconCalendarCheck :width="16" />
|
||||||
|
{{ t('set_today') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<AppearsOn :field="field" />
|
||||||
|
<div class="text-center">
|
||||||
|
<input
|
||||||
|
ref="input"
|
||||||
|
v-model="value"
|
||||||
|
class="base-input !text-2xl text-center w-full"
|
||||||
|
:required="field.required"
|
||||||
|
type="date"
|
||||||
|
:name="`values[${field.uuid}]`"
|
||||||
|
@focus="$emit('focus')"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconCalendarCheck } from '@tabler/icons-vue'
|
||||||
|
import AppearsOn from './appears_on'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DateStep',
|
||||||
|
components: {
|
||||||
|
IconCalendarCheck,
|
||||||
|
AppearsOn
|
||||||
|
},
|
||||||
|
inject: ['t'],
|
||||||
|
props: {
|
||||||
|
field: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['update:model-value', 'focus'],
|
||||||
|
computed: {
|
||||||
|
value: {
|
||||||
|
set (value) {
|
||||||
|
this.$emit('update:model-value', value)
|
||||||
|
},
|
||||||
|
get () {
|
||||||
|
return this.modelValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setCurrentDate () {
|
||||||
|
const inputEl = this.$refs.input
|
||||||
|
|
||||||
|
inputEl.valueAsDate = new Date(new Date().getTime() - new Date().getTimezoneOffset() * 60000)
|
||||||
|
|
||||||
|
inputEl.dispatchEvent(new Event('input', { bubbles: true }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="!isFormVisible"
|
v-if="!isFormVisible"
|
||||||
|
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 rounded-none border-x-0 md:border md:rounded-full bottom-0 w-full md:mb-4 text-base"
|
||||||
@click.prevent="isFormVisible = true"
|
@click.prevent="isFormVisible = true"
|
||||||
>
|
>
|
||||||
@@ -23,11 +24,13 @@
|
|||||||
<div
|
<div
|
||||||
v-show="isFormVisible"
|
v-show="isFormVisible"
|
||||||
id="form_container"
|
id="form_container"
|
||||||
class="shadow-md bg-base-100 absolute bottom-0 md:bottom-4 w-full border-base-200 border p-4 rounded"
|
class="shadow-md bg-base-100 absolute bottom-0 w-full border-base-200 border p-4 rounded"
|
||||||
|
:class="{ 'md:bottom-4': isBreakpointMd }"
|
||||||
:style="{ backgroundColor: backgroundColor }"
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-if="!isCompleted"
|
v-if="!isCompleted"
|
||||||
|
id="minimize_form_button"
|
||||||
class="absolute right-0 mr-2 mt-2 top-0 hidden md:block"
|
class="absolute right-0 mr-2 mt-2 top-0 hidden md:block"
|
||||||
title="Minimize"
|
title="Minimize"
|
||||||
@click.prevent="isFormVisible = false"
|
@click.prevent="isFormVisible = false"
|
||||||
@@ -43,7 +46,7 @@
|
|||||||
ref="form"
|
ref="form"
|
||||||
:action="submitPath"
|
:action="submitPath"
|
||||||
method="post"
|
method="post"
|
||||||
class="md:mx-16"
|
:class="{ 'md:mx-16': isBreakpointMd }"
|
||||||
@submit.prevent="submitStep"
|
@submit.prevent="submitStep"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -58,58 +61,25 @@
|
|||||||
>
|
>
|
||||||
<div class="md:mt-4">
|
<div class="md:mt-4">
|
||||||
<div v-if="['cells', 'text'].includes(currentField.type)">
|
<div v-if="['cells', 'text'].includes(currentField.type)">
|
||||||
<label
|
<TextStep
|
||||||
v-if="currentField.name"
|
:key="currentField.uuid"
|
||||||
:for="currentField.uuid"
|
v-model="values[currentField.uuid]"
|
||||||
class="label text-2xl mb-2"
|
:field="currentField"
|
||||||
>{{ currentField.name }}
|
@focus="scrollIntoField(currentField)"
|
||||||
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="py-1"
|
|
||||||
/>
|
/>
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
:id="currentField.uuid"
|
|
||||||
v-model="values[currentField.uuid]"
|
|
||||||
class="base-input !text-2xl w-full"
|
|
||||||
:required="currentField.required"
|
|
||||||
:placeholder="`${t('type_here')}...${currentField.required ? '' : ` (${t('optional')})`}`"
|
|
||||||
type="text"
|
|
||||||
:name="`values[${currentField.uuid}]`"
|
|
||||||
@focus="$refs.areas.scrollIntoField(currentField)"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="currentField.type === 'date'">
|
|
||||||
<label
|
|
||||||
v-if="currentField.name"
|
|
||||||
:for="currentField.uuid"
|
|
||||||
class="label text-2xl mb-2"
|
|
||||||
>{{ currentField.name }}
|
|
||||||
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="py-1"
|
|
||||||
/>
|
|
||||||
<div class="text-center">
|
|
||||||
<input
|
|
||||||
:id="currentField.uuid"
|
|
||||||
v-model="values[currentField.uuid]"
|
|
||||||
class="base-input !text-2xl text-center w-full"
|
|
||||||
:required="currentField.required"
|
|
||||||
type="date"
|
|
||||||
:name="`values[${currentField.uuid}]`"
|
|
||||||
@focus="$refs.areas.scrollIntoField(currentField)"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<DateStep
|
||||||
|
v-else-if="currentField.type === 'date'"
|
||||||
|
:key="currentField.uuid"
|
||||||
|
v-model="values[currentField.uuid]"
|
||||||
|
:field="currentField"
|
||||||
|
@focus="scrollIntoField(currentField)"
|
||||||
|
/>
|
||||||
<div v-else-if="currentField.type === 'select'">
|
<div v-else-if="currentField.type === 'select'">
|
||||||
<label
|
<label
|
||||||
v-if="currentField.name"
|
v-if="currentField.name"
|
||||||
:for="currentField.uuid"
|
:for="currentField.uuid"
|
||||||
|
dir="auto"
|
||||||
class="label text-2xl mb-2"
|
class="label text-2xl mb-2"
|
||||||
>{{ currentField.name }}
|
>{{ currentField.name }}
|
||||||
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
||||||
@@ -118,13 +88,15 @@
|
|||||||
v-else
|
v-else
|
||||||
class="py-1"
|
class="py-1"
|
||||||
/>
|
/>
|
||||||
|
<AppearsOn :field="currentField" />
|
||||||
<select
|
<select
|
||||||
:id="currentField.uuid"
|
:id="currentField.uuid"
|
||||||
|
dir="auto"
|
||||||
:required="currentField.required"
|
:required="currentField.required"
|
||||||
class="select base-input !text-2xl w-full text-center font-normal"
|
class="select base-input !text-2xl w-full text-center font-normal"
|
||||||
:name="`values[${currentField.uuid}]`"
|
:name="`values[${currentField.uuid}]`"
|
||||||
@change="values[currentField.uuid] = $event.target.value"
|
@change="values[currentField.uuid] = $event.target.value"
|
||||||
@focus="$refs.areas.scrollIntoField(currentField)"
|
@focus="scrollIntoField(currentField)"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
value=""
|
value=""
|
||||||
@@ -133,12 +105,12 @@
|
|||||||
{{ t('select_your_option') }}
|
{{ t('select_your_option') }}
|
||||||
</option>
|
</option>
|
||||||
<option
|
<option
|
||||||
v-for="(option, index) in currentField.options"
|
v-for="option in currentField.options"
|
||||||
:key="index"
|
:key="option.uuid"
|
||||||
:selected="values[currentField.uuid] == option"
|
:selected="values[currentField.uuid] == option.value"
|
||||||
:value="option"
|
:value="option.value"
|
||||||
>
|
>
|
||||||
{{ option }}
|
{{ option.value }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -146,6 +118,7 @@
|
|||||||
<label
|
<label
|
||||||
v-if="currentField.name"
|
v-if="currentField.name"
|
||||||
:for="currentField.uuid"
|
:for="currentField.uuid"
|
||||||
|
dir="auto"
|
||||||
class="label text-2xl mb-2"
|
class="label text-2xl mb-2"
|
||||||
>{{ currentField.name }}
|
>{{ currentField.name }}
|
||||||
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
||||||
@@ -154,23 +127,23 @@
|
|||||||
<div class="space-y-3.5 mx-auto">
|
<div class="space-y-3.5 mx-auto">
|
||||||
<div
|
<div
|
||||||
v-for="(option, index) in currentField.options"
|
v-for="(option, index) in currentField.options"
|
||||||
:key="index"
|
:key="option.uuid"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
:for="currentField.uuid + option"
|
:for="option.uuid"
|
||||||
class="flex items-center space-x-3"
|
class="flex items-center space-x-3"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
:id="currentField.uuid + option"
|
:id="option.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
type="radio"
|
type="radio"
|
||||||
class="base-radio !h-7 !w-7"
|
class="base-radio !h-7 !w-7"
|
||||||
:name="`values[${currentField.uuid}]`"
|
:name="`values[${currentField.uuid}]`"
|
||||||
:value="option"
|
:value="option.value || `${t('option')} ${index + 1}`"
|
||||||
:required="currentField.required"
|
:required="currentField.required"
|
||||||
>
|
>
|
||||||
<span class="text-xl">
|
<span class="text-xl">
|
||||||
{{ option }}
|
{{ option.value || `${t('option')} ${index + 1}` }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -179,6 +152,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<MultiSelectStep
|
<MultiSelectStep
|
||||||
v-else-if="currentField.type === 'multiple'"
|
v-else-if="currentField.type === 'multiple'"
|
||||||
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
/>
|
/>
|
||||||
@@ -224,8 +198,11 @@
|
|||||||
:id="field.uuid"
|
:id="field.uuid"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="base-checkbox !h-7 !w-7"
|
class="base-checkbox !h-7 !w-7"
|
||||||
|
:oninvalid="`this.setCustomValidity('${t('please_check_the_box_to_continue')}')`"
|
||||||
|
:onchange="`this.setCustomValidity(validity.valueMissing ? '${t('please_check_the_box_to_continue')}' : '');`"
|
||||||
|
:required="field.required"
|
||||||
:checked="!!values[field.uuid]"
|
:checked="!!values[field.uuid]"
|
||||||
@click="[$refs.areas.scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
|
@click="[scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
|
||||||
>
|
>
|
||||||
<span class="text-xl">
|
<span class="text-xl">
|
||||||
{{ field.name || field.type + ' ' + (index + 1) }}
|
{{ field.name || field.type + ' ' + (index + 1) }}
|
||||||
@@ -236,61 +213,85 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ImageStep
|
<ImageStep
|
||||||
v-else-if="currentField.type === 'image'"
|
v-else-if="currentField.type === 'image' || currentField.type === 'stamp'"
|
||||||
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
@attached="[attachments.push($event), $refs.areas.scrollIntoField(currentField)]"
|
@attached="[attachments.push($event), scrollIntoField(currentField)]"
|
||||||
/>
|
/>
|
||||||
<SignatureStep
|
<SignatureStep
|
||||||
v-else-if="currentField.type === 'signature'"
|
v-else-if="currentField.type === 'signature'"
|
||||||
ref="currentStep"
|
ref="currentStep"
|
||||||
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
|
:previous-value="previousSignatureValue"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
|
:with-typed-signature="withTypedSignature"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
@attached="attachments.push($event)"
|
@attached="attachments.push($event)"
|
||||||
@start="$refs.areas.scrollIntoField(currentField)"
|
@start="scrollIntoField(currentField)"
|
||||||
@minimize="isFormVisible = false"
|
@minimize="isFormVisible = false"
|
||||||
/>
|
/>
|
||||||
<InitialsStep
|
<InitialsStep
|
||||||
v-else-if="currentField.type === 'initials'"
|
v-else-if="currentField.type === 'initials'"
|
||||||
ref="currentStep"
|
ref="currentStep"
|
||||||
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
|
:previous-value="previousInitialsValue"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
@attached="attachments.push($event)"
|
@attached="attachments.push($event)"
|
||||||
@start="$refs.areas.scrollIntoField(currentField)"
|
@start="scrollIntoField(currentField)"
|
||||||
@focus="$refs.areas.scrollIntoField(currentField)"
|
@focus="scrollIntoField(currentField)"
|
||||||
@minimize="isFormVisible = false"
|
@minimize="isFormVisible = false"
|
||||||
/>
|
/>
|
||||||
<AttachmentStep
|
<AttachmentStep
|
||||||
v-else-if="currentField.type === 'file'"
|
v-else-if="currentField.type === 'file'"
|
||||||
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
@attached="[attachments.push($event), $refs.areas.scrollIntoField(currentField)]"
|
@attached="[attachments.push($event), scrollIntoField(currentField)]"
|
||||||
/>
|
/>
|
||||||
<PhoneStep
|
<PhoneStep
|
||||||
v-else-if="currentField.type === 'phone'"
|
v-else-if="currentField.type === 'phone'"
|
||||||
ref="currentStep"
|
ref="currentStep"
|
||||||
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
:default-value="submitter.phone"
|
:default-value="submitter.phone"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
@focus="$refs.areas.scrollIntoField(currentField)"
|
@focus="scrollIntoField(currentField)"
|
||||||
|
@submit="submitStep"
|
||||||
|
/>
|
||||||
|
<PaymentStep
|
||||||
|
v-else-if="currentField.type === 'payment'"
|
||||||
|
ref="currentStep"
|
||||||
|
:key="currentField.uuid"
|
||||||
|
v-model="values[currentField.uuid]"
|
||||||
|
:field="currentField"
|
||||||
|
:submitter-slug="submitterSlug"
|
||||||
|
@attached="attachments.push($event)"
|
||||||
|
@focus="scrollIntoField(currentField)"
|
||||||
@submit="submitStep"
|
@submit="submitStep"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-6 md:mt-8">
|
<div
|
||||||
|
v-if="currentField.type !== 'payment' || submittedValues[currentField.uuid]"
|
||||||
|
class="mt-6 md:mt-8"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
|
id="submit_form_button"
|
||||||
|
ref="submitButton"
|
||||||
type="submit"
|
type="submit"
|
||||||
class="base-button w-full flex justify-center"
|
class="base-button w-full flex justify-center"
|
||||||
:disabled="isButtonDisabled"
|
:disabled="isButtonDisabled"
|
||||||
@@ -311,6 +312,12 @@
|
|||||||
><span>...</span></span>
|
><span>...</span></span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="showFillAllRequiredFields"
|
||||||
|
class="text-center mt-1"
|
||||||
|
>
|
||||||
|
{{ t('please_fill_all_required_fields') }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<FormCompleted
|
<FormCompleted
|
||||||
@@ -318,22 +325,31 @@
|
|||||||
:is-demo="isDemo"
|
:is-demo="isDemo"
|
||||||
:attribution="attribution"
|
:attribution="attribution"
|
||||||
:completed-button="completedButton"
|
:completed-button="completedButton"
|
||||||
|
:with-send-copy-button="withSendCopyButton"
|
||||||
|
:with-download-button="withDownloadButton"
|
||||||
:with-confetti="withConfetti"
|
:with-confetti="withConfetti"
|
||||||
:can-send-email="canSendEmail && !!submitter.email"
|
:can-send-email="canSendEmail && !!submitter.email"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
/>
|
/>
|
||||||
<div class="flex justify-center">
|
<div
|
||||||
<div class="flex items-center mt-5 mb-1">
|
v-if="stepFields.length < 80"
|
||||||
|
class="flex justify-center"
|
||||||
|
>
|
||||||
|
<div class="flex items-center mt-4 mb-1 flex-wrap">
|
||||||
<a
|
<a
|
||||||
v-for="(step, index) in stepFields"
|
v-for="(step, index) in stepFields"
|
||||||
:key="step[0].uuid"
|
:key="step[0].uuid"
|
||||||
href="#"
|
href="#"
|
||||||
class="inline border border-base-300 h-3 w-3 rounded-full mx-1"
|
class="inline border border-base-300 h-3 w-3 rounded-full mx-1 mt-1"
|
||||||
:class="{ 'bg-base-300': index === currentStep, 'bg-base-content': index < currentStep || isCompleted, 'bg-white': index > currentStep }"
|
:class="{ 'bg-base-300': index === currentStep, 'bg-base-content': (index < currentStep && stepFields[index].every((f) => !f.required || ![null, undefined, ''].includes(values[f.uuid]))) || isCompleted, 'bg-white': index > currentStep }"
|
||||||
@click.prevent="isCompleted ? '' : [saveStep(), goToStep(step, true)]"
|
@click.prevent="isCompleted ? '' : [saveStep(), goToStep(step, true)]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="mt-5"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -346,8 +362,12 @@ import InitialsStep from './initials_step'
|
|||||||
import AttachmentStep from './attachment_step'
|
import AttachmentStep from './attachment_step'
|
||||||
import MultiSelectStep from './multi_select_step'
|
import MultiSelectStep from './multi_select_step'
|
||||||
import PhoneStep from './phone_step'
|
import PhoneStep from './phone_step'
|
||||||
|
import PaymentStep from './payment_step'
|
||||||
|
import TextStep from './text_step'
|
||||||
|
import DateStep from './date_step'
|
||||||
import FormCompleted from './completed'
|
import FormCompleted from './completed'
|
||||||
import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||||
|
import AppearsOn from './appears_on'
|
||||||
import { t } from './i18n'
|
import { t } from './i18n'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -356,18 +376,23 @@ export default {
|
|||||||
FieldAreas,
|
FieldAreas,
|
||||||
ImageStep,
|
ImageStep,
|
||||||
SignatureStep,
|
SignatureStep,
|
||||||
|
AppearsOn,
|
||||||
AttachmentStep,
|
AttachmentStep,
|
||||||
InitialsStep,
|
InitialsStep,
|
||||||
MultiSelectStep,
|
MultiSelectStep,
|
||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
|
DateStep,
|
||||||
IconArrowsDiagonal,
|
IconArrowsDiagonal,
|
||||||
|
TextStep,
|
||||||
PhoneStep,
|
PhoneStep,
|
||||||
|
PaymentStep,
|
||||||
IconArrowsDiagonalMinimize2,
|
IconArrowsDiagonalMinimize2,
|
||||||
FormCompleted
|
FormCompleted
|
||||||
},
|
},
|
||||||
provide () {
|
provide () {
|
||||||
return {
|
return {
|
||||||
baseUrl: this.baseUrl,
|
baseUrl: this.baseUrl,
|
||||||
|
scrollIntoArea: this.scrollIntoArea,
|
||||||
t: this.t
|
t: this.t
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -386,11 +411,28 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
onComplete: {
|
||||||
|
type: Function,
|
||||||
|
required: false,
|
||||||
|
default () {
|
||||||
|
return () => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
withConfetti: {
|
withConfetti: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
withTypedSignature: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
isBreakpointMd: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
baseUrl: {
|
baseUrl: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -401,11 +443,6 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
authenticityToken: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
backgroundColor: {
|
backgroundColor: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -416,6 +453,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
allowToSkip: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
goToLast: {
|
goToLast: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -436,6 +478,16 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
},
|
},
|
||||||
|
withSendCopyButton: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
withDownloadButton: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
completedButton: {
|
completedButton: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -446,6 +498,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
isCompleted: false,
|
isCompleted: false,
|
||||||
isFormVisible: true,
|
isFormVisible: true,
|
||||||
|
showFillAllRequiredFields: false,
|
||||||
currentStep: 0,
|
currentStep: 0,
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
submittedValues: {},
|
submittedValues: {},
|
||||||
@@ -456,16 +509,32 @@ export default {
|
|||||||
currentStepFields () {
|
currentStepFields () {
|
||||||
return this.stepFields[this.currentStep]
|
return this.stepFields[this.currentStep]
|
||||||
},
|
},
|
||||||
|
queryParams () {
|
||||||
|
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
|
||||||
},
|
},
|
||||||
|
previousSignatureValue () {
|
||||||
|
const signatureField = [...this.fields].reverse().find((field) => field.type === 'signature' && !!this.values[field.uuid])
|
||||||
|
|
||||||
|
return this.values[signatureField?.uuid]
|
||||||
|
},
|
||||||
|
previousInitialsValue () {
|
||||||
|
const initialsField = [...this.fields].reverse().find((field) => field.type === 'initials' && !!this.values[field.uuid])
|
||||||
|
|
||||||
|
return this.values[initialsField?.uuid]
|
||||||
|
},
|
||||||
isAnonymousChecboxes () {
|
isAnonymousChecboxes () {
|
||||||
return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name) && this.currentStepFields.length > 4
|
return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name) && this.currentStepFields.length > 4
|
||||||
},
|
},
|
||||||
isButtonDisabled () {
|
isButtonDisabled () {
|
||||||
if (this.recalculateButtonDisabledKey) {
|
if (this.recalculateButtonDisabledKey) {
|
||||||
return this.isSubmitting ||
|
return this.isSubmitting ||
|
||||||
(this.currentField.required && ['image', 'file'].includes(this.currentField.type) && !this.values[this.currentField.uuid]?.length) ||
|
(this.currentField.required && ['image', 'file', 'multiple'].includes(this.currentField.type) && !this.values[this.currentField.uuid]?.length) ||
|
||||||
(this.currentField.required && this.currentField.type === 'signature' && !this.values[this.currentField.uuid]?.length && this.$refs.currentStep && !this.$refs.currentStep.isSignatureStarted) ||
|
(this.currentField.required && this.currentField.type === 'signature' && !this.values[this.currentField.uuid]?.length && this.$refs.currentStep && !this.$refs.currentStep.isSignatureStarted) ||
|
||||||
(this.currentField.required && this.currentField.type === 'initials' && !this.values[this.currentField.uuid]?.length && this.$refs.currentStep && !this.$refs.currentStep.isInitialsStarted)
|
(this.currentField.required && this.currentField.type === 'initials' && !this.values[this.currentField.uuid]?.length && this.$refs.currentStep && !this.$refs.currentStep.isInitialsStarted)
|
||||||
} else {
|
} else {
|
||||||
@@ -476,7 +545,7 @@ export default {
|
|||||||
return this.currentStepFields[0]
|
return this.currentStepFields[0]
|
||||||
},
|
},
|
||||||
stepFields () {
|
stepFields () {
|
||||||
return this.fields.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 (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox') {
|
||||||
@@ -502,11 +571,33 @@ export default {
|
|||||||
mounted () {
|
mounted () {
|
||||||
this.submittedValues = JSON.parse(JSON.stringify(this.values))
|
this.submittedValues = JSON.parse(JSON.stringify(this.values))
|
||||||
|
|
||||||
if (this.goToLast) {
|
this.fields.forEach((field) => {
|
||||||
this.currentStep = Math.min(
|
if (field.default_value && !field.readonly) {
|
||||||
this.stepFields.indexOf([...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.values[f.uuid]))) + 1,
|
this.values[field.uuid] = field.default_value
|
||||||
this.stepFields.length - 1
|
}
|
||||||
)
|
})
|
||||||
|
|
||||||
|
if (this.queryParams.get('field_uuid')) {
|
||||||
|
const stepIndex = this.stepFields.findIndex((fields) => {
|
||||||
|
return fields.some((f) => f.uuid === this.queryParams.get('field_uuid'))
|
||||||
|
})
|
||||||
|
|
||||||
|
this.currentStep = Math.max(stepIndex, 0)
|
||||||
|
} else if (this.goToLast) {
|
||||||
|
const requiredEmptyStepIndex = this.stepFields.indexOf(this.stepFields.find((fields) => fields.some((f) => f.required && !this.submittedValues[f.uuid])))
|
||||||
|
const lastFilledStepIndex = this.stepFields.indexOf([...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.submittedValues[f.uuid]))) + 1
|
||||||
|
|
||||||
|
const indexesList = [this.stepFields.length - 1]
|
||||||
|
|
||||||
|
if (requiredEmptyStepIndex !== -1) {
|
||||||
|
indexesList.push(requiredEmptyStepIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastFilledStepIndex !== -1) {
|
||||||
|
indexesList.push(lastFilledStepIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentStep = Math.min(...indexesList)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
|
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
|
||||||
@@ -537,7 +628,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
t,
|
t,
|
||||||
maybeTrackEmailClick () {
|
maybeTrackEmailClick () {
|
||||||
const queryParams = new URLSearchParams(window.location.search)
|
const { queryParams } = this
|
||||||
|
|
||||||
if (queryParams.has('t')) {
|
if (queryParams.has('t')) {
|
||||||
const t = queryParams.get('t')
|
const t = queryParams.get('t')
|
||||||
@@ -561,7 +652,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
maybeTrackSmsClick () {
|
maybeTrackSmsClick () {
|
||||||
const queryParams = new URLSearchParams(window.location.search)
|
const { queryParams } = this
|
||||||
|
|
||||||
if (queryParams.has('c')) {
|
if (queryParams.has('c')) {
|
||||||
const c = queryParams.get('c')
|
const c = queryParams.get('c')
|
||||||
@@ -597,12 +688,13 @@ export default {
|
|||||||
},
|
},
|
||||||
goToStep (step, scrollToArea = false, clickUpload = false) {
|
goToStep (step, scrollToArea = false, clickUpload = false) {
|
||||||
this.currentStep = this.stepFields.indexOf(step)
|
this.currentStep = this.stepFields.indexOf(step)
|
||||||
|
this.showFillAllRequiredFields = false
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.recalculateButtonDisabledKey = Math.random()
|
this.recalculateButtonDisabledKey = Math.random()
|
||||||
|
|
||||||
if (scrollToArea) {
|
if (scrollToArea) {
|
||||||
this.$refs.areas.scrollIntoField(step[0])
|
this.scrollIntoField(step[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$refs.form.querySelector('input[type="date"], input[type="text"], select')?.focus()
|
this.$refs.form.querySelector('input[type="date"], input[type="text"], select')?.focus()
|
||||||
@@ -613,35 +705,50 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
saveStep (formData) {
|
saveStep (formData) {
|
||||||
|
const currentFieldUuid = this.currentField.uuid
|
||||||
|
|
||||||
if (this.isCompleted) {
|
if (this.isCompleted) {
|
||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
} else {
|
} else {
|
||||||
return fetch(this.baseUrl + this.submitPath, {
|
return fetch(this.baseUrl + this.submitPath, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData || new FormData(this.$refs.form)
|
body: formData || new FormData(this.$refs.form)
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
this.submittedValues[currentFieldUuid] = this.values[currentFieldUuid]
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
scrollIntoField (field) {
|
||||||
|
return this.$refs.areas.scrollIntoField(field)
|
||||||
|
},
|
||||||
|
scrollIntoArea (area) {
|
||||||
|
return this.$refs.areas.scrollIntoArea(area)
|
||||||
|
},
|
||||||
async submitStep () {
|
async submitStep () {
|
||||||
this.isSubmitting = true
|
this.isSubmitting = true
|
||||||
|
|
||||||
const stepPromise = ['signature', 'phone', 'initials'].includes(this.currentField.type)
|
const stepPromise = ['signature', 'phone', 'initials', 'payment'].includes(this.currentField.type)
|
||||||
? this.$refs.currentStep.submit
|
? this.$refs.currentStep.submit
|
||||||
: () => Promise.resolve({})
|
: () => Promise.resolve({})
|
||||||
|
|
||||||
stepPromise().then(async () => {
|
stepPromise().then(async () => {
|
||||||
const emptyRequiredField = this.stepFields.find((fields, index) => {
|
const emptyRequiredField = this.stepFields.find((fields, index) => {
|
||||||
return index < this.currentStep && fields[0].required && fields[0].type === 'phone' && !this.submittedValues[fields[0].uuid]
|
return index < this.currentStep && fields[0].required && (fields[0].type === 'phone' || !this.allowToSkip) && !this.submittedValues[fields[0].uuid]?.length
|
||||||
})
|
})
|
||||||
|
|
||||||
const formData = new FormData(this.$refs.form)
|
const formData = new FormData(this.$refs.form)
|
||||||
|
const isLastStep = this.currentStep === this.stepFields.length - 1
|
||||||
|
|
||||||
if (this.currentStep === this.stepFields.length - 1 && !emptyRequiredField) {
|
if (isLastStep && !emptyRequiredField) {
|
||||||
formData.append('completed', 'true')
|
formData.append('completed', 'true')
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.saveStep(formData).then(async (response) => {
|
await this.saveStep(formData).then(async (response) => {
|
||||||
if (response.status === 422) {
|
if (response.status === 422 || response.status === 500) {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
||||||
alert(data.error || 'Value is invalid')
|
alert(data.error || 'Value is invalid')
|
||||||
@@ -649,17 +756,29 @@ export default {
|
|||||||
return Promise.reject(new Error(data.error))
|
return Promise.reject(new Error(data.error))
|
||||||
}
|
}
|
||||||
|
|
||||||
this.submittedValues[this.currentField.uuid] = this.values[this.currentField.uuid]
|
if (isLastStep) {
|
||||||
|
this.isSecondWalkthrough = true
|
||||||
|
}
|
||||||
|
|
||||||
const nextStep = emptyRequiredField || this.stepFields[this.currentStep + 1]
|
const nextStep = (isLastStep && emptyRequiredField) || this.stepFields[this.currentStep + 1]
|
||||||
|
|
||||||
if (nextStep) {
|
if (nextStep) {
|
||||||
this.goToStep(nextStep, true)
|
this.goToStep(nextStep, true)
|
||||||
|
|
||||||
|
if (emptyRequiredField === nextStep) {
|
||||||
|
this.showFillAllRequiredFields = true
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.isCompleted = true
|
this.isCompleted = true
|
||||||
|
|
||||||
|
const respData = await response.text()
|
||||||
|
|
||||||
|
if (respData) {
|
||||||
|
this.onComplete(JSON.parse(respData))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error('Error submitting form:', error)
|
console.error(error)
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.isSubmitting = false
|
this.isSubmitting = false
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ const en = {
|
|||||||
submit_form: 'Submit Form',
|
submit_form: 'Submit Form',
|
||||||
type_here: 'Type here',
|
type_here: 'Type here',
|
||||||
optional: 'optional',
|
optional: 'optional',
|
||||||
|
option: 'Option',
|
||||||
|
appears_on: 'Appears on',
|
||||||
|
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 hightlighted checkboxes and click',
|
||||||
submit: 'submit',
|
submit: 'submit',
|
||||||
@@ -20,6 +23,7 @@ const en = {
|
|||||||
form_has_been_completed: 'Form has been completed!',
|
form_has_been_completed: 'Form has been completed!',
|
||||||
create_a_free_account: 'Create a Free Account',
|
create_a_free_account: 'Create a Free Account',
|
||||||
signed_with: 'Signed with',
|
signed_with: 'Signed with',
|
||||||
|
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 internatioanl format: +1xxx',
|
||||||
@@ -28,13 +32,26 @@ const en = {
|
|||||||
sending: 'Sending...',
|
sending: 'Sending...',
|
||||||
resend_code: 'Re-send code',
|
resend_code: 'Re-send code',
|
||||||
verification_code_has_been_resent: 'Verification code has been re-sent via SMS',
|
verification_code_has_been_resent: 'Verification code has been re-sent via SMS',
|
||||||
email_has_been_sent: 'Email has been sent'
|
please_fill_all_required_fields: 'Please fill all required fields',
|
||||||
|
set_today: 'Set Today',
|
||||||
|
toggle_multiline_text: 'Toggle Multiline Text',
|
||||||
|
draw_signature: 'Draw signature',
|
||||||
|
type_initial: 'Type initials',
|
||||||
|
draw: 'Draw',
|
||||||
|
type: 'Type',
|
||||||
|
type_text: 'Type text',
|
||||||
|
date: 'Date',
|
||||||
|
email_has_been_sent: 'Email has been sent',
|
||||||
|
processing: 'Processing',
|
||||||
|
pay_with_strip: 'Pay with Stripe'
|
||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
submit_form: 'Enviar Formulario',
|
submit_form: 'Enviar Formulario',
|
||||||
type_here: 'Escribe aquí',
|
type_here: 'Escribe aquí',
|
||||||
optional: 'opcional',
|
optional: 'opcional',
|
||||||
|
appears_on: 'Aparece en',
|
||||||
|
page: 'Página',
|
||||||
select_your_option: 'Selecciona tu opción',
|
select_your_option: 'Selecciona tu opción',
|
||||||
complete_hightlighted_checkboxes_and_click: 'Completa las casillas resaltadas y haz clic',
|
complete_hightlighted_checkboxes_and_click: 'Completa las casillas resaltadas y haz clic',
|
||||||
submit: 'enviar',
|
submit: 'enviar',
|
||||||
@@ -53,6 +70,7 @@ const es = {
|
|||||||
form_has_been_completed: '¡El formulario ha sido completado!',
|
form_has_been_completed: '¡El formulario ha sido completado!',
|
||||||
create_a_free_account: 'Crear una Cuenta Gratuita',
|
create_a_free_account: 'Crear una Cuenta Gratuita',
|
||||||
signed_with: 'Firmado con',
|
signed_with: 'Firmado con',
|
||||||
|
please_check_the_box_to_continue: 'Por favor marque la casilla para continuar',
|
||||||
open_source_documents_software: 'software de documentos de código abierto',
|
open_source_documents_software: 'software de documentos de código abierto',
|
||||||
verified_phone_number: 'Verificar número de teléfono',
|
verified_phone_number: 'Verificar número de teléfono',
|
||||||
use_international_format: 'Usar formato internacional: +1xxx',
|
use_international_format: 'Usar formato internacional: +1xxx',
|
||||||
@@ -61,13 +79,26 @@ const es = {
|
|||||||
sending: 'Enviando...',
|
sending: 'Enviando...',
|
||||||
resend_code: 'Reenviar código',
|
resend_code: 'Reenviar código',
|
||||||
verification_code_has_been_resent: 'El código de verificación ha sido reenviado por SMS',
|
verification_code_has_been_resent: 'El código de verificación ha sido reenviado por SMS',
|
||||||
email_has_been_sent: 'El correo electrónico ha sido enviado'
|
please_fill_all_required_fields: 'Por favor, complete todos los campos obligatorios',
|
||||||
|
set_today: 'Establecer Hoy',
|
||||||
|
date: 'Fecha',
|
||||||
|
toggle_multiline_text: 'Alternar Texto Multilínea',
|
||||||
|
draw_signature: 'Dibujar firma',
|
||||||
|
type_initial: 'Escribir iniciales',
|
||||||
|
draw: 'Dibujar',
|
||||||
|
type: 'Escribir',
|
||||||
|
type_text: 'Escribir texto',
|
||||||
|
email_has_been_sent: 'El correo electrónico ha sido enviado',
|
||||||
|
processing: 'Procesando',
|
||||||
|
pay_with_strip: 'Pagar con Stripe'
|
||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
submit_form: 'Invia Modulo',
|
submit_form: 'Invia Modulo',
|
||||||
type_here: 'Digita qui',
|
type_here: 'Digita qui',
|
||||||
optional: 'opzionale',
|
optional: 'opzionale',
|
||||||
|
appears_on: 'Compare su',
|
||||||
|
page: 'Pagina',
|
||||||
select_your_option: 'Seleziona la tua opzione',
|
select_your_option: 'Seleziona la tua opzione',
|
||||||
complete_hightlighted_checkboxes_and_click: 'Completa le caselle evidenziate e fai clic',
|
complete_hightlighted_checkboxes_and_click: 'Completa le caselle evidenziate e fai clic',
|
||||||
submit: 'invia',
|
submit: 'invia',
|
||||||
@@ -86,6 +117,7 @@ const it = {
|
|||||||
form_has_been_completed: 'Il modulo è stato completato!',
|
form_has_been_completed: 'Il modulo è stato completato!',
|
||||||
create_a_free_account: 'Crea un Account Gratuito',
|
create_a_free_account: 'Crea un Account Gratuito',
|
||||||
signed_with: 'Firmato con',
|
signed_with: 'Firmato con',
|
||||||
|
please_check_the_box_to_continue: 'Si prega di spuntare la casella per continuare',
|
||||||
open_source_documents_software: 'software di documenti open source',
|
open_source_documents_software: 'software di documenti open source',
|
||||||
verified_phone_number: 'Verifica numero di telefono',
|
verified_phone_number: 'Verifica numero di telefono',
|
||||||
use_international_format: 'Usa formato internazionale: +1xxx',
|
use_international_format: 'Usa formato internazionale: +1xxx',
|
||||||
@@ -94,13 +126,26 @@ const it = {
|
|||||||
sending: 'Invio in corso...',
|
sending: 'Invio in corso...',
|
||||||
resend_code: 'Rinvia codice',
|
resend_code: 'Rinvia codice',
|
||||||
verification_code_has_been_resent: 'Il codice di verifica è stato rinviato tramite SMS',
|
verification_code_has_been_resent: 'Il codice di verifica è stato rinviato tramite SMS',
|
||||||
email_has_been_sent: "L'email è stata inviata"
|
please_fill_all_required_fields: 'Si prega di compilare tutti i campi obbligatori',
|
||||||
|
set_today: 'Imposta Oggi',
|
||||||
|
date: 'Data',
|
||||||
|
draw_signature: 'Disegna firma',
|
||||||
|
type_initial: 'Inserisci iniziali',
|
||||||
|
draw: 'Disegna',
|
||||||
|
type: 'Inserisci',
|
||||||
|
type_text: 'Inserisci testo',
|
||||||
|
toggle_multiline_text: 'Attiva Testo Multilinea',
|
||||||
|
email_has_been_sent: "L'email è stata inviata",
|
||||||
|
processing: 'Elaborazione',
|
||||||
|
pay_with_strip: 'Paga con Stripe'
|
||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
submit_form: 'Formular absenden',
|
submit_form: 'Formular absenden',
|
||||||
type_here: 'Hier eingeben',
|
type_here: 'Hier eingeben',
|
||||||
optional: 'optional',
|
optional: 'optional',
|
||||||
|
appears_on: 'Erscheint auf',
|
||||||
|
page: 'Seite',
|
||||||
select_your_option: 'Wähle deine Option',
|
select_your_option: 'Wähle deine Option',
|
||||||
complete_hightlighted_checkboxes_and_click: 'Markierte Kontrollkästchen ausfüllen und klicken',
|
complete_hightlighted_checkboxes_and_click: 'Markierte Kontrollkästchen ausfüllen und klicken',
|
||||||
submit: 'absenden',
|
submit: 'absenden',
|
||||||
@@ -119,6 +164,7 @@ const de = {
|
|||||||
form_has_been_completed: 'Formular wurde ausgefüllt!',
|
form_has_been_completed: 'Formular wurde ausgefüllt!',
|
||||||
create_a_free_account: 'Kostenloses Konto erstellen',
|
create_a_free_account: 'Kostenloses Konto erstellen',
|
||||||
signed_with: 'Unterschrieben mit',
|
signed_with: 'Unterschrieben mit',
|
||||||
|
please_check_the_box_to_continue: 'Bitte setzen Sie das Häkchen, um fortzufahren',
|
||||||
open_source_documents_software: 'Open-Source-Dokumentensoftware',
|
open_source_documents_software: 'Open-Source-Dokumentensoftware',
|
||||||
verified_phone_number: 'Telefonnummer überprüfen',
|
verified_phone_number: 'Telefonnummer überprüfen',
|
||||||
use_international_format: 'Internationales Format verwenden: +1xxx',
|
use_international_format: 'Internationales Format verwenden: +1xxx',
|
||||||
@@ -127,13 +173,26 @@ const de = {
|
|||||||
sending: 'Senden...',
|
sending: 'Senden...',
|
||||||
resend_code: 'Code erneut senden',
|
resend_code: 'Code erneut senden',
|
||||||
verification_code_has_been_resent: 'Die Verifizierungscode wurde erneut per SMS gesendet',
|
verification_code_has_been_resent: 'Die Verifizierungscode wurde erneut per SMS gesendet',
|
||||||
email_has_been_sent: 'Die E-Mail wurde gesendet'
|
please_fill_all_required_fields: 'Bitte füllen Sie alle erforderlichen Felder aus',
|
||||||
|
set_today: 'Heute einstellen',
|
||||||
|
date: 'Datum',
|
||||||
|
draw_signature: 'Unterschrift zeichnen',
|
||||||
|
type_initial: 'Initialen eingeben',
|
||||||
|
draw: 'Zeichnen',
|
||||||
|
type: 'Eingeben',
|
||||||
|
type_text: 'Text eingeben',
|
||||||
|
toggle_multiline_text: 'Mehrzeiligen Text umschalten',
|
||||||
|
email_has_been_sent: 'Die E-Mail wurde gesendet',
|
||||||
|
processing: 'Verarbeitung',
|
||||||
|
pay_with_strip: 'Mit Stripe bezahlen'
|
||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
submit_form: 'Envoyer le Formulaire',
|
submit_form: 'Envoyer le Formulaire',
|
||||||
type_here: 'Tapez ici',
|
type_here: 'Tapez ici',
|
||||||
optional: 'facultatif',
|
optional: 'facultatif',
|
||||||
|
appears_on: 'Apparaît sur',
|
||||||
|
page: 'Page',
|
||||||
select_your_option: 'Sélectionnez votre option',
|
select_your_option: 'Sélectionnez votre option',
|
||||||
complete_hightlighted_checkboxes_and_click: 'Complétez les cases à cocher en surbrillance et cliquez',
|
complete_hightlighted_checkboxes_and_click: 'Complétez les cases à cocher en surbrillance et cliquez',
|
||||||
submit: 'envoyer',
|
submit: 'envoyer',
|
||||||
@@ -152,6 +211,7 @@ const fr = {
|
|||||||
form_has_been_completed: 'Le formulaire a été complété !',
|
form_has_been_completed: 'Le formulaire a été complété !',
|
||||||
create_a_free_account: 'Créer un Compte Gratuit',
|
create_a_free_account: 'Créer un Compte Gratuit',
|
||||||
signed_with: 'Signé avec',
|
signed_with: 'Signé avec',
|
||||||
|
please_check_the_box_to_continue: 'Veuillez cocher la case pour continuer',
|
||||||
open_source_documents_software: 'logiciel de documents open source',
|
open_source_documents_software: 'logiciel de documents open source',
|
||||||
verified_phone_number: 'Vérifier le numéro de téléphone',
|
verified_phone_number: 'Vérifier le numéro de téléphone',
|
||||||
use_international_format: 'Utiliser le format international : +1xxx',
|
use_international_format: 'Utiliser le format international : +1xxx',
|
||||||
@@ -160,13 +220,26 @@ const fr = {
|
|||||||
sending: 'Envoi en cours...',
|
sending: 'Envoi en cours...',
|
||||||
resend_code: 'Renvoyer le code',
|
resend_code: 'Renvoyer le code',
|
||||||
verification_code_has_been_resent: 'Le code de vérification a été renvoyé par SMS',
|
verification_code_has_been_resent: 'Le code de vérification a été renvoyé par SMS',
|
||||||
email_has_been_sent: "L'email a été envoyé"
|
please_fill_all_required_fields: 'Veuillez remplir tous les champs obligatoires',
|
||||||
|
set_today: "Définir Aujourd'hui",
|
||||||
|
date: 'Date',
|
||||||
|
draw_signature: 'Dessiner une signature',
|
||||||
|
type_initial: 'Saisir les initiales',
|
||||||
|
draw: 'Dessiner',
|
||||||
|
type: 'Saisir',
|
||||||
|
type_text: 'Saisir du texte',
|
||||||
|
toggle_multiline_text: 'Basculer le Texte Multiligne',
|
||||||
|
email_has_been_sent: "L'email a été envoyé",
|
||||||
|
processing: 'Traitement',
|
||||||
|
pay_with_strip: 'Paiement avec Stripe'
|
||||||
}
|
}
|
||||||
|
|
||||||
const pl = {
|
const pl = {
|
||||||
submit_form: 'Wyślij Formularz',
|
submit_form: 'Wyślij Formularz',
|
||||||
type_here: 'Wpisz tutaj',
|
type_here: 'Wpisz tutaj',
|
||||||
optional: 'opcjonalny',
|
optional: 'opcjonalny',
|
||||||
|
appears_on: 'Pojawia się na',
|
||||||
|
page: 'Strona',
|
||||||
select_your_option: 'Wybierz swoją opcję',
|
select_your_option: 'Wybierz swoją opcję',
|
||||||
complete_hightlighted_checkboxes_and_click: 'Wypełnij zaznaczone pola wyboru i kliknij',
|
complete_hightlighted_checkboxes_and_click: 'Wypełnij zaznaczone pola wyboru i kliknij',
|
||||||
submit: 'wyślij',
|
submit: 'wyślij',
|
||||||
@@ -185,6 +258,7 @@ const pl = {
|
|||||||
form_has_been_completed: 'Formularz został wypełniony!',
|
form_has_been_completed: 'Formularz został wypełniony!',
|
||||||
create_a_free_account: 'Utwórz darmowe konto',
|
create_a_free_account: 'Utwórz darmowe konto',
|
||||||
signed_with: 'Podpisane za pomocą',
|
signed_with: 'Podpisane za pomocą',
|
||||||
|
please_check_the_box_to_continue: 'Proszę zaznaczyć pole, aby kontynuować',
|
||||||
open_source_documents_software: 'oprogramowanie do dokumentów open source',
|
open_source_documents_software: 'oprogramowanie do dokumentów open source',
|
||||||
verified_phone_number: 'Zweryfikuj numer telefonu',
|
verified_phone_number: 'Zweryfikuj numer telefonu',
|
||||||
use_international_format: 'Użyj międzynarodowego formatu: +1xxx',
|
use_international_format: 'Użyj międzynarodowego formatu: +1xxx',
|
||||||
@@ -193,13 +267,26 @@ const pl = {
|
|||||||
sending: 'Wysyłanie...',
|
sending: 'Wysyłanie...',
|
||||||
resend_code: 'Ponownie wyślij kod',
|
resend_code: 'Ponownie wyślij kod',
|
||||||
verification_code_has_been_resent: 'Kod weryfikacyjny został ponownie wysłany',
|
verification_code_has_been_resent: 'Kod weryfikacyjny został ponownie wysłany',
|
||||||
email_has_been_sent: 'E-mail został wysłany'
|
please_fill_all_required_fields: 'Proszę wypełnić wszystkie wymagane pola',
|
||||||
|
set_today: 'Ustaw Dziś',
|
||||||
|
date: 'Data',
|
||||||
|
draw_signature: 'Rysuj podpis',
|
||||||
|
type_initial: 'Wprowadź inicjały',
|
||||||
|
draw: 'Rysuj',
|
||||||
|
type: 'Wprowadź',
|
||||||
|
type_text: 'Wprowadź tekst',
|
||||||
|
toggle_multiline_text: 'Przełącz Tekst Wielolinijkowy',
|
||||||
|
email_has_been_sent: 'E-mail został wysłany',
|
||||||
|
processing: 'Przetwarzanie',
|
||||||
|
pay_with_strip: 'Płatność za pomocą Stripe'
|
||||||
}
|
}
|
||||||
|
|
||||||
const uk = {
|
const uk = {
|
||||||
submit_form: 'Надіслати Форму',
|
submit_form: 'Надіслати Форму',
|
||||||
type_here: 'Введіть тут',
|
type_here: 'Введіть тут',
|
||||||
optional: 'необов’язково',
|
optional: 'необов’язково',
|
||||||
|
appears_on: "З'являється на",
|
||||||
|
page: 'Сторінка',
|
||||||
select_your_option: 'Виберіть варіант',
|
select_your_option: 'Виберіть варіант',
|
||||||
complete_hightlighted_checkboxes_and_click: 'Заповніть позначені прапорці та натисніть',
|
complete_hightlighted_checkboxes_and_click: 'Заповніть позначені прапорці та натисніть',
|
||||||
submit: 'надіслати',
|
submit: 'надіслати',
|
||||||
@@ -218,6 +305,7 @@ const uk = {
|
|||||||
form_has_been_completed: 'Форму заповнено!',
|
form_has_been_completed: 'Форму заповнено!',
|
||||||
create_a_free_account: 'Створити безкоштовний обліковий запис',
|
create_a_free_account: 'Створити безкоштовний обліковий запис',
|
||||||
signed_with: 'Підписано за допомогою',
|
signed_with: 'Підписано за допомогою',
|
||||||
|
please_check_the_box_to_continue: 'Будь ласка, позначте прапорець, щоб продовжити',
|
||||||
open_source_documents_software: 'відкритий програмний засіб для документів',
|
open_source_documents_software: 'відкритий програмний засіб для документів',
|
||||||
verified_phone_number: 'Підтвердіть номер телефону',
|
verified_phone_number: 'Підтвердіть номер телефону',
|
||||||
use_international_format: 'Використовуйте міжнародний формат: +1xxx',
|
use_international_format: 'Використовуйте міжнародний формат: +1xxx',
|
||||||
@@ -226,13 +314,26 @@ const uk = {
|
|||||||
sending: 'Надсилаю...',
|
sending: 'Надсилаю...',
|
||||||
resend_code: 'Повторно відправити код',
|
resend_code: 'Повторно відправити код',
|
||||||
verification_code_has_been_resent: 'Код підтвердження був повторно надісланий',
|
verification_code_has_been_resent: 'Код підтвердження був повторно надісланий',
|
||||||
email_has_been_sent: 'Електронний лист був відправлений'
|
please_fill_all_required_fields: "Будь ласка, заповніть всі обов'язкові поля",
|
||||||
|
set_today: 'Задати Сьогодні',
|
||||||
|
date: 'Дата',
|
||||||
|
draw_signature: 'Намалюйте підпис',
|
||||||
|
type_initial: 'Введіть ініціали',
|
||||||
|
draw: 'Підпис',
|
||||||
|
type: 'Текст',
|
||||||
|
type_text: 'Введіть текст',
|
||||||
|
toggle_multiline_text: 'Перемкнути Багаторядковий Текст',
|
||||||
|
email_has_been_sent: 'Електронний лист був відправлений',
|
||||||
|
processing: 'Обробка',
|
||||||
|
pay_with_strip: 'Сплатити за допомогою Stripe'
|
||||||
}
|
}
|
||||||
|
|
||||||
const cs = {
|
const cs = {
|
||||||
submit_form: 'Odeslat formulář',
|
submit_form: 'Odeslat formulář',
|
||||||
type_here: 'Zadejte zde',
|
type_here: 'Zadejte zde',
|
||||||
optional: 'volitelné',
|
optional: 'volitelné',
|
||||||
|
appears_on: 'Zobrazuje se na',
|
||||||
|
page: 'Stránka',
|
||||||
select_your_option: 'Vyberte svou volbu',
|
select_your_option: 'Vyberte svou volbu',
|
||||||
complete_hightlighted_checkboxes_and_click: 'Označte zvýrazněné zaškrtávací políčka a klikněte na',
|
complete_hightlighted_checkboxes_and_click: 'Označte zvýrazněné zaškrtávací políčka a klikněte na',
|
||||||
submit: 'odeslat',
|
submit: 'odeslat',
|
||||||
@@ -251,6 +352,7 @@ const cs = {
|
|||||||
form_has_been_completed: 'Formulář byl dokončen!',
|
form_has_been_completed: 'Formulář byl dokončen!',
|
||||||
create_a_free_account: 'Vytvořit bezplatný účet',
|
create_a_free_account: 'Vytvořit bezplatný účet',
|
||||||
signed_with: 'Podepsáno pomocí',
|
signed_with: 'Podepsáno pomocí',
|
||||||
|
please_check_the_box_to_continue: 'Prosím, zaškrtněte políčko pro pokračování',
|
||||||
open_source_documents_software: 'open source software pro dokumenty',
|
open_source_documents_software: 'open source software pro dokumenty',
|
||||||
verified_phone_number: 'Ověřte telefonní číslo',
|
verified_phone_number: 'Ověřte telefonní číslo',
|
||||||
use_international_format: 'Použijte mezinárodní formát: +1xxx',
|
use_international_format: 'Použijte mezinárodní formát: +1xxx',
|
||||||
@@ -259,13 +361,26 @@ const cs = {
|
|||||||
sending: 'Odesílání...',
|
sending: 'Odesílání...',
|
||||||
resend_code: 'Znovu odeslat kód',
|
resend_code: 'Znovu odeslat kód',
|
||||||
verification_code_has_been_resent: 'Ověřovací kód byl znovu odeslán',
|
verification_code_has_been_resent: 'Ověřovací kód byl znovu odeslán',
|
||||||
email_has_been_sent: 'E-mail byl odeslán'
|
please_fill_all_required_fields: 'Prosím vyplňte všechny povinné položky',
|
||||||
|
set_today: 'Nastavit Dnes',
|
||||||
|
date: 'Datum',
|
||||||
|
draw_signature: 'Nakreslit podpis',
|
||||||
|
type_initial: 'Zadat iniciály',
|
||||||
|
draw: 'Kreslit',
|
||||||
|
type: 'Zadat',
|
||||||
|
type_text: 'Zadat text',
|
||||||
|
toggle_multiline_text: 'Přepnout Víceřádkový Text',
|
||||||
|
email_has_been_sent: 'E-mail byl odeslán',
|
||||||
|
processing: 'Zpracování',
|
||||||
|
pay_with_strip: 'Zaplacení přes Stripe'
|
||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
submit_form: 'Enviar Formulário',
|
submit_form: 'Enviar Formulário',
|
||||||
type_here: 'Digite aqui',
|
type_here: 'Digite aqui',
|
||||||
optional: 'opcional',
|
optional: 'opcional',
|
||||||
|
appears_on: 'Aparece em',
|
||||||
|
page: 'Página',
|
||||||
select_your_option: 'Selecione sua opção',
|
select_your_option: 'Selecione sua opção',
|
||||||
complete_hightlighted_checkboxes_and_click: 'Complete as caixas de seleção destacadas e clique',
|
complete_hightlighted_checkboxes_and_click: 'Complete as caixas de seleção destacadas e clique',
|
||||||
submit: 'enviar',
|
submit: 'enviar',
|
||||||
@@ -284,6 +399,7 @@ const pt = {
|
|||||||
form_has_been_completed: 'O formulário foi concluído!',
|
form_has_been_completed: 'O formulário foi concluído!',
|
||||||
create_a_free_account: 'Criar uma Conta Gratuita',
|
create_a_free_account: 'Criar uma Conta Gratuita',
|
||||||
signed_with: 'Assinado com',
|
signed_with: 'Assinado com',
|
||||||
|
please_check_the_box_to_continue: 'Por favor, marque a caixa para continuar',
|
||||||
open_source_documents_software: 'software de documentos de código aberto',
|
open_source_documents_software: 'software de documentos de código aberto',
|
||||||
verified_phone_number: 'Verificar Número de Telefone',
|
verified_phone_number: 'Verificar Número de Telefone',
|
||||||
use_international_format: 'Use formato internacional: +1xxx',
|
use_international_format: 'Use formato internacional: +1xxx',
|
||||||
@@ -292,7 +408,18 @@ const pt = {
|
|||||||
sending: 'Enviando...',
|
sending: 'Enviando...',
|
||||||
resend_code: 'Reenviar código',
|
resend_code: 'Reenviar código',
|
||||||
verification_code_has_been_resent: 'O código de verificação foi reenviado via SMS',
|
verification_code_has_been_resent: 'O código de verificação foi reenviado via SMS',
|
||||||
email_has_been_sent: 'Email enviado'
|
please_fill_all_required_fields: 'Por favor, preencha todos os campos obrigatórios',
|
||||||
|
set_today: 'Definir Hoje',
|
||||||
|
date: 'Data',
|
||||||
|
draw_signature: 'Desenhar assinatura',
|
||||||
|
type_initial: 'Inserir iniciais',
|
||||||
|
draw: 'Desenhar',
|
||||||
|
type: 'Inserir',
|
||||||
|
type_text: 'Inserir texto',
|
||||||
|
toggle_multiline_text: 'Alternar Texto Multilinha',
|
||||||
|
email_has_been_sent: 'Email enviado',
|
||||||
|
processing: 'Processamento',
|
||||||
|
pay_with_strip: 'Pagar com Stripe'
|
||||||
}
|
}
|
||||||
|
|
||||||
const i18n = { en, es, it, de, fr, pl, uk, cs, pt }
|
const i18n = { en, es, it, de, fr, pl, uk, cs, pt }
|
||||||
|
|||||||
@@ -1,41 +1,65 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div dir="auto">
|
||||||
<div class="flex justify-between items-center w-full mb-2">
|
<div class="flex justify-between items-center w-full mb-2">
|
||||||
<label
|
<label
|
||||||
class="label text-2xl"
|
class="label text-2xl"
|
||||||
>{{ field.name || t('initials') }}</label>
|
>{{ field.name || t('initials') }}</label>
|
||||||
<div class="space-x-2 flex">
|
<div class="space-x-2 flex">
|
||||||
<span
|
<span
|
||||||
|
v-if="isDrawInitials"
|
||||||
class="tooltip"
|
class="tooltip"
|
||||||
|
:data-tip="t('type_initials')"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
id="type_text_button"
|
||||||
|
href="#"
|
||||||
|
class="btn btn-outline font-medium btn-sm"
|
||||||
|
@click.prevent="toggleTextInput"
|
||||||
|
>
|
||||||
|
<IconTextSize :width="16" />
|
||||||
|
<span class="hidden sm:inline">
|
||||||
|
{{ t('type') }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="tooltip ml-2"
|
||||||
:data-tip="t('draw_initials')"
|
:data-tip="t('draw_initials')"
|
||||||
>
|
>
|
||||||
<button
|
<a
|
||||||
id="type_text_button"
|
id="type_text_button"
|
||||||
class="btn btn-sm btn-circle"
|
href="#"
|
||||||
:class="{ 'btn-neutral': isDrawInitials, 'btn-outline': !isDrawInitials }"
|
class="btn btn-outline font-medium btn-sm"
|
||||||
@click.prevent="toggleTextInput"
|
@click.prevent="toggleTextInput"
|
||||||
>
|
>
|
||||||
<IconSignature :width="16" />
|
<IconSignature :width="16" />
|
||||||
</button>
|
<span class="hidden sm:inline">
|
||||||
|
{{ t('draw') }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<a
|
||||||
v-if="modelValue"
|
v-if="modelValue || computedPreviousValue"
|
||||||
class="btn btn-outline btn-sm"
|
href="#"
|
||||||
|
class="btn font-medium btn-outline btn-sm"
|
||||||
@click.prevent="remove"
|
@click.prevent="remove"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload :width="16" />
|
||||||
{{ t('clear') }}
|
{{ t('clear') }}
|
||||||
</button>
|
</a>
|
||||||
<button
|
<a
|
||||||
v-else
|
v-else
|
||||||
class="btn btn-outline btn-sm"
|
href="#"
|
||||||
|
class="btn font-medium btn-outline btn-sm"
|
||||||
@click.prevent="clear"
|
@click.prevent="clear"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload :width="16" />
|
||||||
{{ t('clear') }}
|
{{ t('clear') }}
|
||||||
</button>
|
</a>
|
||||||
<button
|
<a
|
||||||
title="Minimize"
|
title="Minimize"
|
||||||
|
href="#"
|
||||||
class="py-1.5 inline md:hidden"
|
class="py-1.5 inline md:hidden"
|
||||||
@click.prevent="$emit('minimize')"
|
@click.prevent="$emit('minimize')"
|
||||||
>
|
>
|
||||||
@@ -43,26 +67,27 @@
|
|||||||
:width="20"
|
:width="20"
|
||||||
:height="20"
|
:height="20"
|
||||||
/>
|
/>
|
||||||
</button>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<AppearsOn :field="field" />
|
||||||
<input
|
<input
|
||||||
:value="modelValue"
|
:value="modelValue || computedPreviousValue"
|
||||||
type="hidden"
|
type="hidden"
|
||||||
:name="`values[${field.uuid}]`"
|
:name="`values[${field.uuid}]`"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="modelValue"
|
v-if="modelValue || computedPreviousValue"
|
||||||
:src="attachmentsIndex[modelValue].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-72"
|
||||||
>
|
>
|
||||||
<canvas
|
<canvas
|
||||||
v-show="!modelValue"
|
v-show="!modelValue && !computedPreviousValue"
|
||||||
ref="canvas"
|
ref="canvas"
|
||||||
class="bg-white border border-base-300 rounded"
|
class="bg-white border border-base-300 rounded-2xl"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
v-if="!isDrawInitials"
|
v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
|
||||||
id="initials_text_input"
|
id="initials_text_input"
|
||||||
ref="textInput"
|
ref="textInput"
|
||||||
class="base-input !text-2xl w-full mt-6 text-center"
|
class="base-input !text-2xl w-full mt-6 text-center"
|
||||||
@@ -77,12 +102,16 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
||||||
import { IconReload, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
import { IconReload, IconTextSize, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||||
|
import SignaturePad from 'signature_pad'
|
||||||
|
import AppearsOn from './appears_on'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'InitialsStep',
|
name: 'InitialsStep',
|
||||||
components: {
|
components: {
|
||||||
|
AppearsOn,
|
||||||
IconReload,
|
IconReload,
|
||||||
|
IconTextSize,
|
||||||
IconSignature,
|
IconSignature,
|
||||||
IconArrowsDiagonalMinimize2
|
IconArrowsDiagonalMinimize2
|
||||||
},
|
},
|
||||||
@@ -106,6 +135,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
},
|
},
|
||||||
|
previousValue: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -115,10 +149,20 @@ export default {
|
|||||||
emits: ['attached', 'update:model-value', 'start', 'minimize', 'focus'],
|
emits: ['attached', 'update:model-value', 'start', 'minimize', 'focus'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isInitialsStarted: false,
|
isInitialsStarted: !!this.previousValue,
|
||||||
|
isUsePreviousValue: true,
|
||||||
isDrawInitials: false
|
isDrawInitials: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
computedPreviousValue () {
|
||||||
|
if (this.isUsePreviousValue) {
|
||||||
|
return this.previousValue
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
async mounted () {
|
async mounted () {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (this.$refs.canvas) {
|
if (this.$refs.canvas) {
|
||||||
@@ -133,8 +177,6 @@ export default {
|
|||||||
import('@rails/activestorage')
|
import('@rails/activestorage')
|
||||||
}
|
}
|
||||||
|
|
||||||
const { default: SignaturePad } = await import('signature_pad')
|
|
||||||
|
|
||||||
if (this.$refs.canvas) {
|
if (this.$refs.canvas) {
|
||||||
this.pad = new SignaturePad(this.$refs.canvas)
|
this.pad = new SignaturePad(this.$refs.canvas)
|
||||||
|
|
||||||
@@ -148,6 +190,9 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
remove () {
|
remove () {
|
||||||
this.$emit('update:model-value', '')
|
this.$emit('update:model-value', '')
|
||||||
|
|
||||||
|
this.isUsePreviousValue = false
|
||||||
|
this.isInitialsStarted = false
|
||||||
},
|
},
|
||||||
clear () {
|
clear () {
|
||||||
this.pad.clear()
|
this.pad.clear()
|
||||||
@@ -189,7 +234,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async submit () {
|
async submit () {
|
||||||
if (this.modelValue) {
|
if (this.modelValue || this.computedPreviousValue) {
|
||||||
|
if (this.computedPreviousValue) {
|
||||||
|
this.$emit('update:model-value', this.computedPreviousValue)
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,30 +2,38 @@
|
|||||||
<label
|
<label
|
||||||
v-if="field.name"
|
v-if="field.name"
|
||||||
:for="field.uuid"
|
:for="field.uuid"
|
||||||
|
dir="auto"
|
||||||
class="label text-2xl mb-2"
|
class="label text-2xl mb-2"
|
||||||
>{{ field.name }}</label>
|
>{{ field.name }}</label>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
|
<input
|
||||||
|
v-if="modelValue.length === 0"
|
||||||
|
type="text"
|
||||||
|
:name="`values[${field.uuid}][]`"
|
||||||
|
:value="''"
|
||||||
|
class="hidden"
|
||||||
|
>
|
||||||
<div class="space-y-3.5 mx-auto">
|
<div class="space-y-3.5 mx-auto">
|
||||||
<div
|
<div
|
||||||
v-for="(option, index) in field.options"
|
v-for="(option, index) in field.options"
|
||||||
:key="index"
|
:key="option.uuid"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
:for="field.uuid + option"
|
:for="option.uuid"
|
||||||
class="flex items-center space-x-3"
|
class="flex items-center space-x-3"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
:id="field.uuid + option"
|
:id="option.uuid"
|
||||||
:ref="setInputRef"
|
:ref="setInputRef"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:name="`values[${field.uuid}][]`"
|
:name="`values[${field.uuid}][]`"
|
||||||
:value="option"
|
:value="optionValue(option, index)"
|
||||||
class="base-checkbox !h-7 !w-7"
|
class="base-checkbox !h-7 !w-7"
|
||||||
:checked="modelValue.includes(option)"
|
:checked="(modelValue || []).includes(optionValue(option, index))"
|
||||||
@change="onChange"
|
@change="onChange"
|
||||||
>
|
>
|
||||||
<span class="text-xl">
|
<span class="text-xl">
|
||||||
{{ option }}
|
{{ optionValue(option, index) }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -36,6 +44,7 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: 'MultiSelectStep',
|
name: 'MultiSelectStep',
|
||||||
|
inject: ['t'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -62,8 +71,15 @@ export default {
|
|||||||
this.inputRefs.push(el)
|
this.inputRefs.push(el)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
optionValue (option, index) {
|
||||||
|
if (option.value) {
|
||||||
|
return option.value
|
||||||
|
} else {
|
||||||
|
return `${this.t('option')} ${index + 1}`
|
||||||
|
}
|
||||||
|
},
|
||||||
onChange () {
|
onChange () {
|
||||||
this.$emit('update:model-value', this.inputRefs.filter(e => e.checked).map(e => e.value))
|
this.$emit('update:model-value', this.inputRefs.filter(e => e.checked).map((e, index) => this.optionValue(e, index)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
<template>
|
||||||
|
<label
|
||||||
|
v-if="!modelValue && !sessionId"
|
||||||
|
:for="field.uuid"
|
||||||
|
class="label text-2xl mb-2"
|
||||||
|
>{{ field.name || defaultName }}
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
:value="modelValue"
|
||||||
|
hidden
|
||||||
|
:name="`values[${field.uuid}]`"
|
||||||
|
class="hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="modelValue && !sessionId"
|
||||||
|
class=" text-2xl mb-2"
|
||||||
|
>
|
||||||
|
Already paid
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<button
|
||||||
|
v-if="sessionId"
|
||||||
|
disabled
|
||||||
|
class="base-button w-full"
|
||||||
|
>
|
||||||
|
<IconLoader
|
||||||
|
width="22"
|
||||||
|
class="animate-spin"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{{ t('processing') }}...
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
:id="field.uuid"
|
||||||
|
class="btn bg-[#7B73FF] text-white hover:bg-[#0A2540] text-lg w-full"
|
||||||
|
:class="{ disabled: isCreatingCheckout }"
|
||||||
|
:disabled="isCreatingCheckout"
|
||||||
|
@click.prevent="startCheckout"
|
||||||
|
>
|
||||||
|
<IconInnerShadowTop
|
||||||
|
v-if="isCreatingCheckout"
|
||||||
|
width="22"
|
||||||
|
class="animate-spin"
|
||||||
|
/>
|
||||||
|
<IconBrandStripe
|
||||||
|
v-else
|
||||||
|
width="22"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{{ t('pay_with_strip') }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconBrandStripe, IconInnerShadowTop, IconLoader } from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PaymentStep',
|
||||||
|
components: {
|
||||||
|
IconBrandStripe,
|
||||||
|
IconInnerShadowTop,
|
||||||
|
IconLoader
|
||||||
|
},
|
||||||
|
inject: ['baseUrl', 't'],
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
field: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
submitterSlug: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['focus', 'submit', 'update:model-value', 'attached'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isCreatingCheckout: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
queryParams () {
|
||||||
|
return new URLSearchParams(window.location.search)
|
||||||
|
},
|
||||||
|
sessionId () {
|
||||||
|
return this.queryParams.get('stripe_session_id')
|
||||||
|
},
|
||||||
|
defaultName () {
|
||||||
|
const { price, currency } = this.field.preferences || {}
|
||||||
|
|
||||||
|
const formattedPrice = new Intl.NumberFormat([], {
|
||||||
|
style: 'currency',
|
||||||
|
currency
|
||||||
|
}).format(price)
|
||||||
|
|
||||||
|
return `Pay ${formattedPrice}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
if (this.sessionId) {
|
||||||
|
this.$emit('submit')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async submit () {
|
||||||
|
if (this.sessionId) {
|
||||||
|
return fetch((this.baseUrl || '/embed').replace('/embed', '/api/stripe_payments/' + this.sessionId), {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({
|
||||||
|
submitter_slug: this.submitterSlug
|
||||||
|
}),
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
}).then(async (resp) => {
|
||||||
|
if (resp.status === 422 || resp.status === 500) {
|
||||||
|
const data = await resp.json()
|
||||||
|
|
||||||
|
alert(data.error || 'Unexpected error')
|
||||||
|
|
||||||
|
return Promise.reject(new Error(data.error))
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachment = await resp.json()
|
||||||
|
|
||||||
|
window.history.replaceState({}, document.title, window.location.pathname)
|
||||||
|
|
||||||
|
this.$emit('update:model-value', attachment.uuid)
|
||||||
|
this.$emit('attached', attachment)
|
||||||
|
|
||||||
|
return resp
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return Promise.resolve({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
startCheckout () {
|
||||||
|
this.isCreatingCheckout = true
|
||||||
|
|
||||||
|
fetch((this.baseUrl || '/embed').replace('/embed', '/api/stripe_payments'), {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
submitter_slug: this.submitterSlug,
|
||||||
|
field_uuid: this.field.uuid
|
||||||
|
}),
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
}).then(async (resp) => {
|
||||||
|
if (resp.status === 422 || resp.status === 500) {
|
||||||
|
const data = await resp.json()
|
||||||
|
|
||||||
|
alert(data.message || 'Unexpected error')
|
||||||
|
|
||||||
|
return Promise.reject(new Error(data.message))
|
||||||
|
}
|
||||||
|
|
||||||
|
const { url } = await resp.json()
|
||||||
|
|
||||||
|
const link = document.createElement('a')
|
||||||
|
|
||||||
|
link.href = url
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
link.click()
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
this.isCreatingCheckout = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
>
|
>
|
||||||
<div class="flex justify-between mt-2 -mb-2 md:-mb-4">
|
<div class="flex justify-between mt-2 -mb-2 md:-mb-4">
|
||||||
<a
|
<a
|
||||||
|
v-if="!defaultValue"
|
||||||
href="#"
|
href="#"
|
||||||
class="link"
|
class="link"
|
||||||
@click.prevent="isCodeSent = false"
|
@click.prevent="isCodeSent = false"
|
||||||
@@ -48,6 +49,7 @@
|
|||||||
:id="field.uuid"
|
:id="field.uuid"
|
||||||
ref="phone"
|
ref="phone"
|
||||||
:value="modelValue || defaultValue"
|
:value="modelValue || defaultValue"
|
||||||
|
:readonly="!!defaultValue"
|
||||||
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\-]+$"
|
||||||
|
|||||||
@@ -1,29 +1,50 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div dir="auto">
|
||||||
<div class="flex justify-between items-center w-full mb-2">
|
<div class="flex justify-between items-center w-full mb-2">
|
||||||
<label
|
<label
|
||||||
class="label text-2xl"
|
class="label text-2xl"
|
||||||
>{{ field.name || t('signature') }}</label>
|
>{{ field.name || t('signature') }}</label>
|
||||||
<div class="space-x-2 flex">
|
<div class="space-x-2 flex">
|
||||||
<span
|
<span
|
||||||
|
v-if="isTextSignature"
|
||||||
class="tooltip"
|
class="tooltip"
|
||||||
data-tip="Type text"
|
:data-tip="t('draw_signature')"
|
||||||
>
|
>
|
||||||
<button
|
<a
|
||||||
id="type_text_button"
|
id="type_text_button"
|
||||||
class="btn btn-sm btn-circle"
|
href="#"
|
||||||
:class="{ 'btn-neutral': isTextSignature, 'btn-outline': !isTextSignature }"
|
class="btn btn-outline btn-sm font-medium"
|
||||||
|
@click.prevent="toggleTextInput"
|
||||||
|
>
|
||||||
|
<IconSignature :width="16" />
|
||||||
|
<span class="hidden sm:inline">
|
||||||
|
{{ t('draw') }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="withTypedSignature"
|
||||||
|
class="tooltip ml-2"
|
||||||
|
:data-tip="t('type_text')"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
id="type_text_button"
|
||||||
|
href="#"
|
||||||
|
class="btn btn-outline btn-sm font-medium"
|
||||||
@click.prevent="toggleTextInput"
|
@click.prevent="toggleTextInput"
|
||||||
>
|
>
|
||||||
<IconTextSize :width="16" />
|
<IconTextSize :width="16" />
|
||||||
</button>
|
<span class="hidden sm:inline">
|
||||||
|
{{ t('type') }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="tooltip"
|
class="tooltip"
|
||||||
data-tip="Take photo"
|
data-tip="Take photo"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="btn btn-outline btn-sm btn-circle"
|
class="btn btn-outline btn-sm font-medium"
|
||||||
>
|
>
|
||||||
<IconCamera :width="16" />
|
<IconCamera :width="16" />
|
||||||
<input
|
<input
|
||||||
@@ -32,25 +53,31 @@
|
|||||||
accept="image/*"
|
accept="image/*"
|
||||||
@change="drawImage"
|
@change="drawImage"
|
||||||
>
|
>
|
||||||
|
<span class="hidden sm:inline">
|
||||||
|
{{ t('upload') }}
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<a
|
||||||
v-if="modelValue"
|
v-if="modelValue || computedPreviousValue"
|
||||||
class="btn btn-outline btn-sm"
|
href="#"
|
||||||
|
class="btn btn-outline btn-sm font-medium"
|
||||||
@click.prevent="remove"
|
@click.prevent="remove"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload :width="16" />
|
||||||
{{ t('redraw') }}
|
{{ t('redraw') }}
|
||||||
</button>
|
</a>
|
||||||
<button
|
<a
|
||||||
v-else
|
v-else
|
||||||
class="btn btn-outline btn-sm"
|
href="#"
|
||||||
|
class="btn btn-outline btn-sm font-medium"
|
||||||
@click.prevent="clear"
|
@click.prevent="clear"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload :width="16" />
|
||||||
{{ t('clear') }}
|
{{ t('clear') }}
|
||||||
</button>
|
</a>
|
||||||
<button
|
<a
|
||||||
|
href="#"
|
||||||
title="Minimize"
|
title="Minimize"
|
||||||
class="py-1.5 inline md:hidden"
|
class="py-1.5 inline md:hidden"
|
||||||
@click.prevent="$emit('minimize')"
|
@click.prevent="$emit('minimize')"
|
||||||
@@ -59,23 +86,25 @@
|
|||||||
:width="20"
|
:width="20"
|
||||||
:height="20"
|
:height="20"
|
||||||
/>
|
/>
|
||||||
</button>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<AppearsOn :field="field" />
|
||||||
<input
|
<input
|
||||||
:value="modelValue"
|
:value="modelValue || computedPreviousValue"
|
||||||
type="hidden"
|
type="hidden"
|
||||||
:name="`values[${field.uuid}]`"
|
:name="`values[${field.uuid}]`"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="modelValue"
|
v-if="modelValue || computedPreviousValue"
|
||||||
:src="attachmentsIndex[modelValue].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-72"
|
||||||
>
|
>
|
||||||
<canvas
|
<canvas
|
||||||
v-show="!modelValue"
|
v-show="!modelValue && !computedPreviousValue"
|
||||||
ref="canvas"
|
ref="canvas"
|
||||||
class="bg-white border border-base-300 rounded"
|
style="padding: 1px; 0"
|
||||||
|
class="bg-white border border-base-300 rounded-2xl"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
v-if="isTextSignature"
|
v-if="isTextSignature"
|
||||||
@@ -91,15 +120,21 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconReload, IconCamera, IconTextSize, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||||
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
||||||
|
import SignaturePad from 'signature_pad'
|
||||||
|
import AppearsOn from './appears_on'
|
||||||
|
|
||||||
|
let isFontLoaded = false
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SignatureStep',
|
name: 'SignatureStep',
|
||||||
components: {
|
components: {
|
||||||
|
AppearsOn,
|
||||||
IconReload,
|
IconReload,
|
||||||
IconCamera,
|
IconCamera,
|
||||||
IconTextSize,
|
IconTextSize,
|
||||||
|
IconSignature,
|
||||||
IconArrowsDiagonalMinimize2
|
IconArrowsDiagonalMinimize2
|
||||||
},
|
},
|
||||||
inject: ['baseUrl', 't'],
|
inject: ['baseUrl', 't'],
|
||||||
@@ -117,11 +152,21 @@ export default {
|
|||||||
required: true,
|
required: true,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
withTypedSignature: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
attachmentsIndex: {
|
attachmentsIndex: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
},
|
},
|
||||||
|
previousValue: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -131,10 +176,20 @@ export default {
|
|||||||
emits: ['attached', 'update:model-value', 'start', 'minimize'],
|
emits: ['attached', 'update:model-value', 'start', 'minimize'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isSignatureStarted: false,
|
isSignatureStarted: !!this.previousValue,
|
||||||
|
isUsePreviousValue: true,
|
||||||
isTextSignature: false
|
isTextSignature: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
computedPreviousValue () {
|
||||||
|
if (this.isUsePreviousValue) {
|
||||||
|
return this.previousValue
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
async mounted () {
|
async mounted () {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (this.$refs.canvas) {
|
if (this.$refs.canvas) {
|
||||||
@@ -147,8 +202,6 @@ export default {
|
|||||||
import('@rails/activestorage')
|
import('@rails/activestorage')
|
||||||
}
|
}
|
||||||
|
|
||||||
const { default: SignaturePad } = await import('signature_pad')
|
|
||||||
|
|
||||||
if (this.$refs.canvas) {
|
if (this.$refs.canvas) {
|
||||||
this.pad = new SignaturePad(this.$refs.canvas)
|
this.pad = new SignaturePad(this.$refs.canvas)
|
||||||
|
|
||||||
@@ -162,6 +215,22 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
remove () {
|
remove () {
|
||||||
this.$emit('update:model-value', '')
|
this.$emit('update:model-value', '')
|
||||||
|
|
||||||
|
this.isUsePreviousValue = false
|
||||||
|
this.isSignatureStarted = false
|
||||||
|
},
|
||||||
|
loadFont () {
|
||||||
|
if (!isFontLoaded) {
|
||||||
|
const font = new FontFace('Dancing Script', `url(${this.baseUrl}/fonts/DancingScript.otf) format("opentype")`)
|
||||||
|
|
||||||
|
font.load().then((loadedFont) => {
|
||||||
|
document.fonts.add(loadedFont)
|
||||||
|
|
||||||
|
isFontLoaded = true
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('Font loading failed:', error)
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
clear () {
|
clear () {
|
||||||
this.pad.clear()
|
this.pad.clear()
|
||||||
@@ -178,8 +247,8 @@ export default {
|
|||||||
const canvas = this.$refs.canvas
|
const canvas = this.$refs.canvas
|
||||||
const context = canvas.getContext('2d')
|
const context = canvas.getContext('2d')
|
||||||
|
|
||||||
const fontFamily = 'Arial'
|
const fontFamily = 'Dancing Script'
|
||||||
const fontSize = '44px'
|
const fontSize = '38px'
|
||||||
const fontStyle = 'italic'
|
const fontStyle = 'italic'
|
||||||
const fontWeight = ''
|
const fontWeight = ''
|
||||||
|
|
||||||
@@ -197,6 +266,8 @@ export default {
|
|||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$refs.textInput.focus()
|
this.$refs.textInput.focus()
|
||||||
|
|
||||||
|
this.loadFont()
|
||||||
|
|
||||||
this.$emit('start')
|
this.$emit('start')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -250,7 +321,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async submit () {
|
async submit () {
|
||||||
if (this.modelValue) {
|
if (this.modelValue || this.computedPreviousValue) {
|
||||||
|
if (this.computedPreviousValue) {
|
||||||
|
this.$emit('update:model-value', this.computedPreviousValue)
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
<template>
|
||||||
|
<label
|
||||||
|
v-if="field.name"
|
||||||
|
:for="field.uuid"
|
||||||
|
dir="auto"
|
||||||
|
class="label text-2xl mb-2"
|
||||||
|
>{{ field.name }}
|
||||||
|
<template v-if="!field.required">({{ t('optional') }})</template>
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="py-1"
|
||||||
|
/>
|
||||||
|
<AppearsOn :field="field" />
|
||||||
|
<div class="items-center flex">
|
||||||
|
<input
|
||||||
|
v-if="!isTextArea"
|
||||||
|
:id="field.uuid"
|
||||||
|
v-model="text"
|
||||||
|
:maxlength="cellsMaxLegth"
|
||||||
|
dir="auto"
|
||||||
|
class="base-input !text-2xl w-full !pr-11 -mr-10"
|
||||||
|
:required="field.required"
|
||||||
|
: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')})`}`"
|
||||||
|
type="text"
|
||||||
|
:name="`values[${field.uuid}]`"
|
||||||
|
@focus="$emit('focus')"
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
v-if="isTextArea"
|
||||||
|
:id="field.uuid"
|
||||||
|
ref="textarea"
|
||||||
|
v-model="text"
|
||||||
|
class="base-textarea !text-2xl w-full"
|
||||||
|
:placeholder="`${t('type_here')}...${field.required ? '' : ` (${t('optional')})`}`"
|
||||||
|
:required="field.required"
|
||||||
|
:name="`values[${field.uuid}]`"
|
||||||
|
@input="resizeTextarea"
|
||||||
|
@focus="$emit('focus')"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="!isTextArea && field.type !== 'cells'"
|
||||||
|
class="tooltip"
|
||||||
|
:data-tip="t('toggle_multiline_text')"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="btn btn-ghost btn-circle btn-sm"
|
||||||
|
@click.prevent="toggleTextArea"
|
||||||
|
>
|
||||||
|
<IconAlignBoxLeftTop />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconAlignBoxLeftTop } from '@tabler/icons-vue'
|
||||||
|
import AppearsOn from './appears_on'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TextStep',
|
||||||
|
components: {
|
||||||
|
IconAlignBoxLeftTop,
|
||||||
|
AppearsOn
|
||||||
|
},
|
||||||
|
inject: ['t'],
|
||||||
|
props: {
|
||||||
|
field: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['update:model-value', 'focus'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isTextArea: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
cellsMaxLegth () {
|
||||||
|
if (this.field.type === 'cells') {
|
||||||
|
const area = this.field.areas?.[0]
|
||||||
|
|
||||||
|
if (area) {
|
||||||
|
return parseInt(area.w / area.cell_w) + 1
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
set (value) {
|
||||||
|
this.$emit('update:model-value', value)
|
||||||
|
},
|
||||||
|
get () {
|
||||||
|
return this.modelValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.isTextArea = this.modelValue?.includes('\n')
|
||||||
|
|
||||||
|
if (this.isTextArea) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.resizeTextarea()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resizeTextarea () {
|
||||||
|
const textarea = this.$refs.textarea
|
||||||
|
|
||||||
|
textarea.style.height = 'auto'
|
||||||
|
textarea.style.height = textarea.scrollHeight + 'px'
|
||||||
|
},
|
||||||
|
toggleTextArea () {
|
||||||
|
this.isTextArea = true
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.textarea.focus()
|
||||||
|
this.$refs.textarea.setSelectionRange(this.$refs.textarea.value.length, this.$refs.textarea.value.length)
|
||||||
|
|
||||||
|
this.resizeTextarea()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
:style="positionStyle"
|
:style="positionStyle"
|
||||||
@pointerdown.stop
|
@pointerdown.stop
|
||||||
@mousedown.stop="startDrag"
|
@mousedown.stop="startDrag"
|
||||||
|
@touchstart="startTouchDrag"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="isSelected || isDraw"
|
v-if="isSelected || isDraw"
|
||||||
@@ -22,7 +23,7 @@
|
|||||||
:style="{ left: (cellW / area.w * 100) + '%' }"
|
:style="{ left: (cellW / area.w * 100) + '%' }"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-if="index === 0"
|
v-if="index === 0 && editable"
|
||||||
class="h-2.5 w-2.5 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-ew-resize z-10"
|
class="h-2.5 w-2.5 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-ew-resize z-10"
|
||||||
style="left: -4px"
|
style="left: -4px"
|
||||||
@mousedown.stop="startResizeCell"
|
@mousedown.stop="startResizeCell"
|
||||||
@@ -32,7 +33,7 @@
|
|||||||
<div
|
<div
|
||||||
v-if="field?.type"
|
v-if="field?.type"
|
||||||
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 group-hover:flex group-hover:z-10"
|
||||||
:class="{ 'flex z-10': isNameFocus || isSelected, hidden: !isNameFocus && !isSelected }"
|
: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
|
||||||
@@ -41,6 +42,7 @@
|
|||||||
v-model="field.submitter_uuid"
|
v-model="field.submitter_uuid"
|
||||||
class="border-r"
|
class="border-r"
|
||||||
:compact="true"
|
:compact="true"
|
||||||
|
:editable="editable && !defaultField"
|
||||||
:menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px]'"
|
:menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px]'"
|
||||||
:submitters="template.submitters"
|
:submitters="template.submitters"
|
||||||
@update:model-value="save"
|
@update:model-value="save"
|
||||||
@@ -49,6 +51,7 @@
|
|||||||
<FieldType
|
<FieldType
|
||||||
v-model="field.type"
|
v-model="field.type"
|
||||||
:button-width="27"
|
:button-width="27"
|
||||||
|
:editable="editable && !defaultField"
|
||||||
:button-classes="'px-1'"
|
:button-classes="'px-1'"
|
||||||
:menu-classes="'bg-white rounded-t-none'"
|
:menu-classes="'bg-white rounded-t-none'"
|
||||||
@update:model-value="[maybeUpdateOptions(), save()]"
|
@update:model-value="[maybeUpdateOptions(), save()]"
|
||||||
@@ -57,15 +60,16 @@
|
|||||||
<span
|
<span
|
||||||
v-if="field.type !== 'checkbox' || field.name"
|
v-if="field.type !== 'checkbox' || field.name"
|
||||||
ref="name"
|
ref="name"
|
||||||
contenteditable
|
:contenteditable="editable && !defaultField"
|
||||||
|
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"
|
||||||
@keydown.enter.prevent="onNameEnter"
|
@keydown.enter.prevent="onNameEnter"
|
||||||
@focus="onNameFocus"
|
@focus="onNameFocus"
|
||||||
@blur="onNameBlur"
|
@blur="onNameBlur"
|
||||||
>{{ field.name || defaultName }}</span>
|
>{{ optionIndexText }} {{ field.name || defaultName }}</span>
|
||||||
<div
|
<div
|
||||||
v-if="isNameFocus && field.type !== 'checkbox'"
|
v-if="isNameFocus && !['checkbox', 'phone'].includes(field.type)"
|
||||||
class="flex items-center ml-1.5"
|
class="flex items-center ml-1.5"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -80,10 +84,10 @@
|
|||||||
class="label text-xs"
|
class="label text-xs"
|
||||||
@click.prevent="field.required = !field.required"
|
@click.prevent="field.required = !field.required"
|
||||||
@mousedown.prevent
|
@mousedown.prevent
|
||||||
>Required</label>
|
>{{ t('required') }}</label>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
v-else
|
v-else-if="editable"
|
||||||
class="pr-1"
|
class="pr-1"
|
||||||
title="Remove"
|
title="Remove"
|
||||||
@click.prevent="$emit('remove')"
|
@click.prevent="$emit('remove')"
|
||||||
@@ -92,28 +96,42 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="opacity-50 flex items-center justify-center h-full w-full"
|
class="flex items-center h-full w-full"
|
||||||
:class="bgColors[submitterIndex]"
|
:class="[bgColors[submitterIndex], field?.default_value ? '' : 'justify-center']"
|
||||||
>
|
>
|
||||||
<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 h-full"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
v-if="field?.default_value"
|
||||||
|
:class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref="textContainer"
|
||||||
|
class="flex items-center px-0.5"
|
||||||
|
>
|
||||||
|
<span class="whitespace-pre-wrap">{{ field.default_value }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<component
|
<component
|
||||||
:is="fieldIcons[field.type]"
|
:is="fieldIcons[field.type]"
|
||||||
|
v-else
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
class="max-h-10"
|
class="max-h-10 opacity-50"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
ref="touchTarget"
|
||||||
class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer"
|
class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
v-if="field?.type"
|
v-if="field?.type && editable"
|
||||||
class="h-2.5 w-2.5 -right-1 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-nwse-resize"
|
class="h-4 w-4 md:h-2.5 md:w-2.5 -right-1 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-nwse-resize"
|
||||||
@mousedown.stop="startResize"
|
@mousedown.stop="startResize"
|
||||||
|
@touchstart="startTouchResize"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -123,6 +141,7 @@ 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 } from '@tabler/icons-vue'
|
import { IconX } from '@tabler/icons-vue'
|
||||||
|
import { v4 } from 'uuid'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FieldArea',
|
name: 'FieldArea',
|
||||||
@@ -131,7 +150,7 @@ export default {
|
|||||||
FieldSubmitter,
|
FieldSubmitter,
|
||||||
IconX
|
IconX
|
||||||
},
|
},
|
||||||
inject: ['template', 'selectedAreaRef', 'save'],
|
inject: ['template', 'selectedAreaRef', 'save', 't'],
|
||||||
props: {
|
props: {
|
||||||
area: {
|
area: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -142,6 +161,16 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
defaultField: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -154,6 +183,7 @@ export default {
|
|||||||
isResize: false,
|
isResize: false,
|
||||||
isDragged: false,
|
isDragged: false,
|
||||||
isNameFocus: false,
|
isNameFocus: false,
|
||||||
|
textOverflowChars: 0,
|
||||||
dragFrom: { x: 0, y: 0 }
|
dragFrom: { x: 0, y: 0 }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -161,6 +191,13 @@ export default {
|
|||||||
defaultName: Field.computed.defaultName,
|
defaultName: Field.computed.defaultName,
|
||||||
fieldNames: FieldType.computed.fieldNames,
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
fieldIcons: FieldType.computed.fieldIcons,
|
fieldIcons: FieldType.computed.fieldIcons,
|
||||||
|
optionIndexText () {
|
||||||
|
if (this.area.option_uuid && this.field.options) {
|
||||||
|
return `${this.field.options.findIndex((o) => o.uuid === this.area.option_uuid) + 1}.`
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
},
|
||||||
cells () {
|
cells () {
|
||||||
const cells = []
|
const cells = []
|
||||||
|
|
||||||
@@ -182,20 +219,30 @@ export default {
|
|||||||
},
|
},
|
||||||
borderColors () {
|
borderColors () {
|
||||||
return [
|
return [
|
||||||
'border-red-500',
|
'border-red-500/80',
|
||||||
'border-sky-500',
|
'border-sky-500/80',
|
||||||
'border-emerald-500',
|
'border-emerald-500/80',
|
||||||
'border-yellow-300',
|
'border-yellow-300/80',
|
||||||
'border-purple-600'
|
'border-purple-600/80',
|
||||||
|
'border-pink-500/80',
|
||||||
|
'border-cyan-500/80',
|
||||||
|
'border-orange-500/80',
|
||||||
|
'border-lime-500/80',
|
||||||
|
'border-indigo-500/80'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
bgColors () {
|
bgColors () {
|
||||||
return [
|
return [
|
||||||
'bg-red-100',
|
'bg-red-100/80',
|
||||||
'bg-sky-100',
|
'bg-sky-100/80',
|
||||||
'bg-emerald-100',
|
'bg-emerald-100/80',
|
||||||
'bg-yellow-100',
|
'bg-yellow-100/80',
|
||||||
'bg-purple-100'
|
'bg-purple-100/80',
|
||||||
|
'bg-pink-100/80',
|
||||||
|
'bg-cyan-100/80',
|
||||||
|
'bg-orange-100/80',
|
||||||
|
'bg-lime-100/80',
|
||||||
|
'bg-indigo-100/80'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
isSelected () {
|
isSelected () {
|
||||||
@@ -212,6 +259,20 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
'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.textOverflowChars = this.$el.clientHeight < this.$refs.textContainer.clientHeight ? this.field.default_value.length : 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onNameFocus (e) {
|
onNameFocus (e) {
|
||||||
this.selectedAreaRef.value = this.area
|
this.selectedAreaRef.value = this.area
|
||||||
@@ -249,12 +310,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
maybeUpdateOptions () {
|
maybeUpdateOptions () {
|
||||||
|
delete this.field.default_value
|
||||||
|
|
||||||
if (!['radio', 'multiple', 'select'].includes(this.field.type)) {
|
if (!['radio', 'multiple', 'select'].includes(this.field.type)) {
|
||||||
delete this.field.options
|
delete this.field.options
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['select', 'multiple', 'radio'].includes(this.field.type)) {
|
if (['select', 'multiple', 'radio'].includes(this.field.type)) {
|
||||||
this.field.options ||= ['']
|
this.field.options ||= [{ value: '', uuid: v4() }]
|
||||||
}
|
}
|
||||||
|
|
||||||
(this.field.areas || []).forEach((area) => {
|
(this.field.areas || []).forEach((area) => {
|
||||||
@@ -300,6 +363,10 @@ export default {
|
|||||||
startDrag (e) {
|
startDrag (e) {
|
||||||
this.selectedAreaRef.value = this.area
|
this.selectedAreaRef.value = this.area
|
||||||
|
|
||||||
|
if (!this.editable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const rect = e.target.getBoundingClientRect()
|
const rect = e.target.getBoundingClientRect()
|
||||||
|
|
||||||
this.dragFrom = { x: e.clientX - rect.left, y: e.clientY - rect.top }
|
this.dragFrom = { x: e.clientX - rect.left, y: e.clientY - rect.top }
|
||||||
@@ -309,6 +376,47 @@ export default {
|
|||||||
|
|
||||||
this.$emit('start-drag')
|
this.$emit('start-drag')
|
||||||
},
|
},
|
||||||
|
startTouchDrag (e) {
|
||||||
|
if (e.target !== this.$refs.touchTarget) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$refs?.name?.blur()
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.isDragged = true
|
||||||
|
|
||||||
|
const rect = e.target.getBoundingClientRect()
|
||||||
|
|
||||||
|
this.selectedAreaRef.value = this.area
|
||||||
|
|
||||||
|
this.dragFrom = { x: rect.left - e.touches[0].clientX, y: rect.top - e.touches[0].clientY }
|
||||||
|
|
||||||
|
this.$el.getRootNode().addEventListener('touchmove', this.touchDrag)
|
||||||
|
this.$el.getRootNode().addEventListener('touchend', this.stopTouchDrag)
|
||||||
|
|
||||||
|
this.$emit('start-drag')
|
||||||
|
},
|
||||||
|
touchDrag (e) {
|
||||||
|
const page = this.$parent.$refs.mask.previousSibling
|
||||||
|
const rect = page.getBoundingClientRect()
|
||||||
|
|
||||||
|
this.area.x = (this.dragFrom.x + e.touches[0].clientX - rect.left) / rect.width
|
||||||
|
this.area.y = (this.dragFrom.y + e.touches[0].clientY - rect.top) / rect.height
|
||||||
|
},
|
||||||
|
stopTouchDrag () {
|
||||||
|
this.$el.getRootNode().removeEventListener('touchmove', this.touchDrag)
|
||||||
|
this.$el.getRootNode().removeEventListener('touchend', this.stopTouchDrag)
|
||||||
|
|
||||||
|
if (this.isDragged) {
|
||||||
|
this.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isDragged = 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)
|
||||||
@@ -335,6 +443,33 @@ export default {
|
|||||||
|
|
||||||
this.$emit('stop-resize')
|
this.$emit('stop-resize')
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
|
startTouchResize (e) {
|
||||||
|
this.selectedAreaRef.value = this.area
|
||||||
|
|
||||||
|
this.$refs?.name?.blur()
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.$el.getRootNode().addEventListener('touchmove', this.touchResize)
|
||||||
|
this.$el.getRootNode().addEventListener('touchend', this.stopTouchResize)
|
||||||
|
|
||||||
|
this.$emit('start-resize', 'nwse')
|
||||||
|
},
|
||||||
|
touchResize (e) {
|
||||||
|
const page = this.$parent.$refs.mask.previousSibling
|
||||||
|
const rect = page.getBoundingClientRect()
|
||||||
|
|
||||||
|
this.area.w = (e.touches[0].clientX - rect.left) / rect.width - this.area.x
|
||||||
|
this.area.h = (e.touches[0].clientY - rect.top) / rect.height - this.area.y
|
||||||
|
},
|
||||||
|
stopTouchResize () {
|
||||||
|
this.$el.getRootNode().removeEventListener('touchmove', this.touchResize)
|
||||||
|
this.$el.getRootNode().removeEventListener('touchend', this.stopTouchResize)
|
||||||
|
|
||||||
|
this.$emit('stop-resize')
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
style="max-width: 1600px"
|
style="max-width: 1600px"
|
||||||
class="mx-auto pl-4 h-full"
|
class="mx-auto pl-3 md:pl-4 h-full"
|
||||||
>
|
>
|
||||||
<div class="flex justify-between py-1.5 items-center pr-4">
|
<div
|
||||||
<div class="flex space-x-3">
|
v-if="$slots.buttons || withTitle"
|
||||||
|
class="flex justify-between py-1.5 items-center pr-4 top-0 z-10"
|
||||||
|
:class="{ sticky: withStickySubmitters || isBreakpointLg }"
|
||||||
|
:style="{ backgroundColor }"
|
||||||
|
>
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
<a
|
<a
|
||||||
v-if="withLogo"
|
v-if="withLogo"
|
||||||
href="/"
|
href="/"
|
||||||
@@ -12,8 +17,10 @@
|
|||||||
<Logo />
|
<Logo />
|
||||||
</a>
|
</a>
|
||||||
<Contenteditable
|
<Contenteditable
|
||||||
|
v-if="withTitle"
|
||||||
:model-value="template.name"
|
:model-value="template.name"
|
||||||
class="text-3xl font-semibold focus:text-clip"
|
:editable="editable"
|
||||||
|
class="text-xl md:text-3xl font-semibold focus:text-clip"
|
||||||
:icon-stroke-width="2.3"
|
:icon-stroke-width="2.3"
|
||||||
@update:model-value="updateName"
|
@update:model-value="updateName"
|
||||||
/>
|
/>
|
||||||
@@ -24,17 +31,33 @@
|
|||||||
name="buttons"
|
name="buttons"
|
||||||
/>
|
/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
<a
|
||||||
|
:href="template.submitters.length > 1 ? `/templates/${template.id}/submissions/new?selfsign=true` : `/d/${template.slug}`"
|
||||||
|
class="btn btn-primary btn-ghost text-base hidden md:flex"
|
||||||
|
:target="template.submitters.length > 1 ? '' : '_blank'"
|
||||||
|
:data-turbo-frame="template.submitters.length > 1 ? 'modal' : ''"
|
||||||
|
@click="maybeShowEmptyTemplateAlert"
|
||||||
|
>
|
||||||
|
<IconWritingSign
|
||||||
|
width="20"
|
||||||
|
class="inline"
|
||||||
|
/>
|
||||||
|
<span class="hidden md:inline">
|
||||||
|
{{ t('sign_yourself') }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
<a
|
<a
|
||||||
:href="`/templates/${template.id}/submissions/new`"
|
:href="`/templates/${template.id}/submissions/new`"
|
||||||
data-turbo-frame="modal"
|
data-turbo-frame="modal"
|
||||||
class="btn btn-primary text-base"
|
class="white-button md:!px-6"
|
||||||
|
@click="maybeShowEmptyTemplateAlert"
|
||||||
>
|
>
|
||||||
<IconUsersPlus
|
<IconUsersPlus
|
||||||
width="20"
|
width="20"
|
||||||
class="inline"
|
class="inline"
|
||||||
/>
|
/>
|
||||||
<span class="hidden md:inline">
|
<span class="hidden md:inline">
|
||||||
Recipients
|
{{ t('send') }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
@@ -53,7 +76,7 @@
|
|||||||
width="22"
|
width="22"
|
||||||
/>
|
/>
|
||||||
<span class="hidden md:inline">
|
<span class="hidden md:inline">
|
||||||
Save
|
{{ t('save') }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
@@ -61,9 +84,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex"
|
class="flex"
|
||||||
style="max-height: calc(100% - 60px)"
|
:class="$slots.buttons || withTitle ? 'md:max-h-[calc(100%_-_60px)]' : 'md:max-h-[100%]'"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
v-if="withDocumentsList"
|
||||||
ref="previews"
|
ref="previews"
|
||||||
:style="{ 'display': isBreakpointLg ? 'none' : 'initial' }"
|
:style="{ 'display': isBreakpointLg ? 'none' : 'initial' }"
|
||||||
class="overflow-y-auto overflow-x-hidden w-52 flex-none pr-3 mt-0.5 pt-0.5 hidden lg:block"
|
class="overflow-y-auto overflow-x-hidden w-52 flex-none pr-3 mt-0.5 pt-0.5 hidden lg:block"
|
||||||
@@ -75,6 +99,8 @@
|
|||||||
:item="item"
|
:item="item"
|
||||||
:document="sortedDocuments[index]"
|
:document="sortedDocuments[index]"
|
||||||
:accept-file-types="acceptFileTypes"
|
:accept-file-types="acceptFileTypes"
|
||||||
|
:with-replace-button="withUploadButton"
|
||||||
|
:editable="editable"
|
||||||
:template="template"
|
:template="template"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
@scroll-to="scrollIntoDocument(item)"
|
@scroll-to="scrollIntoDocument(item)"
|
||||||
@@ -86,10 +112,10 @@
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="sticky bottom-0 py-2"
|
class="sticky bottom-0 py-2"
|
||||||
:class="{ 'bg-base-100': withStickySubmitters }"
|
:style="withStickySubmitters ? { backgroundColor } : {}"
|
||||||
>
|
>
|
||||||
<Upload
|
<Upload
|
||||||
v-if="sortedDocuments.length"
|
v-if="sortedDocuments.length && editable && withUploadButton"
|
||||||
:accept-file-types="acceptFileTypes"
|
:accept-file-types="acceptFileTypes"
|
||||||
:template-id="template.id"
|
:template-id="template.id"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
@@ -97,13 +123,13 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5">
|
<div class="w-full overflow-y-hidden md:overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5">
|
||||||
<div
|
<div
|
||||||
ref="documents"
|
ref="documents"
|
||||||
class="pr-3.5 pl-0.5"
|
class="pr-3.5 pl-0.5"
|
||||||
>
|
>
|
||||||
<Dropzone
|
<Dropzone
|
||||||
v-if="!sortedDocuments.length"
|
v-if="!sortedDocuments.length && withUploadButton"
|
||||||
:template-id="template.id"
|
:template-id="template.id"
|
||||||
:accept-file-types="acceptFileTypes"
|
:accept-file-types="acceptFileTypes"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
@@ -119,16 +145,21 @@
|
|||||||
:areas-index="fieldAreasIndex[document.uuid]"
|
:areas-index="fieldAreasIndex[document.uuid]"
|
||||||
:selected-submitter="selectedSubmitter"
|
:selected-submitter="selectedSubmitter"
|
||||||
:document="document"
|
:document="document"
|
||||||
:is-drag="!!dragFieldType"
|
:is-drag="!!dragField"
|
||||||
|
:default-fields="defaultFields"
|
||||||
|
:allow-draw="!onlyDefinedFields"
|
||||||
:draw-field="drawField"
|
:draw-field="drawField"
|
||||||
|
:editable="editable"
|
||||||
|
:base-url="baseUrl"
|
||||||
@draw="onDraw"
|
@draw="onDraw"
|
||||||
@drop-field="onDropfield"
|
@drop-field="onDropfield"
|
||||||
@remove-area="removeArea"
|
@remove-area="removeArea"
|
||||||
/>
|
/>
|
||||||
<DocumentControls
|
<DocumentControls
|
||||||
v-if="isBreakpointLg"
|
v-if="isBreakpointLg && editable"
|
||||||
:with-arrows="template.schema.length > 1"
|
:with-arrows="template.schema.length > 1"
|
||||||
:item="template.schema.find((item) => item.attachment_uuid === document.uuid)"
|
:item="template.schema.find((item) => item.attachment_uuid === document.uuid)"
|
||||||
|
:with-replace-button="withUploadButton"
|
||||||
:document="document"
|
:document="document"
|
||||||
:template="template"
|
:template="template"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
@@ -141,10 +172,11 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<div
|
<div
|
||||||
v-if="sortedDocuments.length && isBreakpointLg"
|
v-if="sortedDocuments.length && isBreakpointLg && editable"
|
||||||
class="pb-4"
|
class="pb-4"
|
||||||
>
|
>
|
||||||
<Upload
|
<Upload
|
||||||
|
v-if="withUploadButton"
|
||||||
:template-id="template.id"
|
:template-id="template.id"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
@success="updateFromUpload"
|
@success="updateFromUpload"
|
||||||
@@ -152,38 +184,55 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<MobileDrawField
|
||||||
v-if="sortedDocuments.length"
|
v-if="drawField && isBreakpointLg"
|
||||||
class="sticky md:hidden"
|
:draw-field="drawField"
|
||||||
style="bottom: 100px"
|
:fields="template.fields"
|
||||||
>
|
:submitters="template.submitters"
|
||||||
<div class="px-4 py-3 rounded-2xl bg-base-200 flex items-center justify-between ml-4 mr-6">
|
:selected-submitter="selectedSubmitter"
|
||||||
<span class="w-full text-center text-lg">
|
class="md:hidden"
|
||||||
You need a larger screen to use builder tools.
|
:editable="editable"
|
||||||
</span>
|
@cancel="[drawField = null, drawOption = null]"
|
||||||
</div>
|
@change-submitter="[selectedSubmitter = $event, drawField.submitter_uuid = $event.uuid]"
|
||||||
</div>
|
/>
|
||||||
|
<MobileFields
|
||||||
|
v-if="sortedDocuments.length && !drawField && editable"
|
||||||
|
:fields="template.fields"
|
||||||
|
:default-fields="defaultFields"
|
||||||
|
:selected-submitter="selectedSubmitter"
|
||||||
|
@select="startFieldDraw($event)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
v-if="withFieldsList"
|
||||||
class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block"
|
class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block"
|
||||||
:class="drawField ? 'overflow-hidden' : 'overflow-auto'"
|
:class="drawField ? 'overflow-hidden' : 'overflow-y-auto overflow-x-hidden'"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="drawField"
|
v-if="drawField"
|
||||||
class="sticky inset-0 bg-base-100 h-full"
|
class="sticky inset-0 h-full z-20"
|
||||||
|
:style="{ backgroundColor }"
|
||||||
>
|
>
|
||||||
<div class="bg-base-300 rounded-lg p-5 text-center space-y-4">
|
<div class="bg-base-300 rounded-lg p-5 text-center space-y-4">
|
||||||
<p>
|
<p>
|
||||||
Draw {{ drawField.name }} field on the document
|
{{ t('draw_field_on_the_document').replace('{field}', drawField.name) }}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<div>
|
||||||
<button
|
<button
|
||||||
class="base-button"
|
class="base-button"
|
||||||
@click="drawField = null"
|
@click="clearDrawField"
|
||||||
>
|
>
|
||||||
Cancel
|
{{ t('cancel') }}
|
||||||
</button>
|
</button>
|
||||||
</p>
|
<a
|
||||||
|
v-if="!drawOption && !drawField.areas.length && !['stamp', 'signature', 'initials'].includes(drawField.type)"
|
||||||
|
href="#"
|
||||||
|
class="link block mt-3 text-sm"
|
||||||
|
@click.prevent="[drawField = null, drawOption = null]"
|
||||||
|
>
|
||||||
|
{{ t('or_add_field_without_drawing') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -192,11 +241,16 @@
|
|||||||
:fields="template.fields"
|
:fields="template.fields"
|
||||||
:submitters="template.submitters"
|
:submitters="template.submitters"
|
||||||
:selected-submitter="selectedSubmitter"
|
:selected-submitter="selectedSubmitter"
|
||||||
|
:with-help="withHelp"
|
||||||
|
:default-submitters="defaultSubmitters"
|
||||||
|
:default-fields="defaultFields"
|
||||||
:with-sticky-submitters="withStickySubmitters"
|
:with-sticky-submitters="withStickySubmitters"
|
||||||
@set-draw="drawField = $event"
|
:only-defined-fields="onlyDefinedFields"
|
||||||
@set-drag="dragFieldType = $event"
|
:editable="editable"
|
||||||
|
@set-draw="[drawField = $event.field, drawOption = $event.option]"
|
||||||
|
@set-drag="dragField = $event"
|
||||||
@change-submitter="selectedSubmitter = $event"
|
@change-submitter="selectedSubmitter = $event"
|
||||||
@drag-end="dragFieldType = null"
|
@drag-end="dragField = null"
|
||||||
@scroll-to-area="scrollToArea"
|
@scroll-to-area="scrollToArea"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -209,14 +263,17 @@
|
|||||||
import Upload from './upload'
|
import Upload from './upload'
|
||||||
import Dropzone from './dropzone'
|
import Dropzone from './dropzone'
|
||||||
import Fields from './fields'
|
import Fields from './fields'
|
||||||
|
import MobileDrawField from './mobile_draw_field'
|
||||||
import Document from './document'
|
import Document from './document'
|
||||||
import Logo from './logo'
|
import Logo from './logo'
|
||||||
import Contenteditable from './contenteditable'
|
import Contenteditable from './contenteditable'
|
||||||
import DocumentPreview from './preview'
|
import DocumentPreview from './preview'
|
||||||
import DocumentControls from './controls'
|
import DocumentControls from './controls'
|
||||||
import { IconUsersPlus, IconDeviceFloppy, IconInnerShadowTop } from '@tabler/icons-vue'
|
import MobileFields from './mobile_fields'
|
||||||
|
import { IconUsersPlus, IconDeviceFloppy, IconWritingSign, IconInnerShadowTop } from '@tabler/icons-vue'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
import { en as i18nEn } from './i18n'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TemplateBuilder',
|
name: 'TemplateBuilder',
|
||||||
@@ -224,6 +281,9 @@ export default {
|
|||||||
Upload,
|
Upload,
|
||||||
Document,
|
Document,
|
||||||
Fields,
|
Fields,
|
||||||
|
MobileDrawField,
|
||||||
|
IconWritingSign,
|
||||||
|
MobileFields,
|
||||||
Logo,
|
Logo,
|
||||||
Dropzone,
|
Dropzone,
|
||||||
DocumentPreview,
|
DocumentPreview,
|
||||||
@@ -237,9 +297,11 @@ export default {
|
|||||||
return {
|
return {
|
||||||
template: this.template,
|
template: this.template,
|
||||||
save: this.save,
|
save: this.save,
|
||||||
|
t: this.t,
|
||||||
baseFetch: this.baseFetch,
|
baseFetch: this.baseFetch,
|
||||||
backgroundColor: this.backgroundColor,
|
backgroundColor: this.backgroundColor,
|
||||||
withPhone: this.withPhone,
|
withPhone: this.withPhone,
|
||||||
|
withPayment: this.withPayment,
|
||||||
selectedAreaRef: computed(() => this.selectedAreaRef)
|
selectedAreaRef: computed(() => this.selectedAreaRef)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -253,11 +315,41 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
i18n: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
backgroundColor: {
|
backgroundColor: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
withHelp: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
autosave: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
defaultFields: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
defaultSubmitters: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
acceptFileTypes: {
|
acceptFileTypes: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -273,16 +365,53 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
onUpload: {
|
||||||
|
type: Function,
|
||||||
|
required: false,
|
||||||
|
default () {
|
||||||
|
return () => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
withStickySubmitters: {
|
withStickySubmitters: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
withUploadButton: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
withTitle: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
withFieldsList: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
withDocumentsList: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
withPhone: {
|
withPhone: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
withPayment: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
onlyDefinedFields: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -296,7 +425,8 @@ export default {
|
|||||||
isSaving: false,
|
isSaving: false,
|
||||||
selectedSubmitter: null,
|
selectedSubmitter: null,
|
||||||
drawField: null,
|
drawField: null,
|
||||||
dragFieldType: null
|
drawOption: null,
|
||||||
|
dragField: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -326,6 +456,22 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
|
const existingSubmittersUuids = this.defaultSubmitters.map((name) => {
|
||||||
|
return this.template.submitters.find(e => e.name === name)?.uuid
|
||||||
|
})
|
||||||
|
|
||||||
|
this.defaultSubmitters.forEach((name, index) => {
|
||||||
|
const submitter = (this.template.submitters[index] ||= {})
|
||||||
|
|
||||||
|
submitter.name = name
|
||||||
|
|
||||||
|
if (existingSubmittersUuids.filter(Boolean).length) {
|
||||||
|
submitter.uuid = existingSubmittersUuids[index] || v4()
|
||||||
|
} else {
|
||||||
|
submitter.uuid ||= v4()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
this.selectedSubmitter = this.template.submitters[0]
|
this.selectedSubmitter = this.template.submitters[0]
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
@@ -340,6 +486,12 @@ export default {
|
|||||||
window.addEventListener('keydown', this.onKeyDown)
|
window.addEventListener('keydown', this.onKeyDown)
|
||||||
|
|
||||||
window.addEventListener('resize', this.onWindowResize)
|
window.addEventListener('resize', this.onWindowResize)
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (document.location.search?.includes('stripe_connect_success')) {
|
||||||
|
document.querySelector('form[action="/auth/stripe_connect"]')?.closest('.dropdown')?.querySelector('label')?.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
unmounted () {
|
unmounted () {
|
||||||
document.removeEventListener('keyup', this.onKeyUp)
|
document.removeEventListener('keyup', this.onKeyUp)
|
||||||
@@ -351,6 +503,43 @@ export default {
|
|||||||
this.documentRefs = []
|
this.documentRefs = []
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
t (key) {
|
||||||
|
return this.i18n[key] || i18nEn[key] || key
|
||||||
|
},
|
||||||
|
startFieldDraw ({ name, type }) {
|
||||||
|
const existingField = this.template.fields?.find((f) => f.submitter_uuid === this.selectedSubmitter.uuid && name && name === f.name)
|
||||||
|
|
||||||
|
if (existingField) {
|
||||||
|
this.drawField = existingField
|
||||||
|
} else {
|
||||||
|
const field = {
|
||||||
|
name: name || '',
|
||||||
|
uuid: v4(),
|
||||||
|
required: type !== 'checkbox',
|
||||||
|
areas: [],
|
||||||
|
submitter_uuid: this.selectedSubmitter.uuid,
|
||||||
|
type
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['select', 'multiple', 'radio'].includes(type)) {
|
||||||
|
field.options = [{ value: '', uuid: v4() }]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'stamp') {
|
||||||
|
field.readonly = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'date') {
|
||||||
|
field.preferences = {
|
||||||
|
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.drawField = field
|
||||||
|
}
|
||||||
|
|
||||||
|
this.drawOption = null
|
||||||
|
},
|
||||||
undo () {
|
undo () {
|
||||||
if (this.undoStack.length > 1) {
|
if (this.undoStack.length > 1) {
|
||||||
this.undoStack.pop()
|
this.undoStack.pop()
|
||||||
@@ -396,13 +585,25 @@ export default {
|
|||||||
|
|
||||||
ref.$el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
ref.$el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||||
},
|
},
|
||||||
|
clearDrawField () {
|
||||||
|
if (this.drawField && !this.drawOption && this.drawField.areas.length === 0) {
|
||||||
|
const fieldIndex = this.template.fields.indexOf(this.drawField)
|
||||||
|
|
||||||
|
if (fieldIndex !== -1) {
|
||||||
|
this.template.fields.splice(fieldIndex, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.drawField = null
|
||||||
|
this.drawOption = null
|
||||||
|
},
|
||||||
onKeyUp (e) {
|
onKeyUp (e) {
|
||||||
if (e.code === 'Escape') {
|
if (e.code === 'Escape') {
|
||||||
this.drawField = null
|
this.clearDrawField()
|
||||||
|
|
||||||
this.selectedAreaRef.value = null
|
this.selectedAreaRef.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['Backspace', 'Delete'].includes(e.key) && this.selectedAreaRef.value && document.activeElement === document.body) {
|
if (this.editable && ['Backspace', 'Delete'].includes(e.key) && this.selectedAreaRef.value && document.activeElement === document.body) {
|
||||||
this.removeArea(this.selectedAreaRef.value)
|
this.removeArea(this.selectedAreaRef.value)
|
||||||
|
|
||||||
this.selectedAreaRef.value = null
|
this.selectedAreaRef.value = null
|
||||||
@@ -445,10 +646,69 @@ export default {
|
|||||||
},
|
},
|
||||||
onDraw (area) {
|
onDraw (area) {
|
||||||
if (this.drawField) {
|
if (this.drawField) {
|
||||||
|
if (this.drawOption) {
|
||||||
|
const areaWithoutOption = this.drawField.areas?.find((a) => !a.option_uuid)
|
||||||
|
|
||||||
|
if (areaWithoutOption && !this.drawField.areas.find((a) => a.option_uuid === this.drawField.options[0].uuid)) {
|
||||||
|
areaWithoutOption.option_uuid = this.drawField.options[0].uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
area.option_uuid = this.drawOption.uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
if (area.w === 0 || area.h === 0) {
|
||||||
|
const previousArea = this.drawField.areas?.[this.drawField.areas.length - 1]
|
||||||
|
|
||||||
|
if (this.selectedField?.type === this.drawField.type) {
|
||||||
|
area.w = this.selectedAreaRef.value.w
|
||||||
|
area.h = this.selectedAreaRef.value.h
|
||||||
|
} else if (previousArea) {
|
||||||
|
area.w = previousArea.w
|
||||||
|
area.h = previousArea.h
|
||||||
|
} else {
|
||||||
|
const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid)
|
||||||
|
const pageMask = documentRef.pageRefs[area.page].$refs.mask
|
||||||
|
|
||||||
|
if (this.drawField.type === 'checkbox' || this.drawOption) {
|
||||||
|
area.w = pageMask.clientWidth / 30 / pageMask.clientWidth
|
||||||
|
area.h = (pageMask.clientWidth / 30 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight)
|
||||||
|
} else if (this.drawField.type === 'image') {
|
||||||
|
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
|
||||||
|
area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight)
|
||||||
|
} else if (this.drawField.type === 'signature' || this.drawField.type === 'stamp') {
|
||||||
|
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
|
||||||
|
area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight) / 2
|
||||||
|
} else if (this.drawField.type === 'initials') {
|
||||||
|
area.w = pageMask.clientWidth / 10 / pageMask.clientWidth
|
||||||
|
area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth)
|
||||||
|
} else {
|
||||||
|
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
|
||||||
|
area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
area.x -= area.w / 2
|
||||||
|
area.y -= area.h / 2
|
||||||
|
}
|
||||||
|
|
||||||
this.drawField.areas ||= []
|
this.drawField.areas ||= []
|
||||||
this.drawField.areas.push(area)
|
|
||||||
|
const insertBeforeAreaIndex = this.drawField.areas.findIndex((a) => {
|
||||||
|
return a.attachment_uuid === area.attachment_uuid && a.page > area.page
|
||||||
|
})
|
||||||
|
|
||||||
|
if (insertBeforeAreaIndex !== -1) {
|
||||||
|
this.drawField.areas.splice(insertBeforeAreaIndex, 0, area)
|
||||||
|
} else {
|
||||||
|
this.drawField.areas.push(area)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.template.fields.indexOf(this.drawField) === -1) {
|
||||||
|
this.template.fields.push(this.drawField)
|
||||||
|
}
|
||||||
|
|
||||||
this.drawField = null
|
this.drawField = null
|
||||||
|
this.drawOption = null
|
||||||
|
|
||||||
this.selectedAreaRef.value = area
|
this.selectedAreaRef.value = area
|
||||||
|
|
||||||
@@ -498,14 +758,24 @@ export default {
|
|||||||
onDropfield (area) {
|
onDropfield (area) {
|
||||||
const field = {
|
const field = {
|
||||||
name: '',
|
name: '',
|
||||||
type: this.dragFieldType,
|
|
||||||
uuid: v4(),
|
uuid: v4(),
|
||||||
submitter_uuid: this.selectedSubmitter.uuid,
|
submitter_uuid: this.selectedSubmitter.uuid,
|
||||||
required: this.dragFieldType !== 'checkbox'
|
required: this.dragField.type !== 'checkbox',
|
||||||
|
...this.dragField
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['select', 'multiple', 'radio'].includes(this.dragFieldType)) {
|
if (['select', 'multiple', 'radio'].includes(field.type)) {
|
||||||
field.options = ['']
|
field.options = [{ value: '', uuid: v4() }]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.type === 'stamp') {
|
||||||
|
field.readonly = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.type === 'date') {
|
||||||
|
field.preferences = {
|
||||||
|
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fieldArea = {
|
const fieldArea = {
|
||||||
@@ -519,27 +789,27 @@ export default {
|
|||||||
|
|
||||||
let baseArea
|
let baseArea
|
||||||
|
|
||||||
if (this.selectedField?.type === this.dragFieldType) {
|
if (this.selectedField?.type === field.type) {
|
||||||
baseArea = this.selectedAreaRef.value
|
baseArea = this.selectedAreaRef.value
|
||||||
} else if (previousField?.areas?.length) {
|
} else if (previousField?.areas?.length) {
|
||||||
baseArea = previousField.areas[previousField.areas.length - 1]
|
baseArea = previousField.areas[previousField.areas.length - 1]
|
||||||
} else {
|
} else {
|
||||||
if (['checkbox'].includes(this.dragFieldType)) {
|
if (['checkbox'].includes(field.type)) {
|
||||||
baseArea = {
|
baseArea = {
|
||||||
w: area.maskW / 30 / area.maskW,
|
w: area.maskW / 30 / area.maskW,
|
||||||
h: area.maskW / 30 / area.maskW * (area.maskW / area.maskH)
|
h: area.maskW / 30 / area.maskW * (area.maskW / area.maskH)
|
||||||
}
|
}
|
||||||
} else if (this.dragFieldType === 'image') {
|
} else if (field.type === 'image') {
|
||||||
baseArea = {
|
baseArea = {
|
||||||
w: area.maskW / 5 / area.maskW,
|
w: area.maskW / 5 / area.maskW,
|
||||||
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH)
|
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH)
|
||||||
}
|
}
|
||||||
} else if (this.dragFieldType === 'signature') {
|
} else if (field.type === 'signature' || field.type === 'stamp') {
|
||||||
baseArea = {
|
baseArea = {
|
||||||
w: area.maskW / 5 / area.maskW,
|
w: area.maskW / 5 / area.maskW,
|
||||||
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2
|
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2
|
||||||
}
|
}
|
||||||
} else if (this.dragFieldType === 'initials') {
|
} else if (field.type === 'initials') {
|
||||||
baseArea = {
|
baseArea = {
|
||||||
w: area.maskW / 10 / area.maskW,
|
w: area.maskW / 10 / area.maskW,
|
||||||
h: area.maskW / 35 / area.maskW
|
h: area.maskW / 35 / area.maskW
|
||||||
@@ -556,7 +826,7 @@ export default {
|
|||||||
fieldArea.h = baseArea.h
|
fieldArea.h = baseArea.h
|
||||||
fieldArea.y = fieldArea.y - baseArea.h / 2
|
fieldArea.y = fieldArea.y - baseArea.h / 2
|
||||||
|
|
||||||
if (this.dragFieldType === 'cells') {
|
if (field.type === 'cells') {
|
||||||
fieldArea.cell_w = baseArea.cell_w || (baseArea.w / 5)
|
fieldArea.cell_w = baseArea.cell_w || (baseArea.w / 5)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -578,6 +848,14 @@ export default {
|
|||||||
this.scrollIntoDocument(schema[0])
|
this.scrollIntoDocument(schema[0])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (this.template.name === 'New Document') {
|
||||||
|
this.template.name = this.template.schema[0].name
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.onUpload) {
|
||||||
|
this.onUpload(this.template)
|
||||||
|
}
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
updateName (value) {
|
updateName (value) {
|
||||||
@@ -611,6 +889,10 @@ export default {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (this.onUpload) {
|
||||||
|
this.onUpload(this.template)
|
||||||
|
}
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
moveDocument (item, direction) {
|
moveDocument (item, direction) {
|
||||||
@@ -628,14 +910,25 @@ export default {
|
|||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
onSaveClick () {
|
maybeShowEmptyTemplateAlert (e) {
|
||||||
this.isSaving = true
|
if (!this.template.fields.length) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
this.save().then(() => {
|
alert('Please draw fields to prepare the document.')
|
||||||
window.Turbo.visit(`/templates/${this.template.id}`)
|
}
|
||||||
}).finally(() => {
|
},
|
||||||
this.isSaving = false
|
onSaveClick () {
|
||||||
})
|
if (this.template.fields.length) {
|
||||||
|
this.isSaving = true
|
||||||
|
|
||||||
|
this.save().then(() => {
|
||||||
|
window.Turbo.visit(`/templates/${this.template.id}`)
|
||||||
|
}).finally(() => {
|
||||||
|
this.isSaving = false
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
alert('Please draw fields to prepare the document.')
|
||||||
|
}
|
||||||
},
|
},
|
||||||
scrollToArea (area) {
|
scrollToArea (area) {
|
||||||
const documentRef = this.documentRefs.find((a) => a.document.uuid === area.attachment_uuid)
|
const documentRef = this.documentRefs.find((a) => a.document.uuid === area.attachment_uuid)
|
||||||
@@ -650,11 +943,17 @@ export default {
|
|||||||
headers: { ...this.fetchOptions.headers, ...options.headers }
|
headers: { ...this.fetchOptions.headers, ...options.headers }
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
save () {
|
save ({ force } = { force: false }) {
|
||||||
if (this.$el.closest('template-builder')) {
|
if (!this.autosave && !force) {
|
||||||
this.$el.closest('template-builder').dataset.template = JSON.stringify(this.template)
|
return Promise.resolve({})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$el.closest('template-builder')) {
|
||||||
|
this.$el.closest('template-builder').dataset.template = JSON.stringify(this.template)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
this.pushUndo()
|
this.pushUndo()
|
||||||
|
|
||||||
return this.baseFetch(`/api/templates/${this.template.id}`, {
|
return this.baseFetch(`/api/templates/${this.template.id}`, {
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
ref="contenteditable"
|
ref="contenteditable"
|
||||||
contenteditable
|
dir="auto"
|
||||||
|
:contenteditable="editable"
|
||||||
style="min-width: 2px"
|
style="min-width: 2px"
|
||||||
:class="iconInline ? 'inline' : 'block'"
|
:class="iconInline ? 'inline' : 'block'"
|
||||||
class="peer outline-none focus:block"
|
class="peer outline-none focus:block"
|
||||||
@@ -24,10 +25,10 @@
|
|||||||
*
|
*
|
||||||
</span>
|
</span>
|
||||||
<IconPencil
|
<IconPencil
|
||||||
class="cursor-pointer flex-none opacity-0 group-hover/contenteditable:opacity-100 align-middle peer-focus:hidden"
|
class="cursor-pointer flex-none opacity-0 group-hover/contenteditable-container:opacity-100 group-hover/contenteditable:opacity-100 align-middle peer-focus:hidden"
|
||||||
:style="iconInline ? {} : { right: -(1.1 * iconWidth) + 'px' }"
|
:style="iconInline ? {} : { right: -(1.1 * iconWidth) + 'px' }"
|
||||||
title="Edit"
|
title="Edit"
|
||||||
:class="{ 'ml-1': !withRequired, 'absolute': !iconInline, 'inline align-bottom': iconInline }"
|
:class="{ invisible: !editable, 'ml-1': !withRequired, 'absolute': !iconInline, 'inline align-bottom': iconInline }"
|
||||||
:width="iconWidth"
|
:width="iconWidth"
|
||||||
:stroke-width="iconStrokeWidth"
|
:stroke-width="iconStrokeWidth"
|
||||||
@click="[focusContenteditable(), selectOnEditClick && selectContent()]"
|
@click="[focusContenteditable(), selectOnEditClick && selectContent()]"
|
||||||
@@ -69,6 +70,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
iconStrokeWidth: {
|
iconStrokeWidth: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
@update:model-value="onUpdateName"
|
@update:model-value="onUpdateName"
|
||||||
/>
|
/>
|
||||||
<ReplaceButton
|
<ReplaceButton
|
||||||
|
v-if="withReplaceButton"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
:template-id="template.id"
|
:template-id="template.id"
|
||||||
@click.stop
|
@click.stop
|
||||||
@@ -67,6 +68,11 @@ export default {
|
|||||||
required: true,
|
required: true,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
withReplaceButton: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
withArrows: {
|
withArrows: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -5,8 +5,11 @@
|
|||||||
:key="image.id"
|
:key="image.id"
|
||||||
:ref="setPageRefs"
|
:ref="setPageRefs"
|
||||||
:number="index"
|
:number="index"
|
||||||
|
:editable="editable"
|
||||||
:areas="areasIndex[index]"
|
:areas="areasIndex[index]"
|
||||||
|
:allow-draw="allowDraw"
|
||||||
:is-drag="isDrag"
|
:is-drag="isDrag"
|
||||||
|
:default-fields="defaultFields"
|
||||||
:draw-field="drawField"
|
:draw-field="drawField"
|
||||||
:selected-submitter="selectedSubmitter"
|
:selected-submitter="selectedSubmitter"
|
||||||
:image="image"
|
:image="image"
|
||||||
@@ -34,15 +37,35 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
},
|
},
|
||||||
|
defaultFields: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
allowDraw: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
selectedSubmitter: {
|
selectedSubmitter: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
drawField: {
|
drawField: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
baseUrl: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
isDrag: {
|
isDrag: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -56,8 +79,33 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
basePreviewUrl () {
|
||||||
|
if (this.baseUrl) {
|
||||||
|
return new URL(this.baseUrl).origin
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
numberOfPages () {
|
||||||
|
return this.document.metadata?.pdf?.number_of_pages || this.document.preview_images.length
|
||||||
|
},
|
||||||
sortedPreviewImages () {
|
sortedPreviewImages () {
|
||||||
return [...this.document.preview_images].sort((a, b) => parseInt(a.filename) - parseInt(b.filename))
|
const lazyloadMetadata = this.document.preview_images[this.document.preview_images.length - 1].metadata
|
||||||
|
|
||||||
|
return [...Array(this.numberOfPages).keys()].map((i) => {
|
||||||
|
return this.previewImagesIndex[i] || {
|
||||||
|
metadata: lazyloadMetadata,
|
||||||
|
id: Math.random().toString(),
|
||||||
|
url: this.basePreviewUrl + `/preview/${this.document.uuid}/${i}.jpg`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
previewImagesIndex () {
|
||||||
|
return this.document.preview_images.reduce((acc, e) => {
|
||||||
|
acc[parseInt(e.filename)] = e
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeUpdate () {
|
beforeUpdate () {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
{{ message }}
|
{{ message }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<span class="font-medium">Click to upload</span> or drag and drop files
|
<span class="font-medium">{{ t('click_to_upload') }}</span> {{ t('or_drag_and_drop_files') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -61,7 +61,7 @@ export default {
|
|||||||
IconCloudUpload,
|
IconCloudUpload,
|
||||||
IconInnerShadowTop
|
IconInnerShadowTop
|
||||||
},
|
},
|
||||||
inject: ['baseFetch'],
|
inject: ['baseFetch', 't'],
|
||||||
props: {
|
props: {
|
||||||
templateId: {
|
templateId: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
@@ -91,13 +91,13 @@ export default {
|
|||||||
},
|
},
|
||||||
message () {
|
message () {
|
||||||
if (this.isLoading) {
|
if (this.isLoading) {
|
||||||
return 'Uploading...'
|
return this.t('uploading')
|
||||||
} else if (this.isProcessing) {
|
} else if (this.isProcessing) {
|
||||||
return 'Processing...'
|
return this.t('processing_')
|
||||||
} else if (this.acceptFileTypes === 'image/*, application/pdf') {
|
} else if (this.acceptFileTypes === 'image/*, application/pdf') {
|
||||||
return 'Add PDF documents or images'
|
return this.t('add_pdf_documents_or_images')
|
||||||
} else {
|
} else {
|
||||||
return 'Add documents or images'
|
return this.t('add_documents_or_images')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="border border-base-300 rounded rounded-tr-none relative group"
|
class="border border-base-300 rounded rounded-tr-none relative group"
|
||||||
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between relative">
|
<div class="flex items-center justify-between relative group/contenteditable-container">
|
||||||
<div
|
<div
|
||||||
class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer"
|
class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer"
|
||||||
@click="scrollToFirstArea"
|
@click="scrollToFirstArea"
|
||||||
@@ -13,6 +14,7 @@
|
|||||||
<div class="flex items-center p-1 space-x-1">
|
<div class="flex items-center p-1 space-x-1">
|
||||||
<FieldType
|
<FieldType
|
||||||
v-model="field.type"
|
v-model="field.type"
|
||||||
|
:editable="editable && !defaultField"
|
||||||
:button-width="20"
|
:button-width="20"
|
||||||
@update:model-value="[maybeUpdateOptions(), save()]"
|
@update:model-value="[maybeUpdateOptions(), save()]"
|
||||||
@click="scrollToFirstArea"
|
@click="scrollToFirstArea"
|
||||||
@@ -20,6 +22,7 @@
|
|||||||
<Contenteditable
|
<Contenteditable
|
||||||
ref="name"
|
ref="name"
|
||||||
:model-value="field.name || defaultName"
|
:model-value="field.name || defaultName"
|
||||||
|
:editable="editable && !defaultField"
|
||||||
:icon-inline="true"
|
:icon-inline="true"
|
||||||
:icon-width="18"
|
:icon-width="18"
|
||||||
:icon-stroke-width="1.6"
|
:icon-stroke-width="1.6"
|
||||||
@@ -31,7 +34,7 @@
|
|||||||
v-if="isNameFocus"
|
v-if="isNameFocus"
|
||||||
class="flex items-center relative"
|
class="flex items-center relative"
|
||||||
>
|
>
|
||||||
<template v-if="field.type !== 'checkbox'">
|
<template v-if="field.type != 'phone'">
|
||||||
<input
|
<input
|
||||||
:id="`required-checkbox-${field.uuid}`"
|
:id="`required-checkbox-${field.uuid}`"
|
||||||
v-model="field.required"
|
v-model="field.required"
|
||||||
@@ -44,32 +47,130 @@
|
|||||||
class="label text-xs"
|
class="label text-xs"
|
||||||
@click.prevent="field.required = !field.required"
|
@click.prevent="field.required = !field.required"
|
||||||
@mousedown.prevent
|
@mousedown.prevent
|
||||||
>Required</label>
|
>{{ t('required') }}</label>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else-if="editable"
|
||||||
class="flex items-center space-x-1"
|
class="flex items-center space-x-1"
|
||||||
>
|
>
|
||||||
|
<button
|
||||||
|
v-if="field && !field.areas.length"
|
||||||
|
title="Draw"
|
||||||
|
class="relative cursor-pointer text-transparent group-hover:text-base-content"
|
||||||
|
@click="$emit('set-draw', { field })"
|
||||||
|
>
|
||||||
|
<IconNewSection
|
||||||
|
:width="18"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<PaymentSettings
|
||||||
|
v-if="field.type === 'payment'"
|
||||||
|
:field="field"
|
||||||
|
/>
|
||||||
<span
|
<span
|
||||||
v-if="field.areas?.length"
|
v-else
|
||||||
class="dropdown dropdown-end"
|
class="dropdown dropdown-end"
|
||||||
|
@mouseenter="renderDropdown = true"
|
||||||
|
@touchstart="renderDropdown = true"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
title="Areas"
|
title="Settings"
|
||||||
class="cursor-pointer text-transparent group-hover:text-base-content"
|
class="cursor-pointer text-transparent group-hover:text-base-content"
|
||||||
>
|
>
|
||||||
<IconShape
|
<IconSettings
|
||||||
:width="18"
|
:width="18"
|
||||||
:stroke-width="1.6"
|
:stroke-width="1.6"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<ul
|
<ul
|
||||||
|
v-if="renderDropdown"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="mt-1.5 dropdown-content menu menu-xs p-2 shadow bg-base-100 rounded-box w-52 z-10"
|
class="mt-1.5 dropdown-content menu menu-xs p-2 shadow bg-base-100 rounded-box w-52 z-10"
|
||||||
|
draggable="true"
|
||||||
|
@dragstart.prevent.stop
|
||||||
@click="closeDropdown"
|
@click="closeDropdown"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
v-if="field.type === 'text' && !defaultField"
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="field.default_value"
|
||||||
|
type="text"
|
||||||
|
:placeholder="t('default_value')"
|
||||||
|
dir="auto"
|
||||||
|
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
|
||||||
|
@blur="save"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
v-if="field.default_value"
|
||||||
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>
|
||||||
|
{{ t('default_value') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="field.type === 'date'"
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
v-model="field.preferences.format"
|
||||||
|
:placeholder="t('format')"
|
||||||
|
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0"
|
||||||
|
@change="save"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="format in dateFormats"
|
||||||
|
:key="format"
|
||||||
|
:value="format"
|
||||||
|
>
|
||||||
|
{{ formatDate(new Date(), format) }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<label
|
||||||
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>
|
||||||
|
{{ t('format') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<li
|
||||||
|
v-if="field.type != 'phone'"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<label class="cursor-pointer py-1.5">
|
||||||
|
<input
|
||||||
|
v-model="field.required"
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-xs"
|
||||||
|
@update:model-value="save"
|
||||||
|
>
|
||||||
|
<span class="label-text">{{ t('required') }}</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
v-if="field.type === 'text' && !defaultField"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<label class="cursor-pointer py-1.5">
|
||||||
|
<input
|
||||||
|
v-model="field.readonly"
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-xs"
|
||||||
|
@update:model-value="save"
|
||||||
|
>
|
||||||
|
<span class="label-text">{{ t('read_only') }}</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<hr class="pb-0.5 mt-0.5">
|
||||||
<li
|
<li
|
||||||
v-for="(area, index) in field.areas || []"
|
v-for="(area, index) in field.areas || []"
|
||||||
:key="index"
|
:key="index"
|
||||||
@@ -83,20 +184,20 @@
|
|||||||
:width="20"
|
:width="20"
|
||||||
:stroke-width="1.6"
|
:stroke-width="1.6"
|
||||||
/>
|
/>
|
||||||
Page {{ area.page + 1 }}
|
{{ t('page') }} {{ area.page + 1 }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li v-if="!field.areas?.length || !['radio', 'multiple'].includes(field.type)">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="text-sm py-1 px-2"
|
class="text-sm py-1 px-2"
|
||||||
@click.prevent="$emit('set-draw', field)"
|
@click.prevent="$emit('set-draw', { field })"
|
||||||
>
|
>
|
||||||
<IconNewSection
|
<IconNewSection
|
||||||
:width="20"
|
:width="20"
|
||||||
:stroke-width="1.6"
|
:stroke-width="1.6"
|
||||||
/>
|
/>
|
||||||
Draw New Area
|
{{ t('draw_new_area') }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells'].includes(field.type)">
|
<li v-if="field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells'].includes(field.type)">
|
||||||
@@ -109,24 +210,13 @@
|
|||||||
:width="20"
|
:width="20"
|
||||||
:stroke-width="1.6"
|
:stroke-width="1.6"
|
||||||
/>
|
/>
|
||||||
Copy to All Pages
|
{{ t('copy_to_all_pages') }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
v-else
|
class="relative text-transparent group-hover:text-base-content pr-1"
|
||||||
title="Areas"
|
|
||||||
class="relative cursor-pointer text-transparent group-hover:text-base-content"
|
|
||||||
@click="$emit('set-draw', field)"
|
|
||||||
>
|
|
||||||
<IconShape
|
|
||||||
:width="18"
|
|
||||||
:stroke-width="1.6"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="relative text-transparent group-hover:text-base-content"
|
|
||||||
title="Remove"
|
title="Remove"
|
||||||
@click="$emit('remove', field)"
|
@click="$emit('remove', field)"
|
||||||
>
|
>
|
||||||
@@ -135,48 +225,62 @@
|
|||||||
:stroke-width="1.6"
|
:stroke-width="1.6"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<div class="flex flex-col pr-1 text-transparent group-hover:text-base-content">
|
|
||||||
<button
|
|
||||||
title="Up"
|
|
||||||
class="relative"
|
|
||||||
style="font-size: 10px; margin-bottom: -2px"
|
|
||||||
@click="$emit('move-up')"
|
|
||||||
>
|
|
||||||
▲
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
title="Down"
|
|
||||||
class="relative"
|
|
||||||
style="font-size: 10px; margin-top: -2px"
|
|
||||||
@click="$emit('move-down')"
|
|
||||||
>
|
|
||||||
▼
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="field.options"
|
v-if="field.options"
|
||||||
|
ref="options"
|
||||||
class="border-t border-base-300 mx-2 pt-2 space-y-1.5"
|
class="border-t border-base-300 mx-2 pt-2 space-y-1.5"
|
||||||
|
draggable="true"
|
||||||
|
@dragstart.prevent.stop
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(option, index) in field.options"
|
v-for="(option, index) in field.options"
|
||||||
:key="index"
|
:key="option.uuid"
|
||||||
class="flex space-x-1.5 items-center"
|
class="flex space-x-1.5 items-center"
|
||||||
>
|
>
|
||||||
<span class="text-sm w-3.5">
|
<span class="text-sm w-3.5">
|
||||||
{{ index + 1 }}.
|
{{ index + 1 }}.
|
||||||
</span>
|
</span>
|
||||||
|
<div
|
||||||
|
v-if="['radio', 'multiple'].includes(field.type) && (index > 0 || field.areas.find((a) => a.option_uuid) || !field.areas.length) && !field.areas.find((a) => a.option_uuid === option.uuid)"
|
||||||
|
class="items-center flex w-full"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="option.value"
|
||||||
|
class="w-full input input-primary input-xs text-sm bg-transparent !pr-7 -mr-6"
|
||||||
|
type="text"
|
||||||
|
dir="auto"
|
||||||
|
required
|
||||||
|
:placeholder="`${t('option')} ${index + 1}`"
|
||||||
|
@blur="save"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
title="Draw"
|
||||||
|
tabindex="-1"
|
||||||
|
@click.prevent="$emit('set-draw', { field, option })"
|
||||||
|
>
|
||||||
|
<IconNewSection
|
||||||
|
:width="18"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
v-model="field.options[index]"
|
v-else
|
||||||
|
v-model="option.value"
|
||||||
class="w-full input input-primary input-xs text-sm bg-transparent"
|
class="w-full input input-primary input-xs text-sm bg-transparent"
|
||||||
|
:placeholder="`${t('option')} ${index + 1}`"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
|
dir="auto"
|
||||||
|
@focus="maybeFocusOnOptionArea(option)"
|
||||||
@blur="save"
|
@blur="save"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="text-sm w-3.5"
|
class="text-sm w-3.5"
|
||||||
@click="[field.options.splice(index, 1), save()]"
|
tabindex="-1"
|
||||||
|
@click="removeOption(option)"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
@@ -184,9 +288,9 @@
|
|||||||
<button
|
<button
|
||||||
v-if="field.options"
|
v-if="field.options"
|
||||||
class="text-center text-sm w-full pb-1"
|
class="text-center text-sm w-full pb-1"
|
||||||
@click="[field.options.push(''), save()]"
|
@click="addOption"
|
||||||
>
|
>
|
||||||
+ Add option
|
+ {{ t('add_option') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -196,52 +300,129 @@
|
|||||||
<script>
|
<script>
|
||||||
import Contenteditable from './contenteditable'
|
import Contenteditable from './contenteditable'
|
||||||
import FieldType from './field_type'
|
import FieldType from './field_type'
|
||||||
import { IconShape, IconNewSection, IconTrashX, IconCopy } from '@tabler/icons-vue'
|
import PaymentSettings from './payment_settings'
|
||||||
|
import { IconShape, IconNewSection, IconTrashX, IconCopy, IconSettings } from '@tabler/icons-vue'
|
||||||
|
import { v4 } from 'uuid'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TemplateField',
|
name: 'TemplateField',
|
||||||
components: {
|
components: {
|
||||||
Contenteditable,
|
Contenteditable,
|
||||||
|
IconSettings,
|
||||||
IconShape,
|
IconShape,
|
||||||
|
PaymentSettings,
|
||||||
IconNewSection,
|
IconNewSection,
|
||||||
IconTrashX,
|
IconTrashX,
|
||||||
IconCopy,
|
IconCopy,
|
||||||
FieldType
|
FieldType
|
||||||
},
|
},
|
||||||
inject: ['template', 'save'],
|
inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef', 't'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
defaultField: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['set-draw', 'remove', 'move-up', 'move-down', 'scroll-to'],
|
emits: ['set-draw', 'remove', 'scroll-to'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isNameFocus: false
|
isNameFocus: false,
|
||||||
|
showPaymentModal: false,
|
||||||
|
renderDropdown: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
fieldNames: FieldType.computed.fieldNames,
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
|
dateFormats () {
|
||||||
|
return [
|
||||||
|
'MM/DD/YYYY',
|
||||||
|
'DD/MM/YYYY',
|
||||||
|
'YYYY-MM-DD',
|
||||||
|
'DD-MM-YYYY',
|
||||||
|
'DD.MM.YYYY',
|
||||||
|
'MMM D, YYYY',
|
||||||
|
'MMMM D, YYYY',
|
||||||
|
'D MMM YYYY',
|
||||||
|
'D MMMM YYYY'
|
||||||
|
]
|
||||||
|
},
|
||||||
defaultName () {
|
defaultName () {
|
||||||
const typeIndex = this.template.fields.filter((f) => f.type === this.field.type).indexOf(this.field)
|
if (this.field.type === 'payment' && this.field.preferences?.price) {
|
||||||
|
const { price, currency } = this.field.preferences || {}
|
||||||
|
|
||||||
const suffix = { multiple: 'Select', radio: 'Group' }[this.field.type] || 'Field'
|
const formattedPrice = new Intl.NumberFormat([], {
|
||||||
|
style: 'currency',
|
||||||
|
currency
|
||||||
|
}).format(price)
|
||||||
|
|
||||||
return `${this.fieldNames[this.field.type]} ${suffix} ${typeIndex + 1}`
|
return `${this.fieldNames[this.field.type]} ${formattedPrice}`
|
||||||
|
} else {
|
||||||
|
const typeIndex = this.template.fields.filter((f) => f.type === this.field.type).indexOf(this.field)
|
||||||
|
|
||||||
|
const suffix = { multiple: this.t('select'), radio: this.t('group') }[this.field.type] || this.t('field')
|
||||||
|
|
||||||
|
return `${this.fieldNames[this.field.type]} ${suffix} ${typeIndex + 1}`
|
||||||
|
}
|
||||||
},
|
},
|
||||||
areas () {
|
areas () {
|
||||||
return this.field.areas || []
|
return this.field.areas || []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created () {
|
||||||
|
this.field.preferences ||= {}
|
||||||
|
|
||||||
|
if (this.field.type === 'date') {
|
||||||
|
this.field.preferences.format ||=
|
||||||
|
(Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
formatDate (date, format) {
|
||||||
|
const monthFormats = {
|
||||||
|
M: 'numeric',
|
||||||
|
MM: '2-digit',
|
||||||
|
MMM: 'short',
|
||||||
|
MMMM: 'long'
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayFormats = {
|
||||||
|
D: 'numeric',
|
||||||
|
DD: '2-digit'
|
||||||
|
}
|
||||||
|
|
||||||
|
const yearFormats = {
|
||||||
|
YYYY: 'numeric',
|
||||||
|
YY: '2-digit'
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = new Intl.DateTimeFormat([], {
|
||||||
|
day: dayFormats[format.match(/D+/)],
|
||||||
|
month: monthFormats[format.match(/M+/)],
|
||||||
|
year: yearFormats[format.match(/Y+/)]
|
||||||
|
}).formatToParts(date)
|
||||||
|
|
||||||
|
return format
|
||||||
|
.replace(/D+/, parts.find((p) => p.type === 'day').value)
|
||||||
|
.replace(/M+/, parts.find((p) => p.type === 'month').value)
|
||||||
|
.replace(/Y+/, parts.find((p) => p.type === 'year').value)
|
||||||
|
},
|
||||||
copyToAllPages (field) {
|
copyToAllPages (field) {
|
||||||
const areaString = JSON.stringify(field.areas[0])
|
const areaString = JSON.stringify(field.areas[0])
|
||||||
|
|
||||||
this.template.documents.forEach((attachment) => {
|
this.template.documents.forEach((attachment) => {
|
||||||
attachment.preview_images.forEach((page) => {
|
attachment.preview_images.forEach((page) => {
|
||||||
if (!field.areas.find((area) => area.attachment_uuid === attachment.uuid && area.page === parseInt(page.filename))) {
|
if (!field.areas.find((area) => area.attachment_uuid === attachment.uuid && area.page === parseInt(page.filename))) {
|
||||||
field.areas.push({ ...JSON.parse(areaString), page: parseInt(page.filename) })
|
field.areas.push({ ...JSON.parse(areaString), attachment_uuid: attachment.uuid, page: parseInt(page.filename) })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -261,19 +442,45 @@ export default {
|
|||||||
}, 1)
|
}, 1)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
maybeFocusOnOptionArea (option) {
|
||||||
|
const area = this.field.areas.find((a) => a.option_uuid === option.uuid)
|
||||||
|
|
||||||
|
if (area) {
|
||||||
|
this.selectedAreaRef.value = area
|
||||||
|
}
|
||||||
|
},
|
||||||
scrollToFirstArea () {
|
scrollToFirstArea () {
|
||||||
return this.field.areas?.[0] && this.$emit('scroll-to', this.field.areas[0])
|
return this.field.areas?.[0] && this.$emit('scroll-to', this.field.areas[0])
|
||||||
},
|
},
|
||||||
closeDropdown () {
|
closeDropdown () {
|
||||||
document.activeElement.blur()
|
document.activeElement.blur()
|
||||||
},
|
},
|
||||||
|
addOption () {
|
||||||
|
this.field.options.push({ value: '', uuid: v4() })
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const inputs = this.$refs.options.querySelectorAll('input')
|
||||||
|
|
||||||
|
inputs[inputs.length - 1]?.focus()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
|
removeOption (option) {
|
||||||
|
this.field.options.splice(this.field.options.indexOf(option), 1)
|
||||||
|
this.field.areas.splice(this.field.areas.findIndex((a) => a.option_uuid === option.uuid), 1)
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
maybeUpdateOptions () {
|
maybeUpdateOptions () {
|
||||||
|
delete this.field.default_value
|
||||||
|
|
||||||
if (!['radio', 'multiple', 'select'].includes(this.field.type)) {
|
if (!['radio', 'multiple', 'select'].includes(this.field.type)) {
|
||||||
delete this.field.options
|
delete this.field.options
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['radio', 'multiple', 'select'].includes(this.field.type)) {
|
if (['radio', 'multiple', 'select'].includes(this.field.type)) {
|
||||||
this.field.options ||= ['']
|
this.field.options ||= [{ value: '', uuid: v4() }]
|
||||||
}
|
}
|
||||||
|
|
||||||
(this.field.areas || []).forEach((area) => {
|
(this.field.areas || []).forEach((area) => {
|
||||||
|
|||||||
@@ -1,5 +1,97 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="dropdown">
|
<div
|
||||||
|
v-if="mobileView"
|
||||||
|
@mouseenter="renderDropdown = true"
|
||||||
|
@touchstart="renderDropdown = true"
|
||||||
|
>
|
||||||
|
<div class="flex space-x-2 items-end">
|
||||||
|
<div class="group/contenteditable-container bg-base-100 rounded-md p-2 border border-base-300 w-full flex justify-between items-end">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<span
|
||||||
|
class="w-3 h-3 flex-shrink-0 rounded-full"
|
||||||
|
:class="colors[submitters.indexOf(selectedSubmitter)]"
|
||||||
|
/>
|
||||||
|
<Contenteditable
|
||||||
|
v-model="selectedSubmitter.name"
|
||||||
|
class="cursor-text"
|
||||||
|
:icon-inline="true"
|
||||||
|
:editable="editable"
|
||||||
|
:select-on-edit-click="true"
|
||||||
|
:icon-width="18"
|
||||||
|
@update:model-value="$emit('name-change', selectedSubmitter)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown dropdown-top dropdown-end">
|
||||||
|
<label
|
||||||
|
tabindex="0"
|
||||||
|
class="bg-base-100 cursor-pointer rounded-md p-2 border border-base-300 w-full flex justify-center"
|
||||||
|
>
|
||||||
|
<IconChevronUp
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<ul
|
||||||
|
v-if="editable && renderDropdown"
|
||||||
|
tabindex="0"
|
||||||
|
class="rounded-md min-w-max mb-2"
|
||||||
|
:class="menuClasses"
|
||||||
|
@click="closeDropdown"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-for="(submitter, index) in submitters"
|
||||||
|
:key="submitter.uuid"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="flex px-2 group justify-between items-center"
|
||||||
|
:class="{ 'active': submitter === selectedSubmitter }"
|
||||||
|
@click.prevent="selectSubmitter(submitter)"
|
||||||
|
>
|
||||||
|
<span class="py-1 flex items-center">
|
||||||
|
<span
|
||||||
|
class="rounded-full w-3 h-3 ml-1 mr-3"
|
||||||
|
:class="colors[index]"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{{ submitter.name }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
v-if="submitters.length > 1 && editable"
|
||||||
|
class="px-2"
|
||||||
|
@click.stop="remove(submitter)"
|
||||||
|
>
|
||||||
|
<IconTrashX :width="18" />
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li v-if="submitters.length < 10 && editable">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="flex px-2"
|
||||||
|
@click.prevent="addSubmitter"
|
||||||
|
>
|
||||||
|
<IconUserPlus
|
||||||
|
:width="20"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
<span class="py-1">
|
||||||
|
{{ t('add') }} {{ names[submitters.length] }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="dropdown"
|
||||||
|
@mouseenter="renderDropdown = true"
|
||||||
|
@touchstart="renderDropdown = true"
|
||||||
|
>
|
||||||
<label
|
<label
|
||||||
v-if="compact"
|
v-if="compact"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -13,8 +105,9 @@
|
|||||||
</label>
|
</label>
|
||||||
<label
|
<label
|
||||||
v-else
|
v-else
|
||||||
|
ref="label"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="cursor-pointer rounded-md p-2 border border-base-300 w-full flex justify-between"
|
class="group cursor-pointer group/contenteditable-container rounded-md p-2 border border-base-300 hover:border-content w-full flex justify-between"
|
||||||
>
|
>
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<span
|
<span
|
||||||
@@ -25,12 +118,13 @@
|
|||||||
v-model="selectedSubmitter.name"
|
v-model="selectedSubmitter.name"
|
||||||
class="cursor-text"
|
class="cursor-text"
|
||||||
:icon-inline="true"
|
:icon-inline="true"
|
||||||
|
:editable="editable"
|
||||||
:select-on-edit-click="true"
|
:select-on-edit-click="true"
|
||||||
:icon-width="18"
|
:icon-width="18"
|
||||||
@update:model-value="$emit('name-change', selectedSubmitter)"
|
@update:model-value="$emit('name-change', selectedSubmitter)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span class="flex items-center">
|
<span class="flex items-center transition-all duration-75 group-hover:border border-base-content/20 border-dashed w-6 h-6 flex justify-center items-center rounded">
|
||||||
<IconPlus
|
<IconPlus
|
||||||
width="18"
|
width="18"
|
||||||
height="18"
|
height="18"
|
||||||
@@ -38,6 +132,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<ul
|
<ul
|
||||||
|
v-if="(editable || !compact) && renderDropdown"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
:class="menuClasses"
|
:class="menuClasses"
|
||||||
@click="closeDropdown"
|
@click="closeDropdown"
|
||||||
@@ -61,16 +156,39 @@
|
|||||||
{{ submitter.name }}
|
{{ submitter.name }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<div
|
||||||
v-if="!compact && submitters.length > 1"
|
v-if="!compact && submitters.length > 1 && editable"
|
||||||
class="hidden group-hover:block px-2"
|
class="flex"
|
||||||
@click.stop="remove(submitter)"
|
|
||||||
>
|
>
|
||||||
<IconTrashX :width="18" />
|
<div class="flex-col pr-1 hidden group-hover:flex -mt-1 h-0">
|
||||||
</button>
|
<button
|
||||||
|
title="Up"
|
||||||
|
class="relative w-2"
|
||||||
|
style="font-size: 10px; margin-bottom: -4px"
|
||||||
|
@click.stop="[move(submitter, -1), $refs.label.focus()] "
|
||||||
|
>
|
||||||
|
▲
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
title="Down"
|
||||||
|
class="relative w-2"
|
||||||
|
style="font-size: 10px; margin-top: -4px"
|
||||||
|
@click.stop="[move(submitter, 1), $refs.label.focus()] "
|
||||||
|
>
|
||||||
|
▼
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="!compact && submitters.length > 1 && editable"
|
||||||
|
class="hidden group-hover:block px-2"
|
||||||
|
@click.stop="remove(submitter)"
|
||||||
|
>
|
||||||
|
<IconTrashX :width="18" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="submitters.length < 5">
|
<li v-if="submitters.length < 10 && editable">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="flex px-2"
|
class="flex px-2"
|
||||||
@@ -81,7 +199,7 @@
|
|||||||
:stroke-width="1.6"
|
:stroke-width="1.6"
|
||||||
/>
|
/>
|
||||||
<span class="py-1">
|
<span class="py-1">
|
||||||
Add Submitter
|
{{ t('add') }} {{ names[submitters.length] }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -90,7 +208,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconUserPlus, IconTrashX, IconPlus } from '@tabler/icons-vue'
|
import { IconUserPlus, IconTrashX, IconPlus, IconChevronUp } from '@tabler/icons-vue'
|
||||||
import Contenteditable from './contenteditable'
|
import Contenteditable from './contenteditable'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
|
|
||||||
@@ -100,18 +218,30 @@ export default {
|
|||||||
IconUserPlus,
|
IconUserPlus,
|
||||||
Contenteditable,
|
Contenteditable,
|
||||||
IconPlus,
|
IconPlus,
|
||||||
IconTrashX
|
IconTrashX,
|
||||||
|
IconChevronUp
|
||||||
},
|
},
|
||||||
|
inject: ['t', 'save'],
|
||||||
props: {
|
props: {
|
||||||
submitters: {
|
submitters: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
compact: {
|
compact: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
mobileView: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
@@ -123,6 +253,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['update:model-value', 'remove', 'new-submitter', 'name-change'],
|
emits: ['update:model-value', 'remove', 'new-submitter', 'name-change'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
renderDropdown: false
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
colors () {
|
colors () {
|
||||||
return [
|
return [
|
||||||
@@ -130,16 +265,26 @@ export default {
|
|||||||
'bg-sky-500',
|
'bg-sky-500',
|
||||||
'bg-emerald-500',
|
'bg-emerald-500',
|
||||||
'bg-yellow-300',
|
'bg-yellow-300',
|
||||||
'bg-purple-600'
|
'bg-purple-600',
|
||||||
|
'bg-pink-500',
|
||||||
|
'bg-cyan-500',
|
||||||
|
'bg-orange-500',
|
||||||
|
'bg-lime-500',
|
||||||
|
'bg-indigo-500'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
names () {
|
names () {
|
||||||
return [
|
return [
|
||||||
'First Submitter',
|
this.t('first_party'),
|
||||||
'Second Submitter',
|
this.t('second_party'),
|
||||||
'Third Submitter',
|
this.t('third_party'),
|
||||||
'Fourth Submitter',
|
this.t('fourth_party'),
|
||||||
'Fifth Submitter'
|
this.t('fifth_party'),
|
||||||
|
this.t('sixth_party'),
|
||||||
|
this.t('seventh_party'),
|
||||||
|
this.t('eighth_party'),
|
||||||
|
this.t('ninth_party'),
|
||||||
|
this.t('tenth_party')
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
selectedSubmitter () {
|
selectedSubmitter () {
|
||||||
@@ -155,6 +300,23 @@ export default {
|
|||||||
this.$emit('remove', submitter)
|
this.$emit('remove', submitter)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
move (submitter, direction) {
|
||||||
|
const currentIndex = this.submitters.indexOf(submitter)
|
||||||
|
|
||||||
|
this.submitters.splice(currentIndex, 1)
|
||||||
|
|
||||||
|
if (currentIndex + direction > this.submitters.length) {
|
||||||
|
this.submitters.unshift(submitter)
|
||||||
|
} else if (currentIndex + direction < 0) {
|
||||||
|
this.submitters.push(submitter)
|
||||||
|
} else {
|
||||||
|
this.submitters.splice(currentIndex + direction, 0, submitter)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectSubmitter(submitter)
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
addSubmitter () {
|
addSubmitter () {
|
||||||
const newSubmitter = {
|
const newSubmitter = {
|
||||||
name: this.names[this.submitters.length],
|
name: this.names[this.submitters.length],
|
||||||
|
|||||||
@@ -1,20 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<span class="dropdown">
|
<span
|
||||||
<label
|
class="dropdown"
|
||||||
tabindex="0"
|
@mouseenter="renderDropdown = true"
|
||||||
:title="fieldNames[modelValue]"
|
@touchstart="renderDropdown = true"
|
||||||
class="cursor-pointer"
|
>
|
||||||
>
|
<slot>
|
||||||
<component
|
<label
|
||||||
:is="fieldIcons[modelValue]"
|
tabindex="0"
|
||||||
:width="buttonWidth"
|
:title="fieldNames[modelValue]"
|
||||||
:class="buttonClasses"
|
class="cursor-pointer"
|
||||||
:stroke-width="1.6"
|
>
|
||||||
/>
|
<component
|
||||||
</label>
|
:is="fieldIcons[modelValue]"
|
||||||
|
:width="buttonWidth"
|
||||||
|
:class="buttonClasses"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</slot>
|
||||||
<ul
|
<ul
|
||||||
|
v-if="editable && renderDropdown"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="dropdown-content menu menu-xs p-2 shadow rounded-box w-52 z-10"
|
class="dropdown-content menu menu-xs p-2 shadow rounded-box w-52 z-10 mb-3"
|
||||||
:class="menuClasses"
|
:class="menuClasses"
|
||||||
@click="closeDropdown"
|
@click="closeDropdown"
|
||||||
>
|
>
|
||||||
@@ -22,7 +29,7 @@
|
|||||||
v-for="(icon, type) in fieldIcons"
|
v-for="(icon, type) in fieldIcons"
|
||||||
:key="type"
|
:key="type"
|
||||||
>
|
>
|
||||||
<li v-if="withPhone || type !== 'phone'">
|
<li v-if="(withPhone || type != 'phone') && (withPayment || type != 'payment')">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="text-sm py-1 px-2"
|
class="text-sm py-1 px-2"
|
||||||
@@ -43,11 +50,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper } from '@tabler/icons-vue'
|
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FiledTypeDropdown',
|
name: 'FiledTypeDropdown',
|
||||||
inject: ['withPhone'],
|
inject: ['withPhone', 'withPayment', 't'],
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -63,6 +70,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
buttonWidth: {
|
buttonWidth: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -70,21 +82,28 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['update:model-value'],
|
emits: ['update:model-value'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
renderDropdown: false
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
fieldNames () {
|
fieldNames () {
|
||||||
return {
|
return {
|
||||||
text: 'Text',
|
text: this.t('text'),
|
||||||
signature: 'Signature',
|
signature: this.t('signature'),
|
||||||
initials: 'Initials',
|
initials: this.t('initials'),
|
||||||
date: 'Date',
|
date: this.t('date'),
|
||||||
image: 'Image',
|
image: this.t('image'),
|
||||||
file: 'File',
|
file: this.t('file'),
|
||||||
select: 'Select',
|
select: this.t('select'),
|
||||||
checkbox: 'Checkbox',
|
checkbox: this.t('checkbox'),
|
||||||
multiple: 'Multiple',
|
multiple: this.t('multiple'),
|
||||||
radio: 'Radio',
|
radio: this.t('radio'),
|
||||||
cells: 'Cells',
|
cells: this.t('cells'),
|
||||||
phone: 'Phone'
|
stamp: this.t('stamp'),
|
||||||
|
payment: this.t('payment'),
|
||||||
|
phone: this.t('phone')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fieldIcons () {
|
fieldIcons () {
|
||||||
@@ -95,11 +114,13 @@ export default {
|
|||||||
date: IconCalendarEvent,
|
date: IconCalendarEvent,
|
||||||
image: IconPhoto,
|
image: IconPhoto,
|
||||||
file: IconPaperclip,
|
file: IconPaperclip,
|
||||||
select: IconSelect,
|
|
||||||
checkbox: IconCheckbox,
|
checkbox: IconCheckbox,
|
||||||
radio: IconCircleDot,
|
radio: IconCircleDot,
|
||||||
|
select: IconSelect,
|
||||||
multiple: IconChecks,
|
multiple: IconChecks,
|
||||||
cells: IconColumns3,
|
cells: IconColumns3,
|
||||||
|
stamp: IconRubberStamp,
|
||||||
|
payment: IconCreditCard,
|
||||||
phone: IconPhoneCheck
|
phone: IconPhoneCheck
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,64 +5,83 @@
|
|||||||
class="w-full rounded-lg"
|
class="w-full rounded-lg"
|
||||||
:class="{ 'bg-base-100': withStickySubmitters }"
|
:class="{ 'bg-base-100': withStickySubmitters }"
|
||||||
:submitters="submitters"
|
:submitters="submitters"
|
||||||
|
:editable="editable && !defaultSubmitters.length"
|
||||||
@new-submitter="save"
|
@new-submitter="save"
|
||||||
@remove="removeSubmitter"
|
@remove="removeSubmitter"
|
||||||
@name-change="save"
|
@name-change="save"
|
||||||
@update:model-value="$emit('change-submitter', submitters.find((s) => s.uuid === $event))"
|
@update:model-value="$emit('change-submitter', submitters.find((s) => s.uuid === $event))"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-1 mt-2">
|
<div
|
||||||
|
ref="fields"
|
||||||
|
class="mb-1 mt-2"
|
||||||
|
@dragover.prevent="onFieldDragover"
|
||||||
|
@drop="reorderFields"
|
||||||
|
>
|
||||||
<Field
|
<Field
|
||||||
v-for="field in submitterFields"
|
v-for="field in submitterFields"
|
||||||
:key="field.uuid"
|
:key="field.uuid"
|
||||||
|
:data-uuid="field.uuid"
|
||||||
:field="field"
|
:field="field"
|
||||||
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
|
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
|
||||||
|
:editable="editable && (!dragField || dragField !== field)"
|
||||||
|
:default-field="defaultFields.find((f) => f.name === field.name)"
|
||||||
|
:draggable="editable"
|
||||||
|
@dragstart="dragField = field"
|
||||||
|
@dragend="dragField = null"
|
||||||
@remove="removeField"
|
@remove="removeField"
|
||||||
@move-up="move(field, -1)"
|
|
||||||
@move-down="move(field, 1)"
|
|
||||||
@scroll-to="$emit('scroll-to-area', $event)"
|
@scroll-to="$emit('scroll-to-area', $event)"
|
||||||
@set-draw="$emit('set-draw', $event)"
|
@set-draw="$emit('set-draw', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-3 gap-1 pb-2">
|
<div v-if="submitterDefaultFields.length">
|
||||||
|
<hr class="mb-2">
|
||||||
|
<template
|
||||||
|
v-for="field in submitterDefaultFields"
|
||||||
|
:key="field.name"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
|
draggable="true"
|
||||||
|
class="border border-base-300 rounded rounded-tr-none relative group mb-2"
|
||||||
|
@dragstart="onDragstart({ type: 'text', ...field })"
|
||||||
|
@dragend="$emit('drag-end')"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between relative cursor-grab">
|
||||||
|
<div class="flex items-center p-1 space-x-1">
|
||||||
|
<IconDrag />
|
||||||
|
<FieldType
|
||||||
|
:model-value="field.type || 'text'"
|
||||||
|
:editable="false"
|
||||||
|
:button-width="20"
|
||||||
|
/>
|
||||||
|
<span class="block pl-0.5">
|
||||||
|
{{ field.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="editable && !onlyDefinedFields"
|
||||||
|
class="grid grid-cols-3 gap-1 pb-2"
|
||||||
|
>
|
||||||
<template
|
<template
|
||||||
v-for="(icon, type) in fieldIcons"
|
v-for="(icon, type) in fieldIcons"
|
||||||
:key="type"
|
:key="type"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-if="withPhone || type != 'phone'"
|
v-if="(withPhone || type != 'phone') && (withPayment || type != 'payment')"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
class="flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
|
class="group flex items-center justify-center border border-dashed border-base-300 hover:border-base-content/20 w-full rounded relative"
|
||||||
:style="{ backgroundColor: backgroundColor }"
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
@dragstart="onDragstart(type)"
|
@dragstart="onDragstart({ type: type })"
|
||||||
@dragend="$emit('drag-end')"
|
@dragend="$emit('drag-end')"
|
||||||
@click="addField(type)"
|
@click="addField(type)"
|
||||||
>
|
>
|
||||||
<div class="w-0 absolute left-0">
|
<div class="flex items-console group-hover:bg-base-200/50 transition-all cursor-grab h-full absolute left-0">
|
||||||
<svg
|
<IconDrag class=" my-auto" />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="cursor-grab"
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
fill="none"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke="none"
|
|
||||||
d="M0 0h24v24H0z"
|
|
||||||
fill="none"
|
|
||||||
/>
|
|
||||||
<path d="M9 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
|
||||||
<path d="M9 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
|
||||||
<path d="M9 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
|
||||||
<path d="M15 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
|
||||||
<path d="M15 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
|
||||||
<path d="M15 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center flex-col px-2 py-2">
|
<div class="flex items-center flex-col px-2 py-2">
|
||||||
<component :is="icon" />
|
<component :is="icon" />
|
||||||
@@ -72,8 +91,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else-if="type == 'phone'"
|
||||||
class="tooltip tooltip-bottom-end flex"
|
class="tooltip tooltip-bottom flex"
|
||||||
|
:class="{'tooltip-bottom-start': !withPayment, 'tooltip-bottom': withPayment }"
|
||||||
data-tip="Unlock SMS-verified phone number field with paid plan. Use text field for phone numbers without verification."
|
data-tip="Unlock SMS-verified phone number field with paid plan. Use text field for phone numbers without verification."
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@@ -100,21 +120,18 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="fields.length < 4"
|
v-if="fields.length < 4 && editable && withHelp"
|
||||||
class="text-xs p-2 border border-base-200 rounded"
|
class="text-xs p-2 border border-base-200 rounded"
|
||||||
>
|
>
|
||||||
<ul class="list-disc list-outside ml-3">
|
<ul class="list-disc list-outside ml-3">
|
||||||
<li>
|
<li>
|
||||||
Draw a text field on the page with a mouse
|
Draw a text field on the page with a mouse
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
Single click on the page to add a checkbox
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
Drag & drop any other field type on the page
|
Drag & drop any other field type on the page
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
Click on the field type above to add it to the form without drawing it on the document
|
Click on the field type above to start drawing it
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,20 +143,48 @@ import { v4 } from 'uuid'
|
|||||||
import FieldType from './field_type'
|
import FieldType from './field_type'
|
||||||
import FieldSubmitter from './field_submitter'
|
import FieldSubmitter from './field_submitter'
|
||||||
import { IconLock } from '@tabler/icons-vue'
|
import { IconLock } from '@tabler/icons-vue'
|
||||||
|
import IconDrag from './icon_drag'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TemplateFields',
|
name: 'TemplateFields',
|
||||||
components: {
|
components: {
|
||||||
Field,
|
Field,
|
||||||
|
FieldType,
|
||||||
FieldSubmitter,
|
FieldSubmitter,
|
||||||
|
IconDrag,
|
||||||
IconLock
|
IconLock
|
||||||
},
|
},
|
||||||
inject: ['save', 'backgroundColor', 'withPhone'],
|
inject: ['save', 'backgroundColor', 'withPhone', 'withPayment', 't'],
|
||||||
props: {
|
props: {
|
||||||
fields: {
|
fields: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
withHelp: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
defaultFields: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
onlyDefinedFields: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
defaultSubmitters: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
withStickySubmitters: {
|
withStickySubmitters: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -155,16 +200,54 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['set-draw', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter'],
|
emits: ['set-draw', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
dragField: null
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
fieldNames: FieldType.computed.fieldNames,
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
fieldIcons: FieldType.computed.fieldIcons,
|
fieldIcons: FieldType.computed.fieldIcons,
|
||||||
submitterFields () {
|
submitterFields () {
|
||||||
return this.fields.filter((f) => f.submitter_uuid === this.selectedSubmitter.uuid)
|
return this.fields.filter((f) => f.submitter_uuid === this.selectedSubmitter.uuid)
|
||||||
|
},
|
||||||
|
submitterDefaultFields () {
|
||||||
|
return this.defaultFields.filter((f) => {
|
||||||
|
return !this.submitterFields.find((field) => field.name === f.name) && (!f.role || f.role === this.selectedSubmitter.name)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onDragstart (fieldType) {
|
onDragstart (field) {
|
||||||
this.$emit('set-drag', fieldType)
|
this.$emit('set-drag', field)
|
||||||
|
},
|
||||||
|
onFieldDragover (e) {
|
||||||
|
const targetField = e.target.closest('[data-uuid]')
|
||||||
|
const dragField = this.$refs.fields.querySelector(`[data-uuid="${this.dragField.uuid}"]`)
|
||||||
|
|
||||||
|
if (dragField && targetField && targetField !== dragField) {
|
||||||
|
const fields = Array.from(this.$refs.fields.children)
|
||||||
|
const currentIndex = fields.indexOf(dragField)
|
||||||
|
const targetIndex = fields.indexOf(targetField)
|
||||||
|
|
||||||
|
if (currentIndex < targetIndex) {
|
||||||
|
targetField.after(dragField)
|
||||||
|
} else {
|
||||||
|
targetField.before(dragField)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reorderFields () {
|
||||||
|
Array.from(this.$refs.fields.children).forEach((el, index) => {
|
||||||
|
if (el.dataset.uuid !== this.fields[index].uuid) {
|
||||||
|
const field = this.fields.find((f) => f.uuid === el.dataset.uuid)
|
||||||
|
|
||||||
|
this.fields.splice(this.fields.indexOf(field), 1)
|
||||||
|
this.fields.splice(index, 0, field)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.save()
|
||||||
},
|
},
|
||||||
removeSubmitter (submitter) {
|
removeSubmitter (submitter) {
|
||||||
[...this.fields].forEach((field) => {
|
[...this.fields].forEach((field) => {
|
||||||
@@ -181,26 +264,6 @@ export default {
|
|||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
move (field, direction) {
|
|
||||||
const currentIndex = this.submitterFields.indexOf(field)
|
|
||||||
const fieldsIndex = this.fields.indexOf(field)
|
|
||||||
|
|
||||||
this.fields.splice(fieldsIndex, 1)
|
|
||||||
|
|
||||||
if (currentIndex + direction > this.submitterFields.length) {
|
|
||||||
const firstIndex = this.fields.indexOf(this.submitterFields[0])
|
|
||||||
|
|
||||||
this.fields.splice(firstIndex, 0, field)
|
|
||||||
} else if (currentIndex + direction < 0) {
|
|
||||||
const lastIndex = this.fields.indexOf(this.submitterFields[this.submitterFields.length - 1])
|
|
||||||
|
|
||||||
this.fields.splice(lastIndex + 1, 0, field)
|
|
||||||
} else {
|
|
||||||
this.fields.splice(fieldsIndex + direction, 0, field)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.save()
|
|
||||||
},
|
|
||||||
removeField (field) {
|
removeField (field) {
|
||||||
this.fields.splice(this.fields.indexOf(field), 1)
|
this.fields.splice(this.fields.indexOf(field), 1)
|
||||||
|
|
||||||
@@ -217,11 +280,25 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (['select', 'multiple', 'radio'].includes(type)) {
|
if (['select', 'multiple', 'radio'].includes(type)) {
|
||||||
field.options = ['']
|
field.options = [{ value: '', uuid: v4() }]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'stamp') {
|
||||||
|
field.readonly = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'date') {
|
||||||
|
field.preferences = {
|
||||||
|
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fields.push(field)
|
this.fields.push(field)
|
||||||
|
|
||||||
|
if (!['payment', 'file'].includes(type)) {
|
||||||
|
this.$emit('set-draw', { field })
|
||||||
|
}
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
const en = {
|
||||||
|
sign_yourself: 'Sign Yourself',
|
||||||
|
send: 'Send',
|
||||||
|
save: 'Save',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
draw_field_on_the_document: 'Draw {field} field on the document',
|
||||||
|
click_to_upload: 'Click to upload',
|
||||||
|
or_drag_and_drop_files: 'or drag and drop files',
|
||||||
|
uploading: 'Uploading',
|
||||||
|
processing_: 'Processing...',
|
||||||
|
add_pdf_documents_or_images: 'Add PDF documents or images',
|
||||||
|
add_documents_or_images: 'Add documents or images',
|
||||||
|
required: 'Required',
|
||||||
|
default_value: 'Default value',
|
||||||
|
format: 'Format',
|
||||||
|
read_only: 'Read-only',
|
||||||
|
page: 'Page',
|
||||||
|
draw_new_area: 'Draw New Area',
|
||||||
|
copy_to_all_pages: 'Copy to All Pages',
|
||||||
|
add_option: 'Add option',
|
||||||
|
option: 'Option',
|
||||||
|
first_party: 'First Party',
|
||||||
|
second_party: 'Second Party',
|
||||||
|
third_party: 'Third Party',
|
||||||
|
fourth_party: 'Fourth Party',
|
||||||
|
fifth_party: 'Fifth Party',
|
||||||
|
sixth_party: 'Sixth Party',
|
||||||
|
seventh_party: 'Seventh Party',
|
||||||
|
eighth_party: 'Eighth Party',
|
||||||
|
ninth_party: 'Ninth Party',
|
||||||
|
tenth_party: 'Tenth Party',
|
||||||
|
add: 'Add',
|
||||||
|
or_add_field_without_drawing: 'Or add field without drawing',
|
||||||
|
text: 'Text',
|
||||||
|
signature: 'Signature',
|
||||||
|
initials: 'Initials',
|
||||||
|
date: 'Date',
|
||||||
|
image: 'Image',
|
||||||
|
file: 'File',
|
||||||
|
select: 'Select',
|
||||||
|
checkbox: 'Checkbox',
|
||||||
|
multiple: 'Multiple',
|
||||||
|
radio: 'Radio',
|
||||||
|
cells: 'Cells',
|
||||||
|
stamp: 'Stamp',
|
||||||
|
payment: 'Payment',
|
||||||
|
phone: 'Phone',
|
||||||
|
field: 'Field',
|
||||||
|
group: 'Group',
|
||||||
|
draw_a_text_field_on_the_page_with_a_mouse: 'Draw a text field on the page with a mouse',
|
||||||
|
draw_field: 'Draw {field} Field',
|
||||||
|
replace: 'Replace',
|
||||||
|
uploading_: 'Uploading...',
|
||||||
|
add_document: 'Add Document'
|
||||||
|
}
|
||||||
|
|
||||||
|
export { en }
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke="none"
|
||||||
|
d="M0 0h24v24H0z"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
<path d="M9 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||||
|
<path d="M9 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||||
|
<path d="M9 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||||
|
<path d="M15 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||||
|
<path d="M15 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||||
|
<path d="M15 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'DragIcon'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<template>
|
||||||
|
<div class="fixed text-center w-full bottom-0 pr-6 mb-4">
|
||||||
|
<span class="w-full bg-base-200 px-4 py-2 rounded-md inline-flex space-x-2 mx-auto items-center justify-between mb-2 z-20 md:hidden">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<component
|
||||||
|
:is="fieldIcons[drawField.type]"
|
||||||
|
:width="20"
|
||||||
|
:height="20"
|
||||||
|
class="inline"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
<span>{{ t('draw_field').replace('{field}', fieldNames[drawField.type]) }}</span>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="link block text-center"
|
||||||
|
@click.prevent="$emit('cancel')"
|
||||||
|
>
|
||||||
|
{{ t('cancel') }}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<FieldSubmitter
|
||||||
|
:model-value="selectedSubmitter.uuid"
|
||||||
|
:submitters="submitters"
|
||||||
|
:editable="editable"
|
||||||
|
:mobile-view="true"
|
||||||
|
@new-submitter="save"
|
||||||
|
@remove="removeSubmitter"
|
||||||
|
@name-change="save"
|
||||||
|
@update:model-value="$emit('change-submitter', submitters.find((s) => s.uuid === $event))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Field from './field'
|
||||||
|
import FieldType from './field_type'
|
||||||
|
import FieldSubmitter from './field_submitter'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'MobileDrawField',
|
||||||
|
components: {
|
||||||
|
Field,
|
||||||
|
FieldSubmitter
|
||||||
|
},
|
||||||
|
inject: ['save', 't'],
|
||||||
|
props: {
|
||||||
|
drawField: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
submitters: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
fields: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
selectedSubmitter: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['change-submitter', 'cancel'],
|
||||||
|
computed: {
|
||||||
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
|
fieldIcons: FieldType.computed.fieldIcons
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
removeSubmitter (submitter) {
|
||||||
|
[...this.fields].forEach((field) => {
|
||||||
|
if (field.submitter_uuid === submitter.uuid) {
|
||||||
|
this.removeField(field)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.submitters.splice(this.submitters.indexOf(submitter), 1)
|
||||||
|
|
||||||
|
if (this.selectedSubmitter === submitter) {
|
||||||
|
this.$emit('change-submitter', this.submitters[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
|
removeField (field) {
|
||||||
|
this.fields.splice(this.fields.indexOf(field), 1)
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
<template>
|
||||||
|
<span
|
||||||
|
class="dropdown dropdown-top dropdown-end fixed bottom-4 right-4 z-10 md:hidden"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="btn btn-neutral text-white btn-circle btn-lg group"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<IconPlus
|
||||||
|
class="group-focus:hidden"
|
||||||
|
width="28"
|
||||||
|
height="28"
|
||||||
|
/>
|
||||||
|
<IconX
|
||||||
|
class="hidden group-focus:inline"
|
||||||
|
width="28"
|
||||||
|
height="28"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<ul
|
||||||
|
tabindex="0"
|
||||||
|
class="dropdown-content menu menu-xs p-2 shadow rounded-box w-52 z-10 mb-3 mt-1.5 bg-base-100"
|
||||||
|
@click="closeDropdown"
|
||||||
|
>
|
||||||
|
<template v-if="submitterDefaultFields.length">
|
||||||
|
<template
|
||||||
|
v-for="field in submitterDefaultFields"
|
||||||
|
:key="field.name"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="text-sm py-1 px-2"
|
||||||
|
@click.prevent="$emit('select', { name: field.name || '', type: field.type || 'text' })"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="fieldIcons[field.type || 'text']"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
:width="20"
|
||||||
|
/>
|
||||||
|
{{ field.name }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<template
|
||||||
|
v-for="(icon, type) in fieldIcons"
|
||||||
|
:key="type"
|
||||||
|
>
|
||||||
|
<li v-if="(withPhone || type != 'phone') && (withPayment || type != 'payment')">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="text-sm py-1 px-2"
|
||||||
|
:class="{ 'active': type === modelValue }"
|
||||||
|
@click.prevent="$emit('select', { type })"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="icon"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
:width="20"
|
||||||
|
/>
|
||||||
|
{{ fieldNames[type] }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { IconPlus, IconX } from '@tabler/icons-vue'
|
||||||
|
import FieldType from './field_type'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'MobileFields',
|
||||||
|
components: {
|
||||||
|
IconPlus,
|
||||||
|
IconX
|
||||||
|
},
|
||||||
|
inject: ['withPhone', 'withPayment', 'backgroundColor', 't'],
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
fields: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
selectedSubmitter: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
defaultFields: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['select'],
|
||||||
|
computed: {
|
||||||
|
...FieldType.computed,
|
||||||
|
submitterFields () {
|
||||||
|
return this.fields.filter((f) => f.submitter_uuid === this.selectedSubmitter.uuid)
|
||||||
|
},
|
||||||
|
submitterDefaultFields () {
|
||||||
|
return this.defaultFields.filter((f) => {
|
||||||
|
return (!f.role || f.role === this.selectedSubmitter.name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
closeDropdown () {
|
||||||
|
document.activeElement.blur()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative cursor-crosshair select-none">
|
<div
|
||||||
|
class="relative cursor-crosshair select-none"
|
||||||
|
:style="drawField ? 'touch-action: none' : ''"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
ref="image"
|
ref="image"
|
||||||
|
loading="lazy"
|
||||||
:src="image.url"
|
:src="image.url"
|
||||||
:width="width"
|
:width="width"
|
||||||
class="border rounded mb-4"
|
|
||||||
:height="height"
|
:height="height"
|
||||||
loading="lazy"
|
class="border rounded mb-4"
|
||||||
|
@load="onImageLoad"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="top-0 bottom-0 left-0 right-0 absolute"
|
class="top-0 bottom-0 left-0 right-0 absolute"
|
||||||
@@ -18,6 +22,8 @@
|
|||||||
:ref="setAreaRefs"
|
:ref="setAreaRefs"
|
||||||
:area="item.area"
|
:area="item.area"
|
||||||
:field="item.field"
|
:field="item.field"
|
||||||
|
:editable="editable"
|
||||||
|
:default-field="defaultFields.find((f) => f.name === item.field.name)"
|
||||||
@start-resize="resizeDirection = $event"
|
@start-resize="resizeDirection = $event"
|
||||||
@stop-resize="resizeDirection = null"
|
@stop-resize="resizeDirection = null"
|
||||||
@start-drag="isMove = true"
|
@start-drag="isMove = true"
|
||||||
@@ -32,12 +38,13 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-show="resizeDirection || isMove || isDrag || showMask"
|
v-show="resizeDirection || isMove || isDrag || showMask || (drawField && isMobile)"
|
||||||
id="mask"
|
id="mask"
|
||||||
ref="mask"
|
ref="mask"
|
||||||
class="top-0 bottom-0 left-0 right-0 absolute z-10"
|
class="top-0 bottom-0 left-0 right-0 absolute"
|
||||||
:class="{ 'cursor-grab': isDrag || isMove, 'cursor-nwse-resize': drawField, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }"
|
:class="{ 'z-10': !isMobile, 'cursor-grab': isDrag || isMove, 'cursor-nwse-resize': drawField, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }"
|
||||||
@pointermove="onPointermove"
|
@pointermove="onPointermove"
|
||||||
|
@pointerdown="onStartDraw"
|
||||||
@dragover.prevent
|
@dragover.prevent
|
||||||
@drop="onDrop"
|
@drop="onDrop"
|
||||||
@pointerup="onPointerup"
|
@pointerup="onPointerup"
|
||||||
@@ -63,6 +70,16 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
defaultFields: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
allowDraw: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
selectedSubmitter: {
|
selectedSubmitter: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
@@ -72,6 +89,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
isDrag: {
|
isDrag: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -93,6 +115,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isMobile () {
|
||||||
|
return /android|iphone|ipad/i.test(navigator.userAgent)
|
||||||
|
},
|
||||||
resizeDirectionClasses () {
|
resizeDirectionClasses () {
|
||||||
return {
|
return {
|
||||||
nwse: 'cursor-nwse-resize',
|
nwse: 'cursor-nwse-resize',
|
||||||
@@ -110,6 +135,10 @@ export default {
|
|||||||
this.areaRefs = []
|
this.areaRefs = []
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
onImageLoad (e) {
|
||||||
|
e.target.setAttribute('width', e.target.naturalWidth)
|
||||||
|
e.target.setAttribute('height', e.target.naturalHeight)
|
||||||
|
},
|
||||||
setAreaRefs (el) {
|
setAreaRefs (el) {
|
||||||
if (el) {
|
if (el) {
|
||||||
this.areaRefs.push(el)
|
this.areaRefs.push(el)
|
||||||
@@ -125,6 +154,18 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
onStartDraw (e) {
|
onStartDraw (e) {
|
||||||
|
if (!this.allowDraw) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isMobile && !this.drawField) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.editable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.showMask = true
|
this.showMask = true
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
<template>
|
||||||
|
<span
|
||||||
|
class="dropdown dropdown-end"
|
||||||
|
:class="{ 'dropdown-open': (!field.preferences?.price || !isConnected) && !isLoading }"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
tabindex="0"
|
||||||
|
title="Settings"
|
||||||
|
class="cursor-pointer text-transparent group-hover:text-base-content"
|
||||||
|
>
|
||||||
|
<IconSettings
|
||||||
|
:width="18"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<ul
|
||||||
|
tabindex="0"
|
||||||
|
class="mt-1.5 dropdown-content menu menu-xs p-2 shadow bg-base-100 rounded-box w-52 z-10"
|
||||||
|
draggable="true"
|
||||||
|
@dragstart.prevent.stop
|
||||||
|
@click="closeDropdown"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
v-model="field.preferences.currency"
|
||||||
|
placeholder="Price"
|
||||||
|
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0"
|
||||||
|
@change="save"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="currency in currencies"
|
||||||
|
:key="currency"
|
||||||
|
:value="currency"
|
||||||
|
>
|
||||||
|
{{ currency }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<label
|
||||||
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>
|
||||||
|
Currency
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="field.preferences.price"
|
||||||
|
type="number"
|
||||||
|
placeholder="Price"
|
||||||
|
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
|
||||||
|
@blur="save"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
v-if="field.preferences.price"
|
||||||
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>
|
||||||
|
Price
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="!isConnected || isOauthSuccess"
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="isConnected && isOauthSuccess"
|
||||||
|
class="text-sm text-center"
|
||||||
|
>
|
||||||
|
<IconCircleCheck
|
||||||
|
class="inline text-green-600 w-4 h-4"
|
||||||
|
/>
|
||||||
|
Stripe Connected
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
v-if="!isConnected"
|
||||||
|
data-turbo="false"
|
||||||
|
action="/auth/stripe_connect"
|
||||||
|
accept-charset="UTF-8"
|
||||||
|
target="_blank"
|
||||||
|
method="post"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="state"
|
||||||
|
:value="oauthState"
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="redirect_uri"
|
||||||
|
:value="redirectUri"
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="scope"
|
||||||
|
value="read_write"
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="authenticity_token"
|
||||||
|
:value="authenticityToken"
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
:disabled="isLoading"
|
||||||
|
class="btn bg-[#7B73FF] hover:bg-[#0A2540] btn-sm text-white w-full"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="isLoading"
|
||||||
|
class="flex items-center space-x-1"
|
||||||
|
>
|
||||||
|
<IconInnerShadowTop
|
||||||
|
class="w-4 h-4 animate-spin inline"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
Connect Stripe
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="flex items-center space-x-1"
|
||||||
|
>
|
||||||
|
<IconBrandStripe
|
||||||
|
class="w-4 h-4 inline"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
Connect Stripe
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<a
|
||||||
|
v-if="!isConnected"
|
||||||
|
class="block link text-center mt-1"
|
||||||
|
href="https://www.docuseal.co/blog/accept-payments-and-request-signatures-with-ease"
|
||||||
|
target="_blank"
|
||||||
|
data-turbo="false"
|
||||||
|
>Learn more</a>
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconSettings, IconCircleCheck, IconBrandStripe, IconInnerShadowTop } from '@tabler/icons-vue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const isConnected = ref(false)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PaymentSettings',
|
||||||
|
components: {
|
||||||
|
IconSettings,
|
||||||
|
IconCircleCheck,
|
||||||
|
IconInnerShadowTop,
|
||||||
|
IconBrandStripe
|
||||||
|
},
|
||||||
|
inject: ['backgroundColor', 'save'],
|
||||||
|
props: {
|
||||||
|
field: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isLoading: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isConnected: () => isConnected.value,
|
||||||
|
isOauthSuccess () {
|
||||||
|
return document.location.search?.includes('stripe_connect_success')
|
||||||
|
},
|
||||||
|
redirectUri () {
|
||||||
|
return document.location.origin + '/auth/stripe_connect/callback'
|
||||||
|
},
|
||||||
|
currencies () {
|
||||||
|
return ['USD', 'EUR', 'GBP']
|
||||||
|
},
|
||||||
|
authenticityToken () {
|
||||||
|
return document.querySelector('meta[name="csrf-token"]')?.content
|
||||||
|
},
|
||||||
|
oauthState () {
|
||||||
|
const params = new URLSearchParams('')
|
||||||
|
|
||||||
|
params.set('redir', document.location.href)
|
||||||
|
|
||||||
|
return params.toString()
|
||||||
|
},
|
||||||
|
defaultCurrency () {
|
||||||
|
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||||
|
|
||||||
|
if (userTimezone.startsWith('Europe')) {
|
||||||
|
return 'EUR'
|
||||||
|
} else if (userTimezone.includes('London') || userTimezone.includes('Belfast')) {
|
||||||
|
return 'GBP'
|
||||||
|
} else {
|
||||||
|
return 'USD'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.field.preferences ||= {}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.field.preferences.currency ||= this.defaultCurrency
|
||||||
|
|
||||||
|
if (!this.isConnected) {
|
||||||
|
this.checkStatus()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
checkStatus () {
|
||||||
|
this.isLoading = true
|
||||||
|
|
||||||
|
fetch('/api/stripe_connect').then(async (resp) => {
|
||||||
|
const { status } = await resp.json()
|
||||||
|
|
||||||
|
if (status === 'connected') {
|
||||||
|
isConnected.value = true
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
this.isLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
closeDropdown () {
|
||||||
|
document.activeElement.blur()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -12,10 +12,14 @@
|
|||||||
class="group flex justify-end cursor-pointer top-0 bottom-0 left-0 right-0 absolute p-1"
|
class="group flex justify-end cursor-pointer top-0 bottom-0 left-0 right-0 absolute p-1"
|
||||||
@click="$emit('scroll-to', item)"
|
@click="$emit('scroll-to', item)"
|
||||||
>
|
>
|
||||||
<div class="flex justify-between w-full">
|
<div
|
||||||
|
v-if="editable"
|
||||||
|
class="flex justify-between w-full"
|
||||||
|
>
|
||||||
<div style="width: 26px" />
|
<div style="width: 26px" />
|
||||||
<div class="">
|
<div class="">
|
||||||
<ReplaceButton
|
<ReplaceButton
|
||||||
|
v-if="withReplaceButton"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
:template-id="template.id"
|
:template-id="template.id"
|
||||||
:accept-file-types="acceptFileTypes"
|
:accept-file-types="acceptFileTypes"
|
||||||
@@ -63,6 +67,7 @@
|
|||||||
<Contenteditable
|
<Contenteditable
|
||||||
:model-value="item.name"
|
:model-value="item.name"
|
||||||
:icon-width="16"
|
:icon-width="16"
|
||||||
|
:editable="editable"
|
||||||
style="max-width: 95%"
|
style="max-width: 95%"
|
||||||
class="mx-auto"
|
class="mx-auto"
|
||||||
@update:model-value="onUpdateName"
|
@update:model-value="onUpdateName"
|
||||||
@@ -95,11 +100,21 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
acceptFileTypes: {
|
acceptFileTypes: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'image/*, application/pdf'
|
default: 'image/*, application/pdf'
|
||||||
},
|
},
|
||||||
|
withReplaceButton: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
isDirectUpload: {
|
isDirectUpload: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
@@ -114,7 +129,7 @@ export default {
|
|||||||
emits: ['scroll-to', 'change', 'remove', 'up', 'down', 'replace'],
|
emits: ['scroll-to', 'change', 'remove', 'up', 'down', 'replace'],
|
||||||
computed: {
|
computed: {
|
||||||
previewImage () {
|
previewImage () {
|
||||||
return this.document.preview_images[0]
|
return [...this.document.preview_images].sort((a, b) => parseInt(a.filename) - parseInt(b.filename))[0]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import Upload from './upload'
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ReplaceDocument',
|
name: 'ReplaceDocument',
|
||||||
inject: ['baseFetch'],
|
inject: ['baseFetch', 't'],
|
||||||
props: {
|
props: {
|
||||||
templateId: {
|
templateId: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
@@ -56,11 +56,11 @@ export default {
|
|||||||
},
|
},
|
||||||
message () {
|
message () {
|
||||||
if (this.isLoading) {
|
if (this.isLoading) {
|
||||||
return 'Uploading...'
|
return this.t('uploading_')
|
||||||
} else if (this.isProcessing) {
|
} else if (this.isProcessing) {
|
||||||
return 'Processing...'
|
return this.t('processing_')
|
||||||
} else {
|
} else {
|
||||||
return 'Replace'
|
return this.t('replace')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,13 +15,13 @@
|
|||||||
width="20"
|
width="20"
|
||||||
/>
|
/>
|
||||||
<span v-if="isLoading">
|
<span v-if="isLoading">
|
||||||
Uploading...
|
{{ t('uploading_') }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="isProcessing">
|
<span v-else-if="isProcessing">
|
||||||
Processing...
|
{{ t('processing_') }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
Add Document
|
{{ t('add_document') }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<form
|
<form
|
||||||
@@ -50,7 +50,7 @@ export default {
|
|||||||
IconUpload,
|
IconUpload,
|
||||||
IconInnerShadowTop
|
IconInnerShadowTop
|
||||||
},
|
},
|
||||||
inject: ['baseFetch'],
|
inject: ['baseFetch', 't'],
|
||||||
props: {
|
props: {
|
||||||
templateId: {
|
templateId: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
|
|||||||
@@ -10,33 +10,40 @@ class ProcessSubmitterCompletionJob < ApplicationJob
|
|||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(submitter)
|
Submissions::EnsureResultGenerated.call(submitter)
|
||||||
|
|
||||||
if submitter.account.encrypted_configs.exists?(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
if is_all_completed && submitter.completed_at == submitter.submission.submitters.maximum(:completed_at)
|
||||||
SendFormCompletedWebhookRequestJob.perform_later(submitter)
|
Submissions::GenerateAuditTrail.call(submitter.submission)
|
||||||
|
|
||||||
|
enqueue_completed_emails(submitter)
|
||||||
end
|
end
|
||||||
|
|
||||||
return unless is_all_completed
|
return if Accounts.load_webhook_configs(submitter.account).blank?
|
||||||
return if submitter.completed_at != submitter.submission.submitters.maximum(:completed_at)
|
|
||||||
|
|
||||||
Submissions::GenerateAuditTrail.call(submitter.submission)
|
SendFormCompletedWebhookRequestJob.perform_later(submitter)
|
||||||
|
|
||||||
enqueue_emails(submitter)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def enqueue_emails(submitter)
|
def enqueue_completed_emails(submitter)
|
||||||
user = submitter.submission.created_by_user || submitter.template.author
|
user = submitter.submission.created_by_user || submitter.template.author
|
||||||
|
|
||||||
if submitter.template.account.users.exists?(id: user.id)
|
if submitter.template.account.users.exists?(id: user.id) &&
|
||||||
bcc = submitter.submission.template.account.account_configs
|
submitter.submission.preferences['send_email'] != false
|
||||||
.find_by(key: AccountConfig::BCC_EMAILS)&.value
|
SubmitterMailer.completed_email(submitter, user).deliver_later!
|
||||||
|
|
||||||
SubmitterMailer.completed_email(submitter, user, bcc:).deliver_later!
|
bcc = submitter.submission.template.account.account_configs
|
||||||
|
.find_by(key: AccountConfig::BCC_EMAILS)&.value.presence
|
||||||
|
|
||||||
|
SubmitterMailer.completed_email(submitter, user, to: bcc).deliver_later! if bcc
|
||||||
end
|
end
|
||||||
|
|
||||||
to = submitter.submission.submitters.sort_by(&:completed_at).select(&:email?).map(&:friendly_name).join(', ')
|
to = build_to_addresses(submitter)
|
||||||
|
|
||||||
SubmitterMailer.documents_copy_email(submitter, to:).deliver_later! if to.present?
|
SubmitterMailer.documents_copy_email(submitter, to:).deliver_later! if to.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def build_to_addresses(submitter)
|
||||||
|
submitter.submission.submitters.reject { |e| e.preferences['send_email'] == false }
|
||||||
|
.sort_by(&:completed_at).select(&:email?).map(&:friendly_name).join(', ')
|
||||||
|
end
|
||||||
|
|
||||||
def enqueue_next_submitter_request_notification(submitter)
|
def enqueue_next_submitter_request_notification(submitter)
|
||||||
next_submitter_item =
|
next_submitter_item =
|
||||||
submitter.submission.template_submitters.find do |e|
|
submitter.submission.template_submitters.find do |e|
|
||||||
@@ -49,6 +56,6 @@ class ProcessSubmitterCompletionJob < ApplicationJob
|
|||||||
|
|
||||||
next_submitter = submitter.submission.submitters.find { |s| s.uuid == next_submitter_item['uuid'] }
|
next_submitter = submitter.submission.submitters.find { |s| s.uuid == next_submitter_item['uuid'] }
|
||||||
|
|
||||||
Submitters.send_signature_requests([next_submitter], send_email: true)
|
Submitters.send_signature_requests([next_submitter])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -4,7 +4,7 @@ class SendFormCompletedWebhookRequestJob < ApplicationJob
|
|||||||
USER_AGENT = 'DocuSeal.co Webhook'
|
USER_AGENT = 'DocuSeal.co Webhook'
|
||||||
|
|
||||||
def perform(submitter)
|
def perform(submitter)
|
||||||
config = submitter.submission.account.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
config = Accounts.load_webhook_configs(submitter.submission.account)
|
||||||
|
|
||||||
return if config.blank? || config.value.blank?
|
return if config.blank? || config.value.blank?
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ class SendFormCompletedWebhookRequestJob < ApplicationJob
|
|||||||
Faraday.post(config.value,
|
Faraday.post(config.value,
|
||||||
{
|
{
|
||||||
event_type: 'form.completed',
|
event_type: 'form.completed',
|
||||||
timestamp: Time.current.iso8601,
|
timestamp: Time.current,
|
||||||
data: Submitters::SerializeForWebhook.call(submitter)
|
data: Submitters::SerializeForWebhook.call(submitter)
|
||||||
}.to_json,
|
}.to_json,
|
||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ class SendFormStartedWebhookRequestJob < ApplicationJob
|
|||||||
USER_AGENT = 'DocuSeal.co Webhook'
|
USER_AGENT = 'DocuSeal.co Webhook'
|
||||||
|
|
||||||
def perform(submitter)
|
def perform(submitter)
|
||||||
config = submitter.submission.account.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
config = Accounts.load_webhook_configs(submitter.submission.account)
|
||||||
|
|
||||||
return if config.blank? || config.value.blank?
|
return if config.blank? || config.value.blank?
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ class SendFormStartedWebhookRequestJob < ApplicationJob
|
|||||||
Faraday.post(config.value,
|
Faraday.post(config.value,
|
||||||
{
|
{
|
||||||
event_type: 'form.started',
|
event_type: 'form.started',
|
||||||
timestamp: Time.current.iso8601,
|
timestamp: Time.current,
|
||||||
data: Submitters::SerializeForWebhook.call(submitter)
|
data: Submitters::SerializeForWebhook.call(submitter)
|
||||||
}.to_json,
|
}.to_json,
|
||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ class SendFormViewedWebhookRequestJob < ApplicationJob
|
|||||||
USER_AGENT = 'DocuSeal.co Webhook'
|
USER_AGENT = 'DocuSeal.co Webhook'
|
||||||
|
|
||||||
def perform(submitter)
|
def perform(submitter)
|
||||||
config = submitter.submission.account.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
config = Accounts.load_webhook_configs(submitter.submission.account)
|
||||||
|
|
||||||
return if config.blank? || config.value.blank?
|
return if config.blank? || config.value.blank?
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ class SendFormViewedWebhookRequestJob < ApplicationJob
|
|||||||
Faraday.post(config.value,
|
Faraday.post(config.value,
|
||||||
{
|
{
|
||||||
event_type: 'form.viewed',
|
event_type: 'form.viewed',
|
||||||
timestamp: Time.current.iso8601,
|
timestamp: Time.current,
|
||||||
data: Submitters::SerializeForWebhook.call(submitter)
|
data: Submitters::SerializeForWebhook.call(submitter)
|
||||||
}.to_json,
|
}.to_json,
|
||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ class SendSubmitterInvitationEmailJob < ApplicationJob
|
|||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submitter = Submitter.find(params['submitter_id'])
|
submitter = Submitter.find(params['submitter_id'])
|
||||||
|
|
||||||
SubmitterMailer.invitation_email(submitter, subject: params['subject'], body: params['body']).deliver_now!
|
SubmitterMailer.invitation_email(submitter).deliver_now!
|
||||||
|
|
||||||
SubmissionEvent.create!(submitter:, event_type: 'send_email')
|
SubmissionEvent.create!(submitter:, event_type: 'send_email')
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,39 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class SubmitterMailer < ApplicationMailer
|
class SubmitterMailer < ApplicationMailer
|
||||||
def invitation_email(submitter, body: nil, subject: nil)
|
MAX_ATTACHMENTS_SIZE = 10.megabytes
|
||||||
|
|
||||||
|
DEFAULT_INVITATION_SUBJECT = 'You are invited to submit a form'
|
||||||
|
|
||||||
|
def invitation_email(submitter)
|
||||||
@current_account = submitter.submission.template.account
|
@current_account = submitter.submission.template.account
|
||||||
@submitter = submitter
|
@submitter = submitter
|
||||||
@body = body.presence
|
|
||||||
|
|
||||||
@email_config = @current_account.account_configs.find_by(key: AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY)
|
if submitter.preferences['email_message_uuid']
|
||||||
|
@email_message = submitter.account.email_messages.find_by(uuid: submitter.preferences['email_message_uuid'])
|
||||||
|
end
|
||||||
|
|
||||||
|
@body = @email_message&.body.presence
|
||||||
|
@subject = @email_message&.subject.presence
|
||||||
|
|
||||||
|
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY)
|
||||||
|
|
||||||
subject =
|
subject =
|
||||||
if @email_config || subject.present?
|
if @email_config || @subject
|
||||||
ReplaceEmailVariables.call(subject.presence || @email_config.value['subject'], submitter:)
|
ReplaceEmailVariables.call(@subject || @email_config.value['subject'], submitter:)
|
||||||
else
|
else
|
||||||
'You have been invited to submit a form'
|
DEFAULT_INVITATION_SUBJECT
|
||||||
end
|
end
|
||||||
|
|
||||||
mail(to: @submitter.friendly_name,
|
mail(
|
||||||
from: from_address_for_submitter(submitter),
|
to: @submitter.friendly_name,
|
||||||
subject:,
|
from: from_address_for_submitter(submitter),
|
||||||
reply_to: (submitter.submission.created_by_user || submitter.template.author)&.friendly_name)
|
subject:,
|
||||||
|
reply_to: (submitter.submission.created_by_user || submitter.template.author)&.friendly_name&.sub(/\+\w+@/, '@')
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def completed_email(submitter, user, bcc: nil)
|
def completed_email(submitter, user, to: nil)
|
||||||
@current_account = submitter.submission.template.account
|
@current_account = submitter.submission.template.account
|
||||||
@submitter = submitter
|
@submitter = submitter
|
||||||
@submission = submitter.submission
|
@submission = submitter.submission
|
||||||
@@ -29,7 +41,7 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(submitter)
|
Submissions::EnsureResultGenerated.call(submitter)
|
||||||
|
|
||||||
@email_config = @current_account.account_configs.find_by(key: AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY)
|
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY)
|
||||||
|
|
||||||
add_completed_email_attachments!(submitter)
|
add_completed_email_attachments!(submitter)
|
||||||
|
|
||||||
@@ -43,8 +55,7 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
end
|
end
|
||||||
|
|
||||||
mail(from: from_address_for_submitter(submitter),
|
mail(from: from_address_for_submitter(submitter),
|
||||||
to: user.friendly_name,
|
to: to || (user.role == 'integration' ? user.friendly_name.sub(/\+\w+@/, '@') : user.friendly_name),
|
||||||
bcc:,
|
|
||||||
subject:)
|
subject:)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -56,13 +67,13 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
|
|
||||||
@documents = add_completed_email_attachments!(submitter)
|
@documents = add_completed_email_attachments!(submitter)
|
||||||
|
|
||||||
@email_config = @current_account.account_configs.find_by(key: AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
|
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
|
||||||
|
|
||||||
subject =
|
subject =
|
||||||
if @email_config
|
if @email_config
|
||||||
ReplaceEmailVariables.call(@email_config.value['subject'], submitter:)
|
ReplaceEmailVariables.call(@email_config.value['subject'], submitter:)
|
||||||
else
|
else
|
||||||
'Your copy of documents'
|
'Your document copy'
|
||||||
end
|
end
|
||||||
|
|
||||||
mail(from: from_address_for_submitter(submitter),
|
mail(from: from_address_for_submitter(submitter),
|
||||||
@@ -75,18 +86,51 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
def add_completed_email_attachments!(submitter)
|
def add_completed_email_attachments!(submitter)
|
||||||
documents = Submitters.select_attachments_for_download(submitter)
|
documents = Submitters.select_attachments_for_download(submitter)
|
||||||
|
|
||||||
documents.each do |attachment|
|
total_size = 0
|
||||||
attachments[attachment.filename.to_s] = attachment.download
|
audit_trail_data = nil
|
||||||
end
|
|
||||||
|
|
||||||
if submitter.submission.audit_trail.present?
|
if submitter.submission.audit_trail.present?
|
||||||
attachments[submitter.submission.audit_trail.filename.to_s] = submitter.submission.audit_trail.download
|
audit_trail_data = submitter.submission.audit_trail.download
|
||||||
|
|
||||||
|
total_size = audit_trail_data.size
|
||||||
|
end
|
||||||
|
|
||||||
|
total_size = add_attachments_with_size_limit(documents, total_size)
|
||||||
|
|
||||||
|
attachments[submitter.submission.audit_trail.filename.to_s] = audit_trail_data if audit_trail_data
|
||||||
|
|
||||||
|
file_fields = submitter.submission.template_fields.select { |e| e['type'].in?(%w[file payment]) }
|
||||||
|
|
||||||
|
if file_fields.pluck('submitter_uuid').uniq.size == 1
|
||||||
|
storage_attachments =
|
||||||
|
submitter.attachments.where(uuid: submitter.values.values_at(*file_fields.pluck('uuid')).flatten)
|
||||||
|
|
||||||
|
add_attachments_with_size_limit(storage_attachments, total_size)
|
||||||
end
|
end
|
||||||
|
|
||||||
documents
|
documents
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def add_attachments_with_size_limit(storage_attachments, current_size)
|
||||||
|
total_size = current_size
|
||||||
|
|
||||||
|
storage_attachments.each do |attachment|
|
||||||
|
total_size += attachment.byte_size
|
||||||
|
|
||||||
|
break if total_size >= MAX_ATTACHMENTS_SIZE
|
||||||
|
|
||||||
|
attachments[attachment.filename.to_s] = attachment.download
|
||||||
|
end
|
||||||
|
|
||||||
|
total_size
|
||||||
|
end
|
||||||
|
|
||||||
def from_address_for_submitter(submitter)
|
def from_address_for_submitter(submitter)
|
||||||
submitter.submission.created_by_user&.friendly_name || submitter.submission.template.author.friendly_name
|
if submitter.submission.source.in?(%w[api embed]) &&
|
||||||
|
(from_email = AccountConfig.find_by(account: submitter.account, key: 'integration_from_email')&.value.presence)
|
||||||
|
from_email
|
||||||
|
else
|
||||||
|
(submitter.submission.created_by_user || submitter.submission.template.author).friendly_name
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class UserMailer < ApplicationMailer
|
class UserMailer < ApplicationMailer
|
||||||
def invitation_email(user)
|
def invitation_email(user, invited_by: nil)
|
||||||
@current_account = user.account
|
@current_account = invited_by&.account || user.account
|
||||||
@user = user
|
@user = user
|
||||||
@token = @user.send(:set_reset_password_token)
|
@token = @user.send(:set_reset_password_token)
|
||||||
|
|
||||||
mail(to: @user.friendly_name,
|
mail(to: @user.friendly_name,
|
||||||
subject: 'You have been invited to Docuseal')
|
subject: 'You are invited to DocuSeal')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
# fk_rails_... (user_id => users.id)
|
# fk_rails_... (user_id => users.id)
|
||||||
#
|
#
|
||||||
class AccessToken < ApplicationRecord
|
class AccessToken < ApplicationRecord
|
||||||
TOKEN_LENGTH = 22
|
TOKEN_LENGTH = 43
|
||||||
|
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class Account < ApplicationRecord
|
|||||||
has_many :users, dependent: :destroy
|
has_many :users, dependent: :destroy
|
||||||
has_many :encrypted_configs, dependent: :destroy
|
has_many :encrypted_configs, dependent: :destroy
|
||||||
has_many :account_configs, dependent: :destroy
|
has_many :account_configs, dependent: :destroy
|
||||||
|
has_many :email_messages, dependent: :destroy
|
||||||
has_many :templates, dependent: :destroy
|
has_many :templates, dependent: :destroy
|
||||||
has_many :template_folders, dependent: :destroy
|
has_many :template_folders, dependent: :destroy
|
||||||
has_one :default_template_folder, -> { where(name: TemplateFolder::DEFAULT_NAME) },
|
has_one :default_template_folder, -> { where(name: TemplateFolder::DEFAULT_NAME) },
|
||||||
|
|||||||
@@ -25,14 +25,17 @@ class AccountConfig < ApplicationRecord
|
|||||||
SUBMITTER_COMPLETED_EMAIL_KEY = 'submitter_completed_email'
|
SUBMITTER_COMPLETED_EMAIL_KEY = 'submitter_completed_email'
|
||||||
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY = 'submitter_documents_copy_email'
|
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY = 'submitter_documents_copy_email'
|
||||||
BCC_EMAILS = 'bcc_emails'
|
BCC_EMAILS = 'bcc_emails'
|
||||||
|
FORCE_MFA = 'force_mfa'
|
||||||
|
ALLOW_TYPED_SIGNATURE = 'allow_typed_signature'
|
||||||
SUBMITTER_REMAILERS = 'submitter_reminders'
|
SUBMITTER_REMAILERS = 'submitter_reminders'
|
||||||
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
|
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
|
||||||
|
ESIGNING_PREFERENCE_KEY = 'esigning_preference'
|
||||||
|
|
||||||
DEFAULT_VALUES = {
|
DEFAULT_VALUES = {
|
||||||
SUBMITTER_INVITATION_EMAIL_KEY => {
|
SUBMITTER_INVITATION_EMAIL_KEY => {
|
||||||
'subject' => 'You have been invited to submit a form',
|
'subject' => 'You are invited to submit a form',
|
||||||
'body' => "Hi there,\n\n" \
|
'body' => "Hi there,\n\n" \
|
||||||
"You have been invited to submit the \"{{template.name}}\" form:\n\n" \
|
"You have been invited to submit the \"{{template.name}}\" form.\n\n" \
|
||||||
"{{submitter.link}}\n\n" \
|
"{{submitter.link}}\n\n" \
|
||||||
"Please contact us by replying to this email if you didn't request this.\n\n" \
|
"Please contact us by replying to this email if you didn't request this.\n\n" \
|
||||||
"Thanks,\n" \
|
"Thanks,\n" \
|
||||||
@@ -45,11 +48,11 @@ class AccountConfig < ApplicationRecord
|
|||||||
'{{submission.link}}'
|
'{{submission.link}}'
|
||||||
},
|
},
|
||||||
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => {
|
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => {
|
||||||
'subject' => 'Your copy of documents',
|
'subject' => 'Your document copy',
|
||||||
'body' => "Hi there,\n\n" \
|
'body' => "Hi there,\n\n" \
|
||||||
"Please check the copy of your \"{{template.name}}\" submission in the email attachments.\n" \
|
"Please check the copy of your \"{{template.name}}\" submission in the email attachments.\n" \
|
||||||
"Alternatively, you can download the copy using:\n\n" \
|
"Alternatively, you can download your copy using:\n\n" \
|
||||||
"{{documents.links}}\n\n" \
|
"{{documents.link}}\n\n" \
|
||||||
"Thanks,\n" \
|
"Thanks,\n" \
|
||||||
'{{account.name}}'
|
'{{account.name}}'
|
||||||
}
|
}
|
||||||
@@ -57,5 +60,5 @@ class AccountConfig < ApplicationRecord
|
|||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
|
|
||||||
serialize :value, JSON
|
serialize :value, coder: JSON
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: email_messages
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# body :text not null
|
||||||
|
# sha1 :string not null
|
||||||
|
# subject :text not null
|
||||||
|
# uuid :string not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# account_id :bigint not null
|
||||||
|
# author_id :bigint not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_email_messages_on_account_id (account_id)
|
||||||
|
# index_email_messages_on_sha1 (sha1)
|
||||||
|
# index_email_messages_on_uuid (uuid)
|
||||||
|
#
|
||||||
|
# Foreign Keys
|
||||||
|
#
|
||||||
|
# fk_rails_... (account_id => accounts.id)
|
||||||
|
# fk_rails_... (author_id => users.id)
|
||||||
|
#
|
||||||
|
class EmailMessage < ApplicationRecord
|
||||||
|
belongs_to :author, class_name: 'User'
|
||||||
|
belongs_to :account
|
||||||
|
|
||||||
|
attribute :uuid, :string, default: -> { SecureRandom.uuid }
|
||||||
|
|
||||||
|
before_validation :set_sha1, on: :create
|
||||||
|
|
||||||
|
def set_sha1
|
||||||
|
self.sha1 = Digest::SHA1.hexdigest({ subject:, body: }.to_json)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -25,6 +25,7 @@ class EncryptedConfig < ApplicationRecord
|
|||||||
FILES_STORAGE_KEY = 'active_storage',
|
FILES_STORAGE_KEY = 'active_storage',
|
||||||
EMAIL_SMTP_KEY = 'action_mailer_smtp',
|
EMAIL_SMTP_KEY = 'action_mailer_smtp',
|
||||||
ESIGN_CERTS_KEY = 'esign_certs',
|
ESIGN_CERTS_KEY = 'esign_certs',
|
||||||
|
TIMESTAMP_SERVER_URL_KEY = 'timestamp_server_url',
|
||||||
APP_URL_KEY = 'app_url',
|
APP_URL_KEY = 'app_url',
|
||||||
WEBHOOK_URL_KEY = 'webhook_url'
|
WEBHOOK_URL_KEY = 'webhook_url'
|
||||||
].freeze
|
].freeze
|
||||||
@@ -33,5 +34,5 @@ class EncryptedConfig < ApplicationRecord
|
|||||||
|
|
||||||
encrypts :value
|
encrypts :value
|
||||||
|
|
||||||
serialize :value, JSON
|
serialize :value, coder: JSON
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: encrypted_user_configs
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# key :string not null
|
||||||
|
# value :text not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# user_id :bigint not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_encrypted_user_configs_on_user_id (user_id)
|
||||||
|
# index_encrypted_user_configs_on_user_id_and_key (user_id,key) UNIQUE
|
||||||
|
#
|
||||||
|
# Foreign Keys
|
||||||
|
#
|
||||||
|
# fk_rails_... (user_id => users.id)
|
||||||
|
#
|
||||||
|
class EncryptedUserConfig < ApplicationRecord
|
||||||
|
belongs_to :user
|
||||||
|
|
||||||
|
encrypts :value
|
||||||
|
|
||||||
|
serialize :value, coder: JSON
|
||||||
|
end
|
||||||
@@ -5,7 +5,9 @@
|
|||||||
# Table name: submissions
|
# Table name: submissions
|
||||||
#
|
#
|
||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
# deleted_at :datetime
|
# archived_at :datetime
|
||||||
|
# preferences :text not null
|
||||||
|
# slug :string not null
|
||||||
# source :text not null
|
# source :text not null
|
||||||
# submitters_order :string not null
|
# submitters_order :string not null
|
||||||
# template_fields :text
|
# template_fields :text
|
||||||
@@ -19,6 +21,7 @@
|
|||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
# index_submissions_on_created_by_user_id (created_by_user_id)
|
# index_submissions_on_created_by_user_id (created_by_user_id)
|
||||||
|
# index_submissions_on_slug (slug) UNIQUE
|
||||||
# index_submissions_on_template_id (template_id)
|
# index_submissions_on_template_id (template_id)
|
||||||
#
|
#
|
||||||
# Foreign Keys
|
# Foreign Keys
|
||||||
@@ -34,20 +37,27 @@ class Submission < ApplicationRecord
|
|||||||
has_many :submitters, dependent: :destroy
|
has_many :submitters, dependent: :destroy
|
||||||
has_many :submission_events, dependent: :destroy
|
has_many :submission_events, dependent: :destroy
|
||||||
|
|
||||||
serialize :template_fields, JSON
|
attribute :preferences, :string, default: -> { {} }
|
||||||
serialize :template_schema, JSON
|
|
||||||
serialize :template_submitters, JSON
|
serialize :template_fields, coder: JSON
|
||||||
|
serialize :template_schema, coder: JSON
|
||||||
|
serialize :template_submitters, coder: JSON
|
||||||
|
serialize :preferences, coder: JSON
|
||||||
|
|
||||||
attribute :source, :string, default: 'link'
|
attribute :source, :string, default: 'link'
|
||||||
attribute :submitters_order, :string, default: 'random'
|
attribute :submitters_order, :string, default: 'random'
|
||||||
|
|
||||||
|
attribute :slug, :string, default: -> { SecureRandom.base58(14) }
|
||||||
|
|
||||||
has_one_attached :audit_trail
|
has_one_attached :audit_trail
|
||||||
|
|
||||||
has_many :template_schema_documents,
|
has_many :template_schema_documents,
|
||||||
->(e) { where(uuid: (e.template_schema.presence || e.template.schema).pluck('attachment_uuid')) },
|
->(e) { where(uuid: (e.template_schema.presence || e.template.schema).pluck('attachment_uuid')) },
|
||||||
through: :template, source: :documents_attachments
|
through: :template, source: :documents_attachments
|
||||||
|
|
||||||
scope :active, -> { where(deleted_at: nil) }
|
scope :active, -> { where(archived_at: nil) }
|
||||||
|
scope :pending, -> { joins(:submitters).where(submitters: { completed_at: nil }).distinct }
|
||||||
|
scope :completed, -> { where.not(id: pending.select(:submission_id)) }
|
||||||
|
|
||||||
enum :source, {
|
enum :source, {
|
||||||
invite: 'invite',
|
invite: 'invite',
|
||||||
@@ -60,4 +70,11 @@ class Submission < ApplicationRecord
|
|||||||
random: 'random',
|
random: 'random',
|
||||||
preserved: 'preserved'
|
preserved: 'preserved'
|
||||||
}, scope: false, prefix: true
|
}, scope: false, prefix: true
|
||||||
|
|
||||||
|
def audit_trail_url
|
||||||
|
return if audit_trail.blank?
|
||||||
|
|
||||||
|
Rails.application.routes.url_helpers.rails_storage_proxy_url(audit_trail, **Docuseal.default_url_options)
|
||||||
|
end
|
||||||
|
alias audit_log_url audit_trail_url
|
||||||
end
|
end
|
||||||
Loaded 100 of 271 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user