mirror of
https://github.com/docusealco/docuseal.git
synced 2026-08-07 23:32:07 +00:00
Compare commits
206 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 22afd01fd2 | |||
| fd9eb09dd6 | |||
| c53ed7f984 | |||
| 1d87e210be | |||
| 5fec952b8c | |||
| c4fbcc103a | |||
| 9333a1ba45 | |||
| de5cd92dba | |||
| 83e32bec9f | |||
| 4603efc04e | |||
| 6c73c82a27 | |||
| 8e71d9f9e9 | |||
| 80f60a5d88 | |||
| 2c33ec382d | |||
| 884865cffc | |||
| 9537fe8c91 | |||
| c8fe36a2b6 | |||
| 7ec3282b56 | |||
| ab8c41a520 | |||
| 4c1c70ff04 | |||
| d7a6e80bb1 | |||
| f87ef670d1 | |||
| daba2505bc | |||
| 71528eda45 | |||
| 269e2beeaa | |||
| 72dd2c6bc5 | |||
| 9758f7a15f | |||
| 2521eb5a4a | |||
| bf6afc5e61 | |||
| 291d869843 | |||
| cdfecb2a8f | |||
| c0df8e0ec3 | |||
| a21f7d1653 | |||
| 79e2514416 | |||
| 80a7385183 | |||
| 1c8a7b6a7c | |||
| b46ced2b6f | |||
| 5ae479644e | |||
| cb66eb1293 | |||
| b4d5a2cad4 | |||
| 32691b0c0d | |||
| 9163f744a3 | |||
| 23ecce51a3 | |||
| 747ffa8cf1 | |||
| 4e230899ed | |||
| aff9f753c7 | |||
| 1127fac500 | |||
| 7707e1cd2b | |||
| 30274f7f1a | |||
| a5da6cc781 | |||
| 201e4010c5 | |||
| b9bbdbffae | |||
| 9e8e1996e8 | |||
| b99235397a | |||
| c0fd06daf3 | |||
| 90536294df | |||
| f8e9787675 | |||
| bf9b785946 | |||
| dab3a6f031 | |||
| 88466193c5 | |||
| c2f7601dad | |||
| 189f7b2d7b | |||
| 5d87160ee9 | |||
| 0cdcdb8898 | |||
| c20619b5c8 | |||
| 93f84abce4 | |||
| 3c6a32caba | |||
| 72c08e5fdb | |||
| 022d80b5ce | |||
| f48da51802 | |||
| 709d04b33f | |||
| 3a251dd063 | |||
| 9cc92b59b3 | |||
| e0469b10a7 | |||
| 39eb67b162 | |||
| 8d9bea3b0f | |||
| 7015d8dde8 | |||
| 6b54072cb5 | |||
| b898708306 | |||
| 1b1cf36839 | |||
| 18b89edc19 | |||
| 70c7ef4247 | |||
| a8196709be | |||
| a9d249ccf0 | |||
| cd83d918e5 | |||
| b64a84a362 | |||
| f4426a8ee0 | |||
| df8c44d2ce | |||
| 65ce3d4822 | |||
| e5b1d94579 | |||
| 0b4754ca24 | |||
| b3dc8d55be | |||
| e5ab7667b9 | |||
| 2a4e6435f1 | |||
| 804431c44a | |||
| f3b26bf7a0 | |||
| aa27d90017 | |||
| 0fa8cf2dd0 | |||
| 634b5c37ba | |||
| 6e16f81fc6 | |||
| ffc4024f70 | |||
| a6981185b2 | |||
| 0361fabb66 | |||
| 04e7f101be | |||
| 86d680cfdf | |||
| 7bc2152057 | |||
| 74ed9a5040 | |||
| 30115fc0dc | |||
| 8e8e5e0e29 | |||
| b58ce1b571 | |||
| df5c4e4210 | |||
| ba2e0999f1 | |||
| a1f5dd08c7 | |||
| c1123fef63 | |||
| 4dc6149530 | |||
| 1245ff2cce | |||
| 284204fd78 | |||
| f7be74eb73 | |||
| aeea619059 | |||
| ea2b7f20e7 | |||
| f40f1d25de | |||
| c04bb2d7cf | |||
| 4c1ccd65bf | |||
| 54e064b0fc | |||
| 9a8b72883c | |||
| 5d75ee2e47 | |||
| 7357fa4871 | |||
| f6850b5427 | |||
| 1e9a181e60 | |||
| 7224a1ccec | |||
| 9b935a8180 | |||
| c4a693846e | |||
| 02c7cdcd97 | |||
| 36bee92e8e | |||
| 6542804f44 | |||
| d805fd614c | |||
| 21299dc65f | |||
| 0df0946c2a | |||
| 1bb38d50a1 | |||
| 242987cb41 | |||
| 5456abde54 | |||
| 4a07427440 | |||
| cf60dae2ff | |||
| 659997c6c1 | |||
| 389a47a152 | |||
| b6b81ca487 | |||
| 719c1786f1 | |||
| 4685bac4b3 | |||
| a05275cc08 | |||
| 1abf1b1658 | |||
| 8893dc5698 | |||
| f8467b2da1 | |||
| f39b8b000e | |||
| 961036fa31 | |||
| 8b9056894e | |||
| 7f979e9396 | |||
| 8db1192d0e | |||
| 3bd515951a | |||
| 2bd5b9676b | |||
| 993698ec01 | |||
| fcdc44ddbe | |||
| 0cb6b0ae12 | |||
| 0388927c5b | |||
| 2c8f5d2129 | |||
| 2bffbf83c6 | |||
| ec01529014 | |||
| ea1abf5662 | |||
| fcd5263af1 | |||
| 1fa8616dbc | |||
| f627277b45 | |||
| 809c3ea270 | |||
| 9caa9d79d4 | |||
| a8f1c1c323 | |||
| 59fbea5c5f | |||
| d3273d879a | |||
| ea227e82c9 | |||
| 676b330aa3 | |||
| 8d3295e125 | |||
| 8cb74e0bcc | |||
| 26749a4dbc | |||
| 14915d37b4 | |||
| 16d6d58efb | |||
| b2b97a313a | |||
| 2974e6a64f | |||
| dae34d0aa5 | |||
| e9d0671184 | |||
| 897e33bf6a | |||
| f37e93a409 | |||
| 561ac63c93 | |||
| 149e4be07a | |||
| eb98dc9e06 | |||
| ce7d965358 | |||
| bb73024859 | |||
| 631a442e2a | |||
| 20a082ae8e | |||
| 154d706be1 | |||
| aaae257c75 | |||
| ec944a2283 | |||
| 69d63ecffe | |||
| 06a1e2992d | |||
| ec9293bac0 | |||
| 8bf727c43c | |||
| 708b8afdf5 | |||
| 6f7b9b1ebf | |||
| 4f48c4fd9f | |||
| 9dac90e6cc |
@@ -1,13 +1,26 @@
|
||||
name: Build Docker Images
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*.*.*"
|
||||
workflow_dispatch:
|
||||
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:
|
||||
build:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
runs-on: ${{ inputs.os }}
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
@@ -16,23 +29,23 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
-
|
||||
name: Docker meta
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
docuseal/docuseal
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
images: docuseal/docuseal
|
||||
tags: latest,${{ inputs.version }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
image: ${{ inputs.image }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Create .version file
|
||||
run: echo ${{ github.ref_name }} > .version
|
||||
run: echo ${{ inputs.version }} > .version
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
|
||||
@@ -37,3 +37,4 @@ yarn-debug.log*
|
||||
/docuseal
|
||||
/ee
|
||||
dump.rdb
|
||||
*.onnx
|
||||
|
||||
@@ -42,6 +42,15 @@ Metrics/CyclomaticComplexity:
|
||||
Metrics/PerceivedComplexity:
|
||||
Max: 15
|
||||
|
||||
Style/MultipleComparison:
|
||||
Enabled: false
|
||||
|
||||
Style/NumericPredicate:
|
||||
Enabled: false
|
||||
|
||||
Naming/PredicateMethod:
|
||||
Enabled: false
|
||||
|
||||
Layout/LineLength:
|
||||
AllowedPatterns: ['\A\s*#']
|
||||
|
||||
@@ -68,6 +77,9 @@ RSpec/ExampleLength:
|
||||
RSpec/MultipleMemoizedHelpers:
|
||||
Max: 15
|
||||
|
||||
RSpec/AnyInstance:
|
||||
Enabled: false
|
||||
|
||||
Metrics/BlockNesting:
|
||||
Max: 5
|
||||
|
||||
|
||||
+6
-2
@@ -9,6 +9,7 @@ RUN apk --no-cache add fontforge wget && \
|
||||
wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && \
|
||||
wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && \
|
||||
wget https://github.com/impallari/DancingScript/raw/master/OFL.txt && \
|
||||
wget -O /model.onnx "https://github.com/docusealco/fields-detection/releases/download/1.0.0/model_704_int8.onnx" && \
|
||||
wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" && \
|
||||
mkdir -p /pdfium-linux && \
|
||||
tar -xzf pdfium-linux.tgz -C /pdfium-linux
|
||||
@@ -50,7 +51,7 @@ ENV OPENSSL_CONF=/app/openssl_legacy.cnf
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev@edge redis libheif@edge vips-heif@edge gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
||||
RUN apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev yaml-dev redis libheif vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
||||
|
||||
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
|
||||
\n\
|
||||
@@ -66,7 +67,9 @@ activate = 1' >> /app/openssl_legacy.cnf
|
||||
|
||||
COPY ./Gemfile ./Gemfile.lock ./
|
||||
|
||||
RUN apk add --no-cache build-base && bundle install && apk del --no-cache build-base && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf
|
||||
RUN apk add --no-cache build-base && bundle install && apk del --no-cache build-base && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf && ln -sf /usr/lib/libonnxruntime.so.1 $(ruby -e "print Dir[Gem::Specification.find_by_name('onnxruntime').gem_dir + '/vendor/*.so'].first")
|
||||
|
||||
RUN echo 'https://dl-cdn.alpinelinux.org/alpine/edge/main' >> /etc/apk/repositories && echo 'https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache onnxruntime
|
||||
|
||||
COPY ./bin ./bin
|
||||
COPY ./app ./app
|
||||
@@ -83,6 +86,7 @@ COPY --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.
|
||||
COPY --from=download /fonts/FreeSans.ttf /usr/share/fonts/freefont
|
||||
COPY --from=download /pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so
|
||||
COPY --from=download /pdfium-linux/licenses/pdfium.txt /usr/lib/libpdfium-LICENSE.txt
|
||||
COPY --from=download /model.onnx /app/tmp/model.onnx
|
||||
COPY --from=webpack /app/public/packs ./public/packs
|
||||
|
||||
RUN ln -s /fonts /app/public/fonts
|
||||
|
||||
@@ -11,6 +11,7 @@ gem 'azure-storage-blob', require: false
|
||||
gem 'bootsnap', require: false
|
||||
gem 'cancancan'
|
||||
gem 'csv'
|
||||
gem 'csv-safe'
|
||||
gem 'devise'
|
||||
gem 'devise-two-factor'
|
||||
gem 'dotenv', require: false
|
||||
@@ -23,7 +24,9 @@ gem 'image_processing'
|
||||
gem 'jwt'
|
||||
gem 'lograge'
|
||||
gem 'mysql2', require: false
|
||||
gem 'numo-narray'
|
||||
gem 'oj'
|
||||
gem 'onnxruntime'
|
||||
gem 'pagy'
|
||||
gem 'pg', require: false
|
||||
gem 'premailer-rails'
|
||||
|
||||
+132
-104
@@ -1,29 +1,29 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (8.0.1)
|
||||
actionpack (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
actioncable (8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
zeitwerk (~> 2.6)
|
||||
actionmailbox (8.0.1)
|
||||
actionpack (= 8.0.1)
|
||||
activejob (= 8.0.1)
|
||||
activerecord (= 8.0.1)
|
||||
activestorage (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
actionmailbox (8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
activejob (= 8.0.2.1)
|
||||
activerecord (= 8.0.2.1)
|
||||
activestorage (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
mail (>= 2.8.0)
|
||||
actionmailer (8.0.1)
|
||||
actionpack (= 8.0.1)
|
||||
actionview (= 8.0.1)
|
||||
activejob (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
actionmailer (8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
actionview (= 8.0.2.1)
|
||||
activejob (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
mail (>= 2.8.0)
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (8.0.1)
|
||||
actionview (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
actionpack (8.0.2.1)
|
||||
actionview (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
nokogiri (>= 1.8.5)
|
||||
rack (>= 2.2.4)
|
||||
rack-session (>= 1.0.1)
|
||||
@@ -31,35 +31,35 @@ GEM
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
useragent (~> 0.16)
|
||||
actiontext (8.0.1)
|
||||
actionpack (= 8.0.1)
|
||||
activerecord (= 8.0.1)
|
||||
activestorage (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
actiontext (8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
activerecord (= 8.0.2.1)
|
||||
activestorage (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
actionview (8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
activejob (8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
activejob (8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
activerecord (8.0.1)
|
||||
activemodel (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
activemodel (8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
activerecord (8.0.2.1)
|
||||
activemodel (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
timeout (>= 0.4.0)
|
||||
activestorage (8.0.1)
|
||||
actionpack (= 8.0.1)
|
||||
activejob (= 8.0.1)
|
||||
activerecord (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
activestorage (8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
activejob (= 8.0.2.1)
|
||||
activerecord (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
marcel (~> 1.0)
|
||||
activesupport (8.0.1)
|
||||
activesupport (8.0.2.1)
|
||||
base64
|
||||
benchmark (>= 0.3)
|
||||
bigdecimal
|
||||
@@ -76,7 +76,7 @@ GEM
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
annotaterb (4.14.0)
|
||||
arabic-letter-connector (0.1.1)
|
||||
ast (2.4.2)
|
||||
ast (2.4.3)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.1027.0)
|
||||
aws-sdk-core (3.214.0)
|
||||
@@ -104,9 +104,9 @@ GEM
|
||||
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
|
||||
net-http-persistent (~> 4.0)
|
||||
nokogiri (~> 1, >= 1.10.8)
|
||||
base64 (0.2.0)
|
||||
base64 (0.3.0)
|
||||
bcrypt (3.1.20)
|
||||
benchmark (0.4.0)
|
||||
benchmark (0.4.1)
|
||||
better_html (2.1.1)
|
||||
actionview (>= 6.0)
|
||||
activesupport (>= 6.0)
|
||||
@@ -114,7 +114,7 @@ GEM
|
||||
erubi (~> 1.4)
|
||||
parser (>= 2.4)
|
||||
smart_properties
|
||||
bigdecimal (3.1.8)
|
||||
bigdecimal (3.2.2)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.18.4)
|
||||
msgpack (~> 1.2)
|
||||
@@ -141,8 +141,8 @@ GEM
|
||||
cldr-plurals-runtime-rb (1.1.0)
|
||||
cmdparse (3.0.7)
|
||||
coderay (1.1.3)
|
||||
concurrent-ruby (1.3.4)
|
||||
connection_pool (2.4.1)
|
||||
concurrent-ruby (1.3.5)
|
||||
connection_pool (2.5.3)
|
||||
crack (1.0.0)
|
||||
bigdecimal
|
||||
rexml
|
||||
@@ -150,6 +150,8 @@ GEM
|
||||
css_parser (1.21.0)
|
||||
addressable
|
||||
csv (3.3.2)
|
||||
csv-safe (3.3.1)
|
||||
csv (~> 3.0)
|
||||
cuprite (0.15.1)
|
||||
capybara (~> 3.0)
|
||||
ferrum (~> 0.15.0)
|
||||
@@ -174,8 +176,9 @@ GEM
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
docile (1.4.1)
|
||||
dotenv (3.1.7)
|
||||
drb (2.2.1)
|
||||
drb (2.2.3)
|
||||
email_typo (0.2.3)
|
||||
erb (5.0.2)
|
||||
erb_lint (0.7.0)
|
||||
activesupport
|
||||
better_html (>= 2.0.1)
|
||||
@@ -270,26 +273,28 @@ GEM
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
hashdiff (1.1.2)
|
||||
hexapdf (1.0.3)
|
||||
hexapdf (1.4.0)
|
||||
cmdparse (~> 3.0, >= 3.0.3)
|
||||
geom2d (~> 0.4, >= 0.4.1)
|
||||
openssl (>= 2.2.1)
|
||||
strscan (>= 3.1.2)
|
||||
htmlentities (4.3.4)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.14.6)
|
||||
i18n (1.14.7)
|
||||
concurrent-ruby (~> 1.0)
|
||||
image_processing (1.13.0)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
io-console (0.8.0)
|
||||
irb (1.14.3)
|
||||
io-console (0.8.1)
|
||||
irb (1.15.2)
|
||||
pp (>= 0.6.0)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
jmespath (1.6.2)
|
||||
json (2.9.1)
|
||||
json (2.15.0)
|
||||
jwt (2.9.3)
|
||||
base64
|
||||
language_server-protocol (3.17.0.3)
|
||||
language_server-protocol (3.17.0.5)
|
||||
launchy (3.0.1)
|
||||
addressable (~> 2.8)
|
||||
childprocess (~> 5.0)
|
||||
@@ -300,13 +305,14 @@ GEM
|
||||
letter_opener (~> 1.9)
|
||||
railties (>= 6.1)
|
||||
rexml
|
||||
logger (1.6.4)
|
||||
lint_roller (1.1.0)
|
||||
logger (1.7.0)
|
||||
lograge (0.14.0)
|
||||
actionpack (>= 4)
|
||||
activesupport (>= 4)
|
||||
railties (>= 4)
|
||||
request_store (~> 1.0)
|
||||
loofah (2.23.1)
|
||||
loofah (2.24.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
mail (2.8.1)
|
||||
@@ -319,8 +325,8 @@ GEM
|
||||
method_source (1.1.0)
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.8)
|
||||
minitest (5.25.4)
|
||||
mini_portile2 (2.8.9)
|
||||
minitest (5.25.5)
|
||||
msgpack (1.7.5)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.4.1)
|
||||
@@ -328,43 +334,54 @@ GEM
|
||||
mysql2 (0.5.6)
|
||||
net-http-persistent (4.0.5)
|
||||
connection_pool (~> 2.2)
|
||||
net-imap (0.5.6)
|
||||
net-imap (0.5.9)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
net-protocol
|
||||
net-protocol (0.2.2)
|
||||
timeout
|
||||
net-smtp (0.5.0)
|
||||
net-smtp (0.5.1)
|
||||
net-protocol
|
||||
nio4r (2.7.4)
|
||||
nokogiri (1.18.8)
|
||||
nokogiri (1.18.9)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.8-aarch64-linux-gnu)
|
||||
nokogiri (1.18.9-aarch64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.8-aarch64-linux-musl)
|
||||
nokogiri (1.18.9-aarch64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.8-arm64-darwin)
|
||||
nokogiri (1.18.9-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.8-x86_64-linux-gnu)
|
||||
nokogiri (1.18.9-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.8-x86_64-linux-musl)
|
||||
nokogiri (1.18.9-x86_64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
oj (3.16.8)
|
||||
numo-narray (0.9.2.1)
|
||||
oj (3.16.11)
|
||||
bigdecimal (>= 3.0)
|
||||
ostruct (>= 0.2)
|
||||
onnxruntime (0.10.1)
|
||||
ffi
|
||||
onnxruntime (0.10.1-aarch64-linux)
|
||||
ffi
|
||||
onnxruntime (0.10.1-arm64-darwin)
|
||||
ffi
|
||||
onnxruntime (0.10.1-x86_64-linux)
|
||||
ffi
|
||||
openssl (3.3.0)
|
||||
orm_adapter (0.5.0)
|
||||
os (1.1.4)
|
||||
ostruct (0.6.1)
|
||||
ostruct (0.6.3)
|
||||
package_json (0.1.0)
|
||||
pagy (9.3.3)
|
||||
parallel (1.26.3)
|
||||
parser (3.3.6.0)
|
||||
parallel (1.27.0)
|
||||
parser (3.3.9.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
pg (1.5.9)
|
||||
pp (0.6.2)
|
||||
prettyprint
|
||||
premailer (1.27.0)
|
||||
addressable
|
||||
css_parser (>= 1.19.0)
|
||||
@@ -375,42 +392,45 @@ GEM
|
||||
premailer (~> 1.7, >= 1.7.9)
|
||||
pretender (0.5.0)
|
||||
actionpack (>= 6.1)
|
||||
prettyprint (0.2.0)
|
||||
prism (1.5.1)
|
||||
pry (0.15.0)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
pry-rails (0.3.11)
|
||||
pry (>= 0.13.0)
|
||||
psych (5.2.2)
|
||||
psych (5.2.6)
|
||||
date
|
||||
stringio
|
||||
public_suffix (6.0.1)
|
||||
puma (6.5.0)
|
||||
nio4r (~> 2.0)
|
||||
racc (1.8.1)
|
||||
rack (3.1.16)
|
||||
rack (3.2.3)
|
||||
rack-proxy (0.7.7)
|
||||
rack
|
||||
rack-session (2.0.0)
|
||||
rack-session (2.1.1)
|
||||
base64 (>= 0.1.0)
|
||||
rack (>= 3.0.0)
|
||||
rack-test (2.1.0)
|
||||
rack-test (2.2.0)
|
||||
rack (>= 1.3)
|
||||
rackup (2.2.1)
|
||||
rack (>= 3)
|
||||
rails (8.0.1)
|
||||
actioncable (= 8.0.1)
|
||||
actionmailbox (= 8.0.1)
|
||||
actionmailer (= 8.0.1)
|
||||
actionpack (= 8.0.1)
|
||||
actiontext (= 8.0.1)
|
||||
actionview (= 8.0.1)
|
||||
activejob (= 8.0.1)
|
||||
activemodel (= 8.0.1)
|
||||
activerecord (= 8.0.1)
|
||||
activestorage (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
rails (8.0.2.1)
|
||||
actioncable (= 8.0.2.1)
|
||||
actionmailbox (= 8.0.2.1)
|
||||
actionmailer (= 8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
actiontext (= 8.0.2.1)
|
||||
actionview (= 8.0.2.1)
|
||||
activejob (= 8.0.2.1)
|
||||
activemodel (= 8.0.2.1)
|
||||
activerecord (= 8.0.2.1)
|
||||
activestorage (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 8.0.1)
|
||||
rails-dom-testing (2.2.0)
|
||||
railties (= 8.0.2.1)
|
||||
rails-dom-testing (2.3.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
nokogiri (>= 1.6)
|
||||
@@ -424,22 +444,23 @@ GEM
|
||||
actionview (> 3.1)
|
||||
activesupport (> 3.1)
|
||||
railties (> 3.1)
|
||||
railties (8.0.1)
|
||||
actionpack (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
railties (8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
irb (~> 1.13)
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.2.1)
|
||||
rdoc (6.10.0)
|
||||
rake (13.3.0)
|
||||
rdoc (6.14.2)
|
||||
erb
|
||||
psych (>= 4.0.0)
|
||||
redis-client (0.23.0)
|
||||
connection_pool
|
||||
regexp_parser (2.9.3)
|
||||
reline (0.6.0)
|
||||
regexp_parser (2.11.3)
|
||||
reline (0.6.2)
|
||||
io-console (~> 0.5)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
@@ -451,7 +472,7 @@ GEM
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
retriable (3.1.2)
|
||||
rexml (3.4.0)
|
||||
rexml (3.4.4)
|
||||
rotp (6.3.0)
|
||||
rouge (4.5.2)
|
||||
rqrcode (2.2.0)
|
||||
@@ -475,18 +496,20 @@ GEM
|
||||
rspec-mocks (~> 3.13)
|
||||
rspec-support (~> 3.13)
|
||||
rspec-support (3.13.2)
|
||||
rubocop (1.69.2)
|
||||
rubocop (1.81.1)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.36.2, < 2.0)
|
||||
rubocop-ast (>= 1.47.1, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.37.0)
|
||||
parser (>= 3.3.1.0)
|
||||
rubocop-ast (1.47.1)
|
||||
parser (>= 3.3.7.2)
|
||||
prism (~> 1.4)
|
||||
rubocop-performance (1.23.0)
|
||||
rubocop (>= 1.48.1, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
@@ -538,10 +561,11 @@ GEM
|
||||
sqlite3 (2.5.0-arm64-darwin)
|
||||
sqlite3 (2.5.0-x86_64-linux-gnu)
|
||||
sqlite3 (2.5.0-x86_64-linux-musl)
|
||||
stringio (3.1.2)
|
||||
stringio (3.1.7)
|
||||
strip_attributes (1.14.1)
|
||||
activemodel (>= 3.0, < 9.0)
|
||||
thor (1.3.2)
|
||||
strscan (3.1.5)
|
||||
thor (1.4.0)
|
||||
timeout (0.4.3)
|
||||
trailblazer-option (0.1.2)
|
||||
turbo-rails (2.0.11)
|
||||
@@ -556,9 +580,9 @@ GEM
|
||||
tzinfo-data (1.2024.2)
|
||||
tzinfo (>= 1.0.0)
|
||||
uber (0.1.0)
|
||||
unicode-display_width (3.1.2)
|
||||
unicode-emoji (~> 4.0, >= 4.0.4)
|
||||
unicode-emoji (4.0.4)
|
||||
unicode-display_width (3.2.0)
|
||||
unicode-emoji (~> 4.1)
|
||||
unicode-emoji (4.1.0)
|
||||
uniform_notifier (1.16.0)
|
||||
uri (1.0.3)
|
||||
useragent (0.16.11)
|
||||
@@ -574,12 +598,13 @@ GEM
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webrick (1.9.1)
|
||||
websocket-driver (0.7.6)
|
||||
websocket-driver (0.8.0)
|
||||
base64
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
zeitwerk (2.7.1)
|
||||
zeitwerk (2.7.3)
|
||||
|
||||
PLATFORMS
|
||||
aarch64-linux
|
||||
@@ -602,6 +627,7 @@ DEPENDENCIES
|
||||
cancancan
|
||||
capybara
|
||||
csv
|
||||
csv-safe
|
||||
cuprite
|
||||
debug
|
||||
devise
|
||||
@@ -621,7 +647,9 @@ DEPENDENCIES
|
||||
letter_opener_web
|
||||
lograge
|
||||
mysql2
|
||||
numo-narray
|
||||
oj
|
||||
onnxruntime
|
||||
pagy
|
||||
pg
|
||||
premailer-rails
|
||||
|
||||
@@ -42,7 +42,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
||||
- PDF signature verification
|
||||
- Users management
|
||||
- Mobile-optimized
|
||||
- 6 UI languages with signing available in 14 languages
|
||||
- 7 UI languages with signing available in 14 languages
|
||||
- API and Webhooks for integrations
|
||||
- Easy to deploy in minutes
|
||||
|
||||
|
||||
@@ -15,9 +15,11 @@ class AccountConfigsController < ApplicationController
|
||||
AccountConfig::ESIGNING_PREFERENCE_KEY,
|
||||
AccountConfig::FORM_WITH_CONFETTI_KEY,
|
||||
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
|
||||
AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY,
|
||||
AccountConfig::FORCE_SSO_AUTH_KEY,
|
||||
AccountConfig::FLATTEN_RESULT_PDF_KEY,
|
||||
AccountConfig::ENFORCE_SIGNING_ORDER_KEY,
|
||||
AccountConfig::WITH_FILE_LINKS_KEY,
|
||||
AccountConfig::WITH_SIGNATURE_ID,
|
||||
AccountConfig::COMBINE_PDF_RESULT_KEY,
|
||||
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
|
||||
|
||||
@@ -8,7 +8,8 @@ class AccountsController < ApplicationController
|
||||
'es-ES' => 'Español',
|
||||
'pt-PT' => 'Português',
|
||||
'de-DE' => 'Deutsch',
|
||||
'it-IT' => 'Italiano'
|
||||
'it-IT' => 'Italiano',
|
||||
'nl-NL' => 'Nederlands'
|
||||
}.freeze
|
||||
|
||||
before_action :load_account
|
||||
@@ -27,7 +28,7 @@ class AccountsController < ApplicationController
|
||||
unless URI.parse(@encrypted_config.value.to_s).class.in?([URI::HTTP, URI::HTTPS])
|
||||
@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
|
||||
|
||||
@encrypted_config.save!
|
||||
@@ -39,13 +40,14 @@ class AccountsController < ApplicationController
|
||||
redirect_to settings_account_path, notice: I18n.t('account_information_has_been_updated')
|
||||
end
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
render :show, status: :unprocessable_entity
|
||||
render :show, status: :unprocessable_content
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize!(:manage, current_account)
|
||||
|
||||
true_user.update!(locked_at: Time.current, email: true_user.email.sub('@', '+removed@'))
|
||||
true_user.account.update!(archived_at: Time.current)
|
||||
|
||||
# rubocop:disable Layout/LineLength
|
||||
render turbo_stream: turbo_stream.replace(
|
||||
|
||||
@@ -13,7 +13,7 @@ module Api
|
||||
def show
|
||||
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)
|
||||
|
||||
return head :not_found
|
||||
@@ -24,8 +24,9 @@ module Api
|
||||
attachment = blob.attachments.take
|
||||
|
||||
@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?
|
||||
send_blob_byte_range_data blob, request.headers['Range']
|
||||
@@ -41,16 +42,22 @@ module Api
|
||||
|
||||
private
|
||||
|
||||
def authorization_check!(attachment)
|
||||
is_authorized = attachment.name.in?(%w[logo preview_images]) ||
|
||||
(current_user && attachment.record.account.id == current_user.account_id) ||
|
||||
(current_user && !Docuseal.multitenant? && current_user.role == 'superadmin') ||
|
||||
!attachment.record.account.account_configs
|
||||
.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value
|
||||
def authorization_check!(attachment, record, exp)
|
||||
return if attachment.name == 'logo'
|
||||
return if exp.to_i >= Time.current.to_i
|
||||
return if current_user && current_ability.can?(:read, record)
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
@@ -16,7 +16,7 @@ module Api
|
||||
check_authorization
|
||||
|
||||
rescue_from Params::BaseValidator::InvalidParameterError do |e|
|
||||
render json: { error: e.message }, status: :unprocessable_entity
|
||||
render json: { error: e.message }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
rescue_from RateLimit::LimitApproached do |e|
|
||||
@@ -33,7 +33,7 @@ module Api
|
||||
rescue_from JSON::ParserError do |e|
|
||||
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
|
||||
|
||||
|
||||
@@ -10,10 +10,21 @@ module Api
|
||||
def create
|
||||
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))
|
||||
Rollbar.error("Empty signature: #{submitter.id}") if defined?(Rollbar)
|
||||
if params[:type].in?(%w[initials signature])
|
||||
image = Vips::Image.new_from_file(params[:file].path)
|
||||
|
||||
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_entity
|
||||
if ImageUtils.blank?(image)
|
||||
Rollbar.error("Empty signature: #{submitter.id}") if defined?(Rollbar)
|
||||
|
||||
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
if ImageUtils.error?(image)
|
||||
Rollbar.error("Error signature: #{submitter.id}") if defined?(Rollbar)
|
||||
|
||||
return render json: { error: "#{params[:type]} error, try to sign on another device" },
|
||||
status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
attachment = Submitters.create_attachment!(submitter, params)
|
||||
@@ -23,6 +34,10 @@ module Api
|
||||
end
|
||||
|
||||
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
|
||||
rescue Submitters::MaliciousFileExtension => e
|
||||
Rollbar.error(e) if defined?(Rollbar)
|
||||
|
||||
render json: { error: e.message }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def build_new_cookie_signatures_json(submitter, attachment)
|
||||
|
||||
@@ -11,18 +11,21 @@ module Api
|
||||
params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present?
|
||||
|
||||
submitters = paginate(
|
||||
submitters.preload(template: :folder, submission: [:submitters, { audit_trail_attachment: :blob,
|
||||
combined_document_attachment: :blob }],
|
||||
submitters.preload(template: { folder: :parent_folder },
|
||||
submission: [:submitters, { audit_trail_attachment: :blob,
|
||||
combined_document_attachment: :blob }],
|
||||
documents_attachments: :blob, attachments_attachments: :blob),
|
||||
field: :completed_at
|
||||
)
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
render json: {
|
||||
data: submitters.map do |s|
|
||||
{
|
||||
event_type: 'form.completed',
|
||||
timestamp: s.completed_at,
|
||||
data: Submitters::SerializeForWebhook.call(s)
|
||||
data: Submitters::SerializeForWebhook.call(s, expires_at:)
|
||||
}
|
||||
end,
|
||||
pagination: {
|
||||
|
||||
@@ -34,10 +34,12 @@ module Api
|
||||
associations: [:blob]
|
||||
).call
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
render json: {
|
||||
id: @submission.id,
|
||||
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
|
||||
|
||||
@@ -14,16 +14,19 @@ module Api
|
||||
:created_by_user, :submission_events,
|
||||
template: :folder,
|
||||
submitters: { documents_attachments: :blob, attachments_attachments: :blob },
|
||||
audit_trail_attachment: :blob
|
||||
audit_trail_attachment: :blob,
|
||||
combined_document_attachment: :blob
|
||||
),
|
||||
field: :completed_at)
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
render json: {
|
||||
data: submissions.map do |s|
|
||||
{
|
||||
event_type: 'submission.completed',
|
||||
timestamp: s.completed_at,
|
||||
data: Submissions::SerializeForApi.call(s, s.submitters)
|
||||
data: Submissions::SerializeForApi.call(s, s.submitters, expires_at:)
|
||||
}
|
||||
end,
|
||||
pagination: {
|
||||
|
||||
@@ -14,14 +14,16 @@ module Api
|
||||
submissions = filter_submissions(submissions, params)
|
||||
|
||||
submissions = paginate(submissions.preload(:created_by_user, :submitters,
|
||||
template: :folder,
|
||||
template: { folder: :parent_folder },
|
||||
combined_document_attachment: :blob,
|
||||
audit_trail_attachment: :blob))
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
render json: {
|
||||
data: submissions.map do |s|
|
||||
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,
|
||||
pagination: {
|
||||
count: submissions.size,
|
||||
@@ -41,7 +43,7 @@ module Api
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
render json: Submissions::SerializeForApi.call(@submission, submitters, params)
|
||||
@@ -50,12 +52,12 @@ module Api
|
||||
def create
|
||||
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?
|
||||
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
|
||||
|
||||
params[:send_email] = true unless params.key?(:send_email)
|
||||
@@ -82,7 +84,7 @@ module Api
|
||||
DownloadUtils::UnableToDownload => e
|
||||
Rollbar.warning(e) if defined?(Rollbar)
|
||||
|
||||
render json: { error: e.message }, status: :unprocessable_entity
|
||||
render json: { error: e.message }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
def destroy
|
||||
@@ -104,9 +106,10 @@ module Api
|
||||
submissions = submissions.where(slug: params[:slug]) if params[:slug].present?
|
||||
|
||||
if params[:template_folder].present?
|
||||
folder_ids = TemplateFolder.accessible_by(current_ability).where(name: params[:template_folder]).pluck(:id)
|
||||
folders =
|
||||
TemplateFolders.filter_by_full_name(TemplateFolder.accessible_by(current_ability), params[:template_folder])
|
||||
|
||||
submissions = submissions.joins(:template).where(template: { folder_id: folder_ids })
|
||||
submissions = submissions.joins(:template).where(template: { folder_id: folders.pluck(:id) })
|
||||
end
|
||||
|
||||
if params.key?(:archived)
|
||||
@@ -144,7 +147,7 @@ module Api
|
||||
is_send_email = !params[:send_email].in?(['false', false])
|
||||
|
||||
if (emails = (params[:emails] || params[:email]).presence) &&
|
||||
(params[:submission].blank? && params[:submitters].blank?)
|
||||
params[:submission].blank? && params[:submitters].blank?
|
||||
Submissions.create_from_emails(template:,
|
||||
user: current_user,
|
||||
source: :api,
|
||||
@@ -181,14 +184,15 @@ module Api
|
||||
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
|
||||
:require_phone_2fa, :expire_at, :name,
|
||||
{
|
||||
variables: {},
|
||||
message: %i[subject body],
|
||||
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||
: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],
|
||||
fields: [:name, :uuid, :default_value, :value, :title, :description,
|
||||
: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)
|
||||
)
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
render json: {
|
||||
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,
|
||||
pagination: {
|
||||
count: submitters.size,
|
||||
@@ -34,7 +36,7 @@ module Api
|
||||
|
||||
def update
|
||||
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
|
||||
|
||||
submission = @submitter.submission
|
||||
@@ -74,7 +76,7 @@ module Api
|
||||
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
|
||||
Rollbar.warning(e) if defined?(Rollbar)
|
||||
|
||||
render json: { error: e.message }, status: :unprocessable_entity
|
||||
render json: { error: e.message }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
def submitter_params
|
||||
|
||||
@@ -26,13 +26,15 @@ module Api
|
||||
original_template: @template,
|
||||
documents: params[:documents])
|
||||
|
||||
Templates.maybe_assign_access(cloned_template)
|
||||
|
||||
cloned_template.save!
|
||||
|
||||
WebhookUrls.enqueue_events(cloned_template, 'template.created')
|
||||
|
||||
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
|
||||
|
||||
@@ -7,7 +7,7 @@ module Api
|
||||
def index
|
||||
templates = filter_templates(@templates, params)
|
||||
|
||||
templates = paginate(templates.preload(:author, :folder))
|
||||
templates = paginate(templates.preload(:author, folder: :parent_folder))
|
||||
|
||||
schema_documents =
|
||||
ActiveStorage::Attachment.where(record_id: templates.map(&:id),
|
||||
@@ -24,13 +24,14 @@ module Api
|
||||
name: :preview_images)
|
||||
.preload(:blob)
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
render json: {
|
||||
data: templates.map do |t|
|
||||
Templates::SerializeForApi.call(
|
||||
t,
|
||||
schema_documents.select { |e| e.record_id == t.id },
|
||||
preview_image_attachments
|
||||
)
|
||||
Templates::SerializeForApi.call(t,
|
||||
schema_documents: schema_documents.select { |e| e.record_id == t.id },
|
||||
preview_image_attachments:,
|
||||
expires_at:)
|
||||
end,
|
||||
pagination: {
|
||||
count: templates.size,
|
||||
@@ -92,9 +93,9 @@ module Api
|
||||
templates = templates.where(slug: params[:slug]) if params[:slug].present?
|
||||
|
||||
if params[:folder].present?
|
||||
folder_ids = TemplateFolder.accessible_by(current_ability).where(name: params[:folder]).pluck(:id)
|
||||
folders = TemplateFolders.filter_by_full_name(TemplateFolder.accessible_by(current_ability), params[:folder])
|
||||
|
||||
templates = templates.where(folder_id: folder_ids)
|
||||
templates = templates.where(folder_id: folders.pluck(:id))
|
||||
end
|
||||
|
||||
templates
|
||||
@@ -106,14 +107,15 @@ module Api
|
||||
:external_id,
|
||||
: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,
|
||||
:required, :readonly, :default_value,
|
||||
:title, :description,
|
||||
:title, :description, :prefillable,
|
||||
{ preferences: {},
|
||||
default_value: [],
|
||||
conditions: [%i[field_uuid value action operation]],
|
||||
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]] }]]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -7,8 +7,8 @@ module Api
|
||||
def merge
|
||||
files = params[:files] || []
|
||||
|
||||
return render json: { error: 'Files are required' }, status: :unprocessable_entity if files.blank?
|
||||
return render json: { error: 'At least 2 files are required' }, status: :unprocessable_entity if files.size < 2
|
||||
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_content if files.size < 2
|
||||
|
||||
render json: {
|
||||
data: Base64.encode64(PdfUtils.merge(files.map { |base64| StringIO.new(Base64.decode64(base64)) }).string)
|
||||
@@ -35,7 +35,7 @@ module Api
|
||||
end
|
||||
}
|
||||
rescue HexaPDF::MalformedPDFError
|
||||
render json: { error: 'Malformed PDF' }, status: :unprocessable_entity
|
||||
render json: { error: 'Malformed PDF' }, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,6 +13,8 @@ class ApplicationController < ActionController::Base
|
||||
before_action :maybe_redirect_to_setup, unless: :signed_in?
|
||||
before_action :authenticate_user!, unless: :devise_controller?
|
||||
|
||||
before_action :set_csp, if: -> { request.get? && !request.headers['HTTP_X_TURBO'] }
|
||||
|
||||
helper_method :button_title,
|
||||
:current_account,
|
||||
: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
|
||||
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
|
||||
|
||||
@@ -17,8 +17,10 @@ class ConsoleRedirectController < ApplicationController
|
||||
scope: :console,
|
||||
exp: 1.minute.from_now.to_i)
|
||||
|
||||
path = Addressable::URI.parse(params[:redir]).path if params[:redir].to_s.starts_with?(Docuseal::CONSOLE_URL)
|
||||
redir_uri = Addressable::URI.parse(params[:redir])
|
||||
path = redir_uri.path if params[:redir].to_s.starts_with?(Docuseal::CONSOLE_URL)
|
||||
|
||||
redirect_to("#{Docuseal::CONSOLE_URL}#{path}?#{{ auth: }.to_query}", allow_other_host: true)
|
||||
redirect_to "#{Docuseal::CONSOLE_URL}#{path}?#{{ **redir_uri&.query_values, 'auth' => auth }.to_query}",
|
||||
allow_other_host: true
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,12 +15,12 @@ class EmailSmtpSettingsController < ApplicationController
|
||||
|
||||
redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved')
|
||||
else
|
||||
render :index, status: :unprocessable_entity
|
||||
render :index, status: :unprocessable_content
|
||||
end
|
||||
rescue StandardError => e
|
||||
flash[:alert] = e.message
|
||||
|
||||
render :index, status: :unprocessable_entity
|
||||
render :index, status: :unprocessable_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -53,7 +53,7 @@ class EsignSettingsController < ApplicationController
|
||||
@cert_record.errors.add(:name, I18n.t('already_exists'))
|
||||
|
||||
return render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'),
|
||||
status: :unprocessable_entity
|
||||
status: :unprocessable_content
|
||||
end
|
||||
|
||||
save_new_cert!(@encrypted_config, @cert_record)
|
||||
@@ -64,7 +64,7 @@ class EsignSettingsController < ApplicationController
|
||||
|
||||
@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
|
||||
|
||||
def update
|
||||
|
||||
@@ -24,7 +24,7 @@ class MfaSetupController < ApplicationController
|
||||
|
||||
@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
|
||||
|
||||
@@ -36,7 +36,7 @@ class MfaSetupController < ApplicationController
|
||||
else
|
||||
@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
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
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
|
||||
attribute :user
|
||||
end
|
||||
@@ -16,4 +20,10 @@ class PasswordsController < Devise::PasswordsController
|
||||
Current.user = resource
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def after_resetting_password_path_for(_)
|
||||
new_session_path(resource_name)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,6 +4,7 @@ class PersonalizationSettingsController < ApplicationController
|
||||
ALLOWED_KEYS = [
|
||||
AccountConfig::FORM_COMPLETED_BUTTON_KEY,
|
||||
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
|
||||
AccountConfig::SUBMITTER_INVITATION_REMINDER_EMAIL_KEY,
|
||||
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
|
||||
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY,
|
||||
AccountConfig::FORM_COMPLETED_MESSAGE_KEY,
|
||||
|
||||
@@ -11,16 +11,16 @@ class ProfileController < ApplicationController
|
||||
if current_user.update(contact_params)
|
||||
redirect_to settings_profile_index_path, notice: I18n.t('contact_information_has_been_update')
|
||||
else
|
||||
render :index, status: :unprocessable_entity
|
||||
render :index, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def update_password
|
||||
if current_user.update(password_params)
|
||||
if current_user.update_with_password(password_params)
|
||||
bypass_sign_in(current_user)
|
||||
redirect_to settings_profile_index_path, notice: I18n.t('password_has_been_changed')
|
||||
else
|
||||
render :index, status: :unprocessable_entity
|
||||
render :index, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
@@ -31,6 +31,6 @@ class ProfileController < ApplicationController
|
||||
end
|
||||
|
||||
def password_params
|
||||
params.require(:user).permit(:password, :password_confirmation)
|
||||
params.require(:user).permit(:password, :password_confirmation, :current_password)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class RevealAccessTokenController < ApplicationController
|
||||
def show
|
||||
authorize!(:manage, current_user.access_token)
|
||||
end
|
||||
|
||||
def create
|
||||
authorize!(:manage, current_user.access_token)
|
||||
|
||||
if current_user.valid_password?(params[:password])
|
||||
render turbo_stream: turbo_stream.replace(:access_token_container,
|
||||
partial: 'reveal_access_token/access_token',
|
||||
locals: { token: current_user.access_token.token })
|
||||
else
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'reveal_access_token/show',
|
||||
locals: { error_message: I18n.t('wrong_password') }),
|
||||
status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -29,7 +29,7 @@ class SendSubmissionEmailController < ApplicationController
|
||||
|
||||
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
||||
|
||||
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! unless already_sent?(@submitter)
|
||||
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! if can_send?(@submitter)
|
||||
|
||||
respond_to do |f|
|
||||
f.html { render :success }
|
||||
@@ -39,8 +39,11 @@ class SendSubmissionEmailController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def already_sent?(submitter)
|
||||
EmailEvent.exists?(tag: :submitter_documents_copy, email: submitter.email, emailable: submitter,
|
||||
event_type: :send, created_at: SEND_DURATION.ago..Time.current)
|
||||
def can_send?(submitter)
|
||||
return false if submitter.account.archived_at?
|
||||
return false if EmailEvent.exists?(tag: :submitter_documents_copy, email: submitter.email, emailable: submitter,
|
||||
event_type: :send, created_at: SEND_DURATION.ago..Time.current)
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
@@ -16,7 +16,7 @@ class SessionsController < Devise::SessionsController
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
super
|
||||
|
||||
@@ -23,10 +23,10 @@ class SetupController < ApplicationController
|
||||
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'))
|
||||
|
||||
return render :index, status: :unprocessable_entity
|
||||
return render :index, status: :unprocessable_content
|
||||
end
|
||||
|
||||
return render :index, status: :unprocessable_entity unless @account.valid?
|
||||
return render :index, status: :unprocessable_content unless @account.valid?
|
||||
|
||||
if @user.save
|
||||
encrypted_configs = [
|
||||
@@ -42,7 +42,7 @@ class SetupController < ApplicationController
|
||||
|
||||
redirect_to newsletter_path
|
||||
else
|
||||
render :index, status: :unprocessable_entity
|
||||
render :index, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ class StartFormController < ApplicationController
|
||||
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record?
|
||||
@error_message = multiple_submitters_error_message
|
||||
|
||||
return render :show, status: :unprocessable_entity
|
||||
return render :show, status: :unprocessable_content
|
||||
end
|
||||
|
||||
if (is_new_record = @submitter.new_record?)
|
||||
@@ -60,7 +60,7 @@ class StartFormController < ApplicationController
|
||||
|
||||
redirect_to submit_form_path(@submitter.slug)
|
||||
else
|
||||
render :show, status: :unprocessable_entity
|
||||
render :show, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -206,7 +206,7 @@ class StartFormController < ApplicationController
|
||||
end
|
||||
|
||||
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)
|
||||
|
||||
@@ -223,7 +223,7 @@ class StartFormController < ApplicationController
|
||||
|
||||
redirect_to submit_form_path(submitter.slug)
|
||||
else
|
||||
render :show, status: :unprocessable_entity
|
||||
render :show, status: :unprocessable_content
|
||||
end
|
||||
else
|
||||
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')
|
||||
else
|
||||
render :index, status: :unprocessable_entity
|
||||
render :index, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -32,10 +32,7 @@ class SubmissionsController < ApplicationController
|
||||
def create
|
||||
save_template_message(@template, params) if params[:save_message] == '1'
|
||||
|
||||
if params[:is_custom_message] != '1'
|
||||
params.delete(:subject)
|
||||
params.delete(:body)
|
||||
end
|
||||
[params.delete(:subject), params.delete(:body)] if params[:is_custom_message] != '1'
|
||||
|
||||
submissions =
|
||||
if params[:emails].present?
|
||||
@@ -46,11 +43,16 @@ class SubmissionsController < ApplicationController
|
||||
emails: params[:emails],
|
||||
params: params.merge('send_completed_email' => true))
|
||||
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,
|
||||
user: current_user,
|
||||
source: :invite,
|
||||
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))
|
||||
end
|
||||
|
||||
@@ -62,10 +64,9 @@ class SubmissionsController < ApplicationController
|
||||
|
||||
redirect_to template_path(@template), notice: I18n.t('new_recipients_have_been_added')
|
||||
rescue Submissions::CreateFromSubmitters::BaseError => e
|
||||
render turbo_stream: turbo_stream.replace(:submitters_error,
|
||||
partial: 'submissions/error',
|
||||
locals: { error: e.message }),
|
||||
status: :unprocessable_entity
|
||||
render turbo_stream: turbo_stream.replace(:submitters_error, partial: 'submissions/error',
|
||||
locals: { error: e.message }),
|
||||
status: :unprocessable_content
|
||||
end
|
||||
|
||||
def destroy
|
||||
@@ -95,7 +96,7 @@ class SubmissionsController < ApplicationController
|
||||
end
|
||||
|
||||
def submissions_params
|
||||
params.permit(submission: { submitters: [%i[uuid email phone name]] })
|
||||
params.permit(submission: { submitters: [:uuid, :email, :phone, :name, { values: {} }] })
|
||||
end
|
||||
|
||||
def load_template
|
||||
|
||||
@@ -70,7 +70,7 @@ class SubmissionsDownloadController < ApplicationController
|
||||
return if submitter.submission.submitters.order(:completed_at).last != submitter
|
||||
|
||||
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,
|
||||
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
|
||||
|
||||
@@ -10,11 +10,16 @@ class SubmissionsExportController < ApplicationController
|
||||
attachments_attachments: :blob })
|
||||
.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'
|
||||
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format]),
|
||||
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format], expires_at:),
|
||||
filename: "#{@template.name}.csv"
|
||||
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"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
class SubmissionsFiltersController < ApplicationController
|
||||
ALLOWED_NAMES = %w[
|
||||
author
|
||||
folder
|
||||
completed_at
|
||||
status
|
||||
created_at
|
||||
|
||||
@@ -41,6 +41,9 @@ class SubmissionsPreviewController < ApplicationController
|
||||
|
||||
def completed
|
||||
@submission = Submission.find_by!(slug: params[:submissions_preview_slug])
|
||||
|
||||
raise ActionController::RoutingError, I18n.t('not_found') if @submission.account.archived_at?
|
||||
|
||||
@template = @submission.template
|
||||
|
||||
render :completed, layout: 'form'
|
||||
|
||||
@@ -48,20 +48,20 @@ class SubmitFormController < ApplicationController
|
||||
|
||||
def update
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
if @submitter.declined_at?
|
||||
return render json: { error: I18n.t('form_has_been_declined') },
|
||||
status: :unprocessable_entity
|
||||
status: :unprocessable_content
|
||||
end
|
||||
|
||||
Submitters::SubmitValues.call(@submitter, params, request)
|
||||
@@ -70,12 +70,14 @@ class SubmitFormController < ApplicationController
|
||||
rescue Submitters::SubmitValues::RequiredFieldError => e
|
||||
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
|
||||
render json: { error: e.message }, status: :unprocessable_entity
|
||||
render json: { error: e.message }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
def completed; end
|
||||
def completed
|
||||
raise ActionController::RoutingError, I18n.t('not_found') if @submitter.account.archived_at?
|
||||
end
|
||||
|
||||
def success; end
|
||||
|
||||
|
||||
@@ -11,13 +11,13 @@ class SubmitFormDownloadController < ApplicationController
|
||||
|
||||
return redirect_to submitter_download_index_path(@submitter.slug) if @submitter.completed_at?
|
||||
|
||||
return head :unprocessable_entity if @submitter.declined_at? ||
|
||||
@submitter.submission.archived_at? ||
|
||||
@submitter.submission.expired? ||
|
||||
@submitter.submission.template&.archived_at? ||
|
||||
AccountConfig.exists?(account_id: @submitter.account_id,
|
||||
key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
|
||||
value: false)
|
||||
return head :unprocessable_content if @submitter.declined_at? ||
|
||||
@submitter.submission.archived_at? ||
|
||||
@submitter.submission.expired? ||
|
||||
@submitter.submission.template&.archived_at? ||
|
||||
AccountConfig.exists?(account_id: @submitter.account_id,
|
||||
key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
|
||||
value: false)
|
||||
|
||||
last_completed_submitter = @submitter.submission.submitters
|
||||
.where.not(id: @submitter.id)
|
||||
|
||||
@@ -7,7 +7,7 @@ class SubmitFormInviteController < ApplicationController
|
||||
def create
|
||||
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')
|
||||
optional_invite_submitters = filter_invite_submitters(submitter, 'optional_invite_by_uuid')
|
||||
@@ -34,7 +34,7 @@ class SubmitFormInviteController < ApplicationController
|
||||
|
||||
head :ok
|
||||
else
|
||||
head :unprocessable_entity
|
||||
head :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -3,9 +3,15 @@
|
||||
class TemplateDocumentsController < ApplicationController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
FILES_TTL = 5.minutes
|
||||
|
||||
def index
|
||||
render json: @template.schema_documents.map { |d| ActiveStorage::Blob.proxy_url(d.blob, expires_at: FILES_TTL.from_now.to_i) }
|
||||
end
|
||||
|
||||
def create
|
||||
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
|
||||
|
||||
old_fields_hash = @template.fields.hash
|
||||
@@ -28,6 +34,6 @@ class TemplateDocumentsController < ApplicationController
|
||||
)
|
||||
}
|
||||
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
|
||||
|
||||
@@ -3,14 +3,31 @@
|
||||
class TemplateFoldersAutocompleteController < ApplicationController
|
||||
load_and_authorize_resource :template_folder, parent: false
|
||||
|
||||
LIMIT = 100
|
||||
LIMIT = 30
|
||||
|
||||
def index
|
||||
templates_query = Template.accessible_by(current_ability).where(archived_at: nil)
|
||||
parent_name, name =
|
||||
if params[:parent_name].present?
|
||||
[params[:parent_name], params[:q]]
|
||||
else
|
||||
params[:q].to_s.split(' /', 2).map(&:squish)
|
||||
end
|
||||
|
||||
template_folders = @template_folders.where(id: templates_query.select(:folder_id))
|
||||
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
|
||||
if name
|
||||
parent_folder = @template_folders.find_by(name: parent_name, parent_folder_id: nil)
|
||||
else
|
||||
name = parent_name
|
||||
end
|
||||
|
||||
render json: template_folders.as_json(only: %i[name archived_at])
|
||||
template_folders = TemplateFolders.filter_active_folders(@template_folders.where(parent_folder:),
|
||||
Template.accessible_by(current_ability))
|
||||
|
||||
name = name.to_s.downcase
|
||||
|
||||
template_folders = TemplateFolders.search(template_folders, name).order(id: :desc).limit(LIMIT)
|
||||
|
||||
render json: template_folders.preload(:parent_folder)
|
||||
.sort_by { |e| e.name.downcase.index(name) || Float::MAX }
|
||||
.as_json(only: %i[name archived_at], methods: %i[full_name])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,13 +5,39 @@ class TemplateFoldersController < ApplicationController
|
||||
|
||||
helper_method :selected_order
|
||||
|
||||
def show
|
||||
@templates = @template_folder.templates.active.accessible_by(current_ability)
|
||||
.preload(:author, :template_accesses)
|
||||
@templates = Templates.search(current_user, @templates, params[:q])
|
||||
@templates = Templates::Order.call(@templates, current_user, selected_order)
|
||||
TEMPLATES_PER_PAGE = 12
|
||||
FOLDERS_PER_PAGE = 18
|
||||
|
||||
@pagy, @templates = pagy_auto(@templates, limit: 12)
|
||||
def show
|
||||
@templates = Template.active.accessible_by(current_ability)
|
||||
.where(folder: [@template_folder, *(params[:q].present? ? @template_folder.subfolders : [])])
|
||||
.preload(:author, :template_accesses)
|
||||
|
||||
@template_folders =
|
||||
@template_folder.subfolders.where(id: Template.accessible_by(current_ability).active.select(:folder_id))
|
||||
|
||||
@template_folders = TemplateFolders.search(@template_folders, params[:q])
|
||||
@template_folders = TemplateFolders.sort(@template_folders, current_user, selected_order)
|
||||
|
||||
if @templates.exists?
|
||||
@templates = Templates.search(current_user, @templates, params[:q])
|
||||
@templates = Templates::Order.call(@templates, current_user, selected_order)
|
||||
|
||||
limit =
|
||||
if @template_folders.size < 4
|
||||
TEMPLATES_PER_PAGE
|
||||
else
|
||||
(@template_folders.size < 7 ? 9 : 6)
|
||||
end
|
||||
|
||||
@pagy, @templates = pagy_auto(@templates, limit:)
|
||||
|
||||
load_related_submissions if params[:q].present? && @templates.blank?
|
||||
else
|
||||
@pagy, @template_folders = pagy(@template_folders, limit: FOLDERS_PER_PAGE)
|
||||
|
||||
@templates = @templates.none
|
||||
end
|
||||
end
|
||||
|
||||
def edit; end
|
||||
@@ -40,4 +66,21 @@ class TemplateFoldersController < ApplicationController
|
||||
def template_folder_params
|
||||
params.require(:template_folder).permit(:name)
|
||||
end
|
||||
|
||||
def load_related_submissions
|
||||
@related_submissions =
|
||||
Submission.accessible_by(current_ability)
|
||||
.where(archived_at: nil)
|
||||
.where(template_id: current_account.templates.active
|
||||
.where(folder: [@template_folder, *@template_folder.subfolders])
|
||||
.select(:id))
|
||||
.preload(:template_accesses, :created_by_user,
|
||||
template: :author,
|
||||
submitters: :start_form_submission_events)
|
||||
|
||||
@related_submissions = Submissions.search(current_user, @related_submissions, params[:q])
|
||||
.order(id: :desc)
|
||||
|
||||
@related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,9 +4,27 @@ class TemplatesArchivedController < ApplicationController
|
||||
load_and_authorize_resource :template, parent: false
|
||||
|
||||
def index
|
||||
@templates = @templates.where.not(archived_at: nil).preload(:author, :folder, :template_accesses).order(id: :desc)
|
||||
@templates = @templates.where.not(archived_at: nil)
|
||||
.preload(:author, :template_accesses, folder: :parent_folder)
|
||||
.order(id: :desc)
|
||||
|
||||
@templates = Templates.search(current_user, @templates, params[:q])
|
||||
|
||||
@pagy, @templates = pagy_auto(@templates, limit: 12)
|
||||
|
||||
return unless params[:q].present? && @templates.blank?
|
||||
|
||||
@related_submissions =
|
||||
Submission.accessible_by(current_ability)
|
||||
.joins(:template)
|
||||
.where.not(templates: { archived_at: nil })
|
||||
.preload(:template_accesses, :created_by_user,
|
||||
template: :author,
|
||||
submitters: :start_form_submission_events)
|
||||
|
||||
@related_submissions = Submissions.search(current_user, @related_submissions, params[:q])
|
||||
.order(id: :desc)
|
||||
|
||||
@related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,7 +4,7 @@ class TemplatesCloneAndReplaceController < ApplicationController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def create
|
||||
return head :unprocessable_entity if params[:files].blank?
|
||||
return head :unprocessable_content if params[:files].blank?
|
||||
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [@template],
|
||||
@@ -17,6 +17,8 @@ class TemplatesCloneAndReplaceController < ApplicationController
|
||||
|
||||
documents = Templates::ReplaceAttachments.call(cloned_template, params, extract_fields: true)
|
||||
|
||||
Templates.maybe_assign_access(cloned_template)
|
||||
|
||||
cloned_template.save!
|
||||
|
||||
Templates::CloneAttachments.call(template: cloned_template, original_template: @template,
|
||||
@@ -31,7 +33,7 @@ class TemplatesCloneAndReplaceController < ApplicationController
|
||||
rescue Templates::CreateAttachments::PdfEncrypted
|
||||
respond_to do |f|
|
||||
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
|
||||
|
||||
@@ -69,6 +69,8 @@ class TemplatesController < ApplicationController
|
||||
@template.account = current_account
|
||||
end
|
||||
|
||||
Templates.maybe_assign_access(@template)
|
||||
|
||||
if @template.save
|
||||
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
||||
|
||||
@@ -78,7 +80,7 @@ class TemplatesController < ApplicationController
|
||||
|
||||
maybe_redirect_to_template(@template)
|
||||
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
|
||||
|
||||
@@ -116,15 +118,17 @@ class TemplatesController < ApplicationController
|
||||
def template_params
|
||||
params.require(:template).permit(
|
||||
:name,
|
||||
{ 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]],
|
||||
{ schema: [[:attachment_uuid, :google_drive_file_id, :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 order]],
|
||||
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||
:required, :readonly, :default_value,
|
||||
:title, :description,
|
||||
:title, :description, :prefillable,
|
||||
{ preferences: {},
|
||||
default_value: [],
|
||||
conditions: [%i[field_uuid value action operation]],
|
||||
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]] }]] }
|
||||
)
|
||||
end
|
||||
|
||||
@@ -11,10 +11,11 @@ class TemplatesDashboardController < ApplicationController
|
||||
helper_method :selected_order
|
||||
|
||||
def index
|
||||
@template_folders = @template_folders.where(id: @templates.active.select(:folder_id))
|
||||
@template_folders =
|
||||
TemplateFolders.filter_active_folders(@template_folders.where(parent_folder_id: nil), @templates)
|
||||
|
||||
@template_folders = TemplateFolders.search(@template_folders, params[:q])
|
||||
@template_folders = sort_template_folders(@template_folders, current_user, selected_order)
|
||||
@template_folders = TemplateFolders.sort(@template_folders, current_user, selected_order)
|
||||
|
||||
@pagy, @template_folders = pagy(
|
||||
@template_folders,
|
||||
@@ -68,40 +69,6 @@ class TemplatesDashboardController < ApplicationController
|
||||
Templates.search(current_user, rel, params[:q])
|
||||
end
|
||||
|
||||
def sort_template_folders(template_folders, current_user, order)
|
||||
case order
|
||||
when 'used_at'
|
||||
subquery =
|
||||
Template.left_joins(:submissions)
|
||||
.group(:folder_id)
|
||||
.where(account_id: current_user.account_id)
|
||||
.select(
|
||||
:folder_id,
|
||||
Template.arel_table[:updated_at].maximum.as('updated_at_max'),
|
||||
Submission.arel_table[:created_at].maximum.as('submission_created_at_max')
|
||||
)
|
||||
|
||||
template_folders = template_folders.joins(
|
||||
Template.arel_table
|
||||
.join(subquery.arel.as('templates'), Arel::Nodes::OuterJoin)
|
||||
.on(TemplateFolder.arel_table[:id].eq(Template.arel_table[:folder_id]))
|
||||
.join_sources
|
||||
)
|
||||
|
||||
template_folders.order(
|
||||
Arel::Nodes::Case.new
|
||||
.when(Template.arel_table[:submission_created_at_max].gt(Template.arel_table[:updated_at_max]))
|
||||
.then(Template.arel_table[:submission_created_at_max])
|
||||
.else(Template.arel_table[:updated_at_max])
|
||||
.desc
|
||||
)
|
||||
when 'name'
|
||||
template_folders.order(name: :asc)
|
||||
else
|
||||
template_folders.order(id: :desc)
|
||||
end
|
||||
end
|
||||
|
||||
def selected_order
|
||||
@selected_order ||=
|
||||
if cookies.permanent[:dashboard_templates_order].blank? ||
|
||||
|
||||
@@ -3,19 +3,29 @@
|
||||
class TemplatesDebugController < ApplicationController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
DEBUG_FILE = ''
|
||||
|
||||
def show
|
||||
attachment = @template.documents.first
|
||||
schema_uuids = @template.schema.index_by { |e| e['attachment_uuid'] }
|
||||
attachment = @template.documents.find { |a| schema_uuids[a.uuid] }
|
||||
|
||||
data = attachment.download
|
||||
pdf = HexaPDF::Document.new(io: StringIO.new(data))
|
||||
|
||||
fields = Templates::FindAcroFields.call(pdf, attachment, data)
|
||||
unless attachment.image?
|
||||
pdf = HexaPDF::Document.new(io: StringIO.new(data))
|
||||
|
||||
fields = Templates::FindAcroFields.call(pdf, attachment, data)
|
||||
end
|
||||
|
||||
fields, = Templates::DetectFields.call(StringIO.new(data), attachment:) if fields.blank?
|
||||
|
||||
attachment.metadata['pdf'] ||= {}
|
||||
attachment.metadata['pdf']['fields'] = fields
|
||||
|
||||
@template.update!(fields: Templates::ProcessDocument.normalize_attachment_fields(@template, [attachment]))
|
||||
|
||||
debug_file if DEBUG_FILE.present?
|
||||
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [@template],
|
||||
associations: [schema_documents: { preview_images_attachments: :blob }]
|
||||
@@ -31,4 +41,27 @@ class TemplatesDebugController < ApplicationController
|
||||
|
||||
render 'templates/edit', layout: 'plain'
|
||||
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
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TemplatesDetectFieldsController < ApplicationController
|
||||
include ActionController::Live
|
||||
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def create
|
||||
response.headers['Content-Type'] = 'text/event-stream'
|
||||
|
||||
sse = SSE.new(response.stream)
|
||||
|
||||
documents = @template.schema_documents.preload(:blob)
|
||||
|
||||
documents.each do |document|
|
||||
io = StringIO.new(document.download)
|
||||
|
||||
Templates::DetectFields.call(io, attachment: document) do |(attachment_uuid, page, fields)|
|
||||
sse.write({ attachment_uuid:, page:, fields: })
|
||||
end
|
||||
end
|
||||
|
||||
sse.write({ completed: true })
|
||||
ensure
|
||||
response.stream.close
|
||||
end
|
||||
end
|
||||
@@ -6,7 +6,9 @@ class TemplatesFoldersController < ApplicationController
|
||||
def edit; end
|
||||
|
||||
def update
|
||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:name])
|
||||
name = [params[:parent_name], params[:name]].compact_blank.join(' / ')
|
||||
|
||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, name)
|
||||
|
||||
if @template.save
|
||||
redirect_back(fallback_location: template_path(@template), notice: I18n.t('document_template_has_been_moved'))
|
||||
|
||||
@@ -3,6 +3,15 @@
|
||||
class TemplatesPreferencesController < ApplicationController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
RESETTABLE_PREFERENCE_KEYS = {
|
||||
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY => %w[request_email_subject request_email_body submitters],
|
||||
AccountConfig::SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => %w[invitation_reminder_email_subject
|
||||
invitation_reminder_email_body],
|
||||
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => %w[documents_copy_email_subject documents_copy_email_body],
|
||||
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY => %w[completed_notification_email_subject
|
||||
completed_notification_email_body]
|
||||
}.freeze
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@@ -15,19 +24,38 @@ class TemplatesPreferencesController < ApplicationController
|
||||
head :ok
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize!(:update, @template)
|
||||
|
||||
config_key = params[:config_key]
|
||||
preferences_to_delete = RESETTABLE_PREFERENCE_KEYS[config_key]
|
||||
|
||||
return head :ok if preferences_to_delete.blank?
|
||||
|
||||
preferences_to_delete.each do |key|
|
||||
@template.preferences.delete(key)
|
||||
end
|
||||
|
||||
@template.save!
|
||||
|
||||
render turbo_stream: turbo_stream.replace("#{config_key}_form",
|
||||
partial: "templates_preferences/#{config_key}_form"),
|
||||
status: :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def template_params
|
||||
params.require(:template).permit(
|
||||
preferences: %i[bcc_completed request_email_subject request_email_body
|
||||
invitation_reminder_email_subject invitation_reminder_email_body
|
||||
documents_copy_email_subject documents_copy_email_body
|
||||
documents_copy_email_enabled documents_copy_email_attach_audit
|
||||
documents_copy_email_attach_documents documents_copy_email_reply_to
|
||||
completed_notification_email_attach_documents
|
||||
completed_redirect_url validate_unique_submitters
|
||||
require_all_submitters submitters_order require_phone_2fa
|
||||
default_expire_at_duration shared_link_2fa
|
||||
default_expire_at
|
||||
default_expire_at_duration shared_link_2fa default_expire_at request_email_enabled
|
||||
completed_notification_email_subject completed_notification_email_body
|
||||
completed_notification_email_enabled completed_notification_email_attach_audit] +
|
||||
[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 =
|
||||
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!
|
||||
|
||||
render json: { submitters: @template.submitters }
|
||||
@@ -18,7 +22,7 @@ class TemplatesRecipientsController < ApplicationController
|
||||
|
||||
def submitters_params
|
||||
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|
|
||||
next if s[:uuid].blank?
|
||||
@@ -29,6 +33,7 @@ class TemplatesRecipientsController < ApplicationController
|
||||
s.delete(:is_requester)
|
||||
end
|
||||
|
||||
s[:order] = s[:order].to_i if s[:order].present?
|
||||
s.delete(:invite_by_uuid) if s[: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.name = File.basename((url_params || params)[:files].first.original_filename, '.*')
|
||||
|
||||
Templates.maybe_assign_access(template)
|
||||
|
||||
template.save!
|
||||
|
||||
template
|
||||
|
||||
@@ -24,12 +24,22 @@ class UsersController < ApplicationController
|
||||
def edit; end
|
||||
|
||||
def create
|
||||
if User.accessible_by(current_ability).exists?(email: @user.email)
|
||||
@user.errors.add(:email, I18n.t('already_exists'))
|
||||
existing_user = User.accessible_by(current_ability).find_by(email: @user.email)
|
||||
|
||||
return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity
|
||||
if existing_user
|
||||
if existing_user.archived_at? &&
|
||||
current_ability.can?(:manage, existing_user) && current_ability.can?(:manage, @user.account)
|
||||
existing_user.assign_attributes(@user.slice(:first_name, :last_name, :role, :account_id))
|
||||
existing_user.archived_at = nil
|
||||
@user = existing_user
|
||||
else
|
||||
@user.errors.add(:email, I18n.t('already_exists'))
|
||||
|
||||
return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
@user.password = SecureRandom.hex if @user.password.blank?
|
||||
@user.role = User::ADMIN_ROLE unless role_valid?(@user.role)
|
||||
|
||||
if @user.save
|
||||
@@ -37,7 +47,7 @@ class UsersController < ApplicationController
|
||||
|
||||
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_invited')
|
||||
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
|
||||
|
||||
@@ -54,10 +64,10 @@ class UsersController < ApplicationController
|
||||
@user.account = account
|
||||
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')
|
||||
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
|
||||
|
||||
@@ -83,7 +93,7 @@ class UsersController < ApplicationController
|
||||
|
||||
def user_params
|
||||
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))
|
||||
|
||||
|
||||
@@ -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 PromptPassword from './elements/prompt_password'
|
||||
import EmailsTextarea from './elements/emails_textarea'
|
||||
import ToggleSubmit from './elements/toggle_submit'
|
||||
import ToggleOnSubmit from './elements/toggle_on_submit'
|
||||
import CheckOnClick from './elements/check_on_click'
|
||||
import PasswordInput from './elements/password_input'
|
||||
@@ -34,9 +35,22 @@ import MaskedInput from './elements/masked_input'
|
||||
import SetDateButton from './elements/set_date_button'
|
||||
import IndeterminateCheckbox from './elements/indeterminate_checkbox'
|
||||
import AppTour from './elements/app_tour'
|
||||
import AppTourStart from './elements/app_tour_start'
|
||||
import DashboardDropzone from './elements/dashboard_dropzone'
|
||||
import RequiredCheckboxGroup from './elements/required_checkbox_group'
|
||||
import PageContainer from './elements/page_container'
|
||||
import EmailEditor from './elements/email_editor'
|
||||
import MountOnClick from './elements/mount_on_click'
|
||||
import RemoveOnEvent from './elements/remove_on_event'
|
||||
import ScrollTo from './elements/scroll_to'
|
||||
import SetValue from './elements/set_value'
|
||||
import ReviewForm from './elements/review_form'
|
||||
import ShowOnValue from './elements/show_on_value'
|
||||
import CustomValidation from './elements/custom_validation'
|
||||
import ToggleClasses from './elements/toggle_classes'
|
||||
import AutosizeField from './elements/autosize_field'
|
||||
import GoogleDriveFilePicker from './elements/google_drive_file_picker'
|
||||
import OpenModal from './elements/open_modal'
|
||||
|
||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||
|
||||
@@ -53,6 +67,9 @@ document.addEventListener('keyup', (e) => {
|
||||
})
|
||||
|
||||
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) => {
|
||||
const resp = event.detail?.formSubmission?.result?.fetchResponse?.response
|
||||
|
||||
@@ -95,6 +112,7 @@ safeRegisterElement('submit-form', SubmitForm)
|
||||
safeRegisterElement('prompt-password', PromptPassword)
|
||||
safeRegisterElement('emails-textarea', EmailsTextarea)
|
||||
safeRegisterElement('toggle-cookies', ToggleCookies)
|
||||
safeRegisterElement('toggle-submit', ToggleSubmit)
|
||||
safeRegisterElement('toggle-on-submit', ToggleOnSubmit)
|
||||
safeRegisterElement('password-input', PasswordInput)
|
||||
safeRegisterElement('search-input', SearchInput)
|
||||
@@ -105,10 +123,23 @@ safeRegisterElement('masked-input', MaskedInput)
|
||||
safeRegisterElement('set-date-button', SetDateButton)
|
||||
safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox)
|
||||
safeRegisterElement('app-tour', AppTour)
|
||||
safeRegisterElement('app-tour-start', AppTourStart)
|
||||
safeRegisterElement('dashboard-dropzone', DashboardDropzone)
|
||||
safeRegisterElement('check-on-click', CheckOnClick)
|
||||
safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup)
|
||||
safeRegisterElement('page-container', PageContainer)
|
||||
safeRegisterElement('email-editor', EmailEditor)
|
||||
safeRegisterElement('mount-on-click', MountOnClick)
|
||||
safeRegisterElement('remove-on-event', RemoveOnEvent)
|
||||
safeRegisterElement('scroll-to', ScrollTo)
|
||||
safeRegisterElement('set-value', SetValue)
|
||||
safeRegisterElement('review-form', ReviewForm)
|
||||
safeRegisterElement('show-on-value', ShowOnValue)
|
||||
safeRegisterElement('custom-validation', CustomValidation)
|
||||
safeRegisterElement('toggle-classes', ToggleClasses)
|
||||
safeRegisterElement('autosize-field', AutosizeField)
|
||||
safeRegisterElement('google-drive-file-picker', GoogleDriveFilePicker)
|
||||
safeRegisterElement('open-modal', OpenModal)
|
||||
|
||||
safeRegisterElement('template-builder', class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
@@ -125,6 +156,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
||||
withPhone: this.dataset.withPhone === 'true',
|
||||
withVerification: ['true', 'false'].includes(this.dataset.withVerification) ? this.dataset.withVerification === 'true' : null,
|
||||
withLogo: this.dataset.withLogo !== 'false',
|
||||
withFieldsDetection: this.dataset.withFieldsDetection === 'true',
|
||||
editable: this.dataset.editable !== 'false',
|
||||
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
||||
withPayment: this.dataset.withPayment === 'true',
|
||||
@@ -133,7 +165,9 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
||||
withSendButton: this.dataset.withSendButton !== 'false',
|
||||
withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false',
|
||||
withConditions: this.dataset.withConditions === 'true',
|
||||
withGoogleDrive: this.dataset.withGoogleDrive === 'true',
|
||||
withReplaceAndCloneUpload: true,
|
||||
withDownload: true,
|
||||
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
||||
acceptFileTypes: this.dataset.acceptFileTypes,
|
||||
showTourStartForm: this.dataset.showTourStartForm === 'true'
|
||||
|
||||
@@ -19,7 +19,7 @@ button .disabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button[disabled] .disabled {
|
||||
button[disabled] .disabled, button.btn-disabled .disabled {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ button .enabled {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
button[disabled] .enabled {
|
||||
button[disabled] .enabled, button.btn-disabled .enabled {
|
||||
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('')
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ export default targetable(class extends HTMLElement {
|
||||
static [target.static] = [
|
||||
'form',
|
||||
'fileDropzone',
|
||||
'folderDropzone',
|
||||
'fileDropzoneLoading'
|
||||
]
|
||||
|
||||
@@ -25,12 +26,13 @@ export default targetable(class extends HTMLElement {
|
||||
window.addEventListener('dragleave', this.onWindowDragleave)
|
||||
|
||||
this.fileDropzone?.addEventListener('drop', this.onDropFile)
|
||||
this.folderDropzone?.addEventListener('drop', this.onDropNewFolder)
|
||||
|
||||
this.folderCards.forEach((el) => el.addEventListener('drop', (e) => this.onDropFolder(e, el)))
|
||||
this.templateCards.forEach((el) => el.addEventListener('drop', this.onDropTemplate))
|
||||
this.templateCards.forEach((el) => el.addEventListener('dragstart', this.onTemplateDragStart))
|
||||
|
||||
return [this.fileDropzone, ...this.folderCards, ...this.templateCards].forEach((el) => {
|
||||
return [this.fileDropzone, this.folderDropzone, ...this.folderCards, ...this.templateCards].forEach((el) => {
|
||||
el?.addEventListener('dragover', this.onDragover)
|
||||
el?.addEventListener('dragleave', this.onDragleave)
|
||||
})
|
||||
@@ -46,6 +48,10 @@ export default targetable(class extends HTMLElement {
|
||||
onTemplateDragStart = (e) => {
|
||||
const id = e.target.href.split('/').pop()
|
||||
|
||||
this.folderCards.forEach((el) => el.classList.remove('bg-base-200', 'before:hidden'))
|
||||
this.folderDropzone?.classList?.remove('hidden')
|
||||
window.flash?.remove()
|
||||
|
||||
e.dataTransfer.effectAllowed = 'move'
|
||||
|
||||
if (id) {
|
||||
@@ -104,7 +110,7 @@ export default targetable(class extends HTMLElement {
|
||||
} else {
|
||||
const formData = new FormData()
|
||||
|
||||
formData.append('name', el.innerText.trim())
|
||||
formData.append('name', el.dataset.fullName)
|
||||
|
||||
fetch(`/templates/${templateId}/folder`, {
|
||||
method: 'PUT',
|
||||
@@ -176,6 +182,24 @@ export default targetable(class extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
onDropNewFolder (e) {
|
||||
e.preventDefault()
|
||||
|
||||
const templateId = e.dataTransfer.getData('template_id')
|
||||
|
||||
const a = document.createElement('a')
|
||||
|
||||
a.href = `/templates/${templateId}/folder/edit?autocomplete=false`
|
||||
a.dataset.turboFrame = 'modal'
|
||||
a.classList.add('hidden')
|
||||
|
||||
document.body.append(a)
|
||||
|
||||
a.click()
|
||||
|
||||
a.remove()
|
||||
}
|
||||
|
||||
onDragleave () {
|
||||
this.style.backgroundColor = null
|
||||
|
||||
@@ -199,6 +223,7 @@ export default targetable(class extends HTMLElement {
|
||||
|
||||
this.isDrag = true
|
||||
|
||||
window.flash?.remove()
|
||||
this.fileDropzone?.classList?.remove('hidden')
|
||||
|
||||
this.hiddenOnDrag.forEach((el) => { el.style.display = 'none' })
|
||||
@@ -212,6 +237,7 @@ export default targetable(class extends HTMLElement {
|
||||
this.isDrag = false
|
||||
|
||||
this.fileDropzone?.classList?.add('hidden')
|
||||
this.folderDropzone?.classList?.add('hidden')
|
||||
|
||||
this.hiddenOnDrag.forEach((el) => { el.style.display = null })
|
||||
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import { target, targetable } from '@github/catalyst/lib/targetable'
|
||||
|
||||
let loaderPromise = null
|
||||
|
||||
function loadCodeMirror () {
|
||||
if (!loaderPromise) {
|
||||
loaderPromise = Promise.all([
|
||||
import(/* webpackChunkName: "email-editor" */ '@codemirror/view'),
|
||||
import(/* webpackChunkName: "email-editor" */ '@codemirror/commands'),
|
||||
import(/* webpackChunkName: "email-editor" */ '@codemirror/language'),
|
||||
import(/* webpackChunkName: "email-editor" */ '@codemirror/lang-html'),
|
||||
import(/* webpackChunkName: "email-editor" */ '@specious/htmlflow')
|
||||
]).then(([view, commands, language, html, htmlflow]) => {
|
||||
return {
|
||||
minimalSetup: [
|
||||
commands.history(),
|
||||
language.syntaxHighlighting(language.defaultHighlightStyle, { fallback: true }),
|
||||
view.keymap.of([...commands.defaultKeymap, ...commands.historyKeymap])
|
||||
],
|
||||
EditorView: view.EditorView,
|
||||
html: html.html,
|
||||
htmlflow: htmlflow.default || htmlflow
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return loaderPromise
|
||||
}
|
||||
|
||||
export default targetable(class extends HTMLElement {
|
||||
static [target.static] = [
|
||||
'codeViewTab',
|
||||
'previewViewTab',
|
||||
'editorContainer',
|
||||
'previewIframe'
|
||||
]
|
||||
|
||||
connectedCallback () {
|
||||
this.mount()
|
||||
|
||||
if (this.input.value) {
|
||||
this.showPreviewView()
|
||||
} else {
|
||||
this.showCodeView()
|
||||
}
|
||||
|
||||
this.previewViewTab.addEventListener('click', this.showPreviewView)
|
||||
this.codeViewTab.addEventListener('click', this.showCodeView)
|
||||
}
|
||||
|
||||
showCodeView = () => {
|
||||
this.editorView.dispatch({
|
||||
changes: { from: 0, to: this.editorView.state.doc.length, insert: this.input.value }
|
||||
})
|
||||
|
||||
this.previewViewTab.classList.remove('tab-active', 'tab-bordered')
|
||||
this.previewViewTab.classList.add('pb-[3px]')
|
||||
this.codeViewTab.classList.remove('pb-[3px]')
|
||||
this.codeViewTab.classList.add('tab-active', 'tab-bordered')
|
||||
this.editorContainer.classList.remove('hidden')
|
||||
this.previewIframe.classList.add('hidden')
|
||||
}
|
||||
|
||||
showPreviewView = () => {
|
||||
this.previewIframe.srcdoc = this.input.value
|
||||
|
||||
this.codeViewTab.classList.remove('tab-active', 'tab-bordered')
|
||||
this.codeViewTab.classList.add('pb-[3px]')
|
||||
this.previewViewTab.classList.remove('pb-[3px]')
|
||||
this.previewViewTab.classList.add('tab-active', 'tab-bordered')
|
||||
this.editorContainer.classList.add('hidden')
|
||||
this.previewIframe.classList.remove('hidden')
|
||||
}
|
||||
|
||||
async mount () {
|
||||
this.input = this.querySelector('input[type="hidden"]')
|
||||
this.input.style.display = 'none'
|
||||
|
||||
const { EditorView, minimalSetup, html, htmlflow } = await loadCodeMirror()
|
||||
|
||||
this.editorView = new EditorView({
|
||||
doc: this.input.value,
|
||||
parent: this.editorContainer,
|
||||
extensions: [
|
||||
html(),
|
||||
minimalSetup,
|
||||
EditorView.lineWrapping,
|
||||
EditorView.updateListener.of(update => {
|
||||
if (update.docChanged) this.input.value = update.state.doc.toString()
|
||||
}),
|
||||
EditorView.theme({
|
||||
'&': {
|
||||
backgroundColor: 'white',
|
||||
color: 'black',
|
||||
fontSize: '14px',
|
||||
fontFamily: 'monospace'
|
||||
},
|
||||
'&.cm-focused': {
|
||||
outline: 'none'
|
||||
},
|
||||
'&.cm-editor': {
|
||||
borderRadius: '0.375rem',
|
||||
border: 'none'
|
||||
},
|
||||
'.cm-gutters': {
|
||||
display: 'none'
|
||||
}
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
this.previewIframe.srcdoc = this.editorView.state.doc.toString()
|
||||
|
||||
this.previewIframe.onload = () => {
|
||||
const previewIframeDoc = this.previewIframe.contentDocument
|
||||
|
||||
if (previewIframeDoc.body) {
|
||||
previewIframeDoc.body.contentEditable = true
|
||||
}
|
||||
|
||||
const contentDocument = this.previewIframe.contentDocument || this.previewIframe.contentWindow.document
|
||||
|
||||
contentDocument.body.addEventListener('input', async () => {
|
||||
const html = contentDocument.documentElement.outerHTML.replace(' contenteditable="true"', '')
|
||||
const prettifiedHtml = await htmlflow(html)
|
||||
|
||||
this.input.value = prettifiedHtml
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -2,6 +2,8 @@ import autocomplete from 'autocompleter'
|
||||
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
if (this.dataset.enabled === 'false') return
|
||||
|
||||
autocomplete({
|
||||
input: this.input,
|
||||
preventSubmit: this.dataset.submitOnSelect === 'true' ? 0 : 1,
|
||||
@@ -14,12 +16,16 @@ export default class extends HTMLElement {
|
||||
}
|
||||
|
||||
onSelect = (item) => {
|
||||
this.input.value = item.name
|
||||
this.input.value = this.dataset.parentName ? item.name : item.full_name
|
||||
}
|
||||
|
||||
fetch = (text, resolve) => {
|
||||
const queryParams = new URLSearchParams({ q: text })
|
||||
|
||||
if (this.dataset.parentName) {
|
||||
queryParams.append('parent_name', this.dataset.parentName)
|
||||
}
|
||||
|
||||
fetch('/template_folders_autocomplete?' + queryParams).then(async (resp) => {
|
||||
const items = await resp.json()
|
||||
|
||||
@@ -34,7 +40,7 @@ export default class extends HTMLElement {
|
||||
|
||||
div.setAttribute('dir', 'auto')
|
||||
|
||||
div.textContent = item.name
|
||||
div.textContent = this.dataset.parentName ? item.name : item.full_name
|
||||
|
||||
return div
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
const iframeTemplate = this.querySelector('template')
|
||||
|
||||
this.observer = new IntersectionObserver((entries) => {
|
||||
if (entries.some(e => e.isIntersecting)) {
|
||||
iframeTemplate.parentElement.prepend(iframeTemplate.content)
|
||||
|
||||
this.observer.disconnect()
|
||||
}
|
||||
})
|
||||
|
||||
this.observer.observe(this)
|
||||
|
||||
window.addEventListener('message', this.messageHandler)
|
||||
}
|
||||
|
||||
messageHandler = (event) => {
|
||||
if (event.data.type === 'google-drive-files-picked') {
|
||||
this.form.querySelectorAll('input[name="google_drive_file_ids[]"]').forEach(el => el.remove())
|
||||
|
||||
const files = event.data.files || []
|
||||
|
||||
files.forEach((file) => {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'hidden'
|
||||
input.name = 'google_drive_file_ids[]'
|
||||
input.value = file.id
|
||||
this.form.appendChild(input)
|
||||
})
|
||||
|
||||
this.form.querySelector('button[type="submit"]').click()
|
||||
this.loader.classList.remove('hidden')
|
||||
} else if (event.data.type === 'google-drive-picker-loaded') {
|
||||
this.loader.classList.add('hidden')
|
||||
this.form.classList.remove('hidden')
|
||||
} else if (event.data.type === 'google-drive-picker-request-oauth') {
|
||||
document.getElementById(this.dataset.oauthButtonId).classList.remove('hidden')
|
||||
this.classList.add('hidden')
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback () {
|
||||
this.observer?.unobserve(this)
|
||||
window.removeEventListener('message', this.messageHandler)
|
||||
}
|
||||
|
||||
get form () {
|
||||
return this.querySelector('form')
|
||||
}
|
||||
|
||||
get loader () {
|
||||
return document.getElementById('google_drive_loader')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.addEventListener('click', () => {
|
||||
document.body.append(this.template.content)
|
||||
})
|
||||
}
|
||||
|
||||
get template () {
|
||||
return document.getElementById(this.dataset.templateId)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
const src = this.getAttribute('src')
|
||||
const link = document.createElement('a')
|
||||
|
||||
link.href = src
|
||||
link.setAttribute('data-turbo-frame', 'modal')
|
||||
link.style.display = 'none'
|
||||
|
||||
this.appendChild(link)
|
||||
|
||||
link.click()
|
||||
|
||||
window.history.replaceState({}, document.title, window.location.pathname)
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.image.addEventListener('load', (e) => {
|
||||
this.image.setAttribute('width', e.target.naturalWidth)
|
||||
this.image.setAttribute('height', e.target.naturalHeight)
|
||||
const image = this.querySelector('img')
|
||||
|
||||
image.addEventListener('load', (e) => {
|
||||
image.setAttribute('width', e.target.naturalWidth)
|
||||
image.setAttribute('height', e.target.naturalHeight)
|
||||
|
||||
this.style.aspectRatio = `${e.target.naturalWidth} / ${e.target.naturalHeight}`
|
||||
})
|
||||
}
|
||||
|
||||
get image () {
|
||||
return this.querySelector('img')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
const eventType = this.dataset.on || 'click'
|
||||
const selector = document.getElementById(this.dataset.selectorId) || this
|
||||
const eventElement = eventType === 'submit' ? this.querySelector('form') : this
|
||||
|
||||
eventElement.addEventListener(eventType, (event) => {
|
||||
if (eventType === 'click') {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
selector.remove()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.querySelectorAll('input[type="radio"]').forEach(radio => {
|
||||
radio.addEventListener('change', (event) => {
|
||||
const rating = parseInt(event.target.value)
|
||||
|
||||
if (rating === 10) {
|
||||
window.review_comment.value = ''
|
||||
window.review_comment.classList.add('hidden')
|
||||
window.review_submit.classList.add('hidden')
|
||||
event.target.form.submit()
|
||||
} else {
|
||||
window.review_comment.classList.remove('hidden')
|
||||
window.review_submit.classList.remove('hidden')
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.selector = document.getElementById(this.dataset.selectorId)
|
||||
|
||||
this.addEventListener('click', () => {
|
||||
this.selector.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
history.replaceState(null, null, `#${this.dataset.selectorId}`)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,14 @@ export default class extends HTMLElement {
|
||||
this.input.classList.remove('w-60')
|
||||
}
|
||||
})
|
||||
|
||||
this.button.addEventListener('click', (event) => {
|
||||
if (!this.input.value && document.activeElement !== this.input) {
|
||||
event.preventDefault()
|
||||
|
||||
this.input.focus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
get input () {
|
||||
@@ -22,4 +30,8 @@ export default class extends HTMLElement {
|
||||
get 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 {
|
||||
connectedCallback () {
|
||||
const form = this.querySelector('form') || (this.querySelector('input, button, select') || this.lastElementChild).form
|
||||
|
||||
if (this.dataset.interval) {
|
||||
this.interval = setInterval(() => {
|
||||
this.querySelector('form').requestSubmit()
|
||||
form.requestSubmit()
|
||||
}, parseInt(this.dataset.interval))
|
||||
} else if (this.dataset.on) {
|
||||
this.lastElementChild.addEventListener(this.dataset.on, (event) => {
|
||||
if (this.dataset.disable === 'true') {
|
||||
form.querySelector('[type="submit"]')?.setAttribute('disabled', true)
|
||||
}
|
||||
|
||||
if (this.dataset.submitIfValue === 'true') {
|
||||
if (event.target.value) {
|
||||
form.requestSubmit()
|
||||
}
|
||||
} else {
|
||||
form.requestSubmit()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.querySelector('form').requestSubmit()
|
||||
form.requestSubmit()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
const button = this.querySelector('a, button')
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
this.dataset.classes.split(' ').forEach((cls) => {
|
||||
button.classList.toggle(cls)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,18 @@ export default actionable(class extends HTMLElement {
|
||||
trigger (event) {
|
||||
const elementIds = JSON.parse(this.dataset.elementIds)
|
||||
|
||||
elementIds.forEach((elementId) => {
|
||||
document.getElementById(elementId).classList.toggle('hidden', (event.target.dataset.toggleId || event.target.value) !== elementId)
|
||||
})
|
||||
if (event.target.type === 'checkbox') {
|
||||
elementIds.forEach((elementId) => {
|
||||
document.getElementById(elementId)?.classList.toggle('hidden')
|
||||
})
|
||||
} else {
|
||||
elementIds.forEach((elementId) => {
|
||||
document.getElementById(elementId).classList.toggle('hidden', (event.target.dataset.toggleId || event.target.value) !== elementId)
|
||||
})
|
||||
}
|
||||
|
||||
if (this.dataset.focusId) {
|
||||
document.getElementById(this.dataset.focusId)?.focus()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import ToggleSubmit from './elements/toggle_submit'
|
||||
import FetchForm from './elements/fetch_form'
|
||||
import ScrollButtons from './elements/scroll_buttons'
|
||||
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)
|
||||
|
||||
@@ -14,6 +15,7 @@ safeRegisterElement('toggle-submit', ToggleSubmit)
|
||||
safeRegisterElement('fetch-form', FetchForm)
|
||||
safeRegisterElement('scroll-buttons', ScrollButtons)
|
||||
safeRegisterElement('page-container', PageContainer)
|
||||
safeRegisterElement('submit-form', SubmitForm)
|
||||
safeRegisterElement('submission-form', class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.appElem = document.createElement('div')
|
||||
|
||||
@@ -19,7 +19,7 @@ button .disabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button[disabled] .disabled {
|
||||
button[disabled] .disabled, button.btn-disabled .disabled {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ button .enabled {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
button[disabled] .enabled {
|
||||
button[disabled] .enabled, button.btn-disabled .enabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ function mouseoverListener (event) {
|
||||
|
||||
const requestOptions = {
|
||||
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',
|
||||
redirect: 'follow'
|
||||
}
|
||||
|
||||
@@ -29,8 +29,15 @@
|
||||
{{ optionValue(option) }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ field.title || field.name || fieldNames[field.type] }}
|
||||
<template v-if="field.type === 'checkbox' && !field.name">
|
||||
<MarkdownContent
|
||||
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 }}
|
||||
</template>
|
||||
<template v-else-if="!field.required && field.type !== 'checkbox'">
|
||||
@@ -56,11 +63,11 @@
|
||||
<div
|
||||
v-else-if="field.type === 'signature' && signature"
|
||||
class="flex justify-between h-full gap-1 overflow-hidden w-full"
|
||||
:class="isNarrow && (withSignatureId || field.preferences?.reason_field_uuid) ? 'flex-row' : 'flex-col'"
|
||||
:class="isNarrow && (isShowSignatureId || field.preferences?.reason_field_uuid) ? 'flex-row' : 'flex-col'"
|
||||
>
|
||||
<div
|
||||
class="flex overflow-hidden"
|
||||
:class="isNarrow && (withSignatureId || field.preferences?.reason_field_uuid) ? 'w-1/2' : 'flex-grow'"
|
||||
:class="isNarrow && (isShowSignatureId || field.preferences?.reason_field_uuid) ? 'w-1/2' : 'flex-grow'"
|
||||
style="min-height: 50%"
|
||||
>
|
||||
<img
|
||||
@@ -69,7 +76,7 @@
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-if="withSignatureId || field.preferences?.reason_field_uuid"
|
||||
v-if="isShowSignatureId || field.preferences?.reason_field_uuid"
|
||||
class="text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem]"
|
||||
:class="isNarrow ? 'w-1/2' : 'w-full'"
|
||||
>
|
||||
@@ -212,6 +219,48 @@
|
||||
>
|
||||
{{ formatNumber(modelValue, field.preferences?.format) }}
|
||||
</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
|
||||
v-else
|
||||
class="whitespace-pre-wrap"
|
||||
@@ -222,12 +271,14 @@
|
||||
</template>
|
||||
|
||||
<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'
|
||||
|
||||
export default {
|
||||
name: 'FieldArea',
|
||||
components: {
|
||||
IconPaperclip,
|
||||
MarkdownContent,
|
||||
IconCheck
|
||||
},
|
||||
inject: ['t'],
|
||||
@@ -251,6 +302,16 @@ export default {
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
pageWidth: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0
|
||||
},
|
||||
pageHeight: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0
|
||||
},
|
||||
isValueSet: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -333,6 +394,20 @@ export default {
|
||||
verification: this.t('verify_id')
|
||||
}
|
||||
},
|
||||
strikethroughWidth () {
|
||||
if (this.isInlineSize) {
|
||||
return '0.6cqmin'
|
||||
} else {
|
||||
return 'clamp(0px, 0.5vw, 6px)'
|
||||
}
|
||||
},
|
||||
isShowSignatureId () {
|
||||
if ([true, false].includes(this.field.preferences?.with_signature_id)) {
|
||||
return this.field.preferences.with_signature_id
|
||||
} else {
|
||||
return this.withSignatureId
|
||||
}
|
||||
},
|
||||
alignClasses () {
|
||||
if (!this.field.preferences) {
|
||||
return { 'items-center': true }
|
||||
@@ -415,10 +490,14 @@ export default {
|
||||
},
|
||||
formattedDate () {
|
||||
if (this.field.type === 'date' && this.modelValue) {
|
||||
return this.formatDate(
|
||||
this.modelValue === '{{date}}' ? new Date() : new Date(this.modelValue),
|
||||
this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
|
||||
)
|
||||
try {
|
||||
return this.formatDate(
|
||||
this.modelValue === '{{date}}' ? new Date() : new Date(this.modelValue),
|
||||
this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
|
||||
)
|
||||
} catch {
|
||||
return this.modelValue
|
||||
}
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
@@ -460,9 +539,6 @@ export default {
|
||||
fontScale () {
|
||||
return 1000 / 612.0
|
||||
},
|
||||
ladscapeScale () {
|
||||
return 8.5 / 11.0
|
||||
},
|
||||
computedStyle () {
|
||||
const { x, y, w, h } = this.area
|
||||
|
||||
@@ -477,6 +553,10 @@ export default {
|
||||
style.color = this.field.preferences.color
|
||||
}
|
||||
|
||||
if (this.field.preferences?.background) {
|
||||
style.background = this.field.preferences.background
|
||||
}
|
||||
|
||||
return style
|
||||
},
|
||||
isNarrow () {
|
||||
|
||||
@@ -11,30 +11,37 @@
|
||||
v-for="(area, areaIndex) in field.areas"
|
||||
:key="areaIndex"
|
||||
>
|
||||
<Teleport
|
||||
v-if="findPageElementForArea(area)"
|
||||
:to="findPageElementForArea(area)"
|
||||
<template
|
||||
v-for="(pageElem, index) in [findPageElementForArea(area)]"
|
||||
:key="index"
|
||||
>
|
||||
<FieldArea
|
||||
:ref="setAreaRef"
|
||||
v-model="values[field.uuid]"
|
||||
:values="values"
|
||||
:field="field"
|
||||
:area="area"
|
||||
:submittable="submittable"
|
||||
:field-index="fieldIndex"
|
||||
:is-inline-size="isInlineSize"
|
||||
:scroll-padding="scrollPadding"
|
||||
:submitter="submitter"
|
||||
:with-field-placeholder="withFieldPlaceholder"
|
||||
:with-signature-id="withSignatureId"
|
||||
:is-active="currentStep === step"
|
||||
:with-label="withLabel && !withFieldPlaceholder && step.length < 2"
|
||||
:is-value-set="step.some((f) => f.uuid in values)"
|
||||
:attachments-index="attachmentsIndex"
|
||||
@click="[$emit('focus-step', stepIndex), maybeScrollOnClick(field, area)]"
|
||||
/>
|
||||
</Teleport>
|
||||
<Teleport
|
||||
v-if="pageElem"
|
||||
:to="pageElem"
|
||||
>
|
||||
<FieldArea
|
||||
:ref="setAreaRef"
|
||||
v-model="values[field.uuid]"
|
||||
:values="values"
|
||||
:field="field"
|
||||
:area="area"
|
||||
:submittable="submittable"
|
||||
:page-width="1400"
|
||||
:page-height="(1400.0 / pageElem.offsetWidth) * pageElem.offsetHeight"
|
||||
:field-index="fieldIndex"
|
||||
:is-inline-size="isInlineSize"
|
||||
:scroll-padding="scrollPadding"
|
||||
:submitter="submitter"
|
||||
:with-field-placeholder="withFieldPlaceholder"
|
||||
:with-signature-id="withSignatureId"
|
||||
:is-active="currentStep === step"
|
||||
:with-label="withLabel && !withFieldPlaceholder && step.length < 2"
|
||||
:is-value-set="step.some((f) => f.uuid in values)"
|
||||
:attachments-index="attachmentsIndex"
|
||||
@click="[$emit('focus-step', stepIndex), maybeScrollOnClick(field, area)]"
|
||||
/>
|
||||
</Teleport>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<div class="space-y-3 mt-5">
|
||||
<a
|
||||
v-if="completedButton.url"
|
||||
:href="sanitizeHref(completedButton.url)"
|
||||
:href="sanitizeUrl(completedButton.url)"
|
||||
rel="noopener noreferrer nofollow"
|
||||
class="white-button flex items-center w-full completed-form-completed-button"
|
||||
>
|
||||
@@ -102,6 +102,7 @@
|
||||
<script>
|
||||
import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop, IconLogin } from '@tabler/icons-vue'
|
||||
import MarkdownContent from './markdown_content'
|
||||
import { sanitizeUrl } from '@braintree/sanitize-url'
|
||||
|
||||
export default {
|
||||
name: 'FormCompleted',
|
||||
@@ -198,6 +199,7 @@ export default {
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
sanitizeUrl,
|
||||
sendCopyToEmail () {
|
||||
this.isSendingCopy = true
|
||||
|
||||
@@ -252,11 +254,6 @@ export default {
|
||||
this.isDownloading = false
|
||||
})
|
||||
},
|
||||
sanitizeHref (href) {
|
||||
if (href && href.trim().match(/^((?:https?:\/\/)|\/)/)) {
|
||||
return href.replace(/javascript:/g, '')
|
||||
}
|
||||
},
|
||||
downloadSafariIos (urls) {
|
||||
const fileRequests = urls.map((url) => {
|
||||
return fetch(url).then(async (resp) => {
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
</template>
|
||||
</label>
|
||||
<button
|
||||
v-if="withToday"
|
||||
class="btn btn-outline btn-sm !normal-case font-normal set-current-date-button"
|
||||
@click.prevent="[setCurrentDate(), $emit('focus')]"
|
||||
>
|
||||
@@ -46,6 +47,8 @@
|
||||
:id="field.uuid"
|
||||
ref="input"
|
||||
v-model="value"
|
||||
:min="validationMin"
|
||||
:max="validationMax"
|
||||
class="base-input !text-2xl text-center w-full"
|
||||
:required="field.required"
|
||||
type="date"
|
||||
@@ -89,6 +92,44 @@ export default {
|
||||
},
|
||||
emits: ['update:model-value', 'focus', 'submit'],
|
||||
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: {
|
||||
set (value) {
|
||||
this.$emit('update:model-value', value)
|
||||
|
||||
@@ -112,11 +112,23 @@ export default {
|
||||
onSelectFiles (e) {
|
||||
e.preventDefault()
|
||||
|
||||
this.uploadFiles(this.$refs.input.files).then(() => {
|
||||
if (this.$refs.input) {
|
||||
this.$refs.input.value = ''
|
||||
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) {
|
||||
this.$refs.input.value = ''
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
async uploadFiles (files) {
|
||||
this.isLoading = true
|
||||
@@ -151,12 +163,20 @@ export default {
|
||||
return fetch(this.baseUrl + '/api/attachments', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}).then(resp => resp.json()).then((data) => {
|
||||
return data
|
||||
}).then(async (resp) => {
|
||||
const data = await resp.json()
|
||||
|
||||
if (resp.status === 422) {
|
||||
alert(data.error)
|
||||
} else {
|
||||
return data
|
||||
}
|
||||
})
|
||||
}
|
||||
})).then((result) => {
|
||||
this.$emit('upload', result)
|
||||
if (result && result[0]) {
|
||||
this.$emit('upload', result)
|
||||
}
|
||||
}).finally(() => {
|
||||
this.isLoading = false
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<FormulaFieldAreas
|
||||
v-if="formulaFields.length"
|
||||
:fields="formulaFields"
|
||||
:readonly-values="readonlyConditionalFieldValues"
|
||||
:readonly-values="readonlyFieldValues"
|
||||
:values="values"
|
||||
/>
|
||||
<Teleport
|
||||
@@ -349,10 +349,10 @@
|
||||
:id="field.uuid"
|
||||
type="checkbox"
|
||||
class="base-checkbox !h-7 !w-7"
|
||||
:oninvalid="`this.setCustomValidity('${t('please_check_the_box_to_continue')}')`"
|
||||
:onchange="`this.setCustomValidity(validity.valueMissing ? '${t('please_check_the_box_to_continue')}' : '');`"
|
||||
:required="field.required"
|
||||
:checked="!!values[field.uuid]"
|
||||
@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]]"
|
||||
>
|
||||
<span
|
||||
@@ -454,7 +454,9 @@
|
||||
v-model="values[currentField.uuid]"
|
||||
:field="currentField"
|
||||
:submitter-slug="submitterSlug"
|
||||
:fields="formulaFields"
|
||||
:values="values"
|
||||
:readonly-values="readonlyFieldValues"
|
||||
@attached="attachments.push($event)"
|
||||
@focus="scrollIntoField(currentField)"
|
||||
@submit="!isSubmitting && submitStep()"
|
||||
@@ -572,6 +574,7 @@ import FormCompleted from './completed'
|
||||
import { IconInnerShadowTop, IconArrowsDiagonal, IconWritingSign, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||
import AppearsOn from './appears_on'
|
||||
import i18n from './i18n'
|
||||
import { sanitizeUrl } from '@braintree/sanitize-url'
|
||||
|
||||
const isEmpty = (obj) => {
|
||||
if (obj == null) return true
|
||||
@@ -871,7 +874,14 @@ export default {
|
||||
},
|
||||
readonlyConditionalFieldValues () {
|
||||
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
|
||||
}, {})
|
||||
@@ -971,7 +981,10 @@ export default {
|
||||
return this.currentStepFields[0]
|
||||
},
|
||||
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 () {
|
||||
const verificationFields = []
|
||||
@@ -1014,7 +1027,11 @@ export default {
|
||||
const aArea = (fieldAreasIndex[aField.uuid] ||= [...(aField.areas || [])].sort(sortArea)[0])
|
||||
const bArea = (fieldAreasIndex[bField.uuid] ||= [...(bField.areas || [])].sort(sortArea)[0])
|
||||
|
||||
return sortArea(aArea, bArea)
|
||||
if (aArea && bArea) {
|
||||
return sortArea(aArea, bArea)
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1476,7 +1493,7 @@ export default {
|
||||
}
|
||||
|
||||
if (this.completedRedirectUrl) {
|
||||
window.location.href = this.completedRedirectUrl
|
||||
window.location.href = sanitizeUrl(this.completedRedirectUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,13 @@ export default {
|
||||
computed: {
|
||||
isInlineSize () {
|
||||
return CSS.supports('container-type: size')
|
||||
},
|
||||
fieldsUuidIndex () {
|
||||
return this.fields.reduce((acc, field) => {
|
||||
acc[field.uuid] = field
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
},
|
||||
async mounted () {
|
||||
@@ -96,8 +103,19 @@ export default {
|
||||
findPageElementForArea (area) {
|
||||
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) {
|
||||
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
|
||||
})
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ const en = {
|
||||
type_text: 'Type text',
|
||||
email_has_been_sent: 'Email has been sent',
|
||||
processing: 'Processing',
|
||||
pay_with_strip: 'Pay with Stripe',
|
||||
pay_with_stripe: 'Pay with Stripe',
|
||||
reupload: 'Reupload',
|
||||
upload: 'Upload',
|
||||
files: 'Files',
|
||||
@@ -192,7 +192,7 @@ const es = {
|
||||
type_text: 'Escribir texto',
|
||||
email_has_been_sent: 'El correo electrónico ha sido enviado',
|
||||
processing: 'Procesando',
|
||||
pay_with_strip: 'Pagar con Stripe',
|
||||
pay_with_stripe: 'Pagar con Stripe',
|
||||
reupload: 'Volver a subir',
|
||||
upload: 'Subir',
|
||||
files: 'Archivos',
|
||||
@@ -222,7 +222,7 @@ const it = {
|
||||
value_is_invalid: 'Il valore non è valido',
|
||||
verification_code_is_invalid: 'Il codice di verifica non è valido',
|
||||
drawn_signature_on_a_touchscreen_device: 'Firma disegnata su un dispositivo con schermo tattile',
|
||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scansiona il codice QR con l\'app della fotocamera per aprire il modulo sul cellulare e disegnare la tua firma',
|
||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: "Scansiona il codice QR con l'app della fotocamera per aprire il modulo sul cellulare e disegnare la tua firma",
|
||||
by_clicking_you_agree_to_the: 'Cliccando su "{button}", accetti il',
|
||||
electronic_signature_disclosure: 'Divulgazione della Firma Elettronica',
|
||||
esignature_disclosure: 'Divulgazione della eFirma',
|
||||
@@ -293,7 +293,7 @@ const it = {
|
||||
toggle_multiline_text: 'Attiva Testo Multilinea',
|
||||
email_has_been_sent: "L'email è stata inviata",
|
||||
processing: 'Elaborazione',
|
||||
pay_with_strip: 'Paga con Stripe',
|
||||
pay_with_stripe: 'Paga con Stripe',
|
||||
reupload: 'Ricarica',
|
||||
upload: 'Carica',
|
||||
files: 'File',
|
||||
@@ -304,64 +304,64 @@ const it = {
|
||||
const de = {
|
||||
please_upload_an_image_file: 'Bitte laden Sie eine Bilddatei hoch',
|
||||
must_be_characters_length: 'Muss {number} Zeichen lang sein',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Vervollständigen Sie alle erforderlichen Felder, um mit der Identitätsverifizierung fortzufahren.',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Füllen Sie alle Pflichtfelder aus, um mit der Identitätsprüfung fortzufahren.',
|
||||
verify_id: 'ID überprüfen',
|
||||
identity_verification: 'Identitätsüberprüfung',
|
||||
identity_verification: 'Identitätsprüfung',
|
||||
complete: 'Abschließen',
|
||||
fill_all_required_fields_to_complete: 'Alle erforderlichen Felder ausfüllen, um abzuschließen',
|
||||
sign_and_complete: 'Signieren und Abschließen',
|
||||
fill_all_required_fields_to_complete: 'Füllen Sie alle Pflichtfelder aus, um abzuschließen',
|
||||
sign_and_complete: 'Unterzeichnen und abschließen',
|
||||
text: 'Text',
|
||||
by_clicking_you_agree_to_the: 'Durch Klicken auf "{button}" stimmen Sie dem',
|
||||
electronic_signature_disclosure: 'Hinweis zur E-Signatur',
|
||||
esignature_disclosure: 'Hinweis zur eSignatur',
|
||||
signature: 'Unterschrift',
|
||||
initials: 'Initialen',
|
||||
drawn_signature_on_a_touchscreen_device: 'Auf einem Touchscreen-Gerät gezeichnete Unterschrift',
|
||||
approved: 'Genehmigt',
|
||||
reviewed: 'Geprüft',
|
||||
other: 'Sonstiges',
|
||||
authored_by_me: 'Von mir erstellt',
|
||||
invite: 'Einladen',
|
||||
email: 'E-Mail',
|
||||
approved: 'Genehmigt',
|
||||
reviewed: 'Überprüft',
|
||||
other: 'Andere',
|
||||
authored_by_me: 'Von mir verfasst',
|
||||
approved_by: 'Genehmigt von',
|
||||
reviewed_by: 'Überprüft von',
|
||||
authored_by: 'Verfasst von',
|
||||
reviewed_by: 'Geprüft von',
|
||||
authored_by: 'Erstellt von',
|
||||
select_a_reason: 'Grund auswählen',
|
||||
value_is_invalid: 'Wert ist ungültig',
|
||||
verification_code_is_invalid: 'Bestätigungscode ist ungültig',
|
||||
drawn_signature_on_a_touchscreen_device: 'Gezeichnete Unterschrift auf einem Touchscreen-Gerät',
|
||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scannen Sie den QR-Code mit der Kamera-App, um das Formular auf dem Handy zu öffnen und Ihre Unterschrift zu zeichnen',
|
||||
by_clicking_you_agree_to_the: 'Durch Klicken auf "{button}" stimmen Sie zu, dass Sie die',
|
||||
electronic_signature_disclosure: 'Elektronische Unterschriftenoffenlegung',
|
||||
esignature_disclosure: 'eSignatur Offenlegung',
|
||||
already_paid: 'Bereits bezahlt',
|
||||
minimize: 'Minimieren',
|
||||
text: 'Text',
|
||||
signature: 'Unterschrift',
|
||||
digitally_signed_by: 'Digital unterschrieben von',
|
||||
reason: 'Grund',
|
||||
initials: 'Initialen',
|
||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scannen Sie den QR-Code mit der Kamera-App, um das Formular auf dem Mobilgerät zu öffnen und Ihre Unterschrift zu zeichnen',
|
||||
date: 'Datum',
|
||||
number: 'Nummer',
|
||||
number: 'Zahl',
|
||||
value_is_invalid: 'Wert ist ungültig',
|
||||
verification_code_is_invalid: 'Verifizierungscode ist ungültig',
|
||||
already_paid: 'Bereits bezahlt',
|
||||
image: 'Bild',
|
||||
pay: 'Bezahlen',
|
||||
take_photo: 'Foto aufnehmen',
|
||||
number_phone_is_invalid: 'Telefonnummer ({number}) ist ungültig',
|
||||
file: 'Datei',
|
||||
select: 'Auswählen',
|
||||
digitally_signed_by: 'Elektronisch signiert von',
|
||||
reason: 'Grund',
|
||||
select: 'Auswahl',
|
||||
checkbox: 'Checkbox',
|
||||
multiple: 'Mehrere',
|
||||
multiple: 'Mehrfach',
|
||||
radio: 'Radio',
|
||||
cells: 'Zellen',
|
||||
stamp: 'Stempel',
|
||||
minimize: 'Minimieren',
|
||||
payment: 'Zahlung',
|
||||
number_phone_is_invalid: '{number} Telefonnummer ist ungültig',
|
||||
phone: 'Telefon',
|
||||
start_now: 'Jetzt starten',
|
||||
continue: 'Fortsetzen',
|
||||
sign_now: 'Jetzt unterschreiben',
|
||||
continue: 'Weiter',
|
||||
sign_now: 'Jetzt unterzeichnen',
|
||||
type_here_: 'Hier eingeben...',
|
||||
optional: 'optional',
|
||||
option: 'Option',
|
||||
appears_on: 'Erscheint auf',
|
||||
page: 'Seite',
|
||||
take_photo: 'Foto aufnehmen',
|
||||
select_your_option: 'Wähle deine Option',
|
||||
complete_hightlighted_checkboxes_and_click: 'Markierte Kontrollkästchen ausfüllen und klicken',
|
||||
submit: 'absenden',
|
||||
next: 'weiter',
|
||||
click_to_upload: 'Klicken zum Hochladen',
|
||||
select_your_option: 'Option auswählen',
|
||||
complete_hightlighted_checkboxes_and_click: 'Füllen Sie die markierten Kontrollkästchen aus und klicken Sie auf',
|
||||
submit: 'Einreichen',
|
||||
next: 'Weiter',
|
||||
click_to_upload: 'Zum Hochladen klicken',
|
||||
or_drag_and_drop_files: 'oder Dateien hierher ziehen und ablegen',
|
||||
send_copy_via_email: 'Kopie per E-Mail senden',
|
||||
download: 'Herunterladen',
|
||||
@@ -370,137 +370,137 @@ const de = {
|
||||
draw_initials: 'Initialen zeichnen',
|
||||
type_signature_here: 'Unterschrift hier eingeben',
|
||||
type_initial_here: 'Initialen hier eingeben',
|
||||
form_has_been_completed: 'Formular wurde ausgefüllt!',
|
||||
document_has_been_signed: 'Dokument wurde unterschrieben!',
|
||||
documents_have_been_signed: 'Dokumente wurden unterschrieben!',
|
||||
form_has_been_completed: 'Formular wurde abgeschlossen!',
|
||||
document_has_been_signed: 'Dokument wurde unterzeichnet!',
|
||||
documents_have_been_signed: 'Dokumente wurden unterzeichnet!',
|
||||
create_a_free_account: 'Kostenloses Konto erstellen',
|
||||
powered_by: 'Bereitgestellt von',
|
||||
please_check_the_box_to_continue: 'Bitte setzen Sie das Häkchen, um fortzufahren.',
|
||||
please_check_the_box_to_continue: 'Bitte aktivieren Sie das Kontrollkästchen, um fortzufahren.',
|
||||
open_source_documents_software: 'Open-Source-Dokumentensoftware',
|
||||
verified_phone_number: 'Telefonnummer überprüfen',
|
||||
verified_phone_number: 'Telefonnummer verifizieren',
|
||||
use_international_format: 'Internationales Format verwenden: +1xxx',
|
||||
six_digits_code: '6-stelliger Code',
|
||||
change_phone_number: 'Telefonnummer ändern',
|
||||
sending: 'Senden...',
|
||||
resend_code: 'Code erneut senden',
|
||||
verification_code_has_been_resent: 'Die Verifizierungscode wurde erneut per SMS gesendet',
|
||||
please_fill_all_required_fields: 'Bitte füllen Sie alle erforderlichen Felder aus',
|
||||
set_today: 'Heute einstellen',
|
||||
verification_code_has_been_resent: 'Der Verifizierungscode wurde per SMS erneut gesendet',
|
||||
please_fill_all_required_fields: 'Bitte füllen Sie alle Pflichtfelder aus',
|
||||
set_today: 'Heute',
|
||||
toggle_multiline_text: 'Mehrzeiligen Text umschalten',
|
||||
draw_signature: 'Unterschrift zeichnen',
|
||||
type_initial: 'Initialen eingeben',
|
||||
draw: 'Zeichnen',
|
||||
type: 'Eingeben',
|
||||
type_text: 'Text eingeben',
|
||||
toggle_multiline_text: 'Mehrzeiligen Text umschalten',
|
||||
email_has_been_sent: 'Die E-Mail wurde gesendet',
|
||||
email_has_been_sent: 'E-Mail wurde gesendet',
|
||||
processing: 'Verarbeitung',
|
||||
pay_with_strip: 'Mit Stripe bezahlen',
|
||||
pay_with_stripe: 'Mit Stripe bezahlen',
|
||||
reupload: 'Erneut hochladen',
|
||||
upload: 'Hochladen',
|
||||
files: 'Dateien',
|
||||
signature_is_too_small_or_simple_please_redraw: 'Die Unterschrift ist zu klein oder zu einfach. Bitte erneut zeichnen.',
|
||||
wait_countdown_seconds: 'Warte {countdown} Sekunden'
|
||||
signature_is_too_small_or_simple_please_redraw: 'Die Unterschrift ist zu klein oder zu einfach. Bitte neu zeichnen.',
|
||||
wait_countdown_seconds: 'Bitte {countdown} Sekunden warten'
|
||||
}
|
||||
|
||||
const fr = {
|
||||
please_upload_an_image_file: 'Veuillez télécharger un fichier image',
|
||||
must_be_characters_length: 'Doit contenir {number} caractères',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour continuer la vérification de l'identité.",
|
||||
verify_id: "Vérification de l'ID",
|
||||
identity_verification: "Vérification de l'identité",
|
||||
please_upload_an_image_file: 'Veuillez téléverser un fichier image',
|
||||
must_be_characters_length: 'Doit comporter {number} caractères',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour poursuivre la vérification d'identité.",
|
||||
verify_id: "Vérifier l'ID",
|
||||
identity_verification: "Vérification d'identité",
|
||||
complete: 'Terminer',
|
||||
fill_all_required_fields_to_complete: 'Veuillez remplir tous les champs obligatoires pour compléter',
|
||||
sign_and_complete: 'Signer et Terminer',
|
||||
invite: 'Inviter',
|
||||
email: 'Courriel',
|
||||
fill_all_required_fields_to_complete: 'Remplissez tous les champs obligatoires pour terminer',
|
||||
sign_and_complete: 'Signer et terminer',
|
||||
text: 'Texte',
|
||||
by_clicking_you_agree_to_the: 'En cliquant sur "{button}", vous acceptez la',
|
||||
electronic_signature_disclosure: 'Déclaration relative à la signature électronique',
|
||||
esignature_disclosure: 'Déclaration eSignature',
|
||||
signature: 'Signature',
|
||||
initials: 'Initiales',
|
||||
drawn_signature_on_a_touchscreen_device: 'Signature dessinée sur un appareil à écran tactile',
|
||||
approved: 'Approuvé',
|
||||
reviewed: 'Révisé',
|
||||
other: 'Autre',
|
||||
authored_by_me: 'Rédigé par moi',
|
||||
invite: 'Inviter',
|
||||
email: 'E-mail',
|
||||
approved_by: 'Approuvé par',
|
||||
reviewed_by: 'Révisé par',
|
||||
authored_by: 'Rédigé par',
|
||||
select_a_reason: 'Sélectionnez une raison',
|
||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scannez le QR avec l’appareil photo pour ouvrir le formulaire et signer sur mobile',
|
||||
date: 'Date',
|
||||
number: 'Nombre',
|
||||
value_is_invalid: 'La valeur est invalide',
|
||||
verification_code_is_invalid: 'Le code de vérification est invalide',
|
||||
drawn_signature_on_a_touchscreen_device: 'Signature dessinée sur un appareil à écran tactile',
|
||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scannez le code QR avec l\'application de l\'appareil photo pour ouvrir le formulaire sur mobile et dessiner votre signature',
|
||||
by_clicking_you_agree_to_the: 'En cliquant sur "{button}", vous acceptez la',
|
||||
electronic_signature_disclosure: 'Divulgation de Signature Électronique',
|
||||
esignature_disclosure: 'Divulgation de la eSignature',
|
||||
minimize: 'Réduire',
|
||||
text: 'Texte',
|
||||
already_paid: 'Déjà payé',
|
||||
signature: 'Signature',
|
||||
initials: 'Initiales',
|
||||
digitally_signed_by: 'Signé numériquement par',
|
||||
reason: 'Raison',
|
||||
pay: 'Payer',
|
||||
date: 'Date',
|
||||
number: 'Numéro',
|
||||
image: 'Image',
|
||||
pay: 'Payer',
|
||||
take_photo: 'Prendre une photo',
|
||||
number_phone_is_invalid: "Le numéro de téléphone {number} n'est pas valide",
|
||||
file: 'Fichier',
|
||||
select: 'Choisir',
|
||||
digitally_signed_by: 'Signé électroniquement par',
|
||||
reason: 'Motif',
|
||||
select: 'Sélectionner',
|
||||
checkbox: 'Coche',
|
||||
multiple: 'Multiple',
|
||||
radio: 'Radio',
|
||||
cells: 'Cellules',
|
||||
stamp: 'Tampon',
|
||||
minimize: 'Réduire',
|
||||
payment: 'Paiement',
|
||||
number_phone_is_invalid: '{number} téléphone est invalide',
|
||||
phone: 'Téléphone',
|
||||
start_now: 'Commencer maintenant',
|
||||
continue: 'Continuer',
|
||||
sign_now: 'Signer maintenant',
|
||||
type_here_: 'Tapez ici...',
|
||||
type_here_: 'Saisissez ici...',
|
||||
optional: 'facultatif',
|
||||
option: 'Option',
|
||||
appears_on: 'Apparaît sur',
|
||||
page: 'Page',
|
||||
take_photo: 'Prendre une photo',
|
||||
select_your_option: 'Sélectionnez votre option',
|
||||
complete_hightlighted_checkboxes_and_click: 'Complétez les cases à cocher en surbrillance et cliquez',
|
||||
submit: 'envoyer',
|
||||
complete_hightlighted_checkboxes_and_click: 'Cochez les cases mises en évidence puis cliquez',
|
||||
submit: 'soumettre',
|
||||
next: 'suivant',
|
||||
click_to_upload: 'Cliquez pour télécharger',
|
||||
or_drag_and_drop_files: 'ou faites glisser-déposer les fichiers',
|
||||
click_to_upload: 'Cliquez pour téléverser',
|
||||
or_drag_and_drop_files: 'ou faites glisser et déposez des fichiers',
|
||||
send_copy_via_email: 'Envoyer une copie par e-mail',
|
||||
download: 'Télécharger',
|
||||
clear: 'Effacer',
|
||||
redraw: 'Redessiner',
|
||||
draw_initials: 'Dessiner les initiales',
|
||||
type_signature_here: 'Tapez la signature ici',
|
||||
type_initial_here: 'Tapez les initiales ici',
|
||||
form_has_been_completed: 'Le formulaire a été complété !',
|
||||
document_has_been_signed: 'Le document a été signé!',
|
||||
documents_have_been_signed: 'Les documents ont été signés!',
|
||||
create_a_free_account: 'Créer un Compte Gratuit',
|
||||
type_signature_here: 'Saisissez la signature ici',
|
||||
type_initial_here: 'Saisissez les initiales ici',
|
||||
form_has_been_completed: 'Le formulaire a été rempli !',
|
||||
document_has_been_signed: 'Le document a été signé !',
|
||||
documents_have_been_signed: 'Les documents ont été signés !',
|
||||
create_a_free_account: 'Créer un compte gratuit',
|
||||
powered_by: 'Propulsé par',
|
||||
please_check_the_box_to_continue: 'Veuillez cocher la case pour continuer.',
|
||||
open_source_documents_software: 'logiciel de documents open source',
|
||||
verified_phone_number: 'Vérifier le numéro de téléphone',
|
||||
use_international_format: 'Utiliser le format international : +1xxx',
|
||||
use_international_format: 'Utilisez le format international : +1xxx',
|
||||
six_digits_code: 'Code à 6 chiffres',
|
||||
change_phone_number: 'Changer le numéro de téléphone',
|
||||
sending: 'Envoi en cours...',
|
||||
change_phone_number: 'Changer de numéro de téléphone',
|
||||
sending: 'Envoi...',
|
||||
resend_code: 'Renvoyer le code',
|
||||
verification_code_has_been_resent: 'Le code de vérification a été renvoyé par SMS',
|
||||
please_fill_all_required_fields: 'Veuillez remplir tous les champs obligatoires',
|
||||
set_today: "Définir Aujourd'hui",
|
||||
draw_signature: 'Dessiner une signature',
|
||||
set_today: "Définir à aujourd'hui",
|
||||
toggle_multiline_text: 'Basculer le texte multiligne',
|
||||
draw_signature: 'Dessiner la signature',
|
||||
type_initial: 'Saisir les initiales',
|
||||
draw: 'Dessiner',
|
||||
type: 'Saisir',
|
||||
type_text: 'Saisir du texte',
|
||||
toggle_multiline_text: 'Basculer le Texte Multiligne',
|
||||
email_has_been_sent: "L'email a été envoyé",
|
||||
processing: 'Traitement',
|
||||
pay_with_strip: 'Paiement avec Stripe',
|
||||
reupload: 'Recharger',
|
||||
upload: 'Télécharger',
|
||||
email_has_been_sent: "L'e-mail a été envoyé",
|
||||
processing: 'Traitement en cours',
|
||||
pay_with_stripe: 'Payer avec Stripe',
|
||||
reupload: 'Retéléverser',
|
||||
upload: 'Téléverser',
|
||||
files: 'Fichiers',
|
||||
signature_is_too_small_or_simple_please_redraw: 'La signature est trop petite ou trop simple. Veuillez la redessiner.',
|
||||
wait_countdown_seconds: 'Attendez {countdown} secondes'
|
||||
wait_countdown_seconds: 'Veuillez patienter {countdown} secondes'
|
||||
}
|
||||
|
||||
const pl = {
|
||||
@@ -596,7 +596,7 @@ const pl = {
|
||||
toggle_multiline_text: 'Przełącz Tekst Wielolinijkowy',
|
||||
email_has_been_sent: 'E-mail został wysłany',
|
||||
processing: 'Przetwarzanie',
|
||||
pay_with_strip: 'Płatność za pomocą Stripe',
|
||||
pay_with_stripe: 'Płatność za pomocą Stripe',
|
||||
reupload: 'Ponowne przesłanie',
|
||||
upload: 'Przesyłanie',
|
||||
files: 'Pliki',
|
||||
@@ -798,7 +798,7 @@ const cs = {
|
||||
toggle_multiline_text: 'Přepnout Víceřádkový Text',
|
||||
email_has_been_sent: 'E-mail byl odeslán',
|
||||
processing: 'Zpracování',
|
||||
pay_with_strip: 'Zaplacení přes Stripe',
|
||||
pay_with_stripe: 'Zaplacení přes Stripe',
|
||||
reupload: 'Znovu nahrát',
|
||||
upload: 'Nahrát',
|
||||
files: 'Soubory',
|
||||
@@ -899,7 +899,7 @@ const pt = {
|
||||
toggle_multiline_text: 'Alternar Texto Multilinha',
|
||||
email_has_been_sent: 'Email enviado',
|
||||
processing: 'Processamento',
|
||||
pay_with_strip: 'Pagar com Stripe',
|
||||
pay_with_stripe: 'Pagar com Stripe',
|
||||
reupload: 'Reenviar',
|
||||
upload: 'Carregar',
|
||||
files: 'Arquivos',
|
||||
@@ -1000,7 +1000,7 @@ const he = {
|
||||
type_text: 'הקלד טקסט',
|
||||
email_has_been_sent: 'האימייל נשלח',
|
||||
processing: 'מעבד',
|
||||
pay_with_strip: 'שלם עם סטרייפ',
|
||||
pay_with_stripe: 'שלם עם סטרייפ',
|
||||
reupload: 'העלה שוב',
|
||||
upload: 'העלאה',
|
||||
files: 'קבצים',
|
||||
@@ -1101,7 +1101,7 @@ const nl = {
|
||||
type_text: 'Typ tekst',
|
||||
email_has_been_sent: 'E-mail is verzonden',
|
||||
processing: 'Verwerken',
|
||||
pay_with_strip: 'Betalen met Stripe',
|
||||
pay_with_stripe: 'Betalen met Stripe',
|
||||
reupload: 'Opnieuw uploaden',
|
||||
upload: 'Uploaden',
|
||||
files: 'Bestanden',
|
||||
@@ -1303,7 +1303,7 @@ const ko = {
|
||||
type_text: '텍스트 입력',
|
||||
email_has_been_sent: '이메일이 전송되었습니다',
|
||||
processing: '처리 중',
|
||||
pay_with_strip: '스트라이프로 결제',
|
||||
pay_with_stripe: '스트라이프로 결제',
|
||||
reupload: '다시 업로드',
|
||||
upload: '업로드',
|
||||
files: '파일',
|
||||
@@ -1404,7 +1404,7 @@ const ja = {
|
||||
type_text: 'テキストを入力',
|
||||
email_has_been_sent: 'メールが送信されました',
|
||||
processing: '処理中',
|
||||
pay_with_strip: 'Stripeで支払う',
|
||||
pay_with_stripe: 'Stripeで支払う',
|
||||
reupload: '再アップロード',
|
||||
upload: 'アップロード',
|
||||
files: 'ファイル',
|
||||
|
||||
@@ -123,6 +123,11 @@
|
||||
v-if="!isDrawInitials"
|
||||
class="absolute top-0 right-0 left-0 bottom-0"
|
||||
/>
|
||||
<label
|
||||
v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
|
||||
for="initials_text_input"
|
||||
class="absolute top-0 right-0 left-0 bottom-0"
|
||||
/>
|
||||
<canvas
|
||||
v-show="!modelValue && !computedPreviousValue"
|
||||
ref="canvas"
|
||||
|
||||
@@ -1,35 +1,32 @@
|
||||
<template>
|
||||
<span>
|
||||
<template
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
>
|
||||
<a
|
||||
v-if="item.startsWith('<a') && item.endsWith('</a>')"
|
||||
:href="sanitizeHref(extractAttr(item, 'href'))"
|
||||
rel="noopener noreferrer nofollow"
|
||||
:class="extractAttr(item, 'class') || 'link'"
|
||||
target="_blank"
|
||||
>
|
||||
{{ extractText(item) }}
|
||||
</a>
|
||||
<b
|
||||
v-else-if="item.startsWith('<b>') || item.startsWith('<strong>')"
|
||||
>
|
||||
{{ extractText(item) }}
|
||||
</b>
|
||||
<i
|
||||
v-else-if="item.startsWith('<i>') || item.startsWith('<em>')"
|
||||
>
|
||||
{{ extractText(item) }}
|
||||
</i>
|
||||
<br
|
||||
v-else-if="item === '<br>' || item === '\n'"
|
||||
>
|
||||
<span v-if="textOnly">
|
||||
{{ dom.body.textContent }}
|
||||
</span>
|
||||
<template v-else>
|
||||
<template
|
||||
v-else
|
||||
v-for="(item, index) in nodes || dom.body.childNodes"
|
||||
:key="index"
|
||||
>
|
||||
{{ item }}
|
||||
<a
|
||||
v-if="item.tagName === 'A' && item.getAttribute('href') !== 'undefined'"
|
||||
:href="sanitizeUrl(item.getAttribute('href'))"
|
||||
rel="noopener noreferrer nofollow"
|
||||
:class="item.getAttribute('class') || 'link'"
|
||||
target="_blank"
|
||||
>
|
||||
<MarkdownContent :nodes="item.childNodes" />
|
||||
</a>
|
||||
<component
|
||||
:is="item.tagName"
|
||||
v-else-if="safeTags.includes(item.tagName)"
|
||||
>
|
||||
<MarkdownContent :nodes="item.childNodes" />
|
||||
</component>
|
||||
<br v-else-if="item.tagName === 'BR' || item.nodeValue === '\n'">
|
||||
<template v-else>
|
||||
{{ item.textContent }}
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</span>
|
||||
@@ -37,8 +34,7 @@
|
||||
|
||||
<script>
|
||||
import snarkdown from 'snarkdown'
|
||||
|
||||
const htmlSplitRegexp = /(<a.+?<\/a>|<i>.+?<\/i>|<b>.+?<\/b>|<em>.+?<\/em>|<strong>.+?<\/strong>|<br>)/
|
||||
import { sanitizeUrl } from '@braintree/sanitize-url'
|
||||
|
||||
export default {
|
||||
name: 'MarkdownContent',
|
||||
@@ -47,10 +43,23 @@ export default {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
nodes: {
|
||||
type: [Array, Object],
|
||||
require: false,
|
||||
default: null
|
||||
},
|
||||
textOnly: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
items () {
|
||||
safeTags () {
|
||||
return ['UL', 'I', 'EM', 'B', 'STRONG', 'P']
|
||||
},
|
||||
dom () {
|
||||
const linkParts = this.string.split(/(https?:\/\/[^\s)]+)/g)
|
||||
|
||||
const text = linkParts.map((part, index) => {
|
||||
@@ -61,25 +70,11 @@ export default {
|
||||
}
|
||||
}).join('')
|
||||
|
||||
return snarkdown(text.replace(/\n/g, '<br>')).split(htmlSplitRegexp)
|
||||
return new DOMParser().parseFromString(snarkdown(text.replace(/\n/g, '<br>')), 'text/html')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
sanitizeHref (href) {
|
||||
if (href && href.trim().match(/^((?:https?:\/\/)|\/)/)) {
|
||||
return href.replace(/javascript:/g, '')
|
||||
}
|
||||
},
|
||||
extractAttr (text, attr) {
|
||||
if (text.includes(attr)) {
|
||||
return text.split(attr).pop().split('"')[1]
|
||||
}
|
||||
},
|
||||
extractText (text) {
|
||||
if (text) {
|
||||
return text.match(/>(.+?)</)?.[1]
|
||||
}
|
||||
}
|
||||
sanitizeUrl
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -39,8 +39,10 @@
|
||||
:id="field.uuid"
|
||||
v-model="number"
|
||||
type="number"
|
||||
:step="field.validation?.step || 'any'"
|
||||
:min="field.validation?.min"
|
||||
:max="field.validation?.max"
|
||||
class="base-input !text-2xl w-full"
|
||||
step="any"
|
||||
:required="field.required"
|
||||
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||
:name="`values[${field.uuid}]`"
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
width="22"
|
||||
/>
|
||||
<span>
|
||||
{{ t('pay_with_strip') }}
|
||||
{{ t('pay_with_stripe') }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -92,10 +92,20 @@ export default {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
readonlyValues: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({})
|
||||
},
|
||||
values: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
fields: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
submitterSlug: {
|
||||
type: String,
|
||||
required: true
|
||||
@@ -109,6 +119,13 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
fieldsUuidIndex () {
|
||||
return this.fields.reduce((acc, field) => {
|
||||
acc[field.uuid] = field
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
},
|
||||
queryParams () {
|
||||
return new URLSearchParams(window.location.search)
|
||||
},
|
||||
@@ -116,6 +133,10 @@ export default {
|
||||
return this.queryParams.get('stripe_session_id')
|
||||
},
|
||||
defaultName () {
|
||||
if (this.field.preferences?.price_id || this.field.preferences?.payment_link_id) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const { price, currency } = this.field.preferences || {}
|
||||
|
||||
const formatter = new Intl.NumberFormat([], {
|
||||
@@ -178,12 +199,23 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
calculateFormula () {
|
||||
const transformedFormula = this.field.preferences.formula.replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||
return this.values[uuid] || 0.0
|
||||
const transformedFormula = this.normalizeFormula(this.field.preferences.formula).replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||
return this.readonlyValues[uuid] || this.values[uuid] || 0.0
|
||||
})
|
||||
|
||||
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 () {
|
||||
if (this.sessionId) {
|
||||
return fetch(this.baseUrl + '/api/stripe_payments/' + this.sessionId, {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</label>
|
||||
<div class="space-x-2 flex flex-none">
|
||||
<span
|
||||
v-if="isTextSignature && format !== 'typed' && format !== 'upload'"
|
||||
v-if="isTextSignature && format !== 'typed_or_upload' && format !== 'typed' && format !== 'upload'"
|
||||
class="tooltip"
|
||||
:data-tip="t('draw_signature')"
|
||||
>
|
||||
@@ -38,7 +38,7 @@
|
||||
</a>
|
||||
</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="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
|
||||
:data-tip="t('type_text')"
|
||||
@@ -85,8 +85,8 @@
|
||||
{{ t(format === 'upload' ? 'reupload' : 'redraw') }}
|
||||
</a>
|
||||
<span
|
||||
v-if="withQrButton && !modelValue && !computedPreviousValue && format !== 'typed' && format !== 'upload'"
|
||||
class=" tooltip"
|
||||
v-if="withQrButton && !modelValue && !computedPreviousValue && format !== 'typed_or_upload' && format !== 'typed' && format !== 'upload'"
|
||||
class="tooltip before:translate-x-[-90%]"
|
||||
:data-tip="t('drawn_signature_on_a_touchscreen_device')"
|
||||
>
|
||||
<a
|
||||
@@ -211,7 +211,7 @@
|
||||
@input="updateWrittenSignature"
|
||||
>
|
||||
<select
|
||||
v-if="requireSigningReason && !isOtherReason"
|
||||
v-if="withSigningReason && !isOtherReason"
|
||||
class="select base-input !text-2xl w-full mt-6 text-center"
|
||||
:class="{ 'text-gray-300': !reason }"
|
||||
required
|
||||
@@ -226,24 +226,37 @@
|
||||
>
|
||||
{{ t('select_a_reason') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="(label, option) in defaultReasons"
|
||||
:key="option"
|
||||
:value="option"
|
||||
:selected="reason === option"
|
||||
class="text-base-content"
|
||||
>
|
||||
{{ label }}
|
||||
</option>
|
||||
<option
|
||||
value="other"
|
||||
class="text-base-content"
|
||||
>
|
||||
{{ t('other') }}
|
||||
</option>
|
||||
<template v-if="field.preferences?.reasons">
|
||||
<option
|
||||
v-for="option in field.preferences.reasons"
|
||||
:key="option"
|
||||
:value="option"
|
||||
:selected="reason === option"
|
||||
class="text-base-content"
|
||||
>
|
||||
{{ option }}
|
||||
</option>
|
||||
</template>
|
||||
<template v-else>
|
||||
<option
|
||||
v-for="(label, option) in defaultReasons"
|
||||
:key="option"
|
||||
:value="option"
|
||||
:selected="reason === option"
|
||||
class="text-base-content"
|
||||
>
|
||||
{{ label }}
|
||||
</option>
|
||||
<option
|
||||
value="other"
|
||||
class="text-base-content"
|
||||
>
|
||||
{{ t('other') }}
|
||||
</option>
|
||||
</template>
|
||||
</select>
|
||||
<input
|
||||
v-if="requireSigningReason && isOtherReason"
|
||||
v-if="withSigningReason && isOtherReason"
|
||||
class="base-input !text-2xl w-full mt-6"
|
||||
required
|
||||
:name="`values[${field.preferences.reason_field_uuid}]`"
|
||||
@@ -253,7 +266,7 @@
|
||||
@input="$emit('update:reason', $event.target.value)"
|
||||
>
|
||||
<input
|
||||
v-if="requireSigningReason"
|
||||
v-if="withSigningReason"
|
||||
hidden
|
||||
name="with_reason"
|
||||
:value="field.preferences.reason_field_uuid"
|
||||
@@ -395,7 +408,7 @@ export default {
|
||||
isShowQr: false,
|
||||
isOtherReason: false,
|
||||
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()
|
||||
}
|
||||
},
|
||||
@@ -406,6 +419,9 @@ export default {
|
||||
format () {
|
||||
return this.field.preferences?.format
|
||||
},
|
||||
withSigningReason () {
|
||||
return this.requireSigningReason || this.field.preferences?.reasons?.length
|
||||
},
|
||||
defaultReasons () {
|
||||
return {
|
||||
[this.t('approved_by')]: this.t('approved'),
|
||||
@@ -424,10 +440,11 @@ export default {
|
||||
created () {
|
||||
this.isSignatureStarted = !!this.computedPreviousValue
|
||||
|
||||
if (this.requireSigningReason) {
|
||||
if (this.withSigningReason) {
|
||||
this.field.preferences ||= {}
|
||||
this.field.preferences.reason_field_uuid ||= v4()
|
||||
this.isOtherReason = this.reason && !this.defaultReasons[this.reason]
|
||||
this.isOtherReason = this.reason && !this.defaultReasons[this.reason] &&
|
||||
(!this.field.preferences?.reasons?.length || !this.field.preferences.reasons.includes(this.reason))
|
||||
}
|
||||
},
|
||||
async mounted () {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<MarkdownContent :string="field.description" />
|
||||
</div>
|
||||
<div
|
||||
v-if="emptyValueRequiredStep && emptyValueRequiredStep[0] !== field"
|
||||
v-if="isRequiredFieldEmpty"
|
||||
class="px-1 field-description-text"
|
||||
>
|
||||
{{ t('complete_all_required_fields_to_proceed_with_identity_verification') }}
|
||||
@@ -100,6 +100,9 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isRequiredFieldEmpty () {
|
||||
return this.emptyValueRequiredStep && this.emptyValueRequiredStep[0] !== this.field
|
||||
},
|
||||
countryCode () {
|
||||
const browserTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
const browserTz = browserTimeZone.split('/')[1]
|
||||
@@ -131,17 +134,19 @@ export default {
|
||||
}
|
||||
},
|
||||
async mounted () {
|
||||
this.isLoading = true
|
||||
if (!this.isRequiredFieldEmpty) {
|
||||
this.isLoading = true
|
||||
|
||||
if (new URLSearchParams(window.location.search).get('submit') === 'true') {
|
||||
this.$emit('submit')
|
||||
} else {
|
||||
Promise.all([
|
||||
import('@eid-easy/eideasy-widget'),
|
||||
this.start()
|
||||
]).finally(() => {
|
||||
this.isLoading = false
|
||||
})
|
||||
if (new URLSearchParams(window.location.search).get('submit') === 'true') {
|
||||
this.$emit('submit')
|
||||
} else {
|
||||
Promise.all([
|
||||
import('@eid-easy/eideasy-widget'),
|
||||
this.start()
|
||||
]).finally(() => {
|
||||
this.isLoading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -149,12 +154,17 @@ export default {
|
||||
return fetch(this.baseUrl + `/api/identity_verification/${this.field.uuid}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
submitter_slug: this.submitterSlug
|
||||
submitter_slug: this.submitterSlug,
|
||||
redirect_url: document.location.href
|
||||
}),
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}).then(async (resp) => {
|
||||
this.eidEasyData = await resp.json()
|
||||
|
||||
if (this.eidEasyData.check_completed) {
|
||||
this.$emit('submit')
|
||||
}
|
||||
|
||||
if (this.eidEasyData.available_methods[0] === 'itsme-qes-signature' &&
|
||||
this.eidEasyData.available_methods.length === 1) {
|
||||
const redirectUrl = new URL('https://id.eideasy.com/sign_contract_external')
|
||||
@@ -165,7 +175,7 @@ export default {
|
||||
redirectUrl.searchParams.append('lang', this.locale)
|
||||
|
||||
this.redirectUrl = redirectUrl.toString()
|
||||
} else {
|
||||
} else if (this.$refs.widgetContainer) {
|
||||
const eidEasyWidget = document.createElement('eideasy-widget')
|
||||
|
||||
for (const key in this.widgetSettings) {
|
||||
@@ -178,15 +188,23 @@ export default {
|
||||
})
|
||||
},
|
||||
async submit () {
|
||||
return fetch(this.baseUrl + '/api/identity_verification', {
|
||||
const resp = await fetch(this.baseUrl + '/api/identity_verification', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
submitter_slug: this.submitterSlug
|
||||
}),
|
||||
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
|
||||
v-if="isSelected || isDraw"
|
||||
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
|
||||
v-if="field.type === 'cells' && (isSelected || isDraw)"
|
||||
@@ -39,7 +39,7 @@
|
||||
@pointerdown.stop
|
||||
>
|
||||
<FieldSubmitter
|
||||
v-if="field.type != 'heading'"
|
||||
v-if="field.type != 'heading' && field.type != 'strikethrough'"
|
||||
v-model="field.submitter_uuid"
|
||||
class="border-r roles-dropdown"
|
||||
:compact="true"
|
||||
@@ -72,11 +72,11 @@
|
||||
@blur="onNameBlur"
|
||||
>{{ optionIndexText }} {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || defaultName }}</span>
|
||||
<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"
|
||||
>
|
||||
<input
|
||||
v-if="!isValueInput"
|
||||
v-if="!isValueInput && !isSelectInput"
|
||||
:id="`required-checkbox-${field.uuid}`"
|
||||
v-model="field.required"
|
||||
type="checkbox"
|
||||
@@ -84,14 +84,14 @@
|
||||
@mousedown.prevent
|
||||
>
|
||||
<label
|
||||
v-if="!isValueInput"
|
||||
v-if="!isValueInput && !isSelectInput"
|
||||
:for="`required-checkbox-${field.uuid}`"
|
||||
class="label text-xs"
|
||||
@click.prevent="field.required = !field.required"
|
||||
@mousedown.prevent
|
||||
>{{ t('required') }}</label>
|
||||
<input
|
||||
v-if="isValueInput"
|
||||
v-if="isValueInput || isSelectInput"
|
||||
:id="`readonly-checkbox-${field.uuid}`"
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-xs no-animation rounded"
|
||||
@@ -100,7 +100,7 @@
|
||||
@mousedown.prevent
|
||||
>
|
||||
<label
|
||||
v-if="isValueInput"
|
||||
v-if="isValueInput || isSelectInput"
|
||||
:for="`readonly-checkbox-${field.uuid}`"
|
||||
class="label text-xs"
|
||||
@click.prevent="field.readonly = !(field.readonly ?? true)"
|
||||
@@ -140,6 +140,8 @@
|
||||
:background-color="'white'"
|
||||
:with-required="false"
|
||||
:with-areas="false"
|
||||
:with-signature-id="withSignatureId"
|
||||
:with-prefillable="withPrefillable"
|
||||
@click-formula="isShowFormulaModal = true"
|
||||
@click-font="isShowFontModal = true"
|
||||
@click-description="isShowDescriptionModal = true"
|
||||
@@ -162,30 +164,81 @@
|
||||
ref="touchValueTarget"
|
||||
class="flex h-full w-full field-area"
|
||||
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"
|
||||
>
|
||||
<span
|
||||
v-if="field"
|
||||
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
|
||||
v-if="isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas && field.type !== 'checkbox')"
|
||||
:class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type) }"
|
||||
v-if="field.type === 'strikethrough'"
|
||||
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"
|
||||
>
|
||||
<div
|
||||
ref="textContainer"
|
||||
class="flex items-center px-0.5"
|
||||
:style="{ color: field.preferences?.color }"
|
||||
:class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type) }"
|
||||
:class="{ 'w-full h-full': isWFullType }"
|
||||
>
|
||||
<IconCheck
|
||||
v-if="field.type == 'checkbox'"
|
||||
class="aspect-square mx-auto"
|
||||
: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
|
||||
v-else-if="field.type === 'number' && !isValueInput && (field.default_value || field.default_value == 0)"
|
||||
class="whitespace-pre-wrap"
|
||||
@@ -208,14 +261,39 @@
|
||||
{{ char }}
|
||||
</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
|
||||
v-else
|
||||
ref="defaultValue"
|
||||
: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 }"
|
||||
: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"
|
||||
@focus="selectedAreaRef.value = area"
|
||||
@paste.prevent="onPaste"
|
||||
@keydown.enter="onDefaultValueEnter"
|
||||
>{{ field.default_value }}</span>
|
||||
@@ -223,7 +301,7 @@
|
||||
</div>
|
||||
<component
|
||||
:is="fieldIcons[field.type]"
|
||||
v-else
|
||||
v-else-if="!isCheckboxInput"
|
||||
width="100%"
|
||||
height="100%"
|
||||
class="max-h-10 opacity-50"
|
||||
@@ -231,12 +309,12 @@
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="!isValueInput"
|
||||
v-if="!isValueInput && !isSelectInput"
|
||||
ref="touchTarget"
|
||||
class="absolute top-0 bottom-0 right-0 left-0"
|
||||
:class="isDragged ? 'cursor-grab' : 'cursor-pointer'"
|
||||
@dblclick="maybeToggleDefaultValue"
|
||||
@click="maybeToggleCheckboxValue"
|
||||
@click="inputMode && maybeToggleCheckboxValue()"
|
||||
/>
|
||||
<span
|
||||
v-if="field?.type && editable"
|
||||
@@ -251,6 +329,7 @@
|
||||
<FormulaModal
|
||||
:field="field"
|
||||
:editable="editable && !defaultField"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@close="isShowFormulaModal = false"
|
||||
/>
|
||||
@@ -262,6 +341,7 @@
|
||||
<FontModal
|
||||
:field="field"
|
||||
:editable="editable && !defaultField"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@close="isShowFontModal = false"
|
||||
/>
|
||||
@@ -273,6 +353,7 @@
|
||||
<ConditionsModal
|
||||
:item="field"
|
||||
:build-default-name="buildDefaultName"
|
||||
:default-field="defaultField"
|
||||
@close="isShowConditionsModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
@@ -283,6 +364,7 @@
|
||||
<DescriptionModal
|
||||
:field="field"
|
||||
:editable="editable && !defaultField"
|
||||
:default-field="defaultField"
|
||||
:build-default-name="buildDefaultName"
|
||||
@close="isShowDescriptionModal = false"
|
||||
/>
|
||||
@@ -347,6 +429,26 @@ export default {
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
withSignatureId: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
withPrefillable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
pageWidth: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0
|
||||
},
|
||||
pageHeight: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0
|
||||
},
|
||||
defaultSubmitters: {
|
||||
type: Array,
|
||||
required: false,
|
||||
@@ -386,6 +488,34 @@ export default {
|
||||
fieldNames: FieldType.computed.fieldNames,
|
||||
fieldLabels: FieldType.computed.fieldLabels,
|
||||
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 () {
|
||||
let fontSize = ''
|
||||
|
||||
@@ -405,27 +535,37 @@ export default {
|
||||
|
||||
return { fontSize, lineHeight: `calc(${fontSize} * ${this.lineHeight})` }
|
||||
},
|
||||
optionsUuidIndex () {
|
||||
return this.field.options.reduce((acc, option) => {
|
||||
acc[option.uuid] = option
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
},
|
||||
fontSizePx () {
|
||||
return parseInt(this.field?.preferences?.font_size || 11) * this.fontScale
|
||||
},
|
||||
lineHeight () {
|
||||
return 1.3
|
||||
},
|
||||
fontScale () {
|
||||
return 1040 / 612.0
|
||||
basePageWidth () {
|
||||
return 1040.0
|
||||
},
|
||||
ladscapeScale () {
|
||||
return 8.5 / 11.0
|
||||
fontScale () {
|
||||
return this.basePageWidth / 612.0
|
||||
},
|
||||
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 () {
|
||||
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 () {
|
||||
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
||||
@@ -540,6 +680,11 @@ export default {
|
||||
closeDropdown () {
|
||||
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 () {
|
||||
if (!this.editable) {
|
||||
return
|
||||
@@ -549,22 +694,41 @@ export default {
|
||||
this.isContenteditable = true
|
||||
|
||||
this.focusValueInput()
|
||||
} else if (this.field.type === 'checkbox') {
|
||||
this.field.readonly = !this.field.readonly
|
||||
this.field.default_value === true ? delete this.field.default_value : this.field.default_value = true
|
||||
|
||||
this.save()
|
||||
} else if (this.field.type === 'date') {
|
||||
this.field.readonly = !this.field.readonly
|
||||
this.field.default_value === '{{date}}' ? delete this.field.default_value : this.field.default_value = '{{date}}'
|
||||
|
||||
this.save()
|
||||
} else {
|
||||
this.maybeToggleCheckboxValue()
|
||||
}
|
||||
},
|
||||
maybeToggleCheckboxValue () {
|
||||
if (this.inputMode && this.field.type === 'checkbox') {
|
||||
this.field.readonly = !this.field.readonly
|
||||
if (this.field.type === 'checkbox') {
|
||||
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()
|
||||
}
|
||||
@@ -658,10 +822,15 @@ export default {
|
||||
delete this.field.options
|
||||
}
|
||||
|
||||
if (['heading'].includes(this.field.type)) {
|
||||
if (this.field.type === 'heading') {
|
||||
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)) {
|
||||
this.field.options ||= [{ value: '', uuid: v4() }]
|
||||
}
|
||||
@@ -740,7 +909,7 @@ export default {
|
||||
}
|
||||
},
|
||||
drag (e) {
|
||||
if (e.target.id === 'mask') {
|
||||
if (e.target.id === 'mask' && this.editable) {
|
||||
this.isDragged = true
|
||||
|
||||
this.area.x = (e.offsetX - this.dragFrom.x) / e.target.clientWidth
|
||||
@@ -756,7 +925,9 @@ export default {
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
this.isDragged = true
|
||||
if (this.editable) {
|
||||
this.isDragged = true
|
||||
}
|
||||
|
||||
const rect = e.target.getBoundingClientRect()
|
||||
|
||||
@@ -809,7 +980,9 @@ export default {
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
this.isDragged = true
|
||||
if (this.editable) {
|
||||
this.isDragged = true
|
||||
}
|
||||
|
||||
const rect = e.target.getBoundingClientRect()
|
||||
|
||||
|
||||
@@ -175,7 +175,10 @@
|
||||
{{ t('save') }}
|
||||
</span>
|
||||
</button>
|
||||
<div class="dropdown dropdown-end">
|
||||
<div
|
||||
class="dropdown dropdown-end"
|
||||
:class="{ 'dropdown-open': isDownloading }"
|
||||
>
|
||||
<label
|
||||
tabindex="0"
|
||||
class="base-button !rounded-l-none !pl-1 !pr-2 !border-l-neutral-500"
|
||||
@@ -209,6 +212,30 @@
|
||||
<span class="whitespace-nowrap">{{ t('preferences') }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="withDownload">
|
||||
<button
|
||||
class="flex space-x-2"
|
||||
:disabled="isDownloading"
|
||||
@click.stop.prevent="download"
|
||||
>
|
||||
<IconInnerShadowTop
|
||||
v-if="isDownloading"
|
||||
class="animate-spin w-6 h-6 flex-shrink-0"
|
||||
/>
|
||||
<IconDownload
|
||||
v-else
|
||||
class="w-6 h-6 flex-shrink-0"
|
||||
/>
|
||||
<span
|
||||
v-if="isDownloading"
|
||||
class="whitespace-nowrap"
|
||||
>{{ t('downloading_') }}</span>
|
||||
<span
|
||||
v-else
|
||||
class="whitespace-nowrap"
|
||||
>{{ t('download') }}</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</span>
|
||||
@@ -260,8 +287,12 @@
|
||||
:style="{ backgroundColor }"
|
||||
>
|
||||
<Upload
|
||||
v-if="sortedDocuments.length && editable && withUploadButton"
|
||||
v-if="editable && withUploadButton"
|
||||
v-show="sortedDocuments.length"
|
||||
ref="upload"
|
||||
:accept-file-types="acceptFileTypes"
|
||||
:authenticity-token="authenticityToken"
|
||||
:with-google-drive="withGoogleDrive"
|
||||
:template-id="template.id"
|
||||
@success="updateFromUpload"
|
||||
/>
|
||||
@@ -297,6 +328,8 @@
|
||||
v-if="withUploadButton"
|
||||
:template-id="template.id"
|
||||
:accept-file-types="acceptFileTypes"
|
||||
:with-google-drive="withGoogleDrive"
|
||||
@click-google-drive="$refs.upload.openGoogleDriveModal()"
|
||||
@success="updateFromUpload"
|
||||
/>
|
||||
<button
|
||||
@@ -330,6 +363,8 @@
|
||||
:input-mode="inputMode"
|
||||
:default-fields="[...defaultRequiredFields, ...defaultFields]"
|
||||
:allow-draw="!onlyDefinedFields || drawField"
|
||||
:with-signature-id="withSignatureId"
|
||||
:with-prefillable="withPrefillable"
|
||||
:data-document-uuid="document.uuid"
|
||||
:default-submitters="defaultSubmitters"
|
||||
:drag-field-placeholder="fieldsDragFieldRef.value || dragField"
|
||||
@@ -366,6 +401,8 @@
|
||||
v-if="withUploadButton"
|
||||
:template-id="template.id"
|
||||
:accept-file-types="acceptFileTypes"
|
||||
:authenticity-token="authenticityToken"
|
||||
:with-google-drive="withGoogleDrive"
|
||||
@success="updateFromUpload"
|
||||
/>
|
||||
<button
|
||||
@@ -400,7 +437,10 @@
|
||||
:style="{ backgroundColor }"
|
||||
>
|
||||
<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') }}
|
||||
</p>
|
||||
<div>
|
||||
@@ -411,7 +451,7 @@
|
||||
{{ t('cancel') }}
|
||||
</button>
|
||||
<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="#"
|
||||
class="link block mt-3 text-sm"
|
||||
@click.prevent="[addField(drawFieldType), drawField = null, drawOption = null, withSelectedFieldType ? '' : drawFieldType = '', showDrawField = false]"
|
||||
@@ -436,11 +476,15 @@
|
||||
:default-required-fields="defaultRequiredFields"
|
||||
:field-types="fieldTypes"
|
||||
:with-sticky-submitters="withStickySubmitters"
|
||||
:with-fields-detection="withFieldsDetection"
|
||||
:with-signature-id="withSignatureId"
|
||||
:with-prefillable="withPrefillable"
|
||||
:only-defined-fields="onlyDefinedFields"
|
||||
:editable="editable"
|
||||
:show-tour-start-form="showTourStartForm"
|
||||
@add-field="addField"
|
||||
@set-draw="[drawField = $event.field, drawOption = $event.option]"
|
||||
@select-submitter="selectedSubmitter = $event"
|
||||
@set-draw-type="[drawFieldType = $event, showDrawField = true]"
|
||||
@set-drag="dragField = $event"
|
||||
@set-drag-placeholder="$refs.dragPlaceholder.dragPlaceholder = $event"
|
||||
@@ -495,7 +539,7 @@ import DocumentPreview from './preview'
|
||||
import DocumentControls from './controls'
|
||||
import MobileFields from './mobile_fields'
|
||||
import FieldSubmitter from './field_submitter'
|
||||
import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments } from '@tabler/icons-vue'
|
||||
import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments, IconDownload } from '@tabler/icons-vue'
|
||||
import { v4 } from 'uuid'
|
||||
import { ref, computed, toRaw } from 'vue'
|
||||
import * as i18n from './i18n'
|
||||
@@ -521,6 +565,7 @@ export default {
|
||||
Contenteditable,
|
||||
IconUsersPlus,
|
||||
IconChevronDown,
|
||||
IconDownload,
|
||||
IconAdjustments,
|
||||
IconEye,
|
||||
IconDeviceFloppy
|
||||
@@ -563,6 +608,16 @@ export default {
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
withSignatureId: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
withDownload: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
backgroundColor: {
|
||||
type: String,
|
||||
required: false,
|
||||
@@ -598,6 +653,11 @@ export default {
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
withFieldsDetection: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
withAddPageButton: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -651,7 +711,7 @@ export default {
|
||||
acceptFileTypes: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'image/*, application/pdf'
|
||||
default: 'image/*, application/pdf, application/zip'
|
||||
},
|
||||
baseUrl: {
|
||||
type: String,
|
||||
@@ -754,6 +814,11 @@ export default {
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
withGoogleDrive: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
onlyDefinedFields: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -774,6 +839,7 @@ export default {
|
||||
return {
|
||||
documentRefs: [],
|
||||
isBreakpointLg: false,
|
||||
isDownloading: false,
|
||||
isLoadingBlankPage: false,
|
||||
isSaving: false,
|
||||
selectedSubmitter: null,
|
||||
@@ -794,6 +860,13 @@ export default {
|
||||
language () {
|
||||
return this.locale.split('-')[0].toLowerCase()
|
||||
},
|
||||
withPrefillable () {
|
||||
if (this.template.fields) {
|
||||
return this.template.fields.some((f) => f.prefillable)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
},
|
||||
isInlineSize () {
|
||||
return CSS.supports('container-type: size')
|
||||
},
|
||||
@@ -925,6 +998,75 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
toRaw,
|
||||
download () {
|
||||
this.isDownloading = true
|
||||
|
||||
this.baseFetch(`/templates/${this.template.id}/documents`).then(async (response) => {
|
||||
if (response.ok) {
|
||||
const urls = await response.json()
|
||||
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
|
||||
const isSafariIos = isMobileSafariIos || /iPhone|iPad|iPod/i.test(navigator.userAgent)
|
||||
|
||||
if (isSafariIos && urls.length > 1) {
|
||||
this.downloadSafariIos(urls)
|
||||
} else {
|
||||
this.downloadUrls(urls)
|
||||
}
|
||||
} else {
|
||||
alert(this.t('failed_to_download_files'))
|
||||
}
|
||||
})
|
||||
},
|
||||
downloadUrls (urls) {
|
||||
const fileRequests = urls.map((url) => {
|
||||
return () => {
|
||||
return fetch(url).then(async (resp) => {
|
||||
const blobUrl = URL.createObjectURL(await resp.blob())
|
||||
const link = document.createElement('a')
|
||||
|
||||
link.href = blobUrl
|
||||
link.setAttribute('download', decodeURI(url.split('/').pop()))
|
||||
|
||||
link.click()
|
||||
|
||||
URL.revokeObjectURL(blobUrl)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
fileRequests.reduce(
|
||||
(prevPromise, request) => prevPromise.then(() => request()),
|
||||
Promise.resolve()
|
||||
).finally(() => {
|
||||
this.isDownloading = false
|
||||
})
|
||||
},
|
||||
downloadSafariIos (urls) {
|
||||
const fileRequests = urls.map((url) => {
|
||||
return fetch(url).then(async (resp) => {
|
||||
const blob = await resp.blob()
|
||||
const blobUrl = URL.createObjectURL(blob.slice(0, blob.size, 'application/octet-stream'))
|
||||
const link = document.createElement('a')
|
||||
|
||||
link.href = blobUrl
|
||||
link.setAttribute('download', decodeURI(url.split('/').pop()))
|
||||
|
||||
return link
|
||||
})
|
||||
})
|
||||
|
||||
Promise.all(fileRequests).then((links) => {
|
||||
links.forEach((link, index) => {
|
||||
setTimeout(() => {
|
||||
link.click()
|
||||
|
||||
URL.revokeObjectURL(link.href)
|
||||
}, index * 50)
|
||||
})
|
||||
}).finally(() => {
|
||||
this.isDownloading = false
|
||||
})
|
||||
},
|
||||
onDragover (e) {
|
||||
if (this.$refs.dragPlaceholder?.dragPlaceholder) {
|
||||
this.$refs.dragPlaceholder.isMask = e.target.id === 'mask'
|
||||
@@ -1049,7 +1191,7 @@ export default {
|
||||
}
|
||||
|
||||
if (['select', 'multiple', 'radio'].includes(type)) {
|
||||
field.options = [{ value: '', uuid: v4() }]
|
||||
field.options = [{ value: '', uuid: v4() }, { value: '', uuid: v4() }]
|
||||
}
|
||||
|
||||
if (type === 'stamp') {
|
||||
@@ -1068,6 +1210,16 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'strikethrough') {
|
||||
field.readonly = true
|
||||
field.default_value = true
|
||||
}
|
||||
|
||||
if (type === 'signature' && [true, false].includes(this.withSignatureId)) {
|
||||
field.preferences ||= {}
|
||||
field.preferences.with_signature_id = this.withSignatureId
|
||||
}
|
||||
|
||||
this.template.fields.push(field)
|
||||
|
||||
this.save()
|
||||
@@ -1088,7 +1240,7 @@ export default {
|
||||
}
|
||||
|
||||
if (['select', 'multiple', 'radio'].includes(type)) {
|
||||
field.options = [{ value: '', uuid: v4() }]
|
||||
field.options = [{ value: '', uuid: v4() }, { value: '', uuid: v4() }]
|
||||
}
|
||||
|
||||
if (type === 'stamp') {
|
||||
@@ -1244,24 +1396,31 @@ export default {
|
||||
if (!field.areas.length) {
|
||||
this.template.fields.splice(this.template.fields.indexOf(field), 1)
|
||||
|
||||
this.template.fields.forEach((f) => {
|
||||
(f.conditions || []).forEach((c) => {
|
||||
this.removeFieldConditions(field)
|
||||
}
|
||||
|
||||
this.save()
|
||||
},
|
||||
removeFieldConditions (field) {
|
||||
this.template.fields.forEach((f) => {
|
||||
if (f.conditions) {
|
||||
f.conditions.forEach((c) => {
|
||||
if (c.field_uuid === field.uuid) {
|
||||
f.conditions.splice(f.conditions.indexOf(c), 1)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
this.template.schema.forEach((item) => {
|
||||
(item.conditions || []).forEach((c) => {
|
||||
this.template.schema.forEach((item) => {
|
||||
if (item.conditions) {
|
||||
item.conditions.forEach((c) => {
|
||||
if (c.field_uuid === field.uuid) {
|
||||
item.conditions.splice(item.conditions.indexOf(c), 1)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
this.save()
|
||||
}
|
||||
})
|
||||
},
|
||||
pasteField () {
|
||||
const field = this.template.fields.find((f) => f.areas?.includes(this.copiedArea))
|
||||
@@ -1280,7 +1439,13 @@ export default {
|
||||
this.copiedArea.option_uuid ||= field.options[0].uuid
|
||||
area.option_uuid = v4()
|
||||
|
||||
field.options.push({ uuid: area.option_uuid })
|
||||
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.areas.push(area)
|
||||
} else {
|
||||
@@ -1323,6 +1488,9 @@ export default {
|
||||
} else if (type === 'initials') {
|
||||
area.w = pageMask.clientWidth / 10 / 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 {
|
||||
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
|
||||
area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth)
|
||||
@@ -1456,7 +1624,7 @@ export default {
|
||||
if (this.dragField?.options?.length) {
|
||||
field.options = this.dragField.options.map(option => ({ value: option, uuid: v4() }))
|
||||
} else {
|
||||
field.options = [{ value: '', uuid: v4() }]
|
||||
field.options = [{ value: '', uuid: v4() }, { value: '', uuid: v4() }]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1466,14 +1634,23 @@ export default {
|
||||
field.default_value = '{{date}}'
|
||||
}
|
||||
|
||||
if (['stamp', 'heading'].includes(field.type)) {
|
||||
if (['stamp', 'heading', 'strikethrough'].includes(field.type)) {
|
||||
field.readonly = true
|
||||
|
||||
if (field.type === 'strikethrough') {
|
||||
field.default_value = true
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'date') {
|
||||
field.preferences ||= {}
|
||||
field.preferences.format ||= this.defaultDateFormat
|
||||
}
|
||||
|
||||
if (field.type === 'signature' && [true, false].includes(this.withSignatureId)) {
|
||||
field.preferences ||= {}
|
||||
field.preferences.with_signature_id = this.withSignatureId
|
||||
}
|
||||
}
|
||||
|
||||
const fieldArea = {
|
||||
@@ -1550,6 +1727,11 @@ export default {
|
||||
w: area.maskW / 10 / 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 {
|
||||
baseArea = {
|
||||
w: area.maskW / 5 / area.maskW,
|
||||
@@ -1683,16 +1865,24 @@ export default {
|
||||
})
|
||||
})
|
||||
|
||||
this.template.fields =
|
||||
this.template.fields.filter((f) => !removedFieldUuids.includes(f.uuid) || f.areas?.length)
|
||||
this.template.fields = this.template.fields.reduce((acc, f) => {
|
||||
if (removedFieldUuids.includes(f.uuid) && !f.areas?.length) {
|
||||
this.removeFieldConditions(f)
|
||||
} else {
|
||||
acc.push(f)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
this.save()
|
||||
}
|
||||
},
|
||||
onDocumentReplace (data) {
|
||||
const { replaceSchemaItem, schema, documents } = data
|
||||
const { google_drive_file_id, ...cleanedReplaceSchemaItem } = replaceSchemaItem
|
||||
|
||||
this.template.schema.splice(this.template.schema.indexOf(replaceSchemaItem), 1, { ...replaceSchemaItem, ...schema[0] })
|
||||
this.template.schema.splice(this.template.schema.indexOf(replaceSchemaItem), 1, { ...cleanedReplaceSchemaItem, ...schema[0] })
|
||||
this.template.documents.push(...documents)
|
||||
|
||||
if (data.fields) {
|
||||
|
||||
@@ -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="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||
<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>
|
||||
<a
|
||||
href="#"
|
||||
@@ -160,6 +160,11 @@ export default {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
defaultField: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
buildDefaultName: {
|
||||
type: Function,
|
||||
required: true
|
||||
@@ -172,10 +177,13 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
excludeTypes () {
|
||||
return ['heading', 'strikethrough']
|
||||
},
|
||||
fields () {
|
||||
if (this.item.submitter_uuid) {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
dir="auto"
|
||||
:contenteditable="editable"
|
||||
style="min-width: 2px"
|
||||
:class="iconInline ? 'inline' : 'block'"
|
||||
class="peer outline-none focus:block"
|
||||
:class="[iconInline ? 'inline' : 'block', hideIcon ? 'focus:block' : '']"
|
||||
class="peer outline-none"
|
||||
@paste.prevent="onPaste"
|
||||
@keydown.enter.prevent="blurContenteditable"
|
||||
@focus="$emit('focus', $event)"
|
||||
@@ -18,22 +18,19 @@
|
||||
{{ value }}
|
||||
</span>
|
||||
<span
|
||||
v-if="withRequired"
|
||||
title="Required"
|
||||
class="text-red-500 peer-focus:hidden"
|
||||
@click="focusContenteditable"
|
||||
class="relative inline"
|
||||
:class="{ 'peer-focus:hidden': hideIcon, 'peer-focus:invisible': !hideIcon }"
|
||||
>
|
||||
*
|
||||
<IconPencil
|
||||
class="cursor-pointer flex-none opacity-0 group-hover/contenteditable-container:opacity-100 group-hover/contenteditable:opacity-100 align-middle pl-1"
|
||||
:style="iconInline ? {} : { right: -(1.1 * iconWidth) + 'px' }"
|
||||
:title="t('edit')"
|
||||
:class="{ invisible: !editable, 'absolute top-1/2 -translate-y-1/2': !iconInline || floatIcon, 'inline align-bottom': iconInline, 'left-0': floatIcon }"
|
||||
:width="iconWidth + 4"
|
||||
:stroke-width="iconStrokeWidth"
|
||||
@click="[focusContenteditable(), selectOnEditClick && selectContent()]"
|
||||
/>
|
||||
</span>
|
||||
<IconPencil
|
||||
class="cursor-pointer flex-none opacity-0 group-hover/contenteditable-container:opacity-100 group-hover/contenteditable:opacity-100 align-middle peer-focus:hidden"
|
||||
:style="iconInline ? {} : { right: -(1.1 * iconWidth) + 'px' }"
|
||||
:title="t('edit')"
|
||||
:class="{ invisible: !editable, 'ml-1': !withRequired, 'absolute': !iconInline, 'inline align-bottom': iconInline }"
|
||||
:width="iconWidth"
|
||||
:stroke-width="iconStrokeWidth"
|
||||
@click="[focusContenteditable(), selectOnEditClick && selectContent()]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -62,7 +59,12 @@ export default {
|
||||
required: false,
|
||||
default: 30
|
||||
},
|
||||
withRequired: {
|
||||
hideIcon: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
floatIcon: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user