Compare commits

..
62 Commits
Author SHA1 Message Date
Alex Turchyn 993698ec01 Merge from docusealco/wip 2025-08-18 12:33:13 +03:00
Pete Matsyburka fcdc44ddbe update rails 2025-08-14 09:44:04 +03:00
Pete Matsyburka 0cb6b0ae12 fix zip 2025-08-13 11:18:49 +03:00
Pete Matsyburka 0388927c5b fix readonly date 2025-08-12 22:33:38 +03:00
Pete Matsyburka 2c8f5d2129 debug file 2025-08-12 20:01:52 +03:00
Pete Matsyburka 2bffbf83c6 handle zip upload 2025-08-12 13:07:16 +03:00
Pete Matsyburka ec01529014 show readonly fields 2025-08-11 16:58:14 +03:00
Pete Matsyburka ea1abf5662 fix test email 2025-08-08 16:33:19 +03:00
Pete Matsyburka fcd5263af1 add hour minute variable 2025-08-07 11:33:11 +03:00
Pete Matsyburka 1fa8616dbc fix submitters order 2025-08-04 14:46:46 +03:00
Alex Turchyn f627277b45 Merge from docusealco/wip 2025-08-04 12:59:10 +03:00
Pete Matsyburka 809c3ea270 fix template 2fa email 2025-07-31 19:23:27 +03:00
Pete Matsyburka 9caa9d79d4 fix tooltip 2025-07-31 13:32:28 +03:00
Pete Matsyburka a8f1c1c323 add new signature formats 2025-07-31 13:13:31 +03:00
Pete Matsyburka 59fbea5c5f fix migration 2025-07-30 18:45:28 +03:00
Pete Matsyburka d3273d879a add order ui 2025-07-30 10:22:38 +03:00
Pete Matsyburka ea227e82c9 force smtp auto 2025-07-29 22:49:20 +03:00
Pete Matsyburka 676b330aa3 add submitters order param 2025-07-29 17:47:47 +03:00
Pete Matsyburka 8d3295e125 better signature validation 2025-07-29 13:51:11 +03:00
Pete Matsyburka 8cb74e0bcc fix build 2025-07-28 16:55:21 +03:00
Alex Turchyn 26749a4dbc Merge from docusealco/wip 2025-07-28 12:44:35 +03:00
Pete Matsyburka 14915d37b4 fix build 2025-07-28 09:02:17 +03:00
Pete Matsyburka 16d6d58efb fix typo 2025-07-27 10:42:56 +03:00
Pete Matsyburka b2b97a313a prefillable fields 2025-07-27 10:27:12 +03:00
Pete Matsyburka 2974e6a64f remove unused posthog 2025-07-25 09:41:24 +03:00
Pete Matsyburka dae34d0aa5 remove multiple signatures toggle 2025-07-24 15:13:39 +03:00
Alex Turchyn e9d0671184 remove duplicate translations 2025-07-24 14:39:28 +03:00
Alex Turchyn 897e33bf6a simplify shared links modal UI 2025-07-24 10:13:34 +03:00
Alex Turchyn f37e93a409 remove duplicate translations 2025-07-24 09:43:17 +03:00
Alex Turchyn 561ac63c93 email editor 2025-07-24 09:41:57 +03:00
Pete Matsyburka 149e4be07a do not send invitation if completed 2025-07-23 10:20:21 +03:00
Pete Matsyburka eb98dc9e06 adjust folder form 2025-07-22 15:18:48 +03:00
Pete Matsyburka ce7d965358 update 2025-07-22 10:24:40 +03:00
Pete Matsyburka bb73024859 folder name to string 2025-07-22 09:49:58 +03:00
Pete Matsyburka 631a442e2a fix builder file drop modal close 2025-07-21 16:43:03 +03:00
Pete Matsyburka 20a082ae8e adjust image field validation 2025-07-21 16:26:08 +03:00
Alex Turchyn 154d706be1 Merge from docusealco/wip 2025-07-21 13:38:05 +03:00
Alex Turchyn aaae257c75 group initial and signature images in audit logs 2025-07-20 19:56:10 +03:00
Pete Matsyburka ec944a2283 add subfolders 2025-07-20 08:28:19 +03:00
Pete Matsyburka 69d63ecffe with signature id reason 2025-07-18 11:08:22 +03:00
Pete Matsyburka 06a1e2992d fix redirect url 2025-07-17 18:33:24 +03:00
Pete Matsyburka ec9293bac0 with signature id 2025-07-17 10:54:44 +03:00
Pete Matsyburka 8bf727c43c adjust font auto 2025-07-15 09:19:49 +03:00
Pete Matsyburka 708b8afdf5 optimize 2025-07-14 17:13:04 +03:00
Pete Matsyburka 6f7b9b1ebf code highligh 2025-07-14 13:43:26 +03:00
Pete Matsyburka 4f48c4fd9f fix gh font 2025-07-14 13:36:34 +03:00
Pete Matsyburka 9dac90e6cc cleanup 2025-07-14 13:29:39 +03:00
Alex Turchyn 0741a6f485 Merge from docusealco/wip 2025-07-14 13:26:14 +03:00
Pete Matsyburka 1f338dfd2b adjust font size 2025-07-14 12:53:15 +03:00
Pete Matsyburka 0310c26edd fix safari template folder drag&drop 2025-07-12 12:35:19 +03:00
Pete Matsyburka e43570d4c1 adjust nav 2025-07-12 11:51:10 +03:00
Pete Matsyburka d7142c2935 adjust setup nav 2025-07-12 11:16:14 +03:00
Pete Matsyburka fa6dc04ef1 add gh tooltip 2025-07-12 10:57:07 +03:00
Pete Matsyburka c5ec7a4b37 image step validation 2025-07-11 21:52:55 +03:00
Pete Matsyburka bd3d643318 simplify nav 2025-07-11 21:24:00 +03:00
Pete Matsyburka 1ae635963c update eid 2025-07-11 19:54:03 +03:00
Alex Turchyn a61c3e798d shared link 2fa 2025-07-11 09:30:24 +03:00
Pete Matsyburka 931addedbe adjust submitter index 2025-07-10 23:10:40 +03:00
Pete Matsyburka 9aa4a547cf with submitter timezone 2025-07-10 20:56:34 +03:00
Pete Matsyburka 82a7df5e3f add pdf/a-3b 2025-07-10 12:58:19 +03:00
Pete Matsyburka 0ff87383d0 fix selfsign 2025-07-08 16:48:57 +03:00
Pete Matsyburka ea3dec443f refactor 2025-07-07 15:40:23 +03:00
173 changed files with 2616 additions and 676 deletions

No files matched your search

+24 -11
View File
@@ -1,13 +1,26 @@
name: Build Docker Images
on:
push:
tags:
- "*.*.*"
workflow_dispatch:
inputs:
version:
description: Version
type: string
required: true
image:
description: QEMU image
type: string
required: false
default: tonistiigi/binfmt:latest
os:
description: OS
type: string
required: false
default: ubuntu-24.04-arm
jobs:
build:
runs-on: ubuntu-24.04-arm
runs-on: ${{ inputs.os }}
timeout-minutes: 20
steps:
@@ -16,23 +29,23 @@ jobs:
with:
submodules: recursive
-
name: Docker meta
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
docuseal/docuseal
tags: |
type=semver,pattern={{version}}
images: docuseal/docuseal
tags: latest,${{ inputs.version }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
image: ${{ inputs.image }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Create .version file
run: echo ${{ github.ref_name }} > .version
run: echo ${{ inputs.version }} > .version
- name: Login to Docker Hub
uses: docker/login-action@v3
+1 -1
View File
@@ -63,7 +63,7 @@ RSpec/MultipleExpectations:
Max: 25
RSpec/ExampleLength:
Max: 50
Max: 500
RSpec/MultipleMemoizedHelpers:
Max: 15
+1 -1
View File
@@ -50,7 +50,7 @@ ENV OPENSSL_CONF=/app/openssl_legacy.cnf
WORKDIR /app
RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev@edge redis libheif@edge vips-heif@edge gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev@edge yaml-dev redis libheif@edge vips-heif@edge gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
\n\
+94 -86
View File
@@ -1,29 +1,29 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (8.0.1)
actionpack (= 8.0.1)
activesupport (= 8.0.1)
actioncable (8.0.2.1)
actionpack (= 8.0.2.1)
activesupport (= 8.0.2.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (8.0.1)
actionpack (= 8.0.1)
activejob (= 8.0.1)
activerecord (= 8.0.1)
activestorage (= 8.0.1)
activesupport (= 8.0.1)
actionmailbox (8.0.2.1)
actionpack (= 8.0.2.1)
activejob (= 8.0.2.1)
activerecord (= 8.0.2.1)
activestorage (= 8.0.2.1)
activesupport (= 8.0.2.1)
mail (>= 2.8.0)
actionmailer (8.0.1)
actionpack (= 8.0.1)
actionview (= 8.0.1)
activejob (= 8.0.1)
activesupport (= 8.0.1)
actionmailer (8.0.2.1)
actionpack (= 8.0.2.1)
actionview (= 8.0.2.1)
activejob (= 8.0.2.1)
activesupport (= 8.0.2.1)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (8.0.1)
actionview (= 8.0.1)
activesupport (= 8.0.1)
actionpack (8.0.2.1)
actionview (= 8.0.2.1)
activesupport (= 8.0.2.1)
nokogiri (>= 1.8.5)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
@@ -31,35 +31,35 @@ GEM
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (8.0.1)
actionpack (= 8.0.1)
activerecord (= 8.0.1)
activestorage (= 8.0.1)
activesupport (= 8.0.1)
actiontext (8.0.2.1)
actionpack (= 8.0.2.1)
activerecord (= 8.0.2.1)
activestorage (= 8.0.2.1)
activesupport (= 8.0.2.1)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (8.0.1)
activesupport (= 8.0.1)
actionview (8.0.2.1)
activesupport (= 8.0.2.1)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (8.0.1)
activesupport (= 8.0.1)
activejob (8.0.2.1)
activesupport (= 8.0.2.1)
globalid (>= 0.3.6)
activemodel (8.0.1)
activesupport (= 8.0.1)
activerecord (8.0.1)
activemodel (= 8.0.1)
activesupport (= 8.0.1)
activemodel (8.0.2.1)
activesupport (= 8.0.2.1)
activerecord (8.0.2.1)
activemodel (= 8.0.2.1)
activesupport (= 8.0.2.1)
timeout (>= 0.4.0)
activestorage (8.0.1)
actionpack (= 8.0.1)
activejob (= 8.0.1)
activerecord (= 8.0.1)
activesupport (= 8.0.1)
activestorage (8.0.2.1)
actionpack (= 8.0.2.1)
activejob (= 8.0.2.1)
activerecord (= 8.0.2.1)
activesupport (= 8.0.2.1)
marcel (~> 1.0)
activesupport (8.0.1)
activesupport (8.0.2.1)
base64
benchmark (>= 0.3)
bigdecimal
@@ -104,9 +104,9 @@ GEM
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
net-http-persistent (~> 4.0)
nokogiri (~> 1, >= 1.10.8)
base64 (0.2.0)
base64 (0.3.0)
bcrypt (3.1.20)
benchmark (0.4.0)
benchmark (0.4.1)
better_html (2.1.1)
actionview (>= 6.0)
activesupport (>= 6.0)
@@ -114,7 +114,7 @@ GEM
erubi (~> 1.4)
parser (>= 2.4)
smart_properties
bigdecimal (3.1.8)
bigdecimal (3.2.2)
bindex (0.8.1)
bootsnap (1.18.4)
msgpack (~> 1.2)
@@ -141,8 +141,8 @@ GEM
cldr-plurals-runtime-rb (1.1.0)
cmdparse (3.0.7)
coderay (1.1.3)
concurrent-ruby (1.3.4)
connection_pool (2.4.1)
concurrent-ruby (1.3.5)
connection_pool (2.5.3)
crack (1.0.0)
bigdecimal
rexml
@@ -174,8 +174,9 @@ GEM
rake (>= 12.0.0, < 14.0.0)
docile (1.4.1)
dotenv (3.1.7)
drb (2.2.1)
drb (2.2.3)
email_typo (0.2.3)
erb (5.0.2)
erb_lint (0.7.0)
activesupport
better_html (>= 2.0.1)
@@ -276,13 +277,14 @@ GEM
openssl (>= 2.2.1)
htmlentities (4.3.4)
httpclient (2.8.3)
i18n (1.14.6)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
image_processing (1.13.0)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
io-console (0.8.0)
irb (1.14.3)
io-console (0.8.1)
irb (1.15.2)
pp (>= 0.6.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jmespath (1.6.2)
@@ -300,13 +302,13 @@ GEM
letter_opener (~> 1.9)
railties (>= 6.1)
rexml
logger (1.6.4)
logger (1.7.0)
lograge (0.14.0)
actionpack (>= 4)
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
loofah (2.23.1)
loofah (2.24.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
@@ -319,8 +321,8 @@ GEM
method_source (1.1.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
mini_portile2 (2.8.8)
minitest (5.25.4)
mini_portile2 (2.8.9)
minitest (5.25.5)
msgpack (1.7.5)
multi_json (1.15.0)
multipart-post (2.4.1)
@@ -328,28 +330,28 @@ GEM
mysql2 (0.5.6)
net-http-persistent (4.0.5)
connection_pool (~> 2.2)
net-imap (0.5.6)
net-imap (0.5.9)
date
net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.2.2)
timeout
net-smtp (0.5.0)
net-smtp (0.5.1)
net-protocol
nio4r (2.7.4)
nokogiri (1.18.8)
nokogiri (1.18.9)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.18.8-aarch64-linux-gnu)
nokogiri (1.18.9-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.8-aarch64-linux-musl)
nokogiri (1.18.9-aarch64-linux-musl)
racc (~> 1.4)
nokogiri (1.18.8-arm64-darwin)
nokogiri (1.18.9-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.8-x86_64-linux-gnu)
nokogiri (1.18.9-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.8-x86_64-linux-musl)
nokogiri (1.18.9-x86_64-linux-musl)
racc (~> 1.4)
oj (3.16.8)
bigdecimal (>= 3.0)
@@ -365,6 +367,8 @@ GEM
ast (~> 2.4.1)
racc
pg (1.5.9)
pp (0.6.2)
prettyprint
premailer (1.27.0)
addressable
css_parser (>= 1.19.0)
@@ -375,42 +379,44 @@ GEM
premailer (~> 1.7, >= 1.7.9)
pretender (0.5.0)
actionpack (>= 6.1)
prettyprint (0.2.0)
pry (0.15.0)
coderay (~> 1.1)
method_source (~> 1.0)
pry-rails (0.3.11)
pry (>= 0.13.0)
psych (5.2.2)
psych (5.2.6)
date
stringio
public_suffix (6.0.1)
puma (6.5.0)
nio4r (~> 2.0)
racc (1.8.1)
rack (3.1.16)
rack (3.2.0)
rack-proxy (0.7.7)
rack
rack-session (2.0.0)
rack-session (2.1.1)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.1.0)
rack-test (2.2.0)
rack (>= 1.3)
rackup (2.2.1)
rack (>= 3)
rails (8.0.1)
actioncable (= 8.0.1)
actionmailbox (= 8.0.1)
actionmailer (= 8.0.1)
actionpack (= 8.0.1)
actiontext (= 8.0.1)
actionview (= 8.0.1)
activejob (= 8.0.1)
activemodel (= 8.0.1)
activerecord (= 8.0.1)
activestorage (= 8.0.1)
activesupport (= 8.0.1)
rails (8.0.2.1)
actioncable (= 8.0.2.1)
actionmailbox (= 8.0.2.1)
actionmailer (= 8.0.2.1)
actionpack (= 8.0.2.1)
actiontext (= 8.0.2.1)
actionview (= 8.0.2.1)
activejob (= 8.0.2.1)
activemodel (= 8.0.2.1)
activerecord (= 8.0.2.1)
activestorage (= 8.0.2.1)
activesupport (= 8.0.2.1)
bundler (>= 1.15.0)
railties (= 8.0.1)
rails-dom-testing (2.2.0)
railties (= 8.0.2.1)
rails-dom-testing (2.3.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
@@ -424,22 +430,23 @@ GEM
actionview (> 3.1)
activesupport (> 3.1)
railties (> 3.1)
railties (8.0.1)
actionpack (= 8.0.1)
activesupport (= 8.0.1)
railties (8.0.2.1)
actionpack (= 8.0.2.1)
activesupport (= 8.0.2.1)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.2.1)
rdoc (6.10.0)
rake (13.3.0)
rdoc (6.14.2)
erb
psych (>= 4.0.0)
redis-client (0.23.0)
connection_pool
regexp_parser (2.9.3)
reline (0.6.0)
reline (0.6.2)
io-console (~> 0.5)
representable (3.2.0)
declarative (< 0.1.0)
@@ -538,10 +545,10 @@ GEM
sqlite3 (2.5.0-arm64-darwin)
sqlite3 (2.5.0-x86_64-linux-gnu)
sqlite3 (2.5.0-x86_64-linux-musl)
stringio (3.1.2)
stringio (3.1.7)
strip_attributes (1.14.1)
activemodel (>= 3.0, < 9.0)
thor (1.3.2)
thor (1.4.0)
timeout (0.4.3)
trailblazer-option (0.1.2)
turbo-rails (2.0.11)
@@ -574,12 +581,13 @@ GEM
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.9.1)
websocket-driver (0.7.6)
websocket-driver (0.8.0)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.7.1)
zeitwerk (2.7.3)
PLATFORMS
aarch64-linux
+2 -2
View File
@@ -27,7 +27,7 @@ class AccountsController < ApplicationController
unless URI.parse(@encrypted_config.value.to_s).class.in?([URI::HTTP, URI::HTTPS])
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url'))
return render :show, status: :unprocessable_entity
return render :show, status: :unprocessable_content
end
@encrypted_config.save!
@@ -39,7 +39,7 @@ class AccountsController < ApplicationController
redirect_to settings_account_path, notice: I18n.t('account_information_has_been_updated')
end
rescue ActiveRecord::RecordInvalid
render :show, status: :unprocessable_entity
render :show, status: :unprocessable_content
end
def destroy
+2 -2
View File
@@ -16,7 +16,7 @@ module Api
check_authorization
rescue_from Params::BaseValidator::InvalidParameterError do |e|
render json: { error: e.message }, status: :unprocessable_entity
render json: { error: e.message }, status: :unprocessable_content
end
rescue_from RateLimit::LimitApproached do |e|
@@ -33,7 +33,7 @@ module Api
rescue_from JSON::ParserError do |e|
Rollbar.warning(e) if defined?(Rollbar)
render json: { error: "JSON parse error: #{e.message}" }, status: :unprocessable_entity
render json: { error: "JSON parse error: #{e.message}" }, status: :unprocessable_content
end
end
+14 -3
View File
@@ -10,10 +10,21 @@ module Api
def create
submitter = Submitter.find_by!(slug: params[:submitter_slug])
if params[:type].in?(%w[initials signature]) && ImageUtils.blank?(Vips::Image.new_from_file(params[:file].path))
Rollbar.error("Empty signature: #{submitter.id}") if defined?(Rollbar)
if params[:type].in?(%w[initials signature])
image = Vips::Image.new_from_file(params[:file].path)
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_entity
if ImageUtils.blank?(image)
Rollbar.error("Empty signature: #{submitter.id}") if defined?(Rollbar)
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_content
end
if ImageUtils.error?(image)
Rollbar.error("Error signature: #{submitter.id}") if defined?(Rollbar)
return render json: { error: "#{params[:type]} error, try to sign on another device" },
status: :unprocessable_content
end
end
attachment = Submitters.create_attachment!(submitter, params)
@@ -11,8 +11,9 @@ module Api
params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present?
submitters = paginate(
submitters.preload(template: :folder, submission: [:submitters, { audit_trail_attachment: :blob,
combined_document_attachment: :blob }],
submitters.preload(template: { folder: :parent_folder },
submission: [:submitters, { audit_trail_attachment: :blob,
combined_document_attachment: :blob }],
documents_attachments: :blob, attachments_attachments: :blob),
field: :completed_at
)
@@ -14,7 +14,7 @@ module Api
submissions = filter_submissions(submissions, params)
submissions = paginate(submissions.preload(:created_by_user, :submitters,
template: :folder,
template: { folder: :parent_folder },
combined_document_attachment: :blob,
audit_trail_attachment: :blob))
@@ -50,12 +50,12 @@ module Api
def create
Params::SubmissionCreateValidator.call(params)
return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil?
return render json: { error: 'Template not found' }, status: :unprocessable_content if @template.nil?
if @template.fields.blank?
Rollbar.warning("Template does not contain fields: #{@template.id}") if defined?(Rollbar)
return render json: { error: 'Template does not contain fields' }, status: :unprocessable_entity
return render json: { error: 'Template does not contain fields' }, status: :unprocessable_content
end
params[:send_email] = true unless params.key?(:send_email)
@@ -82,7 +82,7 @@ module Api
DownloadUtils::UnableToDownload => e
Rollbar.warning(e) if defined?(Rollbar)
render json: { error: e.message }, status: :unprocessable_entity
render json: { error: e.message }, status: :unprocessable_content
end
def destroy
@@ -104,9 +104,10 @@ module Api
submissions = submissions.where(slug: params[:slug]) if params[:slug].present?
if params[:template_folder].present?
folder_ids = TemplateFolder.accessible_by(current_ability).where(name: params[:template_folder]).pluck(:id)
folders =
TemplateFolders.filter_by_full_name(TemplateFolder.accessible_by(current_ability), params[:template_folder])
submissions = submissions.joins(:template).where(template: { folder_id: folder_ids })
submissions = submissions.joins(:template).where(template: { folder_id: folders.pluck(:id) })
end
if params.key?(:archived)
@@ -184,7 +185,7 @@ module Api
message: %i[subject body],
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
:require_phone_2fa,
:require_phone_2fa, :order,
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
fields: [:name, :uuid, :default_value, :value, :title, :description,
:readonly, :required, :validation_pattern, :invalid_message,
+2 -2
View File
@@ -34,7 +34,7 @@ module Api
def update
if @submitter.completed_at?
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_entity
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_content
end
submission = @submitter.submission
@@ -74,7 +74,7 @@ module Api
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
Rollbar.warning(e) if defined?(Rollbar)
render json: { error: e.message }, status: :unprocessable_entity
render json: { error: e.message }, status: :unprocessable_content
end
def submitter_params
+5 -5
View File
@@ -7,7 +7,7 @@ module Api
def index
templates = filter_templates(@templates, params)
templates = paginate(templates.preload(:author, :folder))
templates = paginate(templates.preload(:author, folder: :parent_folder))
schema_documents =
ActiveStorage::Attachment.where(record_id: templates.map(&:id),
@@ -92,9 +92,9 @@ module Api
templates = templates.where(slug: params[:slug]) if params[:slug].present?
if params[:folder].present?
folder_ids = TemplateFolder.accessible_by(current_ability).where(name: params[:folder]).pluck(:id)
folders = TemplateFolders.filter_by_full_name(TemplateFolder.accessible_by(current_ability), params[:folder])
templates = templates.where(folder_id: folder_ids)
templates = templates.where(folder_id: folders.pluck(:id))
end
templates
@@ -106,10 +106,10 @@ module Api
:external_id,
:shared_link,
{
submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email]],
submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email order]],
fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value,
:title, :description,
:title, :description, :prefillable,
{ preferences: {},
conditions: [%i[field_uuid value action operation]],
options: [%i[value uuid]],
+3 -3
View File
@@ -7,8 +7,8 @@ module Api
def merge
files = params[:files] || []
return render json: { error: 'Files are required' }, status: :unprocessable_entity if files.blank?
return render json: { error: 'At least 2 files are required' }, status: :unprocessable_entity if files.size < 2
return render json: { error: 'Files are required' }, status: :unprocessable_content if files.blank?
return render json: { error: 'At least 2 files are required' }, status: :unprocessable_content if files.size < 2
render json: {
data: Base64.encode64(PdfUtils.merge(files.map { |base64| StringIO.new(Base64.decode64(base64)) }).string)
@@ -35,7 +35,7 @@ module Api
end
}
rescue HexaPDF::MalformedPDFError
render json: { error: 'Malformed PDF' }, status: :unprocessable_entity
render json: { error: 'Malformed PDF' }, status: :unprocessable_content
end
end
end
@@ -15,12 +15,12 @@ class EmailSmtpSettingsController < ApplicationController
redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved')
else
render :index, status: :unprocessable_entity
render :index, status: :unprocessable_content
end
rescue StandardError => e
flash[:alert] = e.message
render :index, status: :unprocessable_entity
render :index, status: :unprocessable_content
end
private
+2 -2
View File
@@ -53,7 +53,7 @@ class EsignSettingsController < ApplicationController
@cert_record.errors.add(:name, I18n.t('already_exists'))
return render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'),
status: :unprocessable_entity
status: :unprocessable_content
end
save_new_cert!(@encrypted_config, @cert_record)
@@ -64,7 +64,7 @@ class EsignSettingsController < ApplicationController
@cert_record.errors.add(:password, e.message)
render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), status: :unprocessable_entity
render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), status: :unprocessable_content
end
def update
+2 -2
View File
@@ -24,7 +24,7 @@ class MfaSetupController < ApplicationController
@error_message = I18n.t('code_is_invalid')
render turbo_stream: turbo_stream.replace(:mfa_form, partial: 'mfa_setup/form'), status: :unprocessable_entity
render turbo_stream: turbo_stream.replace(:mfa_form, partial: 'mfa_setup/form'), status: :unprocessable_content
end
end
@@ -36,7 +36,7 @@ class MfaSetupController < ApplicationController
else
@error_message = I18n.t('code_is_invalid')
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_entity
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_content
end
end
+2 -2
View File
@@ -11,7 +11,7 @@ class ProfileController < ApplicationController
if current_user.update(contact_params)
redirect_to settings_profile_index_path, notice: I18n.t('contact_information_has_been_update')
else
render :index, status: :unprocessable_entity
render :index, status: :unprocessable_content
end
end
@@ -20,7 +20,7 @@ class ProfileController < ApplicationController
bypass_sign_in(current_user)
redirect_to settings_profile_index_path, notice: I18n.t('password_has_been_changed')
else
render :index, status: :unprocessable_entity
render :index, status: :unprocessable_content
end
end
+1 -1
View File
@@ -16,7 +16,7 @@ class SessionsController < Devise::SessionsController
end
if User.exists?(email:, otp_required_for_login: true) && sign_in_params[:otp_attempt].blank?
return render :otp, locals: { resource: User.new(sign_in_params) }, status: :unprocessable_entity
return render :otp, locals: { resource: User.new(sign_in_params) }, status: :unprocessable_content
end
super
+3 -3
View File
@@ -23,10 +23,10 @@ class SetupController < ApplicationController
unless URI.parse(encrypted_config_params[:value].to_s).class.in?([URI::HTTP, URI::HTTPS])
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url'))
return render :index, status: :unprocessable_entity
return render :index, status: :unprocessable_content
end
return render :index, status: :unprocessable_entity unless @account.valid?
return render :index, status: :unprocessable_content unless @account.valid?
if @user.save
encrypted_configs = [
@@ -42,7 +42,7 @@ class SetupController < ApplicationController
redirect_to newsletter_path
else
render :index, status: :unprocessable_entity
render :index, status: :unprocessable_content
end
end
+58 -16
View File
@@ -6,12 +6,15 @@ class StartFormController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
around_action :with_browser_locale, only: %i[show completed]
around_action :with_browser_locale, only: %i[show update completed]
before_action :maybe_redirect_com, only: %i[show completed]
before_action :load_resubmit_submitter, only: :update
before_action :load_template
before_action :authorize_start!, only: :update
COOKIES_TTL = 12.hours
COOKIES_DEFAULTS = { httponly: true, secure: Rails.env.production? }.freeze
def show
raise ActionController::RoutingError, I18n.t('not_found') if @template.preferences['require_phone_2fa']
@@ -20,6 +23,7 @@ class StartFormController < ApplicationController
.submitters.new(account_id: @template.account_id,
uuid: (filter_undefined_submitters(@template).first ||
@template.submitters.first)['uuid'])
render :email_verification if params[:email_verification]
else
Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar)
@@ -38,7 +42,7 @@ class StartFormController < ApplicationController
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record?
@error_message = multiple_submitters_error_message
return render :show, status: :unprocessable_entity
return render :show, status: :unprocessable_content
end
if (is_new_record = @submitter.new_record?)
@@ -49,21 +53,14 @@ class StartFormController < ApplicationController
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
end
if @submitter.errors.blank? && @submitter.save
if is_new_record
WebhookUrls.enqueue_events(@submitter.submission, 'submission.created')
SearchEntries.enqueue_reindex(@submitter)
if @submitter.submission.expire_at?
ProcessSubmissionExpiredJob.perform_at(@submitter.submission.expire_at,
'submission_id' => @submitter.submission_id)
end
end
if @template.preferences['shared_link_2fa'] == true
handle_require_2fa(@submitter, is_new_record:)
elsif @submitter.errors.blank? && @submitter.save
enqueue_new_submitter_jobs(@submitter) if is_new_record
redirect_to submit_form_path(@submitter.slug)
else
render :show, status: :unprocessable_entity
render :show, status: :unprocessable_content
end
end
end
@@ -89,6 +86,16 @@ class StartFormController < ApplicationController
private
def enqueue_new_submitter_jobs(submitter)
WebhookUrls.enqueue_events(submitter.submission, 'submission.created')
SearchEntries.enqueue_reindex(submitter)
return unless submitter.submission.expire_at?
ProcessSubmissionExpiredJob.perform_at(submitter.submission.expire_at, 'submission_id' => submitter.submission_id)
end
def load_resubmit_submitter
@resubmit_submitter =
if params[:resubmit].present? && !params[:resubmit].in?([true, 'true'])
@@ -123,7 +130,7 @@ class StartFormController < ApplicationController
.order(id: :desc)
.where(declined_at: nil)
.where(external_id: nil)
.where(ip: [nil, request.remote_ip])
.where(template.preferences['shared_link_2fa'] == true ? {} : { ip: [nil, request.remote_ip] })
.then { |rel| params[:resubmit].present? || params[:selfsign].present? ? rel.where(completed_at: nil) : rel }
.find_or_initialize_by(find_params)
@@ -173,7 +180,7 @@ class StartFormController < ApplicationController
end
def submitter_params
return current_user.slice(:email) if params[:selfsign]
return { 'email' => current_user.email, 'name' => current_user.full_name } if params[:selfsign]
return @resubmit_submitter.slice(:name, :phone, :email) if @resubmit_submitter.present?
params.require(:submitter).permit(:email, :phone, :name).tap do |attrs|
@@ -197,4 +204,39 @@ class StartFormController < ApplicationController
I18n.t('not_found')
end
end
def handle_require_2fa(submitter, is_new_record:)
return render :show, status: :unprocessable_content if submitter.errors.present?
is_otp_verified = Submitters.verify_link_otp!(params[:one_time_code], submitter)
if cookies.encrypted[:email_2fa_slug] == submitter.slug || is_otp_verified
if submitter.save
enqueue_new_submitter_jobs(submitter) if is_new_record
if is_otp_verified
SubmissionEvents.create_with_tracking_data(submitter, 'email_verified', request)
cookies.encrypted[:email_2fa_slug] =
{ value: submitter.slug, expires: COOKIES_TTL.from_now, **COOKIES_DEFAULTS }
end
redirect_to submit_form_path(submitter.slug)
else
render :show, status: :unprocessable_content
end
else
Submitters.send_shared_link_email_verification_code(submitter, request:)
render :email_verification
end
rescue Submitters::UnableToSendCode, Submitters::InvalidOtp => e
redirect_to start_form_path(submitter.submission.template.slug,
params: submitter_params.merge(email_verification: true)),
alert: e.message
rescue RateLimit::LimitApproached
redirect_to start_form_path(submitter.submission.template.slug,
params: submitter_params.merge(email_verification: true)),
alert: I18n.t(:too_many_attempts)
end
end
@@ -0,0 +1,31 @@
# frozen_string_literal: true
class StartFormEmail2faSendController < ApplicationController
around_action :with_browser_locale
skip_before_action :authenticate_user!
skip_authorization_check
def create
@template = Template.find_by!(slug: params[:slug])
@submitter = @template.submissions.new(account_id: @template.account_id)
.submitters.new(**submitter_params, account_id: @template.account_id)
Submitters.send_shared_link_email_verification_code(@submitter, request:)
redir_params = { notice: I18n.t(:code_has_been_resent) } if params[:resend]
redirect_to start_form_path(@template.slug, params: submitter_params.merge(email_verification: true)),
**redir_params
rescue Submitters::UnableToSendCode => e
redirect_to start_form_path(@template.slug, params: submitter_params.merge(email_verification: true)),
alert: e.message
end
private
def submitter_params
params.require(:submitter).permit(:name, :email, :phone)
end
end
@@ -13,7 +13,7 @@ class StorageSettingsController < ApplicationController
redirect_to settings_storage_index_path, notice: I18n.t('changes_have_been_saved')
else
render :index, status: :unprocessable_entity
render :index, status: :unprocessable_content
end
end
@@ -12,6 +12,7 @@ class SubmissionEventsController < ApplicationController
'send_2fa_sms' => '2fa',
'send_sms' => 'send',
'phone_verified' => 'phone_check',
'email_verified' => 'email_check',
'click_sms' => 'hand_click',
'decline_form' => 'x',
'start_verification' => 'player_play',
+11 -10
View File
@@ -32,10 +32,7 @@ class SubmissionsController < ApplicationController
def create
save_template_message(@template, params) if params[:save_message] == '1'
if params[:is_custom_message] != '1'
params.delete(:subject)
params.delete(:body)
end
[params.delete(:subject), params.delete(:body)] if params[:is_custom_message] != '1'
submissions =
if params[:emails].present?
@@ -46,11 +43,16 @@ class SubmissionsController < ApplicationController
emails: params[:emails],
params: params.merge('send_completed_email' => true))
else
submissions_attrs = submissions_params[:submission].to_h.values
submissions_attrs, =
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_attrs, @template)
Submissions.create_from_submitters(template: @template,
user: current_user,
source: :invite,
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
submissions_attrs: submissions_params[:submission].to_h.values,
submissions_attrs:,
params: params.merge('send_completed_email' => true))
end
@@ -62,10 +64,9 @@ class SubmissionsController < ApplicationController
redirect_to template_path(@template), notice: I18n.t('new_recipients_have_been_added')
rescue Submissions::CreateFromSubmitters::BaseError => e
render turbo_stream: turbo_stream.replace(:submitters_error,
partial: 'submissions/error',
locals: { error: e.message }),
status: :unprocessable_entity
render turbo_stream: turbo_stream.replace(:submitters_error, partial: 'submissions/error',
locals: { error: e.message }),
status: :unprocessable_content
end
def destroy
@@ -95,7 +96,7 @@ class SubmissionsController < ApplicationController
end
def submissions_params
params.permit(submission: { submitters: [%i[uuid email phone name]] })
params.permit(submission: { submitters: [:uuid, :email, :phone, :name, { values: {} }] })
end
def load_template
+17 -7
View File
@@ -9,6 +9,7 @@ class SubmitFormController < ApplicationController
before_action :load_submitter, only: %i[show update completed]
before_action :maybe_render_locked_page, only: :show
before_action :maybe_require_link_2fa, only: %i[show update]
CONFIG_KEYS = [].freeze
@@ -47,20 +48,20 @@ class SubmitFormController < ApplicationController
def update
if @submitter.completed_at?
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_entity
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_content
end
if @submitter.template&.archived_at? || @submitter.submission.archived_at?
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_entity
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at?
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content
end
if @submitter.submission.expired?
return render json: { error: I18n.t('form_has_been_expired') }, status: :unprocessable_entity
return render json: { error: I18n.t('form_has_been_expired') }, status: :unprocessable_content
end
if @submitter.declined_at?
return render json: { error: I18n.t('form_has_been_declined') },
status: :unprocessable_entity
status: :unprocessable_content
end
Submitters::SubmitValues.call(@submitter, params, request)
@@ -69,9 +70,9 @@ class SubmitFormController < ApplicationController
rescue Submitters::SubmitValues::RequiredFieldError => e
Rollbar.warning("Required field #{@submitter.id}: #{e.message}") if defined?(Rollbar)
render json: { field_uuid: e.message }, status: :unprocessable_entity
render json: { field_uuid: e.message }, status: :unprocessable_content
rescue Submitters::SubmitValues::ValidationError => e
render json: { error: e.message }, status: :unprocessable_entity
render json: { error: e.message }, status: :unprocessable_content
end
def completed; end
@@ -80,6 +81,15 @@ class SubmitFormController < ApplicationController
private
def maybe_require_link_2fa
return if @submitter.submission.source != 'link'
return unless @submitter.submission.template&.preferences&.dig('shared_link_2fa') == true
return if cookies.encrypted[:email_2fa_slug] == @submitter.slug
return if @submitter.email == current_user&.email && current_user&.account_id == @submitter.account_id
redirect_to start_form_path(@submitter.submission.template.slug)
end
def maybe_render_locked_page
return render :archived if @submitter.submission.template&.archived_at? ||
@submitter.submission.archived_at? ||
@@ -11,13 +11,13 @@ class SubmitFormDownloadController < ApplicationController
return redirect_to submitter_download_index_path(@submitter.slug) if @submitter.completed_at?
return head :unprocessable_entity if @submitter.declined_at? ||
@submitter.submission.archived_at? ||
@submitter.submission.expired? ||
@submitter.submission.template&.archived_at? ||
AccountConfig.exists?(account_id: @submitter.account_id,
key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
value: false)
return head :unprocessable_content if @submitter.declined_at? ||
@submitter.submission.archived_at? ||
@submitter.submission.expired? ||
@submitter.submission.template&.archived_at? ||
AccountConfig.exists?(account_id: @submitter.account_id,
key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
value: false)
last_completed_submitter = @submitter.submission.submitters
.where.not(id: @submitter.id)
@@ -7,7 +7,7 @@ class SubmitFormInviteController < ApplicationController
def create
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return head :unprocessable_entity unless can_invite?(submitter)
return head :unprocessable_content unless can_invite?(submitter)
invite_submitters = filter_invite_submitters(submitter, 'invite_by_uuid')
optional_invite_submitters = filter_invite_submitters(submitter, 'optional_invite_by_uuid')
@@ -34,7 +34,7 @@ class SubmitFormInviteController < ApplicationController
head :ok
else
head :unprocessable_entity
head :unprocessable_content
end
end
@@ -5,7 +5,7 @@ class TemplateDocumentsController < ApplicationController
def create
if params[:blobs].blank? && params[:files].blank?
return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_entity
return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_content
end
old_fields_hash = @template.fields.hash
@@ -28,6 +28,6 @@ class TemplateDocumentsController < ApplicationController
)
}
rescue Templates::CreateAttachments::PdfEncrypted
render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_entity
render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_content
end
end
@@ -3,14 +3,31 @@
class TemplateFoldersAutocompleteController < ApplicationController
load_and_authorize_resource :template_folder, parent: false
LIMIT = 100
LIMIT = 30
def index
templates_query = Template.accessible_by(current_ability).where(archived_at: nil)
parent_name, name =
if params[:parent_name].present?
[params[:parent_name], params[:q]]
else
params[:q].to_s.split(' /', 2).map(&:squish)
end
template_folders = @template_folders.where(id: templates_query.select(:folder_id))
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
if name
parent_folder = @template_folders.find_by(name: parent_name, parent_folder_id: nil)
else
name = parent_name
end
render json: template_folders.as_json(only: %i[name archived_at])
template_folders = TemplateFolders.filter_active_folders(@template_folders.where(parent_folder:),
Template.accessible_by(current_ability))
name = name.to_s.downcase
template_folders = TemplateFolders.search(template_folders, name).order(id: :desc).limit(LIMIT)
render json: template_folders.preload(:parent_folder)
.sort_by { |e| e.name.downcase.index(name) || Float::MAX }
.as_json(only: %i[name archived_at], methods: %i[full_name])
end
end
+49 -6
View File
@@ -5,13 +5,39 @@ class TemplateFoldersController < ApplicationController
helper_method :selected_order
def show
@templates = @template_folder.templates.active.accessible_by(current_ability)
.preload(:author, :template_accesses)
@templates = Templates.search(current_user, @templates, params[:q])
@templates = Templates::Order.call(@templates, current_user, selected_order)
TEMPLATES_PER_PAGE = 12
FOLDERS_PER_PAGE = 18
@pagy, @templates = pagy_auto(@templates, limit: 12)
def show
@templates = Template.active.accessible_by(current_ability)
.where(folder: [@template_folder, *(params[:q].present? ? @template_folder.subfolders : [])])
.preload(:author, :template_accesses)
@template_folders =
@template_folder.subfolders.where(id: Template.accessible_by(current_ability).active.select(:folder_id))
@template_folders = TemplateFolders.search(@template_folders, params[:q])
@template_folders = TemplateFolders.sort(@template_folders, current_user, selected_order)
if @templates.exists?
@templates = Templates.search(current_user, @templates, params[:q])
@templates = Templates::Order.call(@templates, current_user, selected_order)
limit =
if @template_folders.size < 4
TEMPLATES_PER_PAGE
else
(@template_folders.size < 7 ? 9 : 6)
end
@pagy, @templates = pagy_auto(@templates, limit:)
load_related_submissions if params[:q].present? && @templates.blank?
else
@pagy, @template_folders = pagy(@template_folders, limit: FOLDERS_PER_PAGE)
@templates = @templates.none
end
end
def edit; end
@@ -40,4 +66,21 @@ class TemplateFoldersController < ApplicationController
def template_folder_params
params.require(:template_folder).permit(:name)
end
def load_related_submissions
@related_submissions =
Submission.accessible_by(current_ability)
.where(archived_at: nil)
.where(template_id: current_account.templates.active
.where(folder: [@template_folder, *@template_folder.subfolders])
.select(:id))
.preload(:template_accesses, :created_by_user,
template: :author,
submitters: :start_form_submission_events)
@related_submissions = Submissions.search(current_user, @related_submissions, params[:q])
.order(id: :desc)
@related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5)
end
end
@@ -4,9 +4,27 @@ class TemplatesArchivedController < ApplicationController
load_and_authorize_resource :template, parent: false
def index
@templates = @templates.where.not(archived_at: nil).preload(:author, :folder, :template_accesses).order(id: :desc)
@templates = @templates.where.not(archived_at: nil)
.preload(:author, :template_accesses, folder: :parent_folder)
.order(id: :desc)
@templates = Templates.search(current_user, @templates, params[:q])
@pagy, @templates = pagy_auto(@templates, limit: 12)
return unless params[:q].present? && @templates.blank?
@related_submissions =
Submission.accessible_by(current_ability)
.joins(:template)
.where.not(templates: { archived_at: nil })
.preload(:template_accesses, :created_by_user,
template: :author,
submitters: :start_form_submission_events)
@related_submissions = Submissions.search(current_user, @related_submissions, params[:q])
.order(id: :desc)
@related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5)
end
end
@@ -4,7 +4,7 @@ class TemplatesCloneAndReplaceController < ApplicationController
load_and_authorize_resource :template
def create
return head :unprocessable_entity if params[:files].blank?
return head :unprocessable_content if params[:files].blank?
ActiveRecord::Associations::Preloader.new(
records: [@template],
@@ -31,7 +31,7 @@ class TemplatesCloneAndReplaceController < ApplicationController
rescue Templates::CreateAttachments::PdfEncrypted
respond_to do |f|
f.html { render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password) }
f.json { render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_entity }
f.json { render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_content }
end
end
end
+3 -3
View File
@@ -78,7 +78,7 @@ class TemplatesController < ApplicationController
maybe_redirect_to_template(@template)
else
render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_entity
render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_content
end
end
@@ -117,10 +117,10 @@ class TemplatesController < ApplicationController
params.require(:template).permit(
:name,
{ schema: [[:attachment_uuid, :name, { conditions: [%i[field_uuid value action operation]] }]],
submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid optional_invite_by_uuid email]],
submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid optional_invite_by_uuid email order]],
fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value,
:title, :description,
:title, :description, :prefillable,
{ preferences: {},
conditions: [%i[field_uuid value action operation]],
options: [%i[value uuid]],
@@ -11,10 +11,11 @@ class TemplatesDashboardController < ApplicationController
helper_method :selected_order
def index
@template_folders = @template_folders.where(id: @templates.active.select(:folder_id))
@template_folders =
TemplateFolders.filter_active_folders(@template_folders.where(parent_folder_id: nil), @templates)
@template_folders = TemplateFolders.search(@template_folders, params[:q])
@template_folders = sort_template_folders(@template_folders, current_user, selected_order)
@template_folders = TemplateFolders.sort(@template_folders, current_user, selected_order)
@pagy, @template_folders = pagy(
@template_folders,
@@ -68,40 +69,6 @@ class TemplatesDashboardController < ApplicationController
Templates.search(current_user, rel, params[:q])
end
def sort_template_folders(template_folders, current_user, order)
case order
when 'used_at'
subquery =
Template.left_joins(:submissions)
.group(:folder_id)
.where(account_id: current_user.account_id)
.select(
:folder_id,
Template.arel_table[:updated_at].maximum.as('updated_at_max'),
Submission.arel_table[:created_at].maximum.as('submission_created_at_max')
)
template_folders = template_folders.joins(
Template.arel_table
.join(subquery.arel.as('templates'), Arel::Nodes::OuterJoin)
.on(TemplateFolder.arel_table[:id].eq(Template.arel_table[:folder_id]))
.join_sources
)
template_folders.order(
Arel::Nodes::Case.new
.when(Template.arel_table[:submission_created_at_max].gt(Template.arel_table[:updated_at_max]))
.then(Template.arel_table[:submission_created_at_max])
.else(Template.arel_table[:updated_at_max])
.desc
)
when 'name'
template_folders.order(name: :asc)
else
template_folders.order(id: :desc)
end
end
def selected_order
@selected_order ||=
if cookies.permanent[:dashboard_templates_order].blank? ||
@@ -3,6 +3,8 @@
class TemplatesDebugController < ApplicationController
load_and_authorize_resource :template
DEBUG_FILE = ''
def show
attachment = @template.documents.first
@@ -16,6 +18,8 @@ class TemplatesDebugController < ApplicationController
@template.update!(fields: Templates::ProcessDocument.normalize_attachment_fields(@template, [attachment]))
debug_file if DEBUG_FILE.present?
ActiveRecord::Associations::Preloader.new(
records: [@template],
associations: [schema_documents: { preview_images_attachments: :blob }]
@@ -31,4 +35,27 @@ class TemplatesDebugController < ApplicationController
render 'templates/edit', layout: 'plain'
end
def debug_file
tempfile = Tempfile.new
tempfile.binmode
tempfile.write(File.read(DEBUG_FILE))
tempfile.rewind
filename = File.basename(DEBUG_FILE)
file = ActionDispatch::Http::UploadedFile.new(
tempfile:,
filename:,
type: Marcel::MimeType.for(tempfile)
)
params = { files: [file] }
documents = Templates::CreateAttachments.call(@template, params)
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
@template.update!(schema:)
end
end
@@ -6,7 +6,9 @@ class TemplatesFoldersController < ApplicationController
def edit; end
def update
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:name])
name = [params[:parent_name], params[:name]].compact_blank.join(' / ')
@template.folder = TemplateFolders.find_or_create_by_name(current_user, name)
if @template.save
redirect_back(fallback_location: template_path(@template), notice: I18n.t('document_template_has_been_moved'))
@@ -26,8 +26,8 @@ class TemplatesPreferencesController < ApplicationController
completed_notification_email_attach_documents
completed_redirect_url validate_unique_submitters
require_all_submitters submitters_order require_phone_2fa
default_expire_at_duration
default_expire_at
default_expire_at_duration shared_link_2fa
default_expire_at request_email_enabled
completed_notification_email_subject completed_notification_email_body
completed_notification_email_enabled completed_notification_email_attach_audit] +
[completed_message: %i[title body],
@@ -0,0 +1,26 @@
# frozen_string_literal: true
class TemplatesPrefillableFieldsController < ApplicationController
PREFILLABLE_FIELD_TYPES = %w[text number cells date checkbox select radio phone].freeze
load_and_authorize_resource :template
def create
authorize!(:update, @template)
field = @template.fields.find { |f| f['uuid'] == params[:field_uuid] }
if params[:prefillable] == 'false'
field.delete('prefillable')
field.delete('readonly')
elsif params[:prefillable] == 'true'
field['prefillable'] = true
field['readonly'] = true
end
@template.save!
render turbo_stream: turbo_stream.replace(:prefillable_fields_list, partial: 'list',
locals: { template: @template })
end
end
@@ -9,6 +9,10 @@ class TemplatesRecipientsController < ApplicationController
@template.submitters =
submitters_params.map { |s| s.reject { |_, v| v.is_a?(String) && v.blank? } }
if @template.submitters.each_with_index.all? { |s, index| s['order'] == index }
@template.submitters.each { |s| s.delete('order') }
end
@template.save!
render json: { submitters: @template.submitters }
@@ -18,7 +22,7 @@ class TemplatesRecipientsController < ApplicationController
def submitters_params
permit_params = { submitters: [%i[name uuid is_requester optional_invite_by_uuid
invite_by_uuid linked_to_uuid email option]] }
invite_by_uuid linked_to_uuid email option order]] }
params.require(:template).permit(permit_params).fetch(:submitters, {}).values.filter_map do |s|
next if s[:uuid].blank?
@@ -29,6 +33,7 @@ class TemplatesRecipientsController < ApplicationController
s.delete(:is_requester)
end
s[:order] = s[:order].to_i if s[:order].present?
s.delete(:invite_by_uuid) if s[:invite_by_uuid].blank?
s.delete(:optional_invite_by_uuid) if s[:optional_invite_by_uuid].blank?
+3 -3
View File
@@ -27,7 +27,7 @@ class UsersController < ApplicationController
if User.accessible_by(current_ability).exists?(email: @user.email)
@user.errors.add(:email, I18n.t('already_exists'))
return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity
return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_content
end
@user.role = User::ADMIN_ROLE unless role_valid?(@user.role)
@@ -37,7 +37,7 @@ class UsersController < ApplicationController
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_invited')
else
render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity
render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_content
end
end
@@ -57,7 +57,7 @@ class UsersController < ApplicationController
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
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_content
end
end
+1 -1
View File
@@ -52,7 +52,7 @@ class WebhookEventsController < ApplicationController
turbo_stream.replace(helpers.dom_id(@webhook_event),
partial: 'event_row',
locals: { with_status: true, webhook_url: @webhook_url, webhook_event: @webhook_event }),
turbo_stream.replace("drawer_events_#{helpers.dom_id(@webhook_event)}",
turbo_stream.replace(helpers.dom_id(@webhook_event, :drawer_events),
partial: 'drawer_events',
locals: { webhook_url: @webhook_url, webhook_event: @webhook_event })
]
+6
View File
@@ -36,6 +36,9 @@ import IndeterminateCheckbox from './elements/indeterminate_checkbox'
import AppTour from './elements/app_tour'
import DashboardDropzone from './elements/dashboard_dropzone'
import RequiredCheckboxGroup from './elements/required_checkbox_group'
import PageContainer from './elements/page_container'
import EmailEditor from './elements/email_editor'
import MountOnClick from './elements/mount_on_click'
import * as TurboInstantClick from './lib/turbo_instant_click'
@@ -107,6 +110,9 @@ safeRegisterElement('app-tour', AppTour)
safeRegisterElement('dashboard-dropzone', DashboardDropzone)
safeRegisterElement('check-on-click', CheckOnClick)
safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup)
safeRegisterElement('page-container', PageContainer)
safeRegisterElement('email-editor', EmailEditor)
safeRegisterElement('mount-on-click', MountOnClick)
safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () {
+8
View File
@@ -147,3 +147,11 @@ button[disabled] .enabled {
outline-offset: 3px;
outline-color: hsl(var(--bc) / 0.2);
}
.font-times {
font-family: "Times New Roman", Times, ui-serif, serif, Cambria, Georgia;
}
.font-courier {
font-family: "Courier New", Consolas, "Liberation Mono", monospace, ui-monospace, SFMono-Regular, Menlo, Monaco;
}
+29 -3
View File
@@ -15,6 +15,7 @@ export default targetable(class extends HTMLElement {
static [target.static] = [
'form',
'fileDropzone',
'folderDropzone',
'fileDropzoneLoading'
]
@@ -25,12 +26,13 @@ export default targetable(class extends HTMLElement {
window.addEventListener('dragleave', this.onWindowDragleave)
this.fileDropzone?.addEventListener('drop', this.onDropFile)
this.folderDropzone?.addEventListener('drop', this.onDropNewFolder)
this.folderCards.forEach((el) => el.addEventListener('drop', (e) => this.onDropFolder(e, el)))
this.templateCards.forEach((el) => el.addEventListener('drop', this.onDropTemplate))
this.templateCards.forEach((el) => el.addEventListener('dragstart', this.onTemplateDragStart))
return [this.fileDropzone, ...this.folderCards, ...this.templateCards].forEach((el) => {
return [this.fileDropzone, this.folderDropzone, ...this.folderCards, ...this.templateCards].forEach((el) => {
el?.addEventListener('dragover', this.onDragover)
el?.addEventListener('dragleave', this.onDragleave)
})
@@ -46,6 +48,10 @@ export default targetable(class extends HTMLElement {
onTemplateDragStart = (e) => {
const id = e.target.href.split('/').pop()
this.folderCards.forEach((el) => el.classList.remove('bg-base-200', 'before:hidden'))
this.folderDropzone?.classList?.remove('hidden')
window.flash?.remove()
e.dataTransfer.effectAllowed = 'move'
if (id) {
@@ -98,13 +104,13 @@ export default targetable(class extends HTMLElement {
el.classList.add('opacity-50')
if (e.dataTransfer.files.length) {
const params = new URLSearchParams({ folder_name: el.innerText }).toString()
const params = new URLSearchParams({ folder_name: el.innerText.trim() }).toString()
this.uploadFiles(e.dataTransfer.files, `/templates_upload?${params}`)
} else {
const formData = new FormData()
formData.append('name', el.innerText)
formData.append('name', el.dataset.fullName)
fetch(`/templates/${templateId}/folder`, {
method: 'PUT',
@@ -176,6 +182,24 @@ export default targetable(class extends HTMLElement {
}
}
onDropNewFolder (e) {
e.preventDefault()
const templateId = e.dataTransfer.getData('template_id')
const a = document.createElement('a')
a.href = `/templates/${templateId}/folder/edit?autocomplete=false`
a.dataset.turboFrame = 'modal'
a.classList.add('hidden')
document.body.append(a)
a.click()
a.remove()
}
onDragleave () {
this.style.backgroundColor = null
@@ -199,6 +223,7 @@ export default targetable(class extends HTMLElement {
this.isDrag = true
window.flash?.remove()
this.fileDropzone?.classList?.remove('hidden')
this.hiddenOnDrag.forEach((el) => { el.style.display = 'none' })
@@ -212,6 +237,7 @@ export default targetable(class extends HTMLElement {
this.isDrag = false
this.fileDropzone?.classList?.add('hidden')
this.folderDropzone?.classList?.add('hidden')
this.hiddenOnDrag.forEach((el) => { el.style.display = null })
+131
View File
@@ -0,0 +1,131 @@
import { target, targetable } from '@github/catalyst/lib/targetable'
let loaderPromise = null
function loadCodeMirror () {
if (!loaderPromise) {
loaderPromise = Promise.all([
import(/* webpackChunkName: "email-editor" */ '@codemirror/view'),
import(/* webpackChunkName: "email-editor" */ '@codemirror/commands'),
import(/* webpackChunkName: "email-editor" */ '@codemirror/language'),
import(/* webpackChunkName: "email-editor" */ '@codemirror/lang-html'),
import(/* webpackChunkName: "email-editor" */ '@specious/htmlflow')
]).then(([view, commands, language, html, htmlflow]) => {
return {
minimalSetup: [
commands.history(),
language.syntaxHighlighting(language.defaultHighlightStyle, { fallback: true }),
view.keymap.of([...commands.defaultKeymap, ...commands.historyKeymap])
],
EditorView: view.EditorView,
html: html.html,
htmlflow: htmlflow.default || htmlflow
}
})
}
return loaderPromise
}
export default targetable(class extends HTMLElement {
static [target.static] = [
'codeViewTab',
'previewViewTab',
'editorContainer',
'previewIframe'
]
connectedCallback () {
this.mount()
if (this.input.value) {
this.showPreviewView()
} else {
this.showCodeView()
}
this.previewViewTab.addEventListener('click', this.showPreviewView)
this.codeViewTab.addEventListener('click', this.showCodeView)
}
showCodeView = () => {
this.editorView.dispatch({
changes: { from: 0, to: this.editorView.state.doc.length, insert: this.input.value }
})
this.previewViewTab.classList.remove('tab-active', 'tab-bordered')
this.previewViewTab.classList.add('pb-[3px]')
this.codeViewTab.classList.remove('pb-[3px]')
this.codeViewTab.classList.add('tab-active', 'tab-bordered')
this.editorContainer.classList.remove('hidden')
this.previewIframe.classList.add('hidden')
}
showPreviewView = () => {
this.previewIframe.srcdoc = this.input.value
this.codeViewTab.classList.remove('tab-active', 'tab-bordered')
this.codeViewTab.classList.add('pb-[3px]')
this.previewViewTab.classList.remove('pb-[3px]')
this.previewViewTab.classList.add('tab-active', 'tab-bordered')
this.editorContainer.classList.add('hidden')
this.previewIframe.classList.remove('hidden')
}
async mount () {
this.input = this.querySelector('input[type="hidden"]')
this.input.style.display = 'none'
const { EditorView, minimalSetup, html, htmlflow } = await loadCodeMirror()
this.editorView = new EditorView({
doc: this.input.value,
parent: this.editorContainer,
extensions: [
html(),
minimalSetup,
EditorView.lineWrapping,
EditorView.updateListener.of(update => {
if (update.docChanged) this.input.value = update.state.doc.toString()
}),
EditorView.theme({
'&': {
backgroundColor: 'white',
color: 'black',
fontSize: '14px',
fontFamily: 'monospace'
},
'&.cm-focused': {
outline: 'none'
},
'&.cm-editor': {
borderRadius: '0.375rem',
border: 'none'
},
'.cm-gutters': {
display: 'none'
}
})
]
})
this.previewIframe.srcdoc = this.editorView.state.doc.toString()
this.previewIframe.onload = () => {
const previewIframeDoc = this.previewIframe.contentDocument
if (previewIframeDoc.body) {
previewIframeDoc.body.contentEditable = true
}
const contentDocument = this.previewIframe.contentDocument || this.previewIframe.contentWindow.document
contentDocument.body.addEventListener('input', async () => {
const html = contentDocument.documentElement.outerHTML.replace(' contenteditable="true"', '')
const prettifiedHtml = await htmlflow(html)
this.input.value = prettifiedHtml
})
}
}
})
@@ -2,6 +2,8 @@ import autocomplete from 'autocompleter'
export default class extends HTMLElement {
connectedCallback () {
if (this.dataset.enabled === 'false') return
autocomplete({
input: this.input,
preventSubmit: this.dataset.submitOnSelect === 'true' ? 0 : 1,
@@ -14,12 +16,16 @@ export default class extends HTMLElement {
}
onSelect = (item) => {
this.input.value = item.name
this.input.value = this.dataset.parentName ? item.name : item.full_name
}
fetch = (text, resolve) => {
const queryParams = new URLSearchParams({ q: text })
if (this.dataset.parentName) {
queryParams.append('parent_name', this.dataset.parentName)
}
fetch('/template_folders_autocomplete?' + queryParams).then(async (resp) => {
const items = await resp.json()
@@ -34,7 +40,7 @@ export default class extends HTMLElement {
div.setAttribute('dir', 'auto')
div.textContent = item.name
div.textContent = this.dataset.parentName ? item.name : item.full_name
return div
}
+11
View File
@@ -0,0 +1,11 @@
export default class extends HTMLElement {
connectedCallback () {
this.addEventListener('click', () => {
document.body.append(this.template.content)
})
}
get template () {
return document.getElementById(this.dataset.templateId)
}
}
+12
View File
@@ -0,0 +1,12 @@
export default class extends HTMLElement {
connectedCallback () {
const image = this.querySelector('img')
image.addEventListener('load', (e) => {
image.setAttribute('width', e.target.naturalWidth)
image.setAttribute('height', e.target.naturalHeight)
this.style.aspectRatio = `${e.target.naturalWidth} / ${e.target.naturalHeight}`
})
}
}
@@ -7,5 +7,9 @@ export default actionable(class extends HTMLElement {
elementIds.forEach((elementId) => {
document.getElementById(elementId).classList.toggle('hidden', (event.target.dataset.toggleId || event.target.value) !== elementId)
})
if (this.dataset.focusId) {
document.getElementById(this.dataset.focusId)?.focus()
}
}
})
+2
View File
@@ -5,6 +5,7 @@ import DownloadButton from './elements/download_button'
import ToggleSubmit from './elements/toggle_submit'
import FetchForm from './elements/fetch_form'
import ScrollButtons from './elements/scroll_buttons'
import PageContainer from './elements/page_container'
const safeRegisterElement = (name, element, options = {}) => !window.customElements.get(name) && window.customElements.define(name, element, options)
@@ -12,6 +13,7 @@ safeRegisterElement('download-button', DownloadButton)
safeRegisterElement('toggle-submit', ToggleSubmit)
safeRegisterElement('fetch-form', FetchForm)
safeRegisterElement('scroll-buttons', ScrollButtons)
safeRegisterElement('page-container', PageContainer)
safeRegisterElement('submission-form', class extends HTMLElement {
connectedCallback () {
this.appElem = document.createElement('div')
+8
View File
@@ -70,3 +70,11 @@ button[disabled] .enabled {
.base-radio {
@apply radio bg-white radio-sm;
}
.font-times {
font-family: "Times New Roman", Times, ui-serif, serif, Cambria, Georgia;
}
.font-courier {
font-family: "Courier New", Consolas, "Liberation Mono", monospace, ui-monospace, SFMono-Regular, Menlo, Monaco;
}
+49 -14
View File
@@ -2,8 +2,8 @@
<div
class="flex absolute lg:text-base -outline-offset-1 field-area"
dir="auto"
:style="computedStyle"
:class="{ '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, 'outline-red-500 outline-dashed outline-2 z-10 field-area-active': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
:style="[computedStyle, fontStyle]"
:class="{ 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'outline-red-500 outline-dashed outline-2 z-10 field-area-active': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
>
<div
v-if="(!withFieldPlaceholder || !field.name || field.type === 'cells') && !isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
@@ -56,11 +56,11 @@
<div
v-else-if="field.type === 'signature' && signature"
class="flex justify-between h-full gap-1 overflow-hidden w-full"
:class="isNarrow && (withSignatureId || field.preferences?.reason_field_uuid) ? 'flex-row' : 'flex-col'"
:class="isNarrow && (isShowSignatureId || field.preferences?.reason_field_uuid) ? 'flex-row' : 'flex-col'"
>
<div
class="flex overflow-hidden"
:class="isNarrow && (withSignatureId || field.preferences?.reason_field_uuid) ? 'w-1/2' : 'flex-grow'"
:class="isNarrow && (isShowSignatureId || field.preferences?.reason_field_uuid) ? 'w-1/2' : 'flex-grow'"
style="min-height: 50%"
>
<img
@@ -69,7 +69,7 @@
>
</div>
<div
v-if="withSignatureId || field.preferences?.reason_field_uuid"
v-if="isShowSignatureId || field.preferences?.reason_field_uuid"
class="text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem]"
:class="isNarrow ? 'w-1/2' : 'w-full'"
>
@@ -236,6 +236,11 @@ export default {
type: Object,
required: true
},
isInlineSize: {
type: Boolean,
required: false,
default: true
},
submitter: {
type: Object,
required: false,
@@ -328,6 +333,13 @@ export default {
verification: this.t('verify_id')
}
},
isShowSignatureId () {
if ([true, false].includes(this.field.preferences?.with_signature_id)) {
return this.field.preferences.with_signature_id
} else {
return this.withSignatureId
}
},
alignClasses () {
if (!this.field.preferences) {
return { 'items-center': true }
@@ -348,8 +360,8 @@ export default {
}
return {
'font-mono': this.field.preferences.font === 'Courier',
'font-serif': this.field.preferences.font === 'Times',
'font-courier': this.field.preferences.font === 'Courier',
'font-times': this.field.preferences.font === 'Times',
'font-bold': ['bold_italic', 'bold'].includes(this.field.preferences.font_type),
italic: ['bold_italic', 'italic'].includes(this.field.preferences.font_type)
}
@@ -427,6 +439,34 @@ export default {
return []
}
},
fontStyle () {
let fontSize = ''
if (this.isInlineSize) {
if (this.textOverflowChars) {
fontSize = `${this.fontSizePx / 1.5 / 10}cqmin`
} else {
fontSize = `${this.fontSizePx / 10}cqmin`
}
} else {
if (this.textOverflowChars) {
fontSize = `clamp(1pt, ${this.fontSizePx / 1.5 / 10}vw, ${this.fontSizePx / 1.5}px)`
} else {
fontSize = `clamp(1pt, ${this.fontSizePx / 10}vw, ${this.fontSizePx}px)`
}
}
return { fontSize, lineHeight: `calc(${fontSize} * ${this.lineHeight})` }
},
fontSizePx () {
return parseInt(this.field?.preferences?.font_size || 11) * this.fontScale
},
lineHeight () {
return 1.3
},
fontScale () {
return 1000 / 612.0
},
computedStyle () {
const { x, y, w, h } = this.area
@@ -437,11 +477,6 @@ export default {
height: h * 100 + '%'
}
if (this.field.preferences?.font_size) {
style.fontSize = `clamp(4pt, 1.6vw, ${parseInt(this.field.preferences.font_size) * 1.23}pt)`
style.lineHeight = `clamp(6pt, 2.0vw, ${parseInt(this.field.preferences.font_size) * 1.23 + 3}pt)`
}
if (this.field.preferences?.color) {
style.color = this.field.preferences.color
}
@@ -456,7 +491,7 @@ export default {
modelValue () {
this.$nextTick(() => {
if (['date', 'text', 'number'].includes(this.field.type) && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.modelValue}`.length)) {
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? `${this.modelValue || (this.withFieldPlaceholder ? this.field.name : '')}`.length : 0
this.textOverflowChars = this.$refs.textContainer.scrollHeight > (this.$refs.textContainer.clientHeight + 1) ? `${this.modelValue || (this.withFieldPlaceholder ? this.field.name : '')}`.length : 0
}
})
}
@@ -464,7 +499,7 @@ export default {
mounted () {
this.$nextTick(() => {
if (['date', 'text', 'number'].includes(this.field.type) && this.$refs.textContainer) {
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? `${this.modelValue || (this.withFieldPlaceholder ? this.field.name : '')}`.length : 0
this.textOverflowChars = this.$refs.textContainer.scrollHeight > (this.$refs.textContainer.clientHeight + 1) ? `${this.modelValue || (this.withFieldPlaceholder ? this.field.name : '')}`.length : 0
}
})
},
+4
View File
@@ -23,6 +23,7 @@
:area="area"
:submittable="submittable"
:field-index="fieldIndex"
:is-inline-size="isInlineSize"
:scroll-padding="scrollPadding"
:submitter="submitter"
:with-field-placeholder="withFieldPlaceholder"
@@ -110,6 +111,9 @@ export default {
}
},
computed: {
isInlineSize () {
return CSS.supports('container-type: size')
},
isMobileContainer () {
const root = this.$root.$el.parentNode.getRootNode()
const container = root.body || root.querySelector('div')
+28 -4
View File
@@ -95,16 +95,40 @@ export default {
},
methods: {
onDropFiles (e) {
this.uploadFiles(e.dataTransfer.files)
const files = Array.from(e.dataTransfer.files).filter((f) => {
if (this.accept === 'image/*') {
return f.type.startsWith('image')
} else {
return true
}
})
if (this.accept === 'image/*' && !files.length) {
alert(this.t('please_upload_an_image_file'))
} else {
this.uploadFiles(files)
}
},
onSelectFiles (e) {
e.preventDefault()
this.uploadFiles(this.$refs.input.files).then(() => {
if (this.$refs.input) {
this.$refs.input.value = ''
const files = Array.from(this.$refs.input.files).filter((f) => {
if (this.accept === 'image/*') {
return f.type.startsWith('image')
} else {
return true
}
})
if (this.accept === 'image/*' && !files.length) {
alert(this.t('please_upload_an_image_file'))
} else {
this.uploadFiles(files).then(() => {
if (this.$refs.input) {
this.$refs.input.value = ''
}
})
}
},
async uploadFiles (files) {
this.isLoading = true
+1 -1
View File
@@ -67,7 +67,7 @@
<button
v-if="!isFormVisible"
id="expand_form_button"
class="btn btn-neutral flex text-white absolute bottom-0 w-full mb-3 expand-form-button"
class="btn btn-neutral flex text-white absolute bottom-0 w-full mb-3 expand-form-button text-base"
style="width: 96%; margin-left: 2%"
@click.prevent="[isFormVisible = true, scrollIntoField(currentField)]"
>
@@ -14,6 +14,7 @@
<FieldArea
v-if="isMathLoaded"
:model-value="calculateFormula(field)"
:is-inline-size="isInlineSize"
:field="field"
:area="area"
:submittable="false"
@@ -54,6 +55,11 @@ export default {
isMathLoaded: false
}
},
computed: {
isInlineSize () {
return CSS.supports('container-type: size')
}
},
async mounted () {
const {
create,
+14
View File
@@ -1,4 +1,5 @@
const en = {
please_upload_an_image_file: 'Please upload an image file',
must_be_characters_length: 'Must be {number} characters length',
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.',
verify_id: 'Verify ID',
@@ -99,6 +100,7 @@ const en = {
}
const es = {
please_upload_an_image_file: 'Por favor, sube un archivo de imagen',
must_be_characters_length: 'Debe tener {number} caracteres de longitud',
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete todos los campos requeridos para continuar con la verificación de identidad.',
verify_id: 'Verificar ID',
@@ -199,6 +201,7 @@ const es = {
}
const it = {
please_upload_an_image_file: 'Per favore carica un file immagine',
must_be_characters_length: 'Deve essere lungo {number} caratteri',
complete_all_required_fields_to_proceed_with_identity_verification: "Compila tutti i campi obbligatori per procedere con la verifica dell'identità.",
verify_id: 'Verifica ID',
@@ -299,6 +302,7 @@ const it = {
}
const de = {
please_upload_an_image_file: 'Bitte laden Sie eine Bilddatei hoch',
must_be_characters_length: 'Muss {number} Zeichen lang sein',
complete_all_required_fields_to_proceed_with_identity_verification: 'Vervollständigen Sie alle erforderlichen Felder, um mit der Identitätsverifizierung fortzufahren.',
verify_id: 'ID überprüfen',
@@ -399,6 +403,7 @@ const de = {
}
const fr = {
please_upload_an_image_file: 'Veuillez télécharger un fichier image',
must_be_characters_length: 'Doit contenir {number} caractères',
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour continuer la vérification de l'identité.",
verify_id: "Vérification de l'ID",
@@ -499,6 +504,7 @@ const fr = {
}
const pl = {
please_upload_an_image_file: 'Proszę przesłać plik obrazu',
must_be_characters_length: 'Musi mieć długość {number} znaków',
complete_all_required_fields_to_proceed_with_identity_verification: 'Uzupełnij wszystkie wymagane pola, aby kontynuować weryfikację tożsamości.',
verify_id: 'Zweryfikuj ID',
@@ -599,6 +605,7 @@ const pl = {
}
const uk = {
please_upload_an_image_file: 'Будь ласка, завантажте файл зображення',
must_be_characters_length: 'Має містити {number} символів',
complete_all_required_fields_to_proceed_with_identity_verification: "Заповніть всі обов'язкові поля, щоб продовжити перевірку особи.",
verify_id: 'Підтвердження ідентичності',
@@ -699,6 +706,7 @@ const uk = {
}
const cs = {
please_upload_an_image_file: 'Nahrajte prosím obrázkový soubor',
must_be_characters_length: 'Musí mít délku {number} znaků',
complete_all_required_fields_to_proceed_with_identity_verification: 'Vyplňte všechna povinná pole, abyste mohli pokračovat v ověření identity.',
verify_id: 'Ověřit ID',
@@ -799,6 +807,7 @@ const cs = {
}
const pt = {
please_upload_an_image_file: 'Por favor, envie um arquivo de imagem',
must_be_characters_length: 'Deve ter {number} caracteres',
complete_all_required_fields_to_proceed_with_identity_verification: 'Preencha todos os campos obrigatórios para prosseguir com a verificação de identidade.',
verify_id: 'Verificar ID',
@@ -899,6 +908,7 @@ const pt = {
}
const he = {
please_upload_an_image_file: 'אנא העלה קובץ תמונה',
must_be_characters_length: 'חייב להיות באורך של {number} תווים',
complete_all_required_fields_to_proceed_with_identity_verification: 'מלא את כל השדות הנדרשים כדי להמשיך עם אימות זהות.',
verify_id: 'אמת מזהה',
@@ -999,6 +1009,7 @@ const he = {
}
const nl = {
please_upload_an_image_file: 'Upload alstublieft een afbeeldingsbestand',
must_be_characters_length: 'Moet {number} tekens lang zijn',
complete_all_required_fields_to_proceed_with_identity_verification: 'Vul alle verplichte velden in om door te gaan met de identiteitsverificatie.',
verify_id: 'Verifiëren ID',
@@ -1099,6 +1110,7 @@ const nl = {
}
const ar = {
please_upload_an_image_file: 'يرجى تحميل ملف صورة',
must_be_characters_length: 'يجب أن يكون الطول {number} حرفًا',
complete_all_required_fields_to_proceed_with_identity_verification: 'أكمل جميع الحقول المطلوبة للمتابعة في التحقق من الهوية.',
verify_id: 'تحقق من الهوية',
@@ -1199,6 +1211,7 @@ const ar = {
}
const ko = {
please_upload_an_image_file: '이미지 파일을 업로드해 주세요',
must_be_characters_length: '{number}자여야 합니다',
complete_all_required_fields_to_proceed_with_identity_verification: '신원 확인을 진행하려면 모든 필수 필드를 작성하십시오.',
verify_id: '아이디 확인',
@@ -1299,6 +1312,7 @@ const ko = {
}
const ja = {
please_upload_an_image_file: '画像ファイルをアップロードしてください',
must_be_characters_length: '{number}文字でなければなりません',
complete_all_required_fields_to_proceed_with_identity_verification: '本人確認を進めるには、すべての必須項目を入力してください。',
verify_id: '本人確認',
@@ -21,7 +21,7 @@
</label>
<div class="space-x-2 flex flex-none">
<span
v-if="isTextSignature && format !== 'typed' && format !== 'upload'"
v-if="isTextSignature && format !== 'typed_or_upload' && format !== 'typed' && format !== 'upload'"
class="tooltip"
:data-tip="t('draw_signature')"
>
@@ -38,7 +38,7 @@
</a>
</span>
<span
v-else-if="withTypedSignature && format !== 'typed' && format !== 'drawn' && format !== 'upload'"
v-else-if="withTypedSignature && format !== 'drawn_or_upload' && format !== 'typed_or_upload' && format !== 'typed' && format !== 'drawn' && format !== 'upload'"
class="tooltip ml-2"
:class="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
:data-tip="t('type_text')"
@@ -85,8 +85,8 @@
{{ t(format === 'upload' ? 'reupload' : 'redraw') }}
</a>
<span
v-if="withQrButton && !modelValue && !computedPreviousValue && format !== 'typed' && format !== 'upload'"
class=" tooltip"
v-if="withQrButton && !modelValue && !computedPreviousValue && format !== 'typed_or_upload' && format !== 'typed' && format !== 'upload'"
class="tooltip before:translate-x-[-90%]"
:data-tip="t('drawn_signature_on_a_touchscreen_device')"
>
<a
@@ -395,7 +395,7 @@ export default {
isShowQr: false,
isOtherReason: false,
isUsePreviousValue: true,
isTextSignature: this.field.preferences?.format === 'typed',
isTextSignature: this.field.preferences?.format === 'typed' || this.field.preferences?.format === 'typed_or_upload',
uploadImageInputKey: Math.random().toString()
}
},
@@ -149,7 +149,8 @@ export default {
return fetch(this.baseUrl + `/api/identity_verification/${this.field.uuid}`, {
method: 'PUT',
body: JSON.stringify({
submitter_slug: this.submitterSlug
submitter_slug: this.submitterSlug,
redirect_url: document.location.href
}),
headers: { 'Content-Type': 'application/json' }
}).then(async (resp) => {
+47 -6
View File
@@ -140,6 +140,8 @@
:background-color="'white'"
:with-required="false"
:with-areas="false"
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
@click-formula="isShowFormulaModal = true"
@click-font="isShowFontModal = true"
@click-description="isShowDescriptionModal = true"
@@ -172,7 +174,8 @@
>
<div
v-if="isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas && field.type !== 'checkbox')"
:class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type), 'text-[1.6vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars }"
:class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type) }"
:style="fontStyle"
>
<div
ref="textContainer"
@@ -315,7 +318,7 @@ export default {
FieldSubmitter,
IconX
},
inject: ['template', 'selectedAreaRef', 'save', 't'],
inject: ['template', 'selectedAreaRef', 'save', 't', 'isInlineSize'],
props: {
area: {
type: Object,
@@ -346,6 +349,16 @@ export default {
required: false,
default: false
},
withSignatureId: {
type: Boolean,
required: false,
default: null
},
withPrefillable: {
type: Boolean,
required: false,
default: false
},
defaultSubmitters: {
type: Array,
required: false,
@@ -385,6 +398,34 @@ export default {
fieldNames: FieldType.computed.fieldNames,
fieldLabels: FieldType.computed.fieldLabels,
fieldIcons: FieldType.computed.fieldIcons,
fontStyle () {
let fontSize = ''
if (this.isInlineSize) {
if (this.textOverflowChars) {
fontSize = `${this.fontSizePx / 1.5 / 10}cqmin`
} else {
fontSize = `${this.fontSizePx / 10}cqmin`
}
} else {
if (this.textOverflowChars) {
fontSize = `clamp(1pt, ${this.fontSizePx / 1.5 / 10}vw, ${this.fontSizePx / 1.5}px)`
} else {
fontSize = `clamp(1pt, ${this.fontSizePx / 10}vw, ${this.fontSizePx}px)`
}
}
return { fontSize, lineHeight: `calc(${fontSize} * ${this.lineHeight})` }
},
fontSizePx () {
return parseInt(this.field?.preferences?.font_size || 11) * this.fontScale
},
lineHeight () {
return 1.3
},
fontScale () {
return 1040 / 612.0
},
isDefaultValuePresent () {
if (this.field?.type === 'radio' && this.field?.areas?.length > 1) {
return false
@@ -413,8 +454,8 @@ export default {
'justify-center': this.field.preferences.align === 'center',
'justify-start': this.field.preferences.align === 'left',
'justify-end': this.field.preferences.align === 'right',
'font-mono': this.field.preferences.font === 'Courier',
'font-serif': this.field.preferences.font === 'Times',
'font-courier': this.field.preferences.font === 'Courier',
'font-times': this.field.preferences.font === 'Times',
'font-bold': ['bold_italic', 'bold'].includes(this.field.preferences.font_type),
italic: ['bold_italic', 'italic'].includes(this.field.preferences.font_type)
}
@@ -491,7 +532,7 @@ export default {
'field.default_value' () {
this.$nextTick(() => {
if (['date', 'text', 'number'].includes(this.field.type) && this.field.default_value && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.field.default_value}`.length)) {
this.textOverflowChars = this.$el.clientHeight < this.$refs.textContainer.clientHeight ? `${this.field.default_value}`.length : 0
this.textOverflowChars = (this.$el.clientHeight + 1) < this.$refs.textContainer.clientHeight ? `${this.field.default_value}`.length : 0
}
})
}
@@ -499,7 +540,7 @@ export default {
mounted () {
this.$nextTick(() => {
if (['date', 'text', 'number'].includes(this.field.type) && this.field.default_value && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.field.default_value}`.length)) {
this.textOverflowChars = this.$el.clientHeight < this.$refs.textContainer.clientHeight ? `${this.field.default_value}`.length : 0
this.textOverflowChars = (this.$el.clientHeight + 1) < this.$refs.textContainer.clientHeight ? `${this.field.default_value}`.length : 0
}
})
},
+31 -1
View File
@@ -330,6 +330,8 @@
:input-mode="inputMode"
:default-fields="[...defaultRequiredFields, ...defaultFields]"
:allow-draw="!onlyDefinedFields || drawField"
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
:data-document-uuid="document.uuid"
:default-submitters="defaultSubmitters"
:drag-field-placeholder="fieldsDragFieldRef.value || dragField"
@@ -436,6 +438,8 @@
:default-required-fields="defaultRequiredFields"
:field-types="fieldTypes"
:with-sticky-submitters="withStickySubmitters"
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
:only-defined-fields="onlyDefinedFields"
:editable="editable"
:show-tour-start-form="showTourStartForm"
@@ -542,6 +546,7 @@ export default {
isPaymentConnected: this.isPaymentConnected,
withFormula: this.withFormula,
withConditions: this.withConditions,
isInlineSize: this.isInlineSize,
defaultDrawFieldType: this.defaultDrawFieldType,
selectedAreaRef: computed(() => this.selectedAreaRef),
fieldsDragFieldRef: computed(() => this.fieldsDragFieldRef)
@@ -562,6 +567,11 @@ export default {
required: false,
default: false
},
withSignatureId: {
type: Boolean,
required: false,
default: null
},
backgroundColor: {
type: String,
required: false,
@@ -650,7 +660,7 @@ export default {
acceptFileTypes: {
type: String,
required: false,
default: 'image/*, application/pdf'
default: 'image/*, application/pdf, application/zip'
},
baseUrl: {
type: String,
@@ -793,6 +803,16 @@ export default {
language () {
return this.locale.split('-')[0].toLowerCase()
},
withPrefillable () {
if (this.template.fields) {
return this.template.fields.some((f) => f.prefillable)
} else {
return false
}
},
isInlineSize () {
return CSS.supports('container-type: size')
},
isMobile () {
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
@@ -1064,6 +1084,11 @@ export default {
}
}
if (type === 'signature' && [true, false].includes(this.withSignatureId)) {
field.preferences ||= {}
field.preferences.with_signature_id = this.withSignatureId
}
this.template.fields.push(field)
this.save()
@@ -1470,6 +1495,11 @@ export default {
field.preferences ||= {}
field.preferences.format ||= this.defaultDateFormat
}
if (field.type === 'signature' && [true, false].includes(this.withSignatureId)) {
field.preferences ||= {}
field.preferences.with_signature_id = this.withSignatureId
}
}
const fieldArea = {
+1 -1
View File
@@ -66,7 +66,7 @@ export default {
acceptFileTypes: {
type: String,
required: false,
default: 'image/*, application/pdf'
default: 'image/*, application/pdf, application/zip'
},
withReplaceButton: {
type: Boolean,
+17 -4
View File
@@ -10,6 +10,8 @@
:data-page="index"
:areas="areasIndex[index]"
:allow-draw="allowDraw"
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
:is-drag="isDrag"
:with-field-placeholder="withFieldPlaceholder"
:default-fields="defaultFields"
@@ -29,6 +31,7 @@
</template>
<script>
import Page from './page'
import { reactive } from 'vue'
export default {
name: 'TemplateDocument',
@@ -65,6 +68,16 @@ export default {
required: false,
default: false
},
withSignatureId: {
type: Boolean,
required: false,
default: null
},
withPrefillable: {
type: Boolean,
required: false,
default: false
},
drawFieldType: {
type: String,
required: false,
@@ -123,14 +136,14 @@ export default {
return this.document.metadata?.pdf?.number_of_pages || this.document.preview_images.length
},
sortedPreviewImages () {
const lazyloadMetadata = this.document.preview_images[this.document.preview_images.length - 1].metadata
const lazyloadMetadata = this.document.preview_images[this.document.preview_images.length - 1]?.metadata || { width: 1400, height: 1812 }
return [...Array(this.numberOfPages).keys()].map((i) => {
return this.previewImagesIndex[i] || {
metadata: lazyloadMetadata,
return this.previewImagesIndex[i] || reactive({
metadata: { ...lazyloadMetadata },
id: Math.random().toString(),
url: this.basePreviewUrl + `/preview/${this.document.signed_uuid || this.document.uuid}/${i}.jpg`
}
})
})
},
previewImagesIndex () {
+3 -3
View File
@@ -107,7 +107,7 @@ export default {
acceptFileTypes: {
type: String,
required: false,
default: 'image/*, application/pdf'
default: 'image/*, application/pdf, application/zip'
}
},
emits: ['success', 'error', 'loading'],
@@ -131,7 +131,7 @@ export default {
message () {
if (this.isLoading) {
return this.t('uploading')
} else if (this.acceptFileTypes === 'image/*, application/pdf') {
} else if (this.acceptFileTypes === 'image/*, application/pdf, application/zip') {
return this.title || this.t('add_pdf_documents_or_images')
} else {
return this.title || this.t('add_documents_or_images')
@@ -146,7 +146,7 @@ export default {
methods: {
upload: Upload.methods.upload,
onDropFiles (e) {
if (this.acceptFileTypes !== 'image/*, application/pdf' || [...e.dataTransfer.files].every((f) => f.type.match(/(?:image\/)|(?:application\/pdf)/))) {
if (this.acceptFileTypes !== 'image/*, application/pdf, application/zip' || [...e.dataTransfer.files].every((f) => f.type.match(/(?:image\/)|(?:application\/pdf)|(?:application\/zip)/))) {
this.$refs.input.files = e.dataTransfer.files
this.upload()
+12
View File
@@ -125,6 +125,8 @@
:field="field"
:default-field="defaultField"
:editable="editable"
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
:background-color="dropdownBgColor"
@click-formula="isShowFormulaModal = true"
@click-font="isShowFontModal = true"
@@ -302,6 +304,16 @@ export default {
type: Object,
required: true
},
withSignatureId: {
type: Boolean,
required: false,
default: null
},
withPrefillable: {
type: Boolean,
required: false,
default: false
},
withOptions: {
type: Boolean,
required: false,
@@ -304,7 +304,7 @@
{{ t('any') }}
</option>
<option
v-for="type in ['drawn', 'typed', 'drawn_or_typed', 'upload']"
v-for="type in ['drawn', 'typed', 'drawn_or_typed', 'drawn_or_upload', 'upload']"
:key="type"
:value="type"
:selected="field.preferences?.format === type"
@@ -320,6 +320,21 @@
{{ t('format') }}
</label>
</div>
<li
v-if="[true, false].includes(withSignatureId) && field.type === 'signature'"
@click.stop
>
<label class="cursor-pointer py-1.5">
<input
:checked="field.preferences?.with_signature_id"
type="checkbox"
:disabled="!editable || (defaultField && [true, false].includes(defaultField.required))"
class="toggle toggle-xs"
@change="[field.preferences ||= {}, field.preferences.with_signature_id = $event.target.checked, save()]"
>
<span class="label-text">{{ t('signature_id') }}</span>
</label>
</li>
<li
v-if="withRequired && field.type !== 'phone' && field.type !== 'stamp' && field.type !== 'verification'"
@click.stop
@@ -391,6 +406,21 @@
<span class="label-text">{{ t('read_only') }}</span>
</label>
</li>
<li
v-if="withPrefillable && ['text', 'number', 'cells', 'date', 'checkbox', 'select', 'radio', 'phone'].includes(field['type'])"
@click.stop
>
<label class="cursor-pointer py-1.5">
<input
v-model="field.prefillable"
type="checkbox"
:disabled="!editable || (defaultField && [true, false].includes(defaultField.prefillable))"
class="toggle toggle-xs"
@update:model-value="save"
>
<span class="label-text">{{ t('prefillable') }}</span>
</label>
</li>
<hr
v-if="field.type != 'stamp'"
class="pb-0.5 mt-0.5"
@@ -525,6 +555,11 @@ export default {
type: Object,
required: true
},
withSignatureId: {
type: Boolean,
required: false,
default: null
},
backgroundColor: {
type: String,
required: false,
@@ -535,6 +570,11 @@ export default {
required: false,
default: null
},
withPrefillable: {
type: Boolean,
required: false,
default: false
},
withRequired: {
type: Boolean,
required: false,
@@ -26,6 +26,8 @@
:field="field"
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
:editable="editable"
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
:default-field="defaultFieldsIndex[field.name]"
:draggable="editable"
@dragstart="[fieldsDragFieldRef.value = field, removeDragOverlay($event), setDragPlaceholder($event)]"
@@ -253,6 +255,16 @@ export default {
required: false,
default: null
},
withSignatureId: {
type: Boolean,
required: false,
default: null
},
withPrefillable: {
type: Boolean,
required: false,
default: false
},
template: {
type: Object,
required: true
@@ -163,7 +163,7 @@
class="flex border border-base-content/20 rounded-xl bg-white px-4 h-16 modal-field-font-preview"
:style="{
color: preferences.color || 'black',
fontSize: (preferences.font_size || 12) + 'pt',
fontSize: (preferences.font_size || 11) + 'pt',
}"
:class="textClasses"
>
@@ -222,8 +222,8 @@ export default {
fonts () {
return [
{ value: null, label: 'Default' },
{ value: 'Times', label: 'Times', class: 'font-serif' },
{ value: 'Courier', label: 'Courier', class: 'font-mono' }
{ value: 'Times', label: 'Times', class: 'font-times' },
{ value: 'Courier', label: 'Courier', class: 'font-courier' }
]
},
types () {
@@ -258,8 +258,8 @@ export default {
},
textClasses () {
return {
'font-mono': this.preferences.font === 'Courier',
'font-serif': this.preferences.font === 'Times',
'font-courier': this.preferences.font === 'Courier',
'font-times': this.preferences.font === 'Times',
'justify-center': this.preferences.align === 'center',
'justify-start': this.preferences.align === 'left',
'justify-end': this.preferences.align === 'right',
@@ -2,6 +2,8 @@
<div
v-if="isDragging || isLoading"
class="modal modal-open"
@dragover.prevent
@drop.prevent="$emit('error')"
>
<div class="flex flex-col gap-2 p-4 items-center bg-base-100 h-full max-h-[85vh] max-w-6xl rounded-2xl w-full">
<Dropzone
@@ -76,7 +78,7 @@ export default {
acceptFileTypes: {
type: String,
required: false,
default: 'image/*, application/pdf'
default: 'image/*, application/pdf, application/zip'
}
},
emits: ['add', 'replace', 'replace-and-clone', 'error'],
+18
View File
@@ -1,4 +1,6 @@
const en = {
prefillable: 'Prefillable',
signature_id: 'Signature ID',
error_message: 'Error message',
length: 'Length',
min: 'Min',
@@ -58,6 +60,7 @@ const en = {
any: 'Any',
drawn: 'Drawn',
drawn_or_typed: 'Drawn or Typed',
drawn_or_upload: 'Drawn or Upload',
upload: 'Upload',
formula: 'Formula',
typed: 'Typed',
@@ -172,6 +175,8 @@ const en = {
}
const es = {
prefillable: 'Rellenable',
signature_id: 'ID de Firma',
error_message: 'Mensaje de error',
length: 'Longitud',
min: 'Mín',
@@ -318,6 +323,7 @@ const es = {
any: 'Cualquier',
drawn: 'Dibujado',
drawn_or_typed: 'Dibujado o Escrito',
drawn_or_upload: 'Dibujado o Subido',
upload: 'Subir',
typed: 'Escrito',
none: 'Ninguno',
@@ -345,6 +351,8 @@ const es = {
}
const it = {
prefillable: 'Precompilabile',
signature_id: 'ID firma',
error_message: 'Messaggio di errore',
length: 'Lunghezza',
min: 'Min',
@@ -404,6 +412,7 @@ const it = {
any: 'Qualsiasi',
drawn: 'Disegnato',
drawn_or_typed: 'Disegnato o Digitato',
drawn_or_upload: 'Disegnato o Caricato',
upload: 'Caricare',
formula: 'Formula',
typed: 'Digitato',
@@ -518,6 +527,8 @@ const it = {
}
const pt = {
prefillable: 'Pré-preenchível',
signature_id: 'ID da Assinatura',
error_message: 'Mensagem de erro',
length: 'Comprimento',
min: 'Mín',
@@ -664,6 +675,7 @@ const pt = {
any: 'Qualquer',
drawn: 'Desenhado',
drawn_or_typed: 'Desenhado ou Digitado',
drawn_or_upload: 'Desenhado ou Enviado',
upload: 'Carregar',
typed: 'Digitado',
none: 'Nenhum',
@@ -691,6 +703,8 @@ const pt = {
}
const fr = {
prefillable: 'Pré-remplissable',
signature_id: 'ID de signature',
error_message: 'Message d\'erreur',
length: 'Longueur',
min: 'Min',
@@ -837,6 +851,7 @@ const fr = {
any: 'Tout',
drawn: 'Dessiné',
drawn_or_typed: 'Dessiné ou Tapé',
drawn_or_upload: 'Dessiné ou Téléchargé',
upload: 'Téléverser',
typed: 'Tapé',
none: 'Aucun',
@@ -864,6 +879,8 @@ const fr = {
}
const de = {
prefillable: 'Vorausfüllbar',
signature_id: 'Signatur-ID',
error_message: 'Fehlermeldung',
length: 'Länge',
min: 'Min',
@@ -1010,6 +1027,7 @@ const de = {
any: 'Jede',
drawn: 'Gezeichnet',
drawn_or_typed: 'Gezeichnet oder getippt',
drawn_or_upload: 'Gezeichnet oder hochgeladen',
upload: 'Upload',
typed: 'Getippt',
none: 'Keine',
+19 -6
View File
@@ -1,8 +1,9 @@
<template>
<div
class="relative select-none"
:class="{ 'cursor-crosshair': allowDraw }"
:style="drawField ? 'touch-action: none' : ''"
class="relative select-none mb-4 before:border before:rounded before:top-0 before:bottom-0 before:left-0 before:right-0 before:absolute"
:class="{ 'cursor-crosshair': allowDraw, 'touch-none': !!drawField }"
style="container-type: size"
:style="{ aspectRatio: `${width} / ${height}`}"
>
<img
ref="image"
@@ -10,7 +11,7 @@
:src="image.url"
:width="width"
:height="height"
class="border rounded mb-4"
class="rounded"
@load="onImageLoad"
>
<div
@@ -26,6 +27,8 @@
:field="item.field"
:editable="editable"
:with-field-placeholder="withFieldPlaceholder"
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
:default-field="defaultFieldsIndex[item.field.name]"
:default-submitters="defaultSubmitters"
:max-page="totalPages - 1"
@@ -77,6 +80,16 @@ export default {
required: false,
default: null
},
withSignatureId: {
type: Boolean,
required: false,
default: null
},
withPrefillable: {
type: Boolean,
required: false,
default: false
},
areas: {
type: Array,
required: false,
@@ -191,8 +204,8 @@ export default {
},
methods: {
onImageLoad (e) {
e.target.setAttribute('width', e.target.naturalWidth)
e.target.setAttribute('height', e.target.naturalHeight)
this.image.metadata.width = e.target.naturalWidth
this.image.metadata.height = e.target.naturalHeight
},
setAreaRefs (el) {
if (el) {
+1 -1
View File
@@ -157,7 +157,7 @@ export default {
acceptFileTypes: {
type: String,
required: false,
default: 'image/*, application/pdf'
default: 'image/*, application/pdf, application/zip'
},
withReplaceButton: {
type: Boolean,
+1 -1
View File
@@ -35,7 +35,7 @@ export default {
acceptFileTypes: {
type: String,
required: false,
default: 'image/*, application/pdf'
default: 'image/*, application/pdf, application/zip'
}
},
emits: ['success'],
+1 -1
View File
@@ -57,7 +57,7 @@ export default {
acceptFileTypes: {
type: String,
required: false,
default: 'image/*, application/pdf'
default: 'image/*, application/pdf, application/zip'
}
},
emits: ['success', 'error'],
+23 -9
View File
@@ -135,19 +135,33 @@ class ProcessSubmitterCompletionJob
end
def enqueue_next_submitter_request_notification(submitter)
next_submitter_item =
submitter.submission.template_submitters.find do |e|
sub = submitter.submission.submitters.find { |s| s.uuid == e['uuid'] }
submission = submitter.submission
submitters_index = submission.submitters.index_by(&:uuid)
next unless sub
next_submitter_items =
if submission.template_submitters.any? { |s| s['order'] }
submitter_groups =
submission.template_submitters.group_by.with_index { |s, index| s['order'] || index }
sub.completed_at.blank? && sub.sent_at.blank?
current_group_index = submitter_groups.find { |_, group| group.any? { |s| s['uuid'] == submitter.uuid } }&.first
if submitter_groups[current_group_index + 1] &&
submitters_index.values_at(*submitter_groups[current_group_index].pluck('uuid'))
.compact.all?(&:completed_at?)
submitter_groups[current_group_index + 1]
end
else
submission.template_submitters.find do |e|
sub = submitters_index[e['uuid']]
next unless sub
sub.completed_at.blank? && sub.sent_at.blank?
end
end
return unless next_submitter_item
next_submitters = submitters_index.values_at(*Array.wrap(next_submitter_items).pluck('uuid')).compact
next_submitter = submitter.submission.submitters.find { |s| s.uuid == next_submitter_item['uuid'] }
Submitters.send_signature_requests([next_submitter])
Submitters.send_signature_requests(next_submitters)
end
end
@@ -6,6 +6,7 @@ class SendSubmitterInvitationEmailJob
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
return if submitter.completed_at?
return if submitter.submission.source == 'invite' && !Accounts.can_send_emails?(submitter.account, on_events: true)
unless Accounts.can_send_invitation_emails?(submitter.account)
+7 -2
View File
@@ -30,6 +30,7 @@ class SubmitterMailer < ApplicationMailer
@submitter.template&.preferences&.dig('request_email_subject').presence
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY)
@body ||= fetch_config_email_body(@email_config, @submitter)
assign_message_metadata('submitter_invitation', @submitter)
@@ -71,7 +72,7 @@ class SubmitterMailer < ApplicationMailer
@subject ||= @email_config.value['subject'] if @email_config
@body = template_preferences['completed_notification_email_body'].presence
@body ||= @email_config.value['body'] if @email_config
@body ||= fetch_config_email_body(@email_config, @submitter)
assign_message_metadata('submitter_completed', @submitter)
@@ -127,7 +128,7 @@ class SubmitterMailer < ApplicationMailer
@subject ||= @email_config.value['subject'] if @email_config
@body = template_preferences['documents_copy_email_body'].presence
@body ||= @email_config.value['body'] if @email_config
@body ||= fetch_config_email_body(@email_config, @submitter)
assign_message_metadata('submitter_documents_copy', @submitter)
reply_to = build_submitter_reply_to(submitter, email_config: @email_config, documents_copy_email: true)
@@ -246,4 +247,8 @@ class SubmitterMailer < ApplicationMailer
user.friendly_name
end
end
def fetch_config_email_body(email_config, _submitter = nil)
email_config ? email_config.value['body'].presence : nil
end
end
+14
View File
@@ -0,0 +1,14 @@
# frozen_string_literal: true
class TemplateMailer < ApplicationMailer
def otp_verification_email(template, email:)
@current_account = template.account
@template = template
@otp_code = EmailVerificationCodes.generate([email.downcase.strip, template.slug].join(':'))
assign_message_metadata('otp_verification_email', template)
mail(to: email, subject: I18n.t('email_verification'))
end
end
+2 -1
View File
@@ -41,8 +41,9 @@ class AccountConfig < ApplicationRecord
FORCE_SSO_AUTH_KEY = 'force_sso_auth'
FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf'
WITH_SIGNATURE_ID = 'with_signature_id'
WITH_SIGNATURE_ID_REASON_KEY = 'with_signature_id_reason'
WITH_AUDIT_VALUES_KEY = 'with_audit_values'
WITH_AUDIT_SUBMITTER_TIMEZONE_KEY = 'with_audit_submitter_timezone'
WITH_SUBMITTER_TIMEZONE_KEY = 'with_submitter_timezone'
REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason'
REUSE_SIGNATURE_KEY = 'reuse_signature'
COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key'
+1
View File
@@ -47,6 +47,7 @@ class SubmissionEvent < ApplicationRecord
click_email: 'click_email',
click_sms: 'click_sms',
phone_verified: 'phone_verified',
email_verified: 'email_verified',
start_form: 'start_form',
start_verification: 'start_verification',
complete_verification: 'complete_verification',
+4 -2
View File
@@ -72,12 +72,14 @@ class Template < ApplicationRecord
scope :active, -> { where(archived_at: nil) }
scope :archived, -> { where.not(archived_at: nil) }
delegate :name, to: :folder, prefix: true
def application_key
external_id
end
def folder_name
folder.full_name
end
private
def maybe_set_default_folder
+23 -9
View File
@@ -4,36 +4,50 @@
#
# Table name: template_folders
#
# id :bigint not null, primary key
# archived_at :datetime
# name :string not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# author_id :bigint not null
# id :bigint not null, primary key
# archived_at :datetime
# name :string not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# author_id :bigint not null
# parent_folder_id :bigint
#
# Indexes
#
# index_template_folders_on_account_id (account_id)
# index_template_folders_on_author_id (author_id)
# index_template_folders_on_account_id (account_id)
# index_template_folders_on_author_id (author_id)
# index_template_folders_on_parent_folder_id (parent_folder_id)
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id)
# fk_rails_... (author_id => users.id)
# fk_rails_... (parent_folder_id => template_folders.id)
#
class TemplateFolder < ApplicationRecord
DEFAULT_NAME = 'Default'
belongs_to :author, class_name: 'User'
belongs_to :account
belongs_to :parent_folder, class_name: 'TemplateFolder', optional: true
has_many :templates, dependent: :destroy, foreign_key: :folder_id, inverse_of: :folder
has_many :subfolders, class_name: 'TemplateFolder', foreign_key: :parent_folder_id, inverse_of: :parent_folder,
dependent: :destroy
has_many :active_templates, -> { where(archived_at: nil) },
class_name: 'Template', dependent: :destroy, foreign_key: :folder_id, inverse_of: :folder
scope :active, -> { where(archived_at: nil) }
def full_name
if parent_folder_id?
[parent_folder.name, name].join(' / ')
else
name
end
end
def default?
name == DEFAULT_NAME
end
+3 -3
View File
@@ -53,7 +53,7 @@
<span class="top-0 right-0 absolute">
<%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
</span>
<pre data-prefix="$"><code class="overflow-hidden w-full"><%= text %></code></pre>
<pre data-prefix="$"><code class="overflow-hidden w-full"><%== HighlightCode.call(text, 'Shell', theme: 'base16.dark') %></code></pre>
</div>
</div>
</div>
@@ -79,7 +79,7 @@
<span class="top-0 right-0 absolute">
<%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
</span>
<pre data-prefix="$"><code class="overflow-hidden w-full"><%= text %></code></pre>
<pre data-prefix="$"><code class="overflow-hidden w-full"><%== HighlightCode.call(text, 'Shell', theme: 'base16.dark') %></code></pre>
</div>
</div>
</div>
@@ -101,7 +101,7 @@
<span class="top-0 right-0 absolute">
<%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
</span>
<pre data-prefix="$"><code class="overflow-hidden w-full"><%= text %></code></pre>
<pre data-prefix="$"><code class="overflow-hidden w-full"><%== HighlightCode.call(text, 'Shell', theme: 'base16.dark') %></code></pre>
</div>
</div>
</div>
+11 -9
View File
@@ -35,17 +35,19 @@
<%= ff.select :authentication, options_for_select([%w[Plain plain], %w[Login login], %w[CRAM-MD5 cram_md5]], value.fetch('authentication', 'plain')), { prompt: true }, required: true, class: 'base-select' %>
</div>
</div>
<div class="form-control">
<%= ff.label :security_label, 'SMTP Security', class: 'label' %>
<div class="flex items-center space-x-6">
<% [%w[Auto none], %w[SSL ssl], %w[TLS tls], %w[Noverify noverify]].each do |(label, val)| %>
<%= ff.label :security, value: val, for: "#{val}_radio", class: 'label' do %>
<%= ff.radio_button :security, val, checked: (value['security'].blank? && val == 'none') || value['security'] == val, id: "#{val}_radio", class: 'base-radio mr-2' %>
<%= label %>
<% if !Docuseal.multitenant? || can?(:manage, :personalization_advanced) %>
<div class="form-control">
<%= ff.label :security_label, 'SMTP Security', class: 'label' %>
<div class="flex items-center space-x-6">
<% [%w[Auto none], %w[SSL ssl], %w[TLS tls], %w[Noverify noverify]].each do |(label, val)| %>
<%= ff.label :security, value: val, for: "#{val}_radio", class: 'label' do %>
<%= ff.radio_button :security, val, checked: (value['security'].blank? && val == 'none') || value['security'] == val, id: "#{val}_radio", class: 'base-radio mr-2' %>
<%= label %>
<% end %>
<% end %>
<% end %>
</div>
</div>
</div>
<% end %>
<div class="form-control">
<%= ff.label :from_email, t('send_from_email'), class: 'label' %>
<%= ff.email_field :from_email, value: value['from_email'], required: !Docuseal.multitenant?, class: 'base-input' %>
+1 -1
View File
@@ -141,7 +141,7 @@
<%= t('preferences') %>
</h2>
</div>
<% if can?(:manage, account_config) && (can?(:manage, :personalization_advanced) || !Docuseal.multitenant?) %>
<% if can?(:manage, account_config) && (can?(:manage, :personalization_advanced) || account_config.persisted?) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %>
<div class="flex items-center justify-between py-2.5">
+3
View File
@@ -0,0 +1,3 @@
<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="M5 12l14 0" /><path d="M5 12l6 6" /><path d="M5 12l6 -6" />
</svg>

After

Width:  |  Height:  |  Size: 351 B

@@ -0,0 +1,3 @@
<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 21a9 9 0 1 0 0 -18a9 9 0 0 0 0 18" /><path d="M8 12l4 4" /><path d="M8 12h8" /><path d="M12 8l-4 4" />
</svg>

After

Width:  |  Height:  |  Size: 398 B

@@ -0,0 +1,3 @@
<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 21a9 9 0 1 0 0 -18a9 9 0 0 0 0 18" /><path d="M9 12l4 4" /><path d="M13 8l-4 4" />
</svg>

After

Width:  |  Height:  |  Size: 378 B

+6
View File
@@ -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 stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M11 19h-6a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v6" />
<path d="M3 7l9 6l9 -6" />
<path d="M15 19l2 2l4 -4" />
</svg>

After

Width:  |  Height:  |  Size: 420 B

+3
View File
@@ -0,0 +1,3 @@
<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 19h-7a2 2 0 0 1 -2 -2v-11a2 2 0 0 1 2 -2h4l3 3h7a2 2 0 0 1 2 2v3.5" /><path d="M16 19h6" /><path d="M19 16v6" />
</svg>

After

Width:  |  Height:  |  Size: 408 B

+3
View File
@@ -0,0 +1,3 @@
<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="M17 5l-10 14" />
</svg>

After

Width:  |  Height:  |  Size: 308 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" wi width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#000000" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M8.243 7.34l-6.38 .925l-.113 .023a1 1 0 0 0 -.44 1.684l4.622 4.499l-1.09 6.355l-.013 .11a1 1 0 0 0 1.464 .944l5.706 -3l5.693 3l.1 .046a1 1 0 0 0 1.352 -1.1l-1.091 -6.355l4.624 -4.5l.078 -.085a1 1 0 0 0 -.633 -1.62l-6.38 -.926l-2.852 -5.78a1 1 0 0 0 -1.794 0l-2.853 5.78z" stroke-width="0" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 604 B

-1
View File
@@ -18,7 +18,6 @@
<link href="<%= canonical_url %>" rel="canonical">
<% end %>
<%= stylesheet_pack_tag 'application', media: 'all' %>
<%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %>
<%= render 'shared/plausible' if !signed_in? && ENV['PLAUSIBLE_DOMAIN'] %>
</head>
<body>
-1
View File
@@ -12,7 +12,6 @@
<%= javascript_pack_tag 'form', defer: true %>
<% end %>
<%= stylesheet_pack_tag 'form', media: 'all' %>
<%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %>
</head>
<body>
<%= yield %>
-1
View File
@@ -12,7 +12,6 @@
<%= javascript_pack_tag 'application', defer: true %>
<% end %>
<%= stylesheet_pack_tag 'application', media: 'all' %>
<%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %>
</head>
<body>
<% if flash.present? %><%= render 'shared/flash' %><% end %>
@@ -13,17 +13,7 @@
<%= ff.label :subject, t('subject'), class: 'label' %>
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :body, t('body'), class: 'label' %>
<span class="tooltip" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<%= render 'personalization_settings/email_body_field', ff:, config: f.object %>
<% if can?(:manage, :reply_to) || can?(:manage, :personalization_advanced) %>
<div class="form-control">
<%= ff.label :reply_to, t('reply_to'), class: 'label' %>
@@ -0,0 +1,11 @@
<div class="form-control">
<div class="flex items-center">
<%= ff.label :body, t('body'), class: 'label' %>
<span class="tooltip" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[local_assigns[:config].key].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :body, required: true, class: 'base-input w-full !rounded-2xl py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
@@ -13,17 +13,7 @@
<%= ff.label :subject, t('subject'), class: 'label' %>
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :body, t('body'), class: 'label' %>
<span class="tooltip" 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, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<%= render 'personalization_settings/email_body_field', ff:, config: f.object %>
<% end %>
<div class="form-control pt-2">
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
@@ -18,17 +18,7 @@
</div>
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :body, t('body'), class: 'label' %>
<span class="tooltip" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<%= render 'personalization_settings/email_body_field', ff:, config: f.object %>
<div class="flex items-center justify-between pt-2.5 mx-1">
<span>
<%= t('attach_documents') %>
+6 -7
View File
@@ -2,16 +2,15 @@
if (!window.customElements.get('autosize-field')) {
window.customElements.define('autosize-field', class extends HTMLElement {
connectedCallback() {
const originalFontValue = this.field.style.fontSize
if (this.field.scrollHeight > this.field.clientHeight) {
this.field.classList.remove('text-[1.6vw]', 'lg:text-base');
this.field.classList.add('text-[1.0vw]', 'lg:text-[0.70rem]');
this.field.style.fontSize = `calc(${originalFontValue} / 1.5)`;
this.field.style.lineHeight = `calc(${this.field.style.fontSize} * 1.3)`;
if (this.field.scrollHeight > this.field.clientHeight) {
this.field.classList.add('text-[0.8vw]', 'lg:text-[0.50rem]');
}
if (this.field.scrollHeight > this.field.clientHeight) {
this.field.classList.add('text-[0.7vw]', 'lg:text-[0.45rem]');
this.field.style.fontSize = `calc(${originalFontValue} / 2.0)`;
this.field.style.lineHeight = `calc(${this.field.style.fontSize} * 1.3)`;
}
}
}
+5 -2
View File
@@ -1,3 +1,6 @@
<a href="<%= Docuseal::GITHUB_URL %>" target="_blank" class="inline">
<img alt="GitHub Repo stars" src="https://www.docuseal.com/github-badge.svg" style="height: 22px">
<a target="_blank" href="<%= Docuseal::GITHUB_URL %>" rel="noopener noreferrer nofollow" class="relative flex items-center rounded-full px-2 py-0.5 text-xs leading-4 mt-1 text-base-content border border-base-300 tooltip tooltip-bottom" data-tip="Give a star on GitHub">
<span class="flex items-center justify-between space-x-0.5 font-medium">
<%= svg_icon('start', class: 'h-3 w-3') %>
<span>9k</span>
</span>
</a>
Loaded 100 of 173 files, more files were not shown because too many files have changed in this diff. Show more