Compare commits

..
142 Commits
Author SHA1 Message Date
Alex Turchyn 42371d7ce3 Merge from docusealco/wip 2025-03-02 15:37:12 +02:00
Pete Matsyburka 4116746be3 destroy email events 2025-02-28 12:12:02 +02:00
Pete Matsyburka b2b2856ac0 handle required pdf fields 2025-02-27 21:37:32 +02:00
Alex Turchyn b65bca213f add brakeman to CI 2025-02-27 18:43:57 +02:00
Pete Matsyburka 7a72e2cbb7 allow http 2025-02-27 10:59:32 +02:00
Pete Matsyburka 3a73398a1e fix email 2025-02-27 10:01:33 +02:00
Pete Matsyburka cce20d9230 fix rubocop 2025-02-25 14:39:11 +02:00
Pete Matsyburka 8b72c91951 add console audit tables 2025-02-25 13:16:11 +02:00
Pete Matsyburka b0d8e47a75 fix annotate model 2025-02-25 13:10:33 +02:00
Pete Matsyburka 85824faa2b https webhooks 2025-02-25 10:33:16 +02:00
Pete Matsyburka 8a507339af add brakeman 2025-02-25 00:54:05 +02:00
Pete Matsyburka 4636849118 fix permitted params 2025-02-24 23:59:30 +02:00
Pete Matsyburka cc52cae9fc fix not found page 2025-02-24 14:19:48 +02:00
Alex Turchyn b6bb4d2c8b Merge from docusealco/wip 2025-02-24 14:05:38 +02:00
Pete Matsyburka 5032676bd2 fix url download 2025-02-24 13:24:16 +02:00
Pete Matsyburka 146b37f8cd fix clone 2025-02-21 22:21:31 +02:00
Pete Matsyburka 47b3a2d09e multiple recipient emails 2025-02-21 21:49:33 +02:00
Pete Matsyburka f2306b334b update gem 2025-02-20 13:02:58 +02:00
Pete Matsyburka 3affa65571 fix formula condition 2025-02-20 12:13:52 +02:00
Pete Matsyburka 6f3be16826 fix dockerfile 2025-02-18 16:50:48 +02:00
Pete Matsyburka 07ce76537b use ubuntu arm 2025-02-18 16:50:01 +02:00
Pete Matsyburka a1fb38b8cb add email variables 2025-02-18 12:18:59 +02:00
Alex Turchyn 1b52a99662 Merge from docusealco/wip 2025-02-17 22:55:09 +02:00
Pete Matsyburka f5facfa6b9 use ubuntu latest build 2025-02-17 22:49:14 +02:00
Pete Matsyburka 21c6144dc6 fix document controller 2025-02-17 18:45:52 +02:00
Pete Matsyburka 2c155f0e42 use ip to reopen 2025-02-17 12:46:44 +02:00
Alex Turchyn 33cc0a8ab5 Merge from docusealco/wip 2025-02-17 12:36:04 +02:00
Pete Matsyburka 1e65a7e52b fix combined 2025-02-14 22:28:59 +02:00
Pete Matsyburka 14e2dbe1aa detailed party email form 2025-02-14 18:12:15 +02:00
Pete Matsyburka a823a6ac56 fix template clone 2025-02-14 18:12:09 +02:00
Pete Matsyburka cfc4e001ba refactor annots 2025-02-14 12:33:49 +02:00
Pete Matsyburka 496cc855f6 refactor 2025-02-14 11:00:12 +02:00
Pete Matsyburka bfb2db07a5 fix events modal 2025-02-13 16:46:57 +02:00
Pete Matsyburka 9f4f868246 use erb_lint 2025-02-13 13:19:33 +02:00
Pete Matsyburka f91f02f786 remove seats info 2025-02-13 00:10:43 +02:00
Pete Matsyburka 1da72e3e7a update gem 2025-02-12 21:31:56 +02:00
Pete Matsyburka 2e2c0e087d skip missing font 2025-02-12 20:38:46 +02:00
Pete Matsyburka 0c72f58ab0 fix combined 2025-02-12 20:38:46 +02:00
Alex Turchyn d9d969cb79 add translations 2025-02-12 14:31:49 +02:00
Pete Matsyburka 8d685d1f73 adjust event log 2025-02-11 22:37:47 +02:00
Alex Turchyn e406620044 add 'Event Log' modal 2025-02-11 22:23:51 +02:00
Pete Matsyburka a315674421 update gem 2025-02-11 11:18:15 +02:00
Alex Turchyn 8d953f210f add attachment count attribute to template factory 2025-02-11 11:16:32 +02:00
Alex Turchyn 7439ef07ad Merge from docusealco/wip 2025-02-10 13:08:17 +02:00
Pete Matsyburka fc4b4de6ed customize template email per party 2025-02-09 14:13:04 +02:00
Alex Turchyn 96e4733f87 add submissions filtering by 'sent' and 'opened' statuses 2025-02-07 20:20:37 +02:00
Pete Matsyburka 8b6731a289 show logo 2025-02-07 14:48:04 +02:00
Pete Matsyburka f93ac68e9c adjust field drag 2025-02-07 14:39:23 +02:00
Alex Turchyn 439df4879c add stamp text logo 2025-02-07 14:15:35 +02:00
Alex Turchyn 83b6ce8574 fix logos 2025-02-07 14:15:18 +02:00
Pete Matsyburka 6ea07bc13b show minimize on mobile if with description 2025-02-06 13:49:09 +02:00
Alex Turchyn a15624690b show unauthorized message in test environment 2025-02-06 13:41:16 +02:00
Pete Matsyburka 974bb85fb7 rescue malformed PDF 2025-02-06 12:55:27 +02:00
Pete Matsyburka 5dc80601b2 courier font 2025-02-05 18:17:55 +02:00
Pete Matsyburka df2e53bc40 font mono 2025-02-05 16:13:31 +02:00
Alex Turchyn 6e685bbc32 Merge from docusealco/wip 2025-02-05 13:44:10 +02:00
Pete Matsyburka 0b3ca0de96 fix ci 2025-02-04 19:37:42 +02:00
Pete Matsyburka aed3f1f141 fix build 2025-02-04 18:47:15 +02:00
Pete Matsyburka 1a7981e919 update actions 2025-02-04 16:55:24 +02:00
Pete Matsyburka 4add805371 fix field align 2025-02-04 16:19:49 +02:00
Alex Turchyn 66c18317b7 Merge from docusealco/wip 2025-02-04 16:11:47 +02:00
Pete Matsyburka 9edbb885b4 use arm runner 2025-02-04 15:51:31 +02:00
Alex Turchyn c67c80d6bc Merge from docusealco/wip 2025-02-04 13:44:17 +02:00
Pete Matsyburka 0399531e0c html image 2025-02-03 18:38:52 +02:00
Pete Matsyburka 54426556d9 field conditions 2025-02-02 21:36:38 +02:00
Alex Turchyn fa74ee1755 add Progressive Web App 2025-02-02 14:31:42 +02:00
Pete Matsyburka eb53af12c4 adjust form 2025-02-02 12:36:42 +02:00
Pete Matsyburka 38efcb99c0 fix verification 2025-01-31 21:32:15 +02:00
Pete Matsyburka a92d914674 adjust form 2025-01-31 20:11:57 +02:00
Pete Matsyburka 219a698aa5 adjust i18n 2025-01-31 15:26:53 +02:00
Alex Turchyn 5bed6448dc make custom fields titles smaller on mobile devices 2025-01-31 15:19:04 +02:00
Alex Turchyn 6d2a8ca3d3 update translations 2025-01-31 14:43:06 +02:00
Alex Turchyn bfb2e0043d update email attribution footer for Test Mode 2025-01-31 14:43:06 +02:00
Pete Matsyburka 922101b12c fix dashboard toggle 2025-01-31 12:03:52 +02:00
Pete Matsyburka 9ff41a0c4a scroll on mobile field click 2025-01-29 16:08:09 +02:00
Pete Matsyburka 071f187605 fix template conditions 2025-01-29 00:56:14 +02:00
Pete Matsyburka ab6cd0e89c adjust send email job 2025-01-28 18:52:12 +02:00
Pete Matsyburka 97c0dad7ff add with fields search 2025-01-28 14:13:06 +02:00
Pete Matsyburka bc0917a190 fix sign in date 2025-01-28 11:16:24 +02:00
Alex Turchyn 5718acecff add translations 2025-01-28 11:15:33 +02:00
Alex Turchyn 395d43dbeb Merge from docusealco/wip 2025-01-27 13:21:32 +02:00
Pete Matsyburka 37de5e0d0d add resubmit from dashboard 2025-01-25 20:22:48 +02:00
Pete Matsyburka 44e848a1f0 fix heading move 2025-01-25 00:07:20 +02:00
Pete Matsyburka f6d59d731c fix email typo 2025-01-24 13:32:12 +02:00
Pete Matsyburka 87b039e709 fix submitter names 2025-01-23 22:21:01 +02:00
Pete Matsyburka e0c6a4e08b fix submission preview page 2025-01-23 17:11:50 +02:00
Pete Matsyburka 80b06550c2 adjust template link 2025-01-23 11:39:43 +02:00
Pete Matsyburka ec8e35ed7a fix email typo 2025-01-22 23:22:34 +02:00
Alex Turchyn 4dc325a6bb change open/read timeout for webhook requests 2025-01-22 20:58:47 +02:00
Pete Matsyburka d52c57df72 adjust i18n 2025-01-22 11:49:04 +02:00
Alex Turchyn 0cb2557447 add warning message to navbar 2025-01-21 13:10:27 +02:00
Pete Matsyburka cd05c1fd19 search field by title 2025-01-20 23:08:19 +02:00
Pete Matsyburka 5458e82b6e increase parties limit 2025-01-20 21:01:13 +02:00
Pete Matsyburka c59c58624b convert icon to png 2025-01-20 14:46:43 +02:00
Alex Turchyn 2da059d440 Merge from docusealco/wip 2025-01-20 12:48:34 +02:00
Pete Matsyburka b0aabd9e79 add signed option in filename 2025-01-19 16:16:00 +02:00
Pete Matsyburka f1cd60dfad error message 2025-01-19 16:02:40 +02:00
Alex Turchyn a264a8522a add compliances section 2025-01-18 16:39:20 +02:00
Pete Matsyburka 794e339425 add verification settings 2025-01-18 16:35:07 +02:00
Pete Matsyburka 9aaf61e658 add number currency format 2025-01-17 21:08:13 +02:00
Pete Matsyburka 37faa7ece6 do not remove condition on replace 2025-01-17 17:11:10 +02:00
Pete Matsyburka 666e3fde4c fix document remove 2025-01-17 17:06:37 +02:00
Pete Matsyburka 995e040a4e Revert "do not remove condition on replace"
This reverts commit c24ae1751e.
2025-01-17 16:55:11 +02:00
Pete Matsyburka 185ac2cbee refactor 2025-01-17 15:52:49 +02:00
Pete Matsyburka a4e372ff5c fix android scroll 2025-01-17 01:49:36 +02:00
Pete Matsyburka b4834c7674 fix filename date 2025-01-16 16:30:06 +02:00
Pete Matsyburka c24ae1751e do not remove condition on replace 2025-01-16 01:05:06 +02:00
Pete Matsyburka 9bd0ad70c4 add default field options 2025-01-16 00:40:22 +02:00
Pete Matsyburka f210f1d8bf Revert "fix scrolling to fields on Android devices"
This reverts commit e3f3895aac.
2025-01-15 13:57:30 +02:00
Alex Turchyn 92b61992f2 fix vertical scrolling for mobile devices 2025-01-15 02:52:31 +02:00
Alex Turchyn e3f3895aac fix scrolling to fields on Android devices 2025-01-14 23:23:09 +02:00
Alex Turchyn c696890cf5 fix dropdowns in Firefox 2025-01-14 01:57:20 +02:00
Alex Turchyn 288d204d8e Merge from docusealco/wip 2025-01-13 13:00:50 +02:00
Pete Matsyburka 5a96f4f1eb adjust ios mobile 2025-01-11 14:02:04 +02:00
Alex Turchyn 5ff4f9889e add user reviews 2025-01-11 13:09:38 +02:00
Alex Turchyn 78fb93fd02 fix detection of unknown mobile IOS devices 2025-01-11 13:06:21 +02:00
Pete Matsyburka 3c83d459bd add document page attributes 2025-01-10 20:33:23 +02:00
Pete Matsyburka 6cdb8e7132 fix draw cursor 2025-01-10 20:33:16 +02:00
Pete Matsyburka e2b93cbcab fix colors 2025-01-10 15:12:09 +02:00
Pete Matsyburka a59cc7c44e verification redirect 2025-01-10 11:50:54 +02:00
Pete Matsyburka 1984b99e04 fix formula with conditions 2025-01-09 22:25:46 +02:00
Pete Matsyburka fe3e8fba3e fix readonly formula values 2025-01-09 12:26:56 +02:00
Pete Matsyburka 97ecdf2ff0 fix invite submitters api order 2025-01-09 01:09:26 +02:00
Alex Turchyn e4c62087d0 fix translations 2025-01-08 18:17:46 +02:00
Pete Matsyburka 1f9faddfd0 add reply to 2025-01-07 20:38:56 +02:00
Pete Matsyburka 6dff6483bf fix default field title 2025-01-07 17:30:21 +02:00
Pete Matsyburka 2e43eb189f fix define submitters order 2025-01-07 13:04:59 +02:00
Alex Turchyn cee471bfa5 Merge from docusealco/wip 2025-01-06 12:18:51 +02:00
Pete Matsyburka 81521dee81 add ability to reorder fields 2025-01-04 14:20:47 +02:00
Pete Matsyburka 3cac62d0fd fix typo 2025-01-04 01:42:25 +02:00
Pete Matsyburka 24ed3f2e7c filter submissions api 2025-01-04 01:31:54 +02:00
Alex Turchyn 02ad620c9f add translations 2025-01-04 01:10:22 +02:00
Alex Turchyn 9051d5a835 add a text hint under user roles 2025-01-03 18:39:50 +02:00
Alex Turchyn 0abd270f56 update translations 2025-01-03 01:09:29 +02:00
Pete Matsyburka fde9affd70 fix send to next 2025-01-02 23:32:24 +02:00
Pete Matsyburka 4edee2971a superadmin file download 2025-01-02 22:42:08 +02:00
Pete Matsyburka 313d7f8408 adjust response 2025-01-02 22:42:02 +02:00
Alex Turchyn db60810272 add email OTP 2025-01-01 14:38:24 +02:00
Pete Matsyburka b3507c1a3a adjust test toggle size 2024-12-31 19:57:41 +02:00
Alex Turchyn 19b5a07bac Add 'Test mode' switch to the profile dropdown 2024-12-31 19:36:14 +02:00
Pete Matsyburka 616aced4e4 fix spec 2024-12-30 21:38:02 +02:00
Pete Matsyburka c59f2a022a increase retries 2024-12-28 16:58:20 +02:00
151 changed files with 2385 additions and 971 deletions

No files matched your search

+58
View File
@@ -0,0 +1,58 @@
---
:position: before
:position_in_additional_file_patterns: before
:position_in_class: before
:position_in_factory: before
:position_in_fixture: before
:position_in_routes: before
:position_in_serializer: before
:position_in_test: before
:classified_sort: true
:exclude_controllers: true
:exclude_factories: true
:exclude_fixtures: false
:exclude_helpers: true
:exclude_scaffolds: true
:exclude_serializers: false
:exclude_sti_subclasses: false
:exclude_tests: false
:force: false
:format_markdown: false
:format_rdoc: false
:format_yard: false
:frozen: false
:ignore_model_sub_dir: false
:ignore_unknown_models: false
:include_version: false
:show_check_constraints: false
:show_complete_foreign_keys: false
:show_foreign_keys: true
:show_indexes: true
:simple_indexes: false
:sort: false
:timestamp: false
:trace: false
:with_comment: true
:with_column_comments: true
:with_table_comments: true
:active_admin: false
:command:
:debug: false
:hide_default_column_types: ''
:hide_limit_column_types: ''
:ignore_columns:
:ignore_routes:
:models: true
:routes: false
:skip_on_db_migrate: false
:target_action: :do_annotations
:wrapper:
:wrapper_close:
:wrapper_open:
:classes_default_to_s: []
:additional_file_patterns: []
:model_dir:
- app/models
:require: []
:root_dir:
- ''
View File
File renamed without changes.
+37 -10
View File
@@ -7,13 +7,13 @@ jobs:
name: Rubocop
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.4.1
- name: Cache gems
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
@@ -31,13 +31,13 @@ jobs:
name: Erblint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.4.1
- name: Cache gems
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
@@ -49,13 +49,13 @@ jobs:
bundle config path vendor/bundle
bundle install --jobs 4 --retry 4
- name: Run Erblint
run: bundle exec erblint ./app
run: bundle exec erb_lint ./app
eslint:
name: ESLint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v1
with:
@@ -63,7 +63,7 @@ jobs:
- name: Cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v1
- uses: actions/cache@v4
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -77,6 +77,33 @@ jobs:
run: |
./node_modules/eslint/bin/eslint.js "app/javascript/**/*.js"
brakeman:
name: Brakeman
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.4.1
- name: Cache gems
uses: actions/cache@v4
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gem-
- name: Install gems
run: |
gem install bundler
bundle config path vendor/bundle
bundle install --jobs 4 --retry 4
yarn install
sudo apt-get update
sudo apt-get install libvips
- name: Run Brakeman
run: bundle exec brakeman -q --exit-on-warn
rspec:
name: RSpec
runs-on: ubuntu-latest
@@ -96,7 +123,7 @@ jobs:
--health-retries 5
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
@@ -110,12 +137,12 @@ jobs:
with:
chrome-version: 125
- name: Cache node_modules
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
- name: Cache gems
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
+5 -5
View File
@@ -7,7 +7,7 @@ on:
jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04-arm
steps:
- name: Checkout code
@@ -25,22 +25,22 @@ jobs:
tags: |
type=semver,pattern={{version}}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Create .version file
run: echo ${{ github.ref_name }} > .version
- name: Login to Docker Hub
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
uses: docker/build-push-action@v6
with:
context: .
push: true
+3 -3
View File
@@ -1,4 +1,4 @@
FROM ruby:3.4.1-alpine as fonts
FROM ruby:3.4.1-alpine AS fonts
WORKDIR /fonts
@@ -6,7 +6,7 @@ RUN apk --no-cache add fontforge wget && wget https://github.com/satbyy/go-noto-
RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")'
FROM ruby:3.4.1-alpine as webpack
FROM ruby:3.4.1-alpine AS webpack
ENV RAILS_ENV=production
ENV NODE_ENV=production
@@ -32,7 +32,7 @@ COPY ./app/views ./app/views
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
FROM ruby:3.4.1-alpine as app
FROM ruby:3.4.1-alpine AS app
ENV RAILS_ENV=production
ENV BUNDLE_WITHOUT="development:test"
+3 -1
View File
@@ -46,7 +46,6 @@ gem 'twitter_cldr', require: false
gem 'tzinfo-data'
group :development, :test do
gem 'annotate'
gem 'better_html'
gem 'bullet'
gem 'debug'
@@ -63,6 +62,9 @@ group :development, :test do
end
group :development do
gem 'annotaterb'
gem 'brakeman', require: false
gem 'foreman', require: false
gem 'letter_opener_web'
gem 'web-console'
end
+33 -15
View File
@@ -74,9 +74,7 @@ GEM
uri (>= 0.13.1)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
annotate (2.6.5)
activerecord (>= 2.3.0)
rake (>= 0.8.7)
annotaterb (4.14.0)
arabic-letter-connector (0.1.1)
ast (2.4.2)
aws-eventstream (1.3.0)
@@ -120,6 +118,8 @@ GEM
bindex (0.8.1)
bootsnap (1.18.4)
msgpack (~> 1.2)
brakeman (7.0.0)
racc
builder (3.3.0)
bullet (8.0.0)
activesupport (>= 3.0.0)
@@ -223,9 +223,13 @@ GEM
concurrent-ruby (~> 1.1)
webrick (~> 1.7)
websocket-driver (~> 0.7)
ffi (1.17.0)
ffi (1.17.0-aarch64-linux-gnu)
ffi (1.17.0-x86_64-linux-gnu)
ffi (1.17.1)
ffi (1.17.1-aarch64-linux-gnu)
ffi (1.17.1-aarch64-linux-musl)
ffi (1.17.1-arm64-darwin)
ffi (1.17.1-x86_64-linux-gnu)
ffi (1.17.1-x86_64-linux-musl)
foreman (0.88.1)
geom2d (0.4.1)
globalid (1.2.1)
activesupport (>= 6.1)
@@ -324,7 +328,7 @@ GEM
mysql2 (0.5.6)
net-http-persistent (4.0.5)
connection_pool (~> 2.2)
net-imap (0.5.3)
net-imap (0.5.6)
date
net-protocol
net-pop (0.1.2)
@@ -334,12 +338,18 @@ GEM
net-smtp (0.5.0)
net-protocol
nio4r (2.7.4)
nokogiri (1.17.2)
nokogiri (1.18.3)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.17.2-aarch64-linux)
nokogiri (1.18.3-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.17.2-x86_64-linux)
nokogiri (1.18.3-aarch64-linux-musl)
racc (~> 1.4)
nokogiri (1.18.3-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.3-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.3-x86_64-linux-musl)
racc (~> 1.4)
oj (3.16.8)
bigdecimal (>= 3.0)
@@ -377,7 +387,7 @@ GEM
puma (6.5.0)
nio4r (~> 2.0)
racc (1.8.1)
rack (3.1.8)
rack (3.1.10)
rack-proxy (0.7.7)
rack
rack-session (2.0.0)
@@ -520,10 +530,13 @@ GEM
simplecov-html (0.13.1)
simplecov_json_formatter (0.1.4)
smart_properties (1.17.0)
sqlite3 (2.4.1)
sqlite3 (2.5.0)
mini_portile2 (~> 2.8.0)
sqlite3 (2.4.1-aarch64-linux-gnu)
sqlite3 (2.4.1-x86_64-linux-gnu)
sqlite3 (2.5.0-aarch64-linux-gnu)
sqlite3 (2.5.0-aarch64-linux-musl)
sqlite3 (2.5.0-arm64-darwin)
sqlite3 (2.5.0-x86_64-linux-gnu)
sqlite3 (2.5.0-x86_64-linux-musl)
stringio (3.1.2)
strip_attributes (1.14.1)
activemodel (>= 3.0, < 9.0)
@@ -569,17 +582,21 @@ GEM
PLATFORMS
aarch64-linux
aarch64-linux-musl
arm64-darwin
ruby
x86_64-linux
x86_64-linux-musl
DEPENDENCIES
annotate
annotaterb
arabic-letter-connector
aws-sdk-s3
aws-sdk-secretsmanager
azure-storage-blob
better_html
bootsnap
brakeman
bullet
cancancan
capybara
@@ -595,6 +612,7 @@ DEPENDENCIES
faker
faraday
faraday-follow_redirects
foreman
google-cloud-storage
hexapdf
image_processing
@@ -39,7 +39,7 @@ class AccountConfigsController < ApplicationController
end
def account_config_params
params.required(:account_config).permit!.tap do |attrs|
params.required(:account_config).permit(:key, :value, { value: {} }, { value: [] }).tap do |attrs|
attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
end
end
+1 -1
View File
@@ -50,7 +50,7 @@ class AccountsController < ApplicationController
# rubocop:disable Layout/LineLength
render turbo_stream: turbo_stream.replace(
:account_delete_button,
html: helpers.tag.p(I18n.t('your_account_removal_request_will_be_processed_within_2_weeks_please_contact_us_if_you_want_to_keep_your_account'))
html: helpers.tag.p(I18n.t('your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account'))
)
# rubocop:enable Layout/LineLength
end
@@ -42,6 +42,7 @@ module Api
is_authorized = attachment.name.in?(%w[logo preview_images]) ||
(current_user && attachment.record.account.id == current_user.account_id) ||
(current_user && !Docuseal.multitenant? && current_user.role == 'superadmin') ||
!attachment.record.account.account_configs
.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value
@@ -10,7 +10,7 @@ module Api
last_submitter = @submission.submitters.max_by(&:completed_at)
if last_submitter.documents_attachments.blank?
last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(submitter)
last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(last_submitter)
end
last_submitter.documents_attachments
+15 -1
View File
@@ -17,6 +17,8 @@ module Api
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] })
end
submissions = Submissions::Filter.call(submissions, current_user, params)
submissions = paginate(submissions.preload(:created_by_user, :submitters,
template: :folder,
combined_document_attachment: :blob,
@@ -116,7 +118,19 @@ module Api
end
end
json = { submitters: json } if request.path.ends_with?('/init')
if request.path.ends_with?('/init')
json =
if submissions.size == 1
{
id: submissions.first.id,
submitters: json,
expire_at: submissions.first.expire_at,
created_at: submissions.first.created_at
}
else
{ submitters: json }
end
end
json
end
+7 -1
View File
@@ -146,9 +146,15 @@ module Api
if attrs[:completed]
submitter.values = Submitters::SubmitValues.merge_default_values(submitter)
submitter.values = Submitters::SubmitValues.merge_formula_values(submitter)
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
formula_values = Submitters::SubmitValues.build_formula_values(submitter)
if formula_values.present?
submitter.values = submitter.values.merge(formula_values)
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
end
submitter.values = submitter.values.transform_values do |v|
v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v
end
+2
View File
@@ -34,6 +34,8 @@ module Api
}
end
}
rescue HexaPDF::MalformedPDFError
render json: { error: 'Malformed PDF' }, status: :unprocessable_entity
end
end
end
+1 -1
View File
@@ -30,7 +30,7 @@ class ApplicationController < ActionController::Base
redirect_to request.referer, alert: 'Too many requests', status: :too_many_requests
end
if Rails.env.production?
if Rails.env.production? || Rails.env.test?
rescue_from CanCan::AccessDenied do |e|
Rollbar.warning(e) if defined?(Rollbar)
@@ -39,7 +39,7 @@ class NotificationsSettingsController < ApplicationController
end
def email_config_params
params.require(:account_config).permit!.tap do |attrs|
params.require(:account_config).permit(:key, :value, { value: {} }, { value: [] }).tap do |attrs|
attrs[:key] = nil unless attrs[:key].in?([AccountConfig::BCC_EMAILS, AccountConfig::SUBMITTER_REMINDERS])
end
end
@@ -50,7 +50,7 @@ class PersonalizationSettingsController < ApplicationController
end
def account_config_params
attrs = params.require(:account_config).permit!
attrs = params.require(:account_config).permit(:key, :value, { value: {} }, { value: [] })
return attrs if attrs[:value].is_a?(String)
+4
View File
@@ -0,0 +1,4 @@
# frozen_string_literal: true
class PwaController < ActionController::Base
end
@@ -9,8 +9,6 @@ class SendSubmissionEmailController < ApplicationController
SEND_DURATION = 30.minutes
def success; end
def create
@submitter =
if params[:template_slug]
@@ -31,7 +29,7 @@ class SendSubmissionEmailController < ApplicationController
end
respond_to do |f|
f.html { redirect_to success_send_submission_email_index_path }
f.html { render :success }
f.json { head :ok }
end
end
+5 -1
View File
@@ -34,6 +34,8 @@ class StartFormController < ApplicationController
assign_submission_attributes(@submitter, @template)
Submissions::AssignDefinedSubmitters.call(@submitter.submission)
else
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
end
if @submitter.save
@@ -64,8 +66,10 @@ class StartFormController < ApplicationController
.or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
.order(id: :desc)
.where(declined_at: nil)
.where(external_id: nil)
.where(ip: [nil, request.remote_ip])
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
.find_or_initialize_by(**submitter_params.compact_blank)
.find_or_initialize_by(email: submitter_params[:email], **submitter_params.compact_blank)
end
def assign_submission_attributes(submitter, template)
@@ -0,0 +1,25 @@
# frozen_string_literal: true
class SubmissionEventsController < ApplicationController
SUBMISSION_EVENT_ICONS = {
'view_form' => 'eye',
'start_form' => 'player_play',
'complete_form' => 'check',
'send_email' => 'mail_forward',
'click_email' => 'hand_click',
'api_complete_form' => 'check',
'send_reminder_email' => 'mail_forward',
'send_2fa_sms' => '2fa',
'send_sms' => 'send',
'phone_verified' => 'phone_check',
'click_sms' => 'hand_click',
'decline_form' => 'x',
'start_verification' => 'player_play',
'complete_verification' => 'check',
'invite_party' => 'user_plus'
}.freeze
load_and_authorize_resource :submission
def index; end
end
@@ -20,7 +20,10 @@ class SubmissionsPreviewController < ApplicationController
@submission ||= Submission.find_by!(slug: params[:slug])
if @submission.account.archived_at? || (!@submission.submitters.all?(&:completed_at?) && current_user.blank?)
raise ActionController::RoutingError, I18n.t('not_found') if @submission.account.archived_at?
if !@submission.submitters.all?(&:completed_at?) && !signature_valid &&
(!current_user || !current_ability.can?(:read, @submission))
raise ActionController::RoutingError, I18n.t('not_found')
end
@@ -37,6 +40,7 @@ class SubmissionsPreviewController < ApplicationController
def completed
@submission = Submission.find_by!(slug: params[:submissions_preview_slug])
@template = @submission.template
render :completed, layout: 'form'
end
@@ -0,0 +1,58 @@
# frozen_string_literal: true
class SubmittersResubmitController < ApplicationController
load_and_authorize_resource :submitter, parent: false
def update
return redirect_to submit_form_path(slug: @submitter.slug) if @submitter.email != current_user.email
submission = @submitter.template.submissions.new(created_by_user: current_user,
submitters_order: :preserved,
**@submitter.submission.slice(:template_fields,
:account_id,
:template_schema,
:template_submitters,
:preferences))
@submitter.submission.submitters.each do |submitter|
new_submitter = submission.submitters.new(submitter.slice(:uuid, :email, :phone, :name,
:preferences, :metadata, :account_id))
next unless submitter.uuid == @submitter.uuid
assign_submitter_values(new_submitter, submitter)
@new_submitter ||= new_submitter
end
submission.save!
redirect_to submit_form_path(slug: @new_submitter.slug)
end
private
def assign_submitter_values(new_submitter, submitter)
attachments_index = submitter.attachments.index_by(&:uuid)
submitter.submission.template_fields.each do |field|
next if field['submitter_uuid'] != submitter.uuid
next if field['default_value'] == '{{date}}'
next if field['type'] == 'stamp'
next if field['type'] == 'signature'
next if field.dig('preferences', 'formula').present?
value = submitter.values[field['uuid']]
next if value.blank?
if field['type'].in?(%w[image file initials])
Array.wrap(value).each do |attachment_uuid|
new_submitter.attachments << attachments_index[attachment_uuid].dup
end
end
new_submitter.values[field['uuid']] = value
end
end
end
@@ -22,14 +22,16 @@ class TemplatesPreferencesController < ApplicationController
preferences: %i[bcc_completed request_email_subject request_email_body
documents_copy_email_subject documents_copy_email_body
documents_copy_email_enabled documents_copy_email_attach_audit
documents_copy_email_attach_documents
documents_copy_email_attach_documents documents_copy_email_reply_to
completed_notification_email_attach_documents
completed_redirect_url
submitters_order
completed_notification_email_subject completed_notification_email_body
completed_notification_email_enabled completed_notification_email_attach_audit] +
[completed_message: %i[title body]]
[completed_message: %i[title body],
submitters: [%i[uuid request_email_subject request_email_body]]]
).tap do |attrs|
attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1'
attrs[:preferences] = attrs[:preferences].transform_values do |value|
if %w[true false].include?(value)
value == 'true'
+1 -1
View File
@@ -26,7 +26,7 @@ class UserConfigsController < ApplicationController
end
def user_config_params
params.required(:user_config).permit!.tap do |attrs|
params.required(:user_config).permit(:key, :value, { value: {} }, { value: [] }).tap do |attrs|
attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
end
end
+7 -5
View File
@@ -45,17 +45,16 @@ class UsersController < ApplicationController
return redirect_to settings_users_path, notice: I18n.t('unable_to_update_user') if Docuseal.demo?
attrs = user_params.compact_blank.merge(user_params.slice(:archived_at))
attrs.delete(:role) if !role_valid?(attrs[:role]) || current_user == @user
if params.dig(:user, :account_id).present?
account = Account.accessible_by(current_ability).find(params[:user][:account_id])
account = Account.accessible_by(current_ability).find(params.dig(:user, :account_id))
authorize!(:manage, account)
@user.account = account
end
if @user.update(attrs)
if @user.update(attrs.except(current_user == @user ? :role : nil))
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_updated')
else
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity
@@ -84,8 +83,11 @@ class UsersController < ApplicationController
def user_params
if params.key?(:user)
params.require(:user).permit(:email, :first_name, :last_name, :password,
:role, :archived_at, :account_id)
permitted_params = %i[email first_name last_name password archived_at]
permitted_params << :role if role_valid?(params.dig(:user, :role))
params.require(:user).permit(permitted_params)
else
{}
end
@@ -3,6 +3,8 @@ export default class extends HTMLElement {
this.resize()
this.textarea.addEventListener('input', () => this.resize())
this.observeVisibility()
}
resize () {
@@ -11,6 +13,28 @@ export default class extends HTMLElement {
}
}
observeVisibility () {
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.resize()
this.observer.unobserve(this.textarea)
}
})
},
{
threshold: 0.1
}
)
this.observer.observe(this.textarea)
}
disconnectedCallback () {
this.observer.unobserve(this.textarea)
}
get textarea () {
return this.querySelector('textarea')
}
+2 -1
View File
@@ -20,7 +20,8 @@ export default targetable(class extends HTMLElement {
fetch(this.dataset.src).then(async (response) => {
if (response.ok) {
const urls = await response.json()
const isSafariIos = /iPhone|iPad|iPod/i.test(navigator.userAgent)
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
const isSafariIos = isMobileSafariIos || /iPhone|iPad|iPod/i.test(navigator.userAgent)
if (isSafariIos && urls.length > 1) {
this.downloadSafariIos(urls)
+23 -4
View File
@@ -3,7 +3,7 @@
class="field-area flex absolute lg:text-base -outline-offset-1"
dir="auto"
:style="computedStyle"
:class="{ 'font-serif': field.preferences?.font === 'Times', 'text-[1.6vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'field-area-active outline-red-500 outline-dashed outline-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
:class="{ 'font-mono': field.preferences?.font === 'Courier', 'font-serif': field.preferences?.font === 'Times', 'text-[1.6vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'field-area-active outline-red-500 outline-dashed outline-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
>
<div
v-if="(!withFieldPlaceholder || !field.name || field.type === 'cells') && !isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
@@ -191,13 +191,22 @@
class="whitespace-pre-wrap text-gray-400"
:class="{ 'w-full': field.preferences?.align }"
>{{ field.name }}</span>
<span v-else-if="Array.isArray(modelValue)">
<span
v-else-if="Array.isArray(modelValue)"
:class="{ 'w-full': field.preferences?.align }"
>
{{ modelValue.join(', ') }}
</span>
<span v-else-if="field.type === 'date'">
<span
v-else-if="field.type === 'date'"
:class="{ 'w-full': field.preferences?.align }"
>
{{ formattedDate }}
</span>
<span v-else-if="field.type === 'number'">
<span
v-else-if="field.type === 'number'"
class="w-full"
>
{{ formatNumber(modelValue, field.preferences?.format) }}
</span>
<span
@@ -447,8 +456,18 @@ export default {
}
},
formatNumber (number, format) {
if (!number && number !== 0) {
return ''
}
if (format === 'comma') {
return new Intl.NumberFormat('en-US').format(number)
} else if (format === 'usd') {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
} else if (format === 'gbp') {
return new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
} else if (format === 'eur') {
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
} else if (format === 'dot') {
return new Intl.NumberFormat('de-DE').format(number)
} else if (format === 'space') {
+19 -7
View File
@@ -31,7 +31,7 @@
:with-label="withLabel && !withFieldPlaceholder && step.length < 2"
:is-value-set="step.some((f) => f.uuid in values)"
:attachments-index="attachmentsIndex"
@click="$emit('focus-step', stepIndex)"
@click="[$emit('focus-step', stepIndex), maybeScrollOnClick(field, area)]"
/>
</Teleport>
</template>
@@ -109,6 +109,14 @@ export default {
areaRefs: []
}
},
computed: {
isMobileContainer () {
const root = this.$root.$el.parentNode.getRootNode()
const container = root.body || root.querySelector('div')
return container.style.overflow === 'hidden'
}
},
beforeUpdate () {
this.areaRefs = []
},
@@ -121,14 +129,16 @@ export default {
this.scrollIntoArea(field.areas[0])
}
},
maybeScrollOnClick (field, area) {
if (['text', 'number', 'cells'].includes(field.type) && this.isMobileContainer) {
this.scrollIntoArea(area)
}
},
scrollIntoArea (area) {
const areaRef = this.areaRefs.find((a) => a.area === area)
if (areaRef) {
const root = this.$root.$el.parentNode.getRootNode()
const container = root.body || root.querySelector('div')
if (container.style.overflow === 'hidden') {
if (this.isMobileContainer) {
this.scrollInContainer(areaRef.$el)
} else {
const targetRect = areaRef.$refs.scrollToElem.getBoundingClientRect()
@@ -157,13 +167,15 @@ export default {
const formContainer = root.getElementById('form_container')
const container = root.body || root.querySelector('div')
const padding = 64
const isAndroid = /android/i.test(navigator.userAgent)
const padding = isAndroid ? 128 : 64
const scrollboxTop = isAndroid ? scrollbox.getBoundingClientRect().top : 0
const boxRect = scrollbox.children[0].getBoundingClientRect()
const targetRect = target.getBoundingClientRect()
const targetTopRelativeToBox = targetRect.top - boxRect.top
scrollbox.scrollTo({ top: targetTopRelativeToBox - container.offsetHeight + formContainer.offsetHeight + target.offsetHeight + padding, behavior: 'smooth' })
scrollbox.scrollTo({ top: targetTopRelativeToBox + scrollboxTop - container.offsetHeight + formContainer.offsetHeight + target.offsetHeight + padding, behavior: 'smooth' })
},
setAreaRef (el) {
if (el) {
@@ -49,7 +49,7 @@
<MarkdownContent :string="field.description" />
</div>
<FileDropzone
:message="`${t('upload')} ${field.name || t('files')}${field.required ? '' : ` (${t('optional')})`}`"
:message="`${t('upload')} ${(field.title || field.name) || t('files')}${field.required ? '' : ` (${t('optional')})`}`"
:submitter-slug="submitterSlug"
:multiple="true"
:dry-run="dryRun"
+2 -1
View File
@@ -215,7 +215,8 @@ export default {
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`).then(async (response) => {
if (response.ok) {
const urls = await response.json()
const isSafariIos = /iPhone|iPad|iPod/i.test(navigator.userAgent)
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
const isSafariIos = isMobileSafariIos || /iPhone|iPad|iPod/i.test(navigator.userAgent)
if (isSafariIos && urls.length > 1) {
this.downloadSafariIos(urls)
+11 -5
View File
@@ -1,22 +1,28 @@
<template>
<div dir="auto">
<div
class="flex justify-between items-center w-full"
class="flex justify-between items-end w-full mb-3.5 sm:mb-4"
:class="{ 'mb-2': !field.description }"
>
<label
v-if="showFieldNames"
:for="field.uuid"
class="label text-2xl"
class="label text-xl sm:text-2xl py-0"
>
<MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>
{{ field.name && showFieldNames ? field.name : t('date') }}
<template v-if="!field.required">
{{ field.name || t('date') }}
</template>
<template v-if="!field.required">
<span
class="ml-1"
:class="{ 'hidden sm:inline': (field.title || field.name || t('date') ).length > 10 }"
>
({{ t('optional') }})
</template>
</span>
</template>
</label>
<button
+1 -1
View File
@@ -128,7 +128,7 @@ export default {
}
})
} else {
if (file.type === 'image/bmp') {
if (file.type === 'image/bmp' || file.type === 'image/vnd.microsoft.icon') {
file = await this.convertBmpToPng(file)
}
+33 -12
View File
@@ -15,7 +15,7 @@
/>
<FieldAreas
:steps="readonlyConditionalFields.map((e) => [e])"
:values="readonlyConditionalFields.reduce((acc, f) => { acc[f.uuid] = (values[f.uuid] || f.default_value); return acc }, {})"
:values="readonlyConditionalFieldValues"
:submitter="submitter"
:attachments-index="attachmentsIndex"
:submittable="false"
@@ -23,6 +23,7 @@
<FormulaFieldAreas
v-if="formulaFields.length"
:fields="formulaFields"
:readonly-values="readonlyConditionalFieldValues"
:values="values"
/>
<Teleport
@@ -99,7 +100,8 @@
<button
v-if="!isCompleted"
id="minimize_form_button"
class="absolute right-0 mr-2 mt-2 top-0 hidden md:block"
class="absolute right-0 top-0"
:class="currentField?.description?.length > 100 ? 'mr-1 mt-1 md:mr-2 md:mt-2': 'mr-2 mt-2 hidden md:block'"
:title="t('minimize')"
@click.prevent="minimizeForm"
>
@@ -163,7 +165,7 @@
v-if="showFieldNames && (currentField.name || currentField.title)"
:for="currentField.uuid"
dir="auto"
class="label text-2xl"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
:class="{ 'mb-2': !currentField.description }"
>
<MarkdownContent
@@ -172,7 +174,11 @@
/>
<template v-else>
{{ currentField.name }}
<template v-if="!currentField.required">({{ t('optional') }})</template>
</template>
<template v-if="!currentField.required">
<span :class="{ 'hidden sm:inline': (currentField.title || currentField.name).length > 20 }">
({{ t('optional') }})
</span>
</template>
</label>
<div
@@ -220,7 +226,7 @@
v-if="showFieldNames && (currentField.name || currentField.title)"
:for="currentField.uuid"
dir="auto"
class="label text-2xl"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
:class="{ 'mb-2': !currentField.description }"
>
<MarkdownContent
@@ -229,7 +235,11 @@
/>
<template v-else>
{{ currentField.name }}
<template v-if="!currentField.required">({{ t('optional') }})</template>
</template>
<template v-if="!currentField.required">
<span :class="{ 'hidden sm:inline': (currentField.title || currentField.name).length > 20 }">
({{ t('optional') }})
</span>
</template>
</label>
<div
@@ -465,7 +475,7 @@
</div>
<div
v-if="(currentField.type !== 'payment' && currentField.type !== 'verification') || submittedValues[currentField.uuid]"
:class="currentField.type === 'signature' ? 'mt-2' : 'mt-6 md:mt-8'"
:class="currentField.type === 'signature' ? 'mt-2' : 'mt-4 md:mt-6'"
>
<button
id="submit_form_button"
@@ -521,9 +531,9 @@
/>
<div
v-if="stepFields.length < 80"
class="flex justify-center"
class="flex justify-center mt-3 sm:mt-4 mb-0 sm:mb-1"
>
<div class="flex items-center mt-4 mb-1 flex-wrap">
<div class="flex items-center flex-wrap">
<a
v-for="(step, index) in stepFields"
:key="step[0].uuid"
@@ -855,7 +865,16 @@ export default {
},
computed: {
isMobile () {
return /android|iphone|ipad/i.test(navigator.userAgent)
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
return isMobileSafariIos || /android|iphone|ipad/i.test(navigator.userAgent)
},
readonlyConditionalFieldValues () {
return this.readonlyConditionalFields.reduce((acc, f) => {
acc[f.uuid] = (this.values[f.uuid] || f.default_value)
return acc
}, {})
},
attachmentConditionsIndex () {
return this.schema.reduce((acc, item) => {
@@ -1018,7 +1037,7 @@ export default {
}, [])
},
formulaFields () {
return this.fields.filter((f) => f.preferences?.formula && f.type !== 'payment')
return this.fields.filter((f) => f.preferences?.formula && f.type !== 'payment' && this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f))
},
attachmentsIndex () {
return this.attachments.reduce((acc, a) => {
@@ -1103,7 +1122,9 @@ export default {
this.minimizeForm()
}
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
const isMobile = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit|android/i.test(navigator.userAgent)
if (isMobile || /iPhone|iPad|iPod/i.test(navigator.userAgent)) {
this.$nextTick(() => {
const root = this.$root.$el.parentNode.getRootNode()
const scrollbox = root.getElementById('scrollbox')
@@ -38,6 +38,11 @@ export default {
required: false,
default: () => []
},
readonlyValues: {
type: Object,
required: false,
default: () => ({})
},
values: {
type: Object,
required: false,
@@ -87,7 +92,7 @@ export default {
},
calculateFormula (field) {
const transformedFormula = field.preferences.formula.replace(/{{(.*?)}}/g, (match, uuid) => {
return this.values[uuid] || 0.0
return this.readonlyValues[uuid] || this.values[uuid] || 0.0
})
return this.math.evaluate(transformedFormula.toLowerCase())
+14 -4
View File
@@ -1,9 +1,19 @@
<template>
<div v-if="modelValue">
<div class="flex justify-between items-center w-full mb-2">
<div class="flex justify-between items-end w-full mb-3.5 md:mb-4">
<label
class="label text-2xl"
>{{ showFieldNames && field.name ? field.name : t('image') }}</label>
v-if="showFieldNames"
:for="field.uuid"
class="label text-xl sm:text-2xl py-0"
>
<MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>
{{ field.name || t('image') }}
</template>
</label>
<button
class="btn btn-outline btn-sm"
@click.prevent="remove"
@@ -35,7 +45,7 @@
<MarkdownContent :string="field.description" />
</div>
<FileDropzone
:message="`${t('upload')} ${field.name || t('image')}${field.required ? '' : ` (${t('optional')})`}`"
:message="`${t('upload')} ${(field.title || field.name) || t('image')}${field.required ? '' : ` (${t('optional')})`}`"
:submitter-slug="submitterSlug"
:dry-run="dryRun"
:accept="'image/*'"
@@ -1,21 +1,22 @@
<template>
<div dir="auto">
<div
class="flex justify-between items-center w-full"
class="flex justify-between items-end w-full mb-3.5 md:mb-4"
:class="{ 'mb-2': !field.description }"
>
<label
class="label text-2xl"
v-if="showFieldNames"
class="label text-xl sm:text-2xl py-0"
>
<MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>
{{ showFieldNames && field.name ? field.name : t('initials') }}
{{ field.name || t('initials') }}
</template>
</label>
<div class="space-x-2 flex">
<div class="space-x-2 flex flex-none">
<span
v-if="isDrawInitials"
class="tooltip"
@@ -40,7 +40,7 @@
>
</div>
<div
class="mt-6 md:mt-8"
class="mt-4 md:mt-6"
>
<button
type="submit"
@@ -3,13 +3,19 @@
v-if="showFieldNames && (field.name || field.title)"
:for="field.uuid"
dir="auto"
class="label text-2xl"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
:class="{ 'mb-2': !field.description }"
><MarkdownContent
v-if="field.title"
:string="field.title"
/>
>
<MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>{{ field.name }}</template>
<template v-if="!field.required">
<span :class="{ 'hidden sm:inline': (field.title || field.name).length > 20 }">
({{ t('optional') }})
</span>
</template>
</label>
<div
v-if="field.description"
@@ -3,15 +3,18 @@
v-if="showFieldNames && (field.name || field.title)"
:for="field.uuid"
dir="auto"
class="label text-2xl"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
:class="{ 'mb-2': !field.description }"
><MarkdownContent
v-if="field.title"
:string="field.title"
/>
>
<MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>{{ field.name }}</template>
<template v-if="!field.required">
({{ t('optional') }})
<span :class="{ 'hidden sm:inline': (field.title || field.name).length > 20 }">
({{ t('optional') }})
</span>
</template>
</label>
<div
@@ -1,7 +1,7 @@
<template>
<label
v-if="!modelValue && !sessionId"
class="label text-2xl mb-2"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
>
<MarkdownContent
v-if="field.title"
@@ -1,8 +1,9 @@
<template>
<div>
<label
v-if="showFieldNames"
:for="isCodeSent ? 'one_time_code' : field.uuid"
class="label text-2xl"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
:class="{ 'mb-2': !field.description }"
>
<MarkdownContent
@@ -10,10 +11,7 @@
:string="field.title"
/>
<template v-else>
{{ showFieldNames && field.name ? field.name : t('verified_phone_number') }}
<template v-if="!field.required">
({{ t('optional') }})
</template>
{{ field.name || t('verified_phone_number') }}
</template>
</label>
<div
@@ -297,7 +295,7 @@ export default {
const data = await resp.json()
if (resp.status === 422) {
alert(this.t('number_phone_is_invalid').replace('{number}', this.fullInternationalPhoneValue))
alert(data.error || this.t('number_phone_is_invalid').replace('{number}', this.fullInternationalPhoneValue))
} else if (resp.status === 429) {
alert(data.error)
}
@@ -4,21 +4,22 @@
class="relative"
>
<div
class="flex justify-between items-center w-full"
class="flex justify-between items-end w-full mb-3.5 md:mb-4"
:class="{ 'mb-2': !field.description }"
>
<label
class="label text-2xl"
v-if="showFieldNames"
class="label text-xl sm:text-2xl py-0"
>
<MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>
{{ showFieldNames && field.name ? field.name : t('signature') }}
{{ field.name || t('signature') }}
</template>
</label>
<div class="space-x-2 flex">
<div class="space-x-2 flex flex-none">
<span
v-if="isTextSignature && format !== 'typed' && format !== 'upload'"
class="tooltip"
@@ -39,6 +40,7 @@
<span
v-else-if="withTypedSignature && format !== 'typed' && format !== 'drawn' && format !== 'upload'"
class="tooltip ml-2"
:class="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
:data-tip="t('type_text')"
>
<a
@@ -56,6 +58,7 @@
<span
v-if="format !== 'typed' && format !== 'drawn' && format !== 'upload' && format !== 'drawn_or_typed'"
class="tooltip"
:class="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
:data-tip="t('take_photo')"
>
<label
+9 -6
View File
@@ -3,15 +3,18 @@
v-if="showFieldNames && (field.name || field.title)"
:for="field.uuid"
dir="auto"
class="label text-2xl"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
:class="{ 'mb-2': !field.description }"
><MarkdownContent
v-if="field.title"
:string="field.title"
/>
>
<MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>{{ field.name }}</template>
<template v-if="!field.required">
({{ t('optional') }})
<span :class="{ 'hidden sm:inline': (field.title || field.name).length > 20 }">
({{ t('optional') }})
</span>
</template>
</label>
<div
@@ -1,6 +1,6 @@
<template>
<label
class="label text-2xl mb-2"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5"
>
<MarkdownContent
v-if="field.title"
@@ -31,6 +31,15 @@
class="animate-spin h-10"
/>
</div>
<div v-else-if="redirectUrl">
<a
:href="redirectUrl"
target="_blank"
class="white-button w-full"
>
{{ t('verify_id') }}
</a>
</div>
<div
ref="widgetContainer"
/>
@@ -87,6 +96,7 @@ export default {
return {
isCreatingCheckout: false,
isMathLoaded: false,
redirectUrl: '',
isLoading: false,
eidEasyData: {}
}
@@ -125,12 +135,16 @@ export default {
async mounted () {
this.isLoading = true
Promise.all([
import('@eid-easy/eideasy-widget'),
this.start()
]).finally(() => {
this.isLoading = false
})
if (new URLSearchParams(window.location.search).get('submit') === 'true') {
this.$emit('submit')
} else {
Promise.all([
import('@eid-easy/eideasy-widget'),
this.start()
]).finally(() => {
this.isLoading = false
})
}
},
methods: {
start () {
@@ -143,14 +157,26 @@ export default {
}).then(async (resp) => {
this.eidEasyData = await resp.json()
const eidEasyWidget = document.createElement('eideasy-widget')
if (this.eidEasyData.available_methods[0] === 'itsme-qes-signature' &&
this.eidEasyData.available_methods.length === 1) {
const redirectUrl = new URL('https://id.eideasy.com/sign_contract_external')
for (const key in this.widgetSettings) {
eidEasyWidget[key] = this.widgetSettings[key]
redirectUrl.searchParams.append('client_id', this.eidEasyData.client_id)
redirectUrl.searchParams.append('doc_id', this.eidEasyData.doc_id)
redirectUrl.searchParams.append('country', this.countryCode)
redirectUrl.searchParams.append('lang', this.locale)
this.redirectUrl = redirectUrl.toString()
} else {
const eidEasyWidget = document.createElement('eideasy-widget')
for (const key in this.widgetSettings) {
eidEasyWidget[key] = this.widgetSettings[key]
}
this.$refs.widgetContainer.innerHTML = ''
this.$refs.widgetContainer.appendChild(eidEasyWidget)
}
this.$refs.widgetContainer.innerHTML = ''
this.$refs.widgetContainer.appendChild(eidEasyWidget)
})
},
async submit () {
+32 -40
View File
@@ -10,7 +10,7 @@
<div
v-if="isSelected || isDraw"
class="top-0 bottom-0 right-0 left-0 absolute border border-1.5 pointer-events-none"
:class="field.type === 'heading' ? '' : borderColors[submitterIndex]"
:class="field.type === 'heading' ? '' : borderColors[submitterIndex % borderColors.length]"
/>
<div
v-if="field.type === 'cells' && (isSelected || isDraw)"
@@ -20,7 +20,7 @@
v-for="(cellW, index) in cells"
:key="index"
class="absolute top-0 bottom-0 border-r"
:class="field.type === 'heading' ? '' : borderColors[submitterIndex]"
:class="field.type === 'heading' ? '' : borderColors[submitterIndex % borderColors.length]"
:style="{ left: (cellW / area.w * 100) + '%' }"
>
<span
@@ -70,7 +70,7 @@
@keydown.enter.prevent="onNameEnter"
@focus="onNameFocus"
@blur="onNameBlur"
>{{ optionIndexText }} {{ (defaultField ? (field.title || field.name) : field.name) || defaultName }}</span>
>{{ optionIndexText }} {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || defaultName }}</span>
<div
v-if="isSettingsFocus || (isValueInput && field.type !== 'heading') || (isNameFocus && !['checkbox', 'phone'].includes(field.type))"
class="flex items-center ml-1.5"
@@ -161,7 +161,7 @@
ref="touchValueTarget"
class="flex items-center h-full w-full"
dir="auto"
:class="[isValueInput ? 'bg-opacity-50' : 'bg-opacity-80', field.type === 'heading' ? 'bg-gray-50' : bgColors[submitterIndex], isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? (alignClasses[field.preferences?.align] || '') : 'justify-center']"
:class="[isValueInput ? 'bg-opacity-50' : 'bg-opacity-80', field.type === 'heading' ? 'bg-gray-50' : bgColors[submitterIndex % bgColors.length], isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? (alignClasses[field.preferences?.align] || '') : 'justify-center']"
@click="focusValueInput"
>
<span
@@ -212,7 +212,7 @@
:contenteditable="isValueInput"
class="whitespace-pre-wrap outline-none empty:before:content-[attr(placeholder)] before:text-gray-400"
:class="{ 'cursor-text': isValueInput }"
:placeholder="withFieldPlaceholder && !isValueInput ? field.name || defaultName : t('type_value')"
:placeholder="withFieldPlaceholder && !isValueInput ? defaultField?.title || field.title || field.name || defaultName : t('type_value')"
@blur="onDefaultValueBlur"
@paste.prevent="onPaste"
@keydown.enter="onDefaultValueEnter"
@@ -361,6 +361,7 @@ export default {
isMoved: false,
renderDropdown: false,
isNameFocus: false,
isHeadingSelected: false,
textOverflowChars: 0,
dragFrom: { x: 0, y: 0 }
}
@@ -377,7 +378,7 @@ export default {
}
},
isValueInput () {
return (this.field.type === 'heading' && this.isSelected) || this.isContenteditable || (this.inputMode && ['text', 'number', 'date'].includes(this.field.type))
return (this.field.type === 'heading' && this.isHeadingSelected) || this.isContenteditable || (this.inputMode && ['text', 'number', 'date'].includes(this.field.type))
},
modalContainerEl () {
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
@@ -420,16 +421,6 @@ export default {
},
borderColors () {
return [
'border-red-500/80',
'border-sky-500/80',
'border-emerald-500/80',
'border-yellow-300/80',
'border-purple-600/80',
'border-pink-500/80',
'border-cyan-500/80',
'border-orange-500/80',
'border-lime-500/80',
'border-indigo-500/80',
'border-red-500/80',
'border-sky-500/80',
'border-emerald-500/80',
@@ -444,16 +435,6 @@ export default {
},
bgColors () {
return [
'bg-red-100',
'bg-sky-100',
'bg-emerald-100',
'bg-yellow-100',
'bg-purple-100',
'bg-pink-100',
'bg-cyan-100',
'bg-orange-100',
'bg-lime-100',
'bg-indigo-100',
'bg-red-100',
'bg-sky-100',
'bg-emerald-100',
@@ -499,13 +480,13 @@ export default {
methods: {
buildDefaultName: Field.methods.buildDefaultName,
closeDropdown () {
document.activeElement.blur()
this.$el.getRootNode().activeElement.blur()
},
maybeToggleDefaultValue () {
if (['text', 'number'].includes(this.field.type)) {
this.isContenteditable = true
this.$nextTick(() => this.focusValueInput())
this.focusValueInput()
} else if (this.field.type === 'checkbox') {
this.field.readonly = !this.field.readonly
this.field.default_value === true ? delete this.field.default_value : this.field.default_value = true
@@ -527,20 +508,28 @@ export default {
}
},
focusValueInput (e) {
if (this.$refs.defaultValue !== document.activeElement) {
this.$refs.defaultValue.focus()
this.$nextTick(() => {
if (this.$refs.defaultValue && this.$refs.defaultValue !== document.activeElement) {
this.$refs.defaultValue.focus()
if (this.$refs.defaultValue.innerText.length && this.$refs.defaultValue !== e?.target) {
window.getSelection().collapse(
this.$refs.defaultValue.firstChild,
this.$refs.defaultValue.innerText.length
)
if (this.$refs.defaultValue.innerText.length && this.$refs.defaultValue !== e?.target) {
window.getSelection().collapse(
this.$refs.defaultValue.firstChild,
this.$refs.defaultValue.innerText.length
)
}
}
}
})
},
formatNumber (number, format) {
if (format === 'comma') {
return new Intl.NumberFormat('en-US').format(number)
} else if (format === 'usd') {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
} else if (format === 'gbp') {
return new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
} else if (format === 'eur') {
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
} else if (format === 'dot') {
return new Intl.NumberFormat('de-DE').format(number)
} else if (format === 'space') {
@@ -646,6 +635,7 @@ export default {
const text = this.$refs.defaultValue.innerText.trim()
this.isContenteditable = false
this.isHeadingSelected = false
if (text) {
if (this.field.type === 'number') {
@@ -763,10 +753,6 @@ export default {
this.selectedAreaRef.value = this.area
if (this.field.type === 'heading') {
this.$nextTick(() => this.focusValueInput())
}
this.dragFrom = { x: rect.left - e.clientX, y: rect.top - e.clientY }
this.$el.getRootNode().addEventListener('mousemove', this.mouseMove)
@@ -801,6 +787,12 @@ export default {
this.save()
}
if (this.field.type === 'heading') {
this.isHeadingSelected = !this.isMoved
this.focusValueInput()
}
this.isDragged = false
this.isMoved = false
+123 -20
View File
@@ -179,6 +179,7 @@
:with-arrows="template.schema.length > 1"
:item="item"
:document="sortedDocuments[index]"
:data-document-uuid="item.attachment_uuid"
:accept-file-types="acceptFileTypes"
:with-replace-button="withUploadButton"
:editable="editable"
@@ -187,6 +188,7 @@
@remove="onDocumentRemove"
@replace="onDocumentReplace"
@up="moveDocument(item, -1)"
@reorder="reorderFields"
@down="moveDocument(item, 1)"
@change="save"
/>
@@ -220,8 +222,8 @@
</div>
<div
id="pages_container"
class="w-full overflow-y-hidden overflow-x-hidden mt-0.5 pt-0.5"
:class="isMobile ? 'overflow-y-auto' : 'md:overflow-y-auto'"
class="w-full overflow-x-hidden mt-0.5 pt-0.5"
:class="isMobile ? 'overflow-y-auto' : 'overflow-y-hidden md:overflow-y-auto'"
>
<div
ref="documents"
@@ -265,6 +267,7 @@
:input-mode="inputMode"
:default-fields="[...defaultRequiredFields, ...defaultFields]"
:allow-draw="!onlyDefinedFields"
:data-document-uuid="document.uuid"
:default-submitters="defaultSubmitters"
:with-field-placeholder="withFieldPlaceholder"
:draw-field="drawField"
@@ -363,6 +366,7 @@
:with-help="withHelp"
:default-submitters="defaultSubmitters"
:draw-field-type="drawFieldType"
:with-fields-search="withFieldsSearch"
:default-fields="[...defaultRequiredFields, ...defaultFields]"
:template="template"
:default-required-fields="defaultRequiredFields"
@@ -620,6 +624,11 @@ export default {
required: false,
default: true
},
withFieldsSearch: {
type: Boolean,
required: false,
default: null
},
withFieldsList: {
type: Boolean,
required: false,
@@ -700,7 +709,9 @@ export default {
return this.locale.split('-')[0].toLowerCase()
},
isMobile () {
return /android|iphone|ipad/i.test(navigator.userAgent)
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
return isMobileSafariIos || /android|iphone|ipad/i.test(navigator.userAgent)
},
defaultDateFormat () {
const isUsBrowser = Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US')
@@ -769,13 +780,17 @@ export default {
}
})
const defineSubmittersUuids = this.defineSubmitters.map((name) => {
return this.template.submitters.find(e => e.name === name)?.uuid
})
this.defineSubmitters.forEach((name, index) => {
const submitter = (this.template.submitters[index] ||= {})
submitter.name = name || this.submitterDefaultNames[index]
if (existingSubmittersUuids.filter(Boolean).length) {
submitter.uuid = existingSubmittersUuids[index] || submitter.uuid || v4()
if (defineSubmittersUuids.filter(Boolean).length || existingSubmittersUuids.filter(Boolean).length) {
submitter.uuid = defineSubmittersUuids[index] || existingSubmittersUuids[index] || submitter.uuid || v4()
} else {
submitter.uuid ||= v4()
}
@@ -818,6 +833,89 @@ export default {
this.documentRefs = []
},
methods: {
reorderFields (item) {
const itemFields = []
const fields = []
const fieldAreasIndex = {}
const attachmentUuids = this.template.schema.map((e) => e.attachment_uuid)
this.template.fields.forEach((f) => {
if (f.areas?.length) {
const firstArea = f.areas.reduce((min, a) => {
return attachmentUuids.indexOf(a.attachment_uuid) < attachmentUuids.indexOf(min.attachment_uuid) ? a : min
}, f.areas[0])
if (firstArea.attachment_uuid === item.attachment_uuid) {
itemFields.push(f)
} else {
fields.push(f)
}
} else {
fields.push(f)
}
})
const sortArea = (aArea, bArea) => {
if (aArea.attachment_uuid === bArea.attachment_uuid) {
if (aArea.page === bArea.page) {
if (Math.abs(aArea.y - bArea.y) < 0.01) {
if (aArea.x === bArea.x) {
return 0
} else {
return aArea.x - bArea.x
}
} else {
return aArea.y - bArea.y
}
} else {
return aArea.page - bArea.page
}
} else {
return attachmentUuids.indexOf(aArea.attachment_uuid) - attachmentUuids.indexOf(bArea.attachment_uuid)
}
}
itemFields.sort((aField, bField) => {
const aArea = (fieldAreasIndex[aField.uuid] ||= [...(aField.areas || [])].sort(sortArea)[0])
const bArea = (fieldAreasIndex[bField.uuid] ||= [...(bField.areas || [])].sort(sortArea)[0])
return sortArea(aArea, bArea)
})
const insertBeforeAttachmentUuids = attachmentUuids.slice(this.template.schema.indexOf(item) + 1)
let sortedFields = []
if (insertBeforeAttachmentUuids.length) {
const insertAfterField = fields.find((f) => {
if (f.areas?.length) {
return f.areas.find((a) => insertBeforeAttachmentUuids.includes(a.attachment_uuid))
} else {
return false
}
})
if (insertAfterField) {
fields.splice(fields.indexOf(insertAfterField), 0, ...itemFields)
sortedFields = fields
} else {
sortedFields = fields.concat(itemFields)
}
} else {
if (fields.length && itemFields.length && this.template.fields.indexOf(fields[0]) > this.template.fields.indexOf(itemFields[0])) {
sortedFields = itemFields.concat(fields)
} else {
sortedFields = fields.concat(itemFields)
}
}
if (this.template.fields.length === sortedFields.length) {
this.template.fields = sortedFields
this.save()
}
},
closeDropdown () {
document.activeElement.blur()
},
@@ -1228,7 +1326,11 @@ export default {
if (!this.fieldsDragFieldRef.value) {
if (['select', 'multiple', 'radio'].includes(field.type)) {
field.options = [{ value: '', uuid: v4() }]
if (this.dragField?.options?.length) {
field.options = this.dragField.options.map(option => ({ value: option, uuid: v4() }))
} else {
field.options = [{ value: '', uuid: v4() }]
}
}
if (['stamp', 'heading'].includes(field.type)) {
@@ -1299,7 +1401,6 @@ export default {
const lastArea = field.areas[field.areas.length - 1]
if (lastArea) {
fieldArea.x -= lastArea.w / 2
fieldArea.w = lastArea.w
fieldArea.h = lastArea.h
}
@@ -1331,6 +1432,8 @@ export default {
const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid)
const areaRef = documentRef.pageRefs[area.page].areaRefs.find((ref) => ref.area === this.selectedAreaRef.value)
areaRef.isHeadingSelected = true
areaRef.focusValueInput()
})
}
@@ -1418,29 +1521,29 @@ export default {
onDocumentRemove (item) {
if (window.confirm(this.t('are_you_sure_'))) {
this.template.schema.splice(this.template.schema.indexOf(item), 1)
}
const removedFieldUuids = []
const removedFieldUuids = []
this.template.fields.forEach((field) => {
[...(field.areas || [])].forEach((area) => {
if (area.attachment_uuid === item.attachment_uuid) {
field.areas.splice(field.areas.indexOf(area), 1)
this.template.fields.forEach((field) => {
[...(field.areas || [])].forEach((area) => {
if (area.attachment_uuid === item.attachment_uuid) {
field.areas.splice(field.areas.indexOf(area), 1)
removedFieldUuids.push(field.uuid)
}
removedFieldUuids.push(field.uuid)
}
})
})
})
this.template.fields =
this.template.fields.filter((f) => !removedFieldUuids.includes(f.uuid) || f.areas?.length)
this.template.fields =
this.template.fields.filter((f) => !removedFieldUuids.includes(f.uuid) || f.areas?.length)
this.save()
this.save()
}
},
onDocumentReplace (data) {
const { replaceSchemaItem, schema, documents } = data
this.template.schema.splice(this.template.schema.indexOf(replaceSchemaItem), 1, schema[0])
this.template.schema.splice(this.template.schema.indexOf(replaceSchemaItem), 1, { ...replaceSchemaItem, ...schema[0] })
this.template.documents.push(...documents)
if (data.fields) {
@@ -177,7 +177,7 @@ export default {
fields () {
if (this.item.submitter_uuid) {
return this.template.fields.reduce((acc, f) => {
if (f !== this.item && f.submitter_uuid === this.item.submitter_uuid) {
if (f !== this.item) {
acc.push(f)
}
@@ -7,6 +7,7 @@
:input-mode="inputMode"
:number="index"
:editable="editable"
:data-page="index"
:areas="areasIndex[index]"
:allow-draw="allowDraw"
:is-drag="isDrag"
+6 -6
View File
@@ -23,7 +23,7 @@
/>
<Contenteditable
ref="name"
:model-value="(defaultField ? (field.title || field.name) : field.name) || defaultName"
:model-value="(defaultField ? (defaultField.title || field.title || field.name) : field.name) || defaultName"
:editable="editable && !defaultField && field.type != 'heading'"
:icon-inline="true"
:icon-width="18"
@@ -192,14 +192,14 @@
class="w-full input input-primary input-xs text-sm bg-transparent"
:placeholder="`${t('option')} ${index + 1}`"
type="text"
:readonly="!editable"
:readonly="!editable || defaultField"
required
dir="auto"
@focus="maybeFocusOnOptionArea(option)"
@blur="save"
>
<button
v-if="editable"
v-if="editable && !defaultField"
class="text-sm w-3.5"
tabindex="-1"
@click="removeOption(option)"
@@ -208,11 +208,11 @@
</button>
</div>
<div
v-if="field.options && !editable"
v-if="field.options && (!editable || defaultField)"
class="pb-1"
/>
<button
v-else-if="field.options && editable"
v-else-if="field.options && editable && !defaultField"
class="text-center text-sm w-full pb-1"
@click="addOption"
>
@@ -392,7 +392,7 @@ export default {
return this.sortedAreas[0] && this.$emit('scroll-to', this.sortedAreas[0])
},
closeDropdown () {
document.activeElement.blur()
this.$el.getRootNode().activeElement.blur()
},
addOption () {
this.field.options.push({ value: '', uuid: v4() })
@@ -26,6 +26,33 @@
{{ t('format') }}
</label>
</div>
<div
v-if="field.type === 'verification'"
class="py-1.5 px-1 relative"
@click.stop
>
<select
:placeholder="t('method')"
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
@change="[field.preferences ||= {}, field.preferences.method = $event.target.value, save()]"
>
<option
v-for="method in ['QeS', 'AeS']"
:key="method"
:value="method.toLowerCase()"
:selected="method.toLowerCase() === field.preferences?.method || (method === 'QeS' && !field.preferences?.method)"
>
{{ method }}
</option>
</select>
<label
:style="{ backgroundColor }"
class="absolute -top-1 left-2.5 px-1 h-4"
style="font-size: 8px"
>
{{ t('method') }}
</label>
</div>
<div
v-if="['number', 'cells'].includes(field.type)"
class="py-1.5 px-1 relative"
@@ -229,7 +256,7 @@
</label>
</div>
<li
v-if="withRequired && field.type != 'phone' && field.type != 'stamp'"
v-if="withRequired && field.type !== 'phone' && field.type !== 'stamp' && field.type !== 'verification'"
@click.stop
>
<label class="cursor-pointer py-1.5">
@@ -461,6 +488,9 @@ export default {
numberFormats () {
return [
'none',
'usd',
'eur',
'gbp',
'comma',
'dot',
'space'
@@ -541,6 +571,12 @@ export default {
formatNumber (number, format) {
if (format === 'comma') {
return new Intl.NumberFormat('en-US').format(number)
} else if (format === 'usd') {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
} else if (format === 'gbp') {
return new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
} else if (format === 'eur') {
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
} else if (format === 'dot') {
return new Intl.NumberFormat('de-DE').format(number)
} else if (format === 'space') {
@@ -9,7 +9,7 @@
<div class="flex items-center space-x-2">
<span
class="w-3 h-3 flex-shrink-0 rounded-full"
:class="colors[submitters.indexOf(selectedSubmitter)]"
:class="colors[submitters.indexOf(selectedSubmitter) % colors.length]"
/>
<Contenteditable
v-model="selectedSubmitter.name"
@@ -53,7 +53,7 @@
<span class="py-1 flex items-center">
<span
class="rounded-full w-3 h-3 ml-1 mr-3"
:class="colors[index]"
:class="colors[index % colors.length]"
/>
<span>
{{ submitter.name }}
@@ -101,7 +101,7 @@
>
<button
class="mx-1 w-3 h-3 rounded-full"
:class="colors[submitters.indexOf(selectedSubmitter)]"
:class="colors[submitters.indexOf(selectedSubmitter) % colors.length]"
/>
</label>
<label
@@ -113,7 +113,7 @@
<div class="flex items-center space-x-2">
<span
class="w-3 h-3 rounded-full"
:class="colors[submitters.indexOf(selectedSubmitter)]"
:class="colors[submitters.indexOf(selectedSubmitter) % colors.length]"
/>
<Contenteditable
v-model="selectedSubmitter.name"
@@ -143,6 +143,7 @@
<li
v-for="(submitter, index) in submitters"
:key="submitter.uuid"
class="w-full"
>
<a
href="#"
@@ -153,7 +154,7 @@
<span class="py-1 flex items-center">
<span
class="rounded-full w-3 h-3 ml-1 mr-3"
:class="colors[index]"
:class="colors[index % colors.length]"
/>
<span>
{{ submitter.name }}
@@ -191,7 +192,10 @@
</div>
</a>
</li>
<li v-if="submitters.length < names.length && editable && allowAddNew">
<li
v-if="submitters.length < names.length && editable && allowAddNew"
class="w-full"
>
<a
href="#"
class="flex px-2"
@@ -215,6 +219,14 @@ import { IconUserPlus, IconTrashX, IconPlus, IconChevronUp, IconChevronDown } fr
import Contenteditable from './contenteditable'
import { v4 } from 'uuid'
function getOrdinalSuffix (num) {
if (num % 10 === 1 && num % 100 !== 11) return 'st'
if (num % 10 === 2 && num % 100 !== 12) return 'nd'
if (num % 10 === 3 && num % 100 !== 13) return 'rd'
return 'th'
}
export default {
name: 'FieldSubmitter',
components: {
@@ -275,16 +287,6 @@ export default {
computed: {
colors () {
return [
'bg-red-500',
'bg-sky-500',
'bg-emerald-500',
'bg-yellow-300',
'bg-purple-600',
'bg-pink-500',
'bg-cyan-500',
'bg-orange-500',
'bg-lime-500',
'bg-indigo-500',
'bg-red-500',
'bg-sky-500',
'bg-emerald-500',
@@ -298,6 +300,14 @@ export default {
]
},
names () {
const generatedNames = []
for (let i = 21; i < 101; i++) {
const suffix = getOrdinalSuffix(i)
generatedNames.push(`${i}${suffix} ${this.t('party')}`)
}
return [
this.t('first_party'),
this.t('second_party'),
@@ -318,7 +328,8 @@ export default {
this.t('seventeenth_party'),
this.t('eighteenth_party'),
this.t('nineteenth_party'),
this.t('twentieth_party')
this.t('twentieth_party'),
...generatedNames
]
},
lastPartyIndex () {
@@ -372,7 +383,7 @@ export default {
this.$emit('new-submitter', newSubmitter)
},
closeDropdown () {
document.activeElement.blur()
this.$el.getRootNode().activeElement.blur()
}
}
}
@@ -170,7 +170,7 @@ export default {
},
methods: {
closeDropdown () {
document.activeElement.blur()
this.$el.getRootNode().activeElement.blur()
}
}
}
+20 -4
View File
@@ -5,7 +5,7 @@
class="roles-dropdown w-full rounded-lg"
:style="withStickySubmitters ? { backgroundColor } : {}"
:submitters="submitters"
:menu-style="{ backgroundColor: ['', null, 'transparent'].includes(backgroundColor) ? 'white' : backgroundColor }"
:menu-style="{ overflow: 'auto', display: 'flex', flexDirection: 'row', maxHeight: 'calc(100vh - 120px)', backgroundColor: ['', null, 'transparent'].includes(backgroundColor) ? 'white' : backgroundColor }"
:editable="editable && !defaultSubmitters.length"
@new-submitter="save"
@remove="removeSubmitter"
@@ -26,7 +26,7 @@
:field="field"
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
:editable="editable && (!fieldsDragFieldRef.value || fieldsDragFieldRef.value !== field)"
:default-field="defaultFields.find((f) => f.name === field.name)"
:default-field="defaultFieldsIndex[field.name]"
:draggable="editable"
@dragstart="fieldsDragFieldRef.value = field"
@dragend="fieldsDragFieldRef.value = null"
@@ -229,6 +229,11 @@ export default {
type: Array,
required: true
},
withFieldsSearch: {
type: Boolean,
required: false,
default: null
},
template: {
type: Object,
required: true
@@ -297,7 +302,18 @@ export default {
fieldNames: FieldType.computed.fieldNames,
fieldIcons: FieldType.computed.fieldIcons,
isShowFieldSearch () {
return this.submitterDefaultFields.length > 15
if (this.withFieldsSearch === false) {
return false
} else {
return this.submitterDefaultFields.length > 15
}
},
defaultFieldsIndex () {
return this.defaultFields.reduce((acc, field) => {
acc[field.name] = field
return acc
}, {})
},
fieldIconsSorted () {
if (this.fieldTypes.length) {
@@ -320,7 +336,7 @@ export default {
},
filteredSubmitterDefaultFields () {
if (this.defaultFieldsSearch) {
return this.submitterDefaultFields.filter((f) => f.name.toLowerCase().includes(this.defaultFieldsSearch.toLowerCase()))
return this.submitterDefaultFields.filter((f) => (f.title || f.name).toLowerCase().includes(this.defaultFieldsSearch.toLowerCase()))
} else {
return this.submitterDefaultFields
}
+18
View File
@@ -1,4 +1,7 @@
const en = {
party: 'Party',
method: 'Method',
reorder_fields: 'Reorder fields',
verify_id: 'Verify ID',
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Obtain qualified electronic signature (QeS) with the trusted provider. Click to learn more.',
editable: 'Editable',
@@ -158,6 +161,9 @@ const en = {
}
const es = {
party: 'Parte',
method: 'Método',
reorder_fields: 'Reordenar campos',
verify_id: 'Verificar ID',
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Obtenga una firma electrónica cualificada (QeS) con el proveedor de confianza. Haga clic para obtener más información.',
recurrent: 'Recurrente',
@@ -317,6 +323,9 @@ const es = {
}
const it = {
party: 'Parte',
method: 'Metodo',
reorder_fields: 'Riordina i campi',
verify_id: 'Verifica ID',
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Ottieni una firma elettronica qualificata (QeS) con il fornitore di fiducia. Clicca per saperne di più.',
ricorrente: 'Ricorrente',
@@ -476,6 +485,9 @@ const it = {
}
const pt = {
party: 'Parte',
method: 'Método',
reorder_fields: 'Reorganizar campos',
verify_id: 'Verificar ID',
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Obtenha a assinatura eletrônica qualificada (QeS) com o provedor confiável. Clique para saber mais.',
recurrent: 'Recurrente',
@@ -635,6 +647,9 @@ const pt = {
}
const fr = {
party: 'Partie',
method: 'Méthode',
reorder_fields: 'Réorganiser les champs',
verify_id: "Vérifier l'ID",
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Obtenez une signature électronique qualifiée (QeS) avec le fournisseur de confiance. Cliquez pour en savoir plus.',
recurrent: 'Récurrent',
@@ -794,6 +809,9 @@ const fr = {
}
const de = {
party: 'Partei',
method: 'Verfahren',
reorder_fields: 'Felder neu anordnen',
verify_id: 'ID überprüfen',
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Erhalten Sie eine qualifizierte elektronische Signatur (QeS) beim vertrauenswürdigen Anbieter. Klicken Sie hier, um mehr zu erfahren.',
wiederkehrend: 'Wiederkehrend',
+13 -3
View File
@@ -1,6 +1,7 @@
<template>
<div
class="relative cursor-crosshair select-none"
class="relative select-none"
:class="{ 'cursor-crosshair': allowDraw }"
:style="drawField ? 'touch-action: none' : ''"
>
<img
@@ -25,7 +26,7 @@
:field="item.field"
:editable="editable"
:with-field-placeholder="withFieldPlaceholder"
:default-field="defaultFields.find((f) => f.name === item.field.name)"
:default-field="defaultFieldsIndex[item.field.name]"
:default-submitters="defaultSubmitters"
:max-page="totalPages - 1"
@start-resize="resizeDirection = $event"
@@ -142,6 +143,13 @@ export default {
}
},
computed: {
defaultFieldsIndex () {
return this.defaultFields.reduce((acc, field) => {
acc[field.name] = field
return acc
}, {})
},
defaultFieldType () {
if (this.drawFieldType) {
return this.drawFieldType
@@ -154,7 +162,9 @@ export default {
}
},
isMobile () {
return /android|iphone|ipad/i.test(navigator.userAgent)
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
return isMobileSafariIos || /android|iphone|ipad/i.test(navigator.userAgent)
},
resizeDirectionClasses () {
return {
+44 -27
View File
@@ -56,23 +56,39 @@
</button>
</div>
<div
v-if="withArrows"
class="flex flex-col space-y-1"
>
<button
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
style="width: 24px; height: 24px"
@click.stop="$emit('up', item)"
<span
:data-tip="t('reorder_fields')"
class="tooltip tooltip-left before:text-xs"
>
&uarr;
</button>
<button
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
style="width: 24px; height: 24px"
@click.stop="$emit('down', item)"
>
&darr;
</button>
<button
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors p-0"
@click.stop="$emit('reorder', item)"
>
<IconSortDescending2
:width="18"
:height="18"
:stroke-width="1.6"
/>
</button>
</span>
<template v-if="withArrows">
<button
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
style="width: 24px; height: 24px"
@click.stop="$emit('up', item)"
>
&uarr;
</button>
<button
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
style="width: 24px; height: 24px"
@click.stop="$emit('down', item)"
>
&darr;
</button>
</template>
</div>
</div>
</div>
@@ -88,23 +104,23 @@
@update:model-value="onUpdateName"
/>
</div>
<Teleport
v-if="isShowConditionsModal"
:to="modalContainerEl"
>
<ConditionsModal
:item="item"
:build-default-name="buildDefaultName"
@close="isShowConditionsModal = false"
/>
</Teleport>
</div>
<Teleport
v-if="isShowConditionsModal"
:to="modalContainerEl"
>
<ConditionsModal
:item="item"
:build-default-name="buildDefaultName"
@close="isShowConditionsModal = false"
/>
</Teleport>
</template>
<script>
import Contenteditable from './contenteditable'
import Upload from './upload'
import { IconRouteAltLeft } from '@tabler/icons-vue'
import { IconRouteAltLeft, IconSortDescending2 } from '@tabler/icons-vue'
import ConditionsModal from './conditions_modal'
import ReplaceButton from './replace'
import Field from './field'
@@ -116,7 +132,8 @@ export default {
Contenteditable,
IconRouteAltLeft,
ConditionsModal,
ReplaceButton
ReplaceButton,
IconSortDescending2
},
inject: ['t'],
props: {
@@ -153,7 +170,7 @@ export default {
default: true
}
},
emits: ['scroll-to', 'change', 'remove', 'up', 'down', 'replace'],
emits: ['scroll-to', 'change', 'remove', 'up', 'down', 'replace', 'reorder'],
data () {
return {
isShowConditionsModal: false
@@ -120,6 +120,8 @@ class ProcessSubmitterCompletionJob
submitter.submission.template_submitters.find do |e|
sub = submitter.submission.submitters.find { |s| s.uuid == e['uuid'] }
next unless sub
sub.completed_at.blank? && sub.sent_at.blank?
end
@@ -5,9 +5,7 @@ class SendFormCompletedWebhookRequestJob
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
MAX_ATTEMPTS = 20
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
@@ -21,19 +19,8 @@ class SendFormCompletedWebhookRequestJob
ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin
Faraday.post(webhook_url.url,
{
event_type: 'form.completed',
timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter)
}.to_json,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT,
**webhook_url.secret.to_h)
rescue Faraday::Error
nil
end
resp = SendWebhookRequest.call(webhook_url, event_type: 'form.completed',
data: Submitters::SerializeForWebhook.call(submitter))
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
@@ -5,8 +5,6 @@ class SendFormDeclinedWebhookRequestJob
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
@@ -19,19 +17,8 @@ class SendFormDeclinedWebhookRequestJob
ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin
Faraday.post(webhook_url.url,
{
event_type: 'form.declined',
timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter)
}.to_json,
**webhook_url.secret.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
nil
end
resp = SendWebhookRequest.call(webhook_url, event_type: 'form.declined',
data: Submitters::SerializeForWebhook.call(submitter))
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
@@ -5,8 +5,6 @@ class SendFormStartedWebhookRequestJob
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
@@ -19,19 +17,8 @@ class SendFormStartedWebhookRequestJob
ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin
Faraday.post(webhook_url.url,
{
event_type: 'form.started',
timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter)
}.to_json,
**webhook_url.secret.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
nil
end
resp = SendWebhookRequest.call(webhook_url, event_type: 'form.started',
data: Submitters::SerializeForWebhook.call(submitter))
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
@@ -5,8 +5,6 @@ class SendFormViewedWebhookRequestJob
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
@@ -19,19 +17,8 @@ class SendFormViewedWebhookRequestJob
ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin
Faraday.post(webhook_url.url,
{
event_type: 'form.viewed',
timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter)
}.to_json,
**webhook_url.secret.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
nil
end
resp = SendWebhookRequest.call(webhook_url, event_type: 'form.viewed',
data: Submitters::SerializeForWebhook.call(submitter))
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
@@ -5,8 +5,6 @@ class SendSubmissionArchivedWebhookRequestJob
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
@@ -17,19 +15,8 @@ class SendSubmissionArchivedWebhookRequestJob
return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.archived')
resp = begin
Faraday.post(webhook_url.url,
{
event_type: 'submission.archived',
timestamp: Time.current,
data: submission.as_json(only: %i[id archived_at])
}.to_json,
**webhook_url.secret.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
nil
end
resp = SendWebhookRequest.call(webhook_url, event_type: 'submission.archived',
data: submission.as_json(only: %i[id archived_at]))
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
@@ -5,8 +5,6 @@ class SendSubmissionCompletedWebhookRequestJob
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
@@ -17,19 +15,8 @@ class SendSubmissionCompletedWebhookRequestJob
return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.completed')
resp = begin
Faraday.post(webhook_url.url,
{
event_type: 'submission.completed',
timestamp: Time.current,
data: Submissions::SerializeForApi.call(submission)
}.to_json,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT,
**webhook_url.secret.to_h)
rescue Faraday::Error
nil
end
resp = SendWebhookRequest.call(webhook_url, event_type: 'submission.completed',
data: Submissions::SerializeForApi.call(submission))
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
@@ -5,8 +5,6 @@ class SendSubmissionCreatedWebhookRequestJob
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
@@ -17,19 +15,8 @@ class SendSubmissionCreatedWebhookRequestJob
return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.created')
resp = begin
Faraday.post(webhook_url.url,
{
event_type: 'submission.created',
timestamp: Time.current,
data: Submissions::SerializeForApi.call(submission)
}.to_json,
**webhook_url.secret.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
nil
end
resp = SendWebhookRequest.call(webhook_url, event_type: 'submission.created',
data: Submissions::SerializeForApi.call(submission))
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
@@ -8,6 +8,12 @@ class SendSubmitterInvitationEmailJob
return if submitter.submission.source == 'invite' && !Accounts.can_send_emails?(submitter.account, on_events: true)
unless Accounts.can_send_invitation_emails?(submitter.account)
Rollbar.warning("Skip email: #{submitter.account.id}") if defined?(Rollbar)
return
end
mail = SubmitterMailer.invitation_email(submitter)
Submitters::ValidateSending.call(submitter, mail)
@@ -17,6 +23,6 @@ class SendSubmitterInvitationEmailJob
SubmissionEvent.create!(submitter:, event_type: 'send_email')
submitter.sent_at ||= Time.current
submitter.save
submitter.save!
end
end
@@ -5,8 +5,6 @@ class SendTemplateCreatedWebhookRequestJob
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
@@ -17,19 +15,8 @@ class SendTemplateCreatedWebhookRequestJob
return if webhook_url.url.blank? || webhook_url.events.exclude?('template.created')
resp = begin
Faraday.post(webhook_url.url,
{
event_type: 'template.created',
timestamp: Time.current,
data: Templates::SerializeForApi.call(template)
}.to_json,
**webhook_url.secret.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
nil
end
resp = SendWebhookRequest.call(webhook_url, event_type: 'template.created',
data: Templates::SerializeForApi.call(template))
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
@@ -5,8 +5,6 @@ class SendTemplateUpdatedWebhookRequestJob
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
@@ -17,19 +15,8 @@ class SendTemplateUpdatedWebhookRequestJob
return if webhook_url.url.blank? || webhook_url.events.exclude?('template.updated')
resp = begin
Faraday.post(webhook_url.url,
{
event_type: 'template.updated',
timestamp: Time.current,
data: Templates::SerializeForApi.call(template)
}.to_json,
**webhook_url.secret.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
nil
end
resp = SendWebhookRequest.call(webhook_url, event_type: 'template.updated',
data: Templates::SerializeForApi.call(template))
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
+33 -12
View File
@@ -14,8 +14,20 @@ class SubmitterMailer < ApplicationMailer
@email_message = submitter.account.email_messages.find_by(uuid: submitter.preferences['email_message_uuid'])
end
@body = @email_message&.body.presence || @submitter.template.preferences['request_email_body'].presence
@subject = @email_message&.subject.presence || @submitter.template.preferences['request_email_subject'].presence
template_submitters_index =
if @email_message.blank?
build_submitter_preferences_index(@submitter)
else
{}
end
@body = @email_message&.body.presence ||
template_submitters_index.dig(@submitter.uuid, 'request_email_body').presence ||
@submitter.template.preferences['request_email_body'].presence
@subject = @email_message&.subject.presence ||
template_submitters_index.dig(@submitter.uuid, 'request_email_subject').presence ||
@submitter.template.preferences['request_email_subject'].presence
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY)
@@ -24,14 +36,7 @@ class SubmitterMailer < ApplicationMailer
reply_to = build_submitter_reply_to(@submitter)
I18n.with_locale(@current_account.locale) do
subject =
if @email_config || @subject
ReplaceEmailVariables.call(@subject || @email_config.value['subject'], submitter:)
elsif @submitter.with_signature_fields?
I18n.t(:you_are_invited_to_sign_a_document)
else
I18n.t(:you_are_invited_to_submit_a_form)
end
subject = build_invite_subject(@subject, @email_config, submitter)
mail(
to: @submitter.friendly_name,
@@ -121,7 +126,7 @@ class SubmitterMailer < ApplicationMailer
@body ||= @email_config.value['body'] if @email_config
assign_message_metadata('submitter_documents_copy', @submitter)
reply_to = build_submitter_reply_to(submitter)
reply_to = build_submitter_reply_to(submitter, email_config: @email_config, documents_copy_email: true)
I18n.with_locale(@current_account.locale) do
subject =
@@ -140,8 +145,10 @@ class SubmitterMailer < ApplicationMailer
private
def build_submitter_reply_to(submitter)
def build_submitter_reply_to(submitter, email_config: nil, documents_copy_email: nil)
reply_to = submitter.preferences['reply_to'].presence
reply_to ||= submitter.template.preferences['documents_copy_email_reply_to'].presence if documents_copy_email
reply_to ||= email_config.value['reply_to'].presence if email_config
if reply_to.blank? && (submitter.submission.created_by_user || submitter.template.author)&.email != submitter.email
reply_to = (submitter.submission.created_by_user || submitter.template.author)&.friendly_name&.sub(/\+\w+@/, '@')
@@ -194,6 +201,20 @@ class SubmitterMailer < ApplicationMailer
user.role == 'integration' ? user.friendly_name.sub(/\+\w+@/, '@') : user.friendly_name
end
def build_invite_subject(subject, email_config, submitter)
if email_config || subject
ReplaceEmailVariables.call(subject || email_config.value['subject'], submitter:)
elsif submitter.with_signature_fields?
I18n.t(:you_are_invited_to_sign_a_document)
else
I18n.t(:you_are_invited_to_submit_a_form)
end
end
def build_submitter_preferences_index(submitter)
submitter.template.preferences['submitters'].to_a.index_by { |e| e['uuid'] }
end
def add_attachments_with_size_limit(submitter, storage_attachments, current_size, filename_format = nil)
total_size = current_size
+11
View File
@@ -55,6 +55,7 @@ class Submitter < ApplicationRecord
has_many_attached :attachments
has_many_attached :preview_documents
has_many :template_accesses, through: :template
has_many :email_events, as: :emailable, dependent: (Docuseal.multitenant? ? nil : :destroy)
has_many :document_generation_events, dependent: :destroy
has_many :submission_events, dependent: :destroy
@@ -63,6 +64,8 @@ class Submitter < ApplicationRecord
scope :completed, -> { where.not(completed_at: nil) }
after_destroy :anonymize_email_events, if: -> { Docuseal.multitenant? }
def status
if declined_at?
'declined'
@@ -108,4 +111,12 @@ class Submitter < ApplicationRecord
fields.any? { |f| f['submitter_uuid'] == uuid && signature_field_types.include?(f['type']) }
end
end
private
def anonymize_email_events
email_events.each do |event|
event.update!(email: Digest::MD5.base64digest(event.email))
end
end
end
Whitespace-only changes.
+1
View File
@@ -153,6 +153,7 @@
<% end %>
</div>
<% end %>
<%= render 'compliances' %>
<%= render 'integrations' %>
<% if can?(:manage, current_account) && Docuseal.multitenant? && true_user == current_user %>
<div class="px-1 mt-8">
@@ -0,0 +1 @@
<%= hidden_field_tag :redir, params[:redir] if params[:redir].present? %>
@@ -0,0 +1,4 @@
<div class="form-control">
<%= label_tag 'user[otp_attempt]', t('two_factor_code_from_authenticator_app'), class: 'label' %>
<%= text_field_tag 'user[otp_attempt]', nil, autofocus: true, autocomplete: 'off', placeholder: 'XXX-XXX', required: true, class: 'base-input' %>
</div>
+1 -3
View File
@@ -3,9 +3,7 @@
<%= render 'devise/shared/select_server' if Docuseal.multitenant? %>
<h1 class="text-4xl font-bold text-center mt-8"><%= t('sign_in') %></h1>
<%= form_for(resource, as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: session_path(resource_name)) do |f| %>
<% if params[:redir].present? %>
<%= hidden_field_tag :redir, params[:redir] %>
<% end %>
<%= render 'hidden_fields' %>
<div class="space-y-2" dir="auto">
<div class="form-control">
<%= f.label :email, t(:email), class: 'label' %>
+1 -6
View File
@@ -8,12 +8,7 @@
<% if params[:redir].present? %>
<%= hidden_field_tag :redir, params[:redir] %>
<% end %>
<div class="space-y-2">
<div class="form-control">
<%= f.label :otp_attempt, t('two_factor_code_from_authenticator_app'), class: 'label' %>
<%= f.text_field :otp_attempt, autofocus: true, placeholder: 'XXX-XXX', required: true, class: 'base-input' %>
</div>
</div>
<%= render 'otp_form', **local_assigns %>
<div class="form-control">
<%= f.button button_title(title: t('sign_in'), disabled_with: t('signing_in')), class: 'base-button' %>
</div>
+1 -1
View File
@@ -173,7 +173,7 @@
<%= t('document_download_filename_format') %>
</span>
<div class="mt-3">
<%= f.select :value, [["#{I18n.t('document_name')}.pdf", '{document.name}'], ["#{I18n.t('document_name')} - name@domain.com.pdf", '{document.name} - {submission.submitters}'], ["#{I18n.t('document_name')} - name@domain.com - #{I18n.l(Time.current.beginning_of_year.in_time_zone(current_account.timezone), format: :short)}.pdf", '{document.name} - {submission.submitters} - {submission.completed_at}']], {}, class: 'base-select', onchange: 'this.form.requestSubmit()' %>
<%= f.select :value, [["#{I18n.t('document_name')}.pdf", '{document.name}'], ["#{I18n.t('document_name')} - #{I18n.t(:signed)}.pdf", '{document.name} - {submission.status}'], ["#{I18n.t('document_name')} - name@domain.com.pdf", '{document.name} - {submission.submitters}'], ["#{I18n.t('document_name')} - name@domain.com - #{I18n.l(Time.current.beginning_of_year.in_time_zone(current_account.timezone), format: :short)}.pdf", '{document.name} - {submission.submitters} - {submission.completed_at}']], {}, class: 'base-select', onchange: 'this.form.requestSubmit()' %>
</div>
</div>
<% end %>
+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
<path d="M7 16h-4l3.47 -4.66a2 2 0 1 0 -3.47 -1.54"></path>
<path d="M10 16v-8h4"></path> <path d="M10 12l3 0"></path>
<path d="M17 16v-6a2 2 0 0 1 4 0v6"></path>
<path d="M17 13l4 0"></path>
</svg>

After

Width:  |  Height:  |  Size: 426 B

+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="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
</svg>

After

Width:  |  Height:  |  Size: 397 B

+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
<path d="M8 13v-8.5a1.5 1.5 0 0 1 3 0v7.5"></path>
<path d="M11 11.5v-2a1.5 1.5 0 0 1 3 0v2.5"></path>
<path d="M14 10.5a1.5 1.5 0 0 1 3 0v1.5"></path>
<path d="M17 11.5a1.5 1.5 0 0 1 3 0v4.5a6 6 0 0 1 -6 6h-2h.208a6 6 0 0 1 -5.012 -2.7l-.196 -.3c-.312 -.479 -1.407 -2.388 -3.286 -5.728a1.5 1.5 0 0 1 .536 -2.022a1.867 1.867 0 0 1 2.28 .28l1.47 1.47"></path>
<path d="M5 3l-1 -1"></path>
<path d="M4 7h-1"></path>
<path d="M14 3l1 -1"></path>
<path d="M15 6h1"></path>
</svg>

After

Width:  |  Height:  |  Size: 711 B

+11
View File
@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
<path d="M4 12h.01"></path>
<path d="M4 6h.01"></path>
<path d="M4 18h.01"></path>
<path d="M8 18h2"></path>
<path d="M8 12h2"></path>
<path d="M8 6h2"></path>
<path d="M14 6h6"></path>
<path d="M14 12h6"></path>
<path d="M14 18h6"></path>
</svg>

After

Width:  |  Height:  |  Size: 484 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
<path d="M3 9l9 6l9 -6l-9 -6l-9 6"></path>
<path d="M21 9v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-10"></path>
<path d="M3 19l6 -6"></path>
<path d="M15 13l6 6"></path>
</svg>

After

Width:  |  Height:  |  Size: 401 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
<path d="M5 4h4l2 5l-2.5 1.5a11 11 0 0 0 5 5l1.5 -2.5l5 2v4a2 2 0 0 1 -2 2a16 16 0 0 1 -15 -15a2 2 0 0 1 2 -2"></path>
<path d="M15 6l2 2l4 -4"></path>
</svg>

After

Width:  |  Height:  |  Size: 382 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
<path d="M7 4v16l13 -8z"></path>
</svg>

After

Width:  |  Height:  |  Size: 261 B

+3
View File
@@ -2,6 +2,9 @@
<html data-theme="docuseal" lang="<%= I18n.locale %>">
<head>
<%= render 'layouts/head_tags' %>
<% if Docuseal.enable_pwa? %>
<link rel="manifest" href="/manifest.json">
<% end %>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
@@ -8,7 +8,7 @@
<div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<%= f.hidden_field :key %>
<%= f.fields_for :value, Struct.new(:subject, :body, :attach_audit_log, :attach_documents).new(*f.object.value.values_at('subject', 'body', 'attach_audit_log', 'attach_documents')) do |ff| %>
<%= f.fields_for :value, Struct.new(:subject, :body, :reply_to, :attach_audit_log, :attach_documents).new(*f.object.value.values_at('subject', 'body', 'reply_to', 'attach_audit_log', 'attach_documents')) do |ff| %>
<div class="form-control">
<%= ff.label :subject, t('subject'), class: 'label' %>
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
@@ -24,6 +24,12 @@
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<% if can?(:manage, :reply_to) %>
<div class="form-control">
<%= ff.label :reply_to, t('reply_to'), class: 'label' %>
<%= ff.email_field :reply_to, class: 'base-input', dir: 'auto', placeholder: t(:email) %>
</div>
<% end %>
<div class="flex items-center justify-between pt-2.5 mx-1">
<span>
<%= t('attach_documents') %>
+25
View File
@@ -0,0 +1,25 @@
{
"name": "<%= Docuseal.product_name %>",
"short_name": "<%= Docuseal.product_name %>",
"id": "/",
"icons": [
{
"src": "/logo.svg",
"type": "image/svg+xml",
"sizes": "any"
},
{
"src": "/apple-touch-icon.png",
"type": "image/png",
"sizes": "192x192"
}
],
"start_url": "/",
"display": "standalone",
"scope": "/",
"orientation": "natural",
"description": "<%= Docuseal.product_name %> is an open source platform that provides secure and efficient digital document signing and processing.",
"categories": ["productivity", "utilities"],
"theme_color": "#FAF7F4",
"background_color": "#FAF7F4"
}
@@ -2,7 +2,7 @@
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<div class="flex items-center justify-center">
<%= render 'start_form/docuseal_logo' %>
<%= render 'start_form/banner' %>
</div>
<div class="text-center text-4xl font-bold">
<%= t('email_has_been_sent') %>
+1 -1
View File
@@ -1 +1 @@
<% 'stats stat stat-figure stat-title stat-value text-accent' %>
<% 'stats stat stat-figure stat-title stat-value text-accent w-fit hover:bg-white' %>
+5 -1
View File
@@ -2,5 +2,9 @@
---
</p>
<p>
<%= t('sent_using_product_name_free_document_signing_html', product_url: "#{Docuseal::PRODUCT_EMAIL_URL}/start", product_name: Docuseal.product_name) %>
<% if @current_account&.testing? %>
<%= t('sent_using_product_name_in_testing_mode_html', product_url: "#{Docuseal::PRODUCT_EMAIL_URL}/start", product_name: Docuseal.product_name) %>
<% else %>
<%= t('sent_using_product_name_free_document_signing_html', product_url: "#{Docuseal::PRODUCT_EMAIL_URL}/start", product_name: Docuseal.product_name) %>
<% end %>
</p>
+11
View File
@@ -1,3 +1,4 @@
<%= render 'shared/navbar_warning' %>
<div class="max-w-6xl mb-4 mx-auto px-4 md:px-2 py-3 flex items-center justify-between">
<div class="flex items-center space-x-4">
<a href="<%= root_path %>" class="text-2xl font-bold items-center flex space-x-2">
@@ -49,6 +50,16 @@
<% end %>
</li>
<% end %>
<% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %>
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'w-full py-1' } do |f| %>
<label class="flex items-center pl-6 pr-4 py-2 border-y border-base-300 -ml-2 -mr-2" for="testing_toggle">
<%= f.check_box :testing_toggle, class: 'toggle', checked: current_account.testing?, onchange: 'this.form.requestSubmit()', style: 'height: 0.885rem; width: 1.35rem; --handleoffset: 0.395rem; margin-left: -2px; margin-right: 8px' %>
<span class="whitespace-nowrap">
<%= t('test_mode') %>
</span>
</label>
<% end %>
<% end %>
<li>
<%= button_to destroy_user_session_path, method: :delete, data: { turbo: false }, class: 'flex items-center' do %>
<%= svg_icon('logout', class: 'w-5 h-5 flex-shrink-0 stroke-2 mr-2 inline') %>
Whitespace-only changes.
Whitespace-only changes.
+5 -5
View File
@@ -90,16 +90,16 @@
<% end %>
<%= render 'shared/settings_nav_extra2' %>
<% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %>
<li>
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'flex w-full' } do |f| %>
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'w-full' } do |f| %>
<li>
<label class="flex items-center text-base hover:bg-base-300 w-full justify-between" for="testing_toggle">
<span class="mr-2 w-full">
<%= t('test_environment') %>
<%= t('test_mode') %>
</span>
<%= f.check_box :testing_toggle, class: 'toggle toggle-sm', checked: current_account.testing?, onchange: 'this.form.requestSubmit()' %>
</label>
<% end %>
</li>
</li>
<% end %>
<% end %>
<% end %>
</ul>
+1 -1
View File
@@ -2,7 +2,7 @@
<div class="hidden md:flex alert py-1 text-sm font-medium gap-x-2 whitespace-nowrap">
<a href="<%= testing_api_settings_path %>" data-turbo-frame="modal" class="link font-semibold flex">
<%= svg_icon('code_circle', class: 'w-5 h-5 mr-1') %>
<span><%= t('testing_environment') %></span>
<span><%= t('test_mode') %></span>
</a>
<span>
|
+1 -1
View File
@@ -2,7 +2,7 @@
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'flex' } do |f| %>
<label class="flex items-center justify-between" for="testing_toggle">
<span class="mr-2 text-lg">
<%= t('test_environment') %>
<%= t('test_mode') %>
</span>
<%= f.check_box :testing_toggle, class: 'toggle', checked: current_account.testing?, onchange: 'this.form.requestSubmit()' %>
</label>
@@ -0,0 +1,49 @@
<% event_colors = %w[bg-red-200 bg-sky-200 bg-emerald-200 bg-yellow-200 bg-purple-200 bg-pink-200 bg-cyan-200 bg-orange-200 bg-lime-200 bg-indigo-200] %>
<% submitters_uuids = (@submission.template_submitters || @submission.template.submitters).pluck('uuid') %>
<%= render 'shared/turbo_modal_large', title: t('event_log') do %>
<div class="pl-8 pr-4 py-4">
<ol class="relative border-s border-base-300 space-y-6">
<li class="ml-7">
<span class="absolute flex items-center justify-center w-7 h-7 rounded-full -start-3.5 ring-8 ring-base-100 text-base-content bg-gray-200">
<%= svg_icon('file_text', class: 'w-4 h-4') %>
</span>
<p class="text-sm leading-none text-base-content/60 pt-1.5">
<%= l(@submission.created_at.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) %>
</p>
<p class="text-base-content/80 mt-1">
<% if @submission.source == 'invite' %>
<%= t('submission_created_by_email_html', email: @submission.created_by_user.email) %>
<% elsif @submission.created_by_user %>
<%= t('submission_created_by_email_via_source_html', email: @submission.created_by_user.email, source: t("submission_sources.#{@submission.source}")) %>
<% else %>
<%= t('submission_created_via_source_html', source: t("submission_sources.#{@submission.source}")) %>
<% end %>
</p>
</li>
<% @submission.submission_events.order(:event_timestamp).each do |event| %>
<% submitter = @submission.submitters.find { |e| e.id == event.submitter_id } %>
<% bg_class = event_colors[submitters_uuids.index(submitter.uuid) % event_colors.length] %>
<% submitter_name = event.event_type.include?('sms') || event.event_type.include?('phone') ? (event.data['phone'] || submitter.phone) : (submitter.name || submitter.email || submitter.phone) %>
<li class="ml-7">
<span class="absolute flex items-center justify-center w-7 h-7 rounded-full -start-3.5 ring-8 ring-base-100 text-base-content <%= bg_class %>">
<%= svg_icon(SubmissionEventsController::SUBMISSION_EVENT_ICONS.fetch(event.event_type, 'circle_dot'), class: 'w-4 h-4') %>
</span>
<p class="text-sm leading-none text-base-content/60 pt-1.5">
<%= l(event.event_timestamp.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) %>
</p>
<p class="text-base-content/80 mt-1">
<% if event.event_type == 'complete_verification' %>
<%= t('submission_event_names.complete_verification_by_html', provider: event.data['method'], submitter_name:) %>
<% elsif event.event_type == 'invite_party' && (invited_submitter = @submission.submitters.find { |e| e.uuid == event.data['uuid'] }) && (name = @submission.template_submitters.find { |e| e['uuid'] == event.data['uuid'] }&.dig('name')) %>
<%= t('submission_event_names.invite_party_by_html', invited_submitter_name: [invited_submitter.name || invited_submitter.email || invited_submitter.phone, name].join(' '), submitter_name:) %>
<% elsif event.event_type.include?('send_') %>
<%= t("submission_event_names.#{event.event_type}_to_html", submitter_name:) %>
<% else %>
<%= t("submission_event_names.#{event.event_type}_by_html", submitter_name:) %>
<% end %>
</p>
</li>
<% end %>
</ol>
</div>
<% end %>
+52 -4
View File
@@ -1,3 +1,4 @@
<% submitter_preferences_index = template.preferences['submitters'].to_a.index_by { |e| e['uuid'] } %>
<div class="form-control">
<% can_send_emails = Accounts.can_send_emails?(current_account) %>
<div class="flex justify-between items-center">
@@ -36,10 +37,18 @@
<% config = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) %>
<div id="message_field" class="card card-compact bg-base-300/40 hidden">
<div class="card-body">
<div class="form-control space-y-2">
<%= tag.input id: toggle_uuid = SecureRandom.uuid, value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: local_assigns[:message_per_submitter] != false && template.preferences['submitters'].to_a.size > 1 %>
<div class="peer-checked:hidden form-control space-y-2">
<div class="form-control">
<%= f.label :subject, t('subject'), class: 'label' %>
<%= f.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || template.preferences['request_email_subject'].presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
<div class="flex justify-between">
<%= f.label :subject, t('subject'), class: 'label' %>
<% if template.submitters.size > 1 && template.submitters.size < 5 && local_assigns[:message_per_submitter] != false %>
<label for="<%= toggle_uuid %>" class="label underline">
<%= t('edit_per_party') %>
</label>
<% end %>
</div>
<%= f.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_subject').presence || template.preferences['request_email_subject'].presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
@@ -49,7 +58,7 @@
</span>
</div>
<autoresize-textarea>
<%= f.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || template.preferences['request_email_body'].presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
<%= f.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_body').presence || template.preferences['request_email_body'].presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
</autoresize-textarea>
<% unless local_assigns.fetch(:disable_save_as_default_template_option, false) %>
<label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer">
@@ -60,5 +69,44 @@
</div>
<%= render 'submissions/message_fields' %>
</div>
<% if template.submitters.size > 1 && template.submitters.size < 5 && local_assigns[:message_per_submitter] != false %>
<div class="hidden peer-checked:block form-control space-y-2">
<% uuid = SecureRandom.uuid %>
<% options = template.submitters.map { |e| [e['name'], "request_email_#{uuid}_#{e['uuid']}"] } %>
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="flex relative px-1">
<ul class="tabs w-full flex flex-nowrap">
<% options.each_with_index do |(label, val), index| %>
<div class="w-full">
<%= f.radio_button :selected, val, checked: index.zero?, id: "#{val}_radio", data: { action: 'click:toggle-visible#trigger' }, class: 'hidden peer' %>
<%= f.label :selected, label, value: val, for: "#{val}_radio", class: 'tab w-full tab-lifted peer-checked:tab-active !bg-transparent' %>
</div>
<% end %>
</ul>
</toggle-visible>
<% template.submitters.each_with_index do |submitter, index| %>
<%= fields_for :submitter_preferences, nil, index: submitter['uuid'] do |ff| %>
<div id="request_email_<%= uuid %>_<%= submitter['uuid'] %>" class="<%= 'hidden' if index != 0 %>">
<div class="form-control">
<div class="flex justify-between">
<%= ff.label :subject, t('subject'), class: 'label' %>
</div>
<%= ff.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_subject').presence || template.preferences['request_email_subject'].presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :message, t('body'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_body').presence || template.preferences['request_email_body'].presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
</autoresize-textarea>
</div>
</div>
<% end %>
<% end %>
</div>
<% end %>
</div>
</div>
+2 -1
View File
@@ -1,6 +1,7 @@
<% align = field.dig('preferences', 'align') %>
<% color = field.dig('preferences', 'color') %>
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-serif' if field.dig('preferences', 'font') == 'Times' %> <%= align == 'right' ? 'justify-end' : (align == 'center' ? 'justify-center' : '') %>" style="<%= "color: #{color}; " if color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size'].to_i * 1.23}pt); line-height: `clamp(6pt, 2.0vw, #{(field['preferences']['font_size'].to_i * 1.23) + 3}pt)`" if field.dig('preferences', 'font_size') %>">
<% font = field.dig('preferences', 'font') %>
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-mono' if font == 'Courier' %> <%= 'font-serif' if font == 'Times' %> <%= align == 'right' ? 'justify-end' : (align == 'center' ? 'justify-center' : '') %>" style="<%= "color: #{color}; " if color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size'].to_i * 1.23}pt); line-height: `clamp(6pt, 2.0vw, #{(field['preferences']['font_size'].to_i * 1.23) + 3}pt)`" if field.dig('preferences', 'font_size') %>">
<% if field['type'] == 'signature' %>
<div class="flex flex-col justify-between h-full overflow-hidden">
<div class="flex-grow flex overflow-hidden" style="min-height: 50%">
Loaded 100 of 151 files, more files were not shown because too many files have changed in this diff. Show more