Compare commits

...
83 Commits
Author SHA1 Message Date
Pete Matsyburka 4741e64178 fix specs 2024-09-02 20:33:28 +03:00
Pete Matsyburka 25fe35e212 ruby 3.3.4 2024-09-02 20:29:47 +03:00
Pete Matsyburka 92909cea09 clear base64 prefix 2024-09-02 20:11:13 +03:00
Pete Matsyburka b16322dd5b add first name var 2024-09-02 20:11:13 +03:00
dependabot[bot] a04c04141c Bump webpack in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [webpack](https://github.com/webpack/webpack).


Updates `webpack` from 5.82.1 to 5.94.0
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.82.1...v5.94.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-02 20:11:13 +03:00
Pete Matsyburka 90f21d3ed9 add expired condition 2024-09-02 20:11:13 +03:00
Pete Matsyburka f31225e8c2 fix spec 2024-09-02 20:11:13 +03:00
Oleksandr Turchyn eead1b2bc4 add basic API specs 2024-09-02 20:11:13 +03:00
Pete Matsyburka f90a4e5576 adjust document id pdf 2024-09-02 20:11:13 +03:00
Pete Matsyburka 50987ce38b hide app url input 2024-09-02 20:11:13 +03:00
Pete Matsyburka 26113ac3a6 allow signing link with multiple signers 2024-09-02 20:11:13 +03:00
Pete Matsyburka 0b7b72ce16 clear log 2024-09-02 20:11:13 +03:00
Pete Matsyburka 95d48b2808 adjust submissions response 2024-09-02 20:11:13 +03:00
Pete Matsyburka 8fecf94a28 fix typo 2024-09-02 20:11:13 +03:00
Pete Matsyburka 66dd87b290 adjust padding 2024-09-02 20:11:13 +03:00
Pete Matsyburka d20e8cc37d remove checkboxes label 2024-09-02 20:11:13 +03:00
Pete Matsyburka c189e2ed20 clear log 2024-09-02 20:11:13 +03:00
Pete Matsyburka 57f8f45320 archived template preview 2024-09-02 20:11:13 +03:00
Pete Matsyburka 5684baf1e0 add app url var 2024-08-25 16:03:23 +03:00
Pete Matsyburka b6f307690d add combine pdf toggle 2024-08-25 16:03:23 +03:00
Pete Matsyburka 66c44f8ff5 adjust error page 2024-08-25 16:03:23 +03:00
dependabot[bot] 8f3592c481 Bump rexml from 3.3.3 to 3.3.6 in the bundler group across 1 directory
Bumps the bundler group with 1 update in the / directory: [rexml](https://github.com/ruby/rexml).


Updates `rexml` from 3.3.3 to 3.3.6
- [Release notes](https://github.com/ruby/rexml/releases)
- [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md)
- [Commits](https://github.com/ruby/rexml/compare/v3.3.3...v3.3.6)

---
updated-dependencies:
- dependency-name: rexml
  dependency-type: indirect
  dependency-group: bundler
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-25 16:03:23 +03:00
Pete Matsyburka dc8b1e0741 rescue error 2024-08-25 16:03:23 +03:00
Pete Matsyburka b806a6afc5 disable decline button 2024-08-25 16:03:23 +03:00
Pete Matsyburka d43f8f4dd8 add event data 2024-08-25 16:03:23 +03:00
Pete Matsyburka 37c9bf5741 add reply to 2024-08-25 16:03:23 +03:00
Pete Matsyburka a0d37f516f add audit document id 2024-08-25 16:03:23 +03:00
Pete Matsyburka 2764a18252 decline submission 2024-08-25 16:03:23 +03:00
Pete Matsyburka 96cf228e74 fix upload signature 2024-08-25 16:03:23 +03:00
Pete Matsyburka ff6ff3a51e adjust template resp 2024-08-25 16:03:23 +03:00
Pete Matsyburka 9472a6b5b0 allow to upload initial 2024-08-25 16:03:23 +03:00
Pete Matsyburka 5f3a8bf391 fix condition 2024-08-19 19:51:24 +03:00
Pete Matsyburka a0a0fdd1b5 clean log 2024-08-19 19:51:24 +03:00
Pete Matsyburka 7a6f901b09 add log 2024-08-19 19:51:24 +03:00
Pete Matsyburka c4f05fbd9e remove unique index 2024-08-19 19:51:24 +03:00
Pete Matsyburka 50c342ec5c fix spec 2024-08-19 19:51:24 +03:00
Pete Matsyburka a9cfd2bcfc update submission index 2024-08-19 19:51:24 +03:00
Pete Matsyburka e2bdb2acee use account_id 2024-08-19 19:51:24 +03:00
Pete Matsyburka b8828b503c populate account_id 2024-08-19 19:51:24 +03:00
Pete Matsyburka d63f62109b add submitter account_id 2024-08-19 19:51:24 +03:00
Pete Matsyburka c04483f0ea allow to update external_id 2024-08-19 19:51:24 +03:00
Pete Matsyburka 1efaf30fb7 fix net smtp without creds 2024-08-19 19:51:24 +03:00
Pete Matsyburka 2e462e9633 fix erblint 2024-08-19 19:51:24 +03:00
Pete Matsyburka 838df5f41d fix image documents 2024-08-19 19:51:24 +03:00
Pete Matsyburka 32445f0e19 adjust mobile ui 2024-08-19 19:51:24 +03:00
Pete Matsyburka 5f9067baee fix mobile field types 2024-08-19 19:51:24 +03:00
Pete Matsyburka ad56d5ee14 adjust mobile ui 2024-08-19 19:51:24 +03:00
Pete Matsyburka a091d121d9 confgurable SMTP timeout 2024-08-19 19:51:24 +03:00
Pete Matsyburka cd9c3ee3c1 fix ci 2024-08-19 19:51:24 +03:00
Pete Matsyburka 534c3a2e10 add submission.completed 2024-08-19 19:51:24 +03:00
Pete Matsyburka 7bc8e194df fix font url 2024-08-19 19:51:24 +03:00
Pete Matsyburka de5c4d5bcd adjust retries 2024-08-19 19:51:24 +03:00
dependabot[bot] 1e6b5c684f bump deps 2024-08-10 15:34:13 +03:00
Pete Matsyburka 4651f6b652 simplify variables 2024-08-10 15:34:13 +03:00
Pete Matsyburka e5b0a2355f use aws secret 2024-08-10 15:34:13 +03:00
Pete Matsyburka db22fe7518 fix condition 2024-08-10 15:34:13 +03:00
Pete Matsyburka 0dcd97f49b add verify tool 2024-08-10 15:34:13 +03:00
Pete Matsyburka de14330449 allow to remove page area 2024-08-10 15:34:13 +03:00
Pete Matsyburka 173658138c fix conditions chain 2024-08-10 15:34:13 +03:00
Pete Matsyburka 000f3c72fc fix go to step 2024-08-10 15:34:13 +03:00
Pete Matsyburka 561b16e9fb fix pdf 2024-08-07 17:04:54 +03:00
Pete Matsyburka 507ca49112 adjust ai link 2024-08-07 14:34:32 +03:00
Pete Matsyburka 94d5ab4ace add reason 2024-08-06 00:49:40 +03:00
Pete Matsyburka 91d44a3e31 adjust date format 2024-08-05 19:38:47 +03:00
Pete Matsyburka 478167ea4f add webhook secret 2024-08-05 13:30:04 +03:00
Pete Matsyburka 6b1c3bba19 add testing footer 2024-08-05 10:45:05 +03:00
Pete Matsyburka 73841f751c fix icons 2024-08-04 23:41:32 +03:00
Pete Matsyburka 22dbe3291f adjust compose mount folder 2024-08-04 01:28:22 +03:00
Pete Matsyburka 1dac3fe434 fix cell 2024-08-04 00:13:34 +03:00
Pete Matsyburka d0c49ac04c i18n 2024-08-03 09:26:26 +03:00
Pete Matsyburka 3877dbf83f remove lookbehind regexp 2024-08-02 19:02:58 +03:00
Alex Turchyn d15fd1fb84 allow to view password during registration 2024-08-01 23:31:54 +03:00
Pete Matsyburka 0a105490fe process noreply 2024-08-01 23:22:33 +03:00
Pete Matsyburka 0bd35b526c handle date enter 2024-08-01 18:47:17 +03:00
Pete Matsyburka 3a2910f717 add expire at 2024-08-01 18:34:09 +03:00
Pete Matsyburka c4a91c2b34 extract markdown link 2024-08-01 01:31:25 +03:00
Pete Matsyburka 5643380916 configure page quality 2024-08-01 00:14:33 +03:00
Pete Matsyburka cf6b378ccb adjust payment step 2024-07-31 22:56:18 +03:00
Pete Matsyburka 66bc8cfb5f archive without confirm 2024-07-31 17:57:52 +03:00
Pete Matsyburka 994bd1fe5c dummy script 2024-07-31 10:53:51 +03:00
Pete Matsyburka 12528b6527 fix cell field 2024-07-30 21:18:56 +03:00
Pete Matsyburka d135c93939 fix n+1 2024-07-29 17:23:05 +03:00
Pete Matsyburka 637011d55a remove log 2024-07-29 16:50:25 +03:00
141 changed files with 2935 additions and 632 deletions

No files matched your search

+5 -3
View File
@@ -11,7 +11,7 @@ jobs:
- name: Install Ruby - name: Install Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 3.3.3 ruby-version: 3.3.4
- name: Cache gems - name: Cache gems
uses: actions/cache@v1 uses: actions/cache@v1
with: with:
@@ -35,7 +35,7 @@ jobs:
- name: Install Ruby - name: Install Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 3.3.3 ruby-version: 3.3.4
- name: Cache gems - name: Cache gems
uses: actions/cache@v1 uses: actions/cache@v1
with: with:
@@ -100,13 +100,15 @@ jobs:
- name: Install Ruby - name: Install Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 3.3.3 ruby-version: 3.3.4
- name: Set up Node - name: Set up Node
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 20.9.0 node-version: 20.9.0
- name: Install Chrome - name: Install Chrome
uses: browser-actions/setup-chrome@latest uses: browser-actions/setup-chrome@latest
with:
chrome-version: 125
- name: Cache node_modules - name: Cache node_modules
uses: actions/cache@v1 uses: actions/cache@v1
with: with:
+3
View File
@@ -34,6 +34,7 @@ Metrics/MethodLength:
Max: 30 Max: 30
Exclude: Exclude:
- 'db/migrate/**' - 'db/migrate/**'
- 'spec/**/*'
Metrics/CyclomaticComplexity: Metrics/CyclomaticComplexity:
Max: 15 Max: 15
@@ -46,6 +47,8 @@ Layout/LineLength:
Metrics/AbcSize: Metrics/AbcSize:
Max: 45 Max: 45
Exclude:
- spec/**/*
Metrics/ModuleLength: Metrics/ModuleLength:
Max: 500 Max: 500
+4 -4
View File
@@ -1,12 +1,12 @@
FROM ruby:3.3.3-alpine as fonts FROM ruby:3.3.4-alpine as fonts
WORKDIR /fonts WORKDIR /fonts
RUN apk --no-cache add fontforge wget && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/v2031/DancingScript-Regular.otf && wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt RUN apk --no-cache add fontforge wget && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt
RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")' RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")'
FROM ruby:3.3.3-alpine as webpack FROM ruby:3.3.4-alpine as webpack
ENV RAILS_ENV=production ENV RAILS_ENV=production
ENV NODE_ENV=production ENV NODE_ENV=production
@@ -32,7 +32,7 @@ COPY ./app/views ./app/views
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
FROM ruby:3.3.3-alpine as app FROM ruby:3.3.4-alpine as app
ENV RAILS_ENV=production ENV RAILS_ENV=production
ENV BUNDLE_WITHOUT="development:test" ENV BUNDLE_WITHOUT="development:test"
+3 -1
View File
@@ -2,10 +2,11 @@
source 'https://rubygems.org' source 'https://rubygems.org'
ruby '3.3.3' ruby '3.3.4'
gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic' gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic'
gem 'aws-sdk-s3', require: false gem 'aws-sdk-s3', require: false
gem 'aws-sdk-secretsmanager', require: false
gem 'azure-storage-blob', require: false gem 'azure-storage-blob', require: false
gem 'bootsnap', require: false gem 'bootsnap', require: false
gem 'cancancan' gem 'cancancan'
@@ -22,6 +23,7 @@ gem 'image_processing'
gem 'jwt' gem 'jwt'
gem 'lograge' gem 'lograge'
gem 'mysql2', require: false gem 'mysql2', require: false
gem 'net-smtp', '0.4.0'
gem 'oj' gem 'oj'
gem 'pagy' gem 'pagy'
gem 'pg', require: false gem 'pg', require: false
+73 -68
View File
@@ -1,35 +1,35 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (7.1.3.2) actioncable (7.1.3.4)
actionpack (= 7.1.3.2) actionpack (= 7.1.3.4)
activesupport (= 7.1.3.2) activesupport (= 7.1.3.4)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
actionmailbox (7.1.3.2) actionmailbox (7.1.3.4)
actionpack (= 7.1.3.2) actionpack (= 7.1.3.4)
activejob (= 7.1.3.2) activejob (= 7.1.3.4)
activerecord (= 7.1.3.2) activerecord (= 7.1.3.4)
activestorage (= 7.1.3.2) activestorage (= 7.1.3.4)
activesupport (= 7.1.3.2) activesupport (= 7.1.3.4)
mail (>= 2.7.1) mail (>= 2.7.1)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
actionmailer (7.1.3.2) actionmailer (7.1.3.4)
actionpack (= 7.1.3.2) actionpack (= 7.1.3.4)
actionview (= 7.1.3.2) actionview (= 7.1.3.4)
activejob (= 7.1.3.2) activejob (= 7.1.3.4)
activesupport (= 7.1.3.2) activesupport (= 7.1.3.4)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
actionpack (7.1.3.2) actionpack (7.1.3.4)
actionview (= 7.1.3.2) actionview (= 7.1.3.4)
activesupport (= 7.1.3.2) activesupport (= 7.1.3.4)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
racc racc
rack (>= 2.2.4) rack (>= 2.2.4)
@@ -37,35 +37,35 @@ GEM
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
actiontext (7.1.3.2) actiontext (7.1.3.4)
actionpack (= 7.1.3.2) actionpack (= 7.1.3.4)
activerecord (= 7.1.3.2) activerecord (= 7.1.3.4)
activestorage (= 7.1.3.2) activestorage (= 7.1.3.4)
activesupport (= 7.1.3.2) activesupport (= 7.1.3.4)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (7.1.3.2) actionview (7.1.3.4)
activesupport (= 7.1.3.2) activesupport (= 7.1.3.4)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.11) erubi (~> 1.11)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
activejob (7.1.3.2) activejob (7.1.3.4)
activesupport (= 7.1.3.2) activesupport (= 7.1.3.4)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (7.1.3.2) activemodel (7.1.3.4)
activesupport (= 7.1.3.2) activesupport (= 7.1.3.4)
activerecord (7.1.3.2) activerecord (7.1.3.4)
activemodel (= 7.1.3.2) activemodel (= 7.1.3.4)
activesupport (= 7.1.3.2) activesupport (= 7.1.3.4)
timeout (>= 0.4.0) timeout (>= 0.4.0)
activestorage (7.1.3.2) activestorage (7.1.3.4)
actionpack (= 7.1.3.2) actionpack (= 7.1.3.4)
activejob (= 7.1.3.2) activejob (= 7.1.3.4)
activerecord (= 7.1.3.2) activerecord (= 7.1.3.4)
activesupport (= 7.1.3.2) activesupport (= 7.1.3.4)
marcel (~> 1.0) marcel (~> 1.0)
activesupport (7.1.3.2) activesupport (7.1.3.4)
base64 base64
bigdecimal bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
@@ -96,6 +96,9 @@ GEM
aws-sdk-core (~> 3, >= 3.191.0) aws-sdk-core (~> 3, >= 3.191.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8) aws-sigv4 (~> 1.8)
aws-sdk-secretsmanager (1.91.0)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.8.0) aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
azure-storage-blob (2.0.3) azure-storage-blob (2.0.3)
@@ -119,7 +122,7 @@ GEM
bindex (0.8.1) bindex (0.8.1)
bootsnap (1.18.3) bootsnap (1.18.3)
msgpack (~> 1.2) msgpack (~> 1.2)
builder (3.2.4) builder (3.3.0)
bullet (7.1.6) bullet (7.1.6)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.11) uniform_notifier (~> 1.11)
@@ -139,7 +142,7 @@ GEM
cldr-plurals-runtime-rb (1.1.0) cldr-plurals-runtime-rb (1.1.0)
cmdparse (3.0.7) cmdparse (3.0.7)
coderay (1.1.3) coderay (1.1.3)
concurrent-ruby (1.2.3) concurrent-ruby (1.3.4)
connection_pool (2.4.1) connection_pool (2.4.1)
crack (1.0.0) crack (1.0.0)
bigdecimal bigdecimal
@@ -181,7 +184,7 @@ GEM
rainbow rainbow
rubocop rubocop
smart_properties smart_properties
erubi (1.12.0) erubi (1.13.0)
factory_bot (6.4.6) factory_bot (6.4.6)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
factory_bot_rails (6.4.3) factory_bot_rails (6.4.3)
@@ -308,8 +311,8 @@ GEM
method_source (1.1.0) method_source (1.1.0)
mini_magick (4.12.0) mini_magick (4.12.0)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.6) mini_portile2 (2.8.7)
minitest (5.23.0) minitest (5.24.1)
msgpack (1.7.2) msgpack (1.7.2)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.4.0) multipart-post (2.4.0)
@@ -317,7 +320,7 @@ GEM
mysql2 (0.5.6) mysql2 (0.5.6)
net-http-persistent (4.0.2) net-http-persistent (4.0.2)
connection_pool (~> 2.2) connection_pool (~> 2.2)
net-imap (0.4.10) net-imap (0.4.14)
date date
net-protocol net-protocol
net-pop (0.1.2) net-pop (0.1.2)
@@ -327,12 +330,12 @@ GEM
net-smtp (0.4.0) net-smtp (0.4.0)
net-protocol net-protocol
nio4r (2.7.1) nio4r (2.7.1)
nokogiri (1.16.5) nokogiri (1.16.7)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.5-aarch64-linux) nokogiri (1.16.7-aarch64-linux)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.5-x86_64-linux) nokogiri (1.16.7-x86_64-linux)
racc (~> 1.4) racc (~> 1.4)
oj (3.16.3) oj (3.16.3)
bigdecimal (>= 3.0) bigdecimal (>= 3.0)
@@ -366,8 +369,8 @@ GEM
public_suffix (5.0.5) public_suffix (5.0.5)
puma (6.4.2) puma (6.4.2)
nio4r (~> 2.0) nio4r (~> 2.0)
racc (1.8.0) racc (1.8.1)
rack (3.0.11) rack (3.1.7)
rack-proxy (0.7.7) rack-proxy (0.7.7)
rack rack
rack-session (2.0.0) rack-session (2.0.0)
@@ -377,20 +380,20 @@ GEM
rackup (2.1.0) rackup (2.1.0)
rack (>= 3) rack (>= 3)
webrick (~> 1.8) webrick (~> 1.8)
rails (7.1.3.2) rails (7.1.3.4)
actioncable (= 7.1.3.2) actioncable (= 7.1.3.4)
actionmailbox (= 7.1.3.2) actionmailbox (= 7.1.3.4)
actionmailer (= 7.1.3.2) actionmailer (= 7.1.3.4)
actionpack (= 7.1.3.2) actionpack (= 7.1.3.4)
actiontext (= 7.1.3.2) actiontext (= 7.1.3.4)
actionview (= 7.1.3.2) actionview (= 7.1.3.4)
activejob (= 7.1.3.2) activejob (= 7.1.3.4)
activemodel (= 7.1.3.2) activemodel (= 7.1.3.4)
activerecord (= 7.1.3.2) activerecord (= 7.1.3.4)
activestorage (= 7.1.3.2) activestorage (= 7.1.3.4)
activesupport (= 7.1.3.2) activesupport (= 7.1.3.4)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 7.1.3.2) railties (= 7.1.3.4)
rails-dom-testing (2.2.0) rails-dom-testing (2.2.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
minitest minitest
@@ -405,9 +408,9 @@ GEM
actionview (> 3.1) actionview (> 3.1)
activesupport (> 3.1) activesupport (> 3.1)
railties (> 3.1) railties (> 3.1)
railties (7.1.3.2) railties (7.1.3.4)
actionpack (= 7.1.3.2) actionpack (= 7.1.3.4)
activesupport (= 7.1.3.2) activesupport (= 7.1.3.4)
irb irb
rackup (>= 1.0.0) rackup (>= 1.0.0)
rake (>= 12.2) rake (>= 12.2)
@@ -417,7 +420,7 @@ GEM
rake (13.2.1) rake (13.2.1)
rdoc (6.6.3.1) rdoc (6.6.3.1)
psych (>= 4.0.0) psych (>= 4.0.0)
redis-client (0.22.0) redis-client (0.22.2)
connection_pool connection_pool
regexp_parser (2.9.2) regexp_parser (2.9.2)
reline (0.5.7) reline (0.5.7)
@@ -432,7 +435,7 @@ GEM
actionpack (>= 5.2) actionpack (>= 5.2)
railties (>= 5.2) railties (>= 5.2)
retriable (3.1.2) retriable (3.1.2)
rexml (3.3.2) rexml (3.3.6)
strscan strscan
rotp (6.3.0) rotp (6.3.0)
rqrcode (2.2.0) rqrcode (2.2.0)
@@ -494,7 +497,7 @@ GEM
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 5.2) railties (>= 5.2)
semantic_range (>= 2.3.0) semantic_range (>= 2.3.0)
sidekiq (7.2.2) sidekiq (7.2.4)
concurrent-ruby (< 2) concurrent-ruby (< 2)
connection_pool (>= 2.3.0) connection_pool (>= 2.3.0)
rack (>= 2.2.4) rack (>= 2.2.4)
@@ -563,6 +566,7 @@ DEPENDENCIES
annotate annotate
arabic-letter-connector arabic-letter-connector
aws-sdk-s3 aws-sdk-s3
aws-sdk-secretsmanager
azure-storage-blob azure-storage-blob
better_html better_html
bootsnap bootsnap
@@ -588,6 +592,7 @@ DEPENDENCIES
letter_opener_web letter_opener_web
lograge lograge
mysql2 mysql2
net-smtp (= 0.4.0)
oj oj
pagy pagy
pg pg
@@ -620,7 +625,7 @@ DEPENDENCIES
webmock webmock
RUBY VERSION RUBY VERSION
ruby 3.3.3p89 ruby 3.3.4p94
BUNDLED WITH BUNDLED WITH
2.5.3 2.5.3
@@ -14,7 +14,9 @@ class AccountConfigsController < ApplicationController
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY, AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
AccountConfig::FORCE_SSO_AUTH_KEY, AccountConfig::FORCE_SSO_AUTH_KEY,
AccountConfig::FLATTEN_RESULT_PDF_KEY, AccountConfig::FLATTEN_RESULT_PDF_KEY,
AccountConfig::WITH_SIGNATURE_ID AccountConfig::WITH_SIGNATURE_ID,
AccountConfig::COMBINE_PDF_RESULT_KEY,
AccountConfig::REQUIRE_SIGNING_REASON_KEY
].freeze ].freeze
InvalidKey = Class.new(StandardError) InvalidKey = Class.new(StandardError)
@@ -11,7 +11,8 @@ module Api
params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present? params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present?
submitters = paginate( submitters = paginate(
submitters.preload(template: :folder, submission: [:submitters, { audit_trail_attachment: :blob }], submitters.preload(template: :folder, submission: [:submitters, { audit_trail_attachment: :blob,
combined_document_attachment: :blob }],
documents_attachments: :blob, attachments_attachments: :blob), documents_attachments: :blob, attachments_attachments: :blob),
field: :completed_at field: :completed_at
) )
@@ -0,0 +1,52 @@
# frozen_string_literal: true
module Api
class SubmissionEventsController < ApiBaseController
load_and_authorize_resource :submission, parent: false
def index
submissions = build_completed_query(@submissions)
params[:after] = Time.zone.at(params[:after].to_i) if params[:after].present?
params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present?
submissions = paginate(submissions.preload(
:created_by_user, :submission_events,
template: :folder,
submitters: { documents_attachments: :blob, attachments_attachments: :blob },
audit_trail_attachment: :blob
),
field: :completed_at)
render json: {
data: submissions.map do |s|
{
event_type: 'submission.completed',
timestamp: s.completed_at,
data: Submissions::SerializeForApi.call(s, s.submitters)
}
end,
pagination: {
count: submissions.size,
next: submissions.last&.completed_at&.to_i,
prev: submissions.first&.completed_at&.to_i
}
}
end
private
def build_completed_query(submissions)
submissions = submissions.where(
Submitter.where(completed_at: nil).where(
Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
).select(1).arel.exists.not
)
submissions.joins(:submitters)
.group(:id)
.select(Submission.arel_table[Arel.star],
Submitter.arel_table[:completed_at].maximum.as('completed_at'))
end
end
end
@@ -19,10 +19,14 @@ module Api
submissions = paginate(submissions.preload(:created_by_user, :submitters, submissions = paginate(submissions.preload(:created_by_user, :submitters,
template: :folder, template: :folder,
combined_document_attachment: :blob,
audit_trail_attachment: :blob)) audit_trail_attachment: :blob))
render json: { render json: {
data: submissions.as_json(Submissions::SerializeForApi::SERIALIZE_PARAMS), data: submissions.map do |s|
Submissions::SerializeForApi.call(s, s.submitters, params,
with_events: false, with_documents: false, with_values: false)
end,
pagination: { pagination: {
count: submissions.size, count: submissions.size,
next: submissions.last&.id, next: submissions.last&.id,
@@ -147,6 +151,7 @@ module Api
def submissions_params def submissions_params
permitted_attrs = [ permitted_attrs = [
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last, :send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
:expire_at,
{ {
message: %i[subject body], message: %i[subject body],
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role, submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
@@ -77,6 +77,10 @@ module Api
with_urls: true, with_urls: true,
with_events: false, with_events: false,
params:) params:)
rescue Submitters::NormalizeValues::BaseError => e
Rollbar.warning(e) if defined?(Rollbar)
render json: { error: e.message }, status: :unprocessable_entity
end end
def submitter_params def submitter_params
@@ -95,6 +95,7 @@ module Api
def template_params def template_params
permitted_params = [ permitted_params = [
:name, :name,
:external_id,
{ {
submitters: [%i[name uuid]], submitters: [%i[name uuid]],
fields: [[:uuid, :submitter_uuid, :name, :type, fields: [[:uuid, :submitter_uuid, :name, :type,
+24
View File
@@ -14,5 +14,29 @@ module Api
data: Base64.encode64(PdfUtils.merge(files.map { |base64| StringIO.new(Base64.decode64(base64)) }).string) data: Base64.encode64(PdfUtils.merge(files.map { |base64| StringIO.new(Base64.decode64(base64)) }).string)
} }
end end
def verify
file = Base64.decode64(params[:file])
pdf = HexaPDF::Document.new(io: StringIO.new(file))
trusted_certs = Accounts.load_trusted_certs(current_account)
is_checksum_found = ActiveStorage::Attachment.joins(:blob)
.where(name: 'documents', record_type: 'Submitter')
.exists?(blob: { checksum: Digest::MD5.base64digest(file) })
render json: {
checksum_status: is_checksum_found ? 'verified' : 'not_found',
signatures: pdf.signatures.map do |sig|
{
verification_result: sig.verify(trusted_certs:).messages,
signer_name: sig.signer_name,
signing_reason: sig.signing_reason,
signing_time: sig.signing_time,
signature_type: sig.signature_type
}
end
}
end
end end
end end
@@ -0,0 +1,39 @@
# frozen_string_literal: true
class EmbedScriptsController < ActionController::Metal
DUMMY_SCRIPT = <<~JAVASCRIPT.freeze
const DummyBuilder = class extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<div style="text-align: center; padding: 20px; font-family: Arial, sans-serif;">
<h2>Upgrade to Pro</h2>
<p>Unlock embedded components by upgrading to Pro</p>
<div style="margin-top: 40px;">
<a href="#{Docuseal::CONSOLE_URL}/on_premise" target="_blank" style="padding: 15px 25px; background-color: #222; color: white; text-decoration: none; border-radius: 5px; font-size: 16px; cursor: pointer;">
Learn More
</a>
</div>
</div>
`;
}
};
const DummyForm = class extends DummyBuilder {};
if (!window.customElements.get('docuseal-builder')) {
window.customElements.define('docuseal-builder', DummyBuilder);
}
if (!window.customElements.get('docuseal-form')) {
window.customElements.define('docuseal-form', DummyForm);
}
JAVASCRIPT
def show
headers['Content-Type'] = 'application/javascript'
self.response_body = DUMMY_SCRIPT
self.status = 200
end
end
+28 -8
View File
@@ -17,15 +17,12 @@ class StartFormController < ApplicationController
def update def update
return redirect_to start_form_path(@template.slug) if @template.archived_at? return redirect_to start_form_path(@template.slug) if @template.archived_at?
@submitter = Submitter.where(submission: @template.submissions.where(archived_at: nil)) @submitter = find_or_initialize_submitter(@template, submitter_params)
.order(id: :desc)
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
.find_or_initialize_by(**submitter_params.compact_blank)
if @submitter.completed_at? if @submitter.completed_at?
redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email]) redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email])
else else
if @template.submitters.to_a.size > 1 && @submitter.new_record? if @template.submitters.to_a.size > 2 && @submitter.new_record?
@error_message = 'Not found' @error_message = 'Not found'
return render :show return render :show
@@ -55,11 +52,18 @@ class StartFormController < ApplicationController
private private
def find_or_initialize_submitter(template, submitter_params)
Submitter.where(submission: template.submissions.where(expire_at: Time.current..)
.or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
.order(id: :desc)
.where(declined_at: nil)
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
.find_or_initialize_by(**submitter_params.compact_blank)
end
def assign_submission_attributes(submitter, template) def assign_submission_attributes(submitter, template)
resubmit_submitter = resubmit_submitter =
if params[:resubmit].present? (Submitter.where(submission: template.submissions).find_by(slug: params[:resubmit]) if params[:resubmit].present?)
Submitter.where(submission: @template.submissions).find_by(slug: params[:resubmit])
end
submitter.assign_attributes( submitter.assign_attributes(
uuid: template.submitters.first['uuid'], uuid: template.submitters.first['uuid'],
@@ -81,9 +85,25 @@ class StartFormController < ApplicationController
template_submitters: template.submitters, template_submitters: template.submitters,
source: :link) source: :link)
maybe_assign_default_second_submitter(submitter.submission)
submitter.account_id = submitter.submission.account_id
submitter submitter
end end
def maybe_assign_default_second_submitter(submission)
return unless submission.new_record?
return if submission.template.submitters.to_a.size != 2
submission.submitters_order = 'preserved'
submission.submitters.new(
account_id: submission.account_id,
uuid: submission.template.submitters.second['uuid'],
email: submission.template.author.email
)
end
def submitter_params def submitter_params
params.require(:submitter).permit(:email, :phone, :name).tap do |attrs| params.require(:submitter).permit(:email, :phone, :name).tap do |attrs|
attrs[:email] = Submissions.normalize_email(attrs[:email]) attrs[:email] = Submissions.normalize_email(attrs[:email])
@@ -33,7 +33,7 @@ class SubmissionsDownloadController < ApplicationController
return head :not_found return head :not_found
end end
if params[:combined] if params[:combined] == 'true'
url = build_combined_url(submitter) url = build_combined_url(submitter)
if url if url
@@ -14,6 +14,8 @@ class SubmitFormController < ApplicationController
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at? return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
return render :archived if @submitter.submission.template.archived_at? || @submitter.submission.archived_at? return render :archived if @submitter.submission.template.archived_at? || @submitter.submission.archived_at?
return render :expired if @submitter.submission.expired?
return render :declined if @submitter.declined_at?
Submitters.preload_with_pages(@submitter) Submitters.preload_with_pages(@submitter)
@@ -51,6 +53,12 @@ class SubmitFormController < ApplicationController
return render json: { error: 'Form has been archived.' }, status: :unprocessable_entity return render json: { error: 'Form has been archived.' }, status: :unprocessable_entity
end end
if submitter.submission.expired?
return render json: { error: 'Form has been expired.' }, status: :unprocessable_entity
end
return render json: { error: 'Form has been declined.' }, status: :unprocessable_entity if submitter.declined_at?
Submitters::SubmitValues.call(submitter, params, request) Submitters::SubmitValues.call(submitter, params, request)
head :ok head :ok
@@ -0,0 +1,29 @@
# frozen_string_literal: true
class SubmitFormDeclineController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
def create
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return redirect_to submit_form_path(submitter.slug) if submitter.declined_at? ||
submitter.completed_at? ||
submitter.submission.archived_at? ||
submitter.submission.expired? ||
submitter.submission.template.archived_at?
ApplicationRecord.transaction do
submitter.update!(declined_at: Time.current)
SubmissionEvents.create_with_tracking_data(submitter, 'decline_form', request, { reason: params[:reason] })
end
user = submitter.submission.created_by_user || submitter.template.author
SubmitterMailer.declined_email(submitter, user).deliver_later!
SendFormDeclinedWebhookRequestJob.perform_async('submitter_id' => submitter.id)
redirect_to submit_form_path(submitter.slug)
end
end
@@ -0,0 +1,37 @@
# frozen_string_literal: true
class SubmitFormDownloadController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
FILES_TTL = 5.minutes
def index
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return redirect_to submitter_download_index_path(submitter.slug) if submitter.completed_at?
return head :unprocessable_entity if submitter.declined_at? ||
submitter.submission.archived_at? ||
submitter.submission.expired? ||
submitter.submission.template.archived_at?
last_completed_submitter = submitter.submission.submitters
.where.not(id: submitter.id)
.where.not(completed_at: nil)
.max_by(&:completed_at)
attachments =
if last_completed_submitter
Submitters.select_attachments_for_download(last_completed_submitter)
else
submitter.submission.template.schema_documents.preload(:blob)
end
urls = attachments.map do |attachment|
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
end
render json: urls
end
end
@@ -7,8 +7,10 @@ class SubmitFormValuesController < ApplicationController
def index def index
submitter = Submitter.find_by!(slug: params[:submit_form_slug]) submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return render json: {} if submitter.completed_at? return render json: {} if submitter.completed_at? || submitter.declined_at?
return render json: {} if submitter.submission.template.archived_at? || submitter.submission.archived_at? return render json: {} if submitter.submission.template.archived_at? ||
submitter.submission.archived_at? ||
submitter.submission.expired?
value = submitter.values[params['field_uuid']] value = submitter.values[params['field_uuid']]
attachment = submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present? attachment = submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present?
+1 -1
View File
@@ -123,7 +123,7 @@ class TemplatesController < ApplicationController
if template.account == current_account if template.account == current_account
redirect_to(edit_template_path(@template)) redirect_to(edit_template_path(@template))
else else
redirect_back(fallback_location: root_path, notice: 'Template has been clonned') redirect_back(fallback_location: root_path, notice: 'Template has been cloned')
end end
end end
@@ -9,26 +9,7 @@ class VerifyPdfSignatureController < ApplicationController
HexaPDF::Document.new(io: file.open) HexaPDF::Document.new(io: file.open)
end end
cert_data = trusted_certs = Accounts.load_trusted_certs(current_account)
if Docuseal.multitenant?
value = EncryptedConfig.find_by(account: current_account, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
Docuseal::CERTS.merge(value)
else
EncryptedConfig.find_by(key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
end
default_pkcs = GenerateCertificate.load_pkcs(cert_data)
custom_certs = cert_data.fetch('custom', []).map do |e|
OpenSSL::PKCS12.new(Base64.urlsafe_decode64(e['data']), e['password'].to_s)
end
trusted_certs = [default_pkcs.certificate,
*default_pkcs.ca_certs,
*custom_certs.map(&:certificate),
*custom_certs.flat_map(&:ca_certs).compact,
*Docuseal.trusted_certs]
render turbo_stream: turbo_stream.replace('result', partial: 'result', render turbo_stream: turbo_stream.replace('result', partial: 'result',
locals: { pdfs:, files: params[:files], trusted_certs: }) locals: { pdfs:, files: params[:files], trusted_certs: })
@@ -5,6 +5,7 @@ class WebhookPreferencesController < ApplicationController
form.viewed form.viewed
form.started form.started
form.completed form.completed
form.declined
template.created template.created
template.updated template.updated
submission.created submission.created
@@ -0,0 +1,29 @@
# frozen_string_literal: true
class WebhookSecretController < ApplicationController
before_action :load_encrypted_config
authorize_resource :encrypted_config, parent: false
def index; end
def create
@encrypted_config.assign_attributes(value: {
encrypted_config_params[:key] => encrypted_config_params[:value]
}.compact_blank)
@encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook Secret has been saved.')
end
private
def load_encrypted_config
@encrypted_config =
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_SECRET_KEY)
end
def encrypted_config_params
params.require(:encrypted_config).permit(value: %i[key value]).fetch(:value, {})
end
end
+7
View File
@@ -24,6 +24,8 @@ import SubmitForm from './elements/submit_form'
import PromptPassword from './elements/prompt_password' import PromptPassword from './elements/prompt_password'
import EmailsTextarea from './elements/emails_textarea' import EmailsTextarea from './elements/emails_textarea'
import ToggleOnSubmit from './elements/toggle_on_submit' import ToggleOnSubmit from './elements/toggle_on_submit'
import PasswordInput from './elements/password_input'
import SearchInput from './elements/search_input'
import * as TurboInstantClick from './lib/turbo_instant_click' import * as TurboInstantClick from './lib/turbo_instant_click'
@@ -85,6 +87,8 @@ safeRegisterElement('prompt-password', PromptPassword)
safeRegisterElement('emails-textarea', EmailsTextarea) safeRegisterElement('emails-textarea', EmailsTextarea)
safeRegisterElement('toggle-cookies', ToggleCookies) safeRegisterElement('toggle-cookies', ToggleCookies)
safeRegisterElement('toggle-on-submit', ToggleOnSubmit) safeRegisterElement('toggle-on-submit', ToggleOnSubmit)
safeRegisterElement('password-input', PasswordInput)
safeRegisterElement('search-input', SearchInput)
safeRegisterElement('template-builder', class extends HTMLElement { safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () { connectedCallback () {
@@ -95,6 +99,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
this.app = createApp(TemplateBuilder, { this.app = createApp(TemplateBuilder, {
template: reactive(JSON.parse(this.dataset.template)), template: reactive(JSON.parse(this.dataset.template)),
backgroundColor: '#faf7f5', backgroundColor: '#faf7f5',
locale: this.dataset.locale,
withPhone: this.dataset.withPhone === 'true', withPhone: this.dataset.withPhone === 'true',
withLogo: this.dataset.withLogo !== 'false', withLogo: this.dataset.withLogo !== 'false',
editable: this.dataset.editable !== 'false', editable: this.dataset.editable !== 'false',
@@ -102,6 +107,8 @@ safeRegisterElement('template-builder', class extends HTMLElement {
withPayment: this.dataset.withPayment === 'true', withPayment: this.dataset.withPayment === 'true',
isPaymentConnected: this.dataset.isPaymentConnected === 'true', isPaymentConnected: this.dataset.isPaymentConnected === 'true',
withFormula: this.dataset.withFormula === 'true', withFormula: this.dataset.withFormula === 'true',
withSendButton: this.dataset.withSendButton !== 'false',
withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false',
withConditions: this.dataset.withConditions === 'true', withConditions: this.dataset.withConditions === 'true',
currencies: (this.dataset.currencies || '').split(',').filter(Boolean), currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
acceptFileTypes: this.dataset.acceptFileTypes acceptFileTypes: this.dataset.acceptFileTypes
+35
View File
@@ -0,0 +1,35 @@
import { target, targetable } from '@github/catalyst/lib/targetable'
export default targetable(class extends HTMLElement {
static [target.static] = [
'visiblePasswordIcon',
'hiddenPasswordIcon',
'passwordInput',
'togglePasswordVisibility'
]
connectedCallback () {
this.togglePasswordVisibility.addEventListener('click', this.handleTogglePasswordVisibility)
document.addEventListener('turbo:submit-start', this.setInitialPasswordType)
}
disconnectedCallback () {
this.togglePasswordVisibility.removeEventListener('click', this.handleTogglePasswordVisibility)
document.removeEventListener('turbo:submit-start', this.setInitialPasswordType)
}
handleTogglePasswordVisibility = () => {
this.passwordInput.type = this.passwordInput.type === 'password' ? 'text' : 'password'
this.toggleIcon()
}
setInitialPasswordType = () => {
this.passwordInput.type = 'password'
this.toggleIcon()
}
toggleIcon = () => {
this.visiblePasswordIcon.classList.toggle('hidden', this.passwordInput.type === 'password')
this.hiddenPasswordIcon.classList.toggle('hidden', this.passwordInput.type === 'text')
}
})
+25
View File
@@ -0,0 +1,25 @@
export default class extends HTMLElement {
connectedCallback () {
this.input.addEventListener('focus', () => {
if (this.title) {
this.title.classList.add('hidden', 'md:block')
this.input.classList.add('w-60')
}
})
this.input.addEventListener('blur', (e) => {
if (this.title && !e.target.value) {
this.title.classList.remove('hidden')
this.input.classList.remove('w-60')
}
})
}
get input () {
return this.querySelector('input')
}
get title () {
return document.querySelector(this.dataset.title)
}
}
+1
View File
@@ -23,6 +23,7 @@ safeRegisterElement('submission-form', class extends HTMLElement {
dryRun: this.dataset.dryRun === 'true', dryRun: this.dataset.dryRun === 'true',
expand: ['true', 'false'].includes(this.dataset.expand) ? this.dataset.expand === 'true' : null, expand: ['true', 'false'].includes(this.dataset.expand) ? this.dataset.expand === 'true' : null,
withSignatureId: this.dataset.withSignatureId === 'true', withSignatureId: this.dataset.withSignatureId === 'true',
requireSigningReason: this.dataset.requireSigningReason === 'true',
withConfetti: this.dataset.withConfetti !== 'false', withConfetti: this.dataset.withConfetti !== 'false',
withDisclosure: this.dataset.withDisclosure === 'true', withDisclosure: this.dataset.withDisclosure === 'true',
withTypedSignature: this.dataset.withTypedSignature !== 'false', withTypedSignature: this.dataset.withTypedSignature !== 'false',
+8 -2
View File
@@ -18,9 +18,15 @@ if (token) {
transform (payload) { transform (payload) {
payload.body.telemetry = [] payload.body.telemetry = []
const string = JSON.stringify(payload) if (payload.request.query_string) {
payload.request.query_string = ''
}
return JSON.parse(string.replace(/(\/[des]\/)\w{6}/g, (_, m) => m)) if (payload.request.url) {
payload.request.url = payload.request.url.replace(/(\/[sdep]\/)(\w{5})[^/]+/, '$1$2')
}
return payload
}, },
payload: { payload: {
client: { client: {
+6 -1
View File
@@ -75,7 +75,7 @@
ID: {{ signature.uuid }} ID: {{ signature.uuid }}
</div> </div>
<div> <div>
{{ t('reason') }}: {{ t('digitally_signed_by') }} {{ submitter.name }} {{ t('reason') }}: {{ values[field.preferences?.reason_field_uuid] || t('digitally_signed_by') }} {{ submitter.name }}
<template v-if="submitter.email"> <template v-if="submitter.email">
&lt;{{ submitter.email }}&gt; &lt;{{ submitter.email }}&gt;
</template> </template>
@@ -258,6 +258,11 @@ export default {
required: false, required: false,
default: '' default: ''
}, },
values: {
type: Object,
required: false,
default: () => ({})
},
isActive: { isActive: {
type: Boolean, type: Boolean,
required: false, required: false,
+4 -3
View File
@@ -1,6 +1,6 @@
<template> <template>
<template <template
v-for="step in steps" v-for="(step, stepIndex) in steps"
:key="step[0].uuid" :key="step[0].uuid"
> >
<template <template
@@ -18,6 +18,7 @@
<FieldArea <FieldArea
:ref="setAreaRef" :ref="setAreaRef"
v-model="values[field.uuid]" v-model="values[field.uuid]"
:values="values"
:field="field" :field="field"
:area="area" :area="area"
:submittable="true" :submittable="true"
@@ -27,10 +28,10 @@
:with-field-placeholder="withFieldPlaceholder" :with-field-placeholder="withFieldPlaceholder"
:with-signature-id="withSignatureId" :with-signature-id="withSignatureId"
:is-active="currentStep === step" :is-active="currentStep === step"
:with-label="withLabel && !withFieldPlaceholder" :with-label="withLabel && !withFieldPlaceholder && step.length < 2"
:is-value-set="step.some((f) => f.uuid in values)" :is-value-set="step.some((f) => f.uuid in values)"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
@click="$emit('focus-step', step)" @click="$emit('focus-step', stepIndex)"
/> />
</Teleport> </Teleport>
</template> </template>
@@ -181,6 +181,10 @@ export default {
spread: 140 spread: 140
}) })
} }
if (window.decline_button) {
window.decline_button.setAttribute('disabled', 'true')
}
}, },
methods: { methods: {
sendCopyToEmail () { sendCopyToEmail () {
+9 -1
View File
@@ -43,6 +43,7 @@
:required="field.required" :required="field.required"
type="date" type="date"
:name="`values[${field.uuid}]`" :name="`values[${field.uuid}]`"
@keydown.enter="onEnter"
@focus="$emit('focus')" @focus="$emit('focus')"
> >
</div> </div>
@@ -78,7 +79,7 @@ export default {
default: '' default: ''
} }
}, },
emits: ['update:model-value', 'focus'], emits: ['update:model-value', 'focus', 'submit'],
computed: { computed: {
value: { value: {
set (value) { set (value) {
@@ -90,6 +91,13 @@ export default {
} }
}, },
methods: { methods: {
onEnter (e) {
if (this.modelValue) {
e.preventDefault()
this.$emit('submit')
}
},
setCurrentDate () { setCurrentDate () {
const inputEl = this.$refs.input const inputEl = this.$refs.input
+25 -9
View File
@@ -106,6 +106,7 @@
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:show-field-names="showFieldNames" :show-field-names="showFieldNames"
:field="currentField" :field="currentField"
@submit="submitStep"
@focus="scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
/> />
<div v-else-if="currentField.type === 'select'"> <div v-else-if="currentField.type === 'select'">
@@ -326,17 +327,20 @@
ref="currentStep" ref="currentStep"
:key="currentField.uuid" :key="currentField.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:reason="values[currentField.preferences?.reason_field_uuid]"
:field="currentField" :field="currentField"
:previous-value="previousSignatureValueFor(currentField) || previousSignatureValue" :previous-value="previousSignatureValueFor(currentField) || previousSignatureValue"
:with-typed-signature="withTypedSignature" :with-typed-signature="withTypedSignature"
:remember-signature="rememberSignature" :remember-signature="rememberSignature"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:require-signing-reason="requireSigningReason"
:button-text="buttonText" :button-text="buttonText"
:dry-run="dryRun" :dry-run="dryRun"
:with-disclosure="withDisclosure" :with-disclosure="withDisclosure"
:with-qr-button="withQrButton" :with-qr-button="withQrButton"
:submitter="submitter" :submitter="submitter"
:show-field-names="showFieldNames" :show-field-names="showFieldNames"
@update:reason="values[currentField.preferences?.reason_field_uuid] = $event"
@attached="attachments.push($event)" @attached="attachments.push($event)"
@start="scrollIntoField(currentField)" @start="scrollIntoField(currentField)"
@minimize="isFormVisible = false" @minimize="isFormVisible = false"
@@ -388,6 +392,7 @@
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:field="currentField" :field="currentField"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
:values="values"
@attached="attachments.push($event)" @attached="attachments.push($event)"
@focus="scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
@submit="submitStep" @submit="submitStep"
@@ -448,7 +453,7 @@
href="#" href="#"
class="inline border border-base-300 h-3 w-3 rounded-full mx-1 mt-1" class="inline border border-base-300 h-3 w-3 rounded-full mx-1 mt-1"
:class="{ 'bg-base-300': index === currentStep, 'bg-base-content': (index < currentStep && stepFields[index].every((f) => !f.required || ![null, undefined, ''].includes(values[f.uuid]))) || isCompleted, 'bg-white': index > currentStep }" :class="{ 'bg-base-300': index === currentStep, 'bg-base-content': (index < currentStep && stepFields[index].every((f) => !f.required || ![null, undefined, ''].includes(values[f.uuid]))) || isCompleted, 'bg-white': index > currentStep }"
@click.prevent="isCompleted ? '' : [saveStep(), goToStep(step, true)]" @click.prevent="isCompleted ? '' : [saveStep(), goToStep(index, true)]"
/> />
</div> </div>
</div> </div>
@@ -547,6 +552,11 @@ export default {
required: false, required: false,
default: '-80px' default: '-80px'
}, },
requireSigningReason: {
type: Boolean,
required: false,
default: false
},
canSendEmail: { canSendEmail: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -808,7 +818,7 @@ export default {
}, []) }, [])
}, },
formulaFields () { formulaFields () {
return this.fields.filter((f) => f.preferences?.formula) return this.fields.filter((f) => f.preferences?.formula && f.type !== 'payment')
}, },
attachmentsIndex () { attachmentsIndex () {
return this.attachments.reduce((acc, a) => { return this.attachments.reduce((acc, a) => {
@@ -913,18 +923,22 @@ export default {
checkFieldConditions (field) { checkFieldConditions (field) {
if (field.conditions?.length) { if (field.conditions?.length) {
return field.conditions.reduce((acc, c) => { return field.conditions.reduce((acc, c) => {
const field = this.fieldsUuidIndex[c.field_uuid]
if (['not_empty', 'checked', 'equal', 'contains'].includes(c.action) && field && !this.checkFieldConditions(field)) {
return false
}
if (['empty', 'unchecked'].includes(c.action)) { if (['empty', 'unchecked'].includes(c.action)) {
return acc && isEmpty(this.values[c.field_uuid]) return acc && isEmpty(this.values[c.field_uuid])
} else if (['not_empty', 'checked'].includes(c.action)) { } else if (['not_empty', 'checked'].includes(c.action)) {
return acc && !isEmpty(this.values[c.field_uuid]) return acc && !isEmpty(this.values[c.field_uuid])
} else if (['equal', 'contains'].includes(c.action)) { } else if (['equal', 'contains'].includes(c.action)) {
const field = this.fieldsUuidIndex[c.field_uuid]
const option = field.options.find((o) => o.uuid === c.value) const option = field.options.find((o) => o.uuid === c.value)
const values = [this.values[c.field_uuid]].flat() const values = [this.values[c.field_uuid]].flat()
return acc && values.includes(this.optionValue(option, field.options.indexOf(option))) return acc && values.includes(this.optionValue(option, field.options.indexOf(option)))
} else if (['not_equal', 'does_not_contain'].includes(c.action)) { } else if (['not_equal', 'does_not_contain'].includes(c.action)) {
const field = this.fieldsUuidIndex[c.field_uuid]
const option = field.options.find((o) => o.uuid === c.value) const option = field.options.find((o) => o.uuid === c.value)
const values = [this.values[c.field_uuid]].flat() const values = [this.values[c.field_uuid]].flat()
@@ -1014,8 +1028,8 @@ export default {
return null return null
} }
}, },
goToStep (step, scrollToArea = false, clickUpload = false) { goToStep (stepIndex, scrollToArea = false, clickUpload = false) {
this.currentStep = this.stepFields.indexOf(step) this.currentStep = stepIndex
this.showFillAllRequiredFields = false this.showFillAllRequiredFields = false
this.$nextTick(() => { this.$nextTick(() => {
@@ -1023,7 +1037,7 @@ export default {
if (!this.isCompleted) { if (!this.isCompleted) {
if (scrollToArea) { if (scrollToArea) {
this.scrollIntoField(step[0]) this.scrollIntoField(this.currentField)
} }
this.enableScrollIntoField = false this.enableScrollIntoField = false
@@ -1112,7 +1126,9 @@ export default {
if (response.status === 422 || response.status === 500) { if (response.status === 422 || response.status === 500) {
const data = await response.json() const data = await response.json()
alert(data.error || 'Value is invalid') const i18nError = data.error ? this.t(data.error.replace(/\s+/g, '_').toLowerCase()) : ''
alert(i18nError !== data.error ? i18nError : (data.error || this.t('value_is_invalid')))
return Promise.reject(new Error(data.error)) return Promise.reject(new Error(data.error))
} }
@@ -1128,7 +1144,7 @@ export default {
this.isFormVisible = false this.isFormVisible = false
} }
this.goToStep(nextStep, this.autoscrollFields) this.goToStep(this.stepFields.indexOf(nextStep), this.autoscrollFields)
if (emptyRequiredField === nextStep) { if (emptyRequiredField === nextStep) {
this.showFillAllRequiredFields = true this.showFillAllRequiredFields = true
+128
View File
@@ -6,9 +6,19 @@ const en = {
signature: 'Signature', signature: 'Signature',
initials: 'Initials', initials: 'Initials',
drawn_signature_on_a_touchscreen_device: 'Drawn signature on a touchscreen device', drawn_signature_on_a_touchscreen_device: 'Drawn signature on a touchscreen device',
approved: 'Approved',
reviewed: 'Reviewed',
other: 'Other',
authored_by_me: 'Authored by me',
approved_by: 'Approved by',
reviewed_by: 'Reviewed by',
authored_by: 'Authored by',
select_a_reason: 'Select a reason',
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scan the QR code with the camera app to open the form on mobile and draw your signature', scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scan the QR code with the camera app to open the form on mobile and draw your signature',
date: 'Date', date: 'Date',
number: 'Number', number: 'Number',
value_is_invalid: 'Value is invalid',
verification_code_is_invalid: 'Verification code is invalid',
already_paid: 'Already paid', already_paid: 'Already paid',
image: 'Image', image: 'Image',
pay: 'Pay', pay: 'Pay',
@@ -75,6 +85,16 @@ const en = {
} }
const es = { const es = {
approved: 'Aprobado',
reviewed: 'Revisado',
other: 'Otro',
authored_by_me: 'Escrito por mí',
approved_by: 'Aprobado por',
reviewed_by: 'Revisado por',
authored_by: 'Escrito por',
select_a_reason: 'Selecciona una razón',
value_is_invalid: 'El valor no es válido',
verification_code_is_invalid: 'El código de verificación no es válido',
drawn_signature_on_a_touchscreen_device: 'Firma dibujada en un dispositivo con pantalla táctil', drawn_signature_on_a_touchscreen_device: 'Firma dibujada en un dispositivo con pantalla táctil',
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Escanea el código QR con la aplicación de la cámara para abrir el formulario en el móvil y dibujar tu firma', scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Escanea el código QR con la aplicación de la cámara para abrir el formulario en el móvil y dibujar tu firma',
by_clicking_you_agree_to_the: 'Al hacer clic en "{button}", usted acepta el', by_clicking_you_agree_to_the: 'Al hacer clic en "{button}", usted acepta el',
@@ -150,6 +170,16 @@ const es = {
} }
const it = { const it = {
approved: 'Approvato',
reviewed: 'Revisionato',
other: 'Altro',
authored_by_me: 'Creato da me',
approved_by: 'Approvato da',
reviewed_by: 'Revisionato da',
authored_by: 'Creato da',
select_a_reason: 'Seleziona una ragione',
value_is_invalid: 'Il valore non è valido',
verification_code_is_invalid: 'Il codice di verifica non è valido',
drawn_signature_on_a_touchscreen_device: 'Firma disegnata su un dispositivo con schermo tattile', drawn_signature_on_a_touchscreen_device: 'Firma disegnata su un dispositivo con schermo tattile',
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scansiona il codice QR con l\'app della fotocamera per aprire il modulo sul cellulare e disegnare la tua firma', scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scansiona il codice QR con l\'app della fotocamera per aprire il modulo sul cellulare e disegnare la tua firma',
by_clicking_you_agree_to_the: 'Cliccando su "{button}", accetti il', by_clicking_you_agree_to_the: 'Cliccando su "{button}", accetti il',
@@ -225,6 +255,16 @@ const it = {
} }
const de = { const de = {
approved: 'Genehmigt',
reviewed: 'Überprüft',
other: 'Andere',
authored_by_me: 'Von mir verfasst',
approved_by: 'Genehmigt von',
reviewed_by: 'Überprüft von',
authored_by: 'Verfasst von',
select_a_reason: 'Grund auswählen',
value_is_invalid: 'Wert ist ungültig',
verification_code_is_invalid: 'Bestätigungscode ist ungültig',
drawn_signature_on_a_touchscreen_device: 'Gezeichnete Unterschrift auf einem Touchscreen-Gerät', drawn_signature_on_a_touchscreen_device: 'Gezeichnete Unterschrift auf einem Touchscreen-Gerät',
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scannen Sie den QR-Code mit der Kamera-App, um das Formular auf dem Handy zu öffnen und Ihre Unterschrift zu zeichnen', scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scannen Sie den QR-Code mit der Kamera-App, um das Formular auf dem Handy zu öffnen und Ihre Unterschrift zu zeichnen',
by_clicking_you_agree_to_the: 'Durch Klicken auf "{button}" stimmen Sie zu, dass Sie die', by_clicking_you_agree_to_the: 'Durch Klicken auf "{button}" stimmen Sie zu, dass Sie die',
@@ -300,6 +340,16 @@ const de = {
} }
const fr = { const fr = {
approved: 'Approuvé',
reviewed: 'Révisé',
other: 'Autre',
authored_by_me: 'Rédigé par moi',
approved_by: 'Approuvé par',
reviewed_by: 'Révisé par',
authored_by: 'Rédigé par',
select_a_reason: 'Sélectionnez une raison',
value_is_invalid: 'La valeur est invalide',
verification_code_is_invalid: 'Le code de vérification est invalide',
drawn_signature_on_a_touchscreen_device: 'Signature dessinée sur un appareil à écran tactile', drawn_signature_on_a_touchscreen_device: 'Signature dessinée sur un appareil à écran tactile',
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scannez le code QR avec l\'application de l\'appareil photo pour ouvrir le formulaire sur mobile et dessiner votre signature', scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scannez le code QR avec l\'application de l\'appareil photo pour ouvrir le formulaire sur mobile et dessiner votre signature',
by_clicking_you_agree_to_the: 'En cliquant sur "{button}", vous acceptez la', by_clicking_you_agree_to_the: 'En cliquant sur "{button}", vous acceptez la',
@@ -375,6 +425,16 @@ const fr = {
} }
const pl = { const pl = {
approved: 'Zaakceptowany',
reviewed: 'Przejrzany',
other: 'Inny',
authored_by_me: 'Napisane przeze mnie',
approved_by: 'Zaakceptowany przez',
reviewed_by: 'Przejrzany przez',
authored_by: 'Napisane przez',
select_a_reason: 'Wybierz powód',
value_is_invalid: 'Wartość jest nieprawidłowa',
verification_code_is_invalid: 'Kod weryfikacyjny jest nieprawidłowy',
drawn_signature_on_a_touchscreen_device: 'Podpis odręczny na urządzeniu z ekranem dotykowym', drawn_signature_on_a_touchscreen_device: 'Podpis odręczny na urządzeniu z ekranem dotykowym',
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Zeskanuj kod QR za pomocą aplikacji aparatu, aby otworzyć formularz na telefonie i narysować swój podpis', scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Zeskanuj kod QR za pomocą aplikacji aparatu, aby otworzyć formularz na telefonie i narysować swój podpis',
by_clicking_you_agree_to_the: 'Klikając na "{button}", zgadzasz się na', by_clicking_you_agree_to_the: 'Klikając na "{button}", zgadzasz się na',
@@ -450,6 +510,16 @@ const pl = {
} }
const uk = { const uk = {
approved: 'Затверджено',
reviewed: 'Переглянуто',
other: 'Інше',
authored_by_me: 'Авторство моє',
approved_by: 'Затверджено',
reviewed_by: 'Переглянуто',
authored_by: 'Автор',
select_a_reason: 'Виберіть причину',
value_is_invalid: 'Значення є неправильним',
verification_code_is_invalid: 'Код підтвердження є неправильним',
drawn_signature_on_a_touchscreen_device: 'Підпис на сенсорному пристрої', drawn_signature_on_a_touchscreen_device: 'Підпис на сенсорному пристрої',
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Скануйте QR-код за допомогою програми камери, щоб відкрити форму на мобільному пристрої та намалювати свій підпис', scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Скануйте QR-код за допомогою програми камери, щоб відкрити форму на мобільному пристрої та намалювати свій підпис',
by_clicking_you_agree_to_the: 'Натиснувши на "{button}", ви погоджуєтеся з', by_clicking_you_agree_to_the: 'Натиснувши на "{button}", ви погоджуєтеся з',
@@ -525,6 +595,16 @@ const uk = {
} }
const cs = { const cs = {
approved: 'Schváleno',
reviewed: 'Zkontrolováno',
other: 'Jiné',
authored_by_me: 'Autorem jsem já',
approved_by: 'Schváleno kým',
reviewed_by: 'Zkontrolováno kým',
authored_by: 'Autorem',
select_a_reason: 'Vyberte důvod',
value_is_invalid: 'Hodnota je neplatná',
verification_code_is_invalid: 'Ověřovací kód je neplatný',
drawn_signature_on_a_touchscreen_device: 'Namalovaný podpis na dotykovém zařízení', drawn_signature_on_a_touchscreen_device: 'Namalovaný podpis na dotykovém zařízení',
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Naskenujte QR kód pomocí aplikace fotoaparátu, abyste otevřeli formulář na mobilním zařízení a nakreslili svůj podpis', scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Naskenujte QR kód pomocí aplikace fotoaparátu, abyste otevřeli formulář na mobilním zařízení a nakreslili svůj podpis',
by_clicking_you_agree_to_the: 'Kliknutím na "{button}" souhlasíte s', by_clicking_you_agree_to_the: 'Kliknutím na "{button}" souhlasíte s',
@@ -600,6 +680,16 @@ const cs = {
} }
const pt = { const pt = {
approved: 'Aprovado',
reviewed: 'Revisado',
other: 'Outro',
authored_by_me: 'Autorizado por mim',
approved_by: 'Aprovado por',
reviewed_by: 'Revisado por',
authored_by: 'Autorizado por',
select_a_reason: 'Selecione um motivo',
value_is_invalid: 'Valor é inválido',
verification_code_is_invalid: 'Código de verificação é inválido',
drawn_signature_on_a_touchscreen_device: 'Assinatura desenhada em um dispositivo com tela sensível ao toque', drawn_signature_on_a_touchscreen_device: 'Assinatura desenhada em um dispositivo com tela sensível ao toque',
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Escaneie o código QR com o aplicativo da câmera para abrir o formulário no celular e desenhar sua assinatura', scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Escaneie o código QR com o aplicativo da câmera para abrir o formulário no celular e desenhar sua assinatura',
by_clicking_you_agree_to_the: 'Ao clicar em "{button}", você concorda com o', by_clicking_you_agree_to_the: 'Ao clicar em "{button}", você concorda com o',
@@ -675,6 +765,16 @@ const pt = {
} }
const he = { const he = {
approved: 'מאושר',
reviewed: 'נסקר',
other: 'אחר',
authored_by_me: 'נכתב על ידי',
approved_by: 'אושר על ידי',
reviewed_by: 'נסקר על ידי',
authored_by: 'נכתב על ידי',
select_a_reason: 'בחר סיבה',
value_is_invalid: 'ערך לא תקין',
verification_code_is_invalid: 'קוד האימות אינו תקין',
drawn_signature_on_a_touchscreen_device: 'חתימה שנוצרה במכשיר עם מסך מגע', drawn_signature_on_a_touchscreen_device: 'חתימה שנוצרה במכשיר עם מסך מגע',
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'סרוק את קוד ה-QR באמצעות אפליקציית המצלמה כדי לפתוח את הטופס במובייל ולצייר את החתימה שלך', scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'סרוק את קוד ה-QR באמצעות אפליקציית המצלמה כדי לפתוח את הטופס במובייל ולצייר את החתימה שלך',
by_clicking_you_agree_to_the: 'על ידי לחיצה על "{button}", אתה מסכים ל', by_clicking_you_agree_to_the: 'על ידי לחיצה על "{button}", אתה מסכים ל',
@@ -751,6 +851,16 @@ const he = {
} }
const nl = { const nl = {
approved: 'Goedgekeurd',
reviewed: 'Beoordeeld',
other: 'Anders',
authored_by_me: 'Door mij geschreven',
approved_by: 'Goedgekeurd door',
reviewed_by: 'Beoordeeld door',
authored_by: 'Geschreven door',
select_a_reason: 'Selecteer een reden',
value_is_invalid: 'Waarde is ongeldig',
verification_code_is_invalid: 'Verificatiecode is ongeldig',
drawn_signature_on_a_touchscreen_device: 'Getekende handtekening op een apparaat met een touchscreen', drawn_signature_on_a_touchscreen_device: 'Getekende handtekening op een apparaat met een touchscreen',
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scan de QR-code met de camera-app om het formulier op mobiel te openen en uw handtekening te tekenen', scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scan de QR-code met de camera-app om het formulier op mobiel te openen en uw handtekening te tekenen',
by_clicking_you_agree_to_the: 'Door op "{button}" te klikken, gaat u akkoord met de', by_clicking_you_agree_to_the: 'Door op "{button}" te klikken, gaat u akkoord met de',
@@ -827,6 +937,16 @@ const nl = {
} }
const ar = { const ar = {
approved: 'موافق عليه',
reviewed: 'تمت مراجعته',
other: 'آخر',
authored_by_me: 'مؤلف بواسطتي',
approved_by: 'موافق عليه من قبل',
reviewed_by: 'مراجع من قبل',
authored_by: 'مؤلف من قبل',
select_a_reason: 'اختر سببًا',
value_is_invalid: 'القيمة غير صالحة',
verification_code_is_invalid: 'رمز التحقق غير صالح',
drawn_signature_on_a_touchscreen_device: 'توقيع مرسوم على جهاز بشاشة تعمل باللمس', drawn_signature_on_a_touchscreen_device: 'توقيع مرسوم على جهاز بشاشة تعمل باللمس',
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'امسح رمز الاستجابة السريعة باستخدام تطبيق الكاميرا لفتح النموذج على الهاتف المحمول ورسم توقيعك', scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'امسح رمز الاستجابة السريعة باستخدام تطبيق الكاميرا لفتح النموذج على الهاتف المحمول ورسم توقيعك',
by_clicking_you_agree_to_the: 'بالنقر فوق "{button}"، أنت توافق على', by_clicking_you_agree_to_the: 'بالنقر فوق "{button}"، أنت توافق على',
@@ -902,6 +1022,14 @@ const ar = {
} }
const ko = { const ko = {
approved: '승인됨',
reviewed: '검토됨',
other: '기타',
authored_by_me: '내가 작성한',
approved_by: '승인자',
reviewed_by: '검토자',
authored_by: '작성자',
select_a_reason: '이유 선택',
drawn_signature_on_a_touchscreen_device: '터치스크린 장치에서 그린 서명', drawn_signature_on_a_touchscreen_device: '터치스크린 장치에서 그린 서명',
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: '카메라 앱으로 QR 코드를 스캔하여 모바일에서 양식을 열고 서명을 그리세요', scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: '카메라 앱으로 QR 코드를 스캔하여 모바일에서 양식을 열고 서명을 그리세요',
by_clicking_you_agree_to_the: '"{button}"를 클릭함으로써, 다음에 동의하게 됩니다', by_clicking_you_agree_to_the: '"{button}"를 클릭함으로써, 다음에 동의하게 됩니다',
@@ -50,6 +50,26 @@
</span> </span>
</a> </a>
</span> </span>
<span
class="tooltip"
:data-tip="t('click_to_upload')"
>
<label
class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap"
>
<IconUpload :width="16" />
<input
:key="uploadImageInputKey"
type="file"
hidden
accept="image/*"
@change="drawImage"
>
<span class="hidden sm:inline">
{{ t('upload') }}
</span>
</label>
</span>
<a <a
v-if="modelValue || computedPreviousValue" v-if="modelValue || computedPreviousValue"
href="#" href="#"
@@ -97,7 +117,7 @@
<img <img
v-if="modelValue || computedPreviousValue" v-if="modelValue || computedPreviousValue"
:src="attachmentsIndex[modelValue || computedPreviousValue].url" :src="attachmentsIndex[modelValue || computedPreviousValue].url"
class="mx-auto bg-white border border-base-300 rounded max-h-72" class="mx-auto bg-white border border-base-300 rounded max-h-44"
> >
<canvas <canvas
v-show="!modelValue && !computedPreviousValue" v-show="!modelValue && !computedPreviousValue"
@@ -120,16 +140,18 @@
<script> <script>
import { cropCanvasAndExportToPNG } from './crop_canvas' import { cropCanvasAndExportToPNG } from './crop_canvas'
import { IconReload, IconTextSize, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue' import { IconReload, IconTextSize, IconUpload, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
import SignaturePad from 'signature_pad' import SignaturePad from 'signature_pad'
import AppearsOn from './appears_on' import AppearsOn from './appears_on'
import MarkdownContent from './markdown_content' import MarkdownContent from './markdown_content'
import SignatureStep from './signature_step'
const scale = 3 const scale = 3
export default { export default {
name: 'InitialsStep', name: 'InitialsStep',
components: { components: {
IconUpload,
AppearsOn, AppearsOn,
IconReload, IconReload,
IconTextSize, IconTextSize,
@@ -178,7 +200,8 @@ export default {
return { return {
isInitialsStarted: !!this.previousValue, isInitialsStarted: !!this.previousValue,
isUsePreviousValue: true, isUsePreviousValue: true,
isDrawInitials: false isDrawInitials: false,
uploadImageInputKey: Math.random().toString()
} }
}, },
computed: { computed: {
@@ -229,6 +252,15 @@ export default {
this.intersectionObserver?.disconnect() this.intersectionObserver?.disconnect()
}, },
methods: { methods: {
drawOnCanvas: SignatureStep.methods.drawOnCanvas,
drawImage (event) {
this.remove()
this.isInitialsStarted = true
this.drawOnCanvas(event.target.files[0], this.$refs.canvas)
this.uploadImageInputKey = Math.random().toString()
},
remove () { remove () {
this.$emit('update:model-value', '') this.$emit('update:model-value', '')
@@ -51,7 +51,17 @@ export default {
}, },
computed: { computed: {
items () { items () {
return snarkdown(this.string.replace(/\n/g, '<br>')).split(htmlSplitRegexp) const linkParts = this.string.split(/(https?:\/\/[^\s)]+)/g)
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)
} }
}, },
methods: { methods: {
@@ -93,6 +93,10 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
values: {
type: Object,
required: true
},
submitterSlug: { submitterSlug: {
type: String, type: String,
required: true required: true
@@ -101,7 +105,8 @@ export default {
emits: ['focus', 'submit', 'update:model-value', 'attached'], emits: ['focus', 'submit', 'update:model-value', 'attached'],
data () { data () {
return { return {
isCreatingCheckout: false isCreatingCheckout: false,
isMathLoaded: false
} }
}, },
computed: { computed: {
@@ -114,15 +119,23 @@ export default {
defaultName () { defaultName () {
const { price, currency } = this.field.preferences || {} const { price, currency } = this.field.preferences || {}
const formattedPrice = new Intl.NumberFormat([], { const formatter = new Intl.NumberFormat([], {
style: 'currency', style: 'currency',
currency currency
}).format(price) })
return this.t('pay') + ' ' + formattedPrice if (this.field.preferences?.formula) {
if (this.isMathLoaded) {
return this.t('pay') + ' ' + formatter.format(this.calculateFormula())
} else {
return ''
}
} else {
return this.t('pay') + ' ' + formatter.format(price)
}
} }
}, },
mounted () { async mounted () {
if (this.sessionId) { if (this.sessionId) {
this.$emit('submit') this.$emit('submit')
} }
@@ -130,8 +143,48 @@ export default {
if (!this.sessionId) { if (!this.sessionId) {
this.postCheckout({ checkStatus: true }) this.postCheckout({ checkStatus: true })
} }
if (this.field.preferences?.formula) {
const {
create,
evaluateDependencies,
addDependencies,
subtractDependencies,
divideDependencies,
multiplyDependencies,
powDependencies,
roundDependencies,
absDependencies,
sinDependencies,
tanDependencies,
cosDependencies
} = await import('mathjs')
this.math = create({
evaluateDependencies,
addDependencies,
subtractDependencies,
divideDependencies,
multiplyDependencies,
powDependencies,
roundDependencies,
absDependencies,
sinDependencies,
tanDependencies,
cosDependencies
})
this.isMathLoaded = true
}
}, },
methods: { methods: {
calculateFormula () {
const transformedFormula = this.field.preferences.formula.replace(/{{(.*?)}}/g, (match, uuid) => {
return this.values[uuid] || 0.0
})
return this.math.evaluate(transformedFormula.toLowerCase())
},
async submit () { async submit () {
if (this.sessionId) { if (this.sessionId) {
return fetch(this.baseUrl + '/api/stripe_payments/' + this.sessionId, { return fetch(this.baseUrl + '/api/stripe_payments/' + this.sessionId, {
@@ -194,6 +194,48 @@
type="text" type="text"
@input="updateWrittenSignature" @input="updateWrittenSignature"
> >
<select
v-if="requireSigningReason && !isOtherReason"
class="select base-input !text-2xl w-full mt-6 text-center"
required
:name="`values[${field.preferences.reason_field_uuid}]`"
@change="$event.target.value === 'other' ? [reason = '', isOtherReason = true] : $emit('update:reason', $event.target.value)"
>
<option
value=""
disabled
:selected="!reason"
>
{{ t('select_a_reason') }}
</option>
<option
v-for="(label, option) in defaultReasons"
:key="option"
:value="option"
:selected="reason === option"
>
{{ label }}
</option>
<option value="other">
{{ t('other') }}
</option>
</select>
<input
v-if="requireSigningReason && isOtherReason"
class="base-input !text-2xl w-full mt-6"
required
:name="`values[${field.preferences.reason_field_uuid}]`"
:placeholder="t('type_here_')"
:value="reason"
type="text"
@input="$emit('update:reason', $event.target.value)"
>
<input
v-if="requireSigningReason"
hidden
name="with_reason"
:value="field.preferences.reason_field_uuid"
>
<div <div
v-if="isShowQr" v-if="isShowQr"
dir="auto" dir="auto"
@@ -231,6 +273,7 @@ import { cropCanvasAndExportToPNG } from './crop_canvas'
import SignaturePad from 'signature_pad' import SignaturePad from 'signature_pad'
import AppearsOn from './appears_on' import AppearsOn from './appears_on'
import MarkdownContent from './markdown_content' import MarkdownContent from './markdown_content'
import { v4 } from 'uuid'
let isFontLoaded = false let isFontLoaded = false
@@ -255,6 +298,11 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
requireSigningReason: {
type: Boolean,
required: false,
default: false
},
submitter: { submitter: {
type: Object, type: Object,
required: true required: true
@@ -304,17 +352,23 @@ export default {
required: false, required: false,
default: '' default: ''
}, },
reason: {
type: String,
required: false,
default: ''
},
modelValue: { modelValue: {
type: String, type: String,
required: false, required: false,
default: '' default: ''
} }
}, },
emits: ['attached', 'update:model-value', 'start', 'minimize'], emits: ['attached', 'update:model-value', 'start', 'minimize', 'update:reason'],
data () { data () {
return { return {
isSignatureStarted: !!this.previousValue, isSignatureStarted: !!this.previousValue,
isShowQr: false, isShowQr: false,
isOtherReason: false,
isUsePreviousValue: true, isUsePreviousValue: true,
isTextSignature: this.field.preferences?.format === 'typed', isTextSignature: this.field.preferences?.format === 'typed',
uploadImageInputKey: Math.random().toString() uploadImageInputKey: Math.random().toString()
@@ -324,6 +378,13 @@ export default {
submitterSlug () { submitterSlug () {
return this.submitter.slug return this.submitter.slug
}, },
defaultReasons () {
return {
[this.t('approved_by')]: this.t('approved'),
[this.t('reviewed_by')]: this.t('reviewed'),
[this.t('authored_by')]: this.t('authored_by_me')
}
},
computedPreviousValue () { computedPreviousValue () {
if (this.isUsePreviousValue) { if (this.isUsePreviousValue) {
return this.previousValue return this.previousValue
@@ -332,6 +393,13 @@ export default {
} }
} }
}, },
created () {
if (this.requireSigningReason) {
this.field.preferences ||= {}
this.field.preferences.reason_field_uuid ||= v4()
this.isOtherReason = this.reason && !this.defaultReasons[this.reason]
}
},
async mounted () { async mounted () {
this.$nextTick(() => { this.$nextTick(() => {
if (this.$refs.canvas) { if (this.$refs.canvas) {
@@ -501,8 +569,11 @@ export default {
this.remove() this.remove()
this.isSignatureStarted = true this.isSignatureStarted = true
const file = event.target.files[0] this.drawOnCanvas(event.target.files[0], this.$refs.canvas)
this.uploadImageInputKey = Math.random().toString()
},
drawOnCanvas (file, canvas) {
if (file && file.type.match('image.*')) { if (file && file.type.match('image.*')) {
const reader = new FileReader() const reader = new FileReader()
@@ -512,7 +583,6 @@ export default {
img.src = event.target.result img.src = event.target.result
img.onload = () => { img.onload = () => {
const canvas = this.$refs.canvas
const context = canvas.getContext('2d') const context = canvas.getContext('2d')
const aspectRatio = img.width / img.height const aspectRatio = img.width / img.height
@@ -547,8 +617,6 @@ export default {
} }
reader.readAsDataURL(file) reader.readAsDataURL(file)
this.uploadImageInputKey = Math.random().toString()
} }
}, },
maybeSetSignedUuid (signedUuid) { maybeSetSignedUuid (signedUuid) {
+35 -3
View File
@@ -58,6 +58,7 @@
/> />
<template v-else> <template v-else>
<a <a
v-if="withSignYourselfButton"
:href="template.submitters.length > 1 ? `/templates/${template.id}/submissions/new?selfsign=true` : `/d/${template.slug}`" :href="template.submitters.length > 1 ? `/templates/${template.id}/submissions/new?selfsign=true` : `/d/${template.slug}`"
class="btn btn-primary btn-ghost text-base hidden md:flex" class="btn btn-primary btn-ghost text-base hidden md:flex"
:target="template.submitters.length > 1 ? '' : '_blank'" :target="template.submitters.length > 1 ? '' : '_blank'"
@@ -73,6 +74,7 @@
</span> </span>
</a> </a>
<a <a
v-if="withSendButton"
:href="`/templates/${template.id}/submissions/new?with_link=true`" :href="`/templates/${template.id}/submissions/new?with_link=true`"
data-turbo-frame="modal" data-turbo-frame="modal"
class="white-button md:!px-6" class="white-button md:!px-6"
@@ -435,6 +437,7 @@ export default {
save: this.save, save: this.save,
t: this.t, t: this.t,
currencies: this.currencies, currencies: this.currencies,
locale: this.locale,
baseFetch: this.baseFetch, baseFetch: this.baseFetch,
fieldTypes: this.fieldTypes, fieldTypes: this.fieldTypes,
backgroundColor: this.backgroundColor, backgroundColor: this.backgroundColor,
@@ -468,11 +471,26 @@ export default {
required: false, required: false,
default: '' default: ''
}, },
locale: {
type: String,
required: false,
default: ''
},
editable: { editable: {
type: Boolean, type: Boolean,
required: false, required: false,
default: true default: true
}, },
withSendButton: {
type: Boolean,
required: false,
default: true
},
withSignYourselfButton: {
type: Boolean,
required: false,
default: true
},
inputMode: { inputMode: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -649,6 +667,20 @@ export default {
computed: { computed: {
selectedAreaRef: () => ref(), selectedAreaRef: () => ref(),
fieldsDragFieldRef: () => ref(), fieldsDragFieldRef: () => ref(),
defaultDateFormat () {
const isUsBrowser = Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US')
const isUsTimezone = new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }).format(new Date()).match(/\s(?:CST|CDT|PST|PDT|EST|EDT)$/)
return this.localeDateFormats[this.locale] || ((isUsBrowser || isUsTimezone) ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
},
localeDateFormats () {
return {
'de-DE': 'DD.MM.YYYY',
'fr-FR': 'DD/MM/YYYY',
'it-IT': 'DD/MM/YYYY',
'es-ES': 'DD/MM/YYYY'
}
},
fieldAreasIndex () { fieldAreasIndex () {
const areas = {} const areas = {}
@@ -768,7 +800,7 @@ export default {
if (type === 'date') { if (type === 'date') {
field.preferences = { field.preferences = {
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') || new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }).format(new Date()).match(/\s(?:CST|CDT|PST|PDT|EST|EDT)$/) ? 'MM/DD/YYYY' : 'DD/MM/YYYY' format: this.defaultDateFormat
} }
} }
@@ -801,7 +833,7 @@ export default {
if (type === 'date') { if (type === 'date') {
field.preferences = { field.preferences = {
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') || new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }).format(new Date()).match(/\s(?:CST|CDT|PST|PDT|EST|EDT)$/) ? 'MM/DD/YYYY' : 'DD/MM/YYYY' format: this.defaultDateFormat
} }
} }
@@ -1138,7 +1170,7 @@ export default {
if (field.type === 'date') { if (field.type === 'date') {
field.preferences = { field.preferences = {
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') || new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }).format(new Date()).match(/\s(?:CST|CDT|PST|PDT|EST|EDT)$/) ? 'MM/DD/YYYY' : 'DD/MM/YYYY' format: this.defaultDateFormat
} }
} }
} }
+10 -3
View File
@@ -94,6 +94,7 @@
:field="field" :field="field"
@click-condition="isShowConditionsModal = true" @click-condition="isShowConditionsModal = true"
@click-description="isShowDescriptionModal = true" @click-description="isShowDescriptionModal = true"
@click-formula="isShowFormulaModal = true"
/> />
<span <span
v-else-if="field.type !== 'heading'" v-else-if="field.type !== 'heading'"
@@ -129,6 +130,7 @@
@click-description="isShowDescriptionModal = true" @click-description="isShowDescriptionModal = true"
@click-condition="isShowConditionsModal = true" @click-condition="isShowConditionsModal = true"
@set-draw="$emit('set-draw', $event)" @set-draw="$emit('set-draw', $event)"
@remove-area="removeArea"
@scroll-to="$emit('scroll-to', $event)" @scroll-to="$emit('scroll-to', $event)"
/> />
</ul> </ul>
@@ -280,7 +282,7 @@ export default {
IconMathFunction, IconMathFunction,
FieldType FieldType
}, },
inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef', 't'], inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef', 't', 'locale'],
props: { props: {
field: { field: {
type: Object, type: Object,
@@ -340,12 +342,17 @@ export default {
if (this.field.type === 'date') { if (this.field.type === 'date') {
this.field.preferences.format ||= this.field.preferences.format ||=
(Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') || new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }).format(new Date()).match(/\s(?:CST|CDT|PST|PDT|EST|EDT)$/) ? 'MM/DD/YYYY' : 'DD/MM/YYYY') ({ 'de-DE': 'DD.MM.YYYY' }[this.locale] || ((Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') || new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }).format(new Date()).match(/\s(?:CST|CDT|PST|PDT|EST|EDT)$/)) ? 'MM/DD/YYYY' : 'DD/MM/YYYY'))
} }
}, },
methods: { methods: {
removeArea (area) {
this.field.areas.splice(this.field.areas.indexOf(area), 1)
this.save()
},
buildDefaultName (field, fields) { buildDefaultName (field, fields) {
if (field.type === 'payment' && field.preferences?.price) { if (field.type === 'payment' && field.preferences?.price && !field.preferences?.formula) {
const { price, currency } = field.preferences || {} const { price, currency } = field.preferences || {}
const formattedPrice = new Intl.NumberFormat([], { const formattedPrice = new Intl.NumberFormat([], {
@@ -358,7 +358,7 @@
> >
<a <a
href="#" href="#"
class="text-sm py-1 px-2" class="text-sm py-1 px-2 group/1"
@click.prevent="$emit('scroll-to', area)" @click.prevent="$emit('scroll-to', area)"
> >
<IconShape <IconShape
@@ -367,6 +367,11 @@
/> />
{{ t('page') }} {{ t('page') }}
<template v-if="template.schema.length > 1">{{ template.schema.findIndex((item) => item.attachment_uuid === area.attachment_uuid) + 1 }}-</template>{{ area.page + 1 }} <template v-if="template.schema.length > 1">{{ template.schema.findIndex((item) => item.attachment_uuid === area.attachment_uuid) + 1 }}-</template>{{ area.page + 1 }}
<IconX
:width="12"
class="group-hover/1:inline hidden"
@click.prevent.stop="[$emit('remove-area', area), $event.target.closest('.dropdown').querySelector('label').focus()]"
/>
</a> </a>
</li> </li>
<li v-if="!field.areas?.length || !['radio', 'multiple'].includes(field.type)"> <li v-if="!field.areas?.length || !['radio', 'multiple'].includes(field.type)">
@@ -399,7 +404,7 @@
</template> </template>
<script> <script>
import { IconRouteAltLeft, IconShape, IconMathFunction, IconNewSection, IconInfoCircle, IconCopy } from '@tabler/icons-vue' import { IconRouteAltLeft, IconShape, IconX, IconMathFunction, IconNewSection, IconInfoCircle, IconCopy } from '@tabler/icons-vue'
export default { export default {
name: 'FieldSettings', name: 'FieldSettings',
@@ -409,7 +414,8 @@ export default {
IconMathFunction, IconMathFunction,
IconRouteAltLeft, IconRouteAltLeft,
IconCopy, IconCopy,
IconNewSection IconNewSection,
IconX
}, },
inject: ['template', 'save', 't'], inject: ['template', 'save', 't'],
props: { props: {
@@ -443,7 +449,7 @@ export default {
default: true default: true
} }
}, },
emits: ['set-draw', 'scroll-to', 'click-formula', 'click-description', 'click-condition'], emits: ['set-draw', 'scroll-to', 'click-formula', 'click-description', 'click-condition', 'remove-area'],
data () { data () {
return { return {
} }
@@ -204,7 +204,10 @@ export default {
alert('Some fields are missing in the formula.') alert('Some fields are missing in the formula.')
} else { } else {
this.field.preferences.formula = normalizedFormula this.field.preferences.formula = normalizedFormula
this.field.readonly = !!normalizedFormula
if (this.field.type !== 'payment') {
this.field.readonly = !!normalizedFormula
}
this.save() this.save()
+1
View File
@@ -17,6 +17,7 @@ const en = {
display_title: 'Display title', display_title: 'Display title',
with_logo: 'With logo', with_logo: 'With logo',
unchecked: 'Unchecked', unchecked: 'Unchecked',
price: 'Price',
type_value: 'Type value', type_value: 'Type value',
equal: 'Equal', equal: 'Equal',
not_equal: 'Not equal', not_equal: 'Not equal',
@@ -52,7 +52,7 @@
</template> </template>
<template v-else> <template v-else>
<template <template
v-for="(icon, type) in fieldIcons" v-for="(icon, type) in fieldIconsSorted"
:key="type" :key="type"
> >
<li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')"> <li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')">
@@ -1,7 +1,7 @@
<template> <template>
<span <span
class="dropdown dropdown-end" class="dropdown dropdown-end"
:class="{ 'dropdown-open': (!field.preferences?.price || !isConnected) && !isLoading }" :class="{ 'dropdown-open': ((!field.preferences?.price && !field.preferences?.formula) || !isConnected) && !isLoading }"
> >
<label <label
tabindex="0" tabindex="0"
@@ -26,7 +26,7 @@
> >
<select <select
v-model="field.preferences.currency" v-model="field.preferences.currency"
placeholder="Price" :placeholder="t('price')"
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0" class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0"
@change="save" @change="save"
> >
@@ -51,14 +51,23 @@
@click.stop @click.stop
> >
<input <input
v-if="field.preferences.formula"
type="number"
:placeholder="t('price')"
disabled="true"
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
@blur="save"
>
<input
v-else
v-model="field.preferences.price" v-model="field.preferences.price"
type="number" type="number"
placeholder="Price" :placeholder="t('price')"
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0" class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
@blur="save" @blur="save"
> >
<label <label
v-if="field.preferences.price" v-if="field.preferences.price && !field.preferences.formula"
:style="{ backgroundColor: backgroundColor }" :style="{ backgroundColor: backgroundColor }"
class="absolute -top-1 left-2.5 px-1 h-4" class="absolute -top-1 left-2.5 px-1 h-4"
style="font-size: 8px" style="font-size: 8px"
@@ -149,8 +158,21 @@
data-turbo="false" data-turbo="false"
>Learn more</a> >Learn more</a>
</div> </div>
<li class="mb-1">
<label
class="label-text cursor-pointer text-center w-full flex items-center"
@click="$emit('click-formula')"
>
<IconMathFunction
width="18"
/>
<span class="text-sm">
{{ t('formula') }}
</span>
</label>
</li>
<hr> <hr>
<li class="mt-1"> <li>
<label <label
class="label-text cursor-pointer text-center w-full flex items-center" class="label-text cursor-pointer text-center w-full flex items-center"
@click="$emit('click-description')" @click="$emit('click-description')"
@@ -181,7 +203,7 @@
</template> </template>
<script> <script>
import { IconSettings, IconCircleCheck, IconInfoCircle, IconBrandStripe, IconInnerShadowTop, IconRouteAltLeft } from '@tabler/icons-vue' import { IconMathFunction, IconSettings, IconCircleCheck, IconInfoCircle, IconBrandStripe, IconInnerShadowTop, IconRouteAltLeft } from '@tabler/icons-vue'
import { ref } from 'vue' import { ref } from 'vue'
const isConnected = ref(false) const isConnected = ref(false)
@@ -193,6 +215,7 @@ export default {
IconCircleCheck, IconCircleCheck,
IconRouteAltLeft, IconRouteAltLeft,
IconInfoCircle, IconInfoCircle,
IconMathFunction,
IconInnerShadowTop, IconInnerShadowTop,
IconBrandStripe IconBrandStripe
}, },
@@ -203,7 +226,7 @@ export default {
required: true required: true
} }
}, },
emits: ['click-condition', 'click-description'], emits: ['click-condition', 'click-description', 'click-formula'],
data () { data () {
return { return {
isLoading: false isLoading: false
+25 -8
View File
@@ -15,15 +15,19 @@ class ProcessSubmitterCompletionJob
Submissions::EnsureResultGenerated.call(submitter) Submissions::EnsureResultGenerated.call(submitter)
if is_all_completed && submitter.completed_at == submitter.submission.submitters.maximum(:completed_at) if is_all_completed && submitter.completed_at == submitter.submission.submitters.maximum(:completed_at)
if submitter.submission.account.account_configs.exists?(key: AccountConfig::COMBINE_PDF_RESULT_KEY, value: true)
Submissions::GenerateCombinedAttachment.call(submitter)
end
Submissions::GenerateAuditTrail.call(submitter.submission) Submissions::GenerateAuditTrail.call(submitter.submission)
enqueue_completed_emails(submitter) enqueue_completed_emails(submitter)
end end
enqueue_completed_webhooks(submitter) enqueue_completed_webhooks(submitter, is_all_completed:)
end end
def enqueue_completed_webhooks(submitter) def enqueue_completed_webhooks(submitter, is_all_completed: false)
webhook_config = Accounts.load_webhook_config(submitter.account) webhook_config = Accounts.load_webhook_config(submitter.account)
if webhook_config if webhook_config
@@ -31,13 +35,26 @@ class ProcessSubmitterCompletionJob
'encrypted_config_id' => webhook_config.id }) 'encrypted_config_id' => webhook_config.id })
end end
webhook_ids = submitter.account.webhook_urls.where( webhook_urls = submitter.account.webhook_urls
Arel::Table.new(:webhook_urls)[:events].matches('%"form.completed"%')
).pluck(:id)
webhook_ids.each do |webhook_id| webhook_urls = webhook_urls.where(
SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id, Arel::Table.new(:webhook_urls)[:events].matches('%"form.completed"%')
'webhook_url_id' => webhook_id }) ).or(
webhook_urls.where(
Arel::Table.new(:webhook_urls)[:events].matches('%"submission.completed"%')
)
)
webhook_urls.each do |webhook|
if webhook.events.include?('form.completed')
SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id,
'webhook_url_id' => webhook.id })
end
if webhook.events.include?('submission.completed') && is_all_completed
SendSubmissionCompletedWebhookRequestJob.perform_async({ 'submission_id' => submitter.submission_id,
'webhook_url_id' => webhook.id })
end
end end
end end
@@ -14,7 +14,7 @@ class SendFormCompletedWebhookRequestJob
attempt = params['attempt'].to_i attempt = params['attempt'].to_i
url = load_url(submitter, params) url, secret = load_url_and_secret(submitter, params)
return if url.blank? return if url.blank?
@@ -29,6 +29,7 @@ class SendFormCompletedWebhookRequestJob
timestamp: Time.current, timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter) data: Submitters::SerializeForWebhook.call(submitter)
}.to_json, }.to_json,
**secret.to_h,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT) 'User-Agent' => USER_AGENT)
rescue Faraday::Error rescue Faraday::Error
@@ -45,9 +46,11 @@ class SendFormCompletedWebhookRequestJob
end end
end end
def load_url(submitter, params) def load_url_and_secret(submitter, params)
if params['encrypted_config_id'] if params['encrypted_config_id']
url = EncryptedConfig.find(params['encrypted_config_id']).value config = EncryptedConfig.find(params['encrypted_config_id'])
url = config.value
return if url.blank? return if url.blank?
@@ -55,7 +58,10 @@ class SendFormCompletedWebhookRequestJob
return if preferences['form.completed'] == false return if preferences['form.completed'] == false
url secret = EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h
[url, secret]
elsif params['webhook_url_id'] elsif params['webhook_url_id']
webhook_url = submitter.account.webhook_urls.find(params['webhook_url_id']) webhook_url = submitter.account.webhook_urls.find(params['webhook_url_id'])
@@ -0,0 +1,51 @@
# frozen_string_literal: true
class SendFormDeclinedWebhookRequestJob
include Sidekiq::Job
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
attempt = params['attempt'].to_i
config = Accounts.load_webhook_config(submitter.submission.account)
url = config&.value.presence
return if url.blank?
preferences = Accounts.load_webhook_preferences(submitter.submission.account)
return if preferences['form.declined'] == false
ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin
Faraday.post(url,
{
event_type: 'form.declined',
timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter)
}.to_json,
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
nil
end
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
SendFormDeclinedWebhookRequestJob.perform_in((2**attempt).minutes, {
'submitter_id' => submitter.id,
'attempt' => attempt + 1,
'last_status' => resp&.status.to_i
})
end
end
end
@@ -13,7 +13,8 @@ class SendFormStartedWebhookRequestJob
submitter = Submitter.find(params['submitter_id']) submitter = Submitter.find(params['submitter_id'])
attempt = params['attempt'].to_i attempt = params['attempt'].to_i
url = Accounts.load_webhook_url(submitter.submission.account) config = Accounts.load_webhook_config(submitter.submission.account)
url = config&.value.presence
return if url.blank? return if url.blank?
@@ -30,6 +31,8 @@ class SendFormStartedWebhookRequestJob
timestamp: Time.current, timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter) data: Submitters::SerializeForWebhook.call(submitter)
}.to_json, }.to_json,
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT) 'User-Agent' => USER_AGENT)
rescue Faraday::Error rescue Faraday::Error
@@ -13,7 +13,8 @@ class SendFormViewedWebhookRequestJob
submitter = Submitter.find(params['submitter_id']) submitter = Submitter.find(params['submitter_id'])
attempt = params['attempt'].to_i attempt = params['attempt'].to_i
url = Accounts.load_webhook_url(submitter.submission.account) config = Accounts.load_webhook_config(submitter.submission.account)
url = config&.value.presence
return if url.blank? return if url.blank?
@@ -30,6 +31,8 @@ class SendFormViewedWebhookRequestJob
timestamp: Time.current, timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter) data: Submitters::SerializeForWebhook.call(submitter)
}.to_json, }.to_json,
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT) 'User-Agent' => USER_AGENT)
rescue Faraday::Error rescue Faraday::Error
@@ -13,7 +13,9 @@ class SendSubmissionArchivedWebhookRequestJob
submission = Submission.find(params['submission_id']) submission = Submission.find(params['submission_id'])
attempt = params['attempt'].to_i attempt = params['attempt'].to_i
url = Accounts.load_webhook_url(submission.account)
config = Accounts.load_webhook_config(submission.account)
url = config&.value.presence
return if url.blank? return if url.blank?
@@ -28,6 +30,8 @@ class SendSubmissionArchivedWebhookRequestJob
timestamp: Time.current, timestamp: Time.current,
data: submission.as_json(only: %i[id archived_at]) data: submission.as_json(only: %i[id archived_at])
}.to_json, }.to_json,
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT) 'User-Agent' => USER_AGENT)
rescue Faraday::Error rescue Faraday::Error
@@ -0,0 +1,47 @@
# frozen_string_literal: true
class SendSubmissionCompletedWebhookRequestJob
include Sidekiq::Job
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
submission = Submission.find(params['submission_id'])
attempt = params['attempt'].to_i
webhook_url = submission.account.webhook_urls.find(params['webhook_url_id'])
url = webhook_url.url if webhook_url.events.include?('submission.completed')
return if url.blank?
resp = begin
Faraday.post(url,
{
event_type: 'submission.completed',
timestamp: Time.current,
data: Submissions::SerializeForApi.call(submission)
}.to_json,
**EncryptedConfig.find_or_initialize_by(account_id: submission.account_id,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
nil
end
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
SendSubmissionCompletedWebhookRequestJob.perform_in((2**attempt).minutes, {
**params,
'attempt' => attempt + 1,
'last_status' => resp&.status.to_i
})
end
end
end
@@ -13,7 +13,9 @@ class SendSubmissionCreatedWebhookRequestJob
submission = Submission.find(params['submission_id']) submission = Submission.find(params['submission_id'])
attempt = params['attempt'].to_i attempt = params['attempt'].to_i
url = Accounts.load_webhook_url(submission.account)
config = Accounts.load_webhook_config(submission.account)
url = config&.value.presence
return if url.blank? return if url.blank?
@@ -28,6 +30,8 @@ class SendSubmissionCreatedWebhookRequestJob
timestamp: Time.current, timestamp: Time.current,
data: Submissions::SerializeForApi.call(submission) data: Submissions::SerializeForApi.call(submission)
}.to_json, }.to_json,
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT) 'User-Agent' => USER_AGENT)
rescue Faraday::Error rescue Faraday::Error
@@ -13,7 +13,9 @@ class SendTemplateCreatedWebhookRequestJob
template = Template.find(params['template_id']) template = Template.find(params['template_id'])
attempt = params['attempt'].to_i attempt = params['attempt'].to_i
url = Accounts.load_webhook_url(template.account)
config = Accounts.load_webhook_config(template.account)
url = config&.value.presence
return if url.blank? return if url.blank?
@@ -28,6 +30,8 @@ class SendTemplateCreatedWebhookRequestJob
timestamp: Time.current, timestamp: Time.current,
data: Templates::SerializeForApi.call(template) data: Templates::SerializeForApi.call(template)
}.to_json, }.to_json,
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT) 'User-Agent' => USER_AGENT)
rescue Faraday::Error rescue Faraday::Error
@@ -13,7 +13,9 @@ class SendTemplateUpdatedWebhookRequestJob
template = Template.find(params['template_id']) template = Template.find(params['template_id'])
attempt = params['attempt'].to_i attempt = params['attempt'].to_i
url = Accounts.load_webhook_url(template.account)
config = Accounts.load_webhook_config(template.account)
url = config&.value.presence
return if url.blank? return if url.blank?
@@ -28,6 +30,8 @@ class SendTemplateUpdatedWebhookRequestJob
timestamp: Time.current, timestamp: Time.current,
data: Templates::SerializeForApi.call(template) data: Templates::SerializeForApi.call(template)
}.to_json, }.to_json,
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT) 'User-Agent' => USER_AGENT)
rescue Faraday::Error rescue Faraday::Error
+37 -6
View File
@@ -6,6 +6,8 @@ class SubmitterMailer < ApplicationMailer
DEFAULT_INVITATION_SUBJECT = 'You are invited to submit a form' DEFAULT_INVITATION_SUBJECT = 'You are invited to submit a form'
NO_REPLY_REGEXP = /no-?reply@/i
def invitation_email(submitter) def invitation_email(submitter)
@current_account = submitter.submission.account @current_account = submitter.submission.account
@submitter = submitter @submitter = submitter
@@ -28,12 +30,13 @@ class SubmitterMailer < ApplicationMailer
assign_message_metadata('submitter_invitation', @submitter) assign_message_metadata('submitter_invitation', @submitter)
reply_to = build_submitter_reply_to(@submitter)
mail( mail(
to: @submitter.friendly_name, to: @submitter.friendly_name,
from: from_address_for_submitter(submitter), from: from_address_for_submitter(submitter),
subject:, subject:,
reply_to: submitter.preferences['reply_to'].presence || reply_to:
(submitter.submission.created_by_user || submitter.template.author)&.friendly_name&.sub(/\+\w+@/, '@')
) )
end end
@@ -75,6 +78,24 @@ class SubmitterMailer < ApplicationMailer
subject:) subject:)
end end
def declined_email(submitter, user)
@current_account = submitter.submission.account
@submitter = submitter
@submission = submitter.submission
@user = user
assign_message_metadata('submitter_declined', @submitter)
I18n.with_locale(submitter.account.locale) do
mail(from: from_address_for_submitter(submitter),
to: user.role == 'integration' ? user.friendly_name.sub(/\+\w+@/, '@') : user.friendly_name,
reply_to: @submitter.friendly_name,
subject: I18n.t(:name_declined_by_submitter,
name: @submission.template.name.truncate(20),
submitter: @submitter.name || @submitter.email || @submitter.phone))
end
end
def documents_copy_email(submitter, to: nil, sig: false) def documents_copy_email(submitter, to: nil, sig: false)
@current_account = submitter.submission.account @current_account = submitter.submission.account
@submitter = submitter @submitter = submitter
@@ -104,16 +125,26 @@ class SubmitterMailer < ApplicationMailer
assign_message_metadata('submitter_documents_copy', @submitter) assign_message_metadata('submitter_documents_copy', @submitter)
reply_to = build_submitter_reply_to(submitter)
mail(from: from_address_for_submitter(submitter), mail(from: from_address_for_submitter(submitter),
to: to || @submitter.friendly_name, to: to || @submitter.friendly_name,
reply_to: @submitter.preferences['reply_to'].presence || reply_to:,
(@submitter.submission.created_by_user ||
@submitter.template.author)&.friendly_name&.sub(/\+\w+@/, '@'),
subject:) subject:)
end end
private private
def build_submitter_reply_to(submitter)
reply_to =
submitter.preferences['reply_to'].presence ||
(submitter.submission.created_by_user || submitter.template.author)&.friendly_name&.sub(/\+\w+@/, '@')
return nil if reply_to.to_s.match?(NO_REPLY_REGEXP)
reply_to
end
def build_completed_subject(submitter) def build_completed_subject(submitter)
submitters = submitter.submission.submitters.order(:completed_at) submitters = submitter.submission.submitters.order(:completed_at)
.map { |e| e.name || e.email || e.phone }.join(', ') .map { |e| e.name || e.email || e.phone }.join(', ')
@@ -126,7 +157,7 @@ class SubmitterMailer < ApplicationMailer
total_size = 0 total_size = 0
audit_trail_data = nil audit_trail_data = nil
if with_audit_log && submitter.submission.audit_trail.present? if with_audit_log && submitter.submission.audit_trail.present? && documents.first&.name != 'combined_document'
audit_trail_data = submitter.submission.audit_trail.download audit_trail_data = submitter.submission.audit_trail.download
total_size = audit_trail_data.size total_size = audit_trail_data.size
+1 -1
View File
@@ -29,7 +29,7 @@ class Account < ApplicationRecord
has_one :default_template_folder, -> { where(name: TemplateFolder::DEFAULT_NAME) }, has_one :default_template_folder, -> { where(name: TemplateFolder::DEFAULT_NAME) },
class_name: 'TemplateFolder', dependent: :destroy, inverse_of: :account class_name: 'TemplateFolder', dependent: :destroy, inverse_of: :account
has_many :submissions, dependent: :destroy has_many :submissions, dependent: :destroy
has_many :submitters, through: :submissions has_many :submitters, dependent: :destroy
has_many :account_linked_accounts, dependent: :destroy has_many :account_linked_accounts, dependent: :destroy
has_many :email_events, dependent: :destroy has_many :email_events, dependent: :destroy
has_many :webhook_urls, dependent: :destroy has_many :webhook_urls, dependent: :destroy
+2
View File
@@ -39,6 +39,8 @@ class AccountConfig < ApplicationRecord
FORCE_SSO_AUTH_KEY = 'force_sso_auth' FORCE_SSO_AUTH_KEY = 'force_sso_auth'
FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf' FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf'
WITH_SIGNATURE_ID = 'with_signature_id' WITH_SIGNATURE_ID = 'with_signature_id'
REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason'
COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key'
DEFAULT_VALUES = { DEFAULT_VALUES = {
SUBMITTER_INVITATION_EMAIL_KEY => { SUBMITTER_INVITATION_EMAIL_KEY => {
+2 -1
View File
@@ -27,7 +27,8 @@ class EncryptedConfig < ApplicationRecord
ESIGN_CERTS_KEY = 'esign_certs', ESIGN_CERTS_KEY = 'esign_certs',
TIMESTAMP_SERVER_URL_KEY = 'timestamp_server_url', TIMESTAMP_SERVER_URL_KEY = 'timestamp_server_url',
APP_URL_KEY = 'app_url', APP_URL_KEY = 'app_url',
WEBHOOK_URL_KEY = 'webhook_url' WEBHOOK_URL_KEY = 'webhook_url',
WEBHOOK_SECRET_KEY = 'webhook_secret'
].freeze ].freeze
belongs_to :account belongs_to :account
+12 -1
View File
@@ -6,6 +6,7 @@
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# archived_at :datetime # archived_at :datetime
# expire_at :datetime
# preferences :text not null # preferences :text not null
# slug :string not null # slug :string not null
# source :text not null # source :text not null
@@ -21,7 +22,7 @@
# #
# Indexes # Indexes
# #
# index_submissions_on_account_id (account_id) # index_submissions_on_account_id_and_id (account_id,id)
# index_submissions_on_created_by_user_id (created_by_user_id) # index_submissions_on_created_by_user_id (created_by_user_id)
# index_submissions_on_slug (slug) UNIQUE # index_submissions_on_slug (slug) UNIQUE
# index_submissions_on_template_id (template_id) # index_submissions_on_template_id (template_id)
@@ -75,10 +76,20 @@ class Submission < ApplicationRecord
preserved: 'preserved' preserved: 'preserved'
}, scope: false, prefix: true }, scope: false, prefix: true
def expired?
expire_at && expire_at <= Time.current
end
def audit_trail_url def audit_trail_url
return if audit_trail.blank? return if audit_trail.blank?
ActiveStorage::Blob.proxy_url(audit_trail.blob) ActiveStorage::Blob.proxy_url(audit_trail.blob)
end end
alias audit_log_url audit_trail_url alias audit_log_url audit_trail_url
def combined_document_url
return if combined_document.blank?
ActiveStorage::Blob.proxy_url(combined_document.blob)
end
end end
+1
View File
@@ -48,6 +48,7 @@ class SubmissionEvent < ApplicationRecord
start_form: 'start_form', start_form: 'start_form',
view_form: 'view_form', view_form: 'view_form',
complete_form: 'complete_form', complete_form: 'complete_form',
decline_form: 'decline_form',
api_complete_form: 'api_complete_form' api_complete_form: 'api_complete_form'
}, scope: false }, scope: false
+20 -7
View File
@@ -6,6 +6,7 @@
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# completed_at :datetime # completed_at :datetime
# declined_at :datetime
# email :string # email :string
# ip :string # ip :string
# metadata :text not null # metadata :text not null
@@ -20,15 +21,17 @@
# values :text not null # values :text not null
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# account_id :bigint not null
# external_id :string # external_id :string
# submission_id :bigint not null # submission_id :bigint not null
# #
# Indexes # Indexes
# #
# index_submitters_on_email (email) # index_submitters_on_account_id_and_id (account_id,id)
# index_submitters_on_external_id (external_id) # index_submitters_on_email (email)
# index_submitters_on_slug (slug) UNIQUE # index_submitters_on_external_id (external_id)
# index_submitters_on_submission_id (submission_id) # index_submitters_on_slug (slug) UNIQUE
# index_submitters_on_submission_id (submission_id)
# #
# Foreign Keys # Foreign Keys
# #
@@ -36,8 +39,8 @@
# #
class Submitter < ApplicationRecord class Submitter < ApplicationRecord
belongs_to :submission belongs_to :submission
belongs_to :account
has_one :template, through: :submission has_one :template, through: :submission
has_one :account, through: :submission
attribute :values, :string, default: -> { {} } attribute :values, :string, default: -> { {} }
attribute :preferences, :string, default: -> { {} } attribute :preferences, :string, default: -> { {} }
@@ -57,7 +60,9 @@ class Submitter < ApplicationRecord
scope :completed, -> { where.not(completed_at: nil) } scope :completed, -> { where.not(completed_at: nil) }
def status def status
if completed_at? if declined_at?
'declined'
elsif completed_at?
'completed' 'completed'
elsif opened_at? elsif opened_at?
'opened' 'opened'
@@ -80,7 +85,15 @@ class Submitter < ApplicationRecord
end end
end end
def first_name
name.split(/\s+/, 2).first
end
def last_name
name.split(/\s+/, 2).last
end
def status_event_at def status_event_at
completed_at || opened_at || sent_at || created_at declined_at || completed_at || opened_at || sent_at || created_at
end end
end end
+25 -1
View File
@@ -22,7 +22,7 @@
</div> </div>
<% end %> <% end %>
<% encrypted_config = @encrypted_config || EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::APP_URL_KEY) %> <% encrypted_config = @encrypted_config || EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::APP_URL_KEY) %>
<% if !Docuseal.multitenant? && can?(:manage, encrypted_config) && !current_account.testing? %> <% if !Docuseal.multitenant? && can?(:manage, encrypted_config) && !current_account.testing? && ENV['APP_URL'].blank? %>
<%= fields_for encrypted_config do |ff| %> <%= fields_for encrypted_config do |ff| %>
<div class="form-control"> <div class="form-control">
<%= ff.label :value, 'App URL', class: 'label' %> <%= ff.label :value, 'App URL', class: 'label' %>
@@ -63,6 +63,18 @@
</div> </div>
<% end %> <% end %>
<% end %> <% end %>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::REQUIRE_SIGNING_REASON_KEY) %>
<% if can?(:manage, account_config) %>
<%= 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">
<span>
Require signing reason
</span>
<%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()' %>
</div>
<% end %>
<% end %>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::ALLOW_TYPED_SIGNATURE) %> <% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::ALLOW_TYPED_SIGNATURE) %>
<% if can?(:manage, account_config) %> <% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %> <%= form_for account_config, url: account_configs_path, method: :post do |f| %>
@@ -111,6 +123,18 @@
</div> </div>
<% end %> <% end %>
<% end %> <% end %>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::COMBINE_PDF_RESULT_KEY) %>
<% if can?(:manage, account_config) %>
<%= 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">
<span>
Combine completed documents and Audit Log
</span>
<%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()' %>
</div>
<% end %>
<% end %>
</div> </div>
<% end %> <% end %>
<%= render 'integrations' %> <%= render 'integrations' %>
+1 -1
View File
@@ -1,4 +1,4 @@
<form action="<%= root_path %>" method="get" class="bg-base-200 px-1.5 rounded-xl py-1"> <form action="<%= root_path %>" method="get" class="bg-base-200 px-1.5 rounded-xl py-1 whitespace-nowrap">
<toggle-cookies data-value="templates" data-key="dashboard_view" class="sm:tooltip tooltip-top" data-tip="Templates"> <toggle-cookies data-value="templates" data-key="dashboard_view" class="sm:tooltip tooltip-top" data-tip="Templates">
<button class="<%= local_assigns[:selected] == 'submissions' ? 'btn !border !rounded-lg btn-square !p-0 !btn-sm !h-8 !w-9' : 'btn btn-neutral !rounded-lg btn-square !p-0 hover:text-neutral-300 !btn-sm !h-8 !w-9 disabled:btn-neutral' %>"> <button class="<%= local_assigns[:selected] == 'submissions' ? 'btn !border !rounded-lg btn-square !p-0 !btn-sm !h-8 !w-9' : 'btn btn-neutral !rounded-lg btn-square !p-0 hover:text-neutral-300 !btn-sm !h-8 !w-9 disabled:btn-neutral' %>">
<%= svg_icon('layout_grid', class: 'w-6 h-6 stroke-2') %> <%= svg_icon('layout_grid', class: 'w-6 h-6 stroke-2') %>
+3 -11
View File
@@ -13,20 +13,13 @@
} }
.error-page h1 { .error-page h1 {
font-weight: 700;
font-size: 3em;
color: #291434;
margin: 0 0 0.1em;
}
.error-page h2 {
font-weight: 700; font-weight: 700;
font-size: 10em; font-size: 10em;
color: #3BBFF7; color: #3BBFF7;
margin: 0 0 0.1em; margin: 0 0 0.1em;
} }
.error-page h3 { .error-page h2 {
font-weight: 600; font-weight: 600;
font-size: 1.75em; font-size: 1.75em;
color: #291434; color: #291434;
@@ -56,9 +49,8 @@
<body class="error-page"> <body class="error-page">
<div class="dialog"> <div class="dialog">
<div> <div>
<h1>DocuSeal</h1> <h1>404</h1>
<h2>404</h2> <h2>Sorry, we couldn't find this page.</h2>
<h3>Sorry, we couldn't find this page.</h3>
</div> </div>
<a rel="noopener noreferrer" href="/" class="btn">Back to homepage</a> <a rel="noopener noreferrer" href="/" class="btn">Back to homepage</a>
</div> </div>
+3 -11
View File
@@ -13,20 +13,13 @@
} }
.error-page h1 { .error-page h1 {
font-weight: 700;
font-size: 3em;
color: #291434;
margin: 0 0 0.1em;
}
.error-page h2 {
font-weight: 700; font-weight: 700;
font-size: 10em; font-size: 10em;
color: #FABE22; color: #FABE22;
margin: 0 0 0.1em; margin: 0 0 0.1em;
} }
.error-page h3 { .error-page h2 {
font-weight: 600; font-weight: 600;
font-size: 1.75em; font-size: 1.75em;
color: #291434; color: #291434;
@@ -56,9 +49,8 @@
<body class="error-page"> <body class="error-page">
<div class="dialog"> <div class="dialog">
<div> <div>
<h1>DocuSeal</h1> <h1>422</h1>
<h2>422</h2> <h2>You're not allowed to access this page</h2>
<h3>You're not allowed to access this page</h3>
</div> </div>
<a rel="noopener noreferrer" href="/" class="btn">Back to homepage</a> <a rel="noopener noreferrer" href="/" class="btn">Back to homepage</a>
</div> </div>
+3 -11
View File
@@ -13,20 +13,13 @@
} }
.error-page h1 { .error-page h1 {
font-weight: 700;
font-size: 3em;
color: #291434;
margin: 0 0 0.1em;
}
.error-page h2 {
font-weight: 700; font-weight: 700;
font-size: 10em; font-size: 10em;
color: #F97272; color: #F97272;
margin: 0 0 0.1em; margin: 0 0 0.1em;
} }
.error-page h3 { .error-page h2 {
font-weight: 600; font-weight: 600;
font-size: 1.75em; font-size: 1.75em;
color: #291434; color: #291434;
@@ -56,9 +49,8 @@
<body class="error-page"> <body class="error-page">
<div class="dialog"> <div class="dialog">
<div> <div>
<h1>DocuSeal</h1> <h1>500</h1>
<h2>500</h2> <h2>We're sorry, but something went wrong</h2>
<h3>We're sorry, but something went wrong</h3>
</div> </div>
<a rel="noopener noreferrer" href="/" class="btn">Back to homepage</a> <a rel="noopener noreferrer" href="/" class="btn">Back to homepage</a>
</div> </div>
+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
<path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" />
</svg>

After

Width:  |  Height:  |  Size: 431 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M10.585 10.587a2 2 0 0 0 2.829 2.828" />
<path d="M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9.055 9.055 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87" />
<path d="M3 3l18 18" />
</svg>

After

Width:  |  Height:  |  Size: 572 B

+1 -1
View File
@@ -2,7 +2,7 @@
<html data-theme="docuseal" lang="<%= I18n.locale %>"> <html data-theme="docuseal" lang="<%= I18n.locale %>">
<head> <head>
<%= render 'layouts/head_tags' %> <%= render 'layouts/head_tags' %>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<%= csrf_meta_tags %> <%= csrf_meta_tags %>
<%= csp_meta_tag %> <%= csp_meta_tag %>
<% if ENV['ROLLBAR_CLIENT_TOKEN'] %> <% if ENV['ROLLBAR_CLIENT_TOKEN'] %>
+11 -1
View File
@@ -34,7 +34,17 @@
<%= fields_for @user do |ff| %> <%= fields_for @user do |ff| %>
<div class="form-control"> <div class="form-control">
<%= ff.label :password, class: 'label' %> <%= ff.label :password, class: 'label' %>
<%= ff.password_field :password, required: true, placeholder: '************', class: 'base-input' %> <password-input class="block relative">
<%= ff.password_field :password, required: true, placeholder: '************', class: 'base-input w-full pr-10', data: { target: 'password-input.passwordInput' } %>
<button data-target="password-input.togglePasswordVisibility" type="button" class="absolute inset-y-0 h-12 right-0 pr-3 flex items-center">
<span data-target="password-input.hiddenPasswordIcon">
<%= svg_icon('eye', class: 'w-6 h-6 shrink-0 bg-white') %>
</span>
<span data-target="password-input.visiblePasswordIcon" class="hidden">
<%= svg_icon('eye_off', class: 'w-6 h-6 shrink-0 bg-white') %>
</span>
</button>
</password-input>
</div> </div>
<% end %> <% end %>
<%= fields_for @encrypted_config do |ff| %> <%= fields_for @encrypted_config do |ff| %>
+19
View File
@@ -0,0 +1,19 @@
<% uuid = SecureRandom.uuid %>
<label id="<%= local_assigns[:button_id] %>" for="<%= uuid %>" class="<%= local_assigns[:btn_class] %>"><%= local_assigns[:btn_text] %></label>
<input type="checkbox" id="<%= uuid %>" class="modal-toggle">
<div id="<%= local_assigns[:id] %>" class="modal items-start !animate-none overflow-y-auto">
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none">
<% if local_assigns[:title] %>
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span>
<%= local_assigns[:title] %>
</span>
<label for="<%= uuid %>" class="text-xl">&times;</label>
</div>
<% end %>
<div>
<%= yield %>
</div>
</div>
<label class="modal-backdrop" for="<%= uuid %>">Close</label>
</div>
+4 -2
View File
@@ -1,4 +1,4 @@
<form action="<%= url_for %>" method="get" class="items-center hidden md:flex"> <form action="<%= url_for %>" method="get" class="items-center flex">
<% if params[:status].present? %> <% if params[:status].present? %>
<input name="status" value="<%= params[:status] %>" class="hidden"> <input name="status" value="<%= params[:status] %>" class="hidden">
<% end %> <% end %>
@@ -9,7 +9,9 @@
</a> </a>
</div> </div>
<% end %> <% end %>
<input id="search" name="q" value="<%= params[:q] %>" class="input input-ghost text-lg pr-10 -mr-12 <%= 'pl-8 input-outlined' if params[:q].present? %>" placeholder="<%= local_assigns[:placeholder] %>"> <search-input data-title="<%= local_assigns[:title_selector] || 'h1' %>">
<input id="search" name="q" value="<%= params[:q] %>" class="input input-ghost text-lg pr-10 -mr-12 w-0 md:w-64 <%= 'pl-8 input-outlined w-64' if params[:q].present? %>" placeholder="<%= local_assigns[:placeholder] %>">
</search-input>
<button type="submit" title="Search" class="btn btn-ghost btn-circle" onclick="window.search.value || document.activeElement === window.search ? null : [event.preventDefault(), window.search.focus()]"> <button type="submit" title="Search" class="btn btn-ghost btn-circle" onclick="window.search.value || document.activeElement === window.search ? null : [event.preventDefault(), window.search.focus()]">
<span class="enabled"> <span class="enabled">
<%= svg_icon('search', class: 'w-6 h-6 stroke-2') %> <%= svg_icon('search', class: 'w-6 h-6 stroke-2') %>
+1 -1
View File
@@ -118,7 +118,7 @@
<%= svg_icon('brand_discord', class: 'w-8 h-8') %> <%= svg_icon('brand_discord', class: 'w-8 h-8') %>
</a> </a>
</div> </div>
<%= content_for(:gpt_link) || capture do %> <%= capture do %>
<div class="tooltip" data-tip="AI Assistant"> <div class="tooltip" data-tip="AI Assistant">
<a href="<%= Docuseal::CHATGPT_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md"> <a href="<%= Docuseal::CHATGPT_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md">
<%= svg_icon('brand_openai', class: 'w-8 h-8') %> <%= svg_icon('brand_openai', class: 'w-8 h-8') %>
+2 -2
View File
@@ -11,7 +11,7 @@
ID: <%= attachment.uuid %> ID: <%= attachment.uuid %>
</div> </div>
<div> <div>
<%= t('reason') %>: <%= t('digitally_signed_by') %> <%= submitter.name %> <%= t('reason') %>: <%= submitter.values[field.dig('preferences', 'reason_field_uuid')].presence || t('digitally_signed_by') %> <%= submitter.name %>
<% if submitter.email %> <% if submitter.email %>
&lt;<%= submitter.email %>&gt; &lt;<%= submitter.email %>&gt;
<% end %> <% end %>
@@ -46,7 +46,7 @@
<%= svg_icon('check', class: "aspect-square #{area['w'] > area['h'] ? '!w-auto !h-full' : '!w-full !h-auto'}") %> <%= svg_icon('check', class: "aspect-square #{area['w'] > area['h'] ? '!w-auto !h-full' : '!w-full !h-auto'}") %>
</div> </div>
<% end %> <% end %>
<% elsif field['type'] == 'cells' && area['cell_w'] %> <% elsif field['type'] == 'cells' && area['cell_w'].to_f > 0.0 %>
<% cell_width = area['cell_w'] / area['w'] * 100 %> <% cell_width = area['cell_w'] / area['w'] * 100 %>
<div class="w-full flex items-center"> <div class="w-full flex items-center">
<% (0..(area['w'] / area['cell_w']).ceil).each do |index| %> <% (0..(area['w'] / area['cell_w']).ceil).each do |index| %>
+21 -9
View File
@@ -1,7 +1,7 @@
<% if params[:controller] == 'submissions_preview' %> <% if params[:controller] == 'submissions_preview' %>
<%= render 'submissions/preview_tags' %> <%= render 'submissions/preview_tags' %>
<% end %> <% end %>
<% with_signature_id = @submission.account.account_configs.exists?(key: AccountConfig::WITH_SIGNATURE_ID, value: true) %> <% with_signature_id, is_combined_enabled = AccountConfig.where(account_id: @submission.account_id, key: [AccountConfig::COMBINE_PDF_RESULT_KEY, AccountConfig::WITH_SIGNATURE_ID], value: true).then { |configs| [configs.any? { |e| e.key == AccountConfig::WITH_SIGNATURE_ID }, configs.any? { |e| e.key == AccountConfig::COMBINE_PDF_RESULT_KEY }] } %>
<div style="max-width: 1600px" class="mx-auto pl-4"> <div style="max-width: 1600px" class="mx-auto pl-4">
<div class="flex justify-between py-1.5 items-center pr-4 sticky top-0 md:relative z-10 bg-base-100"> <div class="flex justify-between py-1.5 items-center pr-4 sticky top-0 md:relative z-10 bg-base-100">
<a href="<%= signed_in? && @submission.account_id == current_account&.id ? template_path(@submission.template) : '/' %>" class="flex items-center space-x-3 py-1"> <a href="<%= signed_in? && @submission.account_id == current_account&.id ? template_path(@submission.template) : '/' %>" class="flex items-center space-x-3 py-1">
@@ -20,7 +20,7 @@
<% if last_submitter = @submission.submitters.to_a.select(&:completed_at?).max_by(&:completed_at) %> <% if last_submitter = @submission.submitters.to_a.select(&:completed_at?).max_by(&:completed_at) %>
<% is_all_completed = @submission.submitters.to_a.all?(&:completed_at?) %> <% is_all_completed = @submission.submitters.to_a.all?(&:completed_at?) %>
<div class="join relative"> <div class="join relative">
<download-button data-src="<%= submitter_download_index_path(last_submitter.slug, { sig: params[:sig] }.compact) %>" class="base-button <%= '!rounded-r-none !pr-2' if is_all_completed %>"> <download-button data-src="<%= submitter_download_index_path(last_submitter.slug, { sig: params[:sig], combined: is_combined_enabled }.compact) %>" class="base-button <%= '!rounded-r-none !pr-2' if is_all_completed && !is_combined_enabled %>">
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton"> <span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
<%= svg_icon('download', class: 'w-6 h-6') %> <%= svg_icon('download', class: 'w-6 h-6') %>
<span class="hidden md:inline">Download</span> <span class="hidden md:inline">Download</span>
@@ -30,7 +30,7 @@
<span class="hidden md:inline">Downloading</span> <span class="hidden md:inline">Downloading</span>
</span> </span>
</download-button> </download-button>
<% if is_all_completed %> <% if is_all_completed && !is_combined_enabled %>
<div class="dropdown dropdown-end"> <div class="dropdown dropdown-end">
<label tabindex="0" class="base-button !rounded-l-none !pl-1 !pr-2 !border-l-neutral-500"> <label tabindex="0" class="base-button !rounded-l-none !pl-1 !pr-2 !border-l-neutral-500">
<span class="text-sm align-text-top"> <span class="text-sm align-text-top">
@@ -54,8 +54,8 @@
</div> </div>
<% end %> <% end %>
</div> </div>
<% elsif @submission.submitters.to_a.size == 1 %> <% elsif @submission.submitters.to_a.size == 1 && !@submission.expired? && !@submission.submitters.to_a.first.declined_at? %>
<%= render 'shared/clipboard_copy', text: start_form_url(slug: @submission.template.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Share Link', copied_title: 'Copied to Clipboard' %> <%= render 'shared/clipboard_copy', text: submit_form_url(slug: @submission.submitters.to_a.first.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Share Link', copied_title: 'Copied to Clipboard' %>
<% end %> <% end %>
</div> </div>
</div> </div>
@@ -143,18 +143,30 @@
<div class="flex items-center space-x-1 mt-1"> <div class="flex items-center space-x-1 mt-1">
<%= svg_icon('writing', class: 'w-5 h-5') %> <%= svg_icon('writing', class: 'w-5 h-5') %>
<span> <span>
<%= submitter&.completed_at? ? l(submitter.completed_at.in_time_zone(@submission.account.timezone), format: :long, locale: @submission.account.locale) : 'Not completed yet' %> <% if submitter&.declined_at? %>
Declined on <%= l(submitter.declined_at.in_time_zone(@submission.account.timezone), format: :short, locale: @submission.account.locale) %>
<% else %>
<%= submitter&.completed_at? ? l(submitter.completed_at.in_time_zone(@submission.account.timezone), format: :long, locale: @submission.account.locale) : 'Not completed yet' %>
<% end %>
</span> </span>
</div> </div>
<% if signed_in? && submitter && submitter.email && !submitter.completed_at && !@submission.archived_at? && can?(:update, submitter) && Accounts.can_send_emails?(current_account) %> <% if submitter&.declined_at? %>
<div class="flex items-center space-x-1 mt-1">
<span>
Reason:
<%= simple_format(submitter.submission_events.find_by(event_type: :decline_form).data['reason']) %>
</span>
</div>
<% end %>
<% if signed_in? && submitter && submitter.email && !submitter.completed_at && !@submission.archived_at? && can?(:update, submitter) && Accounts.can_send_emails?(current_account) && !@submission.expired? && !submitter.declined_at? %>
<div class="mt-2 mb-1"> <div class="mt-2 mb-1">
<%= button_to button_title(title: submitter.sent_at? ? 'Re-send Email' : 'Send Email', disabled_with: 'Sending'), submitter_send_email_index_path(submitter_slug: submitter.slug), class: 'btn btn-sm btn-primary w-full' %> <%= button_to button_title(title: submitter.sent_at? ? 'Re-send Email' : 'Send Email', disabled_with: 'Sending'), submitter_send_email_index_path(submitter_slug: submitter.slug), class: 'btn btn-sm btn-primary w-full' %>
</div> </div>
<% end %> <% end %>
<% if signed_in? && submitter && submitter.phone && !submitter.completed_at && !@submission.archived_at? && can?(:update, submitter) %> <% if signed_in? && submitter && submitter.phone && !submitter.completed_at && !@submission.archived_at? && can?(:update, submitter) && !@submission.expired? && !submitter.declined_at? %>
<%= render 'submissions/send_sms_button', submitter: %> <%= render 'submissions/send_sms_button', submitter: %>
<% end %> <% end %>
<% if signed_in? && submitter && !submitter.completed_at? && !@submission.archived_at? && can?(:create, submitter) %> <% if signed_in? && submitter && !submitter.completed_at? && !@submission.archived_at? && can?(:create, submitter) && !@submission.expired? && !submitter.declined_at? %>
<div class="mt-2 mb-1"> <div class="mt-2 mb-1">
<a class="btn btn-sm btn-primary w-full" target="_blank" href="<%= submit_form_path(slug: submitter.slug) %>"> <a class="btn btn-sm btn-primary w-full" target="_blank" href="<%= submit_form_path(slug: submitter.slug) %>">
Sign In-person Sign In-person
@@ -5,7 +5,9 @@
<% end %> <% end %>
</div> </div>
<div class="flex justify-between mb-4 items-center"> <div class="flex justify-between mb-4 items-center">
<h1 class="text-4xl font-bold">Submissions <span class="badge badge-outline badge-lg align-middle">Archived</span></h1> <div>
<h1 class="text-4xl font-bold md:block <%= 'hidden' if params[:q].present? %>">Submissions <span class="badge badge-outline badge-lg align-middle">Archived</span></h1>
</div>
<% if params[:q].present? || @pagy.pages > 1 %> <% if params[:q].present? || @pagy.pages > 1 %>
<%= render 'shared/search_input', placeholder: 'Search...' %> <%= render 'shared/search_input', placeholder: 'Search...' %>
<% end %> <% end %>
@@ -5,7 +5,7 @@
<div class="mr-2"> <div class="mr-2">
<%= render 'dashboard/toggle_view', selected: 'submissions' %> <%= render 'dashboard/toggle_view', selected: 'submissions' %>
</div> </div>
<h1 class="text-3xl sm:text-4xl font-bold">Submissions</h1> <h1 class="text-2xl md:text-3xl sm:text-4xl font-bold md:block <%= 'hidden' if params[:q].present? %>">Submissions</h1>
</div> </div>
<div class="flex space-x-2"> <div class="flex space-x-2">
<% if params[:q].present? || @pagy.pages > 1 %> <% if params[:q].present? || @pagy.pages > 1 %>
@@ -0,0 +1,11 @@
<%= form_for '', url: submit_form_decline_index_path(submitter.slug), method: :post do |f| %>
<div class="mt-4 text-center">
<%= t(:notify_the_sender_with_the_reason_you_declined) %>
</div>
<div class="form-control mt-2">
<%= f.text_area :reason, required: true, class: 'base-input w-full py-2 h-40', dir: 'auto', placeholder: t('provide_a_reason'), style: 'height: 200px', rows: '10' %>
</div>
<toggle-submit dir="auto" class="form-control mt-4">
<%= f.button button_title(title: t(:decline)), class: 'base-button' %>
</toggle-submit>
<% end %>
@@ -1,3 +1,3 @@
<% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid created_at], methods: %i[url filename content_type]) %> <% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid created_at], methods: %i[url filename content_type]) %>
<% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %> <% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %>
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-with-signature-id="<%= configs[:with_signature_id] %>" data-with-confetti="<%= configs[:with_confetti] %>" data-completed-redirect-url="<%= submitter.preferences['completed_redirect_url'] %>" data-completed-message="<%= configs[:completed_message].to_json %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.preferences.key?('go_to_last') ? submitter.preferences['go_to_last'] : submitter.opened_at? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(submitter.submission.account) %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>" data-previous-signature-value="<%= local_assigns[:signature_attachment]&.uuid %>" data-remember-signature="<%= configs[:prefill_signature] %>" data-dry-run="<%= local_assigns[:dry_run] %>" data-expand="<%= local_assigns[:expand] %>" data-scroll-padding="<%= local_assigns[:scroll_padding] %>"></submission-form> <submission-form data-is-demo="<%= Docuseal.demo? %>" data-require-signing-reason="<%= configs[:require_signing_reason] %>" data-with-signature-id="<%= configs[:with_signature_id] %>" data-with-confetti="<%= configs[:with_confetti] %>" data-completed-redirect-url="<%= submitter.preferences['completed_redirect_url'] %>" data-completed-message="<%= configs[:completed_message].to_json %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.preferences.key?('go_to_last') ? submitter.preferences['go_to_last'] : submitter.opened_at? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(submitter.submission.account) %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>" data-previous-signature-value="<%= local_assigns[:signature_attachment]&.uuid %>" data-remember-signature="<%= configs[:prefill_signature] %>" data-dry-run="<%= local_assigns[:dry_run] %>" data-expand="<%= local_assigns[:expand] %>" data-scroll-padding="<%= local_assigns[:scroll_padding] %>"></submission-form>
+21
View File
@@ -0,0 +1,21 @@
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<div class="flex items-center justify-center">
<%= render 'start_form/banner' %>
</div>
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
<div class="flex items-center">
<div class="mr-3">
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
</div>
<div dir="auto">
<p class="text-lg font-bold mb-1"><%= @submitter.submission.template.name %></p>
<p class="text-sm"><%= t('form_has_been_declined_on_html', time: l(@submitter.declined_at, format: :long)) %></p>
</div>
</div>
</div>
</div>
</div>
</div>
<%= render 'shared/attribution', link_path: '/start', account: @submitter.account %>
+21
View File
@@ -0,0 +1,21 @@
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<div class="flex items-center justify-center">
<%= render 'start_form/banner' %>
</div>
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
<div class="flex items-center">
<div class="mr-3">
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
</div>
<div dir="auto">
<p class="text-lg font-bold mb-1"><%= @submitter.submission.template.name %></p>
<p class="text-sm"><%= t('form_expired_at_html', time: l(@submitter.submission.expire_at, format: :long)) %></p>
</div>
</div>
</div>
</div>
</div>
</div>
<%= render 'shared/attribution', link_path: '/start', account: @submitter.account %>
+27 -3
View File
@@ -2,12 +2,36 @@
<% content_for(:html_description, "#{@submitter.account.name} has invited you to fill and sign documents online effortlessly with a secure, fast, and user-friendly digital document signing solution.") %> <% content_for(:html_description, "#{@submitter.account.name} has invited you to fill and sign documents online effortlessly with a secure, fast, and user-friendly digital document signing solution.") %>
<% fields_index = Templates.build_field_areas_index(@submitter.submission.template_fields || @submitter.submission.template.fields) %> <% fields_index = Templates.build_field_areas_index(@submitter.submission.template_fields || @submitter.submission.template.fields) %>
<% values = @submitter.submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %> <% values = @submitter.submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %>
<% submitters_index = @submitter.submission.submitters.index_by(&:uuid) %>
<% page_blob_struct = Struct.new(:url, :metadata, keyword_init: true) %> <% page_blob_struct = Struct.new(:url, :metadata, keyword_init: true) %>
<div style="max-height: -webkit-fill-available;"> <div style="max-height: -webkit-fill-available;">
<div id="scrollbox"> <div id="scrollbox">
<div class="mx-auto block pb-72" style="max-width: 1000px"> <div class="mx-auto block pb-72" style="max-width: 1000px">
<%# flex block w-full sticky top-0 z-50 space-x-2 items-center bg-yellow-100 p-2 border-y border-yellow-200 %> <%# flex block w-full sticky top-0 z-50 space-x-2 items-center bg-yellow-100 p-2 border-y border-yellow-200 %>
<%= local_assigns[:banner_html] || render('submit_form/banner') %> <%= local_assigns[:banner_html] || capture do %>
<%= render('submit_form/banner') %>
<div class="sticky top-0 z-50 bg-base-100 py-2 px-2 flex items-center md:-mx-[8px]" style="margin-bottom: -16px">
<div class="text-xl md:text-2xl font-medium focus:text-clip" style="width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
<%= @submitter.submission.template.name %>
</div>
<div class="flex items-center space-x-2" style="margin-left: 20px; flex-shrink: 0">
<div>
<%= render 'shared/html_modal', title: t(:decline), btn_text: t(:decline), btn_class: 'btn btn-sm !px-5', button_id: 'decline_button' do %>
<%= render 'submit_form/decline_form', submitter: @submitter %>
<% end %>
</div>
<download-button data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm !px-4">
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
<span><%= t('download') %></span>
</span>
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
<%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
<span><%= t('downloading') %></span>
</span>
</download-button>
</div>
</div>
<% end %>
<% (@submitter.submission.template_schema || @submitter.submission.template.schema).each do |item| %> <% (@submitter.submission.template_schema || @submitter.submission.template.schema).each do |item| %>
<% document = @submitter.submission.template_schema_documents.find { |a| a.uuid == item['attachment_uuid'] } %> <% document = @submitter.submission.template_schema_documents.find { |a| a.uuid == item['attachment_uuid'] } %>
<% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %> <% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %>
@@ -28,7 +52,7 @@
<% next if field['redacted'] && field['submitter_uuid'] != @submitter.uuid %> <% next if field['redacted'] && field['submitter_uuid'] != @submitter.uuid %>
<% next if value == '{{date}}' && field['submitter_uuid'] != @submitter.uuid %> <% next if value == '{{date}}' && field['submitter_uuid'] != @submitter.uuid %>
<% next if field.dig('preferences', 'formula').present? && field['submitter_uuid'] == @submitter.uuid %> <% next if field.dig('preferences', 'formula').present? && field['submitter_uuid'] == @submitter.uuid %>
<%= render 'submissions/value', area:, field:, attachments_index: @attachments_index, value:, locale: @submitter.account.locale, timezone: @submitter.account.timezone, submitter: @submitter, with_signature_id: @form_configs[:with_signature_id] %> <%= render 'submissions/value', area:, field:, attachments_index: @attachments_index, value:, locale: @submitter.account.locale, timezone: @submitter.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: @form_configs[:with_signature_id] %>
<% end %> <% end %>
</div> </div>
</div> </div>
@@ -41,7 +65,7 @@
<div class="fixed bottom-0 w-full h-0 z-20"> <div class="fixed bottom-0 w-full h-0 z-20">
<div class="mx-auto" style="max-width: 1000px"> <div class="mx-auto" style="max-width: 1000px">
<div class="relative md:mx-32"> <div class="relative md:mx-32">
<%= render 'submit_form/submission_form', attachments_index: @attachments_index, submitter: @submitter, signature_attachment: @signature_attachment, configs: @form_configs, dry_run: local_assigns[:dry_run], expand: local_assigns[:expand], scroll_padding: local_assigns[:scroll_padding] %> <%= render 'submit_form/submission_form', attachments_index: @attachments_index, submitter: @submitter, signature_attachment: @signature_attachment, configs: @form_configs, dry_run: local_assigns[:dry_run], expand: local_assigns[:expand], scroll_padding: local_assigns.fetch(:scroll_padding, '-110px') %>
</div> </div>
</div> </div>
</div> </div>
@@ -0,0 +1,4 @@
<p><%= t('hi_there') %>,</p>
<p><%= t('name_declined_by_submitter_with_the_following_reason', name: @submitter.submission.template.name, submitter: @submitter.name || @submitter.email || @submitter.phone) %></p>
<%= simple_format(@submitter.submission_events.find_by(event_type: :decline_form).data['reason']) %>
<p><%= link_to t('view'), submission_url(@submitter.submission) %></p>
@@ -1,7 +1,7 @@
<% if @email_config || @body.present? %> <% if @email_config || @body.present? %>
<% body = (@body.presence || @email_config.value['body']).to_s %> <% body = (@body.presence || @email_config.value['body']).to_s %>
<%= render 'custom_content', content: body, submitter: @submitter %> <%= render 'custom_content', content: body, submitter: @submitter %>
<% if !body.include?(ReplaceEmailVariables::SUBMITTER_LINK) && !body.include?(ReplaceEmailVariables::SUBMITTER_ID) && !body.include?(ReplaceEmailVariables::SUBMISSION_LINK) && !body.include?(ReplaceEmailVariables::TEMPLATE_ID) && !@submitter.submission.source.in?(%w[api embed]) %> <% 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> <p><%= link_to nil, submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email')) %></p>
<% end %> <% end %>
<% else %> <% else %>
+1 -1
View File
@@ -5,7 +5,7 @@
<% end %> <% end %>
</div> </div>
<div class="flex justify-between mb-4 items-center"> <div class="flex justify-between mb-4 items-center">
<h1 class="text-4xl font-bold flex items-center space-x-2"> <h1 class="text-4xl font-bold flex items-center space-x-2 md:flex <%= 'hidden' if params[:q].present? %>">
<%= svg_icon('folder', class: 'w-9 h-9 flex-shrink-0') %> <%= svg_icon('folder', class: 'w-9 h-9 flex-shrink-0') %>
<span class="peer"> <span class="peer">
<%= @template_folder.name %> <%= @template_folder.name %>
+32 -13
View File
@@ -1,4 +1,4 @@
<% status_badges = { 'awaiting' => 'badge-info', 'sent' => 'badge-info', 'completed' => 'badge-success', 'opened' => 'badge-warning' } %> <% status_badges = { 'awaiting' => 'badge-info', 'sent' => 'badge-info', 'completed' => 'badge-success', 'opened' => 'badge-warning', 'declined' => 'badge-error' } %>
<div class="bg-base-200 rounded-2xl flex flex-col sm:flex-row items-strech"> <div class="bg-base-200 rounded-2xl flex flex-col sm:flex-row items-strech">
<% if local_assigns[:with_template] %> <% if local_assigns[:with_template] %>
<% template = submission.template %> <% template = submission.template %>
@@ -7,12 +7,12 @@
<div class="font-medium items-start w-full group-hover:link text-sm flex space-x-1"> <div class="font-medium items-start w-full group-hover:link text-sm flex space-x-1">
<%= svg_icon('file_text', class: 'w-4 h-4 mt-0.5 flex-shrink-0') %> <%= svg_icon('file_text', class: 'w-4 h-4 mt-0.5 flex-shrink-0') %>
<span style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;"> <span style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;">
<%= template.name %> <% template.name.split(/(_)/).each do |item| %><%= item %><wbr><% end %>
<%= svg_icon('arrow_right', class: 'w-4 h-4 sm:inline group-hover:visible invisible hidden') %> <%= svg_icon('arrow_right', class: 'w-4 h-4 sm:inline group-hover:visible invisible hidden') %>
</span> </span>
</div> </div>
</div> </div>
<div class="flex-shrink-0 sm:mt-2"> <div class="flex-shrink-0 sm:mt-2 max-w-[50%] md:max-w-full">
<div class="flex text-xs flex-col text-base-content/60 space-y-0.5"> <div class="flex text-xs flex-col text-base-content/60 space-y-0.5">
<span class="flex items-center space-x-1"> <span class="flex items-center space-x-1">
<%= svg_icon('user', class: 'w-4 h-4 flex-shrink-0') %> <%= svg_icon('user', class: 'w-4 h-4 flex-shrink-0') %>
@@ -34,11 +34,19 @@
<% submitter = submitters.first %> <% submitter = submitters.first %>
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<span class="flex flex-col md:flex-row md:items-center gap-3"> <span class="flex flex-col md:flex-row md:items-center gap-3">
<div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>"> <% if submission.expired? %>
<span class="badge <%= status_badges[submitter.status] %> md:w-32 badge-lg bg-opacity-50 uppercase text-sm font-semibold"> <div class="tooltip flex" data-tip="<%= l(submission.expire_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<%= submitter.status %> <span class="badge badge-error md:w-32 bg-opacity-50 badge-lg uppercase text-sm font-semibold">
</span> Expired
</div> </span>
</div>
<% else %>
<div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<span class="badge <%= status_badges[submitter.status] %> md:w-32 badge-lg bg-opacity-50 uppercase text-sm font-semibold">
<%= submitter.status %>
</span>
</div>
<% end %>
<span class="text-lg break-all flex items-center"> <span class="text-lg break-all flex items-center">
<%= submitter.name || submitter.email || submitter.phone %> <%= submitter.name || submitter.email || submitter.phone %>
</span> </span>
@@ -61,7 +69,7 @@
</download-button> </download-button>
</button> </button>
</form> </form>
<% elsif !submission.archived_at? && !template.archived_at? %> <% elsif !submission.archived_at? && !template.archived_at? && !submission.expired? && !submitter.declined_at? %>
<% if current_user.email == submitter.email %> <% if current_user.email == submitter.email %>
<form data-turbo="false" method="get" action="<%= submit_form_url(slug: submitter.slug) %>" class="flex-1 md:flex-none"> <form data-turbo="false" method="get" action="<%= submit_form_url(slug: submitter.slug) %>" class="flex-1 md:flex-none">
<button onclick="event.stopPropagation()" class="btn btn-sm btn-neutral btn-outline bg-white w-full md:w-36 flex"> <button onclick="event.stopPropagation()" class="btn btn-sm btn-neutral btn-outline bg-white w-full md:w-36 flex">
@@ -82,7 +90,7 @@
</div> </div>
<% if !submission.archived_at? && !template.archived_at? && can?(:destroy, submission) %> <% if !submission.archived_at? && !template.archived_at? && can?(:destroy, submission) %>
<span data-tip="Archive" class="sm:tooltip tooltip-top"> <span data-tip="Archive" class="sm:tooltip tooltip-top">
<%= button_to button_title(title: nil, disabled_with: 'Arch', icon: svg_icon('archive', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: 'Archive', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %> <%= button_to button_title(title: nil, disabled_with: 'Arch', icon: svg_icon('archive', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: 'Archive', method: :delete, onclick: 'event.stopPropagation()' %>
</span> </span>
<% end %> <% end %>
<% if local_assigns[:archived] && can?(:destroy, submission) %> <% if local_assigns[:archived] && can?(:destroy, submission) %>
@@ -101,12 +109,18 @@
<%= latest_submitter.status %> <%= latest_submitter.status %>
</span> </span>
</div> </div>
<% elsif submission.expired? %>
<div class="tooltip flex" data-tip="<%= l(submission.expire_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<span class="badge badge-error md:w-32 bg-opacity-50 badge-lg uppercase text-sm font-semibold">
Expired
</span>
</div>
<% end %> <% end %>
<div class="w-full <%= is_submission_completed ? 'space-y-1' : 'space-y-4' %> md:space-y-0"> <div class="w-full <%= is_submission_completed ? 'space-y-1' : 'space-y-4' %> md:space-y-0">
<% submitters.each_with_index do |submitter, index| %> <% submitters.each_with_index do |submitter, index| %>
<div class="relative flex justify-between items-start md:items-center space-x-3"> <div class="relative flex justify-between items-start md:items-center space-x-3">
<span class="flex flex-col md:flex-row md:items-center gap-2"> <span class="flex flex-col md:flex-row md:items-center gap-2">
<% unless is_submission_completed %> <% if !is_submission_completed && !submission.expired? %>
<div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>"> <div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<span class="badge md:w-24 <%= status_badges[submitter.status] %> bg-opacity-50 uppercase text-xs font-semibold"> <span class="badge md:w-24 <%= status_badges[submitter.status] %> bg-opacity-50 uppercase text-xs font-semibold">
<%= submitter.status %> <%= submitter.status %>
@@ -132,7 +146,7 @@
</download-button> </download-button>
</button> </button>
</form> </form>
<% elsif !template.archived_at? && !submission.archived_at? && !is_submission_completed %> <% elsif !template.archived_at? && !submission.archived_at? && !is_submission_completed && !submission.expired? && !submitter.declined_at? %>
<div class="relative flex items-center space-x-3"> <div class="relative flex items-center space-x-3">
<% if current_user.email == submitter.email %> <% if current_user.email == submitter.email %>
<form data-turbo="false" method="get" action="<%= submit_form_url(slug: submitter.slug) %>"> <form data-turbo="false" method="get" action="<%= submit_form_url(slug: submitter.slug) %>">
@@ -178,7 +192,12 @@
</div> </div>
<% if !submission.archived_at? && !template.archived_at? %> <% if !submission.archived_at? && !template.archived_at? %>
<span data-tip="Archive" class="sm:tooltip tooltip-top"> <span data-tip="Archive" class="sm:tooltip tooltip-top">
<%= button_to button_title(title: nil, disabled_with: 'Arch', icon: svg_icon('archive', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: 'Archive', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %> <%= button_to button_title(title: nil, disabled_with: 'Arch', icon: svg_icon('archive', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: 'Archive', method: :delete, onclick: 'event.stopPropagation()' %>
</span>
<% end %>
<% if local_assigns[:archived] && can?(:destroy, submission) %>
<span data-tip="Remove" class="sm:tooltip tooltip-top">
<%= button_to button_title(title: nil, disabled_with: 'Rem', icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission, permanently: true), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: 'Remove', method: :delete, data: { turbo_confirm: 'Submission deletion is irreversible and will permanently remove all associated signed documents with it. Are you sure?' }, onclick: 'event.stopPropagation()' %>
</span> </span>
<% end %> <% end %>
</div> </div>
+2 -2
View File
@@ -1,7 +1,7 @@
<div class="h-36 relative group"> <div class="h-36 relative group">
<a href="<%= template_path(template) %>" class="flex h-full flex-col justify-between rounded-2xl pt-6 px-7 w-full bg-base-200 peer"> <a href="<%= template_path(template) %>" class="flex h-full flex-col justify-between rounded-2xl pt-6 px-7 w-full bg-base-200 peer">
<div class="pb-4 text-xl font-semibold" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;"> <div class="pb-4 text-xl font-semibold" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;">
<%= template.name %> <% template.name.split(/(_)/).each do |item| %><%= item %><wbr><% end %>
</div> </div>
<div class="pb-6 pt-1 space-y-1"> <div class="pb-6 pt-1 space-y-1">
<p class="flex items-center space-x-1 text-xs text-base-content/60"> <p class="flex items-center space-x-1 text-xs text-base-content/60">
@@ -57,7 +57,7 @@
<% end %> <% end %>
<% if can?(:destroy, template) %> <% if can?(:destroy, template) %>
<span class="tooltip tooltip-left" data-tip="<%= template.archived_at? ? 'Delete' : 'Archive' %>"> <span class="tooltip tooltip-left" data-tip="<%= template.archived_at? ? 'Delete' : 'Archive' %>">
<%= button_to template_path(template), data: { turbo_confirm: template.archived_at? ? 'Template deletion is irreversible and will permanently remove all associated signed documents with it. Are you sure?' : 'Are you sure?' }, params: { permanently: template.archived_at? }.compact_blank, method: :delete, class: 'btn btn-xs hover:btn-outline bg-base-200 btn-circle', aria_label: 'Restore' do %> <%= button_to template_path(template), data: template.archived_at? ? { turbo_confirm: 'Template deletion is irreversible and will permanently remove all associated signed documents with it. Are you sure?' } : {}, params: { permanently: template.archived_at? }.compact_blank, method: :delete, class: 'btn btn-xs hover:btn-outline bg-base-200 btn-circle', aria_label: 'Restore' do %>
<%= svg_icon(template.archived_at? ? 'trash' : 'archive', class: 'w-4 h-4 enabled') %> <%= svg_icon(template.archived_at? ? 'trash' : 'archive', class: 'w-4 h-4 enabled') %>
<%= svg_icon('loader', class: 'w-4 h-4 animate-spin disabled') %> <%= svg_icon('loader', class: 'w-4 h-4 animate-spin disabled') %>
<% end %> <% end %>
+12 -4
View File
@@ -1,7 +1,7 @@
<div class="flex flex-col items-start md:flex-row space-y-2 md:space-y-0 md:space-x-2 md:justify-between md:items-start mb-6 md:mb-3"> <div class="flex flex-col items-start md:flex-row space-y-2 md:space-y-0 md:space-x-2 md:justify-between md:items-start mb-6 md:mb-3">
<div class="relative flex items-start justify-between w-full space-x-0"> <div class="relative flex items-start justify-between w-full space-x-0">
<div> <div>
<h1 class="text-4xl font-semibold" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;"> <h1 class="text-3xl md:text-4xl font-semibold" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;">
<% template.name.split(/(_)/).each do |item| %><%= item %><wbr><% end %> <% template.name.split(/(_)/).each do |item| %><%= item %><wbr><% end %>
<% if template.archived_at? %> <% if template.archived_at? %>
<span class="ml-1 badge badge-outline badge-lg align-middle">Archived</span> <span class="ml-1 badge badge-outline badge-lg align-middle">Archived</span>
@@ -70,7 +70,7 @@
<span>Edit</span> <span>Edit</span>
</span> </span>
<% end %> <% end %>
<% elsif can?(:read, template) %> <% elsif can?(:read, template) && !template.archived_at? %>
<%= link_to template_preview_path(template), class: 'btn btn-outline btn-sm flex-1' do %> <%= link_to template_preview_path(template), class: 'btn btn-outline btn-sm flex-1' do %>
<span class="flex items-center justify-center space-x-2"> <span class="flex items-center justify-center space-x-2">
<%= svg_icon('file_text', class: 'w-6 h-6') %> <%= svg_icon('file_text', class: 'w-6 h-6') %>
@@ -79,8 +79,16 @@
<% end %> <% end %>
<% end %> <% end %>
<% end %> <% end %>
<% if template.archived_at? && can?(:create, template) %> <% if template.archived_at? %>
<%= button_to button_title(title: 'Restore', disabled_with: 'Restoring', icon: svg_icon('rotate', class: 'w-6 h-6')), template_restore_index_path(template), class: 'btn btn-outline btn-sm flex-1' %> <% if can?(:create, template) %>
<%= button_to button_title(title: 'Restore', disabled_with: 'Restoring', icon: svg_icon('rotate', class: 'w-6 h-6')), template_restore_index_path(template), class: 'btn btn-outline btn-sm flex-1' %>
<% end %>
<%= link_to template_preview_path(template), class: 'btn btn-outline btn-sm flex-1' do %>
<span class="flex items-center justify-center space-x-2">
<%= svg_icon('file_text', class: 'w-6 h-6') %>
<span>Preview</span>
</span>
<% end %>
<% end %> <% end %>
</div> </div>
</div> </div>
+1 -1
View File
@@ -1 +1 @@
<template-builder class="grid" data-template="<%= @template_data %>"></template-builder> <template-builder class="grid" data-template="<%= @template_data %>" data-with-sign-yourself-button="<%= !@template.archived_at? %>" data-with-send-button="<%= !@template.archived_at? && can?(:create, @template.submissions.new(account: current_account)) %>" data-locale="<%= current_account.locale %>"></template-builder>
+4 -2
View File
@@ -3,10 +3,12 @@
<% if !@pagy.count.zero? || params[:q].present? || params[:status].present? %> <% if !@pagy.count.zero? || params[:q].present? || params[:status].present? %>
<div class="<%= is_show_tabs ? 'mb-4' : 'mb-6' %>"> <div class="<%= is_show_tabs ? 'mb-4' : 'mb-6' %>">
<div class="flex justify-between items-center md:items-end"> <div class="flex justify-between items-center md:items-end">
<p class="text-3xl font-bold">Submissions</p> <div>
<h2 class="text-3xl font-bold md:block <%= 'hidden' if params[:q].present? %>">Submissions</h2>
</div>
<div class="flex justify-end space-x-2"> <div class="flex justify-end space-x-2">
<% if params[:q].present? || params[:status].present? || @pagy.pages > 1 %> <% if params[:q].present? || params[:status].present? || @pagy.pages > 1 %>
<%= render 'shared/search_input' %> <%= render 'shared/search_input', title_selector: 'h2' %>
<% end %> <% end %>
<%= link_to new_template_submissions_export_path(@template), class: 'hidden md:flex btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %> <%= link_to new_template_submissions_export_path(@template), class: 'hidden md:flex btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('download', class: 'w-6 h-6 stroke-2') %> <%= svg_icon('download', class: 'w-6 h-6 stroke-2') %>
+3 -1
View File
@@ -5,7 +5,9 @@
<% end %> <% end %>
</div> </div>
<div class="flex justify-between mb-4 items-center"> <div class="flex justify-between mb-4 items-center">
<h1 class="text-4xl font-bold"><span class="hidden md:inline">Document</span> Templates <span class="badge badge-outline badge-lg align-middle">Archived</span></h1> <div>
<h1 class="text-4xl font-bold md:block <%= 'hidden' if params[:q].present? %>"><span class="hidden md:inline">Document</span> Templates <span class="badge badge-outline badge-lg align-middle">Archived</span></h1>
</div>
<% if params[:q].present? || @pagy.pages > 1 %> <% if params[:q].present? || @pagy.pages > 1 %>
<%= render 'shared/search_input', placeholder: 'Search...' %> <%= render 'shared/search_input', placeholder: 'Search...' %>
<% end %> <% end %>
@@ -6,11 +6,11 @@
<% end %> <% end %>
</div> </div>
<div class="flex justify-between mb-6 md:items-end flex-col md:flex-row"> <div class="flex justify-between mb-6 md:items-end flex-col md:flex-row">
<p class="text-3xl font-bold">Submissions <span class="badge badge-outline badge-lg align-middle">Archived</span></p> <div>
<div class="flex space-x-2 mt-3 md:mt-0"> <h1 class="text-3xl font-bold md:block">Submissions <span class="badge badge-outline badge-lg align-middle">Archived</span></h1>
<% if params[:q].present? || @pagy.pages > 1 %> </div>
<%= render 'shared/search_input' %> <div class="flex space-x-2 mt-3 md:mt-0 justify-end">
<% end %> <%= render 'shared/search_input' %>
<%= link_to new_template_submissions_export_path(@template), class: 'order-3 md:order-1 btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %> <%= link_to new_template_submissions_export_path(@template), class: 'order-3 md:order-1 btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('download', class: 'w-6 h-6 stroke-2') %> <%= svg_icon('download', class: 'w-6 h-6 stroke-2') %>
<span>Export</span> <span>Export</span>
+1 -1
View File
@@ -7,7 +7,7 @@
<%= render 'dashboard/toggle_view', selected: 'templates' %> <%= render 'dashboard/toggle_view', selected: 'templates' %>
</div> </div>
<% end %> <% end %>
<h1 class="text-3xl sm:text-4xl font-bold"><span class="hidden md:inline">Document</span> Templates</h1> <h1 class="text-2xl md:text-3xl sm:text-4xl font-bold md:block <%= 'hidden' if params[:q].present? %>"><span class="hidden md:inline">Document</span> Templates</h1>
</div> </div>
<div class="flex space-x-2"> <div class="flex space-x-2">
<% if params[:q].present? || @pagy.pages > 1 || @template_folders.present? %> <% if params[:q].present? || @pagy.pages > 1 || @template_folders.present? %>
+1 -1
View File
@@ -1 +1 @@
<template-builder class="grid" data-editable="false" data-template="<%= @template_data %>"></template-builder> <template-builder class="grid" data-editable="false" data-with-sign-yourself-button="<%= !@template.archived_at? %>" data-with-send-button="<%= !@template.archived_at? && can?(:create, @template.submissions.new(account: current_account)) %>" data-template="<%= @template_data %>"></template-builder>
+19
View File
@@ -0,0 +1,19 @@
<%= render 'shared/turbo_modal', title: 'Webhook Secret' do %>
<%= form_for @encrypted_config, url: webhook_secret_index_path, method: :post, html: { class: 'space-y-4' }, data: { turbo_frame: :_top } do |f| %>
<div class="space-y-2">
<%= f.fields_for :value, Struct.new(:key, :value).new(*@encrypted_config.value.to_a.first) do |ff| %>
<div class="form-control">
<%= ff.label :key, class: 'label' %>
<%= ff.text_field :key, class: 'base-input', placeholder: 'X-Example-Header' %>
</div>
<div class="form-control">
<%= ff.label :value, class: 'label' %>
<%= ff.text_field :value, class: 'base-input' %>
</div>
<% end %>
</div>
<div class="form-control pt-2">
<%= f.button button_title, class: 'base-button' %>
</div>
<% end %>
<% end %>
+4 -1
View File
@@ -9,9 +9,12 @@
<div class="card-body p-6"> <div class="card-body p-6">
<%= form_for @encrypted_config, url: settings_webhooks_path, method: :post, html: { autocomplete: 'off' } do |f| %> <%= form_for @encrypted_config, url: settings_webhooks_path, method: :post, html: { autocomplete: 'off' } do |f| %>
<%= f.label :value, 'Webhook URL', class: 'text-sm font-semibold' %> <%= f.label :value, 'Webhook URL', class: 'text-sm font-semibold' %>
<div class="flex flex-row flex-wrap space-y-2 md:space-y-0 md:flex-nowrap md:space-x-4 mt-2"> <div class="flex flex-row flex-wrap space-y-2 md:space-y-0 md:flex-nowrap md:space-x-2 mt-2">
<%= f.url_field :value, class: 'input font-mono input-bordered w-full', placeholder: 'https://example.com/hook' %> <%= f.url_field :value, class: 'input font-mono input-bordered w-full', placeholder: 'https://example.com/hook' %>
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button w-full md:w-32' %> <%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button w-full md:w-32' %>
<a href="<%= webhook_secret_index_path %>" data-turbo-frame="modal" class="white-button w-full md:w-auto">
Add Secret
</a>
</div> </div>
<% end %> <% end %>
<% preference = current_account.account_configs.find_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)&.value || {} %> <% preference = current_account.account_configs.find_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)&.value || {} %>
+32 -16
View File
@@ -1,25 +1,41 @@
# frozen_string_literal: true # frozen_string_literal: true
if ENV['RAILS_ENV'] == 'production' && ENV['SECRET_KEY_BASE'].to_s.empty? if ENV['RAILS_ENV'] == 'production'
require 'dotenv' if !ENV['AWS_SECRET_MANAGER_ID'].to_s.empty?
require 'securerandom' require 'aws-sdk-secretsmanager'
dotenv_path = "#{ENV.fetch('WORKDIR', '.')}/docuseal.env" client = Aws::SecretsManager::Client.new
unless File.exist?(dotenv_path) secret_id = ENV.fetch('AWS_SECRET_MANAGER_ID', '')
default_env = <<~TEXT
DATABASE_URL= # keep empty to use sqlite or specify postgresql database URL
SECRET_KEY_BASE=#{SecureRandom.hex(64)}
TEXT
File.write(dotenv_path, default_env) client.get_secret_value(secret_id:).secret_string.split("\n").each do |line|
key, value = line.split('=', 2)
ENV[key] = value if !key.to_s.empty? && !value.to_s.empty?
end
RubyVM::YJIT.enable if ENV['RUBY_YJIT_ENABLE'] == 'true'
elsif ENV['SECRET_KEY_BASE'].to_s.empty?
require 'dotenv'
require 'securerandom'
dotenv_path = "#{ENV.fetch('WORKDIR', '.')}/docuseal.env"
unless File.exist?(dotenv_path)
default_env = <<~TEXT
DATABASE_URL= # keep empty to use sqlite or specify postgresql database URL
SECRET_KEY_BASE=#{SecureRandom.hex(64)}
TEXT
File.write(dotenv_path, default_env)
end
database_url = ENV.fetch('DATABASE_URL', nil)
Dotenv.load(dotenv_path)
ENV['DATABASE_URL'] = ENV['DATABASE_URL'].to_s.empty? ? database_url : ENV.fetch('DATABASE_URL', nil)
end end
database_url = ENV.fetch('DATABASE_URL', nil)
Dotenv.load(dotenv_path)
ENV['DATABASE_URL'] = ENV['DATABASE_URL'].to_s.empty? ? database_url : ENV.fetch('DATABASE_URL', nil)
end end
if ENV['DATABASE_URL'].to_s.split('@').last.to_s.split('/').first.to_s.include?('_') if ENV['DATABASE_URL'].to_s.split('@').last.to_s.split('/').first.to_s.include?('_')
+25 -32
View File
@@ -126,39 +126,32 @@ Rails.application.configure do
config.lograge.enabled = true config.lograge.enabled = true
config.lograge.base_controller_class = ['ActionController::API', 'ActionController::Base'] config.lograge.base_controller_class = ['ActionController::API', 'ActionController::Base']
if ENV['MULTITENANT'] == 'true' config.lograge.formatter = ->(data) { data.except(:path, :location).to_json }
config.lograge.formatter = ->(data) { data.except(:path, :location).to_json }
config.lograge.custom_payload do |controller| config.lograge.custom_payload do |controller|
params = params =
begin begin
controller.request.try(:params) || {} controller.request.try(:params) || {}
rescue StandardError rescue StandardError
{} {}
end end
{ {
fwd: controller.request.remote_ip, fwd: controller.request.remote_ip,
params: { params: {
id: params[:id], id: params[:id],
sig: (params[:signed_uuid] || params[:signed_id]).to_s.split('--').first, template_id: params[:template_id],
slug: (params[:slug] || submission_id: params[:submission_id],
params[:submitter_slug] || submitter_id: params[:submitter_id],
params[:submission_slug] || sig: (params[:signed_uuid] || params[:signed_id]).to_s.split('--').first,
params[:submit_form_slug] || slug: (params[:slug] ||
params[:template_slug]).to_s.last(5) params[:submitter_slug] ||
}.compact_blank, params[:submission_slug] ||
host: controller.request.host, params[:submit_form_slug] ||
uid: controller.instance_variable_get(:@current_user).try(:id) params[:template_slug]).to_s.first(5)
} }.compact_blank,
end host: controller.request.host,
else uid: controller.instance_variable_get(:@current_user).try(:id)
config.lograge.formatter = Lograge::Formatters::Json.new }
config.lograge.custom_payload do |controller|
{
fwd: controller.request.remote_ip
}
end
end end
end end
+14
View File
@@ -11,6 +11,20 @@ module HexaPDF
string_algorithm.encrypt(key, str).dup string_algorithm.encrypt(key, str).dup
end end
end end
module AES
module ClassMethods
def unpad(data)
padding_length = data.getbyte(-1)
if !padding_length || padding_length > BLOCK_SIZE || padding_length.zero? ||
data[-padding_length, padding_length].each_byte.any? { |byte| byte != padding_length }
data
else
data[0...-padding_length]
end
end
end
end
end end
module Type module Type
Loaded 100 of 141 files, more files were not shown because too many files have changed in this diff. Show more