mirror of
https://github.com/docusealco/docuseal
synced 2026-11-05 14:42:33 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93f84abce4 | ||
|
|
3c6a32caba | ||
|
|
72c08e5fdb | ||
|
|
022d80b5ce | ||
|
|
f48da51802 | ||
|
|
709d04b33f | ||
|
|
3a251dd063 | ||
|
|
9cc92b59b3 | ||
|
|
e0469b10a7 | ||
|
|
39eb67b162 | ||
|
|
8d9bea3b0f | ||
|
|
7015d8dde8 | ||
|
|
6b54072cb5 | ||
|
|
b898708306 | ||
|
|
1b1cf36839 | ||
|
|
18b89edc19 | ||
|
|
70c7ef4247 | ||
|
|
a8196709be | ||
|
|
a9d249ccf0 | ||
|
|
cd83d918e5 | ||
|
|
b64a84a362 | ||
|
|
f4426a8ee0 | ||
|
|
df8c44d2ce | ||
|
|
65ce3d4822 | ||
|
|
e5b1d94579 | ||
|
|
0b4754ca24 | ||
|
|
b3dc8d55be | ||
|
|
e5ab7667b9 | ||
|
|
2a4e6435f1 | ||
|
|
804431c44a | ||
|
|
f3b26bf7a0 | ||
|
|
aa27d90017 | ||
|
|
0fa8cf2dd0 | ||
|
|
634b5c37ba | ||
|
|
6e16f81fc6 | ||
|
|
ffc4024f70 | ||
|
|
a6981185b2 | ||
|
|
0361fabb66 | ||
|
|
04e7f101be | ||
|
|
86d680cfdf | ||
|
|
7bc2152057 | ||
|
|
74ed9a5040 | ||
|
|
30115fc0dc | ||
|
|
8e8e5e0e29 | ||
|
|
b58ce1b571 | ||
|
|
df5c4e4210 | ||
|
|
ba2e0999f1 | ||
|
|
a1f5dd08c7 | ||
|
|
c1123fef63 | ||
|
|
4dc6149530 | ||
|
|
1245ff2cce | ||
|
|
284204fd78 | ||
|
|
f7be74eb73 | ||
|
|
aeea619059 | ||
|
|
ea2b7f20e7 | ||
|
|
f40f1d25de | ||
|
|
c04bb2d7cf | ||
|
|
4c1ccd65bf | ||
|
|
54e064b0fc | ||
|
|
9a8b72883c | ||
|
|
5d75ee2e47 | ||
|
|
7357fa4871 | ||
|
|
f6850b5427 | ||
|
|
1e9a181e60 | ||
|
|
7224a1ccec | ||
|
|
9b935a8180 | ||
|
|
c4a693846e | ||
|
|
02c7cdcd97 | ||
|
|
36bee92e8e | ||
|
|
6542804f44 | ||
|
|
d805fd614c | ||
|
|
21299dc65f | ||
|
|
0df0946c2a | ||
|
|
1bb38d50a1 | ||
|
|
242987cb41 | ||
|
|
5456abde54 | ||
|
|
4a07427440 | ||
|
|
cf60dae2ff | ||
|
|
659997c6c1 | ||
|
|
389a47a152 | ||
|
|
b6b81ca487 | ||
|
|
719c1786f1 | ||
|
|
4685bac4b3 | ||
|
|
a05275cc08 | ||
|
|
1abf1b1658 | ||
|
|
8893dc5698 | ||
|
|
f8467b2da1 | ||
|
|
f39b8b000e | ||
|
|
961036fa31 | ||
|
|
8b9056894e | ||
|
|
7f979e9396 | ||
|
|
8db1192d0e | ||
|
|
3bd515951a | ||
|
|
2bd5b9676b | ||
|
|
993698ec01 | ||
|
|
fcdc44ddbe | ||
|
|
0cb6b0ae12 | ||
|
|
0388927c5b | ||
|
|
2c8f5d2129 | ||
|
|
2bffbf83c6 | ||
|
|
ec01529014 | ||
|
|
ea1abf5662 | ||
|
|
fcd5263af1 | ||
|
|
1fa8616dbc | ||
|
|
f627277b45 | ||
|
|
809c3ea270 | ||
|
|
9caa9d79d4 | ||
|
|
a8f1c1c323 | ||
|
|
59fbea5c5f | ||
|
|
d3273d879a | ||
|
|
ea227e82c9 | ||
|
|
676b330aa3 | ||
|
|
8d3295e125 | ||
|
|
8cb74e0bcc | ||
|
|
26749a4dbc | ||
|
|
14915d37b4 | ||
|
|
16d6d58efb | ||
|
|
b2b97a313a | ||
|
|
2974e6a64f | ||
|
|
dae34d0aa5 | ||
|
|
e9d0671184 | ||
|
|
897e33bf6a | ||
|
|
f37e93a409 | ||
|
|
561ac63c93 | ||
|
|
149e4be07a | ||
|
|
eb98dc9e06 | ||
|
|
ce7d965358 | ||
|
|
bb73024859 | ||
|
|
631a442e2a | ||
|
|
20a082ae8e |
No files matched your search
@@ -1,13 +1,26 @@
|
|||||||
name: Build Docker Images
|
name: Build Docker Images
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_dispatch:
|
||||||
tags:
|
inputs:
|
||||||
- "*.*.*"
|
version:
|
||||||
|
description: Version
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
image:
|
||||||
|
description: QEMU image
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: tonistiigi/binfmt:latest
|
||||||
|
os:
|
||||||
|
description: OS
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: ubuntu-24.04-arm
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-24.04-arm
|
runs-on: ${{ inputs.os }}
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -16,23 +29,23 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
-
|
- name: Docker meta
|
||||||
name: Docker meta
|
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: |
|
images: docuseal/docuseal
|
||||||
docuseal/docuseal
|
tags: latest,${{ inputs.version }}
|
||||||
tags: |
|
|
||||||
type=semver,pattern={{version}}
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
with:
|
||||||
|
image: ${{ inputs.image }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Create .version file
|
- name: Create .version file
|
||||||
run: echo ${{ github.ref_name }} > .version
|
run: echo ${{ inputs.version }} > .version
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ RSpec/ExampleLength:
|
|||||||
RSpec/MultipleMemoizedHelpers:
|
RSpec/MultipleMemoizedHelpers:
|
||||||
Max: 15
|
Max: 15
|
||||||
|
|
||||||
|
RSpec/AnyInstance:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
Metrics/BlockNesting:
|
Metrics/BlockNesting:
|
||||||
Max: 5
|
Max: 5
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -50,7 +50,7 @@ ENV OPENSSL_CONF=/app/openssl_legacy.cnf
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev@edge redis libheif@edge vips-heif@edge gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev@edge yaml-dev redis libheif@edge vips-heif@edge gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
||||||
|
|
||||||
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
|
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
|
||||||
\n\
|
\n\
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ gem 'azure-storage-blob', require: false
|
|||||||
gem 'bootsnap', require: false
|
gem 'bootsnap', require: false
|
||||||
gem 'cancancan'
|
gem 'cancancan'
|
||||||
gem 'csv'
|
gem 'csv'
|
||||||
|
gem 'csv-safe'
|
||||||
gem 'devise'
|
gem 'devise'
|
||||||
gem 'devise-two-factor'
|
gem 'devise-two-factor'
|
||||||
gem 'dotenv', require: false
|
gem 'dotenv', require: false
|
||||||
|
|||||||
+104
-91
@@ -1,29 +1,29 @@
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (8.0.1)
|
actioncable (8.0.2.1)
|
||||||
actionpack (= 8.0.1)
|
actionpack (= 8.0.2.1)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.0.2.1)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
actionmailbox (8.0.1)
|
actionmailbox (8.0.2.1)
|
||||||
actionpack (= 8.0.1)
|
actionpack (= 8.0.2.1)
|
||||||
activejob (= 8.0.1)
|
activejob (= 8.0.2.1)
|
||||||
activerecord (= 8.0.1)
|
activerecord (= 8.0.2.1)
|
||||||
activestorage (= 8.0.1)
|
activestorage (= 8.0.2.1)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.0.2.1)
|
||||||
mail (>= 2.8.0)
|
mail (>= 2.8.0)
|
||||||
actionmailer (8.0.1)
|
actionmailer (8.0.2.1)
|
||||||
actionpack (= 8.0.1)
|
actionpack (= 8.0.2.1)
|
||||||
actionview (= 8.0.1)
|
actionview (= 8.0.2.1)
|
||||||
activejob (= 8.0.1)
|
activejob (= 8.0.2.1)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.0.2.1)
|
||||||
mail (>= 2.8.0)
|
mail (>= 2.8.0)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
actionpack (8.0.1)
|
actionpack (8.0.2.1)
|
||||||
actionview (= 8.0.1)
|
actionview (= 8.0.2.1)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.0.2.1)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
rack (>= 2.2.4)
|
rack (>= 2.2.4)
|
||||||
rack-session (>= 1.0.1)
|
rack-session (>= 1.0.1)
|
||||||
@@ -31,35 +31,35 @@ GEM
|
|||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.6)
|
rails-html-sanitizer (~> 1.6)
|
||||||
useragent (~> 0.16)
|
useragent (~> 0.16)
|
||||||
actiontext (8.0.1)
|
actiontext (8.0.2.1)
|
||||||
actionpack (= 8.0.1)
|
actionpack (= 8.0.2.1)
|
||||||
activerecord (= 8.0.1)
|
activerecord (= 8.0.2.1)
|
||||||
activestorage (= 8.0.1)
|
activestorage (= 8.0.2.1)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.0.2.1)
|
||||||
globalid (>= 0.6.0)
|
globalid (>= 0.6.0)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (8.0.1)
|
actionview (8.0.2.1)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.0.2.1)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.11)
|
erubi (~> 1.11)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.6)
|
rails-html-sanitizer (~> 1.6)
|
||||||
activejob (8.0.1)
|
activejob (8.0.2.1)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.0.2.1)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (8.0.1)
|
activemodel (8.0.2.1)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.0.2.1)
|
||||||
activerecord (8.0.1)
|
activerecord (8.0.2.1)
|
||||||
activemodel (= 8.0.1)
|
activemodel (= 8.0.2.1)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.0.2.1)
|
||||||
timeout (>= 0.4.0)
|
timeout (>= 0.4.0)
|
||||||
activestorage (8.0.1)
|
activestorage (8.0.2.1)
|
||||||
actionpack (= 8.0.1)
|
actionpack (= 8.0.2.1)
|
||||||
activejob (= 8.0.1)
|
activejob (= 8.0.2.1)
|
||||||
activerecord (= 8.0.1)
|
activerecord (= 8.0.2.1)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.0.2.1)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
activesupport (8.0.1)
|
activesupport (8.0.2.1)
|
||||||
base64
|
base64
|
||||||
benchmark (>= 0.3)
|
benchmark (>= 0.3)
|
||||||
bigdecimal
|
bigdecimal
|
||||||
@@ -104,9 +104,9 @@ 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.2.0)
|
base64 (0.3.0)
|
||||||
bcrypt (3.1.20)
|
bcrypt (3.1.20)
|
||||||
benchmark (0.4.0)
|
benchmark (0.4.1)
|
||||||
better_html (2.1.1)
|
better_html (2.1.1)
|
||||||
actionview (>= 6.0)
|
actionview (>= 6.0)
|
||||||
activesupport (>= 6.0)
|
activesupport (>= 6.0)
|
||||||
@@ -114,7 +114,7 @@ GEM
|
|||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
parser (>= 2.4)
|
parser (>= 2.4)
|
||||||
smart_properties
|
smart_properties
|
||||||
bigdecimal (3.1.8)
|
bigdecimal (3.2.2)
|
||||||
bindex (0.8.1)
|
bindex (0.8.1)
|
||||||
bootsnap (1.18.4)
|
bootsnap (1.18.4)
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
@@ -141,8 +141,8 @@ GEM
|
|||||||
cldr-plurals-runtime-rb (1.1.0)
|
cldr-plurals-runtime-rb (1.1.0)
|
||||||
cmdparse (3.0.7)
|
cmdparse (3.0.7)
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
concurrent-ruby (1.3.4)
|
concurrent-ruby (1.3.5)
|
||||||
connection_pool (2.4.1)
|
connection_pool (2.5.3)
|
||||||
crack (1.0.0)
|
crack (1.0.0)
|
||||||
bigdecimal
|
bigdecimal
|
||||||
rexml
|
rexml
|
||||||
@@ -150,6 +150,8 @@ GEM
|
|||||||
css_parser (1.21.0)
|
css_parser (1.21.0)
|
||||||
addressable
|
addressable
|
||||||
csv (3.3.2)
|
csv (3.3.2)
|
||||||
|
csv-safe (3.3.1)
|
||||||
|
csv (~> 3.0)
|
||||||
cuprite (0.15.1)
|
cuprite (0.15.1)
|
||||||
capybara (~> 3.0)
|
capybara (~> 3.0)
|
||||||
ferrum (~> 0.15.0)
|
ferrum (~> 0.15.0)
|
||||||
@@ -174,8 +176,9 @@ GEM
|
|||||||
rake (>= 12.0.0, < 14.0.0)
|
rake (>= 12.0.0, < 14.0.0)
|
||||||
docile (1.4.1)
|
docile (1.4.1)
|
||||||
dotenv (3.1.7)
|
dotenv (3.1.7)
|
||||||
drb (2.2.1)
|
drb (2.2.3)
|
||||||
email_typo (0.2.3)
|
email_typo (0.2.3)
|
||||||
|
erb (5.0.2)
|
||||||
erb_lint (0.7.0)
|
erb_lint (0.7.0)
|
||||||
activesupport
|
activesupport
|
||||||
better_html (>= 2.0.1)
|
better_html (>= 2.0.1)
|
||||||
@@ -270,23 +273,25 @@ GEM
|
|||||||
os (>= 0.9, < 2.0)
|
os (>= 0.9, < 2.0)
|
||||||
signet (>= 0.16, < 2.a)
|
signet (>= 0.16, < 2.a)
|
||||||
hashdiff (1.1.2)
|
hashdiff (1.1.2)
|
||||||
hexapdf (1.0.3)
|
hexapdf (1.4.0)
|
||||||
cmdparse (~> 3.0, >= 3.0.3)
|
cmdparse (~> 3.0, >= 3.0.3)
|
||||||
geom2d (~> 0.4, >= 0.4.1)
|
geom2d (~> 0.4, >= 0.4.1)
|
||||||
openssl (>= 2.2.1)
|
openssl (>= 2.2.1)
|
||||||
|
strscan (>= 3.1.2)
|
||||||
htmlentities (4.3.4)
|
htmlentities (4.3.4)
|
||||||
httpclient (2.8.3)
|
httpclient (2.8.3)
|
||||||
i18n (1.14.6)
|
i18n (1.14.7)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
image_processing (1.13.0)
|
image_processing (1.13.0)
|
||||||
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.8.0)
|
io-console (0.8.1)
|
||||||
irb (1.14.3)
|
irb (1.15.2)
|
||||||
|
pp (>= 0.6.0)
|
||||||
rdoc (>= 4.0.0)
|
rdoc (>= 4.0.0)
|
||||||
reline (>= 0.4.2)
|
reline (>= 0.4.2)
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.9.1)
|
json (2.13.2)
|
||||||
jwt (2.9.3)
|
jwt (2.9.3)
|
||||||
base64
|
base64
|
||||||
language_server-protocol (3.17.0.3)
|
language_server-protocol (3.17.0.3)
|
||||||
@@ -300,13 +305,13 @@ GEM
|
|||||||
letter_opener (~> 1.9)
|
letter_opener (~> 1.9)
|
||||||
railties (>= 6.1)
|
railties (>= 6.1)
|
||||||
rexml
|
rexml
|
||||||
logger (1.6.4)
|
logger (1.7.0)
|
||||||
lograge (0.14.0)
|
lograge (0.14.0)
|
||||||
actionpack (>= 4)
|
actionpack (>= 4)
|
||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
railties (>= 4)
|
railties (>= 4)
|
||||||
request_store (~> 1.0)
|
request_store (~> 1.0)
|
||||||
loofah (2.23.1)
|
loofah (2.24.1)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
mail (2.8.1)
|
mail (2.8.1)
|
||||||
@@ -319,8 +324,8 @@ GEM
|
|||||||
method_source (1.1.0)
|
method_source (1.1.0)
|
||||||
mini_magick (4.13.2)
|
mini_magick (4.13.2)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.8)
|
mini_portile2 (2.8.9)
|
||||||
minitest (5.25.4)
|
minitest (5.25.5)
|
||||||
msgpack (1.7.5)
|
msgpack (1.7.5)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multipart-post (2.4.1)
|
multipart-post (2.4.1)
|
||||||
@@ -328,36 +333,36 @@ GEM
|
|||||||
mysql2 (0.5.6)
|
mysql2 (0.5.6)
|
||||||
net-http-persistent (4.0.5)
|
net-http-persistent (4.0.5)
|
||||||
connection_pool (~> 2.2)
|
connection_pool (~> 2.2)
|
||||||
net-imap (0.5.6)
|
net-imap (0.5.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.2)
|
net-protocol (0.2.2)
|
||||||
timeout
|
timeout
|
||||||
net-smtp (0.5.0)
|
net-smtp (0.5.1)
|
||||||
net-protocol
|
net-protocol
|
||||||
nio4r (2.7.4)
|
nio4r (2.7.4)
|
||||||
nokogiri (1.18.8)
|
nokogiri (1.18.9)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.8-aarch64-linux-gnu)
|
nokogiri (1.18.9-aarch64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.8-aarch64-linux-musl)
|
nokogiri (1.18.9-aarch64-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.8-arm64-darwin)
|
nokogiri (1.18.9-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.8-x86_64-linux-gnu)
|
nokogiri (1.18.9-x86_64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.8-x86_64-linux-musl)
|
nokogiri (1.18.9-x86_64-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
oj (3.16.8)
|
oj (3.16.11)
|
||||||
bigdecimal (>= 3.0)
|
bigdecimal (>= 3.0)
|
||||||
ostruct (>= 0.2)
|
ostruct (>= 0.2)
|
||||||
openssl (3.3.0)
|
openssl (3.3.0)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
ostruct (0.6.1)
|
ostruct (0.6.3)
|
||||||
package_json (0.1.0)
|
package_json (0.1.0)
|
||||||
pagy (9.3.3)
|
pagy (9.3.3)
|
||||||
parallel (1.26.3)
|
parallel (1.26.3)
|
||||||
@@ -365,6 +370,8 @@ GEM
|
|||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
pg (1.5.9)
|
pg (1.5.9)
|
||||||
|
pp (0.6.2)
|
||||||
|
prettyprint
|
||||||
premailer (1.27.0)
|
premailer (1.27.0)
|
||||||
addressable
|
addressable
|
||||||
css_parser (>= 1.19.0)
|
css_parser (>= 1.19.0)
|
||||||
@@ -375,42 +382,44 @@ GEM
|
|||||||
premailer (~> 1.7, >= 1.7.9)
|
premailer (~> 1.7, >= 1.7.9)
|
||||||
pretender (0.5.0)
|
pretender (0.5.0)
|
||||||
actionpack (>= 6.1)
|
actionpack (>= 6.1)
|
||||||
|
prettyprint (0.2.0)
|
||||||
pry (0.15.0)
|
pry (0.15.0)
|
||||||
coderay (~> 1.1)
|
coderay (~> 1.1)
|
||||||
method_source (~> 1.0)
|
method_source (~> 1.0)
|
||||||
pry-rails (0.3.11)
|
pry-rails (0.3.11)
|
||||||
pry (>= 0.13.0)
|
pry (>= 0.13.0)
|
||||||
psych (5.2.2)
|
psych (5.2.6)
|
||||||
date
|
date
|
||||||
stringio
|
stringio
|
||||||
public_suffix (6.0.1)
|
public_suffix (6.0.1)
|
||||||
puma (6.5.0)
|
puma (6.5.0)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rack (3.1.16)
|
rack (3.2.0)
|
||||||
rack-proxy (0.7.7)
|
rack-proxy (0.7.7)
|
||||||
rack
|
rack
|
||||||
rack-session (2.0.0)
|
rack-session (2.1.1)
|
||||||
|
base64 (>= 0.1.0)
|
||||||
rack (>= 3.0.0)
|
rack (>= 3.0.0)
|
||||||
rack-test (2.1.0)
|
rack-test (2.2.0)
|
||||||
rack (>= 1.3)
|
rack (>= 1.3)
|
||||||
rackup (2.2.1)
|
rackup (2.2.1)
|
||||||
rack (>= 3)
|
rack (>= 3)
|
||||||
rails (8.0.1)
|
rails (8.0.2.1)
|
||||||
actioncable (= 8.0.1)
|
actioncable (= 8.0.2.1)
|
||||||
actionmailbox (= 8.0.1)
|
actionmailbox (= 8.0.2.1)
|
||||||
actionmailer (= 8.0.1)
|
actionmailer (= 8.0.2.1)
|
||||||
actionpack (= 8.0.1)
|
actionpack (= 8.0.2.1)
|
||||||
actiontext (= 8.0.1)
|
actiontext (= 8.0.2.1)
|
||||||
actionview (= 8.0.1)
|
actionview (= 8.0.2.1)
|
||||||
activejob (= 8.0.1)
|
activejob (= 8.0.2.1)
|
||||||
activemodel (= 8.0.1)
|
activemodel (= 8.0.2.1)
|
||||||
activerecord (= 8.0.1)
|
activerecord (= 8.0.2.1)
|
||||||
activestorage (= 8.0.1)
|
activestorage (= 8.0.2.1)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.0.2.1)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 8.0.1)
|
railties (= 8.0.2.1)
|
||||||
rails-dom-testing (2.2.0)
|
rails-dom-testing (2.3.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
minitest
|
minitest
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
@@ -424,22 +433,23 @@ GEM
|
|||||||
actionview (> 3.1)
|
actionview (> 3.1)
|
||||||
activesupport (> 3.1)
|
activesupport (> 3.1)
|
||||||
railties (> 3.1)
|
railties (> 3.1)
|
||||||
railties (8.0.1)
|
railties (8.0.2.1)
|
||||||
actionpack (= 8.0.1)
|
actionpack (= 8.0.2.1)
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.0.2.1)
|
||||||
irb (~> 1.13)
|
irb (~> 1.13)
|
||||||
rackup (>= 1.0.0)
|
rackup (>= 1.0.0)
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0, >= 1.2.2)
|
thor (~> 1.0, >= 1.2.2)
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.2.1)
|
rake (13.3.0)
|
||||||
rdoc (6.10.0)
|
rdoc (6.14.2)
|
||||||
|
erb
|
||||||
psych (>= 4.0.0)
|
psych (>= 4.0.0)
|
||||||
redis-client (0.23.0)
|
redis-client (0.23.0)
|
||||||
connection_pool
|
connection_pool
|
||||||
regexp_parser (2.9.3)
|
regexp_parser (2.9.3)
|
||||||
reline (0.6.0)
|
reline (0.6.2)
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
representable (3.2.0)
|
representable (3.2.0)
|
||||||
declarative (< 0.1.0)
|
declarative (< 0.1.0)
|
||||||
@@ -451,7 +461,7 @@ GEM
|
|||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
retriable (3.1.2)
|
retriable (3.1.2)
|
||||||
rexml (3.4.0)
|
rexml (3.4.4)
|
||||||
rotp (6.3.0)
|
rotp (6.3.0)
|
||||||
rouge (4.5.2)
|
rouge (4.5.2)
|
||||||
rqrcode (2.2.0)
|
rqrcode (2.2.0)
|
||||||
@@ -538,10 +548,11 @@ GEM
|
|||||||
sqlite3 (2.5.0-arm64-darwin)
|
sqlite3 (2.5.0-arm64-darwin)
|
||||||
sqlite3 (2.5.0-x86_64-linux-gnu)
|
sqlite3 (2.5.0-x86_64-linux-gnu)
|
||||||
sqlite3 (2.5.0-x86_64-linux-musl)
|
sqlite3 (2.5.0-x86_64-linux-musl)
|
||||||
stringio (3.1.2)
|
stringio (3.1.7)
|
||||||
strip_attributes (1.14.1)
|
strip_attributes (1.14.1)
|
||||||
activemodel (>= 3.0, < 9.0)
|
activemodel (>= 3.0, < 9.0)
|
||||||
thor (1.3.2)
|
strscan (3.1.5)
|
||||||
|
thor (1.4.0)
|
||||||
timeout (0.4.3)
|
timeout (0.4.3)
|
||||||
trailblazer-option (0.1.2)
|
trailblazer-option (0.1.2)
|
||||||
turbo-rails (2.0.11)
|
turbo-rails (2.0.11)
|
||||||
@@ -574,12 +585,13 @@ GEM
|
|||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff (>= 0.4.0, < 2.0.0)
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
webrick (1.9.1)
|
webrick (1.9.1)
|
||||||
websocket-driver (0.7.6)
|
websocket-driver (0.8.0)
|
||||||
|
base64
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
xpath (3.2.0)
|
xpath (3.2.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
zeitwerk (2.7.1)
|
zeitwerk (2.7.3)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
aarch64-linux
|
aarch64-linux
|
||||||
@@ -602,6 +614,7 @@ DEPENDENCIES
|
|||||||
cancancan
|
cancancan
|
||||||
capybara
|
capybara
|
||||||
csv
|
csv
|
||||||
|
csv-safe
|
||||||
cuprite
|
cuprite
|
||||||
debug
|
debug
|
||||||
devise
|
devise
|
||||||
|
|||||||
@@ -15,9 +15,11 @@ class AccountConfigsController < ApplicationController
|
|||||||
AccountConfig::ESIGNING_PREFERENCE_KEY,
|
AccountConfig::ESIGNING_PREFERENCE_KEY,
|
||||||
AccountConfig::FORM_WITH_CONFETTI_KEY,
|
AccountConfig::FORM_WITH_CONFETTI_KEY,
|
||||||
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
|
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
|
||||||
|
AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY,
|
||||||
AccountConfig::FORCE_SSO_AUTH_KEY,
|
AccountConfig::FORCE_SSO_AUTH_KEY,
|
||||||
AccountConfig::FLATTEN_RESULT_PDF_KEY,
|
AccountConfig::FLATTEN_RESULT_PDF_KEY,
|
||||||
AccountConfig::ENFORCE_SIGNING_ORDER_KEY,
|
AccountConfig::ENFORCE_SIGNING_ORDER_KEY,
|
||||||
|
AccountConfig::WITH_FILE_LINKS_KEY,
|
||||||
AccountConfig::WITH_SIGNATURE_ID,
|
AccountConfig::WITH_SIGNATURE_ID,
|
||||||
AccountConfig::COMBINE_PDF_RESULT_KEY,
|
AccountConfig::COMBINE_PDF_RESULT_KEY,
|
||||||
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
|
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class AccountsController < ApplicationController
|
|||||||
unless URI.parse(@encrypted_config.value.to_s).class.in?([URI::HTTP, URI::HTTPS])
|
unless URI.parse(@encrypted_config.value.to_s).class.in?([URI::HTTP, URI::HTTPS])
|
||||||
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url'))
|
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url'))
|
||||||
|
|
||||||
return render :show, status: :unprocessable_entity
|
return render :show, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
@encrypted_config.save!
|
@encrypted_config.save!
|
||||||
@@ -39,7 +39,7 @@ class AccountsController < ApplicationController
|
|||||||
redirect_to settings_account_path, notice: I18n.t('account_information_has_been_updated')
|
redirect_to settings_account_path, notice: I18n.t('account_information_has_been_updated')
|
||||||
end
|
end
|
||||||
rescue ActiveRecord::RecordInvalid
|
rescue ActiveRecord::RecordInvalid
|
||||||
render :show, status: :unprocessable_entity
|
render :show, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ module Api
|
|||||||
def show
|
def show
|
||||||
blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
|
blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
|
||||||
|
|
||||||
if blob_uuid.blank? || (purp.present? && purp != 'blob') || (exp && exp < Time.current.to_i)
|
if blob_uuid.blank? || purp != 'blob'
|
||||||
Rollbar.error('Blob not found') if defined?(Rollbar)
|
Rollbar.error('Blob not found') if defined?(Rollbar)
|
||||||
|
|
||||||
return head :not_found
|
return head :not_found
|
||||||
@@ -24,8 +24,9 @@ module Api
|
|||||||
attachment = blob.attachments.take
|
attachment = blob.attachments.take
|
||||||
|
|
||||||
@record = attachment.record
|
@record = attachment.record
|
||||||
|
@record = @record.record if @record.is_a?(ActiveStorage::Attachment)
|
||||||
|
|
||||||
authorization_check!(attachment) if exp.blank?
|
authorization_check!(attachment, @record, exp)
|
||||||
|
|
||||||
if request.headers['Range'].present?
|
if request.headers['Range'].present?
|
||||||
send_blob_byte_range_data blob, request.headers['Range']
|
send_blob_byte_range_data blob, request.headers['Range']
|
||||||
@@ -41,16 +42,22 @@ module Api
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def authorization_check!(attachment)
|
def authorization_check!(attachment, record, exp)
|
||||||
is_authorized = attachment.name.in?(%w[logo preview_images]) ||
|
return if attachment.name == 'logo'
|
||||||
(current_user && attachment.record.account.id == current_user.account_id) ||
|
return if exp.to_i >= Time.current.to_i
|
||||||
(current_user && !Docuseal.multitenant? && current_user.role == 'superadmin') ||
|
return if current_user && current_ability.can?(:read, record)
|
||||||
!attachment.record.account.account_configs
|
|
||||||
.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value
|
|
||||||
|
|
||||||
return if is_authorized
|
if exp.blank?
|
||||||
|
configs = record.account.account_configs.where(key: [AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
|
||||||
|
AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY])
|
||||||
|
|
||||||
Rollbar.error('Blob aunauthorized') if defined?(Rollbar)
|
require_auth = configs.any? { |c| c.key == AccountConfig::DOWNLOAD_LINKS_AUTH_KEY && c.value }
|
||||||
|
require_ttl = configs.none? { |c| c.key == AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY && c.value == false }
|
||||||
|
|
||||||
|
return if !require_ttl && !require_auth
|
||||||
|
end
|
||||||
|
|
||||||
|
Rollbar.error('Blob unauthorized') if defined?(Rollbar)
|
||||||
|
|
||||||
raise CanCan::AccessDenied
|
raise CanCan::AccessDenied
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ module Api
|
|||||||
check_authorization
|
check_authorization
|
||||||
|
|
||||||
rescue_from Params::BaseValidator::InvalidParameterError do |e|
|
rescue_from Params::BaseValidator::InvalidParameterError do |e|
|
||||||
render json: { error: e.message }, status: :unprocessable_entity
|
render json: { error: e.message }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
rescue_from RateLimit::LimitApproached do |e|
|
rescue_from RateLimit::LimitApproached do |e|
|
||||||
@@ -33,7 +33,7 @@ module Api
|
|||||||
rescue_from JSON::ParserError do |e|
|
rescue_from JSON::ParserError do |e|
|
||||||
Rollbar.warning(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
render json: { error: "JSON parse error: #{e.message}" }, status: :unprocessable_entity
|
render json: { error: "JSON parse error: #{e.message}" }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,21 @@ module Api
|
|||||||
def create
|
def create
|
||||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
if params[:type].in?(%w[initials signature]) && ImageUtils.blank?(Vips::Image.new_from_file(params[:file].path))
|
if params[:type].in?(%w[initials signature])
|
||||||
|
image = Vips::Image.new_from_file(params[:file].path)
|
||||||
|
|
||||||
|
if ImageUtils.blank?(image)
|
||||||
Rollbar.error("Empty signature: #{submitter.id}") if defined?(Rollbar)
|
Rollbar.error("Empty signature: #{submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_entity
|
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_content
|
||||||
|
end
|
||||||
|
|
||||||
|
if ImageUtils.error?(image)
|
||||||
|
Rollbar.error("Error signature: #{submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return render json: { error: "#{params[:type]} error, try to sign on another device" },
|
||||||
|
status: :unprocessable_content
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
attachment = Submitters.create_attachment!(submitter, params)
|
attachment = Submitters.create_attachment!(submitter, params)
|
||||||
|
|||||||
@@ -18,12 +18,14 @@ module Api
|
|||||||
field: :completed_at
|
field: :completed_at
|
||||||
)
|
)
|
||||||
|
|
||||||
|
expires_at = Accounts.link_expires_at(current_account)
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
data: submitters.map do |s|
|
data: submitters.map do |s|
|
||||||
{
|
{
|
||||||
event_type: 'form.completed',
|
event_type: 'form.completed',
|
||||||
timestamp: s.completed_at,
|
timestamp: s.completed_at,
|
||||||
data: Submitters::SerializeForWebhook.call(s)
|
data: Submitters::SerializeForWebhook.call(s, expires_at:)
|
||||||
}
|
}
|
||||||
end,
|
end,
|
||||||
pagination: {
|
pagination: {
|
||||||
|
|||||||
@@ -34,10 +34,12 @@ module Api
|
|||||||
associations: [:blob]
|
associations: [:blob]
|
||||||
).call
|
).call
|
||||||
|
|
||||||
|
expires_at = Accounts.link_expires_at(current_account)
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
id: @submission.id,
|
id: @submission.id,
|
||||||
documents: documents.map do |attachment|
|
documents: documents.map do |attachment|
|
||||||
{ name: attachment.filename.base, url: ActiveStorage::Blob.proxy_url(attachment.blob) }
|
{ name: attachment.filename.base, url: ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) }
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -14,16 +14,19 @@ module Api
|
|||||||
:created_by_user, :submission_events,
|
:created_by_user, :submission_events,
|
||||||
template: :folder,
|
template: :folder,
|
||||||
submitters: { documents_attachments: :blob, attachments_attachments: :blob },
|
submitters: { documents_attachments: :blob, attachments_attachments: :blob },
|
||||||
audit_trail_attachment: :blob
|
audit_trail_attachment: :blob,
|
||||||
|
combined_document_attachment: :blob
|
||||||
),
|
),
|
||||||
field: :completed_at)
|
field: :completed_at)
|
||||||
|
|
||||||
|
expires_at = Accounts.link_expires_at(current_account)
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
data: submissions.map do |s|
|
data: submissions.map do |s|
|
||||||
{
|
{
|
||||||
event_type: 'submission.completed',
|
event_type: 'submission.completed',
|
||||||
timestamp: s.completed_at,
|
timestamp: s.completed_at,
|
||||||
data: Submissions::SerializeForApi.call(s, s.submitters)
|
data: Submissions::SerializeForApi.call(s, s.submitters, expires_at:)
|
||||||
}
|
}
|
||||||
end,
|
end,
|
||||||
pagination: {
|
pagination: {
|
||||||
|
|||||||
@@ -18,10 +18,12 @@ module Api
|
|||||||
combined_document_attachment: :blob,
|
combined_document_attachment: :blob,
|
||||||
audit_trail_attachment: :blob))
|
audit_trail_attachment: :blob))
|
||||||
|
|
||||||
|
expires_at = Accounts.link_expires_at(current_account)
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
data: submissions.map do |s|
|
data: submissions.map do |s|
|
||||||
Submissions::SerializeForApi.call(s, s.submitters, params,
|
Submissions::SerializeForApi.call(s, s.submitters, params,
|
||||||
with_events: false, with_documents: false, with_values: false)
|
with_events: false, with_documents: false, with_values: false, expires_at:)
|
||||||
end,
|
end,
|
||||||
pagination: {
|
pagination: {
|
||||||
count: submissions.size,
|
count: submissions.size,
|
||||||
@@ -41,7 +43,7 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
if @submission.audit_trail_attachment.blank? && submitters.all?(&:completed_at?)
|
if @submission.audit_trail_attachment.blank? && submitters.all?(&:completed_at?)
|
||||||
@submission.audit_trail_attachment = Submissions::GenerateAuditTrail.call(@submission)
|
@submission.audit_trail_attachment = Submissions::EnsureAuditGenerated.call(@submission)
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: Submissions::SerializeForApi.call(@submission, submitters, params)
|
render json: Submissions::SerializeForApi.call(@submission, submitters, params)
|
||||||
@@ -50,12 +52,12 @@ module Api
|
|||||||
def create
|
def create
|
||||||
Params::SubmissionCreateValidator.call(params)
|
Params::SubmissionCreateValidator.call(params)
|
||||||
|
|
||||||
return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil?
|
return render json: { error: 'Template not found' }, status: :unprocessable_content if @template.nil?
|
||||||
|
|
||||||
if @template.fields.blank?
|
if @template.fields.blank?
|
||||||
Rollbar.warning("Template does not contain fields: #{@template.id}") if defined?(Rollbar)
|
Rollbar.warning("Template does not contain fields: #{@template.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
return render json: { error: 'Template does not contain fields' }, status: :unprocessable_entity
|
return render json: { error: 'Template does not contain fields' }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
params[:send_email] = true unless params.key?(:send_email)
|
params[:send_email] = true unless params.key?(:send_email)
|
||||||
@@ -82,7 +84,7 @@ module Api
|
|||||||
DownloadUtils::UnableToDownload => e
|
DownloadUtils::UnableToDownload => e
|
||||||
Rollbar.warning(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
render json: { error: e.message }, status: :unprocessable_entity
|
render json: { error: e.message }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@@ -182,14 +184,15 @@ module Api
|
|||||||
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
|
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
|
||||||
:require_phone_2fa, :expire_at, :name,
|
:require_phone_2fa, :expire_at, :name,
|
||||||
{
|
{
|
||||||
|
variables: {},
|
||||||
message: %i[subject body],
|
message: %i[subject body],
|
||||||
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||||
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
|
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
|
||||||
:require_phone_2fa,
|
:require_phone_2fa, :order,
|
||||||
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
|
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
|
||||||
fields: [:name, :uuid, :default_value, :value, :title, :description,
|
fields: [:name, :uuid, :default_value, :value, :title, :description,
|
||||||
:readonly, :required, :validation_pattern, :invalid_message,
|
:readonly, :required, :validation_pattern, :invalid_message,
|
||||||
{ default_value: [], value: [], preferences: {} }] }]]
|
{ default_value: [], value: [], preferences: {}, validation: {} }] }]]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ module Api
|
|||||||
documents_attachments: :blob, attachments_attachments: :blob)
|
documents_attachments: :blob, attachments_attachments: :blob)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
expires_at = Accounts.link_expires_at(current_account)
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
data: submitters.map do |s|
|
data: submitters.map do |s|
|
||||||
Submitters::SerializeForApi.call(s, with_template: true, with_events: true, params:)
|
Submitters::SerializeForApi.call(s, with_template: true, with_events: true, params:, expires_at:)
|
||||||
end,
|
end,
|
||||||
pagination: {
|
pagination: {
|
||||||
count: submitters.size,
|
count: submitters.size,
|
||||||
@@ -34,7 +36,7 @@ module Api
|
|||||||
|
|
||||||
def update
|
def update
|
||||||
if @submitter.completed_at?
|
if @submitter.completed_at?
|
||||||
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_entity
|
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
submission = @submitter.submission
|
submission = @submitter.submission
|
||||||
@@ -74,7 +76,7 @@ module Api
|
|||||||
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
|
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
|
||||||
Rollbar.warning(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
render json: { error: e.message }, status: :unprocessable_entity
|
render json: { error: e.message }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
def submitter_params
|
def submitter_params
|
||||||
|
|||||||
@@ -26,13 +26,15 @@ module Api
|
|||||||
original_template: @template,
|
original_template: @template,
|
||||||
documents: params[:documents])
|
documents: params[:documents])
|
||||||
|
|
||||||
|
Templates.maybe_assign_access(cloned_template)
|
||||||
|
|
||||||
cloned_template.save!
|
cloned_template.save!
|
||||||
|
|
||||||
WebhookUrls.enqueue_events(cloned_template, 'template.created')
|
WebhookUrls.enqueue_events(cloned_template, 'template.created')
|
||||||
|
|
||||||
SearchEntries.enqueue_reindex(cloned_template)
|
SearchEntries.enqueue_reindex(cloned_template)
|
||||||
|
|
||||||
render json: Templates::SerializeForApi.call(cloned_template, schema_documents)
|
render json: Templates::SerializeForApi.call(cloned_template, schema_documents:)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -24,13 +24,14 @@ module Api
|
|||||||
name: :preview_images)
|
name: :preview_images)
|
||||||
.preload(:blob)
|
.preload(:blob)
|
||||||
|
|
||||||
|
expires_at = Accounts.link_expires_at(current_account)
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
data: templates.map do |t|
|
data: templates.map do |t|
|
||||||
Templates::SerializeForApi.call(
|
Templates::SerializeForApi.call(t,
|
||||||
t,
|
schema_documents: schema_documents.select { |e| e.record_id == t.id },
|
||||||
schema_documents.select { |e| e.record_id == t.id },
|
preview_image_attachments:,
|
||||||
preview_image_attachments
|
expires_at:)
|
||||||
)
|
|
||||||
end,
|
end,
|
||||||
pagination: {
|
pagination: {
|
||||||
count: templates.size,
|
count: templates.size,
|
||||||
@@ -106,14 +107,15 @@ module Api
|
|||||||
:external_id,
|
:external_id,
|
||||||
:shared_link,
|
:shared_link,
|
||||||
{
|
{
|
||||||
submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email]],
|
submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email order]],
|
||||||
fields: [[:uuid, :submitter_uuid, :name, :type,
|
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||||
:required, :readonly, :default_value,
|
:required, :readonly, :default_value,
|
||||||
:title, :description,
|
:title, :description, :prefillable,
|
||||||
{ preferences: {},
|
{ preferences: {},
|
||||||
|
default_value: [],
|
||||||
conditions: [%i[field_uuid value action operation]],
|
conditions: [%i[field_uuid value action operation]],
|
||||||
options: [%i[value uuid]],
|
options: [%i[value uuid]],
|
||||||
validation: %i[message pattern],
|
validation: %i[message pattern min max step],
|
||||||
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
|
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ module Api
|
|||||||
def merge
|
def merge
|
||||||
files = params[:files] || []
|
files = params[:files] || []
|
||||||
|
|
||||||
return render json: { error: 'Files are required' }, status: :unprocessable_entity if files.blank?
|
return render json: { error: 'Files are required' }, status: :unprocessable_content if files.blank?
|
||||||
return render json: { error: 'At least 2 files are required' }, status: :unprocessable_entity if files.size < 2
|
return render json: { error: 'At least 2 files are required' }, status: :unprocessable_content if files.size < 2
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
data: Base64.encode64(PdfUtils.merge(files.map { |base64| StringIO.new(Base64.decode64(base64)) }).string)
|
data: Base64.encode64(PdfUtils.merge(files.map { |base64| StringIO.new(Base64.decode64(base64)) }).string)
|
||||||
@@ -35,7 +35,7 @@ module Api
|
|||||||
end
|
end
|
||||||
}
|
}
|
||||||
rescue HexaPDF::MalformedPDFError
|
rescue HexaPDF::MalformedPDFError
|
||||||
render json: { error: 'Malformed PDF' }, status: :unprocessable_entity
|
render json: { error: 'Malformed PDF' }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -13,6 +13,8 @@ class ApplicationController < ActionController::Base
|
|||||||
before_action :maybe_redirect_to_setup, unless: :signed_in?
|
before_action :maybe_redirect_to_setup, unless: :signed_in?
|
||||||
before_action :authenticate_user!, unless: :devise_controller?
|
before_action :authenticate_user!, unless: :devise_controller?
|
||||||
|
|
||||||
|
before_action :set_csp, if: -> { request.get? && !request.headers['HTTP_X_TURBO'] }
|
||||||
|
|
||||||
helper_method :button_title,
|
helper_method :button_title,
|
||||||
:current_account,
|
:current_account,
|
||||||
:form_link_host,
|
:form_link_host,
|
||||||
@@ -123,4 +125,21 @@ class ApplicationController < ActionController::Base
|
|||||||
|
|
||||||
redirect_to request.url.gsub('.co/', '.com/'), allow_other_host: true, status: :moved_permanently
|
redirect_to request.url.gsub('.co/', '.com/'), allow_other_host: true, status: :moved_permanently
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_csp
|
||||||
|
request.content_security_policy = current_content_security_policy.tap do |policy|
|
||||||
|
policy.default_src :self
|
||||||
|
policy.script_src :self
|
||||||
|
policy.style_src :self, :unsafe_inline
|
||||||
|
policy.img_src :self, :https, :http, :blob, :data
|
||||||
|
policy.font_src :self, :https, :http, :blob, :data
|
||||||
|
policy.manifest_src :self
|
||||||
|
policy.media_src :self
|
||||||
|
policy.frame_src :self
|
||||||
|
policy.worker_src :self, :blob
|
||||||
|
policy.connect_src :self
|
||||||
|
|
||||||
|
policy.directives['connect-src'] << 'ws:' if Rails.env.development?
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -15,12 +15,12 @@ class EmailSmtpSettingsController < ApplicationController
|
|||||||
|
|
||||||
redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved')
|
redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved')
|
||||||
else
|
else
|
||||||
render :index, status: :unprocessable_entity
|
render :index, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
flash[:alert] = e.message
|
flash[:alert] = e.message
|
||||||
|
|
||||||
render :index, status: :unprocessable_entity
|
render :index, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class EsignSettingsController < ApplicationController
|
|||||||
@cert_record.errors.add(:name, I18n.t('already_exists'))
|
@cert_record.errors.add(:name, I18n.t('already_exists'))
|
||||||
|
|
||||||
return render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'),
|
return render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'),
|
||||||
status: :unprocessable_entity
|
status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
save_new_cert!(@encrypted_config, @cert_record)
|
save_new_cert!(@encrypted_config, @cert_record)
|
||||||
@@ -64,7 +64,7 @@ class EsignSettingsController < ApplicationController
|
|||||||
|
|
||||||
@cert_record.errors.add(:password, e.message)
|
@cert_record.errors.add(:password, e.message)
|
||||||
|
|
||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), status: :unprocessable_entity
|
render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class MfaSetupController < ApplicationController
|
|||||||
|
|
||||||
@error_message = I18n.t('code_is_invalid')
|
@error_message = I18n.t('code_is_invalid')
|
||||||
|
|
||||||
render turbo_stream: turbo_stream.replace(:mfa_form, partial: 'mfa_setup/form'), status: :unprocessable_entity
|
render turbo_stream: turbo_stream.replace(:mfa_form, partial: 'mfa_setup/form'), status: :unprocessable_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ class MfaSetupController < ApplicationController
|
|||||||
else
|
else
|
||||||
@error_message = I18n.t('code_is_invalid')
|
@error_message = I18n.t('code_is_invalid')
|
||||||
|
|
||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_entity
|
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class PasswordsController < Devise::PasswordsController
|
class PasswordsController < Devise::PasswordsController
|
||||||
|
# rubocop:disable Rails/LexicallyScopedActionFilter
|
||||||
|
skip_before_action :require_no_authentication, only: %i[edit update]
|
||||||
|
# rubocop:enable Rails/LexicallyScopedActionFilter
|
||||||
|
|
||||||
class Current < ActiveSupport::CurrentAttributes
|
class Current < ActiveSupport::CurrentAttributes
|
||||||
attribute :user
|
attribute :user
|
||||||
end
|
end
|
||||||
@@ -16,4 +20,10 @@ class PasswordsController < Devise::PasswordsController
|
|||||||
Current.user = resource
|
Current.user = resource
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def after_resetting_password_path_for(_)
|
||||||
|
new_session_path(resource_name)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -11,16 +11,16 @@ class ProfileController < ApplicationController
|
|||||||
if current_user.update(contact_params)
|
if current_user.update(contact_params)
|
||||||
redirect_to settings_profile_index_path, notice: I18n.t('contact_information_has_been_update')
|
redirect_to settings_profile_index_path, notice: I18n.t('contact_information_has_been_update')
|
||||||
else
|
else
|
||||||
render :index, status: :unprocessable_entity
|
render :index, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_password
|
def update_password
|
||||||
if current_user.update(password_params)
|
if current_user.update_with_password(password_params)
|
||||||
bypass_sign_in(current_user)
|
bypass_sign_in(current_user)
|
||||||
redirect_to settings_profile_index_path, notice: I18n.t('password_has_been_changed')
|
redirect_to settings_profile_index_path, notice: I18n.t('password_has_been_changed')
|
||||||
else
|
else
|
||||||
render :index, status: :unprocessable_entity
|
render :index, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -31,6 +31,6 @@ class ProfileController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def password_params
|
def password_params
|
||||||
params.require(:user).permit(:password, :password_confirmation)
|
params.require(:user).permit(:password, :password_confirmation, :current_password)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class RevealAccessTokenController < ApplicationController
|
||||||
|
def show
|
||||||
|
authorize!(:manage, current_user.access_token)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize!(:manage, current_user.access_token)
|
||||||
|
|
||||||
|
if current_user.valid_password?(params[:password])
|
||||||
|
render turbo_stream: turbo_stream.replace(:access_token_container,
|
||||||
|
partial: 'reveal_access_token/access_token',
|
||||||
|
locals: { token: current_user.access_token.token })
|
||||||
|
else
|
||||||
|
render turbo_stream: turbo_stream.replace(:modal, template: 'reveal_access_token/show',
|
||||||
|
locals: { error_message: I18n.t('wrong_password') }),
|
||||||
|
status: :unprocessable_content
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -16,7 +16,7 @@ class SessionsController < Devise::SessionsController
|
|||||||
end
|
end
|
||||||
|
|
||||||
if User.exists?(email:, otp_required_for_login: true) && sign_in_params[:otp_attempt].blank?
|
if User.exists?(email:, otp_required_for_login: true) && sign_in_params[:otp_attempt].blank?
|
||||||
return render :otp, locals: { resource: User.new(sign_in_params) }, status: :unprocessable_entity
|
return render :otp, locals: { resource: User.new(sign_in_params) }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
super
|
super
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ class SetupController < ApplicationController
|
|||||||
unless URI.parse(encrypted_config_params[:value].to_s).class.in?([URI::HTTP, URI::HTTPS])
|
unless URI.parse(encrypted_config_params[:value].to_s).class.in?([URI::HTTP, URI::HTTPS])
|
||||||
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url'))
|
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url'))
|
||||||
|
|
||||||
return render :index, status: :unprocessable_entity
|
return render :index, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
return render :index, status: :unprocessable_entity unless @account.valid?
|
return render :index, status: :unprocessable_content unless @account.valid?
|
||||||
|
|
||||||
if @user.save
|
if @user.save
|
||||||
encrypted_configs = [
|
encrypted_configs = [
|
||||||
@@ -42,7 +42,7 @@ class SetupController < ApplicationController
|
|||||||
|
|
||||||
redirect_to newsletter_path
|
redirect_to newsletter_path
|
||||||
else
|
else
|
||||||
render :index, status: :unprocessable_entity
|
render :index, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class StartFormController < ApplicationController
|
|||||||
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record?
|
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record?
|
||||||
@error_message = multiple_submitters_error_message
|
@error_message = multiple_submitters_error_message
|
||||||
|
|
||||||
return render :show, status: :unprocessable_entity
|
return render :show, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
if (is_new_record = @submitter.new_record?)
|
if (is_new_record = @submitter.new_record?)
|
||||||
@@ -60,7 +60,7 @@ class StartFormController < ApplicationController
|
|||||||
|
|
||||||
redirect_to submit_form_path(@submitter.slug)
|
redirect_to submit_form_path(@submitter.slug)
|
||||||
else
|
else
|
||||||
render :show, status: :unprocessable_entity
|
render :show, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -206,7 +206,7 @@ class StartFormController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def handle_require_2fa(submitter, is_new_record:)
|
def handle_require_2fa(submitter, is_new_record:)
|
||||||
return render :show, status: :unprocessable_entity if submitter.errors.present?
|
return render :show, status: :unprocessable_content if submitter.errors.present?
|
||||||
|
|
||||||
is_otp_verified = Submitters.verify_link_otp!(params[:one_time_code], submitter)
|
is_otp_verified = Submitters.verify_link_otp!(params[:one_time_code], submitter)
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ class StartFormController < ApplicationController
|
|||||||
|
|
||||||
redirect_to submit_form_path(submitter.slug)
|
redirect_to submit_form_path(submitter.slug)
|
||||||
else
|
else
|
||||||
render :show, status: :unprocessable_entity
|
render :show, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
Submitters.send_shared_link_email_verification_code(submitter, request:)
|
Submitters.send_shared_link_email_verification_code(submitter, request:)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class StorageSettingsController < ApplicationController
|
|||||||
|
|
||||||
redirect_to settings_storage_index_path, notice: I18n.t('changes_have_been_saved')
|
redirect_to settings_storage_index_path, notice: I18n.t('changes_have_been_saved')
|
||||||
else
|
else
|
||||||
render :index, status: :unprocessable_entity
|
render :index, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -32,10 +32,7 @@ class SubmissionsController < ApplicationController
|
|||||||
def create
|
def create
|
||||||
save_template_message(@template, params) if params[:save_message] == '1'
|
save_template_message(@template, params) if params[:save_message] == '1'
|
||||||
|
|
||||||
if params[:is_custom_message] != '1'
|
[params.delete(:subject), params.delete(:body)] if params[:is_custom_message] != '1'
|
||||||
params.delete(:subject)
|
|
||||||
params.delete(:body)
|
|
||||||
end
|
|
||||||
|
|
||||||
submissions =
|
submissions =
|
||||||
if params[:emails].present?
|
if params[:emails].present?
|
||||||
@@ -46,11 +43,16 @@ class SubmissionsController < ApplicationController
|
|||||||
emails: params[:emails],
|
emails: params[:emails],
|
||||||
params: params.merge('send_completed_email' => true))
|
params: params.merge('send_completed_email' => true))
|
||||||
else
|
else
|
||||||
|
submissions_attrs = submissions_params[:submission].to_h.values
|
||||||
|
|
||||||
|
submissions_attrs, =
|
||||||
|
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_attrs, @template)
|
||||||
|
|
||||||
Submissions.create_from_submitters(template: @template,
|
Submissions.create_from_submitters(template: @template,
|
||||||
user: current_user,
|
user: current_user,
|
||||||
source: :invite,
|
source: :invite,
|
||||||
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
|
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
|
||||||
submissions_attrs: submissions_params[:submission].to_h.values,
|
submissions_attrs:,
|
||||||
params: params.merge('send_completed_email' => true))
|
params: params.merge('send_completed_email' => true))
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -62,10 +64,9 @@ class SubmissionsController < ApplicationController
|
|||||||
|
|
||||||
redirect_to template_path(@template), notice: I18n.t('new_recipients_have_been_added')
|
redirect_to template_path(@template), notice: I18n.t('new_recipients_have_been_added')
|
||||||
rescue Submissions::CreateFromSubmitters::BaseError => e
|
rescue Submissions::CreateFromSubmitters::BaseError => e
|
||||||
render turbo_stream: turbo_stream.replace(:submitters_error,
|
render turbo_stream: turbo_stream.replace(:submitters_error, partial: 'submissions/error',
|
||||||
partial: 'submissions/error',
|
|
||||||
locals: { error: e.message }),
|
locals: { error: e.message }),
|
||||||
status: :unprocessable_entity
|
status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@@ -95,7 +96,7 @@ class SubmissionsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def submissions_params
|
def submissions_params
|
||||||
params.permit(submission: { submitters: [%i[uuid email phone name]] })
|
params.permit(submission: { submitters: [:uuid, :email, :phone, :name, { values: {} }] })
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_template
|
def load_template
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
return if submitter.submission.submitters.order(:completed_at).last != submitter
|
return if submitter.submission.submitters.order(:completed_at).last != submitter
|
||||||
|
|
||||||
attachment = submitter.submission.combined_document_attachment
|
attachment = submitter.submission.combined_document_attachment
|
||||||
attachment ||= Submissions::GenerateCombinedAttachment.call(submitter)
|
attachment ||= Submissions::EnsureCombinedGenerated.call(submitter)
|
||||||
|
|
||||||
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
|
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
|
||||||
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
|
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
|
||||||
|
|||||||
@@ -10,11 +10,16 @@ class SubmissionsExportController < ApplicationController
|
|||||||
attachments_attachments: :blob })
|
attachments_attachments: :blob })
|
||||||
.order(id: :asc)
|
.order(id: :asc)
|
||||||
|
|
||||||
|
submissions = Submissions.search(current_user, submissions, params[:q], search_values: true)
|
||||||
|
submissions = Submissions::Filter.call(submissions, current_user, params)
|
||||||
|
|
||||||
|
expires_at = Accounts.link_expires_at(current_account)
|
||||||
|
|
||||||
if params[:format] == 'csv'
|
if params[:format] == 'csv'
|
||||||
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format]),
|
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format], expires_at:),
|
||||||
filename: "#{@template.name}.csv"
|
filename: "#{@template.name}.csv"
|
||||||
elsif params[:format] == 'xlsx'
|
elsif params[:format] == 'xlsx'
|
||||||
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format]),
|
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format], expires_at:),
|
||||||
filename: "#{@template.name}.xlsx"
|
filename: "#{@template.name}.xlsx"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
class SubmissionsFiltersController < ApplicationController
|
class SubmissionsFiltersController < ApplicationController
|
||||||
ALLOWED_NAMES = %w[
|
ALLOWED_NAMES = %w[
|
||||||
author
|
author
|
||||||
|
folder
|
||||||
completed_at
|
completed_at
|
||||||
status
|
status
|
||||||
created_at
|
created_at
|
||||||
|
|||||||
@@ -48,20 +48,20 @@ class SubmitFormController < ApplicationController
|
|||||||
|
|
||||||
def update
|
def update
|
||||||
if @submitter.completed_at?
|
if @submitter.completed_at?
|
||||||
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_entity
|
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at?
|
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at?
|
||||||
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_entity
|
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
if @submitter.submission.expired?
|
if @submitter.submission.expired?
|
||||||
return render json: { error: I18n.t('form_has_been_expired') }, status: :unprocessable_entity
|
return render json: { error: I18n.t('form_has_been_expired') }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
if @submitter.declined_at?
|
if @submitter.declined_at?
|
||||||
return render json: { error: I18n.t('form_has_been_declined') },
|
return render json: { error: I18n.t('form_has_been_declined') },
|
||||||
status: :unprocessable_entity
|
status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
Submitters::SubmitValues.call(@submitter, params, request)
|
Submitters::SubmitValues.call(@submitter, params, request)
|
||||||
@@ -70,9 +70,9 @@ class SubmitFormController < ApplicationController
|
|||||||
rescue Submitters::SubmitValues::RequiredFieldError => e
|
rescue Submitters::SubmitValues::RequiredFieldError => e
|
||||||
Rollbar.warning("Required field #{@submitter.id}: #{e.message}") if defined?(Rollbar)
|
Rollbar.warning("Required field #{@submitter.id}: #{e.message}") if defined?(Rollbar)
|
||||||
|
|
||||||
render json: { field_uuid: e.message }, status: :unprocessable_entity
|
render json: { field_uuid: e.message }, status: :unprocessable_content
|
||||||
rescue Submitters::SubmitValues::ValidationError => e
|
rescue Submitters::SubmitValues::ValidationError => e
|
||||||
render json: { error: e.message }, status: :unprocessable_entity
|
render json: { error: e.message }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
def completed; end
|
def completed; end
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class SubmitFormDownloadController < ApplicationController
|
|||||||
|
|
||||||
return redirect_to submitter_download_index_path(@submitter.slug) if @submitter.completed_at?
|
return redirect_to submitter_download_index_path(@submitter.slug) if @submitter.completed_at?
|
||||||
|
|
||||||
return head :unprocessable_entity if @submitter.declined_at? ||
|
return head :unprocessable_content if @submitter.declined_at? ||
|
||||||
@submitter.submission.archived_at? ||
|
@submitter.submission.archived_at? ||
|
||||||
@submitter.submission.expired? ||
|
@submitter.submission.expired? ||
|
||||||
@submitter.submission.template&.archived_at? ||
|
@submitter.submission.template&.archived_at? ||
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ class SubmitFormInviteController < ApplicationController
|
|||||||
def create
|
def create
|
||||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||||
|
|
||||||
return head :unprocessable_entity unless can_invite?(submitter)
|
return head :unprocessable_content unless can_invite?(submitter)
|
||||||
|
|
||||||
invite_submitters = filter_invite_submitters(submitter, 'invite_by_uuid')
|
invite_submitters = filter_invite_submitters(submitter, 'invite_by_uuid')
|
||||||
optional_invite_submitters = filter_invite_submitters(submitter, 'optional_invite_by_uuid')
|
optional_invite_submitters = filter_invite_submitters(submitter, 'optional_invite_by_uuid')
|
||||||
@@ -34,7 +34,7 @@ class SubmitFormInviteController < ApplicationController
|
|||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
else
|
else
|
||||||
head :unprocessable_entity
|
head :unprocessable_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ class TemplateDocumentsController < ApplicationController
|
|||||||
|
|
||||||
def create
|
def create
|
||||||
if params[:blobs].blank? && params[:files].blank?
|
if params[:blobs].blank? && params[:files].blank?
|
||||||
return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_entity
|
return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
old_fields_hash = @template.fields.hash
|
old_fields_hash = @template.fields.hash
|
||||||
@@ -28,6 +28,6 @@ class TemplateDocumentsController < ApplicationController
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
rescue Templates::CreateAttachments::PdfEncrypted
|
rescue Templates::CreateAttachments::PdfEncrypted
|
||||||
render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_entity
|
render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -4,7 +4,7 @@ class TemplatesCloneAndReplaceController < ApplicationController
|
|||||||
load_and_authorize_resource :template
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
def create
|
def create
|
||||||
return head :unprocessable_entity if params[:files].blank?
|
return head :unprocessable_content if params[:files].blank?
|
||||||
|
|
||||||
ActiveRecord::Associations::Preloader.new(
|
ActiveRecord::Associations::Preloader.new(
|
||||||
records: [@template],
|
records: [@template],
|
||||||
@@ -17,6 +17,8 @@ class TemplatesCloneAndReplaceController < ApplicationController
|
|||||||
|
|
||||||
documents = Templates::ReplaceAttachments.call(cloned_template, params, extract_fields: true)
|
documents = Templates::ReplaceAttachments.call(cloned_template, params, extract_fields: true)
|
||||||
|
|
||||||
|
Templates.maybe_assign_access(cloned_template)
|
||||||
|
|
||||||
cloned_template.save!
|
cloned_template.save!
|
||||||
|
|
||||||
Templates::CloneAttachments.call(template: cloned_template, original_template: @template,
|
Templates::CloneAttachments.call(template: cloned_template, original_template: @template,
|
||||||
@@ -31,7 +33,7 @@ class TemplatesCloneAndReplaceController < ApplicationController
|
|||||||
rescue Templates::CreateAttachments::PdfEncrypted
|
rescue Templates::CreateAttachments::PdfEncrypted
|
||||||
respond_to do |f|
|
respond_to do |f|
|
||||||
f.html { render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password) }
|
f.html { render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password) }
|
||||||
f.json { render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_entity }
|
f.json { render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_content }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -69,6 +69,8 @@ class TemplatesController < ApplicationController
|
|||||||
@template.account = current_account
|
@template.account = current_account
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Templates.maybe_assign_access(@template)
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -78,7 +80,7 @@ class TemplatesController < ApplicationController
|
|||||||
|
|
||||||
maybe_redirect_to_template(@template)
|
maybe_redirect_to_template(@template)
|
||||||
else
|
else
|
||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_entity
|
render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -117,14 +119,15 @@ class TemplatesController < ApplicationController
|
|||||||
params.require(:template).permit(
|
params.require(:template).permit(
|
||||||
:name,
|
:name,
|
||||||
{ schema: [[:attachment_uuid, :name, { conditions: [%i[field_uuid value action operation]] }]],
|
{ schema: [[:attachment_uuid, :name, { conditions: [%i[field_uuid value action operation]] }]],
|
||||||
submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid optional_invite_by_uuid email]],
|
submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid optional_invite_by_uuid email order]],
|
||||||
fields: [[:uuid, :submitter_uuid, :name, :type,
|
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||||
:required, :readonly, :default_value,
|
:required, :readonly, :default_value,
|
||||||
:title, :description,
|
:title, :description, :prefillable,
|
||||||
{ preferences: {},
|
{ preferences: {},
|
||||||
|
default_value: [],
|
||||||
conditions: [%i[field_uuid value action operation]],
|
conditions: [%i[field_uuid value action operation]],
|
||||||
options: [%i[value uuid]],
|
options: [%i[value uuid]],
|
||||||
validation: %i[message pattern],
|
validation: %i[message pattern min max step],
|
||||||
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
class TemplatesDebugController < ApplicationController
|
class TemplatesDebugController < ApplicationController
|
||||||
load_and_authorize_resource :template
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
DEBUG_FILE = ''
|
||||||
|
|
||||||
def show
|
def show
|
||||||
attachment = @template.documents.first
|
attachment = @template.documents.first
|
||||||
|
|
||||||
@@ -16,6 +18,8 @@ class TemplatesDebugController < ApplicationController
|
|||||||
|
|
||||||
@template.update!(fields: Templates::ProcessDocument.normalize_attachment_fields(@template, [attachment]))
|
@template.update!(fields: Templates::ProcessDocument.normalize_attachment_fields(@template, [attachment]))
|
||||||
|
|
||||||
|
debug_file if DEBUG_FILE.present?
|
||||||
|
|
||||||
ActiveRecord::Associations::Preloader.new(
|
ActiveRecord::Associations::Preloader.new(
|
||||||
records: [@template],
|
records: [@template],
|
||||||
associations: [schema_documents: { preview_images_attachments: :blob }]
|
associations: [schema_documents: { preview_images_attachments: :blob }]
|
||||||
@@ -31,4 +35,27 @@ class TemplatesDebugController < ApplicationController
|
|||||||
|
|
||||||
render 'templates/edit', layout: 'plain'
|
render 'templates/edit', layout: 'plain'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def debug_file
|
||||||
|
tempfile = Tempfile.new
|
||||||
|
tempfile.binmode
|
||||||
|
tempfile.write(File.read(DEBUG_FILE))
|
||||||
|
tempfile.rewind
|
||||||
|
|
||||||
|
filename = File.basename(DEBUG_FILE)
|
||||||
|
|
||||||
|
file = ActionDispatch::Http::UploadedFile.new(
|
||||||
|
tempfile:,
|
||||||
|
filename:,
|
||||||
|
type: Marcel::MimeType.for(tempfile)
|
||||||
|
)
|
||||||
|
|
||||||
|
params = { files: [file] }
|
||||||
|
|
||||||
|
documents = Templates::CreateAttachments.call(@template, params)
|
||||||
|
|
||||||
|
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
|
||||||
|
|
||||||
|
@template.update!(schema:)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -27,7 +27,7 @@ class TemplatesPreferencesController < ApplicationController
|
|||||||
completed_redirect_url validate_unique_submitters
|
completed_redirect_url validate_unique_submitters
|
||||||
require_all_submitters submitters_order require_phone_2fa
|
require_all_submitters submitters_order require_phone_2fa
|
||||||
default_expire_at_duration shared_link_2fa
|
default_expire_at_duration shared_link_2fa
|
||||||
default_expire_at
|
default_expire_at request_email_enabled
|
||||||
completed_notification_email_subject completed_notification_email_body
|
completed_notification_email_subject completed_notification_email_body
|
||||||
completed_notification_email_enabled completed_notification_email_attach_audit] +
|
completed_notification_email_enabled completed_notification_email_attach_audit] +
|
||||||
[completed_message: %i[title body],
|
[completed_message: %i[title body],
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesPrefillableFieldsController < ApplicationController
|
||||||
|
PREFILLABLE_FIELD_TYPES = %w[text number cells date checkbox select radio phone].freeze
|
||||||
|
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize!(:update, @template)
|
||||||
|
|
||||||
|
field = @template.fields.find { |f| f['uuid'] == params[:field_uuid] }
|
||||||
|
|
||||||
|
if params[:prefillable] == 'false'
|
||||||
|
field.delete('prefillable')
|
||||||
|
field.delete('readonly')
|
||||||
|
elsif params[:prefillable] == 'true'
|
||||||
|
field['prefillable'] = true
|
||||||
|
field['readonly'] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
@template.save!
|
||||||
|
|
||||||
|
render turbo_stream: turbo_stream.replace(:prefillable_fields_list, partial: 'list',
|
||||||
|
locals: { template: @template })
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -9,6 +9,10 @@ class TemplatesRecipientsController < ApplicationController
|
|||||||
@template.submitters =
|
@template.submitters =
|
||||||
submitters_params.map { |s| s.reject { |_, v| v.is_a?(String) && v.blank? } }
|
submitters_params.map { |s| s.reject { |_, v| v.is_a?(String) && v.blank? } }
|
||||||
|
|
||||||
|
if @template.submitters.each_with_index.all? { |s, index| s['order'] == index }
|
||||||
|
@template.submitters.each { |s| s.delete('order') }
|
||||||
|
end
|
||||||
|
|
||||||
@template.save!
|
@template.save!
|
||||||
|
|
||||||
render json: { submitters: @template.submitters }
|
render json: { submitters: @template.submitters }
|
||||||
@@ -18,7 +22,7 @@ class TemplatesRecipientsController < ApplicationController
|
|||||||
|
|
||||||
def submitters_params
|
def submitters_params
|
||||||
permit_params = { submitters: [%i[name uuid is_requester optional_invite_by_uuid
|
permit_params = { submitters: [%i[name uuid is_requester optional_invite_by_uuid
|
||||||
invite_by_uuid linked_to_uuid email option]] }
|
invite_by_uuid linked_to_uuid email option order]] }
|
||||||
|
|
||||||
params.require(:template).permit(permit_params).fetch(:submitters, {}).values.filter_map do |s|
|
params.require(:template).permit(permit_params).fetch(:submitters, {}).values.filter_map do |s|
|
||||||
next if s[:uuid].blank?
|
next if s[:uuid].blank?
|
||||||
@@ -29,6 +33,7 @@ class TemplatesRecipientsController < ApplicationController
|
|||||||
s.delete(:is_requester)
|
s.delete(:is_requester)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
s[:order] = s[:order].to_i if s[:order].present?
|
||||||
s.delete(:invite_by_uuid) if s[:invite_by_uuid].blank?
|
s.delete(:invite_by_uuid) if s[:invite_by_uuid].blank?
|
||||||
s.delete(:optional_invite_by_uuid) if s[:optional_invite_by_uuid].blank?
|
s.delete(:optional_invite_by_uuid) if s[:optional_invite_by_uuid].blank?
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ class TemplatesUploadsController < ApplicationController
|
|||||||
template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
||||||
template.name = File.basename((url_params || params)[:files].first.original_filename, '.*')
|
template.name = File.basename((url_params || params)[:files].first.original_filename, '.*')
|
||||||
|
|
||||||
|
Templates.maybe_assign_access(template)
|
||||||
|
|
||||||
template.save!
|
template.save!
|
||||||
|
|
||||||
template
|
template
|
||||||
|
|||||||
@@ -27,9 +27,10 @@ class UsersController < ApplicationController
|
|||||||
if User.accessible_by(current_ability).exists?(email: @user.email)
|
if User.accessible_by(current_ability).exists?(email: @user.email)
|
||||||
@user.errors.add(:email, I18n.t('already_exists'))
|
@user.errors.add(:email, I18n.t('already_exists'))
|
||||||
|
|
||||||
return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity
|
return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@user.password = SecureRandom.hex if @user.password.blank?
|
||||||
@user.role = User::ADMIN_ROLE unless role_valid?(@user.role)
|
@user.role = User::ADMIN_ROLE unless role_valid?(@user.role)
|
||||||
|
|
||||||
if @user.save
|
if @user.save
|
||||||
@@ -37,7 +38,7 @@ class UsersController < ApplicationController
|
|||||||
|
|
||||||
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_invited')
|
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_invited')
|
||||||
else
|
else
|
||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity
|
render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -54,10 +55,10 @@ class UsersController < ApplicationController
|
|||||||
@user.account = account
|
@user.account = account
|
||||||
end
|
end
|
||||||
|
|
||||||
if @user.update(attrs.except(current_user == @user ? :role : nil))
|
if @user.update(attrs.except(*(current_user == @user ? %i[password otp_required_for_login role] : %i[password])))
|
||||||
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_updated')
|
redirect_back fallback_location: settings_users_path, notice: I18n.t('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_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ class UsersController < ApplicationController
|
|||||||
|
|
||||||
def user_params
|
def user_params
|
||||||
if params.key?(:user)
|
if params.key?(:user)
|
||||||
permitted_params = %i[email first_name last_name password archived_at]
|
permitted_params = %i[email first_name last_name password archived_at otp_required_for_login]
|
||||||
|
|
||||||
permitted_params << :role if role_valid?(params.dig(:user, :role))
|
permitted_params << :role if role_valid?(params.dig(:user, :role))
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class UsersSendResetPasswordController < ApplicationController
|
||||||
|
load_and_authorize_resource :user
|
||||||
|
|
||||||
|
LIMIT_DURATION = 10.minutes
|
||||||
|
|
||||||
|
def update
|
||||||
|
authorize!(:manage, @user)
|
||||||
|
|
||||||
|
if @user.reset_password_sent_at && @user.reset_password_sent_at > LIMIT_DURATION.ago
|
||||||
|
redirect_back fallback_location: settings_users_path, notice: I18n.t('email_has_been_sent_already')
|
||||||
|
else
|
||||||
|
@user.send_reset_password_instructions
|
||||||
|
|
||||||
|
redirect_back fallback_location: settings_users_path,
|
||||||
|
notice: I18n.t('an_email_with_password_reset_instructions_has_been_sent')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -23,6 +23,7 @@ import SignatureForm from './elements/signature_form'
|
|||||||
import SubmitForm from './elements/submit_form'
|
import SubmitForm from './elements/submit_form'
|
||||||
import PromptPassword from './elements/prompt_password'
|
import PromptPassword from './elements/prompt_password'
|
||||||
import EmailsTextarea from './elements/emails_textarea'
|
import EmailsTextarea from './elements/emails_textarea'
|
||||||
|
import ToggleSubmit from './elements/toggle_submit'
|
||||||
import ToggleOnSubmit from './elements/toggle_on_submit'
|
import ToggleOnSubmit from './elements/toggle_on_submit'
|
||||||
import CheckOnClick from './elements/check_on_click'
|
import CheckOnClick from './elements/check_on_click'
|
||||||
import PasswordInput from './elements/password_input'
|
import PasswordInput from './elements/password_input'
|
||||||
@@ -34,9 +35,20 @@ import MaskedInput from './elements/masked_input'
|
|||||||
import SetDateButton from './elements/set_date_button'
|
import SetDateButton from './elements/set_date_button'
|
||||||
import IndeterminateCheckbox from './elements/indeterminate_checkbox'
|
import IndeterminateCheckbox from './elements/indeterminate_checkbox'
|
||||||
import AppTour from './elements/app_tour'
|
import AppTour from './elements/app_tour'
|
||||||
|
import AppTourStart from './elements/app_tour_start'
|
||||||
import DashboardDropzone from './elements/dashboard_dropzone'
|
import DashboardDropzone from './elements/dashboard_dropzone'
|
||||||
import RequiredCheckboxGroup from './elements/required_checkbox_group'
|
import RequiredCheckboxGroup from './elements/required_checkbox_group'
|
||||||
import PageContainer from './elements/page_container'
|
import PageContainer from './elements/page_container'
|
||||||
|
import EmailEditor from './elements/email_editor'
|
||||||
|
import MountOnClick from './elements/mount_on_click'
|
||||||
|
import RemoveOnEvent from './elements/remove_on_event'
|
||||||
|
import ScrollTo from './elements/scroll_to'
|
||||||
|
import SetValue from './elements/set_value'
|
||||||
|
import ReviewForm from './elements/review_form'
|
||||||
|
import ShowOnValue from './elements/show_on_value'
|
||||||
|
import CustomValidation from './elements/custom_validation'
|
||||||
|
import ToggleClasses from './elements/toggle_classes'
|
||||||
|
import AutosizeField from './elements/autosize_field'
|
||||||
|
|
||||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||||
|
|
||||||
@@ -53,6 +65,9 @@ document.addEventListener('keyup', (e) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
|
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
|
||||||
|
document.addEventListener('turbo:before-fetch-request', (event) => {
|
||||||
|
event.detail.fetchOptions.headers['X-Turbo'] = 'true'
|
||||||
|
})
|
||||||
document.addEventListener('turbo:submit-end', async (event) => {
|
document.addEventListener('turbo:submit-end', async (event) => {
|
||||||
const resp = event.detail?.formSubmission?.result?.fetchResponse?.response
|
const resp = event.detail?.formSubmission?.result?.fetchResponse?.response
|
||||||
|
|
||||||
@@ -95,6 +110,7 @@ safeRegisterElement('submit-form', SubmitForm)
|
|||||||
safeRegisterElement('prompt-password', PromptPassword)
|
safeRegisterElement('prompt-password', PromptPassword)
|
||||||
safeRegisterElement('emails-textarea', EmailsTextarea)
|
safeRegisterElement('emails-textarea', EmailsTextarea)
|
||||||
safeRegisterElement('toggle-cookies', ToggleCookies)
|
safeRegisterElement('toggle-cookies', ToggleCookies)
|
||||||
|
safeRegisterElement('toggle-submit', ToggleSubmit)
|
||||||
safeRegisterElement('toggle-on-submit', ToggleOnSubmit)
|
safeRegisterElement('toggle-on-submit', ToggleOnSubmit)
|
||||||
safeRegisterElement('password-input', PasswordInput)
|
safeRegisterElement('password-input', PasswordInput)
|
||||||
safeRegisterElement('search-input', SearchInput)
|
safeRegisterElement('search-input', SearchInput)
|
||||||
@@ -105,10 +121,21 @@ safeRegisterElement('masked-input', MaskedInput)
|
|||||||
safeRegisterElement('set-date-button', SetDateButton)
|
safeRegisterElement('set-date-button', SetDateButton)
|
||||||
safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox)
|
safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox)
|
||||||
safeRegisterElement('app-tour', AppTour)
|
safeRegisterElement('app-tour', AppTour)
|
||||||
|
safeRegisterElement('app-tour-start', AppTourStart)
|
||||||
safeRegisterElement('dashboard-dropzone', DashboardDropzone)
|
safeRegisterElement('dashboard-dropzone', DashboardDropzone)
|
||||||
safeRegisterElement('check-on-click', CheckOnClick)
|
safeRegisterElement('check-on-click', CheckOnClick)
|
||||||
safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup)
|
safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup)
|
||||||
safeRegisterElement('page-container', PageContainer)
|
safeRegisterElement('page-container', PageContainer)
|
||||||
|
safeRegisterElement('email-editor', EmailEditor)
|
||||||
|
safeRegisterElement('mount-on-click', MountOnClick)
|
||||||
|
safeRegisterElement('remove-on-event', RemoveOnEvent)
|
||||||
|
safeRegisterElement('scroll-to', ScrollTo)
|
||||||
|
safeRegisterElement('set-value', SetValue)
|
||||||
|
safeRegisterElement('review-form', ReviewForm)
|
||||||
|
safeRegisterElement('show-on-value', ShowOnValue)
|
||||||
|
safeRegisterElement('custom-validation', CustomValidation)
|
||||||
|
safeRegisterElement('toggle-classes', ToggleClasses)
|
||||||
|
safeRegisterElement('autosize-field', AutosizeField)
|
||||||
|
|
||||||
safeRegisterElement('template-builder', class extends HTMLElement {
|
safeRegisterElement('template-builder', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ button .disabled {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
button[disabled] .disabled {
|
button[disabled] .disabled, button.btn-disabled .disabled {
|
||||||
display: initial;
|
display: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ button .enabled {
|
|||||||
display: initial;
|
display: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
button[disabled] .enabled {
|
button[disabled] .enabled, button.btn-disabled .enabled {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.querySelector('form').addEventListener('submit', () => {
|
||||||
|
window.app_tour.start()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
const originalFontValue = this.field.style.fontSize
|
||||||
|
|
||||||
|
if (this.field.scrollHeight > this.field.clientHeight) {
|
||||||
|
this.field.style.fontSize = `calc(${originalFontValue} / 1.5)`
|
||||||
|
this.field.style.lineHeight = `calc(${this.field.style.fontSize} * 1.3)`
|
||||||
|
|
||||||
|
if (this.field.scrollHeight > this.field.clientHeight) {
|
||||||
|
this.field.style.fontSize = `calc(${originalFontValue} / 2.0)`
|
||||||
|
this.field.style.lineHeight = `calc(${this.field.style.fontSize} * 1.3)`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.field.classList.remove('hidden')
|
||||||
|
}
|
||||||
|
|
||||||
|
get field () {
|
||||||
|
return this.closest('field-value')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
const input = this.querySelector('input')
|
||||||
|
const invalidMessage = this.dataset.invalidMessage || ''
|
||||||
|
|
||||||
|
input.addEventListener('invalid', () => {
|
||||||
|
input.setCustomValidity(input.value ? invalidMessage : '')
|
||||||
|
})
|
||||||
|
|
||||||
|
input.addEventListener('input', () => {
|
||||||
|
input.setCustomValidity('')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import { target, targetable } from '@github/catalyst/lib/targetable'
|
||||||
|
|
||||||
|
let loaderPromise = null
|
||||||
|
|
||||||
|
function loadCodeMirror () {
|
||||||
|
if (!loaderPromise) {
|
||||||
|
loaderPromise = Promise.all([
|
||||||
|
import(/* webpackChunkName: "email-editor" */ '@codemirror/view'),
|
||||||
|
import(/* webpackChunkName: "email-editor" */ '@codemirror/commands'),
|
||||||
|
import(/* webpackChunkName: "email-editor" */ '@codemirror/language'),
|
||||||
|
import(/* webpackChunkName: "email-editor" */ '@codemirror/lang-html'),
|
||||||
|
import(/* webpackChunkName: "email-editor" */ '@specious/htmlflow')
|
||||||
|
]).then(([view, commands, language, html, htmlflow]) => {
|
||||||
|
return {
|
||||||
|
minimalSetup: [
|
||||||
|
commands.history(),
|
||||||
|
language.syntaxHighlighting(language.defaultHighlightStyle, { fallback: true }),
|
||||||
|
view.keymap.of([...commands.defaultKeymap, ...commands.historyKeymap])
|
||||||
|
],
|
||||||
|
EditorView: view.EditorView,
|
||||||
|
html: html.html,
|
||||||
|
htmlflow: htmlflow.default || htmlflow
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return loaderPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
export default targetable(class extends HTMLElement {
|
||||||
|
static [target.static] = [
|
||||||
|
'codeViewTab',
|
||||||
|
'previewViewTab',
|
||||||
|
'editorContainer',
|
||||||
|
'previewIframe'
|
||||||
|
]
|
||||||
|
|
||||||
|
connectedCallback () {
|
||||||
|
this.mount()
|
||||||
|
|
||||||
|
if (this.input.value) {
|
||||||
|
this.showPreviewView()
|
||||||
|
} else {
|
||||||
|
this.showCodeView()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.previewViewTab.addEventListener('click', this.showPreviewView)
|
||||||
|
this.codeViewTab.addEventListener('click', this.showCodeView)
|
||||||
|
}
|
||||||
|
|
||||||
|
showCodeView = () => {
|
||||||
|
this.editorView.dispatch({
|
||||||
|
changes: { from: 0, to: this.editorView.state.doc.length, insert: this.input.value }
|
||||||
|
})
|
||||||
|
|
||||||
|
this.previewViewTab.classList.remove('tab-active', 'tab-bordered')
|
||||||
|
this.previewViewTab.classList.add('pb-[3px]')
|
||||||
|
this.codeViewTab.classList.remove('pb-[3px]')
|
||||||
|
this.codeViewTab.classList.add('tab-active', 'tab-bordered')
|
||||||
|
this.editorContainer.classList.remove('hidden')
|
||||||
|
this.previewIframe.classList.add('hidden')
|
||||||
|
}
|
||||||
|
|
||||||
|
showPreviewView = () => {
|
||||||
|
this.previewIframe.srcdoc = this.input.value
|
||||||
|
|
||||||
|
this.codeViewTab.classList.remove('tab-active', 'tab-bordered')
|
||||||
|
this.codeViewTab.classList.add('pb-[3px]')
|
||||||
|
this.previewViewTab.classList.remove('pb-[3px]')
|
||||||
|
this.previewViewTab.classList.add('tab-active', 'tab-bordered')
|
||||||
|
this.editorContainer.classList.add('hidden')
|
||||||
|
this.previewIframe.classList.remove('hidden')
|
||||||
|
}
|
||||||
|
|
||||||
|
async mount () {
|
||||||
|
this.input = this.querySelector('input[type="hidden"]')
|
||||||
|
this.input.style.display = 'none'
|
||||||
|
|
||||||
|
const { EditorView, minimalSetup, html, htmlflow } = await loadCodeMirror()
|
||||||
|
|
||||||
|
this.editorView = new EditorView({
|
||||||
|
doc: this.input.value,
|
||||||
|
parent: this.editorContainer,
|
||||||
|
extensions: [
|
||||||
|
html(),
|
||||||
|
minimalSetup,
|
||||||
|
EditorView.lineWrapping,
|
||||||
|
EditorView.updateListener.of(update => {
|
||||||
|
if (update.docChanged) this.input.value = update.state.doc.toString()
|
||||||
|
}),
|
||||||
|
EditorView.theme({
|
||||||
|
'&': {
|
||||||
|
backgroundColor: 'white',
|
||||||
|
color: 'black',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontFamily: 'monospace'
|
||||||
|
},
|
||||||
|
'&.cm-focused': {
|
||||||
|
outline: 'none'
|
||||||
|
},
|
||||||
|
'&.cm-editor': {
|
||||||
|
borderRadius: '0.375rem',
|
||||||
|
border: 'none'
|
||||||
|
},
|
||||||
|
'.cm-gutters': {
|
||||||
|
display: 'none'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
this.previewIframe.srcdoc = this.editorView.state.doc.toString()
|
||||||
|
|
||||||
|
this.previewIframe.onload = () => {
|
||||||
|
const previewIframeDoc = this.previewIframe.contentDocument
|
||||||
|
|
||||||
|
if (previewIframeDoc.body) {
|
||||||
|
previewIframeDoc.body.contentEditable = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentDocument = this.previewIframe.contentDocument || this.previewIframe.contentWindow.document
|
||||||
|
|
||||||
|
contentDocument.body.addEventListener('input', async () => {
|
||||||
|
const html = contentDocument.documentElement.outerHTML.replace(' contenteditable="true"', '')
|
||||||
|
const prettifiedHtml = await htmlflow(html)
|
||||||
|
|
||||||
|
this.input.value = prettifiedHtml
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.addEventListener('click', () => {
|
||||||
|
document.body.append(this.template.content)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get template () {
|
||||||
|
return document.getElementById(this.dataset.templateId)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
const eventType = this.dataset.on || 'click'
|
||||||
|
const selector = document.getElementById(this.dataset.selectorId) || this
|
||||||
|
const eventElement = eventType === 'submit' ? this.querySelector('form') : this
|
||||||
|
|
||||||
|
eventElement.addEventListener(eventType, (event) => {
|
||||||
|
if (eventType === 'click') {
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
selector.remove()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.querySelectorAll('input[type="radio"]').forEach(radio => {
|
||||||
|
radio.addEventListener('change', (event) => {
|
||||||
|
const rating = parseInt(event.target.value)
|
||||||
|
|
||||||
|
if (rating === 10) {
|
||||||
|
window.review_comment.value = ''
|
||||||
|
window.review_comment.classList.add('hidden')
|
||||||
|
window.review_submit.classList.add('hidden')
|
||||||
|
event.target.form.submit()
|
||||||
|
} else {
|
||||||
|
window.review_comment.classList.remove('hidden')
|
||||||
|
window.review_submit.classList.remove('hidden')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.selector = document.getElementById(this.dataset.selectorId)
|
||||||
|
|
||||||
|
this.addEventListener('click', () => {
|
||||||
|
this.selector.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||||
|
history.replaceState(null, null, `#${this.dataset.selectorId}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,14 @@ export default class extends HTMLElement {
|
|||||||
this.input.classList.remove('w-60')
|
this.input.classList.remove('w-60')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.button.addEventListener('click', (event) => {
|
||||||
|
if (!this.input.value && document.activeElement !== this.input) {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
this.input.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
get input () {
|
get input () {
|
||||||
@@ -22,4 +30,8 @@ export default class extends HTMLElement {
|
|||||||
get title () {
|
get title () {
|
||||||
return document.querySelector(this.dataset.title)
|
return document.querySelector(this.dataset.title)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get button () {
|
||||||
|
return this.querySelector('button')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
const input = this.dataset.inputId ? document.getElementById(this.dataset.inputId) : this.querySelector('input')
|
||||||
|
|
||||||
|
this.firstElementChild.addEventListener(this.dataset.on || 'click', () => {
|
||||||
|
if (this.dataset.emptyOnly !== 'true' || !input.value) {
|
||||||
|
input.value = this.dataset.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.addEventListener('change', (event) => {
|
||||||
|
const targetValue = this.dataset.value
|
||||||
|
const selectorId = this.dataset.selectorId
|
||||||
|
const targetElement = document.getElementById(selectorId)
|
||||||
|
|
||||||
|
if (event.target.value === targetValue) {
|
||||||
|
targetElement.classList.remove('hidden')
|
||||||
|
} else {
|
||||||
|
targetElement.classList.add('hidden')
|
||||||
|
targetElement.value = ''
|
||||||
|
event.target.form.requestSubmit()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,27 @@
|
|||||||
export default class extends HTMLElement {
|
export default class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
|
const form = this.querySelector('form') || (this.querySelector('input, button, select') || this.lastElementChild).form
|
||||||
|
|
||||||
if (this.dataset.interval) {
|
if (this.dataset.interval) {
|
||||||
this.interval = setInterval(() => {
|
this.interval = setInterval(() => {
|
||||||
this.querySelector('form').requestSubmit()
|
form.requestSubmit()
|
||||||
}, parseInt(this.dataset.interval))
|
}, parseInt(this.dataset.interval))
|
||||||
|
} else if (this.dataset.on) {
|
||||||
|
this.lastElementChild.addEventListener(this.dataset.on, (event) => {
|
||||||
|
if (this.dataset.disable === 'true') {
|
||||||
|
form.querySelector('[type="submit"]')?.setAttribute('disabled', true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.dataset.submitIfValue === 'true') {
|
||||||
|
if (event.target.value) {
|
||||||
|
form.requestSubmit()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.querySelector('form').requestSubmit()
|
form.requestSubmit()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
form.requestSubmit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
const button = this.querySelector('a, button')
|
||||||
|
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
this.dataset.classes.split(' ').forEach((cls) => {
|
||||||
|
button.classList.toggle(cls)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,8 +4,18 @@ export default actionable(class extends HTMLElement {
|
|||||||
trigger (event) {
|
trigger (event) {
|
||||||
const elementIds = JSON.parse(this.dataset.elementIds)
|
const elementIds = JSON.parse(this.dataset.elementIds)
|
||||||
|
|
||||||
|
if (event.target.type === 'checkbox') {
|
||||||
|
elementIds.forEach((elementId) => {
|
||||||
|
document.getElementById(elementId)?.classList.toggle('hidden')
|
||||||
|
})
|
||||||
|
} else {
|
||||||
elementIds.forEach((elementId) => {
|
elementIds.forEach((elementId) => {
|
||||||
document.getElementById(elementId).classList.toggle('hidden', (event.target.dataset.toggleId || event.target.value) !== elementId)
|
document.getElementById(elementId).classList.toggle('hidden', (event.target.dataset.toggleId || event.target.value) !== elementId)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.dataset.focusId) {
|
||||||
|
document.getElementById(this.dataset.focusId)?.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
@@ -6,6 +6,7 @@ import ToggleSubmit from './elements/toggle_submit'
|
|||||||
import FetchForm from './elements/fetch_form'
|
import FetchForm from './elements/fetch_form'
|
||||||
import ScrollButtons from './elements/scroll_buttons'
|
import ScrollButtons from './elements/scroll_buttons'
|
||||||
import PageContainer from './elements/page_container'
|
import PageContainer from './elements/page_container'
|
||||||
|
import SubmitForm from './elements/submit_form'
|
||||||
|
|
||||||
const safeRegisterElement = (name, element, options = {}) => !window.customElements.get(name) && window.customElements.define(name, element, options)
|
const safeRegisterElement = (name, element, options = {}) => !window.customElements.get(name) && window.customElements.define(name, element, options)
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ safeRegisterElement('toggle-submit', ToggleSubmit)
|
|||||||
safeRegisterElement('fetch-form', FetchForm)
|
safeRegisterElement('fetch-form', FetchForm)
|
||||||
safeRegisterElement('scroll-buttons', ScrollButtons)
|
safeRegisterElement('scroll-buttons', ScrollButtons)
|
||||||
safeRegisterElement('page-container', PageContainer)
|
safeRegisterElement('page-container', PageContainer)
|
||||||
|
safeRegisterElement('submit-form', SubmitForm)
|
||||||
safeRegisterElement('submission-form', class extends HTMLElement {
|
safeRegisterElement('submission-form', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.appElem = document.createElement('div')
|
this.appElem = document.createElement('div')
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ button .disabled {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
button[disabled] .disabled {
|
button[disabled] .disabled, button.btn-disabled .disabled {
|
||||||
display: initial;
|
display: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ button .enabled {
|
|||||||
display: initial;
|
display: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
button[disabled] .enabled {
|
button[disabled] .enabled, button.btn-disabled .enabled {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ function mouseoverListener (event) {
|
|||||||
|
|
||||||
const requestOptions = {
|
const requestOptions = {
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
headers: { Accept: 'text/html, application/xhtml+xml', 'VND.PREFETCH': 'true' },
|
headers: { Accept: 'text/html, application/xhtml+xml', 'VND.PREFETCH': 'true', 'X-Turbo': 'true' },
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
redirect: 'follow'
|
redirect: 'follow'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,8 +29,15 @@
|
|||||||
{{ optionValue(option) }}
|
{{ optionValue(option) }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ field.title || field.name || fieldNames[field.type] }}
|
<MarkdownContent
|
||||||
<template v-if="field.type === 'checkbox' && !field.name">
|
v-if="field.title"
|
||||||
|
:text-only="true"
|
||||||
|
:string="field.title"
|
||||||
|
/>
|
||||||
|
<template v-else>
|
||||||
|
{{ field.name || fieldNames[field.type] }}
|
||||||
|
</template>
|
||||||
|
<template v-if="field.type === 'checkbox' && !field.name && !field.title">
|
||||||
{{ fieldIndex + 1 }}
|
{{ fieldIndex + 1 }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="!field.required && field.type !== 'checkbox'">
|
<template v-else-if="!field.required && field.type !== 'checkbox'">
|
||||||
@@ -212,6 +219,48 @@
|
|||||||
>
|
>
|
||||||
{{ formatNumber(modelValue, field.preferences?.format) }}
|
{{ formatNumber(modelValue, field.preferences?.format) }}
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="field.type === 'strikethrough'"
|
||||||
|
class="w-full h-full flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
v-if="(((1000.0 / pageWidth) * pageHeight) * area.h) < 40.0"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
>
|
||||||
|
<line
|
||||||
|
x1="0"
|
||||||
|
y1="50%"
|
||||||
|
x2="100%"
|
||||||
|
y2="50%"
|
||||||
|
:stroke="field.preferences?.color || 'red'"
|
||||||
|
:stroke-width="strikethroughWidth"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
:style="{ overflow: 'visible', width: `calc(100% - ${strikethroughWidth})`, height: `calc(100% - ${strikethroughWidth})` }"
|
||||||
|
>
|
||||||
|
<line
|
||||||
|
x1="0"
|
||||||
|
y1="0"
|
||||||
|
x2="100%"
|
||||||
|
y2="100%"
|
||||||
|
:stroke="field.preferences?.color || 'red'"
|
||||||
|
:stroke-width="strikethroughWidth"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="100%"
|
||||||
|
y1="0"
|
||||||
|
x2="0"
|
||||||
|
y2="100%"
|
||||||
|
:stroke="field.preferences?.color || 'red'"
|
||||||
|
:stroke-width="strikethroughWidth"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
class="whitespace-pre-wrap"
|
class="whitespace-pre-wrap"
|
||||||
@@ -222,12 +271,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import MarkdownContent from './markdown_content'
|
||||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconId } from '@tabler/icons-vue'
|
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconId } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FieldArea',
|
name: 'FieldArea',
|
||||||
components: {
|
components: {
|
||||||
IconPaperclip,
|
IconPaperclip,
|
||||||
|
MarkdownContent,
|
||||||
IconCheck
|
IconCheck
|
||||||
},
|
},
|
||||||
inject: ['t'],
|
inject: ['t'],
|
||||||
@@ -251,6 +302,16 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
pageWidth: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
pageHeight: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
isValueSet: {
|
isValueSet: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -333,6 +394,13 @@ export default {
|
|||||||
verification: this.t('verify_id')
|
verification: this.t('verify_id')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
strikethroughWidth () {
|
||||||
|
if (this.isInlineSize) {
|
||||||
|
return '0.6cqmin'
|
||||||
|
} else {
|
||||||
|
return 'clamp(0px, 0.5vw, 6px)'
|
||||||
|
}
|
||||||
|
},
|
||||||
isShowSignatureId () {
|
isShowSignatureId () {
|
||||||
if ([true, false].includes(this.field.preferences?.with_signature_id)) {
|
if ([true, false].includes(this.field.preferences?.with_signature_id)) {
|
||||||
return this.field.preferences.with_signature_id
|
return this.field.preferences.with_signature_id
|
||||||
@@ -422,10 +490,14 @@ export default {
|
|||||||
},
|
},
|
||||||
formattedDate () {
|
formattedDate () {
|
||||||
if (this.field.type === 'date' && this.modelValue) {
|
if (this.field.type === 'date' && this.modelValue) {
|
||||||
|
try {
|
||||||
return this.formatDate(
|
return this.formatDate(
|
||||||
this.modelValue === '{{date}}' ? new Date() : new Date(this.modelValue),
|
this.modelValue === '{{date}}' ? new Date() : new Date(this.modelValue),
|
||||||
this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
|
this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
|
||||||
)
|
)
|
||||||
|
} catch {
|
||||||
|
return this.modelValue
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,14 @@
|
|||||||
<template
|
<template
|
||||||
v-for="(area, areaIndex) in field.areas"
|
v-for="(area, areaIndex) in field.areas"
|
||||||
:key="areaIndex"
|
:key="areaIndex"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-for="(pageElem, index) in [findPageElementForArea(area)]"
|
||||||
|
:key="index"
|
||||||
>
|
>
|
||||||
<Teleport
|
<Teleport
|
||||||
v-if="findPageElementForArea(area)"
|
v-if="pageElem"
|
||||||
:to="findPageElementForArea(area)"
|
:to="pageElem"
|
||||||
>
|
>
|
||||||
<FieldArea
|
<FieldArea
|
||||||
:ref="setAreaRef"
|
:ref="setAreaRef"
|
||||||
@@ -22,6 +26,8 @@
|
|||||||
:field="field"
|
:field="field"
|
||||||
:area="area"
|
:area="area"
|
||||||
:submittable="submittable"
|
:submittable="submittable"
|
||||||
|
:page-width="1400"
|
||||||
|
:page-height="(1400.0 / pageElem.offsetWidth) * pageElem.offsetHeight"
|
||||||
:field-index="fieldIndex"
|
:field-index="fieldIndex"
|
||||||
:is-inline-size="isInlineSize"
|
:is-inline-size="isInlineSize"
|
||||||
:scroll-padding="scrollPadding"
|
:scroll-padding="scrollPadding"
|
||||||
@@ -38,6 +44,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
<div class="space-y-3 mt-5">
|
<div class="space-y-3 mt-5">
|
||||||
<a
|
<a
|
||||||
v-if="completedButton.url"
|
v-if="completedButton.url"
|
||||||
:href="sanitizeHref(completedButton.url)"
|
:href="sanitizeUrl(completedButton.url)"
|
||||||
rel="noopener noreferrer nofollow"
|
rel="noopener noreferrer nofollow"
|
||||||
class="white-button flex items-center w-full completed-form-completed-button"
|
class="white-button flex items-center w-full completed-form-completed-button"
|
||||||
>
|
>
|
||||||
@@ -102,6 +102,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop, IconLogin } from '@tabler/icons-vue'
|
import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop, IconLogin } from '@tabler/icons-vue'
|
||||||
import MarkdownContent from './markdown_content'
|
import MarkdownContent from './markdown_content'
|
||||||
|
import { sanitizeUrl } from '@braintree/sanitize-url'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FormCompleted',
|
name: 'FormCompleted',
|
||||||
@@ -198,6 +199,7 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
sanitizeUrl,
|
||||||
sendCopyToEmail () {
|
sendCopyToEmail () {
|
||||||
this.isSendingCopy = true
|
this.isSendingCopy = true
|
||||||
|
|
||||||
@@ -252,11 +254,6 @@ export default {
|
|||||||
this.isDownloading = false
|
this.isDownloading = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
sanitizeHref (href) {
|
|
||||||
if (href && href.trim().match(/^((?:https?:\/\/)|\/)/)) {
|
|
||||||
return href.replace(/javascript:/g, '')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
downloadSafariIos (urls) {
|
downloadSafariIos (urls) {
|
||||||
const fileRequests = urls.map((url) => {
|
const fileRequests = urls.map((url) => {
|
||||||
return fetch(url).then(async (resp) => {
|
return fetch(url).then(async (resp) => {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
|
v-if="withToday"
|
||||||
class="btn btn-outline btn-sm !normal-case font-normal set-current-date-button"
|
class="btn btn-outline btn-sm !normal-case font-normal set-current-date-button"
|
||||||
@click.prevent="[setCurrentDate(), $emit('focus')]"
|
@click.prevent="[setCurrentDate(), $emit('focus')]"
|
||||||
>
|
>
|
||||||
@@ -46,6 +47,8 @@
|
|||||||
:id="field.uuid"
|
:id="field.uuid"
|
||||||
ref="input"
|
ref="input"
|
||||||
v-model="value"
|
v-model="value"
|
||||||
|
:min="validationMin"
|
||||||
|
:max="validationMax"
|
||||||
class="base-input !text-2xl text-center w-full"
|
class="base-input !text-2xl text-center w-full"
|
||||||
:required="field.required"
|
:required="field.required"
|
||||||
type="date"
|
type="date"
|
||||||
@@ -89,6 +92,44 @@ export default {
|
|||||||
},
|
},
|
||||||
emits: ['update:model-value', 'focus', 'submit'],
|
emits: ['update:model-value', 'focus', 'submit'],
|
||||||
computed: {
|
computed: {
|
||||||
|
dateNowString () {
|
||||||
|
const today = new Date()
|
||||||
|
|
||||||
|
const yyyy = today.getFullYear()
|
||||||
|
const mm = String(today.getMonth() + 1).padStart(2, '0')
|
||||||
|
const dd = String(today.getDate()).padStart(2, '0')
|
||||||
|
|
||||||
|
return `${yyyy}-${mm}-${dd}`
|
||||||
|
},
|
||||||
|
validationMin () {
|
||||||
|
if (this.field.validation?.min) {
|
||||||
|
return ['{{date}}', '{date}'].includes(this.field.validation.min) ? this.dateNowString : this.field.validation.min
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
validationMax () {
|
||||||
|
if (this.field.validation?.max) {
|
||||||
|
return ['{{date}}', '{date}'].includes(this.field.validation.max) ? this.dateNowString : this.field.validation.max
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
withToday () {
|
||||||
|
const todayDate = new Date().setHours(0, 0, 0, 0)
|
||||||
|
|
||||||
|
if (this.validationMin) {
|
||||||
|
if (new Date(this.validationMin).setHours(0, 0, 0, 0) <= todayDate) {
|
||||||
|
return this.validationMax ? (new Date(this.validationMax).setHours(0, 0, 0, 0) >= todayDate) : true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else if (this.validationMax) {
|
||||||
|
return new Date(this.validationMax).setHours(0, 0, 0, 0) >= todayDate
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
value: {
|
value: {
|
||||||
set (value) {
|
set (value) {
|
||||||
this.$emit('update:model-value', value)
|
this.$emit('update:model-value', value)
|
||||||
|
|||||||
@@ -112,11 +112,23 @@ export default {
|
|||||||
onSelectFiles (e) {
|
onSelectFiles (e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
this.uploadFiles(this.$refs.input.files).then(() => {
|
const files = Array.from(this.$refs.input.files).filter((f) => {
|
||||||
|
if (this.accept === 'image/*') {
|
||||||
|
return f.type.startsWith('image')
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.accept === 'image/*' && !files.length) {
|
||||||
|
alert(this.t('please_upload_an_image_file'))
|
||||||
|
} else {
|
||||||
|
this.uploadFiles(files).then(() => {
|
||||||
if (this.$refs.input) {
|
if (this.$refs.input) {
|
||||||
this.$refs.input.value = ''
|
this.$refs.input.value = ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async uploadFiles (files) {
|
async uploadFiles (files) {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<FormulaFieldAreas
|
<FormulaFieldAreas
|
||||||
v-if="formulaFields.length"
|
v-if="formulaFields.length"
|
||||||
:fields="formulaFields"
|
:fields="formulaFields"
|
||||||
:readonly-values="readonlyConditionalFieldValues"
|
:readonly-values="readonlyFieldValues"
|
||||||
:values="values"
|
:values="values"
|
||||||
/>
|
/>
|
||||||
<Teleport
|
<Teleport
|
||||||
@@ -349,10 +349,10 @@
|
|||||||
: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"
|
:required="field.required"
|
||||||
:checked="!!values[field.uuid]"
|
:checked="!!values[field.uuid]"
|
||||||
|
@invalid="$event.target.setCustomValidity(t('please_check_the_box_to_continue'))"
|
||||||
|
@change="$event.target.setCustomValidity($event.target.validity.valueMissing ? t('please_check_the_box_to_continue') : '')"
|
||||||
@click="[scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
|
@click="[scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -454,7 +454,9 @@
|
|||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
|
:fields="formulaFields"
|
||||||
:values="values"
|
:values="values"
|
||||||
|
:readonly-values="readonlyFieldValues"
|
||||||
@attached="attachments.push($event)"
|
@attached="attachments.push($event)"
|
||||||
@focus="scrollIntoField(currentField)"
|
@focus="scrollIntoField(currentField)"
|
||||||
@submit="!isSubmitting && submitStep()"
|
@submit="!isSubmitting && submitStep()"
|
||||||
@@ -572,6 +574,7 @@ import FormCompleted from './completed'
|
|||||||
import { IconInnerShadowTop, IconArrowsDiagonal, IconWritingSign, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
import { IconInnerShadowTop, IconArrowsDiagonal, IconWritingSign, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||||
import AppearsOn from './appears_on'
|
import AppearsOn from './appears_on'
|
||||||
import i18n from './i18n'
|
import i18n from './i18n'
|
||||||
|
import { sanitizeUrl } from '@braintree/sanitize-url'
|
||||||
|
|
||||||
const isEmpty = (obj) => {
|
const isEmpty = (obj) => {
|
||||||
if (obj == null) return true
|
if (obj == null) return true
|
||||||
@@ -871,7 +874,14 @@ export default {
|
|||||||
},
|
},
|
||||||
readonlyConditionalFieldValues () {
|
readonlyConditionalFieldValues () {
|
||||||
return this.readonlyConditionalFields.reduce((acc, f) => {
|
return this.readonlyConditionalFields.reduce((acc, f) => {
|
||||||
acc[f.uuid] = (this.values[f.uuid] || f.default_value)
|
acc[f.uuid] = isEmpty(this.values[f.uuid]) ? f.default_value : this.values[f.uuid]
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
readonlyFieldValues () {
|
||||||
|
return this.readonlyFields.reduce((acc, f) => {
|
||||||
|
acc[f.uuid] = isEmpty(this.values[f.uuid]) ? f.default_value : this.values[f.uuid]
|
||||||
|
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
@@ -971,7 +981,10 @@ export default {
|
|||||||
return this.currentStepFields[0]
|
return this.currentStepFields[0]
|
||||||
},
|
},
|
||||||
readonlyConditionalFields () {
|
readonlyConditionalFields () {
|
||||||
return this.fields.filter((f) => f.readonly && f.conditions?.length && this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f))
|
return this.readonlyFields.filter((f) => f.conditions?.length)
|
||||||
|
},
|
||||||
|
readonlyFields () {
|
||||||
|
return this.fields.filter((f) => f.readonly && this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f))
|
||||||
},
|
},
|
||||||
stepFields () {
|
stepFields () {
|
||||||
const verificationFields = []
|
const verificationFields = []
|
||||||
@@ -1014,7 +1027,11 @@ export default {
|
|||||||
const aArea = (fieldAreasIndex[aField.uuid] ||= [...(aField.areas || [])].sort(sortArea)[0])
|
const aArea = (fieldAreasIndex[aField.uuid] ||= [...(aField.areas || [])].sort(sortArea)[0])
|
||||||
const bArea = (fieldAreasIndex[bField.uuid] ||= [...(bField.areas || [])].sort(sortArea)[0])
|
const bArea = (fieldAreasIndex[bField.uuid] ||= [...(bField.areas || [])].sort(sortArea)[0])
|
||||||
|
|
||||||
|
if (aArea && bArea) {
|
||||||
return sortArea(aArea, bArea)
|
return sortArea(aArea, bArea)
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1476,7 +1493,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.completedRedirectUrl) {
|
if (this.completedRedirectUrl) {
|
||||||
window.location.href = this.completedRedirectUrl
|
window.location.href = sanitizeUrl(this.completedRedirectUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,13 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
isInlineSize () {
|
isInlineSize () {
|
||||||
return CSS.supports('container-type: size')
|
return CSS.supports('container-type: size')
|
||||||
|
},
|
||||||
|
fieldsUuidIndex () {
|
||||||
|
return this.fields.reduce((acc, field) => {
|
||||||
|
acc[field.uuid] = field
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted () {
|
async mounted () {
|
||||||
@@ -96,8 +103,19 @@ export default {
|
|||||||
findPageElementForArea (area) {
|
findPageElementForArea (area) {
|
||||||
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}`)
|
||||||
},
|
},
|
||||||
|
normalizeFormula (formula, depth = 0) {
|
||||||
|
if (depth > 10) return formula
|
||||||
|
|
||||||
|
return formula.replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||||
|
if (this.fieldsUuidIndex[uuid]) {
|
||||||
|
return `(${this.normalizeFormula(this.fieldsUuidIndex[uuid].preferences.formula, depth + 1)})`
|
||||||
|
} else {
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
calculateFormula (field) {
|
calculateFormula (field) {
|
||||||
const transformedFormula = field.preferences.formula.replace(/{{(.*?)}}/g, (match, uuid) => {
|
const transformedFormula = this.normalizeFormula(field.preferences.formula).replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||||
return this.readonlyValues[uuid] || this.values[uuid] || 0.0
|
return this.readonlyValues[uuid] || this.values[uuid] || 0.0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<span>
|
<span>
|
||||||
|
<span v-if="textOnly">
|
||||||
|
{{ dom.body.textContent }}
|
||||||
|
</span>
|
||||||
|
<template v-else>
|
||||||
<template
|
<template
|
||||||
v-for="(item, index) in items"
|
v-for="(item, index) in nodes || dom.body.childNodes"
|
||||||
:key="index"
|
:key="index"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
v-if="item.startsWith('<a') && item.endsWith('</a>')"
|
v-if="item.tagName === 'A' && item.getAttribute('href') !== 'undefined'"
|
||||||
:href="sanitizeHref(extractAttr(item, 'href'))"
|
:href="sanitizeUrl(item.getAttribute('href'))"
|
||||||
rel="noopener noreferrer nofollow"
|
rel="noopener noreferrer nofollow"
|
||||||
:class="extractAttr(item, 'class') || 'link'"
|
:class="item.getAttribute('class') || 'link'"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
{{ extractText(item) }}
|
<MarkdownContent :nodes="item.childNodes" />
|
||||||
</a>
|
</a>
|
||||||
<b
|
<component
|
||||||
v-else-if="item.startsWith('<b>') || item.startsWith('<strong>')"
|
:is="item.tagName"
|
||||||
|
v-else-if="safeTags.includes(item.tagName)"
|
||||||
>
|
>
|
||||||
{{ extractText(item) }}
|
<MarkdownContent :nodes="item.childNodes" />
|
||||||
</b>
|
</component>
|
||||||
<i
|
<br v-else-if="item.tagName === 'BR' || item.nodeValue === '\n'">
|
||||||
v-else-if="item.startsWith('<i>') || item.startsWith('<em>')"
|
<template v-else>
|
||||||
>
|
{{ item.textContent }}
|
||||||
{{ extractText(item) }}
|
</template>
|
||||||
</i>
|
|
||||||
<br
|
|
||||||
v-else-if="item === '<br>' || item === '\n'"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
v-else
|
|
||||||
>
|
|
||||||
{{ item }}
|
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</span>
|
</span>
|
||||||
@@ -37,8 +34,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import snarkdown from 'snarkdown'
|
import snarkdown from 'snarkdown'
|
||||||
|
import { sanitizeUrl } from '@braintree/sanitize-url'
|
||||||
const htmlSplitRegexp = /(<a.+?<\/a>|<i>.+?<\/i>|<b>.+?<\/b>|<em>.+?<\/em>|<strong>.+?<\/strong>|<br>)/
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MarkdownContent',
|
name: 'MarkdownContent',
|
||||||
@@ -47,10 +43,23 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: ''
|
default: ''
|
||||||
|
},
|
||||||
|
nodes: {
|
||||||
|
type: [Array, Object],
|
||||||
|
require: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
textOnly: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
items () {
|
safeTags () {
|
||||||
|
return ['UL', 'I', 'EM', 'B', 'STRONG', 'P']
|
||||||
|
},
|
||||||
|
dom () {
|
||||||
const linkParts = this.string.split(/(https?:\/\/[^\s)]+)/g)
|
const linkParts = this.string.split(/(https?:\/\/[^\s)]+)/g)
|
||||||
|
|
||||||
const text = linkParts.map((part, index) => {
|
const text = linkParts.map((part, index) => {
|
||||||
@@ -61,25 +70,11 @@ export default {
|
|||||||
}
|
}
|
||||||
}).join('')
|
}).join('')
|
||||||
|
|
||||||
return snarkdown(text.replace(/\n/g, '<br>')).split(htmlSplitRegexp)
|
return new DOMParser().parseFromString(snarkdown(text.replace(/\n/g, '<br>')), 'text/html')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
sanitizeHref (href) {
|
sanitizeUrl
|
||||||
if (href && href.trim().match(/^((?:https?:\/\/)|\/)/)) {
|
|
||||||
return href.replace(/javascript:/g, '')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
extractAttr (text, attr) {
|
|
||||||
if (text.includes(attr)) {
|
|
||||||
return text.split(attr).pop().split('"')[1]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
extractText (text) {
|
|
||||||
if (text) {
|
|
||||||
return text.match(/>(.+?)</)?.[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -39,8 +39,10 @@
|
|||||||
:id="field.uuid"
|
:id="field.uuid"
|
||||||
v-model="number"
|
v-model="number"
|
||||||
type="number"
|
type="number"
|
||||||
|
:step="field.validation?.step || 'any'"
|
||||||
|
:min="field.validation?.min"
|
||||||
|
:max="field.validation?.max"
|
||||||
class="base-input !text-2xl w-full"
|
class="base-input !text-2xl w-full"
|
||||||
step="any"
|
|
||||||
:required="field.required"
|
:required="field.required"
|
||||||
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
|
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||||
:name="`values[${field.uuid}]`"
|
:name="`values[${field.uuid}]`"
|
||||||
|
|||||||
@@ -92,10 +92,20 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
readonlyValues: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
values: {
|
values: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
fields: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
submitterSlug: {
|
submitterSlug: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
@@ -109,6 +119,13 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
fieldsUuidIndex () {
|
||||||
|
return this.fields.reduce((acc, field) => {
|
||||||
|
acc[field.uuid] = field
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
queryParams () {
|
queryParams () {
|
||||||
return new URLSearchParams(window.location.search)
|
return new URLSearchParams(window.location.search)
|
||||||
},
|
},
|
||||||
@@ -116,6 +133,10 @@ export default {
|
|||||||
return this.queryParams.get('stripe_session_id')
|
return this.queryParams.get('stripe_session_id')
|
||||||
},
|
},
|
||||||
defaultName () {
|
defaultName () {
|
||||||
|
if (this.field.preferences?.price_id || this.field.preferences?.payment_link_id) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
const { price, currency } = this.field.preferences || {}
|
const { price, currency } = this.field.preferences || {}
|
||||||
|
|
||||||
const formatter = new Intl.NumberFormat([], {
|
const formatter = new Intl.NumberFormat([], {
|
||||||
@@ -178,12 +199,23 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
calculateFormula () {
|
calculateFormula () {
|
||||||
const transformedFormula = this.field.preferences.formula.replace(/{{(.*?)}}/g, (match, uuid) => {
|
const transformedFormula = this.normalizeFormula(this.field.preferences.formula).replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||||
return this.values[uuid] || 0.0
|
return this.readonlyValues[uuid] || this.values[uuid] || 0.0
|
||||||
})
|
})
|
||||||
|
|
||||||
return this.math.evaluate(transformedFormula.toLowerCase())
|
return this.math.evaluate(transformedFormula.toLowerCase())
|
||||||
},
|
},
|
||||||
|
normalizeFormula (formula, depth = 0) {
|
||||||
|
if (depth > 10) return formula
|
||||||
|
|
||||||
|
return formula.replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||||
|
if (this.fieldsUuidIndex[uuid]) {
|
||||||
|
return `(${this.normalizeFormula(this.fieldsUuidIndex[uuid].preferences.formula, depth + 1)})`
|
||||||
|
} else {
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
async submit () {
|
async submit () {
|
||||||
if (this.sessionId) {
|
if (this.sessionId) {
|
||||||
return fetch(this.baseUrl + '/api/stripe_payments/' + this.sessionId, {
|
return fetch(this.baseUrl + '/api/stripe_payments/' + this.sessionId, {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<div class="space-x-2 flex flex-none">
|
<div class="space-x-2 flex flex-none">
|
||||||
<span
|
<span
|
||||||
v-if="isTextSignature && format !== 'typed' && format !== 'upload'"
|
v-if="isTextSignature && format !== 'typed_or_upload' && format !== 'typed' && format !== 'upload'"
|
||||||
class="tooltip"
|
class="tooltip"
|
||||||
:data-tip="t('draw_signature')"
|
:data-tip="t('draw_signature')"
|
||||||
>
|
>
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-else-if="withTypedSignature && format !== 'typed' && format !== 'drawn' && format !== 'upload'"
|
v-else-if="withTypedSignature && format !== 'drawn_or_upload' && format !== 'typed_or_upload' && format !== 'typed' && format !== 'drawn' && format !== 'upload'"
|
||||||
class="tooltip ml-2"
|
class="tooltip ml-2"
|
||||||
:class="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
|
:class="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
|
||||||
:data-tip="t('type_text')"
|
:data-tip="t('type_text')"
|
||||||
@@ -85,8 +85,8 @@
|
|||||||
{{ t(format === 'upload' ? 'reupload' : 'redraw') }}
|
{{ t(format === 'upload' ? 'reupload' : 'redraw') }}
|
||||||
</a>
|
</a>
|
||||||
<span
|
<span
|
||||||
v-if="withQrButton && !modelValue && !computedPreviousValue && format !== 'typed' && format !== 'upload'"
|
v-if="withQrButton && !modelValue && !computedPreviousValue && format !== 'typed_or_upload' && format !== 'typed' && format !== 'upload'"
|
||||||
class=" tooltip"
|
class="tooltip before:translate-x-[-90%]"
|
||||||
:data-tip="t('drawn_signature_on_a_touchscreen_device')"
|
:data-tip="t('drawn_signature_on_a_touchscreen_device')"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@@ -395,7 +395,7 @@ export default {
|
|||||||
isShowQr: false,
|
isShowQr: false,
|
||||||
isOtherReason: false,
|
isOtherReason: false,
|
||||||
isUsePreviousValue: true,
|
isUsePreviousValue: true,
|
||||||
isTextSignature: this.field.preferences?.format === 'typed',
|
isTextSignature: this.field.preferences?.format === 'typed' || this.field.preferences?.format === 'typed_or_upload',
|
||||||
uploadImageInputKey: Math.random().toString()
|
uploadImageInputKey: Math.random().toString()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<MarkdownContent :string="field.description" />
|
<MarkdownContent :string="field.description" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="emptyValueRequiredStep && emptyValueRequiredStep[0] !== field"
|
v-if="isRequiredFieldEmpty"
|
||||||
class="px-1 field-description-text"
|
class="px-1 field-description-text"
|
||||||
>
|
>
|
||||||
{{ t('complete_all_required_fields_to_proceed_with_identity_verification') }}
|
{{ t('complete_all_required_fields_to_proceed_with_identity_verification') }}
|
||||||
@@ -100,6 +100,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isRequiredFieldEmpty () {
|
||||||
|
return this.emptyValueRequiredStep && this.emptyValueRequiredStep[0] !== this.field
|
||||||
|
},
|
||||||
countryCode () {
|
countryCode () {
|
||||||
const browserTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
const browserTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||||
const browserTz = browserTimeZone.split('/')[1]
|
const browserTz = browserTimeZone.split('/')[1]
|
||||||
@@ -131,6 +134,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted () {
|
async mounted () {
|
||||||
|
if (!this.isRequiredFieldEmpty) {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
|
|
||||||
if (new URLSearchParams(window.location.search).get('submit') === 'true') {
|
if (new URLSearchParams(window.location.search).get('submit') === 'true') {
|
||||||
@@ -143,6 +147,7 @@ export default {
|
|||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
start () {
|
start () {
|
||||||
@@ -156,6 +161,10 @@ export default {
|
|||||||
}).then(async (resp) => {
|
}).then(async (resp) => {
|
||||||
this.eidEasyData = await resp.json()
|
this.eidEasyData = await resp.json()
|
||||||
|
|
||||||
|
if (this.eidEasyData.check_completed) {
|
||||||
|
this.$emit('submit')
|
||||||
|
}
|
||||||
|
|
||||||
if (this.eidEasyData.available_methods[0] === 'itsme-qes-signature' &&
|
if (this.eidEasyData.available_methods[0] === 'itsme-qes-signature' &&
|
||||||
this.eidEasyData.available_methods.length === 1) {
|
this.eidEasyData.available_methods.length === 1) {
|
||||||
const redirectUrl = new URL('https://id.eideasy.com/sign_contract_external')
|
const redirectUrl = new URL('https://id.eideasy.com/sign_contract_external')
|
||||||
@@ -166,7 +175,7 @@ export default {
|
|||||||
redirectUrl.searchParams.append('lang', this.locale)
|
redirectUrl.searchParams.append('lang', this.locale)
|
||||||
|
|
||||||
this.redirectUrl = redirectUrl.toString()
|
this.redirectUrl = redirectUrl.toString()
|
||||||
} else {
|
} else if (this.$refs.widgetContainer) {
|
||||||
const eidEasyWidget = document.createElement('eideasy-widget')
|
const eidEasyWidget = document.createElement('eideasy-widget')
|
||||||
|
|
||||||
for (const key in this.widgetSettings) {
|
for (const key in this.widgetSettings) {
|
||||||
@@ -179,15 +188,23 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
async submit () {
|
async submit () {
|
||||||
return fetch(this.baseUrl + '/api/identity_verification', {
|
const resp = await fetch(this.baseUrl + '/api/identity_verification', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
submitter_slug: this.submitterSlug
|
submitter_slug: this.submitterSlug
|
||||||
}),
|
}),
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
}).then(async (resp) => {
|
|
||||||
return resp
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (resp.status === 404) {
|
||||||
|
throw new Error('Verification not completed yet')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resp.ok) {
|
||||||
|
return resp
|
||||||
|
} else {
|
||||||
|
throw new Error('Verification failed')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<div
|
<div
|
||||||
v-if="isSelected || isDraw"
|
v-if="isSelected || isDraw"
|
||||||
class="top-0 bottom-0 right-0 left-0 absolute border border-1.5 pointer-events-none"
|
class="top-0 bottom-0 right-0 left-0 absolute border border-1.5 pointer-events-none"
|
||||||
:class="field.type === 'heading' ? '' : borderColors[submitterIndex % borderColors.length]"
|
:class="activeBorderClasses"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="field.type === 'cells' && (isSelected || isDraw)"
|
v-if="field.type === 'cells' && (isSelected || isDraw)"
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
@pointerdown.stop
|
@pointerdown.stop
|
||||||
>
|
>
|
||||||
<FieldSubmitter
|
<FieldSubmitter
|
||||||
v-if="field.type != 'heading'"
|
v-if="field.type != 'heading' && field.type != 'strikethrough'"
|
||||||
v-model="field.submitter_uuid"
|
v-model="field.submitter_uuid"
|
||||||
class="border-r roles-dropdown"
|
class="border-r roles-dropdown"
|
||||||
:compact="true"
|
:compact="true"
|
||||||
@@ -72,11 +72,11 @@
|
|||||||
@blur="onNameBlur"
|
@blur="onNameBlur"
|
||||||
>{{ optionIndexText }} {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || defaultName }}</span>
|
>{{ optionIndexText }} {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || defaultName }}</span>
|
||||||
<div
|
<div
|
||||||
v-if="isSettingsFocus || (isValueInput && field.type !== 'heading') || (isNameFocus && !['checkbox', 'phone'].includes(field.type))"
|
v-if="isSettingsFocus || isSelectInput || (isValueInput && field.type !== 'heading') || (isNameFocus && !['checkbox', 'phone'].includes(field.type))"
|
||||||
class="flex items-center ml-1.5"
|
class="flex items-center ml-1.5"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
v-if="!isValueInput"
|
v-if="!isValueInput && !isSelectInput"
|
||||||
:id="`required-checkbox-${field.uuid}`"
|
:id="`required-checkbox-${field.uuid}`"
|
||||||
v-model="field.required"
|
v-model="field.required"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -84,14 +84,14 @@
|
|||||||
@mousedown.prevent
|
@mousedown.prevent
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
v-if="!isValueInput"
|
v-if="!isValueInput && !isSelectInput"
|
||||||
:for="`required-checkbox-${field.uuid}`"
|
:for="`required-checkbox-${field.uuid}`"
|
||||||
class="label text-xs"
|
class="label text-xs"
|
||||||
@click.prevent="field.required = !field.required"
|
@click.prevent="field.required = !field.required"
|
||||||
@mousedown.prevent
|
@mousedown.prevent
|
||||||
>{{ t('required') }}</label>
|
>{{ t('required') }}</label>
|
||||||
<input
|
<input
|
||||||
v-if="isValueInput"
|
v-if="isValueInput || isSelectInput"
|
||||||
:id="`readonly-checkbox-${field.uuid}`"
|
:id="`readonly-checkbox-${field.uuid}`"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="checkbox checkbox-xs no-animation rounded"
|
class="checkbox checkbox-xs no-animation rounded"
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
@mousedown.prevent
|
@mousedown.prevent
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
v-if="isValueInput"
|
v-if="isValueInput || isSelectInput"
|
||||||
:for="`readonly-checkbox-${field.uuid}`"
|
:for="`readonly-checkbox-${field.uuid}`"
|
||||||
class="label text-xs"
|
class="label text-xs"
|
||||||
@click.prevent="field.readonly = !(field.readonly ?? true)"
|
@click.prevent="field.readonly = !(field.readonly ?? true)"
|
||||||
@@ -141,6 +141,7 @@
|
|||||||
:with-required="false"
|
:with-required="false"
|
||||||
:with-areas="false"
|
:with-areas="false"
|
||||||
:with-signature-id="withSignatureId"
|
:with-signature-id="withSignatureId"
|
||||||
|
:with-prefillable="withPrefillable"
|
||||||
@click-formula="isShowFormulaModal = true"
|
@click-formula="isShowFormulaModal = true"
|
||||||
@click-font="isShowFontModal = true"
|
@click-font="isShowFontModal = true"
|
||||||
@click-description="isShowDescriptionModal = true"
|
@click-description="isShowDescriptionModal = true"
|
||||||
@@ -163,30 +164,81 @@
|
|||||||
ref="touchValueTarget"
|
ref="touchValueTarget"
|
||||||
class="flex h-full w-full field-area"
|
class="flex h-full w-full field-area"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
:class="[isValueInput ? 'bg-opacity-50' : 'bg-opacity-80', field.type === 'heading' ? 'bg-gray-50' : bgColors[submitterIndex % bgColors.length], isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? fontClasses : 'justify-center items-center']"
|
:class="[isValueInput ? 'cursor-text' : '', isValueInput || isCheckboxInput || isSelectInput ? 'bg-opacity-50' : 'bg-opacity-80', bgClasses, isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? fontClasses : 'justify-center items-center']"
|
||||||
@click="focusValueInput"
|
@click="focusValueInput"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-if="field"
|
v-if="field"
|
||||||
class="flex justify-center items-center space-x-1"
|
class="flex justify-center items-center space-x-1"
|
||||||
:class="{ 'w-full': ['cells', 'checkbox'].includes(field.type), 'h-full': !isValueInput && !isDefaultValuePresent }"
|
:class="{ 'w-full': isWFullType, 'h-full': !isValueInput && (!isDefaultValuePresent || field.type === 'strikethrough') }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas && field.type !== 'checkbox')"
|
v-if="field.type === 'strikethrough'"
|
||||||
:class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type) }"
|
class="w-full h-full flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
v-if="(((basePageWidth / pageWidth) * pageHeight) * area.h) < 41.6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
>
|
||||||
|
<line
|
||||||
|
x1="0"
|
||||||
|
y1="50%"
|
||||||
|
x2="100%"
|
||||||
|
y2="50%"
|
||||||
|
:stroke="field.preferences?.color || 'red'"
|
||||||
|
:stroke-width="strikethroughWidth"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
:style="{ overflow: 'visible', width: `calc(100% - ${strikethroughWidth})`, height: `calc(100% - ${strikethroughWidth})` }"
|
||||||
|
>
|
||||||
|
<line
|
||||||
|
x1="0"
|
||||||
|
y1="0"
|
||||||
|
x2="100%"
|
||||||
|
y2="100%"
|
||||||
|
:stroke="field.preferences?.color || 'red'"
|
||||||
|
:stroke-width="strikethroughWidth"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="100%"
|
||||||
|
y1="0"
|
||||||
|
x2="0"
|
||||||
|
y2="100%"
|
||||||
|
:stroke="field.preferences?.color || 'red'"
|
||||||
|
:stroke-width="strikethroughWidth"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="isDefaultValuePresent || isValueInput || isSelectInput || (withFieldPlaceholder && field.areas && field.type !== 'checkbox')"
|
||||||
|
:class="{ 'w-full h-full': isWFullType }"
|
||||||
:style="fontStyle"
|
:style="fontStyle"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref="textContainer"
|
ref="textContainer"
|
||||||
class="flex items-center px-0.5"
|
class="flex items-center px-0.5"
|
||||||
:style="{ color: field.preferences?.color }"
|
:style="{ color: field.preferences?.color }"
|
||||||
:class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type) }"
|
:class="{ 'w-full h-full': isWFullType }"
|
||||||
>
|
>
|
||||||
<IconCheck
|
<IconCheck
|
||||||
v-if="field.type == 'checkbox'"
|
v-if="field.type == 'checkbox'"
|
||||||
class="aspect-square mx-auto"
|
class="aspect-square mx-auto"
|
||||||
: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 }"
|
||||||
/>
|
/>
|
||||||
|
<template
|
||||||
|
v-else-if="(field.type === 'radio' || field.type === 'multiple') && field?.areas?.length > 1"
|
||||||
|
>
|
||||||
|
<IconCheck
|
||||||
|
v-if="field.type === 'multiple' ? field.default_value.includes(buildAreaOptionValue(area)) : buildAreaOptionValue(area) === field.default_value"
|
||||||
|
class="aspect-square mx-auto"
|
||||||
|
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
<span
|
<span
|
||||||
v-else-if="field.type === 'number' && !isValueInput && (field.default_value || field.default_value == 0)"
|
v-else-if="field.type === 'number' && !isValueInput && (field.default_value || field.default_value == 0)"
|
||||||
class="whitespace-pre-wrap"
|
class="whitespace-pre-wrap"
|
||||||
@@ -209,14 +261,39 @@
|
|||||||
{{ char }}
|
{{ char }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<select
|
||||||
|
v-else-if="isSelectInput"
|
||||||
|
ref="defaultValueSelect"
|
||||||
|
class="bg-transparent outline-none focus:outline-none w-full"
|
||||||
|
@change="[field.default_value = $event.target.value, field.readonly = !!field.default_value?.length, save()]"
|
||||||
|
@focus="selectedAreaRef.value = area"
|
||||||
|
@keydown.enter="onDefaultValueEnter"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
:disabled="!field.default_value?.length"
|
||||||
|
:selected="!field.default_value?.length"
|
||||||
|
:value="''"
|
||||||
|
>
|
||||||
|
{{ t(field.default_value?.length ? 'none' : 'select') }}
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
v-for="(option, index) in field.options"
|
||||||
|
:key="index"
|
||||||
|
:selected="field.default_value === option.value"
|
||||||
|
:value="option.value"
|
||||||
|
>
|
||||||
|
{{ option.value }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
ref="defaultValue"
|
ref="defaultValue"
|
||||||
:contenteditable="isValueInput"
|
:contenteditable="isValueInput"
|
||||||
class="whitespace-pre-wrap outline-none empty:before:content-[attr(placeholder)] before:text-gray-400"
|
class="whitespace-pre-wrap outline-none empty:before:content-[attr(placeholder)] before:text-base-content/30"
|
||||||
:class="{ 'cursor-text': isValueInput }"
|
:class="{ 'cursor-text': isValueInput }"
|
||||||
:placeholder="withFieldPlaceholder && !isValueInput ? defaultField?.title || field.title || field.name || defaultName : t('type_value')"
|
:placeholder="withFieldPlaceholder && !isValueInput ? defaultField?.title || field.title || field.name || defaultName : (field.type === 'date' ? field.preferences?.format || t('type_value') : t('type_value'))"
|
||||||
@blur="onDefaultValueBlur"
|
@blur="onDefaultValueBlur"
|
||||||
|
@focus="selectedAreaRef.value = area"
|
||||||
@paste.prevent="onPaste"
|
@paste.prevent="onPaste"
|
||||||
@keydown.enter="onDefaultValueEnter"
|
@keydown.enter="onDefaultValueEnter"
|
||||||
>{{ field.default_value }}</span>
|
>{{ field.default_value }}</span>
|
||||||
@@ -224,7 +301,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<component
|
<component
|
||||||
:is="fieldIcons[field.type]"
|
:is="fieldIcons[field.type]"
|
||||||
v-else
|
v-else-if="!isCheckboxInput"
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
class="max-h-10 opacity-50"
|
class="max-h-10 opacity-50"
|
||||||
@@ -232,12 +309,12 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="!isValueInput"
|
v-if="!isValueInput && !isSelectInput"
|
||||||
ref="touchTarget"
|
ref="touchTarget"
|
||||||
class="absolute top-0 bottom-0 right-0 left-0"
|
class="absolute top-0 bottom-0 right-0 left-0"
|
||||||
:class="isDragged ? 'cursor-grab' : 'cursor-pointer'"
|
:class="isDragged ? 'cursor-grab' : 'cursor-pointer'"
|
||||||
@dblclick="maybeToggleDefaultValue"
|
@dblclick="maybeToggleDefaultValue"
|
||||||
@click="maybeToggleCheckboxValue"
|
@click="inputMode && maybeToggleCheckboxValue()"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
v-if="field?.type && editable"
|
v-if="field?.type && editable"
|
||||||
@@ -252,6 +329,7 @@
|
|||||||
<FormulaModal
|
<FormulaModal
|
||||||
:field="field"
|
:field="field"
|
||||||
:editable="editable && !defaultField"
|
:editable="editable && !defaultField"
|
||||||
|
:default-field="defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
@close="isShowFormulaModal = false"
|
@close="isShowFormulaModal = false"
|
||||||
/>
|
/>
|
||||||
@@ -263,6 +341,7 @@
|
|||||||
<FontModal
|
<FontModal
|
||||||
:field="field"
|
:field="field"
|
||||||
:editable="editable && !defaultField"
|
:editable="editable && !defaultField"
|
||||||
|
:default-field="defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
@close="isShowFontModal = false"
|
@close="isShowFontModal = false"
|
||||||
/>
|
/>
|
||||||
@@ -274,6 +353,7 @@
|
|||||||
<ConditionsModal
|
<ConditionsModal
|
||||||
:item="field"
|
:item="field"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
|
:default-field="defaultField"
|
||||||
@close="isShowConditionsModal = false"
|
@close="isShowConditionsModal = false"
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
@@ -284,6 +364,7 @@
|
|||||||
<DescriptionModal
|
<DescriptionModal
|
||||||
:field="field"
|
:field="field"
|
||||||
:editable="editable && !defaultField"
|
:editable="editable && !defaultField"
|
||||||
|
:default-field="defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
@close="isShowDescriptionModal = false"
|
@close="isShowDescriptionModal = false"
|
||||||
/>
|
/>
|
||||||
@@ -353,6 +434,21 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
withPrefillable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
pageWidth: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
pageHeight: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
defaultSubmitters: {
|
defaultSubmitters: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -392,6 +488,34 @@ export default {
|
|||||||
fieldNames: FieldType.computed.fieldNames,
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
fieldLabels: FieldType.computed.fieldLabels,
|
fieldLabels: FieldType.computed.fieldLabels,
|
||||||
fieldIcons: FieldType.computed.fieldIcons,
|
fieldIcons: FieldType.computed.fieldIcons,
|
||||||
|
bgClasses () {
|
||||||
|
if (this.field.type === 'heading') {
|
||||||
|
return 'bg-gray-50'
|
||||||
|
} else if (this.field.type === 'strikethrough') {
|
||||||
|
return 'bg-transparent'
|
||||||
|
} else {
|
||||||
|
return this.bgColors[this.submitterIndex % this.bgColors.length]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
activeBorderClasses () {
|
||||||
|
if (this.field.type === 'heading') {
|
||||||
|
return ''
|
||||||
|
} else if (this.field.type === 'strikethrough') {
|
||||||
|
return 'border-dashed border-gray-300'
|
||||||
|
} else {
|
||||||
|
return this.borderColors[this.submitterIndex % this.borderColors.length]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isWFullType () {
|
||||||
|
return ['cells', 'checkbox', 'radio', 'multiple', 'select', 'strikethrough'].includes(this.field.type)
|
||||||
|
},
|
||||||
|
strikethroughWidth () {
|
||||||
|
if (this.isInlineSize) {
|
||||||
|
return '0.6cqmin'
|
||||||
|
} else {
|
||||||
|
return 'clamp(0px, 0.5vw, 6px)'
|
||||||
|
}
|
||||||
|
},
|
||||||
fontStyle () {
|
fontStyle () {
|
||||||
let fontSize = ''
|
let fontSize = ''
|
||||||
|
|
||||||
@@ -411,24 +535,37 @@ export default {
|
|||||||
|
|
||||||
return { fontSize, lineHeight: `calc(${fontSize} * ${this.lineHeight})` }
|
return { fontSize, lineHeight: `calc(${fontSize} * ${this.lineHeight})` }
|
||||||
},
|
},
|
||||||
|
optionsUuidIndex () {
|
||||||
|
return this.field.options.reduce((acc, option) => {
|
||||||
|
acc[option.uuid] = option
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
fontSizePx () {
|
fontSizePx () {
|
||||||
return parseInt(this.field?.preferences?.font_size || 11) * this.fontScale
|
return parseInt(this.field?.preferences?.font_size || 11) * this.fontScale
|
||||||
},
|
},
|
||||||
lineHeight () {
|
lineHeight () {
|
||||||
return 1.3
|
return 1.3
|
||||||
},
|
},
|
||||||
|
basePageWidth () {
|
||||||
|
return 1040.0
|
||||||
|
},
|
||||||
fontScale () {
|
fontScale () {
|
||||||
return 1040 / 612.0
|
return this.basePageWidth / 612.0
|
||||||
},
|
},
|
||||||
isDefaultValuePresent () {
|
isDefaultValuePresent () {
|
||||||
if (this.field?.type === 'radio' && this.field?.areas?.length > 1) {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return this.field?.default_value || this.field?.default_value === 0
|
return this.field?.default_value || this.field?.default_value === 0
|
||||||
}
|
},
|
||||||
|
isSelectInput () {
|
||||||
|
return this.inputMode && (this.field.type === 'select' || (this.field.type === 'radio' && this.field.areas?.length < 2))
|
||||||
|
},
|
||||||
|
isCheckboxInput () {
|
||||||
|
return this.inputMode && (this.field.type === 'checkbox' || (['radio', 'multiple'].includes(this.field.type) && this.area.option_uuid))
|
||||||
},
|
},
|
||||||
isValueInput () {
|
isValueInput () {
|
||||||
return (this.field.type === 'heading' && this.isHeadingSelected) || this.isContenteditable || (this.inputMode && ['text', 'number', 'date'].includes(this.field.type))
|
return (this.field.type === 'heading' && this.isHeadingSelected) || this.isContenteditable ||
|
||||||
|
(this.inputMode && (['text', 'number'].includes(this.field.type) || (this.field.type === 'date' && this.field.default_value !== '{{date}}')))
|
||||||
},
|
},
|
||||||
modalContainerEl () {
|
modalContainerEl () {
|
||||||
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
||||||
@@ -543,6 +680,11 @@ export default {
|
|||||||
closeDropdown () {
|
closeDropdown () {
|
||||||
this.$el.getRootNode().activeElement.blur()
|
this.$el.getRootNode().activeElement.blur()
|
||||||
},
|
},
|
||||||
|
buildAreaOptionValue (area) {
|
||||||
|
const option = this.optionsUuidIndex[area.option_uuid]
|
||||||
|
|
||||||
|
return option.value || `${this.t('option')} ${this.field.options.indexOf(option) + 1}`
|
||||||
|
},
|
||||||
maybeToggleDefaultValue () {
|
maybeToggleDefaultValue () {
|
||||||
if (!this.editable) {
|
if (!this.editable) {
|
||||||
return
|
return
|
||||||
@@ -552,22 +694,41 @@ export default {
|
|||||||
this.isContenteditable = true
|
this.isContenteditable = true
|
||||||
|
|
||||||
this.focusValueInput()
|
this.focusValueInput()
|
||||||
} else if (this.field.type === 'checkbox') {
|
|
||||||
this.field.readonly = !this.field.readonly
|
|
||||||
this.field.default_value === true ? delete this.field.default_value : this.field.default_value = true
|
|
||||||
|
|
||||||
this.save()
|
|
||||||
} else if (this.field.type === 'date') {
|
} else if (this.field.type === 'date') {
|
||||||
this.field.readonly = !this.field.readonly
|
this.field.readonly = !this.field.readonly
|
||||||
this.field.default_value === '{{date}}' ? delete this.field.default_value : this.field.default_value = '{{date}}'
|
this.field.default_value === '{{date}}' ? delete this.field.default_value : this.field.default_value = '{{date}}'
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
|
} else {
|
||||||
|
this.maybeToggleCheckboxValue()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
maybeToggleCheckboxValue () {
|
maybeToggleCheckboxValue () {
|
||||||
if (this.inputMode && this.field.type === 'checkbox') {
|
if (this.field.type === 'checkbox') {
|
||||||
this.field.readonly = !this.field.readonly
|
|
||||||
this.field.default_value === true ? delete this.field.default_value : this.field.default_value = true
|
this.field.default_value === true ? delete this.field.default_value : this.field.default_value = true
|
||||||
|
this.field.readonly = this.field.default_value === true
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
} else if (this.field.type === 'radio' && this.area.option_uuid) {
|
||||||
|
const value = this.buildAreaOptionValue(this.area)
|
||||||
|
|
||||||
|
this.field.default_value === value ? delete this.field.default_value : this.field.default_value = value
|
||||||
|
|
||||||
|
this.field.readonly = !!this.field.default_value?.length
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
} else if (this.field.type === 'multiple' && this.area.option_uuid) {
|
||||||
|
const value = this.buildAreaOptionValue(this.area)
|
||||||
|
|
||||||
|
if (this.field.default_value?.includes(value)) {
|
||||||
|
this.field.default_value.splice(this.field.default_value.indexOf(value), 1)
|
||||||
|
|
||||||
|
if (!this.field.default_value?.length) delete this.field.default_value
|
||||||
|
} else {
|
||||||
|
Array.isArray(this.field.default_value) ? this.field.default_value.push(value) : this.field.default_value = [value]
|
||||||
|
}
|
||||||
|
|
||||||
|
this.field.readonly = !!this.field.default_value?.length
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
}
|
}
|
||||||
@@ -661,10 +822,15 @@ export default {
|
|||||||
delete this.field.options
|
delete this.field.options
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['heading'].includes(this.field.type)) {
|
if (this.field.type === 'heading') {
|
||||||
this.field.readonly = true
|
this.field.readonly = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.field.type === 'strikethrough') {
|
||||||
|
this.field.readonly = true
|
||||||
|
this.field.default_value = true
|
||||||
|
}
|
||||||
|
|
||||||
if (['select', 'multiple', 'radio'].includes(this.field.type)) {
|
if (['select', 'multiple', 'radio'].includes(this.field.type)) {
|
||||||
this.field.options ||= [{ value: '', uuid: v4() }]
|
this.field.options ||= [{ value: '', uuid: v4() }]
|
||||||
}
|
}
|
||||||
@@ -743,7 +909,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
drag (e) {
|
drag (e) {
|
||||||
if (e.target.id === 'mask') {
|
if (e.target.id === 'mask' && this.editable) {
|
||||||
this.isDragged = true
|
this.isDragged = true
|
||||||
|
|
||||||
this.area.x = (e.offsetX - this.dragFrom.x) / e.target.clientWidth
|
this.area.x = (e.offsetX - this.dragFrom.x) / e.target.clientWidth
|
||||||
@@ -759,7 +925,9 @@ export default {
|
|||||||
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (this.editable) {
|
||||||
this.isDragged = true
|
this.isDragged = true
|
||||||
|
}
|
||||||
|
|
||||||
const rect = e.target.getBoundingClientRect()
|
const rect = e.target.getBoundingClientRect()
|
||||||
|
|
||||||
@@ -812,7 +980,9 @@ export default {
|
|||||||
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (this.editable) {
|
||||||
this.isDragged = true
|
this.isDragged = true
|
||||||
|
}
|
||||||
|
|
||||||
const rect = e.target.getBoundingClientRect()
|
const rect = e.target.getBoundingClientRect()
|
||||||
|
|
||||||
|
|||||||
@@ -331,6 +331,7 @@
|
|||||||
:default-fields="[...defaultRequiredFields, ...defaultFields]"
|
:default-fields="[...defaultRequiredFields, ...defaultFields]"
|
||||||
:allow-draw="!onlyDefinedFields || drawField"
|
:allow-draw="!onlyDefinedFields || drawField"
|
||||||
:with-signature-id="withSignatureId"
|
:with-signature-id="withSignatureId"
|
||||||
|
:with-prefillable="withPrefillable"
|
||||||
:data-document-uuid="document.uuid"
|
:data-document-uuid="document.uuid"
|
||||||
:default-submitters="defaultSubmitters"
|
:default-submitters="defaultSubmitters"
|
||||||
:drag-field-placeholder="fieldsDragFieldRef.value || dragField"
|
:drag-field-placeholder="fieldsDragFieldRef.value || dragField"
|
||||||
@@ -401,7 +402,10 @@
|
|||||||
:style="{ backgroundColor }"
|
:style="{ backgroundColor }"
|
||||||
>
|
>
|
||||||
<div class="bg-base-200 rounded-lg p-5 text-center space-y-4 draw-field-container">
|
<div class="bg-base-200 rounded-lg p-5 text-center space-y-4 draw-field-container">
|
||||||
<p>
|
<p v-if="(drawField?.type || drawFieldType) === 'strikethrough'">
|
||||||
|
{{ t('draw_strikethrough_the_document') }}
|
||||||
|
</p>
|
||||||
|
<p v-else>
|
||||||
{{ t('draw_field_on_the_document') }}
|
{{ t('draw_field_on_the_document') }}
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
@@ -412,7 +416,7 @@
|
|||||||
{{ t('cancel') }}
|
{{ t('cancel') }}
|
||||||
</button>
|
</button>
|
||||||
<a
|
<a
|
||||||
v-if="!drawField && !drawOption && !['stamp', 'signature', 'initials', 'heading'].includes(drawField?.type || drawFieldType)"
|
v-if="!drawField && !drawOption && !['stamp', 'signature', 'initials', 'heading', 'strikethrough'].includes(drawField?.type || drawFieldType)"
|
||||||
href="#"
|
href="#"
|
||||||
class="link block mt-3 text-sm"
|
class="link block mt-3 text-sm"
|
||||||
@click.prevent="[addField(drawFieldType), drawField = null, drawOption = null, withSelectedFieldType ? '' : drawFieldType = '', showDrawField = false]"
|
@click.prevent="[addField(drawFieldType), drawField = null, drawOption = null, withSelectedFieldType ? '' : drawFieldType = '', showDrawField = false]"
|
||||||
@@ -438,6 +442,7 @@
|
|||||||
:field-types="fieldTypes"
|
:field-types="fieldTypes"
|
||||||
:with-sticky-submitters="withStickySubmitters"
|
:with-sticky-submitters="withStickySubmitters"
|
||||||
:with-signature-id="withSignatureId"
|
:with-signature-id="withSignatureId"
|
||||||
|
:with-prefillable="withPrefillable"
|
||||||
:only-defined-fields="onlyDefinedFields"
|
:only-defined-fields="onlyDefinedFields"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
:show-tour-start-form="showTourStartForm"
|
:show-tour-start-form="showTourStartForm"
|
||||||
@@ -543,7 +548,6 @@ export default {
|
|||||||
withPayment: this.withPayment,
|
withPayment: this.withPayment,
|
||||||
isPaymentConnected: this.isPaymentConnected,
|
isPaymentConnected: this.isPaymentConnected,
|
||||||
withFormula: this.withFormula,
|
withFormula: this.withFormula,
|
||||||
withSignatureId: this.withSignatureId,
|
|
||||||
withConditions: this.withConditions,
|
withConditions: this.withConditions,
|
||||||
isInlineSize: this.isInlineSize,
|
isInlineSize: this.isInlineSize,
|
||||||
defaultDrawFieldType: this.defaultDrawFieldType,
|
defaultDrawFieldType: this.defaultDrawFieldType,
|
||||||
@@ -659,7 +663,7 @@ export default {
|
|||||||
acceptFileTypes: {
|
acceptFileTypes: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'image/*, application/pdf'
|
default: 'image/*, application/pdf, application/zip'
|
||||||
},
|
},
|
||||||
baseUrl: {
|
baseUrl: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -802,6 +806,13 @@ export default {
|
|||||||
language () {
|
language () {
|
||||||
return this.locale.split('-')[0].toLowerCase()
|
return this.locale.split('-')[0].toLowerCase()
|
||||||
},
|
},
|
||||||
|
withPrefillable () {
|
||||||
|
if (this.template.fields) {
|
||||||
|
return this.template.fields.some((f) => f.prefillable)
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
isInlineSize () {
|
isInlineSize () {
|
||||||
return CSS.supports('container-type: size')
|
return CSS.supports('container-type: size')
|
||||||
},
|
},
|
||||||
@@ -1057,7 +1068,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (['select', 'multiple', 'radio'].includes(type)) {
|
if (['select', 'multiple', 'radio'].includes(type)) {
|
||||||
field.options = [{ value: '', uuid: v4() }]
|
field.options = [{ value: '', uuid: v4() }, { value: '', uuid: v4() }]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'stamp') {
|
if (type === 'stamp') {
|
||||||
@@ -1076,6 +1087,11 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (field.type === 'strikethrough') {
|
||||||
|
field.readonly = true
|
||||||
|
field.default_value = true
|
||||||
|
}
|
||||||
|
|
||||||
if (type === 'signature' && [true, false].includes(this.withSignatureId)) {
|
if (type === 'signature' && [true, false].includes(this.withSignatureId)) {
|
||||||
field.preferences ||= {}
|
field.preferences ||= {}
|
||||||
field.preferences.with_signature_id = this.withSignatureId
|
field.preferences.with_signature_id = this.withSignatureId
|
||||||
@@ -1101,7 +1117,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (['select', 'multiple', 'radio'].includes(type)) {
|
if (['select', 'multiple', 'radio'].includes(type)) {
|
||||||
field.options = [{ value: '', uuid: v4() }]
|
field.options = [{ value: '', uuid: v4() }, { value: '', uuid: v4() }]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'stamp') {
|
if (type === 'stamp') {
|
||||||
@@ -1257,24 +1273,31 @@ export default {
|
|||||||
if (!field.areas.length) {
|
if (!field.areas.length) {
|
||||||
this.template.fields.splice(this.template.fields.indexOf(field), 1)
|
this.template.fields.splice(this.template.fields.indexOf(field), 1)
|
||||||
|
|
||||||
|
this.removeFieldConditions(field)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
|
removeFieldConditions (field) {
|
||||||
this.template.fields.forEach((f) => {
|
this.template.fields.forEach((f) => {
|
||||||
(f.conditions || []).forEach((c) => {
|
if (f.conditions) {
|
||||||
|
f.conditions.forEach((c) => {
|
||||||
if (c.field_uuid === field.uuid) {
|
if (c.field_uuid === field.uuid) {
|
||||||
f.conditions.splice(f.conditions.indexOf(c), 1)
|
f.conditions.splice(f.conditions.indexOf(c), 1)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.template.schema.forEach((item) => {
|
this.template.schema.forEach((item) => {
|
||||||
(item.conditions || []).forEach((c) => {
|
if (item.conditions) {
|
||||||
|
item.conditions.forEach((c) => {
|
||||||
if (c.field_uuid === field.uuid) {
|
if (c.field_uuid === field.uuid) {
|
||||||
item.conditions.splice(item.conditions.indexOf(c), 1)
|
item.conditions.splice(item.conditions.indexOf(c), 1)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
this.save()
|
|
||||||
},
|
},
|
||||||
pasteField () {
|
pasteField () {
|
||||||
const field = this.template.fields.find((f) => f.areas?.includes(this.copiedArea))
|
const field = this.template.fields.find((f) => f.areas?.includes(this.copiedArea))
|
||||||
@@ -1293,7 +1316,13 @@ export default {
|
|||||||
this.copiedArea.option_uuid ||= field.options[0].uuid
|
this.copiedArea.option_uuid ||= field.options[0].uuid
|
||||||
area.option_uuid = v4()
|
area.option_uuid = v4()
|
||||||
|
|
||||||
|
const lastOption = field.options[field.options.length - 1]
|
||||||
|
|
||||||
|
if (!field.areas.find((a) => lastOption.uuid === a.option_uuid)) {
|
||||||
|
area.option_uuid = lastOption.uuid
|
||||||
|
} else {
|
||||||
field.options.push({ uuid: area.option_uuid })
|
field.options.push({ uuid: area.option_uuid })
|
||||||
|
}
|
||||||
|
|
||||||
field.areas.push(area)
|
field.areas.push(area)
|
||||||
} else {
|
} else {
|
||||||
@@ -1336,6 +1365,9 @@ export default {
|
|||||||
} else if (type === 'initials') {
|
} else if (type === 'initials') {
|
||||||
area.w = pageMask.clientWidth / 10 / pageMask.clientWidth
|
area.w = pageMask.clientWidth / 10 / pageMask.clientWidth
|
||||||
area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth)
|
area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth)
|
||||||
|
} else if (type === 'strikethrough') {
|
||||||
|
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
|
||||||
|
area.h = (pageMask.clientWidth / 70 / pageMask.clientWidth)
|
||||||
} else {
|
} else {
|
||||||
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
|
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
|
||||||
area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth)
|
area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth)
|
||||||
@@ -1469,7 +1501,7 @@ export default {
|
|||||||
if (this.dragField?.options?.length) {
|
if (this.dragField?.options?.length) {
|
||||||
field.options = this.dragField.options.map(option => ({ value: option, uuid: v4() }))
|
field.options = this.dragField.options.map(option => ({ value: option, uuid: v4() }))
|
||||||
} else {
|
} else {
|
||||||
field.options = [{ value: '', uuid: v4() }]
|
field.options = [{ value: '', uuid: v4() }, { value: '', uuid: v4() }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1479,8 +1511,12 @@ export default {
|
|||||||
field.default_value = '{{date}}'
|
field.default_value = '{{date}}'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['stamp', 'heading'].includes(field.type)) {
|
if (['stamp', 'heading', 'strikethrough'].includes(field.type)) {
|
||||||
field.readonly = true
|
field.readonly = true
|
||||||
|
|
||||||
|
if (field.type === 'strikethrough') {
|
||||||
|
field.default_value = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.type === 'date') {
|
if (field.type === 'date') {
|
||||||
@@ -1568,6 +1604,11 @@ export default {
|
|||||||
w: area.maskW / 10 / area.maskW,
|
w: area.maskW / 10 / area.maskW,
|
||||||
h: area.maskW / 35 / area.maskW
|
h: area.maskW / 35 / area.maskW
|
||||||
}
|
}
|
||||||
|
} else if (fieldType === 'strikethrough') {
|
||||||
|
baseArea = {
|
||||||
|
w: area.maskW / 5 / area.maskW,
|
||||||
|
h: area.maskW / 70 / area.maskW
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
baseArea = {
|
baseArea = {
|
||||||
w: area.maskW / 5 / area.maskW,
|
w: area.maskW / 5 / area.maskW,
|
||||||
@@ -1701,8 +1742,15 @@ export default {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
this.template.fields =
|
this.template.fields = this.template.fields.reduce((acc, f) => {
|
||||||
this.template.fields.filter((f) => !removedFieldUuids.includes(f.uuid) || f.areas?.length)
|
if (removedFieldUuids.includes(f.uuid) && !f.areas?.length) {
|
||||||
|
this.removeFieldConditions(f)
|
||||||
|
} else {
|
||||||
|
acc.push(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
||||||
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||||
<span class="modal-title">
|
<span class="modal-title">
|
||||||
{{ t('condition') }} - {{ item.name || buildDefaultName(item, template.fields) }}
|
{{ t('condition') }} - {{ (defaultField ? (defaultField.title || item.title || item.name) : item.name) || buildDefaultName(item, template.fields) }}
|
||||||
</span>
|
</span>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
@@ -160,6 +160,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
defaultField: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
buildDefaultName: {
|
buildDefaultName: {
|
||||||
type: Function,
|
type: Function,
|
||||||
required: true
|
required: true
|
||||||
@@ -172,10 +177,13 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
excludeTypes () {
|
||||||
|
return ['heading', 'strikethrough']
|
||||||
|
},
|
||||||
fields () {
|
fields () {
|
||||||
if (this.item.submitter_uuid) {
|
if (this.item.submitter_uuid) {
|
||||||
return this.template.fields.reduce((acc, f) => {
|
return this.template.fields.reduce((acc, f) => {
|
||||||
if (f !== this.item && (!f.conditions?.length || !f.conditions.find((c) => c.field_uuid === this.item.uuid))) {
|
if (f !== this.item && !this.excludeTypes.includes(f.type) && (!f.conditions?.length || !f.conditions.find((c) => c.field_uuid === this.item.uuid))) {
|
||||||
acc.push(f)
|
acc.push(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export default {
|
|||||||
acceptFileTypes: {
|
acceptFileTypes: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'image/*, application/pdf'
|
default: 'image/*, application/pdf, application/zip'
|
||||||
},
|
},
|
||||||
withReplaceButton: {
|
withReplaceButton: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
||||||
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||||
<span class="modal-title">
|
<span class="modal-title">
|
||||||
{{ field.name || buildDefaultName(field, template.fields) }}
|
{{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field, template.fields) }}
|
||||||
</span>
|
</span>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
@@ -73,6 +73,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
defaultField: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
editable: {
|
editable: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
:areas="areasIndex[index]"
|
:areas="areasIndex[index]"
|
||||||
:allow-draw="allowDraw"
|
:allow-draw="allowDraw"
|
||||||
:with-signature-id="withSignatureId"
|
:with-signature-id="withSignatureId"
|
||||||
|
:with-prefillable="withPrefillable"
|
||||||
:is-drag="isDrag"
|
:is-drag="isDrag"
|
||||||
:with-field-placeholder="withFieldPlaceholder"
|
:with-field-placeholder="withFieldPlaceholder"
|
||||||
:default-fields="defaultFields"
|
:default-fields="defaultFields"
|
||||||
@@ -72,6 +73,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
withPrefillable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
drawFieldType: {
|
drawFieldType: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export default {
|
|||||||
acceptFileTypes: {
|
acceptFileTypes: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'image/*, application/pdf'
|
default: 'image/*, application/pdf, application/zip'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['success', 'error', 'loading'],
|
emits: ['success', 'error', 'loading'],
|
||||||
@@ -131,7 +131,7 @@ export default {
|
|||||||
message () {
|
message () {
|
||||||
if (this.isLoading) {
|
if (this.isLoading) {
|
||||||
return this.t('uploading')
|
return this.t('uploading')
|
||||||
} else if (this.acceptFileTypes === 'image/*, application/pdf') {
|
} else if (this.acceptFileTypes === 'image/*, application/pdf, application/zip') {
|
||||||
return this.title || this.t('add_pdf_documents_or_images')
|
return this.title || this.t('add_pdf_documents_or_images')
|
||||||
} else {
|
} else {
|
||||||
return this.title || this.t('add_documents_or_images')
|
return this.title || this.t('add_documents_or_images')
|
||||||
@@ -146,7 +146,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
upload: Upload.methods.upload,
|
upload: Upload.methods.upload,
|
||||||
onDropFiles (e) {
|
onDropFiles (e) {
|
||||||
if (this.acceptFileTypes !== 'image/*, application/pdf' || [...e.dataTransfer.files].every((f) => f.type.match(/(?:image\/)|(?:application\/pdf)/))) {
|
if (this.acceptFileTypes !== 'image/*, application/pdf, application/zip' || [...e.dataTransfer.files].every((f) => f.type.match(/(?:image\/)|(?:application\/pdf)|(?:application\/zip)/))) {
|
||||||
this.$refs.input.files = e.dataTransfer.files
|
this.$refs.input.files = e.dataTransfer.files
|
||||||
|
|
||||||
this.upload()
|
this.upload()
|
||||||
|
|||||||
@@ -14,7 +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 && field.type != 'heading'"
|
:editable="editable && !defaultField"
|
||||||
:button-width="20"
|
:button-width="20"
|
||||||
:menu-classes="'mt-1.5'"
|
:menu-classes="'mt-1.5'"
|
||||||
:menu-style="{ backgroundColor: dropdownBgColor }"
|
:menu-style="{ backgroundColor: dropdownBgColor }"
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
@click-formula="isShowFormulaModal = true"
|
@click-formula="isShowFormulaModal = true"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
v-else-if="field.type !== 'heading'"
|
v-else
|
||||||
class="dropdown dropdown-end field-settings-dropdown"
|
class="dropdown dropdown-end field-settings-dropdown"
|
||||||
@mouseenter="renderDropdown = true"
|
@mouseenter="renderDropdown = true"
|
||||||
@touchstart="renderDropdown = true"
|
@touchstart="renderDropdown = true"
|
||||||
@@ -126,6 +126,7 @@
|
|||||||
:default-field="defaultField"
|
:default-field="defaultField"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
:with-signature-id="withSignatureId"
|
:with-signature-id="withSignatureId"
|
||||||
|
:with-prefillable="withPrefillable"
|
||||||
:background-color="dropdownBgColor"
|
:background-color="dropdownBgColor"
|
||||||
@click-formula="isShowFormulaModal = true"
|
@click-formula="isShowFormulaModal = true"
|
||||||
@click-font="isShowFontModal = true"
|
@click-font="isShowFontModal = true"
|
||||||
@@ -150,7 +151,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="field.options && withOptions"
|
v-if="field.options && withOptions && (isExpandOptions || field.options.length < 5)"
|
||||||
ref="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"
|
draggable="true"
|
||||||
@@ -215,12 +216,30 @@
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-else-if="field.options && editable && !defaultField"
|
v-else-if="field.options && editable && !defaultField"
|
||||||
class="text-center text-sm w-full pb-1"
|
class="field-add-option text-center text-sm w-full pb-1"
|
||||||
@click="addOption"
|
@click="addOption"
|
||||||
>
|
>
|
||||||
+ {{ t('add_option') }}
|
+ {{ t('add_option') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="field.options && withOptions && !isExpandOptions && field.options.length > 4"
|
||||||
|
class="border-t border-base-300 mx-2 space-y-1.5"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="field-expand-options text-center text-sm w-full py-1 flex space-x-0.5 justify-center items-center"
|
||||||
|
@click="isExpandOptions = true"
|
||||||
|
>
|
||||||
|
<span class="lowercase">
|
||||||
|
{{ field.options.length }} {{ t('options') }}
|
||||||
|
</span>
|
||||||
|
<IconChevronDown
|
||||||
|
class="ml-2 mr-2 mt-0.5"
|
||||||
|
width="15"
|
||||||
|
height="15"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Teleport
|
<Teleport
|
||||||
v-if="isShowFormulaModal"
|
v-if="isShowFormulaModal"
|
||||||
@@ -229,6 +248,7 @@
|
|||||||
<FormulaModal
|
<FormulaModal
|
||||||
:field="field"
|
:field="field"
|
||||||
:editable="editable && !defaultField"
|
:editable="editable && !defaultField"
|
||||||
|
:default-field="defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
@close="isShowFormulaModal = false"
|
@close="isShowFormulaModal = false"
|
||||||
/>
|
/>
|
||||||
@@ -240,6 +260,7 @@
|
|||||||
<FontModal
|
<FontModal
|
||||||
:field="field"
|
:field="field"
|
||||||
:editable="editable && !defaultField"
|
:editable="editable && !defaultField"
|
||||||
|
:default-field="defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
@close="isShowFontModal = false"
|
@close="isShowFontModal = false"
|
||||||
/>
|
/>
|
||||||
@@ -250,6 +271,7 @@
|
|||||||
>
|
>
|
||||||
<ConditionsModal
|
<ConditionsModal
|
||||||
:item="field"
|
:item="field"
|
||||||
|
:default-field="defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
@close="isShowConditionsModal = false"
|
@close="isShowConditionsModal = false"
|
||||||
/>
|
/>
|
||||||
@@ -261,6 +283,7 @@
|
|||||||
<DescriptionModal
|
<DescriptionModal
|
||||||
:field="field"
|
:field="field"
|
||||||
:editable="editable && !defaultField"
|
:editable="editable && !defaultField"
|
||||||
|
:default-field="defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
@close="isShowDescriptionModal = false"
|
@close="isShowDescriptionModal = false"
|
||||||
/>
|
/>
|
||||||
@@ -277,7 +300,7 @@ import FormulaModal from './formula_modal'
|
|||||||
import FontModal from './font_modal'
|
import FontModal from './font_modal'
|
||||||
import ConditionsModal from './conditions_modal'
|
import ConditionsModal from './conditions_modal'
|
||||||
import DescriptionModal from './description_modal'
|
import DescriptionModal from './description_modal'
|
||||||
import { IconRouteAltLeft, IconMathFunction, IconNewSection, IconTrashX, IconSettings } from '@tabler/icons-vue'
|
import { IconRouteAltLeft, IconMathFunction, IconNewSection, IconTrashX, IconSettings, IconChevronDown } from '@tabler/icons-vue'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -287,6 +310,7 @@ export default {
|
|||||||
IconSettings,
|
IconSettings,
|
||||||
FieldSettings,
|
FieldSettings,
|
||||||
PaymentSettings,
|
PaymentSettings,
|
||||||
|
IconChevronDown,
|
||||||
IconNewSection,
|
IconNewSection,
|
||||||
FormulaModal,
|
FormulaModal,
|
||||||
FontModal,
|
FontModal,
|
||||||
@@ -308,6 +332,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
withPrefillable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
withOptions: {
|
withOptions: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -327,6 +356,7 @@ export default {
|
|||||||
emits: ['set-draw', 'remove', 'scroll-to'],
|
emits: ['set-draw', 'remove', 'scroll-to'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
isExpandOptions: false,
|
||||||
isNameFocus: false,
|
isNameFocus: false,
|
||||||
showPaymentModal: false,
|
showPaymentModal: false,
|
||||||
isShowFormulaModal: false,
|
isShowFormulaModal: false,
|
||||||
@@ -391,7 +421,7 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
const typeIndex = fields.filter((f) => f.type === field.type).indexOf(field)
|
const typeIndex = fields.filter((f) => f.type === field.type).indexOf(field)
|
||||||
|
|
||||||
if (field.type === 'heading') {
|
if (field.type === 'heading' || field.type === 'strikethrough') {
|
||||||
return `${this.fieldNames[field.type]} ${typeIndex + 1}`
|
return `${this.fieldNames[field.type]} ${typeIndex + 1}`
|
||||||
} else {
|
} else {
|
||||||
return `${this.fieldLabels[field.type]} ${typeIndex + 1}`
|
return `${this.fieldLabels[field.type]} ${typeIndex + 1}`
|
||||||
@@ -421,6 +451,8 @@ export default {
|
|||||||
this.$el.getRootNode().activeElement.blur()
|
this.$el.getRootNode().activeElement.blur()
|
||||||
},
|
},
|
||||||
addOption () {
|
addOption () {
|
||||||
|
this.isExpandOptions = true
|
||||||
|
|
||||||
this.field.options.push({ value: '', uuid: v4() })
|
this.field.options.push({ value: '', uuid: v4() })
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@@ -453,10 +485,15 @@ export default {
|
|||||||
this.field.options ||= [{ value: '', uuid: v4() }]
|
this.field.options ||= [{ value: '', uuid: v4() }]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['heading'].includes(this.field.type)) {
|
if (this.field.type === 'heading') {
|
||||||
this.field.readonly = true
|
this.field.readonly = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.field.type === 'strikethrough') {
|
||||||
|
this.field.readonly = true
|
||||||
|
this.field.default_value = true
|
||||||
|
}
|
||||||
|
|
||||||
(this.field.areas || []).forEach((area) => {
|
(this.field.areas || []).forEach((area) => {
|
||||||
if (this.field.type === 'cells') {
|
if (this.field.type === 'cells') {
|
||||||
area.cell_w = area.w * 2 / Math.floor(area.w / area.h)
|
area.cell_w = area.w * 2 / Math.floor(area.w / area.h)
|
||||||
|
|||||||
@@ -1,31 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
|
||||||
v-if="field.type === 'number'"
|
|
||||||
class="py-1.5 px-1 relative"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<select
|
|
||||||
:placeholder="t('format')"
|
|
||||||
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
|
|
||||||
@change="[field.preferences ||= {}, field.preferences.format = $event.target.value, save()]"
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
v-for="format in numberFormats"
|
|
||||||
:key="format"
|
|
||||||
:value="format"
|
|
||||||
:selected="format === field.preferences?.format || (format === 'none' && !field.preferences?.format)"
|
|
||||||
>
|
|
||||||
{{ formatNumber(123456789.567, format) }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<label
|
|
||||||
:style="{ backgroundColor }"
|
|
||||||
class="absolute -top-1 left-2.5 px-1 h-4"
|
|
||||||
style="font-size: 8px"
|
|
||||||
>
|
|
||||||
{{ t('format') }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
v-if="field.type === 'verification'"
|
v-if="field.type === 'verification'"
|
||||||
class="py-1.5 px-1 relative"
|
class="py-1.5 px-1 relative"
|
||||||
@@ -217,6 +190,77 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="field.type === 'number'"
|
||||||
|
class="py-1.5 px-1 relative flex space-x-1"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<div class="w-1/2 relative">
|
||||||
|
<input
|
||||||
|
:placeholder="t('min')"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
:value="field.validation?.min"
|
||||||
|
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
|
||||||
|
@input="[field.validation ||= {}, $event.target.value ? field.validation.min = $event.target.value : delete field.validation.min]"
|
||||||
|
@blur="save"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
v-if="field.validation?.min"
|
||||||
|
:style="{ backgroundColor }"
|
||||||
|
class="absolute -top-2.5 left-1.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>
|
||||||
|
{{ t('min') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="w-1/2 relative">
|
||||||
|
<input
|
||||||
|
:placeholder="t('max')"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
|
||||||
|
:value="field.validation?.max"
|
||||||
|
@input="[field.validation ||= {}, $event.target.value ? field.validation.max = $event.target.value : delete field.validation.max]"
|
||||||
|
@blur="save"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
v-if="field.validation?.max"
|
||||||
|
:style="{ backgroundColor }"
|
||||||
|
class="absolute -top-2.5 left-1.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>
|
||||||
|
{{ t('max') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="field.type === 'number'"
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
:placeholder="t('format')"
|
||||||
|
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
|
||||||
|
@change="[field.preferences ||= {}, field.preferences.format = $event.target.value, save()]"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="format in numberFormats"
|
||||||
|
:key="format"
|
||||||
|
:value="format"
|
||||||
|
:selected="format === field.preferences?.format || (format === 'none' && !field.preferences?.format)"
|
||||||
|
>
|
||||||
|
{{ formatNumber(123456789.567, format) }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<label
|
||||||
|
:style="{ backgroundColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>
|
||||||
|
{{ t('format') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation"
|
v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation"
|
||||||
class="py-1.5 px-1 relative"
|
class="py-1.5 px-1 relative"
|
||||||
@@ -304,7 +348,7 @@
|
|||||||
{{ t('any') }}
|
{{ t('any') }}
|
||||||
</option>
|
</option>
|
||||||
<option
|
<option
|
||||||
v-for="type in ['drawn', 'typed', 'drawn_or_typed', 'upload']"
|
v-for="type in ['drawn', 'typed', 'drawn_or_typed', 'drawn_or_upload', 'upload']"
|
||||||
:key="type"
|
:key="type"
|
||||||
:value="type"
|
:value="type"
|
||||||
:selected="field.preferences?.format === type"
|
:selected="field.preferences?.format === type"
|
||||||
@@ -336,7 +380,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="withRequired && field.type !== 'phone' && field.type !== 'stamp' && field.type !== 'verification'"
|
v-if="withRequired && field.type !== 'phone' && field.type !== 'stamp' && field.type !== 'verification' && field.type !== 'strikethrough' && field.type !== 'heading'"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<label class="cursor-pointer py-1.5">
|
<label class="cursor-pointer py-1.5">
|
||||||
@@ -393,7 +437,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="['text', 'number'].includes(field.type) && !defaultField"
|
v-if="['text', 'number'].includes(field.type)"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<label class="cursor-pointer py-1.5">
|
<label class="cursor-pointer py-1.5">
|
||||||
@@ -401,16 +445,32 @@
|
|||||||
v-model="field.readonly"
|
v-model="field.readonly"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="toggle toggle-xs"
|
class="toggle toggle-xs"
|
||||||
|
:disabled="!editable || (defaultField && [true, false].includes(defaultField.readonly))"
|
||||||
@update:model-value="save"
|
@update:model-value="save"
|
||||||
>
|
>
|
||||||
<span class="label-text">{{ t('read_only') }}</span>
|
<span class="label-text">{{ t('read_only') }}</span>
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
|
<li
|
||||||
|
v-if="withPrefillable && ['text', 'number', 'cells', 'date', 'checkbox', 'select', 'radio', 'phone'].includes(field['type'])"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<label class="cursor-pointer py-1.5">
|
||||||
|
<input
|
||||||
|
v-model="field.prefillable"
|
||||||
|
type="checkbox"
|
||||||
|
:disabled="!editable || (defaultField && [true, false].includes(defaultField.prefillable))"
|
||||||
|
class="toggle toggle-xs"
|
||||||
|
@update:model-value="save"
|
||||||
|
>
|
||||||
|
<span class="label-text">{{ t('prefillable') }}</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
<hr
|
<hr
|
||||||
v-if="field.type != 'stamp'"
|
v-if="field.type != 'stamp'"
|
||||||
class="pb-0.5 mt-0.5"
|
class="pb-0.5 mt-0.5"
|
||||||
>
|
>
|
||||||
<li v-if="['text', 'number', 'date', 'select'].includes(field.type)">
|
<li v-if="['text', 'number', 'date', 'select', 'heading'].includes(field.type)">
|
||||||
<label
|
<label
|
||||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||||
@click="$emit('click-font')"
|
@click="$emit('click-font')"
|
||||||
@@ -424,7 +484,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="field.type != 'stamp'"
|
v-if="field.type != 'stamp' && field.type != 'heading' && field.type != 'strikethrough'"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||||
@@ -439,7 +499,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="field.type != 'stamp'"
|
v-if="field.type != 'stamp' && field.type != 'heading'"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||||
@@ -555,6 +615,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
withPrefillable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
withRequired: {
|
withRequired: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading, IconId, IconCalendarCheck } from '@tabler/icons-vue'
|
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading, IconId, IconCalendarCheck, IconStrikethrough } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FiledTypeDropdown',
|
name: 'FiledTypeDropdown',
|
||||||
@@ -97,6 +97,7 @@ export default {
|
|||||||
fieldNames () {
|
fieldNames () {
|
||||||
return {
|
return {
|
||||||
heading: this.t('heading'),
|
heading: this.t('heading'),
|
||||||
|
strikethrough: this.t('strikeout'),
|
||||||
text: this.t('text'),
|
text: this.t('text'),
|
||||||
signature: this.t('signature'),
|
signature: this.t('signature'),
|
||||||
initials: this.t('initials'),
|
initials: this.t('initials'),
|
||||||
@@ -139,6 +140,7 @@ export default {
|
|||||||
fieldIcons () {
|
fieldIcons () {
|
||||||
return {
|
return {
|
||||||
heading: IconHeading,
|
heading: IconHeading,
|
||||||
|
strikethrough: IconStrikethrough,
|
||||||
text: IconTextSize,
|
text: IconTextSize,
|
||||||
signature: IconWritingSign,
|
signature: IconWritingSign,
|
||||||
initials: IconLetterCaseUpper,
|
initials: IconLetterCaseUpper,
|
||||||
@@ -158,6 +160,9 @@ export default {
|
|||||||
verification: IconId
|
verification: IconId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
skipTypes () {
|
||||||
|
return ['heading', 'datenow', 'strikethrough']
|
||||||
|
},
|
||||||
fieldIconsSorted () {
|
fieldIconsSorted () {
|
||||||
if (this.fieldTypes.length) {
|
if (this.fieldTypes.length) {
|
||||||
return this.fieldTypes.reduce((acc, type) => {
|
return this.fieldTypes.reduce((acc, type) => {
|
||||||
@@ -166,7 +171,7 @@ export default {
|
|||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
} else {
|
} else {
|
||||||
return Object.fromEntries(Object.entries(this.fieldIcons).filter(([key]) => key !== 'heading' && key !== 'datenow'))
|
return Object.fromEntries(Object.entries(this.fieldIcons).filter(([key]) => !this.skipTypes.includes(key)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
|
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
:with-signature-id="withSignatureId"
|
:with-signature-id="withSignatureId"
|
||||||
|
:with-prefillable="withPrefillable"
|
||||||
:default-field="defaultFieldsIndex[field.name]"
|
:default-field="defaultFieldsIndex[field.name]"
|
||||||
:draggable="editable"
|
:draggable="editable"
|
||||||
@dragstart="[fieldsDragFieldRef.value = field, removeDragOverlay($event), setDragPlaceholder($event)]"
|
@dragstart="[fieldsDragFieldRef.value = field, removeDragOverlay($event), setDragPlaceholder($event)]"
|
||||||
@@ -259,6 +260,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
withPrefillable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
template: {
|
template: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
@@ -345,6 +351,9 @@ export default {
|
|||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
},
|
},
|
||||||
|
skipTypes () {
|
||||||
|
return ['heading', 'datenow', 'strikethrough']
|
||||||
|
},
|
||||||
fieldIconsSorted () {
|
fieldIconsSorted () {
|
||||||
if (this.fieldTypes.length) {
|
if (this.fieldTypes.length) {
|
||||||
return this.fieldTypes.reduce((acc, type) => {
|
return this.fieldTypes.reduce((acc, type) => {
|
||||||
@@ -353,7 +362,7 @@ export default {
|
|||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
} else {
|
} else {
|
||||||
return Object.fromEntries(Object.entries(this.fieldIcons).filter(([key]) => key !== 'heading' && key !== 'datenow'))
|
return Object.fromEntries(Object.entries(this.fieldIcons).filter(([key]) => !this.skipTypes.includes(key)))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
submitterFields () {
|
submitterFields () {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
||||||
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||||
<span class="modal-title">
|
<span class="modal-title">
|
||||||
{{ t('font') }} - {{ field.name || buildDefaultName(field, template.fields) }}
|
{{ t('font') }} - {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field, template.fields) }}
|
||||||
</span>
|
</span>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
@@ -202,6 +202,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
defaultField: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
editable: {
|
editable: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
||||||
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||||
<span class="modal-title">
|
<span class="modal-title">
|
||||||
{{ t('formula') }} - {{ field.name || buildDefaultName(field, template.fields) }}
|
{{ t('formula') }} - {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field, template.fields) }}
|
||||||
</span>
|
</span>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
@@ -28,14 +28,21 @@
|
|||||||
class="link"
|
class="link"
|
||||||
>{{ t('available_in_pro') }}</a>
|
>{{ t('available_in_pro') }}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-inline mb-2 gap-2 space-y-1">
|
<div class="flex flex-wrap mb-2 gap-y-1 pt-1">
|
||||||
<button
|
<button
|
||||||
v-for="f in fields"
|
v-for="f in fields"
|
||||||
:key="f.uuid"
|
:key="f.uuid"
|
||||||
class="mr-1 btn btn-neutral btn-outline border-base-content/20 btn-sm normal-case font-normal bg-white !rounded-xl"
|
class="mr-1 flex btn btn-neutral btn-outline border-base-content/20 btn-sm normal-case font-normal bg-white !rounded-xl"
|
||||||
@click.prevent="insertTextUnderCursor(`{{${f.name || buildDefaultName(f, template.fields)}}}`)"
|
@click.prevent="insertTextUnderCursor(`{{${f.name || buildDefaultName(f, template.fields)}}}`)"
|
||||||
>
|
>
|
||||||
|
<IconMathFunction
|
||||||
|
v-if="f.preferences?.formula"
|
||||||
|
width="17"
|
||||||
|
height="17"
|
||||||
|
stroke-width="1.5"
|
||||||
|
/>
|
||||||
<IconCodePlus
|
<IconCodePlus
|
||||||
|
v-else
|
||||||
width="20"
|
width="20"
|
||||||
height="20"
|
height="20"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
@@ -57,7 +64,7 @@
|
|||||||
<div class="mb-3 mt-1">
|
<div class="mb-3 mt-1">
|
||||||
<div
|
<div
|
||||||
target="blank"
|
target="blank"
|
||||||
class="text-sm mb-2 inline space-x-2"
|
class="text-sm mb-2 inline space-x-2 font-mono"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="bg-base-200 px-2 rounded-xl"
|
class="bg-base-200 px-2 rounded-xl"
|
||||||
@@ -83,12 +90,6 @@
|
|||||||
>
|
>
|
||||||
/
|
/
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
class="bg-base-200 px-2 rounded-xl"
|
|
||||||
@click="insertTextUnderCursor(' % ')"
|
|
||||||
>
|
|
||||||
%
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
class="bg-base-200 px-2 rounded-xl"
|
class="bg-base-200 px-2 rounded-xl"
|
||||||
@click="insertTextUnderCursor('^')"
|
@click="insertTextUnderCursor('^')"
|
||||||
@@ -122,12 +123,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconCodePlus } from '@tabler/icons-vue'
|
import { IconCodePlus, IconMathFunction } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FormulaModal',
|
name: 'FormulaModal',
|
||||||
components: {
|
components: {
|
||||||
IconCodePlus
|
IconCodePlus,
|
||||||
|
IconMathFunction
|
||||||
},
|
},
|
||||||
inject: ['t', 'save', 'template', 'withFormula'],
|
inject: ['t', 'save', 'template', 'withFormula'],
|
||||||
props: {
|
props: {
|
||||||
@@ -135,6 +137,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
defaultField: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
editable: {
|
editable: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -154,7 +161,7 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
fields () {
|
fields () {
|
||||||
return this.template.fields.reduce((acc, f) => {
|
return this.template.fields.reduce((acc, f) => {
|
||||||
if (f !== this.field && ['number'].includes(f.type) && (!f.preferences?.formula || f.submitter_uuid !== this.field.submitter_uuid)) {
|
if (f !== this.field && ['number'].includes(f.type) && (!f.preferences?.formula || !f.preferences.formula.includes(this.field.uuid))) {
|
||||||
acc.push(f)
|
acc.push(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,9 +231,11 @@ export default {
|
|||||||
|
|
||||||
this.formula = newText
|
this.formula = newText
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
textarea.setSelectionRange(cursorPos + textToInsert.length, cursorPos + textToInsert.length)
|
textarea.setSelectionRange(cursorPos + textToInsert.length, cursorPos + textToInsert.length)
|
||||||
|
|
||||||
textarea.focus()
|
textarea.focus()
|
||||||
|
})
|
||||||
},
|
},
|
||||||
resizeTextarea () {
|
resizeTextarea () {
|
||||||
const textarea = this.$refs.textarea
|
const textarea = this.$refs.textarea
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
<div
|
<div
|
||||||
v-if="isDragging || isLoading"
|
v-if="isDragging || isLoading"
|
||||||
class="modal modal-open"
|
class="modal modal-open"
|
||||||
|
@dragover.prevent
|
||||||
|
@drop.prevent="$emit('error')"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-2 p-4 items-center bg-base-100 h-full max-h-[85vh] max-w-6xl rounded-2xl w-full">
|
<div class="flex flex-col gap-2 p-4 items-center bg-base-100 h-full max-h-[85vh] max-w-6xl rounded-2xl w-full">
|
||||||
<Dropzone
|
<Dropzone
|
||||||
@@ -76,7 +78,7 @@ export default {
|
|||||||
acceptFileTypes: {
|
acceptFileTypes: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'image/*, application/pdf'
|
default: 'image/*, application/pdf, application/zip'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['add', 'replace', 'replace-and-clone', 'error'],
|
emits: ['add', 'replace', 'replace-and-clone', 'error'],
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
const en = {
|
const en = {
|
||||||
|
payment_link: 'Payment link',
|
||||||
|
strikeout: 'Strikeout',
|
||||||
|
draw_strikethrough_the_document: 'Draw strikethrough the document',
|
||||||
|
quantity: 'Quantity',
|
||||||
|
prefillable: 'Prefillable',
|
||||||
signature_id: 'Signature ID',
|
signature_id: 'Signature ID',
|
||||||
error_message: 'Error message',
|
error_message: 'Error message',
|
||||||
length: 'Length',
|
length: 'Length',
|
||||||
@@ -59,6 +64,7 @@ const en = {
|
|||||||
any: 'Any',
|
any: 'Any',
|
||||||
drawn: 'Drawn',
|
drawn: 'Drawn',
|
||||||
drawn_or_typed: 'Drawn or Typed',
|
drawn_or_typed: 'Drawn or Typed',
|
||||||
|
drawn_or_upload: 'Drawn or Upload',
|
||||||
upload: 'Upload',
|
upload: 'Upload',
|
||||||
formula: 'Formula',
|
formula: 'Formula',
|
||||||
typed: 'Typed',
|
typed: 'Typed',
|
||||||
@@ -81,6 +87,7 @@ const en = {
|
|||||||
copy_to_all_pages: 'Copy to All Pages',
|
copy_to_all_pages: 'Copy to All Pages',
|
||||||
add_option: 'Add option',
|
add_option: 'Add option',
|
||||||
option: 'Option',
|
option: 'Option',
|
||||||
|
options: 'Options',
|
||||||
condition: 'Condition',
|
condition: 'Condition',
|
||||||
first_party: 'First Party',
|
first_party: 'First Party',
|
||||||
second_party: 'Second Party',
|
second_party: 'Second Party',
|
||||||
@@ -173,6 +180,11 @@ const en = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
|
payment_link: 'Enlace de pago',
|
||||||
|
strikeout: 'Tachar',
|
||||||
|
draw_strikethrough_the_document: 'Dibujar una línea de tachado en el documento',
|
||||||
|
quantity: 'Cantidad',
|
||||||
|
prefillable: 'Rellenable',
|
||||||
signature_id: 'ID de Firma',
|
signature_id: 'ID de Firma',
|
||||||
error_message: 'Mensaje de error',
|
error_message: 'Mensaje de error',
|
||||||
length: 'Longitud',
|
length: 'Longitud',
|
||||||
@@ -252,6 +264,7 @@ const es = {
|
|||||||
copy_to_all_pages: 'Copiar a todas las páginas',
|
copy_to_all_pages: 'Copiar a todas las páginas',
|
||||||
add_option: 'Agregar opción',
|
add_option: 'Agregar opción',
|
||||||
option: 'Opción',
|
option: 'Opción',
|
||||||
|
options: 'Opciones',
|
||||||
first_party: 'Primera Parte',
|
first_party: 'Primera Parte',
|
||||||
second_party: 'Segunda Parte',
|
second_party: 'Segunda Parte',
|
||||||
third_party: 'Tercera Parte',
|
third_party: 'Tercera Parte',
|
||||||
@@ -320,6 +333,7 @@ const es = {
|
|||||||
any: 'Cualquier',
|
any: 'Cualquier',
|
||||||
drawn: 'Dibujado',
|
drawn: 'Dibujado',
|
||||||
drawn_or_typed: 'Dibujado o Escrito',
|
drawn_or_typed: 'Dibujado o Escrito',
|
||||||
|
drawn_or_upload: 'Dibujado o Subido',
|
||||||
upload: 'Subir',
|
upload: 'Subir',
|
||||||
typed: 'Escrito',
|
typed: 'Escrito',
|
||||||
none: 'Ninguno',
|
none: 'Ninguno',
|
||||||
@@ -347,6 +361,11 @@ const es = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
|
payment_link: 'Link di pagamento',
|
||||||
|
strikeout: 'Barrato',
|
||||||
|
draw_strikethrough_the_document: 'Disegna una linea barrata sul documento',
|
||||||
|
quantity: 'Quantità',
|
||||||
|
prefillable: 'Precompilabile',
|
||||||
signature_id: 'ID firma',
|
signature_id: 'ID firma',
|
||||||
error_message: 'Messaggio di errore',
|
error_message: 'Messaggio di errore',
|
||||||
length: 'Lunghezza',
|
length: 'Lunghezza',
|
||||||
@@ -407,6 +426,7 @@ const it = {
|
|||||||
any: 'Qualsiasi',
|
any: 'Qualsiasi',
|
||||||
drawn: 'Disegnato',
|
drawn: 'Disegnato',
|
||||||
drawn_or_typed: 'Disegnato o Digitato',
|
drawn_or_typed: 'Disegnato o Digitato',
|
||||||
|
drawn_or_upload: 'Disegnato o Caricato',
|
||||||
upload: 'Caricare',
|
upload: 'Caricare',
|
||||||
formula: 'Formula',
|
formula: 'Formula',
|
||||||
typed: 'Digitato',
|
typed: 'Digitato',
|
||||||
@@ -429,6 +449,7 @@ const it = {
|
|||||||
copy_to_all_pages: 'Copia in tutte le pagine',
|
copy_to_all_pages: 'Copia in tutte le pagine',
|
||||||
add_option: 'Aggiungi opzione',
|
add_option: 'Aggiungi opzione',
|
||||||
option: 'Opzione',
|
option: 'Opzione',
|
||||||
|
options: 'Opzioni',
|
||||||
condition: 'Condizione',
|
condition: 'Condizione',
|
||||||
first_party: 'Prima parte',
|
first_party: 'Prima parte',
|
||||||
second_party: 'Seconda parte',
|
second_party: 'Seconda parte',
|
||||||
@@ -521,6 +542,11 @@ const it = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
|
payment_link: 'Link de pagamento',
|
||||||
|
strikeout: 'Tachado',
|
||||||
|
draw_strikethrough_the_document: 'Desenhe uma linha de tachado no documento',
|
||||||
|
quantity: 'Quantidade',
|
||||||
|
prefillable: 'Pré-preenchível',
|
||||||
signature_id: 'ID da Assinatura',
|
signature_id: 'ID da Assinatura',
|
||||||
error_message: 'Mensagem de erro',
|
error_message: 'Mensagem de erro',
|
||||||
length: 'Comprimento',
|
length: 'Comprimento',
|
||||||
@@ -600,6 +626,7 @@ const pt = {
|
|||||||
copy_to_all_pages: 'Copiar para todas as páginas',
|
copy_to_all_pages: 'Copiar para todas as páginas',
|
||||||
add_option: 'Adicionar opção',
|
add_option: 'Adicionar opção',
|
||||||
option: 'Opção',
|
option: 'Opção',
|
||||||
|
options: 'Opções',
|
||||||
first_party: 'Primeira Parte',
|
first_party: 'Primeira Parte',
|
||||||
second_party: 'Segunda Parte',
|
second_party: 'Segunda Parte',
|
||||||
third_party: 'Terceira Parte',
|
third_party: 'Terceira Parte',
|
||||||
@@ -668,6 +695,7 @@ const pt = {
|
|||||||
any: 'Qualquer',
|
any: 'Qualquer',
|
||||||
drawn: 'Desenhado',
|
drawn: 'Desenhado',
|
||||||
drawn_or_typed: 'Desenhado ou Digitado',
|
drawn_or_typed: 'Desenhado ou Digitado',
|
||||||
|
drawn_or_upload: 'Desenhado ou Enviado',
|
||||||
upload: 'Carregar',
|
upload: 'Carregar',
|
||||||
typed: 'Digitado',
|
typed: 'Digitado',
|
||||||
none: 'Nenhum',
|
none: 'Nenhum',
|
||||||
@@ -695,6 +723,11 @@ const pt = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
|
payment_link: 'Lien de paiement',
|
||||||
|
strikeout: 'Barrer',
|
||||||
|
draw_strikethrough_the_document: 'Tracer une ligne de suppression sur le document',
|
||||||
|
quantity: 'Quantité',
|
||||||
|
prefillable: 'Pré-remplissable',
|
||||||
signature_id: 'ID de signature',
|
signature_id: 'ID de signature',
|
||||||
error_message: 'Message d\'erreur',
|
error_message: 'Message d\'erreur',
|
||||||
length: 'Longueur',
|
length: 'Longueur',
|
||||||
@@ -773,6 +806,7 @@ const fr = {
|
|||||||
copy_to_all_pages: 'Copier sur toutes les pages',
|
copy_to_all_pages: 'Copier sur toutes les pages',
|
||||||
add_option: 'Ajouter une option',
|
add_option: 'Ajouter une option',
|
||||||
option: 'Option',
|
option: 'Option',
|
||||||
|
options: 'Options',
|
||||||
first_party: 'Première partie',
|
first_party: 'Première partie',
|
||||||
second_party: 'Deuxième partie',
|
second_party: 'Deuxième partie',
|
||||||
third_party: 'Troisième partie',
|
third_party: 'Troisième partie',
|
||||||
@@ -842,6 +876,7 @@ const fr = {
|
|||||||
any: 'Tout',
|
any: 'Tout',
|
||||||
drawn: 'Dessiné',
|
drawn: 'Dessiné',
|
||||||
drawn_or_typed: 'Dessiné ou Tapé',
|
drawn_or_typed: 'Dessiné ou Tapé',
|
||||||
|
drawn_or_upload: 'Dessiné ou Téléchargé',
|
||||||
upload: 'Téléverser',
|
upload: 'Téléverser',
|
||||||
typed: 'Tapé',
|
typed: 'Tapé',
|
||||||
none: 'Aucun',
|
none: 'Aucun',
|
||||||
@@ -869,6 +904,11 @@ const fr = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
|
payment_link: 'Zahlungslink',
|
||||||
|
strikeout: 'Streichung',
|
||||||
|
draw_strikethrough_the_document: 'Ziehe eine Streichung auf das Dokument',
|
||||||
|
quantity: 'Menge',
|
||||||
|
prefillable: 'Vorausfüllbar',
|
||||||
signature_id: 'Signatur-ID',
|
signature_id: 'Signatur-ID',
|
||||||
error_message: 'Fehlermeldung',
|
error_message: 'Fehlermeldung',
|
||||||
length: 'Länge',
|
length: 'Länge',
|
||||||
@@ -947,6 +987,7 @@ const de = {
|
|||||||
copy_to_all_pages: 'Auf alle Seiten kopieren',
|
copy_to_all_pages: 'Auf alle Seiten kopieren',
|
||||||
add_option: 'Option hinzufügen',
|
add_option: 'Option hinzufügen',
|
||||||
option: 'Option',
|
option: 'Option',
|
||||||
|
options: 'Options',
|
||||||
first_party: 'Erste Partei',
|
first_party: 'Erste Partei',
|
||||||
second_party: 'Zweite Partei',
|
second_party: 'Zweite Partei',
|
||||||
third_party: 'Dritte Partei',
|
third_party: 'Dritte Partei',
|
||||||
@@ -1016,6 +1057,7 @@ const de = {
|
|||||||
any: 'Jede',
|
any: 'Jede',
|
||||||
drawn: 'Gezeichnet',
|
drawn: 'Gezeichnet',
|
||||||
drawn_or_typed: 'Gezeichnet oder getippt',
|
drawn_or_typed: 'Gezeichnet oder getippt',
|
||||||
|
drawn_or_upload: 'Gezeichnet oder hochgeladen',
|
||||||
upload: 'Upload',
|
upload: 'Upload',
|
||||||
typed: 'Getippt',
|
typed: 'Getippt',
|
||||||
none: 'Keine',
|
none: 'Keine',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="relative select-none mb-4 before:border before:rounded before:top-0 before:bottom-0 before:left-0 before:right-0 before:absolute"
|
class="relative select-none mb-4 before:border before:rounded before:top-0 before:bottom-0 before:left-0 before:right-0 before:absolute"
|
||||||
:class="{ 'cursor-crosshair': allowDraw, 'touch-none': !!drawField }"
|
:class="{ 'cursor-crosshair': allowDraw && editable, 'touch-none': !!drawField }"
|
||||||
style="container-type: size"
|
style="container-type: size"
|
||||||
:style="{ aspectRatio: `${width} / ${height}`}"
|
:style="{ aspectRatio: `${width} / ${height}`}"
|
||||||
>
|
>
|
||||||
@@ -24,10 +24,13 @@
|
|||||||
:ref="setAreaRefs"
|
:ref="setAreaRefs"
|
||||||
:area="item.area"
|
:area="item.area"
|
||||||
:input-mode="inputMode"
|
:input-mode="inputMode"
|
||||||
|
:page-width="width"
|
||||||
|
:page-height="height"
|
||||||
:field="item.field"
|
:field="item.field"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
:with-field-placeholder="withFieldPlaceholder"
|
:with-field-placeholder="withFieldPlaceholder"
|
||||||
:with-signature-id="withSignatureId"
|
:with-signature-id="withSignatureId"
|
||||||
|
:with-prefillable="withPrefillable"
|
||||||
:default-field="defaultFieldsIndex[item.field.name]"
|
:default-field="defaultFieldsIndex[item.field.name]"
|
||||||
:default-submitters="defaultSubmitters"
|
:default-submitters="defaultSubmitters"
|
||||||
:max-page="totalPages - 1"
|
:max-page="totalPages - 1"
|
||||||
@@ -39,6 +42,8 @@
|
|||||||
<FieldArea
|
<FieldArea
|
||||||
v-if="newArea"
|
v-if="newArea"
|
||||||
:is-draw="true"
|
:is-draw="true"
|
||||||
|
:page-width="width"
|
||||||
|
:page-height="height"
|
||||||
:field="{ submitter_uuid: selectedSubmitter.uuid, type: drawField?.type || dragFieldPlaceholder?.type || defaultFieldType }"
|
:field="{ submitter_uuid: selectedSubmitter.uuid, type: drawField?.type || dragFieldPlaceholder?.type || defaultFieldType }"
|
||||||
:area="newArea"
|
:area="newArea"
|
||||||
/>
|
/>
|
||||||
@@ -84,6 +89,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
withPrefillable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
areas: {
|
areas: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<span
|
<span
|
||||||
class="dropdown dropdown-end field-settings-dropdown"
|
class="dropdown dropdown-end field-settings-dropdown"
|
||||||
:class="{ 'dropdown-open': ((!field.preferences?.price && !field.preferences?.formula) || !isConnected) && !isLoading }"
|
:class="{ 'dropdown-open': ((!field.preferences?.price && !field.preferences?.formula && !field.preferences?.price_id && !field.preferences?.payment_link_id) || !isConnected) && !isLoading }"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
@click="closeDropdown"
|
@click="closeDropdown"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="!('price_id' in field.preferences)"
|
v-if="!('price_id' in field.preferences) && !('payment_link_id' in field.preferences)"
|
||||||
class="py-1.5 px-1 relative"
|
class="py-1.5 px-1 relative"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
@@ -52,10 +52,9 @@
|
|||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
v-if="field.preferences.formula"
|
v-if="'payment_link_id' in field.preferences"
|
||||||
type="number"
|
v-model="field.preferences.payment_link_id"
|
||||||
:placeholder="t('price')"
|
placeholder="plink_XXXXX"
|
||||||
disabled="true"
|
|
||||||
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
|
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
|
||||||
@blur="save"
|
@blur="save"
|
||||||
>
|
>
|
||||||
@@ -66,6 +65,14 @@
|
|||||||
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
|
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
|
||||||
@blur="save"
|
@blur="save"
|
||||||
>
|
>
|
||||||
|
<input
|
||||||
|
v-else-if="field.preferences.formula"
|
||||||
|
type="number"
|
||||||
|
:placeholder="t('price')"
|
||||||
|
disabled="true"
|
||||||
|
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
|
||||||
|
@blur="save"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
v-else
|
v-else
|
||||||
v-model="field.preferences.price"
|
v-model="field.preferences.price"
|
||||||
@@ -75,29 +82,41 @@
|
|||||||
@blur="save"
|
@blur="save"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
v-if="field.preferences.price && !field.preferences.formula"
|
v-if="(field.preferences.price || field.preferences.price_id || field.preferences.payment_link_id) && (!field.preferences.formula || ('price_id' in field.preferences) || ('payment_link_id' in field.preferences))"
|
||||||
:style="{ backgroundColor: backgroundColor }"
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
class="absolute -top-1 left-2.5 px-1 h-4"
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
style="font-size: 8px"
|
style="font-size: 8px"
|
||||||
>
|
>
|
||||||
{{ t('price') }}
|
{{ 'payment_link_id' in field.preferences ? t('payment_link') : t('price') }}
|
||||||
</label>
|
</label>
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="hover:underline"
|
class="hover:underline"
|
||||||
style="font-size: 11px"
|
style="font-size: 11px"
|
||||||
:class="{'underline': !('price_id' in field.preferences)}"
|
:class="{'underline': !('payment_link_id' in field.preferences)}"
|
||||||
@click="delete field.preferences.price_id"
|
@click="[delete field.preferences.price_id, delete field.preferences.payment_link_id]"
|
||||||
>{{ t('one_off') }}</a>
|
>{{ t('one_off') }}</a>
|
||||||
<span class="h-2.5 border-l border-base-content mx-1" />
|
<span class="h-2.5 border-l border-base-content mx-1" />
|
||||||
|
<template
|
||||||
|
v-if="field.preferences.price_id"
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="hover:underline"
|
class="hover:underline"
|
||||||
style="font-size: 11px"
|
style="font-size: 11px"
|
||||||
:class="{'underline': ('price_id' in field.preferences)}"
|
:class="{'underline': ('price_id' in field.preferences)}"
|
||||||
@click="field.preferences.price_id ??= ''"
|
@click="field.preferences.payment_link_id ??= ''"
|
||||||
>{{ t('recurrent') }}</a>
|
>{{ t('recurrent') }}</a>
|
||||||
|
<span class="h-2.5 border-l border-base-content mx-1" />
|
||||||
|
</template>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="hover:underline"
|
||||||
|
style="font-size: 11px"
|
||||||
|
:class="{'underline': ('payment_link_id' in field.preferences)}"
|
||||||
|
@click="[delete field.preferences.price_id, field.preferences.payment_link_id ??= '']"
|
||||||
|
>{{ t('payment_link') }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -184,7 +203,6 @@
|
|||||||
>{{ t('learn_more') }}</a>
|
>{{ t('learn_more') }}</a>
|
||||||
</div>
|
</div>
|
||||||
<li
|
<li
|
||||||
v-if="!('price_id' in field.preferences)"
|
|
||||||
class="mb-1"
|
class="mb-1"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
@@ -195,7 +213,7 @@
|
|||||||
width="18"
|
width="18"
|
||||||
/>
|
/>
|
||||||
<span class="text-sm">
|
<span class="text-sm">
|
||||||
{{ t('formula') }}
|
{{ 'payment_link_id' in field.preferences ? t('quantity') : t('formula') }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ export default {
|
|||||||
acceptFileTypes: {
|
acceptFileTypes: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'image/*, application/pdf'
|
default: 'image/*, application/pdf, application/zip'
|
||||||
},
|
},
|
||||||
withReplaceButton: {
|
withReplaceButton: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export default {
|
|||||||
acceptFileTypes: {
|
acceptFileTypes: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'image/*, application/pdf'
|
default: 'image/*, application/pdf, application/zip'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['success'],
|
emits: ['success'],
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export default {
|
|||||||
acceptFileTypes: {
|
acceptFileTypes: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'image/*, application/pdf'
|
default: 'image/*, application/pdf, application/zip'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['success', 'error'],
|
emits: ['success', 'error'],
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ class ProcessSubmitterCompletionJob
|
|||||||
|
|
||||||
if is_all_completed && submitter.completed_at == submitter.submission.submitters.maximum(:completed_at)
|
if is_all_completed && submitter.completed_at == submitter.submission.submitters.maximum(:completed_at)
|
||||||
if submitter.submission.account.account_configs.exists?(key: AccountConfig::COMBINE_PDF_RESULT_KEY, value: true)
|
if submitter.submission.account.account_configs.exists?(key: AccountConfig::COMBINE_PDF_RESULT_KEY, value: true)
|
||||||
Submissions::GenerateCombinedAttachment.call(submitter)
|
Submissions::EnsureCombinedGenerated.call(submitter)
|
||||||
end
|
end
|
||||||
|
|
||||||
Submissions::GenerateAuditTrail.call(submitter.submission)
|
Submissions::EnsureAuditGenerated.call(submitter.submission)
|
||||||
|
|
||||||
enqueue_completed_emails(submitter)
|
enqueue_completed_emails(submitter)
|
||||||
end
|
end
|
||||||
@@ -38,12 +38,19 @@ class ProcessSubmitterCompletionJob
|
|||||||
|
|
||||||
submission = submitter.submission
|
submission = submitter.submission
|
||||||
|
|
||||||
|
complete_verification_events, sms_events =
|
||||||
|
submitter.submission_events.where(event_type: %i[send_sms send_2fa_sms complete_verification])
|
||||||
|
.partition { |e| e.event_type == 'complete_verification' }
|
||||||
|
|
||||||
|
complete_verification_event = complete_verification_events.first
|
||||||
|
|
||||||
completed_submitter.assign_attributes(
|
completed_submitter.assign_attributes(
|
||||||
submission_id: submitter.submission_id,
|
submission_id: submitter.submission_id,
|
||||||
account_id: submission.account_id,
|
account_id: submission.account_id,
|
||||||
template_id: submission.template_id,
|
template_id: submission.template_id,
|
||||||
source: submission.source,
|
source: submission.source,
|
||||||
sms_count: submitter.submission_events.where(event_type: %w[send_sms send_2fa_sms]).count,
|
sms_count: sms_events.sum { |e| e.data['segments'] || 1 },
|
||||||
|
verification_method: complete_verification_event&.data&.dig('method'),
|
||||||
completed_at: submitter.completed_at
|
completed_at: submitter.completed_at
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -135,19 +142,33 @@ class ProcessSubmitterCompletionJob
|
|||||||
end
|
end
|
||||||
|
|
||||||
def enqueue_next_submitter_request_notification(submitter)
|
def enqueue_next_submitter_request_notification(submitter)
|
||||||
next_submitter_item =
|
submission = submitter.submission
|
||||||
submitter.submission.template_submitters.find do |e|
|
submitters_index = submission.submitters.index_by(&:uuid)
|
||||||
sub = submitter.submission.submitters.find { |s| s.uuid == e['uuid'] }
|
|
||||||
|
next_submitter_items =
|
||||||
|
if submission.template_submitters.any? { |s| s['order'] }
|
||||||
|
submitter_groups =
|
||||||
|
submission.template_submitters.group_by.with_index { |s, index| s['order'] || index }
|
||||||
|
|
||||||
|
current_group_index = submitter_groups.find { |_, group| group.any? { |s| s['uuid'] == submitter.uuid } }&.first
|
||||||
|
|
||||||
|
if submitter_groups[current_group_index + 1] &&
|
||||||
|
submitters_index.values_at(*submitter_groups[current_group_index].pluck('uuid'))
|
||||||
|
.compact.all?(&:completed_at?)
|
||||||
|
submitter_groups[current_group_index + 1]
|
||||||
|
end
|
||||||
|
else
|
||||||
|
submission.template_submitters.find do |e|
|
||||||
|
sub = submitters_index[e['uuid']]
|
||||||
|
|
||||||
next unless sub
|
next unless sub
|
||||||
|
|
||||||
sub.completed_at.blank? && sub.sent_at.blank?
|
sub.completed_at.blank? && sub.sent_at.blank?
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return unless next_submitter_item
|
next_submitters = submitters_index.values_at(*Array.wrap(next_submitter_items).pluck('uuid')).compact
|
||||||
|
|
||||||
next_submitter = submitter.submission.submitters.find { |s| s.uuid == next_submitter_item['uuid'] }
|
Submitters.send_signature_requests(next_submitters)
|
||||||
|
|
||||||
Submitters.send_signature_requests([next_submitter])
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -6,6 +6,7 @@ class SendSubmitterInvitationEmailJob
|
|||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submitter = Submitter.find(params['submitter_id'])
|
submitter = Submitter.find(params['submitter_id'])
|
||||||
|
|
||||||
|
return if submitter.completed_at?
|
||||||
return if submitter.submission.source == 'invite' && !Accounts.can_send_emails?(submitter.account, on_events: true)
|
return if submitter.submission.source == 'invite' && !Accounts.can_send_emails?(submitter.account, on_events: true)
|
||||||
|
|
||||||
unless Accounts.can_send_invitation_emails?(submitter.account)
|
unless Accounts.can_send_invitation_emails?(submitter.account)
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
@submitter.template&.preferences&.dig('request_email_subject').presence
|
@submitter.template&.preferences&.dig('request_email_subject').presence
|
||||||
|
|
||||||
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY)
|
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY)
|
||||||
|
@body ||= fetch_config_email_body(@email_config, @submitter)
|
||||||
|
|
||||||
assign_message_metadata('submitter_invitation', @submitter)
|
assign_message_metadata('submitter_invitation', @submitter)
|
||||||
|
|
||||||
@@ -71,7 +72,7 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
@subject ||= @email_config.value['subject'] if @email_config
|
@subject ||= @email_config.value['subject'] if @email_config
|
||||||
|
|
||||||
@body = template_preferences['completed_notification_email_body'].presence
|
@body = template_preferences['completed_notification_email_body'].presence
|
||||||
@body ||= @email_config.value['body'] if @email_config
|
@body ||= fetch_config_email_body(@email_config, @submitter)
|
||||||
|
|
||||||
assign_message_metadata('submitter_completed', @submitter)
|
assign_message_metadata('submitter_completed', @submitter)
|
||||||
|
|
||||||
@@ -127,7 +128,7 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
@subject ||= @email_config.value['subject'] if @email_config
|
@subject ||= @email_config.value['subject'] if @email_config
|
||||||
|
|
||||||
@body = template_preferences['documents_copy_email_body'].presence
|
@body = template_preferences['documents_copy_email_body'].presence
|
||||||
@body ||= @email_config.value['body'] if @email_config
|
@body ||= fetch_config_email_body(@email_config, @submitter)
|
||||||
|
|
||||||
assign_message_metadata('submitter_documents_copy', @submitter)
|
assign_message_metadata('submitter_documents_copy', @submitter)
|
||||||
reply_to = build_submitter_reply_to(submitter, email_config: @email_config, documents_copy_email: true)
|
reply_to = build_submitter_reply_to(submitter, email_config: @email_config, documents_copy_email: true)
|
||||||
@@ -246,4 +247,8 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
user.friendly_name
|
user.friendly_name
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def fetch_config_email_body(email_config, _submitter = nil)
|
||||||
|
email_config ? email_config.value['body'].presence : nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
Loaded 100 of 245 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user