mirror of
https://github.com/docusealco/docuseal.git
synced 2026-08-07 15:25:16 +00:00
Compare commits
373 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ed46af8418 | |||
| 095f25a6e9 | |||
| 3cdab30dc8 | |||
| 8e462047ba | |||
| b7f8b65d9d | |||
| da21c37254 | |||
| a89aef4a89 | |||
| dd6516fd11 | |||
| 344a3ce198 | |||
| 62f8283587 | |||
| 1f89accac3 | |||
| 9fcaef4cf7 | |||
| d5738a0631 | |||
| 8bf7a1f95a | |||
| 04cf36891e | |||
| b2d9948c30 | |||
| a2c9ac1707 | |||
| 46a6bd0108 | |||
| 5f069e7a40 | |||
| ff57e5c6ae | |||
| f65b6e2d76 | |||
| 58fd180ae0 | |||
| cd6503c4c3 | |||
| e8b36c2b6d | |||
| 51743f1359 | |||
| 504c42646b | |||
| c61e84d1b4 | |||
| b8ab01c46c | |||
| 9558060bde | |||
| 0741c879f1 | |||
| 2e6dd2867e | |||
| 9f3fcccb61 | |||
| ae08d7abad | |||
| 16808a7ec6 | |||
| b1dfcd6283 | |||
| 369f4c0f08 | |||
| 827de03579 | |||
| 0c7f4e5b43 | |||
| 9c700a3fb6 | |||
| 89bf83febb | |||
| d44b35cf0a | |||
| 891833e273 | |||
| 29d3a80d3f | |||
| 90fef58684 | |||
| 07841722ac | |||
| 9e4da5948b | |||
| dc6e4313a1 | |||
| a68cc0b689 | |||
| d057fb0f67 | |||
| 7e8f045a29 | |||
| 5249d8d18d | |||
| f0fafbadd4 | |||
| 8b51079dcc | |||
| 354bccd6e8 | |||
| 6806772346 | |||
| 60082655d4 | |||
| 7fe56941fd | |||
| abd498dd33 | |||
| 99ca0136ed | |||
| e52830c9b4 | |||
| 755decca27 | |||
| 41604008d1 | |||
| 5bddce8969 | |||
| b25503f141 | |||
| bdf1850448 | |||
| 230d3ccf69 | |||
| e378025a2d | |||
| 04129ded90 | |||
| a7891f89f8 | |||
| 37d4a8e834 | |||
| 528a1216f8 | |||
| f2479bd259 | |||
| 135f0826d2 | |||
| 5710f0177b | |||
| 1a3a0528ba | |||
| 59793ff374 | |||
| 3c4ed42419 | |||
| 6a17b61550 | |||
| 76659497b7 | |||
| ee50c957cb | |||
| a9dd200919 | |||
| 1fa953104a | |||
| 50eb5b070e | |||
| 10fd624bec | |||
| 04ec2f8260 | |||
| d828d79574 | |||
| 01dd3fefe5 | |||
| 45ae954c0c | |||
| 1304849b55 | |||
| 0875faa079 | |||
| 3aa15d6ea6 | |||
| 6557329e97 | |||
| be27ce4161 | |||
| a3391a970e | |||
| 744d45d2c5 | |||
| 310b16f54a | |||
| 6d3ad4a869 | |||
| 1ce2448820 | |||
| 07b4ada5be | |||
| f70bf530ba | |||
| a26aa0f411 | |||
| 584577c21f | |||
| 1503375eed | |||
| 2dec099f20 | |||
| bf6eed4ea2 | |||
| e0dc33cfb4 | |||
| 9c9dc27537 | |||
| 7305637b8c | |||
| a6e22adf2e | |||
| 809c6b1aa8 | |||
| 1536ded2a6 | |||
| 76d54971ce | |||
| 6dec56198c | |||
| d96d252df3 | |||
| 3fca76a716 | |||
| a184fa11f1 | |||
| e9c0b6a703 | |||
| ec7df08496 | |||
| 6627d7eac5 | |||
| 1f8945d710 | |||
| daaa289a5c | |||
| 81a6a48d70 | |||
| b93c7cd261 | |||
| 570482e12d | |||
| 39407557f2 | |||
| 3b040d558d | |||
| 56ba3c2a52 | |||
| c7afe33c72 | |||
| 82500c2d7d | |||
| fb1c1f117b | |||
| 7c6a7513fd | |||
| 6d13119ee0 | |||
| 1e2c752937 | |||
| e82faf8320 | |||
| 4adb1001b0 | |||
| 992a1b26c0 | |||
| c07bc6687c | |||
| eea44bda34 | |||
| 7cdf263da1 | |||
| ee65a5693c | |||
| 0af6ccf35f | |||
| 888f1ec6df | |||
| c95a8616ac | |||
| 5ea6289b7a | |||
| d4a79ca5db | |||
| 70015ce1c4 | |||
| 6b85c28944 | |||
| 6c289cf273 | |||
| a64bc3c618 | |||
| 46cf1e3067 | |||
| 565e1eb2bc | |||
| e689687805 | |||
| 3c3b61fb47 | |||
| 1355d350c5 | |||
| fda911e178 | |||
| f995e1864c | |||
| 911e55ccc3 | |||
| 41fedfcc40 | |||
| 2f9fc95af3 | |||
| 11b0b16ca7 | |||
| f8cb7ffdab | |||
| 8b59c0aaa0 | |||
| bb2fb7a0c2 | |||
| 8f8b36617a | |||
| ff53436fd4 | |||
| cf09a4f733 | |||
| 2303c21cea | |||
| 89e797b95f | |||
| 20375c3a42 | |||
| 339ceda18d | |||
| 97ce32fd52 | |||
| 8c4cbd86f7 | |||
| eaedaa96bb | |||
| bd7ad5fce8 | |||
| 759dac1b70 | |||
| 38577c6235 | |||
| b24fff0f55 | |||
| 9e0efeb092 | |||
| 41152f90fc | |||
| 4486f8040e | |||
| a98caad968 | |||
| ffb75238b4 | |||
| cba3e74f3a | |||
| 8d3f0b5b98 | |||
| a2598b30f7 | |||
| 8d11f5ddae | |||
| 6ffc88b4ed | |||
| 640b451727 | |||
| db6e2eb7ca | |||
| 407816b576 | |||
| 486cc493e6 | |||
| 3eb0b8a89f | |||
| c23eca0ade | |||
| 67de153016 | |||
| ba2da69a67 | |||
| 111bef14e1 | |||
| 22d4c1bd10 | |||
| e3dfb0ae83 | |||
| 8d7c3f0e26 | |||
| b11775ebda | |||
| 608191183a | |||
| 2ba903e170 | |||
| 342b64d502 | |||
| 1a8f048aaa | |||
| 7309581b75 | |||
| 36d59c40a7 | |||
| ed3c7c324b | |||
| fd868360a3 | |||
| 8dc2ebfbe0 | |||
| 997711b34a | |||
| ac01b7129d | |||
| f4b7528c0d | |||
| d7b9ae5df5 | |||
| 4db29126e9 | |||
| 4cfee2fe45 | |||
| db5dbd6ac2 | |||
| 44db775b89 | |||
| 8e2e780a1a | |||
| 7aba2eb4da | |||
| 3e250bfb28 | |||
| 9c02ed66f5 | |||
| f5ae8c60d3 | |||
| e90ac391b2 | |||
| 0e5f949bcf | |||
| b7c270bef8 | |||
| 5f16b506f5 | |||
| 176148fc71 | |||
| 861daaedf4 | |||
| d8430f4e63 | |||
| dd44864485 | |||
| b3bb29f383 | |||
| b4d137c0de | |||
| 62a969d8fe | |||
| 961f09e092 | |||
| b2dfa83f18 | |||
| 37196ff89f | |||
| b50d982497 | |||
| 4a484aca69 | |||
| 0bf37d571a | |||
| d4e94ac2bc | |||
| 75bc789bb1 | |||
| bc231d5bf1 | |||
| ab9008b3be | |||
| 65491936c3 | |||
| e589998840 | |||
| 0afba8d11e | |||
| f622ed168f | |||
| 5a31df40d8 | |||
| db3b80c96c | |||
| bd28a761d0 | |||
| 4f2830a34d | |||
| a5716cd518 | |||
| d035525c7a | |||
| 13fa87c449 | |||
| 246ccfb049 | |||
| cbffb1e5b8 | |||
| c4b007411d | |||
| 04264fe937 | |||
| 0c37ad12b7 | |||
| 8c5298ed2e | |||
| 262118e047 | |||
| e5b63ea2b0 | |||
| 746757d402 | |||
| e6e640328b | |||
| 845782a69c | |||
| fc6baa1b3b | |||
| 47822ecc15 | |||
| 3d0c7f1118 | |||
| e9c8e4d325 | |||
| 39cc82ce0c | |||
| 451f83421a | |||
| 5073f2eefb | |||
| 231fff5508 | |||
| f1d146eca3 | |||
| b6635fcc4f | |||
| fb5e13ee4c | |||
| ba84741a64 | |||
| 65c275ac17 | |||
| 825322d489 | |||
| 739e2abdf8 | |||
| 1d2394e31e | |||
| 1b41af798d | |||
| 118f4a231b | |||
| e48652f425 | |||
| 848f01edf8 | |||
| 43fbc42770 | |||
| 2c736a0eed | |||
| 33ca930055 | |||
| 3b396f8421 | |||
| aa77e3a8d3 | |||
| 75316d8d87 | |||
| a999109a5c | |||
| bdd33c7d6b | |||
| 61c5ee22a0 | |||
| 12c5b909e0 | |||
| 871ef6dda6 | |||
| 40052a2d7c | |||
| 347be0137d | |||
| fe6baba8bf | |||
| ed8c313bd4 | |||
| ca0acb34d6 | |||
| 34ea639c25 | |||
| 680ab9dbed | |||
| 9377766e52 | |||
| 7a2c37454e | |||
| 8f6f418f54 | |||
| 9b7745c565 | |||
| a389e8cc38 | |||
| 2056301f70 | |||
| 93babe3660 | |||
| 4aef2d8eb0 | |||
| c5ec2ed230 | |||
| a156d5f6cc | |||
| f84674cddd | |||
| b7b62c197a | |||
| f5350eac36 | |||
| ee6228db15 | |||
| 3e434c0878 | |||
| 7c5372a8e2 | |||
| 6ab68cc36b | |||
| ee2400a5c5 | |||
| 80be5d2cf5 | |||
| 76cf44a31b | |||
| eb63113227 | |||
| b7045fb3d9 | |||
| 11a5b5842d | |||
| 362994afd5 | |||
| d7682f4ebd | |||
| 56d4fe7fa9 | |||
| 93a852ac14 | |||
| 964db0ab08 | |||
| 8547295da7 | |||
| c75050fa46 | |||
| 65372c52aa | |||
| 12c767b7d3 | |||
| 2fc5e9cfb3 | |||
| bccc78ee65 | |||
| c0793f3ce7 | |||
| a8dd8699a3 | |||
| 91bb25e8b6 | |||
| c3f4480cdc | |||
| 33e226861e | |||
| 4f5acbec9d | |||
| d2c9bbf6ab | |||
| b5e57e1f2f | |||
| c82c8b2272 | |||
| 43f1b598a7 | |||
| 5081e387d0 | |||
| 1e5f55c1d6 | |||
| aa38773be3 | |||
| 96530bd809 | |||
| 127f95a9d0 | |||
| cf31e12348 | |||
| 4adaa531b7 | |||
| c7659563b8 | |||
| 7b5494468f | |||
| d12c67fd54 | |||
| e20eacb5ee | |||
| 98055f0762 | |||
| a46c42c3a0 | |||
| c3d1503361 | |||
| 65b47d27cd | |||
| 968c07f8be | |||
| 91814ca105 | |||
| 9128f24270 | |||
| 264897bbd5 | |||
| 67d59ff1ba | |||
| 81eee56e1e | |||
| 92ea8af786 | |||
| df0616f77e | |||
| a337dff85c | |||
| 48e349c1d2 | |||
| df9fa0fec7 |
@@ -12,7 +12,7 @@ jobs:
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.4.2
|
||||
ruby-version: 4.0.5
|
||||
- name: Cache gems
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.4.2
|
||||
ruby-version: 4.0.5
|
||||
- name: Cache gems
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 20.9.0
|
||||
node-version: 20.19.0
|
||||
- name: Cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.4.2
|
||||
ruby-version: 4.0.5
|
||||
- name: Cache gems
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -132,11 +132,11 @@ jobs:
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.4.2
|
||||
ruby-version: 4.0.5
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 20.9.0
|
||||
node-version: 20.19.0
|
||||
- name: Install Chrome
|
||||
uses: browser-actions/setup-chrome@latest
|
||||
with:
|
||||
@@ -163,7 +163,7 @@ jobs:
|
||||
yarn install
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libvips
|
||||
wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz"
|
||||
wget -O pdfium-linux.tgz "https://github.com/bblanchon/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz"
|
||||
sudo tar -xzf pdfium-linux.tgz --strip-components=1 -C /usr/lib lib/libpdfium.so
|
||||
rm -f pdfium-linux.tgz
|
||||
- name: Run
|
||||
|
||||
+26
-4
@@ -1,4 +1,4 @@
|
||||
require:
|
||||
plugins:
|
||||
- rubocop-performance
|
||||
- rubocop-rails
|
||||
- rubocop-rspec
|
||||
@@ -10,7 +10,7 @@ AllCops:
|
||||
- node_modules/**/*
|
||||
- bin/*
|
||||
- vendor/**/*
|
||||
TargetRubyVersion: '3.3'
|
||||
TargetRubyVersion: '4.0'
|
||||
SuggestExtensions: false
|
||||
|
||||
Metrics/BlockLength:
|
||||
@@ -28,7 +28,7 @@ Lint/MissingSuper:
|
||||
Enabled: false
|
||||
|
||||
Metrics/ParameterLists:
|
||||
Max: 10
|
||||
Max: 12
|
||||
|
||||
Metrics/MethodLength:
|
||||
Max: 30
|
||||
@@ -51,6 +51,15 @@ Style/NumericPredicate:
|
||||
Style/MinMaxComparison:
|
||||
Enabled: false
|
||||
|
||||
Style/EmptyClassDefinition:
|
||||
Enabled: false
|
||||
|
||||
Style/OneClassPerFile:
|
||||
Enabled: false
|
||||
|
||||
Layout/MultilineMethodCallIndentation:
|
||||
Enabled: false
|
||||
|
||||
Naming/PredicateMethod:
|
||||
Enabled: false
|
||||
|
||||
@@ -84,7 +93,7 @@ RSpec/AnyInstance:
|
||||
Enabled: false
|
||||
|
||||
Metrics/BlockNesting:
|
||||
Max: 5
|
||||
Max: 6
|
||||
|
||||
Rails/I18nLocaleTexts:
|
||||
Enabled: false
|
||||
@@ -100,3 +109,16 @@ Rails/ApplicationController:
|
||||
|
||||
Rails/Output:
|
||||
Enabled: false
|
||||
|
||||
Rails/StrongParametersExpect:
|
||||
Enabled: false
|
||||
|
||||
Rails/RedirectBackOrTo:
|
||||
Enabled: false
|
||||
|
||||
Rails/UnknownEnv:
|
||||
Environments:
|
||||
- development
|
||||
- test
|
||||
- production
|
||||
- local
|
||||
|
||||
+18
-20
@@ -1,22 +1,19 @@
|
||||
FROM ruby:3.4.2-alpine AS download
|
||||
FROM ruby:4.0.5-alpine AS download
|
||||
|
||||
WORKDIR /fonts
|
||||
|
||||
RUN apk --no-cache add fontforge wget && \
|
||||
RUN apk --no-cache add wget && \
|
||||
wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && \
|
||||
wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && \
|
||||
wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && \
|
||||
wget https://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/2.1.0/model_704_int8.onnx" && \
|
||||
wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" && \
|
||||
wget https://raw.githubusercontent.com/impallari/DancingScript/master/OFL.txt && \
|
||||
wget https://raw.githubusercontent.com/notofonts/noto-fonts/refs/heads/main/LICENSE && \
|
||||
wget -O /model.onnx "https://github.com/docusealco/fields-detection/releases/download/2.0.0/model_704_int8.onnx" && \
|
||||
wget -O pdfium-linux.tgz "https://github.com/bblanchon/pdfium-binaries/releases/latest/download/pdfium-linux-musl-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" && \
|
||||
mkdir -p /pdfium-linux && \
|
||||
tar -xzf pdfium-linux.tgz -C /pdfium-linux
|
||||
|
||||
RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")'
|
||||
|
||||
FROM ruby:3.4.2-alpine AS webpack
|
||||
FROM ruby:4.0.5-alpine AS webpack
|
||||
|
||||
ENV RAILS_ENV=production
|
||||
ENV NODE_ENV=production
|
||||
@@ -36,22 +33,23 @@ COPY ./config/shakapacker.yml ./config/shakapacker.yml
|
||||
COPY ./postcss.config.js ./postcss.config.js
|
||||
COPY ./tailwind.config.js ./tailwind.config.js
|
||||
COPY ./tailwind.form.config.js ./tailwind.form.config.js
|
||||
COPY ./tailwind.dynamic.config.js ./tailwind.dynamic.config.js
|
||||
COPY ./tailwind.application.config.js ./tailwind.application.config.js
|
||||
COPY ./app/javascript ./app/javascript
|
||||
COPY ./app/views ./app/views
|
||||
|
||||
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
|
||||
|
||||
FROM ruby:3.4.2-alpine AS app
|
||||
FROM ruby:4.0.5-alpine AS app
|
||||
|
||||
ENV RAILS_ENV=production
|
||||
ENV BUNDLE_WITHOUT="development:test"
|
||||
ENV LD_PRELOAD=/lib/libgcompat.so.0
|
||||
ENV OPENSSL_CONF=/etc/openssl_legacy.cnf
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
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 apk add --no-cache libpq vips redis onnxruntime && \
|
||||
rm -f /usr/bin/onnx_test_runner /usr/bin/onnxruntime_test
|
||||
|
||||
RUN addgroup -g 2000 docuseal && adduser -u 2000 -G docuseal -s /bin/sh -D -h /home/docuseal docuseal
|
||||
|
||||
@@ -69,9 +67,7 @@ activate = 1' >> /etc/openssl_legacy.cnf
|
||||
|
||||
COPY --chown=docuseal:docuseal ./Gemfile ./Gemfile.lock ./
|
||||
|
||||
RUN apk add --no-cache build-base && bundle install && apk del --no-cache build-base && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf && 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
|
||||
RUN apk add --no-cache build-base git libpq-dev yaml-dev && bundle install && apk del --no-cache build-base git libpq-dev yaml-dev && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales,resources/unicode_data/properties}'] + Dir['/usr/local/bundle/gems/*/{test,tests,examples,sample,misc,doc,docs}'] + Dir['/usr/local/bundle/gems/*/ext/**/*.{c,h,o,S}']" | xargs rm -rf && ln -sf /usr/lib/libonnxruntime.so.1 $(ruby -e "print Dir[Gem::Specification.find_by_name('onnxruntime').gem_dir + '/vendor/*.so'].first")
|
||||
|
||||
COPY --chown=docuseal:docuseal ./bin ./bin
|
||||
COPY --chown=docuseal:docuseal ./app ./app
|
||||
@@ -81,23 +77,25 @@ COPY --chown=docuseal:docuseal ./log ./log
|
||||
COPY --chown=docuseal:docuseal ./lib ./lib
|
||||
COPY --chown=docuseal:docuseal ./public ./public
|
||||
COPY --chown=docuseal:docuseal ./tmp ./tmp
|
||||
COPY --chown=docuseal:docuseal LICENSE README.md Rakefile config.ru .version ./
|
||||
COPY --chown=docuseal:docuseal LICENSE LICENSE_ADDITIONAL_TERMS README.md Rakefile config.ru .version ./
|
||||
COPY --chown=docuseal:docuseal .version ./public/version
|
||||
|
||||
COPY --chown=docuseal:docuseal --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
||||
COPY --from=download /fonts/FreeSans.ttf /usr/share/fonts/freefont
|
||||
COPY --chown=docuseal:docuseal --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts/LICENSE /fonts/
|
||||
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 --chown=docuseal:docuseal --from=download /model.onnx /app/tmp/model.onnx
|
||||
COPY --chown=docuseal:docuseal --from=webpack /app/public/packs ./public/packs
|
||||
|
||||
RUN ln -s /fonts /app/public/fonts && \
|
||||
RUN mkdir -p /app/public/fonts && ln -s /fonts/DancingScript-Regular.otf /app/public/fonts/ && \
|
||||
mkdir -p /usr/share/fonts/noto && ln -s /fonts/GoNotoKurrent-Regular.ttf /usr/share/fonts/noto/ && ln -s /fonts/GoNotoKurrent-Bold.ttf /usr/share/fonts/noto/ && fc-cache -f && \
|
||||
bundle exec bootsnap precompile -j 1 --gemfile app/ lib/ && \
|
||||
chown -R docuseal:docuseal /app/tmp/cache
|
||||
|
||||
WORKDIR /data/docuseal
|
||||
ENV HOME=/home/docuseal
|
||||
ENV WORKDIR=/data/docuseal
|
||||
ENV VIPS_MAX_COORD=17000
|
||||
ENV VIPS_BLOCK_UNTRUSTED=1
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["/app/bin/bundle", "exec", "puma", "-C", "/app/config/puma.rb", "--dir", "/app"]
|
||||
|
||||
@@ -2,16 +2,17 @@
|
||||
|
||||
source 'https://rubygems.org'
|
||||
|
||||
ruby '3.4.2'
|
||||
ruby '4.0.5'
|
||||
|
||||
gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic'
|
||||
gem 'addressable'
|
||||
gem 'arabic-letter-connector', require: false
|
||||
gem 'aws-sdk-s3', require: false
|
||||
gem 'aws-sdk-secretsmanager', require: false
|
||||
gem 'azure-storage-blob', require: false
|
||||
gem 'azure-blob', require: false
|
||||
gem 'bootsnap', require: false
|
||||
gem 'cancancan'
|
||||
gem 'csv'
|
||||
gem 'csv-safe'
|
||||
gem 'csv', require: false
|
||||
gem 'csv-safe', require: false
|
||||
gem 'devise'
|
||||
gem 'devise-two-factor'
|
||||
gem 'dotenv', require: false
|
||||
@@ -20,31 +21,27 @@ gem 'faraday'
|
||||
gem 'faraday-follow_redirects'
|
||||
gem 'google-cloud-storage', require: false
|
||||
gem 'hexapdf'
|
||||
gem 'image_processing'
|
||||
gem 'jwt'
|
||||
gem 'jwt', require: false
|
||||
gem 'lograge'
|
||||
gem 'mysql2', require: false
|
||||
gem 'numo-narray'
|
||||
gem 'oj'
|
||||
gem 'onnxruntime'
|
||||
gem 'numo-narray-alt', require: false
|
||||
gem 'onnxruntime', require: false
|
||||
gem 'pagy'
|
||||
gem 'pg', require: false
|
||||
gem 'premailer-rails'
|
||||
gem 'pretender'
|
||||
gem 'puma', require: false
|
||||
gem 'rack'
|
||||
gem 'rails'
|
||||
gem 'rails_autolink'
|
||||
gem 'rails-i18n'
|
||||
gem 'rotp'
|
||||
gem 'rouge', require: false
|
||||
gem 'rqrcode'
|
||||
gem 'rqrcode', require: false
|
||||
gem 'ruby-vips'
|
||||
gem 'rubyXL'
|
||||
gem 'rubyXL', require: false
|
||||
gem 'shakapacker'
|
||||
gem 'sidekiq'
|
||||
gem 'sqlite3', require: false
|
||||
gem 'strip_attributes'
|
||||
gem 'trilogy', require: false
|
||||
gem 'turbo-rails'
|
||||
gem 'twitter_cldr', require: false
|
||||
gem 'tzinfo-data'
|
||||
|
||||
+290
-324
@@ -1,29 +1,31 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
action_text-trix (2.1.18)
|
||||
railties
|
||||
actioncable (8.1.3)
|
||||
actionpack (= 8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
zeitwerk (~> 2.6)
|
||||
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)
|
||||
actionmailbox (8.1.3)
|
||||
actionpack (= 8.1.3)
|
||||
activejob (= 8.1.3)
|
||||
activerecord (= 8.1.3)
|
||||
activestorage (= 8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
mail (>= 2.8.0)
|
||||
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)
|
||||
actionmailer (8.1.3)
|
||||
actionpack (= 8.1.3)
|
||||
actionview (= 8.1.3)
|
||||
activejob (= 8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
mail (>= 2.8.0)
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (8.0.2.1)
|
||||
actionview (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
actionpack (8.1.3)
|
||||
actionview (= 8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
nokogiri (>= 1.8.5)
|
||||
rack (>= 2.2.4)
|
||||
rack-session (>= 1.0.1)
|
||||
@@ -31,55 +33,58 @@ GEM
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
useragent (~> 0.16)
|
||||
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)
|
||||
actiontext (8.1.3)
|
||||
action_text-trix (~> 2.1.15)
|
||||
actionpack (= 8.1.3)
|
||||
activerecord (= 8.1.3)
|
||||
activestorage (= 8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
actionview (8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
activejob (8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
activejob (8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
globalid (>= 0.3.6)
|
||||
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)
|
||||
activemodel (8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
activerecord (8.1.3)
|
||||
activemodel (= 8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
timeout (>= 0.4.0)
|
||||
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)
|
||||
activestorage (8.1.3)
|
||||
actionpack (= 8.1.3)
|
||||
activejob (= 8.1.3)
|
||||
activerecord (= 8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
marcel (~> 1.0)
|
||||
activesupport (8.0.2.1)
|
||||
activesupport (8.1.3)
|
||||
base64
|
||||
benchmark (>= 0.3)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||
connection_pool (>= 2.2.5)
|
||||
drb
|
||||
i18n (>= 1.6, < 2)
|
||||
json
|
||||
logger (>= 1.4.2)
|
||||
minitest (>= 5.1)
|
||||
securerandom (>= 0.3)
|
||||
tzinfo (~> 2.0, >= 2.0.5)
|
||||
uri (>= 0.13.1)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
annotaterb (4.14.0)
|
||||
addressable (2.9.0)
|
||||
public_suffix (>= 2.0.2, < 8.0)
|
||||
annotaterb (4.22.0)
|
||||
activerecord (>= 6.0.0)
|
||||
activesupport (>= 6.0.0)
|
||||
arabic-letter-connector (0.1.1)
|
||||
ast (2.4.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1197.0)
|
||||
aws-sdk-core (3.240.0)
|
||||
aws-partitions (1.1233.0)
|
||||
aws-sdk-core (3.244.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
@@ -87,44 +92,38 @@ GEM
|
||||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.118.0)
|
||||
aws-sdk-core (~> 3, >= 3.239.1)
|
||||
aws-sdk-kms (1.123.0)
|
||||
aws-sdk-core (~> 3, >= 3.244.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.208.0)
|
||||
aws-sdk-core (~> 3, >= 3.234.0)
|
||||
aws-sdk-s3 (1.218.0)
|
||||
aws-sdk-core (~> 3, >= 3.244.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-secretsmanager (1.110.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-secretsmanager (1.129.0)
|
||||
aws-sdk-core (~> 3, >= 3.244.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.12.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
azure-storage-blob (2.0.3)
|
||||
azure-storage-common (~> 2.0)
|
||||
nokogiri (~> 1, >= 1.10.8)
|
||||
azure-storage-common (2.0.4)
|
||||
faraday (~> 1.0)
|
||||
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
|
||||
net-http-persistent (~> 4.0)
|
||||
nokogiri (~> 1, >= 1.10.8)
|
||||
azure-blob (0.8.0)
|
||||
cgi
|
||||
rexml
|
||||
base64 (0.3.0)
|
||||
bcrypt (3.1.20)
|
||||
benchmark (0.4.1)
|
||||
better_html (2.1.1)
|
||||
actionview (>= 6.0)
|
||||
activesupport (>= 6.0)
|
||||
bcrypt (3.1.22)
|
||||
better_html (2.2.0)
|
||||
actionview (>= 7.0)
|
||||
activesupport (>= 7.0)
|
||||
ast (~> 2.0)
|
||||
erubi (~> 1.4)
|
||||
parser (>= 2.4)
|
||||
smart_properties
|
||||
bigdecimal (3.2.2)
|
||||
bigdecimal (4.1.2)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.18.4)
|
||||
bootsnap (1.23.0)
|
||||
msgpack (~> 1.2)
|
||||
brakeman (7.0.0)
|
||||
brakeman (8.0.4)
|
||||
racc
|
||||
builder (3.3.0)
|
||||
bullet (8.0.0)
|
||||
bullet (8.1.0)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.11)
|
||||
camertron-eprun (1.1.1)
|
||||
@@ -138,51 +137,50 @@ GEM
|
||||
rack-test (>= 0.6.3)
|
||||
regexp_parser (>= 1.5, < 3.0)
|
||||
xpath (~> 3.2)
|
||||
cgi (0.5.1)
|
||||
childprocess (5.1.0)
|
||||
logger (~> 1.5)
|
||||
chunky_png (1.4.0)
|
||||
cldr-plurals-runtime-rb (1.1.0)
|
||||
cmdparse (3.0.7)
|
||||
coderay (1.1.3)
|
||||
concurrent-ruby (1.3.5)
|
||||
connection_pool (2.5.3)
|
||||
crack (1.0.0)
|
||||
concurrent-ruby (1.3.6)
|
||||
connection_pool (3.0.2)
|
||||
crack (1.0.1)
|
||||
bigdecimal
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
css_parser (1.21.0)
|
||||
addressable
|
||||
csv (3.3.2)
|
||||
csv (3.3.5)
|
||||
csv-safe (3.3.1)
|
||||
csv (~> 3.0)
|
||||
cuprite (0.15.1)
|
||||
cuprite (0.17)
|
||||
capybara (~> 3.0)
|
||||
ferrum (~> 0.15.0)
|
||||
date (3.4.1)
|
||||
debug (1.10.0)
|
||||
ferrum (~> 0.17.0)
|
||||
date (3.5.1)
|
||||
debug (1.11.1)
|
||||
irb (~> 1.10)
|
||||
reline (>= 0.3.8)
|
||||
declarative (0.0.20)
|
||||
devise (4.9.4)
|
||||
devise (5.0.4)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0)
|
||||
railties (>= 7.0)
|
||||
responders
|
||||
warden (~> 1.2.3)
|
||||
devise-two-factor (6.1.0)
|
||||
activesupport (>= 7.0, < 8.1)
|
||||
devise (~> 4.0)
|
||||
railties (>= 7.0, < 8.1)
|
||||
devise-two-factor (6.4.0)
|
||||
activesupport (>= 7.2, < 8.2)
|
||||
devise (>= 4.0, < 6.0)
|
||||
railties (>= 7.2, < 8.2)
|
||||
rotp (~> 6.0)
|
||||
diff-lcs (1.5.1)
|
||||
digest-crc (0.6.5)
|
||||
diff-lcs (1.6.2)
|
||||
digest-crc (0.7.0)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
docile (1.4.1)
|
||||
dotenv (3.1.7)
|
||||
dotenv (3.2.0)
|
||||
drb (2.2.3)
|
||||
email_typo (0.2.3)
|
||||
erb (5.0.2)
|
||||
erb_lint (0.7.0)
|
||||
erb (6.0.4)
|
||||
erb_lint (0.9.0)
|
||||
activesupport
|
||||
better_html (>= 2.0.1)
|
||||
parser (>= 2.7.1.4)
|
||||
@@ -190,117 +188,97 @@ GEM
|
||||
rubocop (>= 1)
|
||||
smart_properties
|
||||
erubi (1.13.1)
|
||||
factory_bot (6.5.0)
|
||||
activesupport (>= 5.0.0)
|
||||
factory_bot_rails (6.4.4)
|
||||
factory_bot (6.5.6)
|
||||
activesupport (>= 6.1.0)
|
||||
factory_bot_rails (6.5.1)
|
||||
factory_bot (~> 6.5)
|
||||
railties (>= 5.0.0)
|
||||
faker (3.5.1)
|
||||
railties (>= 6.1.0)
|
||||
faker (3.6.1)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (1.10.4)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
faraday-httpclient (~> 1.0)
|
||||
faraday-multipart (~> 1.0)
|
||||
faraday-net_http (~> 1.0)
|
||||
faraday-net_http_persistent (~> 1.0)
|
||||
faraday-patron (~> 1.0)
|
||||
faraday-rack (~> 1.0)
|
||||
faraday-retry (~> 1.0)
|
||||
ruby2_keywords (>= 0.0.4)
|
||||
faraday-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.0)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-follow_redirects (0.3.0)
|
||||
faraday (2.14.2)
|
||||
faraday-net_http (>= 2.0, < 3.5)
|
||||
json
|
||||
logger
|
||||
faraday-follow_redirects (0.5.0)
|
||||
faraday (>= 1, < 3)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.1.0)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (1.0.2)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.3)
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
ferrum (0.15)
|
||||
faraday-net_http (3.4.2)
|
||||
net-http (~> 0.5)
|
||||
ferrum (0.17.2)
|
||||
addressable (~> 2.5)
|
||||
base64 (~> 0.2)
|
||||
concurrent-ruby (~> 1.1)
|
||||
webrick (~> 1.7)
|
||||
websocket-driver (~> 0.7)
|
||||
ffi (1.17.1)
|
||||
ffi (1.17.1-aarch64-linux-gnu)
|
||||
ffi (1.17.1-aarch64-linux-musl)
|
||||
ffi (1.17.1-arm64-darwin)
|
||||
ffi (1.17.1-x86_64-linux-gnu)
|
||||
ffi (1.17.1-x86_64-linux-musl)
|
||||
foreman (0.88.1)
|
||||
ffi (1.17.4-aarch64-linux-gnu)
|
||||
ffi (1.17.4-aarch64-linux-musl)
|
||||
ffi (1.17.4-arm64-darwin)
|
||||
ffi (1.17.4-x86_64-linux-gnu)
|
||||
ffi (1.17.4-x86_64-linux-musl)
|
||||
foreman (0.90.0)
|
||||
thor (~> 1.4)
|
||||
geom2d (0.4.1)
|
||||
globalid (1.2.1)
|
||||
globalid (1.3.0)
|
||||
activesupport (>= 6.1)
|
||||
google-apis-core (0.15.1)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (~> 1.9)
|
||||
httpclient (>= 2.8.3, < 3.a)
|
||||
mini_mime (~> 1.0)
|
||||
mutex_m
|
||||
google-apis-core (1.0.2)
|
||||
addressable (~> 2.8, >= 2.8.7)
|
||||
faraday (~> 2.13)
|
||||
faraday-follow_redirects (~> 0.3)
|
||||
googleauth (~> 1.14)
|
||||
mini_mime (~> 1.1)
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.a)
|
||||
google-apis-iamcredentials_v1 (0.22.0)
|
||||
retriable (~> 3.1)
|
||||
google-apis-iamcredentials_v1 (0.26.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-apis-storage_v1 (0.49.0)
|
||||
google-apis-storage_v1 (0.61.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-cloud-core (1.7.1)
|
||||
google-cloud-core (1.8.0)
|
||||
google-cloud-env (>= 1.0, < 3.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (2.2.1)
|
||||
google-cloud-env (2.3.1)
|
||||
base64 (~> 0.2)
|
||||
faraday (>= 1.0, < 3.a)
|
||||
google-cloud-errors (1.4.0)
|
||||
google-cloud-storage (1.54.0)
|
||||
google-cloud-errors (1.6.0)
|
||||
google-cloud-storage (1.59.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
google-apis-core (~> 0.13)
|
||||
google-apis-core (>= 0.18, < 2)
|
||||
google-apis-iamcredentials_v1 (~> 0.18)
|
||||
google-apis-storage_v1 (~> 0.38)
|
||||
google-apis-storage_v1 (>= 0.42)
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (~> 1.9)
|
||||
mini_mime (~> 1.0)
|
||||
google-logging-utils (0.1.0)
|
||||
googleauth (1.12.2)
|
||||
google-logging-utils (0.2.0)
|
||||
googleauth (1.16.2)
|
||||
faraday (>= 1.0, < 3.a)
|
||||
google-cloud-env (~> 2.2)
|
||||
google-logging-utils (~> 0.1)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
jwt (>= 1.4, < 4.0)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
hashdiff (1.1.2)
|
||||
hexapdf (1.4.0)
|
||||
hashdiff (1.2.1)
|
||||
hexapdf (1.7.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.7)
|
||||
i18n (1.14.8)
|
||||
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.1)
|
||||
irb (1.15.2)
|
||||
io-console (0.8.2)
|
||||
irb (1.18.0)
|
||||
pp (>= 0.6.0)
|
||||
prism (>= 1.3.0)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
jmespath (1.6.2)
|
||||
json (2.15.0)
|
||||
jwt (2.9.3)
|
||||
json (2.19.8)
|
||||
jwt (3.2.0)
|
||||
base64
|
||||
language_server-protocol (3.17.0.5)
|
||||
launchy (3.0.1)
|
||||
launchy (3.1.1)
|
||||
addressable (~> 2.8)
|
||||
childprocess (~> 5.0)
|
||||
logger (~> 1.6)
|
||||
letter_opener (1.10.0)
|
||||
launchy (>= 2.2, < 4)
|
||||
letter_opener_web (3.0.0)
|
||||
@@ -315,29 +293,27 @@ GEM
|
||||
activesupport (>= 4)
|
||||
railties (>= 4)
|
||||
request_store (~> 1.0)
|
||||
loofah (2.24.1)
|
||||
loofah (2.25.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
mail (2.8.1)
|
||||
mail (2.9.0)
|
||||
logger
|
||||
mini_mime (>= 0.1.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
marcel (1.0.4)
|
||||
matrix (0.4.2)
|
||||
marcel (1.1.0)
|
||||
matrix (0.4.3)
|
||||
method_source (1.1.0)
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.9)
|
||||
minitest (5.25.5)
|
||||
msgpack (1.7.5)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.4.1)
|
||||
mutex_m (0.3.0)
|
||||
mysql2 (0.5.6)
|
||||
net-http-persistent (4.0.5)
|
||||
connection_pool (~> 2.2)
|
||||
net-imap (0.5.9)
|
||||
minitest (6.0.6)
|
||||
drb (~> 2.0)
|
||||
prism (~> 1.5)
|
||||
msgpack (1.8.0)
|
||||
multi_json (1.19.1)
|
||||
net-http (0.9.1)
|
||||
uri (>= 0.11.1)
|
||||
net-imap (0.6.4)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
@@ -346,124 +322,113 @@ GEM
|
||||
timeout
|
||||
net-smtp (0.5.1)
|
||||
net-protocol
|
||||
nio4r (2.7.4)
|
||||
nokogiri (1.18.9)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
nio4r (2.7.5)
|
||||
nokogiri (1.19.3-aarch64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.9-aarch64-linux-gnu)
|
||||
nokogiri (1.19.3-aarch64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.9-aarch64-linux-musl)
|
||||
nokogiri (1.19.3-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.9-arm64-darwin)
|
||||
nokogiri (1.19.3-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.9-x86_64-linux-gnu)
|
||||
nokogiri (1.19.3-x86_64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.9-x86_64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
numo-narray (0.9.2.1)
|
||||
oj (3.16.11)
|
||||
bigdecimal (>= 3.0)
|
||||
ostruct (>= 0.2)
|
||||
onnxruntime (0.10.1)
|
||||
ffi
|
||||
numo-narray-alt (0.10.3)
|
||||
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)
|
||||
openssl (4.0.1)
|
||||
orm_adapter (0.5.0)
|
||||
os (1.1.4)
|
||||
ostruct (0.6.3)
|
||||
package_json (0.1.0)
|
||||
pagy (9.3.3)
|
||||
parallel (1.27.0)
|
||||
parser (3.3.9.0)
|
||||
package_json (0.2.0)
|
||||
pagy (43.4.4)
|
||||
json
|
||||
uri
|
||||
yaml
|
||||
parallel (1.28.0)
|
||||
parser (3.3.11.1)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
pg (1.5.9)
|
||||
pp (0.6.2)
|
||||
pg (1.6.3-aarch64-linux)
|
||||
pg (1.6.3-aarch64-linux-musl)
|
||||
pg (1.6.3-arm64-darwin)
|
||||
pg (1.6.3-x86_64-linux)
|
||||
pg (1.6.3-x86_64-linux-musl)
|
||||
pp (0.6.3)
|
||||
prettyprint
|
||||
premailer (1.27.0)
|
||||
addressable
|
||||
css_parser (>= 1.19.0)
|
||||
htmlentities (>= 4.0.0)
|
||||
premailer-rails (1.12.0)
|
||||
actionmailer (>= 3)
|
||||
net-smtp
|
||||
premailer (~> 1.7, >= 1.7.9)
|
||||
pretender (0.5.0)
|
||||
actionpack (>= 6.1)
|
||||
pretender (1.0.0)
|
||||
actionpack (>= 7.2)
|
||||
prettyprint (0.2.0)
|
||||
prism (1.5.1)
|
||||
pry (0.15.0)
|
||||
prism (1.9.0)
|
||||
pry (0.16.0)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
reline (>= 0.6.0)
|
||||
pry-rails (0.3.11)
|
||||
pry (>= 0.13.0)
|
||||
psych (5.2.6)
|
||||
psych (5.3.1)
|
||||
date
|
||||
stringio
|
||||
public_suffix (6.0.1)
|
||||
puma (6.5.0)
|
||||
public_suffix (7.0.5)
|
||||
puma (7.2.0)
|
||||
nio4r (~> 2.0)
|
||||
racc (1.8.1)
|
||||
rack (3.2.3)
|
||||
rack (3.2.6)
|
||||
rack-proxy (0.7.7)
|
||||
rack
|
||||
rack-session (2.1.1)
|
||||
rack-session (2.1.2)
|
||||
base64 (>= 0.1.0)
|
||||
rack (>= 3.0.0)
|
||||
rack-test (2.2.0)
|
||||
rack (>= 1.3)
|
||||
rackup (2.2.1)
|
||||
rackup (2.3.1)
|
||||
rack (>= 3)
|
||||
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)
|
||||
rails (8.1.3)
|
||||
actioncable (= 8.1.3)
|
||||
actionmailbox (= 8.1.3)
|
||||
actionmailer (= 8.1.3)
|
||||
actionpack (= 8.1.3)
|
||||
actiontext (= 8.1.3)
|
||||
actionview (= 8.1.3)
|
||||
activejob (= 8.1.3)
|
||||
activemodel (= 8.1.3)
|
||||
activerecord (= 8.1.3)
|
||||
activestorage (= 8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 8.0.2.1)
|
||||
railties (= 8.1.3)
|
||||
rails-dom-testing (2.3.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.6.2)
|
||||
loofah (~> 2.21)
|
||||
rails-html-sanitizer (1.7.0)
|
||||
loofah (~> 2.25)
|
||||
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||
rails-i18n (8.0.1)
|
||||
rails-i18n (8.1.0)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 8.0.0, < 9)
|
||||
rails_autolink (1.1.8)
|
||||
actionview (> 3.1)
|
||||
activesupport (> 3.1)
|
||||
railties (> 3.1)
|
||||
railties (8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
railties (8.1.3)
|
||||
actionpack (= 8.1.3)
|
||||
activesupport (= 8.1.3)
|
||||
irb (~> 1.13)
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
tsort (>= 0.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.3.0)
|
||||
rdoc (6.14.2)
|
||||
rake (13.4.2)
|
||||
rdoc (7.2.0)
|
||||
erb
|
||||
psych (>= 4.0.0)
|
||||
redis-client (0.23.0)
|
||||
tsort
|
||||
redis-client (0.29.0)
|
||||
connection_pool
|
||||
regexp_parser (2.11.3)
|
||||
reline (0.6.2)
|
||||
reline (0.6.3)
|
||||
io-console (~> 0.5)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
@@ -471,35 +436,35 @@ GEM
|
||||
uber (< 0.2.0)
|
||||
request_store (1.7.0)
|
||||
rack (>= 1.4)
|
||||
responders (3.1.1)
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
retriable (3.1.2)
|
||||
responders (3.2.0)
|
||||
actionpack (>= 7.0)
|
||||
railties (>= 7.0)
|
||||
retriable (3.4.1)
|
||||
rexml (3.4.4)
|
||||
rotp (6.3.0)
|
||||
rouge (4.5.2)
|
||||
rqrcode (2.2.0)
|
||||
rouge (4.7.0)
|
||||
rqrcode (3.2.0)
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 1.0)
|
||||
rqrcode_core (1.2.0)
|
||||
rspec-core (3.13.2)
|
||||
rqrcode_core (~> 2.0)
|
||||
rqrcode_core (2.1.0)
|
||||
rspec-core (3.13.6)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-expectations (3.13.3)
|
||||
rspec-expectations (3.13.5)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-mocks (3.13.2)
|
||||
rspec-mocks (3.13.8)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-rails (7.1.0)
|
||||
actionpack (>= 7.0)
|
||||
activesupport (>= 7.0)
|
||||
railties (>= 7.0)
|
||||
rspec-core (~> 3.13)
|
||||
rspec-expectations (~> 3.13)
|
||||
rspec-mocks (~> 3.13)
|
||||
rspec-support (~> 3.13)
|
||||
rspec-support (3.13.2)
|
||||
rubocop (1.81.1)
|
||||
rspec-rails (8.0.4)
|
||||
actionpack (>= 7.2)
|
||||
activesupport (>= 7.2)
|
||||
railties (>= 7.2)
|
||||
rspec-core (>= 3.13.0, < 5.0.0)
|
||||
rspec-expectations (>= 3.13.0, < 5.0.0)
|
||||
rspec-mocks (>= 3.13.0, < 5.0.0)
|
||||
rspec-support (>= 3.13.0, < 5.0.0)
|
||||
rspec-support (3.13.7)
|
||||
rubocop (1.86.0)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
@@ -507,122 +472,127 @@ GEM
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.47.1, < 2.0)
|
||||
rubocop-ast (>= 1.49.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.47.1)
|
||||
rubocop-ast (1.49.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)
|
||||
rubocop-rails (2.27.0)
|
||||
prism (~> 1.7)
|
||||
rubocop-performance (1.26.1)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (>= 1.75.0, < 2.0)
|
||||
rubocop-ast (>= 1.47.1, < 2.0)
|
||||
rubocop-rails (2.34.3)
|
||||
activesupport (>= 4.2.0)
|
||||
lint_roller (~> 1.1)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.52.0, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
rubocop-rspec (3.3.0)
|
||||
rubocop (~> 1.61)
|
||||
rubocop (>= 1.75.0, < 2.0)
|
||||
rubocop-ast (>= 1.44.0, < 2.0)
|
||||
rubocop-rspec (3.9.0)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (~> 1.81)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-vips (2.2.2)
|
||||
ruby-vips (2.3.0)
|
||||
ffi (~> 1.12)
|
||||
logger
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyXL (3.4.33)
|
||||
rubyXL (3.4.35)
|
||||
nokogiri (>= 1.10.8)
|
||||
rubyzip (>= 1.3.0)
|
||||
rubyzip (2.3.2)
|
||||
rubyzip (>= 3.2.2)
|
||||
rubyzip (3.2.2)
|
||||
securerandom (0.4.1)
|
||||
semantic_range (3.1.0)
|
||||
shakapacker (8.0.2)
|
||||
semantic_range (3.1.1)
|
||||
shakapacker (9.7.0)
|
||||
activesupport (>= 5.2)
|
||||
package_json
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 5.2)
|
||||
semantic_range (>= 2.3.0)
|
||||
sidekiq (7.3.7)
|
||||
connection_pool (>= 2.3.0)
|
||||
logger
|
||||
rack (>= 2.2.4)
|
||||
redis-client (>= 0.22.2)
|
||||
signet (0.19.0)
|
||||
sidekiq (8.1.6)
|
||||
connection_pool (>= 3.0.0)
|
||||
json (>= 2.16.0)
|
||||
logger (>= 1.7.0)
|
||||
rack (>= 3.2.0)
|
||||
redis-client (>= 0.29.0)
|
||||
signet (0.21.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
jwt (>= 1.5, < 4.0)
|
||||
multi_json (~> 1.10)
|
||||
simplecov (0.22.0)
|
||||
docile (~> 1.1)
|
||||
simplecov-html (~> 0.11)
|
||||
simplecov_json_formatter (~> 0.1)
|
||||
simplecov-html (0.13.1)
|
||||
simplecov-html (0.13.2)
|
||||
simplecov_json_formatter (0.1.4)
|
||||
smart_properties (1.17.0)
|
||||
sqlite3 (2.5.0)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
sqlite3 (2.5.0-aarch64-linux-gnu)
|
||||
sqlite3 (2.5.0-aarch64-linux-musl)
|
||||
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.7)
|
||||
strip_attributes (1.14.1)
|
||||
sqlite3 (2.9.2-aarch64-linux-gnu)
|
||||
sqlite3 (2.9.2-aarch64-linux-musl)
|
||||
sqlite3 (2.9.2-arm64-darwin)
|
||||
sqlite3 (2.9.2-x86_64-linux-gnu)
|
||||
sqlite3 (2.9.2-x86_64-linux-musl)
|
||||
stringio (3.2.0)
|
||||
strip_attributes (2.0.1)
|
||||
activemodel (>= 3.0, < 9.0)
|
||||
strscan (3.1.5)
|
||||
thor (1.4.0)
|
||||
timeout (0.4.3)
|
||||
strscan (3.1.8)
|
||||
thor (1.5.0)
|
||||
timeout (0.6.1)
|
||||
trailblazer-option (0.1.2)
|
||||
turbo-rails (2.0.11)
|
||||
actionpack (>= 6.0.0)
|
||||
railties (>= 6.0.0)
|
||||
twitter_cldr (6.12.1)
|
||||
trilogy (2.12.2)
|
||||
bigdecimal
|
||||
tsort (0.2.0)
|
||||
turbo-rails (2.0.23)
|
||||
actionpack (>= 7.1.0)
|
||||
railties (>= 7.1.0)
|
||||
twitter_cldr (6.14.0)
|
||||
base64
|
||||
camertron-eprun
|
||||
cldr-plurals-runtime-rb (~> 1.1)
|
||||
tzinfo
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
tzinfo-data (1.2024.2)
|
||||
tzinfo-data (1.2026.1)
|
||||
tzinfo (>= 1.0.0)
|
||||
uber (0.1.0)
|
||||
unicode-display_width (3.2.0)
|
||||
unicode-emoji (~> 4.1)
|
||||
unicode-emoji (4.1.0)
|
||||
uniform_notifier (1.16.0)
|
||||
unicode-emoji (4.2.0)
|
||||
uniform_notifier (1.18.0)
|
||||
uri (1.1.1)
|
||||
useragent (0.16.11)
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
web-console (4.2.1)
|
||||
actionview (>= 6.0.0)
|
||||
activemodel (>= 6.0.0)
|
||||
web-console (4.3.0)
|
||||
actionview (>= 8.0.0)
|
||||
bindex (>= 0.4.0)
|
||||
railties (>= 6.0.0)
|
||||
webmock (3.24.0)
|
||||
railties (>= 8.0.0)
|
||||
webmock (3.26.2)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webrick (1.9.1)
|
||||
webrick (1.9.2)
|
||||
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.3)
|
||||
yaml (0.4.0)
|
||||
zeitwerk (2.7.5)
|
||||
|
||||
PLATFORMS
|
||||
aarch64-linux
|
||||
aarch64-linux-musl
|
||||
arm64-darwin
|
||||
ruby
|
||||
x86_64-linux
|
||||
x86_64-linux-musl
|
||||
|
||||
DEPENDENCIES
|
||||
addressable
|
||||
annotaterb
|
||||
arabic-letter-connector
|
||||
aws-sdk-s3
|
||||
aws-sdk-secretsmanager
|
||||
azure-storage-blob
|
||||
azure-blob
|
||||
better_html
|
||||
bootsnap
|
||||
brakeman
|
||||
@@ -645,24 +615,19 @@ DEPENDENCIES
|
||||
foreman
|
||||
google-cloud-storage
|
||||
hexapdf
|
||||
image_processing
|
||||
jwt
|
||||
letter_opener_web
|
||||
lograge
|
||||
mysql2
|
||||
numo-narray
|
||||
oj
|
||||
numo-narray-alt
|
||||
onnxruntime
|
||||
pagy
|
||||
pg
|
||||
premailer-rails
|
||||
pretender
|
||||
pry-rails
|
||||
puma
|
||||
rack
|
||||
rails
|
||||
rails-i18n
|
||||
rails_autolink
|
||||
rotp
|
||||
rouge
|
||||
rqrcode
|
||||
@@ -678,6 +643,7 @@ DEPENDENCIES
|
||||
simplecov
|
||||
sqlite3
|
||||
strip_attributes
|
||||
trilogy
|
||||
turbo-rails
|
||||
twitter_cldr
|
||||
tzinfo-data
|
||||
@@ -685,7 +651,7 @@ DEPENDENCIES
|
||||
webmock
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.4.2p28
|
||||
ruby 4.0.5
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.3
|
||||
4.0.3
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
Additional Terms
|
||||
|
||||
In accordance with Section 7(b) of the GNU Affero General Public License,
|
||||
a covered work must retain the original DocuSeal attribution in interactive
|
||||
user interfaces.
|
||||
@@ -1,7 +1,7 @@
|
||||
<h1 align="center" style="border-bottom: none">
|
||||
<div>
|
||||
<a href="https://www.docuseal.com">
|
||||
<img alt="DocuSeal" src="https://github.com/docusealco/docuseal/assets/5418788/c12cd051-81cd-4402-bc3a-92f2cfdc1b06" width="80" />
|
||||
<img alt="DocuSeal" src="https://github.com/user-attachments/assets/38b45682-ffa4-4919-abde-d2d422325c44" width="80" />
|
||||
<br>
|
||||
</a>
|
||||
DocuSeal
|
||||
@@ -50,7 +50,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
||||
- Company logo and white-label
|
||||
- User roles
|
||||
- Automated reminders
|
||||
- Invitation and identify verification via SMS
|
||||
- Invitation and identity verification via SMS
|
||||
- Conditional fields and formulas
|
||||
- Bulk send with CSV, XLSX spreadsheet import
|
||||
- SSO / SAML
|
||||
@@ -64,7 +64,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
||||
|
||||
|Heroku|Railway|
|
||||
|:--:|:---:|
|
||||
| [<img alt="Deploy on Heroku" src="https://www.herokucdn.com/deploy/button.svg" height="40">](https://heroku.com/deploy?template=https://github.com/docusealco/docuseal-heroku) | [<img alt="Deploy on Railway" src="https://railway.app/button.svg" height="40">](https://railway.app/template/IGoDnc?referralCode=ruU7JR)|
|
||||
| [<img alt="Deploy on Heroku" src="https://www.herokucdn.com/deploy/button.svg" height="40">](https://heroku.com/deploy?template=https://github.com/docusealco/docuseal-heroku) | [<img alt="Deploy on Railway" src="https://railway.app/button.svg" height="40">](https://railway.com/deploy/IGoDnc?referralCode=ruU7JR)|
|
||||
|**DigitalOcean**|**Render**|
|
||||
| [<img alt="Deploy on DigitalOcean" src="https://www.deploytodo.com/do-btn-blue.svg" height="40">](https://cloud.digitalocean.com/apps/new?repo=https://github.com/docusealco/docuseal-digitalocean/tree/master&refcode=421d50f53990) | [<img alt="Deploy to Render" src="https://render.com/images/deploy-to-render-button.svg" height="40">](https://render.com/deploy?repo=https://github.com/docusealco/docuseal-render)
|
||||
|
||||
@@ -74,7 +74,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
||||
docker run --name docuseal -p 3000:3000 -v.:/data docuseal/docuseal
|
||||
```
|
||||
|
||||
By default DocuSeal docker container uses an SQLite database to store data and configurations. Alternatively, it is possible use PostgreSQL or MySQL databases by specifying the `DATABASE_URL` env variable.
|
||||
By default DocuSeal docker container uses an SQLite database to store data and configurations. Alternatively, it is possible to use PostgreSQL or MySQL databases by specifying the `DATABASE_URL` env variable.
|
||||
|
||||
#### Docker Compose
|
||||
|
||||
@@ -97,8 +97,8 @@ At DocuSeal we have expertise and technologies to make documents creation, filli
|
||||
|
||||
## License
|
||||
|
||||
Distributed under the AGPLv3 License. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) for more information.
|
||||
Unless otherwise noted, all files © 2023 DocuSeal LLC.
|
||||
Distributed under the AGPLv3 License with Section 7(b) Additional Terms. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) and [LICENSE_ADDITIONAL_TERMS](https://github.com/docusealco/docuseal/blob/master/LICENSE_ADDITIONAL_TERMS) for more information.
|
||||
Unless otherwise noted, all files © 2023-2026 DocuSeal LLC.
|
||||
|
||||
## Tools
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ class AccountConfigsController < ApplicationController
|
||||
AccountConfig::FORCE_MFA,
|
||||
AccountConfig::ALLOW_TO_RESUBMIT,
|
||||
AccountConfig::ALLOW_TO_DECLINE_KEY,
|
||||
AccountConfig::ALLOW_TO_DELEGATE_KEY,
|
||||
AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
|
||||
AccountConfig::ESIGNING_PREFERENCE_KEY,
|
||||
AccountConfig::FORM_WITH_CONFETTI_KEY,
|
||||
@@ -23,7 +24,8 @@ class AccountConfigsController < ApplicationController
|
||||
AccountConfig::WITH_SIGNATURE_ID,
|
||||
AccountConfig::COMBINE_PDF_RESULT_KEY,
|
||||
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
|
||||
AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY
|
||||
AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY,
|
||||
AccountConfig::ENABLE_MCP_KEY
|
||||
].freeze
|
||||
|
||||
InvalidKey = Class.new(StandardError)
|
||||
@@ -35,7 +37,7 @@ class AccountConfigsController < ApplicationController
|
||||
end
|
||||
|
||||
def destroy
|
||||
raise InvalidKey unless ALLOWED_KEYS.include?(@account_config.key)
|
||||
raise InvalidKey unless allowed_keys.include?(@account_config.key)
|
||||
|
||||
@account_config.destroy!
|
||||
|
||||
@@ -44,8 +46,12 @@ class AccountConfigsController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def allowed_keys
|
||||
ALLOWED_KEYS
|
||||
end
|
||||
|
||||
def load_account_config
|
||||
raise InvalidKey unless ALLOWED_KEYS.include?(account_config_params[:key])
|
||||
raise InvalidKey unless allowed_keys.include?(account_config_params[:key])
|
||||
|
||||
@account_config =
|
||||
AccountConfig.find_or_initialize_by(account: current_account, key: account_config_params[:key])
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AccountCustomFieldsController < ApplicationController
|
||||
before_action :load_account_config, only: :create
|
||||
|
||||
def create
|
||||
authorize!(:create, Template)
|
||||
|
||||
@account_config.update!(account_config_params)
|
||||
|
||||
render json: @account_config.value
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_account_config
|
||||
@account_config =
|
||||
AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY)
|
||||
end
|
||||
|
||||
def account_config_params
|
||||
params.permit(
|
||||
value: [[:uuid, :name, :type,
|
||||
:required, :readonly, :default_value,
|
||||
:title, :description,
|
||||
{ preferences: {},
|
||||
default_value: [],
|
||||
options: [%i[value uuid]],
|
||||
validation: %i[message pattern min max step],
|
||||
areas: [%i[x y w h cell_w option_uuid]] }]]
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -53,7 +53,9 @@ class AccountsController < ApplicationController
|
||||
# rubocop:disable Layout/LineLength
|
||||
render turbo_stream: turbo_stream.replace(
|
||||
:account_delete_button,
|
||||
html: helpers.tag.p(I18n.t('your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account'))
|
||||
html: helpers.tag.p(I18n.t('your_account_will_be_permanently_deleted_within_2_months_please_contact_us_if_you_want_to_keep_your_account')) +
|
||||
helpers.tag.br +
|
||||
helpers.tag.p(I18n.t('your_email_address_has_been_released_immediately_you_can_now_be_added_to_your_company_team_without_waiting_for_the_deletion_period_to_end'))
|
||||
)
|
||||
# rubocop:enable Layout/LineLength
|
||||
end
|
||||
|
||||
@@ -9,7 +9,9 @@ module Api
|
||||
|
||||
before_action :set_cors_headers
|
||||
before_action :set_noindex_headers
|
||||
before_action :set_security_headers
|
||||
|
||||
# rubocop:disable Metrics
|
||||
def show
|
||||
blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
|
||||
|
||||
@@ -21,6 +23,12 @@ module Api
|
||||
|
||||
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
|
||||
|
||||
if Submitters::DANGEROUS_EXTENSIONS.include?(blob.filename.extension.to_s.downcase)
|
||||
Rollbar.error('Dangerous extension') if defined?(Rollbar)
|
||||
|
||||
return head :unprocessable_content
|
||||
end
|
||||
|
||||
attachment = blob.attachments.take
|
||||
|
||||
@record = attachment.record
|
||||
@@ -33,12 +41,19 @@ module Api
|
||||
else
|
||||
http_cache_forever public: true do
|
||||
response.headers['Accept-Ranges'] = 'bytes'
|
||||
response.headers['Content-Length'] = blob.byte_size.to_s
|
||||
|
||||
send_blob_stream blob, disposition: params[:disposition]
|
||||
if request.head?
|
||||
response.headers['Content-Type'] = blob.content_type_for_serving
|
||||
head :ok
|
||||
else
|
||||
send_blob_stream blob, disposition: params[:disposition]
|
||||
end
|
||||
|
||||
response.headers['Content-Length'] = blob.byte_size.to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics
|
||||
|
||||
private
|
||||
|
||||
@@ -57,8 +72,6 @@ module Api
|
||||
return if !require_ttl && !require_auth
|
||||
end
|
||||
|
||||
Rollbar.error('Blob unauthorized') if defined?(Rollbar)
|
||||
|
||||
raise CanCan::AccessDenied
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,6 +9,7 @@ module Api
|
||||
|
||||
before_action :set_cors_headers
|
||||
before_action :set_noindex_headers
|
||||
before_action :set_security_headers
|
||||
|
||||
# rubocop:disable Metrics
|
||||
def show
|
||||
@@ -18,6 +19,12 @@ module Api
|
||||
|
||||
return head :not_found unless blob
|
||||
|
||||
if Submitters::DANGEROUS_EXTENSIONS.include?(blob.filename.extension.to_s.downcase)
|
||||
Rollbar.error('Dangerous extension') if defined?(Rollbar)
|
||||
|
||||
return head :unprocessable_content
|
||||
end
|
||||
|
||||
is_permitted = blob.attachments.any? do |a|
|
||||
(current_user && a.record.account.id == current_user.account_id) ||
|
||||
a.record.account.account_configs.any? { |e| e.key == 'legacy_blob_proxy' } ||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
module Api
|
||||
class ApiBaseController < ActionController::API
|
||||
include ActiveStorage::SetCurrent
|
||||
include Pagy::Backend
|
||||
include Pagy::Method
|
||||
|
||||
DEFAULT_LIMIT = 10
|
||||
MAX_LIMIT = 100
|
||||
@@ -102,6 +102,10 @@ module Api
|
||||
headers['X-Robots-Tag'] = 'noindex'
|
||||
end
|
||||
|
||||
def set_security_headers
|
||||
response.headers['X-Content-Type-Options'] = 'nosniff'
|
||||
end
|
||||
|
||||
def set_cors_headers
|
||||
headers['Access-Control-Allow-Origin'] = '*'
|
||||
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
|
||||
|
||||
@@ -8,29 +8,37 @@ module Api
|
||||
COOKIE_STORE_LIMIT = 10
|
||||
|
||||
def create
|
||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||
|
||||
unless can_upload?(@submitter)
|
||||
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
file = params[:file]
|
||||
|
||||
if params[:type].in?(%w[initials signature])
|
||||
image = Vips::Image.new_from_file(params[:file].path)
|
||||
image = ImageUtils.load_vips(file.read, content_type: file.content_type)
|
||||
|
||||
if ImageUtils.blank?(image)
|
||||
Rollbar.error("Empty signature: #{submitter.id}") if defined?(Rollbar)
|
||||
Rollbar.error("Empty signature: #{@submitter.id}") if defined?(Rollbar)
|
||||
|
||||
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
if ImageUtils.error?(image)
|
||||
Rollbar.error("Error signature: #{submitter.id}") if defined?(Rollbar)
|
||||
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
|
||||
|
||||
metadata = { analyzed: true, identified: true, width: image.width, height: image.height }
|
||||
end
|
||||
|
||||
attachment = Submitters.create_attachment!(submitter, params)
|
||||
attachment = Submitters.create_attachment!(@submitter, file, metadata:)
|
||||
|
||||
if params[:remember_signature] == 'true' && submitter.email.present?
|
||||
cookies.encrypted[:signature_uuids] = build_new_cookie_signatures_json(submitter, attachment)
|
||||
if params[:remember_signature] == 'true' && @submitter.email.present?
|
||||
cookies.encrypted[:signature_uuids] = build_new_cookie_signatures_json(@submitter, attachment)
|
||||
end
|
||||
|
||||
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
|
||||
@@ -40,6 +48,14 @@ module Api
|
||||
render json: { error: e.message }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
def can_upload?(submitter)
|
||||
!submitter.declined_at? &&
|
||||
!submitter.completed_at? &&
|
||||
!submitter.submission.archived_at? &&
|
||||
!submitter.submission.expired? &&
|
||||
!submitter.submission.template&.archived_at?
|
||||
end
|
||||
|
||||
def build_new_cookie_signatures_json(submitter, attachment)
|
||||
values =
|
||||
begin
|
||||
|
||||
@@ -156,7 +156,7 @@ module Api
|
||||
params:)
|
||||
else
|
||||
submissions_attrs, attachments =
|
||||
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_params, template)
|
||||
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_params, template, purpose: :api)
|
||||
|
||||
submissions = Submissions.create_from_submitters(
|
||||
template:,
|
||||
@@ -172,7 +172,10 @@ module Api
|
||||
Submissions::NormalizeParamUtils.save_default_value_attachments!(attachments, submitters)
|
||||
|
||||
submitters.each do |submitter|
|
||||
SubmissionEvents.create_with_tracking_data(submitter, 'api_complete_form', request) if submitter.completed_at?
|
||||
if submitter.completed_at?
|
||||
Submitters::SubmitValues.maybe_invite_via_field(submitter, request)
|
||||
SubmissionEvents.create_with_tracking_data(submitter, 'api_complete_form', request)
|
||||
end
|
||||
end
|
||||
|
||||
submissions
|
||||
@@ -188,7 +191,7 @@ module Api
|
||||
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_email_2fa, :order, :invite_by,
|
||||
:require_phone_2fa, :require_email_2fa, :order, :index, :invite_by,
|
||||
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
|
||||
fields: [:name, :uuid, :default_value, :value, :title, :description,
|
||||
:readonly, :required, :validation_pattern, :invalid_message,
|
||||
|
||||
@@ -34,18 +34,24 @@ module Api
|
||||
render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true, params:)
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
def update
|
||||
if @submitter.completed_at?
|
||||
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
if @submitter.declined_at?
|
||||
return render json: { error: 'Submitter has already declined the submission.' }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
submission = @submitter.submission
|
||||
role = submission.template_submitters.find { |e| e['uuid'] == @submitter.uuid }['name']
|
||||
|
||||
normalized_params, new_attachments = Submissions::NormalizeParamUtils.normalize_submitter_params!(
|
||||
submitter_params.merge(role:),
|
||||
@submitter.template || Template.new(submitters: submission.template_submitters, account: @submitter.account),
|
||||
for_submitter: @submitter
|
||||
for_submitter: @submitter,
|
||||
purpose: :api
|
||||
)
|
||||
|
||||
Submissions::CreateFromSubmitters.maybe_set_template_fields(submission, [normalized_params],
|
||||
@@ -60,7 +66,10 @@ module Api
|
||||
|
||||
@submitter.submission.save!
|
||||
|
||||
SubmissionEvents.create_with_tracking_data(@submitter, 'api_complete_form', request) if @submitter.completed_at?
|
||||
if @submitter.completed_at?
|
||||
Submitters::SubmitValues.maybe_invite_via_field(@submitter, request)
|
||||
SubmissionEvents.create_with_tracking_data(@submitter, 'api_complete_form', request)
|
||||
end
|
||||
end
|
||||
|
||||
if @submitter.completed_at?
|
||||
@@ -78,6 +87,7 @@ module Api
|
||||
|
||||
render json: { error: e.message }, status: :unprocessable_content
|
||||
end
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
|
||||
def submitter_params
|
||||
submitter_params = params.key?(:submitter) ? params.require(:submitter) : params
|
||||
@@ -151,9 +161,7 @@ module Api
|
||||
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
|
||||
end
|
||||
|
||||
submitter.values = submitter.values.transform_values do |v|
|
||||
v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v
|
||||
end
|
||||
submitter.values = Submitters::SubmitValues.replace_current_date_placeholders(submitter)
|
||||
end
|
||||
|
||||
submitter
|
||||
@@ -195,10 +203,15 @@ module Api
|
||||
|
||||
submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms')
|
||||
submitter.preferences['reply_to'] = submitter_preferences['reply_to'] if submitter_preferences.key?('reply_to')
|
||||
|
||||
if submitter_preferences.key?('require_phone_2fa')
|
||||
submitter.preferences['require_phone_2fa'] = submitter_preferences['require_phone_2fa']
|
||||
end
|
||||
|
||||
if submitter_preferences.key?('require_email_2fa')
|
||||
submitter.preferences['require_email_2fa'] = submitter_preferences['require_email_2fa']
|
||||
end
|
||||
|
||||
if submitter_preferences.key?('go_to_last')
|
||||
submitter.preferences['go_to_last'] = submitter_preferences['go_to_last']
|
||||
end
|
||||
|
||||
@@ -9,7 +9,7 @@ module Api
|
||||
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [@template],
|
||||
associations: [schema_documents: :preview_images_attachments]
|
||||
associations: [{ schema_documents: :preview_images_attachments }]
|
||||
).call
|
||||
|
||||
cloned_template = Templates::Clone.call(
|
||||
|
||||
@@ -9,20 +9,7 @@ module Api
|
||||
|
||||
templates = paginate(templates.preload(:author, folder: :parent_folder))
|
||||
|
||||
schema_documents =
|
||||
ActiveStorage::Attachment.where(record_id: templates.map(&:id),
|
||||
record_type: 'Template',
|
||||
name: :documents,
|
||||
uuid: templates.flat_map { |t| t.schema.pluck('attachment_uuid') })
|
||||
.preload(:blob)
|
||||
|
||||
preview_image_attachments =
|
||||
ActiveStorage::Attachment.joins(:blob)
|
||||
.where(blob: { filename: ['0.png', '0.jpg'] })
|
||||
.where(record_id: schema_documents.map(&:id),
|
||||
record_type: 'ActiveStorage::Attachment',
|
||||
name: :preview_images)
|
||||
.preload(:blob)
|
||||
schema_documents, dynamic_documents, preview_image_attachments = preload_relations(templates)
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
@@ -30,6 +17,7 @@ module Api
|
||||
data: templates.map do |t|
|
||||
Templates::SerializeForApi.call(t,
|
||||
schema_documents: schema_documents.select { |e| e.record_id == t.id },
|
||||
dynamic_documents:,
|
||||
preview_image_attachments:,
|
||||
expires_at:)
|
||||
end,
|
||||
@@ -69,6 +57,7 @@ module Api
|
||||
SearchEntries.enqueue_reindex(@template)
|
||||
|
||||
WebhookUrls.enqueue_events(@template, 'template.updated')
|
||||
WebhookUrls.enqueue_events(@template, 'template.archived') if archived == true
|
||||
|
||||
render json: @template.as_json(only: %i[id updated_at])
|
||||
end
|
||||
@@ -78,6 +67,8 @@ module Api
|
||||
@template.destroy!
|
||||
else
|
||||
@template.update!(archived_at: Time.current)
|
||||
|
||||
WebhookUrls.enqueue_events(@template, 'template.archived')
|
||||
end
|
||||
|
||||
render json: @template.as_json(only: %i[id archived_at])
|
||||
@@ -85,6 +76,41 @@ module Api
|
||||
|
||||
private
|
||||
|
||||
def preload_relations(templates)
|
||||
schema_documents =
|
||||
ActiveStorage::Attachment.where(record_id: templates.map(&:id),
|
||||
record_type: 'Template',
|
||||
name: :documents,
|
||||
uuid: templates.flat_map { |t| t.schema.pluck('attachment_uuid') })
|
||||
.preload(:blob)
|
||||
|
||||
dynamic_document_uuids =
|
||||
templates.flat_map { |t| t.schema.select { |item| item['dynamic'] }.pluck('attachment_uuid') }
|
||||
|
||||
dynamic_documents =
|
||||
if dynamic_document_uuids.present?
|
||||
DynamicDocument.where(template: templates.map(&:id))
|
||||
.where(uuid: dynamic_document_uuids)
|
||||
.preload(current_version: { document_attachment: :blob })
|
||||
.select(:id, :uuid, :template_id, :sha1, :created_at, :updated_at)
|
||||
else
|
||||
DynamicDocument.none
|
||||
end
|
||||
|
||||
preview_attachment_ids =
|
||||
schema_documents.map(&:id) + dynamic_documents.filter_map { |d| d.current_version&.document_attachment&.id }
|
||||
|
||||
preview_image_attachments =
|
||||
ActiveStorage::Attachment.joins(:blob)
|
||||
.where(blob: { filename: ['0.png', '0.jpg'] })
|
||||
.where(record_id: preview_attachment_ids,
|
||||
record_type: 'ActiveStorage::Attachment',
|
||||
name: :preview_images)
|
||||
.preload(:blob)
|
||||
|
||||
[schema_documents, dynamic_documents, preview_image_attachments]
|
||||
end
|
||||
|
||||
def filter_templates(templates, params)
|
||||
templates = Templates.search(current_user, templates, params[:q])
|
||||
templates = params[:archived].in?(['true', true]) ? templates.archived : templates.active
|
||||
@@ -107,7 +133,8 @@ module Api
|
||||
:external_id,
|
||||
:shared_link,
|
||||
{
|
||||
submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email order]],
|
||||
submitters: [%i[name uuid is_requester invite_by_uuid invite_via_field_uuid
|
||||
optional_invite_by_uuid linked_to_uuid email order]],
|
||||
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||
:required, :readonly, :default_value,
|
||||
:title, :description, :prefillable,
|
||||
@@ -116,7 +143,7 @@ module Api
|
||||
conditions: [%i[field_uuid value action operation]],
|
||||
options: [%i[value uuid]],
|
||||
validation: %i[message pattern min max step],
|
||||
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
|
||||
areas: [%i[uuid x y w h cell_w attachment_uuid option_uuid page]] }]]
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ class ApplicationController < ActionController::Base
|
||||
BROWSER_LOCALE_REGEXP = /\A\w{2}(?:-\w{2})?/
|
||||
|
||||
include ActiveStorage::SetCurrent
|
||||
include Pagy::Backend
|
||||
include Pagy::Method
|
||||
|
||||
check_authorization unless: :devise_controller?
|
||||
|
||||
@@ -23,7 +23,7 @@ class ApplicationController < ActionController::Base
|
||||
|
||||
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
|
||||
|
||||
rescue_from Pagy::OverflowError do
|
||||
rescue_from Pagy::RangeError do
|
||||
redirect_to request.path
|
||||
end
|
||||
|
||||
@@ -42,10 +42,6 @@ class ApplicationController < ActionController::Base
|
||||
end
|
||||
|
||||
def default_url_options
|
||||
if request.domain == 'docuseal.com'
|
||||
return { host: 'docuseal.com', protocol: ENV['FORCE_SSL'].present? ? 'https' : 'http' }
|
||||
end
|
||||
|
||||
Docuseal.default_url_options
|
||||
end
|
||||
|
||||
@@ -60,7 +56,7 @@ class ApplicationController < ActionController::Base
|
||||
|
||||
def pagy_auto(collection, **keyword_args)
|
||||
if current_ability.can?(:manage, :countless)
|
||||
pagy_countless(collection, **keyword_args)
|
||||
pagy(:countless, collection, **keyword_args)
|
||||
else
|
||||
pagy(collection, **keyword_args)
|
||||
end
|
||||
|
||||
@@ -40,7 +40,7 @@ class ErrorsController < ActionController::Base
|
||||
render json: { status: error_status_code, error: }.compact, status: error_status_code
|
||||
end
|
||||
|
||||
f.html { render error_status_code.to_s, status: error_status_code }
|
||||
f.any { render error_status_code.to_s, status: error_status_code }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -51,7 +51,7 @@ class ErrorsController < ActionController::Base
|
||||
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
|
||||
headers['Access-Control-Allow-Headers'] = '*'
|
||||
headers['Access-Control-Max-Age'] = '1728000'
|
||||
headers['Access-Control-Allow-Credentials'] = true
|
||||
headers['Access-Control-Allow-Credentials'] = 'true'
|
||||
end
|
||||
|
||||
def error_status_code
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
class EsignSettingsController < ApplicationController
|
||||
DEFAULT_CERT_NAME = 'DocuSeal Self-Host Autogenerated'
|
||||
|
||||
CertFormRecord = Struct.new(:name, :file, :password, keyword_init: true) do
|
||||
CertFormRecord = Struct.new(:name, :file, :password) do
|
||||
include ActiveModel::Validations
|
||||
|
||||
def to_key
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class McpController < ActionController::API
|
||||
before_action :authenticate_user!
|
||||
before_action :verify_mcp_enabled!
|
||||
|
||||
before_action do
|
||||
authorize!(:manage, :mcp)
|
||||
end
|
||||
|
||||
def call
|
||||
return head :ok if request.raw_post.blank?
|
||||
|
||||
body = JSON.parse(request.raw_post)
|
||||
|
||||
result = Mcp::HandleRequest.call(body, current_user, current_ability)
|
||||
|
||||
if result
|
||||
render json: result
|
||||
else
|
||||
head :accepted
|
||||
end
|
||||
rescue CanCan::AccessDenied
|
||||
render json: { jsonrpc: '2.0', id: nil, error: { code: -32_603, message: 'Forbidden' } }, status: :forbidden
|
||||
rescue JSON::ParserError
|
||||
render json: { jsonrpc: '2.0', id: nil, error: { code: -32_700, message: 'Parse error' } }, status: :bad_request
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authenticate_user!
|
||||
render json: { error: 'Not authenticated' }, status: :unauthorized unless current_user
|
||||
end
|
||||
|
||||
def verify_mcp_enabled!
|
||||
return if Docuseal.multitenant?
|
||||
|
||||
return if AccountConfig.exists?(account_id: current_user.account_id,
|
||||
key: AccountConfig::ENABLE_MCP_KEY,
|
||||
value: true)
|
||||
|
||||
render json: { error: 'MCP is disabled' }, status: :forbidden
|
||||
end
|
||||
|
||||
def current_user
|
||||
@current_user ||= user_from_api_key
|
||||
end
|
||||
|
||||
def user_from_api_key
|
||||
token = request.headers['Authorization'].to_s[/\ABearer\s+(.+)\z/, 1]
|
||||
|
||||
return if token.blank?
|
||||
|
||||
sha256 = Digest::SHA256.hexdigest(token)
|
||||
|
||||
User.joins(:mcp_tokens).active.find_by(mcp_tokens: { sha256:, archived_at: nil })
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class McpSettingsController < ApplicationController
|
||||
load_and_authorize_resource :mcp_token, parent: false
|
||||
|
||||
before_action do
|
||||
authorize!(:manage, :mcp)
|
||||
end
|
||||
|
||||
def index
|
||||
@mcp_tokens = @mcp_tokens.active.order(id: :desc)
|
||||
end
|
||||
|
||||
def create
|
||||
@mcp_token = current_user.mcp_tokens.new(mcp_token_params)
|
||||
|
||||
if @mcp_token.save
|
||||
@mcp_tokens = [@mcp_token]
|
||||
|
||||
render :index, status: :created
|
||||
else
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'mcp_settings/new'), status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@mcp_token.update!(archived_at: Time.current)
|
||||
|
||||
redirect_back fallback_location: settings_mcp_index_path, notice: I18n.t('mcp_token_has_been_removed')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mcp_token_params
|
||||
params.require(:mcp_token).permit(:name)
|
||||
end
|
||||
end
|
||||
@@ -6,9 +6,17 @@ class PreviewDocumentPageController < ActionController::API
|
||||
FORMAT = Templates::ProcessDocument::FORMAT
|
||||
|
||||
def show
|
||||
attachment_uuid = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid], purpose: :attachment)
|
||||
result_data =
|
||||
ApplicationRecord.signed_id_verifier.verified(params[:signed_key], purpose: :attachment)
|
||||
|
||||
attachment = ActiveStorage::Attachment.find_by(uuid: attachment_uuid) if attachment_uuid
|
||||
attachment =
|
||||
if result_data.is_a?(Array) && result_data.compact_blank.size == 2
|
||||
attachment_id, attachment_uuid = result_data
|
||||
|
||||
ActiveStorage::Attachment.find_by(id: attachment_id, uuid: attachment_uuid)
|
||||
elsif result_data
|
||||
ActiveStorage::Attachment.find_by(uuid: result_data)
|
||||
end
|
||||
|
||||
return head :not_found unless attachment
|
||||
|
||||
@@ -33,7 +41,7 @@ class PreviewDocumentPageController < ActionController::API
|
||||
end
|
||||
|
||||
def find_or_create_document_tempfile_path(attachment)
|
||||
file_path = "#{Dir.tmpdir}/#{attachment.uuid}"
|
||||
file_path = "#{Dir.tmpdir}/attachment-#{Digest::SHA1.hexdigest("#{attachment.id}-#{attachment.uuid}")}"
|
||||
|
||||
File.open(file_path, File::RDWR | File::CREAT, 0o644) do |f|
|
||||
f.flock(File::LOCK_EX)
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class RevealAccessTokenController < ApplicationController
|
||||
rate_limit to: 4, within: 1.minute, only: %i[create], by: -> { current_user.id }, with: lambda {
|
||||
Rollbar.error('Rate limit api key') if defined?(Rollbar)
|
||||
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'reveal_access_token/show',
|
||||
locals: { error_message: I18n.t(:too_many_attempts) }),
|
||||
status: :unprocessable_content
|
||||
}
|
||||
|
||||
def show
|
||||
authorize!(:manage, current_user.access_token)
|
||||
end
|
||||
|
||||
@@ -14,7 +14,7 @@ class SendSubmissionEmailController < ApplicationController
|
||||
template = Template.find_by!(slug: params[:template_slug])
|
||||
|
||||
@submitter =
|
||||
Submitter.completed.where(submission: template.submissions).find_by!(email: params[:email].to_s.downcase)
|
||||
Submitter.completed.where(submission: template.submissions).find_by(email: params[:email].to_s.downcase)
|
||||
elsif params[:submission_slug]
|
||||
submission = Submission.find_by(slug: params[:submission_slug])
|
||||
|
||||
@@ -27,9 +27,11 @@ class SendSubmissionEmailController < ApplicationController
|
||||
@submitter = Submitter.completed.find_by!(slug: params[:submitter_slug])
|
||||
end
|
||||
|
||||
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
||||
if @submitter
|
||||
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
||||
|
||||
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! if can_send?(@submitter)
|
||||
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! if can_send?(@submitter)
|
||||
end
|
||||
|
||||
respond_to do |f|
|
||||
f.html { render :success }
|
||||
|
||||
@@ -101,11 +101,18 @@ class StartFormController < ApplicationController
|
||||
def load_resubmit_submitter
|
||||
@resubmit_submitter =
|
||||
if params[:resubmit].present? && !params[:resubmit].in?([true, 'true'])
|
||||
Submitter.find_by(slug: params[:resubmit])
|
||||
submitter = Submitter.find_by(slug: params[:resubmit])
|
||||
|
||||
submitter if submitter && can_resubmit?(submitter)
|
||||
end
|
||||
end
|
||||
|
||||
def can_resubmit?(submitter)
|
||||
submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false
|
||||
end
|
||||
|
||||
def authorize_start!
|
||||
return redirect_to submit_form_path(@resubmit_submitter.slug) if @resubmit_submitter && @template.archived_at?
|
||||
return redirect_to start_form_path(@template.slug) if @template.archived_at?
|
||||
|
||||
return if @resubmit_submitter
|
||||
@@ -172,6 +179,8 @@ class StartFormController < ApplicationController
|
||||
submitters: [submitter],
|
||||
source: :link)
|
||||
|
||||
Submissions::CreateFromSubmitters.maybe_set_dynamic_documents(submitter.submission)
|
||||
|
||||
submitter.account_id = submitter.submission.account_id
|
||||
|
||||
submitter
|
||||
|
||||
@@ -16,6 +16,7 @@ class SubmissionEventsController < ApplicationController
|
||||
'email_verified' => 'email_check',
|
||||
'click_sms' => 'hand_click',
|
||||
'decline_form' => 'x',
|
||||
'delegate_form' => 'user_share',
|
||||
'start_verification' => 'player_play',
|
||||
'complete_verification' => 'check',
|
||||
'invite_party' => 'user_plus'
|
||||
|
||||
@@ -26,7 +26,7 @@ class SubmissionsController < ApplicationController
|
||||
unless @submission.submitters.all?(&:completed_at?)
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [@submission],
|
||||
associations: [submitters: :start_form_submission_events]
|
||||
associations: [{ submitters: :start_form_submission_events }]
|
||||
).call
|
||||
end
|
||||
|
||||
@@ -51,18 +51,7 @@ class SubmissionsController < ApplicationController
|
||||
emails: params[:emails],
|
||||
params: params.merge('send_completed_email' => true))
|
||||
else
|
||||
submissions_attrs = submissions_params[:submission].to_h.values
|
||||
|
||||
submissions_attrs, _, new_fields =
|
||||
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_attrs, @template, add_fields: true)
|
||||
|
||||
Submissions.create_from_submitters(template: @template,
|
||||
user: current_user,
|
||||
source: :invite,
|
||||
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
|
||||
submissions_attrs:,
|
||||
new_fields:,
|
||||
params: params.merge('send_completed_email' => true))
|
||||
create_submissions(@template, submissions_params, params)
|
||||
end
|
||||
|
||||
WebhookUrls.enqueue_events(submissions, 'submission.created')
|
||||
@@ -97,6 +86,21 @@ class SubmissionsController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def create_submissions(template, submissions_params, params)
|
||||
submissions_attrs = submissions_params[:submission].to_h.values
|
||||
|
||||
submissions_attrs, _, new_fields =
|
||||
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_attrs, template, add_fields: true)
|
||||
|
||||
Submissions.create_from_submitters(template: template,
|
||||
user: current_user,
|
||||
source: :invite,
|
||||
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
|
||||
submissions_attrs:,
|
||||
new_fields:,
|
||||
params: params.merge('send_completed_email' => true))
|
||||
end
|
||||
|
||||
def save_template_message(template, params)
|
||||
template.preferences['request_email_subject'] = params[:subject] if params[:subject].present?
|
||||
template.preferences['request_email_body'] = params[:body] if params[:body].present?
|
||||
|
||||
@@ -9,7 +9,7 @@ class SubmissionsDebugController < ApplicationController
|
||||
def index
|
||||
@submitter = Submitter.preload({ attachments_attachments: :blob },
|
||||
submission: { template: { documents_attachments: :blob } })
|
||||
.find_by(slug: params[:submitter_slug])
|
||||
.find_by(slug: params[:submit_form_slug])
|
||||
|
||||
respond_to do |f|
|
||||
f.html do
|
||||
|
||||
@@ -1,40 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmissionsDownloadController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
TTL = 40.minutes
|
||||
FILES_TTL = 5.minutes
|
||||
load_and_authorize_resource :submission
|
||||
|
||||
def index
|
||||
@submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
||||
|
||||
signature_valid =
|
||||
if @submitter&.slug == params[:submitter_slug]
|
||||
true
|
||||
else
|
||||
@submitter = nil
|
||||
end
|
||||
|
||||
@submitter ||= Submitter.find_by!(slug: params[:submitter_slug])
|
||||
|
||||
Submissions::EnsureResultGenerated.call(@submitter)
|
||||
|
||||
last_submitter = @submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
|
||||
last_submitter = @submission.submitters.where.not(completed_at: nil).order(:completed_at).last
|
||||
|
||||
return head :not_found unless last_submitter
|
||||
|
||||
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||
|
||||
if last_submitter.completed_at < TTL.ago && !signature_valid && !current_user_submitter?(last_submitter)
|
||||
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
||||
|
||||
return head :not_found
|
||||
end
|
||||
|
||||
if params[:combined] == 'true'
|
||||
url = build_combined_url(@submitter)
|
||||
url = Submitters.build_combined_url(last_submitter)
|
||||
|
||||
if url
|
||||
render json: [url]
|
||||
@@ -42,43 +19,7 @@ class SubmissionsDownloadController < ApplicationController
|
||||
head :not_found
|
||||
end
|
||||
else
|
||||
render json: build_urls(last_submitter)
|
||||
render json: Submitters.build_document_urls(last_submitter)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def current_user_submitter?(submitter)
|
||||
current_user && current_user.account.submitters.exists?(id: submitter.id)
|
||||
end
|
||||
|
||||
def build_urls(submitter)
|
||||
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
|
||||
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
|
||||
|
||||
Submitters.select_attachments_for_download(submitter).map do |attachment|
|
||||
ActiveStorage::Blob.proxy_url(
|
||||
attachment.blob,
|
||||
expires_at: FILES_TTL.from_now.to_i,
|
||||
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def build_combined_url(submitter)
|
||||
return if submitter.submission.submitters.exists?(completed_at: nil)
|
||||
return if submitter.submission.submitters.order(:completed_at).last != submitter
|
||||
|
||||
attachment = submitter.submission.combined_document_attachment
|
||||
attachment ||= Submissions::EnsureCombinedGenerated.call(submitter)
|
||||
|
||||
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
|
||||
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
|
||||
|
||||
ActiveStorage::Blob.proxy_url(
|
||||
attachment.blob,
|
||||
expires_at: FILES_TTL.from_now.to_i,
|
||||
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,10 +5,11 @@ class SubmissionsExportController < ApplicationController
|
||||
load_and_authorize_resource :submission, through: :template, parent: false, only: :index
|
||||
|
||||
def index
|
||||
submissions = @submissions.active
|
||||
.preload(submitters: { documents_attachments: :blob,
|
||||
attachments_attachments: :blob })
|
||||
.order(id: :asc)
|
||||
submissions = params[:archived] == 'true' ? @submissions.archived : @submissions.active
|
||||
|
||||
submissions = submissions.preload(submitters: { documents_attachments: :blob,
|
||||
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)
|
||||
|
||||
@@ -10,13 +10,15 @@ class SubmissionsPreviewController < ApplicationController
|
||||
TTL = 40.minutes
|
||||
|
||||
def show
|
||||
submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
||||
@sig_submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
||||
|
||||
signature_valid =
|
||||
if submitter && submitter.submission.slug == params[:slug]
|
||||
@submission = submitter.submission
|
||||
if @sig_submitter && @sig_submitter.submission.slug == params[:slug]
|
||||
@submission = @sig_submitter.submission
|
||||
|
||||
true
|
||||
else
|
||||
@sig_submitter = nil
|
||||
end
|
||||
|
||||
@submission ||= Submission.find_by!(slug: params[:slug])
|
||||
@@ -36,7 +38,7 @@ class SubmissionsPreviewController < ApplicationController
|
||||
|
||||
@submission = Submissions.preload_with_pages(@submission)
|
||||
|
||||
render 'submissions/show', layout: 'plain'
|
||||
render 'submissions/show', layout: 'plain', locals: { is_preview: true }
|
||||
end
|
||||
|
||||
def completed
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmissionsPreviewDownloadController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
TTL = 40.minutes
|
||||
|
||||
def index
|
||||
@submission = Submission.find_by!(slug: params[:submission_slug] || params[:submissions_preview_slug])
|
||||
|
||||
last_submitter = @submission.submitters.where.not(completed_at: nil).order(:completed_at).last
|
||||
|
||||
return head :not_found unless last_submitter
|
||||
|
||||
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||
|
||||
unless current_user_submission?(@submission)
|
||||
if use_2fa?(@submission)
|
||||
Rollbar.info("2FA download error: #{last_submitter.id}") if defined?(Rollbar)
|
||||
|
||||
return head :not_found
|
||||
end
|
||||
|
||||
if last_submitter.completed_at < TTL.ago
|
||||
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
||||
|
||||
return head :not_found
|
||||
end
|
||||
end
|
||||
|
||||
if params[:combined] == 'true'
|
||||
respond_with_combined(last_submitter)
|
||||
else
|
||||
render json: Submitters.build_document_urls(last_submitter)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def respond_with_combined(submitter)
|
||||
url = Submitters.build_combined_url(submitter)
|
||||
|
||||
if url
|
||||
render json: [url]
|
||||
else
|
||||
head :not_found
|
||||
end
|
||||
end
|
||||
|
||||
def current_user_submission?(submission)
|
||||
current_user && current_ability.can?(:read, submission)
|
||||
end
|
||||
|
||||
def use_2fa?(submission)
|
||||
return true if submission.submitters.any? do |e|
|
||||
e.preferences['require_phone_2fa'] || e.preferences['require_email_2fa']
|
||||
end
|
||||
return true if submission.template&.preferences&.dig('require_phone_2fa')
|
||||
return true if submission.template&.preferences&.dig('require_email_2fa')
|
||||
|
||||
false
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmissionsResendEmailController < ApplicationController
|
||||
load_and_authorize_resource :submission
|
||||
|
||||
before_action do
|
||||
authorize!(:manage, :resend_all)
|
||||
authorize!(:update, @submission)
|
||||
end
|
||||
|
||||
def create
|
||||
submitters = @submission.submitters.reject(&:completed_at?).select { |s| s.email.present? && !s.declined_at? }
|
||||
|
||||
if Docuseal.multitenant?
|
||||
recent_submitter_ids =
|
||||
SubmissionEvent.where(submitter_id: submitters.map(&:id),
|
||||
event_type: 'send_email',
|
||||
created_at: 10.hours.ago..Time.current).pluck(:submitter_id).to_set
|
||||
|
||||
submitters = submitters.reject { |s| recent_submitter_ids.include?(s.id) }
|
||||
end
|
||||
|
||||
submitters.each do |submitter|
|
||||
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => submitter.id)
|
||||
|
||||
submitter.sent_at ||= Time.current
|
||||
submitter.save!
|
||||
end
|
||||
|
||||
notice =
|
||||
if submitters.empty?
|
||||
I18n.t('email_has_been_sent_already')
|
||||
else
|
||||
I18n.t('emails_have_been_sent_to_n_recipients', count: submitters.size)
|
||||
end
|
||||
|
||||
redirect_back(fallback_location: submission_path(@submission), notice:)
|
||||
end
|
||||
end
|
||||
@@ -4,6 +4,8 @@ class SubmissionsUnarchiveController < ApplicationController
|
||||
load_and_authorize_resource :submission
|
||||
|
||||
def create
|
||||
authorize!(:update, @submission)
|
||||
|
||||
@submission.update!(archived_at: nil)
|
||||
|
||||
redirect_to submission_path(@submission), notice: I18n.t('submission_has_been_unarchived')
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmitFormCompletedDownloadController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
TTL = 40.minutes
|
||||
FILES_TTL = 5.minutes
|
||||
|
||||
def index
|
||||
@submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
||||
|
||||
signature_valid =
|
||||
if @submitter&.slug == submitter_slug
|
||||
true
|
||||
else
|
||||
@submitter = nil
|
||||
end
|
||||
|
||||
@submitter ||= Submitter.find_by!(slug: submitter_slug)
|
||||
|
||||
Submissions::EnsureResultGenerated.call(@submitter)
|
||||
|
||||
last_submitter = @submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
|
||||
|
||||
return head :not_found unless last_submitter
|
||||
|
||||
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||
|
||||
if !signature_valid && !current_user_submitter?(last_submitter)
|
||||
unless Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||
Rollbar.info("2FA download error: #{last_submitter.id}") if defined?(Rollbar)
|
||||
|
||||
return head :not_found
|
||||
end
|
||||
|
||||
if last_submitter.completed_at < TTL.ago
|
||||
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
||||
|
||||
return head :not_found
|
||||
end
|
||||
end
|
||||
|
||||
if params[:combined] == 'true'
|
||||
respond_with_combined(last_submitter)
|
||||
else
|
||||
render json: Submitters.build_document_urls(last_submitter)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def submitter_slug
|
||||
params[:submit_form_slug] || params[:submitter_slug] || params[:submitter_id]
|
||||
end
|
||||
|
||||
def respond_with_combined(submitter)
|
||||
url = Submitters.build_combined_url(submitter)
|
||||
|
||||
if url
|
||||
render json: [url]
|
||||
else
|
||||
head :not_found
|
||||
end
|
||||
end
|
||||
|
||||
def current_user_submitter?(submitter)
|
||||
current_user && current_ability.can?(:read, submitter)
|
||||
end
|
||||
end
|
||||
@@ -3,21 +3,22 @@
|
||||
class SubmitFormController < ApplicationController
|
||||
layout 'form'
|
||||
|
||||
around_action :with_browser_locale, only: %i[show completed success]
|
||||
around_action :with_browser_locale, only: %i[show completed success delegated]
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
before_action :load_submitter, only: %i[show update completed]
|
||||
before_action :maybe_redirect_delegated, only: %i[show completed]
|
||||
before_action :maybe_render_locked_page, only: :show
|
||||
before_action :maybe_require_link_2fa, only: %i[show update]
|
||||
before_action :maybe_require_link_2fa, only: %i[show]
|
||||
|
||||
CONFIG_KEYS = [].freeze
|
||||
|
||||
def show
|
||||
submission = @submitter.submission
|
||||
|
||||
return render :email_2fa unless Submitters::AuthorizedForForm.pass_email_2fa?(@submitter, request)
|
||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||
return render :email_2fa if require_email_2fa?(@submitter)
|
||||
|
||||
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
|
||||
|
||||
@@ -48,7 +49,7 @@ class SubmitFormController < ApplicationController
|
||||
end
|
||||
|
||||
def update
|
||||
if require_email_2fa?(@submitter)
|
||||
unless Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||
return render json: { error: I18n.t('verification_required_refresh_the_page_and_pass_2fa') },
|
||||
status: :unprocessable_content
|
||||
end
|
||||
@@ -78,24 +79,31 @@ class SubmitFormController < ApplicationController
|
||||
|
||||
render json: { field_uuid: e.message }, status: :unprocessable_content
|
||||
rescue Submitters::SubmitValues::ValidationError => e
|
||||
Rollbar.warning("Validation error #{@submitter.id}: #{e.message}") if defined?(Rollbar)
|
||||
|
||||
render json: { error: e.message }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
def completed
|
||||
raise ActionController::RoutingError, I18n.t('not_found') if @submitter.account.archived_at?
|
||||
|
||||
redirect_to submit_form_path(params[:submit_form_slug]) if require_email_2fa?(@submitter)
|
||||
return if Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||
|
||||
redirect_to submit_form_path(params[:submit_form_slug])
|
||||
end
|
||||
|
||||
def success; end
|
||||
|
||||
def delegated
|
||||
submitter_version = SubmitterVersion.find_by!(slug: params[:slug] || params[:submit_form_slug])
|
||||
|
||||
@submitter = submitter_version.submitter
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def maybe_require_link_2fa
|
||||
return if @submitter.submission.source != 'link'
|
||||
return unless @submitter.submission.template&.preferences&.dig('shared_link_2fa') == true
|
||||
return if cookies.encrypted[:email_2fa_slug] == @submitter.slug
|
||||
return if @submitter.email == current_user&.email && current_user&.account_id == @submitter.account_id
|
||||
return if Submitters::AuthorizedForForm.pass_link_2fa?(@submitter, current_user, request)
|
||||
|
||||
redirect_to start_form_path(@submitter.submission.template.slug)
|
||||
end
|
||||
@@ -109,20 +117,22 @@ class SubmitFormController < ApplicationController
|
||||
render :declined if @submitter.declined_at?
|
||||
end
|
||||
|
||||
def maybe_redirect_delegated
|
||||
return if @submitter
|
||||
|
||||
submitter_version = SubmitterVersion.find_by!(slug: params[:slug] || params[:submit_form_slug])
|
||||
|
||||
submitter_version.submitter.submission_events.find_by!(event_type: :delegate_form)
|
||||
|
||||
redirect_to submit_form_delegated_path(submitter_version.slug)
|
||||
end
|
||||
|
||||
def load_submitter
|
||||
@submitter = Submitter.find_by!(slug: params[:slug] || params[:submit_form_slug])
|
||||
@submitter = Submitter.find_by(slug: params[:slug] || params[:submit_form_slug])
|
||||
end
|
||||
|
||||
def build_attachments_index(submission)
|
||||
ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments)
|
||||
.preload(:blob).index_by(&:uuid)
|
||||
end
|
||||
|
||||
def require_email_2fa?(submitter)
|
||||
return false if submitter.submission.template&.preferences&.dig('require_email_2fa') != true &&
|
||||
submitter.preferences['require_email_2fa'] != true
|
||||
return false if cookies.encrypted[:email_2fa_slug] == submitter.slug
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,29 +4,38 @@ class SubmitFormDeclineController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
def create
|
||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
before_action :load_submitter
|
||||
|
||||
return redirect_to submit_form_path(submitter.slug) if submitter.declined_at? ||
|
||||
submitter.completed_at? ||
|
||||
submitter.submission.archived_at? ||
|
||||
submitter.submission.expired? ||
|
||||
submitter.submission.template&.archived_at?
|
||||
def create
|
||||
return redirect_to submit_form_path(@submitter.slug) if @submitter.declined_at? ||
|
||||
@submitter.completed_at? ||
|
||||
@submitter.submission.archived_at? ||
|
||||
@submitter.submission.expired? ||
|
||||
@submitter.submission.template&.archived_at? ||
|
||||
!Submitters::AuthorizedForForm.call(@submitter,
|
||||
current_user,
|
||||
request)
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
submitter.update!(declined_at: Time.current)
|
||||
@submitter.update!(declined_at: Time.current)
|
||||
|
||||
SubmissionEvents.create_with_tracking_data(submitter, 'decline_form', request, { reason: params[:reason] })
|
||||
SubmissionEvents.create_with_tracking_data(@submitter, 'decline_form', request, { reason: params[:reason] })
|
||||
end
|
||||
|
||||
user = submitter.submission.created_by_user || submitter.template.author
|
||||
user = @submitter.submission.created_by_user || @submitter.template.author
|
||||
|
||||
if user.user_configs.find_by(key: UserConfig::RECEIVE_DECLINED_EMAIL)&.value != false
|
||||
SubmitterMailer.declined_email(submitter, user).deliver_later!
|
||||
SubmitterMailer.declined_email(@submitter, user).deliver_later!
|
||||
end
|
||||
|
||||
WebhookUrls.enqueue_events(submitter, 'form.declined')
|
||||
WebhookUrls.enqueue_events(@submitter, 'form.declined')
|
||||
|
||||
redirect_to submit_form_path(submitter.slug)
|
||||
redirect_to submit_form_path(@submitter.slug)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_submitter
|
||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmitFormDelegateController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
before_action :load_submitter
|
||||
|
||||
def create
|
||||
return redirect_to submit_form_path(@submitter.slug) if @submitter.declined_at? ||
|
||||
@submitter.completed_at? ||
|
||||
@submitter.submission.archived_at? ||
|
||||
@submitter.submission.expired? ||
|
||||
@submitter.submission.template&.archived_at? ||
|
||||
!Submitters::AuthorizedForForm.call(@submitter,
|
||||
current_user,
|
||||
request)
|
||||
|
||||
@submitter.account.account_configs.find_by!(key: AccountConfig::ALLOW_TO_DELEGATE_KEY, value: true)
|
||||
|
||||
email = Submissions.normalize_email(params[:email])
|
||||
|
||||
return redirect_to submit_form_path(@submitter.slug) if email.blank?
|
||||
|
||||
old_slug = @submitter.slug
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
@submitter.submitter_versions.create!(slug: old_slug, email: @submitter.email,
|
||||
name: @submitter.name, phone: @submitter.phone)
|
||||
|
||||
SubmissionEvents.create_with_tracking_data(@submitter, 'delegate_form', request,
|
||||
{ old_email: @submitter.email, email: })
|
||||
|
||||
@submitter.update!(email:, phone: nil, name: nil, slug: SecureRandom.base58(14))
|
||||
end
|
||||
|
||||
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => @submitter.id)
|
||||
|
||||
redirect_to submit_form_delegated_path(old_slug)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_submitter
|
||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
end
|
||||
end
|
||||
@@ -9,7 +9,7 @@ class SubmitFormDownloadController < ApplicationController
|
||||
def index
|
||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
|
||||
return redirect_to submitter_download_index_path(@submitter.slug) if @submitter.completed_at?
|
||||
return redirect_to submit_form_documents_path(@submitter.slug) if @submitter.completed_at?
|
||||
|
||||
return head :unprocessable_content if @submitter.declined_at? ||
|
||||
@submitter.submission.archived_at? ||
|
||||
@@ -17,7 +17,8 @@ class SubmitFormDownloadController < ApplicationController
|
||||
@submitter.submission.template&.archived_at? ||
|
||||
AccountConfig.exists?(account_id: @submitter.account_id,
|
||||
key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
|
||||
value: false)
|
||||
value: false) ||
|
||||
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||
|
||||
last_completed_submitter = @submitter.submission.submitters
|
||||
.where.not(id: @submitter.id)
|
||||
@@ -32,7 +33,7 @@ class SubmitFormDownloadController < ApplicationController
|
||||
end
|
||||
|
||||
urls = attachments.map do |attachment|
|
||||
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
|
||||
ActiveStorage::Blob.proxy_path(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
|
||||
end
|
||||
|
||||
render json: urls
|
||||
|
||||
@@ -12,7 +12,8 @@ class SubmitFormDrawSignatureController < ApplicationController
|
||||
|
||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||
|
||||
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at?
|
||||
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at? ||
|
||||
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||
return redirect_to submit_form_path(@submitter.slug)
|
||||
end
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@ class SubmitFormInviteController < ApplicationController
|
||||
skip_authorization_check
|
||||
|
||||
def create
|
||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
|
||||
return head :unprocessable_content 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')
|
||||
invite_submitters = filter_invite_submitters(@submitter, 'invite_by_uuid')
|
||||
optional_invite_submitters = filter_invite_submitters(@submitter, 'optional_invite_by_uuid')
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
(invite_submitters + optional_invite_submitters).each do |item|
|
||||
@@ -19,18 +19,20 @@ class SubmitFormInviteController < ApplicationController
|
||||
next unless attrs
|
||||
next if attrs[:email].blank?
|
||||
|
||||
submitter.submission.submitters.create!(**attrs, account_id: submitter.account_id)
|
||||
email = Submissions.normalize_email(attrs[:email])
|
||||
|
||||
SubmissionEvents.create_with_tracking_data(submitter, 'invite_party', request, { uuid: submitter.uuid })
|
||||
@submitter.submission.submitters.create!(uuid: attrs[:uuid], email:, account_id: @submitter.account_id)
|
||||
|
||||
SubmissionEvents.create_with_tracking_data(@submitter, 'invite_party', request, { uuid: @submitter.uuid })
|
||||
end
|
||||
|
||||
submitter.submission.update!(submitters_order: :preserved)
|
||||
@submitter.submission.update!(submitters_order: :preserved)
|
||||
end
|
||||
|
||||
submitter.submission.submitters.reload
|
||||
@submitter.submission.submitters.reload
|
||||
|
||||
if invite_submitters.all? { |s| submitter.submission.submitters.any? { |e| e.uuid == s['uuid'] } }
|
||||
Submitters::SubmitValues.call(submitter, ActionController::Parameters.new(completed: 'true'), request)
|
||||
if invite_submitters.all? { |s| @submitter.submission.submitters.any? { |e| e.uuid == s['uuid'] } }
|
||||
Submitters::SubmitValues.call(@submitter, ActionController::Parameters.new(completed: 'true'), request)
|
||||
|
||||
head :ok
|
||||
else
|
||||
@@ -45,7 +47,8 @@ class SubmitFormInviteController < ApplicationController
|
||||
!submitter.completed_at? &&
|
||||
!submitter.submission.archived_at? &&
|
||||
!submitter.submission.expired? &&
|
||||
!submitter.submission.template&.archived_at?
|
||||
!submitter.submission.template&.archived_at? &&
|
||||
Submitters::AuthorizedForForm.call(submitter, current_user, request)
|
||||
end
|
||||
|
||||
def filter_invite_submitters(submitter, key = 'invite_by_uuid')
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmitFormMetadataController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
def index
|
||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
|
||||
return head :not_found if @submitter.declined_at? ||
|
||||
@submitter.completed_at? ||
|
||||
@submitter.submission.archived_at? ||
|
||||
@submitter.submission.expired? ||
|
||||
@submitter.submission.template&.archived_at? ||
|
||||
@submitter.account.archived_at? ||
|
||||
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||
|
||||
submission = @submitter.submission
|
||||
values = submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) }
|
||||
schema = Submissions.filtered_conditions_schema(submission, values:, include_submitter_uuid: @submitter.uuid)
|
||||
|
||||
documents = schema.filter_map do |item|
|
||||
submission.schema_documents.find { |a| a.uuid == item['attachment_uuid'] }
|
||||
end
|
||||
|
||||
ActiveRecord::Associations::Preloader.new(records: documents, associations: %i[blob record]).call
|
||||
|
||||
text_runs = documents.to_h do |document|
|
||||
[
|
||||
document.uuid,
|
||||
DocumentMetadatas.find_or_create_for_document(document, account_id: document.record.account_id).text_runs
|
||||
]
|
||||
end
|
||||
|
||||
render json: { text_runs: }
|
||||
end
|
||||
end
|
||||
@@ -5,19 +5,21 @@ class SubmitFormValuesController < ApplicationController
|
||||
skip_authorization_check
|
||||
|
||||
def index
|
||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
|
||||
return render json: {} if submitter.completed_at? || submitter.declined_at?
|
||||
return render json: {} if submitter.submission.template&.archived_at? ||
|
||||
submitter.submission.archived_at? ||
|
||||
submitter.submission.expired?
|
||||
return render json: {} if @submitter.completed_at? ||
|
||||
@submitter.declined_at? ||
|
||||
@submitter.submission.template&.archived_at? ||
|
||||
@submitter.submission.archived_at? ||
|
||||
@submitter.submission.expired? ||
|
||||
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||
|
||||
value = submitter.values[params['field_uuid']]
|
||||
attachment = submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present?
|
||||
value = @submitter.values[params['field_uuid']]
|
||||
attachment = @submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present?
|
||||
|
||||
render json: {
|
||||
value:,
|
||||
attachment: attachment&.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
|
||||
}, head: :ok
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,25 +7,34 @@ class SubmittersAutocompleteController < ApplicationController
|
||||
LIMIT = 100
|
||||
|
||||
def index
|
||||
submitters = search_submitters(@submitters)
|
||||
field = SELECT_COLUMNS.find { |c| c == params[:field] }
|
||||
|
||||
submitters = search_submitters(@submitters, field)
|
||||
|
||||
arel_columns = SELECT_COLUMNS.map { |col| Submitter.arel_table[col] }
|
||||
values = submitters.limit(LIMIT).group(arel_columns).pluck(arel_columns)
|
||||
|
||||
values =
|
||||
if field
|
||||
max_ids = submitters.group(field).limit(LIMIT).select(Submitter.arel_table[:id].maximum)
|
||||
|
||||
submitters.where(id: max_ids).order(id: :desc).pluck(arel_columns)
|
||||
else
|
||||
submitters.limit(LIMIT).group(arel_columns).pluck(arel_columns)
|
||||
end
|
||||
|
||||
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
|
||||
attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present?
|
||||
|
||||
render json: attrs
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def search_submitters(submitters)
|
||||
if SELECT_COLUMNS.include?(params[:field])
|
||||
def search_submitters(submitters, field)
|
||||
if field
|
||||
if Docuseal.fulltext_search?
|
||||
Submitters.fulltext_search_field(current_user, submitters, params[:q], params[:field])
|
||||
Submitters.fulltext_search_field(current_user, submitters, params[:q], field)
|
||||
else
|
||||
column = Submitter.arel_table[params[:field].to_sym]
|
||||
column = Submitter.arel_table[field.to_sym]
|
||||
|
||||
term = "#{params[:q].downcase}%"
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmittersDownloadController < ApplicationController
|
||||
load_and_authorize_resource :submitter
|
||||
|
||||
def index
|
||||
Submissions::EnsureResultGenerated.call(@submitter)
|
||||
|
||||
render json: Submitters.build_document_urls(@submitter)
|
||||
end
|
||||
end
|
||||
@@ -1,9 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmittersSendEmailController < ApplicationController
|
||||
load_and_authorize_resource :submitter, id_param: :submitter_slug, find_by: :slug
|
||||
load_and_authorize_resource :submitter
|
||||
|
||||
def create
|
||||
authorize!(:update, @submitter)
|
||||
|
||||
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
|
||||
event_type: 'send_email',
|
||||
created_at: 10.hours.ago..Time.current)
|
||||
|
||||
@@ -6,17 +6,19 @@ class TemplateDocumentsController < ApplicationController
|
||||
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) }
|
||||
render json: @template.schema_documents.map { |d| ActiveStorage::Blob.proxy_path(d.blob, expires_at: FILES_TTL.from_now.to_i) }
|
||||
end
|
||||
|
||||
def create
|
||||
authorize!(:update, @template)
|
||||
|
||||
if params[:blobs].blank? && params[:files].blank?
|
||||
return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
old_fields_hash = @template.fields.hash
|
||||
|
||||
documents = Templates::CreateAttachments.call(@template, params, extract_fields: true)
|
||||
documents, = Templates::CreateAttachments.call(@template, params, extract_fields: true)
|
||||
|
||||
schema = documents.map do |doc|
|
||||
{ attachment_uuid: doc.uuid, name: doc.filename.base }
|
||||
@@ -27,7 +29,7 @@ class TemplateDocumentsController < ApplicationController
|
||||
fields: old_fields_hash == @template.fields.hash ? nil : @template.fields,
|
||||
submitters: old_fields_hash == @template.fields.hash ? nil : @template.submitters,
|
||||
documents: documents.as_json(
|
||||
methods: %i[metadata signed_uuid],
|
||||
methods: %i[metadata signed_key],
|
||||
include: {
|
||||
preview_images: { methods: %i[url metadata filename] }
|
||||
}
|
||||
|
||||
@@ -8,11 +8,14 @@ class TemplatesCloneAndReplaceController < ApplicationController
|
||||
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [@template],
|
||||
associations: [schema_documents: :preview_images_attachments]
|
||||
associations: [{ schema_documents: :preview_images_attachments }]
|
||||
).call
|
||||
|
||||
cloned_template = Templates::Clone.call(@template, author: current_user)
|
||||
cloned_template.name = File.basename(params[:files].first.original_filename, '.*')
|
||||
|
||||
authorize!(:create, cloned_template)
|
||||
|
||||
cloned_template.save!
|
||||
|
||||
documents = Templates::ReplaceAttachments.call(cloned_template, params, extract_fields: true)
|
||||
|
||||
@@ -12,7 +12,7 @@ class TemplatesCloneController < ApplicationController
|
||||
def create
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [@base_template],
|
||||
associations: [schema_documents: :preview_images_attachments]
|
||||
associations: [{ schema_documents: :preview_images_attachments }]
|
||||
).call
|
||||
|
||||
@template = Templates::Clone.call(@base_template, author: current_user,
|
||||
@@ -21,7 +21,7 @@ class TemplatesCloneController < ApplicationController
|
||||
|
||||
authorize!(:create, @template)
|
||||
|
||||
if params[:account_id].present? && true_ability.authorize!(:manage, Account.find(params[:account_id]))
|
||||
if params[:account_id].present? && true_ability.can?(:manage, Account.find(params[:account_id]))
|
||||
@template.account_id = params[:account_id]
|
||||
@template.author = true_user if true_user.account_id == @template.account_id
|
||||
@template.folder = @template.account.default_template_folder if @template.account_id != current_account.id
|
||||
|
||||
@@ -27,18 +27,7 @@ class TemplatesController < ApplicationController
|
||||
def new; end
|
||||
|
||||
def edit
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [@template],
|
||||
associations: [schema_documents: [:blob, { preview_images_attachments: :blob }]]
|
||||
).call
|
||||
|
||||
@template_data =
|
||||
@template.as_json.merge(
|
||||
documents: @template.schema_documents.as_json(
|
||||
methods: %i[metadata signed_uuid],
|
||||
include: { preview_images: { methods: %i[url metadata filename] } }
|
||||
)
|
||||
).to_json
|
||||
@template_data = Templates.serialize_for_builder(@template)
|
||||
|
||||
render :edit, layout: 'plain'
|
||||
end
|
||||
@@ -84,6 +73,8 @@ class TemplatesController < ApplicationController
|
||||
else
|
||||
@template.update!(archived_at: Time.current)
|
||||
|
||||
WebhookUrls.enqueue_events(@template, 'template.archived')
|
||||
|
||||
I18n.t('template_has_been_archived')
|
||||
end
|
||||
|
||||
@@ -95,9 +86,11 @@ class TemplatesController < ApplicationController
|
||||
def template_params
|
||||
params.require(:template).permit(
|
||||
:name,
|
||||
{ schema: [[:attachment_uuid, :google_drive_file_id, :name,
|
||||
{ schema: [[:attachment_uuid, :google_drive_file_id, :name, :dynamic,
|
||||
{ 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]],
|
||||
submitters: [%i[name uuid is_requester linked_to_uuid invite_via_field_uuid
|
||||
invite_by_uuid optional_invite_by_uuid email order]],
|
||||
variables_schema: {},
|
||||
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||
:required, :readonly, :default_value,
|
||||
:title, :description, :prefillable,
|
||||
@@ -106,7 +99,7 @@ class TemplatesController < ApplicationController
|
||||
conditions: [%i[field_uuid value action operation]],
|
||||
options: [%i[value uuid]],
|
||||
validation: %i[message pattern min max step],
|
||||
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
||||
areas: [%i[uuid x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,32 +9,34 @@ class TemplatesDebugController < ApplicationController
|
||||
schema_uuids = @template.schema.index_by { |e| e['attachment_uuid'] }
|
||||
attachment = @template.documents.find { |a| schema_uuids[a.uuid] }
|
||||
|
||||
data = attachment.download
|
||||
if attachment
|
||||
data = attachment.download
|
||||
|
||||
unless attachment.image?
|
||||
pdf = HexaPDF::Document.new(io: StringIO.new(data))
|
||||
unless attachment.image?
|
||||
pdf = HexaPDF::Document.new(io: StringIO.new(data))
|
||||
|
||||
fields = Templates::FindAcroFields.call(pdf, attachment, 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]))
|
||||
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 }]
|
||||
associations: [{ schema_documents: { preview_images_attachments: :blob } }]
|
||||
).call
|
||||
|
||||
@template_data =
|
||||
@template.as_json.merge(
|
||||
documents: @template.schema_documents.as_json(
|
||||
methods: %i[metadata signed_uuid],
|
||||
methods: %i[metadata signed_key],
|
||||
include: { preview_images: { methods: %i[url metadata filename] } }
|
||||
)
|
||||
).to_json
|
||||
@@ -58,9 +60,16 @@ class TemplatesDebugController < ApplicationController
|
||||
|
||||
params = { files: [file] }
|
||||
|
||||
documents = Templates::CreateAttachments.call(@template, params)
|
||||
documents, dynamic_documents = Templates::CreateAttachments.call(@template, params,
|
||||
dynamic: DEBUG_FILE.ends_with?('.docx'))
|
||||
|
||||
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
|
||||
schema = documents.map do |doc|
|
||||
{
|
||||
attachment_uuid: doc.uuid,
|
||||
name: doc.filename.base,
|
||||
dynamic: dynamic_documents.find { |e| e.uuid == doc.uuid }.present?
|
||||
}
|
||||
end
|
||||
|
||||
@template.update!(schema:)
|
||||
end
|
||||
|
||||
@@ -13,10 +13,15 @@ class TemplatesDetectFieldsController < ApplicationController
|
||||
documents = @template.schema_documents.preload(:blob)
|
||||
documents = documents.where(uuid: params[:attachment_uuid]) if params[:attachment_uuid].present?
|
||||
|
||||
page_number = params[:page].present? ? params[:page].to_i : nil
|
||||
page_number = params[:page].presence&.to_i
|
||||
|
||||
documents.each do |document|
|
||||
io = StringIO.new(document.download)
|
||||
io =
|
||||
if document.image?
|
||||
StringIO.new(document.preview_images.joins(:blob).find_by(blob: { filename: ['0.png', '0.jpg'] }).download)
|
||||
else
|
||||
StringIO.new(document.download)
|
||||
end
|
||||
|
||||
Templates::DetectFields.call(io, attachment: document, page_number:) do |(attachment_uuid, page, fields)|
|
||||
sse.write({ attachment_uuid:, page:, fields: })
|
||||
|
||||
@@ -6,6 +6,8 @@ class TemplatesFoldersController < ApplicationController
|
||||
def edit; end
|
||||
|
||||
def update
|
||||
authorize!(:update, @template)
|
||||
|
||||
name = [params[:parent_name], params[:name]].compact_blank.join(' / ')
|
||||
|
||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, name)
|
||||
|
||||
@@ -58,8 +58,8 @@ class TemplatesPreferencesController < ApplicationController
|
||||
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],
|
||||
submitters: [%i[uuid request_email_subject request_email_body]], link_form_fields: []]
|
||||
[{ completed_message: %i[title body],
|
||||
submitters: [%i[uuid request_email_subject request_email_body]], link_form_fields: [] }]
|
||||
).tap do |attrs|
|
||||
attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1'
|
||||
|
||||
|
||||
@@ -4,18 +4,7 @@ class TemplatesPreviewController < ApplicationController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def show
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [@template],
|
||||
associations: [schema_documents: { preview_images_attachments: :blob }]
|
||||
).call
|
||||
|
||||
@template_data =
|
||||
@template.as_json.merge(
|
||||
documents: @template.schema_documents.as_json(
|
||||
methods: %i[metadata signed_uuid],
|
||||
include: { preview_images: { methods: %i[url metadata filename] } }
|
||||
)
|
||||
).to_json
|
||||
@template_data = Templates.serialize_for_builder(@template)
|
||||
|
||||
render :show, layout: 'plain'
|
||||
end
|
||||
|
||||
@@ -22,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 order]] }
|
||||
invite_by_uuid invite_via_field_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?
|
||||
@@ -36,6 +36,7 @@ class TemplatesRecipientsController < ApplicationController
|
||||
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?
|
||||
s.delete(:invite_via_field_uuid) if s[:invite_via_field_uuid].blank?
|
||||
|
||||
normalize_option_value(s)
|
||||
end
|
||||
@@ -53,6 +54,7 @@ class TemplatesRecipientsController < ApplicationController
|
||||
attrs.delete(:email)
|
||||
attrs.delete(:linked_to_uuid)
|
||||
attrs.delete(:invite_by_uuid)
|
||||
attrs.delete(:invite_via_field_uuid)
|
||||
attrs.delete(:optional_invite_by_uuid)
|
||||
when /\Alinked_to_(.*)\z/
|
||||
attrs[:linked_to_uuid] = ::Regexp.last_match(-1)
|
||||
|
||||
@@ -4,8 +4,12 @@ class TemplatesRestoreController < ApplicationController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def create
|
||||
authorize!(:update, @template)
|
||||
|
||||
@template.update!(archived_at: nil)
|
||||
|
||||
WebhookUrls.enqueue_events(@template, 'template.updated')
|
||||
|
||||
redirect_to template_path(@template), notice: I18n.t('template_has_been_unarchived')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TemplatesShareLinkQrController < ApplicationController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def show
|
||||
return render :disabled, layout: 'plain' unless @template.shared_link?
|
||||
|
||||
shared_link_url = start_form_url(slug: @template.slug, host: form_link_host)
|
||||
|
||||
@qr_svg_code = RQRCode::QRCode.new(shared_link_url, level: :m).as_svg(viewbox: true)
|
||||
|
||||
@page_size =
|
||||
if TimeUtils.timezone_abbr(current_account.timezone, Time.current.beginning_of_year).in?(TimeUtils::US_TIMEZONES)
|
||||
'Letter'
|
||||
else
|
||||
'A4'
|
||||
end
|
||||
|
||||
render :show, layout: false
|
||||
end
|
||||
end
|
||||
@@ -5,14 +5,16 @@ class TemplatesUploadsController < ApplicationController
|
||||
|
||||
layout 'plain'
|
||||
|
||||
def show; end
|
||||
def show
|
||||
redirect_to root_path if params[:url].blank?
|
||||
end
|
||||
|
||||
def create
|
||||
url_params = create_file_params_from_url if params[:url].present?
|
||||
|
||||
save_template!(@template, url_params)
|
||||
|
||||
documents = Templates::CreateAttachments.call(@template, url_params || params, extract_fields: true)
|
||||
documents, = Templates::CreateAttachments.call(@template, url_params || params, extract_fields: true)
|
||||
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
|
||||
|
||||
if @template.fields.blank?
|
||||
@@ -56,7 +58,7 @@ class TemplatesUploadsController < ApplicationController
|
||||
def create_file_params_from_url
|
||||
tempfile = Tempfile.new
|
||||
tempfile.binmode
|
||||
tempfile.write(DownloadUtils.call(params[:url]).body)
|
||||
tempfile.write(DownloadUtils.call(params[:url], validate: true).body)
|
||||
tempfile.rewind
|
||||
|
||||
filename = URI.decode_www_form_component(params[:filename]) if params[:filename].present?
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TemplatesVersionsController < ApplicationController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def index
|
||||
versions = @template.template_versions.order(id: :desc).preload(:author)
|
||||
|
||||
render json: versions.as_json(TemplateVersions::SERIALIZE_PARAMS)
|
||||
end
|
||||
|
||||
def show
|
||||
version = @template.template_versions.find(params[:id])
|
||||
|
||||
render json: TemplateVersions.serialize(version)
|
||||
end
|
||||
|
||||
def create
|
||||
authorize!(:update, @template)
|
||||
|
||||
TemplateVersions.find_or_create_for(@template, author: current_user)
|
||||
|
||||
head :ok
|
||||
end
|
||||
end
|
||||
@@ -3,7 +3,7 @@
|
||||
class TestingAccountsController < ApplicationController
|
||||
skip_authorization_check only: :destroy
|
||||
|
||||
def show
|
||||
def create
|
||||
authorize!(:manage, current_account)
|
||||
authorize!(:manage, current_user)
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ class TimestampServerController < ApplicationController
|
||||
uri = Addressable::URI.parse(url)
|
||||
|
||||
conn = Faraday.new(uri.origin) do |c|
|
||||
c.basic_auth(uri.user, uri.password) if uri.password.present?
|
||||
c.request :authorization, :basic, uri.user, uri.password if uri.password.present?
|
||||
end
|
||||
|
||||
response = conn.post(uri.path, req.to_der,
|
||||
|
||||
@@ -11,6 +11,12 @@ class UserInitialsController < ApplicationController
|
||||
|
||||
return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_initials') if file.blank?
|
||||
|
||||
extension = File.extname(file.original_filename).delete_prefix('.').downcase
|
||||
|
||||
if Submitters::DANGEROUS_EXTENSIONS.include?(extension)
|
||||
raise Submitters::MaliciousFileExtension, "File type '.#{extension}' is not allowed."
|
||||
end
|
||||
|
||||
blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
|
||||
filename: file.original_filename,
|
||||
content_type: file.content_type)
|
||||
|
||||
@@ -11,6 +11,12 @@ class UserSignaturesController < ApplicationController
|
||||
|
||||
return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_signature') if file.blank?
|
||||
|
||||
extension = File.extname(file.original_filename).delete_prefix('.').downcase
|
||||
|
||||
if Submitters::DANGEROUS_EXTENSIONS.include?(extension)
|
||||
raise Submitters::MaliciousFileExtension, "File type '.#{extension}' is not allowed."
|
||||
end
|
||||
|
||||
blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
|
||||
filename: file.original_filename,
|
||||
content_type: file.content_type)
|
||||
|
||||
@@ -16,7 +16,19 @@ class UsersController < ApplicationController
|
||||
@users.active.where.not(role: 'integration')
|
||||
end
|
||||
|
||||
@pagy, @users = pagy(@users.preload(account: :account_accesses).where(account: current_account).order(id: :desc))
|
||||
@users = @users.preload(account: :account_accesses).where(account: current_account).order(id: :desc)
|
||||
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
@pagy, @users = pagy(@users)
|
||||
end
|
||||
|
||||
if current_ability.can?(:manage, current_account)
|
||||
format.csv do
|
||||
send_data Users.generate_csv(@users), filename: "users-#{Time.current.iso8601}.csv", type: 'text/csv'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def new; end
|
||||
|
||||
@@ -15,7 +15,7 @@ class WebhookEventsController < ApplicationController
|
||||
Submissions::SerializeForApi.call(@webhook_event.record)
|
||||
when 'template.created', 'template.updated'
|
||||
Templates::SerializeForApi.call(@webhook_event.record)
|
||||
when 'submission.archived'
|
||||
when 'submission.archived', 'template.archived'
|
||||
@webhook_event.record.as_json(only: %i[id archived_at])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class WebhookHmacController < ApplicationController
|
||||
load_and_authorize_resource :webhook_url, parent: false
|
||||
|
||||
def show; end
|
||||
end
|
||||
@@ -15,7 +15,7 @@ class WebhookSettingsController < ApplicationController
|
||||
|
||||
@webhook_events = @webhook_events.where(status: params[:status]) if %w[success error].include?(params[:status])
|
||||
|
||||
@pagy, @webhook_events = pagy_countless(@webhook_events.order(id: :desc))
|
||||
@pagy, @webhook_events = pagy(:countless, @webhook_events.order(id: :desc))
|
||||
|
||||
render :show
|
||||
end
|
||||
@@ -26,15 +26,19 @@ class WebhookSettingsController < ApplicationController
|
||||
|
||||
@webhook_events = @webhook_events.where(status: params[:status]) if %w[success error].include?(params[:status])
|
||||
|
||||
@pagy, @webhook_events = pagy_countless(@webhook_events.order(id: :desc))
|
||||
@pagy, @webhook_events = pagy(:countless, @webhook_events.order(id: :desc))
|
||||
end
|
||||
|
||||
def new; end
|
||||
|
||||
def create
|
||||
@webhook_url.save!
|
||||
if @webhook_url.url.present?
|
||||
@webhook_url.save!
|
||||
|
||||
redirect_to settings_webhooks_path, notice: I18n.t('webhook_url_has_been_saved')
|
||||
redirect_to settings_webhooks_path, notice: I18n.t('webhook_url_has_been_saved')
|
||||
else
|
||||
redirect_back fallback_location: settings_webhooks_path
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
|
||||
@@ -21,6 +21,7 @@ import SubmittersAutocomplete from './elements/submitter_autocomplete'
|
||||
import FolderAutocomplete from './elements/folder_autocomplete'
|
||||
import SignatureForm from './elements/signature_form'
|
||||
import SubmitForm from './elements/submit_form'
|
||||
import ConvertUpload from './elements/convert_upload'
|
||||
import PromptPassword from './elements/prompt_password'
|
||||
import EmailsTextarea from './elements/emails_textarea'
|
||||
import ToggleSubmit from './elements/toggle_submit'
|
||||
@@ -40,19 +41,20 @@ 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 MarkdownEditor from './elements/markdown_editor'
|
||||
import MountOnClick from './elements/mount_on_click'
|
||||
import RemoveOnEvent from './elements/remove_on_event'
|
||||
import ScrollTo from './elements/scroll_to'
|
||||
import SetValue from './elements/set_value'
|
||||
import ReviewForm from './elements/review_form'
|
||||
import ShowOnValue from './elements/show_on_value'
|
||||
import CustomValidation from './elements/custom_validation'
|
||||
import ToggleClasses from './elements/toggle_classes'
|
||||
import AutosizeField from './elements/autosize_field'
|
||||
import GoogleDriveFilePicker from './elements/google_drive_file_picker'
|
||||
import OpenModal from './elements/open_modal'
|
||||
import BarChart from './elements/bar_chart'
|
||||
import FieldCondition from './elements/field_condition'
|
||||
import ConfirmUpload from './elements/confirm_upload'
|
||||
|
||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||
|
||||
@@ -111,6 +113,7 @@ safeRegisterElement('submitters-autocomplete', SubmittersAutocomplete)
|
||||
safeRegisterElement('folder-autocomplete', FolderAutocomplete)
|
||||
safeRegisterElement('signature-form', SignatureForm)
|
||||
safeRegisterElement('submit-form', SubmitForm)
|
||||
safeRegisterElement('convert-upload', ConvertUpload)
|
||||
safeRegisterElement('prompt-password', PromptPassword)
|
||||
safeRegisterElement('emails-textarea', EmailsTextarea)
|
||||
safeRegisterElement('toggle-cookies', ToggleCookies)
|
||||
@@ -131,19 +134,20 @@ safeRegisterElement('check-on-click', CheckOnClick)
|
||||
safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup)
|
||||
safeRegisterElement('page-container', PageContainer)
|
||||
safeRegisterElement('email-editor', EmailEditor)
|
||||
safeRegisterElement('markdown-editor', MarkdownEditor)
|
||||
safeRegisterElement('mount-on-click', MountOnClick)
|
||||
safeRegisterElement('remove-on-event', RemoveOnEvent)
|
||||
safeRegisterElement('scroll-to', ScrollTo)
|
||||
safeRegisterElement('set-value', SetValue)
|
||||
safeRegisterElement('review-form', ReviewForm)
|
||||
safeRegisterElement('show-on-value', ShowOnValue)
|
||||
safeRegisterElement('custom-validation', CustomValidation)
|
||||
safeRegisterElement('toggle-classes', ToggleClasses)
|
||||
safeRegisterElement('autosize-field', AutosizeField)
|
||||
safeRegisterElement('google-drive-file-picker', GoogleDriveFilePicker)
|
||||
safeRegisterElement('open-modal', OpenModal)
|
||||
safeRegisterElement('bar-chart', BarChart)
|
||||
safeRegisterElement('field-condition', FieldCondition)
|
||||
safeRegisterElement('confirm-upload', ConfirmUpload)
|
||||
|
||||
safeRegisterElement('template-builder', class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
@@ -153,24 +157,35 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
||||
|
||||
this.appElem.classList.add('md:h-screen')
|
||||
|
||||
const template = reactive(JSON.parse(this.dataset.template))
|
||||
|
||||
this.app = createApp(TemplateBuilder, {
|
||||
template: reactive(JSON.parse(this.dataset.template)),
|
||||
template,
|
||||
customFields: reactive(JSON.parse(this.dataset.customFields || '[]')),
|
||||
dynamicDocuments: reactive(JSON.parse(this.dataset.dynamicDocuments || '[]')),
|
||||
backgroundColor: '#faf7f5',
|
||||
locale: this.dataset.locale,
|
||||
withPhone: this.dataset.withPhone === 'true',
|
||||
withPrefillable: template.fields?.some((f) => f.prefillable),
|
||||
withVerification: ['true', 'false'].includes(this.dataset.withVerification) ? this.dataset.withVerification === 'true' : null,
|
||||
withKba: ['true', 'false'].includes(this.dataset.withKba) ? this.dataset.withKba === 'true' : null,
|
||||
withLogo: this.dataset.withLogo !== 'false',
|
||||
withFieldsDetection: this.dataset.withFieldsDetection === 'true',
|
||||
withDetectExistingFields: this.dataset.withDetectExistingFields === 'true',
|
||||
withRevisions: true,
|
||||
withRevisionsMenu: this.dataset.withRevisionsMenu === 'true',
|
||||
editable: this.dataset.editable !== 'false',
|
||||
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
||||
withCustomFields: true,
|
||||
withPayment: this.dataset.withPayment === 'true',
|
||||
isPaymentConnected: this.dataset.isPaymentConnected === 'true',
|
||||
withFormula: this.dataset.withFormula === 'true',
|
||||
withSendButton: this.dataset.withSendButton !== 'false',
|
||||
withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false',
|
||||
withConditions: this.dataset.withConditions === 'true',
|
||||
withDynamicDocuments: this.dataset.withDynamicDocuments === 'true',
|
||||
withGoogleDrive: this.dataset.withGoogleDrive === 'true',
|
||||
pagePreviewFormat: this.dataset.pagePreviewFormat || '.jpg',
|
||||
withReplaceAndCloneUpload: true,
|
||||
withDownload: true,
|
||||
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
||||
@@ -184,10 +199,35 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
||||
}
|
||||
|
||||
onSubmit = (e) => {
|
||||
if (e.detail.success && e.detail?.formSubmission?.formElement?.id === 'submitters_form') {
|
||||
e.detail.fetchResponse.response.json().then((data) => {
|
||||
this.component.template.submitters = data.submitters
|
||||
})
|
||||
if (e.detail.success) {
|
||||
if (e.detail?.formSubmission?.formElement?.id === 'submitters_form') {
|
||||
e.detail.fetchResponse.response.json().then((data) => {
|
||||
this.component.template.submitters = data.submitters
|
||||
})
|
||||
}
|
||||
|
||||
if (e.detail?.formSubmission?.formElement?.action?.endsWith('/prefillable_fields')) {
|
||||
e.detail.fetchResponse.response.text().then((data) => {
|
||||
const doc = new DOMParser().parseFromString(data, 'text/html')
|
||||
const fragment = doc.querySelector('turbo-stream template').content
|
||||
|
||||
const prefillableUuidsIndex = {}
|
||||
|
||||
fragment.querySelectorAll('[name="field_uuid"]').forEach((field) => {
|
||||
prefillableUuidsIndex[field.value] = true
|
||||
})
|
||||
|
||||
this.component.template.fields.forEach((field) => {
|
||||
if (prefillableUuidsIndex[field.uuid]) {
|
||||
field.prefillable = true
|
||||
field.readonly = true
|
||||
} else if (field.prefillable) {
|
||||
delete field.prefillable
|
||||
delete field.readonly
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -155,3 +155,7 @@ button[disabled] .enabled, button.btn-disabled .enabled {
|
||||
.font-courier {
|
||||
font-family: "Courier New", Consolas, "Liberation Mono", monospace, ui-monospace, SFMono-Regular, Menlo, Monaco;
|
||||
}
|
||||
|
||||
markdown-editor [contenteditable] p {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
+52
-6
@@ -4,15 +4,27 @@ import { isValidSignatureCanvas } from './submission_form/validate_signature'
|
||||
|
||||
window.customElements.define('draw-signature', class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
const scale = 3
|
||||
|
||||
this.canvas.width = this.canvas.parentNode.clientWidth * scale
|
||||
this.canvas.height = this.canvas.parentNode.clientHeight * scale
|
||||
|
||||
this.canvas.getContext('2d').scale(scale, scale)
|
||||
this.setCanvasSize()
|
||||
|
||||
this.pad = new SignaturePad(this.canvas)
|
||||
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (!this.canvas) return
|
||||
if (!this.canvas.parentNode?.clientWidth) return
|
||||
|
||||
const { width, height } = this.canvas
|
||||
|
||||
this.setCanvasSize()
|
||||
|
||||
if (this.canvas.width !== width || this.canvas.height !== height) {
|
||||
this.redrawCanvas(width, height)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.resizeObserver.observe(this.canvas.parentNode)
|
||||
|
||||
if (this.dataset.color) {
|
||||
this.pad.penColor = this.dataset.color
|
||||
}
|
||||
@@ -57,6 +69,40 @@ window.customElements.define('draw-signature', class extends HTMLElement {
|
||||
this.updateSubmitButtonVisibility()
|
||||
}
|
||||
|
||||
disconnectedCallback () {
|
||||
if (this.resizeObserver) {
|
||||
this.resizeObserver.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
setCanvasSize () {
|
||||
const scale = 3
|
||||
|
||||
const width = this.canvas.parentNode.clientWidth
|
||||
const height = this.canvas.parentNode.clientHeight
|
||||
|
||||
if (this.canvas.width !== width * scale || this.canvas.height !== height * scale) {
|
||||
this.canvas.width = width * scale
|
||||
this.canvas.height = height * scale
|
||||
|
||||
this.canvas.getContext('2d').scale(scale, scale)
|
||||
}
|
||||
}
|
||||
|
||||
redrawCanvas (oldWidth, oldHeight) {
|
||||
if (this.pad && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0 && this.canvas.width > 0 && this.canvas.height > 0) {
|
||||
const sx = this.canvas.width / oldWidth
|
||||
const sy = this.canvas.height / oldHeight
|
||||
|
||||
const scaledData = this.pad.toData().map((stroke) => ({
|
||||
...stroke,
|
||||
points: stroke.points.map((p) => ({ ...p, x: p.x * sx, y: p.y * sy }))
|
||||
}))
|
||||
|
||||
this.pad.fromData(scaledData)
|
||||
}
|
||||
}
|
||||
|
||||
updateSubmitButtonVisibility () {
|
||||
if (this.pad.isEmpty()) {
|
||||
this.submitButton.style.display = 'none'
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { target, targetable } from '@github/catalyst/lib/targetable'
|
||||
|
||||
export default targetable(class extends HTMLElement {
|
||||
static [target.static] = [
|
||||
'prompt',
|
||||
'processing',
|
||||
'logo'
|
||||
]
|
||||
|
||||
connectedCallback () {
|
||||
this.form.addEventListener('submit', this.onSubmit)
|
||||
}
|
||||
|
||||
disconnectedCallback () {
|
||||
this.form.removeEventListener('submit', this.onSubmit)
|
||||
}
|
||||
|
||||
onSubmit = () => {
|
||||
this.prompt.classList.add('hidden')
|
||||
this.processing.classList.remove('hidden')
|
||||
this.logo.classList.add('animate-bounce')
|
||||
}
|
||||
|
||||
get form () {
|
||||
return this.querySelector('form')
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,73 @@
|
||||
export function convertImage (sourceFile, targetType, quality) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onload = function (event) {
|
||||
const img = new Image()
|
||||
|
||||
img.onload = function () {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
canvas.width = img.width
|
||||
canvas.height = img.height
|
||||
ctx.drawImage(img, 0, 0)
|
||||
canvas.toBlob(function (blob) {
|
||||
const ext = targetType === 'image/jpeg' ? '.jpg' : '.png'
|
||||
const newFile = new File([blob], sourceFile.name.replace(/\.\w+$/, ext), { type: targetType })
|
||||
resolve(newFile)
|
||||
}, targetType, quality)
|
||||
}
|
||||
|
||||
img.onerror = () => reject(new Error(`browser cannot decode ${sourceFile.type || sourceFile.name}`))
|
||||
|
||||
img.src = event.target.result
|
||||
}
|
||||
reader.onerror = reject
|
||||
reader.readAsDataURL(sourceFile)
|
||||
})
|
||||
}
|
||||
|
||||
export async function convertImagesInInput (input) {
|
||||
if (!input.files || input.files.length === 0) return
|
||||
|
||||
const dt = new DataTransfer()
|
||||
let didConvert = false
|
||||
|
||||
for (const file of Array.from(input.files)) {
|
||||
let converted = file
|
||||
|
||||
try {
|
||||
if (['image/bmp', 'image/vnd.microsoft.icon', 'image/svg+xml', 'image/gif'].includes(file.type)) {
|
||||
converted = await convertImage(file, 'image/png')
|
||||
didConvert = true
|
||||
} else if (['image/heic', 'image/heif', 'image/heic-sequence', 'image/heif-sequence', 'image/avif', 'image/avif-sequence', 'image/webp'].includes(file.type)) {
|
||||
converted = await convertImage(file, 'image/jpeg', 0.9)
|
||||
didConvert = true
|
||||
}
|
||||
} catch (e) {
|
||||
alert(e.message)
|
||||
}
|
||||
|
||||
dt.items.add(converted)
|
||||
}
|
||||
|
||||
if (didConvert) {
|
||||
input.files = dt.files
|
||||
}
|
||||
}
|
||||
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
const input = this.querySelector('input[type="file"]')
|
||||
const form = input.form
|
||||
|
||||
input.addEventListener('change', async () => {
|
||||
await convertImagesInInput(input)
|
||||
|
||||
form.querySelector('[type="submit"]')?.setAttribute('disabled', true)
|
||||
|
||||
form.requestSubmit()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
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('')
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { target, targets, targetable } from '@github/catalyst/lib/targetable'
|
||||
import { convertImagesInInput } from './convert_upload'
|
||||
|
||||
const loadingIconHtml = `<svg xmlns="http://www.w3.org/2000/svg" class="animate-spin" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
@@ -150,12 +151,16 @@ export default targetable(class extends HTMLElement {
|
||||
if (!this.isLoading) this.hideDraghover()
|
||||
}
|
||||
|
||||
uploadFiles (files, url) {
|
||||
async uploadFiles (files, url) {
|
||||
this.isLoading = true
|
||||
|
||||
this.form.action = url
|
||||
|
||||
this.form.querySelector('[type="file"]').files = files
|
||||
const input = this.form.querySelector('[type="file"]')
|
||||
|
||||
input.files = files
|
||||
|
||||
await convertImagesInInput(input)
|
||||
|
||||
this.form.querySelector('[type="submit"]').click()
|
||||
}
|
||||
|
||||
@@ -5,6 +5,12 @@ export default targetable(class extends HTMLElement {
|
||||
|
||||
connectedCallback () {
|
||||
this.addEventListener('click', () => this.downloadFiles())
|
||||
this.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
this.downloadFiles()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
toggleState () {
|
||||
|
||||
@@ -64,6 +64,22 @@ export default class extends HTMLElement {
|
||||
if (action === 'empty' || action === 'unchecked') return this.isEmpty(actual)
|
||||
if (action === 'not_empty' || action === 'checked') return !this.isEmpty(actual)
|
||||
|
||||
if (['equal', 'not_equal', 'greater_than', 'less_than'].includes(action) && this.sourceEl?.getAttribute('type') === 'number') {
|
||||
if (this.isEmpty(actual) || this.isEmpty(expected)) return false
|
||||
|
||||
const actualNumber = parseFloat(actual)
|
||||
const expectedNumber = parseFloat(expected)
|
||||
|
||||
if (Number.isNaN(actualNumber) || Number.isNaN(expectedNumber)) return false
|
||||
|
||||
if (action === 'equal') return Math.abs(actualNumber - expectedNumber) < Number.EPSILON
|
||||
if (action === 'not_equal') return Math.abs(actualNumber - expectedNumber) > Number.EPSILON
|
||||
if (action === 'greater_than') return actualNumber > expectedNumber
|
||||
if (action === 'less_than') return actualNumber < expectedNumber
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
if (action === 'equal') {
|
||||
const list = Array.isArray(actual) ? actual : [actual]
|
||||
return list.filter((v) => v !== null && v !== undefined).map(String).includes(String(expected))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { actionable } from '@github/catalyst/lib/actionable'
|
||||
import { target, targetable } from '@github/catalyst/lib/targetable'
|
||||
import { convertImagesInInput } from './convert_upload'
|
||||
|
||||
export default actionable(targetable(class extends HTMLElement {
|
||||
static [target.static] = [
|
||||
@@ -38,17 +39,21 @@ export default actionable(targetable(class extends HTMLElement {
|
||||
this.classList.add('border-base-300', 'hover:bg-base-200/30')
|
||||
}
|
||||
|
||||
onDrop (e) {
|
||||
async onDrop (e) {
|
||||
e.preventDefault()
|
||||
|
||||
this.input.files = e.dataTransfer.files
|
||||
|
||||
this.uploadFiles(e.dataTransfer.files)
|
||||
await convertImagesInInput(this.input)
|
||||
|
||||
this.uploadFiles(this.input.files)
|
||||
}
|
||||
|
||||
onSelectFiles (e) {
|
||||
async onSelectFiles (e) {
|
||||
e.preventDefault()
|
||||
|
||||
await convertImagesInInput(this.input)
|
||||
|
||||
this.uploadFiles(this.input.files)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
import { target, targetable } from '@github/catalyst/lib/targetable'
|
||||
import { actionable } from '@github/catalyst/lib/actionable'
|
||||
|
||||
function loadTiptap () {
|
||||
return Promise.all([
|
||||
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/core'),
|
||||
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-bold'),
|
||||
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-italic'),
|
||||
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-paragraph'),
|
||||
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-text'),
|
||||
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-hard-break'),
|
||||
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-document'),
|
||||
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-link'),
|
||||
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-underline'),
|
||||
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extensions'),
|
||||
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/markdown'),
|
||||
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/pm/state'),
|
||||
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/pm/view')
|
||||
]).then(([core, bold, italic, paragraph, text, hardBreak, document, link, underline, extensions, markdown, pmState, pmView]) => ({
|
||||
Editor: core.Editor,
|
||||
Extension: core.Extension,
|
||||
Bold: bold.default || bold,
|
||||
Italic: italic.default || italic,
|
||||
Paragraph: paragraph.default || paragraph,
|
||||
Text: text.default || text,
|
||||
HardBreak: hardBreak.default || hardBreak,
|
||||
Document: document.default || document,
|
||||
Link: link.default || link,
|
||||
Underline: underline.default || underline,
|
||||
UndoRedo: extensions.UndoRedo,
|
||||
Markdown: markdown.Markdown,
|
||||
Plugin: pmState.Plugin,
|
||||
Decoration: pmView.Decoration,
|
||||
DecorationSet: pmView.DecorationSet
|
||||
}))
|
||||
}
|
||||
|
||||
class LinkTooltip {
|
||||
constructor (container, editor, templateEl) {
|
||||
this.container = container
|
||||
this.editor = editor
|
||||
|
||||
this.tooltip = templateEl.content.firstElementChild.cloneNode(true)
|
||||
|
||||
this.input = this.tooltip.querySelector('input')
|
||||
this.saveButton = this.tooltip.querySelector('[data-role="link-save"]')
|
||||
this.removeButton = this.tooltip.querySelector('[data-role="link-remove"]')
|
||||
|
||||
container.style.position = 'relative'
|
||||
container.appendChild(this.tooltip)
|
||||
}
|
||||
|
||||
isVisible () {
|
||||
return !this.tooltip.classList.contains('hidden')
|
||||
}
|
||||
|
||||
normalizeUrl (url) {
|
||||
if (!url) return url
|
||||
if (/^{/i.test(url)) return url
|
||||
if (/^https?:\/\//i.test(url)) return url
|
||||
if (/^mailto:/i.test(url)) return url
|
||||
|
||||
return `https://${url}`
|
||||
}
|
||||
|
||||
show (url, pos, { focus = false } = {}) {
|
||||
this.input.value = url || ''
|
||||
this.removeButton.classList.toggle('hidden', !url)
|
||||
|
||||
this.tooltip.classList.remove('hidden')
|
||||
|
||||
const coords = this.editor.view.coordsAtPos(pos)
|
||||
const containerRect = this.container.getBoundingClientRect()
|
||||
|
||||
this.tooltip.style.left = `${coords.left - containerRect.left}px`
|
||||
this.tooltip.style.top = `${coords.bottom - containerRect.top + 4}px`
|
||||
|
||||
if (focus) this.input.focus()
|
||||
|
||||
this.saveHandler = () => {
|
||||
const inputUrl = this.input.value.trim()
|
||||
|
||||
if (inputUrl) {
|
||||
this.editor.chain().focus().extendMarkRange('link').setLink({ href: this.normalizeUrl(inputUrl) }).run()
|
||||
}
|
||||
|
||||
this.hide()
|
||||
}
|
||||
|
||||
this.removeHandler = () => {
|
||||
this.editor.chain().focus().extendMarkRange('link').unsetLink().run()
|
||||
|
||||
this.hide()
|
||||
}
|
||||
|
||||
this.keyHandler = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
this.saveHandler()
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
this.hide()
|
||||
}
|
||||
}
|
||||
|
||||
this.saveButton.addEventListener('click', this.saveHandler, { once: true })
|
||||
this.removeButton.addEventListener('click', this.removeHandler, { once: true })
|
||||
this.input.addEventListener('keydown', this.keyHandler)
|
||||
}
|
||||
|
||||
hide () {
|
||||
if (this.saveHandler) {
|
||||
this.saveButton.removeEventListener('click', this.saveHandler)
|
||||
this.saveHandler = null
|
||||
}
|
||||
|
||||
if (this.removeHandler) {
|
||||
this.removeButton.removeEventListener('click', this.removeHandler)
|
||||
this.removeHandler = null
|
||||
}
|
||||
|
||||
if (this.keyHandler) {
|
||||
this.input.removeEventListener('keydown', this.keyHandler)
|
||||
this.keyHandler = null
|
||||
}
|
||||
|
||||
this.tooltip.classList.add('hidden')
|
||||
this.currentMark = null
|
||||
}
|
||||
}
|
||||
|
||||
export default actionable(targetable(class extends HTMLElement {
|
||||
static [target.static] = [
|
||||
'textarea',
|
||||
'editorElement',
|
||||
'boldButton',
|
||||
'italicButton',
|
||||
'underlineButton',
|
||||
'linkButton',
|
||||
'linkTooltipTemplate'
|
||||
]
|
||||
|
||||
async connectedCallback () {
|
||||
if (!this.textarea || !this.editorElement) return
|
||||
|
||||
this.textarea.style.display = 'none'
|
||||
this.adjustShortcutsForPlatform()
|
||||
|
||||
const { Editor, Extension, Bold, Italic, Paragraph, Text, HardBreak, UndoRedo, Document, Link, Underline, Markdown, Plugin, Decoration, DecorationSet } = await loadTiptap()
|
||||
|
||||
const buildDecorations = (doc) => {
|
||||
const decorations = []
|
||||
const regex = /\{\{?[a-zA-Z0-9_.-]+\}\}?/g
|
||||
|
||||
doc.descendants((node, pos) => {
|
||||
if (!node.isText) return
|
||||
|
||||
let match
|
||||
|
||||
while ((match = regex.exec(node.text)) !== null) {
|
||||
decorations.push(
|
||||
Decoration.inline(pos + match.index, pos + match.index + match[0].length, {
|
||||
class: 'bg-amber-100 py-0.5 px-1 rounded'
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return DecorationSet.create(doc, decorations)
|
||||
}
|
||||
|
||||
const VariableHighlight = Extension.create({
|
||||
name: 'variableHighlight',
|
||||
addProseMirrorPlugins () {
|
||||
return [new Plugin({
|
||||
state: {
|
||||
init (_, { doc }) {
|
||||
return buildDecorations(doc)
|
||||
},
|
||||
apply (tr, oldSet) {
|
||||
return tr.docChanged ? buildDecorations(tr.doc) : oldSet
|
||||
}
|
||||
},
|
||||
props: {
|
||||
decorations (state) {
|
||||
return this.getState(state)
|
||||
}
|
||||
}
|
||||
})]
|
||||
}
|
||||
})
|
||||
|
||||
this.editor = new Editor({
|
||||
element: this.editorElement,
|
||||
extensions: [
|
||||
Markdown,
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
Bold,
|
||||
Italic,
|
||||
HardBreak.extend({
|
||||
addKeyboardShortcuts () {
|
||||
return {
|
||||
Enter: () => this.editor.commands.setHardBreak()
|
||||
}
|
||||
}
|
||||
}),
|
||||
UndoRedo,
|
||||
Link.extend({
|
||||
inclusive: true,
|
||||
addKeyboardShortcuts: () => ({
|
||||
'Mod-k': () => {
|
||||
this.toggleLink()
|
||||
|
||||
return true
|
||||
}
|
||||
})
|
||||
}).configure({
|
||||
openOnClick: false,
|
||||
HTMLAttributes: {
|
||||
class: 'link',
|
||||
'data-turbo': 'false',
|
||||
style: 'color: #2563eb; text-decoration: underline; cursor: text;'
|
||||
}
|
||||
}),
|
||||
Underline,
|
||||
VariableHighlight
|
||||
],
|
||||
content: (this.textarea.value || '').trim().replace(/ *\n/g, '<br>'),
|
||||
contentType: 'markdown',
|
||||
editorProps: {
|
||||
attributes: {
|
||||
style: 'min-height: 220px',
|
||||
dir: 'auto',
|
||||
class: 'p-3 outline-none focus:outline-none'
|
||||
}
|
||||
},
|
||||
onUpdate: ({ editor }) => {
|
||||
this.textarea.value = editor.getMarkdown()
|
||||
this.textarea.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
},
|
||||
onSelectionUpdate: ({ editor }) => {
|
||||
this.updateToolbarState()
|
||||
this.handleLinkTooltip(editor)
|
||||
},
|
||||
onBlur: () => {
|
||||
setTimeout(() => {
|
||||
if (!this.linkTooltip.tooltip.contains(document.activeElement)) {
|
||||
this.linkTooltip.hide()
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
})
|
||||
|
||||
this.linkTooltip = new LinkTooltip(this, this.editor, this.linkTooltipTemplate)
|
||||
}
|
||||
|
||||
adjustShortcutsForPlatform () {
|
||||
if ((navigator.userAgentData?.platform || navigator.platform)?.toLowerCase()?.includes('mac')) {
|
||||
this.querySelectorAll('.tooltip[data-tip]').forEach(tooltip => {
|
||||
const tip = tooltip.getAttribute('data-tip')
|
||||
|
||||
if (tip && tip.includes('Ctrl')) {
|
||||
tooltip.setAttribute('data-tip', tip.replace(/Ctrl/g, '⌘'))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
bold (e) {
|
||||
e.preventDefault()
|
||||
|
||||
this.editor.chain().focus().toggleBold().run()
|
||||
this.updateToolbarState()
|
||||
}
|
||||
|
||||
italic (e) {
|
||||
e.preventDefault()
|
||||
|
||||
this.editor.chain().focus().toggleItalic().run()
|
||||
this.updateToolbarState()
|
||||
}
|
||||
|
||||
underline (e) {
|
||||
e.preventDefault()
|
||||
|
||||
this.editor.chain().focus().toggleUnderline().run()
|
||||
this.updateToolbarState()
|
||||
}
|
||||
|
||||
linkSelection (e) {
|
||||
e.preventDefault()
|
||||
|
||||
this.toggleLink()
|
||||
this.updateToolbarState()
|
||||
}
|
||||
|
||||
undo (e) {
|
||||
e.preventDefault()
|
||||
|
||||
this.editor.chain().focus().undo().run()
|
||||
this.updateToolbarState()
|
||||
}
|
||||
|
||||
redo (e) {
|
||||
e.preventDefault()
|
||||
|
||||
this.editor.chain().focus().redo().run()
|
||||
this.updateToolbarState()
|
||||
}
|
||||
|
||||
updateToolbarState () {
|
||||
this.boldButton.classList.toggle('bg-base-200', this.editor.isActive('bold'))
|
||||
this.italicButton.classList.toggle('bg-base-200', this.editor.isActive('italic'))
|
||||
this.underlineButton.classList.toggle('bg-base-200', this.editor.isActive('underline'))
|
||||
this.linkButton.classList.toggle('bg-base-200', this.editor.isActive('link'))
|
||||
}
|
||||
|
||||
handleLinkTooltip (editor) {
|
||||
const { from } = editor.state.selection
|
||||
const mark = editor.state.doc.resolve(from).marks().find(m => m.type.name === 'link')
|
||||
|
||||
if (!mark) {
|
||||
if (this.linkTooltip.isVisible()) this.linkTooltip.hide()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (this.linkTooltip.isVisible() && this.linkTooltip.currentMark === mark) return
|
||||
|
||||
let linkStart = from
|
||||
const start = editor.state.doc.resolve(from).start()
|
||||
|
||||
for (let i = from - 1; i >= start; i--) {
|
||||
if (editor.state.doc.resolve(i).marks().some(m => m.eq(mark))) {
|
||||
linkStart = i
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
this.linkTooltip.hide()
|
||||
this.linkTooltip.show(mark.attrs.href, linkStart > start ? linkStart - 1 : linkStart)
|
||||
this.linkTooltip.currentMark = mark
|
||||
}
|
||||
|
||||
toggleLink () {
|
||||
if (this.editor.isActive('link')) {
|
||||
this.linkTooltip.hide()
|
||||
this.editor.chain().focus().extendMarkRange('link').unsetLink().run()
|
||||
this.updateToolbarState()
|
||||
} else {
|
||||
const { from } = this.editor.state.selection
|
||||
|
||||
this.linkTooltip.hide()
|
||||
this.linkTooltip.show(this.editor.getAttributes('link').href, from, { focus: true })
|
||||
}
|
||||
}
|
||||
|
||||
insertVariable (e) {
|
||||
const variable = e.target.closest('[data-variable]')?.dataset.variable
|
||||
|
||||
if (variable) {
|
||||
const { from, to } = this.editor.state.selection
|
||||
|
||||
if (variable.includes('link') && from !== to) {
|
||||
this.editor.chain().focus().setLink({ href: `{${variable}}` }).run()
|
||||
} else {
|
||||
this.editor.chain().focus().insertContent(`{${variable}}`).run()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback () {
|
||||
this.linkTooltip.hide()
|
||||
|
||||
if (this.editor) {
|
||||
this.editor.destroy()
|
||||
}
|
||||
}
|
||||
}))
|
||||
@@ -0,0 +1,18 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
const dialog = document.getElementById(this.dataset.target)
|
||||
|
||||
this.querySelector('button').addEventListener('click', () => {
|
||||
if (dialog) {
|
||||
dialog.inert = false
|
||||
dialog.showModal()
|
||||
}
|
||||
})
|
||||
|
||||
if (dialog) {
|
||||
dialog.addEventListener('close', () => {
|
||||
dialog.inert = true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,11 +47,13 @@ export default class extends HTMLElement {
|
||||
|
||||
this.classList.remove('hidden', '-translate-y-10', 'opacity-0')
|
||||
this.classList.add('translate-y-0', 'opacity-100')
|
||||
this.inert = false
|
||||
}
|
||||
|
||||
hideButtons () {
|
||||
this.classList.remove('translate-y-0', 'opacity-100')
|
||||
this.classList.add('-translate-y-10', 'opacity-0')
|
||||
this.inert = true
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.classList.contains('-translate-y-10')) {
|
||||
|
||||
@@ -5,17 +5,29 @@ export default targetable(class extends HTMLElement {
|
||||
static [target.static] = ['canvas', 'input', 'clear', 'button']
|
||||
|
||||
async connectedCallback () {
|
||||
const scale = 3
|
||||
|
||||
this.canvas.width = this.canvas.parentNode.clientWidth * scale
|
||||
this.canvas.height = this.canvas.parentNode.clientHeight * scale
|
||||
|
||||
this.canvas.getContext('2d').scale(scale, scale)
|
||||
|
||||
const { default: SignaturePad } = await import('signature_pad')
|
||||
|
||||
this.setCanvasSize()
|
||||
|
||||
this.pad = new SignaturePad(this.canvas)
|
||||
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (!this.canvas) return
|
||||
if (!this.canvas.parentNode?.clientWidth) return
|
||||
|
||||
const { width, height } = this.canvas
|
||||
|
||||
this.setCanvasSize()
|
||||
|
||||
if (this.canvas.width !== width || this.canvas.height !== height) {
|
||||
this.redrawCanvas(width, height)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.resizeObserver.observe(this.canvas.parentNode)
|
||||
|
||||
this.clear.addEventListener('click', (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
@@ -47,4 +59,38 @@ export default targetable(class extends HTMLElement {
|
||||
|
||||
this.closest('form').requestSubmit()
|
||||
}
|
||||
|
||||
disconnectedCallback () {
|
||||
if (this.resizeObserver) {
|
||||
this.resizeObserver.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
setCanvasSize () {
|
||||
const scale = 3
|
||||
|
||||
const width = this.canvas.parentNode.clientWidth
|
||||
const height = this.canvas.parentNode.clientWidth / 2.5
|
||||
|
||||
if (this.canvas.width !== width * scale || this.canvas.height !== height * scale) {
|
||||
this.canvas.width = width * scale
|
||||
this.canvas.height = height * scale
|
||||
|
||||
this.canvas.getContext('2d').scale(scale, scale)
|
||||
}
|
||||
}
|
||||
|
||||
redrawCanvas (oldWidth, oldHeight) {
|
||||
if (this.pad && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0 && this.canvas.width > 0 && this.canvas.height > 0) {
|
||||
const sx = this.canvas.width / oldWidth
|
||||
const sy = this.canvas.height / oldHeight
|
||||
|
||||
const scaledData = this.pad.toData().map((stroke) => ({
|
||||
...stroke,
|
||||
points: stroke.points.map((p) => ({ ...p, x: p.x * sx, y: p.y * sy }))
|
||||
}))
|
||||
|
||||
this.pad.fromData(scaledData)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.input.addEventListener('change', (event) => {
|
||||
if (!this.target) return
|
||||
|
||||
const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value
|
||||
const dataValue = this.dataset.value === 'false' ? false : this.dataset.value || true
|
||||
|
||||
if (this.dataset.attribute) {
|
||||
this.target[this.dataset.attribute] = event.target.checked
|
||||
this.target[this.dataset.attribute] = value === dataValue
|
||||
}
|
||||
|
||||
if (this.dataset.className) {
|
||||
this.target.classList.toggle(this.dataset.className, event.target.value !== this.dataset.value)
|
||||
this.target.classList.toggle(this.dataset.className, value !== dataValue)
|
||||
|
||||
if (this.dataset.className === 'hidden' && this.target.tagName === 'INPUT') {
|
||||
this.target.disabled = event.target.value !== this.dataset.value
|
||||
}
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
const button = this.querySelector('a, button')
|
||||
const button = this.querySelector('a, button, label')
|
||||
|
||||
const target = this.dataset.targetId ? document.getElementById(this.dataset.targetId) : button
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
this.dataset.classes.split(' ').forEach((cls) => {
|
||||
button.classList.toggle(cls)
|
||||
if (this.dataset.action === 'remove') {
|
||||
target.classList.remove(cls)
|
||||
} else if (this.dataset.action === 'add') {
|
||||
target.classList.add(cls)
|
||||
} else {
|
||||
target.classList.toggle(cls)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,6 +12,6 @@ export default class extends HTMLElement {
|
||||
}
|
||||
|
||||
get button () {
|
||||
return this.querySelector('button')
|
||||
return this.querySelector('button, label')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import FetchForm from './elements/fetch_form'
|
||||
import ScrollButtons from './elements/scroll_buttons'
|
||||
import PageContainer from './elements/page_container'
|
||||
import SubmitForm from './elements/submit_form'
|
||||
import ModalButton from './elements/modal_button'
|
||||
|
||||
const safeRegisterElement = (name, element, options = {}) => !window.customElements.get(name) && window.customElements.define(name, element, options)
|
||||
|
||||
@@ -16,6 +17,7 @@ safeRegisterElement('fetch-form', FetchForm)
|
||||
safeRegisterElement('scroll-buttons', ScrollButtons)
|
||||
safeRegisterElement('page-container', PageContainer)
|
||||
safeRegisterElement('submit-form', SubmitForm)
|
||||
safeRegisterElement('modal-button', ModalButton)
|
||||
safeRegisterElement('submission-form', class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.appElem = document.createElement('div')
|
||||
@@ -31,12 +33,13 @@ safeRegisterElement('submission-form', class extends HTMLElement {
|
||||
isDemo: this.dataset.isDemo === 'true',
|
||||
attribution: this.dataset.attribution !== 'false',
|
||||
scrollPadding: this.dataset.scrollPadding || '-80px',
|
||||
signatureText: this.dataset.signatureText,
|
||||
language: this.dataset.language,
|
||||
dryRun: this.dataset.dryRun === 'true',
|
||||
expand: ['true', 'false'].includes(this.dataset.expand) ? this.dataset.expand === 'true' : null,
|
||||
withSignatureId: this.dataset.withSignatureId === 'true',
|
||||
requireSigningReason: this.dataset.requireSigningReason === 'true',
|
||||
withConfetti: this.dataset.withConfetti !== 'false',
|
||||
withConfetti: this.dataset.withConfetti === 'true',
|
||||
withFieldLabels: this.dataset.withFieldLabels !== 'false',
|
||||
withDisclosure: this.dataset.withDisclosure === 'true',
|
||||
reuseSignature: this.dataset.reuseSignature !== 'false',
|
||||
@@ -49,7 +52,9 @@ safeRegisterElement('submission-form', class extends HTMLElement {
|
||||
completedMessage: JSON.parse(this.dataset.completedMessage || '{}'),
|
||||
completedRedirectUrl: this.dataset.completedRedirectUrl,
|
||||
attachments: reactive(JSON.parse(this.dataset.attachments)),
|
||||
fields: JSON.parse(this.dataset.fields)
|
||||
fields: JSON.parse(this.dataset.fields),
|
||||
completeButtonContainer: document.getElementById('complete_button_container'),
|
||||
completeButtonScrollContainer: document.getElementById('complete_button_container_scroll')
|
||||
})
|
||||
|
||||
this.app.mount(this.appElem)
|
||||
|
||||
@@ -0,0 +1,434 @@
|
||||
<template>
|
||||
<template
|
||||
v-for="(pages, docUuid) in textRuns"
|
||||
:key="docUuid"
|
||||
>
|
||||
<template
|
||||
v-for="(_, pageIndex) in pages"
|
||||
:key="pageIndex"
|
||||
>
|
||||
<template
|
||||
v-for="(pageElem, i) in [findPageElement(docUuid, pageIndex)]"
|
||||
:key="i"
|
||||
>
|
||||
<Teleport
|
||||
v-if="pageElem"
|
||||
:to="pageElem"
|
||||
>
|
||||
<template
|
||||
v-for="(item, index) in sortedItemsForPage(docUuid, pageIndex)"
|
||||
:key="index"
|
||||
>
|
||||
<template v-if="item.type === 'text_group'">
|
||||
<span
|
||||
class="absolute overflow-hidden text-transparent select-none pointer-events-none"
|
||||
:style="{ left: item.x * 100 + '%', top: item.y * 100 + '%', width: item.w * 100 + '%', height: item.h * 100 + '%' }"
|
||||
>{{ item.text }}</span>
|
||||
<span
|
||||
v-for="(run, runIndex) in item.items"
|
||||
:key="runIndex"
|
||||
aria-hidden="true"
|
||||
class="absolute overflow-hidden text-transparent"
|
||||
:style="{ left: run.x * 100 + '%', top: run.y * 100 + '%', width: run.w * 100 + '%', height: run.h * 100 + '%', fontSize: run.font_size ? (run.font_size / 10) + 'cqmin' : undefined, textAlign: 'justify', textAlignLast: 'justify', textJustify: 'inter-character' }"
|
||||
>{{ run.text }}</span>
|
||||
</template>
|
||||
<FieldArea
|
||||
v-else-if="item.type === 'field_area'"
|
||||
:ref="setAreaRef"
|
||||
v-model="values[item.field.uuid]"
|
||||
:values="values"
|
||||
:field="item.field"
|
||||
:area="item.area"
|
||||
:submittable="true"
|
||||
:page-width="1400"
|
||||
:page-height="(1400.0 / pageElem.offsetWidth) * pageElem.offsetHeight"
|
||||
:field-index="item.fieldIndex"
|
||||
:is-inline-size="isInlineSize"
|
||||
:scroll-padding="scrollPadding"
|
||||
:submitter="submitter"
|
||||
:with-field-placeholder="withFieldPlaceholder"
|
||||
:with-signature-id="withSignatureId"
|
||||
:is-active="currentStep === item.step"
|
||||
:with-label="withLabel && !withFieldPlaceholder && item.step.length < 2"
|
||||
:is-value-set="item.step.some((f) => f.uuid in values)"
|
||||
:attachments-index="attachmentsIndex"
|
||||
@click="[$emit('focus-step', item.stepIndex), maybeScrollOnClick(item.field, item.area)]"
|
||||
/>
|
||||
<FieldArea
|
||||
v-else-if="item.type === 'readonly_field_area'"
|
||||
:model-value="readonlyConditionalFieldValues[item.field.uuid]"
|
||||
:values="readonlyConditionalFieldValues"
|
||||
:field="item.field"
|
||||
:area="item.area"
|
||||
:submittable="false"
|
||||
:page-width="1400"
|
||||
:page-height="(1400.0 / pageElem.offsetWidth) * pageElem.offsetHeight"
|
||||
:field-index="item.fieldIndex"
|
||||
:is-inline-size="isInlineSize"
|
||||
:submitter="submitter"
|
||||
:attachments-index="attachmentsIndex"
|
||||
/>
|
||||
<FieldArea
|
||||
v-else-if="item.type === 'formula_area' && isMathLoaded"
|
||||
:model-value="calculateFormula(item.field)"
|
||||
:is-inline-size="isInlineSize"
|
||||
:field="item.field"
|
||||
:area="item.area"
|
||||
:submittable="false"
|
||||
:field-index="item.fieldIndex"
|
||||
/>
|
||||
</template>
|
||||
</Teleport>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FieldArea from './area'
|
||||
import FormulaAreas from './formula_areas'
|
||||
import FieldAreas from './areas'
|
||||
|
||||
export default {
|
||||
name: 'AccessibilityAreas',
|
||||
components: {
|
||||
FieldArea
|
||||
},
|
||||
inject: ['baseUrl', 't'],
|
||||
props: {
|
||||
submitterSlug: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
filledFieldsIndex: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
steps: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
readonlyConditionalFields: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
readonlyConditionalFieldValues: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({})
|
||||
},
|
||||
formulaFields: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
values: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({})
|
||||
},
|
||||
readonlyValues: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({})
|
||||
},
|
||||
submitter: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
currentStep: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
withFieldPlaceholder: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
withSignatureId: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
withLabel: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
scrollPadding: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '-80px'
|
||||
},
|
||||
scrollEl: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
attachmentsIndex: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({})
|
||||
},
|
||||
fetchOptions: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
emits: ['focus-step'],
|
||||
data () {
|
||||
return {
|
||||
isMathLoaded: false,
|
||||
math: null,
|
||||
textRuns: {},
|
||||
areaRefs: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
fieldValuesIndex () {
|
||||
return this.filledFieldsIndex || this.extractStaticValues()
|
||||
},
|
||||
isMobileContainer: FieldAreas.computed.isMobileContainer,
|
||||
isInlineSize: FieldAreas.computed.isInlineSize,
|
||||
fieldsUuidIndex () {
|
||||
return this.formulaFields.reduce((acc, field) => {
|
||||
acc[field.uuid] = field
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
},
|
||||
fieldAreasIndex () {
|
||||
const index = Object.create(null)
|
||||
|
||||
this.steps.forEach((step, stepIndex) => {
|
||||
step.forEach((field, fieldIndex) => {
|
||||
(field.areas || []).forEach((area) => {
|
||||
index[area.attachment_uuid] ||= Object.create(null)
|
||||
index[area.attachment_uuid][area.page] ||= []
|
||||
index[area.attachment_uuid][area.page].push({
|
||||
type: 'field_area',
|
||||
field,
|
||||
area,
|
||||
step,
|
||||
stepIndex,
|
||||
fieldIndex,
|
||||
x: area.x,
|
||||
y: area.y,
|
||||
w: area.w,
|
||||
h: area.h
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return index
|
||||
},
|
||||
formulaAreasIndex () {
|
||||
const index = Object.create(null)
|
||||
|
||||
this.formulaFields.forEach((field, fieldIndex) => {
|
||||
(field.areas || []).forEach((area) => {
|
||||
index[area.attachment_uuid] ||= Object.create(null)
|
||||
index[area.attachment_uuid][area.page] ||= []
|
||||
index[area.attachment_uuid][area.page].push({
|
||||
type: 'formula_area',
|
||||
field,
|
||||
area,
|
||||
fieldIndex,
|
||||
x: area.x,
|
||||
y: area.y,
|
||||
w: area.w,
|
||||
h: area.h
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return index
|
||||
},
|
||||
readonlyFieldAreasIndex () {
|
||||
const index = Object.create(null)
|
||||
|
||||
this.readonlyConditionalFields.forEach((field, fieldIndex) => {
|
||||
(field.areas || []).forEach((area) => {
|
||||
index[area.attachment_uuid] ||= Object.create(null)
|
||||
index[area.attachment_uuid][area.page] ||= []
|
||||
index[area.attachment_uuid][area.page].push({
|
||||
type: 'readonly_field_area',
|
||||
field,
|
||||
area,
|
||||
fieldIndex,
|
||||
x: area.x,
|
||||
y: area.y,
|
||||
w: area.w,
|
||||
h: area.h
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return index
|
||||
}
|
||||
},
|
||||
beforeUpdate () {
|
||||
this.areaRefs = []
|
||||
},
|
||||
async mounted () {
|
||||
const [metadataResult] = await Promise.all([
|
||||
fetch(this.baseUrl + `/s/${this.submitterSlug}/metadata`, {
|
||||
...this.fetchOptions
|
||||
}).then((r) => r.json()).catch(() => ({})),
|
||||
this.loadMath()
|
||||
])
|
||||
|
||||
this.textRuns = metadataResult.text_runs || {}
|
||||
},
|
||||
methods: {
|
||||
normalizeFormula: FormulaAreas.methods.normalizeFormula,
|
||||
calculateFormula: FormulaAreas.methods.calculateFormula,
|
||||
scrollInContainer: FieldAreas.methods.scrollInContainer,
|
||||
scrollIntoArea: FieldAreas.methods.scrollIntoArea,
|
||||
scrollIntoField: FieldAreas.methods.scrollIntoField,
|
||||
maybeScrollOnClick: FieldAreas.methods.maybeScrollOnClick,
|
||||
setAreaRef (el) {
|
||||
if (el) {
|
||||
this.areaRefs.push(el)
|
||||
}
|
||||
},
|
||||
async loadMath () {
|
||||
if (this.formulaFields.length && !this.isMathLoaded) {
|
||||
const { Calculator } = await import('./calculator')
|
||||
|
||||
this.math = new Calculator()
|
||||
this.isMathLoaded = true
|
||||
}
|
||||
},
|
||||
extractStaticValues () {
|
||||
const result = Object.create(null)
|
||||
const root = this.$root.$el?.parentNode?.getRootNode() || document
|
||||
const pageContainers = root.querySelectorAll('page-container')
|
||||
|
||||
pageContainers.forEach((container) => {
|
||||
const overlay = container.querySelector('[id^="page-"]')
|
||||
|
||||
if (!overlay) return
|
||||
|
||||
const parts = overlay.id.split('-')
|
||||
const pageIndex = parseInt(parts[parts.length - 1])
|
||||
const docUuid = parts.slice(1, -1).join('-')
|
||||
|
||||
const fieldValues = overlay.querySelectorAll('field-value')
|
||||
|
||||
if (!fieldValues.length) return
|
||||
|
||||
result[docUuid] ||= Object.create(null)
|
||||
result[docUuid][pageIndex] = []
|
||||
|
||||
fieldValues.forEach((el) => {
|
||||
const style = el.style
|
||||
const x = parseFloat(style.left) / 100
|
||||
const y = parseFloat(style.top) / 100
|
||||
const w = parseFloat(style.width) / 100
|
||||
const h = parseFloat(style.height) / 100
|
||||
const text = el.textContent.trim()
|
||||
|
||||
if (text) {
|
||||
result[docUuid][pageIndex].push({ type: 'static_value', text, x, y, w, h })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return result
|
||||
},
|
||||
findPageElement (docUuid, pageIndex) {
|
||||
return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${docUuid}-${pageIndex}`)
|
||||
},
|
||||
sortedItemsForPage (docUuid, pageIndex) {
|
||||
const items = []
|
||||
|
||||
const pageTextRuns = this.textRuns[docUuid]?.[pageIndex] || []
|
||||
|
||||
pageTextRuns.forEach((run) => {
|
||||
items.push({
|
||||
type: 'text_run',
|
||||
text: run.text,
|
||||
x: run.x,
|
||||
y: run.y,
|
||||
w: run.w,
|
||||
h: run.h,
|
||||
font_size: run.font_size
|
||||
})
|
||||
})
|
||||
|
||||
const fieldAreas = this.fieldAreasIndex[docUuid]?.[pageIndex] || []
|
||||
items.push(...fieldAreas)
|
||||
|
||||
const readonlyFieldAreas = this.readonlyFieldAreasIndex[docUuid]?.[pageIndex] || []
|
||||
items.push(...readonlyFieldAreas)
|
||||
|
||||
const formulaAreas = this.formulaAreasIndex[docUuid]?.[pageIndex] || []
|
||||
items.push(...formulaAreas)
|
||||
|
||||
const pageFieldValues = this.fieldValuesIndex[docUuid]?.[pageIndex] || []
|
||||
items.push(...pageFieldValues)
|
||||
|
||||
items.sort((a, b) => {
|
||||
const aCenterY = a.y + a.h / 2
|
||||
const bCenterY = b.y + b.h / 2
|
||||
const lineThreshold = Math.min(a.h, b.h) / 2
|
||||
|
||||
if (Math.abs(aCenterY - bCenterY) < lineThreshold) {
|
||||
return a.x - b.x
|
||||
}
|
||||
|
||||
return aCenterY - bCenterY
|
||||
})
|
||||
|
||||
const grouped = []
|
||||
let currentGroup = null
|
||||
|
||||
const closeGroup = () => {
|
||||
if (!currentGroup) return
|
||||
|
||||
const groupItems = currentGroup.items
|
||||
const minX = Math.min(...groupItems.map((i) => i.x))
|
||||
const minY = Math.min(...groupItems.map((i) => i.y))
|
||||
const maxEndX = Math.max(...groupItems.map((i) => i.x + i.w))
|
||||
const maxEndY = Math.max(...groupItems.map((i) => i.y + i.h))
|
||||
|
||||
currentGroup.x = minX
|
||||
currentGroup.y = minY
|
||||
currentGroup.w = maxEndX - minX
|
||||
currentGroup.h = maxEndY - minY
|
||||
currentGroup.text = groupItems.map((i) => i.text).join(' ').replace(/\s+/g, ' ').trim()
|
||||
|
||||
grouped.push(currentGroup)
|
||||
currentGroup = null
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
const isTextLike = item.type === 'text_run' || item.type === 'static_value'
|
||||
|
||||
if (!isTextLike) {
|
||||
closeGroup()
|
||||
grouped.push(item)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!currentGroup) {
|
||||
currentGroup = { type: 'text_group', items: [] }
|
||||
}
|
||||
|
||||
currentGroup.items.push(item)
|
||||
}
|
||||
|
||||
closeGroup()
|
||||
|
||||
return grouped
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -42,10 +42,25 @@ export default {
|
||||
const areas = {}
|
||||
|
||||
this.field.areas?.forEach((area) => {
|
||||
areas[area.attachment_uuid + area.page] ||= area
|
||||
areas[area.attachment_uuid] ||= []
|
||||
areas[area.attachment_uuid].push(area)
|
||||
})
|
||||
|
||||
return Object.values(areas).slice(0, 6)
|
||||
const sortedAreas = Object.values(areas).reduce((acc, group) => {
|
||||
const seen = {}
|
||||
const sortedGroup = [...group].sort((a, b) => a.page - b.page)
|
||||
|
||||
sortedGroup.forEach((area) => {
|
||||
if (!seen[area.page]) {
|
||||
seen[area.page] = true
|
||||
acc.push(area)
|
||||
}
|
||||
})
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
return sortedAreas.slice(0, 6)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex absolute lg:text-base -outline-offset-1 field-area"
|
||||
class="flex absolute lg:text-base -outline-offset-1 focus-visible:outline-blue-500 focus-visible:outline-2 focus-visible:outline field-area"
|
||||
dir="auto"
|
||||
:style="[computedStyle, fontStyle]"
|
||||
:class="{ 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'outline-red-500 outline-dashed outline-2 z-10 field-area-active': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
|
||||
:role="submittable && !isNativeInputField ? 'button' : undefined"
|
||||
:tabindex="submittable && !isNativeInputField ? 0 : undefined"
|
||||
:aria-label="submittable && !isNativeInputField ? fieldAreaLabel : undefined"
|
||||
@keydown.enter.prevent="submittable && !isNativeInputField ? $el.click() : undefined"
|
||||
@keydown.space.prevent="submittable && !isNativeInputField ? $el.click() : undefined"
|
||||
>
|
||||
<div
|
||||
v-if="(!withFieldPlaceholder || !field.name || field.type === 'cells') && !isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
|
||||
@@ -18,6 +23,7 @@
|
||||
width="100%"
|
||||
height="100%"
|
||||
class="max-h-10 text-base-content"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
@@ -129,6 +135,7 @@
|
||||
v-if="submittable"
|
||||
type="checkbox"
|
||||
:value="false"
|
||||
:aria-label="field.name || fieldNames[field.type]"
|
||||
class="aspect-square base-checkbox"
|
||||
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||
:checked="!!modelValue"
|
||||
@@ -149,6 +156,8 @@
|
||||
v-if="submittable"
|
||||
type="radio"
|
||||
:value="false"
|
||||
:name="`radio-area-${field.uuid}`"
|
||||
:aria-label="optionValue(option)"
|
||||
class="aspect-square checked:checkbox checked:checkbox-xs"
|
||||
:class="{ 'base-radio': !modelValue || modelValue !== optionValue(option), '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||
:checked="!!modelValue && modelValue === optionValue(option)"
|
||||
@@ -169,6 +178,7 @@
|
||||
v-if="submittable"
|
||||
type="checkbox"
|
||||
:value="false"
|
||||
:aria-label="optionValue(option)"
|
||||
class="aspect-square base-checkbox"
|
||||
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||
:checked="!!modelValue && modelValue.includes(optionValue(option))"
|
||||
@@ -182,8 +192,8 @@
|
||||
</div>
|
||||
<div
|
||||
v-else-if="field.type === 'cells'"
|
||||
class="w-full flex items-center"
|
||||
:class="{ 'justify-end': field.preferences?.align === 'right', ...fontClasses }"
|
||||
class="w-full flex"
|
||||
:class="{ 'justify-end': field.preferences?.align === 'right', ...alignClasses, ...fontClasses }"
|
||||
>
|
||||
<div
|
||||
v-for="(char, index) in modelValue"
|
||||
@@ -400,6 +410,16 @@ export default {
|
||||
kba: this.t('kba')
|
||||
}
|
||||
},
|
||||
isNativeInputField () {
|
||||
return ['checkbox', 'radio', 'multiple'].includes(this.field.type)
|
||||
},
|
||||
fieldAreaLabel () {
|
||||
const name = this.field.name || this.fieldNames[this.field.type] || this.field.type
|
||||
if (this.area.option_uuid && this.option) {
|
||||
return `${name} - ${this.optionValue(this.option)}`
|
||||
}
|
||||
return name
|
||||
},
|
||||
strikethroughWidth () {
|
||||
if (this.isInlineSize) {
|
||||
return '0.6cqmin'
|
||||
@@ -507,7 +527,8 @@ export default {
|
||||
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')
|
||||
this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'),
|
||||
{ withTimePlaceholders: this.modelValue === '{{date}}' }
|
||||
)
|
||||
} catch {
|
||||
return this.modelValue
|
||||
@@ -622,40 +643,63 @@ export default {
|
||||
return new Intl.NumberFormat('de-DE').format(number)
|
||||
} else if (format === 'space') {
|
||||
return new Intl.NumberFormat('fr-FR').format(number)
|
||||
} else if (format === 'percent') {
|
||||
return `${number}%`
|
||||
} else if (format === 'percent_space') {
|
||||
return `${String(number).replace('.', ',')} %`
|
||||
} else {
|
||||
return number
|
||||
}
|
||||
},
|
||||
formatDate (date, format) {
|
||||
const monthFormats = {
|
||||
M: 'numeric',
|
||||
MM: '2-digit',
|
||||
MMM: 'short',
|
||||
MMMM: 'long'
|
||||
}
|
||||
formatDate (date, format, { withTimePlaceholders = false } = {}) {
|
||||
const monthFormats = { M: 'numeric', MM: '2-digit', MMM: 'short', MMMM: 'long' }
|
||||
const dayFormats = { D: 'numeric', DD: '2-digit' }
|
||||
const yearFormats = { YYYY: 'numeric', YYY: 'numeric', YY: '2-digit' }
|
||||
const hourFormats = { H: 'numeric', HH: '2-digit', h: 'numeric', hh: '2-digit' }
|
||||
const minuteFormats = { m: 'numeric', mm: '2-digit' }
|
||||
const secondFormats = { s: 'numeric', ss: '2-digit' }
|
||||
|
||||
const dayFormats = {
|
||||
D: 'numeric',
|
||||
DD: '2-digit'
|
||||
}
|
||||
const hasTime = /[HhAasz]/.test(format)
|
||||
|
||||
const yearFormats = {
|
||||
YYYY: 'numeric',
|
||||
YYY: 'numeric',
|
||||
YY: '2-digit'
|
||||
}
|
||||
|
||||
const parts = new Intl.DateTimeFormat([], {
|
||||
const opts = {
|
||||
day: dayFormats[format.match(/D+/)],
|
||||
month: monthFormats[format.match(/M+/)],
|
||||
year: yearFormats[format.match(/Y+/)],
|
||||
timeZone: 'UTC'
|
||||
}).formatToParts(date)
|
||||
year: yearFormats[format.match(/Y+/)]
|
||||
}
|
||||
|
||||
return format
|
||||
.replace(/D+/, parts.find((p) => p.type === 'day').value)
|
||||
.replace(/M+/, parts.find((p) => p.type === 'month').value)
|
||||
.replace(/Y+/, parts.find((p) => p.type === 'year').value)
|
||||
if (format.match(/H+/)) { opts.hour = hourFormats[format.match(/H+/)[0]]; opts.hour12 = false }
|
||||
if (format.match(/h+/)) { opts.hour = hourFormats[format.match(/h+/)[0]]; opts.hour12 = true }
|
||||
if (/[Aa]/.test(format) && opts.hour12 === undefined) opts.hour12 = true
|
||||
if (format.match(/m+/)) opts.minute = minuteFormats[format.match(/m+/)[0]]
|
||||
if (format.match(/s+/)) opts.second = secondFormats[format.match(/s+/)[0]]
|
||||
if (/z/.test(format)) opts.timeZoneName = 'short'
|
||||
if (!hasTime) opts.timeZone = 'UTC'
|
||||
|
||||
const partTypes = {
|
||||
M: 'month',
|
||||
D: 'day',
|
||||
Y: 'year',
|
||||
H: 'hour',
|
||||
h: 'hour',
|
||||
m: 'minute',
|
||||
s: 'second',
|
||||
z: 'timeZoneName',
|
||||
A: 'dayPeriod',
|
||||
a: 'dayPeriod'
|
||||
}
|
||||
|
||||
const parts = new Intl.DateTimeFormat([], opts).formatToParts(date)
|
||||
|
||||
return format.replace(/MMMM|MMM|MM|M|DD|D|YYYY|YYY|YY|HH|hh|H|h|mm|m|ss|s|A|a|z/g, (token) => {
|
||||
if (withTimePlaceholders && /^(HH|hh|H|h|mm|m|ss|s|A|a)$/.test(token)) return '--'
|
||||
|
||||
const value = parts.find((p) => p.type === partTypes[token[0]])?.value
|
||||
|
||||
if (token === 'A') return (value || '').toUpperCase()
|
||||
if (token === 'a') return (value || '').toLowerCase()
|
||||
|
||||
return value
|
||||
})
|
||||
},
|
||||
updateMultipleSelectValue (value) {
|
||||
if (this.modelValue?.includes(value)) {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="value.length">
|
||||
<div
|
||||
<ul
|
||||
v-if="value.length"
|
||||
:aria-label="t('uploaded_files')"
|
||||
class="list-none p-0 m-0"
|
||||
>
|
||||
<li
|
||||
v-for="(val, index) in value"
|
||||
:key="index"
|
||||
class="flex mb-2"
|
||||
@@ -20,23 +24,27 @@
|
||||
<IconPaperclip
|
||||
:width="16"
|
||||
class="flex-none"
|
||||
:heigh="16"
|
||||
:height="16"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>
|
||||
{{ attachmentsIndex[val].filename }}
|
||||
</span>
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
class="remove-attachment-button"
|
||||
:aria-label="`${t('clear')} ${attachmentsIndex[val].filename}`"
|
||||
@click.prevent="removeAttachment(val)"
|
||||
>
|
||||
<IconTrashX
|
||||
:width="18"
|
||||
:heigh="19"
|
||||
:height="19"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<template v-else>
|
||||
<input
|
||||
value=""
|
||||
|
||||
@@ -0,0 +1,874 @@
|
||||
/*
|
||||
* Portions of this file are derived from the Dentaku gem
|
||||
* https://github.com/rubysolo/dentaku
|
||||
*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2012 Solomon White
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
* Modifications and JavaScript port
|
||||
* Copyright (c) 2026 DocuSeal, LLC
|
||||
*/
|
||||
|
||||
class CalculatorError extends Error { constructor (msg) { super(msg); this.name = 'CalculatorError' } }
|
||||
class ParseError extends CalculatorError { constructor (msg) { super(msg); this.name = 'ParseError' } }
|
||||
class TokenizerError extends CalculatorError { constructor (msg) { super(msg); this.name = 'TokenizerError' } }
|
||||
class ArgumentError extends CalculatorError { constructor (msg) { super(msg); this.name = 'ArgumentError' } }
|
||||
class ZeroDivisionError extends CalculatorError { constructor () { super('divided by 0'); this.name = 'ZeroDivisionError' } }
|
||||
class UnboundVariableError extends CalculatorError {
|
||||
constructor (vars) {
|
||||
super('no value provided for variables: ' + vars.join(', '))
|
||||
this.name = 'UnboundVariableError'
|
||||
this.unbound = vars
|
||||
}
|
||||
}
|
||||
|
||||
class Token {
|
||||
constructor (category, value, raw) {
|
||||
this.category = category
|
||||
this.value = value
|
||||
this.raw = raw == null ? String(value) : String(raw)
|
||||
}
|
||||
|
||||
is (cat) { return this.category === cat }
|
||||
}
|
||||
|
||||
const OPERATOR_NAMES = {
|
||||
'^': 'pow',
|
||||
'+': 'add',
|
||||
'-': 'subtract',
|
||||
'*': 'multiply',
|
||||
'/': 'divide',
|
||||
'%': 'mod',
|
||||
'|': 'bitor',
|
||||
'&': 'bitand',
|
||||
'<<': 'bitshiftleft',
|
||||
'>>': 'bitshiftright'
|
||||
}
|
||||
const COMPARATOR_NAMES = {
|
||||
'<=': 'le',
|
||||
'>=': 'ge',
|
||||
'!=': 'ne',
|
||||
'<>': 'ne',
|
||||
'<': 'lt',
|
||||
'>': 'gt',
|
||||
'==': 'eq',
|
||||
'=': 'eq'
|
||||
}
|
||||
const GROUPING_NAMES = { '(': 'open', ')': 'close', ',': 'comma' }
|
||||
|
||||
function buildScanners (caseSensitive) {
|
||||
return [
|
||||
(s) => {
|
||||
const m = /^\s+/.exec(s)
|
||||
return m ? { tokens: [], length: m[0].length } : null
|
||||
},
|
||||
(s) => {
|
||||
const m = /^null\b/i.exec(s)
|
||||
return m ? { tokens: [new Token('null', null, m[0])], length: m[0].length } : null
|
||||
},
|
||||
(s) => {
|
||||
const m = /^((?:\d+(?:\.\d+)?|\.\d+)(?:[eE][+-]?\d+)?)\b/.exec(s)
|
||||
if (!m) return null
|
||||
return { tokens: [new Token('numeric', Number(m[0]), m[0])], length: m[0].length }
|
||||
},
|
||||
(s) => {
|
||||
const m = /^0x[0-9a-f]+\b/i.exec(s)
|
||||
if (!m) return null
|
||||
return { tokens: [new Token('numeric', parseInt(m[0].slice(2), 16), m[0])], length: m[0].length }
|
||||
},
|
||||
(s) => {
|
||||
const m = /^"([^"]*)"/.exec(s)
|
||||
if (!m) return null
|
||||
return { tokens: [new Token('string', m[1], m[0])], length: m[0].length }
|
||||
},
|
||||
(s) => {
|
||||
const m = /^'([^']*)'/.exec(s)
|
||||
if (!m) return null
|
||||
return { tokens: [new Token('string', m[1], m[0])], length: m[0].length }
|
||||
},
|
||||
(s, last) => {
|
||||
if (s[0] !== '-') return null
|
||||
const ok = last == null ||
|
||||
last.is('operator') ||
|
||||
last.is('comparator') ||
|
||||
last.is('combinator') ||
|
||||
last.value === 'open' ||
|
||||
last.value === 'comma' ||
|
||||
last.value === 'array_start'
|
||||
if (!ok) return null
|
||||
return { tokens: [new Token('operator', 'negate', '-')], length: 1 }
|
||||
},
|
||||
(s) => {
|
||||
const m = /^(and|or|&&|\|\|)\s/i.exec(s)
|
||||
if (!m) return null
|
||||
const raw = m[1].toLowerCase()
|
||||
const name = raw === '&&' ? 'and' : raw === '||' ? 'or' : raw
|
||||
return { tokens: [new Token('combinator', name, m[0])], length: m[0].length }
|
||||
},
|
||||
(s) => {
|
||||
const m = /^(<<|>>|\^|\+|-|\*|\/|%|\||&)/.exec(s)
|
||||
if (!m) return null
|
||||
return { tokens: [new Token('operator', OPERATOR_NAMES[m[0]], m[0])], length: m[0].length }
|
||||
},
|
||||
(s) => {
|
||||
const m = /^(\(|\)|,)/.exec(s)
|
||||
if (!m) return null
|
||||
return { tokens: [new Token('grouping', GROUPING_NAMES[m[0]], m[0])], length: m[0].length }
|
||||
},
|
||||
(s) => {
|
||||
if (s[0] === '{') return { tokens: [new Token('array', 'array_start', '{')], length: 1 }
|
||||
if (s[0] === '}') return { tokens: [new Token('array', 'array_end', '}')], length: 1 }
|
||||
return null
|
||||
},
|
||||
(s) => {
|
||||
const m = /^(<=|>=|!=|<>|==|=|<|>)/.exec(s)
|
||||
if (!m) return null
|
||||
return { tokens: [new Token('comparator', COMPARATOR_NAMES[m[0]], m[0])], length: m[0].length }
|
||||
},
|
||||
(s) => {
|
||||
const m = /^(true|false)\b/i.exec(s)
|
||||
if (!m) return null
|
||||
return { tokens: [new Token('logical', m[0].toLowerCase() === 'true', m[0])], length: m[0].length }
|
||||
},
|
||||
(s) => {
|
||||
const m = /^(\w+!?)\s*\(/.exec(s)
|
||||
if (!m) return null
|
||||
const name = m[1].toLowerCase()
|
||||
return {
|
||||
tokens: [
|
||||
new Token('function', name, m[1]),
|
||||
new Token('grouping', 'open', '(')
|
||||
],
|
||||
length: m[0].length
|
||||
}
|
||||
},
|
||||
(s) => {
|
||||
const m = /^[A-Za-z_][\w.]*\b/.exec(s)
|
||||
if (!m) return null
|
||||
const value = caseSensitive ? m[0] : m[0].toLowerCase()
|
||||
return { tokens: [new Token('identifier', value, m[0])], length: m[0].length }
|
||||
},
|
||||
(s) => {
|
||||
const m = /^`([^`]*)`/.exec(s)
|
||||
if (!m) return null
|
||||
return { tokens: [new Token('identifier', m[1], m[0])], length: m[0].length }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
function tokenize (input, options) {
|
||||
options = options || {}
|
||||
let remaining = String(input).replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
const scanners = buildScanners(options.caseSensitive === true)
|
||||
const tokens = []
|
||||
let nesting = 0
|
||||
|
||||
while (remaining.length > 0) {
|
||||
let matched = false
|
||||
for (let i = 0; i < scanners.length; i++) {
|
||||
const result = scanners[i](remaining, tokens.length ? tokens[tokens.length - 1] : null)
|
||||
if (result) {
|
||||
for (const tok of result.tokens) {
|
||||
if (tok.category === 'grouping' && tok.value === 'open') nesting++
|
||||
if (tok.category === 'grouping' && tok.value === 'close') {
|
||||
nesting--
|
||||
if (nesting < 0) throw new TokenizerError('too many closing parentheses')
|
||||
}
|
||||
tokens.push(tok)
|
||||
}
|
||||
remaining = remaining.slice(result.length)
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!matched) throw new TokenizerError("parse error at: '" + remaining + "'")
|
||||
}
|
||||
if (nesting > 0) throw new TokenizerError('too many opening parentheses')
|
||||
return tokens
|
||||
}
|
||||
|
||||
function uniq (arr) {
|
||||
const seen = new Set()
|
||||
const out = []
|
||||
for (const x of arr) { if (!seen.has(x)) { seen.add(x); out.push(x) } }
|
||||
return out
|
||||
}
|
||||
|
||||
function toNumber (val) {
|
||||
if (typeof val === 'number') return val
|
||||
if (typeof val === 'boolean') return val ? 1 : 0
|
||||
if (val == null) throw new ArgumentError("'" + val + "' is not coercible to numeric")
|
||||
if (typeof val === 'string') {
|
||||
if (val.length === 0) throw new ArgumentError("'' is not coercible to numeric")
|
||||
const n = Number(val)
|
||||
if (isNaN(n)) throw new ArgumentError("'" + val + "' is not coercible to numeric")
|
||||
return n
|
||||
}
|
||||
throw new ArgumentError("'" + val + "' is not coercible to numeric")
|
||||
}
|
||||
|
||||
class Node {
|
||||
static precedence = 0
|
||||
static arity = null
|
||||
static rightAssociative = false
|
||||
static resolveClass () { return this }
|
||||
static minParamCount () { return this.arity }
|
||||
static maxParamCount () { return this.arity }
|
||||
value () { return null }
|
||||
dependencies () { return [] }
|
||||
get type () { return null }
|
||||
}
|
||||
|
||||
class NilNode extends Node { value () { return null } }
|
||||
|
||||
class Numeric extends Node {
|
||||
constructor (token) { super(); this.v = token.value }
|
||||
value () { return this.v }
|
||||
get type () { return 'numeric' }
|
||||
}
|
||||
|
||||
class Logical extends Node {
|
||||
constructor (token) { super(); this.v = token.value }
|
||||
value () { return this.v }
|
||||
get type () { return 'logical' }
|
||||
}
|
||||
|
||||
class StringNode extends Node {
|
||||
constructor (token) { super(); this.v = token.value }
|
||||
value () { return this.v }
|
||||
get type () { return 'string' }
|
||||
}
|
||||
|
||||
class Identifier extends Node {
|
||||
constructor (token) { super(); this.ident = token.value }
|
||||
value (ctx) {
|
||||
if (ctx && (this.ident in ctx)) {
|
||||
const v = ctx[this.ident]
|
||||
if (v instanceof Node) return v.value(ctx)
|
||||
if (typeof v === 'function') return v()
|
||||
return v
|
||||
}
|
||||
throw new UnboundVariableError([this.ident])
|
||||
}
|
||||
|
||||
dependencies (ctx) {
|
||||
if (ctx && (this.ident in ctx)) {
|
||||
const v = ctx[this.ident]
|
||||
return v instanceof Node ? v.dependencies(ctx) : []
|
||||
}
|
||||
return [this.ident]
|
||||
}
|
||||
}
|
||||
|
||||
class Grouping extends Node {
|
||||
constructor (node) { super(); this.node = node }
|
||||
value (ctx) { return this.node.value(ctx) }
|
||||
get type () { return this.node.type }
|
||||
dependencies (ctx) { return this.node.dependencies(ctx) }
|
||||
}
|
||||
|
||||
class Operation extends Node {
|
||||
constructor (left, right) { super(); this.left = left; this.right = right }
|
||||
dependencies (ctx) {
|
||||
const l = this.left ? this.left.dependencies(ctx) : []
|
||||
const r = this.right ? this.right.dependencies(ctx) : []
|
||||
return uniq(l.concat(r))
|
||||
}
|
||||
}
|
||||
|
||||
class Arithmetic extends Operation { get type () { return 'numeric' } }
|
||||
|
||||
class Addition extends Arithmetic {
|
||||
static precedence = 10
|
||||
value (ctx) {
|
||||
const l = this.left.value(ctx)
|
||||
const r = this.right.value(ctx)
|
||||
if (typeof l === 'string' || typeof r === 'string') return String(l) + String(r)
|
||||
return toNumber(l) + toNumber(r)
|
||||
}
|
||||
}
|
||||
|
||||
class Subtraction extends Arithmetic {
|
||||
static precedence = 10
|
||||
value (ctx) { return toNumber(this.left.value(ctx)) - toNumber(this.right.value(ctx)) }
|
||||
}
|
||||
|
||||
class Multiplication extends Arithmetic {
|
||||
static precedence = 20
|
||||
value (ctx) { return toNumber(this.left.value(ctx)) * toNumber(this.right.value(ctx)) }
|
||||
}
|
||||
|
||||
class Division extends Arithmetic {
|
||||
static precedence = 20
|
||||
value (ctx) {
|
||||
const r = toNumber(this.right.value(ctx))
|
||||
if (r === 0) throw new ZeroDivisionError()
|
||||
return toNumber(this.left.value(ctx)) / r
|
||||
}
|
||||
}
|
||||
|
||||
class Modulo extends Arithmetic {
|
||||
static arity = 2
|
||||
static precedence = 20
|
||||
static resolveClass (nextTok) {
|
||||
if (!nextTok) return Percentage
|
||||
if (nextTok.category === 'operator') return Percentage
|
||||
if (nextTok.category === 'comparator') return Percentage
|
||||
if (nextTok.category === 'combinator') return Percentage
|
||||
if (nextTok.category === 'grouping' && (nextTok.value === 'close' || nextTok.value === 'comma')) return Percentage
|
||||
if (nextTok.category === 'array' && nextTok.value === 'array_end') return Percentage
|
||||
return Modulo
|
||||
}
|
||||
|
||||
value (ctx) {
|
||||
const r = toNumber(this.right.value(ctx))
|
||||
if (r === 0) throw new ZeroDivisionError()
|
||||
return toNumber(this.left.value(ctx)) % r
|
||||
}
|
||||
}
|
||||
|
||||
class Percentage extends Arithmetic {
|
||||
static arity = 1
|
||||
static precedence = 30
|
||||
constructor (child) { super(child, null) }
|
||||
value (ctx) { return toNumber(this.left.value(ctx)) * 0.01 }
|
||||
dependencies (ctx) { return this.left.dependencies(ctx) }
|
||||
}
|
||||
|
||||
class Exponentiation extends Arithmetic {
|
||||
static precedence = 30
|
||||
static rightAssociative = true
|
||||
value (ctx) { return Math.pow(toNumber(this.left.value(ctx)), toNumber(this.right.value(ctx))) }
|
||||
}
|
||||
|
||||
class Negation extends Arithmetic {
|
||||
static arity = 1
|
||||
static precedence = 40
|
||||
static rightAssociative = true
|
||||
constructor (node) { super(node, null) }
|
||||
value (ctx) { return -toNumber(this.left.value(ctx)) }
|
||||
dependencies (ctx) { return this.left.dependencies(ctx) }
|
||||
}
|
||||
|
||||
class BitwiseOr extends Operation {
|
||||
static precedence = 10
|
||||
value (ctx) { return toNumber(this.left.value(ctx)) | toNumber(this.right.value(ctx)) }
|
||||
get type () { return 'numeric' }
|
||||
}
|
||||
class BitwiseAnd extends Operation {
|
||||
static precedence = 10
|
||||
value (ctx) { return toNumber(this.left.value(ctx)) & toNumber(this.right.value(ctx)) }
|
||||
get type () { return 'numeric' }
|
||||
}
|
||||
class BitwiseShiftLeft extends Operation {
|
||||
static precedence = 10
|
||||
value (ctx) { return toNumber(this.left.value(ctx)) << toNumber(this.right.value(ctx)) }
|
||||
get type () { return 'numeric' }
|
||||
}
|
||||
class BitwiseShiftRight extends Operation {
|
||||
static precedence = 10
|
||||
value (ctx) { return toNumber(this.left.value(ctx)) >> toNumber(this.right.value(ctx)) }
|
||||
get type () { return 'numeric' }
|
||||
}
|
||||
|
||||
function cmpCast (v) {
|
||||
if (typeof v === 'string' && /^-?\d*\.?\d+$/.test(v)) return Number(v)
|
||||
return v
|
||||
}
|
||||
class Comparator extends Operation {
|
||||
static precedence = 5
|
||||
get type () { return 'logical' }
|
||||
}
|
||||
class LessThan extends Comparator { value (ctx) { return cmpCast(this.left.value(ctx)) < cmpCast(this.right.value(ctx)) } }
|
||||
class LessThanOrEqual extends Comparator { value (ctx) { return cmpCast(this.left.value(ctx)) <= cmpCast(this.right.value(ctx)) } }
|
||||
class GreaterThan extends Comparator { value (ctx) { return cmpCast(this.left.value(ctx)) > cmpCast(this.right.value(ctx)) } }
|
||||
class GreaterThanOrEqual extends Comparator { value (ctx) { return cmpCast(this.left.value(ctx)) >= cmpCast(this.right.value(ctx)) } }
|
||||
class NotEqual extends Comparator {
|
||||
value (ctx) {
|
||||
const l = cmpCast(this.left.value(ctx))
|
||||
const r = cmpCast(this.right.value(ctx))
|
||||
return l !== r
|
||||
}
|
||||
}
|
||||
class Equal extends Comparator {
|
||||
value (ctx) {
|
||||
const l = cmpCast(this.left.value(ctx))
|
||||
const r = cmpCast(this.right.value(ctx))
|
||||
return l === r
|
||||
}
|
||||
}
|
||||
|
||||
class Combinator extends Operation {
|
||||
static precedence = 0
|
||||
get type () { return 'logical' }
|
||||
}
|
||||
class And extends Combinator { value (ctx) { return Boolean(this.left.value(ctx)) && Boolean(this.right.value(ctx)) } }
|
||||
class Or extends Combinator { value (ctx) { return Boolean(this.left.value(ctx)) || Boolean(this.right.value(ctx)) } }
|
||||
|
||||
class ArrayNode extends Node {
|
||||
static arity = null
|
||||
static minParamCount () { return 0 }
|
||||
static maxParamCount () { return Infinity }
|
||||
constructor (...items) {
|
||||
super()
|
||||
this.items = items
|
||||
}
|
||||
|
||||
value (ctx) { return this.items.map((i) => i.value(ctx)) }
|
||||
dependencies (ctx) {
|
||||
return uniq([].concat.apply([], this.items.map((i) => i.dependencies(ctx))))
|
||||
}
|
||||
|
||||
get type () { return 'array' }
|
||||
}
|
||||
|
||||
class FunctionNode extends Node {
|
||||
static arity = null
|
||||
static minParamCount () { return 0 }
|
||||
static maxParamCount () { return Infinity }
|
||||
constructor (...args) {
|
||||
super()
|
||||
this.args = args
|
||||
}
|
||||
|
||||
dependencies (ctx) {
|
||||
return uniq([].concat.apply([], this.args.map((a) => a.dependencies(ctx))))
|
||||
}
|
||||
}
|
||||
|
||||
const FUNCTION_REGISTRY = Object.create(null)
|
||||
function registerFunctionClass (name, cls) { FUNCTION_REGISTRY[name.toLowerCase()] = cls }
|
||||
function getFunctionClass (name) { return FUNCTION_REGISTRY[name.toLowerCase()] }
|
||||
|
||||
function defineFunction (name, spec) {
|
||||
const min = spec.min == null ? 0 : spec.min
|
||||
const max = spec.max == null ? Infinity : spec.max
|
||||
const call = spec.call
|
||||
const type = spec.type || 'numeric'
|
||||
class F extends FunctionNode {
|
||||
static arity = (min === max) ? min : null
|
||||
static minParamCount () { return min }
|
||||
static maxParamCount () { return max }
|
||||
value (ctx) {
|
||||
const vals = this.args.map((a) => a.value(ctx))
|
||||
return call(vals, ctx)
|
||||
}
|
||||
|
||||
get type () { return type }
|
||||
}
|
||||
registerFunctionClass(name, F)
|
||||
return F
|
||||
}
|
||||
|
||||
function flattenNumeric (vals) {
|
||||
const out = [];
|
||||
(function walk (v) {
|
||||
if (Array.isArray(v)) v.forEach(walk)
|
||||
else out.push(toNumber(v))
|
||||
})(vals)
|
||||
return out
|
||||
}
|
||||
|
||||
defineFunction('abs', { min: 1, max: 1, call: (v) => Math.abs(toNumber(v[0])) })
|
||||
defineFunction('min', { min: 1, call: (v) => Math.min.apply(null, flattenNumeric(v)) })
|
||||
defineFunction('max', { min: 1, call: (v) => Math.max.apply(null, flattenNumeric(v)) })
|
||||
defineFunction('sum', { min: 1, call: (v) => flattenNumeric(v).reduce((a, b) => a + b, 0) })
|
||||
defineFunction('avg', {
|
||||
min: 1,
|
||||
call: (v) => {
|
||||
const nums = flattenNumeric(v)
|
||||
if (nums.length === 0) return 0
|
||||
return nums.reduce((a, b) => a + b, 0) / nums.length
|
||||
}
|
||||
})
|
||||
defineFunction('count', {
|
||||
min: 1,
|
||||
max: 1,
|
||||
call: (v) => Array.isArray(v[0]) ? v[0].length : (v[0] == null ? 0 : 1)
|
||||
})
|
||||
defineFunction('round', {
|
||||
min: 1,
|
||||
max: 2,
|
||||
call: (v) => {
|
||||
const n = toNumber(v[0])
|
||||
const p = v.length > 1 ? toNumber(v[1]) : 0
|
||||
const f = Math.pow(10, p)
|
||||
return Math.round(n * f) / f
|
||||
}
|
||||
})
|
||||
defineFunction('roundup', {
|
||||
min: 1,
|
||||
max: 2,
|
||||
call: (v) => {
|
||||
const n = toNumber(v[0])
|
||||
const p = v.length > 1 ? toNumber(v[1]) : 0
|
||||
const f = Math.pow(10, p)
|
||||
return (n < 0 ? -Math.floor(-n * f) : Math.ceil(n * f)) / f
|
||||
}
|
||||
})
|
||||
defineFunction('rounddown', {
|
||||
min: 1,
|
||||
max: 2,
|
||||
call: (v) => {
|
||||
const n = toNumber(v[0])
|
||||
const p = v.length > 1 ? toNumber(v[1]) : 0
|
||||
const f = Math.pow(10, p)
|
||||
return (n < 0 ? -Math.ceil(-n * f) : Math.floor(n * f)) / f
|
||||
}
|
||||
})
|
||||
|
||||
const MATH_FNS = {
|
||||
sqrt: Math.sqrt,
|
||||
cbrt: Math.cbrt,
|
||||
sin: Math.sin,
|
||||
cos: Math.cos,
|
||||
tan: Math.tan,
|
||||
asin: Math.asin,
|
||||
acos: Math.acos,
|
||||
atan: Math.atan,
|
||||
sinh: Math.sinh,
|
||||
cosh: Math.cosh,
|
||||
tanh: Math.tanh,
|
||||
exp: Math.exp,
|
||||
log: Math.log,
|
||||
log2: Math.log2,
|
||||
log10: Math.log10,
|
||||
floor: Math.floor,
|
||||
ceil: Math.ceil
|
||||
}
|
||||
Object.keys(MATH_FNS).forEach((name) => {
|
||||
const fn = MATH_FNS[name]
|
||||
defineFunction(name, { min: 1, max: 1, call: (v) => fn(toNumber(v[0])) })
|
||||
})
|
||||
defineFunction('atan2', { min: 2, max: 2, call: (v) => Math.atan2(toNumber(v[0]), toNumber(v[1])) })
|
||||
defineFunction('hypot', { min: 1, call: (v) => Math.hypot.apply(null, flattenNumeric(v)) })
|
||||
defineFunction('pow', { min: 2, max: 2, call: (v) => Math.pow(toNumber(v[0]), toNumber(v[1])) })
|
||||
defineFunction('pi', { min: 0, max: 0, call: () => Math.PI })
|
||||
defineFunction('e', { min: 0, max: 0, call: () => Math.E })
|
||||
|
||||
defineFunction('not', { min: 1, max: 1, type: 'logical', call: (v) => !v[0] })
|
||||
defineFunction('and', { min: 1, type: 'logical', call: (v) => v.every((x) => Boolean(x)) })
|
||||
defineFunction('or', { min: 1, type: 'logical', call: (v) => v.some((x) => Boolean(x)) })
|
||||
defineFunction('xor', {
|
||||
min: 2,
|
||||
type: 'logical',
|
||||
call: (v) => v.reduce((acc, x) => acc !== Boolean(x), false)
|
||||
})
|
||||
|
||||
class IfFunction extends FunctionNode {
|
||||
static arity = 3
|
||||
static minParamCount () { return 3 }
|
||||
static maxParamCount () { return 3 }
|
||||
value (ctx) {
|
||||
return this.args[0].value(ctx) ? this.args[1].value(ctx) : this.args[2].value(ctx)
|
||||
}
|
||||
|
||||
get type () { return this.args[1].type }
|
||||
dependencies (ctx) {
|
||||
try {
|
||||
return this.args[0].value(ctx) ? this.args[1].dependencies(ctx) : this.args[2].dependencies(ctx)
|
||||
} catch (e) {
|
||||
return uniq([].concat.apply([], this.args.map((a) => a.dependencies(ctx))))
|
||||
}
|
||||
}
|
||||
}
|
||||
registerFunctionClass('if', IfFunction)
|
||||
|
||||
defineFunction('length', { min: 1, max: 1, call: (v) => v[0] == null ? 0 : String(v[0]).length })
|
||||
defineFunction('upcase', { min: 1, max: 1, type: 'string', call: (v) => String(v[0]).toUpperCase() })
|
||||
defineFunction('downcase', { min: 1, max: 1, type: 'string', call: (v) => String(v[0]).toLowerCase() })
|
||||
defineFunction('concat', { min: 1, type: 'string', call: (v) => v.map((x) => String(x)).join('') })
|
||||
defineFunction('left', {
|
||||
min: 2,
|
||||
max: 2,
|
||||
type: 'string',
|
||||
call: (v) => String(v[0]).slice(0, toNumber(v[1]))
|
||||
})
|
||||
defineFunction('right', {
|
||||
min: 2,
|
||||
max: 2,
|
||||
type: 'string',
|
||||
call: (v) => {
|
||||
const n = toNumber(v[1])
|
||||
return n === 0 ? '' : String(v[0]).slice(-n)
|
||||
}
|
||||
})
|
||||
defineFunction('mid', {
|
||||
min: 3,
|
||||
max: 3,
|
||||
type: 'string',
|
||||
call: (v) => {
|
||||
const s = String(v[0])
|
||||
const start = toNumber(v[1]) - 1
|
||||
const len = toNumber(v[2])
|
||||
return s.slice(start, start + len)
|
||||
}
|
||||
})
|
||||
defineFunction('trim', { min: 1, max: 1, type: 'string', call: (v) => String(v[0]).trim() })
|
||||
defineFunction('contains', {
|
||||
min: 2,
|
||||
max: 2,
|
||||
type: 'logical',
|
||||
call: (v) => {
|
||||
if (Array.isArray(v[0])) return v[0].indexOf(v[1]) !== -1
|
||||
return String(v[0]).indexOf(String(v[1])) !== -1
|
||||
}
|
||||
})
|
||||
|
||||
const OP_CLASSES = {
|
||||
add: Addition,
|
||||
subtract: Subtraction,
|
||||
multiply: Multiplication,
|
||||
divide: Division,
|
||||
pow: Exponentiation,
|
||||
negate: Negation,
|
||||
mod: Modulo,
|
||||
bitor: BitwiseOr,
|
||||
bitand: BitwiseAnd,
|
||||
bitshiftleft: BitwiseShiftLeft,
|
||||
bitshiftright: BitwiseShiftRight,
|
||||
lt: LessThan,
|
||||
gt: GreaterThan,
|
||||
le: LessThanOrEqual,
|
||||
ge: GreaterThanOrEqual,
|
||||
ne: NotEqual,
|
||||
eq: Equal,
|
||||
and: And,
|
||||
or: Or
|
||||
}
|
||||
|
||||
function isOperationClass (cls) {
|
||||
return cls && (cls.prototype instanceof Operation)
|
||||
}
|
||||
|
||||
function parse (tokens) {
|
||||
if (!tokens || tokens.length === 0) return new NilNode()
|
||||
|
||||
const output = []
|
||||
const operations = []
|
||||
const arities = []
|
||||
const skip = new Set()
|
||||
|
||||
function consume (count) {
|
||||
if (count == null) count = 2
|
||||
const op = operations.pop()
|
||||
if (!op) throw new ParseError('invalid statement')
|
||||
|
||||
const outputSize = output.length
|
||||
const opMin = (typeof op.minParamCount === 'function') ? op.minParamCount() : null
|
||||
const opMax = (typeof op.maxParamCount === 'function') ? op.maxParamCount() : null
|
||||
|
||||
const actualArgs = (op.arity != null) ? op.arity : count
|
||||
const minSize = (op.arity != null) ? op.arity : (opMin != null ? opMin : count)
|
||||
const maxSize = (op.arity != null) ? op.arity : (opMax != null ? opMax : count)
|
||||
|
||||
if (outputSize < minSize || actualArgs < minSize) {
|
||||
throw new ParseError((op.name || 'operator') + ' has too few operands (given ' + outputSize + ', expected ' + minSize + ')')
|
||||
}
|
||||
if ((outputSize > maxSize && operations.length === 0) || actualArgs > maxSize) {
|
||||
throw new ParseError((op.name || 'operator') + ' has too many operands (given ' + outputSize + ', expected ' + maxSize + ')')
|
||||
}
|
||||
|
||||
if (op === ArrayNode && output.length === 0) {
|
||||
output.push(new ArrayNode())
|
||||
return
|
||||
}
|
||||
|
||||
if (outputSize < actualArgs) throw new ParseError('invalid statement')
|
||||
const args = []
|
||||
for (let i = 0; i < actualArgs; i++) args.unshift(output.pop())
|
||||
|
||||
// eslint-disable-next-line new-cap
|
||||
output.push(new op(...args))
|
||||
}
|
||||
|
||||
function handleOperator (token, lookahead) {
|
||||
let cls = OP_CLASSES[token.value]
|
||||
if (!cls) throw new ParseError('Unknown operator ' + token.value)
|
||||
if (typeof cls.resolveClass === 'function') cls = cls.resolveClass(lookahead)
|
||||
const prec = cls.precedence
|
||||
if (cls.rightAssociative) {
|
||||
while (operations.length > 0) {
|
||||
const top = operations[operations.length - 1]
|
||||
if (isOperationClass(top) && prec < top.precedence) consume()
|
||||
else break
|
||||
}
|
||||
} else {
|
||||
while (operations.length > 0) {
|
||||
const top = operations[operations.length - 1]
|
||||
if (isOperationClass(top) && prec <= top.precedence) consume()
|
||||
else break
|
||||
}
|
||||
}
|
||||
operations.push(cls)
|
||||
}
|
||||
|
||||
function handleFunction (token) {
|
||||
const fn = getFunctionClass(token.value)
|
||||
if (!fn) throw new ParseError('Undefined function ' + token.value)
|
||||
arities.push(0)
|
||||
operations.push(fn)
|
||||
}
|
||||
|
||||
function handleArray (token) {
|
||||
if (token.value === 'array_start') {
|
||||
operations.push(ArrayNode)
|
||||
arities.push(0)
|
||||
} else {
|
||||
while (operations.length > 0 && operations[operations.length - 1] !== ArrayNode) consume()
|
||||
if (operations[operations.length - 1] !== ArrayNode) throw new ParseError('Unbalanced bracket')
|
||||
consume(arities.pop() + 1)
|
||||
}
|
||||
}
|
||||
|
||||
function handleGrouping (token, lookahead, index) {
|
||||
if (token.value === 'open') {
|
||||
if (lookahead && lookahead.value === 'close') {
|
||||
skip.add(index + 1)
|
||||
arities.pop()
|
||||
consume(0)
|
||||
} else {
|
||||
operations.push(Grouping)
|
||||
}
|
||||
} else if (token.value === 'close') {
|
||||
while (operations.length > 0 && operations[operations.length - 1] !== Grouping) consume()
|
||||
const lparen = operations.pop()
|
||||
if (lparen !== Grouping) throw new ParseError('Unbalanced parenthesis')
|
||||
const top = operations[operations.length - 1]
|
||||
if (top && (top.prototype instanceof FunctionNode)) consume(arities.pop() + 1)
|
||||
} else if (token.value === 'comma') {
|
||||
if (arities.length === 0) throw new ParseError('invalid statement')
|
||||
arities[arities.length - 1] += 1
|
||||
while (operations.length > 0 &&
|
||||
operations[operations.length - 1] !== Grouping &&
|
||||
operations[operations.length - 1] !== ArrayNode) consume()
|
||||
} else {
|
||||
throw new ParseError('Unknown grouping token ' + token.value)
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
if (skip.has(i)) continue
|
||||
const token = tokens[i]
|
||||
const lookahead = tokens[i + 1]
|
||||
|
||||
switch (token.category) {
|
||||
case 'numeric': output.push(new Numeric(token)); break
|
||||
case 'logical': output.push(new Logical(token)); break
|
||||
case 'string': output.push(new StringNode(token)); break
|
||||
case 'identifier': output.push(new Identifier(token)); break
|
||||
case 'null': output.push(new NilNode()); break
|
||||
case 'operator':
|
||||
case 'comparator':
|
||||
case 'combinator':
|
||||
handleOperator(token, lookahead); break
|
||||
case 'function': handleFunction(token); break
|
||||
case 'array': handleArray(token); break
|
||||
case 'grouping': handleGrouping(token, lookahead, i); break
|
||||
default:
|
||||
throw new ParseError('Not implemented for tokens of category ' + token.category)
|
||||
}
|
||||
}
|
||||
|
||||
while (operations.length > 0) consume()
|
||||
|
||||
if (output.length !== 1) throw new ParseError('Invalid statement')
|
||||
return output[0]
|
||||
}
|
||||
|
||||
class Calculator {
|
||||
constructor (options) {
|
||||
options = options || {}
|
||||
this.caseSensitive = options.caseSensitive === true
|
||||
this.memory = Object.create(null)
|
||||
this._astCache = Object.create(null)
|
||||
}
|
||||
|
||||
key (k) { return this.caseSensitive ? k : String(k).toLowerCase() }
|
||||
|
||||
normalize (data) {
|
||||
const out = Object.create(null)
|
||||
if (!data) return out
|
||||
for (const k of Object.keys(data)) out[this.key(k)] = data[k]
|
||||
return out
|
||||
}
|
||||
|
||||
store (data) { Object.assign(this.memory, this.normalize(data)); return this }
|
||||
bind (data) { return this.store(data) }
|
||||
clear () { this.memory = Object.create(null); return this }
|
||||
|
||||
ast (expression) {
|
||||
const cached = this._astCache[expression]
|
||||
if (cached) return cached
|
||||
const node = parse(tokenize(expression, { caseSensitive: this.caseSensitive }))
|
||||
this._astCache[expression] = node
|
||||
return node
|
||||
}
|
||||
|
||||
tokenize (expression) {
|
||||
return tokenize(expression, { caseSensitive: this.caseSensitive })
|
||||
}
|
||||
|
||||
dependencies (expression) {
|
||||
return this.ast(expression).dependencies(this.memory)
|
||||
}
|
||||
|
||||
evaluate (expression, data, onError) {
|
||||
try {
|
||||
return this.evaluateStrict(expression, data)
|
||||
} catch (e) {
|
||||
if (typeof data === 'function') return data(expression, e)
|
||||
if (typeof onError === 'function') return onError(expression, e)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
evaluateStrict (expression, data) {
|
||||
const ctx = Object.assign(Object.create(null), this.memory, this.normalize(data))
|
||||
const node = this.ast(expression)
|
||||
return node.value(ctx)
|
||||
}
|
||||
|
||||
addFunction (name, spec) {
|
||||
defineFunction(name, {
|
||||
min: spec.minArgs == null ? (spec.min == null ? 0 : spec.min) : spec.minArgs,
|
||||
max: spec.maxArgs == null ? (spec.max == null ? Infinity : spec.max) : spec.maxArgs,
|
||||
type: spec.type || 'numeric',
|
||||
call: spec.call
|
||||
})
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
Calculator,
|
||||
tokenize,
|
||||
parse,
|
||||
CalculatorError,
|
||||
ParseError,
|
||||
TokenizerError,
|
||||
UnboundVariableError,
|
||||
ZeroDivisionError,
|
||||
ArgumentError
|
||||
}
|
||||
|
||||
export function addFunction (name, spec) { defineFunction(name, spec) }
|
||||
@@ -3,10 +3,13 @@
|
||||
id="form_completed"
|
||||
class="mx-auto max-w-md flex flex-col completed-form"
|
||||
dir="auto"
|
||||
role="status"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div class="font-medium text-2xl flex items-center space-x-1.5 mx-auto">
|
||||
<IconCircleCheck
|
||||
class="inline text-green-600"
|
||||
aria-hidden="true"
|
||||
:width="30"
|
||||
:height="30"
|
||||
/>
|
||||
@@ -42,8 +45,12 @@
|
||||
<IconInnerShadowTop
|
||||
v-if="isSendingCopy"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<IconMail
|
||||
v-else
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<IconMail v-else />
|
||||
<span>
|
||||
{{ t('send_copy_via_email') }}
|
||||
</span>
|
||||
@@ -57,8 +64,12 @@
|
||||
<IconInnerShadowTop
|
||||
v-if="isDownloading"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<IconDownload
|
||||
v-else
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<IconDownload v-else />
|
||||
<span>
|
||||
{{ t('download') }}
|
||||
</span>
|
||||
@@ -161,6 +172,11 @@ export default {
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
fetchOptions: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({})
|
||||
},
|
||||
completedButton: {
|
||||
type: Object,
|
||||
required: false,
|
||||
@@ -194,7 +210,7 @@ export default {
|
||||
})
|
||||
}
|
||||
|
||||
document.querySelectorAll('#decline_button').forEach((button) => {
|
||||
document.querySelectorAll('#decline_button, #decline_button_mobile, #delegate_button, #delegate_button_mobile').forEach((button) => {
|
||||
button.setAttribute('disabled', 'true')
|
||||
})
|
||||
},
|
||||
@@ -214,7 +230,10 @@ export default {
|
||||
download () {
|
||||
this.isDownloading = true
|
||||
|
||||
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`).then(async (response) => {
|
||||
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`, {
|
||||
method: 'GET',
|
||||
...this.fetchOptions
|
||||
}).then(async (response) => {
|
||||
if (response.ok) {
|
||||
const urls = await response.json()
|
||||
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
|
||||
|
||||
@@ -27,22 +27,27 @@
|
||||
</label>
|
||||
<button
|
||||
v-if="withToday"
|
||||
type="button"
|
||||
class="btn btn-outline btn-sm !normal-case font-normal set-current-date-button"
|
||||
@click.prevent="[setCurrentDate(), $emit('focus')]"
|
||||
>
|
||||
<IconCalendarCheck :width="16" />
|
||||
<IconCalendarCheck
|
||||
:width="16"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{{ t('set_today') }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="field.description"
|
||||
:id="field.uuid + '-desc'"
|
||||
class="mb-3 px-1 field-description-text"
|
||||
dir="auto"
|
||||
>
|
||||
<MarkdownContent :string="field.description" />
|
||||
</div>
|
||||
<AppearsOn :field="field" />
|
||||
<div class="text-center">
|
||||
<div class="text-center flex">
|
||||
<input
|
||||
:id="field.uuid"
|
||||
ref="input"
|
||||
@@ -51,12 +56,19 @@
|
||||
:max="validationMax"
|
||||
class="base-input !text-2xl text-center w-full"
|
||||
:required="field.required"
|
||||
type="date"
|
||||
:name="`values[${field.uuid}]`"
|
||||
:aria-describedby="field.description ? field.uuid + '-desc' : undefined"
|
||||
:type="inputType"
|
||||
:name="formatType === 'datetime' ? undefined : `values[${field.uuid}]`"
|
||||
@keydown.enter="onEnter"
|
||||
@focus="$emit('focus')"
|
||||
@paste="onPaste"
|
||||
>
|
||||
<input
|
||||
v-if="formatType === 'datetime'"
|
||||
type="hidden"
|
||||
:name="`values[${field.uuid}]`"
|
||||
:value="modelValue"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -92,14 +104,19 @@ export default {
|
||||
},
|
||||
emits: ['update:model-value', 'focus', 'submit'],
|
||||
computed: {
|
||||
formatType () {
|
||||
const format = this.field.preferences?.format || ''
|
||||
|
||||
if (/[HhAasz]/.test(format)) return 'datetime'
|
||||
if (format && !/[Dd]/.test(format)) return 'month'
|
||||
|
||||
return 'date'
|
||||
},
|
||||
inputType () {
|
||||
return { datetime: 'datetime-local', month: 'month', date: 'date' }[this.formatType]
|
||||
},
|
||||
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}`
|
||||
return this.formatDateValue(new Date())
|
||||
},
|
||||
validationMin () {
|
||||
if (this.field.validation?.min) {
|
||||
@@ -116,6 +133,8 @@ export default {
|
||||
}
|
||||
},
|
||||
withToday () {
|
||||
if (this.formatType === 'datetime') return false
|
||||
|
||||
const todayDate = new Date().setHours(0, 0, 0, 0)
|
||||
|
||||
if (this.validationMin) {
|
||||
@@ -132,9 +151,25 @@ export default {
|
||||
},
|
||||
value: {
|
||||
set (value) {
|
||||
if (this.formatType === 'datetime' && value) {
|
||||
const d = new Date(value)
|
||||
|
||||
if (!isNaN(d)) {
|
||||
this.$emit('update:model-value', d.toISOString())
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.$emit('update:model-value', value)
|
||||
},
|
||||
get () {
|
||||
if (this.formatType === 'datetime') {
|
||||
const d = new Date(this.modelValue)
|
||||
|
||||
return isNaN(d) ? '' : this.formatDateValue(d)
|
||||
}
|
||||
|
||||
return this.modelValue
|
||||
}
|
||||
}
|
||||
@@ -158,20 +193,32 @@ export default {
|
||||
|
||||
const parsedDate = new Date(pasteData)
|
||||
|
||||
if (!isNaN(parsedDate)) {
|
||||
const inputEl = this.$refs.input
|
||||
if (isNaN(parsedDate)) return
|
||||
|
||||
inputEl.valueAsDate = new Date(parsedDate.getTime() - parsedDate.getTimezoneOffset() * 60000)
|
||||
|
||||
inputEl.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
}
|
||||
this.setInputValue(parsedDate)
|
||||
},
|
||||
setCurrentDate () {
|
||||
this.setInputValue(new Date())
|
||||
},
|
||||
setInputValue (date) {
|
||||
const inputEl = this.$refs.input
|
||||
|
||||
inputEl.valueAsDate = new Date(new Date().getTime() - new Date().getTimezoneOffset() * 60000)
|
||||
if (this.formatType === 'date') {
|
||||
inputEl.valueAsDate = new Date(date.getTime() - date.getTimezoneOffset() * 60000)
|
||||
} else {
|
||||
inputEl.value = this.formatDateValue(date)
|
||||
}
|
||||
|
||||
inputEl.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
},
|
||||
formatDateValue (date) {
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
const ymd = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
||||
|
||||
if (this.formatType === 'month') return ymd.slice(0, 7)
|
||||
if (this.formatType === 'datetime') return `${ymd}T${pad(date.getHours())}:${pad(date.getMinutes())}`
|
||||
|
||||
return ymd
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,8 +39,9 @@
|
||||
ref="input"
|
||||
:multiple="multiple"
|
||||
:accept="accept"
|
||||
:aria-label="message"
|
||||
type="file"
|
||||
class="hidden"
|
||||
class="sr-only"
|
||||
@change="onSelectFiles"
|
||||
>
|
||||
</label>
|
||||
@@ -50,6 +51,36 @@
|
||||
<script>
|
||||
import { IconCloudUpload, IconInnerShadowTop } from '@tabler/icons-vue'
|
||||
|
||||
function convertImage (sourceFile, targetType, quality) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onload = function (event) {
|
||||
const img = new Image()
|
||||
|
||||
img.onload = function () {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
canvas.width = img.width
|
||||
canvas.height = img.height
|
||||
ctx.drawImage(img, 0, 0)
|
||||
canvas.toBlob(function (blob) {
|
||||
const ext = targetType === 'image/jpeg' ? '.jpg' : '.png'
|
||||
const newFile = new File([blob], sourceFile.name.replace(/\.\w+$/, ext), { type: targetType })
|
||||
resolve(newFile)
|
||||
}, targetType, quality)
|
||||
}
|
||||
|
||||
img.onerror = () => reject(new Error(`browser cannot decode ${sourceFile.type || sourceFile.name}`))
|
||||
|
||||
img.src = event.target.result
|
||||
}
|
||||
reader.onerror = reject
|
||||
reader.readAsDataURL(sourceFile)
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'FileDropzone',
|
||||
components: {
|
||||
@@ -152,8 +183,14 @@ export default {
|
||||
}
|
||||
})
|
||||
} else {
|
||||
if (file.type === 'image/bmp' || file.type === 'image/vnd.microsoft.icon') {
|
||||
file = await this.convertBmpToPng(file)
|
||||
try {
|
||||
if (['image/bmp', 'image/vnd.microsoft.icon', 'image/svg+xml', 'image/gif'].includes(file.type)) {
|
||||
file = await convertImage(file, 'image/png')
|
||||
} else if (['image/heic', 'image/heif', 'image/heic-sequence', 'image/heif-sequence', 'image/avif', 'image/avif-sequence', 'image/webp'].includes(file.type)) {
|
||||
file = await convertImage(file, 'image/jpeg', 0.9)
|
||||
}
|
||||
} catch (e) {
|
||||
alert(e.message)
|
||||
}
|
||||
|
||||
formData.append('file', file)
|
||||
@@ -180,32 +217,6 @@ export default {
|
||||
}).finally(() => {
|
||||
this.isLoading = false
|
||||
})
|
||||
},
|
||||
convertBmpToPng (bmpFile) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onload = function (event) {
|
||||
const img = new Image()
|
||||
|
||||
img.onload = function () {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
canvas.width = img.width
|
||||
canvas.height = img.height
|
||||
ctx.drawImage(img, 0, 0)
|
||||
canvas.toBlob(function (blob) {
|
||||
const newFile = new File([blob], bmpFile.name.replace(/\.\w+$/, '.png'), { type: 'image/png' })
|
||||
resolve(newFile)
|
||||
}, 'image/png')
|
||||
}
|
||||
|
||||
img.src = event.target.result
|
||||
}
|
||||
reader.onerror = reject
|
||||
reader.readAsDataURL(bmpFile)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,52 @@
|
||||
<template>
|
||||
<Teleport
|
||||
v-if="withAccessibilityAreas === null && !isAccessibilityMode"
|
||||
to="#sr_only_content"
|
||||
>
|
||||
<button
|
||||
class="sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 focus:z-[100] focus:px-4 focus:py-2 focus:bg-base-100 focus:text-base-content focus:rounded focus:shadow-lg"
|
||||
@click="isAccessibilityMode = true"
|
||||
>
|
||||
{{ t('enter_screen_reader_mode') }}
|
||||
</button>
|
||||
</Teleport>
|
||||
<Teleport
|
||||
v-for="item in (withAccessibilityAreas === null ? schema : [])"
|
||||
:key="item.attachment_uuid"
|
||||
:to="`#document-${item.attachment_uuid} .sr_only_content`"
|
||||
>
|
||||
<button
|
||||
v-if="!isAccessibilityMode"
|
||||
class="sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 focus:z-[100] focus:px-4 focus:py-2 focus:bg-base-100 focus:text-base-content focus:rounded focus:shadow-lg"
|
||||
@click="isAccessibilityMode = true"
|
||||
>
|
||||
{{ t('enter_screen_reader_mode') }}
|
||||
</button>
|
||||
</Teleport>
|
||||
<AccessibilityAreas
|
||||
v-if="withAccessibilityAreas || isAccessibilityMode"
|
||||
ref="areas"
|
||||
:submitter-slug="submitterSlug"
|
||||
:steps="stepFields"
|
||||
:readonly-conditional-fields="readonlyConditionalFields"
|
||||
:readonly-conditional-field-values="readonlyConditionalFieldValues"
|
||||
:formula-fields="formulaFields"
|
||||
:values="values"
|
||||
:readonly-values="readonlyFieldValues"
|
||||
:submitter="submitter"
|
||||
:scroll-el="scrollEl"
|
||||
:current-step="currentStepFields"
|
||||
:with-field-placeholder="withFieldPlaceholder"
|
||||
:with-signature-id="withSignatureId"
|
||||
:with-label="withFieldLabels && !isAnonymousChecboxes && showFieldNames"
|
||||
:scroll-padding="scrollPadding"
|
||||
:attachments-index="attachmentsIndex"
|
||||
:fetch-options="fetchOptions"
|
||||
:filled-fields-index="filledFieldsIndex"
|
||||
@focus-step="[saveStep(), currentField.type !== 'checkbox' ? isFormVisible = true : '', goToStep($event, false, true)]"
|
||||
/>
|
||||
<FieldAreas
|
||||
v-if="!withAccessibilityAreas && !isAccessibilityMode"
|
||||
ref="areas"
|
||||
:steps="stepFields"
|
||||
:values="values"
|
||||
@@ -14,6 +61,7 @@
|
||||
@focus-step="[saveStep(), currentField.type !== 'checkbox' ? isFormVisible = true : '', goToStep($event, false, true)]"
|
||||
/>
|
||||
<FieldAreas
|
||||
v-if="!withAccessibilityAreas && !isAccessibilityMode"
|
||||
:steps="readonlyConditionalFields.map((e) => [e])"
|
||||
:values="readonlyConditionalFieldValues"
|
||||
:submitter="submitter"
|
||||
@@ -21,7 +69,7 @@
|
||||
:submittable="false"
|
||||
/>
|
||||
<FormulaFieldAreas
|
||||
v-if="formulaFields.length"
|
||||
v-if="!withAccessibilityAreas && !isAccessibilityMode && formulaFields.length"
|
||||
:fields="formulaFields"
|
||||
:readonly-values="readonlyFieldValues"
|
||||
:values="values"
|
||||
@@ -47,7 +95,7 @@
|
||||
v-else
|
||||
id="complete_form_button"
|
||||
class="btn btn-sm btn-neutral text-white px-4 w-full flex justify-center"
|
||||
form="steps_form"
|
||||
form="complete_form"
|
||||
type="submit"
|
||||
name="completed"
|
||||
value="true"
|
||||
@@ -57,6 +105,7 @@
|
||||
<IconInnerShadowTop
|
||||
v-if="isSubmittingComplete"
|
||||
class="mr-1 animate-spin w-5 h-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>
|
||||
{{ t('complete') }}
|
||||
@@ -64,12 +113,45 @@
|
||||
</span>
|
||||
</button>
|
||||
</Teleport>
|
||||
<Teleport
|
||||
v-for="ref in (showCompleteButton ? [completeButtonContainer, completeButtonScrollContainer].filter(Boolean) : [])"
|
||||
:key="ref"
|
||||
:to="ref"
|
||||
>
|
||||
<button
|
||||
class="complete-button btn btn-sm btn-neutral text-white px-4"
|
||||
form="complete_form"
|
||||
type="submit"
|
||||
name="completed"
|
||||
value="true"
|
||||
:disabled="isSubmittingComplete"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
<IconInnerShadowTop
|
||||
v-if="isSubmittingComplete"
|
||||
class="mr-1 animate-spin w-5 h-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>
|
||||
{{ t('complete') }}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</Teleport>
|
||||
<form
|
||||
v-if="!isCompleted && !isInvite"
|
||||
id="complete_form"
|
||||
class="hidden"
|
||||
:action="submitPath"
|
||||
method="post"
|
||||
@submit.prevent="submitStep"
|
||||
/>
|
||||
<button
|
||||
v-if="!isFormVisible"
|
||||
id="expand_form_button"
|
||||
class="btn btn-neutral flex text-white absolute bottom-0 w-full mb-3 expand-form-button text-base"
|
||||
style="width: 96%; margin-left: 2%"
|
||||
@click.prevent="[isFormVisible = true, scrollIntoField(currentField)]"
|
||||
@click.prevent="[isFormVisible = true, $nextTick(() => scrollIntoField(currentField))]"
|
||||
>
|
||||
<template v-if="['initials', 'signature'].includes(currentField.type)">
|
||||
<IconWritingSign stroke-width="1.5" />
|
||||
@@ -88,6 +170,7 @@
|
||||
class="absolute right-0 mr-4"
|
||||
:width="20"
|
||||
:height="20"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
@@ -103,11 +186,13 @@
|
||||
class="absolute right-0 top-0 minimize-form-button"
|
||||
:class="currentField?.description?.length > 100 ? 'mr-1 mt-1 md:mr-2 md:mt-2': 'mr-2 mt-2 hidden md:block'"
|
||||
:title="t('minimize')"
|
||||
:aria-label="t('minimize')"
|
||||
@click.prevent="minimizeForm"
|
||||
>
|
||||
<IconArrowsDiagonalMinimize2
|
||||
:width="20"
|
||||
:height="20"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
@@ -193,6 +278,7 @@
|
||||
/>
|
||||
<div
|
||||
v-if="currentField.description"
|
||||
:id="currentField.uuid + '-desc'"
|
||||
dir="auto"
|
||||
class="mb-3 px-1 field-description-text"
|
||||
>
|
||||
@@ -203,6 +289,7 @@
|
||||
:id="currentField.uuid"
|
||||
dir="auto"
|
||||
:required="currentField.required"
|
||||
:aria-describedby="currentField.description ? currentField.uuid + '-desc' : undefined"
|
||||
class="select base-input !text-2xl w-full text-center font-normal"
|
||||
:class="{ 'text-gray-300': !values[currentField.uuid] }"
|
||||
:name="`values[${currentField.uuid}]`"
|
||||
@@ -250,6 +337,7 @@
|
||||
</label>
|
||||
<div
|
||||
v-if="currentField.description"
|
||||
:id="currentField.uuid + '-desc'"
|
||||
dir="auto"
|
||||
class="mb-3 px-1 field-description-text"
|
||||
>
|
||||
@@ -309,6 +397,7 @@
|
||||
>
|
||||
<div
|
||||
v-if="currentField.description"
|
||||
:id="currentField.uuid + '-desc'"
|
||||
dir="auto"
|
||||
class="mb-3 px-1 field-description-text"
|
||||
>
|
||||
@@ -395,11 +484,15 @@
|
||||
v-model="values[currentField.uuid]"
|
||||
:reason="values[currentField.preferences?.reason_field_uuid]"
|
||||
:field="currentField"
|
||||
:values="values"
|
||||
:previous-value="previousSignatureValueFor(currentField) || previousSignatureValue"
|
||||
:touch-attachment-uuid="previousSignatureValue"
|
||||
:with-typed-signature="withTypedSignature"
|
||||
:remember-signature="rememberSignature"
|
||||
:attachments-index="attachmentsIndex"
|
||||
:require-signing-reason="requireSigningReason"
|
||||
:signature-text="signatureText"
|
||||
:signature-src="signatureSrc"
|
||||
:button-text="submitButtonText"
|
||||
:dry-run="dryRun"
|
||||
:with-disclosure="withDisclosure"
|
||||
@@ -407,6 +500,7 @@
|
||||
:submitter="submitter"
|
||||
:show-field-names="showFieldNames"
|
||||
@update:reason="values[currentField.preferences?.reason_field_uuid] = $event"
|
||||
@touch-attachment="attachmentsIndex[previousSignatureValue] ? attachmentsIndex[previousSignatureValue].created_at = new Date() : null"
|
||||
@attached="attachments.push($event)"
|
||||
@start="scrollIntoField(currentField)"
|
||||
@minimize="minimizeForm"
|
||||
@@ -418,6 +512,7 @@
|
||||
v-model="values[currentField.uuid]"
|
||||
:field="currentField"
|
||||
:dry-run="dryRun"
|
||||
:submitter="submitter"
|
||||
:previous-value="previousInitialsValue"
|
||||
:attachments-index="attachmentsIndex"
|
||||
:show-field-names="showFieldNames"
|
||||
@@ -505,6 +600,7 @@
|
||||
<IconInnerShadowTop
|
||||
v-if="isSubmitting"
|
||||
class="mr-1 animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>
|
||||
{{ submitButtonText }}
|
||||
@@ -516,6 +612,7 @@
|
||||
</button>
|
||||
<div
|
||||
v-if="showFillAllRequiredFields"
|
||||
role="alert"
|
||||
class="text-center mt-1"
|
||||
>
|
||||
{{ t('please_fill_all_required_fields') }}
|
||||
@@ -526,6 +623,7 @@
|
||||
v-else-if="isInvite"
|
||||
:submitters="inviteSubmitters"
|
||||
:optional-submitters="optionalInviteSubmitters"
|
||||
:fetch-options="fetchOptions"
|
||||
:submitter-slug="submitterSlug"
|
||||
:authenticity-token="authenticityToken"
|
||||
:url="baseUrl + submitPath + '/invite'"
|
||||
@@ -539,6 +637,7 @@
|
||||
:has-signature-fields="stepFields.some((fields) => fields.some((f) => ['signature', 'initials'].includes(f.type)))"
|
||||
:has-multiple-documents="hasMultipleDocuments"
|
||||
:completed-button="completedRedirectUrl ? {} : completedButton"
|
||||
:fetch-options="fetchOptions"
|
||||
:completed-message="completedRedirectUrl ? {} : completedMessage"
|
||||
:with-send-copy-button="withSendCopyButton && !completedRedirectUrl"
|
||||
:with-download-button="withDownloadButton && !completedRedirectUrl && !dryRun"
|
||||
@@ -546,21 +645,29 @@
|
||||
:can-send-email="canSendEmail && !!submitter.email"
|
||||
:submitter-slug="submitterSlug"
|
||||
/>
|
||||
<div
|
||||
<nav
|
||||
v-if="stepFields.length < 80"
|
||||
:aria-label="t('form_progress')"
|
||||
:aria-hidden="isCompleted"
|
||||
class="flex justify-center mt-3 sm:mt-4 mb-0 sm:mb-1 select-none"
|
||||
>
|
||||
<div class="flex items-center flex-wrap steps-progress">
|
||||
<a
|
||||
<template
|
||||
v-for="(step, index) in stepFields"
|
||||
:key="step[0].uuid"
|
||||
href="#"
|
||||
class="inline border border-base-300 h-3 w-3 rounded-full mx-1 mt-1"
|
||||
:class="{ 'bg-base-300 steps-progress-current': index === currentStep, 'bg-base-content': (index < currentStep && stepFields[index].every((f) => !f.required || ![null, undefined, ''].includes(values[f.uuid]))) || isCompleted, 'bg-white': index > currentStep }"
|
||||
@click.prevent="isCompleted ? '' : [saveStep(), goToStep(index, true)]"
|
||||
/>
|
||||
>
|
||||
<button
|
||||
v-if="!onlyRequiredFields || step.some((f) => f.required)"
|
||||
type="button"
|
||||
:aria-label="`${t('step')} ${index + 1}`"
|
||||
:aria-current="index === currentStep ? 'step' : undefined"
|
||||
class="inline border border-base-300 h-3 w-3 rounded-full mx-1 mt-1 p-0"
|
||||
:class="{ 'bg-base-300 steps-progress-current': index === currentStep, 'bg-base-content': (index < currentStep && stepFields[index].every((f) => !f.required || ![null, undefined, ''].includes(values[f.uuid]))) || isCompleted, 'bg-white': index > currentStep }"
|
||||
@click="isCompleted ? '' : [saveStep(), goToStep(index, true)]"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div
|
||||
v-else
|
||||
class="mt-5"
|
||||
@@ -572,6 +679,7 @@
|
||||
<script>
|
||||
import FieldAreas from './areas'
|
||||
import FormulaFieldAreas from './formula_areas'
|
||||
import AccessibilityAreas from './accessibility_areas'
|
||||
import ImageStep from './image_step'
|
||||
import SignatureStep from './signature_step'
|
||||
import InitialsStep from './initials_step'
|
||||
@@ -592,6 +700,18 @@ import AppearsOn from './appears_on'
|
||||
import i18n from './i18n'
|
||||
import { sanitizeUrl } from '@braintree/sanitize-url'
|
||||
|
||||
if (typeof URL.canParse !== 'function') {
|
||||
URL.canParse = function (url, base) {
|
||||
try {
|
||||
const parsed = new URL(url, base)
|
||||
|
||||
return !!parsed
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isEmpty = (obj) => {
|
||||
if (obj == null) return true
|
||||
|
||||
@@ -618,6 +738,7 @@ export default {
|
||||
name: 'SubmissionForm',
|
||||
components: {
|
||||
FieldAreas,
|
||||
AccessibilityAreas,
|
||||
ImageStep,
|
||||
SignatureStep,
|
||||
AppearsOn,
|
||||
@@ -658,6 +779,11 @@ export default {
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
fetchOptions: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({})
|
||||
},
|
||||
optionalInviteSubmitters: {
|
||||
type: Array,
|
||||
required: false,
|
||||
@@ -678,6 +804,11 @@ export default {
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
onlyRequiredFields: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
requireSigningReason: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -693,6 +824,16 @@ export default {
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
completeButtonContainer: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
completeButtonScrollContainer: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
schema: {
|
||||
type: Array,
|
||||
required: false,
|
||||
@@ -790,6 +931,16 @@ export default {
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
filledFieldsIndex: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
withAccessibilityAreas: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
fields: {
|
||||
type: Array,
|
||||
required: false,
|
||||
@@ -800,6 +951,16 @@ export default {
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
signatureText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
signatureSrc: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
previousSignatureValue: {
|
||||
type: String,
|
||||
required: false,
|
||||
@@ -890,7 +1051,9 @@ export default {
|
||||
isSubmitting: false,
|
||||
isSubmittingComplete: false,
|
||||
submittedValues: {},
|
||||
recalculateButtonDisabledKey: ''
|
||||
isFormStarted: false,
|
||||
recalculateButtonDisabledKey: '',
|
||||
isAccessibilityMode: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -914,10 +1077,12 @@ export default {
|
||||
}, {})
|
||||
},
|
||||
attachmentConditionsIndex () {
|
||||
const cache = {}
|
||||
|
||||
return this.schema.reduce((acc, item) => {
|
||||
if (item.conditions?.length) {
|
||||
if (item.conditions.every((c) => this.fieldsUuidIndex[c.field_uuid])) {
|
||||
acc[item.attachment_uuid] = this.checkFieldConditions(item)
|
||||
acc[item.attachment_uuid] = this.checkFieldConditions(item, cache)
|
||||
} else {
|
||||
acc[item.attachment_uuid] = true
|
||||
}
|
||||
@@ -935,10 +1100,20 @@ export default {
|
||||
})
|
||||
})
|
||||
},
|
||||
showCompleteButton () {
|
||||
return this.completeButtonContainer && !this.isCompleted && !this.isInvite && this.isFormStarted &&
|
||||
!this.stepFields.find((fields) => fields.some((f) => f.required && isEmpty(this.submittedValues[f.uuid])))
|
||||
},
|
||||
submitButtonText () {
|
||||
if (this.alwaysMinimize) {
|
||||
return this.t('submit')
|
||||
} else if (this.stepFields.length === this.currentStep + 1) {
|
||||
} else if (!this.onlyRequiredFields && this.stepFields.length === this.currentStep + 1) {
|
||||
if (this.currentField.type === 'signature') {
|
||||
return this.t('sign_and_complete')
|
||||
} else {
|
||||
return this.t('complete')
|
||||
}
|
||||
} else if (this.onlyRequiredFields && !this.findNextStep(this.currentStep)) {
|
||||
if (this.currentField.type === 'signature') {
|
||||
return this.t('sign_and_complete')
|
||||
} else {
|
||||
@@ -984,7 +1159,9 @@ export default {
|
||||
},
|
||||
previousInitialsValue () {
|
||||
if (this.reuseSignature !== false) {
|
||||
const initialsField = [...this.fields].reverse().find((field) => field.type === 'initials' && !!this.values[field.uuid])
|
||||
const initialsField = this.fields.findLast
|
||||
? this.fields.findLast((field) => field.type === 'initials' && !!this.values[field.uuid])
|
||||
: [...this.fields].reverse().find((field) => field.type === 'initials' && !!this.values[field.uuid])
|
||||
|
||||
return this.values[initialsField?.uuid]
|
||||
} else {
|
||||
@@ -1011,7 +1188,9 @@ export default {
|
||||
return this.readonlyFields.filter((f) => f.conditions?.length)
|
||||
},
|
||||
readonlyFields () {
|
||||
return this.fields.filter((f) => f.readonly && this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f))
|
||||
const cache = {}
|
||||
|
||||
return this.fields.filter((f) => f.readonly && this.checkFieldConditions(f, cache) && this.checkFieldDocumentsConditions(f))
|
||||
},
|
||||
stepFields () {
|
||||
const verificationFields = []
|
||||
@@ -1066,10 +1245,12 @@ export default {
|
||||
sortedFields.push(verificationFields.pop())
|
||||
}
|
||||
|
||||
const cache = {}
|
||||
|
||||
return sortedFields.reduce((acc, f) => {
|
||||
const prevStep = acc[acc.length - 1]
|
||||
|
||||
if (this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f)) {
|
||||
if (this.checkFieldConditions(f, cache) && this.checkFieldDocumentsConditions(f)) {
|
||||
if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox' && !f.description) {
|
||||
prevStep.push(f)
|
||||
} else {
|
||||
@@ -1081,7 +1262,9 @@ export default {
|
||||
}, [])
|
||||
},
|
||||
formulaFields () {
|
||||
return this.fields.filter((f) => f.preferences?.formula && f.type !== 'payment' && this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f))
|
||||
const cache = {}
|
||||
|
||||
return this.fields.filter((f) => f.preferences?.formula && f.type !== 'payment' && this.checkFieldConditions(f, cache) && this.checkFieldDocumentsConditions(f))
|
||||
},
|
||||
attachmentsIndex () {
|
||||
return this.attachments.reduce((acc, a) => {
|
||||
@@ -1143,15 +1326,17 @@ export default {
|
||||
this.currentStep = Math.max(stepIndex, 0)
|
||||
} else if (this.goToLast) {
|
||||
const requiredEmptyStepIndex = this.stepFields.indexOf(this.stepFields.find((fields) => fields.some((f) => f.required && !this.submittedValues[f.uuid])))
|
||||
const lastFilledStepIndex = this.stepFields.indexOf([...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.submittedValues[f.uuid]))) + 1
|
||||
const lastFilledStepIndex = this.stepFields.indexOf(this.stepFields.findLast
|
||||
? this.stepFields.findLast((fields) => fields.some((f) => !!this.submittedValues[f.uuid]))
|
||||
: [...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.submittedValues[f.uuid]))) + 1
|
||||
|
||||
const indexesList = [this.stepFields.length - 1]
|
||||
|
||||
if (requiredEmptyStepIndex !== -1) {
|
||||
if (requiredEmptyStepIndex !== -1 && (!this.onlyRequiredFields || this.stepFields[requiredEmptyStepIndex].some((f) => f.required))) {
|
||||
indexesList.push(requiredEmptyStepIndex)
|
||||
}
|
||||
|
||||
if (lastFilledStepIndex !== -1) {
|
||||
if (lastFilledStepIndex !== -1 && (!this.onlyRequiredFields || this.stepFields[lastFilledStepIndex].some((f) => f.required))) {
|
||||
indexesList.push(lastFilledStepIndex)
|
||||
}
|
||||
|
||||
@@ -1211,27 +1396,35 @@ export default {
|
||||
return true
|
||||
}
|
||||
},
|
||||
checkFieldConditions (field) {
|
||||
checkFieldConditions (field, cache = {}) {
|
||||
const cacheKey = field.uuid || field.attachment_uuid
|
||||
|
||||
if (cache[cacheKey] !== undefined) {
|
||||
return cache[cacheKey]
|
||||
}
|
||||
|
||||
if (field.conditions?.length) {
|
||||
const result = field.conditions.reduce((acc, cond) => {
|
||||
if (cond.operation === 'or') {
|
||||
acc.push(acc.pop() || this.checkFieldCondition(cond))
|
||||
acc.push(acc.pop() || this.checkFieldCondition(cond, cache))
|
||||
} else {
|
||||
acc.push(this.checkFieldCondition(cond))
|
||||
acc.push(this.checkFieldCondition(cond, cache))
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
return !result.includes(false)
|
||||
cache[cacheKey] = !result.includes(false)
|
||||
} else {
|
||||
return true
|
||||
cache[cacheKey] = true
|
||||
}
|
||||
|
||||
return cache[cacheKey]
|
||||
},
|
||||
checkFieldCondition (condition) {
|
||||
checkFieldCondition (condition, cache = {}) {
|
||||
const field = this.fieldsUuidIndex[condition.field_uuid]
|
||||
|
||||
if (['not_empty', 'checked', 'equal', 'contains'].includes(condition.action) && field && !this.checkFieldConditions(field)) {
|
||||
if (['not_empty', 'checked', 'equal', 'contains', 'greater_than', 'less_than'].includes(condition.action) && field && !this.checkFieldConditions(field, cache)) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1241,6 +1434,22 @@ export default {
|
||||
return isEmpty(this.values[condition.field_uuid] ?? defaultValue)
|
||||
} else if (['not_empty', 'checked'].includes(condition.action)) {
|
||||
return !isEmpty(this.values[condition.field_uuid] ?? defaultValue)
|
||||
} else if (field?.type === 'number' && ['equal', 'not_equal', 'greater_than', 'less_than'].includes(condition.action)) {
|
||||
const value = this.values[condition.field_uuid] ?? defaultValue
|
||||
|
||||
if (isEmpty(value) || isEmpty(condition.value)) return false
|
||||
|
||||
const actual = parseFloat(value)
|
||||
const expected = parseFloat(condition.value)
|
||||
|
||||
if (Number.isNaN(actual) || Number.isNaN(expected)) return false
|
||||
|
||||
if (condition.action === 'equal') return Math.abs(actual - expected) < Number.EPSILON
|
||||
if (condition.action === 'not_equal') return Math.abs(actual - expected) > Number.EPSILON
|
||||
if (condition.action === 'greater_than') return actual > expected
|
||||
if (condition.action === 'less_than') return actual < expected
|
||||
|
||||
return false
|
||||
} else if (['equal', 'contains'].includes(condition.action) && field) {
|
||||
if (field.options) {
|
||||
const option = field.options.find((o) => o.uuid === condition.value)
|
||||
@@ -1280,6 +1489,13 @@ export default {
|
||||
return `${this.t('option')} ${index + 1}`
|
||||
}
|
||||
},
|
||||
findNextStep (currentStepIndex) {
|
||||
if (this.onlyRequiredFields) {
|
||||
return this.stepFields.find((step, index) => index > currentStepIndex && step.some((f) => f.required))
|
||||
} else {
|
||||
return this.stepFields[currentStepIndex + 1]
|
||||
}
|
||||
},
|
||||
maybeTrackEmailClick () {
|
||||
const { queryParams } = this
|
||||
|
||||
@@ -1341,9 +1557,9 @@ export default {
|
||||
},
|
||||
previousSignatureValueFor (field) {
|
||||
if (this.reuseSignature !== false) {
|
||||
const signatureField = [...this.fields].reverse().find((f) =>
|
||||
f.type === 'signature' && field.preferences?.format === f.preferences?.format && !!this.values[f.uuid]
|
||||
)
|
||||
const signatureField = this.fields.findLast
|
||||
? this.fields.findLast((f) => f.type === 'signature' && field.preferences?.format === f.preferences?.format && !!this.values[f.uuid])
|
||||
: [...this.fields].reverse().find((f) => f.type === 'signature' && field.preferences?.format === f.preferences?.format && !!this.values[f.uuid])
|
||||
|
||||
return this.values[signatureField?.uuid]
|
||||
} else {
|
||||
@@ -1365,7 +1581,7 @@ export default {
|
||||
}
|
||||
|
||||
this.enableScrollIntoField = false
|
||||
this.$refs.form.querySelector('input[type="date"], input[type="number"], input[type="text"], select')?.focus()
|
||||
this.$refs.form.querySelector('input[type="date"], input[type="number"], input[type="text"], input[type="tel"], textarea, select')?.focus()
|
||||
this.enableScrollIntoField = true
|
||||
|
||||
if (clickUpload && !this.values[this.currentField.uuid] && ['file', 'image'].includes(this.currentField.type)) {
|
||||
@@ -1393,7 +1609,8 @@ export default {
|
||||
} else {
|
||||
return fetch(this.baseUrl + this.submitPath, {
|
||||
method: 'POST',
|
||||
body: formData || new FormData(this.$refs.form)
|
||||
body: formData || new FormData(this.$refs.form),
|
||||
...this.fetchOptions
|
||||
}).then((response) => {
|
||||
if (response.status === 200) {
|
||||
currentFieldUuids.forEach((fieldUuid) => {
|
||||
@@ -1426,7 +1643,7 @@ export default {
|
||||
this.isSubmittingComplete = true
|
||||
}
|
||||
|
||||
const submitStep = this.currentStep
|
||||
const submitStepIndex = this.currentStep
|
||||
|
||||
const stepPromise = ['signature', 'phone', 'initials', 'payment', 'verification', 'kba'].includes(this.currentField.type)
|
||||
? this.$refs.currentStep.submit
|
||||
@@ -1434,7 +1651,7 @@ export default {
|
||||
|
||||
stepPromise().then(async () => {
|
||||
const emptyRequiredField = this.stepFields.find((fields, index) => {
|
||||
if (forceComplete ? index === submitStep : index >= submitStep) {
|
||||
if (forceComplete ? index === submitStepIndex : index >= submitStepIndex) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1444,7 +1661,7 @@ export default {
|
||||
})
|
||||
|
||||
const formData = new FormData(this.$refs.form)
|
||||
const isLastStep = (submitStep === this.stepFields.length - 1) || forceComplete
|
||||
const isLastStep = (this.onlyRequiredFields ? !this.findNextStep(submitStepIndex) : (submitStepIndex === this.stepFields.length - 1)) || forceComplete
|
||||
|
||||
if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length && !this.optionalInviteSubmitters.length) {
|
||||
formData.append('completed', 'true')
|
||||
@@ -1488,7 +1705,9 @@ export default {
|
||||
return Promise.reject(new Error(data.error))
|
||||
}
|
||||
|
||||
const nextStep = (isLastStep && emptyRequiredField) || (forceComplete ? null : this.stepFields[submitStep + 1])
|
||||
this.isFormStarted = true
|
||||
|
||||
const nextStep = (isLastStep && emptyRequiredField) || (forceComplete ? null : this.findNextStep(submitStepIndex))
|
||||
|
||||
if (nextStep) {
|
||||
if (this.alwaysMinimize) {
|
||||
@@ -1524,6 +1743,7 @@ export default {
|
||||
},
|
||||
async performComplete (resp) {
|
||||
this.isCompleted = true
|
||||
this.isFormVisible = true
|
||||
|
||||
if (resp?.text) {
|
||||
const respData = await resp.text()
|
||||
@@ -1535,6 +1755,15 @@ export default {
|
||||
|
||||
if (this.completedRedirectUrl) {
|
||||
window.location.href = sanitizeUrl(this.completedRedirectUrl)
|
||||
} else {
|
||||
this.$nextTick(() => {
|
||||
const root = this.$root.$el.parentNode.getRootNode()
|
||||
const completedEl = root.getElementById('form_completed')
|
||||
|
||||
if (completedEl) {
|
||||
completedEl.focus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,34 +68,9 @@ export default {
|
||||
}
|
||||
},
|
||||
async mounted () {
|
||||
const {
|
||||
create,
|
||||
evaluateDependencies,
|
||||
addDependencies,
|
||||
subtractDependencies,
|
||||
divideDependencies,
|
||||
multiplyDependencies,
|
||||
powDependencies,
|
||||
roundDependencies,
|
||||
absDependencies,
|
||||
sinDependencies,
|
||||
tanDependencies,
|
||||
cosDependencies
|
||||
} = await import('mathjs')
|
||||
const { Calculator } = await import('./calculator')
|
||||
|
||||
this.math = create({
|
||||
evaluateDependencies,
|
||||
addDependencies,
|
||||
subtractDependencies,
|
||||
divideDependencies,
|
||||
multiplyDependencies,
|
||||
powDependencies,
|
||||
roundDependencies,
|
||||
absDependencies,
|
||||
sinDependencies,
|
||||
tanDependencies,
|
||||
cosDependencies
|
||||
})
|
||||
this.math = new Calculator()
|
||||
|
||||
this.isMathLoaded = true
|
||||
},
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
const en = {
|
||||
step: 'Step',
|
||||
form_progress: 'Form progress',
|
||||
close: 'Close',
|
||||
uploaded_files: 'Uploaded files',
|
||||
signature_drawing_area: 'Signature drawing area. Use mouse or touch to draw your signature.',
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'Please upload an image file',
|
||||
must_be_characters_length: 'Must be {number} characters length',
|
||||
must_be_characters_length: 'Must be {number} characters long',
|
||||
must_be_valid_ssn: 'Must be a valid SSN (XXX-XX-XXXX)',
|
||||
must_be_valid_ein: 'Must be a valid EIN (XX-XXXXXXX)',
|
||||
must_be_valid_email: 'Must be a valid email address',
|
||||
must_be_valid_url: 'Must be a valid URL (starting with http:// or https://)',
|
||||
must_be_valid_zip: 'Must be a valid ZIP code (XXXXX or XXXXX-XXXX)',
|
||||
must_contain_numbers_only: 'Must contain numbers only',
|
||||
must_contain_letters_only: 'Must contain letters only',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.',
|
||||
verify_id: 'Verify ID',
|
||||
identity_verification: 'Identity verification',
|
||||
@@ -97,13 +109,27 @@ const en = {
|
||||
upload: 'Upload',
|
||||
files: 'Files',
|
||||
signature_is_too_small_or_simple_please_redraw: 'Signature is too small or simple. Please redraw.',
|
||||
wait_countdown_seconds: 'Wait {countdown} seconds'
|
||||
browser_privacy_settings_block_canvas: 'Your browser privacy settings restrict use of the drawing canvas. Please use a different browser or device, or disable privacy settings that block canvas in order to sign.',
|
||||
wait_countdown_seconds: 'Wait {countdown} seconds',
|
||||
enter_screen_reader_mode: 'Enter screen reader mode'
|
||||
}
|
||||
|
||||
const es = {
|
||||
step: 'Paso',
|
||||
form_progress: 'Progreso del formulario',
|
||||
close: 'Cerrar',
|
||||
uploaded_files: 'Archivos subidos',
|
||||
signature_drawing_area: 'Área de dibujo de firma. Use el ratón o toque para dibujar su firma.',
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'Por favor, sube un archivo de imagen',
|
||||
must_be_characters_length: 'Debe tener {number} caracteres de longitud',
|
||||
must_be_valid_ssn: 'Debe ser un SSN válido (XXX-XX-XXXX)',
|
||||
must_be_valid_ein: 'Debe ser un EIN válido (XX-XXXXXXX)',
|
||||
must_be_valid_email: 'Debe ser una dirección de correo electrónico válida',
|
||||
must_be_valid_url: 'Debe ser una URL válida (que comience con http:// o https://)',
|
||||
must_be_valid_zip: 'Debe ser un ZIP válido (XXXXX o XXXXX-XXXX)',
|
||||
must_contain_numbers_only: 'Debe contener solo números',
|
||||
must_contain_letters_only: 'Debe contener solo letras',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete todos los campos requeridos para continuar con la verificación de identidad.',
|
||||
verify_id: 'Verificar ID',
|
||||
identity_verification: 'Verificación de identidad',
|
||||
@@ -124,7 +150,7 @@ const es = {
|
||||
verification_code_is_invalid: 'El código de verificación no es válido',
|
||||
sign_on_the_touchscreen: 'Firmar en pantalla táctil',
|
||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Escanea el código QR con la aplicación de la cámara para abrir el formulario en el móvil y dibujar tu firma',
|
||||
by_clicking_you_agree_to_the: 'Al hacer clic en "{button}", usted acepta el',
|
||||
by_clicking_you_agree_to_the: 'Al hacer clic en "{button}", usted acepta la',
|
||||
electronic_signature_disclosure: 'Divulgación de Firma Electrónica',
|
||||
esignature_disclosure: 'Divulgación de eFirma',
|
||||
already_paid: 'Ya pagado',
|
||||
@@ -199,13 +225,27 @@ const es = {
|
||||
upload: 'Subir',
|
||||
files: 'Archivos',
|
||||
signature_is_too_small_or_simple_please_redraw: 'La firma es demasiado pequeña o simple. Por favor, vuelve a dibujarla.',
|
||||
wait_countdown_seconds: 'Espera {countdown} segundos'
|
||||
browser_privacy_settings_block_canvas: 'La configuración de privacidad de su navegador restringe el uso del lienzo de dibujo. Utilice un navegador o dispositivo diferente, o desactive la configuración de privacidad que bloquea el lienzo para firmar.',
|
||||
wait_countdown_seconds: 'Espera {countdown} segundos',
|
||||
enter_screen_reader_mode: 'Activar modo lector de pantalla'
|
||||
}
|
||||
|
||||
const it = {
|
||||
step: 'Passo',
|
||||
form_progress: 'Progresso del modulo',
|
||||
close: 'Chiudi',
|
||||
uploaded_files: 'File caricati',
|
||||
signature_drawing_area: 'Area di disegno della firma. Usa il mouse o il tocco per disegnare la tua firma.',
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'Per favore carica un file immagine',
|
||||
must_be_characters_length: 'Deve essere lungo {number} caratteri',
|
||||
must_be_valid_ssn: 'Deve essere un SSN valido (XXX-XX-XXXX)',
|
||||
must_be_valid_ein: 'Deve essere un EIN valido (XX-XXXXXXX)',
|
||||
must_be_valid_email: 'Deve essere un indirizzo email valido',
|
||||
must_be_valid_url: 'Deve essere un URL valido (che inizia con http:// o https://)',
|
||||
must_be_valid_zip: 'Deve essere uno ZIP valido (XXXXX o XXXXX-XXXX)',
|
||||
must_contain_numbers_only: 'Deve contenere solo numeri',
|
||||
must_contain_letters_only: 'Deve contenere solo lettere',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: "Compila tutti i campi obbligatori per procedere con la verifica dell'identità.",
|
||||
verify_id: 'Verifica ID',
|
||||
identity_verification: "Verifica dell'identità",
|
||||
@@ -226,7 +266,7 @@ const it = {
|
||||
verification_code_is_invalid: 'Il codice di verifica non è valido',
|
||||
sign_on_the_touchscreen: 'Firma su schermo tattile',
|
||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: "Scansiona il codice QR con l'app della fotocamera per aprire il modulo sul cellulare e disegnare la tua firma",
|
||||
by_clicking_you_agree_to_the: 'Cliccando su "{button}", accetti il',
|
||||
by_clicking_you_agree_to_the: 'Cliccando su "{button}", accetti la',
|
||||
electronic_signature_disclosure: 'Divulgazione della Firma Elettronica',
|
||||
esignature_disclosure: 'Divulgazione della eFirma',
|
||||
minimize: 'Minimizza',
|
||||
@@ -239,7 +279,7 @@ const it = {
|
||||
date: 'Data',
|
||||
number: 'Numero',
|
||||
image: 'Immagine',
|
||||
pay: 'Pagamento',
|
||||
pay: 'Paga',
|
||||
file: 'File',
|
||||
select: 'Seleziona',
|
||||
checkbox: 'Checkbox',
|
||||
@@ -301,13 +341,27 @@ const it = {
|
||||
upload: 'Carica',
|
||||
files: 'File',
|
||||
signature_is_too_small_or_simple_please_redraw: 'La firma è troppo piccola o semplice. Ridisegnala, per favore.',
|
||||
wait_countdown_seconds: 'Attendi {countdown} secondi'
|
||||
browser_privacy_settings_block_canvas: 'Le impostazioni sulla privacy del browser limitano l\'uso dell\'area di disegno. Utilizza un browser o dispositivo diverso oppure disattiva le impostazioni sulla privacy che bloccano il canvas per firmare.',
|
||||
wait_countdown_seconds: 'Attendi {countdown} secondi',
|
||||
enter_screen_reader_mode: 'Attiva modalità lettore di schermo'
|
||||
}
|
||||
|
||||
const de = {
|
||||
step: 'Schritt',
|
||||
form_progress: 'Formularfortschritt',
|
||||
close: 'Schließen',
|
||||
uploaded_files: 'Hochgeladene Dateien',
|
||||
signature_drawing_area: 'Unterschriftszeichenbereich. Verwenden Sie die Maus oder Berührung, um Ihre Unterschrift zu zeichnen.',
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'Bitte laden Sie eine Bilddatei hoch',
|
||||
must_be_characters_length: 'Muss {number} Zeichen lang sein',
|
||||
must_be_valid_ssn: 'Muss eine gültige SSN sein (XXX-XX-XXXX)',
|
||||
must_be_valid_ein: 'Muss eine gültige EIN sein (XX-XXXXXXX)',
|
||||
must_be_valid_email: 'Muss eine gültige E-Mail-Adresse sein',
|
||||
must_be_valid_url: 'Muss eine gültige URL sein (beginnend mit http:// oder https://)',
|
||||
must_be_valid_zip: 'Muss eine gültige ZIP sein (XXXXX oder XXXXX-XXXX)',
|
||||
must_contain_numbers_only: 'Darf nur Zahlen enthalten',
|
||||
must_contain_letters_only: 'Darf nur Buchstaben enthalten',
|
||||
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ätsprüfung',
|
||||
@@ -403,13 +457,27 @@ const de = {
|
||||
upload: 'Hochladen',
|
||||
files: 'Dateien',
|
||||
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'
|
||||
browser_privacy_settings_block_canvas: 'Die Datenschutzeinstellungen Ihres Browsers schränken die Nutzung der Zeichenfläche ein. Bitte verwenden Sie einen anderen Browser oder ein anderes Gerät oder deaktivieren Sie die Datenschutzeinstellungen, die Canvas blockieren, um zu unterschreiben.',
|
||||
wait_countdown_seconds: 'Bitte {countdown} Sekunden warten',
|
||||
enter_screen_reader_mode: 'Screenreader-Modus aktivieren'
|
||||
}
|
||||
|
||||
const fr = {
|
||||
step: 'Étape',
|
||||
form_progress: 'Progression du formulaire',
|
||||
close: 'Fermer',
|
||||
uploaded_files: 'Fichiers téléchargés',
|
||||
signature_drawing_area: 'Zone de dessin de signature. Utilisez la souris ou le toucher pour dessiner votre signature.',
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'Veuillez téléverser un fichier image',
|
||||
must_be_characters_length: 'Doit comporter {number} caractères',
|
||||
must_be_valid_ssn: 'Doit être un SSN valide (XXX-XX-XXXX)',
|
||||
must_be_valid_ein: 'Doit être un EIN valide (XX-XXXXXXX)',
|
||||
must_be_valid_email: 'Doit être une adresse e-mail valide',
|
||||
must_be_valid_url: 'Doit être une URL valide (commençant par http:// ou https://)',
|
||||
must_be_valid_zip: 'Doit être un ZIP valide (XXXXX ou XXXXX-XXXX)',
|
||||
must_contain_numbers_only: 'Doit contenir uniquement des chiffres',
|
||||
must_contain_letters_only: 'Doit contenir uniquement des lettres',
|
||||
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é",
|
||||
@@ -505,13 +573,27 @@ const fr = {
|
||||
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: 'Veuillez patienter {countdown} secondes'
|
||||
browser_privacy_settings_block_canvas: 'Les paramètres de confidentialité de votre navigateur empêchent l\'utilisation du canevas de dessin. Veuillez utiliser un autre navigateur ou appareil, ou désactiver les paramètres de confidentialité qui bloquent le canevas pour signer.',
|
||||
wait_countdown_seconds: 'Veuillez patienter {countdown} secondes',
|
||||
enter_screen_reader_mode: 'Activer le mode lecteur d\'écran'
|
||||
}
|
||||
|
||||
const pl = {
|
||||
step: 'Krok',
|
||||
form_progress: 'Postęp formularza',
|
||||
close: 'Zamknij',
|
||||
uploaded_files: 'Przesłane pliki',
|
||||
signature_drawing_area: 'Obszar rysowania podpisu. Użyj myszy lub dotyku, aby narysować swój podpis.',
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'Proszę przesłać plik obrazu',
|
||||
must_be_characters_length: 'Musi mieć długość {number} znaków',
|
||||
must_be_valid_ssn: 'Musi być prawidłowym numerem SSN (XXX-XX-XXXX)',
|
||||
must_be_valid_ein: 'Musi być prawidłowym numerem EIN (XX-XXXXXXX)',
|
||||
must_be_valid_email: 'Musi być prawidłowym adresem e-mail',
|
||||
must_be_valid_url: 'Musi być prawidłowym adresem URL (zaczynającym się od http:// lub https://)',
|
||||
must_be_valid_zip: 'Musi być prawidłowym ZIP (XXXXX lub XXXXX-XXXX)',
|
||||
must_contain_numbers_only: 'Może zawierać tylko cyfry',
|
||||
must_contain_letters_only: 'Może zawierać tylko litery',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Uzupełnij wszystkie wymagane pola, aby kontynuować weryfikację tożsamości.',
|
||||
verify_id: 'Zweryfikuj ID',
|
||||
identity_verification: 'Weryfikacja tożsamości',
|
||||
@@ -534,7 +616,7 @@ const pl = {
|
||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Zeskanuj kod QR za pomocą aplikacji aparatu, aby otworzyć formularz na telefonie i narysować swój podpis',
|
||||
by_clicking_you_agree_to_the: 'Klikając na "{button}", zgadzasz się na',
|
||||
electronic_signature_disclosure: 'Ujawnienie Elektronicznej Sygnatury',
|
||||
esignature_disclosure: 'Ujawnienie ePodpisu',
|
||||
esignature_disclosure: 'informację o e-podpisie',
|
||||
minimize: 'Zminimalizuj',
|
||||
text: 'Tekst',
|
||||
already_paid: 'Już zapłacono',
|
||||
@@ -602,18 +684,32 @@ const pl = {
|
||||
toggle_multiline_text: 'Przełącz Tekst Wielolinijkowy',
|
||||
email_has_been_sent: 'E-mail został wysłany',
|
||||
processing: 'Przetwarzanie',
|
||||
pay_with_stripe: 'Płatność za pomocą Stripe',
|
||||
pay_with_stripe: 'Zapłać za pomocą Stripe',
|
||||
reupload: 'Ponowne przesłanie',
|
||||
upload: 'Przesyłanie',
|
||||
files: 'Pliki',
|
||||
signature_is_too_small_or_simple_please_redraw: 'Podpis jest zbyt mały lub zbyt prosty. Proszę narysować go ponownie.',
|
||||
wait_countdown_seconds: 'Poczekaj {countdown} sekund'
|
||||
browser_privacy_settings_block_canvas: 'Ustawienia prywatności przeglądarki blokują użycie obszaru rysowania. Użyj innej przeglądarki lub urządzenia albo wyłącz ustawienia prywatności blokujące canvas, aby podpisać.',
|
||||
wait_countdown_seconds: 'Poczekaj {countdown} sekund',
|
||||
enter_screen_reader_mode: 'Włącz tryb czytnika ekranu'
|
||||
}
|
||||
|
||||
const uk = {
|
||||
step: 'Крок',
|
||||
form_progress: 'Прогрес форми',
|
||||
close: 'Закрити',
|
||||
uploaded_files: 'Завантажені файли',
|
||||
signature_drawing_area: 'Область малювання підпису. Використовуйте мишу або дотик, щоб намалювати свій підпис.',
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'Будь ласка, завантажте файл зображення',
|
||||
must_be_characters_length: 'Має містити {number} символів',
|
||||
must_be_valid_ssn: 'Має бути дійсним SSN (XXX-XX-XXXX)',
|
||||
must_be_valid_ein: 'Має бути дійсним EIN (XX-XXXXXXX)',
|
||||
must_be_valid_email: 'Має бути дійсною адресою електронної пошти',
|
||||
must_be_valid_url: 'Має бути дійсним URL (починаючи з http:// або https://)',
|
||||
must_be_valid_zip: 'Має бути дійсним ZIP (XXXXX або XXXXX-XXXX)',
|
||||
must_contain_numbers_only: 'Має містити лише цифри',
|
||||
must_contain_letters_only: 'Має містити лише літери',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: "Заповніть всі обов'язкові поля, щоб продовжити перевірку особи.",
|
||||
verify_id: 'Підтвердження ідентичності',
|
||||
identity_verification: 'Ідентифікація особи',
|
||||
@@ -631,12 +727,12 @@ const uk = {
|
||||
authored_by: 'Автор',
|
||||
select_a_reason: 'Виберіть причину',
|
||||
value_is_invalid: 'Значення є неправильним',
|
||||
verification_code_is_invalid: 'Код підтвердження є неправильним',
|
||||
verification_code_is_invalid: 'Невірний код підтвердження',
|
||||
sign_on_the_touchscreen: 'Підписати на сенсорному екрані',
|
||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Скануйте QR-код за допомогою програми камери, щоб відкрити форму на мобільному пристрої та намалювати свій підпис',
|
||||
by_clicking_you_agree_to_the: 'Натиснувши на "{button}", ви погоджуєтеся з',
|
||||
electronic_signature_disclosure: 'Розголошення Електронного Підпису',
|
||||
esignature_disclosure: 'Розголошення еПідпису',
|
||||
electronic_signature_disclosure: 'інформацією про е-підпис',
|
||||
esignature_disclosure: 'інформацією про е-підпис',
|
||||
minimize: 'Зменшити',
|
||||
text: 'Текст',
|
||||
already_paid: 'Вже оплачено',
|
||||
@@ -704,23 +800,37 @@ const uk = {
|
||||
toggle_multiline_text: 'Перемкнути Багаторядковий Текст',
|
||||
email_has_been_sent: 'Електронний лист був відправлений',
|
||||
processing: 'Обробка',
|
||||
pay_with_strip: 'Сплатити за допомогою Stripe',
|
||||
pay_with_stripe: 'Сплатити за допомогою Stripe',
|
||||
reupload: 'Перезавантажити',
|
||||
upload: 'Завантажити',
|
||||
files: 'Файли',
|
||||
signature_is_too_small_or_simple_please_redraw: 'Підпис занадто маленький або надто простий. Будь ласка, перемалюйте.',
|
||||
wait_countdown_seconds: 'Зачекайте {countdown} секунд'
|
||||
browser_privacy_settings_block_canvas: 'Налаштування конфіденційності вашого браузера блокують використання полотна для малювання. Будь ласка, скористайтеся іншим браузером або пристроєм, або вимкніть налаштування конфіденційності, що блокують canvas, щоб підписати.',
|
||||
wait_countdown_seconds: 'Зачекайте {countdown} секунд',
|
||||
enter_screen_reader_mode: 'Увімкнути режим читання з екрану'
|
||||
}
|
||||
|
||||
const cs = {
|
||||
step: 'Krok',
|
||||
form_progress: 'Průběh formuláře',
|
||||
close: 'Zavřít',
|
||||
uploaded_files: 'Nahrané soubory',
|
||||
signature_drawing_area: 'Oblast pro kreslení podpisu. Použijte myš nebo dotyk k nakreslení podpisu.',
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'Nahrajte prosím obrázkový soubor',
|
||||
must_be_characters_length: 'Musí mít délku {number} znaků',
|
||||
must_be_valid_ssn: 'Musí být platné SSN (XXX-XX-XXXX)',
|
||||
must_be_valid_ein: 'Musí být platné EIN (XX-XXXXXXX)',
|
||||
must_be_valid_email: 'Musí být platná e-mailová adresa',
|
||||
must_be_valid_url: 'Musí být platná URL (začínající http:// nebo https://)',
|
||||
must_be_valid_zip: 'Musí být platný ZIP (XXXXX nebo XXXXX-XXXX)',
|
||||
must_contain_numbers_only: 'Musí obsahovat pouze čísla',
|
||||
must_contain_letters_only: 'Musí obsahovat pouze písmena',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Vyplňte všechna povinná pole, abyste mohli pokračovat v ověření identity.',
|
||||
verify_id: 'Ověřit ID',
|
||||
identity_verification: 'Ověření identity',
|
||||
complete: 'Dokončit',
|
||||
fill_all_required_fields_to_complete: 'Please complete all mandatory fields',
|
||||
fill_all_required_fields_to_complete: 'Vyplňte povinná pole pro dokončení',
|
||||
sign_and_complete: 'Podepsat a dokončit',
|
||||
invite: 'Pozvat',
|
||||
email: 'E-mail',
|
||||
@@ -728,8 +838,8 @@ const cs = {
|
||||
reviewed: 'Zkontrolováno',
|
||||
other: 'Jiné',
|
||||
authored_by_me: 'Autorem jsem já',
|
||||
approved_by: 'Schváleno kým',
|
||||
reviewed_by: 'Zkontrolováno kým',
|
||||
approved_by: 'Schválil(a)',
|
||||
reviewed_by: 'Zkontroloval(a)',
|
||||
authored_by: 'Autorem',
|
||||
select_a_reason: 'Vyberte důvod',
|
||||
value_is_invalid: 'Hodnota je neplatná',
|
||||
@@ -738,7 +848,7 @@ const cs = {
|
||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Naskenujte QR kód pomocí aplikace fotoaparátu, abyste otevřeli formulář na mobilním zařízení a nakreslili svůj podpis',
|
||||
by_clicking_you_agree_to_the: 'Kliknutím na "{button}" souhlasíte s',
|
||||
electronic_signature_disclosure: 'Zveřejněním Elektronického Podpisu',
|
||||
esignature_disclosure: 'Zveřejnění ePodpisu',
|
||||
esignature_disclosure: 'informací o e-podpisu',
|
||||
already_paid: 'Už zaplaceno',
|
||||
minimize: 'Minimalizovat',
|
||||
text: 'Text',
|
||||
@@ -806,18 +916,32 @@ 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_stripe: 'Zaplacení přes Stripe',
|
||||
pay_with_stripe: 'Zaplatit přes Stripe',
|
||||
reupload: 'Znovu nahrát',
|
||||
upload: 'Nahrát',
|
||||
files: 'Soubory',
|
||||
signature_is_too_small_or_simple_please_redraw: 'Podpis je příliš malý nebo jednoduchý. Nakreslete jej prosím znovu.',
|
||||
wait_countdown_seconds: 'Počkejte {countdown} sekund'
|
||||
browser_privacy_settings_block_canvas: 'Nastavení soukromí vašeho prohlížeče omezuje použití kreslicího plátna. Použijte prosím jiný prohlížeč nebo zařízení, nebo vypněte nastavení soukromí blokující canvas pro podepsání.',
|
||||
wait_countdown_seconds: 'Počkejte {countdown} sekund',
|
||||
enter_screen_reader_mode: 'Zapnout režim čtečky obrazovky'
|
||||
}
|
||||
|
||||
const pt = {
|
||||
step: 'Passo',
|
||||
form_progress: 'Progresso do formulário',
|
||||
close: 'Fechar',
|
||||
uploaded_files: 'Arquivos enviados',
|
||||
signature_drawing_area: 'Área de desenho da assinatura. Use o mouse ou toque para desenhar sua assinatura.',
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'Por favor, envie um arquivo de imagem',
|
||||
must_be_characters_length: 'Deve ter {number} caracteres',
|
||||
must_be_valid_ssn: 'Deve ser um SSN válido (XXX-XX-XXXX)',
|
||||
must_be_valid_ein: 'Deve ser um EIN válido (XX-XXXXXXX)',
|
||||
must_be_valid_email: 'Deve ser um endereço de e-mail válido',
|
||||
must_be_valid_url: 'Deve ser uma URL válida (começando com http:// ou https://)',
|
||||
must_be_valid_zip: 'Deve ser um ZIP válido (XXXXX ou XXXXX-XXXX)',
|
||||
must_contain_numbers_only: 'Deve conter apenas números',
|
||||
must_contain_letters_only: 'Deve conter apenas letras',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Preencha todos os campos obrigatórios para prosseguir com a verificação de identidade.',
|
||||
verify_id: 'Verificar ID',
|
||||
identity_verification: 'Verificação de identidade',
|
||||
@@ -829,18 +953,18 @@ const pt = {
|
||||
approved: 'Aprovado',
|
||||
reviewed: 'Revisado',
|
||||
other: 'Outro',
|
||||
authored_by_me: 'Autorizado por mim',
|
||||
authored_by_me: 'Escrito por mim',
|
||||
approved_by: 'Aprovado por',
|
||||
reviewed_by: 'Revisado por',
|
||||
authored_by: 'Autorizado por',
|
||||
authored_by: 'Escrito por',
|
||||
select_a_reason: 'Selecione um motivo',
|
||||
value_is_invalid: 'Valor é inválido',
|
||||
verification_code_is_invalid: 'Código de verificação é inválido',
|
||||
sign_on_the_touchscreen: 'Assinar na tela sensível',
|
||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Escaneie o código QR com o aplicativo da câmera para abrir o formulário no celular e desenhar sua assinatura',
|
||||
by_clicking_you_agree_to_the: 'Ao clicar em "{button}", você concorda com o',
|
||||
by_clicking_you_agree_to_the: 'Ao clicar em "{button}", você concorda com a',
|
||||
electronic_signature_disclosure: 'Divulgação de Assinatura Eletrônica',
|
||||
esignature_disclosure: 'Divulgação da eAssinatura',
|
||||
esignature_disclosure: 'Informação sobre eAssinatura',
|
||||
already_paid: 'Já pago',
|
||||
minimize: 'Minimizar',
|
||||
text: 'Texto',
|
||||
@@ -909,17 +1033,31 @@ const pt = {
|
||||
email_has_been_sent: 'Email enviado',
|
||||
processing: 'Processamento',
|
||||
pay_with_stripe: 'Pagar com Stripe',
|
||||
reupload: 'Reenviar',
|
||||
reupload: 'Recarregar',
|
||||
upload: 'Carregar',
|
||||
files: 'Arquivos',
|
||||
signature_is_too_small_or_simple_please_redraw: 'A assinatura é muito pequena ou simples. Por favor, redesenhe.',
|
||||
wait_countdown_seconds: 'Aguarde {countdown} segundos'
|
||||
browser_privacy_settings_block_canvas: 'As configurações de privacidade do seu navegador restringem o uso da área de desenho. Use um navegador ou dispositivo diferente, ou desative as configurações de privacidade que bloqueiam o canvas para assinar.',
|
||||
wait_countdown_seconds: 'Aguarde {countdown} segundos',
|
||||
enter_screen_reader_mode: 'Ativar modo leitor de tela'
|
||||
}
|
||||
|
||||
const he = {
|
||||
step: 'שלב',
|
||||
form_progress: 'התקדמות הטופס',
|
||||
close: 'סגור',
|
||||
uploaded_files: 'קבצים שהועלו',
|
||||
signature_drawing_area: 'אזור ציור חתימה. השתמש בעכבר או במגע כדי לצייר את החתימה שלך.',
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'אנא העלה קובץ תמונה',
|
||||
must_be_characters_length: 'חייב להיות באורך של {number} תווים',
|
||||
must_be_valid_ssn: 'חייב להיות SSN חוקי (XXX-XX-XXXX)',
|
||||
must_be_valid_ein: 'חייב להיות EIN חוקי (XX-XXXXXXX)',
|
||||
must_be_valid_email: 'חייבת להיות כתובת אימייל חוקית',
|
||||
must_be_valid_url: 'חייבת להיות כתובת URL חוקית (המתחילה ב-http:// או https://)',
|
||||
must_be_valid_zip: 'חייב להיות ZIP חוקי (XXXXX או XXXXX-XXXX)',
|
||||
must_contain_numbers_only: 'חייב להכיל מספרים בלבד',
|
||||
must_contain_letters_only: 'חייב להכיל אותיות בלבד',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: 'מלא את כל השדות הנדרשים כדי להמשיך עם אימות זהות.',
|
||||
verify_id: 'אמת מזהה',
|
||||
identity_verification: 'אימות זהות',
|
||||
@@ -942,8 +1080,8 @@ const he = {
|
||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'סרוק את קוד ה-QR באמצעות אפליקציית המצלמה כדי לפתוח את הטופס במובייל ולצייר את החתימה שלך',
|
||||
by_clicking_you_agree_to_the: 'על ידי לחיצה על "{button}", אתה מסכים ל',
|
||||
electronic_signature_disclosure: 'חשיפת חתימה אלקטרונית',
|
||||
esignature_disclosure: 'חשיפת ה-eחתימה',
|
||||
minimize: 'לקטן',
|
||||
esignature_disclosure: 'חשיפת חתימה אלקטרונית',
|
||||
minimize: 'מזער',
|
||||
text: 'טקסט',
|
||||
already_paid: 'כבר שולם',
|
||||
signature: 'חתימה',
|
||||
@@ -954,9 +1092,9 @@ const he = {
|
||||
number: 'מספר',
|
||||
image: 'תמונה',
|
||||
file: 'קובץ',
|
||||
pay: 'תשלום',
|
||||
pay: 'שלם',
|
||||
select: 'בחר',
|
||||
checkbox: 'תיק בחירה',
|
||||
checkbox: 'סימון',
|
||||
multiple: 'רב ערכים',
|
||||
radio: 'רדיו',
|
||||
cells: 'תאים',
|
||||
@@ -983,9 +1121,9 @@ const he = {
|
||||
download: 'הורדה',
|
||||
clear: 'נקה',
|
||||
redraw: 'צייר מחדש',
|
||||
draw_initials: 'צייר ציוני ראשי תיבות',
|
||||
draw_initials: 'צייר ראשי תיבות',
|
||||
type_signature_here: 'הקלד חתימה כאן',
|
||||
type_initial_here: 'הקלד ציוני ראשי תיבות כאן',
|
||||
type_initial_here: 'הקלד ראשי תיבות כאן',
|
||||
form_has_been_completed: 'הטופס הושלם',
|
||||
document_has_been_signed: 'המסמך נחתם!',
|
||||
documents_have_been_signed: 'המסמכים נחתמו!',
|
||||
@@ -995,7 +1133,7 @@ const he = {
|
||||
open_source_documents_software: 'תוכנה פתוחה למסמכים',
|
||||
verified_phone_number: 'אימות מספר טלפון',
|
||||
use_international_format: 'השתמש בפורמט בינלאומי: +1xxx',
|
||||
six_digits_code: 'קוד משתמש שש ספרות',
|
||||
six_digits_code: 'קוד שש ספרות',
|
||||
change_phone_number: 'שינוי מספר טלפון',
|
||||
sending: 'שולח',
|
||||
resend_code: 'שלח מחדש קוד',
|
||||
@@ -1004,7 +1142,7 @@ const he = {
|
||||
set_today: 'קבע היום',
|
||||
toggle_multiline_text: 'שנה בין טקסט במספר שורות לטקסט בשורה אחת',
|
||||
draw_signature: 'צייר חתימה',
|
||||
type_initial: 'הקלד ציוני ראשי תיבות',
|
||||
type_initial: 'הקלד ראשי תיבות',
|
||||
draw: 'צייר',
|
||||
type: 'הקלד',
|
||||
type_text: 'הקלד טקסט',
|
||||
@@ -1015,15 +1153,29 @@ const he = {
|
||||
upload: 'העלאה',
|
||||
files: 'קבצים',
|
||||
signature_is_too_small_or_simple_please_redraw: 'החתימה קטנה או פשוטה מדי. אנא חתום מחדש.',
|
||||
wait_countdown_seconds: 'המתן {countdown} שניות'
|
||||
browser_privacy_settings_block_canvas: 'הגדרות הפרטיות של הדפדפן שלך מגבילות את השימוש באזור הציור. אנא השתמש בדפדפן או מכשיר אחר, או בטל את הגדרות הפרטיות החוסמות canvas כדי לחתום.',
|
||||
wait_countdown_seconds: 'המתן {countdown} שניות',
|
||||
enter_screen_reader_mode: 'הפעל מצב קורא מסך'
|
||||
}
|
||||
|
||||
const nl = {
|
||||
step: 'Stap',
|
||||
form_progress: 'Formuliervoortgang',
|
||||
close: 'Sluiten',
|
||||
uploaded_files: 'Geüploade bestanden',
|
||||
signature_drawing_area: 'Handtekening tekengebied. Gebruik de muis of aanraking om uw handtekening te tekenen.',
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'Upload alstublieft een afbeeldingsbestand',
|
||||
must_be_characters_length: 'Moet {number} tekens lang zijn',
|
||||
must_be_valid_ssn: 'Moet een geldig SSN zijn (XXX-XX-XXXX)',
|
||||
must_be_valid_ein: 'Moet een geldig EIN zijn (XX-XXXXXXX)',
|
||||
must_be_valid_email: 'Moet een geldig e-mailadres zijn',
|
||||
must_be_valid_url: 'Moet een geldige URL zijn (beginnend met http:// of https://)',
|
||||
must_be_valid_zip: 'Moet een geldige ZIP zijn (XXXXX of XXXXX-XXXX)',
|
||||
must_contain_numbers_only: 'Mag alleen cijfers bevatten',
|
||||
must_contain_letters_only: 'Mag alleen letters bevatten',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Vul alle verplichte velden in om door te gaan met de identiteitsverificatie.',
|
||||
verify_id: 'Verifiëren ID',
|
||||
verify_id: 'ID verifiëren',
|
||||
identity_verification: 'Identiteitsverificatie',
|
||||
complete: 'Voltooien',
|
||||
fill_all_required_fields_to_complete: 'Vul alle verplichte velden in om te voltooien',
|
||||
@@ -1071,7 +1223,7 @@ const nl = {
|
||||
continue: 'Doorgaan',
|
||||
sign_now: 'Nu ondertekenen',
|
||||
type_here_: 'Typ hier...',
|
||||
optional: 'Optioneel',
|
||||
optional: 'optioneel',
|
||||
option: 'Optie',
|
||||
appears_on: 'Verschijnt op',
|
||||
page: 'Pagina',
|
||||
@@ -1095,11 +1247,11 @@ const nl = {
|
||||
powered_by: 'Aangedreven door',
|
||||
please_check_the_box_to_continue: 'Vink het vakje aan om door te gaan.',
|
||||
open_source_documents_software: 'Open source documenten software',
|
||||
verified_phone_number: 'Geverifieerd telefoonnummer',
|
||||
verified_phone_number: 'Telefoonnummer verifiëren',
|
||||
use_international_format: 'Gebruik internationaal formaat: +1xxx',
|
||||
six_digits_code: '6-cijferige code',
|
||||
change_phone_number: 'Wijzig telefoonnummer',
|
||||
sending: 'Voltooien...',
|
||||
sending: 'Verzenden...',
|
||||
resend_code: 'Code opnieuw verzenden',
|
||||
verification_code_has_been_resent: 'Verificatiecode is opnieuw verzonden via SMS',
|
||||
please_fill_all_required_fields: 'Vul alle verplichte velden in',
|
||||
@@ -1117,13 +1269,27 @@ const nl = {
|
||||
upload: 'Uploaden',
|
||||
files: 'Bestanden',
|
||||
signature_is_too_small_or_simple_please_redraw: 'De handtekening is te klein of te eenvoudig. Teken opnieuw.',
|
||||
wait_countdown_seconds: 'Wacht {countdown} seconden'
|
||||
browser_privacy_settings_block_canvas: 'De privacyinstellingen van uw browser beperken het gebruik van het tekenveld. Gebruik een andere browser of ander apparaat, of schakel de privacyinstellingen uit die canvas blokkeren om te ondertekenen.',
|
||||
wait_countdown_seconds: 'Wacht {countdown} seconden',
|
||||
enter_screen_reader_mode: 'Schermlezer-modus inschakelen'
|
||||
}
|
||||
|
||||
const ar = {
|
||||
step: 'خطوة',
|
||||
form_progress: 'تقدم النموذج',
|
||||
close: 'إغلاق',
|
||||
uploaded_files: 'الملفات المرفوعة',
|
||||
signature_drawing_area: 'منطقة رسم التوقيع. استخدم الماوس أو اللمس لرسم توقيعك.',
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'يرجى تحميل ملف صورة',
|
||||
must_be_characters_length: 'يجب أن يكون الطول {number} حرفًا',
|
||||
must_be_valid_ssn: 'يجب أن يكون SSN صالحًا (XXX-XX-XXXX)',
|
||||
must_be_valid_ein: 'يجب أن يكون EIN صالحًا (XX-XXXXXXX)',
|
||||
must_be_valid_email: 'يجب أن يكون عنوان بريد إلكتروني صالحًا',
|
||||
must_be_valid_url: 'يجب أن يكون عنوان URL صالحًا (يبدأ بـ http:// أو https://)',
|
||||
must_be_valid_zip: 'يجب أن يكون ZIP صالحًا (XXXXX أو XXXXX-XXXX)',
|
||||
must_contain_numbers_only: 'يجب أن يحتوي على أرقام فقط',
|
||||
must_contain_letters_only: 'يجب أن يحتوي على أحرف فقط',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: 'أكمل جميع الحقول المطلوبة للمتابعة في التحقق من الهوية.',
|
||||
verify_id: 'تحقق من الهوية',
|
||||
identity_verification: 'التحقق من الهوية',
|
||||
@@ -1150,7 +1316,7 @@ const ar = {
|
||||
esignature_disclosure: 'كشف التوقيع الإلكتروني',
|
||||
text: 'نص',
|
||||
signature: 'توقيع',
|
||||
initials: 'الاختصارات',
|
||||
initials: 'حروف أولى',
|
||||
date: 'تاريخ',
|
||||
number: 'رقم',
|
||||
digitally_signed_by: 'تم التوقيع رقميًا بواسطة',
|
||||
@@ -1187,9 +1353,9 @@ const ar = {
|
||||
download: 'تحميل',
|
||||
clear: 'مسح',
|
||||
redraw: 'إعادة الرسم',
|
||||
draw_initials: 'ارسم الاختصارات',
|
||||
draw_initials: 'ارسم حروف أولى',
|
||||
type_signature_here: 'اكتب التوقيع هنا',
|
||||
type_initial_here: 'اكتب الاختصارات هنا',
|
||||
type_initial_here: 'اكتب حروف أولى هنا',
|
||||
form_has_been_completed: 'تم إكمال النموذج!',
|
||||
document_has_been_signed: 'تم توقيع الوثيقة!',
|
||||
documents_have_been_signed: 'تم توقيع الوثائق!',
|
||||
@@ -1208,24 +1374,38 @@ const ar = {
|
||||
set_today: 'تعيين اليوم',
|
||||
toggle_multiline_text: 'تبديل النصوص متعددة الأسطر',
|
||||
draw_signature: 'ارسم التوقيع',
|
||||
type_initial: 'اكتب الاختصارات',
|
||||
type_initial: 'اكتب حروف أولى',
|
||||
draw: 'ارسم',
|
||||
type: 'اكتب',
|
||||
type_text: 'اكتب نصًا',
|
||||
email_has_been_sent: 'تم إرسال البريد الإلكتروني',
|
||||
processing: 'جارٍ المعالجة',
|
||||
pay_with_strip: 'الدفع بواسطة Stripe',
|
||||
pay_with_stripe: 'الدفع بواسطة Stripe',
|
||||
reupload: 'إعادة التحميل',
|
||||
upload: 'تحميل',
|
||||
files: 'الملفات',
|
||||
signature_is_too_small_or_simple_please_redraw: 'التوقيع صغير جدًا أو بسيط جدًا. يرجى إعادة رسمه.',
|
||||
wait_countdown_seconds: 'انتظر {countdown} ثانية'
|
||||
browser_privacy_settings_block_canvas: 'إعدادات الخصوصية في متصفحك تمنع استخدام لوحة الرسم. يرجى استخدام متصفح أو جهاز مختلف، أو تعطيل إعدادات الخصوصية التي تحظر canvas للتوقيع.',
|
||||
wait_countdown_seconds: 'انتظر {countdown} ثانية',
|
||||
enter_screen_reader_mode: 'تفعيل وضع قارئ الشاشة'
|
||||
}
|
||||
|
||||
const ko = {
|
||||
step: '단계',
|
||||
form_progress: '양식 진행',
|
||||
close: '닫기',
|
||||
uploaded_files: '업로드된 파일',
|
||||
signature_drawing_area: '서명 그리기 영역. 마우스 또는 터치를 사용하여 서명을 그리세요.',
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: '이미지 파일을 업로드해 주세요',
|
||||
must_be_characters_length: '{number}자여야 합니다',
|
||||
must_be_valid_ssn: '유효한 SSN이어야 합니다 (XXX-XX-XXXX)',
|
||||
must_be_valid_ein: '유효한 EIN이어야 합니다 (XX-XXXXXXX)',
|
||||
must_be_valid_email: '유효한 이메일 주소여야 합니다',
|
||||
must_be_valid_url: '유효한 URL이어야 합니다 (http:// 또는 https://로 시작)',
|
||||
must_be_valid_zip: '유효한 ZIP이어야 합니다 (XXXXX 또는 XXXXX-XXXX)',
|
||||
must_contain_numbers_only: '숫자만 포함해야 합니다',
|
||||
must_contain_letters_only: '문자만 포함해야 합니다',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: '신원 확인을 진행하려면 모든 필수 필드를 작성하십시오.',
|
||||
verify_id: '아이디 확인',
|
||||
identity_verification: '신원 확인',
|
||||
@@ -1254,10 +1434,10 @@ const ko = {
|
||||
signature: '서명',
|
||||
initials: '이니셜',
|
||||
date: '날짜',
|
||||
digitally_signed_by: '디지털 서명이 완료되었습니다',
|
||||
digitally_signed_by: '디지털 서명자:',
|
||||
reason: '이유',
|
||||
number: '숫자',
|
||||
pay: '급여',
|
||||
pay: '결제',
|
||||
image: '이미지',
|
||||
take_photo: '사진 찍기',
|
||||
file: '파일',
|
||||
@@ -1321,13 +1501,27 @@ const ko = {
|
||||
upload: '업로드',
|
||||
files: '파일',
|
||||
signature_is_too_small_or_simple_please_redraw: '서명이 너무 작거나 단순합니다. 다시 그려주세요.',
|
||||
wait_countdown_seconds: '{countdown}초 기다리세요'
|
||||
browser_privacy_settings_block_canvas: '브라우저 개인정보 보호 설정으로 인해 그리기 캔버스를 사용할 수 없습니다. 다른 브라우저나 기기를 사용하거나, 서명을 위해 캔버스를 차단하는 개인정보 보호 설정을 비활성화해 주세요.',
|
||||
wait_countdown_seconds: '{countdown}초 기다리세요',
|
||||
enter_screen_reader_mode: '스크린 리더 모드 활성화'
|
||||
}
|
||||
|
||||
const ja = {
|
||||
step: 'ステップ',
|
||||
form_progress: 'フォームの進捗',
|
||||
close: '閉じる',
|
||||
uploaded_files: 'アップロードされたファイル',
|
||||
signature_drawing_area: '署名描画エリア。マウスまたはタッチを使用して署名を描いてください。',
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: '画像ファイルをアップロードしてください',
|
||||
must_be_characters_length: '{number}文字でなければなりません',
|
||||
must_be_valid_ssn: '有効なSSNである必要があります (XXX-XX-XXXX)',
|
||||
must_be_valid_ein: '有効なEINである必要があります (XX-XXXXXXX)',
|
||||
must_be_valid_email: '有効なメールアドレスである必要があります',
|
||||
must_be_valid_url: '有効なURLである必要があります (http://またはhttps://で始まる)',
|
||||
must_be_valid_zip: '有効なZIPである必要があります (XXXXX または XXXXX-XXXX)',
|
||||
must_contain_numbers_only: '数字のみを含む必要があります',
|
||||
must_contain_letters_only: '文字のみを含む必要があります',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: '本人確認を進めるには、すべての必須項目を入力してください。',
|
||||
verify_id: '本人確認',
|
||||
identity_verification: '本人確認',
|
||||
@@ -1423,7 +1617,9 @@ const ja = {
|
||||
upload: 'アップロード',
|
||||
files: 'ファイル',
|
||||
signature_is_too_small_or_simple_please_redraw: '署名が小さすぎるか単純すぎます。もう一度描いてください。',
|
||||
wait_countdown_seconds: '{countdown} 秒お待ちください'
|
||||
browser_privacy_settings_block_canvas: 'ブラウザのプライバシー設定により、描画キャンバスの使用が制限されています。別のブラウザまたはデバイスを使用するか、署名するためにキャンバスをブロックするプライバシー設定を無効にしてください。',
|
||||
wait_countdown_seconds: '{countdown} 秒お待ちください',
|
||||
enter_screen_reader_mode: 'スクリーンリーダーモードを有効にする'
|
||||
}
|
||||
|
||||
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar, ko, ja }
|
||||
|
||||
@@ -15,16 +15,21 @@
|
||||
</template>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline btn-sm reupload-button"
|
||||
@click.prevent="remove"
|
||||
>
|
||||
<IconReload :width="16" />
|
||||
<IconReload
|
||||
:width="16"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{{ t('reupload') }}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<img
|
||||
:src="attachmentsIndex[modelValue].url"
|
||||
:alt="field.name || t('image')"
|
||||
class="h-52 border border-base-300 rounded mx-auto uploaded-image-preview"
|
||||
>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user