Compare commits

...
122 Commits
Author SHA1 Message Date
Pete Matsyburka 5f32e2a5af adjust create event 2024-04-21 17:01:44 +03:00
Pete Matsyburka 70012fbd7c fix form 2024-04-21 16:40:28 +03:00
Pete Matsyburka 668f6ca975 fix cells draw 2024-04-21 00:32:40 +03:00
Pete Matsyburka e78dd301ef fix download ttl 2024-04-20 23:58:43 +03:00
Pete Matsyburka f8ec3daf10 add minimize prop 2024-04-20 18:53:16 +03:00
Pete Matsyburka 75c929639d fix condition last field error 2024-04-20 17:23:28 +03:00
Pete Matsyburka e9c1242b8d verify field only once 2024-04-20 17:07:31 +03:00
Pete Matsyburka e7eb1d4f12 add noindex 2024-04-20 15:47:48 +03:00
Pete Matsyburka abbd3e809a remove sign now from archived 2024-04-20 12:54:14 +03:00
Pete Matsyburka f430b4f5a6 change default email 2024-04-20 11:25:04 +03:00
Pete Matsyburka e65234b4c8 add archived users view 2024-04-19 16:19:02 +03:00
Pete Matsyburka fe04d22c94 fix email normalize 2024-04-19 12:30:10 +03:00
Pete Matsyburka 44a7a5ebdc fix go to last 2024-04-19 10:33:31 +03:00
Pete Matsyburka 119ef0d0c7 add force sso key 2024-04-19 02:04:35 +03:00
Pete Matsyburka cdb8eb7931 fix pdf number of pages 2024-04-18 22:25:07 +03:00
Pete Matsyburka aef176c0cc downgrade net smtp 2024-04-18 18:43:47 +03:00
Pete Matsyburka 0c21b55865 adjust fixed email log 2024-04-18 17:02:53 +03:00
Pete Matsyburka 964b1444a5 adjust completed action 2024-04-18 16:45:09 +03:00
Pete Matsyburka 48de343948 add fix email log 2024-04-18 10:44:45 +03:00
Pete Matsyburka 2640934c15 skip .om tld typo fix 2024-04-18 10:42:54 +03:00
Pete Matsyburka 442dd4d6db add submitters template_id filter 2024-04-18 01:48:14 +03:00
Pete Matsyburka 3168feb3d6 clone template preferences 2024-04-17 21:09:35 +03:00
Pete Matsyburka fbae4933f6 fix multiple bcc addresses 2024-04-17 20:51:34 +03:00
Pete Matsyburka 78bb66c344 adjust wording 2024-04-17 20:24:31 +03:00
Pete Matsyburka 5af65ef74f adjust drawer 2024-04-17 17:27:43 +03:00
Pete Matsyburka 4d354ae9ff do not load storage configs in dev 2024-04-17 16:51:42 +03:00
Pete Matsyburka 1e57c1ace7 add template preferences 2024-04-17 16:09:09 +03:00
Pete Matsyburka fefd138dfd add devise log 2024-04-17 00:12:05 +03:00
Pete Matsyburka 16a77d3536 add log 2024-04-16 14:07:38 +03:00
Pete Matsyburka e03857bdb7 mark required field on mobile 2024-04-16 10:42:11 +03:00
Pete Matsyburka fc679bcd72 add require file links auth toggle 2024-04-15 20:34:00 +03:00
Pete Matsyburka 652b0da021 add fields param 2024-04-15 19:36:16 +03:00
Pete Matsyburka 2d54b0db2f update deps 2024-04-15 18:33:55 +03:00
Pete Matsyburka 7a29894926 add required fields prop 2024-04-15 16:34:44 +03:00
Pete Matsyburka f9ae79dd5b add go to last 2024-04-13 17:41:49 +03:00
Pete Matsyburka cadb78ca35 check on email events 2024-04-13 12:37:48 +03:00
Pete Matsyburka 36a6b7ff1d fix typo 2024-04-13 11:35:04 +03:00
Pete Matsyburka efee46f113 add bulk source 2024-04-12 17:20:00 +03:00
Pete Matsyburka b6639738c3 adjust dockerfile 2024-04-11 22:52:12 +03:00
Alex Turchyn aab07c5f49 add icons 2024-04-11 22:52:11 +03:00
Pete Matsyburka 33c0784bc6 add folder name to submission resp 2024-04-11 16:36:53 +03:00
Pete Matsyburka 6f5ebfc8a6 fix preview completed 2024-04-11 02:45:38 +03:00
Pete Matsyburka 811091ff14 adjust text signature size 2024-04-10 22:24:19 +03:00
Pete Matsyburka 6666f39928 add redacted param 2024-04-10 19:56:35 +03:00
Pete Matsyburka f67db9fe5f add field uuid param 2024-04-10 13:30:27 +03:00
Pete Matsyburka 45f002803e adjust padding 2024-04-10 10:55:51 +03:00
Pete Matsyburka 8c19f90e88 remove debug 2024-04-10 00:44:36 +03:00
Pete Matsyburka a8f5d5cea7 fix typo 2024-04-09 19:08:05 +03:00
Pete Matsyburka 6788071eec add webhook event preferences 2024-04-09 18:11:58 +03:00
Pete Matsyburka 0c690b3033 custom submit button 2024-04-09 01:54:39 +03:00
Pete Matsyburka dcb963b623 add autoscrollFields prop 2024-04-08 18:37:10 +03:00
Pete Matsyburka 04e30dbdd6 add submitter update params 2024-04-08 10:51:18 +03:00
Pete Matsyburka ac6d5a7aed update readme 2024-04-08 01:18:31 +03:00
Pete Matsyburka 3166a31b67 add icon 2024-04-08 01:02:43 +03:00
Pete Matsyburka fc3283eb3a add Korean i18n 2024-04-08 00:33:54 +03:00
Pete Matsyburka d6a96d4aa3 fix base64 unicode 2024-04-07 18:06:26 +03:00
Pete Matsyburka cda349d71c adjust send email views 2024-04-07 17:58:18 +03:00
Pete Matsyburka 87345a3b3d fix rubocop 2024-04-07 11:14:34 +03:00
Pete Matsyburka b791d0699c fix replace button file types 2024-04-06 21:57:35 +03:00
Pete Matsyburka 2cb15f3282 optimize document upload 2024-04-06 21:57:34 +03:00
Pete Matsyburka 8b823ca0d0 do not fix gob tld 2024-04-06 19:41:17 +03:00
Pete Matsyburka f3f08797f7 optimize document upload 2024-04-06 17:08:45 +03:00
Pete Matsyburka cab9672d25 remove direct upload 2024-04-06 15:00:30 +03:00
Pete Matsyburka c8d3b1bace reorder settings 2024-04-06 12:56:05 +03:00
Pete Matsyburka 5ee51b2b57 fix testing account 2024-04-06 01:20:02 +03:00
Pete Matsyburka 525dc503a6 accept default value array 2024-04-06 01:02:24 +03:00
Pete Matsyburka 12cf28792b add uuid to accounts 2024-04-05 19:56:34 +03:00
Pete Matsyburka 33466c1d07 adjust sso 2024-04-05 19:28:57 +03:00
Pete Matsyburka 226ae2172e word break underscore 2024-04-05 19:14:02 +03:00
Pete Matsyburka 8e2700b8b8 break long titles 2024-04-05 16:44:17 +03:00
Pete Matsyburka 753cbd4d27 allow to add custom form completed message 2024-04-05 16:16:30 +03:00
Pete Matsyburka f16405ca70 assign first party i18n name 2024-04-05 02:21:14 +03:00
Pete Matsyburka 23f052c400 add Korean 2024-04-05 02:02:23 +03:00
Pete Matsyburka 90a0a03e15 validate custom certs in the cloud 2024-04-04 14:08:14 +03:00
Pete Matsyburka 2dba8ae366 add submissions dashboard view 2024-04-04 13:45:11 +03:00
Pete Matsyburka 5bb7ad571c display default date only of the signing party 2024-04-03 10:27:19 +03:00
Pete Matsyburka 0cdb7c2c4f fix date normalization 2024-04-02 11:30:52 +03:00
Pete Matsyburka 11bdc522cd add spreadsheet download 2024-04-01 17:56:08 +03:00
Pete Matsyburka 014835f90d adjust download button state 2024-04-01 15:15:55 +03:00
Pete Matsyburka 05e66b60ee fix result generation with annots 2024-04-01 13:16:54 +03:00
Pete Matsyburka 505b185cf2 fix download indicator 2024-03-31 18:31:09 +03:00
Pete Matsyburka 50c32ac6ff replace date value 2024-03-31 18:20:33 +03:00
Pete Matsyburka 878ab25fd4 access account via submission 2024-03-31 17:40:51 +03:00
Pete Matsyburka 1b31772ff6 show default date on the page 2024-03-31 17:34:23 +03:00
Pete Matsyburka d4940234b7 add bulk placeholder 2024-03-30 23:36:40 +02:00
Pete Matsyburka 9bdc5d4863 adjust batch limit 2024-03-30 14:42:42 +02:00
Pete Matsyburka a73fd7929c add smtp timeout 2024-03-30 12:43:15 +02:00
Pete Matsyburka 8269a61751 adjust payment connected 2024-03-29 23:24:51 +02:00
Pete Matsyburka de072d1437 scroll to multiple checkbox field 2024-03-29 21:43:13 +02:00
Pete Matsyburka 5b24793f2b render links with markdown in emails 2024-03-29 21:21:04 +02:00
Pete Matsyburka 660f7ab8f3 password reset neutral on-prem 2024-03-29 14:50:18 +02:00
Pete Matsyburka 6e3fea5fa0 validate email param 2024-03-28 11:25:32 +02:00
Pete Matsyburka 29ecbecbc2 flatten annots 2024-03-27 18:24:41 +02:00
Pete Matsyburka ad3bbe9892 sctoll to radio group 2024-03-27 18:24:35 +02:00
Pete Matsyburka b570ebee69 remember builder selected field type 2024-03-27 14:21:30 +02:00
Pete Matsyburka 207780fa3f clear fields on remove doc 2024-03-27 00:11:10 +02:00
Pete Matsyburka 8aa219433b render blank option checkbox 2024-03-26 19:28:41 +02:00
Pete Matsyburka cb1e9f2be4 do not send completed emails to integration 2024-03-26 18:01:35 +02:00
Pete Matsyburka 202943f58d fix pidfile issue 2024-03-26 01:17:55 +02:00
Pete Matsyburka 6dd15b2091 use letter page size for us 2024-03-25 20:20:38 +02:00
Pete Matsyburka 1c55e4f18b always display default value 2024-03-25 19:16:12 +02:00
Pete Matsyburka 6cc36b1728 allow to update template roles 2024-03-25 17:37:46 +02:00
Pete Matsyburka 37751cd5c1 update template submitters 2024-03-25 16:34:45 +02:00
Pete Matsyburka 80d882cd20 add reply to param 2024-03-24 14:09:28 +02:00
Pete Matsyburka 8bef75e570 move autocomplete controllers 2024-03-23 23:52:50 +02:00
Pete Matsyburka 077eab7005 refactor templates controller 2024-03-23 23:52:49 +02:00
Pete Matsyburka 715c70d422 rename to users 2024-03-23 21:23:59 +02:00
Pete Matsyburka a3c23f9d53 update readme 2024-03-22 17:35:52 +02:00
Pete Matsyburka c1e67a8af0 add submission id var 2024-03-22 13:11:33 +02:00
Pete Matsyburka 2fdc294293 fix annot 2024-03-21 16:25:49 +02:00
Pete Matsyburka 1f49ba069e disable puma pidfile 2024-03-21 15:03:50 +02:00
Pete Matsyburka 43ef0fc0d0 use FORCE_SSL=false 2024-03-21 14:12:11 +02:00
Pete Matsyburka a1a920e536 allow to remove default value 2024-03-21 00:04:53 +02:00
Pete Matsyburka dec33b4331 add extra small field font size 2024-03-20 16:52:53 +02:00
Pete Matsyburka 432f290633 assign template fields on completed via API 2024-03-20 00:13:16 +02:00
Pete Matsyburka cf98616a27 remove log 2024-03-19 13:57:21 +02:00
Pete Matsyburka 1a720cf1ad allow to clone template from testing 2024-03-19 01:44:16 +02:00
Pete Matsyburka 557c6c34f2 search default fields 2024-03-19 01:21:44 +02:00
Pete Matsyburka 943c76505d remove api error log 2024-03-18 23:18:35 +02:00
Pete Matsyburka 66eb83e6a7 allow to search on value 2024-03-18 20:16:26 +02:00
Pete Matsyburka 0844427357 add rollbar queue 2024-03-18 19:35:19 +02:00
Pete Matsyburka 72e9d9ea2d update readme 2024-03-17 23:16:27 +02:00
185 changed files with 2917 additions and 1346 deletions

No files matched your search

+2 -1
View File
@@ -8,7 +8,8 @@
}, },
"rules": { "rules": {
"vue/no-deprecated-html-element-is": 0, "vue/no-deprecated-html-element-is": 0,
"vue/no-mutating-props": 0 "vue/no-mutating-props": 0,
"vue/one-component-per-file": 0
}, },
"parserOptions": { "parserOptions": {
"ecmaVersion": 2022, "ecmaVersion": 2022,
+1 -1
View File
@@ -44,7 +44,7 @@ Layout/LineLength:
AllowedPatterns: ['\A\s*#'] AllowedPatterns: ['\A\s*#']
Metrics/AbcSize: Metrics/AbcSize:
Max: 40 Max: 45
Metrics/ModuleLength: Metrics/ModuleLength:
Max: 500 Max: 500
+5 -6
View File
@@ -23,10 +23,10 @@ RUN yarn install --network-timeout 1000000
COPY ./bin/shakapacker ./bin/shakapacker COPY ./bin/shakapacker ./bin/shakapacker
COPY ./config/webpack ./config/webpack COPY ./config/webpack ./config/webpack
COPY ./config/shakapacker.yml ./config/shakapacker.yml COPY ./config/shakapacker.yml ./config/shakapacker.yml
COPY ./postcss.config.js ./postcss.config.js ./ COPY ./postcss.config.js ./postcss.config.js
COPY ./tailwind.config.js ./tailwind.config.js ./ COPY ./tailwind.config.js ./tailwind.config.js
COPY ./tailwind.form.config.js ./tailwind.form.config.js ./ COPY ./tailwind.form.config.js ./tailwind.form.config.js
COPY ./tailwind.application.config.js ./tailwind.application.config.js ./ COPY ./tailwind.application.config.js ./tailwind.application.config.js
COPY ./app/javascript ./app/javascript COPY ./app/javascript ./app/javascript
COPY ./app/views ./app/views COPY ./app/views ./app/views
@@ -36,7 +36,6 @@ FROM ruby:3.2.2-alpine3.18 as app
ENV RAILS_ENV=production ENV RAILS_ENV=production
ENV BUNDLE_WITHOUT="development:test" ENV BUNDLE_WITHOUT="development:test"
ENV PIDFILE=/dev/null
WORKDIR /app WORKDIR /app
@@ -67,4 +66,4 @@ WORKDIR /data/docuseal
ENV WORKDIR=/data/docuseal ENV WORKDIR=/data/docuseal
EXPOSE 3000 EXPOSE 3000
CMD ["/app/bin/rails", "server"] CMD ["/app/bin/bundle", "exec", "puma", "-C", "/app/config/puma.rb", "--dir", "/app"]
+175 -164
View File
@@ -2,35 +2,35 @@ GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
Ascii85 (1.1.0) Ascii85 (1.1.0)
actioncable (7.1.2) actioncable (7.1.3.2)
actionpack (= 7.1.2) actionpack (= 7.1.3.2)
activesupport (= 7.1.2) activesupport (= 7.1.3.2)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
actionmailbox (7.1.2) actionmailbox (7.1.3.2)
actionpack (= 7.1.2) actionpack (= 7.1.3.2)
activejob (= 7.1.2) activejob (= 7.1.3.2)
activerecord (= 7.1.2) activerecord (= 7.1.3.2)
activestorage (= 7.1.2) activestorage (= 7.1.3.2)
activesupport (= 7.1.2) activesupport (= 7.1.3.2)
mail (>= 2.7.1) mail (>= 2.7.1)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
actionmailer (7.1.2) actionmailer (7.1.3.2)
actionpack (= 7.1.2) actionpack (= 7.1.3.2)
actionview (= 7.1.2) actionview (= 7.1.3.2)
activejob (= 7.1.2) activejob (= 7.1.3.2)
activesupport (= 7.1.2) activesupport (= 7.1.3.2)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
actionpack (7.1.2) actionpack (7.1.3.2)
actionview (= 7.1.2) actionview (= 7.1.3.2)
activesupport (= 7.1.2) activesupport (= 7.1.3.2)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
racc racc
rack (>= 2.2.4) rack (>= 2.2.4)
@@ -38,35 +38,35 @@ GEM
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
actiontext (7.1.2) actiontext (7.1.3.2)
actionpack (= 7.1.2) actionpack (= 7.1.3.2)
activerecord (= 7.1.2) activerecord (= 7.1.3.2)
activestorage (= 7.1.2) activestorage (= 7.1.3.2)
activesupport (= 7.1.2) activesupport (= 7.1.3.2)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (7.1.2) actionview (7.1.3.2)
activesupport (= 7.1.2) activesupport (= 7.1.3.2)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.11) erubi (~> 1.11)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
activejob (7.1.2) activejob (7.1.3.2)
activesupport (= 7.1.2) activesupport (= 7.1.3.2)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (7.1.2) activemodel (7.1.3.2)
activesupport (= 7.1.2) activesupport (= 7.1.3.2)
activerecord (7.1.2) activerecord (7.1.3.2)
activemodel (= 7.1.2) activemodel (= 7.1.3.2)
activesupport (= 7.1.2) activesupport (= 7.1.3.2)
timeout (>= 0.4.0) timeout (>= 0.4.0)
activestorage (7.1.2) activestorage (7.1.3.2)
actionpack (= 7.1.2) actionpack (= 7.1.3.2)
activejob (= 7.1.2) activejob (= 7.1.3.2)
activerecord (= 7.1.2) activerecord (= 7.1.3.2)
activesupport (= 7.1.2) activesupport (= 7.1.3.2)
marcel (~> 1.0) marcel (~> 1.0)
activesupport (7.1.2) activesupport (7.1.3.2)
base64 base64
bigdecimal bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
@@ -85,17 +85,17 @@ GEM
arabic-letter-connector (0.1.1) arabic-letter-connector (0.1.1)
ast (2.4.2) ast (2.4.2)
aws-eventstream (1.3.0) aws-eventstream (1.3.0)
aws-partitions (1.874.0) aws-partitions (1.913.0)
aws-sdk-core (3.190.1) aws-sdk-core (3.191.6)
aws-eventstream (~> 1, >= 1.3.0) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0) aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8) aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.75.0) aws-sdk-kms (1.79.0)
aws-sdk-core (~> 3, >= 3.188.0) aws-sdk-core (~> 3, >= 3.191.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.142.0) aws-sdk-s3 (1.146.1)
aws-sdk-core (~> 3, >= 3.189.0) aws-sdk-core (~> 3, >= 3.191.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8) aws-sigv4 (~> 1.8)
aws-sigv4 (1.8.0) aws-sigv4 (1.8.0)
@@ -110,52 +110,54 @@ GEM
nokogiri (~> 1, >= 1.10.8) nokogiri (~> 1, >= 1.10.8)
base64 (0.2.0) base64 (0.2.0)
bcrypt (3.1.20) bcrypt (3.1.20)
better_html (2.0.2) better_html (2.1.1)
actionview (>= 6.0) actionview (>= 6.0)
activesupport (>= 6.0) activesupport (>= 6.0)
ast (~> 2.0) ast (~> 2.0)
erubi (~> 1.4) erubi (~> 1.4)
parser (>= 2.4) parser (>= 2.4)
smart_properties smart_properties
bigdecimal (3.1.5) bigdecimal (3.1.7)
bindex (0.8.1) bindex (0.8.1)
bootsnap (1.17.0) bootsnap (1.18.3)
msgpack (~> 1.2) msgpack (~> 1.2)
builder (3.2.4) builder (3.2.4)
bullet (7.1.4) bullet (7.1.6)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.11) uniform_notifier (~> 1.11)
camertron-eprun (1.1.1) camertron-eprun (1.1.1)
cancancan (3.5.0) cancancan (3.5.0)
capybara (3.39.2) capybara (3.40.0)
addressable addressable
matrix matrix
mini_mime (>= 0.1.3) mini_mime (>= 0.1.3)
nokogiri (~> 1.8) nokogiri (~> 1.11)
rack (>= 1.6.0) rack (>= 1.6.0)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0) regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2) xpath (~> 3.2)
childprocess (5.0.0)
chunky_png (1.4.0) chunky_png (1.4.0)
cldr-plurals-runtime-rb (1.1.0) cldr-plurals-runtime-rb (1.1.0)
cmdparse (3.0.7) cmdparse (3.0.7)
coderay (1.1.3) coderay (1.1.3)
concurrent-ruby (1.2.2) concurrent-ruby (1.2.3)
connection_pool (2.4.1) connection_pool (2.4.1)
crack (0.4.5) crack (1.0.0)
bigdecimal
rexml rexml
crass (1.0.6) crass (1.0.6)
css_parser (1.16.0) css_parser (1.17.1)
addressable addressable
cuprite (0.15) cuprite (0.15)
capybara (~> 3.0) capybara (~> 3.0)
ferrum (~> 0.14.0) ferrum (~> 0.14.0)
date (3.3.4) date (3.3.4)
debug (1.9.1) debug (1.9.2)
irb (~> 1.10) irb (~> 1.10)
reline (>= 0.3.8) reline (>= 0.3.8)
declarative (0.0.20) declarative (0.0.20)
devise (4.9.3) devise (4.9.4)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
railties (>= 4.1.0) railties (>= 4.1.0)
@@ -166,13 +168,12 @@ GEM
devise (~> 4.0) devise (~> 4.0)
railties (~> 7.0) railties (~> 7.0)
rotp (~> 6.0) rotp (~> 6.0)
diff-lcs (1.5.0) diff-lcs (1.5.1)
digest-crc (0.6.5) digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0) rake (>= 12.0.0, < 14.0.0)
docile (1.4.0) docile (1.4.0)
dotenv (2.8.1) dotenv (3.1.0)
drb (2.2.0) drb (2.2.1)
ruby2_keywords
email_typo (0.2.3) email_typo (0.2.3)
erb_lint (0.5.0) erb_lint (0.5.0)
activesupport activesupport
@@ -182,12 +183,12 @@ GEM
rubocop rubocop
smart_properties smart_properties
erubi (1.12.0) erubi (1.12.0)
factory_bot (6.4.4) factory_bot (6.4.6)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
factory_bot_rails (6.4.2) factory_bot_rails (6.4.3)
factory_bot (~> 6.4) factory_bot (~> 6.4)
railties (>= 5.0.0) railties (>= 5.0.0)
faker (3.2.2) faker (3.3.1)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
faraday (1.10.3) faraday (1.10.3)
faraday-em_http (~> 1.0) faraday-em_http (~> 1.0)
@@ -225,34 +226,34 @@ GEM
geom2d (0.4.1) geom2d (0.4.1)
globalid (1.2.1) globalid (1.2.1)
activesupport (>= 6.1) activesupport (>= 6.1)
google-apis-core (0.11.2) google-apis-core (0.14.1)
addressable (~> 2.5, >= 2.5.1) addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a) googleauth (~> 1.9)
httpclient (>= 2.8.1, < 3.a) httpclient (>= 2.8.1, < 3.a)
mini_mime (~> 1.0) mini_mime (~> 1.0)
representable (~> 3.0) representable (~> 3.0)
retriable (>= 2.0, < 4.a) retriable (>= 2.0, < 4.a)
rexml rexml
webrick google-apis-iamcredentials_v1 (0.20.0)
google-apis-iamcredentials_v1 (0.17.0) google-apis-core (>= 0.14.0, < 2.a)
google-apis-core (>= 0.11.0, < 2.a) google-apis-storage_v1 (0.37.0)
google-apis-storage_v1 (0.29.0) google-apis-core (>= 0.14.0, < 2.a)
google-apis-core (>= 0.11.0, < 2.a) google-cloud-core (1.7.0)
google-cloud-core (1.6.1)
google-cloud-env (>= 1.0, < 3.a) google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0) google-cloud-errors (~> 1.0)
google-cloud-env (2.1.0) google-cloud-env (2.1.1)
faraday (>= 1.0, < 3.a) faraday (>= 1.0, < 3.a)
google-cloud-errors (1.3.1) google-cloud-errors (1.4.0)
google-cloud-storage (1.45.0) google-cloud-storage (1.49.0)
addressable (~> 2.8) addressable (~> 2.8)
digest-crc (~> 0.4) digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1) google-apis-core (~> 0.13)
google-apis-storage_v1 (~> 0.29.0) google-apis-iamcredentials_v1 (~> 0.18)
google-apis-storage_v1 (~> 0.33)
google-cloud-core (~> 1.6) google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a) googleauth (~> 1.9)
mini_mime (~> 1.0) mini_mime (~> 1.0)
googleauth (1.9.1) googleauth (1.11.0)
faraday (>= 1.0, < 3.a) faraday (>= 1.0, < 3.a)
google-cloud-env (~> 2.1) google-cloud-env (~> 2.1)
jwt (>= 1.4, < 3.0) jwt (>= 1.4, < 3.0)
@@ -261,29 +262,31 @@ GEM
signet (>= 0.16, < 2.a) signet (>= 0.16, < 2.a)
hashdiff (1.1.0) hashdiff (1.1.0)
hashery (2.1.2) hashery (2.1.2)
hexapdf (0.37.1) hexapdf (0.40.0)
cmdparse (~> 3.0, >= 3.0.3) cmdparse (~> 3.0, >= 3.0.3)
geom2d (~> 0.4, >= 0.4.1) geom2d (~> 0.4, >= 0.4.1)
openssl (>= 2.2.1) openssl (>= 2.2.1)
htmlentities (4.3.4) htmlentities (4.3.4)
httpclient (2.8.3) httpclient (2.8.3)
i18n (1.14.1) i18n (1.14.4)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
image_processing (1.12.2) image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5) mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3) ruby-vips (>= 2.0.17, < 3)
io-console (0.7.1) io-console (0.7.2)
irb (1.11.0) irb (1.12.0)
rdoc rdoc
reline (>= 0.3.8) reline (>= 0.4.2)
jmespath (1.6.2) jmespath (1.6.2)
json (2.7.1) json (2.7.2)
jwt (2.7.1) jwt (2.8.1)
base64
language_server-protocol (3.17.0.3) language_server-protocol (3.17.0.3)
launchy (2.5.2) launchy (3.0.0)
addressable (~> 2.8) addressable (~> 2.8)
letter_opener (1.8.1) childprocess (~> 5.0)
launchy (>= 2.2, < 3) letter_opener (1.10.0)
launchy (>= 2.2, < 4)
letter_opener_web (2.0.0) letter_opener_web (2.0.0)
actionmailer (>= 5.2) actionmailer (>= 5.2)
letter_opener (~> 1.7) letter_opener (~> 1.7)
@@ -302,21 +305,21 @@ GEM
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
marcel (1.0.2) marcel (1.0.4)
matrix (0.4.2) matrix (0.4.2)
method_source (1.0.0) method_source (1.1.0)
mini_magick (4.12.0) mini_magick (4.12.0)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.5) mini_portile2 (2.8.6)
minitest (5.20.0) minitest (5.22.3)
msgpack (1.7.2) msgpack (1.7.2)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.3.0) multipart-post (2.4.0)
mutex_m (0.2.0) mutex_m (0.2.0)
mysql2 (0.5.5) mysql2 (0.5.6)
net-http-persistent (4.0.2) net-http-persistent (4.0.2)
connection_pool (~> 2.2) connection_pool (~> 2.2)
net-imap (0.4.9) net-imap (0.4.10)
date date
net-protocol net-protocol
net-pop (0.1.2) net-pop (0.1.2)
@@ -325,18 +328,21 @@ GEM
timeout timeout
net-smtp (0.4.0) net-smtp (0.4.0)
net-protocol net-protocol
nio4r (2.7.0) nio4r (2.7.1)
nokogiri (1.15.5) nokogiri (1.16.4)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.4-arm64-darwin)
racc (~> 1.4)
oj (3.16.3) oj (3.16.3)
bigdecimal (>= 3.0) bigdecimal (>= 3.0)
openssl (3.2.0) openssl (3.2.0)
orm_adapter (0.5.0) orm_adapter (0.5.0)
os (1.1.4) os (1.1.4)
pagy (6.2.0) package_json (0.1.0)
pagy (8.1.2)
parallel (1.24.0) parallel (1.24.0)
parser (3.2.2.4) parser (3.3.0.5)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
pdf-reader (2.12.0) pdf-reader (2.12.0)
@@ -345,8 +351,8 @@ GEM
hashery (~> 2.0) hashery (~> 2.0)
ruby-rc4 ruby-rc4
ttfunk ttfunk
pg (1.5.4) pg (1.5.6)
premailer (1.21.0) premailer (1.23.0)
addressable addressable
css_parser (>= 1.12.0) css_parser (>= 1.12.0)
htmlentities (>= 4.0.0) htmlentities (>= 4.0.0)
@@ -363,11 +369,11 @@ GEM
pry (>= 0.10.4) pry (>= 0.10.4)
psych (5.1.2) psych (5.1.2)
stringio stringio
public_suffix (5.0.4) public_suffix (5.0.5)
puma (6.4.0) puma (6.4.2)
nio4r (~> 2.0) nio4r (~> 2.0)
racc (1.7.3) racc (1.7.3)
rack (2.2.8) rack (2.2.9)
rack-proxy (0.7.7) rack-proxy (0.7.7)
rack rack
rack-session (1.0.2) rack-session (1.0.2)
@@ -377,20 +383,20 @@ GEM
rackup (1.0.0) rackup (1.0.0)
rack (< 3) rack (< 3)
webrick webrick
rails (7.1.2) rails (7.1.3.2)
actioncable (= 7.1.2) actioncable (= 7.1.3.2)
actionmailbox (= 7.1.2) actionmailbox (= 7.1.3.2)
actionmailer (= 7.1.2) actionmailer (= 7.1.3.2)
actionpack (= 7.1.2) actionpack (= 7.1.3.2)
actiontext (= 7.1.2) actiontext (= 7.1.3.2)
actionview (= 7.1.2) actionview (= 7.1.3.2)
activejob (= 7.1.2) activejob (= 7.1.3.2)
activemodel (= 7.1.2) activemodel (= 7.1.3.2)
activerecord (= 7.1.2) activerecord (= 7.1.3.2)
activestorage (= 7.1.2) activestorage (= 7.1.3.2)
activesupport (= 7.1.2) activesupport (= 7.1.3.2)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 7.1.2) railties (= 7.1.3.2)
rails-dom-testing (2.2.0) rails-dom-testing (2.2.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
minitest minitest
@@ -398,35 +404,35 @@ GEM
rails-html-sanitizer (1.6.0) rails-html-sanitizer (1.6.0)
loofah (~> 2.21) loofah (~> 2.21)
nokogiri (~> 1.14) nokogiri (~> 1.14)
rails-i18n (7.0.8) rails-i18n (7.0.9)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8) railties (>= 6.0.0, < 8)
rails_autolink (1.1.8) rails_autolink (1.1.8)
actionview (> 3.1) actionview (> 3.1)
activesupport (> 3.1) activesupport (> 3.1)
railties (> 3.1) railties (> 3.1)
railties (7.1.2) railties (7.1.3.2)
actionpack (= 7.1.2) actionpack (= 7.1.3.2)
activesupport (= 7.1.2) activesupport (= 7.1.3.2)
irb irb
rackup (>= 1.0.0) rackup (>= 1.0.0)
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0, >= 1.2.2) thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.1.0) rake (13.2.1)
rdoc (6.6.2) rdoc (6.6.3.1)
psych (>= 4.0.0) psych (>= 4.0.0)
redis-client (0.19.1) redis-client (0.22.0)
connection_pool connection_pool
regexp_parser (2.8.3) regexp_parser (2.9.0)
reline (0.4.1) reline (0.5.1)
io-console (~> 0.5) io-console (~> 0.5)
representable (3.2.0) representable (3.2.0)
declarative (< 0.1.0) declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0) trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0) uber (< 0.2.0)
request_store (1.5.1) request_store (1.6.0)
rack (>= 1.4) rack (>= 1.4)
responders (3.1.1) responders (3.1.1)
actionpack (>= 5.2) actionpack (>= 5.2)
@@ -438,73 +444,77 @@ GEM
chunky_png (~> 1.0) chunky_png (~> 1.0)
rqrcode_core (~> 1.0) rqrcode_core (~> 1.0)
rqrcode_core (1.2.0) rqrcode_core (1.2.0)
rspec-core (3.12.2) rspec-core (3.13.0)
rspec-support (~> 3.12.0) rspec-support (~> 3.13.0)
rspec-expectations (3.12.3) rspec-expectations (3.13.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0) rspec-support (~> 3.13.0)
rspec-mocks (3.12.6) rspec-mocks (3.13.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0) rspec-support (~> 3.13.0)
rspec-rails (6.1.0) rspec-rails (6.1.2)
actionpack (>= 6.1) actionpack (>= 6.1)
activesupport (>= 6.1) activesupport (>= 6.1)
railties (>= 6.1) railties (>= 6.1)
rspec-core (~> 3.12) rspec-core (~> 3.13)
rspec-expectations (~> 3.12) rspec-expectations (~> 3.13)
rspec-mocks (~> 3.12) rspec-mocks (~> 3.13)
rspec-support (~> 3.12) rspec-support (~> 3.13)
rspec-support (3.12.1) rspec-support (3.13.1)
rubocop (1.59.0) rubocop (1.63.1)
json (~> 2.3) json (~> 2.3)
language_server-protocol (>= 3.17.0) language_server-protocol (>= 3.17.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.2.2.4) parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0) regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0) rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.30.0, < 2.0) rubocop-ast (>= 1.31.1, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0) unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.30.0) rubocop-ast (1.31.2)
parser (>= 3.2.1.0) parser (>= 3.3.0.4)
rubocop-capybara (2.19.0) rubocop-capybara (2.20.0)
rubocop (~> 1.41) rubocop (~> 1.41)
rubocop-factory_bot (2.24.0) rubocop-factory_bot (2.25.1)
rubocop (~> 1.33) rubocop (~> 1.41)
rubocop-performance (1.20.1) rubocop-performance (1.21.0)
rubocop (>= 1.48.1, < 2.0) rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.30.0, < 2.0) rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rails (2.23.1) rubocop-rails (2.24.1)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0) rubocop (>= 1.33.0, < 2.0)
rubocop-ast (>= 1.30.0, < 2.0) rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rspec (2.25.0) rubocop-rspec (2.29.1)
rubocop (~> 1.40) rubocop (~> 1.40)
rubocop-capybara (~> 2.17) rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22) rubocop-factory_bot (~> 2.22)
rubocop-rspec_rails (~> 2.28)
rubocop-rspec_rails (2.28.3)
rubocop (~> 1.40)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
ruby-rc4 (0.1.5) ruby-rc4 (0.1.5)
ruby-vips (2.2.0) ruby-vips (2.2.1)
ffi (~> 1.12) ffi (~> 1.12)
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rubyXL (3.4.25) rubyXL (3.4.27)
nokogiri (>= 1.10.8) nokogiri (>= 1.10.8)
rubyzip (>= 1.3.0) rubyzip (>= 1.3.0)
rubyzip (2.3.2) rubyzip (2.3.2)
semantic_range (3.0.0) semantic_range (3.0.0)
shakapacker (7.1.0) shakapacker (7.2.3)
activesupport (>= 5.2) activesupport (>= 5.2)
package_json
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 5.2) railties (>= 5.2)
semantic_range (>= 2.3.0) semantic_range (>= 2.3.0)
sidekiq (7.2.0) sidekiq (7.2.2)
concurrent-ruby (< 2) concurrent-ruby (< 2)
connection_pool (>= 2.3.0) connection_pool (>= 2.3.0)
rack (>= 2.2.4) rack (>= 2.2.4)
redis-client (>= 0.14.0) redis-client (>= 0.19.0)
signet (0.18.0) signet (0.19.0)
addressable (~> 2.8) addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a) faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0) jwt (>= 1.5, < 3.0)
@@ -516,17 +526,18 @@ GEM
simplecov-html (0.12.3) simplecov-html (0.12.3)
simplecov_json_formatter (0.1.4) simplecov_json_formatter (0.1.4)
smart_properties (1.17.0) smart_properties (1.17.0)
sqlite3 (1.7.0) sqlite3 (1.7.3)
mini_portile2 (~> 2.8.0) mini_portile2 (~> 2.8.0)
sqlite3 (1.7.0-arm64-darwin) sqlite3 (1.7.3-arm64-darwin)
stringio (3.1.0) stringio (3.1.0)
strip_attributes (1.13.0) strip_attributes (1.13.0)
activemodel (>= 3.0, < 8.0) activemodel (>= 3.0, < 8.0)
thor (1.3.0) thor (1.3.1)
timeout (0.4.1) timeout (0.4.1)
trailblazer-option (0.1.2) trailblazer-option (0.1.2)
ttfunk (1.7.0) ttfunk (1.8.0)
turbo-rails (1.5.0) bigdecimal (~> 3.1)
turbo-rails (2.0.5)
actionpack (>= 6.0.0) actionpack (>= 6.0.0)
activejob (>= 6.0.0) activejob (>= 6.0.0)
railties (>= 6.0.0) railties (>= 6.0.0)
@@ -536,7 +547,7 @@ GEM
tzinfo tzinfo
tzinfo (2.0.6) tzinfo (2.0.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
tzinfo-data (1.2023.4) tzinfo-data (1.2024.1)
tzinfo (>= 1.0.0) tzinfo (>= 1.0.0)
uber (0.1.0) uber (0.1.0)
unicode-display_width (2.5.0) unicode-display_width (2.5.0)
@@ -548,7 +559,7 @@ GEM
activemodel (>= 6.0.0) activemodel (>= 6.0.0)
bindex (>= 0.4.0) bindex (>= 0.4.0)
railties (>= 6.0.0) railties (>= 6.0.0)
webmock (3.19.1) webmock (3.23.0)
addressable (>= 2.8.0) addressable (>= 2.8.0)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0) hashdiff (>= 0.4.0, < 2.0.0)
@@ -558,7 +569,7 @@ GEM
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
zeitwerk (2.6.12) zeitwerk (2.6.13)
PLATFORMS PLATFORMS
arm64-darwin-22 arm64-darwin-22
+11 -6
View File
@@ -42,7 +42,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
- PDF signature verification - PDF signature verification
- Users management - Users management
- Mobile-optimized - Mobile-optimized
- Signing available in 12 Languages - Signing available in 13 languages
- API and Webhooks for integrations - API and Webhooks for integrations
- Easy to deploy in minutes - Easy to deploy in minutes
@@ -51,11 +51,13 @@ DocuSeal is an open source platform that provides secure and efficient digital d
- User roles - User roles
- Automated reminders - Automated reminders
- Invitation and identify verification via SMS - Invitation and identify verification via SMS
- Conditional fields and formulas
- Bulk send with CSV, XLSX spreadsheet import
- SSO / SAML - SSO / SAML
- Template creation with HTML API ([Guide](https://www.docuseal.co/guides/create-pdf-document-fillable-form-with-html-api)) - Template creation with HTML API ([Guide](https://www.docuseal.co/guides/create-pdf-document-fillable-form-with-html-api))
- Template creation with PDF or DOCX and field tags API ([Guide](https://www.docuseal.co/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form)) - Template creation with PDF or DOCX and field tags API ([Guide](https://www.docuseal.co/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form))
- Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue) or [JavaScript](https://www.docuseal.co/docs/embedded)) - Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded))
- Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue) or [JavaScript](https://www.docuseal.co/docs/embedded)) - Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded))
- [Learn more](https://www.docuseal.co/pricing) - [Learn more](https://www.docuseal.co/pricing)
## Deploy ## Deploy
@@ -65,9 +67,6 @@ DocuSeal is an open source platform that provides secure and efficient digital d
| [<img alt="Deploy on Heroku" src="https://www.herokucdn.com/deploy/button.svg" height="40">](https://heroku.com/deploy?template=https://github.com/docusealco/docuseal-heroku) | [<img alt="Deploy on Railway" src="https://railway.app/button.svg" height="40">](https://railway.app/template/IGoDnc?referralCode=ruU7JR)| | [<img alt="Deploy on Heroku" src="https://www.herokucdn.com/deploy/button.svg" height="40">](https://heroku.com/deploy?template=https://github.com/docusealco/docuseal-heroku) | [<img alt="Deploy on Railway" src="https://railway.app/button.svg" height="40">](https://railway.app/template/IGoDnc?referralCode=ruU7JR)|
|**DigitalOcean**|**Render**| |**DigitalOcean**|**Render**|
| [<img alt="Deploy on DigitalOcean" src="https://www.deploytodo.com/do-btn-blue.svg" height="40">](https://cloud.digitalocean.com/apps/new?repo=https://github.com/docusealco/docuseal-digitalocean/tree/master&refcode=421d50f53990) | [<img alt="Deploy to Render" src="https://render.com/images/deploy-to-render-button.svg" height="40">](https://render.com/deploy?repo=https://github.com/docusealco/docuseal-render) | [<img alt="Deploy on DigitalOcean" src="https://www.deploytodo.com/do-btn-blue.svg" height="40">](https://cloud.digitalocean.com/apps/new?repo=https://github.com/docusealco/docuseal-digitalocean/tree/master&refcode=421d50f53990) | [<img alt="Deploy to Render" src="https://render.com/images/deploy-to-render-button.svg" height="40">](https://render.com/deploy?repo=https://github.com/docusealco/docuseal-render)
|**Koyeb**|**Elestio**|
| [<img alt="Deploy on Koyeb" src="https://www.koyeb.com/static/images/deploy/button.svg" height="40">](https://app.koyeb.com/deploy?name=docuseal&type=docker&image=docker.io/docuseal/docuseal&env[PORT]=8000&env[DATABASE_URL]=CHANGE_ME&env[SECRET_KEY_BASE]=CHANGE_ME&ports=8000;http;/) | [<img alt="Deploy on Elestio" src="https://pub-da36157c854648669813f3f76c526c2b.r2.dev/deploy-on-elestio-black.png">](https://dash.elest.io/deploy?soft=DocuSeal&id=339) |
#### Docker #### Docker
@@ -100,3 +99,9 @@ At DocuSeal we have expertise and technologies to make documents creation, filli
Distributed under the AGPLv3 License. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) for more information. Distributed under the AGPLv3 License. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) for more information.
Unless otherwise noted, all files © 2023 DocuSeal LLC. Unless otherwise noted, all files © 2023 DocuSeal LLC.
## Tools
- [Signature Maker](https://www.docuseal.co/online-signature)
- [Sign Document Online](https://www.docuseal.co/sign-documents-online)
- [Fill PDF Online](https://www.docuseal.co/fill-pdf)
@@ -9,7 +9,9 @@ class AccountConfigsController < ApplicationController
AccountConfig::FORCE_MFA, AccountConfig::FORCE_MFA,
AccountConfig::ALLOW_TO_RESUBMIT, AccountConfig::ALLOW_TO_RESUBMIT,
AccountConfig::ESIGNING_PREFERENCE_KEY, AccountConfig::ESIGNING_PREFERENCE_KEY,
AccountConfig::FORM_WITH_CONFETTI_KEY AccountConfig::FORM_WITH_CONFETTI_KEY,
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
AccountConfig::FORCE_SSO_AUTH_KEY
].freeze ].freeze
InvalidKey = Class.new(StandardError) InvalidKey = Class.new(StandardError)
@@ -8,6 +8,7 @@ module Api
skip_authorization_check skip_authorization_check
before_action :set_cors_headers before_action :set_cors_headers
before_action :set_noindex_headers
def show def show
blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid]) blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
@@ -20,6 +21,8 @@ module Api
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid) blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
authorization_check!(blob) if exp.blank?
if request.headers['Range'].present? if request.headers['Range'].present?
send_blob_byte_range_data blob, request.headers['Range'] send_blob_byte_range_data blob, request.headers['Range']
else else
@@ -31,5 +34,22 @@ module Api
end end
end end
end end
private
def authorization_check!(blob)
is_authorized =
blob.attachments.all? do |a|
a.name.in?(%w[logo preview_images]) ||
(current_user && a.record.account.id == current_user.account_id) ||
!a.record.account.account_configs.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value
end
return if is_authorized
Rollbar.error('Blob aunauthorized') if defined?(Rollbar)
raise CanCan::AccessDenied
end
end end
end end
@@ -7,6 +7,9 @@ module Api
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
skip_authorization_check skip_authorization_check
before_action :set_cors_headers
before_action :set_noindex_headers
# rubocop:disable Metrics # rubocop:disable Metrics
def show def show
Rollbar.info('Blob legacy') if defined?(Rollbar) Rollbar.info('Blob legacy') if defined?(Rollbar)
+13 -9
View File
@@ -27,8 +27,6 @@ module Api
if Rails.env.production? if Rails.env.production?
rescue_from CanCan::AccessDenied do |e| rescue_from CanCan::AccessDenied do |e|
Rollbar.warning(e) if defined?(Rollbar)
render json: { error: e.message }, status: :forbidden render json: { error: e.message }, status: :forbidden
end end
@@ -52,20 +50,26 @@ module Api
end end
def authenticate_user! def authenticate_user!
@current_user ||=
if request.headers['X-Auth-Token'].present?
sha256 = Digest::SHA256.hexdigest(request.headers['X-Auth-Token'])
User.joins(:access_token).active.find_by(access_token: { sha256: })
end
render json: { error: 'Not authenticated' }, status: :unauthorized unless current_user render json: { error: 'Not authenticated' }, status: :unauthorized unless current_user
end end
def current_user
super || @current_user ||=
if request.headers['X-Auth-Token'].present?
sha256 = Digest::SHA256.hexdigest(request.headers['X-Auth-Token'])
User.joins(:access_token).active.find_by(access_token: { sha256: })
end
end
def current_account def current_account
current_user&.account current_user&.account
end end
def set_noindex_headers
headers['X-Robots-Tag'] = 'noindex'
end
def set_cors_headers def set_cors_headers
headers['Access-Control-Allow-Origin'] = '*' headers['Access-Control-Allow-Origin'] = '*'
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS' headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
+11 -6
View File
@@ -17,7 +17,8 @@ module Api
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] }) submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] })
end end
submissions = paginate(submissions.preload(:created_by_user, :template, :submitters, submissions = paginate(submissions.preload(:created_by_user, :submitters,
template: :folder,
audit_trail_attachment: :blob)) audit_trail_attachment: :blob))
render json: { render json: {
@@ -102,6 +103,8 @@ module Api
def destroy def destroy
@submission.update!(archived_at: Time.current) @submission.update!(archived_at: Time.current)
SendSubmissionArchivedWebhookRequestJob.perform_later(@submission)
render json: @submission.as_json(only: %i[id], methods: %i[archived_at]) render json: @submission.as_json(only: %i[id], methods: %i[archived_at])
end end
@@ -153,7 +156,8 @@ module Api
completed_at opened_at sent_at completed_at opened_at sent_at
created_at updated_at external_id metadata], created_at updated_at external_id metadata],
methods: %i[status application_key] }, methods: %i[status application_key] },
template: { only: %i[id name created_at updated_at] }, template: { only: %i[id name external_id created_at updated_at],
methods: %i[folder_name] },
created_by_user: { only: %i[id email first_name last_name] } created_by_user: { only: %i[id email first_name last_name] }
} }
} }
@@ -161,14 +165,15 @@ module Api
def submissions_params def submissions_params
permitted_attrs = [ permitted_attrs = [
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
{ {
message: %i[subject body], message: %i[subject body],
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role, submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :external_id, :completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body], { metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
fields: [%i[name default_value title description fields: [:name, :uuid, :default_value, :title, :description,
readonly validation_pattern invalid_message]] }]] :readonly, :redacted, :validation_pattern, :invalid_message,
{ default_value: [] }] }]]
} }
] ]
@@ -1,35 +0,0 @@
# frozen_string_literal: true
module Api
class SubmittersAutocompleteController < ApiBaseController
load_and_authorize_resource :submitter, parent: false
SELECT_COLUMNS = %w[email phone name].freeze
LIMIT = 100
def index
submitters = search_submitters(@submitters)
values = submitters.limit(LIMIT).group(SELECT_COLUMNS.join(', ')).pluck(SELECT_COLUMNS.join(', '))
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present?
render json: attrs
end
private
def search_submitters(submitters)
if SELECT_COLUMNS.include?(params[:field])
column = Submitter.arel_table[params[:field].to_sym]
term = "#{params[:q].downcase}%"
submitters.where(column.matches(term, false, true))
else
Submitters.search(submitters, params[:q])
end
end
end
end
+17 -3
View File
@@ -10,6 +10,11 @@ module Api
submitters = submitters.where(external_id: params[:application_key]) if params[:application_key].present? submitters = submitters.where(external_id: params[:application_key]) if params[:application_key].present?
submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present? submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present?
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present? submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
if params[:template_id].present?
submitters = submitters.joins(:submission).where(submission: { template_id: params[:template_id] })
end
submitters = maybe_filder_by_completed_at(submitters, params) submitters = maybe_filder_by_completed_at(submitters, params)
submitters = paginate( submitters = paginate(
@@ -73,10 +78,11 @@ module Api
submitter_params = params.key?(:submitter) ? params.require(:submitter) : params submitter_params = params.key?(:submitter) ? params.require(:submitter) : params
submitter_params.permit( submitter_params.permit(
:send_email, :send_sms, :uuid, :name, :email, :role, :send_email, :send_sms, :reply_to, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :external_id, :completed, :phone, :application_key, :external_id, :go_to_last,
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body], { metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
fields: [%i[name default_value readonly validation_pattern invalid_message]] } fields: [[:name, :uuid, :default_value, :readonly, :redacted, :validation_pattern, :invalid_message,
{ default_value: [] }]] }
) )
end end
@@ -124,6 +130,14 @@ module Api
end end
submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms') submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms')
submitter.preferences['reply_to'] = submitter_preferences['reply_to'] if submitter_preferences.key?('reply_to')
if submitter_preferences.key?('go_to_last')
submitter.preferences['go_to_last'] = submitter_preferences['go_to_last']
end
if submitter_preferences.key?('completed_redirect_url')
submitter.preferences['completed_redirect_url'] = submitter_preferences['completed_redirect_url']
end
return unless submitter_preferences.key?('email_message_uuid') return unless submitter_preferences.key?('email_message_uuid')
@@ -1,16 +0,0 @@
# frozen_string_literal: true
module Api
class TemplateFoldersAutocompleteController < ApiBaseController
load_and_authorize_resource :template_folder, parent: false
LIMIT = 100
def index
template_folders = @template_folders.joins(:templates).where(templates: { archived_at: nil }).distinct
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
render json: template_folders.as_json(only: %i[name archived_at])
end
end
end
+14 -5
View File
@@ -49,6 +49,14 @@ module Api
@template.folder = TemplateFolders.find_or_create_by_name(current_user, folder_name) @template.folder = TemplateFolders.find_or_create_by_name(current_user, folder_name)
end end
Array.wrap(params[:roles].presence || params.dig(:template, :roles).presence).each_with_index do |role, index|
if (item = @template.submitters[index])
item['name'] = role
else
@template.submitters << { 'name' => role, 'uuid' => SecureRandom.uuid }
end
end
archived = params.key?(:archived) ? params[:archived] : params.dig(:template, :archived) archived = params.key?(:archived) ? params[:archived] : params.dig(:template, :archived)
if archived.in?([true, false]) if archived.in?([true, false])
@@ -83,9 +91,9 @@ module Api
end end
def template_params def template_params
permit_params = [ permitted_params = [
:name, :name,
{ schema: [%i[attachment_uuid name]], {
submitters: [%i[name uuid]], submitters: [%i[name uuid]],
fields: [[:uuid, :submitter_uuid, :name, :type, fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value, :required, :readonly, :default_value,
@@ -94,13 +102,14 @@ module Api
conditions: [%i[field_uuid value action]], conditions: [%i[field_uuid value action]],
options: [%i[value uuid]], options: [%i[value uuid]],
validation: %i[message pattern], validation: %i[message pattern],
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] } areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
}
] ]
if params.key?(:template) if params.key?(:template)
params.require(:template).permit(*permit_params) params.require(:template).permit(permitted_params)
else else
params.permit(*permit_params) params.permit(permitted_params)
end end
end end
end end
@@ -1,29 +0,0 @@
# frozen_string_literal: true
module Api
class TemplatesDocumentsController < ApiBaseController
load_and_authorize_resource :template
def create
return head :unprocessable_entity if params[:blobs].blank? && params[:files].blank?
documents = Templates::CreateAttachments.call(@template, params)
schema = documents.map do |doc|
{ attachment_uuid: doc.uuid, name: doc.filename.base }
end
render json: {
schema:,
documents: documents.as_json(
methods: %i[metadata signed_uuid],
include: {
preview_images: { methods: %i[url metadata filename] }
}
)
}
rescue Templates::CreateAttachments::PdfEncrypted
render json: { error: 'PDF encrypted' }, status: :unprocessable_entity
end
end
end
+4 -52
View File
@@ -7,66 +7,18 @@ class DashboardController < ApplicationController
before_action :maybe_render_landing before_action :maybe_render_landing
before_action :maybe_redirect_mfa_setup before_action :maybe_redirect_mfa_setup
load_and_authorize_resource :template_folder, parent: false skip_authorization_check
load_and_authorize_resource :template, parent: false
SHOW_TEMPLATES_FOLDERS_THRESHOLD = 9
TEMPLATES_PER_PAGE = 12
FOLDERS_PER_PAGE = 18
def index def index
@template_folders = filter_template_folders(@template_folders) if cookies.permanent[:dashboard_view] == 'submissions'
SubmissionsDashboardController.dispatch(:index, request, response)
@pagy, @template_folders = pagy(
@template_folders,
items: FOLDERS_PER_PAGE,
page: @template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1
)
if @pagy.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD
@templates = @templates.none
else else
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME } TemplatesDashboardController.dispatch(:index, request, response)
@templates = filter_templates(@templates)
items =
if @template_folders.size < 4
TEMPLATES_PER_PAGE
else
(@template_folders.size < 7 ? 9 : 6)
end
@pagy, @templates = pagy(@templates, items:)
end end
end end
private private
def filter_template_folders(template_folders)
rel = template_folders.joins(:active_templates)
.order(id: :desc)
.distinct
TemplateFolders.search(rel, params[:q])
end
def filter_templates(templates)
rel = templates.active.preload(:author).order(id: :desc)
if params[:q].blank?
if Docuseal.multitenant? && !current_account.testing?
rel = rel.where(folder_id: current_account.default_template_folder.id)
else
shared_template_ids =
TemplateSharing.where(account_id: [current_account.id, TemplateSharing::ALL_ID]).select(:template_id)
rel = rel.where(folder_id: current_account.default_template_folder.id).or(rel.where(id: shared_template_ids))
end
end
Templates.search(rel, params[:q])
end
def maybe_redirect_product_url def maybe_redirect_product_url
return if !Docuseal.multitenant? || signed_in? return if !Docuseal.multitenant? || signed_in?
@@ -35,12 +35,12 @@ class NotificationsSettingsController < ApplicationController
def load_reminder_config def load_reminder_config
@reminder_config = @reminder_config =
AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::SUBMITTER_REMAILERS) AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::SUBMITTER_REMINDERS)
end end
def email_config_params def email_config_params
params.require(:account_config).permit!.tap do |attrs| params.require(:account_config).permit!.tap do |attrs|
attrs[:key] = nil unless attrs[:key].in?([AccountConfig::BCC_EMAILS, AccountConfig::SUBMITTER_REMAILERS]) attrs[:key] = nil unless attrs[:key].in?([AccountConfig::BCC_EMAILS, AccountConfig::SUBMITTER_REMINDERS])
end end
end end
end end
+6
View File
@@ -5,6 +5,12 @@ class PasswordsController < Devise::PasswordsController
attribute :user attribute :user
end end
def create
super do |resource|
resource.errors.clear unless Docuseal.multitenant?
end
end
def update def update
super do |resource| super do |resource|
Current.user = resource Current.user = resource
@@ -5,7 +5,8 @@ class PersonalizationSettingsController < ApplicationController
AccountConfig::FORM_COMPLETED_BUTTON_KEY, AccountConfig::FORM_COMPLETED_BUTTON_KEY,
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY,
AccountConfig::FORM_COMPLETED_MESSAGE_KEY
].freeze ].freeze
InvalidKey = Class.new(StandardError) InvalidKey = Class.new(StandardError)
+2
View File
@@ -7,6 +7,8 @@ class SessionsController < Devise::SessionsController
email = sign_in_params[:email].to_s.downcase email = sign_in_params[:email].to_s.downcase
if Docuseal.multitenant? && !User.exists?(email:) if Docuseal.multitenant? && !User.exists?(email:)
Rollbar.warning('Sign in new user') if defined?(Rollbar)
return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)), return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)),
notice: 'Create a new account' notice: 'Create a new account'
end end
@@ -3,7 +3,6 @@
class SsoSettingsController < ApplicationController class SsoSettingsController < ApplicationController
before_action :load_encrypted_config before_action :load_encrypted_config
authorize_resource :encrypted_config, only: :index authorize_resource :encrypted_config, only: :index
authorize_resource :encrypted_config, parent: false, except: :index
def index; end def index; end
@@ -0,0 +1,15 @@
# frozen_string_literal: true
class SubmissionsArchivedController < ApplicationController
load_and_authorize_resource :submission, parent: false
def index
@submissions = @submissions.joins(:template)
@submissions = @submissions.where.not(archived_at: nil)
.or(@submissions.where.not(templates: { archived_at: nil }))
.preload(:created_by_user, template: :author)
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
end
end
+11
View File
@@ -19,6 +19,8 @@ class SubmissionsController < ApplicationController
def create def create
authorize!(:create, Submission) authorize!(:create, Submission)
save_template_message(@template, params) if params[:save_message] == '1'
if params[:is_custom_message] != '1' if params[:is_custom_message] != '1'
params.delete(:subject) params.delete(:subject)
params.delete(:body) params.delete(:body)
@@ -50,11 +52,20 @@ class SubmissionsController < ApplicationController
def destroy def destroy
@submission.update!(archived_at: Time.current) @submission.update!(archived_at: Time.current)
SendSubmissionArchivedWebhookRequestJob.perform_later(@submission)
redirect_back(fallback_location: template_path(@submission.template), notice: 'Submission has been archived') redirect_back(fallback_location: template_path(@submission.template), notice: 'Submission has been archived')
end end
private private
def save_template_message(template, params)
template.preferences['request_email_subject'] = params[:subject] if params[:subject].present?
template.preferences['request_email_body'] = params[:body] if params[:body].present?
template.save!
end
def submissions_params def submissions_params
params.permit(submission: { submitters: [%i[uuid email phone name]] }) params.permit(submission: { submitters: [%i[uuid email phone name]] })
end end
@@ -0,0 +1,20 @@
# frozen_string_literal: true
class SubmissionsDashboardController < ApplicationController
load_and_authorize_resource :submission, parent: false
def index
@submissions = @submissions.joins(:template)
@submissions = @submissions.where(archived_at: nil)
.where(templates: { archived_at: nil })
.preload(:created_by_user, template: :author)
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
@submissions = @submissions.pending if params[:status] == 'pending'
@submissions = @submissions.completed if params[:status] == 'completed'
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
end
end
@@ -44,7 +44,7 @@ class SubmissionsDownloadController < ApplicationController
def build_urls(submitter) def build_urls(submitter)
Submitters.select_attachments_for_download(submitter).map do |attachment| Submitters.select_attachments_for_download(submitter).map do |attachment|
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.minutes.from_now.to_i) ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
end end
end end
end end
+1 -3
View File
@@ -31,8 +31,6 @@ class SubmitFormController < ApplicationController
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user) Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
cookies[:submitter_sid] = @submitter.signed_id
render(@submitter.submission.template.archived_at? || @submitter.submission.archived_at? ? :archived : :show) render(@submitter.submission.template.archived_at? || @submitter.submission.archived_at? ? :archived : :show)
end end
@@ -57,7 +55,7 @@ class SubmitFormController < ApplicationController
end end
def completed def completed
@submitter = Submitter.find_by!(slug: params[:submit_form_slug]) @submitter = Submitter.completed.find_by!(slug: params[:submit_form_slug])
end end
def success; end def success; end
@@ -0,0 +1,33 @@
# frozen_string_literal: true
class SubmittersAutocompleteController < ApplicationController
load_and_authorize_resource :submitter, parent: false
SELECT_COLUMNS = %w[email phone name].freeze
LIMIT = 100
def index
submitters = search_submitters(@submitters)
values = submitters.limit(LIMIT).group(SELECT_COLUMNS.join(', ')).pluck(SELECT_COLUMNS.join(', '))
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present?
render json: attrs
end
private
def search_submitters(submitters)
if SELECT_COLUMNS.include?(params[:field])
column = Submitter.arel_table[params[:field].to_sym]
term = "#{params[:q].downcase}%"
submitters.where(column.matches(term, false, true))
else
Submitters.search(submitters, params[:q])
end
end
end
@@ -0,0 +1,31 @@
# frozen_string_literal: true
class TemplateDocumentsController < ApplicationController
load_and_authorize_resource :template
def create
return head :unprocessable_entity if params[:blobs].blank? && params[:files].blank?
old_fields_hash = @template.fields.hash
documents = Templates::CreateAttachments.call(@template, params)
schema = documents.map do |doc|
{ attachment_uuid: doc.uuid, name: doc.filename.base }
end
render json: {
schema:,
fields: old_fields_hash == @template.fields.hash ? nil : @template.fields,
submitters: old_fields_hash == @template.fields.hash ? nil : @template.submitters,
documents: documents.as_json(
methods: %i[metadata signed_uuid],
include: {
preview_images: { methods: %i[url metadata filename] }
}
)
}
rescue Templates::CreateAttachments::PdfEncrypted
render json: { error: 'PDF encrypted' }, status: :unprocessable_entity
end
end
@@ -0,0 +1,14 @@
# frozen_string_literal: true
class TemplateFoldersAutocompleteController < ApplicationController
load_and_authorize_resource :template_folder, parent: false
LIMIT = 100
def index
template_folders = @template_folders.joins(:templates).where(templates: { archived_at: nil }).distinct
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
render json: template_folders.as_json(only: %i[name archived_at])
end
end
@@ -6,7 +6,7 @@ class TemplatesArchivedSubmissionsController < ApplicationController
def index def index
@submissions = @submissions.where.not(archived_at: nil) @submissions = @submissions.where.not(archived_at: nil)
@submissions = Submissions.search(@submissions, params[:q]) @submissions = Submissions.search(@submissions, params[:q], search_values: true)
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc)) @pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
+23 -5
View File
@@ -8,7 +8,7 @@ class TemplatesController < ApplicationController
def show def show
submissions = @template.submissions.accessible_by(current_ability) submissions = @template.submissions.accessible_by(current_ability)
submissions = submissions.active if @template.archived_at.blank? submissions = submissions.active if @template.archived_at.blank?
submissions = Submissions.search(submissions, params[:q]) submissions = Submissions.search(submissions, params[:q], search_values: true)
@base_submissions = submissions @base_submissions = submissions
@@ -67,6 +67,12 @@ class TemplatesController < ApplicationController
end end
end end
def update
@template.update!(template_params)
head :ok
end
def destroy def destroy
notice = notice =
if params[:permanently].present? if params[:permanently].present?
@@ -86,6 +92,22 @@ class TemplatesController < ApplicationController
private private
def template_params
params.require(:template).permit(
:name,
{ schema: [%i[attachment_uuid name]],
submitters: [%i[name uuid]],
fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value,
:title, :description,
{ preferences: {},
conditions: [%i[field_uuid value action]],
options: [%i[value uuid]],
validation: %i[message pattern],
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
)
end
def authorized_clone_account_id?(account_id) def authorized_clone_account_id?(account_id)
true_user.account_id.to_s == account_id.to_s || true_user.account.linked_accounts.exists?(id: account_id) true_user.account_id.to_s == account_id.to_s || true_user.account.linked_accounts.exists?(id: account_id)
end end
@@ -98,10 +120,6 @@ class TemplatesController < ApplicationController
end end
end end
def template_params
params.require(:template).permit(:name)
end
def load_base_template def load_base_template
return if params[:base_template_id].blank? return if params[:base_template_id].blank?
@@ -0,0 +1,63 @@
# frozen_string_literal: true
class TemplatesDashboardController < ApplicationController
load_and_authorize_resource :template_folder, parent: false
load_and_authorize_resource :template, parent: false
SHOW_TEMPLATES_FOLDERS_THRESHOLD = 9
TEMPLATES_PER_PAGE = 12
FOLDERS_PER_PAGE = 18
def index
@template_folders = filter_template_folders(@template_folders)
@pagy, @template_folders = pagy(
@template_folders,
items: FOLDERS_PER_PAGE,
page: @template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1
)
if @pagy.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD
@templates = @templates.none
else
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
@templates = filter_templates(@templates)
items =
if @template_folders.size < 4
TEMPLATES_PER_PAGE
else
(@template_folders.size < 7 ? 9 : 6)
end
@pagy, @templates = pagy(@templates, items:)
end
end
private
def filter_template_folders(template_folders)
rel = template_folders.joins(:active_templates)
.order(id: :desc)
.distinct
TemplateFolders.search(rel, params[:q])
end
def filter_templates(templates)
rel = templates.active.preload(:author).order(id: :desc)
if params[:q].blank?
if Docuseal.multitenant? && !current_account.testing?
rel = rel.where(folder_id: current_account.default_template_folder.id)
else
shared_template_ids =
TemplateSharing.where(account_id: [current_account.id, TemplateSharing::ALL_ID]).select(:template_id)
rel = rel.where(folder_id: current_account.default_template_folder.id).or(rel.where(id: shared_template_ids))
end
end
Templates.search(rel, params[:q])
end
end
@@ -0,0 +1,22 @@
# frozen_string_literal: true
class TemplatesPreferencesController < ApplicationController
load_and_authorize_resource :template
def show; end
def create
authorize!(:update, @template)
@template.preferences = @template.preferences.merge(template_params[:preferences])
@template.save!
head :ok
end
private
def template_params
params.require(:template).permit(preferences: %i[bcc_completed request_email_subject request_email_body])
end
end
+22 -6
View File
@@ -7,7 +7,14 @@ class UsersController < ApplicationController
authorize_resource :user, only: :create authorize_resource :user, only: :create
def index def index
@pagy, @users = pagy(@users.active.order(id: :desc)) @users =
if params[:status] == 'archived'
@users.archived
else
@users.active
end
@pagy, @users = pagy(@users.order(id: :desc))
end end
def new; end def new; end
@@ -15,10 +22,19 @@ class UsersController < ApplicationController
def edit; end def edit; end
def create def create
existing_user = User.accessible_by(current_ability).find_by(email: @user.email)
if existing_user
existing_user.archived_at = nil
existing_user.assign_attributes(user_params)
@user = existing_user
end
if @user.save if @user.save
UserMailer.invitation_email(@user).deliver_later! UserMailer.invitation_email(@user).deliver_later!
redirect_to settings_users_path, notice: 'User has been invited' redirect_back fallback_location: settings_users_path, notice: 'User has been invited'
else else
render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity
end end
@@ -27,11 +43,11 @@ class UsersController < ApplicationController
def update def update
return redirect_to settings_users_path, notice: 'Unable to update user.' if Docuseal.demo? return redirect_to settings_users_path, notice: 'Unable to update user.' if Docuseal.demo?
attrs = user_params.compact_blank attrs = user_params.compact_blank.merge(user_params.slice(:archived_at))
attrs.delete(:role) if !role_valid?(attrs[:role]) || current_user == @user attrs.delete(:role) if !role_valid?(attrs[:role]) || current_user == @user
if @user.update(attrs) if @user.update(attrs)
redirect_to settings_users_path, notice: 'User has been updated' redirect_back fallback_location: settings_users_path, notice: 'User has been updated'
else else
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity
end end
@@ -44,7 +60,7 @@ class UsersController < ApplicationController
@user.update!(archived_at: Time.current) @user.update!(archived_at: Time.current)
redirect_to settings_users_path, notice: 'User has been removed' redirect_back fallback_location: settings_users_path, notice: 'User has been removed'
end end
private private
@@ -65,6 +81,6 @@ class UsersController < ApplicationController
end end
def user_params def user_params
params.require(:user).permit(:email, :first_name, :last_name, :password, :role) params.require(:user).permit(:email, :first_name, :last_name, :password, :role, :archived_at)
end end
end end
@@ -9,11 +9,14 @@ class VerifyPdfSignatureController < ApplicationController
HexaPDF::Document.new(io: file.open) HexaPDF::Document.new(io: file.open)
end end
cert_data = if Docuseal.multitenant? cert_data =
Docuseal::CERTS if Docuseal.multitenant?
else value = EncryptedConfig.find_by(account: current_account, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
EncryptedConfig.find_by(key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
end Docuseal::CERTS.merge(value)
else
EncryptedConfig.find_by(key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
end
default_pkcs = GenerateCertificate.load_pkcs(cert_data) default_pkcs = GenerateCertificate.load_pkcs(cert_data)
@@ -0,0 +1,33 @@
# frozen_string_literal: true
class WebhookPreferencesController < ApplicationController
EVENTS = %w[
form.viewed
form.started
form.completed
submission.archived
].freeze
before_action :load_account_config
authorize_resource :account_config, parent: false
def create
@account_config.value[account_config_params[:event]] = account_config_params[:value] == '1'
@account_config.save!
head :ok
end
private
def load_account_config
@account_config =
current_account.account_configs.find_or_initialize_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)
@account_config.value ||= {}
end
def account_config_params
params.permit(:event, :value)
end
end
+28 -2
View File
@@ -3,8 +3,10 @@ import { encodeMethodIntoRequestBody } from '@hotwired/turbo-rails/app/javascrip
import { createApp, reactive } from 'vue' import { createApp, reactive } from 'vue'
import TemplateBuilder from './template_builder/builder' import TemplateBuilder from './template_builder/builder'
import ImportList from './template_builder/import_list'
import ToggleVisible from './elements/toggle_visible' import ToggleVisible from './elements/toggle_visible'
import ToggleCookies from './elements/toggle_cookies'
import DisableHidden from './elements/disable_hidden' import DisableHidden from './elements/disable_hidden'
import TurboModal from './elements/turbo_modal' import TurboModal from './elements/turbo_modal'
import FileDropzone from './elements/file_dropzone' import FileDropzone from './elements/file_dropzone'
@@ -21,6 +23,7 @@ import SignatureForm from './elements/signature_form'
import SubmitForm from './elements/submit_form' import SubmitForm from './elements/submit_form'
import PromptPassword from './elements/prompt_password' import PromptPassword from './elements/prompt_password'
import EmailsTextarea from './elements/emails_textarea' import EmailsTextarea from './elements/emails_textarea'
import ToggleOnSubmit from './elements/toggle_on_submit'
import * as TurboInstantClick from './lib/turbo_instant_click' import * as TurboInstantClick from './lib/turbo_instant_click'
@@ -55,6 +58,8 @@ window.customElements.define('signature-form', SignatureForm)
window.customElements.define('submit-form', SubmitForm) window.customElements.define('submit-form', SubmitForm)
window.customElements.define('prompt-password', PromptPassword) window.customElements.define('prompt-password', PromptPassword)
window.customElements.define('emails-textarea', EmailsTextarea) window.customElements.define('emails-textarea', EmailsTextarea)
window.customElements.define('toggle-cookies', ToggleCookies)
window.customElements.define('toggle-on-submit', ToggleOnSubmit)
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody) document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
document.addEventListener('turbo:submit-end', async (event) => { document.addEventListener('turbo:submit-end', async (event) => {
@@ -91,12 +96,33 @@ window.customElements.define('template-builder', class extends HTMLElement {
withPhone: this.dataset.withPhone === 'true', withPhone: this.dataset.withPhone === 'true',
withLogo: this.dataset.withLogo !== 'false', withLogo: this.dataset.withLogo !== 'false',
editable: this.dataset.editable !== 'false', editable: this.dataset.editable !== 'false',
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
withPayment: this.dataset.withPayment === 'true', withPayment: this.dataset.withPayment === 'true',
isPaymentConnected: this.dataset.isPaymentConnected === 'true',
withFormula: this.dataset.withFormula === 'true', withFormula: this.dataset.withFormula === 'true',
withConditions: this.dataset.withConditions === 'true', withConditions: this.dataset.withConditions === 'true',
currencies: (this.dataset.currencies || '').split(',').filter(Boolean), currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
acceptFileTypes: this.dataset.acceptFileTypes, acceptFileTypes: this.dataset.acceptFileTypes
isDirectUpload: this.dataset.isDirectUpload === 'true' })
this.app.mount(this.appElem)
this.appendChild(this.appElem)
}
disconnectedCallback () {
this.app?.unmount()
this.appElem?.remove()
}
})
window.customElements.define('import-list', class extends HTMLElement {
connectedCallback () {
this.appElem = document.createElement('div')
this.app = createApp(ImportList, {
template: JSON.parse(this.dataset.template),
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content
}) })
this.app.mount(this.appElem) this.app.mount(this.appElem)
+5
View File
@@ -83,6 +83,11 @@ button[disabled] .enabled {
bottom: auto; bottom: auto;
} }
.tooltip-pre:before {
white-space: pre;
text-align: left;
}
.tooltip-bottom-end:after { .tooltip-bottom-end:after {
transform: translateX(-25%); transform: translateX(-25%);
border-color: transparent transparent var(--tooltip-color) transparent; border-color: transparent transparent var(--tooltip-color) transparent;
@@ -7,7 +7,7 @@ export default class extends HTMLElement {
resize () { resize () {
if (this.textarea.clientHeight < this.textarea.scrollHeight) { if (this.textarea.clientHeight < this.textarea.scrollHeight) {
this.textarea.style.height = `${this.textarea.scrollHeight}px` this.textarea.style.height = `${this.textarea.scrollHeight + 5}px`
} }
} }
+3 -3
View File
@@ -48,9 +48,9 @@ export default targetable(class extends HTMLElement {
fileRequests.reduce( fileRequests.reduce(
(prevPromise, request) => prevPromise.then(() => request()), (prevPromise, request) => prevPromise.then(() => request()),
Promise.resolve() Promise.resolve()
) ).finally(() => {
this.toggleState()
this.toggleState() })
} }
downloadSafariIos (urls) { downloadSafariIos (urls) {
+3 -57
View File
@@ -9,10 +9,6 @@ export default actionable(targetable(class extends HTMLElement {
] ]
connectedCallback () { connectedCallback () {
if (this.dataset.isDirectUpload === 'true') {
import('@rails/activestorage')
}
this.addEventListener('drop', this.onDrop) this.addEventListener('drop', this.onDrop)
this.addEventListener('dragover', (e) => e.preventDefault()) this.addEventListener('dragover', (e) => e.preventDefault())
@@ -48,61 +44,11 @@ export default actionable(targetable(class extends HTMLElement {
this.classList.toggle('opacity-50') this.classList.toggle('opacity-50')
} }
async uploadFiles (files) { uploadFiles () {
this.toggleLoading() this.toggleLoading()
if (this.dataset.isDirectUpload === 'true') { if (this.dataset.submitOnUpload) {
const { DirectUpload } = await import('@rails/activestorage') this.closest('form').querySelector('button[type="submit"]').click()
await Promise.all(
Array.from(files).map(async (file) => {
const upload = new DirectUpload(
file,
'/direct_uploads',
this.input
)
return new Promise((resolve, reject) => {
upload.create((error, blob) => {
if (error) {
console.error(error)
return reject(error)
} else {
return resolve(blob)
}
})
}).catch((error) => {
console.error(error)
})
})
).then((blobs) => {
if (this.dataset.submitOnUpload) {
this.querySelectorAll('[name="blob_signed_ids[]"]').forEach((e) => e.remove())
}
blobs.forEach((blob) => {
const input = document.createElement('input')
input.type = 'hidden'
input.name = 'blob_signed_ids[]'
input.value = blob.signed_id
this.append(input)
})
if (this.dataset.submitOnUpload === 'true') {
this.closest('form').querySelector('button[type="submit"]').click()
}
}).finally(() => {
if (this.dataset.submitOnUpload !== 'true') {
this.toggleLoading()
}
})
} else {
if (this.dataset.submitOnUpload) {
this.closest('form').querySelector('button[type="submit"]').click()
}
} }
} }
})) }))
@@ -20,7 +20,7 @@ export default class extends HTMLElement {
fetch = (text, resolve) => { fetch = (text, resolve) => {
const queryParams = new URLSearchParams({ q: text }) const queryParams = new URLSearchParams({ q: text })
fetch('/api/template_folders_autocomplete?' + queryParams).then(async (resp) => { fetch('/template_folders_autocomplete?' + queryParams).then(async (resp) => {
const items = await resp.json() const items = await resp.json()
resolve(items) resolve(items)
@@ -40,7 +40,7 @@ export default class extends HTMLElement {
if (q) { if (q) {
const queryParams = new URLSearchParams({ q, field: this.dataset.field }) const queryParams = new URLSearchParams({ q, field: this.dataset.field })
this.currentFetch ||= fetch('/api/submitters_autocomplete?' + queryParams) this.currentFetch ||= fetch('/submitters_autocomplete?' + queryParams)
this.currentFetch.then(async (resp) => { this.currentFetch.then(async (resp) => {
const items = await resp.json() const items = await resp.json()
+17
View File
@@ -0,0 +1,17 @@
export default class extends HTMLElement {
connectedCallback () {
this.button.addEventListener('click', () => {
const expirationDate = new Date()
expirationDate.setFullYear(expirationDate.getFullYear() + 10)
const expires = expirationDate.toUTCString()
document.cookie = this.dataset.key + '=' + this.dataset.value + '; expires=' + expires + '; path=/'
})
}
get button () {
return this.querySelector('button')
}
}
@@ -0,0 +1,35 @@
export default class extends HTMLElement {
connectedCallback () {
document.addEventListener('turbo:submit-end', this.onSubmitEnd)
this.form.addEventListener('submit', this.onSubmit)
}
disconnectedCallback () {
document.removeEventListener('turbo:submit-end', this.onSubmitEnd)
this.form.removeEventListener('submit', this.onSubmit)
}
onSubmit = () => {
this.element.classList.add('invisible')
}
onSubmitEnd = (event) => {
if (event.target === this.form) {
const resp = event.detail?.formSubmission?.result?.fetchResponse?.response
if (resp?.status / 100 === 2) {
this.element.classList.remove('invisible')
}
}
}
get element () {
return document.getElementById(this.dataset.elementId)
}
get form () {
return this.closest('form')
}
}
+3 -2
View File
@@ -11,14 +11,15 @@ window.customElements.define('submission-form', class extends HTMLElement {
this.app = createApp(Form, { this.app = createApp(Form, {
submitter: JSON.parse(this.dataset.submitter), submitter: JSON.parse(this.dataset.submitter),
canSendEmail: this.dataset.canSendEmail === 'true', canSendEmail: this.dataset.canSendEmail === 'true',
isDirectUpload: this.dataset.isDirectUpload === 'true',
goToLast: this.dataset.goToLast === 'true', goToLast: this.dataset.goToLast === 'true',
isDemo: this.dataset.isDemo === 'true', isDemo: this.dataset.isDemo === 'true',
attribution: this.dataset.attribution !== 'false', attribution: this.dataset.attribution !== 'false',
withConfetti: this.dataset.withConfetti !== 'false', withConfetti: this.dataset.withConfetti !== 'false',
withTypedSignature: this.dataset.withTypedSignature !== 'false', withTypedSignature: this.dataset.withTypedSignature !== 'false',
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
values: reactive(JSON.parse(this.dataset.values)), values: reactive(JSON.parse(this.dataset.values)),
completedButton: JSON.parse(this.dataset.completedButton), completedButton: JSON.parse(this.dataset.completedButton || '{}'),
completedMessage: JSON.parse(this.dataset.completedMessage || '{}'),
completedRedirectUrl: this.dataset.completedRedirectUrl, completedRedirectUrl: this.dataset.completedRedirectUrl,
attachments: reactive(JSON.parse(this.dataset.attachments)), attachments: reactive(JSON.parse(this.dataset.attachments)),
fields: JSON.parse(this.dataset.fields) fields: JSON.parse(this.dataset.fields)
@@ -51,7 +51,6 @@
<FileDropzone <FileDropzone
:message="`${t('upload')} ${field.name || t('files')}${field.required ? '' : ` (${t('optional')})`}`" :message="`${t('upload')} ${field.name || t('files')}${field.required ? '' : ` (${t('optional')})`}`"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
:is-direct-upload="isDirectUpload"
:multiple="true" :multiple="true"
@upload="onUpload" @upload="onUpload"
/> />
@@ -86,11 +85,6 @@ export default {
required: false, required: false,
default: () => ({}) default: () => ({})
}, },
isDirectUpload: {
type: Boolean,
required: true,
default: false
},
modelValue: { modelValue: {
type: Array, type: Array,
required: false, required: false,
+21 -6
View File
@@ -3,16 +3,24 @@
class="mx-auto max-w-md flex flex-col" class="mx-auto max-w-md flex flex-col"
dir="auto" dir="auto"
> >
<p class="font-medium text-2xl flex items-center space-x-1.5 mx-auto"> <div class="font-medium text-2xl flex items-center space-x-1.5 mx-auto">
<IconCircleCheck <IconCircleCheck
class="inline text-green-600" class="inline text-green-600"
:width="30" :width="30"
:height="30" :height="30"
/> />
<span> <span>
{{ t('form_has_been_completed') }} {{ completedMessage.title || t('form_has_been_completed') }}
</span> </span>
</p> </div>
<div
v-if="completedMessage.body"
class="mt-2"
>
<MarkdownContent
:string="completedMessage.body"
/>
</div>
<div class="space-y-3 mt-5"> <div class="space-y-3 mt-5">
<a <a
v-if="completedButton.url" v-if="completedButton.url"
@@ -92,10 +100,12 @@
<script> <script>
import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop, IconLogin } from '@tabler/icons-vue' import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop, IconLogin } from '@tabler/icons-vue'
import MarkdownContent from './markdown_content'
export default { export default {
name: 'FormCompleted', name: 'FormCompleted',
components: { components: {
MarkdownContent,
IconCircleCheck, IconCircleCheck,
IconInnerShadowTop, IconInnerShadowTop,
IconBrandGithub, IconBrandGithub,
@@ -143,6 +153,11 @@ export default {
type: Object, type: Object,
required: false, required: false,
default: () => ({}) default: () => ({})
},
completedMessage: {
type: Object,
required: false,
default: () => ({})
} }
}, },
data () { data () {
@@ -212,9 +227,9 @@ export default {
fileRequests.reduce( fileRequests.reduce(
(prevPromise, request) => prevPromise.then(() => request()), (prevPromise, request) => prevPromise.then(() => request()),
Promise.resolve() Promise.resolve()
) ).finally(() => {
this.isDownloading = false
this.isDownloading = false })
}, },
sanitizeHref (href) { sanitizeHref (href) {
if (href && href.trim().match(/^((?:https?:\/\/)|\/)/)) { if (href && href.trim().match(/^((?:https?:\/\/)|\/)/)) {
+1 -1
View File
@@ -19,7 +19,7 @@
</label> </label>
<button <button
class="btn btn-outline btn-sm !normal-case font-normal" class="btn btn-outline btn-sm !normal-case font-normal"
@click.prevent="setCurrentDate" @click.prevent="[setCurrentDate(), $emit('focus')]"
> >
<IconCalendarCheck :width="16" /> <IconCalendarCheck :width="16" />
{{ t('set_today') }} {{ t('set_today') }}
+16 -73
View File
@@ -70,11 +70,6 @@ export default {
required: false, required: false,
default: '*/*' default: '*/*'
}, },
isDirectUpload: {
type: Boolean,
required: true,
default: false
},
multiple: { multiple: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -92,11 +87,6 @@ export default {
return 'el' + Math.random().toString(32).split('.')[1] return 'el' + Math.random().toString(32).split('.')[1]
} }
}, },
mounted () {
if (this.isDirectUpload) {
import('@rails/activestorage')
}
},
methods: { methods: {
onDropFiles (e) { onDropFiles (e) {
this.uploadFiles(e.dataTransfer.files) this.uploadFiles(e.dataTransfer.files)
@@ -113,72 +103,25 @@ export default {
async uploadFiles (files) { async uploadFiles (files) {
this.isLoading = true this.isLoading = true
if (this.isDirectUpload) { return await Promise.all(
const { DirectUpload } = await import('@rails/activestorage') Array.from(files).map((file) => {
const formData = new FormData()
const blobs = await Promise.all( formData.append('file', file)
Array.from(files).map(async (file) => { formData.append('submitter_slug', this.submitterSlug)
const upload = new DirectUpload( formData.append('name', 'attachments')
file,
'/direct_uploads',
this.$refs.input
)
return new Promise((resolve, reject) => { return fetch(this.baseUrl + '/api/attachments', {
upload.create((error, blob) => { method: 'POST',
if (error) { body: formData
console.error(error) }).then(resp => resp.json()).then((data) => {
return data
return reject(error)
} else {
return resolve(blob)
}
})
}).catch((error) => {
console.error(error)
})
}) })
) })).then((result) => {
this.$emit('upload', result)
return await Promise.all( }).finally(() => {
blobs.map((blob) => { this.isLoading = false
return fetch(this.baseUrl + '/api/attachments', { })
method: 'POST',
body: JSON.stringify({
name: 'attachments',
blob_signed_id: blob.signed_id,
submitter_slug: this.submitterSlug
}),
headers: { 'Content-Type': 'application/json' }
}).then(resp => resp.json()).then((data) => {
return data
})
})).then((result) => {
this.$emit('upload', result)
}).finally(() => {
this.isLoading = false
})
} else {
return await Promise.all(
Array.from(files).map((file) => {
const formData = new FormData()
formData.append('file', file)
formData.append('submitter_slug', this.submitterSlug)
formData.append('name', 'attachments')
return fetch(this.baseUrl + '/api/attachments', {
method: 'POST',
body: formData
}).then(resp => resp.json()).then((data) => {
return data
})
})).then((result) => {
this.$emit('upload', result)
}).finally(() => {
this.isLoading = false
})
}
} }
} }
} }
+75 -18
View File
@@ -16,13 +16,17 @@
<button <button
v-if="!isFormVisible" v-if="!isFormVisible"
id="expand_form_button" id="expand_form_button"
class="btn btn-neutral flex text-white absolute rounded-none border-x-0 md:border md:rounded-full bottom-0 w-full md:mb-4 text-base" class="btn btn-neutral flex text-white absolute bottom-0 w-full mb-3"
style="width: 96%; margin-left: 2%"
@click.prevent="[isFormVisible = true, scrollIntoField(currentField)]" @click.prevent="[isFormVisible = true, scrollIntoField(currentField)]"
> >
<template v-if="['initials', 'signature'].includes(currentField.type)"> <template v-if="['initials', 'signature'].includes(currentField.type)">
<IconWritingSign stroke-width="1.5" /> <IconWritingSign stroke-width="1.5" />
{{ t('sign_now') }} {{ t('sign_now') }}
</template> </template>
<template v-else-if="alwaysMinimize">
{{ t('next') }}
</template>
<template v-else> <template v-else>
{{ t('submit_form') }} {{ t('submit_form') }}
</template> </template>
@@ -208,6 +212,7 @@
:name="`values[${currentField.uuid}]`" :name="`values[${currentField.uuid}]`"
:value="option.value || `${t('option')} ${index + 1}`" :value="option.value || `${t('option')} ${index + 1}`"
:required="currentField.required" :required="currentField.required"
@click="scrollIntoField(currentField)"
> >
<span class="text-xl"> <span class="text-xl">
{{ option.value || `${t('option')} ${index + 1}` }} {{ option.value || `${t('option')} ${index + 1}` }}
@@ -305,7 +310,6 @@
:key="currentField.uuid" :key="currentField.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:field="currentField" :field="currentField"
:is-direct-upload="isDirectUpload"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
:show-field-names="showFieldNames" :show-field-names="showFieldNames"
@@ -318,7 +322,6 @@
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:field="currentField" :field="currentField"
:previous-value="previousSignatureValueFor(currentField)" :previous-value="previousSignatureValueFor(currentField)"
:is-direct-upload="isDirectUpload"
:with-typed-signature="withTypedSignature" :with-typed-signature="withTypedSignature"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@@ -334,7 +337,6 @@
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:field="currentField" :field="currentField"
:previous-value="previousInitialsValue" :previous-value="previousInitialsValue"
:is-direct-upload="isDirectUpload"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:show-field-names="showFieldNames" :show-field-names="showFieldNames"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@@ -347,7 +349,6 @@
v-else-if="currentField.type === 'file'" v-else-if="currentField.type === 'file'"
:key="currentField.uuid" :key="currentField.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:is-direct-upload="isDirectUpload"
:field="currentField" :field="currentField"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@@ -360,6 +361,7 @@
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:field="currentField" :field="currentField"
:show-field-names="showFieldNames" :show-field-names="showFieldNames"
:verified-value="phoneVerifiedValues[currentField.uuid]"
:default-value="submitter.phone" :default-value="submitter.phone"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@focus="scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
@@ -393,7 +395,7 @@
v-if="isSubmitting" v-if="isSubmitting"
class="mr-1 animate-spin" class="mr-1 animate-spin"
/> />
<span v-if="stepFields.length === currentStep + 1"> <span v-if="alwaysMinimize || stepFields.length === currentStep + 1">
{{ t('submit') }} {{ t('submit') }}
</span> </span>
<span v-else> <span v-else>
@@ -417,6 +419,7 @@
:is-demo="isDemo" :is-demo="isDemo"
:attribution="attribution" :attribution="attribution"
:completed-button="completedRedirectUrl ? {} : completedButton" :completed-button="completedRedirectUrl ? {} : completedButton"
:completed-message="completedRedirectUrl ? {} : completedMessage"
:with-send-copy-button="withSendCopyButton && !completedRedirectUrl" :with-send-copy-button="withSendCopyButton && !completedRedirectUrl"
:with-download-button="withDownloadButton && !completedRedirectUrl" :with-download-button="withDownloadButton && !completedRedirectUrl"
:with-confetti="withConfetti" :with-confetti="withConfetti"
@@ -541,11 +544,21 @@ export default {
required: false, required: false,
default: null default: null
}, },
minimize: {
type: Boolean,
required: false,
default: false
},
withConfetti: { withConfetti: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false
}, },
autoscrollFields: {
type: Boolean,
required: false,
default: true
},
showFieldNames: { showFieldNames: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -576,11 +589,6 @@ export default {
required: false, required: false,
default: '' default: ''
}, },
isDirectUpload: {
type: Boolean,
required: false,
default: false
},
allowToSkip: { allowToSkip: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -611,6 +619,11 @@ export default {
required: false, required: false,
default: () => ({}) default: () => ({})
}, },
authenticityToken: {
type: String,
required: false,
default: ''
},
i18n: { i18n: {
type: Object, type: Object,
required: false, required: false,
@@ -635,6 +648,11 @@ export default {
type: Object, type: Object,
required: false, required: false,
default: () => ({}) default: () => ({})
},
completedMessage: {
type: Object,
required: false,
default: () => ({})
} }
}, },
data () { data () {
@@ -643,12 +661,20 @@ export default {
isFormVisible: this.expand !== false, isFormVisible: this.expand !== false,
showFillAllRequiredFields: false, showFillAllRequiredFields: false,
currentStep: 0, currentStep: 0,
phoneVerifiedValues: {},
orientation: screen?.orientation?.type,
isSubmitting: false, isSubmitting: false,
submittedValues: {}, submittedValues: {},
recalculateButtonDisabledKey: '' recalculateButtonDisabledKey: ''
} }
}, },
computed: { computed: {
isMobile () {
return /android|iphone|ipad/i.test(navigator.userAgent)
},
alwaysMinimize () {
return this.minimize || (this.orientation?.includes('landscape') && this.isMobile && parseInt(window.innerHeight) < 550)
},
currentStepFields () { currentStepFields () {
return this.stepFields[this.currentStep] return this.stepFields[this.currentStep]
}, },
@@ -658,9 +684,6 @@ export default {
queryParams () { queryParams () {
return new URLSearchParams(window.location.search) return new URLSearchParams(window.location.search)
}, },
authenticityToken () {
return document.querySelector('meta[name="csrf-token"]')?.content
},
submitterSlug () { submitterSlug () {
return this.submitter.slug return this.submitter.slug
}, },
@@ -724,11 +747,21 @@ export default {
watch: { watch: {
expand (value) { expand (value) {
this.isFormVisible = value this.isFormVisible = value
},
currentStepFields (value) {
if (!value) {
this.currentStep -= 1
}
} }
}, },
beforeUnmount () {
screen?.orientation?.removeEventListener('change', this.onOrientationChange)
},
mounted () { mounted () {
this.submittedValues = JSON.parse(JSON.stringify(this.values)) this.submittedValues = JSON.parse(JSON.stringify(this.values))
screen?.orientation.addEventListener('change', this.onOrientationChange)
this.fields.forEach((field) => { this.fields.forEach((field) => {
if (field.default_value && !field.readonly) { if (field.default_value && !field.readonly) {
this.values[field.uuid] ||= field.default_value this.values[field.uuid] ||= field.default_value
@@ -762,6 +795,10 @@ export default {
this.isFormVisible = false this.isFormVisible = false
} }
if (this.alwaysMinimize) {
this.isFormVisible = false
}
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) { if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
this.$nextTick(() => { this.$nextTick(() => {
const root = this.$root.$el.parentNode.getRootNode() const root = this.$root.$el.parentNode.getRootNode()
@@ -791,6 +828,9 @@ export default {
t (key) { t (key) {
return this.i18n[key] || i18n[this.language?.toLowerCase()]?.[key] || i18n[this.browserLanguage]?.[key] || i18n.en[key] || key return this.i18n[key] || i18n[this.language?.toLowerCase()]?.[key] || i18n[this.browserLanguage]?.[key] || i18n.en[key] || key
}, },
onOrientationChange (event) {
this.orientation = event.target.type
},
checkFieldConditions (field) { checkFieldConditions (field) {
if (field.conditions?.length) { if (field.conditions?.length) {
return field.conditions.reduce((acc, c) => { return field.conditions.reduce((acc, c) => {
@@ -900,9 +940,9 @@ export default {
if (scrollToArea) { if (scrollToArea) {
this.scrollIntoField(step[0]) this.scrollIntoField(step[0])
}
this.$refs.form.querySelector('input[type="date"], input[type="number"], input[type="text"], select')?.focus() this.$refs.form.querySelector('input[type="date"], input[type="number"], input[type="text"], select')?.focus()
}
if (clickUpload && !this.values[this.currentField.uuid] && ['file', 'image'].includes(this.currentField.type)) { if (clickUpload && !this.values[this.currentField.uuid] && ['file', 'image'].includes(this.currentField.type)) {
this.$refs.form.querySelector('input[type="file"]')?.click() this.$refs.form.querySelector('input[type="file"]')?.click()
@@ -911,6 +951,7 @@ export default {
}, },
saveStep (formData) { saveStep (formData) {
const currentFieldUuids = this.currentStepFields.map((f) => f.uuid) const currentFieldUuids = this.currentStepFields.map((f) => f.uuid)
const currentFieldType = this.currentField.type
if (this.isCompleted) { if (this.isCompleted) {
return Promise.resolve({}) return Promise.resolve({})
@@ -922,6 +963,10 @@ export default {
if (response.status === 200) { if (response.status === 200) {
currentFieldUuids.forEach((fieldUuid) => { currentFieldUuids.forEach((fieldUuid) => {
this.submittedValues[fieldUuid] = this.values[fieldUuid] this.submittedValues[fieldUuid] = this.values[fieldUuid]
if (currentFieldType === 'phone') {
this.phoneVerifiedValues[fieldUuid] = this.values[fieldUuid]
}
}) })
} }
@@ -960,7 +1005,15 @@ export default {
formData.append('completed', 'true') formData.append('completed', 'true')
} }
await this.saveStep(formData).then(async (response) => { let saveStepRequest
if (!isLastStep && this.phoneVerifiedValues[this.currentField.uuid] && this.phoneVerifiedValues[this.currentField.uuid] === this.values[this.currentField.uuid]) {
saveStepRequest = Promise.resolve({})
} else {
saveStepRequest = this.saveStep(formData)
}
await saveStepRequest.then(async (response) => {
if (response.status === 422 || response.status === 500) { if (response.status === 422 || response.status === 500) {
const data = await response.json() const data = await response.json()
@@ -976,7 +1029,11 @@ export default {
const nextStep = (isLastStep && emptyRequiredField) || this.stepFields[this.currentStep + 1] const nextStep = (isLastStep && emptyRequiredField) || this.stepFields[this.currentStep + 1]
if (nextStep) { if (nextStep) {
this.goToStep(nextStep, true) if (this.alwaysMinimize) {
this.isFormVisible = false
}
this.goToStep(nextStep, this.autoscrollFields)
if (emptyRequiredField === nextStep) { if (emptyRequiredField === nextStep) {
this.showFillAllRequiredFields = true this.showFillAllRequiredFields = true
+69 -3
View File
@@ -24,7 +24,7 @@ const en = {
appears_on: 'Appears on', appears_on: 'Appears on',
page: 'Page', page: 'Page',
select_your_option: 'Select your option', select_your_option: 'Select your option',
complete_hightlighted_checkboxes_and_click: 'Complete hightlighted checkboxes and click', complete_hightlighted_checkboxes_and_click: 'Complete highlighted checkboxes and click',
submit: 'submit', submit: 'submit',
next: 'next', next: 'next',
click_to_upload: 'Click to upload', click_to_upload: 'Click to upload',
@@ -42,7 +42,7 @@ const en = {
please_check_the_box_to_continue: 'Please check the box to continue', please_check_the_box_to_continue: 'Please check the box to continue',
open_source_documents_software: 'open source documents software', open_source_documents_software: 'open source documents software',
verified_phone_number: 'Verify Phone Number', verified_phone_number: 'Verify Phone Number',
use_international_format: 'Use internatioanl format: +1xxx', use_international_format: 'Use international format: +1xxx',
six_digits_code: '6-digit code', six_digits_code: '6-digit code',
change_phone_number: 'Change phone number', change_phone_number: 'Change phone number',
sending: 'Sending...', sending: 'Sending...',
@@ -782,6 +782,72 @@ const ar = {
files: 'الملفات' files: 'الملفات'
} }
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar } const ko = {
text: '텍스트',
signature: '서명',
initials: '이니셜',
date: '날짜',
number: '숫자',
image: '이미지',
take_photo: '사진 찍기',
file: '파일',
select: '선택',
checkbox: '체크박스',
multiple: '다중',
radio: '라디오',
cells: '셀',
stamp: '스탬프',
minimize: '최소화',
payment: '결제',
phone: '전화',
submit_form: '양식 제출',
sign_now: '지금 서명',
type_here_: '여기에 입력...',
optional: '선택 사항',
option: '옵션',
appears_on: '다음에 나타납니다',
page: '페이지',
select_your_option: '옵션 선택',
complete_hightlighted_checkboxes_and_click: '강조된 체크박스를 완료하고 클릭',
submit: '제출',
next: '다음',
click_to_upload: '업로드하려면 클릭',
or_drag_and_drop_files: '또는 파일을 드래그하여 놓으십시오',
send_copy_via_email: '이메일로 사본 보내기',
download: '다운로드',
clear: '지우기',
redraw: '다시 그리기',
draw_initials: '이니셜 그리기',
type_signature_here: '여기에 서명 입력',
type_initial_here: '여기에 이니셜 입력',
form_has_been_completed: '양식이 완료되었습니다!',
create_a_free_account: '무료 계정 생성',
signed_with: '다음으로 서명:',
please_check_the_box_to_continue: '계속하려면 확인란을 선택하십시오',
open_source_documents_software: '오픈 소스 문서 소프트웨어',
verified_phone_number: '전화번호 확인됨',
use_international_format: '국제 포맷 사용: +1xxx',
six_digits_code: '6자리 코드',
change_phone_number: '전화번호 변경',
sending: '전송 중...',
resend_code: '코드 다시 보내기',
verification_code_has_been_resent: '인증 코드가 SMS로 다시 전송되었습니다',
please_fill_all_required_fields: '필수 필드를 모두 입력하십시오',
set_today: '오늘 날짜로 설정',
toggle_multiline_text: '다중 텍스트 전환',
draw_signature: '서명 그리기',
type_initial: '이니셜 입력',
draw: '그리기',
type: '입력',
type_text: '텍스트 입력',
email_has_been_sent: '이메일이 전송되었습니다',
processing: '처리 중',
pay_with_strip: '스트라이프로 결제',
reupload: '다시 업로드',
upload: '업로드',
files: '파일'
}
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar, ko }
export default i18n export default i18n
@@ -38,7 +38,6 @@
:message="`${t('upload')} ${field.name || t('image')}${field.required ? '' : ` (${t('optional')})`}`" :message="`${t('upload')} ${field.name || t('image')}${field.required ? '' : ` (${t('optional')})`}`"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
:accept="'image/*'" :accept="'image/*'"
:is-direct-upload="isDirectUpload"
@upload="onImageUpload" @upload="onImageUpload"
/> />
</div> </div>
@@ -62,11 +61,6 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
isDirectUpload: {
type: Boolean,
required: true,
default: false
},
showFieldNames: { showFieldNames: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -152,11 +152,6 @@ export default {
required: false, required: false,
default: true default: true
}, },
isDirectUpload: {
type: Boolean,
required: true,
default: false
},
attachmentsIndex: { attachmentsIndex: {
type: Object, type: Object,
required: false, required: false,
@@ -198,14 +193,8 @@ export default {
this.$refs.canvas.getContext('2d').scale(scale, scale) this.$refs.canvas.getContext('2d').scale(scale, scale)
} }
this.$refs.textInput?.focus()
}) })
if (this.isDirectUpload) {
import('@rails/activestorage')
}
if (this.$refs.canvas) { if (this.$refs.canvas) {
this.pad = new SignaturePad(this.$refs.canvas) this.pad = new SignaturePad(this.$refs.canvas)
@@ -293,45 +282,21 @@ export default {
cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => { cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => {
const file = new File([blob], 'initials.png', { type: 'image/png' }) const file = new File([blob], 'initials.png', { type: 'image/png' })
if (this.isDirectUpload) { const formData = new FormData()
const { DirectUpload } = await import('@rails/activestorage')
new DirectUpload( formData.append('file', file)
file, formData.append('submitter_slug', this.submitterSlug)
'/direct_uploads' formData.append('name', 'attachments')
).create((_error, data) => {
fetch(this.baseUrl + '/api/attachments', {
method: 'POST',
body: JSON.stringify({
submitter_slug: this.submitterSlug,
blob_signed_id: data.signed_id,
name: 'attachments'
}),
headers: { 'Content-Type': 'application/json' }
}).then((resp) => resp.json()).then((attachment) => {
this.$emit('update:model-value', attachment.uuid)
this.$emit('attached', attachment)
return resolve(attachment) return fetch(this.baseUrl + '/api/attachments', {
}) method: 'POST',
}) body: formData
} else { }).then((resp) => resp.json()).then((attachment) => {
const formData = new FormData() this.$emit('attached', attachment)
this.$emit('update:model-value', attachment.uuid)
formData.append('file', file) return resolve(attachment)
formData.append('submitter_slug', this.submitterSlug) })
formData.append('name', 'attachments')
return fetch(this.baseUrl + '/api/attachments', {
method: 'POST',
body: formData
}).then((resp) => resp.json()).then((attachment) => {
this.$emit('attached', attachment)
this.$emit('update:model-value', attachment.uuid)
return resolve(attachment)
})
}
}) })
}) })
} }
@@ -45,6 +45,7 @@
<label <label
:for="option.uuid" :for="option.uuid"
class="flex items-center space-x-3" class="flex items-center space-x-3"
@click="scrollIntoField(field)"
> >
<input <input
:id="option.uuid" :id="option.uuid"
@@ -108,6 +108,11 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
verifiedValue: {
type: String,
required: false,
default: ''
},
submitterSlug: { submitterSlug: {
type: String, type: String,
required: true required: true
@@ -164,6 +169,10 @@ export default {
}) })
}, },
async submit () { async submit () {
if (this.verifiedValue && this.verifiedValue === this.modelValue) {
return Promise.resolve({})
}
if (!this.$refs.phone.value.toString().startsWith('+')) { if (!this.$refs.phone.value.toString().startsWith('+')) {
alert(this.t('use_international_format')) alert(this.t('use_international_format'))
@@ -176,11 +176,6 @@ export default {
required: false, required: false,
default: true default: true
}, },
isDirectUpload: {
type: Boolean,
required: true,
default: false
},
withTypedSignature: { withTypedSignature: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -230,10 +225,6 @@ export default {
} }
}) })
if (this.isDirectUpload) {
import('@rails/activestorage')
}
if (this.$refs.canvas) { if (this.$refs.canvas) {
this.pad = new SignaturePad(this.$refs.canvas) this.pad = new SignaturePad(this.$refs.canvas)
@@ -390,45 +381,21 @@ export default {
cropCanvasAndExportToPNG(this.$refs.canvas, { errorOnTooSmall: true }).then(async (blob) => { cropCanvasAndExportToPNG(this.$refs.canvas, { errorOnTooSmall: true }).then(async (blob) => {
const file = new File([blob], 'signature.png', { type: 'image/png' }) const file = new File([blob], 'signature.png', { type: 'image/png' })
if (this.isDirectUpload) { const formData = new FormData()
const { DirectUpload } = await import('@rails/activestorage')
new DirectUpload( formData.append('file', file)
file, formData.append('submitter_slug', this.submitterSlug)
'/direct_uploads' formData.append('name', 'attachments')
).create((_error, data) => {
fetch(this.baseUrl + '/api/attachments', {
method: 'POST',
body: JSON.stringify({
submitter_slug: this.submitterSlug,
blob_signed_id: data.signed_id,
name: 'attachments'
}),
headers: { 'Content-Type': 'application/json' }
}).then((resp) => resp.json()).then((attachment) => {
this.$emit('update:model-value', attachment.uuid)
this.$emit('attached', attachment)
return resolve(attachment) return fetch(this.baseUrl + '/api/attachments', {
}) method: 'POST',
}) body: formData
} else { }).then((resp) => resp.json()).then((attachment) => {
const formData = new FormData() this.$emit('attached', attachment)
this.$emit('update:model-value', attachment.uuid)
formData.append('file', file) return resolve(attachment)
formData.append('submitter_slug', this.submitterSlug) })
formData.append('name', 'attachments')
return fetch(this.baseUrl + '/api/attachments', {
method: 'POST',
body: formData
}).then((resp) => resp.json()).then((attachment) => {
this.$emit('attached', attachment)
this.$emit('update:model-value', attachment.uuid)
return resolve(attachment)
})
}
}).catch((error) => { }).catch((error) => {
return reject(error) return reject(error)
}) })
+2 -2
View File
@@ -42,7 +42,7 @@
v-model="field.submitter_uuid" v-model="field.submitter_uuid"
class="border-r" class="border-r"
:compact="true" :compact="true"
:editable="editable && !defaultField" :editable="editable && (!defaultField || defaultField.role !== submitter?.name)"
:allow-add-new="!defaultSubmitters.length" :allow-add-new="!defaultSubmitters.length"
:menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px] mt-[1px]'" :menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px] mt-[1px]'"
:submitters="template.submitters" :submitters="template.submitters"
@@ -68,7 +68,7 @@
@keydown.enter.prevent="onNameEnter" @keydown.enter.prevent="onNameEnter"
@focus="onNameFocus" @focus="onNameFocus"
@blur="onNameBlur" @blur="onNameBlur"
>{{ optionIndexText }} {{ field.name || defaultName }}</span> >{{ optionIndexText }} {{ (defaultField ? (field.title || field.name) : field.name) || defaultName }}</span>
<div <div
v-if="isNameFocus && !['checkbox', 'phone'].includes(field.type)" v-if="isNameFocus && !['checkbox', 'phone'].includes(field.type)"
class="flex items-center ml-1.5" class="flex items-center ml-1.5"
+207 -70
View File
@@ -26,7 +26,7 @@
@update:model-value="updateName" @update:model-value="updateName"
/> />
</div> </div>
<div class="space-x-3 flex items-center"> <div class="space-x-3 flex items-center flex-shrink-0">
<slot <slot
v-if="$slots.buttons" v-if="$slots.buttons"
name="buttons" name="buttons"
@@ -37,10 +37,10 @@
class="btn btn-primary btn-ghost text-base hidden md:flex" class="btn btn-primary btn-ghost text-base hidden md:flex"
:target="template.submitters.length > 1 ? '' : '_blank'" :target="template.submitters.length > 1 ? '' : '_blank'"
:data-turbo-frame="template.submitters.length > 1 ? 'modal' : ''" :data-turbo-frame="template.submitters.length > 1 ? 'modal' : ''"
@click="maybeShowEmptyTemplateAlert" @click="maybeShowErrorTemplateAlert"
> >
<IconWritingSign <IconWritingSign
width="20" width="22"
class="inline" class="inline"
/> />
<span class="hidden md:inline"> <span class="hidden md:inline">
@@ -51,7 +51,7 @@
:href="`/templates/${template.id}/submissions/new?with_link=true`" :href="`/templates/${template.id}/submissions/new?with_link=true`"
data-turbo-frame="modal" data-turbo-frame="modal"
class="white-button md:!px-6" class="white-button md:!px-6"
@click="maybeShowEmptyTemplateAlert" @click="maybeShowErrorTemplateAlert"
> >
<IconUsersPlus <IconUsersPlus
width="20" width="20"
@@ -115,7 +115,6 @@
:with-replace-button="withUploadButton" :with-replace-button="withUploadButton"
:editable="editable" :editable="editable"
:template="template" :template="template"
:is-direct-upload="isDirectUpload"
@scroll-to="scrollIntoDocument(item)" @scroll-to="scrollIntoDocument(item)"
@remove="onDocumentRemove" @remove="onDocumentRemove"
@replace="onDocumentReplace" @replace="onDocumentReplace"
@@ -131,7 +130,6 @@
v-if="sortedDocuments.length && editable && withUploadButton" v-if="sortedDocuments.length && editable && withUploadButton"
:accept-file-types="acceptFileTypes" :accept-file-types="acceptFileTypes"
:template-id="template.id" :template-id="template.id"
:is-direct-upload="isDirectUpload"
@success="updateFromUpload" @success="updateFromUpload"
/> />
</div> </div>
@@ -148,7 +146,6 @@
v-if="!sortedDocuments.length && withUploadButton" v-if="!sortedDocuments.length && withUploadButton"
:template-id="template.id" :template-id="template.id"
:accept-file-types="acceptFileTypes" :accept-file-types="acceptFileTypes"
:is-direct-upload="isDirectUpload"
@success="updateFromUpload" @success="updateFromUpload"
/> />
<template v-else> <template v-else>
@@ -162,13 +159,14 @@
:selected-submitter="selectedSubmitter" :selected-submitter="selectedSubmitter"
:document="document" :document="document"
:is-drag="!!dragField" :is-drag="!!dragField"
:default-fields="defaultFields" :default-fields="[...defaultRequiredFields, ...defaultFields]"
:allow-draw="!onlyDefinedFields" :allow-draw="!onlyDefinedFields"
:default-submitters="defaultSubmitters" :default-submitters="defaultSubmitters"
:draw-field="drawField" :draw-field="drawField"
:draw-field-type="drawFieldType"
:editable="editable" :editable="editable"
:base-url="baseUrl" :base-url="baseUrl"
@draw="onDraw" @draw="[onDraw($event), withSelectedFieldType ? '' : drawFieldType = '', showDrawField = false]"
@drop-field="onDropfield" @drop-field="onDropfield"
@remove-area="removeArea" @remove-area="removeArea"
/> />
@@ -177,9 +175,9 @@
:with-arrows="template.schema.length > 1" :with-arrows="template.schema.length > 1"
:item="template.schema.find((item) => item.attachment_uuid === document.uuid)" :item="template.schema.find((item) => item.attachment_uuid === document.uuid)"
:with-replace-button="withUploadButton" :with-replace-button="withUploadButton"
:accept-file-types="acceptFileTypes"
:document="document" :document="document"
:template="template" :template="template"
:is-direct-upload="isDirectUpload"
class="pb-2 mb-2 border-b border-base-300 border-dashed" class="pb-2 mb-2 border-b border-base-300 border-dashed"
@remove="onDocumentRemove" @remove="onDocumentRemove"
@replace="onDocumentReplace" @replace="onDocumentReplace"
@@ -195,7 +193,6 @@
<Upload <Upload
v-if="withUploadButton" v-if="withUploadButton"
:template-id="template.id" :template-id="template.id"
:is-direct-upload="isDirectUpload"
:accept-file-types="acceptFileTypes" :accept-file-types="acceptFileTypes"
@success="updateFromUpload" @success="updateFromUpload"
/> />
@@ -210,13 +207,13 @@
:class="drawField ? 'overflow-hidden' : 'overflow-y-auto overflow-x-hidden'" :class="drawField ? 'overflow-hidden' : 'overflow-y-auto overflow-x-hidden'"
> >
<div <div
v-if="drawField" v-if="showDrawField || drawField"
class="sticky inset-0 h-full z-20" class="sticky inset-0 h-full z-20"
:style="{ backgroundColor }" :style="{ backgroundColor }"
> >
<div class="bg-base-300 rounded-lg p-5 text-center space-y-4"> <div class="bg-base-200 rounded-lg p-5 text-center space-y-4">
<p> <p>
{{ t('draw_field_on_the_document').replace('{field}', drawField.name) }} {{ t('draw_field_on_the_document').replace('{field}', drawField?.name || '') }}
</p> </p>
<div> <div>
<button <button
@@ -226,10 +223,10 @@
{{ t('cancel') }} {{ t('cancel') }}
</button> </button>
<a <a
v-if="!drawOption && !drawField.areas.length && !['stamp', 'signature', 'initials'].includes(drawField.type)" v-if="!drawField && !drawOption && !['stamp', 'signature', 'initials'].includes(drawField?.type || drawFieldType)"
href="#" href="#"
class="link block mt-3 text-sm" class="link block mt-3 text-sm"
@click.prevent="[drawField = null, drawOption = null]" @click.prevent="[addField(drawFieldType), drawField = null, drawOption = null, withSelectedFieldType ? '' : drawFieldType = '', showDrawField = false]"
> >
{{ t('or_add_field_without_drawing') }} {{ t('or_add_field_without_drawing') }}
</a> </a>
@@ -244,12 +241,16 @@
:selected-submitter="selectedSubmitter" :selected-submitter="selectedSubmitter"
:with-help="withHelp" :with-help="withHelp"
:default-submitters="defaultSubmitters" :default-submitters="defaultSubmitters"
:default-fields="defaultFields" :draw-field-type="drawFieldType"
:default-fields="[...defaultRequiredFields, ...defaultFields]"
:default-required-fields="defaultRequiredFields"
:field-types="fieldTypes" :field-types="fieldTypes"
:with-sticky-submitters="withStickySubmitters" :with-sticky-submitters="withStickySubmitters"
:only-defined-fields="onlyDefinedFields" :only-defined-fields="onlyDefinedFields"
:editable="editable" :editable="editable"
@add-field="addField"
@set-draw="[drawField = $event.field, drawOption = $event.option]" @set-draw="[drawField = $event.field, drawOption = $event.option]"
@set-draw-type="[drawFieldType = $event, showDrawField = true]"
@set-drag="dragField = $event" @set-drag="dragField = $event"
@change-submitter="selectedSubmitter = $event" @change-submitter="selectedSubmitter = $event"
@drag-end="dragField = null" @drag-end="dragField = null"
@@ -273,7 +274,8 @@
<MobileFields <MobileFields
v-if="sortedDocuments.length && !drawField && editable" v-if="sortedDocuments.length && !drawField && editable"
:fields="template.fields" :fields="template.fields"
:default-fields="defaultFields" :default-fields="[...defaultRequiredFields, ...defaultFields]"
:default-required-fields="defaultRequiredFields"
:field-types="fieldTypes" :field-types="fieldTypes"
:selected-submitter="selectedSubmitter" :selected-submitter="selectedSubmitter"
@select="startFieldDraw($event)" @select="startFieldDraw($event)"
@@ -328,6 +330,7 @@ export default {
backgroundColor: this.backgroundColor, backgroundColor: this.backgroundColor,
withPhone: this.withPhone, withPhone: this.withPhone,
withPayment: this.withPayment, withPayment: this.withPayment,
isPaymentConnected: this.isPaymentConnected,
withFormula: this.withFormula, withFormula: this.withFormula,
withConditions: this.withConditions, withConditions: this.withConditions,
defaultDrawFieldType: this.defaultDrawFieldType, defaultDrawFieldType: this.defaultDrawFieldType,
@@ -340,11 +343,6 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
isDirectUpload: {
type: Boolean,
required: false,
default: false
},
i18n: { i18n: {
type: Object, type: Object,
required: false, required: false,
@@ -375,6 +373,16 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
defaultRequiredFields: {
type: Array,
required: false,
default: () => []
},
withSelectedFieldType: {
type: Boolean,
required: false,
default: false
},
defaultDrawFieldType: { defaultDrawFieldType: {
type: String, type: String,
required: false, required: false,
@@ -424,6 +432,13 @@ export default {
return () => {} return () => {}
} }
}, },
onChange: {
type: Function,
required: false,
default () {
return () => {}
}
},
withStickySubmitters: { withStickySubmitters: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -444,6 +459,11 @@ export default {
required: false, required: false,
default: true default: true
}, },
authenticityToken: {
type: String,
required: false,
default: ''
},
withDocumentsList: { withDocumentsList: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -459,6 +479,11 @@ export default {
required: false, required: false,
default: false default: false
}, },
isPaymentConnected: {
type: Boolean,
required: false,
default: false
},
withFormula: { withFormula: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -486,7 +511,9 @@ export default {
isBreakpointLg: false, isBreakpointLg: false,
isSaving: false, isSaving: false,
selectedSubmitter: null, selectedSubmitter: null,
showDrawField: false,
drawField: null, drawField: null,
drawFieldType: null,
drawOption: null, drawOption: null,
dragField: null dragField: null
} }
@@ -509,6 +536,11 @@ export default {
return areas return areas
}, },
isAllRequiredFieldsAdded () {
return !this.defaultRequiredFields?.some((f) => {
return !this.template.fields?.some((field) => field.name === f.name)
})
},
selectedField () { selectedField () {
return this.template.fields.find((f) => f.areas?.includes(this.selectedAreaRef.value)) return this.template.fields.find((f) => f.areas?.includes(this.selectedAreaRef.value))
}, },
@@ -519,6 +551,12 @@ export default {
} }
}, },
created () { created () {
if (!this.template.fields?.length && this.template.submitters?.length === 1) {
if (this.template.submitters[0]?.name === 'First Party') {
this.template.submitters[0].name = this.t('first_party')
}
}
const existingSubmittersUuids = this.defaultSubmitters.map((name) => { const existingSubmittersUuids = this.defaultSubmitters.map((name) => {
return this.template.submitters.find(e => e.name === name)?.uuid return this.template.submitters.find(e => e.name === name)?.uuid
}) })
@@ -569,6 +607,34 @@ export default {
t (key) { t (key) {
return this.i18n[key] || i18nEn[key] || key return this.i18n[key] || i18nEn[key] || key
}, },
addField (type, area = null) {
const field = {
name: '',
uuid: v4(),
required: type !== 'checkbox',
areas: area ? [area] : [],
submitter_uuid: this.selectedSubmitter.uuid,
type
}
if (['select', 'multiple', 'radio'].includes(type)) {
field.options = [{ value: '', uuid: v4() }]
}
if (type === 'stamp') {
field.readonly = true
}
if (type === 'date') {
field.preferences = {
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
}
}
this.template.fields.push(field)
this.save()
},
startFieldDraw ({ name, type }) { startFieldDraw ({ name, type }) {
const existingField = this.template.fields?.find((f) => f.submitter_uuid === this.selectedSubmitter.uuid && name && name === f.name) const existingField = this.template.fields?.find((f) => f.submitter_uuid === this.selectedSubmitter.uuid && name && name === f.name)
@@ -649,15 +715,13 @@ export default {
ref.$el.scrollIntoView({ behavior: 'smooth', block: 'start' }) ref.$el.scrollIntoView({ behavior: 'smooth', block: 'start' })
}, },
clearDrawField () { clearDrawField () {
if (this.drawField && !this.drawOption && this.drawField.areas.length === 0) {
const fieldIndex = this.template.fields.indexOf(this.drawField)
if (fieldIndex !== -1) {
this.template.fields.splice(fieldIndex, 1)
}
}
this.drawField = null this.drawField = null
this.drawOption = null this.drawOption = null
this.showDrawField = false
if (!this.withSelectedFieldType) {
this.drawFieldType = ''
}
}, },
onKeyUp (e) { onKeyUp (e) {
if (e.code === 'Escape') { if (e.code === 'Escape') {
@@ -707,6 +771,27 @@ export default {
} }
} }
}, },
setDefaultAreaSize (area, type) {
const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid)
const pageMask = documentRef.pageRefs[area.page].$refs.mask
if (type === 'checkbox') {
area.w = pageMask.clientWidth / 30 / pageMask.clientWidth
area.h = (pageMask.clientWidth / 30 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight)
} else if (type === 'image') {
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight)
} else if (type === 'signature' || type === 'stamp') {
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight) / 2
} else if (type === 'initials') {
area.w = pageMask.clientWidth / 10 / pageMask.clientWidth
area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth)
} else {
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth)
}
},
onDraw (area) { onDraw (area) {
if (this.drawField) { if (this.drawField) {
if (this.drawOption) { if (this.drawOption) {
@@ -729,25 +814,7 @@ export default {
area.w = previousArea.w area.w = previousArea.w
area.h = previousArea.h area.h = previousArea.h
} else { } else {
const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid) this.setDefaultAreaSize(area, this.drawOption ? 'checkbox' : this.drawField?.type)
const pageMask = documentRef.pageRefs[area.page].$refs.mask
if (this.drawField.type === 'checkbox' || this.drawOption) {
area.w = pageMask.clientWidth / 30 / pageMask.clientWidth
area.h = (pageMask.clientWidth / 30 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight)
} else if (this.drawField.type === 'image') {
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight)
} else if (this.drawField.type === 'signature' || this.drawField.type === 'stamp') {
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight) / 2
} else if (this.drawField.type === 'initials') {
area.w = pageMask.clientWidth / 10 / pageMask.clientWidth
area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth)
} else {
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth)
}
} }
area.x -= area.w / 2 area.x -= area.w / 2
@@ -782,13 +849,15 @@ export default {
let type = (pageMask.clientWidth * area.w) < 35 ? 'checkbox' : 'text' let type = (pageMask.clientWidth * area.w) < 35 ? 'checkbox' : 'text'
if (this.defaultDrawFieldType && this.defaultDrawFieldType !== 'text') { if (this.drawFieldType) {
type = this.drawFieldType
} else if (this.defaultDrawFieldType && this.defaultDrawFieldType !== 'text') {
type = this.defaultDrawFieldType type = this.defaultDrawFieldType
} else if (this.fieldTypes.length !== 0 && !this.fieldTypes.includes(type)) { } else if (this.fieldTypes.length !== 0 && !this.fieldTypes.includes(type)) {
type = this.fieldTypes[0] type = this.fieldTypes[0]
} }
if (type === 'checkbox') { if (type === 'checkbox' && !this.drawFieldType) {
const previousField = [...this.template.fields].reverse().find((f) => f.type === type) const previousField = [...this.template.fields].reverse().find((f) => f.type === type)
const previousArea = previousField?.areas?.[previousField.areas.length - 1] const previousArea = previousField?.areas?.[previousField.areas.length - 1]
@@ -806,21 +875,22 @@ export default {
} }
} }
if (area.w) { if (this.drawFieldType && (area.w === 0 || area.h === 0)) {
const field = { if (this.selectedField?.type === this.drawFieldType) {
name: '', area.w = this.selectedAreaRef.value.w
uuid: v4(), area.h = this.selectedAreaRef.value.h
required: type !== 'checkbox', } else {
type, this.setDefaultAreaSize(area, this.drawFieldType)
submitter_uuid: this.selectedSubmitter.uuid,
areas: [area]
} }
this.template.fields.push(field) area.x -= area.w / 2
area.y -= area.h / 2
}
if (area.w) {
this.addField(type, area)
this.selectedAreaRef.value = area this.selectedAreaRef.value = area
this.save()
} }
} }
}, },
@@ -921,14 +991,26 @@ export default {
this.save() this.save()
}, },
updateFromUpload ({ schema, documents }) { updateFromUpload (data) {
this.template.schema.push(...schema) this.template.schema.push(...data.schema)
this.template.documents.push(...documents) this.template.documents.push(...data.documents)
if (data.fields) {
this.template.fields = data.fields
}
if (data.submitters) {
this.template.submitters = data.submitters
if (!this.template.submitters.find((s) => s.uuid === this.selectedSubmitter?.uuid)) {
this.selectedSubmitter = this.template.submitters[0]
}
}
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.previews.scrollTop = this.$refs.previews.scrollHeight this.$refs.previews.scrollTop = this.$refs.previews.scrollHeight
this.scrollIntoDocument(schema[0]) this.scrollIntoDocument(data.schema[0])
}) })
if (this.template.name === 'New Document') { if (this.template.name === 'New Document') {
@@ -951,19 +1033,56 @@ export default {
this.template.schema.splice(this.template.schema.indexOf(item), 1) this.template.schema.splice(this.template.schema.indexOf(item), 1)
} }
const removedFieldUuids = []
this.template.fields.forEach((field) => { this.template.fields.forEach((field) => {
[...(field.areas || [])].forEach((area) => { [...(field.areas || [])].forEach((area) => {
if (area.attachment_uuid === item.attachment_uuid) { if (area.attachment_uuid === item.attachment_uuid) {
field.areas.splice(field.areas.indexOf(area), 1) field.areas.splice(field.areas.indexOf(area), 1)
removedFieldUuids.push(field.uuid)
} }
}) })
}) })
this.template.fields =
this.template.fields.filter((f) => !removedFieldUuids.includes(f.uuid) || f.areas?.length)
this.save() this.save()
}, },
onDocumentReplace ({ replaceSchemaItem, schema, documents }) { 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, schema[0])
this.template.documents.push(...documents) this.template.documents.push(...documents)
if (data.fields) {
this.template.fields = data.fields
const removedFieldUuids = []
this.template.fields.forEach((field) => {
[...(field.areas || [])].forEach((area) => {
if (area.attachment_uuid === replaceSchemaItem.attachment_uuid) {
field.areas.splice(field.areas.indexOf(area), 1)
removedFieldUuids.push(field.uuid)
}
})
})
this.template.fields =
this.template.fields.filter((f) => !removedFieldUuids.includes(f.uuid) || f.areas?.length)
}
if (data.submitters) {
this.template.submitters = data.submitters
if (!this.template.submitters.find((s) => s.uuid === this.selectedSubmitter?.uuid)) {
this.selectedSubmitter = this.template.submitters[0]
}
}
this.template.fields.forEach((field) => { this.template.fields.forEach((field) => {
(field.areas || []).forEach((area) => { (field.areas || []).forEach((area) => {
if (area.attachment_uuid === replaceSchemaItem.attachment_uuid) { if (area.attachment_uuid === replaceSchemaItem.attachment_uuid) {
@@ -993,7 +1112,13 @@ export default {
this.save() this.save()
}, },
maybeShowEmptyTemplateAlert (e) { maybeShowErrorTemplateAlert (e) {
if (!this.isAllRequiredFieldsAdded) {
e.preventDefault()
return alert(this.t('add_all_required_fields_to_continue'))
}
if (!this.template.fields.length) { if (!this.template.fields.length) {
e.preventDefault() e.preventDefault()
@@ -1001,6 +1126,10 @@ export default {
} }
}, },
onSaveClick () { onSaveClick () {
if (!this.isAllRequiredFieldsAdded) {
return alert(this.t('add_all_required_fields_to_continue'))
}
if (this.template.fields.length) { if (this.template.fields.length) {
this.isSaving = true this.isSaving = true
@@ -1023,10 +1152,18 @@ export default {
baseFetch (path, options = {}) { baseFetch (path, options = {}) {
return fetch(this.baseUrl + path, { return fetch(this.baseUrl + path, {
...options, ...options,
headers: { ...this.fetchOptions.headers, ...options.headers } headers: {
'X-CSRF-Token': this.authenticityToken,
...this.fetchOptions.headers,
...options.headers
}
}) })
}, },
save ({ force } = { force: false }) { save ({ force } = { force: false }) {
if (this.onChange) {
this.onChange(this.template)
}
if (!this.autosave && !force) { if (!this.autosave && !force) {
return Promise.resolve({}) return Promise.resolve({})
} }
@@ -1039,7 +1176,7 @@ export default {
this.pushUndo() this.pushUndo()
return this.baseFetch(`/api/templates/${this.template.id}`, { return this.baseFetch(`/templates/${this.template.id}`, {
method: 'PUT', method: 'PUT',
body: JSON.stringify({ body: JSON.stringify({
template: { template: {
@@ -14,7 +14,10 @@
@focus="$emit('focus', $event)" @focus="$emit('focus', $event)"
@blur="onBlur" @blur="onBlur"
> >
{{ value }} <template
v-for="(item, index) in (value || '').split(/(_)/)"
:key="index"
>{{ item }}<wbr></template>
</span> </span>
<span <span
v-if="withRequired" v-if="withRequired"
+5 -10
View File
@@ -8,8 +8,8 @@
/> />
<ReplaceButton <ReplaceButton
v-if="withReplaceButton" v-if="withReplaceButton"
:is-direct-upload="isDirectUpload"
:template-id="template.id" :template-id="template.id"
:accept-file-types="acceptFileTypes"
@click.stop @click.stop
@success="$emit('replace', { replaceSchemaItem: item, ...$event })" @success="$emit('replace', { replaceSchemaItem: item, ...$event })"
/> />
@@ -63,10 +63,10 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
isDirectUpload: { acceptFileTypes: {
type: Boolean, type: String,
required: true, required: false,
default: false default: 'image/*, application/pdf'
}, },
withReplaceButton: { withReplaceButton: {
type: Boolean, type: Boolean,
@@ -80,11 +80,6 @@ export default {
} }
}, },
emits: ['change', 'remove', 'up', 'down', 'replace'], emits: ['change', 'remove', 'up', 'down', 'replace'],
mounted () {
if (this.isDirectUpload) {
import('@rails/activestorage')
}
},
methods: { methods: {
upload: Upload.methods.upload, upload: Upload.methods.upload,
onUpdateName (value) { onUpdateName (value) {
@@ -34,6 +34,7 @@
v-model="description" v-model="description"
dir="auto" dir="auto"
class="base-textarea !text-base w-full" class="base-textarea !text-base w-full"
:readonly="!editable"
@input="resizeTextarea" @input="resizeTextarea"
/> />
</div> </div>
@@ -49,6 +50,7 @@
id="title_field" id="title_field"
v-model="title" v-model="title"
dir="auto" dir="auto"
:readonly="!editable"
class="base-input !text-base w-full" class="base-input !text-base w-full"
> >
</div> </div>
@@ -73,6 +75,11 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
editable: {
type: Boolean,
required: false,
default: true
},
buildDefaultName: { buildDefaultName: {
type: Function, type: Function,
required: true required: true
@@ -12,6 +12,7 @@
:default-fields="defaultFields" :default-fields="defaultFields"
:default-submitters="defaultSubmitters" :default-submitters="defaultSubmitters"
:draw-field="drawField" :draw-field="drawField"
:draw-field-type="drawFieldType"
:selected-submitter="selectedSubmitter" :selected-submitter="selectedSubmitter"
:image="image" :image="image"
@drop-field="$emit('drop-field', {...$event, attachment_uuid: document.uuid })" @drop-field="$emit('drop-field', {...$event, attachment_uuid: document.uuid })"
@@ -43,6 +44,11 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
drawFieldType: {
type: String,
required: false,
default: ''
},
defaultSubmitters: { defaultSubmitters: {
type: Array, type: Array,
required: false, required: false,
+1 -11
View File
@@ -5,7 +5,7 @@
@drop.prevent="onDropFiles" @drop.prevent="onDropFiles"
> >
<label <label
class="w-full relative bg-base-200 hover:bg-base-200/70 rounded-md border border-base-content border-dashed" class="w-full relative hover:bg-base-200/30 rounded-md border border-2 border-base-content/10 border-dashed"
:for="inputId" :for="inputId"
:class="{ 'opacity-50': isLoading || isProcessing }" :class="{ 'opacity-50': isLoading || isProcessing }"
> >
@@ -71,11 +71,6 @@ export default {
type: String, type: String,
required: false, required: false,
default: 'image/*, application/pdf' default: 'image/*, application/pdf'
},
isDirectUpload: {
type: Boolean,
required: true,
default: false
} }
}, },
emits: ['success'], emits: ['success'],
@@ -101,11 +96,6 @@ export default {
} }
} }
}, },
mounted () {
if (this.isDirectUpload) {
import('@rails/activestorage')
}
},
methods: { methods: {
upload: Upload.methods.upload, upload: Upload.methods.upload,
onDropFiles (e) { onDropFiles (e) {
+15 -2
View File
@@ -21,7 +21,7 @@
/> />
<Contenteditable <Contenteditable
ref="name" ref="name"
:model-value="field.name || defaultName" :model-value="(defaultField ? (field.title || field.name) : field.name) || defaultName"
:editable="editable && !defaultField" :editable="editable && !defaultField"
:icon-inline="true" :icon-inline="true"
:icon-width="18" :icon-width="18"
@@ -235,6 +235,7 @@
<input <input
v-model="field.required" v-model="field.required"
type="checkbox" type="checkbox"
:disabled="!editable || defaultField"
class="toggle toggle-xs" class="toggle toggle-xs"
@update:model-value="save" @update:model-value="save"
> >
@@ -481,6 +482,7 @@
> >
<FormulaModal <FormulaModal
:field="field" :field="field"
:editable="editable && !defaultField"
:build-default-name="buildDefaultName" :build-default-name="buildDefaultName"
@close="isShowFormulaModal = false" @close="isShowFormulaModal = false"
/> />
@@ -501,6 +503,7 @@
> >
<DescriptionModal <DescriptionModal
:field="field" :field="field"
:editable="editable && !defaultField"
:build-default-name="buildDefaultName" :build-default-name="buildDefaultName"
@close="isShowDescriptionModal = false" @close="isShowDescriptionModal = false"
/> />
@@ -582,7 +585,7 @@ export default {
return this.$el.getRootNode().querySelector('#docuseal_modal_container') return this.$el.getRootNode().querySelector('#docuseal_modal_container')
}, },
dateFormats () { dateFormats () {
return [ const formats = [
'MM/DD/YYYY', 'MM/DD/YYYY',
'DD/MM/YYYY', 'DD/MM/YYYY',
'YYYY-MM-DD', 'YYYY-MM-DD',
@@ -593,6 +596,16 @@ export default {
'D MMM YYYY', 'D MMM YYYY',
'D MMMM YYYY' 'D MMMM YYYY'
] ]
if (Intl.DateTimeFormat().resolvedOptions().timeZone?.includes('Seoul') || navigator.language?.startsWith('ko')) {
formats.push('YYYY년 MM월 DD일')
}
if (this.field.preferences?.format && !formats.includes(this.field.preferences.format)) {
formats.unshift(this.field.preferences.format)
}
return formats
}, },
defaultName () { defaultName () {
return this.buildDefaultName(this.field, this.template.fields) return this.buildDefaultName(this.field, this.template.fields)
@@ -95,7 +95,7 @@
<label <label
v-if="compact" v-if="compact"
tabindex="0" tabindex="0"
:title="selectedSubmitter.name" :title="selectedSubmitter?.name"
class="cursor-pointer text-base-100 flex h-full items-center justify-center" class="cursor-pointer text-base-100 flex h-full items-center justify-center"
> >
<button <button
+94 -59
View File
@@ -36,32 +36,70 @@
</div> </div>
<div v-if="submitterDefaultFields.length"> <div v-if="submitterDefaultFields.length">
<hr class="mb-2"> <hr class="mb-2">
<template <template v-if="isShowFieldSearch">
v-for="field in submitterDefaultFields" <input
:key="field.name" v-model="defaultFieldsSearch"
:placeholder="t('search_field')"
class="input input-ghost input-xs px-0 text-base mb-2 !outline-0 !rounded bg-transparent w-full"
>
<hr class="mb-2">
</template>
<div
class="overflow-auto relative"
:style="{
maxHeight: isShowFieldSearch ? '210px' : '',
minHeight: isShowFieldSearch ? '210px' : ''
}"
> >
<div <div
:style="{ backgroundColor: backgroundColor }" v-if="!filteredSubmitterDefaultFields.length && defaultFieldsSearch"
draggable="true" class="top-0 bottom-0 text-center absolute flex items-center justify-center w-full flex-col"
class="default-field border border-base-300 rounded rounded-tr-none relative group mb-2"
@dragstart="onDragstart({ type: 'text', ...field })"
@dragend="$emit('drag-end')"
> >
<div class="flex items-center justify-between relative cursor-grab"> <div>
<div class="flex items-center p-1 space-x-1"> {{ t('field_not_found') }}
<IconDrag /> </div>
<FieldType <a
:model-value="field.type || 'text'" href="#"
:editable="false" class="link"
:button-width="20" @click.prevent="defaultFieldsSearch = ''"
/> >
<span class="block pl-0.5"> {{ t('clear') }}
{{ field.name }} </a>
</div>
<template
v-for="field in filteredSubmitterDefaultFields"
:key="field.name"
>
<div
:style="{ backgroundColor: backgroundColor }"
draggable="true"
class="default-field border border-base-300 rounded rounded-tr-none relative group mb-2"
@dragstart="onDragstart({ type: 'text', ...field })"
@dragend="$emit('drag-end')"
>
<div class="flex items-center justify-between relative cursor-grab">
<div class="flex items-center p-1 space-x-1">
<IconDrag />
<FieldType
:model-value="field.type || 'text'"
:editable="false"
:button-width="20"
/>
<span class="block pl-0.5">
{{ field.title || field.name }}
</span>
</div>
<span
v-if="defaultRequiredFields.includes(field)"
:data-tip="t('required')"
class="text-red-400 text-3xl pr-1.5 tooltip tooltip-left h-8"
>
*
</span> </span>
</div> </div>
</div> </div>
</div> </template>
</template> </div>
</div> </div>
<div <div
v-if="editable && !onlyDefinedFields" v-if="editable && !onlyDefinedFields"
@@ -74,14 +112,18 @@
<button <button
v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')" v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')"
draggable="true" draggable="true"
class="field-type-button group flex items-center justify-center border border-dashed border-base-300 hover:border-base-content/20 w-full rounded relative" class="field-type-button group flex items-center justify-center border border-dashed w-full rounded relative"
:style="{ backgroundColor: backgroundColor }" :style="{ backgroundColor: backgroundColor }"
:class="drawFieldType === type ? 'border-base-content/40' : 'border-base-300 hover:border-base-content/20'"
@dragstart="onDragstart({ type: type })" @dragstart="onDragstart({ type: type })"
@dragend="$emit('drag-end')" @dragend="$emit('drag-end')"
@click="addField(type)" @click="['file', 'payment'].includes(type) ? $emit('add-field', type) : $emit('set-draw-type', type)"
> >
<div class="flex items-console group-hover:bg-base-200/50 transition-all cursor-grab h-full absolute left-0"> <div
<IconDrag class=" my-auto" /> class="flex items-console transition-all cursor-grab h-full absolute left-0"
:class="drawFieldType === type ? 'bg-base-200/50' : 'group-hover:bg-base-200/50'"
>
<IconDrag class="my-auto" />
</div> </div>
<div class="flex items-center flex-col px-2 py-2"> <div class="flex items-center flex-col px-2 py-2">
<component :is="icon" /> <component :is="icon" />
@@ -139,10 +181,9 @@
<script> <script>
import Field from './field' import Field from './field'
import { v4 } from 'uuid'
import FieldType from './field_type' import FieldType from './field_type'
import FieldSubmitter from './field_submitter' import FieldSubmitter from './field_submitter'
import { IconLock } from '@tabler/icons-vue' import { IconLock, IconCirclePlus } from '@tabler/icons-vue'
import IconDrag from './icon_drag' import IconDrag from './icon_drag'
export default { export default {
@@ -150,6 +191,7 @@ export default {
components: { components: {
Field, Field,
FieldType, FieldType,
IconCirclePlus,
FieldSubmitter, FieldSubmitter,
IconDrag, IconDrag,
IconLock IconLock
@@ -175,11 +217,21 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
defaultRequiredFields: {
type: Array,
required: false,
default: () => []
},
onlyDefinedFields: { onlyDefinedFields: {
type: Boolean, type: Boolean,
required: false, required: false,
default: true default: true
}, },
drawFieldType: {
type: String,
required: false,
default: ''
},
defaultSubmitters: { defaultSubmitters: {
type: Array, type: Array,
required: false, required: false,
@@ -204,10 +256,18 @@ export default {
required: true required: true
} }
}, },
emits: ['set-draw', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter'], emits: ['add-field', 'set-draw', 'set-draw-type', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter'],
data () {
return {
defaultFieldsSearch: ''
}
},
computed: { computed: {
fieldNames: FieldType.computed.fieldNames, fieldNames: FieldType.computed.fieldNames,
fieldIcons: FieldType.computed.fieldIcons, fieldIcons: FieldType.computed.fieldIcons,
isShowFieldSearch () {
return this.submitterDefaultFields.length > 15
},
fieldIconsSorted () { fieldIconsSorted () {
if (this.fieldTypes.length) { if (this.fieldTypes.length) {
return this.fieldTypes.reduce((acc, type) => { return this.fieldTypes.reduce((acc, type) => {
@@ -226,6 +286,13 @@ export default {
return this.defaultFields.filter((f) => { return this.defaultFields.filter((f) => {
return !this.submitterFields.find((field) => field.name === f.name) && (!f.role || f.role === this.selectedSubmitter.name) return !this.submitterFields.find((field) => field.name === f.name) && (!f.role || f.role === this.selectedSubmitter.name)
}) })
},
filteredSubmitterDefaultFields () {
if (this.defaultFieldsSearch) {
return this.submitterDefaultFields.filter((f) => f.name.toLowerCase().includes(this.defaultFieldsSearch.toLowerCase()))
} else {
return this.submitterDefaultFields
}
} }
}, },
methods: { methods: {
@@ -286,38 +353,6 @@ export default {
}) })
}) })
this.save()
},
addField (type, area = null) {
const field = {
name: '',
uuid: v4(),
required: type !== 'checkbox',
areas: [],
submitter_uuid: this.selectedSubmitter.uuid,
type
}
if (['select', 'multiple', 'radio'].includes(type)) {
field.options = [{ value: '', uuid: v4() }]
}
if (type === 'stamp') {
field.readonly = true
}
if (type === 'date') {
field.preferences = {
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
}
}
this.fields.push(field)
if (!['payment', 'file'].includes(type)) {
this.$emit('set-draw', { field })
}
this.save() this.save()
} }
} }
@@ -49,6 +49,7 @@
ref="textarea" ref="textarea"
v-model="formula" v-model="formula"
class="base-textarea !rounded-xl !text-base font-mono w-full !outline-0 !ring-0 !px-3" class="base-textarea !rounded-xl !text-base font-mono w-full !outline-0 !ring-0 !px-3"
:readonly="!editable"
required="true" required="true"
@input="resizeTextarea" @input="resizeTextarea"
/> />
@@ -134,6 +135,11 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
editable: {
type: Boolean,
required: false,
default: true
},
buildDefaultName: { buildDefaultName: {
type: Function, type: Function,
required: true required: true
+4
View File
@@ -1,5 +1,9 @@
const en = { const en = {
search_field: 'Search field',
field_not_found: 'Field not found',
clear: 'Clear',
align: 'Align', align: 'Align',
add_all_required_fields_to_continue: 'Add all required fields to continue',
left: 'Left', left: 'Left',
right: 'Right', right: 'Right',
center: 'Center', center: 'Center',
@@ -0,0 +1,412 @@
<template>
<div>
<div v-if="selectedSheetIndex === null && spreadsheet">
<form @submit.prevent="[selectedSheetIndex = $refs.selectWorksheet.value, buildDefaultMappings()]">
<label class="label">
Select Worksheet
</label>
<select
ref="selectWorksheet"
class="base-select"
>
<option
v-for="(sheet, index) in spreadsheet"
:key="index"
:value="index"
>
{{ sheet[0] || index }}
</option>
</select>
<button class="base-button mt-4 w-full">
Open
</button>
</form>
</div>
<div v-else-if="selectedSheetIndex !== null">
<div
v-for="submitter in submitters"
:key="submitter.uuid"
class="mb-4"
>
<div
v-if="submitters.length > 1"
class="px-3 border-y py-2 border-base-300 text-center w-full"
>
{{ submitter.name }}
</div>
<div class="flex">
<div class="relative w-full py-2 px-2 text-sm">
Recipient field
</div>
<div class="relative w-full py-2 pl-4 text-sm">
Spreadsheet column
</div>
</div>
<div
v-for="mapping in mappings.filter((m) => m.submitter_uuid === submitter.uuid)"
:key="mapping.uuid"
class="mb-2"
>
<div class="flex">
<select
class="base-select !select-sm !h-10"
required
@change="mapping.field_name = $event.target.value"
>
<option
disabled
value=""
:selected="!mapping.field_name"
>
Select Field
</option>
<option
v-for="(field, index) in selectFieldsForSubmitter(submitter)"
:key="index"
:value="field.name"
:selected="mapping.field_name === field.name"
>
{{ field.name }}
</option>
</select>
<div class="flex items-center px-1">
<IconArrowsHorizontal style="width: 19px; height: 19px" />
</div>
<div class="w-full relative">
<select
class="base-select !select-sm !h-10"
required
@change="mapping.column_index = parseInt($event.target.value)"
>
<option
disabled
value=""
:selected="mapping.column_index == null"
>
Select Column
</option>
<template
v-for="(column, index) in columns"
:key="index"
>
<option
v-if="column"
:value="index"
:selected="index === mapping.column_index"
>
{{ column }}
</option>
</template>
</select>
<div
v-if="mapping.column_index != null"
class="absolute top-0 bottom-0 right-1 flex items-center"
>
<span
class="tooltip tooltip-bottom-end pr-1 tooltip-pre"
style="padding-top: 2px"
:data-tip="[0, 1, 2].map((i) => rows[i]?.[mapping.column_index] ?? '---').join('\n')"
>
<button
class="btn btn-xs btn-circle bg-white border-0 border-gray-300"
@click.prevent
>
<IconInfoCircle class="h-4 w-4" />
</button>
</span>
</div>
</div>
<div class="flex items-center pl-1">
<span
class="tooltip tooltip-top"
data-tip="Remove"
>
<button
:disabled="mappings.filter((m) => m.submitter_uuid === submitter.uuid).length < 2"
class="btn btn-xs btn-circle"
@click.prevent="mappings.splice(mappings.indexOf(mapping), 1)"
>
<IconX class="h-3.5 w-3.5" />
</button>
</span>
</div>
</div>
</div>
<div>
<button
class="btn btn-sm btn-primary w-full !normal-case font-medium"
@click.prevent="addMapping(submitter)"
>
<IconPlus class="w-4 h-4" />
New Field Mapping
</button>
</div>
</div>
<div>
<input
name="submissions_json"
hidden
:value="JSON.stringify(submissionsData.slice(0, 1100))"
>
</div>
<div
class="px-3 border-y py-2 border-base-300 text-center w-full text-sm font-semibold"
>
Total entries: {{ submissionsData.length }}
<template v-if="submissionsData.length >= 1000">
/ 1000
</template>
</div>
</div>
<div
v-else
>
<div
class="flex h-52 w-full"
@dragover.prevent
@drop.prevent="onDropFiles"
>
<label
class="w-full relative bg-base-200/20 hover:bg-base-200/30 rounded-md border border-2 border-base-content/10 border-dashed"
for="import_list_file"
:class="{ 'opacity-50': isLoading }"
>
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
<div class="flex flex-col items-center">
<IconInnerShadowTop
v-if="isLoading"
class="animate-spin"
:width="40"
:height="40"
/>
<IconCloudUpload
v-else
:width="40"
:height="40"
/>
<div
class="font-medium text-lg mb-1"
>
Upload CSV or XLSX Spreadsheet
</div>
<div class="text-sm">
<span class="font-medium">Click to Upload</span> or drag and drop files.
</div>
</div>
</div>
<form
ref="form"
class="hidden"
>
<input
id="import_list_file"
ref="input"
type="file"
name="file"
accept=".xlsx, .xls, .csv"
@change="onSelectFile"
>
</form>
</label>
</div>
<div class="text-center mt-2">
Or <a
:download="`${template.name}.csv`"
:href="`data:text/csv;base64,${csvBase64}`"
class="link font-medium"
>download</a> a spreadsheet to fill and import
</div>
</div>
</div>
</template>
<script>
import { IconCloudUpload, IconX, IconPlus, IconArrowsHorizontal, IconInfoCircle, IconInnerShadowTop } from '@tabler/icons-vue'
import { v4 } from 'uuid'
export default {
name: 'FileDropzone',
components: {
IconCloudUpload,
IconX,
IconArrowsHorizontal,
IconPlus,
IconInfoCircle,
IconInnerShadowTop
},
props: {
template: {
type: Object,
required: true
},
authenticityToken: {
type: String,
required: false,
default: ''
}
},
data () {
return {
isLoading: false,
spreadsheet: null,
selectedSheetIndex: null,
mappings: []
}
},
computed: {
table () {
return this.spreadsheet[this.selectedSheetIndex][1]
},
submissionsData () {
const submissions = []
this.rows.forEach((row) => {
const submittersIndex = {}
this.mappings.forEach((mapping) => {
if (mapping.field_name && mapping.column_index != null) {
submittersIndex[mapping.submitter_uuid] ||= { uuid: mapping.submitter_uuid, fields: [] }
if (['name', 'email', 'phone'].includes(mapping.field_name.toLowerCase())) {
submittersIndex[mapping.submitter_uuid][mapping.field_name.toLowerCase()] = row[mapping.column_index]
} else {
submittersIndex[mapping.submitter_uuid].fields.push({
name: mapping.field_name, default_value: row[mapping.column_index], readonly: true
})
}
}
})
if (Object.keys(submittersIndex).length !== 0) {
submissions.push({ submitters: Object.values(submittersIndex) })
}
})
return submissions
},
csvBase64 () {
const rows = []
this.submitters.forEach((submitter) => {
this.selectFieldsForSubmitter(submitter).forEach((field) => {
rows.push(this.submitters.length > 1 ? `${submitter.name} - ${field.name}` : field.name)
})
})
const csv = rows.map(item => {
if (/[",\n]/.test(item)) {
return `"${item.replace(/"/g, '""')}"`
} else {
return item
}
}).join(',')
return window.btoa(window.unescape(window.encodeURIComponent(csv + '\n' + rows.map(() => '').join(',') + '\n')))
},
submitters () {
return this.template.submitters
},
columns () {
return this.table[0]
},
form () {
return this.$el.closest('form')
},
fieldTypes () {
return ['text', 'cells', 'date', 'number', 'radio', 'select', 'checkbox']
},
defaultFields () {
return [
{ name: 'Name' },
{ name: 'Email' },
{ name: 'Phone' }
]
},
rows () {
return this.table.slice(1)
}
},
watch: {
selectedSheetIndex (value) {
if (value !== null) {
document.getElementById('list_form_buttons')?.classList?.remove('hidden')
}
}
},
methods: {
onDropFiles (e) {
this.uploadFile(e.dataTransfer.files[0])
},
onSelectFile (e) {
this.uploadFile(e.target.files[0])
},
addMapping (submitter) {
this.mappings.push({ uuid: v4(), field_name: '', column_index: null, submitter_uuid: submitter.uuid })
},
selectFieldsForSubmitter (submitter) {
const templateFields = this.template.fields.filter((field) => {
return field.submitter_uuid === submitter.uuid &&
field.name &&
this.fieldTypes.includes(field.type) &&
this.defaultFields.every((f) => field.name?.toLowerCase() !== f.name?.toLowerCase())
})
return [...this.defaultFields, ...templateFields]
},
buildDefaultMappings () {
this.submitters.forEach((submitter) => {
const fields = this.selectFieldsForSubmitter(submitter)
fields.forEach((field) => {
const columnIndex = this.columns.findIndex((column, index) => {
return column &&
column.toString().toLowerCase().includes(field.name?.toLowerCase()) &&
this.mappings.every((m) => m.column_index !== index)
})
if (columnIndex !== -1) {
this.mappings.push({ uuid: v4(), field_name: field.name, column_index: columnIndex, submitter_uuid: submitter.uuid })
}
})
if (!this.mappings.some((m) => m.field_name.toLowerCase() === 'name' && m.submitter_uuid === submitter.uuid)) {
this.mappings.unshift({ uuid: v4(), field_name: 'Name', submitter_uuid: submitter.uuid })
}
if (!this.mappings.some((m) => m.field_name.toLowerCase() === 'email' && m.submitter_uuid === submitter.uuid)) {
this.mappings.unshift({ uuid: v4(), field_name: 'Email', submitter_uuid: submitter.uuid })
}
})
},
uploadFile (file) {
this.isLoading = true
const formData = new FormData()
formData.append('file', file)
return fetch('/upload_spreadsheet', {
method: 'POST',
body: formData,
headers: {
'X-CSRF-Token': this.authenticityToken
}
}).then(resp => resp.json()).then((data) => {
if (data.error) {
return alert(data.error)
}
this.spreadsheet = data
if (data.length === 1) {
this.selectedSheetIndex = 0
this.buildDefaultMappings()
}
}).finally(() => {
this.isLoading = false
})
}
}
}
</script>
@@ -38,7 +38,14 @@
:stroke-width="1.6" :stroke-width="1.6"
:width="20" :width="20"
/> />
{{ field.name }} {{ field.title || field.name }}
<span
v-if="defaultRequiredFields.includes(field)"
:data-tip="t('required')"
class="text-red-400 text-2xl tooltip tooltip-left h-6"
>
*
</span>
</a> </a>
</li> </li>
</template> </template>
@@ -99,6 +106,11 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
defaultRequiredFields: {
type: Array,
required: false,
default: () => []
},
defaultFields: { defaultFields: {
type: Array, type: Array,
required: false, required: false,
+9 -2
View File
@@ -77,6 +77,11 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
drawFieldType: {
type: String,
required: false,
default: ''
},
allowDraw: { allowDraw: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -123,7 +128,9 @@ export default {
}, },
computed: { computed: {
defaultFieldType () { defaultFieldType () {
if (this.defaultDrawFieldType && this.defaultDrawFieldType !== 'text') { if (this.drawFieldType) {
return this.drawFieldType
} else if (this.defaultDrawFieldType && this.defaultDrawFieldType !== 'text') {
return this.defaultDrawFieldType return this.defaultDrawFieldType
} else if (this.fieldTypes.length !== 0 && !this.fieldTypes.includes('text')) { } else if (this.fieldTypes.length !== 0 && !this.fieldTypes.includes('text')) {
return this.fieldTypes[0] return this.fieldTypes[0]
@@ -212,7 +219,7 @@ export default {
this.newArea.y = e.offsetY / this.$refs.mask.clientHeight this.newArea.y = e.offsetY / this.$refs.mask.clientHeight
} }
if (this.drawField?.type === 'cells') { if ((this.drawField?.type || this.drawFieldType) === 'cells') {
this.newArea.cell_w = this.newArea.h * (this.$refs.mask.clientHeight / this.$refs.mask.clientWidth) this.newArea.cell_w = this.newArea.h * (this.$refs.mask.clientHeight / this.$refs.mask.clientWidth)
} }
@@ -167,7 +167,7 @@ export default {
IconInnerShadowTop, IconInnerShadowTop,
IconBrandStripe IconBrandStripe
}, },
inject: ['backgroundColor', 'save', 'currencies', 't'], inject: ['backgroundColor', 'save', 'currencies', 't', 'isPaymentConnected'],
props: { props: {
field: { field: {
type: Object, type: Object,
@@ -221,6 +221,8 @@ export default {
mounted () { mounted () {
this.field.preferences.currency ||= this.defaultCurrency this.field.preferences.currency ||= this.defaultCurrency
isConnected.value ||= this.isPaymentConnected
if (!this.isConnected) { if (!this.isConnected) {
this.checkStatus() this.checkStatus()
} }
@@ -20,7 +20,6 @@
<div class=""> <div class="">
<ReplaceButton <ReplaceButton
v-if="withReplaceButton" v-if="withReplaceButton"
:is-direct-upload="isDirectUpload"
:template-id="template.id" :template-id="template.id"
:accept-file-types="acceptFileTypes" :accept-file-types="acceptFileTypes"
class="opacity-0 group-hover:opacity-100" class="opacity-0 group-hover:opacity-100"
@@ -115,11 +114,6 @@ export default {
required: true, required: true,
default: true default: true
}, },
isDirectUpload: {
type: Boolean,
required: true,
default: false
},
withArrows: { withArrows: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -132,11 +126,6 @@ export default {
return [...this.document.preview_images].sort((a, b) => parseInt(a.filename) - parseInt(b.filename))[0] return [...this.document.preview_images].sort((a, b) => parseInt(a.filename) - parseInt(b.filename))[0]
} }
}, },
mounted () {
if (this.isDirectUpload) {
import('@rails/activestorage')
}
},
methods: { methods: {
upload: Upload.methods.upload, upload: Upload.methods.upload,
onUpdateName (value) { onUpdateName (value) {
@@ -36,11 +36,6 @@ export default {
type: String, type: String,
required: false, required: false,
default: 'image/*, application/pdf' default: 'image/*, application/pdf'
},
isDirectUpload: {
type: Boolean,
required: true,
default: false
} }
}, },
emits: ['success'], emits: ['success'],
@@ -64,11 +59,6 @@ export default {
} }
} }
}, },
mounted () {
if (this.isDirectUpload) {
import('@rails/activestorage')
}
},
methods: { methods: {
upload: Upload.methods.upload upload: Upload.methods.upload
} }
+27 -101
View File
@@ -60,11 +60,6 @@ export default {
type: String, type: String,
required: false, required: false,
default: 'image/*, application/pdf' default: 'image/*, application/pdf'
},
isDirectUpload: {
type: Boolean,
required: true,
default: false
} }
}, },
emits: ['success'], emits: ['success'],
@@ -79,112 +74,43 @@ export default {
return 'el' + Math.random().toString(32).split('.')[1] return 'el' + Math.random().toString(32).split('.')[1]
} }
}, },
mounted () {
if (this.isDirectUpload) {
import('@rails/activestorage')
}
},
methods: { methods: {
async upload () { async upload () {
this.isLoading = true this.isLoading = true
if (this.isDirectUpload) { this.baseFetch(`/templates/${this.templateId}/documents`, {
const { DirectUpload } = await import('@rails/activestorage') method: 'POST',
body: new FormData(this.$refs.form)
}).then((resp) => {
if (resp.ok) {
resp.json().then((data) => {
this.$emit('success', data)
this.$refs.input.value = ''
})
} else if (resp.status === 422) {
resp.json().then((data) => {
if (data.error === 'PDF encrypted') {
const formData = new FormData(this.$refs.form)
const blobs = await Promise.all( formData.append('password', prompt('Enter PDF password'))
Array.from(this.$refs.input.files).map(async (file) => {
const upload = new DirectUpload(
file,
'/direct_uploads',
this.$refs.input
)
return new Promise((resolve, reject) => { this.baseFetch(`/templates/${this.templateId}/documents`, {
upload.create((error, blob) => { method: 'POST',
if (error) { body: formData
console.error(error) }).then(async (resp) => {
if (resp.ok) {
return reject(error) this.$emit('success', await resp.json())
this.$refs.input.value = ''
} else { } else {
return resolve(blob) alert('Wrong password')
} }
}) })
}).catch((error) => { }
console.error(error)
})
}) })
).finally(() => { }
this.isLoading = false }).finally(() => {
}) this.isLoading = false
})
this.isProcessing = true
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
method: 'POST',
body: JSON.stringify({ blobs }),
headers: { 'Content-Type': 'application/json' }
}).then((resp) => {
if (resp.ok) {
resp.json().then((data) => {
this.$emit('success', data)
this.$refs.input.value = ''
})
} else if (resp.status === 422) {
resp.json().then((data) => {
if (data.error === 'PDF encrypted') {
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
method: 'POST',
body: JSON.stringify({ blobs, password: prompt('Enter PDF password') }),
headers: { 'Content-Type': 'application/json' }
}).then(async (resp) => {
if (resp.ok) {
this.$emit('success', await resp.json())
this.$refs.input.value = ''
} else {
alert('Wrong password')
}
})
}
})
}
}).finally(() => {
this.isProcessing = false
})
} else {
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
method: 'POST',
body: new FormData(this.$refs.form)
}).then((resp) => {
if (resp.ok) {
resp.json().then((data) => {
this.$emit('success', data)
this.$refs.input.value = ''
})
} else if (resp.status === 422) {
resp.json().then((data) => {
if (data.error === 'PDF encrypted') {
const formData = new FormData(this.$refs.form)
formData.append('password', prompt('Enter PDF password'))
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
method: 'POST',
body: formData
}).then(async (resp) => {
if (resp.ok) {
this.$emit('success', await resp.json())
this.$refs.input.value = ''
} else {
alert('Wrong password')
}
})
}
})
}
}).finally(() => {
this.isLoading = false
})
}
} }
} }
} }
+6 -4
View File
@@ -16,7 +16,7 @@ class ProcessSubmitterCompletionJob < ApplicationJob
enqueue_completed_emails(submitter) enqueue_completed_emails(submitter)
end end
return if Accounts.load_webhook_configs(submitter.account).blank? return if Accounts.load_webhook_url(submitter.account).blank?
SendFormCompletedWebhookRequestJob.perform_later(submitter) SendFormCompletedWebhookRequestJob.perform_later(submitter)
end end
@@ -27,14 +27,16 @@ class ProcessSubmitterCompletionJob < ApplicationJob
user = submission.created_by_user || submitter.template.author user = submission.created_by_user || submitter.template.author
if submitter.account.users.exists?(id: user.id) && submission.preferences['send_email'] != false if submitter.account.users.exists?(id: user.id) && submission.preferences['send_email'] != false
if submitter.submission.submitters.map(&:email).exclude?(user.email) && if submission.submitters.map(&:email).exclude?(user.email) &&
user.user_configs.find_by(key: UserConfig::RECEIVE_COMPLETED_EMAIL)&.value != false user.user_configs.find_by(key: UserConfig::RECEIVE_COMPLETED_EMAIL)&.value != false &&
user.role != 'integration'
SubmitterMailer.completed_email(submitter, user).deliver_later! SubmitterMailer.completed_email(submitter, user).deliver_later!
end end
bcc = submission.preferences['bcc_completed'].presence || bcc = submission.preferences['bcc_completed'].presence ||
submission.template.preferences['bcc_completed'].presence ||
submission.account.account_configs submission.account.account_configs
.find_by(key: AccountConfig::BCC_EMAILS)&.value.presence .find_by(key: AccountConfig::BCC_EMAILS)&.value
bcc.to_s.scan(User::EMAIL_REGEXP).each do |to| bcc.to_s.scan(User::EMAIL_REGEXP).each do |to|
SubmitterMailer.completed_email(submitter, user, to:).deliver_later! SubmitterMailer.completed_email(submitter, user, to:).deliver_later!
@@ -7,16 +7,20 @@ class SendFormCompletedWebhookRequestJob < ApplicationJob
def perform(submitter, params = {}) def perform(submitter, params = {})
attempt = params[:attempt].to_i attempt = params[:attempt].to_i
config = Accounts.load_webhook_configs(submitter.submission.account) url = Accounts.load_webhook_url(submitter.submission.account)
return if config.blank? || config.value.blank? return if url.blank?
preferences = Accounts.load_webhook_preferences(submitter.submission.account)
return if preferences['form.completed'] == false
Submissions::EnsureResultGenerated.call(submitter) Submissions::EnsureResultGenerated.call(submitter)
ActiveStorage::Current.url_options = Docuseal.default_url_options ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin resp = begin
Faraday.post(config.value, Faraday.post(url,
{ {
event_type: 'form.completed', event_type: 'form.completed',
timestamp: Time.current, timestamp: Time.current,
@@ -7,14 +7,18 @@ class SendFormStartedWebhookRequestJob < ApplicationJob
def perform(submitter, params = {}) def perform(submitter, params = {})
attempt = params[:attempt].to_i attempt = params[:attempt].to_i
config = Accounts.load_webhook_configs(submitter.submission.account) url = Accounts.load_webhook_url(submitter.submission.account)
return if config.blank? || config.value.blank? return if url.blank?
preferences = Accounts.load_webhook_preferences(submitter.submission.account)
return if preferences['form.started'] == false
ActiveStorage::Current.url_options = Docuseal.default_url_options ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin resp = begin
Faraday.post(config.value, Faraday.post(url,
{ {
event_type: 'form.started', event_type: 'form.started',
timestamp: Time.current, timestamp: Time.current,
@@ -7,14 +7,18 @@ class SendFormViewedWebhookRequestJob < ApplicationJob
def perform(submitter, params = {}) def perform(submitter, params = {})
attempt = params[:attempt].to_i attempt = params[:attempt].to_i
config = Accounts.load_webhook_configs(submitter.submission.account) url = Accounts.load_webhook_url(submitter.submission.account)
return if config.blank? || config.value.blank? return if url.blank?
preferences = Accounts.load_webhook_preferences(submitter.submission.account)
return if preferences['form.viewed'] == false
ActiveStorage::Current.url_options = Docuseal.default_url_options ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin resp = begin
Faraday.post(config.value, Faraday.post(url,
{ {
event_type: 'form.viewed', event_type: 'form.viewed',
timestamp: Time.current, timestamp: Time.current,
@@ -0,0 +1,40 @@
# frozen_string_literal: true
class SendSubmissionArchivedWebhookRequestJob < ApplicationJob
USER_AGENT = 'DocuSeal.co Webhook'
MAX_ATTEMPTS = 10
def perform(submission, params = {})
attempt = params[:attempt].to_i
url = Accounts.load_webhook_url(submission.account)
return if url.blank?
preferences = Accounts.load_webhook_preferences(submission.account)
return if preferences['submission.archived'].blank?
resp = begin
Faraday.post(url,
{
event_type: 'submission.archived',
timestamp: Time.current,
data: submission.as_json(only: %i[id archived_at])
}.to_json,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
nil
end
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
SendSubmissionArchivedWebhookRequestJob.set(wait: (2**attempt).minutes)
.perform_later(submitter, {
attempt: attempt + 1,
last_status: resp&.status.to_i
})
end
end
end
@@ -4,6 +4,12 @@ class SendSubmitterInvitationEmailJob < ApplicationJob
def perform(params = {}) def perform(params = {})
submitter = Submitter.find(params['submitter_id']) submitter = Submitter.find(params['submitter_id'])
if submitter.submission.source == 'invite' && !Accounts.can_send_emails?(submitter.account, on_events: true)
Rollbar.error("Skip email: #{submitter.id}") if defined?(Rollbar)
return
end
mail = SubmitterMailer.invitation_email(submitter) mail = SubmitterMailer.invitation_email(submitter)
Submitters::ValidateSending.call(submitter, mail) Submitters::ValidateSending.call(submitter, mail)
+1 -1
View File
@@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class ApplicationMailer < ActionMailer::Base class ApplicationMailer < ActionMailer::Base
default from: 'DocuSeal <hi@docuseal.co>' default from: 'DocuSeal <info@docuseal.co>'
layout 'mailer' layout 'mailer'
register_interceptor ActionMailerConfigsInterceptor register_interceptor ActionMailerConfigsInterceptor
+10 -6
View File
@@ -7,15 +7,15 @@ class SubmitterMailer < ApplicationMailer
DEFAULT_INVITATION_SUBJECT = 'You are invited to submit a form' DEFAULT_INVITATION_SUBJECT = 'You are invited to submit a form'
def invitation_email(submitter) def invitation_email(submitter)
@current_account = submitter.submission.template.account @current_account = submitter.submission.account
@submitter = submitter @submitter = submitter
if submitter.preferences['email_message_uuid'] if submitter.preferences['email_message_uuid']
@email_message = submitter.account.email_messages.find_by(uuid: submitter.preferences['email_message_uuid']) @email_message = submitter.account.email_messages.find_by(uuid: submitter.preferences['email_message_uuid'])
end end
@body = @email_message&.body.presence @body = @email_message&.body.presence || @submitter.template.preferences['request_email_body'].presence
@subject = @email_message&.subject.presence @subject = @email_message&.subject.presence || @submitter.template.preferences['request_email_subject'].presence
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) @email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY)
@@ -30,12 +30,13 @@ class SubmitterMailer < ApplicationMailer
to: @submitter.friendly_name, to: @submitter.friendly_name,
from: from_address_for_submitter(submitter), from: from_address_for_submitter(submitter),
subject:, subject:,
reply_to: (submitter.submission.created_by_user || submitter.template.author)&.friendly_name&.sub(/\+\w+@/, '@') reply_to: submitter.preferences['reply_to'].presence ||
(submitter.submission.created_by_user || submitter.template.author)&.friendly_name&.sub(/\+\w+@/, '@')
) )
end end
def completed_email(submitter, user, to: nil) def completed_email(submitter, user, to: nil)
@current_account = submitter.submission.template.account @current_account = submitter.submission.account
@submitter = submitter @submitter = submitter
@submission = submitter.submission @submission = submitter.submission
@user = user @user = user
@@ -63,7 +64,7 @@ class SubmitterMailer < ApplicationMailer
end end
def documents_copy_email(submitter, to: nil, sig: false) def documents_copy_email(submitter, to: nil, sig: false)
@current_account = submitter.submission.template.account @current_account = submitter.submission.account
@submitter = submitter @submitter = submitter
@sig = submitter.signed_id(expires_in: SIGN_TTL, purpose: :download_completed) if sig @sig = submitter.signed_id(expires_in: SIGN_TTL, purpose: :download_completed) if sig
@@ -84,6 +85,9 @@ class SubmitterMailer < ApplicationMailer
mail(from: from_address_for_submitter(submitter), mail(from: from_address_for_submitter(submitter),
to: to || @submitter.friendly_name, to: to || @submitter.friendly_name,
reply_to: @submitter.preferences['reply_to'].presence ||
(@submitter.submission.created_by_user ||
@submitter.template.author)&.friendly_name&.sub(/\+\w+@/, '@'),
subject:) subject:)
end end
+7
View File
@@ -8,10 +8,17 @@
# locale :string not null # locale :string not null
# name :string not null # name :string not null
# timezone :string not null # timezone :string not null
# uuid :string not null
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# #
# Indexes
#
# index_accounts_on_uuid (uuid) UNIQUE
#
class Account < ApplicationRecord class Account < ApplicationRecord
attribute :uuid, :string, default: -> { SecureRandom.uuid }
has_many :users, dependent: :destroy has_many :users, dependent: :destroy
has_many :encrypted_configs, dependent: :destroy has_many :encrypted_configs, dependent: :destroy
has_many :account_configs, dependent: :destroy has_many :account_configs, dependent: :destroy
+5 -1
View File
@@ -28,10 +28,14 @@ class AccountConfig < ApplicationRecord
FORCE_MFA = 'force_mfa' FORCE_MFA = 'force_mfa'
ALLOW_TYPED_SIGNATURE = 'allow_typed_signature' ALLOW_TYPED_SIGNATURE = 'allow_typed_signature'
ALLOW_TO_RESUBMIT = 'allow_to_resubmit' ALLOW_TO_RESUBMIT = 'allow_to_resubmit'
SUBMITTER_REMAILERS = 'submitter_reminders' SUBMITTER_REMINDERS = 'submitter_reminders'
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button' FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
FORM_COMPLETED_MESSAGE_KEY = 'form_completed_message'
FORM_WITH_CONFETTI_KEY = 'form_with_confetti' FORM_WITH_CONFETTI_KEY = 'form_with_confetti'
ESIGNING_PREFERENCE_KEY = 'esigning_preference' ESIGNING_PREFERENCE_KEY = 'esigning_preference'
WEBHOOK_PREFERENCES_KEY = 'webhook_preferences'
DOWNLOAD_LINKS_AUTH_KEY = 'download_links_auth'
FORCE_SSO_AUTH_KEY = 'force_sso_auth'
DEFAULT_VALUES = { DEFAULT_VALUES = {
SUBMITTER_INVITATION_EMAIL_KEY => { SUBMITTER_INVITATION_EMAIL_KEY => {
+1
View File
@@ -63,6 +63,7 @@ class Submission < ApplicationRecord
enum :source, { enum :source, {
invite: 'invite', invite: 'invite',
bulk: 'bulk',
api: 'api', api: 'api',
embed: 'embed', embed: 'embed',
link: 'link' link: 'link'
+2
View File
@@ -53,6 +53,8 @@ class Submitter < ApplicationRecord
has_many :document_generation_events, dependent: :destroy has_many :document_generation_events, dependent: :destroy
has_many :submission_events, dependent: :destroy has_many :submission_events, dependent: :destroy
scope :completed, -> { where.not(completed_at: nil) }
def status def status
if completed_at? if completed_at?
'completed' 'completed'
+5
View File
@@ -8,6 +8,7 @@
# archived_at :datetime # archived_at :datetime
# fields :text not null # fields :text not null
# name :string not null # name :string not null
# preferences :text not null
# schema :text not null # schema :text not null
# slug :string not null # slug :string not null
# source :text not null # source :text not null
@@ -41,12 +42,14 @@ class Template < ApplicationRecord
before_validation :maybe_set_default_folder, on: :create before_validation :maybe_set_default_folder, on: :create
attribute :preferences, :string, default: -> { {} }
attribute :fields, :string, default: -> { [] } attribute :fields, :string, default: -> { [] }
attribute :schema, :string, default: -> { [] } attribute :schema, :string, default: -> { [] }
attribute :submitters, :string, default: -> { [{ name: DEFAULT_SUBMITTER_NAME, uuid: SecureRandom.uuid }] } attribute :submitters, :string, default: -> { [{ name: DEFAULT_SUBMITTER_NAME, uuid: SecureRandom.uuid }] }
attribute :slug, :string, default: -> { SecureRandom.base58(14) } attribute :slug, :string, default: -> { SecureRandom.base58(14) }
attribute :source, :string, default: 'native' attribute :source, :string, default: 'native'
serialize :preferences, coder: JSON
serialize :fields, coder: JSON serialize :fields, coder: JSON
serialize :schema, coder: JSON serialize :schema, coder: JSON
serialize :submitters, coder: JSON serialize :submitters, coder: JSON
@@ -62,6 +65,8 @@ class Template < ApplicationRecord
scope :active, -> { where(archived_at: nil) } scope :active, -> { where(archived_at: nil) }
scope :archived, -> { where.not(archived_at: nil) } scope :archived, -> { where.not(archived_at: nil) }
delegate :name, to: :folder, prefix: true
def application_key def application_key
external_id external_id
end end
+1
View File
@@ -65,6 +65,7 @@ class User < ApplicationRecord
attribute :uuid, :string, default: -> { SecureRandom.uuid } attribute :uuid, :string, default: -> { SecureRandom.uuid }
scope :active, -> { where(archived_at: nil) } scope :active, -> { where(archived_at: nil) }
scope :archived, -> { where.not(archived_at: nil) }
scope :admins, -> { where(role: ADMIN_ROLE) } scope :admins, -> { where(role: ADMIN_ROLE) }
def access_token def access_token
+12
View File
@@ -75,6 +75,18 @@
</div> </div>
<% end %> <% end %>
<% end %> <% end %>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY) %>
<% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %>
<div class="flex items-center justify-between py-2.5">
<span>
Require authentication for file download links
</span>
<%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()' %>
</div>
<% end %>
<% end %>
</div> </div>
<% end %> <% end %>
<% if can?(:manage, current_account) && Docuseal.multitenant? && true_user == current_user %> <% if can?(:manage, current_account) && Docuseal.multitenant? && true_user == current_user %>
+12
View File
@@ -0,0 +1,12 @@
<form action="<%= root_path %>" method="get" class="bg-base-200 px-1.5 rounded-xl py-1">
<toggle-cookies data-value="templates" data-key="dashboard_view" class="sm:tooltip tooltip-top" data-tip="Templates">
<button class="<%= local_assigns[:selected] == 'submissions' ? 'btn !border !rounded-lg btn-square !p-0 !btn-sm !h-8 !w-9' : 'btn btn-neutral !rounded-lg btn-square !p-0 hover:text-neutral-300 !btn-sm !h-8 !w-9 disabled:btn-neutral' %>">
<%= svg_icon('layout_grid', class: 'w-6 h-6 stroke-2') %>
</button>
</toggle-cookies>
<toggle-cookies data-value="submissions" data-key="dashboard_view" class="sm:tooltip tooltip-top" data-tip="Submissions">
<button class="<%= local_assigns[:selected] == 'submissions' ? 'btn btn-neutral !rounded-lg btn-square !p-0 hover:text-neutral-300 !btn-sm !h-8 !w-9' : 'btn !border !rounded-lg btn-square !p-0 !btn-sm !h-8 !w-9 disabled:btn-neutral' %>">
<%= svg_icon('layout_list', class: 'w-6 h-6 stroke-2') %>
</button>
</toggle-cookies>
</form>
-80
View File
@@ -1,80 +0,0 @@
<% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %>
<div class="flex justify-between mb-4 items-center">
<h1 class="text-4xl font-bold"><span class="hidden md:inline">Document</span> Templates</h1>
<div class="flex space-x-2">
<% if params[:q].present? || @pagy.pages > 1 || @template_folders.present? %>
<%= render 'shared/search_input' %>
<% end %>
<% if can?(:create, ::Template) %>
<%= render 'templates/upload_button' %>
<%= link_to new_template_path, class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span class="hidden md:block">Create</span>
<% end %>
<% end %>
</div>
</div>
<% view_archived_html = capture do %>
<% if current_account.templates.where.not(archived_at: nil).exists? %>
<div>
<a href="<%= templates_archived_index_path %>" class="link text-sm">View Archived</a>
</div>
<% end %>
<% end %>
<% if @template_folders.present? %>
<div class="grid gap-4 md:grid-cols-3 <%= 'mb-6' if @templates.present? %>">
<%= render partial: 'template_folders/folder', collection: @template_folders, as: :folder %>
</div>
<% end %>
<% if @templates.present? %>
<div class="grid gap-4 md:grid-cols-3">
<%= render partial: 'templates/template', collection: @templates %>
</div>
<% end %>
<% if params[:q].blank? && @pagy.pages == 1 && ((@template_folders.size < 10 && @templates.size.zero?) || (@template_folders.size < 7 && @templates.size < 4) || (@template_folders.size < 4 && @templates.size < 7)) %>
<%= form_for '', url: templates_upload_path, id: form_id = SecureRandom.uuid, method: :post, class: 'mt-8 block', html: { enctype: 'multipart/form-data' } do %>
<input type="hidden" name="form_id" value="<%= form_id %>">
<button type="submit" class="hidden"></button>
<file-dropzone data-submit-on-upload="true" class="w-full">
<label for="file_dropzone_input" class="w-full block h-52 relative hover:bg-base-200/30 rounded-xl border border-2 border-base-300 border-dashed">
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
<div class="flex flex-col items-center">
<span data-target="file-dropzone.icon" class="flex flex-col items-center">
<span>
<%= svg_icon('cloud_upload', class: 'w-10 h-10') %>
</span>
<div class="font-medium mb-1">
Upload New Document
</div>
<div class="text-xs">
<span class="font-medium">Click to upload</span> or drag and drop
</div>
</span>
<span data-target="file-dropzone.loading" class="flex flex-col items-center hidden">
<%= svg_icon('loader', class: 'w-10 h-10 animate-spin') %>
<div class="font-medium mb-1">
Uploading...
</div>
</span>
</div>
<input id="file_dropzone_input" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="image/*, application/pdf<%= ', .docx, .doc, .xlsx, .xls' if Docuseal.multitenant? %>" multiple>
</div>
</label>
</file-dropzone>
<% end %>
<% end %>
<% if @templates.present? || params[:q].blank? %>
<% if @pagy.pages > 1 %>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', left_additional_html: view_archived_html %>
<% else %>
<div class="mt-2">
<%= view_archived_html %>
</div>
<% end %>
<% elsif params[:q].present? %>
<div class="text-center">
<div class="mt-16 text-3xl font-semibold">
Templates not Found
</div>
</div>
<% end %>
+1 -1
View File
@@ -15,7 +15,7 @@
Analyzing... Analyzing...
</div> </div>
<% end %> <% end %>
<file-dropzone data-is-direct-upload="false" data-name="verify_attachments" data-submit-on-upload="true" class="w-full"> <file-dropzone data-name="verify_attachments" data-submit-on-upload="true" class="w-full">
<label for="file" class="w-full block h-32 relative bg-base-200 hover:bg-base-200/70 rounded-md border border-base-content border-dashed"> <label for="file" class="w-full block h-32 relative bg-base-200 hover:bg-base-200/70 rounded-md border border-base-content border-dashed">
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center"> <div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
@@ -0,0 +1,12 @@
<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="M14 6m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
<path d="M4 6l8 0" />
<path d="M16 6l4 0" />
<path d="M8 12m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
<path d="M4 12l2 0" />
<path d="M10 12l10 0" />
<path d="M17 18m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
<path d="M4 18l11 0" />
<path d="M19 18l1 0" />
</svg>

After

Width:  |  Height:  |  Size: 602 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M5 12l14 0" />
<path d="M15 16l4 -4" />
<path d="M15 8l4 4" />
</svg>

After

Width:  |  Height:  |  Size: 361 B

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

After

Width:  |  Height:  |  Size: 415 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" aria-hidden="true" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
</svg>

After

Width:  |  Height:  |  Size: 338 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M6.06004 6.00016C6.21678 5.55461 6.52614 5.1789 6.93334 4.93958C7.34055 4.70027 7.8193 4.61279 8.28483 4.69264C8.75035 4.77249 9.17259 5.01451 9.47676 5.37585C9.78093 5.73718 9.94741 6.19451 9.94671 6.66683C9.94671 8.00016 7.94671 8.66683 7.94671 8.66683M8.00004 11.3335H8.00671M14.6667 8.00016C14.6667 11.6821 11.6819 14.6668 8.00004 14.6668C4.31814 14.6668 1.33337 11.6821 1.33337 8.00016C1.33337 4.31826 4.31814 1.3335 8.00004 1.3335C11.6819 1.3335 14.6667 4.31826 14.6667 8.00016Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 730 B

@@ -0,0 +1,4 @@
<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="M6 6v6a3 3 0 0 0 3 3h10l-4 -4m0 8l4 -4" />
</svg>

After

Width:  |  Height:  |  Size: 337 B

Loaded 100 of 185 files, more files were not shown because too many files have changed in this diff. Show more