diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 754d06ad..40dc470d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: - name: Install Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: 3.4.2 + ruby-version: 4.0.1 - 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.1 - 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.1 - 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.1 - 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: diff --git a/.rubocop.yml b/.rubocop.yml index ffb49de1..9be296e5 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -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: @@ -84,7 +84,7 @@ RSpec/AnyInstance: Enabled: false Metrics/BlockNesting: - Max: 5 + Max: 6 Rails/I18nLocaleTexts: Enabled: false @@ -100,3 +100,16 @@ Rails/ApplicationController: Rails/Output: Enabled: false + +Rails/StrongParametersExpect: + Enabled: false + +Rails/RedirectBackOrTo: + Enabled: false + +Rails/UnknownEnv: + Environments: + - development + - test + - production + - local diff --git a/Dockerfile b/Dockerfile index 2e54bf38..52531940 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:3.4.2-alpine AS download +FROM ruby:4.0.1-alpine AS download WORKDIR /fonts @@ -9,14 +9,14 @@ RUN apk --no-cache add fontforge wget && \ wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && \ wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && \ wget https://github.com/impallari/DancingScript/raw/master/OFL.txt && \ - wget -O /model.onnx "https://github.com/docusealco/fields-detection/releases/download/2.1.0/model_704_int8.onnx" && \ + 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/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" && \ mkdir -p /pdfium-linux && \ tar -xzf pdfium-linux.tgz -C /pdfium-linux 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.1-alpine AS webpack ENV RAILS_ENV=production ENV NODE_ENV=production @@ -42,16 +42,17 @@ COPY ./app/views ./app/views RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker -FROM ruby:3.4.2-alpine AS app +FROM ruby:4.0.1-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 +ENV VIPS_MAX_COORD=15000 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 sqlite-dev libpq-dev vips-dev yaml-dev redis libheif vips-heif gcompat ttf-freefont onnxruntime && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf RUN addgroup -g 2000 docuseal && adduser -u 2000 -G docuseal -s /bin/sh -D -h /home/docuseal docuseal @@ -69,9 +70,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 && bundle install && apk del --no-cache build-base git && 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") COPY --chown=docuseal:docuseal ./bin ./bin COPY --chown=docuseal:docuseal ./app ./app @@ -81,7 +80,7 @@ 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 diff --git a/Gemfile b/Gemfile index 68f2b403..0d16d1c8 100644 --- a/Gemfile +++ b/Gemfile @@ -2,16 +2,16 @@ source 'https://rubygems.org' -ruby '~> 3.2.0' +ruby '~> 3.2.2' -gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic' +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 @@ -21,12 +21,11 @@ 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 'numo-narray-alt', require: false gem 'oj' -gem 'onnxruntime' +gem 'onnxruntime', require: false gem 'pagy' gem 'pg', require: false gem 'premailer-rails' @@ -34,17 +33,17 @@ 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', github: 'trilogy-libraries/trilogy', glob: 'contrib/ruby/*.gemspec', require: false gem 'turbo-rails' gem 'twitter_cldr', require: false gem 'tzinfo-data' diff --git a/Gemfile.lock b/Gemfile.lock index fa2f9371..8dc9a243 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,29 +1,39 @@ +GIT + remote: https://github.com/trilogy-libraries/trilogy.git + revision: 3963d490459df7a2b5bedb42424c3285f25eab22 + glob: contrib/ruby/*.gemspec + specs: + trilogy (2.10.0) + bigdecimal + 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.16) + railties + actioncable (8.1.2) + actionpack (= 8.1.2) + activesupport (= 8.1.2) 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.2) + actionpack (= 8.1.2) + activejob (= 8.1.2) + activerecord (= 8.1.2) + activestorage (= 8.1.2) + activesupport (= 8.1.2) 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.2) + actionpack (= 8.1.2) + actionview (= 8.1.2) + activejob (= 8.1.2) + activesupport (= 8.1.2) 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.2) + actionview (= 8.1.2) + activesupport (= 8.1.2) nokogiri (>= 1.8.5) rack (>= 2.2.4) rack-session (>= 1.0.1) @@ -31,55 +41,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.2) + action_text-trix (~> 2.1.15) + actionpack (= 8.1.2) + activerecord (= 8.1.2) + activestorage (= 8.1.2) + activesupport (= 8.1.2) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (8.0.2.1) - activesupport (= 8.0.2.1) + actionview (8.1.2) + activesupport (= 8.1.2) 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.2) + activesupport (= 8.1.2) 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.2) + activesupport (= 8.1.2) + activerecord (8.1.2) + activemodel (= 8.1.2) + activesupport (= 8.1.2) 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.2) + actionpack (= 8.1.2) + activejob (= 8.1.2) + activerecord (= 8.1.2) + activesupport (= 8.1.2) marcel (~> 1.0) - activesupport (8.0.2.1) + activesupport (8.1.2) 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.8.8) + public_suffix (>= 2.0.2, < 8.0) + annotaterb (4.20.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.1209.0) + aws-sdk-core (3.241.4) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -87,44 +100,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.121.0) + aws-sdk-core (~> 3, >= 3.241.4) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.208.0) - aws-sdk-core (~> 3, >= 3.234.0) + aws-sdk-s3 (1.212.0) + aws-sdk-core (~> 3, >= 3.241.4) 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.128.0) + aws-sdk-core (~> 3, >= 3.241.4) 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.21) + 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.0.1) bindex (0.8.1) - bootsnap (1.18.4) + bootsnap (1.21.1) msgpack (~> 1.2) - brakeman (7.0.0) + brakeman (7.1.2) 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,28 +145,29 @@ 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) + css_parser (1.21.1) 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) @@ -169,20 +177,20 @@ GEM railties (>= 4.1.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.3.1) + activesupport (>= 7.0, < 8.2) + devise (>= 4.0, < 5.0) + railties (>= 7.0, < 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.1) + erb_lint (0.9.0) activesupport better_html (>= 2.0.1) parser (>= 2.7.1.4) @@ -190,117 +198,101 @@ 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.0) 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.1) + 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.1) 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.3-aarch64-linux-gnu) + ffi (1.17.3-aarch64-linux-musl) + ffi (1.17.3-arm64-darwin) + ffi (1.17.3-x86_64-darwin) + ffi (1.17.3-x86_64-linux-gnu) + ffi (1.17.3-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.59.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.5.0) + google-cloud-storage (1.58.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.1) 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.5.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) + htmlentities (4.4.2) + i18n (1.14.8) concurrent-ruby (~> 1.0) - image_processing (1.13.0) - mini_magick (>= 4.9.5, < 5) + image_processing (1.14.0) + mini_magick (>= 4.9.5, < 6) ruby-vips (>= 2.0.17, < 3) - io-console (0.8.1) - irb (1.15.2) + io-console (0.8.2) + irb (1.16.0) pp (>= 0.6.0) rdoc (>= 4.0.0) reline (>= 0.4.2) jmespath (1.6.2) - json (2.15.0) - jwt (2.9.3) + json (2.18.1) + jwt (3.1.2) 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 +307,28 @@ GEM activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.24.1) + loofah (2.25.0) 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_magick (5.3.1) + logger 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.1) + 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.2) date net-protocol net-pop (0.1.2) @@ -346,44 +337,50 @@ 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.1-aarch64-linux-gnu) racc (~> 1.4) - nokogiri (1.18.9-aarch64-linux-gnu) + nokogiri (1.19.1-aarch64-linux-musl) racc (~> 1.4) - nokogiri (1.18.9-aarch64-linux-musl) + nokogiri (1.19.1-arm64-darwin) racc (~> 1.4) - nokogiri (1.18.9-arm64-darwin) + nokogiri (1.19.1-x86_64-darwin) racc (~> 1.4) - nokogiri (1.18.9-x86_64-linux-gnu) + nokogiri (1.19.1-x86_64-linux-gnu) racc (~> 1.4) - nokogiri (1.18.9-x86_64-linux-musl) + nokogiri (1.19.1-x86_64-linux-musl) racc (~> 1.4) - numo-narray (0.9.2.1) - oj (3.16.11) + numo-narray-alt (0.9.13) + oj (3.16.13) bigdecimal (>= 3.0) ostruct (>= 0.2) - onnxruntime (0.10.1) - ffi onnxruntime (0.10.1-aarch64-linux) ffi onnxruntime (0.10.1-arm64-darwin) ffi + onnxruntime (0.10.1-x86_64-darwin) + ffi onnxruntime (0.10.1-x86_64-linux) ffi - openssl (3.3.0) + openssl (4.0.0) orm_adapter (0.5.0) os (1.1.4) ostruct (0.6.3) - package_json (0.1.0) - pagy (9.3.3) + package_json (0.2.0) + pagy (43.2.8) + json + yaml parallel (1.27.0) - parser (3.3.9.0) + parser (3.3.10.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-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 @@ -393,23 +390,24 @@ GEM actionmailer (>= 3) net-smtp premailer (~> 1.7, >= 1.7.9) - pretender (0.5.0) - actionpack (>= 6.1) + pretender (0.6.0) + actionpack (>= 7.1) prettyprint (0.2.0) - prism (1.5.1) - pry (0.15.0) + prism (1.8.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.2) + puma (7.2.0) nio4r (~> 2.0) racc (1.8.1) - rack (3.2.3) + rack (3.2.5) rack-proxy (0.7.7) rack rack-session (2.1.1) @@ -417,22 +415,22 @@ GEM 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.2) + actioncable (= 8.1.2) + actionmailbox (= 8.1.2) + actionmailer (= 8.1.2) + actionpack (= 8.1.2) + actiontext (= 8.1.2) + actionview (= 8.1.2) + activejob (= 8.1.2) + activemodel (= 8.1.2) + activerecord (= 8.1.2) + activestorage (= 8.1.2) + activesupport (= 8.1.2) bundler (>= 1.15.0) - railties (= 8.0.2.1) + railties (= 8.1.2) rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest @@ -440,30 +438,28 @@ GEM rails-html-sanitizer (1.6.2) loofah (~> 2.21) 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.2) + actionpack (= 8.1.2) + activesupport (= 8.1.2) 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.3.1) + rdoc (7.1.0) erb psych (>= 4.0.0) - redis-client (0.23.0) + tsort + redis-client (0.26.4) 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 +467,35 @@ GEM uber (< 0.2.0) request_store (1.7.0) rack (>= 1.4) - responders (3.1.1) - actionpack (>= 5.2) - railties (>= 5.2) + responders (3.2.0) + actionpack (>= 7.0) + railties (>= 7.0) retriable (3.1.2) 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.7) 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-rails (8.0.2) + actionpack (>= 7.2) + activesupport (>= 7.2) + railties (>= 7.2) 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-support (3.13.6) + rubocop (1.82.1) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -507,86 +503,90 @@ 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.48.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.47.1) + rubocop-ast (1.49.0) 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) + shakapacker (9.5.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.0) + connection_pool (>= 3.0.0) + json (>= 2.16.0) + logger (>= 1.7.0) + rack (>= 3.2.0) + redis-client (>= 0.26.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.0-aarch64-linux-gnu) + sqlite3 (2.9.0-aarch64-linux-musl) + sqlite3 (2.9.0-arm64-darwin) + sqlite3 (2.9.0-x86_64-darwin) + sqlite3 (2.9.0-x86_64-linux-gnu) + sqlite3 (2.9.0-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.7) + thor (1.5.0) + timeout (0.6.0) trailblazer-option (0.1.2) - turbo-rails (2.0.11) - actionpack (>= 6.0.0) - railties (>= 6.0.0) - twitter_cldr (6.12.1) + tsort (0.2.0) + turbo-rails (2.0.21) + 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.2025.3) 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) @@ -596,24 +596,25 @@ GEM activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) - webmock (3.24.0) + webmock (3.26.1) 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.4) PLATFORMS aarch64-linux aarch64-linux-musl arm64-darwin - ruby + x86_64-darwin-24 x86_64-linux x86_64-linux-musl @@ -622,7 +623,7 @@ DEPENDENCIES arabic-letter-connector aws-sdk-s3 aws-sdk-secretsmanager - azure-storage-blob + azure-blob better_html bootsnap brakeman @@ -649,8 +650,7 @@ DEPENDENCIES jwt letter_opener_web lograge - mysql2 - numo-narray + numo-narray-alt oj onnxruntime pagy @@ -662,7 +662,6 @@ DEPENDENCIES rack rails rails-i18n - rails_autolink rotp rouge rqrcode @@ -678,6 +677,7 @@ DEPENDENCIES simplecov sqlite3 strip_attributes + trilogy! turbo-rails twitter_cldr tzinfo-data @@ -685,7 +685,7 @@ DEPENDENCIES webmock RUBY VERSION - ruby 3.2.2p53 + ruby 3.2.2p53 BUNDLED WITH - 2.5.3 + 4.0.3 diff --git a/LICENSE_ADDITIONAL_TERMS b/LICENSE_ADDITIONAL_TERMS new file mode 100644 index 00000000..cac2a50b --- /dev/null +++ b/LICENSE_ADDITIONAL_TERMS @@ -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. diff --git a/README.md b/README.md index 9753da2a..d97620c0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

- DocuSeal + DocuSeal
DocuSeal @@ -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 diff --git a/app/controllers/account_custom_fields_controller.rb b/app/controllers/account_custom_fields_controller.rb new file mode 100644 index 00000000..74e27ea0 --- /dev/null +++ b/app/controllers/account_custom_fields_controller.rb @@ -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 diff --git a/app/controllers/api/api_base_controller.rb b/app/controllers/api/api_base_controller.rb index 70b992ed..ff01fc8f 100644 --- a/app/controllers/api/api_base_controller.rb +++ b/app/controllers/api/api_base_controller.rb @@ -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 diff --git a/app/controllers/api/submissions_controller.rb b/app/controllers/api/submissions_controller.rb index 93c2190f..77b4e615 100644 --- a/app/controllers/api/submissions_controller.rb +++ b/app/controllers/api/submissions_controller.rb @@ -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, diff --git a/app/controllers/api/submitters_controller.rb b/app/controllers/api/submitters_controller.rb index e56eb8b8..f28bf5e1 100644 --- a/app/controllers/api/submitters_controller.rb +++ b/app/controllers/api/submitters_controller.rb @@ -34,6 +34,7 @@ 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 @@ -60,7 +61,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 +82,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 diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb index c8211b7f..c3f1dd42 100644 --- a/app/controllers/api/templates_controller.rb +++ b/app/controllers/api/templates_controller.rb @@ -107,7 +107,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, diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 191f1dc8..592c006d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -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 diff --git a/app/controllers/submissions_download_controller.rb b/app/controllers/submissions_download_controller.rb index 5ae8b2b0..b334eec7 100644 --- a/app/controllers/submissions_download_controller.rb +++ b/app/controllers/submissions_download_controller.rb @@ -28,20 +28,18 @@ class SubmissionsDownloadController < ApplicationController 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) + if !signature_valid && !current_user_submitter?(last_submitter) + return head :not_found unless Submitters::AuthorizedForForm.call(@submitter, current_user, request) - return head :not_found + 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' - url = build_combined_url(@submitter) - - if url - render json: [url] - else - head :not_found - end + respond_with_combined(last_submitter) else render json: build_urls(last_submitter) end @@ -68,27 +66,15 @@ class SubmissionsDownloadController < ApplicationController private - def admin_download?(last_submitter) - # No valid signature link = download from app (e.g. submissions page) → serve unredacted - !@signature_valid - end - def current_user_submitter?(submitter) - current_user && current_user.account.submitters.exists?(id: submitter.id) + current_user && current_ability.can?(:read, submitter) end def build_urls(submitter) filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id, key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value - attachments = if admin_download?(submitter) - Submissions::GenerateResultAttachments.call(submitter, for_admin: true) - Submitters.select_admin_attachments_for_download(submitter) - else - Submitters.select_attachments_for_download(submitter) - end - - attachments.map do |attachment| + Submitters.select_attachments_for_download(submitter).map do |attachment| ActiveStorage::Blob.proxy_url( attachment.blob, expires_at: FILES_TTL.from_now.to_i, @@ -107,7 +93,7 @@ class SubmissionsDownloadController < ApplicationController filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id, key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value - ActiveStorage::Blob.proxy_url( + ActiveStorage::Blob.proxy_path( attachment.blob, expires_at: FILES_TTL.from_now.to_i, filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format) diff --git a/app/controllers/submissions_export_controller.rb b/app/controllers/submissions_export_controller.rb index bfbb7359..d199e585 100644 --- a/app/controllers/submissions_export_controller.rb +++ b/app/controllers/submissions_export_controller.rb @@ -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) diff --git a/app/controllers/submit_form_controller.rb b/app/controllers/submit_form_controller.rb index f723aa32..5a6a0ae5 100644 --- a/app/controllers/submit_form_controller.rb +++ b/app/controllers/submit_form_controller.rb @@ -9,15 +9,15 @@ class SubmitFormController < ApplicationController before_action :load_submitter, only: %i[show update 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 +48,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 @@ -84,7 +84,9 @@ class SubmitFormController < ApplicationController 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 @@ -92,10 +94,7 @@ class SubmitFormController < ApplicationController 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 @@ -117,12 +116,4 @@ class SubmitFormController < ApplicationController 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 diff --git a/app/controllers/submit_form_decline_controller.rb b/app/controllers/submit_form_decline_controller.rb index 918903fe..a8f969c3 100644 --- a/app/controllers/submit_form_decline_controller.rb +++ b/app/controllers/submit_form_decline_controller.rb @@ -11,7 +11,9 @@ class SubmitFormDeclineController < 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) ApplicationRecord.transaction do submitter.update!(declined_at: Time.current) diff --git a/app/controllers/submit_form_download_controller.rb b/app/controllers/submit_form_download_controller.rb index 1aba46ca..e5c04ecb 100644 --- a/app/controllers/submit_form_download_controller.rb +++ b/app/controllers/submit_form_download_controller.rb @@ -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 diff --git a/app/controllers/submit_form_draw_signature_controller.rb b/app/controllers/submit_form_draw_signature_controller.rb index 773eb9e7..5ba141c1 100644 --- a/app/controllers/submit_form_draw_signature_controller.rb +++ b/app/controllers/submit_form_draw_signature_controller.rb @@ -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 diff --git a/app/controllers/submit_form_invite_controller.rb b/app/controllers/submit_form_invite_controller.rb index ab1f26c3..413e2b9a 100644 --- a/app/controllers/submit_form_invite_controller.rb +++ b/app/controllers/submit_form_invite_controller.rb @@ -19,7 +19,9 @@ 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]) + + 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 @@ -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') diff --git a/app/controllers/submit_form_values_controller.rb b/app/controllers/submit_form_values_controller.rb index e1a6b9ab..affd37ba 100644 --- a/app/controllers/submit_form_values_controller.rb +++ b/app/controllers/submit_form_values_controller.rb @@ -7,10 +7,12 @@ class SubmitFormValuesController < ApplicationController def index 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? || + return render json: {} if submitter.completed_at? || + submitter.declined_at? || + submitter.submission.template&.archived_at? || submitter.submission.archived_at? || - submitter.submission.expired? + 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? diff --git a/app/controllers/submitters_autocomplete_controller.rb b/app/controllers/submitters_autocomplete_controller.rb index efbb110a..22599e58 100644 --- a/app/controllers/submitters_autocomplete_controller.rb +++ b/app/controllers/submitters_autocomplete_controller.rb @@ -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}%" diff --git a/app/controllers/template_documents_controller.rb b/app/controllers/template_documents_controller.rb index b29a18f6..51fc4111 100644 --- a/app/controllers/template_documents_controller.rb +++ b/app/controllers/template_documents_controller.rb @@ -6,7 +6,7 @@ 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 diff --git a/app/controllers/templates_clone_controller.rb b/app/controllers/templates_clone_controller.rb index 802a97e0..51a4de28 100644 --- a/app/controllers/templates_clone_controller.rb +++ b/app/controllers/templates_clone_controller.rb @@ -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 diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb index b5fb1df5..46c3e742 100644 --- a/app/controllers/templates_controller.rb +++ b/app/controllers/templates_controller.rb @@ -97,7 +97,8 @@ class TemplatesController < ApplicationController :name, { schema: [[:attachment_uuid, :google_drive_file_id, :name, { conditions: [%i[field_uuid value action operation]] }]], - submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid optional_invite_by_uuid email order]], + submitters: [%i[name uuid is_requester linked_to_uuid invite_via_field_uuid + invite_by_uuid optional_invite_by_uuid email order]], fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value, :title, :description, :prefillable, diff --git a/app/controllers/templates_detect_fields_controller.rb b/app/controllers/templates_detect_fields_controller.rb index 337e8008..56b06cd5 100644 --- a/app/controllers/templates_detect_fields_controller.rb +++ b/app/controllers/templates_detect_fields_controller.rb @@ -13,7 +13,7 @@ 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) diff --git a/app/controllers/templates_recipients_controller.rb b/app/controllers/templates_recipients_controller.rb index 17a4bbb8..64fa58b2 100644 --- a/app/controllers/templates_recipients_controller.rb +++ b/app/controllers/templates_recipients_controller.rb @@ -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) diff --git a/app/controllers/templates_uploads_controller.rb b/app/controllers/templates_uploads_controller.rb index e8c00aea..ddee4e90 100644 --- a/app/controllers/templates_uploads_controller.rb +++ b/app/controllers/templates_uploads_controller.rb @@ -56,7 +56,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? diff --git a/app/controllers/timestamp_server_controller.rb b/app/controllers/timestamp_server_controller.rb index d664a976..40db1544 100644 --- a/app/controllers/timestamp_server_controller.rb +++ b/app/controllers/timestamp_server_controller.rb @@ -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, diff --git a/app/controllers/webhook_settings_controller.rb b/app/controllers/webhook_settings_controller.rb index 482116ef..f9245f49 100644 --- a/app/controllers/webhook_settings_controller.rb +++ b/app/controllers/webhook_settings_controller.rb @@ -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,7 +26,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)) end def new; end diff --git a/app/javascript/application.js b/app/javascript/application.js index ca83685e..7d9150bd 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -40,6 +40,7 @@ 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' @@ -131,6 +132,7 @@ 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) @@ -153,17 +155,24 @@ 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)), backgroundColor: '#FFFFFF', + template, + customFields: reactive(JSON.parse(this.dataset.customFields || '[]')), + 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', 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', diff --git a/app/javascript/application.scss b/app/javascript/application.scss index 253cd2fa..4d2c6412 100644 --- a/app/javascript/application.scss +++ b/app/javascript/application.scss @@ -202,3 +202,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; +} diff --git a/app/javascript/draw.js b/app/javascript/draw.js index a4936e8d..3b95b5ea 100644 --- a/app/javascript/draw.js +++ b/app/javascript/draw.js @@ -4,14 +4,25 @@ import { isValidSignatureCanvas } from './submission_form/validate_signature' window.customElements.define('draw-signature', class extends HTMLElement { connectedCallback () { - const scale = 3 + this.setCanvasSize() - this.canvas.width = this.canvas.parentNode.clientWidth * scale - this.canvas.height = this.canvas.parentNode.clientHeight * scale + this.pad = new SignaturePad(this.canvas) - this.canvas.getContext('2d').scale(scale, scale) + this.resizeObserver = new ResizeObserver(() => { + requestAnimationFrame(() => { + if (!this.canvas) return - this.pad = new SignaturePad(this.canvas) + 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 +68,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) { + 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' diff --git a/app/javascript/elements/field_condition.js b/app/javascript/elements/field_condition.js index 609d9955..b99a8bb3 100644 --- a/app/javascript/elements/field_condition.js +++ b/app/javascript/elements/field_condition.js @@ -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)) diff --git a/app/javascript/elements/markdown_editor.js b/app/javascript/elements/markdown_editor.js new file mode 100644 index 00000000..b9937598 --- /dev/null +++ b/app/javascript/elements/markdown_editor.js @@ -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, '
'), + 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() + } + } +})) diff --git a/app/javascript/elements/signature_form.js b/app/javascript/elements/signature_form.js index 55020081..5fc9af48 100644 --- a/app/javascript/elements/signature_form.js +++ b/app/javascript/elements/signature_form.js @@ -5,16 +5,27 @@ export default targetable(class extends HTMLElement { static [target.static] = ['canvas', 'input', 'clear', 'button'] async connectedCallback () { - const scale = 3 + const { default: SignaturePad } = await import('signature_pad') + + this.setCanvasSize() - this.canvas.width = this.canvas.parentNode.clientWidth * scale - this.canvas.height = this.canvas.parentNode.clientHeight * scale + this.pad = new SignaturePad(this.canvas) - this.canvas.getContext('2d').scale(scale, scale) + this.resizeObserver = new ResizeObserver(() => { + requestAnimationFrame(() => { + if (!this.canvas) return - const { default: SignaturePad } = await import('signature_pad') + const { width, height } = this.canvas - this.pad = new SignaturePad(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 +58,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) { + 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) + } + } }) diff --git a/app/javascript/elements/toggle_attribute.js b/app/javascript/elements/toggle_attribute.js index e9ee3075..5ff6b7c3 100644 --- a/app/javascript/elements/toggle_attribute.js +++ b/app/javascript/elements/toggle_attribute.js @@ -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 } diff --git a/app/javascript/elements/toggle_classes.js b/app/javascript/elements/toggle_classes.js index ab96f293..332ac84f 100644 --- a/app/javascript/elements/toggle_classes.js +++ b/app/javascript/elements/toggle_classes.js @@ -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) + } }) }) } diff --git a/app/javascript/elements/toggle_cookies.js b/app/javascript/elements/toggle_cookies.js index 788c3d8e..e377609a 100644 --- a/app/javascript/elements/toggle_cookies.js +++ b/app/javascript/elements/toggle_cookies.js @@ -15,6 +15,6 @@ export default class extends HTMLElement { } get button () { - return this.querySelector('button') + return this.querySelector('button, label') } } diff --git a/app/javascript/submission_form/area.vue b/app/javascript/submission_form/area.vue index b4887476..4723cb1a 100644 --- a/app/javascript/submission_form/area.vue +++ b/app/javascript/submission_form/area.vue @@ -187,8 +187,8 @@
({}) + }, completedButton: { type: Object, required: false, @@ -214,19 +219,14 @@ export default { download () { this.isDownloading = true - fetch(this.baseUrl + `/submitters/${this.submitterSlug}/signed_download_url`) - .then(async (response) => { - if (!response.ok) { - throw new Error('failed') - } - const { url } = await response.json() - return fetch(url) - }) - .then(async (response) => { - if (response.ok) { - const urls = await response.json() - const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent) - const isSafariIos = isMobileSafariIos || /iPhone|iPad|iPod/i.test(navigator.userAgent) + 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) + const isSafariIos = isMobileSafariIos || /iPhone|iPad|iPod/i.test(navigator.userAgent) if (isSafariIos && urls.length > 1) { this.downloadSafariIos(urls) diff --git a/app/javascript/submission_form/date_step.vue b/app/javascript/submission_form/date_step.vue index 08319dd8..5645b8b0 100644 --- a/app/javascript/submission_form/date_step.vue +++ b/app/javascript/submission_form/date_step.vue @@ -42,7 +42,7 @@
-
+
{ if (obj == null) return true @@ -658,6 +680,11 @@ export default { required: false, default: () => [] }, + fetchOptions: { + type: Object, + required: false, + default: () => ({}) + }, optionalInviteSubmitters: { type: Array, required: false, @@ -678,6 +705,11 @@ export default { required: false, default: false }, + onlyRequiredFields: { + type: Boolean, + required: false, + default: false + }, requireSigningReason: { type: Boolean, required: false, @@ -914,10 +946,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 } @@ -938,7 +972,13 @@ export default { 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 +1024,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 { @@ -1025,7 +1067,9 @@ export default { return { ...this.readonlyConditionalFieldValues, ...redactValues } }, 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 = [] @@ -1080,10 +1124,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 { @@ -1095,7 +1141,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) => { @@ -1157,15 +1205,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) } @@ -1225,27 +1275,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 } @@ -1255,6 +1313,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) @@ -1294,6 +1368,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 @@ -1355,9 +1436,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 { @@ -1407,7 +1488,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) => { @@ -1440,7 +1522,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 @@ -1448,7 +1530,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 } @@ -1458,7 +1540,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') @@ -1502,7 +1584,7 @@ export default { return Promise.reject(new Error(data.error)) } - const nextStep = (isLastStep && emptyRequiredField) || (forceComplete ? null : this.stepFields[submitStep + 1]) + const nextStep = (isLastStep && emptyRequiredField) || (forceComplete ? null : this.findNextStep(submitStepIndex)) if (nextStep) { if (this.alwaysMinimize) { diff --git a/app/javascript/submission_form/i18n.js b/app/javascript/submission_form/i18n.js index 70567992..b9da958d 100644 --- a/app/javascript/submission_form/i18n.js +++ b/app/javascript/submission_form/i18n.js @@ -1,7 +1,7 @@ const en = { 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', 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,6 +97,7 @@ const en = { upload: 'Upload', files: 'Files', signature_is_too_small_or_simple_please_redraw: 'Signature is too small or simple. Please redraw.', + 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' } @@ -199,6 +200,7 @@ 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.', + 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' } @@ -301,6 +303,7 @@ const it = { upload: 'Carica', files: 'File', signature_is_too_small_or_simple_please_redraw: 'La firma è troppo piccola o semplice. Ridisegnala, per favore.', + 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' } @@ -403,6 +406,7 @@ 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.', + 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' } @@ -505,6 +509,7 @@ 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.', + 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' } @@ -607,6 +612,7 @@ const pl = { 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.', + 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' } @@ -709,6 +715,7 @@ const uk = { upload: 'Завантажити', files: 'Файли', signature_is_too_small_or_simple_please_redraw: 'Підпис занадто маленький або надто простий. Будь ласка, перемалюйте.', + browser_privacy_settings_block_canvas: 'Налаштування конфіденційності вашого браузера блокують використання полотна для малювання. Будь ласка, скористайтеся іншим браузером або пристроєм, або вимкніть налаштування конфіденційності, що блокують canvas, щоб підписати.', wait_countdown_seconds: 'Зачекайте {countdown} секунд' } @@ -811,6 +818,7 @@ const cs = { 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.', + 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' } @@ -913,6 +921,7 @@ const pt = { upload: 'Carregar', files: 'Arquivos', signature_is_too_small_or_simple_please_redraw: 'A assinatura é muito pequena ou simples. Por favor, redesenhe.', + 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' } @@ -1015,6 +1024,7 @@ const he = { upload: 'העלאה', files: 'קבצים', signature_is_too_small_or_simple_please_redraw: 'החתימה קטנה או פשוטה מדי. אנא חתום מחדש.', + browser_privacy_settings_block_canvas: 'הגדרות הפרטיות של הדפדפן שלך מגבילות את השימוש באזור הציור. אנא השתמש בדפדפן או מכשיר אחר, או בטל את הגדרות הפרטיות החוסמות canvas כדי לחתום.', wait_countdown_seconds: 'המתן {countdown} שניות' } @@ -1117,6 +1127,7 @@ const nl = { upload: 'Uploaden', files: 'Bestanden', signature_is_too_small_or_simple_please_redraw: 'De handtekening is te klein of te eenvoudig. Teken opnieuw.', + 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' } @@ -1219,6 +1230,7 @@ const ar = { upload: 'تحميل', files: 'الملفات', signature_is_too_small_or_simple_please_redraw: 'التوقيع صغير جدًا أو بسيط جدًا. يرجى إعادة رسمه.', + browser_privacy_settings_block_canvas: 'إعدادات الخصوصية في متصفحك تمنع استخدام لوحة الرسم. يرجى استخدام متصفح أو جهاز مختلف، أو تعطيل إعدادات الخصوصية التي تحظر canvas للتوقيع.', wait_countdown_seconds: 'انتظر {countdown} ثانية' } @@ -1321,6 +1333,7 @@ const ko = { upload: '업로드', files: '파일', signature_is_too_small_or_simple_please_redraw: '서명이 너무 작거나 단순합니다. 다시 그려주세요.', + browser_privacy_settings_block_canvas: '브라우저 개인정보 보호 설정으로 인해 그리기 캔버스를 사용할 수 없습니다. 다른 브라우저나 기기를 사용하거나, 서명을 위해 캔버스를 차단하는 개인정보 보호 설정을 비활성화해 주세요.', wait_countdown_seconds: '{countdown}초 기다리세요' } @@ -1423,6 +1436,7 @@ const ja = { upload: 'アップロード', files: 'ファイル', signature_is_too_small_or_simple_please_redraw: '署名が小さすぎるか単純すぎます。もう一度描いてください。', + browser_privacy_settings_block_canvas: 'ブラウザのプライバシー設定により、描画キャンバスの使用が制限されています。別のブラウザまたはデバイスを使用するか、署名するためにキャンバスをブロックするプライバシー設定を無効にしてください。', wait_countdown_seconds: '{countdown} 秒お待ちください' } diff --git a/app/javascript/submission_form/initials_step.vue b/app/javascript/submission_form/initials_step.vue index 2dede0e4..814b9c12 100644 --- a/app/javascript/submission_form/initials_step.vue +++ b/app/javascript/submission_form/initials_step.vue @@ -150,6 +150,7 @@ diff --git a/app/javascript/template_builder/custom_field.vue b/app/javascript/template_builder/custom_field.vue new file mode 100644 index 00000000..21317261 --- /dev/null +++ b/app/javascript/template_builder/custom_field.vue @@ -0,0 +1,271 @@ + + + diff --git a/app/javascript/template_builder/description_modal.vue b/app/javascript/template_builder/description_modal.vue index 946d961e..b387733f 100644 --- a/app/javascript/template_builder/description_modal.vue +++ b/app/javascript/template_builder/description_modal.vue @@ -9,7 +9,7 @@