Compare commits

..

23 Commits

Author SHA1 Message Date
Alex Turchyn 911e55ccc3 Merge from docusealco/wip 2026-04-13 13:25:16 +03:00
Pete Matsyburka 41fedfcc40 show verification error 2026-04-12 13:30:13 +03:00
Pete Matsyburka 2f9fc95af3 remove unused module 2026-04-11 18:13:25 +03:00
Pete Matsyburka 11b0b16ca7 prefill signature client side 2026-04-11 18:13:25 +03:00
Pete Matsyburka f8cb7ffdab remove mathjs 2026-04-11 18:13:23 +03:00
Pete Matsyburka 8b59c0aaa0 adjust normalize values 2026-04-10 15:20:35 +03:00
Pete Matsyburka bb2fb7a0c2 refactor download 2026-04-10 12:54:48 +03:00
Pete Matsyburka 8f8b36617a fix first party download from preview 2026-04-10 11:15:16 +03:00
Pete Matsyburka ff53436fd4 fix dynamic editor layout 2026-04-08 17:18:38 +03:00
Pete Matsyburka cf09a4f733 update gem 2026-04-08 12:24:31 +03:00
Pete Matsyburka 2303c21cea download users csv 2026-04-08 12:24:22 +03:00
Pete Matsyburka 89e797b95f update message 2026-04-08 10:29:45 +03:00
Pete Matsyburka 20375c3a42 update gem 2026-04-08 08:40:41 +03:00
Pete Matsyburka 339ceda18d upload attachment if not completed 2026-04-07 18:20:16 +03:00
Pete Matsyburka 97ce32fd52 set attachment name 2026-04-07 16:34:15 +03:00
Alex Turchyn 8c4cbd86f7 Merge from docusealco/wip 2026-04-06 15:22:38 +03:00
Pete Matsyburka eaedaa96bb fix rubocop 2026-04-02 13:28:12 +03:00
Pete Matsyburka bd7ad5fce8 update gems 2026-04-02 13:28:12 +03:00
Pete Matsyburka 759dac1b70 update schema 2026-04-01 19:07:57 +03:00
Pete Matsyburka 38577c6235 fix dynamic doc fields assignment 2026-04-01 16:08:49 +03:00
Pete Matsyburka b24fff0f55 fix rtl stamp 2026-04-01 11:10:09 +03:00
Pete Matsyburka 9e0efeb092 fix unicode in stamp 2026-04-01 10:29:31 +03:00
Pete Matsyburka 41152f90fc fix erblint 2026-03-30 15:18:34 +03:00
66 changed files with 1557 additions and 583 deletions
+9
View File
@@ -51,6 +51,15 @@ Style/NumericPredicate:
Style/MinMaxComparison:
Enabled: false
Style/EmptyClassDefinition:
Enabled: false
Style/OneClassPerFile:
Enabled: false
Layout/MultilineMethodCallIndentation:
Enabled: false
Naming/PredicateMethod:
Enabled: false
+7 -10
View File
@@ -2,20 +2,17 @@ FROM ruby:4.0.1-alpine AS download
WORKDIR /fonts
RUN apk --no-cache add fontforge wget && \
RUN apk --no-cache add wget && \
wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && \
wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && \
wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && \
wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && \
wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && \
wget https://github.com/impallari/DancingScript/raw/master/OFL.txt && \
wget https://raw.githubusercontent.com/impallari/DancingScript/master/OFL.txt && \
wget https://raw.githubusercontent.com/notofonts/noto-fonts/refs/heads/main/LICENSE && \
wget -O /model.onnx "https://github.com/docusealco/fields-detection/releases/download/2.0.0/model_704_int8.onnx" && \
wget -O pdfium-linux.tgz "https://github.com/bblanchon/pdfium-binaries/releases/latest/download/pdfium-linux-musl-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" && \
mkdir -p /pdfium-linux && \
tar -xzf pdfium-linux.tgz -C /pdfium-linux
RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")'
FROM ruby:4.0.1-alpine AS webpack
ENV RAILS_ENV=production
@@ -51,7 +48,7 @@ ENV OPENSSL_CONF=/etc/openssl_legacy.cnf
WORKDIR /app
RUN apk add --no-cache libpq vips redis vips-heif ttf-freefont onnxruntime && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
RUN apk add --no-cache libpq vips redis vips-heif fontconfig onnxruntime
RUN addgroup -g 2000 docuseal && adduser -u 2000 -G docuseal -s /bin/sh -D -h /home/docuseal docuseal
@@ -82,14 +79,14 @@ COPY --chown=docuseal:docuseal ./tmp ./tmp
COPY --chown=docuseal:docuseal LICENSE LICENSE_ADDITIONAL_TERMS README.md Rakefile config.ru .version ./
COPY --chown=docuseal:docuseal .version ./public/version
COPY --chown=docuseal:docuseal --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
COPY --from=download /fonts/FreeSans.ttf /usr/share/fonts/freefont
COPY --chown=docuseal:docuseal --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts/LICENSE /fonts/
COPY --from=download /pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so
COPY --from=download /pdfium-linux/licenses/pdfium.txt /usr/lib/libpdfium-LICENSE.txt
COPY --chown=docuseal:docuseal --from=download /model.onnx /app/tmp/model.onnx
COPY --chown=docuseal:docuseal --from=webpack /app/public/packs ./public/packs
RUN ln -s /fonts /app/public/fonts && \
RUN mkdir -p /app/public/fonts && ln -s /fonts/DancingScript-Regular.otf /app/public/fonts/ && \
mkdir -p /usr/share/fonts/noto && ln -s /fonts/GoNotoKurrent-Regular.ttf /usr/share/fonts/noto/ && ln -s /fonts/GoNotoKurrent-Bold.ttf /usr/share/fonts/noto/ && fc-cache -f && \
bundle exec bootsnap precompile -j 1 --gemfile app/ lib/ && \
chown -R docuseal:docuseal /app/tmp/cache
+67 -67
View File
@@ -1,7 +1,7 @@
GEM
remote: https://rubygems.org/
specs:
action_text-trix (2.1.17)
action_text-trix (2.1.18)
railties
actioncable (8.1.3)
actionpack (= 8.1.3)
@@ -75,16 +75,16 @@ GEM
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
addressable (2.8.8)
addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0)
annotaterb (4.20.0)
annotaterb (4.22.0)
activerecord (>= 6.0.0)
activesupport (>= 6.0.0)
arabic-letter-connector (0.1.1)
ast (2.4.3)
aws-eventstream (1.4.0)
aws-partitions (1.1209.0)
aws-sdk-core (3.241.4)
aws-partitions (1.1233.0)
aws-sdk-core (3.244.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
@@ -92,15 +92,15 @@ GEM
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.121.0)
aws-sdk-core (~> 3, >= 3.241.4)
aws-sdk-kms (1.123.0)
aws-sdk-core (~> 3, >= 3.244.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.212.0)
aws-sdk-core (~> 3, >= 3.241.4)
aws-sdk-s3 (1.218.0)
aws-sdk-core (~> 3, >= 3.244.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sdk-secretsmanager (1.128.0)
aws-sdk-core (~> 3, >= 3.241.4)
aws-sdk-secretsmanager (1.129.0)
aws-sdk-core (~> 3, >= 3.244.0)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
@@ -116,11 +116,11 @@ GEM
erubi (~> 1.4)
parser (>= 2.4)
smart_properties
bigdecimal (4.0.1)
bigdecimal (4.1.0)
bindex (0.8.1)
bootsnap (1.21.1)
bootsnap (1.23.0)
msgpack (~> 1.2)
brakeman (7.1.2)
brakeman (8.0.4)
racc
builder (3.3.0)
bullet (8.1.0)
@@ -193,7 +193,7 @@ GEM
factory_bot_rails (6.5.1)
factory_bot (~> 6.5)
railties (>= 6.1.0)
faker (3.6.0)
faker (3.6.1)
i18n (>= 1.8.11, < 2)
faraday (2.14.1)
faraday-net_http (>= 2.0, < 3.5)
@@ -203,17 +203,17 @@ GEM
faraday (>= 1, < 3)
faraday-net_http (3.4.2)
net-http (~> 0.5)
ferrum (0.17.1)
ferrum (0.17.2)
addressable (~> 2.5)
base64 (~> 0.2)
concurrent-ruby (~> 1.1)
webrick (~> 1.7)
websocket-driver (~> 0.7)
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-linux-gnu)
ffi (1.17.3-x86_64-linux-musl)
ffi (1.17.4-aarch64-linux-gnu)
ffi (1.17.4-aarch64-linux-musl)
ffi (1.17.4-arm64-darwin)
ffi (1.17.4-x86_64-linux-gnu)
ffi (1.17.4-x86_64-linux-musl)
foreman (0.90.0)
thor (~> 1.4)
geom2d (0.4.1)
@@ -229,7 +229,7 @@ GEM
retriable (~> 3.1)
google-apis-iamcredentials_v1 (0.26.0)
google-apis-core (>= 0.15.0, < 2.a)
google-apis-storage_v1 (0.59.0)
google-apis-storage_v1 (0.61.0)
google-apis-core (>= 0.15.0, < 2.a)
google-cloud-core (1.8.0)
google-cloud-env (>= 1.0, < 3.a)
@@ -237,8 +237,8 @@ GEM
google-cloud-env (2.3.1)
base64 (~> 0.2)
faraday (>= 1.0, < 3.a)
google-cloud-errors (1.5.0)
google-cloud-storage (1.58.0)
google-cloud-errors (1.6.0)
google-cloud-storage (1.59.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-core (>= 0.18, < 2)
@@ -248,7 +248,7 @@ GEM
googleauth (~> 1.9)
mini_mime (~> 1.0)
google-logging-utils (0.2.0)
googleauth (1.16.1)
googleauth (1.16.2)
faraday (>= 1.0, < 3.a)
google-cloud-env (~> 2.2)
google-logging-utils (~> 0.1)
@@ -257,7 +257,7 @@ GEM
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
hashdiff (1.2.1)
hexapdf (1.5.0)
hexapdf (1.6.0)
cmdparse (~> 3.0, >= 3.0.3)
geom2d (~> 0.4, >= 0.4.1)
openssl (>= 2.2.1)
@@ -274,7 +274,7 @@ GEM
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jmespath (1.6.2)
json (2.19.2)
json (2.19.3)
jwt (3.1.2)
base64
language_server-protocol (3.17.0.5)
@@ -311,7 +311,7 @@ GEM
mini_magick (5.3.1)
logger
mini_mime (1.1.5)
minitest (6.0.2)
minitest (6.0.3)
drb (~> 2.0)
prism (~> 1.5)
msgpack (1.8.0)
@@ -339,7 +339,7 @@ GEM
nokogiri (1.19.2-x86_64-linux-musl)
racc (~> 1.4)
numo-narray-alt (0.10.3)
oj (3.16.13)
oj (3.16.16)
bigdecimal (>= 3.0)
ostruct (>= 0.2)
onnxruntime (0.10.1-aarch64-linux)
@@ -348,16 +348,17 @@ GEM
ffi
onnxruntime (0.10.1-x86_64-linux)
ffi
openssl (4.0.0)
openssl (4.0.1)
orm_adapter (0.5.0)
os (1.1.4)
ostruct (0.6.3)
package_json (0.2.0)
pagy (43.2.8)
pagy (43.4.4)
json
uri
yaml
parallel (1.27.0)
parser (3.3.10.1)
parallel (1.28.0)
parser (3.3.11.1)
ast (~> 2.4.1)
racc
pg (1.6.3-aarch64-linux)
@@ -367,8 +368,8 @@ GEM
pg (1.6.3-x86_64-linux-musl)
pp (0.6.3)
prettyprint
pretender (0.6.0)
actionpack (>= 7.1)
pretender (1.0.0)
actionpack (>= 7.2)
prettyprint (0.2.0)
prism (1.9.0)
pry (0.16.0)
@@ -380,14 +381,14 @@ GEM
psych (5.3.1)
date
stringio
public_suffix (7.0.2)
public_suffix (7.0.5)
puma (7.2.0)
nio4r (~> 2.0)
racc (1.8.1)
rack (3.2.5)
rack (3.2.6)
rack-proxy (0.7.7)
rack
rack-session (2.1.1)
rack-session (2.1.2)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.2.0)
@@ -433,7 +434,7 @@ GEM
erb
psych (>= 4.0.0)
tsort
redis-client (0.26.4)
redis-client (0.28.0)
connection_pool
regexp_parser (2.11.3)
reline (0.6.3)
@@ -447,7 +448,7 @@ GEM
responders (3.2.0)
actionpack (>= 7.0)
railties (>= 7.0)
retriable (3.1.2)
retriable (3.4.1)
rexml (3.4.4)
rotp (6.3.0)
rouge (4.7.0)
@@ -460,19 +461,19 @@ GEM
rspec-expectations (3.13.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.7)
rspec-mocks (3.13.8)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-rails (8.0.2)
rspec-rails (8.0.4)
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.6)
rubocop (1.82.1)
rspec-core (>= 3.13.0, < 5.0.0)
rspec-expectations (>= 3.13.0, < 5.0.0)
rspec-mocks (>= 3.13.0, < 5.0.0)
rspec-support (>= 3.13.0, < 5.0.0)
rspec-support (3.13.7)
rubocop (1.86.0)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
@@ -480,10 +481,10 @@ GEM
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.48.0, < 2.0)
rubocop-ast (>= 1.49.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.49.0)
rubocop-ast (1.49.1)
parser (>= 3.3.7.2)
prism (~> 1.7)
rubocop-performance (1.26.1)
@@ -508,14 +509,14 @@ GEM
rubyzip (>= 3.2.2)
rubyzip (3.2.2)
securerandom (0.4.1)
semantic_range (3.1.0)
shakapacker (9.5.0)
semantic_range (3.1.1)
shakapacker (9.7.0)
activesupport (>= 5.2)
package_json
rack-proxy (>= 0.6.1)
railties (>= 5.2)
semantic_range (>= 2.3.0)
sidekiq (8.1.0)
sidekiq (8.1.2)
connection_pool (>= 3.0.0)
json (>= 2.16.0)
logger (>= 1.7.0)
@@ -533,22 +534,22 @@ GEM
simplecov-html (0.13.2)
simplecov_json_formatter (0.1.4)
smart_properties (1.17.0)
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-linux-gnu)
sqlite3 (2.9.0-x86_64-linux-musl)
sqlite3 (2.9.2-aarch64-linux-gnu)
sqlite3 (2.9.2-aarch64-linux-musl)
sqlite3 (2.9.2-arm64-darwin)
sqlite3 (2.9.2-x86_64-linux-gnu)
sqlite3 (2.9.2-x86_64-linux-musl)
stringio (3.2.0)
strip_attributes (2.0.1)
activemodel (>= 3.0, < 9.0)
strscan (3.1.7)
strscan (3.1.8)
thor (1.5.0)
timeout (0.6.1)
trailblazer-option (0.1.2)
trilogy (2.10.0)
trilogy (2.12.2)
bigdecimal
tsort (0.2.0)
turbo-rails (2.0.21)
turbo-rails (2.0.23)
actionpack (>= 7.1.0)
railties (>= 7.1.0)
twitter_cldr (6.14.0)
@@ -558,7 +559,7 @@ GEM
tzinfo
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
tzinfo-data (1.2025.3)
tzinfo-data (1.2026.1)
tzinfo (>= 1.0.0)
uber (0.1.0)
unicode-display_width (3.2.0)
@@ -569,12 +570,11 @@ GEM
useragent (0.16.11)
warden (1.2.9)
rack (>= 2.0.9)
web-console (4.2.1)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
web-console (4.3.0)
actionview (>= 8.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
webmock (3.26.1)
railties (>= 8.0.0)
webmock (3.26.2)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
+3 -1
View File
@@ -53,7 +53,9 @@ class AccountsController < ApplicationController
# rubocop:disable Layout/LineLength
render turbo_stream: turbo_stream.replace(
:account_delete_button,
html: helpers.tag.p(I18n.t('your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account'))
html: helpers.tag.p(I18n.t('your_account_will_be_permanently_deleted_within_2_months_please_contact_us_if_you_want_to_keep_your_account')) +
helpers.tag.br +
helpers.tag.p(I18n.t('your_email_address_has_been_released_immediately_you_can_now_be_added_to_your_company_team_without_waiting_for_the_deletion_period_to_end'))
)
# rubocop:enable Layout/LineLength
end
@@ -10,6 +10,12 @@ module Api
def create
submitter = Submitter.find_by!(slug: params[:submitter_slug])
unless can_upload?(submitter)
Rollbar.error("Can't upload: #{submitter.id}") if defined?(Rollbar)
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content
end
if params[:type].in?(%w[initials signature])
image = Vips::Image.new_from_file(params[:file].path)
@@ -40,6 +46,14 @@ module Api
render json: { error: e.message }, status: :unprocessable_content
end
def can_upload?(submitter)
!submitter.declined_at? &&
!submitter.completed_at? &&
!submitter.submission.archived_at? &&
!submitter.submission.expired? &&
!submitter.submission.template&.archived_at?
end
def build_new_cookie_signatures_json(submitter, attachment)
values =
begin
@@ -156,7 +156,7 @@ module Api
params:)
else
submissions_attrs, attachments =
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_params, template)
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_params, template, purpose: :api)
submissions = Submissions.create_from_submitters(
template:,
+2 -1
View File
@@ -50,7 +50,8 @@ module Api
normalized_params, new_attachments = Submissions::NormalizeParamUtils.normalize_submitter_params!(
submitter_params.merge(role:),
@submitter.template || Template.new(submitters: submission.template_submitters, account: @submitter.account),
for_submitter: @submitter
for_submitter: @submitter,
purpose: :api
)
Submissions::CreateFromSubmitters.maybe_set_template_fields(submission, [normalized_params],
@@ -9,7 +9,7 @@ module Api
ActiveRecord::Associations::Preloader.new(
records: [@template],
associations: [schema_documents: :preview_images_attachments]
associations: [{ schema_documents: :preview_images_attachments }]
).call
cloned_template = Templates::Clone.call(
+1 -1
View File
@@ -3,7 +3,7 @@
class EsignSettingsController < ApplicationController
DEFAULT_CERT_NAME = 'DocuSeal Self-Host Autogenerated'
CertFormRecord = Struct.new(:name, :file, :password, keyword_init: true) do
CertFormRecord = Struct.new(:name, :file, :password) do
include ActiveModel::Validations
def to_key
+1 -1
View File
@@ -26,7 +26,7 @@ class SubmissionsController < ApplicationController
unless @submission.submitters.all?(&:completed_at?)
ActiveRecord::Associations::Preloader.new(
records: [@submission],
associations: [submitters: :start_form_submission_events]
associations: [{ submitters: :start_form_submission_events }]
).call
end
@@ -9,7 +9,7 @@ class SubmissionsDebugController < ApplicationController
def index
@submitter = Submitter.preload({ attachments_attachments: :blob },
submission: { template: { documents_attachments: :blob } })
.find_by(slug: params[:submitter_slug])
.find_by(slug: params[:submit_form_slug])
respond_to do |f|
f.html do
@@ -1,92 +1,25 @@
# frozen_string_literal: true
class SubmissionsDownloadController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
TTL = 40.minutes
FILES_TTL = 5.minutes
load_and_authorize_resource :submission
def index
@submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
signature_valid =
if @submitter&.slug == params[:submitter_slug]
true
else
@submitter = nil
end
@submitter ||= Submitter.find_by!(slug: params[:submitter_slug])
Submissions::EnsureResultGenerated.call(@submitter)
last_submitter = @submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
last_submitter = @submission.submitters.where.not(completed_at: nil).order(:completed_at).last
return head :not_found unless last_submitter
Submissions::EnsureResultGenerated.call(last_submitter)
if !signature_valid && !current_user_submitter?(last_submitter)
return head :not_found unless Submitters::AuthorizedForForm.call(@submitter, current_user, request)
if last_submitter.completed_at < TTL.ago
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
return head :not_found
end
end
if params[:combined] == 'true'
respond_with_combined(last_submitter)
url = Submitters.build_combined_url(last_submitter)
if url
render json: [url]
else
head :not_found
end
else
render json: build_urls(last_submitter)
render json: Submitters.build_document_urls(last_submitter)
end
end
private
def respond_with_combined(submitter)
url = build_combined_url(submitter)
if url
render json: [url]
else
head :not_found
end
end
def current_user_submitter?(submitter)
current_user && current_ability.can?(:read, submitter)
end
def build_urls(submitter)
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
Submitters.select_attachments_for_download(submitter).map do |attachment|
ActiveStorage::Blob.proxy_path(
attachment.blob,
expires_at: FILES_TTL.from_now.to_i,
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
)
end
end
def build_combined_url(submitter)
return if submitter.submission.submitters.exists?(completed_at: nil)
return if submitter.submission.submitters.order(:completed_at).last != submitter
attachment = submitter.submission.combined_document_attachment
attachment ||= Submissions::EnsureCombinedGenerated.call(submitter)
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
ActiveStorage::Blob.proxy_path(
attachment.blob,
expires_at: FILES_TTL.from_now.to_i,
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
)
end
end
@@ -10,13 +10,15 @@ class SubmissionsPreviewController < ApplicationController
TTL = 40.minutes
def show
submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
@sig_submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
signature_valid =
if submitter && submitter.submission.slug == params[:slug]
@submission = submitter.submission
if @sig_submitter && @sig_submitter.submission.slug == params[:slug]
@submission = @sig_submitter.submission
true
else
@sig_submitter = nil
end
@submission ||= Submission.find_by!(slug: params[:slug])
@@ -36,7 +38,7 @@ class SubmissionsPreviewController < ApplicationController
@submission = Submissions.preload_with_pages(@submission)
render 'submissions/show', layout: 'plain'
render 'submissions/show', layout: 'plain', locals: { is_preview: true }
end
def completed
@@ -0,0 +1,64 @@
# frozen_string_literal: true
class SubmissionsPreviewDownloadController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
TTL = 40.minutes
def index
@submission = Submission.find_by!(slug: params[:submission_slug] || params[:submissions_preview_slug])
last_submitter = @submission.submitters.where.not(completed_at: nil).order(:completed_at).last
return head :not_found unless last_submitter
Submissions::EnsureResultGenerated.call(last_submitter)
unless current_user_submission?(@submission)
if use_2fa?(@submission)
Rollbar.info("2FA download error: #{last_submitter.id}") if defined?(Rollbar)
return head :not_found
end
if last_submitter.completed_at < TTL.ago
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
return head :not_found
end
end
if params[:combined] == 'true'
respond_with_combined(last_submitter)
else
render json: Submitters.build_document_urls(last_submitter)
end
end
private
def respond_with_combined(submitter)
url = Submitters.build_combined_url(submitter)
if url
render json: [url]
else
head :not_found
end
end
def current_user_submission?(submission)
current_user && current_ability.can?(:read, submission)
end
def use_2fa?(submission)
return true if submission.submitters.any? do |e|
e.preferences['require_phone_2fa'] || e.preferences['require_email_2fa']
end
return true if submission.template&.preferences&.dig('require_phone_2fa')
return true if submission.template&.preferences&.dig('require_email_2fa')
false
end
end
@@ -0,0 +1,70 @@
# frozen_string_literal: true
class SubmitFormCompletedDownloadController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
TTL = 40.minutes
FILES_TTL = 5.minutes
def index
@submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
signature_valid =
if @submitter&.slug == submitter_slug
true
else
@submitter = nil
end
@submitter ||= Submitter.find_by!(slug: submitter_slug)
Submissions::EnsureResultGenerated.call(@submitter)
last_submitter = @submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
return head :not_found unless last_submitter
Submissions::EnsureResultGenerated.call(last_submitter)
if !signature_valid && !current_user_submitter?(last_submitter)
unless Submitters::AuthorizedForForm.call(@submitter, current_user, request)
Rollbar.info("2FA download error: #{last_submitter.id}") if defined?(Rollbar)
return head :not_found
end
if last_submitter.completed_at < TTL.ago
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
return head :not_found
end
end
if params[:combined] == 'true'
respond_with_combined(last_submitter)
else
render json: Submitters.build_document_urls(last_submitter)
end
end
private
def submitter_slug
params[:submit_form_slug] || params[:submitter_slug] || params[:submitter_id]
end
def respond_with_combined(submitter)
url = Submitters.build_combined_url(submitter)
if url
render json: [url]
else
head :not_found
end
end
def current_user_submitter?(submitter)
current_user && current_ability.can?(:read, submitter)
end
end
@@ -9,7 +9,7 @@ class SubmitFormDownloadController < ApplicationController
def index
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return redirect_to submitter_download_index_path(@submitter.slug) if @submitter.completed_at?
return redirect_to submit_form_documents_path(@submitter.slug) if @submitter.completed_at?
return head :unprocessable_content if @submitter.declined_at? ||
@submitter.submission.archived_at? ||
@@ -0,0 +1,11 @@
# frozen_string_literal: true
class SubmittersDownloadController < ApplicationController
load_and_authorize_resource :submitter
def index
Submissions::EnsureResultGenerated.call(@submitter)
render json: Submitters.build_document_urls(@submitter)
end
end
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class SubmittersSendEmailController < ApplicationController
load_and_authorize_resource :submitter, id_param: :submitter_slug, find_by: :slug
load_and_authorize_resource :submitter
def create
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
@@ -8,7 +8,7 @@ class TemplatesCloneAndReplaceController < ApplicationController
ActiveRecord::Associations::Preloader.new(
records: [@template],
associations: [schema_documents: :preview_images_attachments]
associations: [{ schema_documents: :preview_images_attachments }]
).call
cloned_template = Templates::Clone.call(@template, author: current_user)
@@ -12,7 +12,7 @@ class TemplatesCloneController < ApplicationController
def create
ActiveRecord::Associations::Preloader.new(
records: [@base_template],
associations: [schema_documents: :preview_images_attachments]
associations: [{ schema_documents: :preview_images_attachments }]
).call
@template = Templates::Clone.call(@base_template, author: current_user,
+1 -1
View File
@@ -29,7 +29,7 @@ class TemplatesController < ApplicationController
def edit
ActiveRecord::Associations::Preloader.new(
records: [@template],
associations: [schema_documents: [:blob, { preview_images_attachments: :blob }]]
associations: [{ schema_documents: [:blob, { preview_images_attachments: :blob }] }]
).call
@template_data =
@@ -30,7 +30,7 @@ class TemplatesDebugController < ApplicationController
ActiveRecord::Associations::Preloader.new(
records: [@template],
associations: [schema_documents: { preview_images_attachments: :blob }]
associations: [{ schema_documents: { preview_images_attachments: :blob } }]
).call
@template_data =
@@ -58,8 +58,8 @@ class TemplatesPreferencesController < ApplicationController
default_expire_at_duration shared_link_2fa default_expire_at request_email_enabled
completed_notification_email_subject completed_notification_email_body
completed_notification_email_enabled completed_notification_email_attach_audit] +
[completed_message: %i[title body],
submitters: [%i[uuid request_email_subject request_email_body]], link_form_fields: []]
[{ completed_message: %i[title body],
submitters: [%i[uuid request_email_subject request_email_body]], link_form_fields: [] }]
).tap do |attrs|
attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1'
@@ -6,7 +6,7 @@ class TemplatesPreviewController < ApplicationController
def show
ActiveRecord::Associations::Preloader.new(
records: [@template],
associations: [schema_documents: { preview_images_attachments: :blob }]
associations: [{ schema_documents: { preview_images_attachments: :blob } }]
).call
@template_data =
+13 -1
View File
@@ -16,7 +16,19 @@ class UsersController < ApplicationController
@users.active.where.not(role: 'integration')
end
@pagy, @users = pagy(@users.preload(account: :account_accesses).where(account: current_account).order(id: :desc))
@users = @users.preload(account: :account_accesses).where(account: current_account).order(id: :desc)
respond_to do |format|
format.html do
@pagy, @users = pagy(@users)
end
if current_ability.can?(:manage, current_account)
format.csv do
send_data Users.generate_csv(@users), filename: "users-#{Time.current.iso8601}.csv", type: 'text/csv'
end
end
end
end
def new; end
+1
View File
@@ -31,6 +31,7 @@ safeRegisterElement('submission-form', class extends HTMLElement {
isDemo: this.dataset.isDemo === 'true',
attribution: this.dataset.attribution !== 'false',
scrollPadding: this.dataset.scrollPadding || '-80px',
signatureText: this.dataset.signatureText,
language: this.dataset.language,
dryRun: this.dataset.dryRun === 'true',
expand: ['true', 'false'].includes(this.dataset.expand) ? this.dataset.expand === 'true' : null,
@@ -0,0 +1,874 @@
/*
* Portions of this file are derived from the Dentaku gem
* https://github.com/rubysolo/dentaku
*
* MIT License
*
* Copyright (c) 2012 Solomon White
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Modifications and JavaScript port
* Copyright (c) 2026 DocuSeal, LLC
*/
class CalculatorError extends Error { constructor (msg) { super(msg); this.name = 'CalculatorError' } }
class ParseError extends CalculatorError { constructor (msg) { super(msg); this.name = 'ParseError' } }
class TokenizerError extends CalculatorError { constructor (msg) { super(msg); this.name = 'TokenizerError' } }
class ArgumentError extends CalculatorError { constructor (msg) { super(msg); this.name = 'ArgumentError' } }
class ZeroDivisionError extends CalculatorError { constructor () { super('divided by 0'); this.name = 'ZeroDivisionError' } }
class UnboundVariableError extends CalculatorError {
constructor (vars) {
super('no value provided for variables: ' + vars.join(', '))
this.name = 'UnboundVariableError'
this.unbound = vars
}
}
class Token {
constructor (category, value, raw) {
this.category = category
this.value = value
this.raw = raw == null ? String(value) : String(raw)
}
is (cat) { return this.category === cat }
}
const OPERATOR_NAMES = {
'^': 'pow',
'+': 'add',
'-': 'subtract',
'*': 'multiply',
'/': 'divide',
'%': 'mod',
'|': 'bitor',
'&': 'bitand',
'<<': 'bitshiftleft',
'>>': 'bitshiftright'
}
const COMPARATOR_NAMES = {
'<=': 'le',
'>=': 'ge',
'!=': 'ne',
'<>': 'ne',
'<': 'lt',
'>': 'gt',
'==': 'eq',
'=': 'eq'
}
const GROUPING_NAMES = { '(': 'open', ')': 'close', ',': 'comma' }
function buildScanners (caseSensitive) {
return [
(s) => {
const m = /^\s+/.exec(s)
return m ? { tokens: [], length: m[0].length } : null
},
(s) => {
const m = /^null\b/i.exec(s)
return m ? { tokens: [new Token('null', null, m[0])], length: m[0].length } : null
},
(s) => {
const m = /^((?:\d+(?:\.\d+)?|\.\d+)(?:[eE][+-]?\d+)?)\b/.exec(s)
if (!m) return null
return { tokens: [new Token('numeric', Number(m[0]), m[0])], length: m[0].length }
},
(s) => {
const m = /^0x[0-9a-f]+\b/i.exec(s)
if (!m) return null
return { tokens: [new Token('numeric', parseInt(m[0].slice(2), 16), m[0])], length: m[0].length }
},
(s) => {
const m = /^"([^"]*)"/.exec(s)
if (!m) return null
return { tokens: [new Token('string', m[1], m[0])], length: m[0].length }
},
(s) => {
const m = /^'([^']*)'/.exec(s)
if (!m) return null
return { tokens: [new Token('string', m[1], m[0])], length: m[0].length }
},
(s, last) => {
if (s[0] !== '-') return null
const ok = last == null ||
last.is('operator') ||
last.is('comparator') ||
last.is('combinator') ||
last.value === 'open' ||
last.value === 'comma' ||
last.value === 'array_start'
if (!ok) return null
return { tokens: [new Token('operator', 'negate', '-')], length: 1 }
},
(s) => {
const m = /^(and|or|&&|\|\|)\s/i.exec(s)
if (!m) return null
const raw = m[1].toLowerCase()
const name = raw === '&&' ? 'and' : raw === '||' ? 'or' : raw
return { tokens: [new Token('combinator', name, m[0])], length: m[0].length }
},
(s) => {
const m = /^(<<|>>|\^|\+|-|\*|\/|%|\||&)/.exec(s)
if (!m) return null
return { tokens: [new Token('operator', OPERATOR_NAMES[m[0]], m[0])], length: m[0].length }
},
(s) => {
const m = /^(\(|\)|,)/.exec(s)
if (!m) return null
return { tokens: [new Token('grouping', GROUPING_NAMES[m[0]], m[0])], length: m[0].length }
},
(s) => {
if (s[0] === '{') return { tokens: [new Token('array', 'array_start', '{')], length: 1 }
if (s[0] === '}') return { tokens: [new Token('array', 'array_end', '}')], length: 1 }
return null
},
(s) => {
const m = /^(<=|>=|!=|<>|==|=|<|>)/.exec(s)
if (!m) return null
return { tokens: [new Token('comparator', COMPARATOR_NAMES[m[0]], m[0])], length: m[0].length }
},
(s) => {
const m = /^(true|false)\b/i.exec(s)
if (!m) return null
return { tokens: [new Token('logical', m[0].toLowerCase() === 'true', m[0])], length: m[0].length }
},
(s) => {
const m = /^(\w+!?)\s*\(/.exec(s)
if (!m) return null
const name = m[1].toLowerCase()
return {
tokens: [
new Token('function', name, m[1]),
new Token('grouping', 'open', '(')
],
length: m[0].length
}
},
(s) => {
const m = /^[A-Za-z_][\w.]*\b/.exec(s)
if (!m) return null
const value = caseSensitive ? m[0] : m[0].toLowerCase()
return { tokens: [new Token('identifier', value, m[0])], length: m[0].length }
},
(s) => {
const m = /^`([^`]*)`/.exec(s)
if (!m) return null
return { tokens: [new Token('identifier', m[1], m[0])], length: m[0].length }
}
]
}
function tokenize (input, options) {
options = options || {}
let remaining = String(input).replace(/\/\*[\s\S]*?\*\//g, '')
const scanners = buildScanners(options.caseSensitive === true)
const tokens = []
let nesting = 0
while (remaining.length > 0) {
let matched = false
for (let i = 0; i < scanners.length; i++) {
const result = scanners[i](remaining, tokens.length ? tokens[tokens.length - 1] : null)
if (result) {
for (const tok of result.tokens) {
if (tok.category === 'grouping' && tok.value === 'open') nesting++
if (tok.category === 'grouping' && tok.value === 'close') {
nesting--
if (nesting < 0) throw new TokenizerError('too many closing parentheses')
}
tokens.push(tok)
}
remaining = remaining.slice(result.length)
matched = true
break
}
}
if (!matched) throw new TokenizerError("parse error at: '" + remaining + "'")
}
if (nesting > 0) throw new TokenizerError('too many opening parentheses')
return tokens
}
function uniq (arr) {
const seen = new Set()
const out = []
for (const x of arr) { if (!seen.has(x)) { seen.add(x); out.push(x) } }
return out
}
function toNumber (val) {
if (typeof val === 'number') return val
if (typeof val === 'boolean') return val ? 1 : 0
if (val == null) throw new ArgumentError("'" + val + "' is not coercible to numeric")
if (typeof val === 'string') {
if (val.length === 0) throw new ArgumentError("'' is not coercible to numeric")
const n = Number(val)
if (isNaN(n)) throw new ArgumentError("'" + val + "' is not coercible to numeric")
return n
}
throw new ArgumentError("'" + val + "' is not coercible to numeric")
}
class Node {
static precedence = 0
static arity = null
static rightAssociative = false
static resolveClass () { return this }
static minParamCount () { return this.arity }
static maxParamCount () { return this.arity }
value () { return null }
dependencies () { return [] }
get type () { return null }
}
class NilNode extends Node { value () { return null } }
class Numeric extends Node {
constructor (token) { super(); this.v = token.value }
value () { return this.v }
get type () { return 'numeric' }
}
class Logical extends Node {
constructor (token) { super(); this.v = token.value }
value () { return this.v }
get type () { return 'logical' }
}
class StringNode extends Node {
constructor (token) { super(); this.v = token.value }
value () { return this.v }
get type () { return 'string' }
}
class Identifier extends Node {
constructor (token) { super(); this.ident = token.value }
value (ctx) {
if (ctx && (this.ident in ctx)) {
const v = ctx[this.ident]
if (v instanceof Node) return v.value(ctx)
if (typeof v === 'function') return v()
return v
}
throw new UnboundVariableError([this.ident])
}
dependencies (ctx) {
if (ctx && (this.ident in ctx)) {
const v = ctx[this.ident]
return v instanceof Node ? v.dependencies(ctx) : []
}
return [this.ident]
}
}
class Grouping extends Node {
constructor (node) { super(); this.node = node }
value (ctx) { return this.node.value(ctx) }
get type () { return this.node.type }
dependencies (ctx) { return this.node.dependencies(ctx) }
}
class Operation extends Node {
constructor (left, right) { super(); this.left = left; this.right = right }
dependencies (ctx) {
const l = this.left ? this.left.dependencies(ctx) : []
const r = this.right ? this.right.dependencies(ctx) : []
return uniq(l.concat(r))
}
}
class Arithmetic extends Operation { get type () { return 'numeric' } }
class Addition extends Arithmetic {
static precedence = 10
value (ctx) {
const l = this.left.value(ctx)
const r = this.right.value(ctx)
if (typeof l === 'string' || typeof r === 'string') return String(l) + String(r)
return toNumber(l) + toNumber(r)
}
}
class Subtraction extends Arithmetic {
static precedence = 10
value (ctx) { return toNumber(this.left.value(ctx)) - toNumber(this.right.value(ctx)) }
}
class Multiplication extends Arithmetic {
static precedence = 20
value (ctx) { return toNumber(this.left.value(ctx)) * toNumber(this.right.value(ctx)) }
}
class Division extends Arithmetic {
static precedence = 20
value (ctx) {
const r = toNumber(this.right.value(ctx))
if (r === 0) throw new ZeroDivisionError()
return toNumber(this.left.value(ctx)) / r
}
}
class Modulo extends Arithmetic {
static arity = 2
static precedence = 20
static resolveClass (nextTok) {
if (!nextTok) return Percentage
if (nextTok.category === 'operator') return Percentage
if (nextTok.category === 'comparator') return Percentage
if (nextTok.category === 'combinator') return Percentage
if (nextTok.category === 'grouping' && (nextTok.value === 'close' || nextTok.value === 'comma')) return Percentage
if (nextTok.category === 'array' && nextTok.value === 'array_end') return Percentage
return Modulo
}
value (ctx) {
const r = toNumber(this.right.value(ctx))
if (r === 0) throw new ZeroDivisionError()
return toNumber(this.left.value(ctx)) % r
}
}
class Percentage extends Arithmetic {
static arity = 1
static precedence = 30
constructor (child) { super(child, null) }
value (ctx) { return toNumber(this.left.value(ctx)) * 0.01 }
dependencies (ctx) { return this.left.dependencies(ctx) }
}
class Exponentiation extends Arithmetic {
static precedence = 30
static rightAssociative = true
value (ctx) { return Math.pow(toNumber(this.left.value(ctx)), toNumber(this.right.value(ctx))) }
}
class Negation extends Arithmetic {
static arity = 1
static precedence = 40
static rightAssociative = true
constructor (node) { super(node, null) }
value (ctx) { return -toNumber(this.left.value(ctx)) }
dependencies (ctx) { return this.left.dependencies(ctx) }
}
class BitwiseOr extends Operation {
static precedence = 10
value (ctx) { return toNumber(this.left.value(ctx)) | toNumber(this.right.value(ctx)) }
get type () { return 'numeric' }
}
class BitwiseAnd extends Operation {
static precedence = 10
value (ctx) { return toNumber(this.left.value(ctx)) & toNumber(this.right.value(ctx)) }
get type () { return 'numeric' }
}
class BitwiseShiftLeft extends Operation {
static precedence = 10
value (ctx) { return toNumber(this.left.value(ctx)) << toNumber(this.right.value(ctx)) }
get type () { return 'numeric' }
}
class BitwiseShiftRight extends Operation {
static precedence = 10
value (ctx) { return toNumber(this.left.value(ctx)) >> toNumber(this.right.value(ctx)) }
get type () { return 'numeric' }
}
function cmpCast (v) {
if (typeof v === 'string' && /^-?\d*\.?\d+$/.test(v)) return Number(v)
return v
}
class Comparator extends Operation {
static precedence = 5
get type () { return 'logical' }
}
class LessThan extends Comparator { value (ctx) { return cmpCast(this.left.value(ctx)) < cmpCast(this.right.value(ctx)) } }
class LessThanOrEqual extends Comparator { value (ctx) { return cmpCast(this.left.value(ctx)) <= cmpCast(this.right.value(ctx)) } }
class GreaterThan extends Comparator { value (ctx) { return cmpCast(this.left.value(ctx)) > cmpCast(this.right.value(ctx)) } }
class GreaterThanOrEqual extends Comparator { value (ctx) { return cmpCast(this.left.value(ctx)) >= cmpCast(this.right.value(ctx)) } }
class NotEqual extends Comparator {
value (ctx) {
const l = cmpCast(this.left.value(ctx))
const r = cmpCast(this.right.value(ctx))
return l !== r
}
}
class Equal extends Comparator {
value (ctx) {
const l = cmpCast(this.left.value(ctx))
const r = cmpCast(this.right.value(ctx))
return l === r
}
}
class Combinator extends Operation {
static precedence = 0
get type () { return 'logical' }
}
class And extends Combinator { value (ctx) { return Boolean(this.left.value(ctx)) && Boolean(this.right.value(ctx)) } }
class Or extends Combinator { value (ctx) { return Boolean(this.left.value(ctx)) || Boolean(this.right.value(ctx)) } }
class ArrayNode extends Node {
static arity = null
static minParamCount () { return 0 }
static maxParamCount () { return Infinity }
constructor (...items) {
super()
this.items = items
}
value (ctx) { return this.items.map((i) => i.value(ctx)) }
dependencies (ctx) {
return uniq([].concat.apply([], this.items.map((i) => i.dependencies(ctx))))
}
get type () { return 'array' }
}
class FunctionNode extends Node {
static arity = null
static minParamCount () { return 0 }
static maxParamCount () { return Infinity }
constructor (...args) {
super()
this.args = args
}
dependencies (ctx) {
return uniq([].concat.apply([], this.args.map((a) => a.dependencies(ctx))))
}
}
const FUNCTION_REGISTRY = Object.create(null)
function registerFunctionClass (name, cls) { FUNCTION_REGISTRY[name.toLowerCase()] = cls }
function getFunctionClass (name) { return FUNCTION_REGISTRY[name.toLowerCase()] }
function defineFunction (name, spec) {
const min = spec.min == null ? 0 : spec.min
const max = spec.max == null ? Infinity : spec.max
const call = spec.call
const type = spec.type || 'numeric'
class F extends FunctionNode {
static arity = (min === max) ? min : null
static minParamCount () { return min }
static maxParamCount () { return max }
value (ctx) {
const vals = this.args.map((a) => a.value(ctx))
return call(vals, ctx)
}
get type () { return type }
}
registerFunctionClass(name, F)
return F
}
function flattenNumeric (vals) {
const out = [];
(function walk (v) {
if (Array.isArray(v)) v.forEach(walk)
else out.push(toNumber(v))
})(vals)
return out
}
defineFunction('abs', { min: 1, max: 1, call: (v) => Math.abs(toNumber(v[0])) })
defineFunction('min', { min: 1, call: (v) => Math.min.apply(null, flattenNumeric(v)) })
defineFunction('max', { min: 1, call: (v) => Math.max.apply(null, flattenNumeric(v)) })
defineFunction('sum', { min: 1, call: (v) => flattenNumeric(v).reduce((a, b) => a + b, 0) })
defineFunction('avg', {
min: 1,
call: (v) => {
const nums = flattenNumeric(v)
if (nums.length === 0) return 0
return nums.reduce((a, b) => a + b, 0) / nums.length
}
})
defineFunction('count', {
min: 1,
max: 1,
call: (v) => Array.isArray(v[0]) ? v[0].length : (v[0] == null ? 0 : 1)
})
defineFunction('round', {
min: 1,
max: 2,
call: (v) => {
const n = toNumber(v[0])
const p = v.length > 1 ? toNumber(v[1]) : 0
const f = Math.pow(10, p)
return Math.round(n * f) / f
}
})
defineFunction('roundup', {
min: 1,
max: 2,
call: (v) => {
const n = toNumber(v[0])
const p = v.length > 1 ? toNumber(v[1]) : 0
const f = Math.pow(10, p)
return (n < 0 ? -Math.floor(-n * f) : Math.ceil(n * f)) / f
}
})
defineFunction('rounddown', {
min: 1,
max: 2,
call: (v) => {
const n = toNumber(v[0])
const p = v.length > 1 ? toNumber(v[1]) : 0
const f = Math.pow(10, p)
return (n < 0 ? -Math.ceil(-n * f) : Math.floor(n * f)) / f
}
})
const MATH_FNS = {
sqrt: Math.sqrt,
cbrt: Math.cbrt,
sin: Math.sin,
cos: Math.cos,
tan: Math.tan,
asin: Math.asin,
acos: Math.acos,
atan: Math.atan,
sinh: Math.sinh,
cosh: Math.cosh,
tanh: Math.tanh,
exp: Math.exp,
log: Math.log,
log2: Math.log2,
log10: Math.log10,
floor: Math.floor,
ceil: Math.ceil
}
Object.keys(MATH_FNS).forEach((name) => {
const fn = MATH_FNS[name]
defineFunction(name, { min: 1, max: 1, call: (v) => fn(toNumber(v[0])) })
})
defineFunction('atan2', { min: 2, max: 2, call: (v) => Math.atan2(toNumber(v[0]), toNumber(v[1])) })
defineFunction('hypot', { min: 1, call: (v) => Math.hypot.apply(null, flattenNumeric(v)) })
defineFunction('pow', { min: 2, max: 2, call: (v) => Math.pow(toNumber(v[0]), toNumber(v[1])) })
defineFunction('pi', { min: 0, max: 0, call: () => Math.PI })
defineFunction('e', { min: 0, max: 0, call: () => Math.E })
defineFunction('not', { min: 1, max: 1, type: 'logical', call: (v) => !v[0] })
defineFunction('and', { min: 1, type: 'logical', call: (v) => v.every((x) => Boolean(x)) })
defineFunction('or', { min: 1, type: 'logical', call: (v) => v.some((x) => Boolean(x)) })
defineFunction('xor', {
min: 2,
type: 'logical',
call: (v) => v.reduce((acc, x) => acc !== Boolean(x), false)
})
class IfFunction extends FunctionNode {
static arity = 3
static minParamCount () { return 3 }
static maxParamCount () { return 3 }
value (ctx) {
return this.args[0].value(ctx) ? this.args[1].value(ctx) : this.args[2].value(ctx)
}
get type () { return this.args[1].type }
dependencies (ctx) {
try {
return this.args[0].value(ctx) ? this.args[1].dependencies(ctx) : this.args[2].dependencies(ctx)
} catch (e) {
return uniq([].concat.apply([], this.args.map((a) => a.dependencies(ctx))))
}
}
}
registerFunctionClass('if', IfFunction)
defineFunction('length', { min: 1, max: 1, call: (v) => v[0] == null ? 0 : String(v[0]).length })
defineFunction('upcase', { min: 1, max: 1, type: 'string', call: (v) => String(v[0]).toUpperCase() })
defineFunction('downcase', { min: 1, max: 1, type: 'string', call: (v) => String(v[0]).toLowerCase() })
defineFunction('concat', { min: 1, type: 'string', call: (v) => v.map((x) => String(x)).join('') })
defineFunction('left', {
min: 2,
max: 2,
type: 'string',
call: (v) => String(v[0]).slice(0, toNumber(v[1]))
})
defineFunction('right', {
min: 2,
max: 2,
type: 'string',
call: (v) => {
const n = toNumber(v[1])
return n === 0 ? '' : String(v[0]).slice(-n)
}
})
defineFunction('mid', {
min: 3,
max: 3,
type: 'string',
call: (v) => {
const s = String(v[0])
const start = toNumber(v[1]) - 1
const len = toNumber(v[2])
return s.slice(start, start + len)
}
})
defineFunction('trim', { min: 1, max: 1, type: 'string', call: (v) => String(v[0]).trim() })
defineFunction('contains', {
min: 2,
max: 2,
type: 'logical',
call: (v) => {
if (Array.isArray(v[0])) return v[0].indexOf(v[1]) !== -1
return String(v[0]).indexOf(String(v[1])) !== -1
}
})
const OP_CLASSES = {
add: Addition,
subtract: Subtraction,
multiply: Multiplication,
divide: Division,
pow: Exponentiation,
negate: Negation,
mod: Modulo,
bitor: BitwiseOr,
bitand: BitwiseAnd,
bitshiftleft: BitwiseShiftLeft,
bitshiftright: BitwiseShiftRight,
lt: LessThan,
gt: GreaterThan,
le: LessThanOrEqual,
ge: GreaterThanOrEqual,
ne: NotEqual,
eq: Equal,
and: And,
or: Or
}
function isOperationClass (cls) {
return cls && (cls.prototype instanceof Operation)
}
function parse (tokens) {
if (!tokens || tokens.length === 0) return new NilNode()
const output = []
const operations = []
const arities = []
const skip = new Set()
function consume (count) {
if (count == null) count = 2
const op = operations.pop()
if (!op) throw new ParseError('invalid statement')
const outputSize = output.length
const opMin = (typeof op.minParamCount === 'function') ? op.minParamCount() : null
const opMax = (typeof op.maxParamCount === 'function') ? op.maxParamCount() : null
const actualArgs = (op.arity != null) ? op.arity : count
const minSize = (op.arity != null) ? op.arity : (opMin != null ? opMin : count)
const maxSize = (op.arity != null) ? op.arity : (opMax != null ? opMax : count)
if (outputSize < minSize || actualArgs < minSize) {
throw new ParseError((op.name || 'operator') + ' has too few operands (given ' + outputSize + ', expected ' + minSize + ')')
}
if ((outputSize > maxSize && operations.length === 0) || actualArgs > maxSize) {
throw new ParseError((op.name || 'operator') + ' has too many operands (given ' + outputSize + ', expected ' + maxSize + ')')
}
if (op === ArrayNode && output.length === 0) {
output.push(new ArrayNode())
return
}
if (outputSize < actualArgs) throw new ParseError('invalid statement')
const args = []
for (let i = 0; i < actualArgs; i++) args.unshift(output.pop())
// eslint-disable-next-line new-cap
output.push(new op(...args))
}
function handleOperator (token, lookahead) {
let cls = OP_CLASSES[token.value]
if (!cls) throw new ParseError('Unknown operator ' + token.value)
if (typeof cls.resolveClass === 'function') cls = cls.resolveClass(lookahead)
const prec = cls.precedence
if (cls.rightAssociative) {
while (operations.length > 0) {
const top = operations[operations.length - 1]
if (isOperationClass(top) && prec < top.precedence) consume()
else break
}
} else {
while (operations.length > 0) {
const top = operations[operations.length - 1]
if (isOperationClass(top) && prec <= top.precedence) consume()
else break
}
}
operations.push(cls)
}
function handleFunction (token) {
const fn = getFunctionClass(token.value)
if (!fn) throw new ParseError('Undefined function ' + token.value)
arities.push(0)
operations.push(fn)
}
function handleArray (token) {
if (token.value === 'array_start') {
operations.push(ArrayNode)
arities.push(0)
} else {
while (operations.length > 0 && operations[operations.length - 1] !== ArrayNode) consume()
if (operations[operations.length - 1] !== ArrayNode) throw new ParseError('Unbalanced bracket')
consume(arities.pop() + 1)
}
}
function handleGrouping (token, lookahead, index) {
if (token.value === 'open') {
if (lookahead && lookahead.value === 'close') {
skip.add(index + 1)
arities.pop()
consume(0)
} else {
operations.push(Grouping)
}
} else if (token.value === 'close') {
while (operations.length > 0 && operations[operations.length - 1] !== Grouping) consume()
const lparen = operations.pop()
if (lparen !== Grouping) throw new ParseError('Unbalanced parenthesis')
const top = operations[operations.length - 1]
if (top && (top.prototype instanceof FunctionNode)) consume(arities.pop() + 1)
} else if (token.value === 'comma') {
if (arities.length === 0) throw new ParseError('invalid statement')
arities[arities.length - 1] += 1
while (operations.length > 0 &&
operations[operations.length - 1] !== Grouping &&
operations[operations.length - 1] !== ArrayNode) consume()
} else {
throw new ParseError('Unknown grouping token ' + token.value)
}
}
for (let i = 0; i < tokens.length; i++) {
if (skip.has(i)) continue
const token = tokens[i]
const lookahead = tokens[i + 1]
switch (token.category) {
case 'numeric': output.push(new Numeric(token)); break
case 'logical': output.push(new Logical(token)); break
case 'string': output.push(new StringNode(token)); break
case 'identifier': output.push(new Identifier(token)); break
case 'null': output.push(new NilNode()); break
case 'operator':
case 'comparator':
case 'combinator':
handleOperator(token, lookahead); break
case 'function': handleFunction(token); break
case 'array': handleArray(token); break
case 'grouping': handleGrouping(token, lookahead, i); break
default:
throw new ParseError('Not implemented for tokens of category ' + token.category)
}
}
while (operations.length > 0) consume()
if (output.length !== 1) throw new ParseError('Invalid statement')
return output[0]
}
class Calculator {
constructor (options) {
options = options || {}
this.caseSensitive = options.caseSensitive === true
this.memory = Object.create(null)
this._astCache = Object.create(null)
}
key (k) { return this.caseSensitive ? k : String(k).toLowerCase() }
normalize (data) {
const out = Object.create(null)
if (!data) return out
for (const k of Object.keys(data)) out[this.key(k)] = data[k]
return out
}
store (data) { Object.assign(this.memory, this.normalize(data)); return this }
bind (data) { return this.store(data) }
clear () { this.memory = Object.create(null); return this }
ast (expression) {
const cached = this._astCache[expression]
if (cached) return cached
const node = parse(tokenize(expression, { caseSensitive: this.caseSensitive }))
this._astCache[expression] = node
return node
}
tokenize (expression) {
return tokenize(expression, { caseSensitive: this.caseSensitive })
}
dependencies (expression) {
return this.ast(expression).dependencies(this.memory)
}
evaluate (expression, data, onError) {
try {
return this.evaluateStrict(expression, data)
} catch (e) {
if (typeof data === 'function') return data(expression, e)
if (typeof onError === 'function') return onError(expression, e)
return undefined
}
}
evaluateStrict (expression, data) {
const ctx = Object.assign(Object.create(null), this.memory, this.normalize(data))
const node = this.ast(expression)
return node.value(ctx)
}
addFunction (name, spec) {
defineFunction(name, {
min: spec.minArgs == null ? (spec.min == null ? 0 : spec.min) : spec.minArgs,
max: spec.maxArgs == null ? (spec.max == null ? Infinity : spec.max) : spec.maxArgs,
type: spec.type || 'numeric',
call: spec.call
})
return this
}
}
export {
Calculator,
tokenize,
parse,
CalculatorError,
ParseError,
TokenizerError,
UnboundVariableError,
ZeroDivisionError,
ArgumentError
}
export function addFunction (name, spec) { defineFunction(name, spec) }
+12
View File
@@ -402,6 +402,8 @@
:remember-signature="rememberSignature"
:attachments-index="attachmentsIndex"
:require-signing-reason="requireSigningReason"
:signature-text="signatureText"
:signature-src="signatureSrc"
:button-text="submitButtonText"
:dry-run="dryRun"
:with-disclosure="withDisclosure"
@@ -832,6 +834,16 @@ export default {
required: false,
default: ''
},
signatureText: {
type: String,
required: false,
default: ''
},
signatureSrc: {
type: String,
required: false,
default: ''
},
previousSignatureValue: {
type: String,
required: false,
@@ -68,34 +68,9 @@ export default {
}
},
async mounted () {
const {
create,
evaluateDependencies,
addDependencies,
subtractDependencies,
divideDependencies,
multiplyDependencies,
powDependencies,
roundDependencies,
absDependencies,
sinDependencies,
tanDependencies,
cosDependencies
} = await import('mathjs')
const { Calculator } = await import('./calculator')
this.math = create({
evaluateDependencies,
addDependencies,
subtractDependencies,
divideDependencies,
multiplyDependencies,
powDependencies,
roundDependencies,
absDependencies,
sinDependencies,
tanDependencies,
cosDependencies
})
this.math = new Calculator()
this.isMathLoaded = true
},
@@ -165,34 +165,9 @@ export default {
}
if (this.field.preferences?.formula) {
const {
create,
evaluateDependencies,
addDependencies,
subtractDependencies,
divideDependencies,
multiplyDependencies,
powDependencies,
roundDependencies,
absDependencies,
sinDependencies,
tanDependencies,
cosDependencies
} = await import('mathjs')
const { Calculator } = await import('./calculator')
this.math = create({
evaluateDependencies,
addDependencies,
subtractDependencies,
divideDependencies,
multiplyDependencies,
powDependencies,
roundDependencies,
absDependencies,
sinDependencies,
tanDependencies,
cosDependencies
})
this.math = new Calculator()
this.isMathLoaded = true
}
@@ -408,6 +408,16 @@ export default {
required: false,
default: ''
},
signatureText: {
type: String,
required: false,
default: ''
},
signatureSrc: {
type: String,
required: false,
default: ''
},
modelValue: {
type: String,
required: false,
@@ -422,7 +432,7 @@ export default {
isOtherReason: false,
isUsePreviousValue: true,
isTouchAttachment: false,
isTextSignature: this.field.preferences?.format === 'typed' || this.field.preferences?.format === 'typed_or_upload',
isTextSignature: !this.signatureSrc && (!!this.signatureText || this.field.preferences?.format === 'typed' || this.field.preferences?.format === 'typed_or_upload'),
uploadImageInputKey: Math.random().toString()
}
},
@@ -482,7 +492,9 @@ export default {
if (entry.isIntersecting) {
this.setCanvasSize()
if (this.isTextSignature) {
if (this.signatureSrc) {
this.$nextTick(() => this.drawSignatureSrc())
} else if (this.isTextSignature) {
this.$nextTick(() => {
if (this.$refs.textInput) {
this.initTypedSignature()
@@ -686,7 +698,9 @@ export default {
}
},
async initTypedSignature () {
if (this.submitter.name) {
if (this.signatureText) {
this.$refs.textInput.value = this.signatureText
} else if (this.submitter.name) {
this.$refs.textInput.value = this.submitter.name
}
@@ -696,6 +710,48 @@ export default {
this.updateWrittenSignature({ target: this.$refs.textInput })
}
},
drawSignatureSrc () {
const canvas = this.$refs.canvas
if (!canvas) return
const img = new Image()
img.crossOrigin = 'anonymous'
img.onload = () => {
const context = canvas.getContext('2d')
const aspectRatio = img.width / img.height
const canvasWidth = canvas.width / scale
const canvasHeight = canvas.height / scale
let targetWidth = canvasWidth
let targetHeight = canvasHeight
if (canvasWidth / canvasHeight > aspectRatio) {
targetWidth = canvasHeight * aspectRatio
} else {
targetHeight = canvasWidth / aspectRatio
}
const x = (canvasWidth - targetWidth) / 2
const y = (canvasHeight - targetHeight) / 2
context.clearRect(0, 0, canvasWidth, canvasHeight)
context.drawImage(img, x, y, targetWidth, targetHeight)
this.isSignatureStarted = true
this.$emit('start')
}
img.onerror = () => {
console.error(`Failed to load signature image from ${this.signatureSrc}. The remote server must send an Access-Control-Allow-Origin header to allow CORS access.`)
}
img.src = this.signatureSrc
},
drawImage (event) {
this.remove()
this.clear()
@@ -20,8 +20,11 @@
{{ t('complete_all_required_fields_to_proceed_with_identity_verification') }}
</div>
<div v-else>
<div v-if="errorMessage">
{{ errorMessage }}
</div>
<div
v-if="isLoading"
v-else-if="isLoading"
class="w-full flex space-x-2 justify-center mb-2"
>
<IconInnerShadowTop
@@ -96,6 +99,7 @@ export default {
isMathLoaded: false,
redirectUrl: '',
isLoading: false,
errorMessage: '',
eidEasyData: {}
}
},
@@ -162,6 +166,12 @@ export default {
}).then(async (resp) => {
this.eidEasyData = await resp.json()
if (this.eidEasyData.error) {
this.errorMessage = this.eidEasyData.error
return
}
if (this.eidEasyData.check_completed) {
this.$emit('submit')
}
@@ -19,10 +19,6 @@ export const tiptapStylesheet = new CSSStyleSheet()
tiptapStylesheet.replaceSync(
`.ProseMirror {
position: relative;
}
.ProseMirror {
word-wrap: break-word;
white-space: pre-wrap;
white-space: break-spaces;
+2 -2
View File
@@ -7,8 +7,8 @@
# id :bigint not null, primary key
# body :text not null
# head :text
# sha1 :text not null
# uuid :uuid not null
# sha1 :string not null
# uuid :string not null
# created_at :datetime not null
# updated_at :datetime not null
# template_id :bigint not null
+2 -2
View File
@@ -20,7 +20,7 @@
<% end %>
</p>
</li>
<% submitter_versions_index = @submission.submitters.preload(:submitter_versions).each_with_object({}) { |s, h| h[s.id] = s.submitter_versions.to_a.sort_by(&:created_at) } %>
<% submitter_versions_index = @submission.submitters.preload(:submitter_versions).to_h { |s| [s.id, s.submitter_versions.to_a.sort_by(&:created_at)] } %>
<% @submission.submission_events.order(:event_timestamp).each do |event| %>
<% submitter = @submission.submitters.find { |e| e.id == event.submitter_id } %>
<% bg_class = event_colors[submitters_uuids.index(submitter.uuid) % event_colors.length] %>
@@ -47,7 +47,7 @@
<% elsif event.event_type == 'invite_party' && (invited_submitter = @submission.submitters.find { |e| e.uuid == event.data['uuid'] }) && (name = @submission.template_submitters.find { |e| e['uuid'] == event.data['uuid'] }&.dig('name')) %>
<%= t('submission_event_names.invite_party_by_html', invited_submitter_name: [invited_submitter.name || invited_submitter.email || invited_submitter.phone, name].join(' '), submitter_name:) %>
<% elsif event.event_type == 'delegate_form' %>
<% delegate_from = event.data['old_email'].presence || (versions.reverse.find { |v| v.created_at <= event.event_timestamp }&.then { |v| v.name || v.phone }) %>
<% delegate_from = event.data['old_email'].presence || versions.rfind { |v| v.created_at <= event.event_timestamp }&.then { |v| v.name || v.phone } %>
<%= t('submission_event_names.delegate_form_by_html', from: delegate_from, to: event.data['email']) %>
<% elsif event.event_type.include?('send_') %>
<%= t("submission_event_names.#{event.event_type}_to_html", submitter_name:) %>
+6 -7
View File
@@ -1,4 +1,4 @@
<% if params[:controller] == 'submissions_preview' %>
<% if local_assigns[:is_preview] %>
<%= render 'submissions/preview_tags' %>
<% end %>
<% font_scale = 1040.0 / PdfUtils::US_LETTER_W %>
@@ -15,7 +15,6 @@
<span class="text-xl md:text-3xl font-semibold focus:text-clip" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;"><% (@submission.name || @submission.template.name).split(/(_)/).each do |item| %><%= item %><wbr><% end %></span>
</a>
<div class="space-x-3 flex items-center">
<% last_submitter = @submission.submitters.to_a.select(&:completed_at?).max_by(&:completed_at) %>
<% is_all_completed = @submission.submitters.to_a.all?(&:completed_at?) %>
<% if signed_in? && can?(:create, @submission) && @submission.archived_at? && !is_all_completed %>
<%= button_to button_title(title: t('unarchive'), disabled_with: t('unarchive')[0..-2], icon: svg_icon('rotate', class: 'w-6 h-6')), submission_unarchive_index_path(@submission), class: 'btn btn-primary btn-ghost text-base hidden md:flex' %>
@@ -31,10 +30,10 @@
<span class="hidden md:block"><%= t('event_log') %></span>
<% end %>
<% end %>
<% if last_submitter %>
<% if @submission.submitters.to_a.any?(&:completed_at?) %>
<% if is_all_completed || !is_combined_enabled %>
<div class="join relative">
<download-button data-src="<%= submitter_download_index_path(last_submitter.slug, { sig: params[:sig], combined: is_combined_enabled }.compact) %>" class="base-button <%= '!rounded-r-none !pr-2' if is_all_completed && !is_combined_enabled %>">
<download-button data-src="<%= local_assigns[:is_preview] ? (@sig_submitter ? submit_form_documents_path(@sig_submitter.slug, { sig: params[:sig], combined: is_combined_enabled }.compact_blank) : submissions_preview_download_index_path(@submission.slug, combined: is_combined_enabled.presence)) : submission_download_index_path(@submission, combined: is_combined_enabled.presence) %>" class="base-button <%= '!rounded-r-none !pr-2' if is_all_completed && !is_combined_enabled %>">
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
<%= svg_icon('download', class: 'w-6 h-6') %>
<span class="hidden md:inline"><%= t('download') %></span>
@@ -53,7 +52,7 @@
</label>
<ul tabindex="0" class="z-10 dropdown-content p-2 mt-2 shadow menu text-base bg-base-100 rounded-box text-right">
<li>
<download-button data-src="<%= submitter_download_index_path(last_submitter.slug, { sig: params[:sig], combined: true }.compact) %>" class="flex items-center">
<download-button data-src="<%= local_assigns[:is_preview] ? (@sig_submitter ? submit_form_documents_path(@sig_submitter.slug, { sig: params[:sig], combined: true }.compact) : submissions_preview_download_index_path(@submission.slug, combined: true)) : submission_download_index_path(@submission, combined: true) %>" class="flex items-center">
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
<%= svg_icon('download', class: 'w-6 h-6 flex-shrink-0') %>
<span class="whitespace-nowrap"><%= t('download_combined_pdf') %></span>
@@ -96,7 +95,7 @@
<% submitters_index = @submission.submitters.index_by(&:uuid) %>
<% submitters_order_index = nil %>
<% attachments_index = ActiveStorage::Attachment.where(record: @submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %>
<% page_blob_struct = Struct.new(:url, :metadata, keyword_init: true) %>
<% page_blob_struct = Struct.new(:url, :metadata) %>
<% bg_classes = %w[bg-red-100 bg-sky-100 bg-emerald-100 bg-yellow-100 bg-purple-100 bg-pink-100 bg-cyan-100 bg-orange-100 bg-lime-100 bg-indigo-100] %>
<% schema.each do |item| %>
<% document = @submission.schema_documents.find { |e| e.uuid == item['attachment_uuid'] } %>
@@ -228,7 +227,7 @@
<% end %>
<% if signed_in? && submitter && submitter.email && !submitter.completed_at && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:update, @submission) && Accounts.can_send_emails?(current_account) && !@submission.expired? && !submitter.declined_at? %>
<div class="mt-2 mb-1">
<%= button_to button_title(title: submitter.sent_at? ? t('re_send_email') : t('send_email'), disabled_with: t('sending')), submitter_send_email_index_path(submitter_slug: submitter.slug), class: 'btn btn-sm btn-primary w-full' %>
<%= button_to button_title(title: submitter.sent_at? ? t('re_send_email') : t('send_email'), disabled_with: t('sending')), submitter_send_email_index_path(submitter), class: 'btn btn-sm btn-primary w-full' %>
</div>
<% end %>
<% if signed_in? && submitter && submitter.phone && !submitter.completed_at && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:update, @submission) && !@submission.expired? && !submitter.declined_at? %>
@@ -2,4 +2,4 @@
<% data_fields = Submissions.filtered_conditions_fields(submitter).to_json %>
<% invite_submitters = (submitter.submission.template_submitters || submitter.submission.template.submitters).select { |s| s['invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } }.to_json %>
<% optional_invite_submitters = (submitter.submission.template_submitters || submitter.submission.template.submitters).select { |s| s['optional_invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } }.to_json %>
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-schema="<%= schema.to_json %>" data-reuse-signature="<%= configs[:reuse_signature] %>" data-require-signing-reason="<%= configs[:require_signing_reason] %>" data-with-signature-id="<%= configs[:with_signature_id] %>" data-with-field-labels="<%= configs[:with_field_labels] %>" data-with-confetti="<%= configs[:with_confetti] %>" data-completed-redirect-url="<%= submitter.preferences['completed_redirect_url'].presence || submitter.submission.template&.preferences&.dig('completed_redirect_url') %>" data-completed-message="<%= (configs[:completed_message]&.compact_blank.presence || submitter.submission.template&.preferences&.dig('completed_message') || {}).to_json %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.preferences.key?('go_to_last') ? submitter.preferences['go_to_last'] : submitter.opened_at? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(submitter.submission.account) %>" data-optional-invite-submitters="<%= optional_invite_submitters %>" data-invite-submitters="<%= invite_submitters %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>" data-previous-signature-value="<%= local_assigns[:signature_attachment]&.uuid %>" data-remember-signature="<%= configs[:prefill_signature] %>" data-dry-run="<%= local_assigns[:dry_run] %>" data-expand="<%= local_assigns[:expand] %>" data-scroll-padding="<%= local_assigns[:scroll_padding] %>" data-language="<%= I18n.locale.to_s.split('-').first %>"></submission-form>
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-schema="<%= schema.to_json %>" data-reuse-signature="<%= configs[:reuse_signature] %>" data-require-signing-reason="<%= configs[:require_signing_reason] %>" data-with-signature-id="<%= configs[:with_signature_id] %>" data-with-field-labels="<%= configs[:with_field_labels] %>" data-with-confetti="<%= configs[:with_confetti] %>" data-completed-redirect-url="<%= submitter.preferences['completed_redirect_url'].presence || submitter.submission.template&.preferences&.dig('completed_redirect_url') %>" data-completed-message="<%= (configs[:completed_message]&.compact_blank.presence || submitter.submission.template&.preferences&.dig('completed_message') || {}).to_json %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.preferences.key?('go_to_last') ? submitter.preferences['go_to_last'] : submitter.opened_at? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(submitter.submission.account) %>" data-optional-invite-submitters="<%= optional_invite_submitters %>" data-invite-submitters="<%= invite_submitters %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>" data-signature-text="<%= params[:signature] %>" data-previous-signature-value="<%= local_assigns[:signature_attachment]&.uuid %>" data-remember-signature="<%= configs[:prefill_signature] %>" data-dry-run="<%= local_assigns[:dry_run] %>" data-expand="<%= local_assigns[:expand] %>" data-scroll-padding="<%= local_assigns[:scroll_padding] %>" data-language="<%= I18n.locale.to_s.split('-').first %>"></submission-form>
+1 -1
View File
@@ -30,7 +30,7 @@
<% end %>
<% end %>
<% if @submitter.completed_at > 30.minutes.ago || (current_user && current_user.account.submitters.exists?(id: @submitter.id)) %>
<download-button data-src="<%= submitter_download_index_path(@submitter.slug) %>" class="base-button w-full">
<download-button data-src="<%= submit_form_documents_path(@submitter.slug) %>" class="base-button w-full">
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
<%= svg_icon('download', class: 'w-6 h-6') %>
<span><%= t('download_documents') %></span>
+1 -1
View File
@@ -5,7 +5,7 @@
<% fields_index = Templates.build_field_areas_index(@submitter.submission.template_fields || @submitter.submission.template.fields) %>
<% values = @submitter.submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %>
<% submitters_index = @submitter.submission.submitters.index_by(&:uuid) %>
<% page_blob_struct = Struct.new(:url, :metadata, keyword_init: true) %>
<% page_blob_struct = Struct.new(:url, :metadata) %>
<% schema = Submissions.filtered_conditions_schema(@submitter.submission, values:, include_submitter_uuid: @submitter.uuid) %>
<% font_scale = 1000.0 / PdfUtils::US_LETTER_W %>
<% decline_modal_checkbox_uuid = nil %>
+3 -3
View File
@@ -66,7 +66,7 @@
<% if submitter.completed_at? %>
<div class="flex-1 md:flex-none">
<div class="w-full md:w-fit">
<download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="btn btn-sm btn-neutral text-white w-full md:w-36">
<download-button data-src="<%= submission_download_index_path(submission) %>" class="btn btn-sm btn-neutral text-white w-full md:w-36">
<span class="flex items-center justify-center space-x-1 <%= 'md:space-x-2' if t('download').length < 11 %>" data-target="download-button.defaultButton">
<%= svg_icon('download', class: 'w-5 h-5 stroke-2') %>
<span class="inline"><%= t('download') %></span>
@@ -154,7 +154,7 @@
</span>
</span>
<% if submitter.completed_at? && !is_submission_completed %>
<download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="absolute md:relative top-0 right-0 btn btn-xs btn-neutral text-white md:w-36 z-[1]">
<download-button data-src="<%= submitter_download_index_path(submitter) %>" class="absolute md:relative top-0 right-0 btn btn-xs btn-neutral text-white md:w-36 z-[1]">
<span class="flex items-center justify-center space-x-1 md:space-x-2" data-target="download-button.defaultButton">
<%= svg_icon('download', class: 'w-4 h-4 stroke-2') %>
<span class="inline"><%= t('download') %></span>
@@ -190,7 +190,7 @@
<% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %>
<div class="flex-1 md:flex-none">
<div class="w-full md:w-fit">
<download-button data-src="<%= submitter_download_index_path(latest_submitter.slug) %>" class="btn btn-sm btn-neutral text-white w-full md:w-36 z-[1]">
<download-button data-src="<%= submission_download_index_path(submission) %>" class="btn btn-sm btn-neutral text-white w-full md:w-36 z-[1]">
<span class="flex items-center justify-center space-x-1 md:space-x-2" data-target="download-button.defaultButton">
<%= svg_icon('download', class: 'w-5 h-5 stroke-2') %>
<span class="inline"><%= t('download') %></span>
+1 -1
View File
@@ -49,7 +49,7 @@
<% end %>
</div>
<% end %>
<%= link_to template_share_link_path(template), class: "absolute md:relative bottom-0 right-0 btn btn-xs md:btn-sm whitespace-nowrap btn-neutral text-white mt-1 px-2", data: { turbo_frame: :modal } do %>
<%= link_to template_share_link_path(template), class: 'absolute md:relative bottom-0 right-0 btn btn-xs md:btn-sm whitespace-nowrap btn-neutral text-white mt-1 px-2', data: { turbo_frame: :modal } do %>
<span class="flex items-center justify-center space-x-2">
<%= svg_icon('link', class: 'w-4 h-4 md:w-6 md:h-6') %>
<span><%= t('link') %></span>
+13 -1
View File
@@ -1,15 +1,27 @@
<div class="flex items-center space-x-1">
<% if (is_with_download = can?(:manage, :download_users) && can?(:manage, current_account)) %>
<%= button_to(t(:download), url_for(format: :csv), class: 'link text-sm', form: { class: 'flex' }, method: :get) %>
<% end %>
<% if %w[archived integration].include?(params[:status]) %>
<% if is_with_download %>
<span class="text-neutral-700">|</span>
<% end %>
<%= link_to t('view_active'), settings_users_path, class: 'link text-sm' %>
<% else %>
<% archived_exists = current_account.users.archived.where.not(role: 'integration').exists? %>
<% if current_account.users.active.exists?(role: 'integration') %>
<% if (integration_exists = current_account.users.active.exists?(role: 'integration')) %>
<% if is_with_download %>
<span class="text-neutral-700">|</span>
<% end %>
<%= link_to t('view_embedding_users'), settings_integration_users_path, class: 'link text-sm' %>
<% if archived_exists %>
<span class="text-neutral-700">|</span>
<% end %>
<% end %>
<% if archived_exists %>
<% if !integration_exists && is_with_download %>
<span class="text-neutral-700">|</span>
<% end %>
<%= link_to t('view_archived'), settings_archived_users_path, class: 'link text-sm' %>
<% end %>
<% end %>
+1 -1
View File
@@ -1,3 +1,3 @@
# frozen_string_literal: true
Pagy.options[:limit] = 10
Pagy::OPTIONS[:limit] = 10
+14 -7
View File
@@ -30,6 +30,7 @@ en: &en
thanks: Thanks
private: Private
_variables: Variables
your_email_address_has_been_released_immediately_you_can_now_be_added_to_your_company_team_without_waiting_for_the_deletion_period_to_end: Your email address has been released immediately. If you're joining a team, you can now be invited to your company team without waiting for the deletion period to end.
make_all_newly_created_templates_private_to_their_creator_and_admins_by_default: Make all newly created templates private to their creator and admins by default.
create_templates_with_admin_access_by_default: Create templates with admin access by default
require_email_2fa: Require email 2FA
@@ -218,7 +219,7 @@ en: &en
you_are_scheduling_your_account_for_deletion_after_deletion_your_data_will_be_permanently_removed_and_cannot_be_recovered_click_ok_if_you_would_like_to_continue: "You are scheduling your account for deletion. After deletion, your data will be permanently removed and cannot be recovered.\n\nClick OK if you would like to continue."
account_information_has_been_updated: Account information has been updated.
should_be_a_valid_url: should be a valid URL
your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Your account removal request will be processed within 2 months. Please contact us if you want to keep your account.
your_account_will_be_permanently_deleted_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Your account will be permanently deleted within 2 months. Please contact us if you want to keep your account.
test_mode: Test mode
copy: Copy
copied: Copied
@@ -1083,6 +1084,7 @@ es: &es
re_connect_stripe: Volver a conectar Stripe
private: Privado
_variables: Variables
your_email_address_has_been_released_immediately_you_can_now_be_added_to_your_company_team_without_waiting_for_the_deletion_period_to_end: Su dirección de correo electrónico ha sido liberada inmediatamente. Si se une a un equipo, ahora puede ser invitado al equipo de su empresa sin esperar a que finalice el período de eliminación.
make_all_newly_created_templates_private_to_their_creator_and_admins_by_default: Hacer que todas las plantillas recién creadas sean privadas para su creador y los administradores por defecto.
create_templates_with_admin_access_by_default: Crear plantillas con acceso de administrador por defecto
require_email_2fa: Requerir 2FA por correo electrónico
@@ -1256,7 +1258,7 @@ es: &es
you_are_scheduling_your_account_for_deletion_after_deletion_your_data_will_be_permanently_removed_and_cannot_be_recovered_click_ok_if_you_would_like_to_continue: "Estás programando la eliminación de tu cuenta. Después de la eliminación, tus datos se eliminarán permanentemente y no podrán recuperarse.\n\nHaz clic en OK si deseas continuar."
account_information_has_been_updated: La información de la cuenta ha sido actualizada.
should_be_a_valid_url: debe ser una URL válida
your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Tu solicitud de eliminación de cuenta se procesará en un plazo de 2 meses. Por favor contáctanos si deseas mantener tu cuenta.
your_account_will_be_permanently_deleted_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Tu solicitud de eliminación de cuenta se procesará en un plazo de 2 meses. Por favor contáctanos si deseas mantener tu cuenta.
test_mode: Modo de prueba
copy: Copiar
copied: Copiado
@@ -2118,6 +2120,7 @@ it: &it
re_connect_stripe: Ricollega Stripe
private: Privato
_variables: Variabili
your_email_address_has_been_released_immediately_you_can_now_be_added_to_your_company_team_without_waiting_for_the_deletion_period_to_end: Il tuo indirizzo email è stato rilasciato immediatamente. Se stai per unirti a un team, ora puoi essere invitato nel team della tua azienda senza aspettare la fine del periodo di eliminazione.
make_all_newly_created_templates_private_to_their_creator_and_admins_by_default: Rendere tutte le nuove template private per il creatore e gli amministratori per impostazione predefinita.
create_templates_with_admin_access_by_default: Crea modelli con accesso amministratore per impostazione predefinita
require_email_2fa: Richiedi 2FA email
@@ -2291,7 +2294,7 @@ it: &it
you_are_scheduling_your_account_for_deletion_after_deletion_your_data_will_be_permanently_removed_and_cannot_be_recovered_click_ok_if_you_would_like_to_continue: "Stai programmando l'eliminazione del tuo account. Dopo l'eliminazione, i tuoi dati saranno rimossi in modo permanente e non potranno essere recuperati.\n\nFai clic su OK se desideri continuare."
account_information_has_been_updated: "Le informazioni dell'account sono state aggiornate."
should_be_a_valid_url: deve essere un URL valido
your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account: "La tua richiesta di rimozione dell'account sarà elaborata entro 2 mesi. Contattaci se desideri mantenere il tuo account."
your_account_will_be_permanently_deleted_within_2_months_please_contact_us_if_you_want_to_keep_your_account: "La tua richiesta di rimozione dell'account sarà elaborata entro 2 mesi. Contattaci se desideri mantenere il tuo account."
test_mode: Modalità di test
copy: Copia
copied: Copiato
@@ -3154,6 +3157,7 @@ fr: &fr
re_connect_stripe: Reconnecter Stripe
private: Privé
_variables: Variables
your_email_address_has_been_released_immediately_you_can_now_be_added_to_your_company_team_without_waiting_for_the_deletion_period_to_end: Votre adresse e-mail a été libérée immédiatement. Si vous rejoignez une équipe, vous pouvez maintenant être invité dans l'équipe de votre entreprise sans attendre la fin de la période de suppression.
make_all_newly_created_templates_private_to_their_creator_and_admins_by_default: Rendre tous les nouveaux modèles privés pour leur créateur et les administrateurs par défaut.
create_templates_with_admin_access_by_default: Créer des modèles avec un accès administrateur par défaut
require_email_2fa: Exiger la 2FA par email
@@ -3327,7 +3331,7 @@ fr: &fr
you_are_scheduling_your_account_for_deletion_after_deletion_your_data_will_be_permanently_removed_and_cannot_be_recovered_click_ok_if_you_would_like_to_continue: "Vous planifiez la suppression de votre compte. Après suppression, vos données seront définitivement supprimées et ne pourront pas être récupérées.\n\nCliquez sur OK pour continuer."
account_information_has_been_updated: Les informations du compte ont été mises à jour.
should_be_a_valid_url: doit être une URL valide
your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Votre demande de suppression de compte sera traitée sous 2 mois. Veuillez nous contacter si vous souhaitez conserver votre compte.
your_account_will_be_permanently_deleted_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Votre demande de suppression de compte sera traitée sous 2 mois. Veuillez nous contacter si vous souhaitez conserver votre compte.
test_mode: Mode test
copy: Copier
copied: Copié
@@ -4186,6 +4190,7 @@ pt: &pt
re_connect_stripe: Reconectar Stripe
private: Privado
_variables: Variáveis
your_email_address_has_been_released_immediately_you_can_now_be_added_to_your_company_team_without_waiting_for_the_deletion_period_to_end: Seu endereço de e-mail foi liberado imediatamente. Se você estiver entrando em uma equipe, agora pode ser convidado para a equipe da sua empresa sem esperar o fim do período de exclusão.
make_all_newly_created_templates_private_to_their_creator_and_admins_by_default: Tornar todos os modelos recém-criados privados para seu criador e administradores por padrão.
create_templates_with_admin_access_by_default: Criar modelos com acesso de administrador por padrão
require_email_2fa: Exigir 2FA por email
@@ -4359,7 +4364,7 @@ pt: &pt
you_are_scheduling_your_account_for_deletion_after_deletion_your_data_will_be_permanently_removed_and_cannot_be_recovered_click_ok_if_you_would_like_to_continue: "Você está agendando a exclusão da sua conta. Após a exclusão, seus dados serão permanentemente removidos e não poderão ser recuperados.\n\nClique em OK se desejar continuar."
account_information_has_been_updated: As informações da conta foram atualizadas.
should_be_a_valid_url: deve ser um URL válido
your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Seu pedido de remoção da conta será processado em até 2 meses. Entre em contato conosco se você quiser manter sua conta.
your_account_will_be_permanently_deleted_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Seu pedido de remoção da conta será processado em até 2 meses. Entre em contato conosco se você quiser manter sua conta.
test_mode: Modo de teste
copy: Copiar
copied: Copiado
@@ -5207,6 +5212,7 @@ de: &de
thanks: Danke
private: Privat
_variables: Variablen
your_email_address_has_been_released_immediately_you_can_now_be_added_to_your_company_team_without_waiting_for_the_deletion_period_to_end: Ihre E-Mail-Adresse wurde sofort freigegeben. Wenn Sie einem Team beitreten, können Sie jetzt in das Team Ihres Unternehmens eingeladen werden, ohne auf das Ende der Löschfrist warten zu müssen.
make_all_newly_created_templates_private_to_their_creator_and_admins_by_default: Alle neu erstellten Vorlagen standardmäßig nur für ihren Ersteller und Administratoren sichtbar machen.
create_templates_with_admin_access_by_default: Vorlagen standardmäßig mit Administratorzugriff erstellen
require_email_2fa: E-Mail 2FA erforderlich
@@ -5394,7 +5400,7 @@ de: &de
you_are_scheduling_your_account_for_deletion_after_deletion_your_data_will_be_permanently_removed_and_cannot_be_recovered_click_ok_if_you_would_like_to_continue: "Sie planen die Löschung Ihres Kontos. Nach der Löschung werden Ihre Daten dauerhaft entfernt und können nicht wiederhergestellt werden.\n\nKlicken Sie auf OK, wenn Sie fortfahren möchten."
account_information_has_been_updated: Die Kontoinformationen wurden aktualisiert.
should_be_a_valid_url: sollte eine gültige URL sein
your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Ihre Anfrage zur Kontolöschung wird innerhalb von 2 Monaten bearbeitet. Bitte kontaktieren Sie uns, wenn Sie Ihr Konto behalten möchten.
your_account_will_be_permanently_deleted_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Ihre Anfrage zur Kontolöschung wird innerhalb von 2 Monaten bearbeitet. Bitte kontaktieren Sie uns, wenn Sie Ihr Konto behalten möchten.
test_mode: Testmodus
copy: Kopieren
copied: Kopiert
@@ -6642,6 +6648,7 @@ nl: &nl
thanks: Bedankt
private: Privé
_variables: Variabelen
your_email_address_has_been_released_immediately_you_can_now_be_added_to_your_company_team_without_waiting_for_the_deletion_period_to_end: Uw e-mailadres is onmiddellijk vrijgegeven. Als u lid wordt van een team, kunt u nu worden uitgenodigd voor het team van uw bedrijf zonder te wachten tot de verwijderingsperiode is afgelopen.
make_all_newly_created_templates_private_to_their_creator_and_admins_by_default: Maak alle nieuw aangemaakte sjablonen standaard privé voor hun maker en admins.
create_templates_with_admin_access_by_default: Sjablonen standaard met admin-toegang maken
require_email_2fa: E-mail 2FA vereist
@@ -6830,7 +6837,7 @@ nl: &nl
you_are_scheduling_your_account_for_deletion_after_deletion_your_data_will_be_permanently_removed_and_cannot_be_recovered_click_ok_if_you_would_like_to_continue: "U plant uw account voor verwijdering. Na verwijdering worden uw gegevens permanent verwijderd en kunnen ze niet worden hersteld.\n\nKlik op OK als u wilt doorgaan."
account_information_has_been_updated: Accountinformatie is bijgewerkt.
should_be_a_valid_url: moet een geldige URL zijn
your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Uw verzoek tot verwijdering van uw account wordt binnen 2 maanden verwerkt. Neem contact met ons op als u uw account wilt behouden.
your_account_will_be_permanently_deleted_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Uw verzoek tot verwijdering van uw account wordt binnen 2 maanden verwerkt. Neem contact met ons op als u uw account wilt behouden.
test_mode: Testmodus
copy: Kopiëren
copied: Gekopieerd
+7 -3
View File
@@ -71,6 +71,7 @@ Rails.application.routes.draw do
resources :submissions, only: %i[show destroy] do
resources :unarchive, only: %i[create], controller: 'submissions_unarchive'
resources :events, only: %i[index], controller: 'submission_events'
resources :download, only: %i[index], controller: 'submissions_download'
end
resources :submitters, only: %i[edit update]
resources :console_redirect, only: %i[index]
@@ -144,9 +145,11 @@ Rails.application.routes.draw do
resources :submit_form, only: %i[show update], path: 's', param: 'slug' do
resources :values, only: %i[index], controller: 'submit_form_values'
resources :download, only: %i[index], controller: 'submit_form_download'
resources :documents, only: %i[index], controller: 'submit_form_completed_download'
resources :decline, only: %i[create], controller: 'submit_form_decline'
resources :delegate, only: %i[create], controller: 'submit_form_delegate'
resources :invite, only: %i[create], controller: 'submit_form_invite'
resources :debug, only: %i[index], controller: 'submissions_debug' if Rails.env.development?
get :completed
get :delegated
end
@@ -155,14 +158,15 @@ Rails.application.routes.draw do
resources :submissions_preview, only: %i[show], path: 'e', param: 'slug' do
get :completed
resources :download, only: %i[index], controller: 'submissions_preview_download'
end
resources :send_submission_email, only: %i[create]
resources :submitters, only: %i[], param: 'slug' do
resources :download, only: %i[index], controller: 'submissions_download'
resources :submitters, only: %i[] do
resources :download, only: %i[index], controller: 'submitters_download', constraints: { submitter_id: /\d+/ }
resources :download, only: %i[index], controller: 'submit_form_completed_download'
resources :send_email, only: %i[create], controller: 'submitters_send_email'
resources :debug, only: %i[index], controller: 'submissions_debug' if Rails.env.development?
end
scope '/settings', as: :settings do
+165 -165
View File
@@ -13,41 +13,41 @@
ActiveRecord::Schema[8.1].define(version: 2026_03_27_100000) do
# These are extensions that must be enabled in order to support this database
enable_extension "btree_gin"
enable_extension "plpgsql"
enable_extension "pg_catalog.plpgsql"
create_table "access_tokens", force: :cascade do |t|
t.bigint "user_id", null: false
t.text "token", null: false
t.text "sha256", null: false
t.datetime "created_at", null: false
t.text "sha256", null: false
t.text "token", null: false
t.datetime "updated_at", null: false
t.bigint "user_id", null: false
t.index ["sha256"], name: "index_access_tokens_on_sha256", unique: true
t.index ["user_id"], name: "index_access_tokens_on_user_id"
end
create_table "account_accesses", force: :cascade do |t|
t.bigint "account_id", null: false
t.bigint "user_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "user_id", null: false
t.index ["account_id", "user_id"], name: "index_account_accesses_on_account_id_and_user_id", unique: true
end
create_table "account_configs", force: :cascade do |t|
t.bigint "account_id", null: false
t.string "key", null: false
t.text "value", null: false
t.datetime "created_at", null: false
t.string "key", null: false
t.datetime "updated_at", null: false
t.text "value", null: false
t.index ["account_id", "key"], name: "index_account_configs_on_account_id_and_key", unique: true
t.index ["account_id"], name: "index_account_configs_on_account_id"
end
create_table "account_linked_accounts", force: :cascade do |t|
t.bigint "account_id", null: false
t.bigint "linked_account_id", null: false
t.text "account_type", null: false
t.datetime "created_at", null: false
t.bigint "linked_account_id", null: false
t.datetime "updated_at", null: false
t.index ["account_id", "linked_account_id"], name: "idx_on_account_id_linked_account_id_48ab9f79d2", unique: true
t.index ["account_id"], name: "index_account_linked_accounts_on_account_id"
@@ -55,37 +55,37 @@ ActiveRecord::Schema[8.1].define(version: 2026_03_27_100000) do
end
create_table "accounts", force: :cascade do |t|
t.datetime "archived_at"
t.datetime "created_at", null: false
t.string "locale", null: false
t.string "name", null: false
t.string "timezone", null: false
t.string "locale", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "uuid", null: false
t.datetime "archived_at"
t.index ["uuid"], name: "index_accounts_on_uuid", unique: true
end
create_table "active_storage_attachments", force: :cascade do |t|
t.string "name", null: false
t.string "uuid", null: false
t.string "record_type", null: false
t.bigint "record_id", null: false
t.bigint "blob_id", null: false
t.datetime "created_at", null: false
t.string "name", null: false
t.bigint "record_id", null: false
t.string "record_type", null: false
t.string "uuid", null: false
t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
t.index ["record_type", "record_id", "name", "blob_id"], name: "idx_on_record_type_record_id_name_blob_id_0be5805727"
t.index ["uuid"], name: "index_active_storage_attachments_on_uuid"
end
create_table "active_storage_blobs", force: :cascade do |t|
t.string "key", null: false
t.string "filename", null: false
t.string "content_type"
t.text "metadata"
t.string "service_name", null: false
t.bigint "byte_size", null: false
t.string "checksum"
t.string "content_type"
t.datetime "created_at", null: false
t.string "filename", null: false
t.string "key", null: false
t.text "metadata"
t.string "service_name", null: false
t.string "uuid"
t.index ["checksum"], name: "index_active_storage_blobs_on_checksum"
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
@@ -99,26 +99,26 @@ ActiveRecord::Schema[8.1].define(version: 2026_03_27_100000) do
end
create_table "completed_documents", force: :cascade do |t|
t.bigint "submitter_id", null: false
t.string "sha256", null: false
t.datetime "created_at", null: false
t.string "sha256", null: false
t.bigint "submitter_id", null: false
t.datetime "updated_at", null: false
t.index ["sha256"], name: "index_completed_documents_on_sha256"
t.index ["submitter_id"], name: "index_completed_documents_on_submitter_id"
end
create_table "completed_submitters", force: :cascade do |t|
t.bigint "submitter_id", null: false
t.bigint "submission_id", null: false
t.bigint "account_id", null: false
t.bigint "template_id"
t.string "source", null: false
t.integer "sms_count", null: false
t.datetime "completed_at", null: false
t.datetime "created_at", null: false
t.boolean "is_first"
t.integer "sms_count", null: false
t.string "source", null: false
t.bigint "submission_id", null: false
t.bigint "submitter_id", null: false
t.bigint "template_id"
t.datetime "updated_at", null: false
t.string "verification_method"
t.boolean "is_first"
t.index ["account_id", "completed_at"], name: "index_completed_submitters_account_id_completed_at_is_first", where: "(is_first = true)"
t.index ["account_id", "completed_at"], name: "index_completed_submitters_on_account_id_and_completed_at"
t.index ["submission_id"], name: "index_completed_submitters_on_submission_id", unique: true, where: "(is_first = true)"
@@ -126,45 +126,45 @@ ActiveRecord::Schema[8.1].define(version: 2026_03_27_100000) do
end
create_table "console1984_commands", force: :cascade do |t|
t.text "statements"
t.datetime "created_at", null: false
t.bigint "sensitive_access_id"
t.bigint "session_id", null: false
t.datetime "created_at", null: false
t.text "statements"
t.datetime "updated_at", null: false
t.index ["sensitive_access_id"], name: "index_console1984_commands_on_sensitive_access_id"
t.index ["session_id", "created_at", "sensitive_access_id"], name: "on_session_and_sensitive_chronologically"
end
create_table "console1984_sensitive_accesses", force: :cascade do |t|
t.datetime "created_at", null: false
t.text "justification"
t.bigint "session_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["session_id"], name: "index_console1984_sensitive_accesses_on_session_id"
end
create_table "console1984_sessions", force: :cascade do |t|
t.text "reason"
t.bigint "user_id", null: false
t.datetime "created_at", null: false
t.text "reason"
t.datetime "updated_at", null: false
t.bigint "user_id", null: false
t.index ["created_at"], name: "index_console1984_sessions_on_created_at"
t.index ["user_id", "created_at"], name: "index_console1984_sessions_on_user_id_and_created_at"
end
create_table "console1984_users", force: :cascade do |t|
t.string "username", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "username", null: false
t.index ["username"], name: "index_console1984_users_on_username"
end
create_table "document_generation_events", force: :cascade do |t|
t.bigint "submitter_id", null: false
t.string "event_name", null: false
t.datetime "created_at", null: false
t.string "event_name", null: false
t.bigint "submitter_id", null: false
t.datetime "updated_at", null: false
t.index ["submitter_id", "event_name"], name: "index_document_generation_events_on_submitter_id_and_event_name", unique: true, where: "((event_name)::text = ANY ((ARRAY['start'::character varying, 'complete'::character varying])::text[]))"
t.index ["submitter_id", "event_name"], name: "index_document_generation_events_on_submitter_id_and_event_name", unique: true, where: "((event_name)::text = ANY (ARRAY[('start'::character varying)::text, ('complete'::character varying)::text]))"
t.index ["submitter_id"], name: "index_document_generation_events_on_submitter_id"
end
@@ -190,31 +190,31 @@ ActiveRecord::Schema[8.1].define(version: 2026_03_27_100000) do
create_table "email_events", force: :cascade do |t|
t.bigint "account_id", null: false
t.string "emailable_type", null: false
t.datetime "created_at", null: false
t.text "data", null: false
t.string "email", null: false
t.bigint "emailable_id", null: false
t.string "emailable_type", null: false
t.datetime "event_datetime", null: false
t.string "event_type", null: false
t.string "message_id", null: false
t.string "tag", null: false
t.string "event_type", null: false
t.string "email", null: false
t.text "data", null: false
t.datetime "event_datetime", null: false
t.datetime "created_at", null: false
t.index ["account_id", "event_datetime"], name: "index_email_events_on_account_id_and_event_datetime"
t.index ["email"], name: "index_email_events_on_email"
t.index ["email"], name: "index_email_events_on_email_event_types", where: "((event_type)::text = ANY ((ARRAY['bounce'::character varying, 'soft_bounce'::character varying, 'permanent_bounce'::character varying, 'complaint'::character varying, 'soft_complaint'::character varying])::text[]))"
t.index ["email"], name: "index_email_events_on_email_event_types", where: "((event_type)::text = ANY (ARRAY[('bounce'::character varying)::text, ('soft_bounce'::character varying)::text, ('permanent_bounce'::character varying)::text, ('complaint'::character varying)::text, ('soft_complaint'::character varying)::text]))"
t.index ["emailable_type", "emailable_id"], name: "index_email_events_on_emailable"
t.index ["message_id"], name: "index_email_events_on_message_id"
end
create_table "email_messages", force: :cascade do |t|
t.string "uuid", null: false
t.bigint "author_id", null: false
t.bigint "account_id", null: false
t.text "subject", null: false
t.bigint "author_id", null: false
t.text "body", null: false
t.string "sha1", null: false
t.datetime "created_at", null: false
t.string "sha1", null: false
t.text "subject", null: false
t.datetime "updated_at", null: false
t.string "uuid", null: false
t.index ["account_id"], name: "index_email_messages_on_account_id"
t.index ["sha1"], name: "index_email_messages_on_sha1"
t.index ["uuid"], name: "index_email_messages_on_uuid"
@@ -222,30 +222,30 @@ ActiveRecord::Schema[8.1].define(version: 2026_03_27_100000) do
create_table "encrypted_configs", force: :cascade do |t|
t.bigint "account_id", null: false
t.string "key", null: false
t.text "value", null: false
t.datetime "created_at", null: false
t.string "key", null: false
t.datetime "updated_at", null: false
t.text "value", null: false
t.index ["account_id", "key"], name: "index_encrypted_configs_on_account_id_and_key", unique: true
t.index ["account_id"], name: "index_encrypted_configs_on_account_id"
end
create_table "encrypted_user_configs", force: :cascade do |t|
t.bigint "user_id", null: false
t.string "key", null: false
t.text "value", null: false
t.datetime "created_at", null: false
t.string "key", null: false
t.datetime "updated_at", null: false
t.bigint "user_id", null: false
t.text "value", null: false
t.index ["user_id", "key"], name: "index_encrypted_user_configs_on_user_id_and_key", unique: true
t.index ["user_id"], name: "index_encrypted_user_configs_on_user_id"
end
create_table "lock_events", force: :cascade do |t|
t.string "key", null: false
t.string "event_name", null: false
t.datetime "created_at", null: false
t.string "event_name", null: false
t.string "key", null: false
t.datetime "updated_at", null: false
t.index ["event_name", "key"], name: "index_lock_events_on_event_name_and_key", unique: true, where: "((event_name)::text = ANY ((ARRAY['start'::character varying, 'complete'::character varying])::text[]))"
t.index ["event_name", "key"], name: "index_lock_events_on_event_name_and_key", unique: true, where: "((event_name)::text = ANY (ARRAY[('start'::character varying)::text, ('complete'::character varying)::text]))"
t.index ["key"], name: "index_lock_events_on_key"
end
@@ -262,31 +262,31 @@ ActiveRecord::Schema[8.1].define(version: 2026_03_27_100000) do
end
create_table "oauth_access_grants", force: :cascade do |t|
t.bigint "resource_owner_id", null: false
t.bigint "application_id", null: false
t.string "token", null: false
t.integer "expires_in", null: false
t.text "redirect_uri", null: false
t.string "scopes", default: "", null: false
t.datetime "created_at", null: false
t.datetime "revoked_at"
t.string "code_challenge"
t.string "code_challenge_method"
t.datetime "created_at", null: false
t.integer "expires_in", null: false
t.text "redirect_uri", null: false
t.bigint "resource_owner_id", null: false
t.datetime "revoked_at"
t.string "scopes", default: "", null: false
t.string "token", null: false
t.index ["application_id"], name: "index_oauth_access_grants_on_application_id"
t.index ["resource_owner_id"], name: "index_oauth_access_grants_on_resource_owner_id"
t.index ["token"], name: "index_oauth_access_grants_on_token", unique: true
end
create_table "oauth_access_tokens", force: :cascade do |t|
t.bigint "resource_owner_id"
t.bigint "application_id", null: false
t.string "token", null: false
t.string "refresh_token"
t.integer "expires_in"
t.string "scopes"
t.datetime "created_at", null: false
t.datetime "revoked_at"
t.integer "expires_in"
t.string "previous_refresh_token", default: "", null: false
t.string "refresh_token"
t.bigint "resource_owner_id"
t.datetime "revoked_at"
t.string "scopes"
t.string "token", null: false
t.index ["application_id"], name: "index_oauth_access_tokens_on_application_id"
t.index ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true
t.index ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id"
@@ -294,25 +294,25 @@ ActiveRecord::Schema[8.1].define(version: 2026_03_27_100000) do
end
create_table "oauth_applications", force: :cascade do |t|
t.string "name", null: false
t.string "uid", null: false
t.string "secret", null: false
t.text "redirect_uri"
t.string "scopes", default: "", null: false
t.boolean "confidential", default: true, null: false
t.datetime "created_at", null: false
t.string "name", null: false
t.text "redirect_uri"
t.string "scopes", default: "", null: false
t.string "secret", null: false
t.string "uid", null: false
t.datetime "updated_at", null: false
t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true
end
create_table "search_entries", force: :cascade do |t|
t.string "record_type", null: false
t.bigint "record_id", null: false
t.bigint "account_id", null: false
t.tsvector "tsvector", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.tsvector "ngram"
t.bigint "record_id", null: false
t.string "record_type", null: false
t.tsvector "tsvector", null: false
t.datetime "updated_at", null: false
t.index ["account_id", "ngram"], name: "index_search_entries_on_account_id_ngram_submission", where: "((record_type)::text = 'Submission'::text)", using: :gin
t.index ["account_id", "ngram"], name: "index_search_entries_on_account_id_ngram_submitter", where: "((record_type)::text = 'Submitter'::text)", using: :gin
t.index ["account_id", "ngram"], name: "index_search_entries_on_account_id_ngram_template", where: "((record_type)::text = 'Template'::text)", using: :gin
@@ -323,15 +323,15 @@ ActiveRecord::Schema[8.1].define(version: 2026_03_27_100000) do
end
create_table "submission_events", force: :cascade do |t|
t.bigint "account_id"
t.datetime "created_at", null: false
t.text "data", null: false
t.datetime "event_timestamp", null: false
t.string "event_type", null: false
t.bigint "submission_id", null: false
t.bigint "submitter_id"
t.text "data", null: false
t.string "event_type", null: false
t.datetime "event_timestamp", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "account_id"
t.index ["account_id", "created_at"], name: "index_submissions_events_on_sms_event_types", where: "((event_type)::text = ANY ((ARRAY['send_sms'::character varying, 'send_2fa_sms'::character varying])::text[]))"
t.index ["account_id", "created_at"], name: "index_submissions_events_on_sms_event_types", where: "((event_type)::text = ANY (ARRAY[('send_sms'::character varying)::text, ('send_2fa_sms'::character varying)::text]))"
t.index ["account_id"], name: "index_submission_events_on_account_id"
t.index ["created_at"], name: "index_submission_events_on_created_at"
t.index ["submission_id"], name: "index_submission_events_on_submission_id"
@@ -339,23 +339,23 @@ ActiveRecord::Schema[8.1].define(version: 2026_03_27_100000) do
end
create_table "submissions", force: :cascade do |t|
t.bigint "template_id"
t.bigint "created_by_user_id"
t.bigint "account_id", null: false
t.datetime "archived_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.text "template_fields"
t.text "template_schema"
t.text "template_submitters"
t.string "source", null: false
t.string "submitters_order", null: false
t.string "slug", null: false
t.text "preferences", null: false
t.bigint "account_id", null: false
t.bigint "created_by_user_id"
t.datetime "expire_at"
t.text "name"
t.text "variables_schema"
t.text "preferences", null: false
t.string "slug", null: false
t.string "source", null: false
t.string "submitters_order", null: false
t.text "template_fields"
t.bigint "template_id"
t.text "template_schema"
t.text "template_submitters"
t.datetime "updated_at", null: false
t.text "variables"
t.text "variables_schema"
t.index ["account_id", "id"], name: "index_submissions_on_account_id_and_id"
t.index ["account_id", "template_id", "id"], name: "index_submissions_on_account_id_and_template_id_and_id", where: "(archived_at IS NULL)"
t.index ["account_id", "template_id", "id"], name: "index_submissions_on_account_id_and_template_id_and_id_archived", where: "(archived_at IS NOT NULL)"
@@ -377,26 +377,26 @@ ActiveRecord::Schema[8.1].define(version: 2026_03_27_100000) do
end
create_table "submitters", force: :cascade do |t|
t.bigint "submission_id", null: false
t.string "uuid", null: false
t.string "email"
t.string "slug", null: false
t.text "values", null: false
t.string "ua"
t.string "ip"
t.datetime "sent_at"
t.datetime "opened_at"
t.bigint "account_id", null: false
t.datetime "completed_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "name"
t.string "phone"
t.string "external_id"
t.text "preferences", null: false
t.text "metadata", null: false
t.bigint "account_id", null: false
t.datetime "declined_at"
t.string "email"
t.string "external_id"
t.string "ip"
t.text "metadata", null: false
t.string "name"
t.datetime "opened_at"
t.string "phone"
t.text "preferences", null: false
t.datetime "sent_at"
t.string "slug", null: false
t.bigint "submission_id", null: false
t.string "timezone"
t.string "ua"
t.datetime "updated_at", null: false
t.string "uuid", null: false
t.text "values", null: false
t.index ["account_id", "id"], name: "index_submitters_on_account_id_and_id"
t.index ["completed_at", "account_id"], name: "index_submitters_on_completed_at_and_account_id"
t.index ["email"], name: "index_submitters_on_email"
@@ -406,52 +406,52 @@ ActiveRecord::Schema[8.1].define(version: 2026_03_27_100000) do
end
create_table "template_accesses", force: :cascade do |t|
t.bigint "template_id", null: false
t.bigint "user_id", null: false
t.datetime "created_at", null: false
t.bigint "template_id", null: false
t.datetime "updated_at", null: false
t.bigint "user_id", null: false
t.index ["template_id", "user_id"], name: "index_template_accesses_on_template_id_and_user_id", unique: true
end
create_table "template_folders", force: :cascade do |t|
t.string "name", null: false
t.bigint "author_id", null: false
t.bigint "account_id", null: false
t.datetime "archived_at"
t.bigint "author_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "name", null: false
t.bigint "parent_folder_id"
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_template_folders_on_account_id"
t.index ["author_id"], name: "index_template_folders_on_author_id"
t.index ["parent_folder_id"], name: "index_template_folders_on_parent_folder_id"
end
create_table "template_sharings", force: :cascade do |t|
t.bigint "template_id", null: false
t.bigint "account_id", null: false
t.string "ability", null: false
t.bigint "account_id", null: false
t.datetime "created_at", null: false
t.bigint "template_id", null: false
t.datetime "updated_at", null: false
t.index ["account_id", "template_id"], name: "index_template_sharings_on_account_id_and_template_id", unique: true
t.index ["template_id"], name: "index_template_sharings_on_template_id"
end
create_table "templates", force: :cascade do |t|
t.string "slug", null: false
t.string "name", null: false
t.text "schema", null: false
t.text "fields", null: false
t.text "submitters", null: false
t.bigint "author_id", null: false
t.bigint "account_id", null: false
t.datetime "archived_at"
t.bigint "author_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.text "source", null: false
t.bigint "folder_id", null: false
t.string "external_id"
t.text "fields", null: false
t.bigint "folder_id", null: false
t.string "name", null: false
t.text "preferences", null: false
t.text "schema", null: false
t.boolean "shared_link", default: false, null: false
t.string "slug", null: false
t.text "source", null: false
t.text "submitters", null: false
t.datetime "updated_at", null: false
t.text "variables_schema"
t.index ["account_id", "folder_id", "id"], name: "index_templates_on_account_id_and_folder_id_and_id", where: "(archived_at IS NULL)"
t.index ["account_id", "id"], name: "index_templates_on_account_id_and_id_archived", where: "(archived_at IS NOT NULL)"
@@ -463,44 +463,44 @@ ActiveRecord::Schema[8.1].define(version: 2026_03_27_100000) do
end
create_table "user_configs", force: :cascade do |t|
t.bigint "user_id", null: false
t.string "key", null: false
t.text "value", null: false
t.datetime "created_at", null: false
t.string "key", null: false
t.datetime "updated_at", null: false
t.bigint "user_id", null: false
t.text "value", null: false
t.index ["user_id", "key"], name: "index_user_configs_on_user_id_and_key", unique: true
t.index ["user_id"], name: "index_user_configs_on_user_id"
end
create_table "users", force: :cascade do |t|
t.string "first_name"
t.string "last_name"
t.string "email", null: false
t.string "role", null: false
t.string "encrypted_password", null: false
t.bigint "account_id", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.integer "sign_in_count", default: 0, null: false
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.integer "failed_attempts", default: 0, null: false
t.string "unlock_token"
t.datetime "locked_at"
t.datetime "archived_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "uuid", null: false
t.string "otp_secret"
t.integer "consumed_timestep"
t.boolean "otp_required_for_login", default: false, null: false
t.datetime "confirmation_sent_at"
t.string "confirmation_token"
t.datetime "confirmed_at"
t.datetime "confirmation_sent_at"
t.integer "consumed_timestep"
t.datetime "created_at", null: false
t.datetime "current_sign_in_at"
t.string "current_sign_in_ip"
t.string "email", null: false
t.string "encrypted_password", null: false
t.integer "failed_attempts", default: 0, null: false
t.string "first_name"
t.string "last_name"
t.datetime "last_sign_in_at"
t.string "last_sign_in_ip"
t.datetime "locked_at"
t.boolean "otp_required_for_login", default: false, null: false
t.string "otp_secret"
t.datetime "remember_created_at"
t.datetime "reset_password_sent_at"
t.string "reset_password_token"
t.string "role", null: false
t.integer "sign_in_count", default: 0, null: false
t.string "unconfirmed_email"
t.string "unlock_token"
t.datetime "updated_at", null: false
t.string "uuid", null: false
t.index ["account_id"], name: "index_users_on_account_id"
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
@@ -509,25 +509,25 @@ ActiveRecord::Schema[8.1].define(version: 2026_03_27_100000) do
end
create_table "webhook_attempts", force: :cascade do |t|
t.bigint "webhook_event_id", null: false
t.text "response_body"
t.integer "response_status_code", null: false
t.integer "attempt", null: false
t.datetime "created_at", null: false
t.text "response_body"
t.integer "response_status_code", null: false
t.datetime "updated_at", null: false
t.bigint "webhook_event_id", null: false
t.index ["webhook_event_id"], name: "index_webhook_attempts_on_webhook_event_id"
end
create_table "webhook_events", force: :cascade do |t|
t.string "uuid", null: false
t.bigint "webhook_url_id", null: false
t.bigint "account_id", null: false
t.datetime "created_at", null: false
t.string "event_type", null: false
t.bigint "record_id", null: false
t.string "record_type", null: false
t.string "event_type", null: false
t.string "status", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "uuid", null: false
t.bigint "webhook_url_id", null: false
t.index ["uuid", "webhook_url_id"], name: "index_webhook_events_on_uuid_and_webhook_url_id", unique: true
t.index ["webhook_url_id", "id"], name: "index_webhook_events_error", where: "((status)::text = 'error'::text)"
t.index ["webhook_url_id", "id"], name: "index_webhook_events_on_webhook_url_id_and_id"
@@ -535,12 +535,12 @@ ActiveRecord::Schema[8.1].define(version: 2026_03_27_100000) do
create_table "webhook_urls", force: :cascade do |t|
t.bigint "account_id", null: false
t.text "url", null: false
t.text "events", null: false
t.string "sha1", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.text "events", null: false
t.text "secret", null: false
t.string "sha1", null: false
t.datetime "updated_at", null: false
t.text "url", null: false
t.index ["account_id"], name: "index_webhook_urls_on_account_id"
t.index ["sha1"], name: "index_webhook_urls_on_sha1"
end
+1 -1
View File
@@ -3,7 +3,7 @@
module GenerateCertificate
SIZE = 2**11
Pkcs12Struct = Struct.new(:certificate, :ca_certs, keyword_init: true)
Pkcs12Struct = Struct.new(:certificate, :ca_certs)
module_function
+2 -2
View File
@@ -92,7 +92,7 @@ module Params
def in_path(params, path = [], skip_blank: false)
old_path = @current_path
@current_path = [old_path, *path].compact_blank.map(&:to_s).join('.')
@current_path = [old_path, *path].compact_blank.join('.')
param = params.dig(*path)
param = nil if skip_blank && param.blank?
@@ -106,7 +106,7 @@ module Params
old_path = @current_path
params.dig(*path)&.each_with_index do |item, index|
@current_path = [old_path, [*path].map(&:to_s).join('.') + "[#{index}]"].compact_blank.join('.')
@current_path = [old_path, [*path].join('.') + "[#{index}]"].compact_blank.join('.')
yield item if item
end
@@ -106,6 +106,8 @@ module Submissions
return submission if template.variables_schema.present? ||
submission.variables_schema.present?
return submission if template.schema.none? { |e| e['dynamic'] }
areas_index = {}
submission.template_schema = []
+3 -3
View File
@@ -438,8 +438,8 @@ module Submissions
composer.text(I18n.t('event_log'), font_size: 12, padding: [10, 0, 20, 0])
submitter_versions_index = submission.submitters.preload(:submitter_versions).each_with_object({}) do |s, h|
h[s.id] = s.submitter_versions.to_a.sort_by(&:created_at)
submitter_versions_index = submission.submitters.preload(:submitter_versions).to_h do |s|
[s.id, s.submitter_versions.to_a.sort_by(&:created_at)]
end
events_data = submission.submission_events.sort_by(&:event_timestamp).filter_map do |event|
@@ -475,7 +475,7 @@ module Submissions
].join("\n")
elsif event.event_type == 'delegate_form'
from = event.data['old_email'].presence ||
versions.reverse.find { |v| v.created_at <= event.event_timestamp }&.then { |v| v.name || v.phone }
versions.rfind { |v| v.created_at <= event.event_timestamp }&.then { |v| v.name || v.phone }
I18n.t('submission_event_names.delegate_form_by_html', from:, to: event.data['email'])
elsif event.event_type.include?('send_')
I18n.t("submission_event_names.#{event.event_type}_to_html", submitter_name:)
+1 -1
View File
@@ -58,7 +58,7 @@ module Submissions
end
def build_table_rows(submissions, expires_at: nil)
submissions.preload(submitters: [attachments_attachments: :blob, documents_attachments: :blob])
submissions.preload(submitters: [{ attachments_attachments: :blob, documents_attachments: :blob }])
.find_each.map do |submission|
submission_data = []
submitters_count = submission.submitters.size
+6 -4
View File
@@ -4,14 +4,14 @@ module Submissions
module NormalizeParamUtils
module_function
def normalize_submissions_params!(submissions_params, template, add_fields: false)
def normalize_submissions_params!(submissions_params, template, add_fields: false, purpose: nil)
attachments = []
fields = []
Array.wrap(submissions_params).each do |submission|
submission[:submitters].each_with_index do |submitter, index|
_, new_attachments, new_fields =
normalize_submitter_params!(submitter, template, submitter[:index] || index, add_fields:)
normalize_submitter_params!(submitter, template, submitter[:index] || index, add_fields:, purpose:)
attachments.push(*new_attachments)
fields.push(*new_fields)
@@ -21,7 +21,8 @@ module Submissions
[submissions_params, attachments, fields]
end
def normalize_submitter_params!(submitter_params, template, index = nil, for_submitter: nil, add_fields: false)
def normalize_submitter_params!(submitter_params, template, index = nil, for_submitter: nil, add_fields: false,
purpose: nil)
with_values = submitter_params[:values].present?
default_values = with_values ? submitter_params[:values] : {}
@@ -44,7 +45,8 @@ module Submissions
role_names: submitter_params[:roles],
for_submitter:,
add_fields:,
throw_errors: !with_values
throw_errors: !with_values,
purpose:
)
submitter_params[:values] = values
+33 -1
View File
@@ -25,6 +25,8 @@ module Submitters
drv scr ins isp mst paf prf shb shs slk ws wsc inf1 inf2
].freeze)
FILES_TTL = 5.minutes
module_function
def search(current_user, submitters, keyword)
@@ -138,7 +140,7 @@ module Submitters
ActiveStorage::Attachment.create!(
blob:,
name: params[:name],
name: 'attachments',
record: submitter
)
end
@@ -254,6 +256,36 @@ module Submitters
true
end
def build_document_urls(submitter, ttl: FILES_TTL)
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
select_attachments_for_download(submitter).map do |attachment|
ActiveStorage::Blob.proxy_path(
attachment.blob,
expires_at: ttl.from_now.to_i,
filename: build_document_filename(submitter, attachment.blob, filename_format)
)
end
end
def build_combined_url(submitter, ttl: FILES_TTL)
return if submitter.submission.submitters.exists?(completed_at: nil)
return if submitter.submission.submitters.order(:completed_at).last != submitter
attachment = submitter.submission.combined_document_attachment
attachment ||= Submissions::EnsureCombinedGenerated.call(submitter)
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
ActiveStorage::Blob.proxy_path(
attachment.blob,
expires_at: ttl.from_now.to_i,
filename: build_document_filename(submitter, attachment.blob, filename_format)
)
end
def populate_completed_is_first
Account.find_each do |account|
submissions_index = {}
+3 -1
View File
@@ -4,6 +4,7 @@ module Submitters
module CreateStampAttachment
WIDTH = 400
HEIGHT = 200
LRM = "\u200E"
TRANSPARENT_PIXEL = "\x89PNG\r\n\u001A\n\u0000\u0000\u0000\rIHDR\u0000" \
"\u0000\u0000\u0001\u0000\u0000\u0000\u0001\b\u0004" \
@@ -83,7 +84,8 @@ module Submitters
name = ERB::Util.html_escape(name)
role = ERB::Util.html_escape(role)
text = %(<span size="90">#{digitally_signed_by}:\n<b>#{name}</b>\n#{role}#{time} #{timezone}</span>)
text =
%(<span size="90">#{LRM}#{digitally_signed_by}:\n#{LRM}<b>#{name}</b>\n#{LRM}#{role}#{time} #{timezone}</span>)
Vips::Image.text(text, width: WIDTH, height: HEIGHT, wrap: :'word-char')
end
@@ -9,29 +9,13 @@ module Submitters
def call(submitter, params, cookies = nil, attachments = [])
attachments = attachments.select { |e| e.record_id == submitter.id && e.record_type == 'Submitter' }
if (value = params[:signature_src].presence || params[:signature].presence)
find_or_create_signature_from_value(submitter, value, attachments)
elsif params[:signed_signature_uuids].present?
if params[:signed_signature_uuids].present?
find_storage_signature(submitter, params[:signed_signature_uuids], attachments)
elsif cookies
find_session_signature(submitter, cookies, attachments)
end
end
def find_or_create_signature_from_value(submitter, value, attachments)
_, attachment = Submitters::NormalizeValues.normalize_attachment_value(value,
{ 'type' => 'signature' },
submitter.account,
attachments,
submitter)
attachment.record ||= submitter
attachment.save!
attachment
end
def sign_signature_uuid(uuid)
ApplicationRecord.signed_id_verifier.generate(uuid, purpose: SIGNED_UUID_PURPPOSE)
end
@@ -1,36 +0,0 @@
# frozen_string_literal: true
module Submitters
module MaybeAssignDefaultEmailSignature
module_function
def call(submitter, params, attachments)
return if params[:t].present? && params[:t] != SubmissionEvents.build_tracking_param(submitter, 'click_email')
return if params[:t].blank? && !submitter.submission_events.exists?(event_type: :click_email)
signature_attachment = find_previous_signature(submitter)
return if signature_attachment.blank?
existing_attachment = attachments.find do |a|
a.blob_id == signature_attachment.blob_id && submitter.id == a.record_id
end
return existing_attachment if existing_attachment
submitter.attachments_attachments.create_or_find_by!(blob_id: signature_attachment.blob_id)
end
def find_previous_signature(submitter)
return if submitter.email.blank?
submitters_query =
Submitter.where(email: submitter.email)
.where.not(completed_at: nil)
.where(SubmissionEvent.where(Submitter.arel_table[:id].eq(SubmissionEvent.arel_table[:submitter_id]))
.where(event_type: :click_email).limit(1).arel.exists)
ActiveStorage::Attachment.where(name: :signature, record: submitters_query).order(:id).last
end
end
end
+12 -6
View File
@@ -19,7 +19,7 @@ module Submitters
# rubocop:disable Metrics
def call(template, values, submitter_name: nil, role_names: nil, for_submitter: nil, throw_errors: false,
add_fields: false)
add_fields: false, purpose: nil)
fields =
if role_names.present?
fetch_roles_fields(template, roles: role_names)
@@ -68,7 +68,7 @@ module Submitters
value_fields.each do |field|
if field['type'].in?(%w[initials signature image file stamp]) && value.present?
new_value, new_attachments =
normalize_attachment_value(value, field, template.account, attachments, for_submitter)
normalize_attachment_value(value, field, template.account, attachments, for_submitter:, purpose:)
attachments.push(*new_attachments)
@@ -153,17 +153,17 @@ module Submitters
.merge(fields.group_by { |e| e['name'].to_s.downcase })
end
def normalize_attachment_value(value, field, account, attachments, for_submitter = nil)
def normalize_attachment_value(value, field, account, attachments, for_submitter: nil, purpose: nil)
if value.is_a?(Array)
new_attachments = value.map do |v|
new_attachment = find_or_build_attachment(v, field, account, for_submitter)
new_attachment = find_or_build_attachment(v, field, account, for_submitter:, purpose:)
attachments.find { |a| a.blob_id == new_attachment.blob_id } || new_attachment
end
[new_attachments.map(&:uuid), new_attachments]
else
new_attachment = find_or_build_attachment(value, field, account, for_submitter)
new_attachment = find_or_build_attachment(value, field, account, for_submitter:, purpose:)
existing_attachment = attachments.find { |a| a.blob_id == new_attachment.blob_id }
@@ -173,11 +173,15 @@ module Submitters
end
end
def find_or_build_attachment(value, field, account, for_submitter = nil)
def find_or_build_attachment(value, field, account, for_submitter: nil, purpose: nil)
type = field['type']
raise InvalidDefaultValue, "Invalid #{type} value" if purpose == :bulk
blob =
if value.match?(%r{\Ahttps?://})
raise InvalidDefaultValue, "Invalid #{type} value" unless purpose == :api
find_or_create_blob_from_url(account, value)
elsif type.in?(%w[signature initials]) && value.length < 60
find_or_create_blob_from_text(account, value, type)
@@ -185,6 +189,8 @@ module Submitters
Marcel::MimeType.for(data).exclude?('octet-stream')
find_or_create_blob_from_base64(account, data, type)
elsif type == 'image' && (value.starts_with?('<html>') || value.starts_with?('<!DOCTYPE'))
raise InvalidDefaultValue, "Invalid #{type} value" unless purpose == :api
find_or_create_blob_from_html(account, value, field)
else
raise InvalidDefaultValue, "Invalid value, url, base64 or text < 60 chars is expected: #{value.first(200)}..."
+2 -2
View File
@@ -15,9 +15,9 @@ module Submitters
ActiveRecord::Associations::Preloader.new(
records: [submitter],
associations: if with_documents
[documents_attachments: :blob, attachments_attachments: :blob]
[{ documents_attachments: :blob, attachments_attachments: :blob }]
elsif with_values
[attachments_attachments: :blob]
[{ attachments_attachments: :blob }]
end
).call
+1 -1
View File
@@ -12,7 +12,7 @@ module Submitters
def call(submitter, expires_at: Accounts.link_expires_at(Account.new(id: submitter.account_id)))
ActiveRecord::Associations::Preloader.new(
records: [submitter], associations: [documents_attachments: :blob, attachments_attachments: :blob]
records: [submitter], associations: [{ documents_attachments: :blob, attachments_attachments: :blob }]
).call
values = build_values_array(submitter, expires_at:)
+2 -2
View File
@@ -4,7 +4,7 @@ module Templates
module DetectFields
module_function
TextFieldBox = Struct.new(:x, :y, :w, :h, keyword_init: true) do
TextFieldBox = Struct.new(:x, :y, :w, :h) do
def endy
@endy ||= y + h
end
@@ -14,7 +14,7 @@ module Templates
end
end
PageNode = Struct.new(:prev, :next, :elem, :page, :attachment_uuid, keyword_init: true)
PageNode = Struct.new(:prev, :next, :elem, :page, :attachment_uuid)
DATE_REGEXP = /
(?:
+1 -1
View File
@@ -4,7 +4,7 @@ module Templates
module ImageToFields
module_function
Field = Struct.new(:type, :x, :y, :w, :h, :confidence, keyword_init: true) do
Field = Struct.new(:type, :x, :y, :w, :h, :confidence) do
def endy
@endy ||= y + h
end
+1 -1
View File
@@ -34,7 +34,7 @@ module Templates
if index.positive? && pdf_fields.present?
preview_document = template.schema[index - 1]
preview_document_last_field = template.fields.reverse.find do |f|
preview_document_last_field = template.fields.rfind do |f|
f['areas']&.any? do |a|
a['attachment_uuid'] == preview_document[:attachment_uuid]
end
+15
View File
@@ -0,0 +1,15 @@
# frozen_string_literal: true
module Users
module_function
def generate_csv(users)
headers = %w[email first_name last_name role current_sign_in_at last_sign_in_at updated_at created_at]
CSVSafe.generate do |csv|
csv << headers
users.each { |user| csv << user.values_at(*headers) }
end
end
end
-1
View File
@@ -42,7 +42,6 @@
"css-minimizer-webpack-plugin": "^5.0.0",
"daisyui": "^3.9.4",
"driver.js": "^1.3.5",
"mathjs": "^12.4.0",
"mini-css-extract-plugin": "^2.10.0",
"postcss": "^8.4.31",
"postcss-import": "^15.1.0",
+3 -4
View File
@@ -9,10 +9,9 @@ require 'rspec/rails'
require 'capybara/cuprite'
require 'capybara/rspec'
require 'webmock/rspec'
require 'sidekiq/testing'
require 'signing_form_helper'
Sidekiq::Testing.fake!
Sidekiq.testing!(:fake)
WebMock.disable_net_connect!(allow_localhost: true)
@@ -69,11 +68,11 @@ RSpec.configure do |config|
end
config.before do |example|
Sidekiq::Testing.inline! if example.metadata[:sidekiq] == :inline
Sidekiq.testing!(:inline) if example.metadata[:sidekiq] == :inline
end
config.after do |example|
Sidekiq::Testing.fake! if example.metadata[:sidekiq] == :inline
Sidekiq.testing!(:fake) if example.metadata[:sidekiq] == :inline
end
config.before(multitenant: true) do
-67
View File
@@ -966,13 +966,6 @@
dependencies:
regenerator-runtime "^0.13.11"
"@babel/runtime@^7.23.9":
version "7.23.9"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.9.tgz#47791a15e4603bb5f905bc0753801cf21d6345f7"
integrity sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==
dependencies:
regenerator-runtime "^0.14.0"
"@babel/template@^7.18.10", "@babel/template@^7.20.7":
version "7.20.7"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8"
@@ -2994,11 +2987,6 @@ commondir@^1.0.1:
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==
complex.js@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/complex.js/-/complex.js-2.1.1.tgz#0675dac8e464ec431fb2ab7d30f41d889fb25c31"
integrity sha512-8njCHOTtFFLtegk6zQo0kkVX1rngygb/KQI6z1qZxlFI3scluC+LVTCFbrkWjBv4vvLlbQ9t88IPMC6k95VTTg==
compressible@~2.0.18:
version "2.0.18"
resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
@@ -3317,11 +3305,6 @@ decache@^3.0.5:
dependencies:
find "^0.2.4"
decimal.js@^10.4.3:
version "10.4.3"
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23"
integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==
deep-is@^0.1.3:
version "0.1.4"
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
@@ -3632,11 +3615,6 @@ escape-html@~1.0.3:
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
escape-latex@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/escape-latex/-/escape-latex-1.2.0.tgz#07c03818cf7dac250cce517f4fda1b001ef2bca1"
integrity sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==
escape-string-regexp@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
@@ -4091,11 +4069,6 @@ forwarded@0.2.0:
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
fraction.js@4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.4.tgz#b2bac8249a610c3396106da97c5a71da75b94b1c"
integrity sha512-pwiTgt0Q7t+GHZA4yaLjObx4vXmmdcS0iSJ19o8d/goUGgItX9UZWKWNnLHehxviD8wU2IWRsnR8cD5+yOJP2Q==
fraction.js@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950"
@@ -4745,11 +4718,6 @@ jackspeak@^3.1.2:
optionalDependencies:
"@pkgjs/parseargs" "^0.11.0"
javascript-natural-sort@^0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz#f9e2303d4507f6d74355a73664d1440fb5a0ef59"
integrity sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==
jest-util@^29.5.0:
version "29.5.0"
resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.5.0.tgz#24a4d3d92fc39ce90425311b23c27a6e0ef16b8f"
@@ -5022,21 +4990,6 @@ math-intrinsics@^1.1.0:
resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
mathjs@^12.4.0:
version "12.4.0"
resolved "https://registry.yarnpkg.com/mathjs/-/mathjs-12.4.0.tgz#875c2ec19e5be69885b29769f78bbb37220322b6"
integrity sha512-4Moy0RNjwMSajEkGGxNUyMMC/CZAcl87WBopvNsJWB4E4EFebpTedr+0/rhqmnOSTH3Wu/3WfiWiw6mqiaHxVw==
dependencies:
"@babel/runtime" "^7.23.9"
complex.js "^2.1.1"
decimal.js "^10.4.3"
escape-latex "^1.2.0"
fraction.js "4.3.4"
javascript-natural-sort "^0.7.1"
seedrandom "^3.0.5"
tiny-emitter "^2.1.0"
typed-function "^4.1.1"
mdn-data@2.0.28:
version "2.0.28"
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.28.tgz#5ec48e7bef120654539069e1ae4ddc81ca490eba"
@@ -6192,11 +6145,6 @@ regenerator-runtime@^0.13.11:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
regenerator-runtime@^0.14.0:
version "0.14.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==
regenerator-transform@^0.15.1:
version "0.15.1"
resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.1.tgz#f6c4e99fc1b4591f780db2586328e4d9a9d8dc56"
@@ -6406,11 +6354,6 @@ schema-utils@^4.2.0, schema-utils@^4.3.0, schema-utils@^4.3.3:
ajv-formats "^2.1.1"
ajv-keywords "^5.1.0"
seedrandom@^3.0.5:
version "3.0.5"
resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7"
integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==
select-hose@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
@@ -7003,11 +6946,6 @@ thunky@^1.0.2:
resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d"
integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==
tiny-emitter@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==
to-fast-properties@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
@@ -7111,11 +7049,6 @@ typed-assert@^1.0.8:
resolved "https://registry.yarnpkg.com/typed-assert/-/typed-assert-1.0.9.tgz#8af9d4f93432c4970ec717e3006f33f135b06213"
integrity sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==
typed-function@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/typed-function/-/typed-function-4.1.1.tgz#38ce3cae31f4f513bcb263563fdad27b2afa73e8"
integrity sha512-Pq1DVubcvibmm8bYcMowjVnnMwPVMeh0DIdA8ad8NZY2sJgapANJmiigSUwlt+EgXxpfIv8MWrQXTIzkfYZLYQ==
uc.micro@^2.0.0, uc.micro@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee"