Compare commits

..

59 Commits

Author SHA1 Message Date
Alex Turchyn 21299dc65f Merge pull from docusealco/wip 2025-08-25 12:37:42 +03:00
Pete Matsyburka 0df0946c2a fix markdown 2025-08-25 10:11:36 +03:00
Pete Matsyburka 1bb38d50a1 adjust formula font 2025-08-24 18:16:43 +03:00
Pete Matsyburka 242987cb41 icon size 2025-08-24 14:25:36 +03:00
Pete Matsyburka 5456abde54 adjust date input 2025-08-24 10:08:47 +03:00
Pete Matsyburka 4a07427440 adjust paste radio 2025-08-24 09:27:51 +03:00
Pete Matsyburka cf60dae2ff fix modal title 2025-08-23 21:46:51 +03:00
Pete Matsyburka 659997c6c1 add 2 options by default 2025-08-23 21:33:46 +03:00
Pete Matsyburka 389a47a152 expand options 2025-08-23 21:16:07 +03:00
Pete Matsyburka b6b81ca487 editable readonly custom field 2025-08-23 20:52:57 +03:00
Pete Matsyburka 719c1786f1 set default value 2025-08-23 20:16:14 +03:00
Pete Matsyburka 4685bac4b3 fix serializers 2025-08-23 11:28:26 +03:00
Pete Matsyburka a05275cc08 update json 2025-08-23 10:41:49 +03:00
Pete Matsyburka 1abf1b1658 fix formula 2025-08-22 18:11:09 +03:00
Pete Matsyburka 8893dc5698 pwa orientation any 2025-08-22 18:11:05 +03:00
Pete Matsyburka f8467b2da1 nested formula 2025-08-21 14:36:49 +03:00
Pete Matsyburka f39b8b000e use csv safe 2025-08-21 13:58:53 +03:00
Pete Matsyburka 961036fa31 update widget 2025-08-21 10:22:06 +03:00
Pete Matsyburka 8b9056894e sanitize url 2025-08-20 21:31:15 +03:00
Pete Matsyburka 7f979e9396 add number validation 2025-08-20 18:32:20 +03:00
Pete Matsyburka 8db1192d0e update hexapdf 2025-08-19 16:34:59 +03:00
Pete Matsyburka 3bd515951a stamp without completed 2025-08-19 12:40:45 +03:00
Pete Matsyburka 2bd5b9676b add with verification 2025-08-19 12:25:23 +03:00
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
120 changed files with 1761 additions and 643 deletions
+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
@@ -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\
+1
View File
@@ -11,6 +11,7 @@ gem 'azure-storage-blob', require: false
gem 'bootsnap', require: false
gem 'cancancan'
gem 'csv'
gem 'csv-safe'
gem 'devise'
gem 'devise-two-factor'
gem 'dotenv', require: false
+103 -90
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
@@ -150,6 +150,8 @@ GEM
css_parser (1.21.0)
addressable
csv (3.3.2)
csv-safe (3.3.1)
csv (~> 3.0)
cuprite (0.15.1)
capybara (~> 3.0)
ferrum (~> 0.15.0)
@@ -174,8 +176,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)
@@ -270,23 +273,25 @@ GEM
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
hashdiff (1.1.2)
hexapdf (1.0.3)
hexapdf (1.4.0)
cmdparse (~> 3.0, >= 3.0.3)
geom2d (~> 0.4, >= 0.4.1)
openssl (>= 2.2.1)
strscan (>= 3.1.2)
htmlentities (4.3.4)
httpclient (2.8.3)
i18n (1.14.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)
json (2.9.1)
json (2.13.2)
jwt (2.9.3)
base64
language_server-protocol (3.17.0.3)
@@ -300,13 +305,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 +324,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,36 +333,36 @@ 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)
oj (3.16.11)
bigdecimal (>= 3.0)
ostruct (>= 0.2)
openssl (3.3.0)
orm_adapter (0.5.0)
os (1.1.4)
ostruct (0.6.1)
ostruct (0.6.3)
package_json (0.1.0)
pagy (9.3.3)
parallel (1.26.3)
@@ -365,6 +370,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 +382,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 +433,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 +548,11 @@ 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)
strscan (3.1.5)
thor (1.4.0)
timeout (0.4.3)
trailblazer-option (0.1.2)
turbo-rails (2.0.11)
@@ -574,12 +585,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
@@ -602,6 +614,7 @@ DEPENDENCIES
cancancan
capybara
csv
csv-safe
cuprite
debug
devise
+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)
@@ -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
@@ -185,11 +185,11 @@ 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,
{ default_value: [], value: [], preferences: {} }] }]]
{ default_value: [], value: [], preferences: {}, validation: {} }] }]]
}
]
+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
+4 -3
View File
@@ -106,14 +106,15 @@ 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: {},
default_value: [],
conditions: [%i[field_uuid value action operation]],
options: [%i[value uuid]],
validation: %i[message pattern],
validation: %i[message pattern min max step],
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
}
]
+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
+4 -4
View File
@@ -42,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?)
@@ -60,7 +60,7 @@ class StartFormController < ApplicationController
redirect_to submit_form_path(@submitter.slug)
else
render :show, status: :unprocessable_entity
render :show, status: :unprocessable_content
end
end
end
@@ -206,7 +206,7 @@ class StartFormController < ApplicationController
end
def handle_require_2fa(submitter, is_new_record:)
return render :show, status: :unprocessable_entity if submitter.errors.present?
return render :show, status: :unprocessable_content if submitter.errors.present?
is_otp_verified = Submitters.verify_link_otp!(params[:one_time_code], submitter)
@@ -223,7 +223,7 @@ class StartFormController < ApplicationController
redirect_to submit_form_path(submitter.slug)
else
render :show, status: :unprocessable_entity
render :show, status: :unprocessable_content
end
else
Submitters.send_shared_link_email_verification_code(submitter, request:)
@@ -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
+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
+6 -6
View File
@@ -48,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.submission.template&.archived_at? || @submitter.submission.archived_at?
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_entity
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)
@@ -70,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
@@ -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
@@ -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
+5 -4
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,14 +117,15 @@ 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: {},
default_value: [],
conditions: [%i[field_uuid value action operation]],
options: [%i[value uuid]],
validation: %i[message pattern],
validation: %i[message pattern min max step],
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
)
end
@@ -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
@@ -27,7 +27,7 @@ class TemplatesPreferencesController < ApplicationController
completed_redirect_url validate_unique_submitters
require_all_submitters submitters_order require_phone_2fa
default_expire_at_duration shared_link_2fa
default_expire_at
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
+4
View File
@@ -37,6 +37,8 @@ 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'
@@ -109,6 +111,8 @@ 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 () {
+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
})
}
}
})
+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)
}
}
@@ -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()
}
}
})
+19 -6
View File
@@ -29,8 +29,15 @@
{{ optionValue(option) }}
</template>
<template v-else>
{{ field.title || field.name || fieldNames[field.type] }}
<template v-if="field.type === 'checkbox' && !field.name">
<MarkdownContent
v-if="field.title"
:text-only="true"
:string="field.title"
/>
<template v-else>
{{ field.name || fieldNames[field.type] }}
</template>
<template v-if="field.type === 'checkbox' && !field.name && !field.title">
{{ fieldIndex + 1 }}
</template>
<template v-else-if="!field.required && field.type !== 'checkbox'">
@@ -222,12 +229,14 @@
</template>
<script>
import MarkdownContent from './markdown_content'
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconId } from '@tabler/icons-vue'
export default {
name: 'FieldArea',
components: {
IconPaperclip,
MarkdownContent,
IconCheck
},
inject: ['t'],
@@ -422,10 +431,14 @@ export default {
},
formattedDate () {
if (this.field.type === 'date' && this.modelValue) {
return this.formatDate(
this.modelValue === '{{date}}' ? new Date() : new Date(this.modelValue),
this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
)
try {
return this.formatDate(
this.modelValue === '{{date}}' ? new Date() : new Date(this.modelValue),
this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
)
} catch {
return this.modelValue
}
} else {
return ''
}
+3 -6
View File
@@ -25,7 +25,7 @@
<div class="space-y-3 mt-5">
<a
v-if="completedButton.url"
:href="sanitizeHref(completedButton.url)"
:href="sanitizeUrl(completedButton.url)"
rel="noopener noreferrer nofollow"
class="white-button flex items-center w-full completed-form-completed-button"
>
@@ -102,6 +102,7 @@
<script>
import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop, IconLogin } from '@tabler/icons-vue'
import MarkdownContent from './markdown_content'
import { sanitizeUrl } from '@braintree/sanitize-url'
export default {
name: 'FormCompleted',
@@ -198,6 +199,7 @@ export default {
})
},
methods: {
sanitizeUrl,
sendCopyToEmail () {
this.isSendingCopy = true
@@ -252,11 +254,6 @@ export default {
this.isDownloading = false
})
},
sanitizeHref (href) {
if (href && href.trim().match(/^((?:https?:\/\/)|\/)/)) {
return href.replace(/javascript:/g, '')
}
},
downloadSafariIos (urls) {
const fileRequests = urls.map((url) => {
return fetch(url).then(async (resp) => {
@@ -26,6 +26,7 @@
</template>
</label>
<button
v-if="withToday"
class="btn btn-outline btn-sm !normal-case font-normal set-current-date-button"
@click.prevent="[setCurrentDate(), $emit('focus')]"
>
@@ -46,6 +47,8 @@
:id="field.uuid"
ref="input"
v-model="value"
:min="validationMin"
:max="validationMax"
class="base-input !text-2xl text-center w-full"
:required="field.required"
type="date"
@@ -89,6 +92,44 @@ export default {
},
emits: ['update:model-value', 'focus', 'submit'],
computed: {
dateNowString () {
const today = new Date()
const yyyy = today.getFullYear()
const mm = String(today.getMonth() + 1).padStart(2, '0')
const dd = String(today.getDate()).padStart(2, '0')
return `${yyyy}-${mm}-${dd}`
},
validationMin () {
if (this.field.validation?.min) {
return ['{{date}}', '{date}'].includes(this.field.validation.min) ? this.dateNowString : this.field.validation.min
} else {
return ''
}
},
validationMax () {
if (this.field.validation?.max) {
return ['{{date}}', '{date}'].includes(this.field.validation.max) ? this.dateNowString : this.field.validation.max
} else {
return ''
}
},
withToday () {
const todayDate = new Date().setHours(0, 0, 0, 0)
if (this.validationMin) {
if (new Date(this.validationMin).setHours(0, 0, 0, 0) <= todayDate) {
return this.validationMax ? (new Date(this.validationMax).setHours(0, 0, 0, 0) >= todayDate) : true
} else {
return false
}
} else if (this.validationMax) {
return new Date(this.validationMax).setHours(0, 0, 0, 0) >= todayDate
} else {
return true
}
},
value: {
set (value) {
this.$emit('update:model-value', value)
+15 -3
View File
@@ -112,11 +112,23 @@ export default {
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
+2 -1
View File
@@ -572,6 +572,7 @@ import FormCompleted from './completed'
import { IconInnerShadowTop, IconArrowsDiagonal, IconWritingSign, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
import AppearsOn from './appears_on'
import i18n from './i18n'
import { sanitizeUrl } from '@braintree/sanitize-url'
const isEmpty = (obj) => {
if (obj == null) return true
@@ -1476,7 +1477,7 @@ export default {
}
if (this.completedRedirectUrl) {
window.location.href = this.completedRedirectUrl
window.location.href = sanitizeUrl(this.completedRedirectUrl)
}
}
}
@@ -58,6 +58,13 @@ export default {
computed: {
isInlineSize () {
return CSS.supports('container-type: size')
},
fieldsUuidIndex () {
return this.fields.reduce((acc, field) => {
acc[field.uuid] = field
return acc
}, {})
}
},
async mounted () {
@@ -96,8 +103,19 @@ export default {
findPageElementForArea (area) {
return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${area.attachment_uuid}-${area.page}`)
},
normalizeFormula (formula, depth = 0) {
if (depth > 10) return formula
return formula.replace(/{{(.*?)}}/g, (match, uuid) => {
if (this.fieldsUuidIndex[uuid]) {
return `(${this.normalizeFormula(this.fieldsUuidIndex[uuid].preferences.formula, depth + 1)})`
} else {
return match
}
})
},
calculateFormula (field) {
const transformedFormula = field.preferences.formula.replace(/{{(.*?)}}/g, (match, uuid) => {
const transformedFormula = this.normalizeFormula(field.preferences.formula).replace(/{{(.*?)}}/g, (match, uuid) => {
return this.readonlyValues[uuid] || this.values[uuid] || 0.0
})
@@ -1,35 +1,32 @@
<template>
<span>
<template
v-for="(item, index) in items"
:key="index"
>
<a
v-if="item.startsWith('<a') && item.endsWith('</a>')"
:href="sanitizeHref(extractAttr(item, 'href'))"
rel="noopener noreferrer nofollow"
:class="extractAttr(item, 'class') || 'link'"
target="_blank"
>
{{ extractText(item) }}
</a>
<b
v-else-if="item.startsWith('<b>') || item.startsWith('<strong>')"
>
{{ extractText(item) }}
</b>
<i
v-else-if="item.startsWith('<i>') || item.startsWith('<em>')"
>
{{ extractText(item) }}
</i>
<br
v-else-if="item === '<br>' || item === '\n'"
>
<span v-if="textOnly">
{{ dom.body.textContent }}
</span>
<template v-else>
<template
v-else
v-for="(item, index) in nodes || dom.body.childNodes"
:key="index"
>
{{ item }}
<a
v-if="item.tagName === 'A' && item.getAttribute('href') !== 'undefined'"
:href="sanitizeUrl(item.getAttribute('href'))"
rel="noopener noreferrer nofollow"
:class="item.getAttribute('class') || 'link'"
target="_blank"
>
<MarkdownContent :nodes="item.childNodes" />
</a>
<component
:is="item.tagName"
v-else-if="safeTags.includes(item.tagName)"
>
<MarkdownContent :nodes="item.childNodes" />
</component>
<br v-else-if="item.tagName === 'BR' || item.nodeValue === '\n'">
<template v-else>
{{ item.textContent }}
</template>
</template>
</template>
</span>
@@ -37,8 +34,7 @@
<script>
import snarkdown from 'snarkdown'
const htmlSplitRegexp = /(<a.+?<\/a>|<i>.+?<\/i>|<b>.+?<\/b>|<em>.+?<\/em>|<strong>.+?<\/strong>|<br>)/
import { sanitizeUrl } from '@braintree/sanitize-url'
export default {
name: 'MarkdownContent',
@@ -47,39 +43,30 @@ export default {
type: String,
required: false,
default: ''
},
nodes: {
type: [Array, Object],
require: false,
default: null
},
textOnly: {
type: Boolean,
required: false,
default: false
}
},
computed: {
items () {
const linkParts = this.string.split(/(https?:\/\/[^\s)]+)/g)
safeTags () {
return ['UL', 'I', 'EM', 'B', 'STRONG', 'P']
},
dom () {
const text = this.string.replace(/(?<!\(\s*)(https?:\/\/[^\s)]+)(?!\s*\))/g, (url) => `[${url}](${url})`)
const text = linkParts.map((part, index) => {
if (part.match(/^https?:\/\//) && !linkParts[index - 1]?.match(/\(\s*$/) && !linkParts[index + 1]?.match(/^\s*\)/)) {
return `[${part}](${part})`
} else {
return part
}
}).join('')
return snarkdown(text.replace(/\n/g, '<br>')).split(htmlSplitRegexp)
return new DOMParser().parseFromString(snarkdown(text.replace(/\n/g, '<br>')), 'text/html')
}
},
methods: {
sanitizeHref (href) {
if (href && href.trim().match(/^((?:https?:\/\/)|\/)/)) {
return href.replace(/javascript:/g, '')
}
},
extractAttr (text, attr) {
if (text.includes(attr)) {
return text.split(attr).pop().split('"')[1]
}
},
extractText (text) {
if (text) {
return text.match(/>(.+?)</)?.[1]
}
}
sanitizeUrl
}
}
</script>
@@ -39,8 +39,10 @@
:id="field.uuid"
v-model="number"
type="number"
:step="field.validation?.step || 'any'"
:min="field.validation?.min"
:max="field.validation?.max"
class="base-input !text-2xl w-full"
step="any"
:required="field.required"
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
:name="`values[${field.uuid}]`"
@@ -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()
}
},
+116 -31
View File
@@ -72,11 +72,11 @@
@blur="onNameBlur"
>{{ optionIndexText }} {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || defaultName }}</span>
<div
v-if="isSettingsFocus || (isValueInput && field.type !== 'heading') || (isNameFocus && !['checkbox', 'phone'].includes(field.type))"
v-if="isSettingsFocus || isSelectInput || (isValueInput && field.type !== 'heading') || (isNameFocus && !['checkbox', 'phone'].includes(field.type))"
class="flex items-center ml-1.5"
>
<input
v-if="!isValueInput"
v-if="!isValueInput && !isSelectInput"
:id="`required-checkbox-${field.uuid}`"
v-model="field.required"
type="checkbox"
@@ -84,14 +84,14 @@
@mousedown.prevent
>
<label
v-if="!isValueInput"
v-if="!isValueInput && !isSelectInput"
:for="`required-checkbox-${field.uuid}`"
class="label text-xs"
@click.prevent="field.required = !field.required"
@mousedown.prevent
>{{ t('required') }}</label>
<input
v-if="isValueInput"
v-if="isValueInput || isSelectInput"
:id="`readonly-checkbox-${field.uuid}`"
type="checkbox"
class="checkbox checkbox-xs no-animation rounded"
@@ -100,7 +100,7 @@
@mousedown.prevent
>
<label
v-if="isValueInput"
v-if="isValueInput || isSelectInput"
:for="`readonly-checkbox-${field.uuid}`"
class="label text-xs"
@click.prevent="field.readonly = !(field.readonly ?? true)"
@@ -141,6 +141,7 @@
: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"
@@ -163,30 +164,39 @@
ref="touchValueTarget"
class="flex h-full w-full field-area"
dir="auto"
:class="[isValueInput ? 'bg-opacity-50' : 'bg-opacity-80', field.type === 'heading' ? 'bg-gray-50' : bgColors[submitterIndex % bgColors.length], isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? fontClasses : 'justify-center items-center']"
:class="[isValueInput ? 'cursor-text' : '', isValueInput || isCheckboxInput || isSelectInput ? 'bg-opacity-50' : 'bg-opacity-80', field.type === 'heading' ? 'bg-gray-50' : bgColors[submitterIndex % bgColors.length], isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? fontClasses : 'justify-center items-center']"
@click="focusValueInput"
>
<span
v-if="field"
class="flex justify-center items-center space-x-1"
:class="{ 'w-full': ['cells', 'checkbox'].includes(field.type), 'h-full': !isValueInput && !isDefaultValuePresent }"
:class="{ 'w-full': isWFullType, 'h-full': !isValueInput && !isDefaultValuePresent }"
>
<div
v-if="isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas && field.type !== 'checkbox')"
:class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type) }"
v-if="isDefaultValuePresent || isValueInput || isSelectInput || (withFieldPlaceholder && field.areas && field.type !== 'checkbox')"
:class="{ 'w-full h-full': isWFullType }"
:style="fontStyle"
>
<div
ref="textContainer"
class="flex items-center px-0.5"
:style="{ color: field.preferences?.color }"
:class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type) }"
:class="{ 'w-full h-full': isWFullType }"
>
<IconCheck
v-if="field.type == 'checkbox'"
class="aspect-square mx-auto"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
/>
<template
v-else-if="(field.type === 'radio' || field.type === 'multiple') && field?.areas?.length > 1"
>
<IconCheck
v-if="field.type === 'multiple' ? field.default_value.includes(buildAreaOptionValue(area)) : buildAreaOptionValue(area) === field.default_value"
class="aspect-square mx-auto"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
/>
</template>
<span
v-else-if="field.type === 'number' && !isValueInput && (field.default_value || field.default_value == 0)"
class="whitespace-pre-wrap"
@@ -209,14 +219,39 @@
{{ char }}
</div>
</div>
<select
v-else-if="isSelectInput"
ref="defaultValueSelect"
class="bg-transparent outline-none focus:outline-none w-full"
@change="[field.default_value = $event.target.value, field.readonly = !!field.default_value?.length, save()]"
@focus="selectedAreaRef.value = area"
@keydown.enter="onDefaultValueEnter"
>
<option
:disabled="!field.default_value?.length"
:selected="!field.default_value?.length"
:value="''"
>
{{ t(field.default_value?.length ? 'none' : 'select') }}
</option>
<option
v-for="(option, index) in field.options"
:key="index"
:selected="field.default_value === option.value"
:value="option.value"
>
{{ option.value }}
</option>
</select>
<span
v-else
ref="defaultValue"
:contenteditable="isValueInput"
class="whitespace-pre-wrap outline-none empty:before:content-[attr(placeholder)] before:text-gray-400"
class="whitespace-pre-wrap outline-none empty:before:content-[attr(placeholder)] before:text-base-content/30"
:class="{ 'cursor-text': isValueInput }"
:placeholder="withFieldPlaceholder && !isValueInput ? defaultField?.title || field.title || field.name || defaultName : t('type_value')"
:placeholder="withFieldPlaceholder && !isValueInput ? defaultField?.title || field.title || field.name || defaultName : (field.type === 'date' ? field.preferences?.format || t('type_value') : t('type_value'))"
@blur="onDefaultValueBlur"
@focus="selectedAreaRef.value = area"
@paste.prevent="onPaste"
@keydown.enter="onDefaultValueEnter"
>{{ field.default_value }}</span>
@@ -224,7 +259,7 @@
</div>
<component
:is="fieldIcons[field.type]"
v-else
v-else-if="!isCheckboxInput"
width="100%"
height="100%"
class="max-h-10 opacity-50"
@@ -232,12 +267,12 @@
</span>
</div>
<div
v-if="!isValueInput"
v-if="!isValueInput && !isSelectInput"
ref="touchTarget"
class="absolute top-0 bottom-0 right-0 left-0"
:class="isDragged ? 'cursor-grab' : 'cursor-pointer'"
@dblclick="maybeToggleDefaultValue"
@click="maybeToggleCheckboxValue"
@click="inputMode && maybeToggleCheckboxValue()"
/>
<span
v-if="field?.type && editable"
@@ -252,6 +287,7 @@
<FormulaModal
:field="field"
:editable="editable && !defaultField"
:default-field="defaultField"
:build-default-name="buildDefaultName"
@close="isShowFormulaModal = false"
/>
@@ -263,6 +299,7 @@
<FontModal
:field="field"
:editable="editable && !defaultField"
:default-field="defaultField"
:build-default-name="buildDefaultName"
@close="isShowFontModal = false"
/>
@@ -274,6 +311,7 @@
<ConditionsModal
:item="field"
:build-default-name="buildDefaultName"
:default-field="defaultField"
@close="isShowConditionsModal = false"
/>
</Teleport>
@@ -284,6 +322,7 @@
<DescriptionModal
:field="field"
:editable="editable && !defaultField"
:default-field="defaultField"
:build-default-name="buildDefaultName"
@close="isShowDescriptionModal = false"
/>
@@ -353,6 +392,11 @@ export default {
required: false,
default: null
},
withPrefillable: {
type: Boolean,
required: false,
default: false
},
defaultSubmitters: {
type: Array,
required: false,
@@ -392,6 +436,9 @@ export default {
fieldNames: FieldType.computed.fieldNames,
fieldLabels: FieldType.computed.fieldLabels,
fieldIcons: FieldType.computed.fieldIcons,
isWFullType () {
return ['cells', 'checkbox', 'radio', 'multiple', 'select'].includes(this.field.type)
},
fontStyle () {
let fontSize = ''
@@ -411,6 +458,13 @@ export default {
return { fontSize, lineHeight: `calc(${fontSize} * ${this.lineHeight})` }
},
optionsUuidIndex () {
return this.field.options.reduce((acc, option) => {
acc[option.uuid] = option
return acc
}, {})
},
fontSizePx () {
return parseInt(this.field?.preferences?.font_size || 11) * this.fontScale
},
@@ -421,14 +475,17 @@ export default {
return 1040 / 612.0
},
isDefaultValuePresent () {
if (this.field?.type === 'radio' && this.field?.areas?.length > 1) {
return false
} else {
return this.field?.default_value || this.field?.default_value === 0
}
return this.field?.default_value || this.field?.default_value === 0
},
isSelectInput () {
return this.inputMode && (this.field.type === 'select' || (this.field.type === 'radio' && this.field.areas?.length < 2))
},
isCheckboxInput () {
return this.inputMode && (this.field.type === 'checkbox' || (['radio', 'multiple'].includes(this.field.type) && this.area.option_uuid))
},
isValueInput () {
return (this.field.type === 'heading' && this.isHeadingSelected) || this.isContenteditable || (this.inputMode && ['text', 'number', 'date'].includes(this.field.type))
return (this.field.type === 'heading' && this.isHeadingSelected) || this.isContenteditable ||
(this.inputMode && (['text', 'number'].includes(this.field.type) || (this.field.type === 'date' && this.field.default_value !== '{{date}}')))
},
modalContainerEl () {
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
@@ -543,6 +600,11 @@ export default {
closeDropdown () {
this.$el.getRootNode().activeElement.blur()
},
buildAreaOptionValue (area) {
const option = this.optionsUuidIndex[area.option_uuid]
return option.value || `${this.t('option')} ${this.field.options.indexOf(option) + 1}`
},
maybeToggleDefaultValue () {
if (!this.editable) {
return
@@ -552,22 +614,41 @@ export default {
this.isContenteditable = true
this.focusValueInput()
} else if (this.field.type === 'checkbox') {
this.field.readonly = !this.field.readonly
this.field.default_value === true ? delete this.field.default_value : this.field.default_value = true
this.save()
} else if (this.field.type === 'date') {
this.field.readonly = !this.field.readonly
this.field.default_value === '{{date}}' ? delete this.field.default_value : this.field.default_value = '{{date}}'
this.save()
} else {
this.maybeToggleCheckboxValue()
}
},
maybeToggleCheckboxValue () {
if (this.inputMode && this.field.type === 'checkbox') {
this.field.readonly = !this.field.readonly
if (this.field.type === 'checkbox') {
this.field.default_value === true ? delete this.field.default_value : this.field.default_value = true
this.field.readonly = this.field.default_value === true
this.save()
} else if (this.field.type === 'radio' && this.area.option_uuid) {
const value = this.buildAreaOptionValue(this.area)
this.field.default_value === value ? delete this.field.default_value : this.field.default_value = value
this.field.readonly = !!this.field.default_value?.length
this.save()
} else if (this.field.type === 'multiple' && this.area.option_uuid) {
const value = this.buildAreaOptionValue(this.area)
if (this.field.default_value?.includes(value)) {
this.field.default_value.splice(this.field.default_value.indexOf(value), 1)
if (!this.field.default_value?.length) delete this.field.default_value
} else {
Array.isArray(this.field.default_value) ? this.field.default_value.push(value) : this.field.default_value = [value]
}
this.field.readonly = !!this.field.default_value?.length
this.save()
}
@@ -743,7 +824,7 @@ export default {
}
},
drag (e) {
if (e.target.id === 'mask') {
if (e.target.id === 'mask' && this.editable) {
this.isDragged = true
this.area.x = (e.offsetX - this.dragFrom.x) / e.target.clientWidth
@@ -759,7 +840,9 @@ export default {
e.preventDefault()
this.isDragged = true
if (this.editable) {
this.isDragged = true
}
const rect = e.target.getBoundingClientRect()
@@ -812,7 +895,9 @@ export default {
e.preventDefault()
this.isDragged = true
if (this.editable) {
this.isDragged = true
}
const rect = e.target.getBoundingClientRect()
+20 -6
View File
@@ -331,6 +331,7 @@
: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"
@@ -438,6 +439,7 @@
: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"
@@ -543,7 +545,6 @@ export default {
withPayment: this.withPayment,
isPaymentConnected: this.isPaymentConnected,
withFormula: this.withFormula,
withSignatureId: this.withSignatureId,
withConditions: this.withConditions,
isInlineSize: this.isInlineSize,
defaultDrawFieldType: this.defaultDrawFieldType,
@@ -659,7 +660,7 @@ export default {
acceptFileTypes: {
type: String,
required: false,
default: 'image/*, application/pdf'
default: 'image/*, application/pdf, application/zip'
},
baseUrl: {
type: String,
@@ -802,6 +803,13 @@ 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')
},
@@ -1057,7 +1065,7 @@ export default {
}
if (['select', 'multiple', 'radio'].includes(type)) {
field.options = [{ value: '', uuid: v4() }]
field.options = [{ value: '', uuid: v4() }, { value: '', uuid: v4() }]
}
if (type === 'stamp') {
@@ -1101,7 +1109,7 @@ export default {
}
if (['select', 'multiple', 'radio'].includes(type)) {
field.options = [{ value: '', uuid: v4() }]
field.options = [{ value: '', uuid: v4() }, { value: '', uuid: v4() }]
}
if (type === 'stamp') {
@@ -1293,7 +1301,13 @@ export default {
this.copiedArea.option_uuid ||= field.options[0].uuid
area.option_uuid = v4()
field.options.push({ uuid: area.option_uuid })
const lastOption = field.options[field.options.length - 1]
if (!field.areas.find((a) => lastOption.uuid === a.option_uuid)) {
area.option_uuid = lastOption.uuid
} else {
field.options.push({ uuid: area.option_uuid })
}
field.areas.push(area)
} else {
@@ -1469,7 +1483,7 @@ export default {
if (this.dragField?.options?.length) {
field.options = this.dragField.options.map(option => ({ value: option, uuid: v4() }))
} else {
field.options = [{ value: '', uuid: v4() }]
field.options = [{ value: '', uuid: v4() }, { value: '', uuid: v4() }]
}
}
@@ -9,7 +9,7 @@
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span class="modal-title">
{{ t('condition') }} - {{ item.name || buildDefaultName(item, template.fields) }}
{{ t('condition') }} - {{ (defaultField ? (defaultField.title || item.title || item.name) : item.name) || buildDefaultName(item, template.fields) }}
</span>
<a
href="#"
@@ -160,6 +160,11 @@ export default {
type: Object,
required: true
},
defaultField: {
type: Object,
required: false,
default: null
},
buildDefaultName: {
type: Function,
required: true
+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,
@@ -9,7 +9,7 @@
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span class="modal-title">
{{ field.name || buildDefaultName(field, template.fields) }}
{{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field, template.fields) }}
</span>
<a
href="#"
@@ -73,6 +73,11 @@ export default {
type: Object,
required: true
},
defaultField: {
type: Object,
required: false,
default: null
},
editable: {
type: Boolean,
required: false,
@@ -11,6 +11,7 @@
:areas="areasIndex[index]"
:allow-draw="allowDraw"
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
:is-drag="isDrag"
:with-field-placeholder="withFieldPlaceholder"
:default-fields="defaultFields"
@@ -72,6 +73,11 @@ export default {
required: false,
default: null
},
withPrefillable: {
type: Boolean,
required: false,
default: false
},
drawFieldType: {
type: String,
required: false,
+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()
+35 -3
View File
@@ -126,6 +126,7 @@
:default-field="defaultField"
:editable="editable"
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
:background-color="dropdownBgColor"
@click-formula="isShowFormulaModal = true"
@click-font="isShowFontModal = true"
@@ -150,7 +151,7 @@
</div>
</div>
<div
v-if="field.options && withOptions"
v-if="field.options && withOptions && (isExpandOptions || field.options.length < 5)"
ref="options"
class="border-t border-base-300 mx-2 pt-2 space-y-1.5"
draggable="true"
@@ -215,12 +216,30 @@
/>
<button
v-else-if="field.options && editable && !defaultField"
class="text-center text-sm w-full pb-1"
class="field-add-option text-center text-sm w-full pb-1"
@click="addOption"
>
+ {{ t('add_option') }}
</button>
</div>
<div
v-else-if="field.options && withOptions && !isExpandOptions && field.options.length > 4"
class="border-t border-base-300 mx-2 space-y-1.5"
>
<button
class="field-expand-options text-center text-sm w-full py-1 flex space-x-0.5 justify-center items-center"
@click="isExpandOptions = true"
>
<span class="lowercase">
{{ field.options.length }} {{ t('options') }}
</span>
<IconChevronDown
class="ml-2 mr-2 mt-0.5"
width="15"
height="15"
/>
</button>
</div>
</div>
<Teleport
v-if="isShowFormulaModal"
@@ -229,6 +248,7 @@
<FormulaModal
:field="field"
:editable="editable && !defaultField"
:default-field="defaultField"
:build-default-name="buildDefaultName"
@close="isShowFormulaModal = false"
/>
@@ -240,6 +260,7 @@
<FontModal
:field="field"
:editable="editable && !defaultField"
:default-field="defaultField"
:build-default-name="buildDefaultName"
@close="isShowFontModal = false"
/>
@@ -250,6 +271,7 @@
>
<ConditionsModal
:item="field"
:default-field="defaultField"
:build-default-name="buildDefaultName"
@close="isShowConditionsModal = false"
/>
@@ -261,6 +283,7 @@
<DescriptionModal
:field="field"
:editable="editable && !defaultField"
:default-field="defaultField"
:build-default-name="buildDefaultName"
@close="isShowDescriptionModal = false"
/>
@@ -277,7 +300,7 @@ import FormulaModal from './formula_modal'
import FontModal from './font_modal'
import ConditionsModal from './conditions_modal'
import DescriptionModal from './description_modal'
import { IconRouteAltLeft, IconMathFunction, IconNewSection, IconTrashX, IconSettings } from '@tabler/icons-vue'
import { IconRouteAltLeft, IconMathFunction, IconNewSection, IconTrashX, IconSettings, IconChevronDown } from '@tabler/icons-vue'
import { v4 } from 'uuid'
export default {
@@ -287,6 +310,7 @@ export default {
IconSettings,
FieldSettings,
PaymentSettings,
IconChevronDown,
IconNewSection,
FormulaModal,
FontModal,
@@ -308,6 +332,11 @@ export default {
required: false,
default: null
},
withPrefillable: {
type: Boolean,
required: false,
default: false
},
withOptions: {
type: Boolean,
required: false,
@@ -327,6 +356,7 @@ export default {
emits: ['set-draw', 'remove', 'scroll-to'],
data () {
return {
isExpandOptions: false,
isNameFocus: false,
showPaymentModal: false,
isShowFormulaModal: false,
@@ -421,6 +451,8 @@ export default {
this.$el.getRootNode().activeElement.blur()
},
addOption () {
this.isExpandOptions = true
this.field.options.push({ value: '', uuid: v4() })
this.$nextTick(() => {
@@ -1,31 +1,4 @@
<template>
<div
v-if="field.type === 'number'"
class="py-1.5 px-1 relative"
@click.stop
>
<select
:placeholder="t('format')"
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
@change="[field.preferences ||= {}, field.preferences.format = $event.target.value, save()]"
>
<option
v-for="format in numberFormats"
:key="format"
:value="format"
:selected="format === field.preferences?.format || (format === 'none' && !field.preferences?.format)"
>
{{ formatNumber(123456789.567, format) }}
</option>
</select>
<label
:style="{ backgroundColor }"
class="absolute -top-1 left-2.5 px-1 h-4"
style="font-size: 8px"
>
{{ t('format') }}
</label>
</div>
<div
v-if="field.type === 'verification'"
class="py-1.5 px-1 relative"
@@ -217,6 +190,77 @@
</label>
</div>
</div>
<div
v-if="field.type === 'number'"
class="py-1.5 px-1 relative flex space-x-1"
@click.stop
>
<div class="w-1/2 relative">
<input
:placeholder="t('min')"
type="number"
min="0"
:value="field.validation?.min"
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
@input="[field.validation ||= {}, $event.target.value ? field.validation.min = $event.target.value : delete field.validation.min]"
@blur="save"
>
<label
v-if="field.validation?.min"
:style="{ backgroundColor }"
class="absolute -top-2.5 left-1.5 px-1 h-4"
style="font-size: 8px"
>
{{ t('min') }}
</label>
</div>
<div class="w-1/2 relative">
<input
:placeholder="t('max')"
type="number"
min="1"
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
:value="field.validation?.max"
@input="[field.validation ||= {}, $event.target.value ? field.validation.max = $event.target.value : delete field.validation.max]"
@blur="save"
>
<label
v-if="field.validation?.max"
:style="{ backgroundColor }"
class="absolute -top-2.5 left-1.5 px-1 h-4"
style="font-size: 8px"
>
{{ t('max') }}
</label>
</div>
</div>
<div
v-if="field.type === 'number'"
class="py-1.5 px-1 relative"
@click.stop
>
<select
:placeholder="t('format')"
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
@change="[field.preferences ||= {}, field.preferences.format = $event.target.value, save()]"
>
<option
v-for="format in numberFormats"
:key="format"
:value="format"
:selected="format === field.preferences?.format || (format === 'none' && !field.preferences?.format)"
>
{{ formatNumber(123456789.567, format) }}
</option>
</select>
<label
:style="{ backgroundColor }"
class="absolute -top-1 left-2.5 px-1 h-4"
style="font-size: 8px"
>
{{ t('format') }}
</label>
</div>
<div
v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation"
class="py-1.5 px-1 relative"
@@ -304,7 +348,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"
@@ -393,7 +437,7 @@
</label>
</li>
<li
v-if="['text', 'number'].includes(field.type) && !defaultField"
v-if="['text', 'number'].includes(field.type)"
@click.stop
>
<label class="cursor-pointer py-1.5">
@@ -401,11 +445,27 @@
v-model="field.readonly"
type="checkbox"
class="toggle toggle-xs"
:disabled="!editable || (defaultField && [true, false].includes(defaultField.readonly))"
@update:model-value="save"
>
<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"
@@ -555,6 +615,11 @@ export default {
required: false,
default: null
},
withPrefillable: {
type: Boolean,
required: false,
default: false
},
withRequired: {
type: Boolean,
required: false,
@@ -27,6 +27,7 @@
: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)]"
@@ -259,6 +260,11 @@ export default {
required: false,
default: null
},
withPrefillable: {
type: Boolean,
required: false,
default: false
},
template: {
type: Object,
required: true
@@ -9,7 +9,7 @@
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span class="modal-title">
{{ t('font') }} - {{ field.name || buildDefaultName(field, template.fields) }}
{{ t('font') }} - {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field, template.fields) }}
</span>
<a
href="#"
@@ -202,6 +202,11 @@ export default {
type: Object,
required: true
},
defaultField: {
type: Object,
required: false,
default: null
},
editable: {
type: Boolean,
required: false,
@@ -9,7 +9,7 @@
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span class="modal-title">
{{ t('formula') }} - {{ field.name || buildDefaultName(field, template.fields) }}
{{ t('formula') }} - {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field, template.fields) }}
</span>
<a
href="#"
@@ -28,14 +28,21 @@
class="link"
>{{ t('available_in_pro') }}</a>
</div>
<div class="flex-inline mb-2 gap-2 space-y-1">
<div class="flex flex-wrap mb-2 gap-y-1 pt-1">
<button
v-for="f in fields"
:key="f.uuid"
class="mr-1 btn btn-neutral btn-outline border-base-content/20 btn-sm normal-case font-normal bg-white !rounded-xl"
class="mr-1 flex btn btn-neutral btn-outline border-base-content/20 btn-sm normal-case font-normal bg-white !rounded-xl"
@click.prevent="insertTextUnderCursor(`{{${f.name || buildDefaultName(f, template.fields)}}}`)"
>
<IconMathFunction
v-if="f.preferences?.formula"
width="17"
height="17"
stroke-width="1.5"
/>
<IconCodePlus
v-else
width="20"
height="20"
stroke-width="1.5"
@@ -57,7 +64,7 @@
<div class="mb-3 mt-1">
<div
target="blank"
class="text-sm mb-2 inline space-x-2"
class="text-sm mb-2 inline space-x-2 font-mono"
>
<button
class="bg-base-200 px-2 rounded-xl"
@@ -83,12 +90,6 @@
>
/
</button>
<button
class="bg-base-200 px-2 rounded-xl"
@click="insertTextUnderCursor(' % ')"
>
%
</button>
<button
class="bg-base-200 px-2 rounded-xl"
@click="insertTextUnderCursor('^')"
@@ -122,12 +123,13 @@
</template>
<script>
import { IconCodePlus } from '@tabler/icons-vue'
import { IconCodePlus, IconMathFunction } from '@tabler/icons-vue'
export default {
name: 'FormulaModal',
components: {
IconCodePlus
IconCodePlus,
IconMathFunction
},
inject: ['t', 'save', 'template', 'withFormula'],
props: {
@@ -135,6 +137,11 @@ export default {
type: Object,
required: true
},
defaultField: {
type: Object,
required: false,
default: null
},
editable: {
type: Boolean,
required: false,
@@ -154,7 +161,7 @@ export default {
computed: {
fields () {
return this.template.fields.reduce((acc, f) => {
if (f !== this.field && ['number'].includes(f.type) && (!f.preferences?.formula || f.submitter_uuid !== this.field.submitter_uuid)) {
if (f !== this.field && ['number'].includes(f.type) && (!f.preferences?.formula || !f.preferences.formula.includes(this.field.uuid))) {
acc.push(f)
}
@@ -224,9 +231,11 @@ export default {
this.formula = newText
textarea.setSelectionRange(cursorPos + textToInsert.length, cursorPos + textToInsert.length)
this.$nextTick(() => {
textarea.setSelectionRange(cursorPos + textToInsert.length, cursorPos + textToInsert.length)
textarea.focus()
textarea.focus()
})
},
resizeTextarea () {
const textarea = this.$refs.textarea
@@ -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,5 @@
const en = {
prefillable: 'Prefillable',
signature_id: 'Signature ID',
error_message: 'Error message',
length: 'Length',
@@ -59,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',
@@ -81,6 +83,7 @@ const en = {
copy_to_all_pages: 'Copy to All Pages',
add_option: 'Add option',
option: 'Option',
options: 'Options',
condition: 'Condition',
first_party: 'First Party',
second_party: 'Second Party',
@@ -173,6 +176,7 @@ const en = {
}
const es = {
prefillable: 'Rellenable',
signature_id: 'ID de Firma',
error_message: 'Mensaje de error',
length: 'Longitud',
@@ -252,6 +256,7 @@ const es = {
copy_to_all_pages: 'Copiar a todas las páginas',
add_option: 'Agregar opción',
option: 'Opción',
options: 'Opciones',
first_party: 'Primera Parte',
second_party: 'Segunda Parte',
third_party: 'Tercera Parte',
@@ -320,6 +325,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',
@@ -347,6 +353,7 @@ const es = {
}
const it = {
prefillable: 'Precompilabile',
signature_id: 'ID firma',
error_message: 'Messaggio di errore',
length: 'Lunghezza',
@@ -407,6 +414,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',
@@ -429,6 +437,7 @@ const it = {
copy_to_all_pages: 'Copia in tutte le pagine',
add_option: 'Aggiungi opzione',
option: 'Opzione',
options: 'Opzioni',
condition: 'Condizione',
first_party: 'Prima parte',
second_party: 'Seconda parte',
@@ -521,6 +530,7 @@ const it = {
}
const pt = {
prefillable: 'Pré-preenchível',
signature_id: 'ID da Assinatura',
error_message: 'Mensagem de erro',
length: 'Comprimento',
@@ -600,6 +610,7 @@ const pt = {
copy_to_all_pages: 'Copiar para todas as páginas',
add_option: 'Adicionar opção',
option: 'Opção',
options: 'Opções',
first_party: 'Primeira Parte',
second_party: 'Segunda Parte',
third_party: 'Terceira Parte',
@@ -668,6 +679,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',
@@ -695,6 +707,7 @@ const pt = {
}
const fr = {
prefillable: 'Pré-remplissable',
signature_id: 'ID de signature',
error_message: 'Message d\'erreur',
length: 'Longueur',
@@ -773,6 +786,7 @@ const fr = {
copy_to_all_pages: 'Copier sur toutes les pages',
add_option: 'Ajouter une option',
option: 'Option',
options: 'Options',
first_party: 'Première partie',
second_party: 'Deuxième partie',
third_party: 'Troisième partie',
@@ -842,6 +856,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',
@@ -869,6 +884,7 @@ const fr = {
}
const de = {
prefillable: 'Vorausfüllbar',
signature_id: 'Signatur-ID',
error_message: 'Fehlermeldung',
length: 'Länge',
@@ -947,6 +963,7 @@ const de = {
copy_to_all_pages: 'Auf alle Seiten kopieren',
add_option: 'Option hinzufügen',
option: 'Option',
options: 'Options',
first_party: 'Erste Partei',
second_party: 'Zweite Partei',
third_party: 'Dritte Partei',
@@ -1016,6 +1033,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',
+7 -1
View File
@@ -1,7 +1,7 @@
<template>
<div
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 }"
:class="{ 'cursor-crosshair': allowDraw && editable, 'touch-none': !!drawField }"
style="container-type: size"
:style="{ aspectRatio: `${width} / ${height}`}"
>
@@ -28,6 +28,7 @@
: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"
@@ -84,6 +85,11 @@ export default {
required: false,
default: null
},
withPrefillable: {
type: Boolean,
required: false,
default: false
},
areas: {
type: Array,
required: false,
+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
+1
View File
@@ -2,6 +2,7 @@
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(':'))
+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">
-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') %>
+1 -1
View File
@@ -17,7 +17,7 @@
"start_url": "/",
"display": "standalone",
"scope": "/",
"orientation": "natural",
"orientation": "any",
"description": "<%= Docuseal.product_name %> is an open source platform that provides secure and efficient digital document signing and processing.",
"categories": ["productivity", "utilities"],
"theme_color": "#FAF7F4",
-4
View File
@@ -1,4 +0,0 @@
<script>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('<%= ENV.fetch('POSTHOG_TOKEN', nil) %>',{api_host:'https://app.posthog.com',autocapture: false,enable_recording_console_log: true,capture_pageview: false,session_recording:{maskAllInputs: false,maskInputOptions: {password: true}}})
</script>
+51 -15
View File
@@ -1,3 +1,4 @@
<% has_phone_field = false %>
<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
<% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? } %>
<dynamic-list class="space-y-4">
@@ -11,6 +12,7 @@
</div>
<div class="grid <%= 'md:grid-cols-2' if submitters.size > 1 %> gap-4">
<% submitters.each_with_index do |item, index| %>
<% prefillable_fields = local_assigns[:prefillable_fields].to_a.select { |f| f['submitter_uuid'] == item['uuid'] } %>
<submitter-item class="form-control">
<% if submitters.size > 1 %>
<label class="label pt-0 pb-1">
@@ -18,30 +20,62 @@
</label>
<% end %>
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
<submitters-autocomplete data-field="name">
<linked-input data-target-id="<%= "detailed_name_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
<%= tag.input type: 'text', name: 'submission[1][submitters][][name]', autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: t('name'), required: index.zero? || template.preferences['require_all_submitters'], value: item['email'].present? ? current_account.submitters.accessible_by(current_ability).where.not(name: nil).order(id: :desc).find_by(email: item['email'])&.name : ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.full_name : ''), dir: 'auto', id: "detailed_name_#{item['uuid']}" %>
</linked-input>
</submitters-autocomplete>
<div class="grid <%= 'md:grid-cols-2 gap-1' if submitters.size == 1 %>">
<% if prefillable_fields.blank? %>
<submitters-autocomplete data-field="name">
<linked-input data-target-id="<%= "detailed_name_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
<%= tag.input type: 'text', name: 'submission[1][submitters][][name]', autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: t('name'), required: index.zero? || template.preferences['require_all_submitters'], value: item['email'].present? ? current_account.submitters.accessible_by(current_ability).where.not(name: nil).order(id: :desc).find_by(email: item['email'])&.name : ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.full_name : ''), dir: 'auto', id: "detailed_name_#{item['uuid']}" %>
</linked-input>
</submitters-autocomplete>
<div class="grid <%= 'md:grid-cols-2 gap-1' if submitters.size == 1 %>">
<submitters-autocomplete data-field="email">
<linked-input data-target-id="<%= "detailed_email_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
<input type="email" multiple name="submission[1][submitters][][email]" autocomplete="off" class="base-input !h-10 mt-1.5 w-full" placeholder="<%= "#{t('email')} (#{t('optional')})" %>" value="<%= item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : '') %>" id="detailed_email_<%= item['uuid'] %>">
</linked-input>
</submitters-autocomplete>
<% has_phone_field = true %>
<submitters-autocomplete data-field="phone">
<linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('#{t('use_international_format_1xxx_')}') : ''", oninput: "this.setCustomValidity('')", name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: local_assigns[:require_phone_2fa] == true ? t(:phone) : "#{t('phone')} (#{t('optional')})", id: "detailed_phone_#{item['uuid']}", required: local_assigns[:require_phone_2fa] == true %>
</linked-input>
</submitters-autocomplete>
</div>
<% end %>
<% if prefillable_fields.present? %>
<submitters-autocomplete data-field="email">
<linked-input data-target-id="<%= "detailed_email_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
<input type="email" multiple name="submission[1][submitters][][email]" autocomplete="off" class="base-input !h-10 mt-1.5 w-full" placeholder="<%= "#{t('email')} (#{t('optional')})" %>" value="<%= item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : '') %>" id="detailed_email_<%= item['uuid'] %>">
<input type="email" multiple name="submission[1][submitters][][email]" autocomplete="off" class="base-input !h-10 w-full" placeholder="<%= t('email') %>" value="<%= item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : '') %>" id="detailed_email_<%= item['uuid'] %>" required>
</linked-input>
</submitters-autocomplete>
<submitters-autocomplete data-field="phone">
<linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('#{t('use_international_format_1xxx_')}') : ''", oninput: "this.setCustomValidity('')", name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: local_assigns[:require_phone_2fa] == true ? t(:phone) : "#{t('phone')} (#{t('optional')})", id: "detailed_phone_#{item['uuid']}", required: local_assigns[:require_phone_2fa] == true %>
</linked-input>
</submitters-autocomplete>
</div>
<% if local_assigns[:require_phone_2fa] == true || prefillable_fields.any? { |f| f['type'] == 'phone' } %>
<% has_phone_field = true %>
<submitters-autocomplete data-field="phone">
<linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('#{t('use_international_format_1xxx_')}') : ''", oninput: "this.setCustomValidity('')", name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: t(:phone), id: "detailed_phone_#{item['uuid']}", required: true %>
</linked-input>
</submitters-autocomplete>
<% end %>
<% prefillable_fields.each do |field| %>
<% if field['type'] == 'checkbox' %>
<label for="detailed_field_<%= field['uuid'] %>" class="flex items-center justify-between mt-1.5 pl-3 pr-2.5 h-10 border border-base-content/20 rounded-full cursor-pointer transition-colors bg-white">
<span class="text-base select-none px-1"> <%= field['title'].presence || field['name'] %></span>
<%= tag.input type: 'checkbox', name: "submission[1][submitters][][values][#{field['uuid']}]", id: "detailed_field_#{field['uuid']}", class: 'toggle toggle-sm', style: 'width: 38px; --handleoffset: 17px', checked: field['default_value'].present? && (field['default_value'] == true || field['default_value'].to_s == '1' || field['default_value'].to_s.downcase == 'true'), required: field['required'], value: 'true' %>
</label>
<% elsif field['type'] == 'select' || field['type'] == 'radio' %>
<%= select_tag "submission[1][submitters][][values][#{field['uuid']}]", options_for_select(field['options'].pluck('value'), field['default_value']), prompt: t(:select), id: "detailed_field_#{field['uuid']}", class: 'select select-sm base-input !h-10 mt-1.5 ', required: field['required'] %>
<% elsif field['type'] == 'date' %>
<%= tag.input type: field['type'], name: "submission[1][submitters][][values][#{field['uuid']}]", autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full border rounded p-3', placeholder: (field['required'] ? field['title'].presence || field['name'] : "#{field['title'].presence || field['name']} (#{t('optional')})"), value: field['default_value'], id: "detailed_field_#{field['uuid']}", required: field['required'] %>
<% elsif field['type'] != 'phone' %>
<%= tag.input type: field['type'], name: "submission[1][submitters][][values][#{field['uuid']}]", autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full border rounded p-3', placeholder: (field['required'] ? field['title'].presence || field['name'] : "#{field['title'].presence || field['name']} (#{t('optional')})"), value: field['default_value'], id: "detailed_field_#{field['uuid']}", required: field['required'] %>
<% end %>
<% end %>
<% end %>
</submitter-item>
<% end %>
</div>
</div>
</div>
</div>
<% if params[:selfsign].blank? %>
<% if params[:selfsign].blank? && local_assigns[:prefillable_fields].blank? %>
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem">
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
<span><%= t('add_new') %></span>
@@ -51,7 +85,9 @@
<div>
<%= render('submitters_order', f:, template:) if Accounts.can_send_emails?(current_account) %>
<%= render 'send_email', f:, template: %>
<%= render 'send_sms', f: %>
<% if has_phone_field %>
<%= render 'send_sms', f: %>
<% end %>
</div>
<div class="form-control">
<%= f.button button_title(title: t('add_recipients')), class: 'base-button' %>
+1 -1
View File
@@ -4,7 +4,7 @@
<% can_send_emails = Accounts.can_send_emails?(current_account) %>
<div class="flex justify-between items-center">
<%= f.label :send_email, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %>
<%= f.check_box :send_email, id: uuid, class: 'base-checkbox', disabled: !can_send_emails || local_assigns[:disable_email], checked: can_send_emails && !local_assigns.key?(:resend_email) && !local_assigns[:disable_email] %>
<%= f.check_box :send_email, id: uuid, class: 'base-checkbox', disabled: !can_send_emails || local_assigns[:disable_email], checked: can_send_emails && !local_assigns.key?(:resend_email) && !local_assigns[:disable_email] && template&.preferences&.dig('request_email_enabled') != false %>
<span class="label"><%= local_assigns[:resend_email] ? t('re_send_email') : t('send_email') %></span>
<% end %>
<div>
@@ -1,4 +1,4 @@
<% if template.preferences['submitters_order'] == 'preserved' %>
<% if template.preferences['submitters_order'] == 'preserved' || template.submitters.any? { |s| s['order'] } %>
<%= f.hidden_field :preserve_order, value: '1' %>
<% elsif template.submitters.size > 1 %>
<div class="form-control">
+7 -5
View File
@@ -1,11 +1,13 @@
<% require_phone_2fa = @template.preferences['require_phone_2fa'] == true %>
<% prefillable_fields = @template.fields.select { |f| f['prefillable'] } %>
<% only_detailed = require_phone_2fa || prefillable_fields.present? %>
<%= render 'shared/turbo_modal_large', title: params[:selfsign] ? t('add_recipients') : t('add_new_recipients') do %>
<% options = [require_phone_2fa ? nil : [t('via_email'), 'email'], require_phone_2fa ? nil : [t('via_phone'), 'phone'], [t('detailed'), 'detailed'], [t('upload_list'), 'list']].compact %>
<% options = [only_detailed ? nil : [t('via_email'), 'email'], only_detailed ? nil : [t('via_phone'), 'phone'], [t('detailed'), 'detailed'], [t('upload_list'), 'list']].compact %>
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center px-2 mt-4 block">
<div class="flex justify-center">
<% options.each_with_index do |(label, value), index| %>
<div>
<%= radio_button_tag 'option', value, value == (require_phone_2fa ? 'detailed' : 'email'), class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %>
<%= radio_button_tag 'option', value, value == (only_detailed ? 'detailed' : 'email'), class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %>
<label for="option_<%= value %>" class="block bg-base-200 md:min-w-[112px] text-sm font-semibold whitespace-nowrap py-1.5 px-4 peer-checked:bg-base-300 <%= 'hidden sm:inline-block' if value == 'list' %> <%= 'rounded-l-3xl' if index.zero? %> <%= 'rounded-r-3xl sm:rounded-r-none' if value == 'detailed' %> <%= 'rounded-r-3xl' if index == options.size - 1 %>">
<%= label %>
</label>
@@ -14,7 +16,7 @@
</div>
</toggle-visible>
<div class="px-5 mb-5 mt-4">
<% unless require_phone_2fa %>
<% unless only_detailed %>
<div id="email">
<%= render 'email_form', template: @template %>
</div>
@@ -22,8 +24,8 @@
<%= render 'phone_form', template: @template %>
</div>
<% end %>
<div id="detailed" class="<%= 'hidden' unless require_phone_2fa %>">
<%= render 'detailed_form', template: @template, require_phone_2fa: %>
<div id="detailed" class="<%= 'hidden' unless only_detailed %>">
<%= render 'detailed_form', template: @template, require_phone_2fa:, prefillable_fields: %>
</div>
<div id="list" class="hidden">
<%= render 'list_form', template: @template %>
+2 -2
View File
@@ -109,7 +109,7 @@
<%= render 'submissions/annotation', annot: %>
<% end %>
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
<% value = values[field['uuid']] %>
<% value = values[field['uuid']].presence || (field['default_value'] != '{{date}}' && field['readonly'] == true && field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
<% value ||= field['default_value'] if field['type'] == 'heading' %>
<% next if value.blank? %>
<% submitter = submitters_index[field['submitter_uuid']] %>
@@ -238,7 +238,7 @@
<div class="px-1.5 mb-4">
<% submitter_fields_index[item['uuid']].to_a.each_with_index do |field, index| %>
<% submitter_field_counters[field['type']] += 1 %>
<% value = values[field['uuid']] %>
<% value = values[field['uuid']].presence || (field['default_value'] != '{{date}}' && field['readonly'] == true && field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
<% next if value.blank? %>
<% next if field['type'] == 'heading' %>
<div class="pt-2.5 border-b border-base-300">
+1 -1
View File
@@ -77,7 +77,7 @@
<%= render 'submit_form/annotations', annots: %>
<% end %>
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
<% value = values[field['uuid']].presence || (field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submitter.submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
<% value = values[field['uuid']].presence || (field['readonly'] == true && field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submitter.submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
<% next if value.blank? %>
<% next if !field['readonly'] && field['submitter_uuid'] == @submitter.uuid %>
<% next if value == '{{date}}' && field['submitter_uuid'] != @submitter.uuid %>
@@ -12,7 +12,6 @@
<%= javascript_pack_tag 'draw', defer: true %>
<% end %>
<%= stylesheet_pack_tag 'form', media: 'all' %>
<%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %>
</head>
<body>
<% field = (@submitter.submission.template_fields || @submitter.template.fields).find { |f| f['type'] == 'signature' && f['uuid'].starts_with?(params[:f]) } %>
@@ -1,14 +1,13 @@
<% if @email_config || @body.present? %>
<% body = (@body.presence || @email_config.value['body']).to_s %>
<%= render 'custom_content', content: body, submitter: @submitter %>
<% if !body.match?(ReplaceEmailVariables::SUBMITTER_LINK) && !body.match?(ReplaceEmailVariables::SUBMITTER_ID) && !body.match?(ReplaceEmailVariables::SUBMISSION_LINK) && !body.match?(ReplaceEmailVariables::TEMPLATE_ID) && !@submitter.submission.source.in?(%w[api embed]) %>
<% if @body.present? %>
<%= render 'custom_content', content: @body, submitter: @submitter %>
<% if !@body.match?(ReplaceEmailVariables::SUBMITTER_LINK) && !@body.match?(ReplaceEmailVariables::SUBMITTER_ID) && !@body.match?(ReplaceEmailVariables::SUBMISSION_LINK) && !@body.match?(ReplaceEmailVariables::TEMPLATE_ID) && !@submitter.submission.source.in?(%w[api embed]) %>
<p><%= link_to nil, submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email')) %></p>
<% end %>
<% else %>
<p><%= t('hi_there') %>,</p>
<p><%= I18n.t(@submitter.with_signature_fields? ? :you_have_been_invited_to_sign_the_name : :you_have_been_invited_to_submit_the_name_form, name: @submitter.submission.name || @submitter.submission.template.name) %></p>
<p><%= link_to I18n.t(@submitter.with_signature_fields? ? :review_and_sign : :review_and_submit), submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email'), host: ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host])) %></p>
<p><%= t('please_contact_us_by_replying_to_this_email_if_you_didn_t_request_this') %></p>
<p><%= t('please_contact_us_by_replying_to_this_email_if_you_have_any_questions') %></p>
<p>
<%= t('thanks') %>,<br><%= @current_account.name %>
</p>
+1 -1
View File
@@ -23,7 +23,7 @@
</div>
</span>
</div>
<input id="file_dropzone_input" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="image/*, application/pdf<%= ', .docx, .doc, .xlsx, .xls, .odt, .rtf' if Docuseal.multitenant? %>" multiple>
<input id="file_dropzone_input" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="image/*, application/pdf, application/zip<%= ", #{Templates::CreateAttachments::DOCUMENT_EXTENSIONS.join(', ')}" if Docuseal.advanced_formats? %>" multiple>
</div>
</label>
</file-dropzone>
+1 -1
View File
@@ -14,6 +14,6 @@
</span>
</button>
<input type="hidden" name="form_id" value="<%= form_id %>">
<input id="upload_template" name="files[]" class="hidden" onchange="this.form.requestSubmit()" type="file" accept="image/*, application/pdf<%= ', .docx, .doc, .xlsx, .xls, .odt, .rtf' if Docuseal.advanced_formats? %>" multiple>
<input id="upload_template" name="files[]" class="hidden" onchange="this.form.requestSubmit()" type="file" accept="image/*, application/pdf, application/zip<%= ", #{Templates::CreateAttachments::DOCUMENT_EXTENSIONS.join(', ')}" if Docuseal.advanced_formats? %>" multiple>
<input hidden name="folder_name" value="<%= local_assigns[:folder_name] %>">
<% end %>
+6 -10
View File
@@ -3,27 +3,23 @@
<% if with_subfolder %>
<%= form_for '', url: template_folder_path(@template), method: :put, data: { turbo_frame: :_top }, html: { id: 'subfolder_form', autocomplete: :off } do |f| %>
<%= f.hidden_field :parent_name, value: @template.folder.parent_folder&.name || @template.folder.name %>
<toggle-visible data-element-ids="<%= %w[folder_form subfolder_form].to_json %>" class="block relative">
<toggle-visible data-element-ids="<%= %w[folder_form subfolder_form].to_json %>" class="block relative" data-focus-id="name">
<div class="flex items-center justify-between mb-2.5">
<label for="is_root_folder" class="flex items-center">
<label for="is_root_folder" class="flex items-center tooltip tooltip-right pr-2 group" data-tip="<%= t('change_parent_folder') %>">
<%= check_box_tag :is_root_folder, 'folder_form', data: { action: 'change:toggle-visible#trigger' }, class: 'hidden' %>
<span class="flex items-center space-x-0.5 mt-1 peer">
<%= svg_icon('folder', class: 'w-6 h-5 flex-shrink-0') %>
<span class="flex items-center mt-1">
<%= svg_icon('folder', class: 'w-5 h-5 flex-shrink-0 group-hover:hidden mr-1') %>
<%= svg_icon('folder_share', class: 'w-5 h-5 flex-shrink-0 hidden group-hover:inline mr-1') %>
<span class="text-md">
<%= @template.folder.parent_folder&.name || @template.folder.name %>
</span>
</span>
<span class="pl-1 tooltip tooltip-right md:opacity-0 hover:opacity-100 peer-hover:opacity-100" data-tip="<%= t('change_parent_folder') %>">
<span href="<%= edit_template_folder_path(@template.id) %>" data-turbo-frame="modal">
<%= svg_icon('pencil', class: 'w-5 h-5') %>
</span>
</span>
</label>
</div>
</toggle-visible>
<div class="form-control mb-6">
<folder-autocomplete class="block" data-submit-on-select="true" data-parent-name="<%= @template.folder.parent_folder&.name || @template.folder.name %>" data-enabled="<%= params[:autocomplete] != 'false' %>">
<%= f.text_field :name, required: true, placeholder: "#{t('new_subfolder_name')}...", class: 'base-input w-full', autofocus: true %>
<%= f.text_field :name, required: true, placeholder: "#{t('new_subfolder_name')}...", class: 'base-input w-full', autofocus: true, id: 'subfolder_name' %>
</folder-autocomplete>
</div>
<div class="form-control">
@@ -1,4 +1,5 @@
<% close_on_submit = local_assigns.fetch(:close_on_submit, true) %>
<% is_order_set = template.submitters.any? { |s| s['order'] } %>
<%= form_for template, url: template_recipients_path(template), method: :post, html: { autocomplete: 'off', class: 'mt-1', id: :submitters_form }, data: { close_on_submit: } do |f| %>
<% unless close_on_submit %>
<toggle-on-submit data-element-id="form_saved_alert"></toggle-on-submit>
@@ -6,11 +7,24 @@
<div class="space-y-3 divide-y">
<% template.submitters.each_with_index do |submitter, index| %>
<div class="<%= 'pt-3' if index.positive? %>">
<%= f.fields_for :submitters, item = Struct.new(:name, :uuid, :is_requester, :email, :invite_by_uuid, :optional_invite_by_uuid, :linked_to_uuid, :option).new(*submitter.values_at('name', 'uuid', 'is_requester', 'email', 'invite_by_uuid', 'optional_invite_by_uuid', 'linked_to_uuid')), index: do |ff| %>
<%= f.fields_for :submitters, item = Struct.new(:name, :uuid, :is_requester, :email, :invite_by_uuid, :optional_invite_by_uuid, :linked_to_uuid, :order, :option).new(*submitter.values_at('name', 'uuid', 'is_requester', 'email', 'invite_by_uuid', 'optional_invite_by_uuid', 'linked_to_uuid', 'order')), index: do |ff| %>
<% item.option = item.is_requester.present? ? 'is_requester' : (item.email.present? ? 'email' : (item.linked_to_uuid.present? ? "linked_to_#{item.linked_to_uuid}" : (item.invite_by_uuid.present? ? "invite_by_#{item.invite_by_uuid}" : (item.optional_invite_by_uuid.present? ? "optional_invite_by_#{item.optional_invite_by_uuid}" : '')))) %>
<%= ff.hidden_field :uuid %>
<div class="form-control">
<%= ff.text_field :name, class: 'w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer mb-2', autocomplete: 'off', placeholder: "#{index + 1}#{(index + 1).ordinal} Party", required: true %>
<div class="flex justify-between">
<%= ff.text_field :name, class: 'w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer mb-2', autocomplete: 'off', placeholder: "#{index + 1}#{(index + 1).ordinal} Party", required: true %>
<% if template.submitters.size > 2 %>
<div id="order_<%= submitter['uuid'] %>" class="mr-0.5">
<% if is_order_set %>
<%= ff.select :order, options_for_select(template.submitters.map.with_index { |_, i| [(i + 1).ordinalize, i] }, submitter['order'].presence || index), {}, class: 'select select-xs text-sm input-bordered bg-white pl-3.5' %>
<% elsif index == 0 %>
<mount-on-click data-template-id="order_fields" class="link whitespace-nowrap text-sm mt-1 mr-1 block">
<%= t('edit_order') %>
</mount-on-click>
<% end %>
</div>
<% end %>
</div>
<% if template.submitters.size == 2 %>
<%= tag.input name: ff.field_name(:email), value: ff.object.email, type: :email, class: 'base-input', multiple: true, autocomplete: 'off', placeholder: t('default_email'), disabled: ff.object.is_requester || ff.object.invite_by_uuid.present? || ff.object.optional_invite_by_uuid.present?, id: field_uuid = SecureRandom.uuid %>
<% else %>
@@ -85,3 +99,16 @@
</div>
<% end %>
</div>
<% if template.submitters.size > 2 && !is_order_set %>
<template id="order_fields">
<% template.submitters.each_with_index do |submitter, index| %>
<turbo-stream action="replace" target="order_<%= submitter['uuid'] %>">
<template>
<div id="order_<%= submitter['uuid'] %>">
<%= select_tag "template[submitters][#{index}][order]", options_for_select(template.submitters.map.with_index { |_, i| [(i + 1).ordinalize, i] }, submitter['order'].presence || index), class: 'select select-xs text-sm input-bordered bg-white pl-3.5' %>
</div>
</template>
</turbo-stream>
<% end %>
</template>
<% end %>
+10 -1
View File
@@ -59,6 +59,7 @@
<%= t('form_preferences') %>
</div>
<div class="collapse-content">
<%= render 'templates_prefillable_fields/form', template: @template %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %>
<toggle-on-submit data-element-id="form_saved_alert"></toggle-on-submit>
<% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY).value %>
@@ -68,7 +69,7 @@
<%= ff.url_field :completed_redirect_url, required: false, class: 'base-input', dir: 'auto' %>
</div>
<%= ff.fields_for :completed_message, ff.object.completed_message do |fff| %>
<div class="form-control mb-2 border-t mt-4">
<div class="form-control mb-2 mt-4">
<%= fff.label :body, t('completion_message'), class: 'label' %>
<autoresize-textarea>
<%= fff.text_area :body, required: false, class: 'base-input w-full py-2', dir: 'auto' %>
@@ -162,6 +163,14 @@
<% end %>
</div>
<% end %>
<%= f.fields_for :preferences, Struct.new(:request_email_enabled).new(@template.preferences['request_email_enabled']) do |ff| %>
<div class="flex items-center justify-between py-2.5 px-1 mb-2">
<span>
<%= 'Send signature request email' %>
</span>
<%= ff.check_box :request_email_enabled, { checked: ff.object.request_email_enabled != false, class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %>
</div>
<% end %>
<div class="form-control pt-2">
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
<div class="flex justify-center">
@@ -0,0 +1,16 @@
<% select_fields = template.fields.filter_map { |f| [f['name'], f['uuid']] if f['name'].present? && f['type'].in?(TemplatesPrefillableFieldsController::PREFILLABLE_FIELD_TYPES) } %>
<% if select_fields.present? %>
<div class="border-b pb-4 mb-4">
<%= form_for '', url: template_prefillable_fields_path(template), method: :post, data: { close_on_submit: false } do |f| %>
<div class="form-control">
<%= f.hidden_field :prefillable, value: 'true' %>
<%= f.label :field_uuid, t(:invite_form_fields), class: 'label' %>
<div class="join w-full">
<%= select_tag :field_uuid, options_for_select(select_fields), prompt: t(:select_field), class: 'base-select w-full join-item', dir: 'auto', required: true %>
<%= f.button button_title(title: t('add'), disabled_with: t('add')), class: 'base-button join-item !px-6' %>
</div>
</div>
<% end %>
<%= render partial: 'templates_prefillable_fields/list', locals: { template: } %>
</div>
<% end %>
@@ -0,0 +1,6 @@
<div id="prefillable_fields_list" class="flex gap-1 px-0.5 mt-2 flex-wrap">
<% template.fields.each do |f| %>
<% next unless f['prefillable'] %>
<%= button_to button_title(title: f['name'].presence || f['type'].capitalize, disabled_with: f['name'].presence || f['type'].capitalize, icon: svg_icon('x', class: 'w-4 h-4'), icon_disabled: svg_icon('loader', class: 'w-4 h-4 animate-spin')), template_prefillable_fields_path(template), params: { field_uuid: f['uuid'], prefillable: 'false' }, class: 'badge badge-lg badge-primary space-x-1 pr-3 pl-2', form: { data: { close_on_submit: false } } %>
<% end %>
</div>
+46 -27
View File
@@ -1,3 +1,5 @@
<% multiple_submitters = @template.submitters.to_a.length > 1 %>
<% enough_defined_submitters = Templates.filter_undefined_submitters(@template.submitters).size < 2 %>
<%= render 'shared/turbo_modal_large', title: t('share_link') do %>
<div class="mt-2 mb-4 px-5">
<%= form_for @template, url: template_share_link_path(@template), method: :post, html: { id: 'shared_link_form', autocomplete: 'off', class: 'mt-3' }, data: { close_on_submit: false } do |f| %>
@@ -12,28 +14,14 @@
</check-on-click>
</div>
<% end %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-4 mb-5' }, data: { close_on_submit: false } do |f| %>
<% link_form_fields = @template.preferences.fetch('link_form_fields', ['email']) %>
<label class="pl-1"><%= t('link_form_fields') %></label>
<required-checkbox-group class="flex flex-col md:flex-row items-center gap-2 w-full mt-2">
<% %w[name email phone].each do |field| %>
<%= label_tag "link_form_fields_#{field}", t(field), class: 'relative flex w-full md:w-1/3 items-center h-12 border-base-300 py-3.5 border rounded-xl' do %>
<%= check_box_tag 'template[preferences][link_form_fields][]', field, link_form_fields.include?(field), class: 'absolute !animate-none checkbox left-3', id: "link_form_fields_#{field}" %>
<span class="font-medium w-full text-center"><%= t(field) %></span>
<% end %>
<% end %>
</required-checkbox-group>
<% end %>
<% if Templates.filter_undefined_submitters(@template.submitters).size > 1 %>
<% if multiple_submitters && !enough_defined_submitters %>
<div class="alert items-start bg-base-100 border-base-300 mt-4">
<%= svg_icon('info_circle', class: 'stroke-current shrink-0 h-6 w-6 mt-1') %>
<div><%= t('this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link') %></div>
</div>
<% end %>
<% if @template.submitters.to_a.length > 1 %>
<div class="collapse collapse-arrow join-item border border-base-300 mt-4 mb-5">
<input type="checkbox" name="accordion">
<div class="collapse-title text-xl font-medium">
<div class="collapse collapse-arrow join-item border border-base-300 mt-4">
<input type="checkbox" name="accordion" class="min-h-0">
<div class="collapse-title py-4 min-h-0">
<%= t('default_parties') %>
</div>
<div class="collapse-content !pb-0">
@@ -41,15 +29,46 @@
</div>
</div>
<% end %>
<% if Docuseal.multitenant? || Accounts.can_send_emails?(current_account) %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-3' }, data: { close_on_submit: false } do |f| %>
<%= f.fields_for :preferences, Struct.new(:shared_link_2fa).new(@template.preferences['shared_link_2fa'] == true) do |ff| %>
<label for="template_preferences_shared_link_2fa" class="flex items-center my-4 justify-between gap-1 alert bg-base-100 border-base-300">
<span><%= t('request_email_otp_verification_with_shared_link') %></span>
<%= ff.check_box :shared_link_2fa, { checked: ff.object.shared_link_2fa == true, disabled: !can?(:update, @template), class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %>
</label>
<div class="collapse collapse-arrow join-item mt-4">
<input id="accordion_checkbox" type="checkbox" name="accordion" class="min-h-0">
<div class="collapse-title min-h-0 after:!right-3 p-1">
<%= t('advanced_settings') %>
</div>
<div class="collapse-content !p-0">
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-4 pt-4 border-t border-base-300' }, data: { close_on_submit: false } do |f| %>
<% link_form_fields = @template.preferences.fetch('link_form_fields', ['email']) %>
<label class="pl-1"><%= t('link_form_fields') %></label>
<required-checkbox-group class="flex flex-col md:flex-row items-center gap-2 w-full mt-2">
<% %w[name email phone].each do |field| %>
<%= label_tag "link_form_fields_#{field}", t(field), class: 'relative flex w-full md:w-1/3 items-center h-14 border-base-300 py-3.5 border rounded-xl' do %>
<%= check_box_tag 'template[preferences][link_form_fields][]', field, link_form_fields.include?(field), class: 'absolute !animate-none checkbox left-3', id: "link_form_fields_#{field}" %>
<span class="font-medium w-full text-center"><%= t(field) %></span>
<% end %>
<% end %>
</required-checkbox-group>
<% end %>
<% end %>
<% end %>
<% if multiple_submitters && enough_defined_submitters %>
<div class="collapse collapse-arrow join-item border border-base-300 mt-4">
<input type="checkbox" name="accordion" class="min-h-0">
<div class="collapse-title py-4 min-h-0">
<%= t('default_parties') %>
</div>
<div class="collapse-content !pb-0">
<%= render 'templates_preferences/recipients', template: @template, close_on_submit: false, with_toggles: false, with_submission_requester: false %>
</div>
</div>
<% end %>
<% if Docuseal.multitenant? || Accounts.can_send_emails?(current_account) %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-4' }, data: { close_on_submit: false } do |f| %>
<%= f.fields_for :preferences, Struct.new(:shared_link_2fa).new(@template.preferences['shared_link_2fa'] == true) do |ff| %>
<label for="template_preferences_shared_link_2fa" class="flex items-center mt-4 h-14 justify-between gap-1 alert bg-base-100 border-base-300">
<span><%= t('request_email_otp_verification_with_shared_link') %></span>
<%= ff.check_box :shared_link_2fa, { checked: ff.object.shared_link_2fa == true, disabled: !can?(:update, @template), class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %>
</label>
<% end %>
<% end %>
<% end %>
</div>
</div>
</div>
<% end %>
@@ -1,7 +1,7 @@
<p><%= @user.first_name.present? ? t('hello_name', name: @user.first_name) : t('hi_there') %>,</p>
<p><%= t('you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_', account_name: @user.account.name, product_name: Docuseal.product_name) %></p>
<p><%= link_to t('sign_up'), invitation_url(reset_password_token: @token) %></p>
<p><%= t('please_contact_us_by_replying_to_this_email_if_you_didn_t_request_this') %></p>
<p><%= t('please_contact_us_by_replying_to_this_email_if_you_have_any_questions') %></p>
<p>
<%= t('thanks') %>,<br><%= @current_account.name %>
</p>
+1 -1
View File
@@ -321,7 +321,7 @@ Devise.setup do |config|
# apps is `200 OK` and `302 Found respectively`, but new apps are generated with
# these new defaults that match Hotwire/Turbo behavior.
# Note: These might become the new default in future versions of Devise.
config.responder.error_status = :unprocessable_entity
config.responder.error_status = :unprocessable_content
config.responder.redirect_status = :see_other
# ==> Configuration for :registerable
+119 -55
View File
@@ -23,6 +23,10 @@ en: &en
pro: Pro
thanks: Thanks
private: Private
select: Select
party: Party
edit_order: Edit Order
invite_form_fields: Invite form fields
default_parties: Default parties
authenticate_embedded_form_preview_with_token: Authenticate embedded form preview with token
stripe_integration: Stripe Integration
@@ -42,7 +46,6 @@ en: &en
send_verification_code: Send verification code
code_has_been_resent: Code has been re-sent
invalid_code: Invalid code
unarchive: Unarchive
signed: Signed
first_party: 'First Party'
remove_filter: Remove filter
@@ -56,7 +59,6 @@ en: &en
accessiable_by: Accessible by
team_access: Team access
document_download_filename_format: Document download filename format
document_name: Document Name
docuseal_trusted_signature: DocuSeal Trusted Signature
hello_name: Hello %{name}
you_are_invited_to_product_name: You are invited to %{product_name}
@@ -73,7 +75,7 @@ en: &en
awaiting_completion_by_the_other_party: "Awaiting completion by the other party"
review_and_sign: Review and Sign
review_and_submit: Review and Submit
please_contact_us_by_replying_to_this_email_if_you_didn_t_request_this: "Please contact us by replying to this email if you have any questions."
please_contact_us_by_replying_to_this_email_if_you_have_any_questions: "Please contact us by replying to this email if you have any questions."
submitter_invitation_sms_body_sign: '{account.name} has invited you to sign a document: {submitter.link}'
verification_code_sms_body: 'Verification code: {code}'
you_are_invited_to_submit_a_form: 'You are invited to submit a form'
@@ -398,7 +400,6 @@ en: &en
send_signature_request_emails_without_limits_with_docuseal_pro: Send signature request emails without limits with DocuSeal Pro
count_emails_used: '%{count} emails used'
has_been_connected: has been connected
but_not_activated: but not activated
sms_not_configured: SMS not Configure
configure_sms_settings_in_order_to_send_text_messages_: 'Configure SMS settings in order to send text messages:'
go_to_sms_settings: Go to SMS settings
@@ -496,7 +497,6 @@ en: &en
edit_user: Edit User
new_user: New User
unarchive: Unarchive
role: Role
last_session: Last session
unlock_more_user_roles_with_docuseal_pro: Unlock more user roles with DocuSeal Pro.
view_active: View Active
@@ -563,8 +563,6 @@ en: &en
new_webhook: New Webhook
delete_webhook: Delete webhook
count_submissions_have_been_created: '%{count} submissions have been created.'
gmail_has_been_connected: Gmail has been connected
microsoft_account_has_been_connected: Microsoft Account has been connected
sms_length_cant_be_longer_than_120_bytes: SMS length can't be longer than 120 bytes
connected_successfully: Connected successfully.
user_nameid_not_found: 'User %{nameid} not found.'
@@ -783,6 +781,19 @@ en: &en
email_verification: Email verification
your_verification_code_to_access_the_name: 'Your verification code to access the "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Please reply to this email if you didn't request this.
advanced_settings: Advanced settings
text: Text
code: Code
custom_html_emails: Custom HTML emails
connect_your_email_to_send_html_emails: Connect your email to send HTML emails
connect_your_email_or_outlook_account_or_add_smtp_settings_to_send_custom_html_emails: Connect your Gmail or Outlook account or add SMTP settings to send custom HTML emails.
connect_gmail_or_outlook: Connect Gmail or Outlook
connect_your_email_to_bulk_send: Connect your email to bulk send
connect_your_email_or_outlook_account_or_add_smtp_settings_to_bulk_send: Connect your Gmail or Outlook account or add SMTP settings to bulk send.
are_you_sure_you_want_to_add_recipients_without_sending_to_send_emails_it_requires_to_connect_gmail_or_outlook: Are you sure you want to add recipients without sending? To send emails it requires to connect Gmail or Outlook.
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" has been completed by <strong>{submission.submitters}</strong>'
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Please check the copy of your "<strong>{template.name}</strong>" in the email attachments.'
you_have_been_invited_to_sign_the_template_name_html: 'You have been invited to sign the "<strong>{template.name}</strong>".'
submission_sources:
api: API
bulk: Bulk Send
@@ -889,6 +900,10 @@ en: &en
range_without_total: "%{from}-%{to} events"
es: &es
party: Parte
edit_order: Edita Pedido
select: Seleccionar
invite_form_fields: Invitar campos del formulario
pro: Pro
default_parties: Partes predeterminadas
authenticate_embedded_form_preview_with_token: Autenticar vista previa del formulario incrustado con token
@@ -922,8 +937,6 @@ es: &es
team_access: Acceso del equipo
remove_filter: Eliminar filtro
document_download_filename_format: Formato del nombre del archivo de descarga del documento
document_name: Nombre del documento
unarchive: Desarchivar
awaiting_completion_by_the_other_party: "Esperando la finalización por la otra parte"
enforce_recipients_order: 'Hacer cumplir el orden de los destinatarios'
first_party: 'Primera Parte'
@@ -944,7 +957,7 @@ es: &es
please_check_the_copy_of_your_name_in_the_email_attachments: 'Por favor, revisa la copia de tu "%{name}" en los archivos adjuntos del correo electrónico.'
review_and_sign: Revisar y Firmar
review_and_submit: Revisar y Enviar
please_contact_us_by_replying_to_this_email_if_you_didn_t_request_this: "Por favor, contáctanos respondiendo a este correo si tienes alguna pregunta."
please_contact_us_by_replying_to_this_email_if_you_have_any_questions: "Por favor, contáctanos respondiendo a este correo si tienes alguna pregunta."
submitter_invitation_sms_body_sign: '{account.name} te ha invitado a firmar un documento: {submitter.link}'
verification_code_sms_body: 'Código de verificación: {code}'
you_are_invited_to_submit_a_form: 'Estás invitado/a a enviar un formulario'
@@ -973,7 +986,7 @@ es: &es
submitter_documents_copy_email_body: |
Hola,
Por favor, revisa la copia de tu "{template.name}" en los archivos adjuntos.
Por favor, revisa la copia de tu "{template.name}" en los archivos adjuntos del correo electrónico.
Alternativamente, puedes revisar y descargar tu copia usando el enlace a continuación:
[{template.name}]({documents.link})
@@ -1268,7 +1281,6 @@ es: &es
send_signature_request_emails_without_limits_with_docuseal_pro: Envía solicitudes de firma sin límites con DocuSeal Pro
count_emails_used: '%{count} correos electrónicos utilizados'
has_been_connected: ha sido conectado
but_not_activated: pero no activado
sms_not_configured: SMS no configurado
configure_sms_settings_in_order_to_send_text_messages_: 'Configura los ajustes de SMS para enviar mensajes de texto:'
go_to_sms_settings: Ir a la configuración de SMS
@@ -1366,7 +1378,6 @@ es: &es
edit_user: Editar usuario
new_user: Nuevo usuario
unarchive: Desarchivar
role: Rol
last_session: Última sesión
unlock_more_user_roles_with_docuseal_pro: Desbloquea más roles de usuario con DocuSeal Pro.
view_active: Ver activos
@@ -1433,8 +1444,6 @@ es: &es
new_webhook: Nuevo Webhook
delete_webhook: Eliminar webhook
count_submissions_have_been_created: '%{count} envíos han sido creados.'
gmail_has_been_connected: Gmail ha sido conectado.
microsoft_account_has_been_connected: La cuenta de Microsoft ha sido conectada.
sms_length_cant_be_longer_than_120_bytes: La longitud del SMS no puede ser mayor a 120 bytes.
connected_successfully: Conectado con éxito.
user_nameid_not_found: 'Usuario %{nameid} no encontrado.'
@@ -1527,7 +1536,6 @@ es: &es
select_field: Campo de Selección
multiple_field: Campo Múltiple
radio_field: Campo de Grupo Radio
radio_field: Campo de Radio
cells_field: Campo de Celdas
stamp_field: Campo de Sello
payment_field: Campo de Pago
@@ -1653,6 +1661,19 @@ es: &es
email_verification: Verificación por correo electrónico
your_verification_code_to_access_the_name: 'Su código de verificación para acceder a "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Por favor, responda este correo si no solicitó esto.
advanced_settings: Configuración avanzada
text: Texto
code: Código
custom_html_emails: Correos electrónicos HTML personalizados
connect_your_email_to_send_html_emails: Conecta tu correo electrónico para enviar correos HTML
connect_your_email_or_outlook_account_or_add_smtp_settings_to_send_custom_html_emails: Conecta tu cuenta de Gmail u Outlook o añade ajustes SMTP para enviar correos HTML personalizados.
connect_gmail_or_outlook: Conectar Gmail u Outlook
connect_your_email_to_bulk_send: Conecta tu correo electrónico para el envío masivo
connect_your_email_or_outlook_account_or_add_smtp_settings_to_bulk_send: Conecta tu cuenta de Gmail u Outlook o añade ajustes SMTP para el envío masivo.
are_you_sure_you_want_to_add_recipients_without_sending_to_send_emails_it_requires_to_connect_gmail_or_outlook: ¿Estás seguro de que quieres añadir destinatarios sin enviar? Para enviar correos electrónicos necesitas conectar Gmail u Outlook.
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" ha sido completado por <strong>{submission.submitters}</strong>'
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Por favor, revisa la copia de tu "<strong>{template.name}</strong>" en los archivos adjuntos del correo electrónico.'
you_have_been_invited_to_sign_the_template_name_html: 'Has sido invitado a firmar el "<strong>{template.name}</strong>".'
submission_sources:
api: API
bulk: Envío masivo
@@ -1759,6 +1780,10 @@ es: &es
range_without_total: "%{from}-%{to} eventos"
it: &it
party: Parte
edit_order: Modifica Ordine
select: Seleziona
invite_form_fields: Invita campi modulo
pro: Pro
default_parties: Parti predefiniti
authenticate_embedded_form_preview_with_token: "Autentica l'anteprima del modulo incorporato con il token"
@@ -1791,8 +1816,6 @@ it: &it
team_access: Accesso al team
remove_filter: Rimuovi filtro
document_download_filename_format: Formato del nome file scaricato
document_name: Nome del Documento
unarchive: Ripristina
awaiting_completion_by_the_other_party: "In attesa di completamento da parte dell'altra parte"
enforce_recipients_order: "Applicare l'ordine dei destinatari"
first_party: 'Prima parte'
@@ -1813,7 +1836,7 @@ it: &it
please_check_the_copy_of_your_name_in_the_email_attachments: "Per favore, controlla la copia del tuo \"%{name}\" negli allegati dell'email."
review_and_sign: Rivedi e Firma
review_and_submit: Rivedi e Invia
please_contact_us_by_replying_to_this_email_if_you_didn_t_request_this: "Per favore, contattaci rispondendo a questa email se hai domande."
please_contact_us_by_replying_to_this_email_if_you_have_any_questions: "Per favore, contattaci rispondendo a questa email se hai domande."
submitter_invitation_sms_body_sign: '{account.name} ti ha invitato a firmare un documento: {submitter.link}'
verification_code_sms_body: 'Codice di verifica: {code}'
you_are_invited_to_submit_a_form: 'Sei stato invitato a inviare un modulo'
@@ -2137,7 +2160,6 @@ it: &it
send_signature_request_emails_without_limits_with_docuseal_pro: Invia email di richiesta di firma senza limiti con DocuSeal Pro
count_emails_used: '%{count} email utilizzate'
has_been_connected: è stato connesso
but_not_activated: ma non attivato
sms_not_configured: SMS non configurato
configure_sms_settings_in_order_to_send_text_messages_: 'Configura le impostazioni SMS per inviare messaggi di testo:'
go_to_sms_settings: Vai alle impostazioni SMS
@@ -2234,7 +2256,6 @@ it: &it
edit_user: Modifica utente
new_user: Nuovo utente
unarchive: Disarchivia
role: Ruolo
last_session: Ultima sessione
unlock_more_user_roles_with_docuseal_pro: Sblocca più ruoli utente con DocuSeal Pro.
view_active: Visualizza attivi
@@ -2301,8 +2322,6 @@ it: &it
new_webhook: Nuovo Webhook
delete_webhook: Elimina webhook
count_submissions_have_been_created: '%{count} invii sono stati creati.'
gmail_has_been_connected: Gmail è stato connesso.
microsoft_account_has_been_connected: "L'account Microsoft è stato connesso."
sms_length_cant_be_longer_than_120_bytes: "La lunghezza dell'SMS non può superare i 120 byte."
connected_successfully: Collegamento avvenuto con successo.
user_nameid_not_found: 'Utente %{nameid} non trovato.'
@@ -2522,6 +2541,19 @@ it: &it
email_verification: Verifica email
your_verification_code_to_access_the_name: 'Il tuo codice per accedere a "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Rispondi a questa email se non hai richiesto questo.
advanced_settings: Impostazioni avanzate
text: Testo
code: Codice
custom_html_emails: Email HTML personalizzate
connect_your_email_to_send_html_emails: Collega la tua email per inviare email HTML
connect_your_email_or_outlook_account_or_add_smtp_settings_to_send_custom_html_emails: Collega il tuo account Gmail o Outlook oppure aggiungi impostazioni SMTP per inviare email HTML personalizzate.
connect_gmail_or_outlook: Collega Gmail o Outlook
connect_your_email_to_bulk_send: Collega la tua email per invio multiplo
connect_your_email_or_outlook_account_or_add_smtp_settings_to_bulk_send: Collega il tuo account Gmail o Outlook oppure aggiungi impostazioni SMTP per invio multiplo.
are_you_sure_you_want_to_add_recipients_without_sending_to_send_emails_it_requires_to_connect_gmail_or_outlook: Sei sicuro di voler aggiungere destinatari senza inviare? Per inviare email devi collegare Gmail o Outlook.
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" è stato completato da <strong>{submission.submitters}</strong>'
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Per favore, controlla la copia del tuo "<strong>{template.name}</strong>" negli allegati dell''email.'
you_have_been_invited_to_sign_the_template_name_html: 'Sei stato invitato a firmare il "<strong>{template.name}</strong>".'
submission_sources:
api: API
bulk: Invio massivo
@@ -2628,6 +2660,10 @@ it: &it
range_without_total: "%{from}-%{to} eventi"
fr: &fr
party: Partie
edit_order: Modifier la commande
select: Sélectionner
invite_form_fields: Inviter des champs de formulaire
pro: Pro
default_parties: Parties par défaut
authenticate_embedded_form_preview_with_token: Authentifier laperçu du formulaire intégré avec un jeton
@@ -2661,8 +2697,6 @@ fr: &fr
team_access: Accès à l'équipe
remove_filter: Supprimer le filtre
document_download_filename_format: Format du nom de fichier du téléchargement de document
document_name: Nom du document
unarchive: Désarchiver
awaiting_completion_by_the_other_party: "En attente de la complétion par l'autre partie"
enforce_recipients_order: "Respecter l'ordre des destinataires"
first_party: 'Première partie'
@@ -2683,7 +2717,7 @@ fr: &fr
please_check_the_copy_of_your_name_in_the_email_attachments: "Veuillez vérifier la copie de votre \"%{name}\" dans les pièces jointes de l'email."
review_and_sign: Consulter et signer
review_and_submit: Consulter et soumettre
please_contact_us_by_replying_to_this_email_if_you_didn_t_request_this: 'Veuillez nous contacter en répondant à cet e-mail si vous avez des questions.'
please_contact_us_by_replying_to_this_email_if_you_have_any_questions: 'Veuillez nous contacter en répondant à cet e-mail si vous avez des questions.'
submitter_invitation_sms_body_sign: '{account.name} vous a invité à signer un document: {submitter.link}'
verification_code_sms_body: 'Code de vérification: {code}'
you_are_invited_to_submit_a_form: Vous êtes invité à soumettre un formulaire
@@ -2699,7 +2733,7 @@ fr: &fr
[Consulter et signer]({submitter.link})
Veuillez nous contacter si vous avez des questions.
Veuillez nous contacter en répondant à cet e-mail si vous avez des questions.
Merci,
{account.name}
@@ -2712,7 +2746,7 @@ fr: &fr
submitter_documents_copy_email_body: |
Bonjour,
Veuillez vérifier la copie de votre "{template.name}" dans les pièces jointes.
Veuillez vérifier la copie de votre "{template.name}" dans les pièces jointes de l'email.
Vous pouvez également consulter et télécharger votre copie en utilisant le lien ci-dessous:
[{template.name}]({documents.link})
@@ -3008,7 +3042,6 @@ fr: &fr
send_signature_request_emails_without_limits_with_docuseal_pro: Envoyez des e-mails de demande de signature sans limites avec DocuSeal Pro
count_emails_used: '%{count} e-mails utilisés'
has_been_connected: a été connecté
but_not_activated: mais pas activé
sms_not_configured: SMS non configuré
configure_sms_settings_in_order_to_send_text_messages_: 'Configurez les paramètres SMS pour envoyer des messages texte'
go_to_sms_settings: Aller aux paramètres SMS
@@ -3106,7 +3139,6 @@ fr: &fr
edit_user: "Modifier l'utilisateur"
new_user: Nouvel utilisateur
unarchive: Désarchiver
role: Rôle
last_session: Dernière session
unlock_more_user_roles_with_docuseal_pro: Déverrouillez plus de rôles d'utilisateurs avec DocuSeal Pro.
view_active: Voir les actifs
@@ -3173,8 +3205,6 @@ fr: &fr
new_webhook: Nouveau Webhook
delete_webhook: Supprimer le webhook
count_submissions_have_been_created: '%{count} soumissions ont été créées.'
gmail_has_been_connected: Gmail a été connecté.
microsoft_account_has_been_connected: Le compte Microsoft a été connecté.
sms_length_cant_be_longer_than_120_bytes: La longueur du SMS ne peut pas dépasser 120 octets.
connected_successfully: Connecté avec succès.
user_nameid_not_found: 'Utilisateur %{nameid} introuvable.'
@@ -3394,6 +3424,19 @@ fr: &fr
email_verification: Vérification de l'email
your_verification_code_to_access_the_name: 'Votre code pour accéder à "%{name}" :'
please_reply_to_this_email_if_you_didnt_request_this: Veuillez répondre à cet email si vous n'avez pas fait cette demande.
advanced_settings: Paramètres avancés
text: Texte
code: Code
custom_html_emails: E-mails HTML personnalisés
connect_your_email_to_send_html_emails: Connectez votre e-mail pour envoyer des e-mails HTML
connect_your_email_or_outlook_account_or_add_smtp_settings_to_send_custom_html_emails: Connectez votre compte Gmail ou Outlook ou ajoutez des paramètres SMTP pour envoyer des e-mails HTML personnalisés.
connect_gmail_or_outlook: Connecter Gmail ou Outlook
connect_your_email_to_bulk_send: Connectez votre e-mail pour l'envoi groupé
connect_your_email_or_outlook_account_or_add_smtp_settings_to_bulk_send: Connectez votre compte Gmail ou Outlook ou ajoutez des paramètres SMTP pour l'envoi groupé.
are_you_sure_you_want_to_add_recipients_without_sending_to_send_emails_it_requires_to_connect_gmail_or_outlook: Êtes-vous sûr de vouloir ajouter des destinataires sans envoi ? Pour envoyer des e-mails, vous devez connecter Gmail ou Outlook.
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" a été complété par <strong>{submission.submitters}</strong>'
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Veuillez vérifier la copie de votre "<strong>{template.name}</strong>" dans les pièces jointes de le-mail.'
you_have_been_invited_to_sign_the_template_name_html: 'Vous avez été invité à signer le "<strong>{template.name}</strong>".'
submission_sources:
api: API
bulk: Envoi en masse
@@ -3500,6 +3543,10 @@ fr: &fr
range_without_total: "%{from} à %{to} événements"
pt: &pt
party: Parte
edit_order: Edita Pedido
select: Selecionar
invite_form_fields: Convidar campos do formulário
pro: Pro
default_parties: Partes padrão
authenticate_embedded_form_preview_with_token: Autenticar visualização incorporada do formulário com token
@@ -3533,8 +3580,6 @@ pt: &pt
team_access: Acesso à equipe
remove_filter: Remover filtro
document_download_filename_format: Formato do nome do arquivo de download do documento
document_name: Nome do documento
unarchive: Desarquivar
awaiting_completion_by_the_other_party: "Aguardando a conclusão pela outra parte"
enforce_recipients_order: 'Forçar a ordem dos recipientes'
first_party: 'Primeira Parte'
@@ -3555,7 +3600,7 @@ pt: &pt
please_check_the_copy_of_your_name_in_the_email_attachments: 'Por favor, verifique a cópia de "%{name}" nos anexos do e-mail.'
review_and_sign: Revisar e assinar
review_and_submit: Revisar e submeter
please_contact_us_by_replying_to_this_email_if_you_didn_t_request_this: 'Por favor, entre em contato conosco respondendo a este e-mail se você tiver alguma dúvida.'
please_contact_us_by_replying_to_this_email_if_you_have_any_questions: 'Por favor, entre em contato conosco respondendo a este e-mail se você tiver alguma dúvida.'
submitter_invitation_sms_body_sign: '{account.name} convidou você para assinar um documento: {submitter.link}'
verification_code_sms_body: 'Código de verificação: {code}'
you_are_invited_to_submit_a_form: Você foi convidado a submeter um formulário
@@ -3567,11 +3612,11 @@ pt: &pt
submitter_invitation_email_sign_body: |
Olá,
Você foi convidado a assinar o documento "{template.name}".
Você foi convidado a assinar "{template.name}".
[Revisar e assinar]({submitter.link})
Por favor, entre em contato conosco se tiver alguma dúvida.
Por favor, entre em contato conosco respondendo a este e-mail se você tiver alguma dúvida.
Obrigado,
{account.name}
@@ -3584,8 +3629,8 @@ pt: &pt
submitter_documents_copy_email_body: |
Olá,
Por favor, verifique a cópia de seu "{template.name}" nos anexos do e-mail.
Alternativamente, você pode revisar e baixar sua cópia usando o link abaixo:
Por favor, verifique a cópia de "{template.name}" nos anexos do e-mail.
Você pode revisar e baixar sua cópia usando o link abaixo:
[{template.name}]({documents.link})
@@ -3879,7 +3924,6 @@ pt: &pt
send_signature_request_emails_without_limits_with_docuseal_pro: Enviar e-mails de solicitação de assinatura sem limites com o DocuSeal Pro
count_emails_used: '%{count} e-mails usados'
has_been_connected: foi conectado
but_not_activated: mas não ativado
sms_not_configured: SMS não configurado
configure_sms_settings_in_order_to_send_text_messages_: 'Configure as configurações de SMS para enviar mensagens de texto:'
go_to_sms_settings: Ir para configurações de SMS
@@ -3977,7 +4021,6 @@ pt: &pt
edit_user: Editar usuário
new_user: Novo usuário
unarchive: Desarquivar
role: Função
last_session: Última sessão
unlock_more_user_roles_with_docuseal_pro: Desbloqueie mais funções de usuário com DocuSeal Pro.
view_active: Ver ativos
@@ -4044,8 +4087,6 @@ pt: &pt
new_webhook: Novo Webhook
delete_webhook: Excluir webhook
count_submissions_have_been_created: '%{count} submissões foram criadas.'
gmail_has_been_connected: O Gmail foi conectado
microsoft_account_has_been_connected: A conta da Microsoft foi conectada
sms_length_cant_be_longer_than_120_bytes: O comprimento do SMS não pode ultrapassar 120 bytes
connected_successfully: Conectado com sucesso.
user_nameid_not_found: 'Usuário %{nameid} não encontrado.'
@@ -4264,6 +4305,19 @@ pt: &pt
email_verification: Verificação de e-mail
your_verification_code_to_access_the_name: 'Seu código de verificação para acessar "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Responda a este e-mail se você não solicitou isso.
advanced_settings: Configurações avançadas
text: Texto
code: Código
custom_html_emails: E-mails HTML personalizados
connect_your_email_to_send_html_emails: Conecte seu e-mail para enviar e-mails HTML
connect_your_email_or_outlook_account_or_add_smtp_settings_to_send_custom_html_emails: Conecte sua conta Gmail ou Outlook ou adicione configurações SMTP para enviar e-mails HTML personalizados.
connect_gmail_or_outlook: Conectar Gmail ou Outlook
connect_your_email_to_bulk_send: Conecte seu e-mail para envio em massa
connect_your_email_or_outlook_account_or_add_smtp_settings_to_bulk_send: Conecte sua conta Gmail ou Outlook ou adicione configurações SMTP para envio em massa.
are_you_sure_you_want_to_add_recipients_without_sending_to_send_emails_it_requires_to_connect_gmail_or_outlook: Tem certeza de que deseja adicionar destinatários sem enviar? Para enviar e-mails é necessário conectar Gmail ou Outlook.
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" foi concluído por <strong>{submission.submitters}</strong>'
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Por favor, verifique a cópia do seu "<strong>{template.name}</strong>" nos anexos do e-mail.'
you_have_been_invited_to_sign_the_template_name_html: 'Você foi convidado a assinar o "<strong>{template.name}</strong>".'
submission_sources:
api: API
bulk: Envio em massa
@@ -4344,7 +4398,6 @@ pt: &pt
settings_account_description: 'Com configurações completas de conta, você pode personalizar a experiência de assinatura eletrônica e convidar mais usuários.'
support: 'Suporte'
support_description: 'Você pode usar nosso assistente de IA ou nos escrever para support@docuseal.com se tiver dúvidas.'
settings: 'Configurações'
settings_template_description: 'Use este botão para acessar a página de configurações da sua conta.'
doorkeeper:
scopes:
@@ -4371,6 +4424,10 @@ pt: &pt
range_without_total: "%{from}-%{to} eventos"
de: &de
party: Partei
edit_order: Bestellung bearbeiten
select: Auswählen
invite_form_fields: Formularfelder einladen
pro: Pro
default_parties: Standardparteien
authenticate_embedded_form_preview_with_token: Authentifizieren Sie die eingebettete Formularvorschau mit Token
@@ -4404,8 +4461,6 @@ de: &de
team_access: Teamzugang
remove_filter: Filter entfernen
document_download_filename_format: Format des Dateinamens beim Herunterladen von Dokumenten
document_name: Dokumentname
unarchive: Wiederherstellen
awaiting_completion_by_the_other_party: "Warten auf die Fertigstellung durch die andere Partei"
enforce_recipients_order: 'Empfängerreihenfolge durchsetzen'
first_party: 'Erste Partei'
@@ -4426,7 +4481,7 @@ de: &de
please_check_the_copy_of_your_name_in_the_email_attachments: 'Bitte überprüfe die Kopie von "%{name}" im E-Mail-Anhang.'
review_and_sign: Überprüfen und unterschreiben
review_and_submit: Überprüfen und einreichen
please_contact_us_by_replying_to_this_email_if_you_didn_t_request_this: 'Bitte kontaktiere uns, indem du auf diese E-Mail antwortest, falls du Fragen hast.'
please_contact_us_by_replying_to_this_email_if_you_have_any_questions: 'Bitte kontaktieren Sie uns, indem Sie auf diese E-Mail antworten, falls Sie Fragen haben.'
submitter_invitation_sms_body_sign: '{account.name} hat dich eingeladen, ein Dokument zu unterschreiben: {submitter.link}'
verification_code_sms_body: 'Verifizierungscode: {code}'
you_are_invited_to_submit_a_form: Du wurdest eingeladen, ein Formular einzureichen
@@ -4438,11 +4493,11 @@ de: &de
submitter_invitation_email_sign_body: |
Hallo,
Du wurdest eingeladen, das Dokument "{template.name}" zu unterschreiben.
Du wurdest eingeladen, "{template.name}" zu unterschreiben.
[Überprüfen und unterschreiben]({submitter.link})
Bitte kontaktiere uns, falls du Fragen hast.
Bitte kontaktieren Sie uns, indem Sie auf diese E-Mail antworten, falls Sie Fragen haben.
Danke,
{account.name}
@@ -4455,8 +4510,8 @@ de: &de
submitter_documents_copy_email_body: |
Hallo,
Bitte überprüfe die Kopie deines "{template.name}" im E-Mail-Anhang.
Alternativ kannst du deine Kopie mit dem untenstehenden Link überprüfen und herunterladen:
Bitte überprüfe die Kopie von "{template.name}" im E-Mail-Anhang.
Du kannst alternativ deine Kopie mit dem untenstehenden Link überprüfen und herunterladen:
[{template.name}]({documents.link})
@@ -4750,7 +4805,6 @@ de: &de
send_signature_request_emails_without_limits_with_docuseal_pro: Unterschriftsanfragen ohne Limits mit DocuSeal Pro senden
count_emails_used: '%{count} E-Mails verwendet'
has_been_connected: wurde verbunden
but_not_activated: aber nicht aktiviert
sms_not_configured: SMS nicht konfiguriert
configure_sms_settings_in_order_to_send_text_messages_: 'Konfiguriere die SMS-Einstellungen, um Textnachrichten zu senden:'
go_to_sms_settings: Zu den SMS-Einstellungen gehen
@@ -4848,7 +4902,6 @@ de: &de
edit_user: Benutzer bearbeiten
new_user: Neuer Benutzer
unarchive: Entarchivieren
role: Rolle
last_session: Letzte Sitzung
unlock_more_user_roles_with_docuseal_pro: Weitere Benutzerrollen mit DocuSeal Pro freischalten.
view_active: Aktive anzeigen
@@ -4915,8 +4968,6 @@ de: &de
new_webhook: Neuer Webhook
delete_webhook: Webhook löschen
count_submissions_have_been_created: '%{count} Einreichungen wurden erstellt.'
gmail_has_been_connected: Gmail wurde verbunden.
microsoft_account_has_been_connected: Microsoft-Konto wurde verbunden.
sms_length_cant_be_longer_than_120_bytes: Die SMS-Länge darf 120 Bytes nicht überschreiten.
connected_successfully: Erfolgreich verbunden.
user_nameid_not_found: 'Benutzer %{nameid} nicht gefunden.'
@@ -5135,6 +5186,19 @@ de: &de
email_verification: E-Mail-Verifizierung
your_verification_code_to_access_the_name: 'Ihr Verifizierungscode für den Zugriff auf "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Antworten Sie auf diese E-Mail, wenn Sie dies nicht angefordert haben.
advanced_settings: Erweiterte Einstellungen
text: Text
code: Code
custom_html_emails: Benutzerdefinierte HTML-E-Mails
connect_your_email_to_send_html_emails: Verbinden Sie Ihr E-Mail-Konto, um HTML-E-Mails zu senden
connect_your_email_or_outlook_account_or_add_smtp_settings_to_send_custom_html_emails: Verbinden Sie Ihr Gmail- oder Outlook-Konto oder fügen Sie SMTP-Einstellungen hinzu, um benutzerdefinierte HTML-E-Mails zu senden.
connect_gmail_or_outlook: Gmail oder Outlook verbinden
connect_your_email_to_bulk_send: Verbinden Sie Ihr E-Mail-Konto für Massenversand
connect_your_email_or_outlook_account_or_add_smtp_settings_to_bulk_send: Verbinden Sie Ihr Gmail- oder Outlook-Konto oder fügen Sie SMTP-Einstellungen für den Massenversand hinzu.
are_you_sure_you_want_to_add_recipients_without_sending_to_send_emails_it_requires_to_connect_gmail_or_outlook: Möchten Sie wirklich Empfänger hinzufügen, ohne zu senden? Zum Senden von E-Mails ist eine Verbindung zu Gmail oder Outlook erforderlich.
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" wurde von <strong>{submission.submitters}</strong> abgeschlossen'
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Bitte prüfen Sie die Kopie Ihres "<strong>{template.name}</strong>" in den E-Mail-Anhängen.'
you_have_been_invited_to_sign_the_template_name_html: 'Du wurdest eingeladen, "<strong>{template.name}</strong>" zu unterschreiben.'
submission_sources:
api: API
bulk: Massenversand
+1
View File
@@ -108,6 +108,7 @@ Rails.application.routes.draw do
resource :preferences, only: %i[show create], controller: 'templates_preferences'
resource :share_link, only: %i[show create], controller: 'templates_share_link'
resources :recipients, only: %i[create], controller: 'templates_recipients'
resources :prefillable_fields, only: %i[create], controller: 'templates_prefillable_fields'
resources :submissions_export, only: %i[index new]
end
resources :preview_document_page, only: %i[show], path: '/preview/:signed_uuid'
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class PopulateSubmitterAccountId < ActiveRecord::Migration[7.1]
disable_ddl_transaction
disable_ddl_transaction!
class MigrationSubmitter < ApplicationRecord
self.table_name = 'submitters'
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class PopulateCompletedSubmittersAndDocuments < ActiveRecord::Migration[7.2]
disable_ddl_transaction
disable_ddl_transaction!
class MigrationSubmitter < ApplicationRecord
self.table_name = 'submitters'
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class PopulateWebhookUrls < ActiveRecord::Migration[7.2]
disable_ddl_transaction
disable_ddl_transaction!
class MigrationWebhookUrl < ActiveRecord::Base
self.table_name = 'webhook_urls'

Some files were not shown because too many files have changed in this diff Show More