Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42371d7ce3 | ||
|
|
4116746be3 | ||
|
|
b2b2856ac0 | ||
|
|
b65bca213f | ||
|
|
7a72e2cbb7 | ||
|
|
3a73398a1e | ||
|
|
cce20d9230 | ||
|
|
8b72c91951 | ||
|
|
b0d8e47a75 | ||
|
|
85824faa2b | ||
|
|
8a507339af | ||
|
|
4636849118 | ||
|
|
cc52cae9fc | ||
|
|
b6bb4d2c8b | ||
|
|
5032676bd2 | ||
|
|
146b37f8cd | ||
|
|
47b3a2d09e | ||
|
|
f2306b334b | ||
|
|
3affa65571 | ||
|
|
6f3be16826 | ||
|
|
07ce76537b | ||
|
|
a1fb38b8cb | ||
|
|
1b52a99662 | ||
|
|
f5facfa6b9 | ||
|
|
21c6144dc6 | ||
|
|
2c155f0e42 | ||
|
|
33cc0a8ab5 | ||
|
|
1e65a7e52b | ||
|
|
14e2dbe1aa | ||
|
|
a823a6ac56 | ||
|
|
cfc4e001ba | ||
|
|
496cc855f6 | ||
|
|
bfb2db07a5 | ||
|
|
9f4f868246 | ||
|
|
f91f02f786 | ||
|
|
1da72e3e7a | ||
|
|
2e2c0e087d | ||
|
|
0c72f58ab0 | ||
|
|
d9d969cb79 | ||
|
|
8d685d1f73 | ||
|
|
e406620044 | ||
|
|
a315674421 | ||
|
|
8d953f210f | ||
|
|
7439ef07ad | ||
|
|
fc4b4de6ed | ||
|
|
96e4733f87 | ||
|
|
8b6731a289 | ||
|
|
f93ac68e9c | ||
|
|
439df4879c | ||
|
|
83b6ce8574 | ||
|
|
6ea07bc13b | ||
|
|
a15624690b | ||
|
|
974bb85fb7 | ||
|
|
5dc80601b2 | ||
|
|
df2e53bc40 | ||
|
|
6e685bbc32 | ||
|
|
0b3ca0de96 | ||
|
|
aed3f1f141 | ||
|
|
1a7981e919 | ||
|
|
4add805371 | ||
|
|
66c18317b7 | ||
|
|
9edbb885b4 | ||
|
|
c67c80d6bc | ||
|
|
0399531e0c | ||
|
|
54426556d9 | ||
|
|
fa74ee1755 | ||
|
|
eb53af12c4 | ||
|
|
38efcb99c0 | ||
|
|
a92d914674 | ||
|
|
219a698aa5 | ||
|
|
5bed6448dc | ||
|
|
6d2a8ca3d3 | ||
|
|
bfb2e0043d | ||
|
|
922101b12c | ||
|
|
9ff41a0c4a | ||
|
|
071f187605 | ||
|
|
ab6cd0e89c | ||
|
|
97c0dad7ff | ||
|
|
bc0917a190 | ||
|
|
5718acecff |
No files matched your search
@@ -0,0 +1,58 @@
|
||||
---
|
||||
:position: before
|
||||
:position_in_additional_file_patterns: before
|
||||
:position_in_class: before
|
||||
:position_in_factory: before
|
||||
:position_in_fixture: before
|
||||
:position_in_routes: before
|
||||
:position_in_serializer: before
|
||||
:position_in_test: before
|
||||
:classified_sort: true
|
||||
:exclude_controllers: true
|
||||
:exclude_factories: true
|
||||
:exclude_fixtures: false
|
||||
:exclude_helpers: true
|
||||
:exclude_scaffolds: true
|
||||
:exclude_serializers: false
|
||||
:exclude_sti_subclasses: false
|
||||
:exclude_tests: false
|
||||
:force: false
|
||||
:format_markdown: false
|
||||
:format_rdoc: false
|
||||
:format_yard: false
|
||||
:frozen: false
|
||||
:ignore_model_sub_dir: false
|
||||
:ignore_unknown_models: false
|
||||
:include_version: false
|
||||
:show_check_constraints: false
|
||||
:show_complete_foreign_keys: false
|
||||
:show_foreign_keys: true
|
||||
:show_indexes: true
|
||||
:simple_indexes: false
|
||||
:sort: false
|
||||
:timestamp: false
|
||||
:trace: false
|
||||
:with_comment: true
|
||||
:with_column_comments: true
|
||||
:with_table_comments: true
|
||||
:active_admin: false
|
||||
:command:
|
||||
:debug: false
|
||||
:hide_default_column_types: ''
|
||||
:hide_limit_column_types: ''
|
||||
:ignore_columns:
|
||||
:ignore_routes:
|
||||
:models: true
|
||||
:routes: false
|
||||
:skip_on_db_migrate: false
|
||||
:target_action: :do_annotations
|
||||
:wrapper:
|
||||
:wrapper_close:
|
||||
:wrapper_open:
|
||||
:classes_default_to_s: []
|
||||
:additional_file_patterns: []
|
||||
:model_dir:
|
||||
- app/models
|
||||
:require: []
|
||||
:root_dir:
|
||||
- ''
|
||||
@@ -7,13 +7,13 @@ jobs:
|
||||
name: Rubocop
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.4.1
|
||||
- name: Cache gems
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: vendor/bundle
|
||||
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
|
||||
@@ -31,13 +31,13 @@ jobs:
|
||||
name: Erblint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.4.1
|
||||
- name: Cache gems
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: vendor/bundle
|
||||
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
|
||||
@@ -49,13 +49,13 @@ jobs:
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 4
|
||||
- name: Run Erblint
|
||||
run: bundle exec erblint ./app
|
||||
run: bundle exec erb_lint ./app
|
||||
|
||||
eslint:
|
||||
name: ESLint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
- name: Cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v4
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
@@ -77,6 +77,33 @@ jobs:
|
||||
run: |
|
||||
./node_modules/eslint/bin/eslint.js "app/javascript/**/*.js"
|
||||
|
||||
brakeman:
|
||||
name: Brakeman
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.4.1
|
||||
- name: Cache gems
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: vendor/bundle
|
||||
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gem-
|
||||
- name: Install gems
|
||||
run: |
|
||||
gem install bundler
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 4
|
||||
yarn install
|
||||
sudo apt-get update
|
||||
sudo apt-get install libvips
|
||||
- name: Run Brakeman
|
||||
run: bundle exec brakeman -q --exit-on-warn
|
||||
|
||||
rspec:
|
||||
name: RSpec
|
||||
runs-on: ubuntu-latest
|
||||
@@ -96,7 +123,7 @@ jobs:
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
@@ -110,12 +137,12 @@ jobs:
|
||||
with:
|
||||
chrome-version: 125
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
|
||||
- name: Cache gems
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: vendor/bundle
|
||||
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -25,22 +25,22 @@ jobs:
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Create .version file
|
||||
run: echo ${{ github.ref_name }} > .version
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM ruby:3.4.1-alpine as fonts
|
||||
FROM ruby:3.4.1-alpine AS fonts
|
||||
|
||||
WORKDIR /fonts
|
||||
|
||||
@@ -6,7 +6,7 @@ RUN apk --no-cache add fontforge wget && wget https://github.com/satbyy/go-noto-
|
||||
|
||||
RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")'
|
||||
|
||||
FROM ruby:3.4.1-alpine as webpack
|
||||
FROM ruby:3.4.1-alpine AS webpack
|
||||
|
||||
ENV RAILS_ENV=production
|
||||
ENV NODE_ENV=production
|
||||
@@ -32,7 +32,7 @@ COPY ./app/views ./app/views
|
||||
|
||||
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
|
||||
|
||||
FROM ruby:3.4.1-alpine as app
|
||||
FROM ruby:3.4.1-alpine AS app
|
||||
|
||||
ENV RAILS_ENV=production
|
||||
ENV BUNDLE_WITHOUT="development:test"
|
||||
|
||||
@@ -46,7 +46,6 @@ gem 'twitter_cldr', require: false
|
||||
gem 'tzinfo-data'
|
||||
|
||||
group :development, :test do
|
||||
gem 'annotate'
|
||||
gem 'better_html'
|
||||
gem 'bullet'
|
||||
gem 'debug'
|
||||
@@ -63,6 +62,9 @@ group :development, :test do
|
||||
end
|
||||
|
||||
group :development do
|
||||
gem 'annotaterb'
|
||||
gem 'brakeman', require: false
|
||||
gem 'foreman', require: false
|
||||
gem 'letter_opener_web'
|
||||
gem 'web-console'
|
||||
end
|
||||
|
||||
@@ -74,9 +74,7 @@ GEM
|
||||
uri (>= 0.13.1)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
annotate (2.6.5)
|
||||
activerecord (>= 2.3.0)
|
||||
rake (>= 0.8.7)
|
||||
annotaterb (4.14.0)
|
||||
arabic-letter-connector (0.1.1)
|
||||
ast (2.4.2)
|
||||
aws-eventstream (1.3.0)
|
||||
@@ -120,6 +118,8 @@ GEM
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.18.4)
|
||||
msgpack (~> 1.2)
|
||||
brakeman (7.0.0)
|
||||
racc
|
||||
builder (3.3.0)
|
||||
bullet (8.0.0)
|
||||
activesupport (>= 3.0.0)
|
||||
@@ -223,9 +223,13 @@ GEM
|
||||
concurrent-ruby (~> 1.1)
|
||||
webrick (~> 1.7)
|
||||
websocket-driver (~> 0.7)
|
||||
ffi (1.17.0)
|
||||
ffi (1.17.0-aarch64-linux-gnu)
|
||||
ffi (1.17.0-x86_64-linux-gnu)
|
||||
ffi (1.17.1)
|
||||
ffi (1.17.1-aarch64-linux-gnu)
|
||||
ffi (1.17.1-aarch64-linux-musl)
|
||||
ffi (1.17.1-arm64-darwin)
|
||||
ffi (1.17.1-x86_64-linux-gnu)
|
||||
ffi (1.17.1-x86_64-linux-musl)
|
||||
foreman (0.88.1)
|
||||
geom2d (0.4.1)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
@@ -324,7 +328,7 @@ GEM
|
||||
mysql2 (0.5.6)
|
||||
net-http-persistent (4.0.5)
|
||||
connection_pool (~> 2.2)
|
||||
net-imap (0.5.3)
|
||||
net-imap (0.5.6)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
@@ -334,12 +338,18 @@ GEM
|
||||
net-smtp (0.5.0)
|
||||
net-protocol
|
||||
nio4r (2.7.4)
|
||||
nokogiri (1.17.2)
|
||||
nokogiri (1.18.3)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.17.2-aarch64-linux)
|
||||
nokogiri (1.18.3-aarch64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.17.2-x86_64-linux)
|
||||
nokogiri (1.18.3-aarch64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.3-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.3-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.3-x86_64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
oj (3.16.8)
|
||||
bigdecimal (>= 3.0)
|
||||
@@ -377,7 +387,7 @@ GEM
|
||||
puma (6.5.0)
|
||||
nio4r (~> 2.0)
|
||||
racc (1.8.1)
|
||||
rack (3.1.8)
|
||||
rack (3.1.10)
|
||||
rack-proxy (0.7.7)
|
||||
rack
|
||||
rack-session (2.0.0)
|
||||
@@ -520,10 +530,13 @@ GEM
|
||||
simplecov-html (0.13.1)
|
||||
simplecov_json_formatter (0.1.4)
|
||||
smart_properties (1.17.0)
|
||||
sqlite3 (2.4.1)
|
||||
sqlite3 (2.5.0)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
sqlite3 (2.4.1-aarch64-linux-gnu)
|
||||
sqlite3 (2.4.1-x86_64-linux-gnu)
|
||||
sqlite3 (2.5.0-aarch64-linux-gnu)
|
||||
sqlite3 (2.5.0-aarch64-linux-musl)
|
||||
sqlite3 (2.5.0-arm64-darwin)
|
||||
sqlite3 (2.5.0-x86_64-linux-gnu)
|
||||
sqlite3 (2.5.0-x86_64-linux-musl)
|
||||
stringio (3.1.2)
|
||||
strip_attributes (1.14.1)
|
||||
activemodel (>= 3.0, < 9.0)
|
||||
@@ -569,17 +582,21 @@ GEM
|
||||
|
||||
PLATFORMS
|
||||
aarch64-linux
|
||||
aarch64-linux-musl
|
||||
arm64-darwin
|
||||
ruby
|
||||
x86_64-linux
|
||||
x86_64-linux-musl
|
||||
|
||||
DEPENDENCIES
|
||||
annotate
|
||||
annotaterb
|
||||
arabic-letter-connector
|
||||
aws-sdk-s3
|
||||
aws-sdk-secretsmanager
|
||||
azure-storage-blob
|
||||
better_html
|
||||
bootsnap
|
||||
brakeman
|
||||
bullet
|
||||
cancancan
|
||||
capybara
|
||||
@@ -595,6 +612,7 @@ DEPENDENCIES
|
||||
faker
|
||||
faraday
|
||||
faraday-follow_redirects
|
||||
foreman
|
||||
google-cloud-storage
|
||||
hexapdf
|
||||
image_processing
|
||||
|
||||
@@ -39,7 +39,7 @@ class AccountConfigsController < ApplicationController
|
||||
end
|
||||
|
||||
def account_config_params
|
||||
params.required(:account_config).permit!.tap do |attrs|
|
||||
params.required(:account_config).permit(:key, :value, { value: {} }, { value: [] }).tap do |attrs|
|
||||
attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,7 +10,7 @@ module Api
|
||||
last_submitter = @submission.submitters.max_by(&:completed_at)
|
||||
|
||||
if last_submitter.documents_attachments.blank?
|
||||
last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(submitter)
|
||||
last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(last_submitter)
|
||||
end
|
||||
|
||||
last_submitter.documents_attachments
|
||||
|
||||
@@ -146,9 +146,15 @@ module Api
|
||||
|
||||
if attrs[:completed]
|
||||
submitter.values = Submitters::SubmitValues.merge_default_values(submitter)
|
||||
submitter.values = Submitters::SubmitValues.merge_formula_values(submitter)
|
||||
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
|
||||
|
||||
formula_values = Submitters::SubmitValues.build_formula_values(submitter)
|
||||
|
||||
if formula_values.present?
|
||||
submitter.values = submitter.values.merge(formula_values)
|
||||
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
|
||||
end
|
||||
|
||||
submitter.values = submitter.values.transform_values do |v|
|
||||
v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v
|
||||
end
|
||||
|
||||
@@ -34,6 +34,8 @@ module Api
|
||||
}
|
||||
end
|
||||
}
|
||||
rescue HexaPDF::MalformedPDFError
|
||||
render json: { error: 'Malformed PDF' }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -30,7 +30,7 @@ class ApplicationController < ActionController::Base
|
||||
redirect_to request.referer, alert: 'Too many requests', status: :too_many_requests
|
||||
end
|
||||
|
||||
if Rails.env.production?
|
||||
if Rails.env.production? || Rails.env.test?
|
||||
rescue_from CanCan::AccessDenied do |e|
|
||||
Rollbar.warning(e) if defined?(Rollbar)
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ class NotificationsSettingsController < ApplicationController
|
||||
end
|
||||
|
||||
def email_config_params
|
||||
params.require(:account_config).permit!.tap do |attrs|
|
||||
params.require(:account_config).permit(:key, :value, { value: {} }, { value: [] }).tap do |attrs|
|
||||
attrs[:key] = nil unless attrs[:key].in?([AccountConfig::BCC_EMAILS, AccountConfig::SUBMITTER_REMINDERS])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -50,7 +50,7 @@ class PersonalizationSettingsController < ApplicationController
|
||||
end
|
||||
|
||||
def account_config_params
|
||||
attrs = params.require(:account_config).permit!
|
||||
attrs = params.require(:account_config).permit(:key, :value, { value: {} }, { value: [] })
|
||||
|
||||
return attrs if attrs[:value].is_a?(String)
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class PwaController < ActionController::Base
|
||||
end
|
||||
@@ -9,8 +9,6 @@ class SendSubmissionEmailController < ApplicationController
|
||||
|
||||
SEND_DURATION = 30.minutes
|
||||
|
||||
def success; end
|
||||
|
||||
def create
|
||||
@submitter =
|
||||
if params[:template_slug]
|
||||
@@ -31,7 +29,7 @@ class SendSubmissionEmailController < ApplicationController
|
||||
end
|
||||
|
||||
respond_to do |f|
|
||||
f.html { redirect_to success_send_submission_email_index_path }
|
||||
f.html { render :success }
|
||||
f.json { head :ok }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -34,6 +34,8 @@ class StartFormController < ApplicationController
|
||||
assign_submission_attributes(@submitter, @template)
|
||||
|
||||
Submissions::AssignDefinedSubmitters.call(@submitter.submission)
|
||||
else
|
||||
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
|
||||
end
|
||||
|
||||
if @submitter.save
|
||||
@@ -65,8 +67,9 @@ class StartFormController < ApplicationController
|
||||
.order(id: :desc)
|
||||
.where(declined_at: nil)
|
||||
.where(external_id: nil)
|
||||
.where(ip: [nil, request.remote_ip])
|
||||
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
|
||||
.find_or_initialize_by(**submitter_params.compact_blank)
|
||||
.find_or_initialize_by(email: submitter_params[:email], **submitter_params.compact_blank)
|
||||
end
|
||||
|
||||
def assign_submission_attributes(submitter, template)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmissionEventsController < ApplicationController
|
||||
SUBMISSION_EVENT_ICONS = {
|
||||
'view_form' => 'eye',
|
||||
'start_form' => 'player_play',
|
||||
'complete_form' => 'check',
|
||||
'send_email' => 'mail_forward',
|
||||
'click_email' => 'hand_click',
|
||||
'api_complete_form' => 'check',
|
||||
'send_reminder_email' => 'mail_forward',
|
||||
'send_2fa_sms' => '2fa',
|
||||
'send_sms' => 'send',
|
||||
'phone_verified' => 'phone_check',
|
||||
'click_sms' => 'hand_click',
|
||||
'decline_form' => 'x',
|
||||
'start_verification' => 'player_play',
|
||||
'complete_verification' => 'check',
|
||||
'invite_party' => 'user_plus'
|
||||
}.freeze
|
||||
|
||||
load_and_authorize_resource :submission
|
||||
|
||||
def index; end
|
||||
end
|
||||
@@ -20,7 +20,7 @@ class SubmissionsPreviewController < ApplicationController
|
||||
|
||||
@submission ||= Submission.find_by!(slug: params[:slug])
|
||||
|
||||
raise ActionController::RoutingError if @submission.account.archived_at?
|
||||
raise ActionController::RoutingError, I18n.t('not_found') if @submission.account.archived_at?
|
||||
|
||||
if !@submission.submitters.all?(&:completed_at?) && !signature_valid &&
|
||||
(!current_user || !current_ability.can?(:read, @submission))
|
||||
@@ -40,6 +40,7 @@ class SubmissionsPreviewController < ApplicationController
|
||||
|
||||
def completed
|
||||
@submission = Submission.find_by!(slug: params[:submissions_preview_slug])
|
||||
@template = @submission.template
|
||||
|
||||
render :completed, layout: 'form'
|
||||
end
|
||||
|
||||
@@ -28,8 +28,10 @@ class TemplatesPreferencesController < ApplicationController
|
||||
submitters_order
|
||||
completed_notification_email_subject completed_notification_email_body
|
||||
completed_notification_email_enabled completed_notification_email_attach_audit] +
|
||||
[completed_message: %i[title body]]
|
||||
[completed_message: %i[title body],
|
||||
submitters: [%i[uuid request_email_subject request_email_body]]]
|
||||
).tap do |attrs|
|
||||
attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1'
|
||||
attrs[:preferences] = attrs[:preferences].transform_values do |value|
|
||||
if %w[true false].include?(value)
|
||||
value == 'true'
|
||||
|
||||
@@ -26,7 +26,7 @@ class UserConfigsController < ApplicationController
|
||||
end
|
||||
|
||||
def user_config_params
|
||||
params.required(:user_config).permit!.tap do |attrs|
|
||||
params.required(:user_config).permit(:key, :value, { value: {} }, { value: [] }).tap do |attrs|
|
||||
attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -45,17 +45,16 @@ class UsersController < ApplicationController
|
||||
return redirect_to settings_users_path, notice: I18n.t('unable_to_update_user') if Docuseal.demo?
|
||||
|
||||
attrs = user_params.compact_blank.merge(user_params.slice(:archived_at))
|
||||
attrs.delete(:role) if !role_valid?(attrs[:role]) || current_user == @user
|
||||
|
||||
if params.dig(:user, :account_id).present?
|
||||
account = Account.accessible_by(current_ability).find(params[:user][:account_id])
|
||||
account = Account.accessible_by(current_ability).find(params.dig(:user, :account_id))
|
||||
|
||||
authorize!(:manage, account)
|
||||
|
||||
@user.account = account
|
||||
end
|
||||
|
||||
if @user.update(attrs)
|
||||
if @user.update(attrs.except(current_user == @user ? :role : nil))
|
||||
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_updated')
|
||||
else
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity
|
||||
@@ -84,8 +83,11 @@ class UsersController < ApplicationController
|
||||
|
||||
def user_params
|
||||
if params.key?(:user)
|
||||
params.require(:user).permit(:email, :first_name, :last_name, :password,
|
||||
:role, :archived_at, :account_id)
|
||||
permitted_params = %i[email first_name last_name password archived_at]
|
||||
|
||||
permitted_params << :role if role_valid?(params.dig(:user, :role))
|
||||
|
||||
params.require(:user).permit(permitted_params)
|
||||
else
|
||||
{}
|
||||
end
|
||||
|
||||
@@ -3,6 +3,8 @@ export default class extends HTMLElement {
|
||||
this.resize()
|
||||
|
||||
this.textarea.addEventListener('input', () => this.resize())
|
||||
|
||||
this.observeVisibility()
|
||||
}
|
||||
|
||||
resize () {
|
||||
@@ -11,6 +13,28 @@ export default class extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
observeVisibility () {
|
||||
this.observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
this.resize()
|
||||
this.observer.unobserve(this.textarea)
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
threshold: 0.1
|
||||
}
|
||||
)
|
||||
|
||||
this.observer.observe(this.textarea)
|
||||
}
|
||||
|
||||
disconnectedCallback () {
|
||||
this.observer.unobserve(this.textarea)
|
||||
}
|
||||
|
||||
get textarea () {
|
||||
return this.querySelector('textarea')
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
class="field-area flex absolute lg:text-base -outline-offset-1"
|
||||
dir="auto"
|
||||
:style="computedStyle"
|
||||
:class="{ 'font-serif': field.preferences?.font === 'Times', 'text-[1.6vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'field-area-active outline-red-500 outline-dashed outline-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
|
||||
:class="{ 'font-mono': field.preferences?.font === 'Courier', 'font-serif': field.preferences?.font === 'Times', 'text-[1.6vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'field-area-active outline-red-500 outline-dashed outline-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
|
||||
>
|
||||
<div
|
||||
v-if="(!withFieldPlaceholder || !field.name || field.type === 'cells') && !isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
|
||||
@@ -191,10 +191,16 @@
|
||||
class="whitespace-pre-wrap text-gray-400"
|
||||
:class="{ 'w-full': field.preferences?.align }"
|
||||
>{{ field.name }}</span>
|
||||
<span v-else-if="Array.isArray(modelValue)">
|
||||
<span
|
||||
v-else-if="Array.isArray(modelValue)"
|
||||
:class="{ 'w-full': field.preferences?.align }"
|
||||
>
|
||||
{{ modelValue.join(', ') }}
|
||||
</span>
|
||||
<span v-else-if="field.type === 'date'">
|
||||
<span
|
||||
v-else-if="field.type === 'date'"
|
||||
:class="{ 'w-full': field.preferences?.align }"
|
||||
>
|
||||
{{ formattedDate }}
|
||||
</span>
|
||||
<span
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
:with-label="withLabel && !withFieldPlaceholder && step.length < 2"
|
||||
:is-value-set="step.some((f) => f.uuid in values)"
|
||||
:attachments-index="attachmentsIndex"
|
||||
@click="$emit('focus-step', stepIndex)"
|
||||
@click="[$emit('focus-step', stepIndex), maybeScrollOnClick(field, area)]"
|
||||
/>
|
||||
</Teleport>
|
||||
</template>
|
||||
@@ -109,6 +109,14 @@ export default {
|
||||
areaRefs: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isMobileContainer () {
|
||||
const root = this.$root.$el.parentNode.getRootNode()
|
||||
const container = root.body || root.querySelector('div')
|
||||
|
||||
return container.style.overflow === 'hidden'
|
||||
}
|
||||
},
|
||||
beforeUpdate () {
|
||||
this.areaRefs = []
|
||||
},
|
||||
@@ -121,14 +129,16 @@ export default {
|
||||
this.scrollIntoArea(field.areas[0])
|
||||
}
|
||||
},
|
||||
maybeScrollOnClick (field, area) {
|
||||
if (['text', 'number', 'cells'].includes(field.type) && this.isMobileContainer) {
|
||||
this.scrollIntoArea(area)
|
||||
}
|
||||
},
|
||||
scrollIntoArea (area) {
|
||||
const areaRef = this.areaRefs.find((a) => a.area === area)
|
||||
|
||||
if (areaRef) {
|
||||
const root = this.$root.$el.parentNode.getRootNode()
|
||||
const container = root.body || root.querySelector('div')
|
||||
|
||||
if (container.style.overflow === 'hidden') {
|
||||
if (this.isMobileContainer) {
|
||||
this.scrollInContainer(areaRef.$el)
|
||||
} else {
|
||||
const targetRect = areaRef.$refs.scrollToElem.getBoundingClientRect()
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<MarkdownContent :string="field.description" />
|
||||
</div>
|
||||
<FileDropzone
|
||||
:message="`${t('upload')} ${field.name || t('files')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||
:message="`${t('upload')} ${(field.title || field.name) || t('files')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||
:submitter-slug="submitterSlug"
|
||||
:multiple="true"
|
||||
:dry-run="dryRun"
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
<template>
|
||||
<div dir="auto">
|
||||
<div
|
||||
class="flex justify-between items-center w-full"
|
||||
class="flex justify-between items-end w-full mb-3.5 sm:mb-4"
|
||||
:class="{ 'mb-2': !field.description }"
|
||||
>
|
||||
<label
|
||||
v-if="showFieldNames"
|
||||
:for="field.uuid"
|
||||
class="label text-2xl"
|
||||
class="label text-xl sm:text-2xl py-0"
|
||||
>
|
||||
<MarkdownContent
|
||||
v-if="field.title"
|
||||
:string="field.title"
|
||||
/>
|
||||
<template v-else>
|
||||
{{ field.name && showFieldNames ? field.name : t('date') }}
|
||||
<template v-if="!field.required">
|
||||
{{ field.name || t('date') }}
|
||||
</template>
|
||||
<template v-if="!field.required">
|
||||
<span
|
||||
class="ml-1"
|
||||
:class="{ 'hidden sm:inline': (field.title || field.name || t('date') ).length > 10 }"
|
||||
>
|
||||
({{ t('optional') }})
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
</label>
|
||||
<button
|
||||
|
||||
@@ -100,7 +100,8 @@
|
||||
<button
|
||||
v-if="!isCompleted"
|
||||
id="minimize_form_button"
|
||||
class="absolute right-0 mr-2 mt-2 top-0 hidden md:block"
|
||||
class="absolute right-0 top-0"
|
||||
:class="currentField?.description?.length > 100 ? 'mr-1 mt-1 md:mr-2 md:mt-2': 'mr-2 mt-2 hidden md:block'"
|
||||
:title="t('minimize')"
|
||||
@click.prevent="minimizeForm"
|
||||
>
|
||||
@@ -164,7 +165,7 @@
|
||||
v-if="showFieldNames && (currentField.name || currentField.title)"
|
||||
:for="currentField.uuid"
|
||||
dir="auto"
|
||||
class="label text-2xl"
|
||||
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
|
||||
:class="{ 'mb-2': !currentField.description }"
|
||||
>
|
||||
<MarkdownContent
|
||||
@@ -173,7 +174,11 @@
|
||||
/>
|
||||
<template v-else>
|
||||
{{ currentField.name }}
|
||||
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
||||
</template>
|
||||
<template v-if="!currentField.required">
|
||||
<span :class="{ 'hidden sm:inline': (currentField.title || currentField.name).length > 20 }">
|
||||
({{ t('optional') }})
|
||||
</span>
|
||||
</template>
|
||||
</label>
|
||||
<div
|
||||
@@ -221,7 +226,7 @@
|
||||
v-if="showFieldNames && (currentField.name || currentField.title)"
|
||||
:for="currentField.uuid"
|
||||
dir="auto"
|
||||
class="label text-2xl"
|
||||
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
|
||||
:class="{ 'mb-2': !currentField.description }"
|
||||
>
|
||||
<MarkdownContent
|
||||
@@ -230,7 +235,11 @@
|
||||
/>
|
||||
<template v-else>
|
||||
{{ currentField.name }}
|
||||
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
||||
</template>
|
||||
<template v-if="!currentField.required">
|
||||
<span :class="{ 'hidden sm:inline': (currentField.title || currentField.name).length > 20 }">
|
||||
({{ t('optional') }})
|
||||
</span>
|
||||
</template>
|
||||
</label>
|
||||
<div
|
||||
@@ -466,7 +475,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="(currentField.type !== 'payment' && currentField.type !== 'verification') || submittedValues[currentField.uuid]"
|
||||
:class="currentField.type === 'signature' ? 'mt-2' : 'mt-6 md:mt-8'"
|
||||
:class="currentField.type === 'signature' ? 'mt-2' : 'mt-4 md:mt-6'"
|
||||
>
|
||||
<button
|
||||
id="submit_form_button"
|
||||
@@ -522,9 +531,9 @@
|
||||
/>
|
||||
<div
|
||||
v-if="stepFields.length < 80"
|
||||
class="flex justify-center"
|
||||
class="flex justify-center mt-3 sm:mt-4 mb-0 sm:mb-1"
|
||||
>
|
||||
<div class="flex items-center mt-4 mb-1 flex-wrap">
|
||||
<div class="flex items-center flex-wrap">
|
||||
<a
|
||||
v-for="(step, index) in stepFields"
|
||||
:key="step[0].uuid"
|
||||
@@ -1028,7 +1037,7 @@ export default {
|
||||
}, [])
|
||||
},
|
||||
formulaFields () {
|
||||
return this.fields.filter((f) => f.preferences?.formula && f.type !== 'payment')
|
||||
return this.fields.filter((f) => f.preferences?.formula && f.type !== 'payment' && this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f))
|
||||
},
|
||||
attachmentsIndex () {
|
||||
return this.attachments.reduce((acc, a) => {
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
<template>
|
||||
<div v-if="modelValue">
|
||||
<div class="flex justify-between items-center w-full mb-2">
|
||||
<div class="flex justify-between items-end w-full mb-3.5 md:mb-4">
|
||||
<label
|
||||
class="label text-2xl"
|
||||
>{{ showFieldNames && field.name ? field.name : t('image') }}</label>
|
||||
v-if="showFieldNames"
|
||||
:for="field.uuid"
|
||||
class="label text-xl sm:text-2xl py-0"
|
||||
>
|
||||
<MarkdownContent
|
||||
v-if="field.title"
|
||||
:string="field.title"
|
||||
/>
|
||||
<template v-else>
|
||||
{{ field.name || t('image') }}
|
||||
</template>
|
||||
</label>
|
||||
<button
|
||||
class="btn btn-outline btn-sm"
|
||||
@click.prevent="remove"
|
||||
@@ -35,7 +45,7 @@
|
||||
<MarkdownContent :string="field.description" />
|
||||
</div>
|
||||
<FileDropzone
|
||||
:message="`${t('upload')} ${field.name || t('image')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||
:message="`${t('upload')} ${(field.title || field.name) || t('image')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||
:submitter-slug="submitterSlug"
|
||||
:dry-run="dryRun"
|
||||
:accept="'image/*'"
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
<template>
|
||||
<div dir="auto">
|
||||
<div
|
||||
class="flex justify-between items-center w-full"
|
||||
class="flex justify-between items-end w-full mb-3.5 md:mb-4"
|
||||
:class="{ 'mb-2': !field.description }"
|
||||
>
|
||||
<label
|
||||
class="label text-2xl"
|
||||
v-if="showFieldNames"
|
||||
class="label text-xl sm:text-2xl py-0"
|
||||
>
|
||||
<MarkdownContent
|
||||
v-if="field.title"
|
||||
:string="field.title"
|
||||
/>
|
||||
<template v-else>
|
||||
{{ showFieldNames && field.name ? field.name : t('initials') }}
|
||||
{{ field.name || t('initials') }}
|
||||
</template>
|
||||
</label>
|
||||
<div class="space-x-2 flex">
|
||||
<div class="space-x-2 flex flex-none">
|
||||
<span
|
||||
v-if="isDrawInitials"
|
||||
class="tooltip"
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="mt-6 md:mt-8"
|
||||
class="mt-4 md:mt-6"
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
|
||||
@@ -3,13 +3,19 @@
|
||||
v-if="showFieldNames && (field.name || field.title)"
|
||||
:for="field.uuid"
|
||||
dir="auto"
|
||||
class="label text-2xl"
|
||||
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
|
||||
:class="{ 'mb-2': !field.description }"
|
||||
><MarkdownContent
|
||||
v-if="field.title"
|
||||
:string="field.title"
|
||||
/>
|
||||
>
|
||||
<MarkdownContent
|
||||
v-if="field.title"
|
||||
:string="field.title"
|
||||
/>
|
||||
<template v-else>{{ field.name }}</template>
|
||||
<template v-if="!field.required">
|
||||
<span :class="{ 'hidden sm:inline': (field.title || field.name).length > 20 }">
|
||||
({{ t('optional') }})
|
||||
</span>
|
||||
</template>
|
||||
</label>
|
||||
<div
|
||||
v-if="field.description"
|
||||
|
||||
@@ -3,15 +3,18 @@
|
||||
v-if="showFieldNames && (field.name || field.title)"
|
||||
:for="field.uuid"
|
||||
dir="auto"
|
||||
class="label text-2xl"
|
||||
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
|
||||
:class="{ 'mb-2': !field.description }"
|
||||
><MarkdownContent
|
||||
v-if="field.title"
|
||||
:string="field.title"
|
||||
/>
|
||||
>
|
||||
<MarkdownContent
|
||||
v-if="field.title"
|
||||
:string="field.title"
|
||||
/>
|
||||
<template v-else>{{ field.name }}</template>
|
||||
<template v-if="!field.required">
|
||||
({{ t('optional') }})
|
||||
<span :class="{ 'hidden sm:inline': (field.title || field.name).length > 20 }">
|
||||
({{ t('optional') }})
|
||||
</span>
|
||||
</template>
|
||||
</label>
|
||||
<div
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<label
|
||||
v-if="!modelValue && !sessionId"
|
||||
class="label text-2xl mb-2"
|
||||
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
|
||||
>
|
||||
<MarkdownContent
|
||||
v-if="field.title"
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
<label
|
||||
v-if="showFieldNames"
|
||||
:for="isCodeSent ? 'one_time_code' : field.uuid"
|
||||
class="label text-2xl"
|
||||
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
|
||||
:class="{ 'mb-2': !field.description }"
|
||||
>
|
||||
<MarkdownContent
|
||||
@@ -10,10 +11,7 @@
|
||||
:string="field.title"
|
||||
/>
|
||||
<template v-else>
|
||||
{{ showFieldNames && field.name ? field.name : t('verified_phone_number') }}
|
||||
<template v-if="!field.required">
|
||||
({{ t('optional') }})
|
||||
</template>
|
||||
{{ field.name || t('verified_phone_number') }}
|
||||
</template>
|
||||
</label>
|
||||
<div
|
||||
|
||||
@@ -4,21 +4,22 @@
|
||||
class="relative"
|
||||
>
|
||||
<div
|
||||
class="flex justify-between items-center w-full"
|
||||
class="flex justify-between items-end w-full mb-3.5 md:mb-4"
|
||||
:class="{ 'mb-2': !field.description }"
|
||||
>
|
||||
<label
|
||||
class="label text-2xl"
|
||||
v-if="showFieldNames"
|
||||
class="label text-xl sm:text-2xl py-0"
|
||||
>
|
||||
<MarkdownContent
|
||||
v-if="field.title"
|
||||
:string="field.title"
|
||||
/>
|
||||
<template v-else>
|
||||
{{ showFieldNames && field.name ? field.name : t('signature') }}
|
||||
{{ field.name || t('signature') }}
|
||||
</template>
|
||||
</label>
|
||||
<div class="space-x-2 flex">
|
||||
<div class="space-x-2 flex flex-none">
|
||||
<span
|
||||
v-if="isTextSignature && format !== 'typed' && format !== 'upload'"
|
||||
class="tooltip"
|
||||
@@ -39,6 +40,7 @@
|
||||
<span
|
||||
v-else-if="withTypedSignature && format !== 'typed' && format !== 'drawn' && format !== 'upload'"
|
||||
class="tooltip ml-2"
|
||||
:class="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
|
||||
:data-tip="t('type_text')"
|
||||
>
|
||||
<a
|
||||
@@ -56,6 +58,7 @@
|
||||
<span
|
||||
v-if="format !== 'typed' && format !== 'drawn' && format !== 'upload' && format !== 'drawn_or_typed'"
|
||||
class="tooltip"
|
||||
:class="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
|
||||
:data-tip="t('take_photo')"
|
||||
>
|
||||
<label
|
||||
|
||||
@@ -3,15 +3,18 @@
|
||||
v-if="showFieldNames && (field.name || field.title)"
|
||||
:for="field.uuid"
|
||||
dir="auto"
|
||||
class="label text-2xl"
|
||||
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
|
||||
:class="{ 'mb-2': !field.description }"
|
||||
><MarkdownContent
|
||||
v-if="field.title"
|
||||
:string="field.title"
|
||||
/>
|
||||
>
|
||||
<MarkdownContent
|
||||
v-if="field.title"
|
||||
:string="field.title"
|
||||
/>
|
||||
<template v-else>{{ field.name }}</template>
|
||||
<template v-if="!field.required">
|
||||
({{ t('optional') }})
|
||||
<span :class="{ 'hidden sm:inline': (field.title || field.name).length > 20 }">
|
||||
({{ t('optional') }})
|
||||
</span>
|
||||
</template>
|
||||
</label>
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<label
|
||||
class="label text-2xl mb-2"
|
||||
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
|
||||
>
|
||||
<MarkdownContent
|
||||
v-if="field.title"
|
||||
@@ -31,6 +31,15 @@
|
||||
class="animate-spin h-10"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="redirectUrl">
|
||||
<a
|
||||
:href="redirectUrl"
|
||||
target="_blank"
|
||||
class="white-button w-full"
|
||||
>
|
||||
{{ t('verify_id') }}
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
ref="widgetContainer"
|
||||
/>
|
||||
@@ -87,6 +96,7 @@ export default {
|
||||
return {
|
||||
isCreatingCheckout: false,
|
||||
isMathLoaded: false,
|
||||
redirectUrl: '',
|
||||
isLoading: false,
|
||||
eidEasyData: {}
|
||||
}
|
||||
@@ -147,14 +157,26 @@ export default {
|
||||
}).then(async (resp) => {
|
||||
this.eidEasyData = await resp.json()
|
||||
|
||||
const eidEasyWidget = document.createElement('eideasy-widget')
|
||||
if (this.eidEasyData.available_methods[0] === 'itsme-qes-signature' &&
|
||||
this.eidEasyData.available_methods.length === 1) {
|
||||
const redirectUrl = new URL('https://id.eideasy.com/sign_contract_external')
|
||||
|
||||
for (const key in this.widgetSettings) {
|
||||
eidEasyWidget[key] = this.widgetSettings[key]
|
||||
redirectUrl.searchParams.append('client_id', this.eidEasyData.client_id)
|
||||
redirectUrl.searchParams.append('doc_id', this.eidEasyData.doc_id)
|
||||
redirectUrl.searchParams.append('country', this.countryCode)
|
||||
redirectUrl.searchParams.append('lang', this.locale)
|
||||
|
||||
this.redirectUrl = redirectUrl.toString()
|
||||
} else {
|
||||
const eidEasyWidget = document.createElement('eideasy-widget')
|
||||
|
||||
for (const key in this.widgetSettings) {
|
||||
eidEasyWidget[key] = this.widgetSettings[key]
|
||||
}
|
||||
|
||||
this.$refs.widgetContainer.innerHTML = ''
|
||||
this.$refs.widgetContainer.appendChild(eidEasyWidget)
|
||||
}
|
||||
|
||||
this.$refs.widgetContainer.innerHTML = ''
|
||||
this.$refs.widgetContainer.appendChild(eidEasyWidget)
|
||||
})
|
||||
},
|
||||
async submit () {
|
||||
|
||||
@@ -366,6 +366,7 @@
|
||||
:with-help="withHelp"
|
||||
:default-submitters="defaultSubmitters"
|
||||
:draw-field-type="drawFieldType"
|
||||
:with-fields-search="withFieldsSearch"
|
||||
:default-fields="[...defaultRequiredFields, ...defaultFields]"
|
||||
:template="template"
|
||||
:default-required-fields="defaultRequiredFields"
|
||||
@@ -623,6 +624,11 @@ export default {
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
withFieldsSearch: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
withFieldsList: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -1395,7 +1401,6 @@ export default {
|
||||
const lastArea = field.areas[field.areas.length - 1]
|
||||
|
||||
if (lastArea) {
|
||||
fieldArea.x -= lastArea.w / 2
|
||||
fieldArea.w = lastArea.w
|
||||
fieldArea.h = lastArea.h
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ export default {
|
||||
fields () {
|
||||
if (this.item.submitter_uuid) {
|
||||
return this.template.fields.reduce((acc, f) => {
|
||||
if (f !== this.item && f.submitter_uuid === this.item.submitter_uuid) {
|
||||
if (f !== this.item) {
|
||||
acc.push(f)
|
||||
}
|
||||
|
||||
|
||||
@@ -229,6 +229,11 @@ export default {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
withFieldsSearch: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
template: {
|
||||
type: Object,
|
||||
required: true
|
||||
@@ -297,7 +302,11 @@ export default {
|
||||
fieldNames: FieldType.computed.fieldNames,
|
||||
fieldIcons: FieldType.computed.fieldIcons,
|
||||
isShowFieldSearch () {
|
||||
return this.submitterDefaultFields.length > 15
|
||||
if (this.withFieldsSearch === false) {
|
||||
return false
|
||||
} else {
|
||||
return this.submitterDefaultFields.length > 15
|
||||
}
|
||||
},
|
||||
defaultFieldsIndex () {
|
||||
return this.defaultFields.reduce((acc, field) => {
|
||||
|
||||
@@ -8,6 +8,12 @@ class SendSubmitterInvitationEmailJob
|
||||
|
||||
return if submitter.submission.source == 'invite' && !Accounts.can_send_emails?(submitter.account, on_events: true)
|
||||
|
||||
unless Accounts.can_send_invitation_emails?(submitter.account)
|
||||
Rollbar.warning("Skip email: #{submitter.account.id}") if defined?(Rollbar)
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
mail = SubmitterMailer.invitation_email(submitter)
|
||||
|
||||
Submitters::ValidateSending.call(submitter, mail)
|
||||
@@ -17,6 +23,6 @@ class SendSubmitterInvitationEmailJob
|
||||
SubmissionEvent.create!(submitter:, event_type: 'send_email')
|
||||
|
||||
submitter.sent_at ||= Time.current
|
||||
submitter.save
|
||||
submitter.save!
|
||||
end
|
||||
end
|
||||
@@ -14,8 +14,20 @@ class SubmitterMailer < ApplicationMailer
|
||||
@email_message = submitter.account.email_messages.find_by(uuid: submitter.preferences['email_message_uuid'])
|
||||
end
|
||||
|
||||
@body = @email_message&.body.presence || @submitter.template.preferences['request_email_body'].presence
|
||||
@subject = @email_message&.subject.presence || @submitter.template.preferences['request_email_subject'].presence
|
||||
template_submitters_index =
|
||||
if @email_message.blank?
|
||||
build_submitter_preferences_index(@submitter)
|
||||
else
|
||||
{}
|
||||
end
|
||||
|
||||
@body = @email_message&.body.presence ||
|
||||
template_submitters_index.dig(@submitter.uuid, 'request_email_body').presence ||
|
||||
@submitter.template.preferences['request_email_body'].presence
|
||||
|
||||
@subject = @email_message&.subject.presence ||
|
||||
template_submitters_index.dig(@submitter.uuid, 'request_email_subject').presence ||
|
||||
@submitter.template.preferences['request_email_subject'].presence
|
||||
|
||||
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY)
|
||||
|
||||
@@ -24,14 +36,7 @@ class SubmitterMailer < ApplicationMailer
|
||||
reply_to = build_submitter_reply_to(@submitter)
|
||||
|
||||
I18n.with_locale(@current_account.locale) do
|
||||
subject =
|
||||
if @email_config || @subject
|
||||
ReplaceEmailVariables.call(@subject || @email_config.value['subject'], submitter:)
|
||||
elsif @submitter.with_signature_fields?
|
||||
I18n.t(:you_are_invited_to_sign_a_document)
|
||||
else
|
||||
I18n.t(:you_are_invited_to_submit_a_form)
|
||||
end
|
||||
subject = build_invite_subject(@subject, @email_config, submitter)
|
||||
|
||||
mail(
|
||||
to: @submitter.friendly_name,
|
||||
@@ -196,6 +201,20 @@ class SubmitterMailer < ApplicationMailer
|
||||
user.role == 'integration' ? user.friendly_name.sub(/\+\w+@/, '@') : user.friendly_name
|
||||
end
|
||||
|
||||
def build_invite_subject(subject, email_config, submitter)
|
||||
if email_config || subject
|
||||
ReplaceEmailVariables.call(subject || email_config.value['subject'], submitter:)
|
||||
elsif submitter.with_signature_fields?
|
||||
I18n.t(:you_are_invited_to_sign_a_document)
|
||||
else
|
||||
I18n.t(:you_are_invited_to_submit_a_form)
|
||||
end
|
||||
end
|
||||
|
||||
def build_submitter_preferences_index(submitter)
|
||||
submitter.template.preferences['submitters'].to_a.index_by { |e| e['uuid'] }
|
||||
end
|
||||
|
||||
def add_attachments_with_size_limit(submitter, storage_attachments, current_size, filename_format = nil)
|
||||
total_size = current_size
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ class Submitter < ApplicationRecord
|
||||
has_many_attached :attachments
|
||||
has_many_attached :preview_documents
|
||||
has_many :template_accesses, through: :template
|
||||
has_many :email_events, as: :emailable, dependent: (Docuseal.multitenant? ? nil : :destroy)
|
||||
|
||||
has_many :document_generation_events, dependent: :destroy
|
||||
has_many :submission_events, dependent: :destroy
|
||||
@@ -63,6 +64,8 @@ class Submitter < ApplicationRecord
|
||||
|
||||
scope :completed, -> { where.not(completed_at: nil) }
|
||||
|
||||
after_destroy :anonymize_email_events, if: -> { Docuseal.multitenant? }
|
||||
|
||||
def status
|
||||
if declined_at?
|
||||
'declined'
|
||||
@@ -108,4 +111,12 @@ class Submitter < ApplicationRecord
|
||||
fields.any? { |f| f['submitter_uuid'] == uuid && signature_field_types.include?(f['type']) }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def anonymize_email_events
|
||||
email_events.each do |event|
|
||||
event.update!(email: Digest::MD5.base64digest(event.email))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
|
||||
<path d="M7 16h-4l3.47 -4.66a2 2 0 1 0 -3.47 -1.54"></path>
|
||||
<path d="M10 16v-8h4"></path> <path d="M10 12l3 0"></path>
|
||||
<path d="M17 16v-6a2 2 0 0 1 4 0v6"></path>
|
||||
<path d="M17 13l4 0"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 426 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 397 B |
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
|
||||
<path d="M8 13v-8.5a1.5 1.5 0 0 1 3 0v7.5"></path>
|
||||
<path d="M11 11.5v-2a1.5 1.5 0 0 1 3 0v2.5"></path>
|
||||
<path d="M14 10.5a1.5 1.5 0 0 1 3 0v1.5"></path>
|
||||
<path d="M17 11.5a1.5 1.5 0 0 1 3 0v4.5a6 6 0 0 1 -6 6h-2h.208a6 6 0 0 1 -5.012 -2.7l-.196 -.3c-.312 -.479 -1.407 -2.388 -3.286 -5.728a1.5 1.5 0 0 1 .536 -2.022a1.867 1.867 0 0 1 2.28 .28l1.47 1.47"></path>
|
||||
<path d="M5 3l-1 -1"></path>
|
||||
<path d="M4 7h-1"></path>
|
||||
<path d="M14 3l1 -1"></path>
|
||||
<path d="M15 6h1"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 711 B |
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
|
||||
<path d="M4 12h.01"></path>
|
||||
<path d="M4 6h.01"></path>
|
||||
<path d="M4 18h.01"></path>
|
||||
<path d="M8 18h2"></path>
|
||||
<path d="M8 12h2"></path>
|
||||
<path d="M8 6h2"></path>
|
||||
<path d="M14 6h6"></path>
|
||||
<path d="M14 12h6"></path>
|
||||
<path d="M14 18h6"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 484 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
|
||||
<path d="M3 9l9 6l9 -6l-9 -6l-9 6"></path>
|
||||
<path d="M21 9v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-10"></path>
|
||||
<path d="M3 19l6 -6"></path>
|
||||
<path d="M15 13l6 6"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 401 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
|
||||
<path d="M5 4h4l2 5l-2.5 1.5a11 11 0 0 0 5 5l1.5 -2.5l5 2v4a2 2 0 0 1 -2 2a16 16 0 0 1 -15 -15a2 2 0 0 1 2 -2"></path>
|
||||
<path d="M15 6l2 2l4 -4"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 382 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
|
||||
<path d="M7 4v16l13 -8z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 261 B |
@@ -2,6 +2,9 @@
|
||||
<html data-theme="docuseal" lang="<%= I18n.locale %>">
|
||||
<head>
|
||||
<%= render 'layouts/head_tags' %>
|
||||
<% if Docuseal.enable_pwa? %>
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<% end %>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<%= csrf_meta_tags %>
|
||||
<%= csp_meta_tag %>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "<%= Docuseal.product_name %>",
|
||||
"short_name": "<%= Docuseal.product_name %>",
|
||||
"id": "/",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/logo.svg",
|
||||
"type": "image/svg+xml",
|
||||
"sizes": "any"
|
||||
},
|
||||
{
|
||||
"src": "/apple-touch-icon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
}
|
||||
],
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"orientation": "natural",
|
||||
"description": "<%= Docuseal.product_name %> is an open source platform that provides secure and efficient digital document signing and processing.",
|
||||
"categories": ["productivity", "utilities"],
|
||||
"theme_color": "#FAF7F4",
|
||||
"background_color": "#FAF7F4"
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="space-y-6 mx-auto">
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-center">
|
||||
<%= render 'start_form/docuseal_logo' %>
|
||||
<%= render 'start_form/banner' %>
|
||||
</div>
|
||||
<div class="text-center text-4xl font-bold">
|
||||
<%= t('email_has_been_sent') %>
|
||||
|
||||
@@ -2,5 +2,9 @@
|
||||
---
|
||||
</p>
|
||||
<p>
|
||||
<%= t('sent_using_product_name_free_document_signing_html', product_url: "#{Docuseal::PRODUCT_EMAIL_URL}/start", product_name: Docuseal.product_name) %>
|
||||
<% if @current_account&.testing? %>
|
||||
<%= t('sent_using_product_name_in_testing_mode_html', product_url: "#{Docuseal::PRODUCT_EMAIL_URL}/start", product_name: Docuseal.product_name) %>
|
||||
<% else %>
|
||||
<%= t('sent_using_product_name_free_document_signing_html', product_url: "#{Docuseal::PRODUCT_EMAIL_URL}/start", product_name: Docuseal.product_name) %>
|
||||
<% end %>
|
||||
</p>
|
||||
@@ -0,0 +1,49 @@
|
||||
<% event_colors = %w[bg-red-200 bg-sky-200 bg-emerald-200 bg-yellow-200 bg-purple-200 bg-pink-200 bg-cyan-200 bg-orange-200 bg-lime-200 bg-indigo-200] %>
|
||||
<% submitters_uuids = (@submission.template_submitters || @submission.template.submitters).pluck('uuid') %>
|
||||
<%= render 'shared/turbo_modal_large', title: t('event_log') do %>
|
||||
<div class="pl-8 pr-4 py-4">
|
||||
<ol class="relative border-s border-base-300 space-y-6">
|
||||
<li class="ml-7">
|
||||
<span class="absolute flex items-center justify-center w-7 h-7 rounded-full -start-3.5 ring-8 ring-base-100 text-base-content bg-gray-200">
|
||||
<%= svg_icon('file_text', class: 'w-4 h-4') %>
|
||||
</span>
|
||||
<p class="text-sm leading-none text-base-content/60 pt-1.5">
|
||||
<%= l(@submission.created_at.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) %>
|
||||
</p>
|
||||
<p class="text-base-content/80 mt-1">
|
||||
<% if @submission.source == 'invite' %>
|
||||
<%= t('submission_created_by_email_html', email: @submission.created_by_user.email) %>
|
||||
<% elsif @submission.created_by_user %>
|
||||
<%= t('submission_created_by_email_via_source_html', email: @submission.created_by_user.email, source: t("submission_sources.#{@submission.source}")) %>
|
||||
<% else %>
|
||||
<%= t('submission_created_via_source_html', source: t("submission_sources.#{@submission.source}")) %>
|
||||
<% end %>
|
||||
</p>
|
||||
</li>
|
||||
<% @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] %>
|
||||
<% submitter_name = event.event_type.include?('sms') || event.event_type.include?('phone') ? (event.data['phone'] || submitter.phone) : (submitter.name || submitter.email || submitter.phone) %>
|
||||
<li class="ml-7">
|
||||
<span class="absolute flex items-center justify-center w-7 h-7 rounded-full -start-3.5 ring-8 ring-base-100 text-base-content <%= bg_class %>">
|
||||
<%= svg_icon(SubmissionEventsController::SUBMISSION_EVENT_ICONS.fetch(event.event_type, 'circle_dot'), class: 'w-4 h-4') %>
|
||||
</span>
|
||||
<p class="text-sm leading-none text-base-content/60 pt-1.5">
|
||||
<%= l(event.event_timestamp.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) %>
|
||||
</p>
|
||||
<p class="text-base-content/80 mt-1">
|
||||
<% if event.event_type == 'complete_verification' %>
|
||||
<%= t('submission_event_names.complete_verification_by_html', provider: event.data['method'], submitter_name:) %>
|
||||
<% 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.include?('send_') %>
|
||||
<%= t("submission_event_names.#{event.event_type}_to_html", submitter_name:) %>
|
||||
<% else %>
|
||||
<%= t("submission_event_names.#{event.event_type}_by_html", submitter_name:) %>
|
||||
<% end %>
|
||||
</p>
|
||||
</li>
|
||||
<% end %>
|
||||
</ol>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -1,3 +1,4 @@
|
||||
<% submitter_preferences_index = template.preferences['submitters'].to_a.index_by { |e| e['uuid'] } %>
|
||||
<div class="form-control">
|
||||
<% can_send_emails = Accounts.can_send_emails?(current_account) %>
|
||||
<div class="flex justify-between items-center">
|
||||
@@ -36,10 +37,18 @@
|
||||
<% config = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) %>
|
||||
<div id="message_field" class="card card-compact bg-base-300/40 hidden">
|
||||
<div class="card-body">
|
||||
<div class="form-control space-y-2">
|
||||
<%= tag.input id: toggle_uuid = SecureRandom.uuid, value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: local_assigns[:message_per_submitter] != false && template.preferences['submitters'].to_a.size > 1 %>
|
||||
<div class="peer-checked:hidden form-control space-y-2">
|
||||
<div class="form-control">
|
||||
<%= f.label :subject, t('subject'), class: 'label' %>
|
||||
<%= f.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || template.preferences['request_email_subject'].presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
|
||||
<div class="flex justify-between">
|
||||
<%= f.label :subject, t('subject'), class: 'label' %>
|
||||
<% if template.submitters.size > 1 && template.submitters.size < 5 && local_assigns[:message_per_submitter] != false %>
|
||||
<label for="<%= toggle_uuid %>" class="label underline">
|
||||
<%= t('edit_per_party') %>
|
||||
</label>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= f.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_subject').presence || template.preferences['request_email_subject'].presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<div class="flex items-center">
|
||||
@@ -49,7 +58,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<autoresize-textarea>
|
||||
<%= f.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || template.preferences['request_email_body'].presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
|
||||
<%= f.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_body').presence || template.preferences['request_email_body'].presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
|
||||
</autoresize-textarea>
|
||||
<% unless local_assigns.fetch(:disable_save_as_default_template_option, false) %>
|
||||
<label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer">
|
||||
@@ -60,5 +69,44 @@
|
||||
</div>
|
||||
<%= render 'submissions/message_fields' %>
|
||||
</div>
|
||||
<% if template.submitters.size > 1 && template.submitters.size < 5 && local_assigns[:message_per_submitter] != false %>
|
||||
<div class="hidden peer-checked:block form-control space-y-2">
|
||||
<% uuid = SecureRandom.uuid %>
|
||||
<% options = template.submitters.map { |e| [e['name'], "request_email_#{uuid}_#{e['uuid']}"] } %>
|
||||
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="flex relative px-1">
|
||||
<ul class="tabs w-full flex flex-nowrap">
|
||||
<% options.each_with_index do |(label, val), index| %>
|
||||
<div class="w-full">
|
||||
<%= f.radio_button :selected, val, checked: index.zero?, id: "#{val}_radio", data: { action: 'click:toggle-visible#trigger' }, class: 'hidden peer' %>
|
||||
<%= f.label :selected, label, value: val, for: "#{val}_radio", class: 'tab w-full tab-lifted peer-checked:tab-active !bg-transparent' %>
|
||||
</div>
|
||||
<% end %>
|
||||
</ul>
|
||||
</toggle-visible>
|
||||
<% template.submitters.each_with_index do |submitter, index| %>
|
||||
<%= fields_for :submitter_preferences, nil, index: submitter['uuid'] do |ff| %>
|
||||
<div id="request_email_<%= uuid %>_<%= submitter['uuid'] %>" class="<%= 'hidden' if index != 0 %>">
|
||||
<div class="form-control">
|
||||
<div class="flex justify-between">
|
||||
<%= ff.label :subject, t('subject'), class: 'label' %>
|
||||
</div>
|
||||
<%= ff.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_subject').presence || template.preferences['request_email_subject'].presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<div class="flex items-center">
|
||||
<%= ff.label :message, t('body'), class: 'label' %>
|
||||
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
|
||||
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
||||
</span>
|
||||
</div>
|
||||
<autoresize-textarea>
|
||||
<%= ff.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_body').presence || template.preferences['request_email_body'].presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
|
||||
</autoresize-textarea>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,6 +1,7 @@
|
||||
<% align = field.dig('preferences', 'align') %>
|
||||
<% color = field.dig('preferences', 'color') %>
|
||||
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-serif' if field.dig('preferences', 'font') == 'Times' %> <%= align == 'right' ? 'justify-end' : (align == 'center' ? 'justify-center' : '') %>" style="<%= "color: #{color}; " if color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size'].to_i * 1.23}pt); line-height: `clamp(6pt, 2.0vw, #{(field['preferences']['font_size'].to_i * 1.23) + 3}pt)`" if field.dig('preferences', 'font_size') %>">
|
||||
<% font = field.dig('preferences', 'font') %>
|
||||
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-mono' if font == 'Courier' %> <%= 'font-serif' if font == 'Times' %> <%= align == 'right' ? 'justify-end' : (align == 'center' ? 'justify-center' : '') %>" style="<%= "color: #{color}; " if color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size'].to_i * 1.23}pt); line-height: `clamp(6pt, 2.0vw, #{(field['preferences']['font_size'].to_i * 1.23) + 3}pt)`" if field.dig('preferences', 'font_size') %>">
|
||||
<% if field['type'] == 'signature' %>
|
||||
<div class="flex flex-col justify-between h-full overflow-hidden">
|
||||
<div class="flex-grow flex overflow-hidden" style="min-height: 50%">
|
||||
|
||||
@@ -19,6 +19,11 @@
|
||||
<%= svg_icon('external_link', class: 'w-6 h-6') %>
|
||||
<span class="hidden md:inline"><%= t('audit_log') %></span>
|
||||
</a>
|
||||
<% else %>
|
||||
<%= link_to submission_events_path(@submission), class: 'white-button', data: { turbo_frame: :modal } do %>
|
||||
<%= svg_icon('logs', class: 'w-6 h-6') %>
|
||||
<span class="hidden md:block"><%= t('event_log') %></span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if last_submitter %>
|
||||
<% if is_all_completed || !is_combined_enabled %>
|
||||
@@ -232,7 +237,7 @@
|
||||
<% elsif field['type'] == 'date' %>
|
||||
<%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), @submission.account.locale) %>
|
||||
<% else %>
|
||||
<%= Array.wrap(value).join(', ') %>
|
||||
<div class="whitespace-pre-wrap"><%= Array.wrap(value).join(', ') %></div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<% query_params = params.permit(:q).merge(filter_params) %>
|
||||
<% if icon = { 'declined' => 'x_circle', 'expired' => 'clock_cancel', 'partially_completed' => 'clock_edit' }[params[:status]] %>
|
||||
<% if icon = { 'declined' => 'x_circle', 'expired' => 'clock_cancel', 'partially_completed' => 'clock_edit', 'sent' => 'send', 'opened' => 'mail_opened' }[params[:status]] %>
|
||||
<div class="flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-34 border-neutral-700">
|
||||
<%= link_to submissions_filter_path('status', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 w-full pr-1 md:max-w-[140px]' do %>
|
||||
<%= svg_icon(icon, class: 'w-5 h-5 shrink-0') %>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="flex flex-col md:flex-row gap-2 mt-5">
|
||||
<div class="form-control w-full">
|
||||
<div id="status" class="radio-select grid grid-cols-2 gap-2 px-1">
|
||||
<% ['', 'pending', 'completed', 'partially_completed', 'declined', 'expired'].each do |status| %>
|
||||
<% ['', 'pending', 'completed', 'partially_completed', 'sent', 'opened', 'declined', 'expired'].each do |status| %>
|
||||
<label class="radio-label cursor-pointer inline-flex items-center space-x-2">
|
||||
<%= radio_button_tag 'status', status, params[:status] == status || (status == '' && params[:status].blank?), class: 'base-radio' %>
|
||||
<span><%= t(status.presence || 'all') %></span>
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
|
||||
<div class="space-y-6 mx-auto">
|
||||
<div class="space-y-6">
|
||||
<% if Docuseal.multitenant? %>
|
||||
<div class="flex items-center justify-center">
|
||||
<%= render 'start_form/docuseal_logo' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="flex items-center justify-center">
|
||||
<%= render 'start_form/banner' %>
|
||||
</div>
|
||||
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
|
||||
<div class="flex items-center">
|
||||
<div class="mr-3">
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<%= render partial: 'submissions/annotation', collection: annots, as: :annot %>
|
||||
@@ -1,5 +1,5 @@
|
||||
<% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid created_at], methods: %i[url filename content_type]) %>
|
||||
<% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %>
|
||||
<% 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-confetti="<%= configs[:with_confetti] %>" data-completed-redirect-url="<%= submitter.preferences['completed_redirect_url'].presence || submitter.submission.template.preferences['completed_redirect_url'] %>" data-completed-message="<%= (configs[:completed_message]&.compact_blank.presence || submitter.submission.template.preferences['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>
|
||||
@@ -47,8 +47,8 @@
|
||||
<div class="relative my-4 shadow-md">
|
||||
<img loading="lazy" src="<%= page.url %>" width="<%= page.metadata['width'] %>" height="<%= page.metadata['height'] %>">
|
||||
<div id="page-<%= [document.uuid, index].join('-') %>" class="top-0 bottom-0 left-0 right-0 absolute">
|
||||
<% document_annots_index[index]&.each do |annot| %>
|
||||
<%= render 'submissions/annotation', annot: %>
|
||||
<% if annots = document_annots_index[index] %>
|
||||
<%= render 'submit_form/annotations', annots: %>
|
||||
<% end %>
|
||||
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
|
||||
<% value = values[field['uuid']].presence || (field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submitter.submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</submitter-item>
|
||||
</div>
|
||||
<div>
|
||||
<%= render 'submissions/send_email', f:, template: @submitter.template, submitter: @submitter, resend_email: @submitter.sent_at?, submitter_email_message: @submitter_email_message, disable_save_as_default_template_option: true %>
|
||||
<%= render 'submissions/send_email', f:, template: @submitter.template, submitter: @submitter, resend_email: @submitter.sent_at?, submitter_email_message: @submitter_email_message, disable_save_as_default_template_option: true, message_per_submitter: false %>
|
||||
<%= render 'submissions/send_sms', f:, resend_sms: @submitter.sent_at? %>
|
||||
</div>
|
||||
<div class="form-control mt-4">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %>
|
||||
<div class="flex justify-between items-center w-full mb-4">
|
||||
<div class="flex items-center flex-grow min-w-0">
|
||||
<% if has_archived || @pagy.count > 0 %>
|
||||
<% if has_archived || @pagy.count > 0 || @template_folders.present? %>
|
||||
<div class="mr-2">
|
||||
<%= render 'dashboard/toggle_view', selected: 'templates' %>
|
||||
</div>
|
||||
|
||||
@@ -83,21 +83,71 @@
|
||||
<div class="collapse-content">
|
||||
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %>
|
||||
<toggle-on-submit data-element-id="email_saved_alert1"></toggle-on-submit>
|
||||
<%= f.fields_for :preferences, Struct.new(:request_email_subject, :request_email_body).new(*(@template.preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence || AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY).value.values_at('subject', 'body'))) do |ff| %>
|
||||
<div class="form-control">
|
||||
<%= ff.label :request_email_subject, t('email_subject'), class: 'label' %>
|
||||
<%= ff.text_field :request_email_subject, required: true, class: 'base-input', dir: 'auto' %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<div class="flex items-center">
|
||||
<%= ff.label :request_email_body, t('email_body'), class: 'label' %>
|
||||
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
|
||||
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
||||
</span>
|
||||
<%= tag.input id: 'request_email_per_submitter', value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: @template.preferences['submitters'].to_a.size > 1 %>
|
||||
<div class="peer-checked:hidden">
|
||||
<%= f.fields_for :preferences, Struct.new(:request_email_subject, :request_email_body).new(*(@template.preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence || AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY).value.values_at('subject', 'body'))) do |ff| %>
|
||||
<div class="form-control">
|
||||
<div class="flex justify-between">
|
||||
<%= ff.label :request_email_subject, t('email_subject'), class: 'label' %>
|
||||
<% if @template.submitters.size > 1 && @template.submitters.size < 5 %>
|
||||
<label for="request_email_per_submitter" class="label underline">
|
||||
<%= t('edit_per_party') %>
|
||||
</label>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= ff.text_field :request_email_subject, required: true, class: 'base-input', dir: 'auto' %>
|
||||
</div>
|
||||
<autoresize-textarea>
|
||||
<%= ff.text_area :request_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
|
||||
</autoresize-textarea>
|
||||
<div class="form-control">
|
||||
<div class="flex items-center">
|
||||
<%= ff.label :request_email_body, t('email_body'), class: 'label' %>
|
||||
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
|
||||
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
||||
</span>
|
||||
</div>
|
||||
<autoresize-textarea>
|
||||
<%= ff.text_area :request_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
|
||||
</autoresize-textarea>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if @template.submitters.size > 1 && @template.submitters.size < 5 %>
|
||||
<div class="hidden peer-checked:block">
|
||||
<% options = @template.submitters.map { |e| [e['name'], "request_email_#{e['uuid']}"] } %>
|
||||
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="flex relative px-1">
|
||||
<ul class="tabs w-full flex flex-nowrap mb-2">
|
||||
<% options.each_with_index do |(label, val), index| %>
|
||||
<div class="w-full">
|
||||
<%= f.radio_button :selected, val, checked: index.zero?, id: "#{val}_radio", data: { action: 'click:toggle-visible#trigger' }, class: 'hidden peer' %>
|
||||
<%= f.label :selected, label, value: val, for: "#{val}_radio", class: 'tab w-full tab-lifted peer-checked:tab-active' %>
|
||||
</div>
|
||||
<% end %>
|
||||
</ul>
|
||||
</toggle-visible>
|
||||
<%= f.fields_for :preferences do |ff| %>
|
||||
<% @template.submitters.each_with_index do |submitter, index| %>
|
||||
<div id="request_email_<%= submitter['uuid'] %>" class="<%= 'hidden' if index != 0 %>">
|
||||
<% submitter_preferences = f.object.preferences['submitters'].to_a.find { |e| e['uuid'] == submitter['uuid'] } || {} %>
|
||||
<%= ff.fields_for :submitters, Struct.new(:request_email_subject, :request_email_body).new(*(submitter_preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence || @template.preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence || AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY).value.values_at('subject', 'body'))), index: nil do |fff| %>
|
||||
<%= fff.hidden_field :uuid, value: submitter['uuid'] %>
|
||||
<div class="form-control">
|
||||
<%= fff.label :request_email_subject, t('email_subject'), class: 'label' %>
|
||||
<%= fff.text_field :request_email_subject, required: true, class: 'base-input', dir: 'auto' %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<div class="flex items-center">
|
||||
<%= fff.label :request_email_body, t('email_body'), class: 'label' %>
|
||||
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
|
||||
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
||||
</span>
|
||||
</div>
|
||||
<autoresize-textarea>
|
||||
<%= fff.text_area :request_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
|
||||
</autoresize-textarea>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="form-control pt-2">
|
||||
@@ -235,12 +285,12 @@
|
||||
<div class="form-control">
|
||||
<%= ff.text_field :name, class: 'w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer mb-2', autocomplete: 'off', placeholder: "#{index + 1}#{(index + 1).ordinal} Party", required: true %>
|
||||
<% if @template.submitters.size == 2 %>
|
||||
<%= ff.email_field :email, class: 'base-input', autocomplete: 'off', placeholder: t('default_email'), disabled: ff.object.is_requester || ff.object.invite_by_uuid.present? || ff.object.optional_invite_by_uuid.present?, id: field_uuid = SecureRandom.uuid %>
|
||||
<%= tag.input name: ff.field_name(:email), value: ff.object.email, type: :email, class: 'base-input', multiple: true, autocomplete: 'off', placeholder: t('default_email'), disabled: ff.object.is_requester || ff.object.invite_by_uuid.present? || ff.object.optional_invite_by_uuid.present?, id: field_uuid = SecureRandom.uuid %>
|
||||
<% else %>
|
||||
<toggle-attribute data-target-id="<%= email_field_uuid = SecureRandom.uuid %>" data-class-name="hidden" data-value="email">
|
||||
<%= ff.select :option, [[t('not_specified'), 'not_set'], [t('submission_requester'), 'is_requester'], [t('specified_email'), 'email'], *(@template.submitters - [submitter]).flat_map { |e| [[t('invite_by_name', name: e['name']), "invite_by_#{e['uuid']}"], [t('invite_by_name', name: e['name']) + " (#{t(:optional).capitalize})", "optional_invite_by_#{e['uuid']}"]] }, *(@template.submitters - [submitter]).map { |e| [t('same_as_name', name: e['name']), "linked_to_#{e['uuid']}"] }], {}, class: 'base-select mb-3' %>
|
||||
</toggle-attribute>
|
||||
<%= ff.email_field :email, class: "base-input #{'hidden' if item.option != 'email'}", autocomplete: 'off', placeholder: t('default_email'), id: email_field_uuid %>
|
||||
<%= tag.input name: ff.field_name(:email), type: :email, value: ff.object.email, multiple: true, class: "base-input #{'hidden' if item.option != 'email'}", autocomplete: 'off', placeholder: t('default_email'), id: email_field_uuid %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if @template.submitters.size == 2 %>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<%= user.last_sign_in_at ? l(user.last_sign_in_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) : '-' %>
|
||||
<%= user.current_sign_in_at ? l(user.current_sign_in_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) : '-' %>
|
||||
</td>
|
||||
<td class="flex items-center space-x-2 justify-end">
|
||||
<% if params[:status].blank? && can?(:update, user) && user.archived_at.blank? %>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"ignored_warnings": [
|
||||
{
|
||||
"fingerprint": "25f4ce5fee1e1180fa1919dc4ee78db3ab3457a956e4679503aa745771a43836",
|
||||
"note": "Permitted parameters are necessary for creating submitters via API"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -20,6 +20,7 @@ en: &en
|
||||
language_ko: 한국어
|
||||
hi_there: Hi there
|
||||
thanks: Thanks
|
||||
edit_per_party: Edit per party
|
||||
reply_to: Reply to
|
||||
pending_by_me: Pending by me
|
||||
partially_completed: Partially completed
|
||||
@@ -42,6 +43,7 @@ en: &en
|
||||
hello_name: Hello %{name}
|
||||
you_are_invited_to_product_name: You are invited to %{product_name}
|
||||
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'You have been invited to %{account_name} %{product_name}. Please sign up using the link below:'
|
||||
sent_using_product_name_in_testing_mode_html: 'Sent using <a href="%{product_url}">%{product_name}</a> in testing mode'
|
||||
sent_using_product_name_free_document_signing_html: 'Sent using <a href="%{product_url}">%{product_name}</a> free document signing.'
|
||||
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Sign documents with trusted certificate provided by DocuSeal. Your documents and data are never shared with DocuSeal. PDF checksum is provided to generate a trusted signature.
|
||||
you_have_been_invited_to_submit_the_name_form: 'You have been invited to submit the "%{name}" form.'
|
||||
@@ -690,6 +692,20 @@ en: &en
|
||||
extremely_satisfied: Extremely Satisfied
|
||||
your_pro_plan_payment_is_overdue: Your Pro plan payment is overdue.
|
||||
click_here_to_update_your_payment_details_and_clear_the_invoice_to_ensure_uninterrupted_service_html: <a href="%{url}" class="link">Click here</a> to update your payment details and clear the invoice to ensure uninterrupted service.
|
||||
overdue_payment: Overdue Payment
|
||||
your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: Your Pro Plan has been suspended due to unpaid invoices. You can update your payment details to settle the invoice and continue using DocuSeal or cancel your subscription.
|
||||
manage_subscription: Manage Subscription
|
||||
submission_created_by_email_html: '<b>Submission created</b> by %{email}'
|
||||
submission_created_by_email_via_source_html: '<b>Submission created</b> by %{email} via %{source}'
|
||||
submission_created_via_source_html: '<b>Submission created</b> via %{source}'
|
||||
pro_user_seats_used: Pro user seats used
|
||||
manage_plan: Manage plan
|
||||
submission_sources:
|
||||
api: API
|
||||
bulk: Bulk Send
|
||||
embed: Embedding
|
||||
invie: Invite
|
||||
link: Link
|
||||
submission_event_names:
|
||||
send_email_to_html: '<b>Email sent</b> to %{submitter_name}'
|
||||
send_reminder_email_to_html: '<b>Reminder email sent</b> to %{submitter_name}'
|
||||
@@ -728,6 +744,7 @@ en: &en
|
||||
read: Read your data
|
||||
|
||||
es: &es
|
||||
edit_per_party: Editar por parte
|
||||
signed: Firmado
|
||||
reply_to: Responder a
|
||||
partially_completed: Parcialmente completado
|
||||
@@ -752,6 +769,7 @@ es: &es
|
||||
hello_name: Hola %{name}
|
||||
you_are_invited_to_product_name: Estás invitado a %{product_name}
|
||||
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Has sido invitado a %{account_name} %{product_name}. Por favor, regístrate usando el enlace a continuación:'
|
||||
sent_using_product_name_in_testing_mode_html: 'Enviado usando <a href="%{product_url}">%{product_name}</a> en Modo de Prueba'
|
||||
sent_using_product_name_free_document_signing_html: 'Enviado usando la firma de documentos gratuita de <a href="%{product_url}">%{product_name}</a>.'
|
||||
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Firme documentos con un certificado de confianza proporcionado por DocuSeal. Sus documentos y datos nunca se comparten con DocuSeal. Se proporciona un checksum de PDF para generar una firma de confianza.
|
||||
hi_there: Hola
|
||||
@@ -1400,6 +1418,20 @@ es: &es
|
||||
extremely_satisfied: Extremadamente satisfecho
|
||||
your_pro_plan_payment_is_overdue: El pago de tu plan Pro está atrasado.
|
||||
click_here_to_update_your_payment_details_and_clear_the_invoice_to_ensure_uninterrupted_service_html: '<a href="%{url}" class="link">Haz clic aquí</a> para actualizar tus datos de pago y liquidar la factura para garantizar un servicio ininterrumpido.'
|
||||
overdue_payment: Pago Atrasado
|
||||
your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: Tu plan Pro ha sido suspendido debido a facturas impagas. Puedes actualizar tus datos de pago para liquidar la factura y seguir usando DocuSeal o cancelar tu suscripción.
|
||||
manage_subscription: Gestionar Suscripción
|
||||
submission_created_by_email_html: '<b>Envío creado</b> por %{email}'
|
||||
submission_created_by_email_via_source_html: '<b>Envío creado</b> por %{email} a través de %{source}'
|
||||
submission_created_via_source_html: '<b>Envío creado</b> a través de %{source}'
|
||||
pro_user_seats_used: Plazas de usuario Pro en uso
|
||||
manage_plan: Gestionar plan
|
||||
submission_sources:
|
||||
api: API
|
||||
bulk: Envío masivo
|
||||
embed: Integración
|
||||
invite: Invitación
|
||||
link: Enlace
|
||||
submission_event_names:
|
||||
send_email_to_html: '<b>Correo electrónico enviado</b> a %{submitter_name}'
|
||||
send_reminder_email_to_html: '<b>Correo de recordatorio enviado</b> a %{submitter_name}'
|
||||
@@ -1438,6 +1470,7 @@ es: &es
|
||||
read: Leer tus datos
|
||||
|
||||
it: &it
|
||||
edit_per_party: Modifica per partito
|
||||
signed: Firmato
|
||||
reply_to: Rispondi a
|
||||
pending_by_me: In sospeso da me
|
||||
@@ -1461,6 +1494,7 @@ it: &it
|
||||
hello_name: Ciao %{name}
|
||||
you_are_invited_to_product_name: Sei stato invitato a %{product_name}
|
||||
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Sei stato invitato a %{account_name} %{product_name}. Registrati utilizzando il link qui sotto:'
|
||||
sent_using_product_name_in_testing_mode_html: 'Inviato utilizzando <a href="%{product_url}">%{product_name}</a> in Modalità di Test'
|
||||
sent_using_product_name_free_document_signing_html: 'Inviato utilizzando la firma di documenti gratuita di <a href="%{product_url}">%{product_name}</a>.'
|
||||
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: "Firma documenti con un certificato di fiducia fornito da DocuSeal. I tuoi documenti e i tuoi dati non vengono mai condivisi con DocuSeal. Il checksum PDF è fornito per generare una firma di fiducia."
|
||||
hi_there: Ciao
|
||||
@@ -2109,6 +2143,20 @@ it: &it
|
||||
extremely_satisfied: Estremamente soddisfatto
|
||||
your_pro_plan_payment_is_overdue: Il pagamento del tuo piano Pro è in ritardo.
|
||||
click_here_to_update_your_payment_details_and_clear_the_invoice_to_ensure_uninterrupted_service_html: '<a href="%{url}" class="link">Fai clic qui</a> per aggiornare i tuoi dati di pagamento e saldare la fattura per garantire un servizio ininterrotto.'
|
||||
overdue_payment: Pagamento Scaduto
|
||||
your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: Il tuo piano Pro è stato sospeso a causa di fatture non pagate. Puoi aggiornare i tuoi dati di pagamento per saldare la fattura e continuare a utilizzare DocuSeal o annullare l'abbonamento.
|
||||
manage_subscription: Gestisci Abbonamento
|
||||
submission_created_by_email_html: '<b>Invio creato</b> da %{email}'
|
||||
submission_created_by_email_via_source_html: '<b>Invio creato</b> da %{email} tramite %{source}'
|
||||
submission_created_via_source_html: '<b>Invio creato</b> tramite %{source}'
|
||||
pro_user_seats_used: Posti utente Pro in uso
|
||||
manage_plan: Gestisci piano
|
||||
submission_sources:
|
||||
api: API
|
||||
bulk: Invio massivo
|
||||
embed: Incorporamento
|
||||
invite: Invito
|
||||
link: Link
|
||||
submission_event_names:
|
||||
send_email_to_html: '<b>E-mail inviato</b> a %{submitter_name}'
|
||||
send_reminder_email_to_html: '<b>E-mail di promemoria inviato</b> a %{submitter_name}'
|
||||
@@ -2147,6 +2195,7 @@ it: &it
|
||||
read: Leggi i tuoi dati
|
||||
|
||||
fr: &fr
|
||||
edit_per_party: Éditer par partie
|
||||
signed: Signé
|
||||
reply_to: Répondre à
|
||||
partially_completed: Partiellement complété
|
||||
@@ -2171,6 +2220,7 @@ fr: &fr
|
||||
hello_name: Bonjour %{name}
|
||||
you_are_invited_to_product_name: Vous êtes invité à %{product_name}
|
||||
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Vous avez été invité à %{account_name} %{product_name}. Veuillez vous inscrire en utilisant le lien ci-dessous:'
|
||||
sent_using_product_name_in_testing_mode_html: 'Envoyé en utilisant <a href="%{product_url}">%{product_name}</a> en Mode Test'
|
||||
sent_using_product_name_free_document_signing_html: 'Envoyé en utilisant la signature de documents gratuite de <a href="%{product_url}">%{product_name}</a>.'
|
||||
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Signez des documents avec un certificat de confiance fourni par DocuSeal. Vos documents et données ne sont jamais partagés avec DocuSeal. Un checksum PDF est fourni pour générer une signature de confiance.
|
||||
hi_there: Bonjour
|
||||
@@ -2820,6 +2870,20 @@ fr: &fr
|
||||
extremely_satisfied: Extrêmement satisfait
|
||||
your_pro_plan_payment_is_overdue: Le paiement de votre plan Pro est en retard.
|
||||
click_here_to_update_your_payment_details_and_clear_the_invoice_to_ensure_uninterrupted_service_html: '<a href="%{url}" class="link">Cliquez ici</a> pour mettre à jour vos informations de paiement et régler la facture afin de garantir un service ininterrompu.'
|
||||
overdue_payment: Paiement En Retard
|
||||
your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: Votre plan Pro a été suspendu en raison de factures impayées. Vous pouvez mettre à jour vos informations de paiement pour régler la facture et continuer à utiliser DocuSeal ou annuler votre abonnement.
|
||||
manage_subscription: Gérer l'Abonnement
|
||||
submission_created_by_email_html: '<b>Soumission créée</b> par %{email}'
|
||||
submission_created_by_email_via_source_html: '<b>Soumission créée</b> par %{email} via %{source}'
|
||||
submission_created_via_source_html: '<b>Soumission créée</b> via %{source}'
|
||||
pro_user_seats_used: Places utilisateur Pro en cours d'utilisation
|
||||
manage_plan: Gérer le plan
|
||||
submission_sources:
|
||||
api: API
|
||||
bulk: Envoi en masse
|
||||
embed: Intégration
|
||||
invite: Invitation
|
||||
link: Lien
|
||||
submission_event_names:
|
||||
send_email_to_html: '<b>E-mail envoyé</b> à %{submitter_name}'
|
||||
send_reminder_email_to_html: '<b>E-mail de rappel envoyé</b> à %{submitter_name}'
|
||||
@@ -2858,6 +2922,7 @@ fr: &fr
|
||||
read: Lire vos données
|
||||
|
||||
pt: &pt
|
||||
edit_per_party: Edita por festa
|
||||
signed: Assinado
|
||||
reply_to: Responder a
|
||||
partially_completed: Parcialmente concluído
|
||||
@@ -2882,6 +2947,7 @@ pt: &pt
|
||||
hello_name: Olá %{name}
|
||||
you_are_invited_to_product_name: Você está convidado para %{product_name}
|
||||
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Você foi convidado para %{account_name} %{product_name}. Inscreva-se usando o link abaixo:'
|
||||
sent_using_product_name_in_testing_mode_html: 'Enviado usando <a href="%{product_url}">%{product_name}</a> no Modo de Teste'
|
||||
sent_using_product_name_free_document_signing_html: 'Enviado usando a assinatura gratuita de documentos de <a href="%{product_url}">%{product_name}</a>.'
|
||||
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Assine documentos com certificado confiável fornecido pela DocuSeal. Seus documentos e dados nunca são compartilhados com a DocuSeal. O checksum do PDF é fornecido para gerar uma assinatura confiável.
|
||||
hi_there: Olá
|
||||
@@ -3530,6 +3596,20 @@ pt: &pt
|
||||
extremely_satisfied: Extremamente satisfeito
|
||||
your_pro_plan_payment_is_overdue: O pagamento do seu plano Pro está atrasado.
|
||||
click_here_to_update_your_payment_details_and_clear_the_invoice_to_ensure_uninterrupted_service_html: '<a href="%{url}" class="link">Clique aqui</a> para atualizar seus dados de pagamento e quitar a fatura para garantir um serviço ininterrupto.'
|
||||
overdue_payment: Pagamento Atrasado
|
||||
your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: Seu plano Pro foi suspenso devido a faturas não pagas. Você pode atualizar seus dados de pagamento para quitar a fatura e continuar usando o DocuSeal ou cancelar sua assinatura.
|
||||
manage_subscription: Gerenciar Assinatura
|
||||
submission_created_by_email_html: '<b>Envio criado</b> por %{email}'
|
||||
submission_created_by_email_via_source_html: '<b>Envio criado</b> por %{email} via %{source}'
|
||||
submission_created_via_source_html: '<b>Envio criado</b> via %{source}'
|
||||
pro_user_seats_used: Lugares de usuário Pro em uso
|
||||
manage_plan: Gerenciar plano
|
||||
submission_sources:
|
||||
api: API
|
||||
bulk: Envio em massa
|
||||
embed: Incorporação
|
||||
invite: Convite
|
||||
link: Link
|
||||
submission_event_names:
|
||||
send_email_to_html: '<b>E-mail enviado</b> para %{submitter_name}'
|
||||
send_reminder_email_to_html: '<b>E-mail de lembrete enviado</b> para %{submitter_name}'
|
||||
@@ -3568,6 +3648,7 @@ pt: &pt
|
||||
read: Ler seus dados
|
||||
|
||||
de: &de
|
||||
edit_per_party: Bearbeiten pro Partei
|
||||
signed: Unterschrieben
|
||||
reply_to: Antworten auf
|
||||
partially_completed: Teilweise abgeschlossen
|
||||
@@ -3592,6 +3673,7 @@ de: &de
|
||||
hello_name: Hallo %{name}
|
||||
you_are_invited_to_product_name: Sie sind eingeladen zu %{product_name}
|
||||
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Sie wurden zu %{account_name} %{product_name} eingeladen. Bitte registrieren Sie sich über den folgenden Link:'
|
||||
sent_using_product_name_in_testing_mode_html: 'Gesendet über <a href="%{product_url}">%{product_name}</a> im Testmodus'
|
||||
sent_using_product_name_free_document_signing_html: 'Gesendet mit der kostenlosen Dokumentensignierung von <a href="%{product_url}">%{product_name}</a>.'
|
||||
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Unterzeichnen Sie Dokumente mit einem vertrauenswürdigen Zertifikat von DocuSeal. Ihre Dokumente und Daten werden niemals mit DocuSeal geteilt. Eine PDF-Prüfziffer wird bereitgestellt, um eine vertrauenswürdige Signatur zu generieren.
|
||||
hi_there: Hallo
|
||||
@@ -4240,6 +4322,20 @@ de: &de
|
||||
extremely_satisfied: Extrem zufrieden
|
||||
your_pro_plan_payment_is_overdue: Ihre Zahlung für den Pro-Plan ist überfällig.
|
||||
click_here_to_update_your_payment_details_and_clear_the_invoice_to_ensure_uninterrupted_service_html: '<a href="%{url}" class="link">Klicken Sie hier</a>, um Ihre Zahlungsdaten zu aktualisieren und die Rechnung zu begleichen, um einen unterbrechungsfreien Service sicherzustellen.'
|
||||
overdue_payment: Überfällige Zahlung
|
||||
your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: Ihr Pro-Plan wurde aufgrund unbezahlter Rechnungen ausgesetzt. Sie können Ihre Zahlungsdaten aktualisieren, um die Rechnung zu begleichen und DocuSeal weiterhin zu nutzen, oder Ihr Abonnement kündigen.
|
||||
manage_subscription: Abonnement Verwalten
|
||||
submission_created_by_email_html: '<b>Übermittlung erstellt</b> von %{email}'
|
||||
submission_created_by_email_via_source_html: '<b>Übermittlung erstellt</b> durch %{email} über %{source}'
|
||||
submission_created_via_source_html: '<b>Übermittlung erstellt</b> über %{source}'
|
||||
pro_user_seats_used: Verwendete Pro-Benutzerplätze
|
||||
manage_plan: Plan verwalten
|
||||
submission_sources:
|
||||
api: API
|
||||
bulk: Massenversand
|
||||
embed: Einbettung
|
||||
invite: Einladung
|
||||
link: Link
|
||||
submission_event_names:
|
||||
send_email_to_html: '<b>E-Mail gesendet</b> an %{submitter_name}'
|
||||
send_reminder_email_to_html: '<b>Erinnerungs-E-Mail gesendet</b> an %{submitter_name}'
|
||||
|
||||
@@ -12,6 +12,7 @@ Rails.application.routes.draw do
|
||||
root 'dashboard#index'
|
||||
|
||||
get 'up' => 'rails/health#show'
|
||||
get 'manifest' => 'pwa#manifest'
|
||||
|
||||
devise_for :users,
|
||||
path: '/', only: %i[sessions passwords omniauth_callbacks],
|
||||
@@ -71,6 +72,7 @@ Rails.application.routes.draw do
|
||||
resources :submissions, only: %i[index], controller: 'submissions_dashboard'
|
||||
resources :submissions, only: %i[show destroy] do
|
||||
resources :unarchive, only: %i[create], controller: 'submissions_unarchive'
|
||||
resources :events, only: %i[index], controller: 'submission_events'
|
||||
end
|
||||
resources :submitters, only: %i[edit update]
|
||||
resources :console_redirect, only: %i[index]
|
||||
@@ -144,9 +146,7 @@ Rails.application.routes.draw do
|
||||
get :completed
|
||||
end
|
||||
|
||||
resources :send_submission_email, only: %i[create] do
|
||||
get :success, on: :collection
|
||||
end
|
||||
resources :send_submission_email, only: %i[create]
|
||||
|
||||
resources :submitters, only: %i[], param: 'slug' do
|
||||
resources :download, only: %i[index], controller: 'submissions_download'
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CreateConsole1984Tables < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
create_table :console1984_sessions do |t|
|
||||
t.text :reason
|
||||
t.references :user, null: false, index: false
|
||||
t.timestamps
|
||||
|
||||
t.index :created_at
|
||||
t.index %i[user_id created_at]
|
||||
end
|
||||
|
||||
create_table :console1984_users do |t|
|
||||
t.string :username, null: false
|
||||
t.timestamps
|
||||
|
||||
t.index [:username]
|
||||
end
|
||||
|
||||
create_table :console1984_commands do |t|
|
||||
t.text :statements
|
||||
t.references :sensitive_access
|
||||
t.references :session, null: false, index: false
|
||||
t.timestamps
|
||||
|
||||
t.index %i[session_id created_at sensitive_access_id], name: 'on_session_and_sensitive_chronologically'
|
||||
end
|
||||
|
||||
create_table :console1984_sensitive_accesses do |t|
|
||||
t.text :justification
|
||||
t.references :session, null: false
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.2].define(version: 2024_12_07_172237) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_02_25_111255) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
||||
@@ -112,6 +112,40 @@ ActiveRecord::Schema[7.2].define(version: 2024_12_07_172237) do
|
||||
t.index ["submitter_id"], name: "index_completed_submitters_on_submitter_id", unique: true
|
||||
end
|
||||
|
||||
create_table "console1984_commands", force: :cascade do |t|
|
||||
t.text "statements"
|
||||
t.bigint "sensitive_access_id"
|
||||
t.bigint "session_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
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.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.datetime "updated_at", 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.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
|
||||
|
||||
@@ -25,7 +25,7 @@ module Abilities
|
||||
account_ids = [user.account_id, TemplateSharing::ALL_ID]
|
||||
|
||||
template.template_sharings.any? do |e|
|
||||
e.account_id.in?(account_ids) && (ability.nil? || e.ability == ability)
|
||||
e.account_id.in?(account_ids) && (ability.nil? || e.ability == 'manage' || e.ability == ability)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -153,6 +153,10 @@ module Accounts
|
||||
EncryptedConfig.exists?(key: EncryptedConfig::EMAIL_SMTP_KEY)
|
||||
end
|
||||
|
||||
def can_send_invitation_emails?(_account)
|
||||
true
|
||||
end
|
||||
|
||||
def normalize_timezone(timezone)
|
||||
tzinfo = TZInfo::Timezone.get(ActiveSupport::TimeZone::MAPPING[timezone] || timezone)
|
||||
|
||||
|
||||
@@ -69,6 +69,10 @@ module Docuseal
|
||||
@default_pkcs ||= GenerateCertificate.load_pkcs(Docuseal::CERTS)
|
||||
end
|
||||
|
||||
def enable_pwa?
|
||||
true
|
||||
end
|
||||
|
||||
def trusted_certs
|
||||
@trusted_certs ||=
|
||||
ENV['TRUSTED_CERTS'].to_s.gsub('\\n', "\n").split("\n\n").map do |base64|
|
||||
|
||||
@@ -8,19 +8,20 @@ module DownloadUtils
|
||||
module_function
|
||||
|
||||
def call(url)
|
||||
uri = Addressable::URI.parse(url)
|
||||
|
||||
if Docuseal.multitenant?
|
||||
raise UnableToDownload, "Error loading: #{uri.display_uri}. Only HTTPS is allowed." if uri.scheme != 'https'
|
||||
|
||||
if uri.host.in?(LOCALHOSTS)
|
||||
raise UnableToDownload, "Error loading: #{uri.display_uri}. Can't download from localhost."
|
||||
end
|
||||
uri = begin
|
||||
URI(url)
|
||||
rescue URI::Error
|
||||
Addressable::URI.parse(url).normalize
|
||||
end
|
||||
|
||||
resp = conn.get(uri.display_uri.to_s)
|
||||
if Docuseal.multitenant?
|
||||
raise UnableToDownload, "Error loading: #{uri}. Only HTTPS is allowed." if uri.scheme != 'https'
|
||||
raise UnableToDownload, "Error loading: #{uri}. Can't download from localhost." if uri.host.in?(LOCALHOSTS)
|
||||
end
|
||||
|
||||
raise UnableToDownload, "Error loading: #{uri.display_uri}" if resp.status >= 400
|
||||
resp = conn.get(uri)
|
||||
|
||||
raise UnableToDownload, "Error loading: #{uri}" if resp.status >= 400
|
||||
|
||||
resp
|
||||
end
|
||||
|
||||
@@ -24,6 +24,10 @@ module PdfIcons
|
||||
StringIO.new(logo_new_data)
|
||||
end
|
||||
|
||||
def stamp_logo_io
|
||||
StringIO.new(stamp_logo_data)
|
||||
end
|
||||
|
||||
def check_data
|
||||
@check_data ||= PATH.join('check.png').read
|
||||
end
|
||||
@@ -39,4 +43,8 @@ module PdfIcons
|
||||
def logo_new_data
|
||||
@logo_new_data ||= PATH.join('logo_new.png').read
|
||||
end
|
||||
|
||||
def stamp_logo_data
|
||||
@stamp_logo_data ||= PATH.join('stamp-logo.png').read
|
||||
end
|
||||
end
|
||||
|
After Width: | Height: | Size: 26 KiB |
@@ -15,7 +15,10 @@ module ReplaceEmailVariables
|
||||
SUBMITTER_SLUG = /\{+submitter\.slug\}+/i
|
||||
SUBMISSION_LINK = /\{+submission\.link\}+/i
|
||||
SUBMISSION_ID = /\{+submission\.id\}+/i
|
||||
SUBMISSION_SUBMITTERS = /\{+submission\.submitters\}+/i
|
||||
SUBMITTERS = /\{+(?:submission\.)?submitters\}+/i
|
||||
SUBMITTERS_N_EMAIL = /\{+submitters\[(?<index>\d+)\]\.email\}+/i
|
||||
SUBMITTERS_N_NAME = /\{+submitters\[(?<index>\d+)\]\.name\}+/i
|
||||
SUBMITTERS_N_FIRST_NAME = /\{+submitters\[(?<index>\d+)\]\.first_name\}+/i
|
||||
DOCUMENTS_LINKS = /\{+documents\.links\}+/i
|
||||
DOCUMENTS_LINK = /\{+documents\.link\}+/i
|
||||
|
||||
@@ -37,13 +40,25 @@ module ReplaceEmailVariables
|
||||
text = replace(text, SUBMISSION_LINK, html_escape:) do
|
||||
submitter.submission ? build_submission_link(submitter.submission) : ''
|
||||
end
|
||||
text = replace(text, SUBMISSION_SUBMITTERS, html_escape:) { build_submission_submitters(submitter.submission) }
|
||||
text = replace(text, SUBMITTERS, html_escape:) { build_submission_submitters(submitter.submission) }
|
||||
text = replace(text, DOCUMENTS_LINKS, html_escape:) { build_documents_links_text(submitter, sig) }
|
||||
text = replace(text, DOCUMENTS_LINK, html_escape:) { build_documents_links_text(submitter, sig) }
|
||||
text = replace(text, ACCOUNT_NAME, html_escape:) { submitter.submission.account.name }
|
||||
text = replace(text, SENDER_NAME, html_escape:) { submitter.submission.created_by_user&.full_name }
|
||||
text = replace(text, SENDER_FIRST_NAME, html_escape:) { submitter.submission.created_by_user&.first_name }
|
||||
|
||||
text = replace(text, SUBMITTERS_N_NAME, html_escape:) do |match|
|
||||
build_submitters_n_field(submitter.submission, match[:index].to_i - 1, :name)
|
||||
end
|
||||
|
||||
text = replace(text, SUBMITTERS_N_EMAIL, html_escape:) do |match|
|
||||
build_submitters_n_field(submitter.submission, match[:index].to_i - 1, :email)
|
||||
end
|
||||
|
||||
text = replace(text, SUBMITTERS_N_FIRST_NAME, html_escape:) do |match|
|
||||
build_submitters_n_field(submitter.submission, match[:index].to_i - 1, :first_name)
|
||||
end
|
||||
|
||||
replace(text, SENDER_EMAIL, html_escape:) { submitter.submission.created_by_user&.email.to_s.sub(/\+\w+@/, '@') }
|
||||
end
|
||||
# rubocop:enable Metrics
|
||||
@@ -54,12 +69,18 @@ module ReplaceEmailVariables
|
||||
)
|
||||
end
|
||||
|
||||
def build_submitters_n_field(submission, index, field_name)
|
||||
uuid = (submission.template_submitters || submission.template.submitters).dig(index, 'uuid')
|
||||
|
||||
submission.submitters.find { |s| s.uuid == uuid }.try(field_name)
|
||||
end
|
||||
|
||||
def replace(text, var, html_escape: false)
|
||||
text.gsub(var) do
|
||||
if html_escape
|
||||
ERB::Util.html_escape(yield)
|
||||
ERB::Util.html_escape(yield(Regexp.last_match))
|
||||
else
|
||||
yield
|
||||
yield(Regexp.last_match)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,10 +3,28 @@
|
||||
module SendWebhookRequest
|
||||
USER_AGENT = 'DocuSeal.com Webhook'
|
||||
|
||||
LOCALHOSTS = %w[0.0.0.0 127.0.0.1 localhost].freeze
|
||||
|
||||
HttpsError = Class.new(StandardError)
|
||||
LocalhostError = Class.new(StandardError)
|
||||
|
||||
module_function
|
||||
|
||||
def call(webhook_url, event_type:, data:)
|
||||
Faraday.post(webhook_url.url) do |req|
|
||||
uri = begin
|
||||
URI(webhook_url.url)
|
||||
rescue URI::Error
|
||||
Addressable::URI.parse(webhook_url.url).normalize
|
||||
end
|
||||
|
||||
if Docuseal.multitenant?
|
||||
raise HttpsError, 'Only HTTPS is allowed.' if uri.scheme != 'https' &&
|
||||
!AccountConfig.exists?(key: :allow_http,
|
||||
account_id: webhook_url.account_id)
|
||||
raise LocalhostError, "Can't send to localhost." if uri.host.in?(LOCALHOSTS)
|
||||
end
|
||||
|
||||
Faraday.post(uri) do |req|
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
req.headers['User-Agent'] = USER_AGENT
|
||||
req.headers.merge!(webhook_url.secret.to_h) if webhook_url.secret.present?
|
||||
|
||||
@@ -107,6 +107,7 @@ module Submissions
|
||||
|
||||
def normalize_email(email)
|
||||
return if email.blank?
|
||||
return if email.is_a?(Numeric)
|
||||
|
||||
return email.downcase if email.to_s.include?(',') ||
|
||||
email.to_s.match?(/\.(?:gob|om|mm|cm|et|mo|nz|za|ie)\z/) ||
|
||||
@@ -142,31 +143,57 @@ module Submissions
|
||||
|
||||
values ||= submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) }
|
||||
|
||||
next unless check_document_conditions(item, values, fields_uuid_index, include_submitter_uuid:)
|
||||
next unless check_item_conditions(item, values, fields_uuid_index, include_submitter_uuid:)
|
||||
end
|
||||
|
||||
item
|
||||
end
|
||||
end
|
||||
|
||||
def check_document_conditions(item, values, fields_index, include_submitter_uuid: nil)
|
||||
def filtered_conditions_fields(submitter, only_submitter_fields: true)
|
||||
fields = submitter.submission.template_fields || submitter.submission.template.fields
|
||||
|
||||
fields_uuid_index = nil
|
||||
values = nil
|
||||
|
||||
fields.filter_map do |field|
|
||||
next if field['submitter_uuid'] != submitter.uuid && only_submitter_fields
|
||||
|
||||
if field['conditions'].present?
|
||||
fields_uuid_index ||= fields.index_by { |f| f['uuid'] }
|
||||
values ||= submitter.submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) }
|
||||
|
||||
submitter_conditions = []
|
||||
|
||||
next unless check_item_conditions(field, values, fields_uuid_index,
|
||||
include_submitter_uuid: submitter.uuid,
|
||||
submitter_conditions_acc: submitter_conditions)
|
||||
|
||||
field = field.merge('conditions' => submitter_conditions) if submitter_conditions != field['conditions']
|
||||
end
|
||||
|
||||
field
|
||||
end
|
||||
end
|
||||
|
||||
def check_item_conditions(item, values, fields_index, include_submitter_uuid: nil, submitter_conditions_acc: nil)
|
||||
return true if item['conditions'].blank?
|
||||
|
||||
item['conditions'].all? do |condition|
|
||||
item['conditions'].each_with_object([]) do |condition, acc|
|
||||
result =
|
||||
if fields_index[condition['field_uuid']]['submitter_uuid'] == include_submitter_uuid
|
||||
submitter_conditions_acc << condition if submitter_conditions_acc
|
||||
|
||||
true
|
||||
else
|
||||
Submitters::SubmitValues.check_field_condition(condition, values, fields_index)
|
||||
end
|
||||
|
||||
item['conditions'].each_with_object([]) do |c, acc|
|
||||
if c['operation'] == 'or'
|
||||
acc.push(acc.pop || result)
|
||||
else
|
||||
acc.push(result)
|
||||
end
|
||||
end.exclude?(false)
|
||||
end
|
||||
if condition['operation'] == 'or'
|
||||
acc.push(acc.pop || result)
|
||||
else
|
||||
acc.push(result)
|
||||
end
|
||||
end.exclude?(false)
|
||||
end
|
||||
end
|
||||
@@ -36,8 +36,8 @@ module Submissions
|
||||
|
||||
is_order_sent = submitters_order == 'random' || index.zero?
|
||||
|
||||
build_submitter(submission:, attrs: submitter_attrs, uuid:,
|
||||
is_order_sent:, user:,
|
||||
build_submitter(submission:, attrs: submitter_attrs,
|
||||
uuid:, is_order_sent:, user:, params:,
|
||||
preferences: preferences.merge(submission_preferences))
|
||||
end
|
||||
|
||||
@@ -69,6 +69,16 @@ module Submissions
|
||||
end
|
||||
end
|
||||
|
||||
def submitter_message_preferences(uuid, params)
|
||||
return {} if params[:request_email_per_submitter] != '1'
|
||||
return {} if params[:is_custom_message] != '1'
|
||||
|
||||
{
|
||||
'subject' => params.dig('submitter_preferences', uuid, 'subject'),
|
||||
'body' => params.dig('submitter_preferences', uuid, 'body')
|
||||
}.compact_blank
|
||||
end
|
||||
|
||||
def maybe_set_template_fields(submission, submitters_attrs, default_submitter_uuid: nil)
|
||||
template_fields = (submission.template_fields || submission.template.fields).deep_dup
|
||||
|
||||
@@ -175,9 +185,10 @@ module Submissions
|
||||
uuid || template.submitters[index]&.dig('uuid')
|
||||
end
|
||||
|
||||
def build_submitter(submission:, attrs:, uuid:, is_order_sent:, user:, preferences:)
|
||||
def build_submitter(submission:, attrs:, uuid:, is_order_sent:, user:, preferences:, params:)
|
||||
email = Submissions.normalize_email(attrs[:email])
|
||||
submitter_preferences = Submitters.normalize_preferences(submission.account, user, attrs)
|
||||
submitter_preferences = Submitters.normalize_preferences(submission.account, user,
|
||||
attrs.merge(submitter_message_preferences(uuid, params)))
|
||||
values = attrs[:values] || {}
|
||||
|
||||
phone_field_uuid = find_phone_field(submission, values)&.dig('uuid')
|
||||
@@ -214,9 +225,15 @@ module Submissions
|
||||
|
||||
def assign_completed_attributes(submitter)
|
||||
submitter.values = Submitters::SubmitValues.merge_default_values(submitter)
|
||||
submitter.values = Submitters::SubmitValues.merge_formula_values(submitter)
|
||||
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
|
||||
|
||||
formula_values = Submitters::SubmitValues.build_formula_values(submitter)
|
||||
|
||||
if formula_values.present?
|
||||
submitter.values = submitter.values.merge(formula_values)
|
||||
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
|
||||
end
|
||||
|
||||
submitter.values = submitter.values.transform_values do |v|
|
||||
v == '{{date}}' ? Time.current.in_time_zone(submitter.submission.account.timezone).to_date.to_s : v
|
||||
end
|
||||
|
||||
@@ -37,30 +37,45 @@ module Submissions
|
||||
submissions.where(created_by_user_id: user&.id || -1)
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
def filter_by_status(submissions, filters)
|
||||
submissions = submissions.pending if filters[:status] == 'pending'
|
||||
submissions = submissions.completed if filters[:status] == 'completed'
|
||||
submissions = submissions.declined if filters[:status] == 'declined'
|
||||
submissions = submissions.expired if filters[:status] == 'expired'
|
||||
|
||||
if filters[:status] == 'partially_completed'
|
||||
submissions =
|
||||
submissions.joins(:submitters)
|
||||
.group(:id)
|
||||
.having(Arel::Nodes::NamedFunction.new(
|
||||
'COUNT', [Arel::Nodes::NamedFunction.new('NULLIF',
|
||||
[Submitter.arel_table[:completed_at].eq(nil),
|
||||
Arel::Nodes.build_quoted(false)])]
|
||||
).gt(0))
|
||||
.having(Arel::Nodes::NamedFunction.new(
|
||||
'COUNT', [Arel::Nodes::NamedFunction.new('NULLIF',
|
||||
[Submitter.arel_table[:completed_at].not_eq(nil),
|
||||
Arel::Nodes.build_quoted(false)])]
|
||||
).gt(0))
|
||||
case filters[:status]
|
||||
when 'pending'
|
||||
submissions.pending
|
||||
when 'completed'
|
||||
submissions.completed
|
||||
when 'declined'
|
||||
submissions.declined
|
||||
when 'expired'
|
||||
submissions.expired
|
||||
when 'sent'
|
||||
submissions.joins(:submitters)
|
||||
.where(submitters: { opened_at: nil, completed_at: nil, declined_at: nil })
|
||||
.where.not(submitters: { sent_at: nil })
|
||||
.group(:id)
|
||||
when 'opened'
|
||||
submissions.joins(:submitters)
|
||||
.where(submitters: { completed_at: nil, declined_at: nil })
|
||||
.where.not(submitters: { opened_at: nil })
|
||||
.group(:id)
|
||||
when 'partially_completed'
|
||||
submissions.joins(:submitters)
|
||||
.group(:id)
|
||||
.having(Arel::Nodes::NamedFunction.new(
|
||||
'COUNT', [Arel::Nodes::NamedFunction.new('NULLIF',
|
||||
[Submitter.arel_table[:completed_at].eq(nil),
|
||||
Arel::Nodes.build_quoted(false)])]
|
||||
).gt(0))
|
||||
.having(Arel::Nodes::NamedFunction.new(
|
||||
'COUNT', [Arel::Nodes::NamedFunction.new('NULLIF',
|
||||
[Submitter.arel_table[:completed_at].not_eq(nil),
|
||||
Arel::Nodes.build_quoted(false)])]
|
||||
).gt(0))
|
||||
else
|
||||
submissions
|
||||
end
|
||||
|
||||
submissions
|
||||
end
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
|
||||
def filter_by_created_at(submissions, filters)
|
||||
submissions = submissions.where(created_at: filters[:created_at_from]..) if filters[:created_at_from].present?
|
||||
|
||||
@@ -23,9 +23,15 @@ module Submissions
|
||||
**Submissions::GenerateResultAttachments.build_signing_params(submitter, pkcs, tsa_url)
|
||||
}
|
||||
|
||||
pdf.sign(io, **sign_params)
|
||||
begin
|
||||
pdf.sign(io, **sign_params)
|
||||
rescue HexaPDF::MalformedPDFError => e
|
||||
Rollbar.error(e) if defined?(Rollbar)
|
||||
|
||||
pdf.sign(io, write_options: { incremental: false }, **sign_params)
|
||||
end
|
||||
else
|
||||
pdf.write(io)
|
||||
pdf.write(io, incremental: true, validate: false)
|
||||
end
|
||||
|
||||
Submissions::GenerateResultAttachments.maybe_enable_ltv(io, sign_params)
|
||||
|
||||
@@ -18,9 +18,12 @@ module Submissions
|
||||
TEXT_TOP_MARGIN = 1
|
||||
MAX_PAGE_ROTATE = 20
|
||||
|
||||
COURIER_FONT = 'Courier'
|
||||
|
||||
A4_SIZE = [595, 842].freeze
|
||||
|
||||
TESTING_FOOTER = 'Testing Document - NOT LEGALLY BINDING'
|
||||
DEFAULT_FONTS = %w[Times Helvetica Courier].freeze
|
||||
|
||||
MISSING_GLYPH_REPLACE = {
|
||||
'▪' => '-',
|
||||
@@ -188,7 +191,9 @@ module Submissions
|
||||
|
||||
fill_color = field.dig('preferences', 'color').presence
|
||||
|
||||
font = pdf.fonts.add(field.dig('preferences', 'font').presence || FONT_NAME)
|
||||
font_name = field.dig('preferences', 'font')
|
||||
font_name = FONT_NAME unless font_name.in?(DEFAULT_FONTS)
|
||||
font = pdf.fonts.add(font_name)
|
||||
|
||||
value = submitter.values[field['uuid']]
|
||||
value = field['default_value'] if field['type'] == 'heading'
|
||||
@@ -435,18 +440,19 @@ module Submissions
|
||||
|
||||
value = TextUtils.maybe_rtl_reverse(Array.wrap(value).join(', '))
|
||||
|
||||
text = HexaPDF::Layout::TextFragment.create(value, font:,
|
||||
fill_color:,
|
||||
font_size:)
|
||||
text_params = { font:, fill_color:, font_size: }
|
||||
text_params[:line_height] = text_params[:font_size] * 1.6 if font_name == COURIER_FONT
|
||||
|
||||
text = HexaPDF::Layout::TextFragment.create(value, **text_params)
|
||||
|
||||
lines = layouter.fit([text], area['w'] * width, height).lines
|
||||
box_height = lines.sum(&:height)
|
||||
|
||||
if preferences_font_size.blank? && box_height > (area['h'] * height) + 1
|
||||
text = HexaPDF::Layout::TextFragment.create(value,
|
||||
font:,
|
||||
fill_color:,
|
||||
font_size: (font_size / 1.4).to_i)
|
||||
text_params[:font_size] = (font_size / 1.4).to_i
|
||||
text_params[:line_height] = text_params[:font_size] * 1.6 if font_name == COURIER_FONT
|
||||
|
||||
text = HexaPDF::Layout::TextFragment.create(value, **text_params)
|
||||
|
||||
lines = layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width, height).lines
|
||||
|
||||
@@ -454,10 +460,10 @@ module Submissions
|
||||
end
|
||||
|
||||
if preferences_font_size.blank? && box_height > (area['h'] * height) + 1
|
||||
text = HexaPDF::Layout::TextFragment.create(value,
|
||||
font:,
|
||||
fill_color:,
|
||||
font_size: (font_size / 1.9).to_i)
|
||||
text_params[:font_size] = (font_size / 1.9).to_i
|
||||
text_params[:line_height] = text_params[:font_size] * 1.6 if font_name == COURIER_FONT
|
||||
|
||||
text = HexaPDF::Layout::TextFragment.create(value, **text_params)
|
||||
|
||||
lines = layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width, height).lines
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ module Submitters
|
||||
end
|
||||
|
||||
def load_logo(_submitter)
|
||||
PdfIcons.logo_io
|
||||
PdfIcons.stamp_logo_io
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -18,7 +18,7 @@ module Submitters
|
||||
|
||||
def find_or_create_signature_from_value(submitter, value, attachments)
|
||||
_, attachment = Submitters::NormalizeValues.normalize_attachment_value(value,
|
||||
'signature',
|
||||
{ 'type' => 'signature' },
|
||||
submitter.account,
|
||||
attachments,
|
||||
submitter)
|
||||
|
||||
@@ -40,7 +40,7 @@ module Submitters
|
||||
|
||||
if field['type'].in?(%w[initials signature image file stamp]) && value.present?
|
||||
new_value, new_attachments =
|
||||
normalize_attachment_value(value, field['type'], template.account, attachments, for_submitter)
|
||||
normalize_attachment_value(value, field, template.account, attachments, for_submitter)
|
||||
|
||||
attachments.push(*new_attachments)
|
||||
|
||||
@@ -109,17 +109,17 @@ module Submitters
|
||||
.merge(fields.index_by { |e| e['name'].to_s.downcase })
|
||||
end
|
||||
|
||||
def normalize_attachment_value(value, type, account, attachments, for_submitter = nil)
|
||||
def normalize_attachment_value(value, field, account, attachments, for_submitter = nil)
|
||||
if value.is_a?(Array)
|
||||
new_attachments = value.map do |v|
|
||||
new_attachment = find_or_build_attachment(v, type, account, for_submitter)
|
||||
new_attachment = find_or_build_attachment(v, field, account, for_submitter)
|
||||
|
||||
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, type, account, for_submitter)
|
||||
new_attachment = find_or_build_attachment(value, field, account, for_submitter)
|
||||
|
||||
existing_attachment = attachments.find { |a| a.blob_id == new_attachment.blob_id }
|
||||
|
||||
@@ -129,7 +129,9 @@ module Submitters
|
||||
end
|
||||
end
|
||||
|
||||
def find_or_build_attachment(value, type, account, for_submitter = nil)
|
||||
def find_or_build_attachment(value, field, account, for_submitter = nil)
|
||||
type = field['type']
|
||||
|
||||
blob =
|
||||
if value.match?(%r{\Ahttps?://})
|
||||
find_or_create_blob_from_url(account, value)
|
||||
@@ -138,6 +140,8 @@ module Submitters
|
||||
elsif (data = Base64.decode64(value.sub(BASE64_PREFIX_REGEXP, ''))) &&
|
||||
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'))
|
||||
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)}..."
|
||||
end
|
||||
@@ -152,6 +156,10 @@ module Submitters
|
||||
attachment
|
||||
end
|
||||
|
||||
def find_or_create_blob_from_html(_account, value, _field)
|
||||
raise InvalidDefaultValue, "HTML content is not allowed: #{value.first(200)}..."
|
||||
end
|
||||
|
||||
def find_or_create_blob_from_base64(account, data, type)
|
||||
checksum = Digest::MD5.base64digest(data)
|
||||
|
||||
|
||||
@@ -53,9 +53,17 @@ module Submitters
|
||||
submitter.completed_at = Time.current
|
||||
submitter.ip = request.remote_ip
|
||||
submitter.ua = request.user_agent
|
||||
|
||||
submitter.values = merge_default_values(submitter)
|
||||
submitter.values = maybe_remove_condition_values(submitter)
|
||||
submitter.values = merge_formula_values(submitter)
|
||||
|
||||
formula_values = build_formula_values(submitter)
|
||||
|
||||
if formula_values.present?
|
||||
submitter.values = submitter.values.merge(formula_values)
|
||||
submitter.values = maybe_remove_condition_values(submitter)
|
||||
end
|
||||
|
||||
submitter.values = submitter.values.transform_values do |v|
|
||||
v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v
|
||||
end
|
||||
@@ -149,7 +157,7 @@ module Submitters
|
||||
default_values.compact_blank.merge(submitter.values)
|
||||
end
|
||||
|
||||
def merge_formula_values(submitter)
|
||||
def build_formula_values(submitter)
|
||||
computed_values = submitter.submission.template_fields.each_with_object({}) do |field, acc|
|
||||
next if field['submitter_uuid'] != submitter.uuid
|
||||
next if field['type'] == 'payment'
|
||||
@@ -161,7 +169,7 @@ module Submitters
|
||||
acc[field['uuid']] = calculate_formula_value(formula, submitter.values.merge(acc.compact_blank))
|
||||
end
|
||||
|
||||
submitter.values.merge(computed_values.compact_blank)
|
||||
computed_values.compact_blank
|
||||
end
|
||||
|
||||
def calculate_formula_value(_formula, _values)
|
||||
@@ -186,10 +194,21 @@ module Submitters
|
||||
attachments_index =
|
||||
Submissions.filtered_conditions_schema(submitter.submission).index_by { |i| i['attachment_uuid'] }
|
||||
|
||||
submitter_values = nil
|
||||
is_other_submitter_conditions = submitter.submission.template_submitters.size > 1
|
||||
|
||||
submitter.submission.template_fields.each do |field|
|
||||
next if field['submitter_uuid'] != submitter.uuid
|
||||
|
||||
submitter.values.delete(field['uuid']) unless check_field_conditions(submitter, field, fields_uuid_index)
|
||||
submitter_values ||= submitter.values
|
||||
|
||||
is_other_submitter_conditions &&= field_conditions_other_submitter?(submitter, field, fields_uuid_index)
|
||||
|
||||
if is_other_submitter_conditions
|
||||
submitter_values = submitter.submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) }
|
||||
end
|
||||
|
||||
submitter.values.delete(field['uuid']) unless check_field_conditions(submitter_values, field, fields_uuid_index)
|
||||
|
||||
if field['areas'].present? && field['areas'].none? { |area| attachments_index[area['attachment_uuid']] }
|
||||
submitter.values.delete(field['uuid'])
|
||||
@@ -199,10 +218,14 @@ module Submitters
|
||||
submitter.values
|
||||
end
|
||||
|
||||
def check_field_conditions(submitter, field, fields_uuid_index)
|
||||
return true if field['conditions'].blank?
|
||||
def field_conditions_other_submitter?(submitter, field, fields_uuid_index)
|
||||
field['conditions'].to_a.any? do |c|
|
||||
fields_uuid_index.dig(c['field_uuid'], 'submitter_uuid') != submitter.uuid
|
||||
end
|
||||
end
|
||||
|
||||
submitter_values = submitter.values
|
||||
def check_field_conditions(submitter_values, field, fields_uuid_index)
|
||||
return true if field['conditions'].blank?
|
||||
|
||||
field['conditions'].each_with_object([]) do |c, acc|
|
||||
if c['operation'] == 'or'
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# This rake task was added by annotate_rb gem.
|
||||
|
||||
# Can set `ANNOTATERB_SKIP_ON_DB_TASKS` to be anything to skip this
|
||||
if Rails.env.development? && ENV['ANNOTATERB_SKIP_ON_DB_TASKS'].nil?
|
||||
require 'annotate_rb'
|
||||
|
||||
AnnotateRb::Core.load_rake_tasks
|
||||
end
|
||||
@@ -1,57 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
if Rails.env.development?
|
||||
require 'annotate'
|
||||
|
||||
task set_annotation_options: :environment do
|
||||
Annotate.set_defaults(
|
||||
'active_admin' => 'false',
|
||||
'additional_file_patterns' => [],
|
||||
'routes' => 'false',
|
||||
'models' => 'true',
|
||||
'position_in_routes' => 'before',
|
||||
'position_in_class' => 'before',
|
||||
'position_in_test' => 'before',
|
||||
'position_in_fixture' => 'before',
|
||||
'position_in_factory' => 'before',
|
||||
'position_in_serializer' => 'before',
|
||||
'show_foreign_keys' => 'true',
|
||||
'show_complete_foreign_keys' => 'false',
|
||||
'show_indexes' => 'true',
|
||||
'simple_indexes' => 'false',
|
||||
'model_dir' => 'app/models',
|
||||
'root_dir' => '',
|
||||
'include_version' => 'false',
|
||||
'require' => '',
|
||||
'exclude_tests' => 'true',
|
||||
'exclude_fixtures' => 'true',
|
||||
'exclude_factories' => 'true',
|
||||
'exclude_serializers' => 'false',
|
||||
'exclude_scaffolds' => 'true',
|
||||
'exclude_controllers' => 'true',
|
||||
'exclude_helpers' => 'true',
|
||||
'exclude_sti_subclasses' => 'false',
|
||||
'ignore_model_sub_dir' => 'false',
|
||||
'ignore_columns' => nil,
|
||||
'ignore_routes' => nil,
|
||||
'ignore_unknown_models' => 'false',
|
||||
'hide_limit_column_types' => 'integer,bigint,boolean',
|
||||
'hide_default_column_types' => 'json,jsonb,hstore',
|
||||
'skip_on_db_migrate' => 'false',
|
||||
'format_bare' => 'true',
|
||||
'format_rdoc' => 'false',
|
||||
'format_yard' => 'false',
|
||||
'format_markdown' => 'false',
|
||||
'sort' => 'false',
|
||||
'force' => 'false',
|
||||
'frozen' => 'false',
|
||||
'classified_sort' => 'true',
|
||||
'trace' => 'false',
|
||||
'wrapper_open' => nil,
|
||||
'wrapper_close' => nil,
|
||||
'with_comment' => 'true'
|
||||
)
|
||||
end
|
||||
|
||||
Annotate.load_tasks
|
||||
end
|
||||
@@ -9,7 +9,6 @@ module Templates
|
||||
|
||||
template.external_id = external_id
|
||||
template.author = author
|
||||
template.preferences = original_template.preferences.deep_dup
|
||||
template.name = name.presence || "#{original_template.name} (#{I18n.t('clone')})"
|
||||
|
||||
if folder_name.present?
|
||||
@@ -18,10 +17,11 @@ module Templates
|
||||
template.folder_id = original_template.folder_id
|
||||
end
|
||||
|
||||
template.submitters, template.fields, template.schema =
|
||||
template.submitters, template.fields, template.schema, template.preferences =
|
||||
update_submitters_and_fields_and_schema(original_template.submitters.deep_dup,
|
||||
original_template.fields.deep_dup,
|
||||
original_template.schema.deep_dup)
|
||||
original_template.schema.deep_dup,
|
||||
original_template.preferences.deep_dup)
|
||||
|
||||
if name.present? && template.schema.size == 1 &&
|
||||
original_template.schema.first['name'] == original_template.name &&
|
||||
@@ -32,7 +32,8 @@ module Templates
|
||||
template
|
||||
end
|
||||
|
||||
def update_submitters_and_fields_and_schema(cloned_submitters, cloned_fields, cloned_schema)
|
||||
# rubocop:disable Metrics, Style/CombinableLoops
|
||||
def update_submitters_and_fields_and_schema(cloned_submitters, cloned_fields, cloned_schema, cloned_preferences)
|
||||
submitter_uuids_replacements = {}
|
||||
field_uuids_replacements = {}
|
||||
|
||||
@@ -43,6 +44,24 @@ module Templates
|
||||
submitter['uuid'] = new_submitter_uuid
|
||||
end
|
||||
|
||||
cloned_submitters.each do |submitter|
|
||||
if submitter['optional_invite_by_uuid'].present?
|
||||
submitter['optional_invite_by_uuid'] = submitter_uuids_replacements[submitter['optional_invite_by_uuid']]
|
||||
end
|
||||
|
||||
if submitter['invite_by_uuid'].present?
|
||||
submitter['invite_by_uuid'] = submitter_uuids_replacements[submitter['invite_by_uuid']]
|
||||
end
|
||||
|
||||
if submitter['linked_to_uuid'].present?
|
||||
submitter['linked_to_uuid'] = submitter_uuids_replacements[submitter['linked_to_uuid']]
|
||||
end
|
||||
end
|
||||
|
||||
cloned_preferences['submitters'].to_a.each do |submitter|
|
||||
submitter['uuid'] = submitter_uuids_replacements[submitter['uuid']]
|
||||
end
|
||||
|
||||
cloned_fields.each do |field|
|
||||
new_field_uuid = SecureRandom.uuid
|
||||
|
||||
@@ -73,7 +92,8 @@ module Templates
|
||||
end
|
||||
end
|
||||
|
||||
[cloned_submitters, cloned_fields, cloned_schema]
|
||||
[cloned_submitters, cloned_fields, cloned_schema, cloned_preferences]
|
||||
end
|
||||
# rubocop:enable Metrics, Style/CombinableLoops
|
||||
end
|
||||
end
|
||||
@@ -101,7 +101,7 @@ module Templates
|
||||
|
||||
{
|
||||
uuid: SecureRandom.uuid,
|
||||
required: false,
|
||||
required: field.flags.include?(:required),
|
||||
preferences: {},
|
||||
areas:,
|
||||
**field_properties
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,11 @@
|
||||
self.addEventListener('install', () => {
|
||||
console.log('DocuSeal App installed')
|
||||
})
|
||||
|
||||
self.addEventListener('activate', () => {
|
||||
console.log('DocuSeal App activated')
|
||||
})
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
event.respondWith(fetch(event.request))
|
||||
})
|
||||
@@ -9,6 +9,7 @@ FactoryBot.define do
|
||||
|
||||
transient do
|
||||
submitter_count { 1 }
|
||||
attachment_count { 1 }
|
||||
only_field_types do
|
||||
%w[text date checkbox radio signature number multiple select initials image file stamp cells phone payment]
|
||||
end
|
||||
@@ -16,20 +17,6 @@ FactoryBot.define do
|
||||
end
|
||||
|
||||
after(:create) do |template, ev|
|
||||
blob = ActiveStorage::Blob.create_and_upload!(
|
||||
io: Rails.root.join('spec/fixtures/sample-document.pdf').open,
|
||||
filename: 'sample-document.pdf',
|
||||
content_type: 'application/pdf'
|
||||
)
|
||||
attachment = ActiveStorage::Attachment.create!(
|
||||
blob:,
|
||||
name: :documents,
|
||||
record: template
|
||||
)
|
||||
|
||||
Templates::ProcessDocument.call(attachment, attachment.download)
|
||||
|
||||
template.schema = [{ attachment_uuid: attachment.uuid, name: 'sample-document' }]
|
||||
number_words = %w[first second third fourth fifth sixth seventh eighth ninth tenth]
|
||||
|
||||
template.submitters = Array.new(ev.submitter_count) do |i|
|
||||
@@ -39,296 +26,319 @@ FactoryBot.define do
|
||||
}
|
||||
end
|
||||
|
||||
template.fields = template.submitters.reduce([]) do |fields, submitter|
|
||||
fields += [
|
||||
{
|
||||
'uuid' => SecureRandom.uuid,
|
||||
'submitter_uuid' => submitter['uuid'],
|
||||
'name' => 'First Name',
|
||||
'type' => 'text',
|
||||
'required' => true,
|
||||
'preferences' => {},
|
||||
'areas' => [
|
||||
{
|
||||
'x' => 0.09273546006944444,
|
||||
'y' => 0.1099851117387033,
|
||||
'w' => 0.2701497395833333,
|
||||
'h' => 0.0372705365913556,
|
||||
'attachment_uuid' => attachment.uuid,
|
||||
'page' => 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'uuid' => SecureRandom.uuid,
|
||||
'submitter_uuid' => submitter['uuid'],
|
||||
'name' => 'Birthday',
|
||||
'type' => 'date',
|
||||
'required' => true,
|
||||
'preferences' => { 'format' => 'DD/MM/YYYY' },
|
||||
'areas' => [
|
||||
{
|
||||
'x' => 0.09166666666666666,
|
||||
'y' => 0.1762778204144282,
|
||||
'w' => 0.2763888888888889,
|
||||
'h' => 0.0359029261474578,
|
||||
'attachment_uuid' => attachment.uuid,
|
||||
'page' => 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'uuid' => SecureRandom.uuid,
|
||||
'submitter_uuid' => submitter['uuid'],
|
||||
'name' => 'Do you agree?',
|
||||
'type' => 'checkbox',
|
||||
'required' => true,
|
||||
'preferences' => {},
|
||||
'areas' => [
|
||||
{
|
||||
'x' => 0.09051106770833334,
|
||||
'y' => 0.227587027259332,
|
||||
'w' => 0.2784450954861111,
|
||||
'h' => 0.04113074042239687,
|
||||
'attachment_uuid' => attachment.uuid,
|
||||
'page' => 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'uuid' => SecureRandom.uuid,
|
||||
'submitter_uuid' => submitter['uuid'],
|
||||
'name' => 'First child',
|
||||
'type' => 'radio',
|
||||
'required' => true,
|
||||
'preferences' => {},
|
||||
'options' => [
|
||||
{ 'value' => 'Girl', 'uuid' => SecureRandom.uuid },
|
||||
{ 'value' => 'Boy', 'uuid' => SecureRandom.uuid }
|
||||
],
|
||||
'areas' => [
|
||||
{
|
||||
'x' => 0.09027777777777778,
|
||||
'y' => 0.3020184190330008,
|
||||
'w' => 0.2,
|
||||
'h' => 0.02857142857142857,
|
||||
'attachment_uuid' => attachment.uuid,
|
||||
'page' => 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'uuid' => SecureRandom.uuid,
|
||||
'submitter_uuid' => submitter['uuid'],
|
||||
'name' => 'Signature',
|
||||
'type' => 'signature',
|
||||
'required' => true,
|
||||
'preferences' => {},
|
||||
'areas' => [
|
||||
{
|
||||
'x' => 0.08611111111111111,
|
||||
'y' => 0.3487183422870299,
|
||||
'w' => 0.2,
|
||||
'h' => 0.0707269155206287,
|
||||
'attachment_uuid' => attachment.uuid,
|
||||
'page' => 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'uuid' => SecureRandom.uuid,
|
||||
'submitter_uuid' => submitter['uuid'],
|
||||
'name' => 'House number',
|
||||
'type' => 'number',
|
||||
'required' => true,
|
||||
'preferences' => {},
|
||||
'areas' => [
|
||||
{
|
||||
'x' => 0.08333333333333333,
|
||||
'y' => 0.4582041442824252,
|
||||
'w' => 0.2,
|
||||
'h' => 0.02857142857142857,
|
||||
'attachment_uuid' => attachment.uuid,
|
||||
'page' => 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'uuid' => SecureRandom.uuid,
|
||||
'submitter_uuid' => submitter['uuid'],
|
||||
'name' => 'Colors',
|
||||
'type' => 'multiple',
|
||||
'required' => true,
|
||||
'preferences' => {},
|
||||
'options' => [
|
||||
{ 'value' => 'Red', 'uuid' => SecureRandom.uuid },
|
||||
{ 'value' => 'Green', 'uuid' => SecureRandom.uuid },
|
||||
{ 'value' => 'Blue', 'uuid' => SecureRandom.uuid }
|
||||
],
|
||||
'areas' => [
|
||||
{
|
||||
'x' => 0.45,
|
||||
'y' => 0.1133998465080583,
|
||||
'w' => 0.2,
|
||||
'h' => 0.02857142857142857,
|
||||
'attachment_uuid' => attachment.uuid,
|
||||
'page' => 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'uuid' => SecureRandom.uuid,
|
||||
'submitter_uuid' => submitter['uuid'],
|
||||
'name' => 'Gender',
|
||||
'type' => 'select',
|
||||
'required' => true,
|
||||
'preferences' => {},
|
||||
'options' => [
|
||||
{ 'value' => 'Male', 'uuid' => SecureRandom.uuid },
|
||||
{ 'value' => 'Female', 'uuid' => SecureRandom.uuid }
|
||||
],
|
||||
'areas' => [
|
||||
{
|
||||
'x' => 0.4513888888888889,
|
||||
'y' => 0.1752954719877206,
|
||||
'w' => 0.2,
|
||||
'h' => 0.02857142857142857,
|
||||
'attachment_uuid' => attachment.uuid,
|
||||
'page' => 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'uuid' => SecureRandom.uuid,
|
||||
'submitter_uuid' => submitter['uuid'],
|
||||
'name' => 'Initials',
|
||||
'type' => 'initials',
|
||||
'required' => true,
|
||||
'preferences' => {},
|
||||
'areas' => [
|
||||
{
|
||||
'x' => 0.4486111111111111,
|
||||
'y' => 0.2273599386032233,
|
||||
'w' => 0.1,
|
||||
'h' => 0.02857142857142857,
|
||||
'attachment_uuid' => attachment.uuid,
|
||||
'page' => 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'uuid' => SecureRandom.uuid,
|
||||
'submitter_uuid' => submitter['uuid'],
|
||||
'name' => 'Avatar',
|
||||
'type' => 'image',
|
||||
'required' => true,
|
||||
'preferences' => {},
|
||||
'areas' => [
|
||||
{
|
||||
'x' => 0.7180555555555556,
|
||||
'y' => 0.1129547198772064,
|
||||
'w' => 0.2,
|
||||
'h' => 0.1414538310412574,
|
||||
'attachment_uuid' => attachment.uuid,
|
||||
'page' => 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'uuid' => SecureRandom.uuid,
|
||||
'submitter_uuid' => submitter['uuid'],
|
||||
'name' => 'Attachment',
|
||||
'type' => 'file',
|
||||
'required' => true,
|
||||
'preferences' => {},
|
||||
'areas' => [
|
||||
{
|
||||
'x' => 0.7166666666666667,
|
||||
'y' => 0.3020107444359171,
|
||||
'w' => 0.2,
|
||||
'h' => 0.02857142857142857,
|
||||
'attachment_uuid' => attachment.uuid,
|
||||
'page' => 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'uuid' => SecureRandom.uuid,
|
||||
'submitter_uuid' => submitter['uuid'],
|
||||
'name' => 'Stamp',
|
||||
'type' => 'stamp',
|
||||
'required' => true,
|
||||
'readonly' => true,
|
||||
'preferences' => {},
|
||||
'areas' => [
|
||||
{
|
||||
'x' => 0.7166666666666667,
|
||||
'y' => 0.3771910974673829,
|
||||
'w' => 0.2,
|
||||
'h' => 0.0707269155206287,
|
||||
'attachment_uuid' => attachment.uuid,
|
||||
'page' => 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'uuid' => SecureRandom.uuid,
|
||||
'submitter_uuid' => submitter['uuid'],
|
||||
'name' => 'Cell code',
|
||||
'type' => 'cells',
|
||||
'required' => true,
|
||||
'preferences' => {},
|
||||
'areas' => [
|
||||
{
|
||||
'x' => 0.4472222222222222,
|
||||
'y' => 0.3530851880276286,
|
||||
'w' => 0.2,
|
||||
'h' => 0.02857142857142857,
|
||||
'cell_w' => 0.04,
|
||||
'attachment_uuid' => attachment.uuid,
|
||||
'page' => 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'uuid' => SecureRandom.uuid,
|
||||
'submitter_uuid' => submitter['uuid'],
|
||||
'name' => 'Payment',
|
||||
'type' => 'payment',
|
||||
'required' => true,
|
||||
'preferences' => { 'currency' => 'EUR', 'price' => 1000 },
|
||||
'areas' => [
|
||||
{
|
||||
'x' => 0.4486111111111111,
|
||||
'y' => 0.43168073676132,
|
||||
'w' => 0.2,
|
||||
'h' => 0.02857142857142857,
|
||||
'attachment_uuid' => attachment.uuid,
|
||||
'page' => 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'uuid' => SecureRandom.uuid,
|
||||
'submitter_uuid' => submitter['uuid'],
|
||||
'name' => 'Mobile Phone',
|
||||
'type' => 'phone',
|
||||
'required' => true,
|
||||
'preferences' => {},
|
||||
'areas' => [
|
||||
{
|
||||
'x' => 0.44443359375,
|
||||
'y' => 0.3010283960092095,
|
||||
'w' => 0.2,
|
||||
'h' => 0.02857142857142857,
|
||||
'attachment_uuid' => attachment.uuid,
|
||||
'page' => 0
|
||||
}
|
||||
]
|
||||
}
|
||||
].select { |f| ev.only_field_types.include?(f['type']) && ev.except_field_types.exclude?(f['type']) }
|
||||
ev.attachment_count.times do |i|
|
||||
attachment_index = i + 1 if i > 0
|
||||
field_index = "(#{attachment_index})" if attachment_index
|
||||
|
||||
fields
|
||||
blob = ActiveStorage::Blob.create_and_upload!(
|
||||
io: Rails.root.join('spec/fixtures/sample-document.pdf').open,
|
||||
filename: 'sample-document.pdf',
|
||||
content_type: 'application/pdf'
|
||||
)
|
||||
attachment = ActiveStorage::Attachment.create!(
|
||||
blob:,
|
||||
name: :documents,
|
||||
record: template
|
||||
)
|
||||
|
||||
Templates::ProcessDocument.call(attachment, attachment.download)
|
||||
|
||||
template.schema << {
|
||||
attachment_uuid: attachment.uuid,
|
||||
name: ['sample-document', attachment_index].compact.join('-')
|
||||
}
|
||||
|
||||
template.fields += template.submitters.reduce([]) do |fields, submitter|
|
||||
fields += [
|
||||
{
|
||||
'uuid' => SecureRandom.uuid,
|
||||
'submitter_uuid' => submitter['uuid'],
|
||||
'name' => ['First Name', field_index].compact.join(' '),
|
||||
'type' => 'text',
|
||||
'required' => true,
|
||||
'preferences' => {},
|
||||
'areas' => [
|
||||
{
|
||||
'x' => 0.09273546006944444,
|
||||
'y' => 0.1099851117387033,
|
||||
'w' => 0.2701497395833333,
|
||||
'h' => 0.0372705365913556,
|
||||
'attachment_uuid' => attachment.uuid,
|
||||
'page' => 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'uuid' => SecureRandom.uuid,
|
||||
'submitter_uuid' => submitter['uuid'],
|
||||
'name' => ['Birthday', field_index].compact.join(' '),
|
||||
'type' => 'date',
|
||||
'required' => true,
|
||||
'preferences' => { 'format' => 'DD/MM/YYYY' },
|
||||
'areas' => [
|
||||
{
|
||||
'x' => 0.09166666666666666,
|
||||
'y' => 0.1762778204144282,
|
||||
'w' => 0.2763888888888889,
|
||||
'h' => 0.0359029261474578,
|
||||
'attachment_uuid' => attachment.uuid,
|
||||
'page' => 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'uuid' => SecureRandom.uuid,
|
||||
'submitter_uuid' => submitter['uuid'],
|
||||
'name' => ['Do you agree?', field_index].compact.join(' '),
|
||||
'type' => 'checkbox',
|
||||
'required' => true,
|
||||
'preferences' => {},
|
||||
'areas' => [
|
||||
{
|
||||
'x' => 0.09051106770833334,
|
||||
'y' => 0.227587027259332,
|
||||
'w' => 0.2784450954861111,
|
||||
'h' => 0.04113074042239687,
|
||||
'attachment_uuid' => attachment.uuid,
|
||||
'page' => 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'uuid' => SecureRandom.uuid,
|
||||
'submitter_uuid' => submitter['uuid'],
|
||||
'name' => ['First child', field_index].compact.join(' '),
|
||||
'type' => 'radio',
|
||||
'required' => true,
|
||||
'preferences' => {},
|
||||
'options' => [
|
||||
{ 'value' => 'Girl', 'uuid' => SecureRandom.uuid },
|
||||
{ 'value' => 'Boy', 'uuid' => SecureRandom.uuid }
|
||||
],
|
||||
'areas' => [
|
||||
{
|
||||
'x' => 0.09027777777777778,
|
||||
'y' => 0.3020184190330008,
|
||||
'w' => 0.2,
|
||||
'h' => 0.02857142857142857,
|
||||
'attachment_uuid' => attachment.uuid,
|
||||
'page' => 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'uuid' => SecureRandom.uuid,
|
||||
'submitter_uuid' => submitter['uuid'],
|
||||
'name' => ['Signature', field_index].compact.join(' '),
|
||||
'type' => 'signature',
|
||||
'required' => true,
|
||||
'preferences' => {},
|
||||
'areas' => [
|
||||
{
|
||||
'x' => 0.08611111111111111,
|
||||
'y' => 0.3487183422870299,
|
||||
'w' => 0.2,
|
||||
'h' => 0.0707269155206287,
|
||||
'attachment_uuid' => attachment.uuid,
|
||||
'page' => 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'uuid' => SecureRandom.uuid,
|
||||
'submitter_uuid' => submitter['uuid'],
|
||||
'name' => ['House number', field_index].compact.join(' '),
|
||||
'type' => 'number',
|
||||
'required' => true,
|
||||
'preferences' => {},
|
||||
'areas' => [
|
||||
{
|
||||
'x' => 0.08333333333333333,
|
||||
'y' => 0.4582041442824252,
|
||||
'w' => 0.2,
|
||||
'h' => 0.02857142857142857,
|
||||
'attachment_uuid' => attachment.uuid,
|
||||
'page' => 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'uuid' => SecureRandom.uuid,
|
||||
'submitter_uuid' => submitter['uuid'],
|
||||
'name' => ['Colors', field_index].compact.join(' '),
|
||||
'type' => 'multiple',
|
||||
'required' => true,
|
||||
'preferences' => {},
|
||||
'options' => [
|
||||
{ 'value' => 'Red', 'uuid' => SecureRandom.uuid },
|
||||
{ 'value' => 'Green', 'uuid' => SecureRandom.uuid },
|
||||
{ 'value' => 'Blue', 'uuid' => SecureRandom.uuid }
|
||||
],
|
||||
'areas' => [
|
||||
{
|
||||
'x' => 0.45,
|
||||
'y' => 0.1133998465080583,
|
||||
'w' => 0.2,
|
||||
'h' => 0.02857142857142857,
|
||||
'attachment_uuid' => attachment.uuid,
|
||||
'page' => 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'uuid' => SecureRandom.uuid,
|
||||
'submitter_uuid' => submitter['uuid'],
|
||||
'name' => ['Gender', field_index].compact.join(' '),
|
||||
'type' => 'select',
|
||||
'required' => true,
|
||||
'preferences' => {},
|
||||
'options' => [
|
||||
{ 'value' => 'Male', 'uuid' => SecureRandom.uuid },
|
||||
{ 'value' => 'Female', 'uuid' => SecureRandom.uuid }
|
||||
],
|
||||
'areas' => [
|
||||
{
|
||||
'x' => 0.4513888888888889,
|
||||
'y' => 0.1752954719877206,
|
||||
'w' => 0.2,
|
||||
'h' => 0.02857142857142857,
|
||||
'attachment_uuid' => attachment.uuid,
|
||||
'page' => 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'uuid' => SecureRandom.uuid,
|
||||
'submitter_uuid' => submitter['uuid'],
|
||||
'name' => ['Initials', field_index].compact.join(' '),
|
||||
'type' => 'initials',
|
||||
'required' => true,
|
||||
'preferences' => {},
|
||||
'areas' => [
|
||||
{
|
||||
'x' => 0.4486111111111111,
|
||||
'y' => 0.2273599386032233,
|
||||
'w' => 0.1,
|
||||
'h' => 0.02857142857142857,
|
||||
'attachment_uuid' => attachment.uuid,
|
||||
'page' => 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'uuid' => SecureRandom.uuid,
|
||||
'submitter_uuid' => submitter['uuid'],
|
||||
'name' => ['Avatar', field_index].compact.join(' '),
|
||||
'type' => 'image',
|
||||
'required' => true,
|
||||
'preferences' => {},
|
||||
'areas' => [
|
||||
{
|
||||
'x' => 0.7180555555555556,
|
||||
'y' => 0.1129547198772064,
|
||||
'w' => 0.2,
|
||||
'h' => 0.1414538310412574,
|
||||
'attachment_uuid' => attachment.uuid,
|
||||
'page' => 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'uuid' => SecureRandom.uuid,
|
||||
'submitter_uuid' => submitter['uuid'],
|
||||
'name' => ['Attachment', field_index].compact.join(' '),
|
||||
'type' => 'file',
|
||||
'required' => true,
|
||||
'preferences' => {},
|
||||
'areas' => [
|
||||
{
|
||||
'x' => 0.7166666666666667,
|
||||
'y' => 0.3020107444359171,
|
||||
'w' => 0.2,
|
||||
'h' => 0.02857142857142857,
|
||||
'attachment_uuid' => attachment.uuid,
|
||||
'page' => 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'uuid' => SecureRandom.uuid,
|
||||
'submitter_uuid' => submitter['uuid'],
|
||||
'name' => ['Stamp', field_index].compact.join(' '),
|
||||
'type' => 'stamp',
|
||||
'required' => true,
|
||||
'readonly' => true,
|
||||
'preferences' => {},
|
||||
'areas' => [
|
||||
{
|
||||
'x' => 0.7166666666666667,
|
||||
'y' => 0.3771910974673829,
|
||||
'w' => 0.2,
|
||||
'h' => 0.0707269155206287,
|
||||
'attachment_uuid' => attachment.uuid,
|
||||
'page' => 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'uuid' => SecureRandom.uuid,
|
||||
'submitter_uuid' => submitter['uuid'],
|
||||
'name' => ['Cell code', field_index].compact.join(' '),
|
||||
'type' => 'cells',
|
||||
'required' => true,
|
||||
'preferences' => {},
|
||||
'areas' => [
|
||||
{
|
||||
'x' => 0.4472222222222222,
|
||||
'y' => 0.3530851880276286,
|
||||
'w' => 0.2,
|
||||
'h' => 0.02857142857142857,
|
||||
'cell_w' => 0.04,
|
||||
'attachment_uuid' => attachment.uuid,
|
||||
'page' => 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'uuid' => SecureRandom.uuid,
|
||||
'submitter_uuid' => submitter['uuid'],
|
||||
'name' => ['Payment', field_index].compact.join(' '),
|
||||
'type' => 'payment',
|
||||
'required' => true,
|
||||
'preferences' => { 'currency' => 'EUR', 'price' => 1000 },
|
||||
'areas' => [
|
||||
{
|
||||
'x' => 0.4486111111111111,
|
||||
'y' => 0.43168073676132,
|
||||
'w' => 0.2,
|
||||
'h' => 0.02857142857142857,
|
||||
'attachment_uuid' => attachment.uuid,
|
||||
'page' => 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'uuid' => SecureRandom.uuid,
|
||||
'submitter_uuid' => submitter['uuid'],
|
||||
'name' => ['Mobile Phone', field_index].compact.join(' '),
|
||||
'type' => 'phone',
|
||||
'required' => true,
|
||||
'preferences' => {},
|
||||
'areas' => [
|
||||
{
|
||||
'x' => 0.44443359375,
|
||||
'y' => 0.3010283960092095,
|
||||
'w' => 0.2,
|
||||
'h' => 0.02857142857142857,
|
||||
'attachment_uuid' => attachment.uuid,
|
||||
'page' => 0
|
||||
}
|
||||
]
|
||||
}
|
||||
].select { |f| ev.only_field_types.include?(f['type']) && ev.except_field_types.exclude?(f['type']) }
|
||||
|
||||
fields
|
||||
end
|
||||
end
|
||||
|
||||
template.save!
|
||||
|
||||