Compare commits

..
1 Commits
Author SHA1 Message Date
Alex Turchyn 6fbc81fae2 increase tokens length 2023-07-08 21:05:29 +03:00
433 changed files with 2121 additions and 17558 deletions

No files matched your search

+18
View File
@@ -0,0 +1,18 @@
spec:
name: docuseal
services:
- name: docuseal
dockerfile_path: Dockerfile
git:
repo_clone_url: https://github.com/docusealco/docuseal.git
branch: master
envs:
- key: SECRET_KEY_BASE
value: "- 64 characters string 64 characters string 64 characters string -"
type: SECRET
scope: RUN_TIME
- key: DATABASE_URL
scope: RUN_TIME
value: ${docuseal-db.DATABASE_URL}
databases:
- name: docuseal-db
-2
View File
@@ -1,6 +1,5 @@
/db/*.sqlite3
**/node_modules
/node_modules
/coverage
/doc
@@ -28,4 +27,3 @@
/attachments
/docuseal
.DS_Store
.env
-2
View File
@@ -30,5 +30,3 @@ linters:
Enabled: false
Style/ZeroLengthPredicate:
Enabled: false
Style/RedundantConstantBase:
Enabled: false
+1 -2
View File
@@ -8,8 +8,7 @@
},
"rules": {
"vue/no-deprecated-html-element-is": 0,
"vue/no-mutating-props": 0,
"vue/no-v-html": 0
"vue/no-mutating-props": 0
},
"parserOptions": {
"ecmaVersion": 2022,
-2
View File
@@ -127,7 +127,6 @@ jobs:
bundle config path vendor/bundle
bundle install --jobs 4 --retry 4
yarn install
sudo apt-get update
sudo apt-get install libvips
- name: Run
env:
@@ -138,5 +137,4 @@ jobs:
run: |
bundle exec rake db:create
bundle exec rake db:migrate
bundle exec rake assets:precompile
bundle exec rspec
-3
View File
@@ -30,9 +30,6 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Create .version file
run: echo ${{ github.ref_name }} > .version
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
-1
View File
@@ -35,4 +35,3 @@ yarn-debug.log*
/coverage
/attachments
/docuseal
/ee
+4 -19
View File
@@ -26,11 +26,8 @@ Style/Documentation:
Lint/MissingSuper:
Enabled: false
Metrics/ParameterLists:
Max: 10
Metrics/MethodLength:
Max: 30
Max: 20
Exclude:
- 'db/migrate/**'
@@ -44,7 +41,7 @@ Layout/LineLength:
AllowedPatterns: ['\A\s*#']
Metrics/AbcSize:
Max: 40
Max: 35
Metrics/ModuleLength:
Max: 500
@@ -56,25 +53,13 @@ RSpec/NestedGroups:
Max: 6
RSpec/MultipleExpectations:
Max: 20
Max: 7
RSpec/ExampleLength:
Max: 40
RSpec/MultipleMemoizedHelpers:
Max: 6
Max: 15
Rails/I18nLocaleTexts:
Enabled: false
Rails/FindEach:
Enabled: false
Rails/SkipsModelValidations:
Enabled: false
Rails/ApplicationController:
Enabled: false
Capybara/ClickLinkOrButtonStyle:
Enabled: false
View File
Whitespace-only changes.
+5 -23
View File
@@ -1,12 +1,4 @@
FROM ruby:3.2.2-alpine3.18 as fonts
WORKDIR /fonts
RUN apk --no-cache add fontforge wget ttf-liberation && cp /usr/share/fonts/liberation/LiberationSans-Regular.ttf /usr/share/fonts/liberation/LiberationSans-Bold.ttf . && wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansArabic/hinted/ttf/NotoSansArabic-Regular.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt
RUN fontforge -lang=py -c 'font1 = fontforge.open("LiberationSans-Regular.ttf"); font2 = fontforge.open("NotoSansArabic-Regular.ttf"); font1.mergeFonts(font2); font1.generate("LiberationSans-Regular.ttf")'
FROM ruby:3.2.2-alpine3.18 as webpack
FROM ruby:3.2.2-alpine as webpack
ENV RAILS_ENV=production
ENV NODE_ENV=production
@@ -32,33 +24,23 @@ COPY ./app/views ./app/views
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
FROM ruby:3.2.2-alpine3.18 as app
FROM ruby:3.2.2-alpine as app
ENV RAILS_ENV=production
ENV BUNDLE_WITHOUT="development:test"
WORKDIR /app
RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils vips-heif libc6-compat ttf-freefont && mkdir /fonts
RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler vips-heif libc6-compat ttf-freefont
COPY ./Gemfile ./Gemfile.lock ./
RUN bundle update --bundler && bundle install && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf
RUN bundle update --bundler && bundle install && rm -rf ~/.bundle
COPY ./bin ./bin
COPY ./app ./app
COPY ./config ./config
COPY ./db ./db
COPY ./log ./log
COPY ./lib ./lib
COPY ./public ./public
COPY ./tmp ./tmp
COPY LICENSE README.md Rakefile config.ru .version ./
COPY . ./
COPY --from=fonts /fonts/LiberationSans-Regular.ttf /fonts/LiberationSans-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
COPY --from=webpack /app/public/packs ./public/packs
RUN ln -s /fonts /app/public/fonts
RUN bundle exec bootsnap precompile --gemfile app/ lib/
WORKDIR /data/docuseal
-15
View File
@@ -4,45 +4,30 @@ source 'https://rubygems.org'
ruby '3.2.2'
gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic'
gem 'aws-sdk-s3', require: false
gem 'azure-storage-blob', require: false
gem 'bootsnap', require: false
gem 'cancancan'
gem 'devise'
gem 'devise-two-factor'
gem 'dotenv', require: false
gem 'email_typo'
gem 'faraday'
gem 'faraday-follow_redirects'
gem 'google-cloud-storage', require: false
gem 'hexapdf'
gem 'image_processing'
gem 'jwt'
gem 'lograge'
gem 'mysql2', require: false
gem 'oj'
gem 'pagy'
gem 'pdf-reader'
gem 'pg', require: false
gem 'premailer-rails'
gem 'pretender'
gem 'puma'
gem 'rack'
gem 'rails'
gem 'rails_autolink'
gem 'rails-i18n'
gem 'rollbar', require: ENV.key?('ROLLBAR_ACCESS_TOKEN')
gem 'rotp'
gem 'rqrcode'
gem 'ruby-vips'
gem 'rubyXL'
gem 'shakapacker'
gem 'sidekiq', require: ENV.key?('REDIS_URL')
gem 'sqlite3', require: false
gem 'strip_attributes'
gem 'turbo-rails'
gem 'twitter_cldr', require: false
gem 'tzinfo-data'
group :development, :test do
+179 -275
View File
@@ -1,104 +1,92 @@
GEM
remote: https://rubygems.org/
specs:
Ascii85 (1.1.0)
actioncable (7.1.2)
actionpack (= 7.1.2)
activesupport (= 7.1.2)
actioncable (7.0.5)
actionpack (= 7.0.5)
activesupport (= 7.0.5)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (7.1.2)
actionpack (= 7.1.2)
activejob (= 7.1.2)
activerecord (= 7.1.2)
activestorage (= 7.1.2)
activesupport (= 7.1.2)
actionmailbox (7.0.5)
actionpack (= 7.0.5)
activejob (= 7.0.5)
activerecord (= 7.0.5)
activestorage (= 7.0.5)
activesupport (= 7.0.5)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
actionmailer (7.1.2)
actionpack (= 7.1.2)
actionview (= 7.1.2)
activejob (= 7.1.2)
activesupport (= 7.1.2)
actionmailer (7.0.5)
actionpack (= 7.0.5)
actionview (= 7.0.5)
activejob (= 7.0.5)
activesupport (= 7.0.5)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.2)
actionpack (7.1.2)
actionview (= 7.1.2)
activesupport (= 7.1.2)
nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4)
rack-session (>= 1.0.1)
rails-dom-testing (~> 2.0)
actionpack (7.0.5)
actionview (= 7.0.5)
activesupport (= 7.0.5)
rack (~> 2.0, >= 2.2.4)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
actiontext (7.1.2)
actionpack (= 7.1.2)
activerecord (= 7.1.2)
activestorage (= 7.1.2)
activesupport (= 7.1.2)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (7.0.5)
actionpack (= 7.0.5)
activerecord (= 7.0.5)
activestorage (= 7.0.5)
activesupport (= 7.0.5)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.1.2)
activesupport (= 7.1.2)
actionview (7.0.5)
activesupport (= 7.0.5)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (7.1.2)
activesupport (= 7.1.2)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (7.0.5)
activesupport (= 7.0.5)
globalid (>= 0.3.6)
activemodel (7.1.2)
activesupport (= 7.1.2)
activerecord (7.1.2)
activemodel (= 7.1.2)
activesupport (= 7.1.2)
timeout (>= 0.4.0)
activestorage (7.1.2)
actionpack (= 7.1.2)
activejob (= 7.1.2)
activerecord (= 7.1.2)
activesupport (= 7.1.2)
activemodel (7.0.5)
activesupport (= 7.0.5)
activerecord (7.0.5)
activemodel (= 7.0.5)
activesupport (= 7.0.5)
activestorage (7.0.5)
actionpack (= 7.0.5)
activejob (= 7.0.5)
activerecord (= 7.0.5)
activesupport (= 7.0.5)
marcel (~> 1.0)
activesupport (7.1.2)
base64
bigdecimal
mini_mime (>= 1.1.0)
activesupport (7.0.5)
concurrent-ruby (~> 1.0, >= 1.0.2)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
minitest (>= 5.1)
mutex_m
tzinfo (~> 2.0)
addressable (2.8.6)
addressable (2.8.4)
public_suffix (>= 2.0.2, < 6.0)
afm (0.2.2)
annotate (3.2.0)
activerecord (>= 3.2, < 8.0)
rake (>= 10.4, < 14.0)
arabic-letter-connector (0.1.1)
ast (2.4.2)
aws-eventstream (1.3.0)
aws-partitions (1.874.0)
aws-sdk-core (3.190.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-eventstream (1.2.0)
aws-partitions (1.781.0)
aws-sdk-core (3.175.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
aws-sigv4 (~> 1.5)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.75.0)
aws-sdk-core (~> 3, >= 3.188.0)
aws-sdk-kms (1.67.0)
aws-sdk-core (~> 3, >= 3.174.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.142.0)
aws-sdk-core (~> 3, >= 3.189.0)
aws-sdk-s3 (1.126.0)
aws-sdk-core (~> 3, >= 3.174.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8)
aws-sigv4 (1.8.0)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.5.2)
aws-eventstream (~> 1, >= 1.0.2)
azure-storage-blob (2.0.3)
azure-storage-common (~> 2.0)
@@ -108,25 +96,21 @@ GEM
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
net-http-persistent (~> 4.0)
nokogiri (~> 1, >= 1.10.8)
base64 (0.2.0)
bcrypt (3.1.20)
better_html (2.0.2)
bcrypt (3.1.19)
better_html (2.0.1)
actionview (>= 6.0)
activesupport (>= 6.0)
ast (~> 2.0)
erubi (~> 1.4)
parser (>= 2.4)
smart_properties
bigdecimal (3.1.5)
bindex (0.8.1)
bootsnap (1.17.0)
bootsnap (1.15.0)
msgpack (~> 1.2)
builder (3.2.4)
bullet (7.1.4)
bullet (7.0.7)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
camertron-eprun (1.1.1)
cancancan (3.5.0)
capybara (3.39.2)
addressable
matrix
@@ -136,8 +120,6 @@ GEM
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
chunky_png (1.4.0)
cldr-plurals-runtime-rb (1.1.0)
cmdparse (3.0.7)
coderay (1.1.3)
concurrent-ruby (1.2.2)
@@ -145,36 +127,28 @@ GEM
crack (0.4.5)
rexml
crass (1.0.6)
css_parser (1.16.0)
css_parser (1.14.0)
addressable
cuprite (0.15)
cuprite (0.14.3)
capybara (~> 3.0)
ferrum (~> 0.14.0)
date (3.3.4)
debug (1.9.1)
irb (~> 1.10)
reline (>= 0.3.8)
ferrum (~> 0.13.0)
date (3.3.3)
debug (1.8.0)
irb (>= 1.5.0)
reline (>= 0.3.1)
declarative (0.0.20)
devise (4.9.3)
devise (4.9.2)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
responders
warden (~> 1.2.3)
devise-two-factor (5.0.0)
activesupport (~> 7.0)
devise (~> 4.0)
railties (~> 7.0)
rotp (~> 6.0)
diff-lcs (1.5.0)
digest-crc (0.6.5)
digest-crc (0.6.4)
rake (>= 12.0.0, < 14.0.0)
docile (1.4.0)
dotenv (2.8.1)
drb (2.2.0)
ruby2_keywords
email_typo (0.2.3)
erb_lint (0.5.0)
erb_lint (0.4.0)
activesupport
better_html (>= 2.0.1)
parser (>= 2.7.1.4)
@@ -182,12 +156,12 @@ GEM
rubocop
smart_properties
erubi (1.12.0)
factory_bot (6.4.4)
factory_bot (6.2.1)
activesupport (>= 5.0.0)
factory_bot_rails (6.4.2)
factory_bot (~> 6.4)
factory_bot_rails (6.2.0)
factory_bot (~> 6.2.0)
railties (>= 5.0.0)
faker (3.2.2)
faker (3.2.0)
i18n (>= 1.8.11, < 2)
faraday (1.10.3)
faraday-em_http (~> 1.0)
@@ -204,8 +178,6 @@ GEM
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.4)
multipart-post (~> 2)
@@ -216,16 +188,16 @@ GEM
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday (~> 1.0)
ferrum (0.14)
ferrum (0.13)
addressable (~> 2.5)
concurrent-ruby (~> 1.1)
webrick (~> 1.7)
websocket-driver (>= 0.6, < 0.8)
ffi (1.16.3)
geom2d (0.4.1)
globalid (1.2.1)
activesupport (>= 6.1)
google-apis-core (0.11.2)
ffi (1.15.5)
geom2d (0.3.1)
globalid (1.1.0)
activesupport (>= 5.0)
google-apis-core (0.11.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
@@ -236,34 +208,33 @@ GEM
webrick
google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.29.0)
google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.6.1)
google-cloud-env (>= 1.0, < 3.a)
google-apis-storage_v1 (0.19.0)
google-apis-core (>= 0.9.0, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (2.1.0)
faraday (>= 1.0, < 3.a)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.3.1)
google-cloud-storage (1.45.0)
google-cloud-storage (1.44.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.29.0)
google-apis-storage_v1 (~> 0.19.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.9.1)
faraday (>= 1.0, < 3.a)
google-cloud-env (~> 2.1)
googleauth (1.6.0)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
hashdiff (1.1.0)
hashery (2.1.2)
hexapdf (0.34.1)
hashdiff (1.0.1)
hexapdf (0.32.2)
cmdparse (~> 3.0, >= 3.0.3)
geom2d (~> 0.4, >= 0.4.1)
geom2d (~> 0.3)
openssl (>= 2.2.1)
htmlentities (4.3.4)
httpclient (2.8.3)
@@ -272,12 +243,11 @@ GEM
image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
io-console (0.7.1)
irb (1.11.0)
rdoc
reline (>= 0.3.8)
io-console (0.6.0)
irb (1.7.0)
reline (>= 0.3.0)
jmespath (1.6.2)
json (2.7.1)
json (2.6.3)
jwt (2.7.1)
language_server-protocol (3.17.0.3)
launchy (2.5.2)
@@ -289,12 +259,12 @@ GEM
letter_opener (~> 1.7)
railties (>= 5.2)
rexml
lograge (0.14.0)
lograge (0.12.0)
actionpack (>= 4)
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
loofah (2.22.0)
loofah (2.21.3)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
@@ -304,48 +274,40 @@ GEM
net-smtp
marcel (1.0.2)
matrix (0.4.2)
memoist (0.16.2)
method_source (1.0.0)
mini_magick (4.12.0)
mini_mime (1.1.5)
mini_portile2 (2.8.5)
minitest (5.20.0)
msgpack (1.7.2)
mini_mime (1.1.2)
mini_portile2 (2.8.2)
minitest (5.18.1)
msgpack (1.7.1)
multi_json (1.15.0)
multipart-post (2.3.0)
mutex_m (0.2.0)
mysql2 (0.5.5)
net-http-persistent (4.0.2)
connection_pool (~> 2.2)
net-imap (0.4.9)
net-imap (0.3.6)
date
net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.2.2)
net-protocol (0.2.1)
timeout
net-smtp (0.4.0)
net-smtp (0.3.3)
net-protocol
nio4r (2.7.0)
nokogiri (1.15.5)
mini_portile2 (~> 2.8.2)
nio4r (2.5.9)
nokogiri (1.15.2-arm64-darwin)
racc (~> 1.4)
oj (3.16.3)
bigdecimal (>= 3.0)
openssl (3.2.0)
oj (3.15.0)
openssl (3.1.0)
orm_adapter (0.5.0)
os (1.1.4)
pagy (6.2.0)
parallel (1.24.0)
parser (3.2.2.4)
pagy (6.0.4)
parallel (1.23.0)
parser (3.2.2.3)
ast (~> 2.4.1)
racc
pdf-reader (2.12.0)
Ascii85 (~> 1.0)
afm (~> 0.2.1)
hashery (~> 2.0)
ruby-rc4
ttfunk
pg (1.5.4)
pg (1.5.3)
premailer (1.21.0)
addressable
css_parser (>= 1.12.0)
@@ -354,73 +316,54 @@ GEM
actionmailer (>= 3)
net-smtp
premailer (~> 1.7, >= 1.7.9)
pretender (0.5.0)
actionpack (>= 6.1)
pry (0.14.2)
coderay (~> 1.1)
method_source (~> 1.0)
pry-rails (0.3.9)
pry (>= 0.10.4)
psych (5.1.2)
stringio
public_suffix (5.0.4)
puma (6.4.0)
public_suffix (5.0.1)
puma (6.3.0)
nio4r (~> 2.0)
racc (1.7.3)
rack (2.2.8)
rack-proxy (0.7.7)
racc (1.7.1)
rack (2.2.7)
rack-proxy (0.7.6)
rack
rack-session (1.0.2)
rack (< 3)
rack-test (2.1.0)
rack (>= 1.3)
rackup (1.0.0)
rack (< 3)
webrick
rails (7.1.2)
actioncable (= 7.1.2)
actionmailbox (= 7.1.2)
actionmailer (= 7.1.2)
actionpack (= 7.1.2)
actiontext (= 7.1.2)
actionview (= 7.1.2)
activejob (= 7.1.2)
activemodel (= 7.1.2)
activerecord (= 7.1.2)
activestorage (= 7.1.2)
activesupport (= 7.1.2)
rails (7.0.5)
actioncable (= 7.0.5)
actionmailbox (= 7.0.5)
actionmailer (= 7.0.5)
actionpack (= 7.0.5)
actiontext (= 7.0.5)
actionview (= 7.0.5)
activejob (= 7.0.5)
activemodel (= 7.0.5)
activerecord (= 7.0.5)
activestorage (= 7.0.5)
activesupport (= 7.0.5)
bundler (>= 1.15.0)
railties (= 7.1.2)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
railties (= 7.0.5)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
nokogiri (~> 1.14)
rails-i18n (7.0.8)
rails-i18n (7.0.7)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8)
rails_autolink (1.1.8)
actionview (> 3.1)
activesupport (> 3.1)
railties (> 3.1)
railties (7.1.2)
actionpack (= 7.1.2)
activesupport (= 7.1.2)
irb
rackup (>= 1.0.0)
railties (7.0.5)
actionpack (= 7.0.5)
activesupport (= 7.0.5)
method_source
rake (>= 12.2)
thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6)
thor (~> 1.0)
zeitwerk (~> 2.5)
rainbow (3.1.1)
rake (13.1.0)
rdoc (6.6.2)
psych (>= 4.0.0)
redis-client (0.19.1)
connection_pool
regexp_parser (2.8.3)
reline (0.4.1)
rake (13.0.6)
regexp_parser (2.8.1)
reline (0.3.5)
io-console (~> 0.5)
representable (3.2.0)
declarative (< 0.1.0)
@@ -428,26 +371,21 @@ GEM
uber (< 0.2.0)
request_store (1.5.1)
rack (>= 1.4)
responders (3.1.1)
responders (3.1.0)
actionpack (>= 5.2)
railties (>= 5.2)
retriable (3.1.2)
rexml (3.2.6)
rollbar (3.4.2)
rotp (6.3.0)
rqrcode (2.2.0)
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
rqrcode_core (1.2.0)
rexml (3.2.5)
rollbar (3.4.0)
rspec-core (3.12.2)
rspec-support (~> 3.12.0)
rspec-expectations (3.12.3)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-mocks (3.12.6)
rspec-mocks (3.12.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-rails (6.1.0)
rspec-rails (6.0.3)
actionpack (>= 6.1)
activesupport (>= 6.1)
railties (>= 6.1)
@@ -455,57 +393,46 @@ GEM
rspec-expectations (~> 3.12)
rspec-mocks (~> 3.12)
rspec-support (~> 3.12)
rspec-support (3.12.1)
rubocop (1.59.0)
rspec-support (3.12.0)
rubocop (1.53.0)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
parser (>= 3.2.2.4)
parser (>= 3.2.2.3)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.30.0, < 2.0)
rubocop-ast (>= 1.28.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.30.0)
rubocop-ast (1.29.0)
parser (>= 3.2.1.0)
rubocop-capybara (2.19.0)
rubocop-capybara (2.18.0)
rubocop (~> 1.41)
rubocop-factory_bot (2.24.0)
rubocop-factory_bot (2.23.1)
rubocop (~> 1.33)
rubocop-performance (1.20.1)
rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.30.0, < 2.0)
rubocop-rails (2.23.1)
rubocop-performance (1.18.0)
rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0)
rubocop-rails (2.20.2)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0)
rubocop-ast (>= 1.30.0, < 2.0)
rubocop-rspec (2.25.0)
rubocop (~> 1.40)
rubocop-rspec (2.22.0)
rubocop (~> 1.33)
rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22)
ruby-progressbar (1.13.0)
ruby-rc4 (0.1.5)
ruby-vips (2.2.0)
ruby-vips (2.1.4)
ffi (~> 1.12)
ruby2_keywords (0.0.5)
rubyXL (3.4.25)
nokogiri (>= 1.10.8)
rubyzip (>= 1.3.0)
rubyzip (2.3.2)
semantic_range (3.0.0)
shakapacker (7.1.0)
shakapacker (7.0.0)
activesupport (>= 5.2)
rack-proxy (>= 0.6.1)
railties (>= 5.2)
semantic_range (>= 2.3.0)
sidekiq (7.2.0)
concurrent-ruby (< 2)
connection_pool (>= 2.3.0)
rack (>= 2.2.4)
redis-client (>= 0.14.0)
signet (0.18.0)
signet (0.17.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
@@ -517,111 +444,88 @@ GEM
simplecov-html (0.12.3)
simplecov_json_formatter (0.1.4)
smart_properties (1.17.0)
sqlite3 (1.7.0)
sqlite3 (1.5.4)
mini_portile2 (~> 2.8.0)
sqlite3 (1.7.0-arm64-darwin)
stringio (3.1.0)
strip_attributes (1.13.0)
activemodel (>= 3.0, < 8.0)
thor (1.3.0)
timeout (0.4.1)
thor (1.2.2)
timeout (0.4.0)
trailblazer-option (0.1.2)
ttfunk (1.7.0)
turbo-rails (1.5.0)
turbo-rails (1.4.0)
actionpack (>= 6.0.0)
activejob (>= 6.0.0)
railties (>= 6.0.0)
twitter_cldr (6.12.0)
camertron-eprun
cldr-plurals-runtime-rb (~> 1.1)
tzinfo
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
tzinfo-data (1.2023.4)
tzinfo-data (1.2023.3)
tzinfo (>= 1.0.0)
uber (0.1.0)
unicode-display_width (2.5.0)
unicode-display_width (2.4.2)
uniform_notifier (1.16.0)
warden (1.2.9)
rack (>= 2.0.9)
web-console (4.2.1)
web-console (4.2.0)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
webmock (3.19.1)
webmock (3.18.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.8.1)
websocket-driver (0.7.6)
websocket-driver (0.7.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.6.12)
zeitwerk (2.6.8)
PLATFORMS
arm64-darwin-22
ruby
DEPENDENCIES
annotate
arabic-letter-connector
aws-sdk-s3
azure-storage-blob
better_html
bootsnap
bullet
cancancan
capybara
cuprite
debug
devise
devise-two-factor
dotenv
email_typo
erb_lint
factory_bot_rails
faker
faraday
faraday-follow_redirects
google-cloud-storage
hexapdf
image_processing
jwt
letter_opener_web
lograge
mysql2
oj
pagy
pdf-reader
pg
premailer-rails
pretender
pry-rails
puma
rack
rails
rails-i18n
rails_autolink
rollbar
rotp
rqrcode
rspec-rails
rubocop
rubocop-performance
rubocop-rails
rubocop-rspec
ruby-vips
rubyXL
shakapacker
sidekiq
simplecov
sqlite3
strip_attributes
turbo-rails
twitter_cldr
tzinfo-data
web-console
webmock
@@ -630,4 +534,4 @@ RUBY VERSION
ruby 3.2.2p53
BUNDLED WITH
2.5.3
2.4.10
+9 -24
View File
@@ -14,7 +14,7 @@
<a href="https://hub.docker.com/r/docuseal/docuseal">
<img alt="Docker releases" src="https://img.shields.io/docker/v/docuseal/docuseal">
</a>
<a href="https://discord.gg/qygYCDGck9">
<a href="https://discord.com/invite/B5wg5wKk">
<img src="https://img.shields.io/discord/1125112641170448454?logo=discord"/>
</a>
<a href="https://twitter.com/intent/follow?screen_name=docusealco">
@@ -26,34 +26,27 @@ DocuSeal is an open source platform that provides secure and efficient digital d
</p>
<h2 align="center">
<a href="https://demo.docuseal.co">✨ Live Demo</a>
<span>|</span>
<a href="https://docuseal.co/sign_up">☁️ Try in Cloud</a>
</h2>
[![Demo](https://github.com/docusealco/docuseal/assets/5418788/d8703ea3-361a-423f-8bfe-eff1bd9dbe14)](https://demo.docuseal.co)
## Features
- [x] PDF form fields builder (WYSIWYG)
- [x] 11 field types available (Signature, Date, File, Checkbox etc.)
- [x] 10 field types available (Signature, Date, File, Checkbox etc.)
- [x] Multiple submitters per document
- [x] Automated emails via SMTP
- [x] Files storage on disk or AWS S3, Google Storage, Azure Cloud
- [x] Files storage on AWS S3, Google Storage, or Azure
- [x] Automatic PDF eSignature
- [x] PDF signature verification
- [x] Users management
- [x] Mobile-optimized
- [x] API and Webhooks for integrations
- [x] Easy to deploy in minutes
## Deploy
|Heroku|Railway|
|:--:|:---:|
| [<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**|
| [<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) |
| Heroku | Railway | DigitalOcean |
| ---------------------------------------------------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| [<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 DigitalOcean" src="https://www.deploytodo.com/do-btn-blue.svg" height="40">](https://cloud.digitalocean.com/apps/new?repo=https://github.com/docusealco/docuseal/tree/master&refcode=421d50f53990) |
#### Docker
@@ -71,19 +64,11 @@ Download docker-compose.yml into your private server:
curl https://raw.githubusercontent.com/docusealco/docuseal/master/docker-compose.yml > docker-compose.yml
```
Run the app under a custom domain over https using docker compose (make sure your DNS points to the server to automatically issue ssl certs with Caddy):
Run docker compose under a custom domain over https (make sure your DNS points to the server to automatically issue ssl certs with Caddy):
```sh
sudo HOST=your-domain-name.com docker compose up
HOST=your-domain-name.com docker-compose up
```
## For Businesses
### Integrate seamless document signing into your web or mobile apps with DocuSeal!
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
[Book a Meeting](https://www.docuseal.co/contact)
## License
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.
DocuSeal is released under the GNU Affero General Public License v3.0.
@@ -1,34 +0,0 @@
# frozen_string_literal: true
class AccountConfigsController < ApplicationController
before_action :load_account_config
authorize_resource :account_config
ALLOWED_KEYS = [
AccountConfig::ALLOW_TYPED_SIGNATURE,
AccountConfig::FORCE_MFA,
AccountConfig::ALLOW_TO_RESUBMIT,
AccountConfig::ESIGNING_PREFERENCE_KEY
].freeze
def create
@account_config.update!(account_config_params)
head :ok
end
private
def load_account_config
return head :not_found unless ALLOWED_KEYS.include?(account_config_params[:key])
@account_config =
AccountConfig.find_or_initialize_by(account: current_account, key: account_config_params[:key])
end
def account_config_params
params.required(:account_config).permit!.tap do |attrs|
attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
end
end
end
-22
View File
@@ -4,15 +4,11 @@ class AccountsController < ApplicationController
LOCALE_OPTIONS = {
'en-US' => 'English (United States)',
'en-GB' => 'English (United Kingdom)',
'fr-FR' => 'French (France)',
'es-ES' => 'Spanish (Spain)',
'pt-PT' => 'Portuguese (Portugal)',
'de-DE' => 'German (Germany)'
}.freeze
before_action :load_account
authorize_resource :account
def show; end
def update
@@ -37,31 +33,13 @@ class AccountsController < ApplicationController
render :show, status: :unprocessable_entity
end
def destroy
authorize!(:manage, current_account)
true_user.update!(locked_at: Time.current)
render turbo_stream: turbo_stream.replace(
:account_delete_button,
html: helpers.tag.p('Your account removal request will be processed within 2 weeks. ' \
'Please contact us if you want to keep your account.')
)
end
private
def load_account
@account = current_account
end
def account_params
params.require(:account).permit(:name, :timezone, :locale)
end
def app_url_params
return {} if params[:encrypted_config].blank?
params.require(:encrypted_config).permit(:value)
end
end
@@ -3,59 +3,11 @@
module Api
class ApiBaseController < ActionController::API
include ActiveStorage::SetCurrent
include Pagy::Backend
DEFAULT_LIMIT = 10
MAX_LIMIT = 100
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
wrap_parameters false
before_action :authenticate_user!
check_authorization
rescue_from Params::BaseValidator::InvalidParameterError do |e|
render json: { error: e.message }, status: :unprocessable_entity
end
if Rails.env.production?
rescue_from CanCan::AccessDenied do |e|
Rollbar.error(e) if defined?(Rollbar)
render json: { error: e.message }, status: :forbidden
end
rescue_from JSON::ParserError do |e|
Rollbar.error(e) if defined?(Rollbar)
render json: { error: "JSON parse error: #{e.message}" }, status: :unprocessable_entity
end
end
private
def paginate(relation)
result = relation.order(id: :desc)
.limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min)
result = result.where(relation.arel_table[:id].lt(params[:after])) if params[:after].present?
result = result.where(relation.arel_table[:id].gt(params[:before])) if params[:before].present?
result
end
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
end
def current_account
current_user&.account
end
+14 -2
View File
@@ -3,12 +3,24 @@
module Api
class AttachmentsController < ApiBaseController
skip_before_action :authenticate_user!
skip_authorization_check
def create
submitter = Submitter.find_by!(slug: params[:submitter_slug])
attachment = Submitters.create_attachment!(submitter, params)
blob =
if (file = params[:file])
ActiveStorage::Blob.create_and_upload!(io: file.open,
filename: file.original_filename,
content_type: file.content_type)
else
ActiveStorage::Blob.find_signed(params[:blob_signed_id])
end
attachment = ActiveStorage::Attachment.create!(
blob:,
name: params[:name],
record: submitter
)
render json: attachment.as_json(only: %i[uuid], methods: %i[url filename content_type])
end
@@ -1,184 +0,0 @@
# frozen_string_literal: true
module Api
class SubmissionsController < ApiBaseController
load_and_authorize_resource :template, only: :create
load_and_authorize_resource :submission, only: %i[show index destroy]
before_action only: :create do
authorize!(:create, Submission)
end
def index
submissions = Submissions.search(@submissions, params[:q])
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
if params[:template_folder].present?
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] })
end
submissions = paginate(submissions.preload(:created_by_user, :template, :submitters,
audit_trail_attachment: :blob))
render json: {
data: submissions.as_json(serialize_params),
pagination: {
count: submissions.size,
next: submissions.last&.id,
prev: submissions.first&.id
}
}
end
def show
submitters = @submission.submitters.preload(documents_attachments: :blob, attachments_attachments: :blob)
submitters.each do |submitter|
if submitter.completed_at? && submitter.documents_attachments.blank?
submitter.documents_attachments = Submissions::EnsureResultGenerated.call(submitter)
end
end
if @submission.audit_trail_attachment.blank? && submitters.all?(&:completed_at?)
@submission.audit_trail_attachment = Submissions::GenerateAuditTrail.call(@submission)
end
serialized_submitters = submitters.map { |submitter| Submitters::SerializeForApi.call(submitter) }
json = @submission.as_json(
serialize_params.deep_merge(
include: { submission_events: { only: %i[id submitter_id event_type event_timestamp] } }
)
)
if submitters.all?(&:completed_at?)
last_submitter = submitters.max_by(&:completed_at)
json[:documents] = serialized_submitters.find { |e| e['id'] == last_submitter.id }['documents']
json[:status] = 'completed'
json[:completed_at] = last_submitter.completed_at
else
json[:documents] = []
json[:status] = 'pending'
json[:completed_at] = nil
end
json[:submitters] = serialized_submitters
render json:
end
def create
Params::SubmissionCreateValidator.call(params, dry_run: !Docuseal.multitenant?)
return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil?
if @template.fields.blank?
Rollbar.error("Template does not contain fields: #{@template.id}") if defined?(Rollbar)
return render json: { error: 'Template does not contain fields' }, status: :unprocessable_entity
end
params[:send_email] = true unless params.key?(:send_email)
params[:send_sms] = false unless params.key?(:send_sms)
submissions = create_submissions(@template, params)
Submissions.send_signature_requests(submissions)
submissions.each do |submission|
if submission.submitters.all?(&:completed_at?) && submission.submitters.last
ProcessSubmitterCompletionJob.perform_later(submission.submitters.last)
end
end
render json: submissions.flat_map(&:submitters)
rescue Submitters::NormalizeValues::UnknownFieldName, Submitters::NormalizeValues::UnknownSubmitterName => e
render json: { error: e.message }, status: :unprocessable_entity
end
def destroy
@submission.update!(archived_at: Time.current)
render json: @submission.as_json(only: %i[id], methods: %i[archived_at])
end
private
def create_submissions(template, params)
is_send_email = !params[:send_email].in?(['false', false])
if (emails = (params[:emails] || params[:email]).presence) &&
(params[:submission].blank? && params[:submitters].blank?)
Submissions.create_from_emails(template:,
user: current_user,
source: :api,
mark_as_sent: is_send_email,
emails:,
params:)
else
submissions_attrs, attachments =
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_params, template)
submissions = Submissions.create_from_submitters(
template:,
user: current_user,
source: :api,
mark_as_sent: is_send_email,
submitters_order: params[:submitters_order] || params[:order] || 'preserved',
submissions_attrs:,
params:
)
submitters = submissions.flat_map(&:submitters)
Submissions::NormalizeParamUtils.save_default_value_attachments!(attachments, submitters)
submitters.each do |submitter|
SubmissionEvents.create_with_tracking_data(submitter, 'api_complete_form', request) if submitter.completed_at?
end
submissions
end
end
def serialize_params
{
only: %i[id source submitters_order created_at updated_at archived_at],
methods: %i[audit_log_url],
include: {
submitters: { only: %i[id slug uuid name email phone
completed_at opened_at sent_at
created_at updated_at external_id metadata],
methods: %i[status application_key] },
template: { only: %i[id name created_at updated_at] },
created_by_user: { only: %i[id email first_name last_name] }
}
}
end
def submissions_params
permitted_attrs = [
:send_email, :send_sms, :bcc_completed, :completed_redirect_url,
{
message: %i[subject body],
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :external_id,
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
fields: [%i[name default_value title description
readonly validation_pattern invalid_message]] }]]
}
]
if params.key?(:submitters)
params.permit(*permitted_attrs)
else
key = params.key?(:submission) ? :submission : :submissions
params.permit(
key => [permitted_attrs]
).fetch(key, [])
end
end
end
end
@@ -1,18 +0,0 @@
# frozen_string_literal: true
module Api
class SubmitterEmailClicksController < ApiBaseController
skip_before_action :authenticate_user!
skip_authorization_check
def create
submitter = Submitter.find_by!(slug: params[:submitter_slug])
if params[:t] == SubmissionEvents.build_tracking_param(submitter, 'click_email')
SubmissionEvents.create_with_tracking_data(submitter, 'click_email', request)
end
render json: {}
end
end
end
@@ -1,18 +0,0 @@
# frozen_string_literal: true
module Api
class SubmitterFormViewsController < ApiBaseController
skip_before_action :authenticate_user!
skip_authorization_check
def create
submitter = Submitter.find_by!(slug: params[:submitter_slug])
SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request)
SendFormViewedWebhookRequestJob.perform_later(submitter)
render json: {}
end
end
end
@@ -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.lower.matches(term))
else
Submitters.search(submitters, params[:q])
end
end
end
end
@@ -1,122 +0,0 @@
# frozen_string_literal: true
module Api
class SubmittersController < ApiBaseController
load_and_authorize_resource :submitter
def index
submitters = Submitters.search(@submitters, params[:q])
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(submission_id: params[:submission_id]) if params[:submission_id].present?
submitters = paginate(
submitters.preload(:template, :submission, :submission_events,
documents_attachments: :blob, attachments_attachments: :blob)
)
render json: {
data: submitters.map { |s| Submitters::SerializeForApi.call(s, with_template: true, with_events: true) },
pagination: {
count: submitters.size,
next: submitters.last&.id,
prev: submitters.first&.id
}
}
end
def show
Submissions::EnsureResultGenerated.call(@submitter) if @submitter.completed_at?
render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true)
end
def update
if @submitter.completed_at?
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_entity
end
role = @submitter.submission.template_submitters.find { |e| e['uuid'] == @submitter.uuid }['name']
normalized_params, new_attachments =
Submissions::NormalizeParamUtils.normalize_submitter_params!(submitter_params.merge(role:), @submitter.template,
for_submitter: @submitter)
Submissions::CreateFromSubmitters.maybe_set_template_fields(@submitter.submission,
[normalized_params],
default_submitter_uuid: @submitter.uuid)
assign_submitter_attrs(@submitter, normalized_params)
ApplicationRecord.transaction do
Submissions::NormalizeParamUtils.save_default_value_attachments!(new_attachments, [@submitter])
@submitter.save!
@submitter.submission.save!
SubmissionEvents.create_with_tracking_data(@submitter, 'api_complete_form', request) if @submitter.completed_at?
end
if @submitter.completed_at?
ProcessSubmitterCompletionJob.perform_later(@submitter)
elsif normalized_params[:send_email] || normalized_params[:send_sms]
Submitters.send_signature_requests([@submitter])
end
render json: Submitters::SerializeForApi.call(@submitter, with_template: false, with_events: false)
end
def submitter_params
submitter_params = params.key?(:submitter) ? params.require(:submitter) : params
submitter_params.permit(
:send_email, :send_sms, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :external_id,
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
fields: [%i[name default_value readonly validation_pattern invalid_message]] }
)
end
private
def assign_submitter_attrs(submitter, attrs)
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '') if attrs.key?(:phone)
submitter.values = submitter.values.merge(attrs[:values].to_unsafe_h) if attrs[:values].present?
submitter.completed_at = attrs[:completed] ? Time.current : submitter.completed_at
submitter.external_id = attrs[:application_key] if attrs.key?(:application_key)
submitter.external_id = attrs[:external_id] if attrs.key?(:external_id)
submitter.metadata = attrs[:metadata] if attrs.key?(:metadata)
assign_submission_fields(submitter.submission)
assign_preferences(submitter, attrs)
submitter
end
def assign_submission_fields(submission)
submission.template_fields ||= submission.template.fields
submission.template_schema ||= submission.template.schema
end
def assign_preferences(submitter, attrs)
submitter_preferences = Submitters.normalize_preferences(submitter.account, current_user, attrs)
submitter.preferences['default_values'] = attrs[:values].to_unsafe_h if attrs[:values].present?
if submitter_preferences.key?('send_email')
submitter.preferences['send_email'] = submitter_preferences['send_email']
end
submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms')
return unless submitter_preferences.key?('email_message_uuid')
submitter.preferences['email_message_uuid'] = submitter_preferences['email_message_uuid']
submitter
end
end
end
@@ -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
@@ -1,35 +0,0 @@
# frozen_string_literal: true
module Api
class TemplatesCloneController < ApiBaseController
load_and_authorize_resource :template
def create
authorize!(:manage, @template)
cloned_template = Templates::Clone.call(
@template,
author: current_user,
name: params[:name],
external_id: params[:external_id].presence || params[:application_key],
folder_name: params[:folder_name]
)
cloned_template.source = :api
cloned_template.save!
Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
render json: cloned_template.as_json(serialize_params)
end
private
def serialize_params
{
include: { author: { only: %i[id email first_name last_name] },
documents: { only: %i[id uuid], methods: %i[url filename] } }
}
end
end
end
+7 -74
View File
@@ -2,89 +2,22 @@
module Api
class TemplatesController < ApiBaseController
load_and_authorize_resource :template
def index
templates = filter_templates(@templates, params)
templates = paginate(templates.preload(:author, documents_attachments: :blob))
render json: {
data: templates.as_json(serialize_params),
pagination: {
count: templates.size,
next: templates.last&.id,
prev: templates.first&.id
}
}
end
def show
render json: @template.as_json(serialize_params)
end
def update
if (folder_name = params[:folder_name] || params.dig(:template, :folder_name))
@template.folder = TemplateFolders.find_or_create_by_name(current_user, folder_name)
end
archived = params.key?(:archived) ? params[:archived] : params.dig(:template, :archived)
if archived.in?([true, false])
@template.archived_at = archived == true ? Time.current : nil
end
@template = current_account.templates.find(params[:id])
@template.update!(template_params)
render json: @template.as_json(only: %i[id updated_at])
end
def destroy
if params[:permanently] == 'true' && !Docuseal.multitenant?
@template.destroy!
else
@template.update!(archived_at: Time.current)
end
render json: @template.as_json(only: %i[id archived_at])
render :ok
end
private
def filter_templates(templates, params)
templates = Templates.search(templates, params[:q])
templates = params[:archived] ? templates.archived : templates.active
templates = templates.where(external_id: params[:application_key]) if params[:application_key].present?
templates = templates.where(external_id: params[:external_id]) if params[:external_id].present?
templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present?
templates
end
def serialize_params
{
methods: %i[application_key],
include: { author: { only: %i[id email first_name last_name] },
documents: { only: %i[id uuid], methods: %i[url preview_image_url filename] } }
}
end
def template_params
permit_params = [
:name,
{ schema: [%i[attachment_uuid name]],
submitters: [%i[name uuid]],
fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value,
{ preferences: {},
options: [%i[value uuid]],
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
]
if params.key?(:template)
params.require(:template).permit(*permit_params)
else
params.permit(*permit_params)
end
params.require(:template).permit(:name,
schema: [%i[attachment_uuid name]],
submitters: [%i[name uuid]],
fields: [[:uuid, :submitter_uuid, :name, :type, :required,
{ options: [], areas: [%i[x y w h cell_w attachment_uuid page]] }]])
end
end
end
@@ -2,12 +2,15 @@
module Api
class TemplatesDocumentsController < ApiBaseController
load_and_authorize_resource :template
def create
return head :unprocessable_entity if params[:blobs].blank? && params[:files].blank?
@template = current_account.templates.find(params[:template_id])
documents = Templates::CreateAttachments.call(@template, params)
documents =
find_or_create_blobs.map do |blob|
document = @template.documents.create!(blob:)
Templates::ProcessDocument.call(document)
end
schema = documents.map do |doc|
{ attachment_uuid: doc.uuid, name: doc.filename.base }
@@ -16,14 +19,25 @@ module Api
render json: {
schema:,
documents: documents.as_json(
methods: [:metadata],
include: {
preview_images: { methods: %i[url metadata filename] }
}
)
}
rescue Templates::CreateAttachments::PdfEncrypted
render json: { error: 'PDF encrypted' }, status: :unprocessable_entity
end
private
def find_or_create_blobs
blobs = params[:blobs]&.map do |attrs|
ActiveStorage::Blob.find_signed(attrs[:signed_id])
end
blobs || params[:files].map do |file|
ActiveStorage::Blob.create_and_upload!(io: file.open,
filename: file.original_filename,
content_type: file.content_type)
end
end
end
end
@@ -1,17 +0,0 @@
# frozen_string_literal: true
class ApiSettingsController < ApplicationController
def index
authorize!(:read, current_user.access_token)
end
def create
authorize!(:manage, current_user.access_token)
current_user.access_token.token = SecureRandom.base58(AccessToken::TOKEN_LENGTH)
current_user.access_token.save!
redirect_back(fallback_location: settings_api_index_path, notice: 'API token as been updated.')
end
end
+3 -42
View File
@@ -1,13 +1,9 @@
# frozen_string_literal: true
class ApplicationController < ActionController::Base
BROWSER_LOCALE_REGEXP = /\A\w{2}(?:-\w{2})?/
include ActiveStorage::SetCurrent
include Pagy::Backend
check_authorization unless: :devise_controller?
before_action :sign_in_for_demo, if: -> { Docuseal.demo? }
before_action :maybe_redirect_to_setup, unless: :signed_in?
before_action :authenticate_user!, unless: :devise_controller?
@@ -16,52 +12,18 @@ class ApplicationController < ActionController::Base
:current_account,
:svg_icon
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
rescue_from Pagy::OverflowError do
redirect_to request.path
end
if Rails.env.production?
rescue_from CanCan::AccessDenied do |e|
Rollbar.error(e) if defined?(Rollbar)
redirect_to root_path, alert: e.message
end
end
def default_url_options
Docuseal.default_url_options
end
def impersonate_user(user)
raise ArgumentError unless user
raise Pretender::Error unless true_user
@impersonated_user = user
request.session[:impersonated_user_id] = user.uuid
end
private
def with_browser_locale(&)
locale = request.env['HTTP_ACCEPT_LANGUAGE'].to_s[BROWSER_LOCALE_REGEXP].to_s
locale =
if locale.starts_with?('en-') && locale != 'en-US'
'en-GB'
else
locale.split('-').first.presence || 'en-GB'
end
locale = 'en-GB' unless I18n.locale_available?(locale)
I18n.with_locale(locale, &)
end
def sign_in_for_demo
sign_in(User.active.order('random()').take) unless signed_in?
sign_in(User.first) unless signed_in?
end
def current_account
@@ -72,9 +34,8 @@ class ApplicationController < ActionController::Base
redirect_to setup_index_path unless User.exists?
end
def button_title(title: 'Submit', disabled_with: 'Submitting', title_class: '', icon: nil, icon_disabled: nil)
render_to_string(partial: 'shared/button_title',
locals: { title:, disabled_with:, title_class:, icon:, icon_disabled: })
def button_title(title: 'Submit', disabled_with: 'Submitting', icon: nil)
render_to_string(partial: 'shared/button_title', locals: { title:, disabled_with:, icon: })
end
def svg_icon(icon_name, class: '')
@@ -1,18 +0,0 @@
# frozen_string_literal: true
class ConsoleRedirectController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
def index
return redirect_to(new_user_session_path({ redir: params[:redir] }.compact)) if true_user.blank?
auth = JsonWebToken.encode(uuid: true_user.uuid,
scope: :console,
exp: 1.minute.from_now.to_i)
path = Addressable::URI.parse(params[:redir]).path if params[:redir].to_s.starts_with?(Docuseal::CONSOLE_URL)
redirect_to("#{Docuseal::CONSOLE_URL}#{path}?#{{ auth: }.to_query}", allow_other_host: true)
end
end
+3 -78
View File
@@ -3,86 +3,11 @@
class DashboardController < ApplicationController
skip_before_action :authenticate_user!, only: %i[index]
before_action :maybe_redirect_product_url
before_action :maybe_render_landing
before_action :maybe_redirect_mfa_setup
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)
return render 'pages/landing' unless signed_in?
@pagy, @template_folders = pagy(
@template_folders,
items: FOLDERS_PER_PAGE,
page: @template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1
)
templates = current_account.templates.active.preload(:author).order(id: :desc)
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?
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
Templates.search(rel, params[:q])
end
def maybe_redirect_product_url
return if !Docuseal.multitenant? || signed_in?
redirect_to Docuseal::PRODUCT_URL, allow_other_host: true
end
def maybe_redirect_mfa_setup
return unless signed_in?
return if current_user.otp_required_for_login
return if !current_user.otp_required_for_login && !AccountConfig.exists?(value: true,
account_id: current_user.account_id,
key: AccountConfig::FORCE_MFA)
redirect_to mfa_setup_path, notice: 'Setup 2FA to continue'
end
def maybe_render_landing
return if signed_in?
render 'pages/landing'
@pagy, @templates = pagy(templates, items: 12)
end
end
@@ -1,24 +1,16 @@
# frozen_string_literal: true
class EmailSmtpSettingsController < ApplicationController
class EmailSettingsController < ApplicationController
before_action :load_encrypted_config
authorize_resource :encrypted_config, only: :index
authorize_resource :encrypted_config, parent: false, only: :create
def index; end
def create
if @encrypted_config.update(email_configs)
SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email']).deliver_now!
if @encrypted_config.update(storage_configs)
redirect_to settings_email_index_path, notice: 'Changes have been saved'
else
render :index, status: :unprocessable_entity
end
rescue StandardError => e
flash[:alert] = e.message
render :index, status: :unprocessable_entity
end
private
@@ -28,7 +20,7 @@ class EmailSmtpSettingsController < ApplicationController
EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::EMAIL_SMTP_KEY)
end
def email_configs
def storage_configs
params.require(:encrypted_config).permit(value: {}).tap do |e|
e[:value].compact_blank!
end
-22
View File
@@ -1,22 +0,0 @@
# frozen_string_literal: true
class EnquiriesController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
def create
if params[:talk_to_sales] == 'on'
Faraday.post(Docuseal::ENQUIRIES_URL,
enquiry_params.merge(type: :talk_to_sales).to_json,
'Content-Type' => 'application/json')
end
head :ok
end
private
def enquiry_params
params.require(:user).permit(:email)
end
end
-39
View File
@@ -1,39 +0,0 @@
# frozen_string_literal: true
class ErrorsController < ActionController::Base
ENTERPRISE_FEATURE_MESSAGE =
'This feature is available in Pro Edition: https://www.docuseal.co/pricing'
ENTERPRISE_PATHS = [
'/templates/html',
'/api/templates/html',
'/templates/pdf',
'/api/templates/pdf',
'/templates/doc',
'/api/templates/doc',
'/templates/docx',
'/api/templates/docx'
].freeze
def show
if request.original_fullpath.in?(ENTERPRISE_PATHS) && error_status_code == 404
return render json: { status: 404, message: ENTERPRISE_FEATURE_MESSAGE }, status: :not_found
end
respond_to do |f|
f.json do
render json: { status: error_status_code }, status: error_status_code
end
f.html { render error_status_code.to_s, status: error_status_code }
end
end
private
def error_status_code
@error_status_code ||=
ActionDispatch::ExceptionWrapper.new(request.env,
request.env['action_dispatch.exception']).status_code
end
end
+10 -104
View File
@@ -1,113 +1,19 @@
# frozen_string_literal: true
class EsignSettingsController < ApplicationController
DEFAULT_CERT_NAME = 'DocuSeal Self-Host Autogenerated'
CertFormRecord = Struct.new(:name, :file, :password, keyword_init: true) do
include ActiveModel::Validations
def to_key
[]
end
end
before_action :load_encrypted_config
authorize_resource :encrypted_config, parent: false, only: %i[new create]
authorize_resource :encrypted_config, only: %i[update destroy show]
def show
cert_data = @encrypted_config.value || {}
default_pkcs = GenerateCertificate.load_pkcs(cert_data) if cert_data['cert'].present?
custom_pkcs_list = (cert_data['custom'] || []).map do |e|
{ 'pkcs' => OpenSSL::PKCS12.new(Base64.urlsafe_decode64(e['data']), e['password'].to_s),
'name' => e['name'],
'status' => e['status'] }
end
@pkcs_list = [
if default_pkcs
{
'pkcs' => default_pkcs,
'name' => DEFAULT_CERT_NAME,
'status' => custom_pkcs_list.any? { |e| e['status'] == 'default' } ? 'validate' : 'default'
}
end,
*custom_pkcs_list
].compact.reverse
end
def new
@cert_record = CertFormRecord.new
end
def create
@cert_record = CertFormRecord.new(**cert_params)
pdfs =
params[:files].map do |file|
HexaPDF::Document.new(io: file.open)
end
if (@encrypted_config.value && @encrypted_config.value['custom']&.any? { |e| e['name'] == @cert_record.name }) ||
@cert_record.name == DEFAULT_CERT_NAME
certs = Accounts.load_signing_certs(current_account)
@cert_record.errors.add(:name, 'already exists')
trusted_certs = [certs[:cert], certs[:sub_ca], certs[:root_ca]]
return render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'),
status: :unprocessable_entity
end
save_new_cert!(@encrypted_config, @cert_record)
redirect_to settings_esign_path, notice: 'Certificate has been successfully added!'
rescue OpenSSL::PKCS12::PKCS12Error
@cert_record.errors.add(:password, "is invalid. Make sure you're uploading a valid .p12 file")
render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), status: :unprocessable_entity
end
def update
@encrypted_config.value['custom'].each { |e| e['status'] = 'validate' }
custom_cert_data = @encrypted_config.value['custom'].find { |e| e['name'] == params[:name] }
custom_cert_data['status'] = 'default' if custom_cert_data
@encrypted_config.save!
redirect_to settings_esign_path, notice: 'Default certificate has been selected'
end
def destroy
@encrypted_config.value['custom'].reject! { |e| e['name'] == params[:name] }
@encrypted_config.save!
redirect_to settings_esign_path, notice: 'Certificate has been removed'
end
private
def load_encrypted_config
@encrypted_config = EncryptedConfig.find_or_initialize_by(account: current_account,
key: EncryptedConfig::ESIGN_CERTS_KEY)
end
def save_new_cert!(cert_configs, cert_record)
pkcs = OpenSSL::PKCS12.new(cert_record.file.read, cert_record.password)
cert_configs.value ||= {}
cert_configs.value['custom'] ||= []
cert_configs.value['custom'].each { |e| e['status'] = 'validate' }
cert_configs.value['custom'] << {
data: Base64.urlsafe_encode64(pkcs.to_der),
password: cert_record.password,
name: cert_record.name,
status: 'default'
}
cert_configs.save!
end
def cert_params
return {} if params[:esign_settings_controller_cert_form_record].blank?
params.require(:esign_settings_controller_cert_form_record).permit(:name, :file, :password)
render turbo_stream: turbo_stream.replace('result', partial: 'result',
locals: { pdfs:, files: params[:files], trusted_certs: })
rescue HexaPDF::MalformedPDFError
render turbo_stream: turbo_stream.replace('result', html: helpers.tag.div('Invalid PDF', id: 'result'))
end
end
-54
View File
@@ -1,54 +0,0 @@
# frozen_string_literal: true
class MfaSetupController < ApplicationController
before_action do
authorize!(:update, current_user)
end
before_action :set_provision_url, only: %i[show new]
def show; end
def new; end
def edit; end
def create
if current_user.validate_and_consume_otp!(params[:otp_attempt])
current_user.otp_required_for_login = true
current_user.save!
redirect_to settings_profile_index_path, notice: '2FA has been configured'
else
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name)
@error_message = 'Code is invalid'
render turbo_stream: turbo_stream.replace(:mfa_form, partial: 'mfa_setup/form'), status: :unprocessable_entity
end
end
def destroy
if current_user.validate_and_consume_otp!(params[:otp_attempt])
current_user.update!(otp_required_for_login: false, otp_secret: nil)
redirect_to settings_profile_index_path, notice: '2FA has been removed'
else
@error_message = 'Code is invalid'
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_entity
end
end
private
def set_provision_url
return redirect_to root_path, alert: '2FA has been set up already' if current_user.otp_required_for_login
current_user.otp_secret ||= User.generate_otp_secret
current_user.save!
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name)
end
end
@@ -1,8 +1,6 @@
# frozen_string_literal: true
class NewslettersController < ApplicationController
skip_authorization_check
def show; end
def update
@@ -1,46 +0,0 @@
# frozen_string_literal: true
class NotificationsSettingsController < ApplicationController
before_action :load_bcc_config, only: :index
before_action :load_reminder_config, only: :index
authorize_resource :bcc_config, only: :index
authorize_resource :reminder_config, only: :index
before_action :build_account_config, only: :create
authorize_resource :account_config, only: :create
def index; end
def create
if @account_config.value.present? ? @account_config.save : @account_config.delete
redirect_back fallback_location: settings_notifications_path, notice: 'Changes have been saved'
else
redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
end
end
private
def build_account_config
@account_config =
AccountConfig.find_or_initialize_by(account: current_account, key: email_config_params[:key])
@account_config.assign_attributes(email_config_params)
end
def load_bcc_config
@bcc_config =
AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::BCC_EMAILS)
end
def load_reminder_config
@reminder_config =
AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::SUBMITTER_REMAILERS)
end
def email_config_params
params.require(:account_config).permit!.tap do |attrs|
attrs[:key] = nil unless attrs[:key].in?([AccountConfig::BCC_EMAILS, AccountConfig::SUBMITTER_REMAILERS])
end
end
end
-13
View File
@@ -1,13 +0,0 @@
# frozen_string_literal: true
class PasswordsController < Devise::PasswordsController
class Current < ActiveSupport::CurrentAttributes
attribute :user
end
def update
super do |resource|
Current.user = resource
end
end
end
@@ -1,34 +0,0 @@
# frozen_string_literal: true
class PersonalizationSettingsController < ApplicationController
def show
authorize!(:read, AccountConfig)
end
def create
account_config =
current_account.account_configs.find_or_initialize_by(key: account_config_params[:key])
authorize!(:create, account_config)
account_config.update!(account_config_params)
redirect_back(fallback_location: settings_personalization_path, notice: 'Settings have been saved.')
end
private
def account_config_params
attrs = params.require(:account_config).permit!
attrs[:value]&.transform_values! do |value|
if value.in?(%w[true false])
value == 'true'
else
value
end
end
attrs
end
end
@@ -1,46 +0,0 @@
# frozen_string_literal: true
class PreviewDocumentPageController < ActionController::API
include ActiveStorage::SetCurrent
FORMAT = Templates::ProcessDocument::FORMAT
def show
attachment = ActiveStorage::Attachment.find_by(uuid: params[:attachment_uuid])
return head :not_found unless attachment
preview_image = attachment.preview_images.joins(:blob).find_by(blob: { filename: "#{params[:id]}#{FORMAT}" })
return redirect_to preview_image.url, allow_other_host: true if preview_image
file_path =
if attachment.service.name == :disk
ActiveStorage::Blob.service.path_for(attachment.key)
else
find_or_create_document_tempfile_path(attachment)
end
io = Templates::ProcessDocument.generate_pdf_preview_from_file(attachment, file_path, params[:id].to_i)
render plain: io.tap(&:rewind).read, content_type: 'image/jpeg'
end
def find_or_create_document_tempfile_path(attachment)
file_path = "#{Dir.tmpdir}/#{attachment.uuid}"
File.open(file_path, File::RDWR | File::CREAT, 0o644) do |f|
f.flock(File::LOCK_EX)
# rubocop:disable Style/ZeroLengthPredicate
if f.size.zero?
f.binmode
f.write(attachment.download)
end
# rubocop:enable Style/ZeroLengthPredicate
end
file_path
end
end
-4
View File
@@ -1,10 +1,6 @@
# frozen_string_literal: true
class ProfileController < ApplicationController
before_action do
authorize!(:manage, current_user)
end
def index; end
def update_contact
+2 -40
View File
@@ -1,62 +1,24 @@
# frozen_string_literal: true
class RegistrationsController < Devise::RegistrationsController
prepend_before_action :require_no_authentication, only: [:show]
prepend_before_action :maybe_redirect_if_signed_in, only: [:show]
def show; end
def create
super
Accounts.create_default_template(resource.account) if resource.account.persisted?
end
private
def after_sign_up_path_for(...)
if params[:redir].present?
return console_redirect_index_path(redir: params[:redir]) if params[:redir].starts_with?(Docuseal::CONSOLE_URL)
return params[:redir]
end
super
end
def maybe_redirect_if_signed_in
return unless signed_in?
return if params[:redir].blank?
redirect_to after_sign_up_path_for(current_user), allow_other_host: true
end
def set_flash_message(key, kind, options = {})
return if key == :alert && kind == 'already_authenticated'
super
end
def build_resource(_hash = {})
account = Account.new(account_params)
account.timezone = Accounts.normalize_timezone(account.timezone)
self.resource = account.users.new(user_params)
account.name ||= resource.full_name if params[:action] == 'create'
end
def user_params
return {} if params[:user].blank?
params.require(:user).permit(:first_name, :last_name, :email, :password).compact_blank.tap do |attrs|
attrs[:password] ||= SecureRandom.hex if params[:action] == 'create'
end
params.require(:user).permit(:first_name, :last_name, :email, :password)
end
def account_params
return {} if params[:account].blank?
params.require(:account).permit(:name, :timezone).compact_blank
params.require(:account).permit(:name, :timezone)
end
end
@@ -5,7 +5,6 @@ class SendSubmissionEmailController < ApplicationController
skip_before_action :authenticate_user!
skip_before_action :verify_authenticity_token
skip_authorization_check
def success; end
-40
View File
@@ -1,40 +0,0 @@
# frozen_string_literal: true
class SessionsController < Devise::SessionsController
before_action :configure_permitted_parameters
def create
if Docuseal.multitenant? && !User.exists?(email: sign_in_params[:email])
return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)),
notice: 'Create a new account'
end
if User.exists?(email: sign_in_params[:email], otp_required_for_login: true) && sign_in_params[:otp_attempt].blank?
return render :otp, locals: { resource: User.new(sign_in_params) }, status: :unprocessable_entity
end
super
end
private
def after_sign_in_path_for(...)
if params[:redir].present?
return console_redirect_index_path(redir: params[:redir]) if params[:redir].starts_with?(Docuseal::CONSOLE_URL)
return params[:redir]
end
super
end
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt])
end
def set_flash_message(key, kind, options = {})
return if key == :alert && kind == 'already_authenticated'
super
end
end
+2 -4
View File
@@ -3,7 +3,6 @@
class SetupController < ApplicationController
skip_before_action :maybe_redirect_to_setup
skip_before_action :authenticate_user!
skip_authorization_check
before_action :redirect_to_root_if_signed, if: :signed_in?
before_action :ensure_first_user_not_created!
@@ -17,17 +16,16 @@ class SetupController < ApplicationController
def create
@account = Account.new(account_params)
@account.timezone = Accounts.normalize_timezone(@account.timezone)
@user = @account.users.new(user_params)
@encrypted_config = EncryptedConfig.new(encrypted_config_params)
unless URI.parse(encrypted_config_params[:value].to_s).class.in?([URI::HTTP, URI::HTTPS])
@encrypted_config = EncryptedConfig.new(encrypted_config_params)
@encrypted_config.errors.add(:value, 'should be a valid URL')
return render :index, status: :unprocessable_entity
end
return render :index, status: :unprocessable_entity unless @account.valid?
if @user.save
encrypted_configs = [
{ key: EncryptedConfig::APP_URL_KEY, value: encrypted_config_params[:value] },
@@ -1,16 +0,0 @@
# frozen_string_literal: true
class SmsSettingsController < ApplicationController
before_action :load_encrypted_config
authorize_resource :encrypted_config, only: :index
authorize_resource :encrypted_config, parent: false, except: :index
def index; end
private
def load_encrypted_config
@encrypted_config =
EncryptedConfig.find_or_initialize_by(account: current_account, key: 'sms_configs')
end
end
@@ -1,16 +0,0 @@
# frozen_string_literal: true
class SsoSettingsController < ApplicationController
before_action :load_encrypted_config
authorize_resource :encrypted_config, only: :index
authorize_resource :encrypted_config, parent: false, except: :index
def index; end
private
def load_encrypted_config
@encrypted_config =
EncryptedConfig.find_or_initialize_by(account: current_account, key: 'saml_configs')
end
end
+12 -49
View File
@@ -4,32 +4,28 @@ class StartFormController < ApplicationController
layout 'form'
skip_before_action :authenticate_user!
skip_authorization_check
around_action :with_browser_locale, only: %i[show completed]
before_action :load_template
def show
@submitter = @template.submissions.new(account_id: @template.account_id)
.submitters.new(uuid: @template.submitters.first['uuid'])
@submitter = @template.submissions.new.submitters.new(uuid: @template.submitters.first['uuid'])
end
def update
@submitter = Submitter.where(submission: @template.submissions.where(archived_at: nil))
.order(id: :desc)
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
.find_or_initialize_by(**submitter_params.compact_blank)
@submitter = Submitter.where(submission: @template.submissions.where(deleted_at: nil))
.find_or_initialize_by(email: submitter_params[:email])
if @submitter.completed_at?
redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email])
else
if @template.submitters.to_a.size > 1 && @submitter.new_record?
@error_message = 'Not found'
@submitter.assign_attributes(
uuid: @template.submitters.first['uuid'],
opened_at: Time.current,
ip: request.remote_ip,
ua: request.user_agent
)
return render :show
end
assign_submission_attributes(@submitter, @template) if @submitter.new_record?
@submitter.submission ||= Submission.new(template: @template)
if @submitter.save
redirect_to submit_form_path(@submitter.slug)
@@ -40,46 +36,13 @@ class StartFormController < ApplicationController
end
def completed
@submitter = Submitter.where(submission: @template.submissions)
.where.not(completed_at: nil)
.find_by!(email: params[:email])
@submitter = Submitter.where(submission: @template.submissions).find_by!(email: params[:email])
end
private
def assign_submission_attributes(submitter, template)
resubmit_submitter =
if params[:resubmit].present?
Submitter.where(submission: @template.submissions).find_by(slug: params[:resubmit])
end
submitter.assign_attributes(
uuid: template.submitters.first['uuid'],
ip: request.remote_ip,
ua: request.user_agent,
values: resubmit_submitter&.preferences&.fetch('default_values', nil) || {},
preferences: resubmit_submitter&.preferences.presence || { 'send_email' => true },
metadata: resubmit_submitter&.metadata.presence || {}
)
if submitter.values.present?
resubmit_submitter.attachments.each do |attachment|
submitter.attachments << attachment.dup if submitter.values.value?(attachment.uuid)
end
end
submitter.submission ||= Submission.new(template:,
account_id: template.account_id,
template_submitters: template.submitters,
source: :link)
submitter
end
def submitter_params
params.require(:submitter).permit(:email, :phone, :name).tap do |attrs|
attrs[:email] = Submissions.normalize_email(attrs[:email])
end
params.require(:submitter).permit(:email)
end
def load_template
@@ -2,8 +2,6 @@
class StorageSettingsController < ApplicationController
before_action :load_encrypted_config
authorize_resource :encrypted_config, only: :index
authorize_resource :encrypted_config, parent: false, only: :create
def index; end
+45 -48
View File
@@ -2,79 +2,76 @@
class SubmissionsController < ApplicationController
before_action :load_template, only: %i[new create]
authorize_resource :template, only: %i[new create]
load_and_authorize_resource :submission, only: %i[show destroy]
PRELOAD_ALL_PAGES_AMOUNT = 200
def show
ActiveRecord::Associations::Preloader.new(
records: [@submission],
associations: [:template, { template_schema_documents: :blob }]
).call
total_pages =
@submission.template_schema_documents.sum { |e| e.metadata.dig('pdf', 'number_of_pages').to_i }
if total_pages < PRELOAD_ALL_PAGES_AMOUNT
ActiveRecord::Associations::Preloader.new(
records: @submission.template_schema_documents,
associations: [:blob, { preview_images_attachments: :blob }]
).call
end
@submission =
Submission.joins(:template).where(template: { account_id: current_account.id })
.preload(template: { documents_attachments: { preview_images_attachments: :blob } })
.find(params[:id])
render :show, layout: 'plain'
end
def new
authorize!(:new, Submission)
end
def new; end
def create
authorize!(:create, Submission)
if params[:is_custom_message] != '1'
params.delete(:subject)
params.delete(:body)
end
submissions =
if params[:emails].present?
Submissions.create_from_emails(template: @template,
user: current_user,
source: :invite,
mark_as_sent: params[:send_email] == '1',
emails: params[:emails],
params: params.merge('send_completed_email' => true))
create_submissions_from_emails
else
Submissions.create_from_submitters(template: @template,
user: current_user,
source: :invite,
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
mark_as_sent: params[:send_email] == '1',
submissions_attrs: submissions_params[:submission].to_h.values,
params: params.merge('send_completed_email' => true))
create_submissions_from_submitters
end
Submissions.send_signature_requests(submissions)
if params[:send_email] == '1'
submissions.flat_map(&:submitters).each do |submitter|
SubmitterMailer.invitation_email(submitter, message: params[:message]).deliver_later!
end
end
redirect_to template_path(@template), notice: 'New recipients have been added'
redirect_to template_path(@template),
notice: "#{submissions.size} #{'recipient'.pluralize(submissions.size)} added"
end
def destroy
@submission.update!(archived_at: Time.current)
submission = Submission.joins(:template).where(template: { account_id: current_account.id })
.find(params[:id])
redirect_back(fallback_location: template_path(@submission.template), notice: 'Submission has been archived')
submission.update!(deleted_at: Time.current)
redirect_back(fallback_location: template_path(submission.template), notice: 'Submission has been archived')
end
private
def create_submissions_from_emails
emails = params[:emails].to_s.scan(User::EMAIL_REGEXP)
emails.map do |email|
submission = @template.submissions.new(created_by_user: current_user)
submission.submitters.new(email:, uuid: @template.submitters.first['uuid'],
sent_at: params[:send_email] == '1' ? Time.current : nil)
submission.tap(&:save!)
end
end
def create_submissions_from_submitters
submissions_params[:submission].to_h.map do |_, attrs|
submission = @template.submissions.new(created_by_user: current_user)
attrs[:submitters].each do |submitter_attrs|
submission.submitters.new(**submitter_attrs, sent_at: params[:send_email] == '1' ? Time.current : nil)
end
submission.tap(&:save!)
end
end
def submissions_params
params.permit(submission: { submitters: [%i[uuid email phone name]] })
params.permit(submission: { submitters: [%i[uuid email]] })
end
def load_template
@template = Template.accessible_by(current_ability).find(params[:template_id])
@template = current_account.templates.find(params[:template_id])
end
end
@@ -4,7 +4,6 @@ class SubmissionsDebugController < ApplicationController
layout 'plain'
skip_before_action :authenticate_user!
skip_authorization_check
def index
@submitter = Submitter.preload({ attachments_attachments: :blob },
@@ -16,13 +15,9 @@ class SubmissionsDebugController < ApplicationController
render 'submit_form/show'
end
f.pdf do
if params[:audit]
Submissions::GenerateAuditTrail.call(@submitter.submission)
else
Submissions::GenerateResultAttachments.call(@submitter)
end
Submissions::GenerateResultAttachments.call(@submitter)
send_data ActiveStorage::Attachment.where(name: params[:audit] ? :audit_trail : :documents).last.download,
send_data ActiveStorage::Attachment.where(name: :documents).last.download,
filename: 'debug.pdf',
disposition: 'inline',
type: 'application/pdf'
@@ -2,19 +2,14 @@
class SubmissionsDownloadController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
def index
submitter = Submitter.find_by(slug: params[:submitter_slug])
Submissions::EnsureResultGenerated.call(submitter)
last_submitter = submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
Submissions::EnsureResultGenerated.call(last_submitter)
urls =
Submitters.select_attachments_for_download(last_submitter).map do |attachment|
Submitters.select_attachments_for_download(submitter).map do |attachment|
helpers.rails_blob_url(attachment)
end
@@ -1,23 +0,0 @@
# frozen_string_literal: true
class SubmissionsExportController < ApplicationController
load_and_authorize_resource :template
load_and_authorize_resource :submission, through: :template, parent: false, only: :index
def index
submissions = @submissions.active
.preload(submitters: { documents_attachments: :blob,
attachments_attachments: :blob })
.order(id: :asc)
if params[:format] == 'csv'
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format]),
filename: "#{@template.name}.csv"
elsif params[:format] == 'xlsx'
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format]),
filename: "#{@template.name}.xlsx"
end
end
def new; end
end
@@ -1,29 +0,0 @@
# frozen_string_literal: true
class SubmissionsPreviewController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
PRELOAD_ALL_PAGES_AMOUNT = 200
def show
@submission = Submission.find_by!(slug: params[:slug])
ActiveRecord::Associations::Preloader.new(
records: [@submission],
associations: [:template, { template_schema_documents: :blob }]
).call
total_pages =
@submission.template_schema_documents.sum { |e| e.metadata.dig('pdf', 'number_of_pages').to_i }
if total_pages < PRELOAD_ALL_PAGES_AMOUNT
ActiveRecord::Associations::Preloader.new(
records: @submission.template_schema_documents,
associations: [:blob, { preview_images_attachments: :blob }]
).call
end
render 'submissions/show', layout: 'plain'
end
end
+24 -36
View File
@@ -3,62 +3,50 @@
class SubmitFormController < ApplicationController
layout 'form'
around_action :with_browser_locale, only: %i[show completed success]
skip_before_action :authenticate_user!
skip_authorization_check
PRELOAD_ALL_PAGES_AMOUNT = 200
def show
@submitter = Submitter.find_by!(slug: params[:slug])
@submitter =
Submitter.preload(submission: { template: { documents_attachments: { preview_images_attachments: :blob } } })
.find_by!(slug: params[:slug])
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
ActiveRecord::Associations::Preloader.new(
records: [@submitter],
associations: [submission: [:template, { template_schema_documents: :blob }]]
).call
total_pages =
@submitter.submission.template_schema_documents.sum { |e| e.metadata.dig('pdf', 'number_of_pages').to_i }
if total_pages < PRELOAD_ALL_PAGES_AMOUNT
ActiveRecord::Associations::Preloader.new(
records: @submitter.submission.template_schema_documents,
associations: [:blob, { preview_images_attachments: :blob }]
).call
end
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
cookies[:submitter_sid] = @submitter.signed_id
render(@submitter.submission.template.archived_at? || @submitter.submission.archived_at? ? :archived : :show)
end
def update
submitter = Submitter.find_by!(slug: params[:slug])
submitter.values.merge!(normalized_values)
submitter.completed_at = Time.current if params[:completed] == 'true'
submitter.opened_at ||= Time.current
submitter.save!
if submitter.completed_at?
return render json: { error: 'Form has been completed already.' }, status: :unprocessable_entity
GenerateSubmitterResultAttachmentsJob.perform_later(submitter)
submitter.submission.template.account.users.active.each do |user|
SubmitterMailer.completed_email(submitter, user).deliver_later!
end
end
if submitter.template.archived_at? || submitter.submission.archived_at?
Rollbar.info("Archived template: #{submitter.template.id}") if defined?(Rollbar)
return render json: { error: 'Form has been archived.' }, status: :unprocessable_entity
end
Submitters::SubmitValues.call(submitter, params, request)
head :ok
rescue Submitters::SubmitValues::ValidationError => e
render json: { error: e.message }, status: :unprocessable_entity
end
def completed
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
end
def success; end
private
def normalized_values
params.fetch(:values, {}).to_unsafe_h.transform_values do |v|
if params[:cast_boolean] == 'true'
v == 'true'
else
v.is_a?(Array) ? v.compact_blank : v
end
end
end
end
@@ -1,21 +0,0 @@
# frozen_string_literal: true
class SubmittersSendEmailController < ApplicationController
load_and_authorize_resource :submitter, id_param: :submitter_slug, find_by: :slug
def create
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
event_type: 'send_email',
created_at: 24.hours.ago..Time.current)
return redirect_back(fallback_location: submission_path(@submitter.submission),
alert: 'Email has been sent already.')
end
SendSubmitterInvitationEmailJob.perform_later('submitter_id' => @submitter.id)
@submitter.sent_at ||= Time.current
@submitter.save!
redirect_back(fallback_location: submission_path(@submitter.submission), notice: 'Email has been sent')
end
end
@@ -1,29 +0,0 @@
# frozen_string_literal: true
class TemplateFoldersController < ApplicationController
load_and_authorize_resource :template_folder
def show
@templates = @template_folder.templates.active.preload(:author).order(id: :desc)
@templates = Templates.search(@templates, params[:q])
@pagy, @templates = pagy(@templates, items: 12)
end
def edit; end
def update
if @template_folder != current_account.default_template_folder &&
@template_folder.update(template_folder_params)
redirect_to folder_path(@template_folder), notice: 'Folder name has been updated'
else
redirect_to folder_path(@template_folder), alert: 'Unable to rename folder'
end
end
private
def template_folder_params
params.require(:template_folder).permit(:name)
end
end
@@ -1,21 +0,0 @@
# frozen_string_literal: true
class TemplateSharingsTestingController < ApplicationController
load_and_authorize_resource :template, parent: true
before_action do
authorize!(:manage, TemplateSharing.new(template: @template))
end
def create
testing_account = Accounts.find_or_create_testing_user(true_user.account).account
if params[:value] == '1'
TemplateSharing.create!(ability: :manage, account: testing_account, template: @template)
else
TemplateSharing.find_by(template: @template, account: testing_account)&.destroy!
end
head :ok
end
end
@@ -1,12 +0,0 @@
# frozen_string_literal: true
class TemplatesArchivedController < ApplicationController
load_and_authorize_resource :template, parent: false
def index
@templates = @templates.where.not(archived_at: nil).preload(:author, :folder).order(id: :desc)
@templates = Templates.search(@templates, params[:q])
@pagy, @templates = pagy(@templates, items: 12)
end
end
@@ -1,15 +0,0 @@
# frozen_string_literal: true
class TemplatesArchivedSubmissionsController < ApplicationController
load_and_authorize_resource :template
load_and_authorize_resource :submission, through: :template, parent: false
def index
@submissions = @submissions.where.not(archived_at: nil)
@submissions = Submissions.search(@submissions, params[:q])
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
rescue ActiveRecord::RecordNotFound
redirect_to root_path
end
end
@@ -1,7 +0,0 @@
# frozen_string_literal: true
class TemplatesCodeModalController < ApplicationController
load_and_authorize_resource :template
def show; end
end
+14 -47
View File
@@ -1,57 +1,32 @@
# frozen_string_literal: true
class TemplatesController < ApplicationController
load_and_authorize_resource :template
before_action :load_base_template, only: %i[new create]
def show
submissions = @template.submissions.accessible_by(current_ability)
submissions = submissions.active if @template.archived_at.blank?
submissions = Submissions.search(submissions, params[:q])
@template = current_account.templates.active.find(params[:id])
@base_submissions = submissions
submissions = submissions.pending if params[:status] == 'pending'
submissions = submissions.completed if params[:status] == 'completed'
@pagy, @submissions = pagy(submissions.preload(:submitters).order(id: :desc))
@pagy, @submissions = pagy(@template.submissions.active.preload(:submitters).order(id: :desc))
rescue ActiveRecord::RecordNotFound
redirect_to root_path
end
def new
@template = current_account.templates.new
@template.name = "#{@base_template.name} (Clone)" if @base_template
end
def edit
ActiveRecord::Associations::Preloader.new(
records: [@template],
associations: [schema_documents: { preview_images_attachments: :blob }]
).call
@template_data =
@template.as_json.merge(
documents: @template.schema_documents.as_json(
methods: [:metadata],
include: { preview_images: { methods: %i[url metadata filename] } }
)
).to_json
@template = current_account.templates.preload(documents_attachments: { preview_images_attachments: :blob })
.find(params[:id])
render :edit, layout: 'plain'
end
def create
if @base_template
@template = Templates::Clone.call(@base_template, author: current_user,
name: params.dig(:template, :name),
folder_name: params[:folder_name])
else
@template.author = current_user
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
end
@template.account = current_account
@template = current_account.templates.new(template_params)
@template.author = current_user
@template.assign_attributes(@base_template.slice(:fields, :schema, :submitters)) if @base_template
if @template.save
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
@@ -63,20 +38,10 @@ class TemplatesController < ApplicationController
end
def destroy
notice =
if params[:permanently].present?
@template.destroy!
@template = current_account.templates.find(params[:id])
@template.update!(deleted_at: Time.current)
Rollbar.info("Remove template: #{@template.id}") if defined?(Rollbar)
'Template has been removed.'
else
@template.update!(archived_at: Time.current)
'Template has been archived.'
end
redirect_back(fallback_location: root_path, notice:)
redirect_back(fallback_location: root_path, notice: 'Template has been archived.')
end
private
@@ -88,6 +53,8 @@ class TemplatesController < ApplicationController
def load_base_template
return if params[:base_template_id].blank?
@base_template = Template.accessible_by(current_ability).find_by(id: params[:base_template_id])
@base_template = current_account.templates
.preload(documents_attachments: :preview_images_attachments)
.find_by(id: params[:base_template_id])
end
end
@@ -1,23 +0,0 @@
# frozen_string_literal: true
class TemplatesFoldersController < ApplicationController
load_and_authorize_resource :template
def edit; end
def update
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:name])
if @template.save
redirect_back(fallback_location: template_path(@template), notice: 'Document template has been moved')
else
redirect_back(fallback_location: template_path(@template), notice: 'Unable to move template into folder')
end
end
private
def template_folder_params
params.require(:template_folder).permit(:name)
end
end
@@ -1,22 +0,0 @@
# frozen_string_literal: true
class TemplatesPreviewController < ApplicationController
load_and_authorize_resource :template
def show
ActiveRecord::Associations::Preloader.new(
records: [@template],
associations: [schema_documents: { preview_images_attachments: :blob }]
).call
@template_data =
@template.as_json.merge(
documents: @template.schema_documents.as_json(
methods: [:metadata],
include: { preview_images: { methods: %i[url metadata filename] } }
)
).to_json
render :show, layout: 'plain'
end
end
@@ -1,11 +0,0 @@
# frozen_string_literal: true
class TemplatesRestoreController < ApplicationController
load_and_authorize_resource :template
def create
@template.update!(archived_at: nil)
redirect_to template_path(@template), notice: 'Template has been unarchived'
end
end
@@ -1,65 +0,0 @@
# frozen_string_literal: true
class TemplatesUploadsController < ApplicationController
load_and_authorize_resource :template, parent: false
layout 'plain'
def show; end
def create
url_params = create_file_params_from_url if params[:url].present?
save_template!(@template, url_params)
documents = Templates::CreateAttachments.call(@template, url_params || params)
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
@template.update!(schema:)
redirect_to edit_template_path(@template)
rescue Templates::CreateAttachments::PdfEncrypted
render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password)
rescue StandardError => e
Rollbar.error(e) if defined?(Rollbar)
redirect_to root_path, alert: 'Unable to upload file'
end
private
def save_template!(template, url_params)
template.account = current_account
template.author = current_user
template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
template.name = File.basename((url_params || params)[:files].first.original_filename, '.*')
template.save!
template
end
def create_file_params_from_url
tempfile = Tempfile.new
tempfile.binmode
tempfile.write(conn.get(Addressable::URI.parse(params[:url]).display_uri.to_s).body)
tempfile.rewind
file = ActionDispatch::Http::UploadedFile.new(
tempfile:,
filename: File.basename(
URI.decode_www_form_component(params[:filename].presence || params[:url])
),
type: Marcel::MimeType.for(tempfile)
)
{ files: [file] }
end
def conn
Faraday.new do |faraday|
faraday.response :follow_redirects
end
end
end
@@ -1,20 +0,0 @@
# frozen_string_literal: true
class TestingAccountsController < ApplicationController
skip_authorization_check only: :destroy
def show
authorize!(:manage, current_account)
authorize!(:manage, current_user)
impersonate_user(Accounts.find_or_create_testing_user(true_user.account))
redirect_back(fallback_location: root_path)
end
def destroy
stop_impersonating_user
redirect_back(fallback_location: root_path)
end
end
@@ -1,12 +0,0 @@
# frozen_string_literal: true
class TestingApiSettingsController < ApplicationController
def index
authorize!(:manage, current_user.access_token)
@webhook_config =
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
authorize!(:manage, @webhook_config)
end
end
@@ -1,53 +0,0 @@
# frozen_string_literal: true
class TimestampServerController < ApplicationController
before_action :build_encrypted_config
authorize_resource :encrypted_config
def create
return head :not_found if Docuseal.multitenant?
test_timeserver_url(@encrypted_config.value) if @encrypted_config.value.present?
if @encrypted_config.value.present? ? @encrypted_config.save : @encrypted_config.delete
redirect_back fallback_location: settings_notifications_path, notice: 'Changes have been saved'
else
redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
end
rescue HexaPDF::Error, SocketError, Submissions::TimestampHandler::TimestampError, OpenSSL::Timestamp::TimestampError
redirect_back fallback_location: settings_notifications_path, alert: 'Invalid Timeserver'
end
private
def test_timeserver_url(url)
pdf = HexaPDF::Document.new
pdf.pages.add
pkcs = Accounts.load_signing_pkcs(current_account)
pdf.sign(StringIO.new,
reason: 'Test',
certificate: pkcs.certificate,
key: pkcs.key,
signature_size: 10_000,
certificate_chain: pkcs.ca_certs || [],
timestamp_handler: Submissions::TimestampHandler.new(tsa_url: url))
end
def load_encrypted_config
@encrypted_config
end
def build_encrypted_config
@encrypted_config =
EncryptedConfig.find_or_initialize_by(account: current_account,
key: EncryptedConfig::TIMESTAMP_SERVER_URL_KEY)
@encrypted_config.assign_attributes(encrypted_config_params)
end
def encrypted_config_params
params.require(:encrypted_config).permit(:value)
end
end
@@ -1,43 +0,0 @@
# frozen_string_literal: true
class UserSignaturesController < ApplicationController
before_action :load_user_config
authorize_resource :user_config
def edit; end
def update
file = params[:file]
return redirect_to settings_profile_index_path, notice: 'Unable to save signature' if file.blank?
blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
filename: file.original_filename,
content_type: file.content_type)
attachment = ActiveStorage::Attachment.create!(
blob:,
name: 'signature',
record: current_user
)
if @user_config.update(value: attachment.uuid)
redirect_to settings_profile_index_path, notice: 'Signature has been saved'
else
redirect_to settings_profile_index_path, notice: 'Unable to save signature'
end
end
def destroy
@user_config.destroy
redirect_to settings_profile_index_path, notice: 'Signature has been removed'
end
private
def load_user_config
@user_config =
UserConfig.find_or_initialize_by(user: current_user, key: UserConfig::SIGNATURE_KEY)
end
end
+20 -34
View File
@@ -1,70 +1,56 @@
# frozen_string_literal: true
class UsersController < ApplicationController
load_and_authorize_resource :user, only: %i[index edit new update destroy]
before_action :build_user, only: :create
authorize_resource :user, only: :create
before_action :load_user, only: %i[edit update destroy]
def index
@pagy, @users = pagy(@users.active.order(id: :desc))
@pagy, @users = pagy(current_account.users.active.order(id: :desc))
end
def new; end
def new
@user = current_account.users.new
end
def edit; end
def create
@user = current_account.users.find_by(email: user_params[:email])&.tap do |user|
user.assign_attributes(user_params)
user.deleted_at = nil
end
@user ||= current_account.users.new(user_params)
if @user.save
UserMailer.invitation_email(@user).deliver_later!
redirect_to settings_users_path, notice: 'User has been invited'
redirect_to settings_users_path, notice: 'User has been invited.'
else
render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity
end
end
def update
return redirect_to settings_users_path, notice: 'Unable to update user.' if Docuseal.demo?
attrs = user_params.compact_blank
attrs.delete(:role) if !role_valid?(attrs[:role]) || current_user == @user
if @user.update(attrs)
redirect_to settings_users_path, notice: 'User has been updated'
if @user.update(user_params.compact_blank)
redirect_to settings_users_path, notice: 'User has been updated.'
else
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity
end
end
def destroy
if Docuseal.demo? || @user.id == current_user.id
return redirect_to settings_users_path, notice: 'Unable to remove user'
end
@user.update!(deleted_at: Time.current)
@user.update!(archived_at: Time.current)
redirect_to settings_users_path, notice: 'User has been removed'
redirect_to settings_users_path, notice: 'User has been removed.'
end
private
def role_valid?(role)
User::ROLES.include?(role)
end
def build_user
@user = current_account.users.find_by(email: user_params[:email])&.tap do |user|
user.assign_attributes(user_params)
user.archived_at = nil
end
@user ||= current_account.users.new(user_params)
@user
def load_user
@user = current_account.users.find(params[:id])
end
def user_params
params.require(:user).permit(:email, :first_name, :last_name, :password, :role)
params.require(:user).permit(:email, :first_name, :last_name, :password)
end
end
@@ -1,34 +0,0 @@
# frozen_string_literal: true
class VerifyPdfSignatureController < ApplicationController
skip_authorization_check
def create
pdfs =
params[:files].map do |file|
HexaPDF::Document.new(io: file.open)
end
cert_data = if Docuseal.multitenant?
Docuseal::CERTS
else
EncryptedConfig.find_by(key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
end
default_pkcs = GenerateCertificate.load_pkcs(cert_data)
custom_certs = cert_data.fetch('custom', []).map do |e|
OpenSSL::PKCS12.new(Base64.urlsafe_decode64(e['data']), e['password'].to_s)
end
trusted_certs = [default_pkcs.certificate,
*default_pkcs.ca_certs,
*custom_certs.map(&:certificate),
*custom_certs.flat_map(&:ca_certs).compact]
render turbo_stream: turbo_stream.replace('result', partial: 'result',
locals: { pdfs:, files: params[:files], trusted_certs: })
rescue HexaPDF::MalformedPDFError
render turbo_stream: turbo_stream.replace('result', html: helpers.tag.div('Invalid PDF', id: 'result'))
end
end
@@ -1,35 +0,0 @@
# frozen_string_literal: true
class WebhookSettingsController < ApplicationController
before_action :load_encrypted_config
authorize_resource :encrypted_config, parent: false
def show; end
def create
@encrypted_config.assign_attributes(encrypted_config_params)
@encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook URL has been saved.')
end
def update
submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last
SendFormCompletedWebhookRequestJob.perform_later(submitter)
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook request has been sent.')
end
private
def load_encrypted_config
@encrypted_config =
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
end
def encrypted_config_params
params.require(:encrypted_config).permit(:value)
end
end
-40
View File
@@ -15,11 +15,6 @@ import DownloadButton from './elements/download_button'
import SetOriginUrl from './elements/set_origin_url'
import SetTimezone from './elements/set_timezone'
import AutoresizeTextarea from './elements/autoresize_textarea'
import SubmittersAutocomplete from './elements/submitter_autocomplete'
import FolderAutocomplete from './elements/folder_autocomplete'
import SignatureForm from './elements/signature_form'
import SubmitForm from './elements/submit_form'
import PromptPassword from './elements/prompt_password'
import * as TurboInstantClick from './lib/turbo_instant_click'
@@ -48,50 +43,15 @@ window.customElements.define('download-button', DownloadButton)
window.customElements.define('set-origin-url', SetOriginUrl)
window.customElements.define('set-timezone', SetTimezone)
window.customElements.define('autoresize-textarea', AutoresizeTextarea)
window.customElements.define('submitters-autocomplete', SubmittersAutocomplete)
window.customElements.define('folder-autocomplete', FolderAutocomplete)
window.customElements.define('signature-form', SignatureForm)
window.customElements.define('submit-form', SubmitForm)
window.customElements.define('prompt-password', PromptPassword)
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
document.addEventListener('turbo:submit-end', async (event) => {
const resp = event.detail?.formSubmission?.result?.fetchResponse?.response
if (!resp?.headers?.get('content-disposition')?.includes('attachment')) {
return
}
const url = URL.createObjectURL(await resp.blob())
const link = document.createElement('a')
link.href = url
link.setAttribute('download', decodeURIComponent(resp.headers.get('content-disposition').split('"')[1]))
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
})
window.customElements.define('template-builder', class extends HTMLElement {
connectedCallback () {
this.appElem = document.createElement('div')
this.appElem.classList.add('md:h-screen')
this.app = createApp(TemplateBuilder, {
template: reactive(JSON.parse(this.dataset.template)),
backgroundColor: '#faf7f5',
withPhone: this.dataset.withPhone === 'true',
withLogo: this.dataset.withLogo !== 'false',
editable: this.dataset.editable !== 'false',
withPayment: this.dataset.withPayment === 'true',
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
acceptFileTypes: this.dataset.acceptFileTypes,
isDirectUpload: this.dataset.isDirectUpload === 'true'
})
-55
View File
@@ -70,58 +70,3 @@ button[disabled] .enabled {
.base-radio {
@apply radio bg-white radio-sm no-animation;
}
.base-select {
@apply select base-input w-full font-normal;
}
.tooltip-bottom-start:before {
transform: translateX(-30%);
top: var(--tooltip-offset);
left: 100%;
right: auto;
bottom: auto;
}
.tooltip-bottom-start:after {
transform: translateX(-75%);
border-color: transparent transparent var(--tooltip-color) transparent;
top: var(--tooltip-tail-offset);
left: 50%;
right: auto;
bottom: auto;
}
.autocomplete {
background: white;
z-index: 1000;
font: 14px/22px "-apple-system", BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
overflow: auto;
box-sizing: border-box;
@apply border border-base-300 mt-1 rounded-md;
}
.autocomplete * {
font: inherit;
}
.autocomplete > div {
@apply px-2 py-1 font-normal text-sm;
}
.autocomplete .group {
background: #eee;
}
.autocomplete > div:hover:not(.group),
.autocomplete > div.selected {
@apply bg-base-300;
cursor: pointer;
}
.input-outlined {
outline-style: solid;
outline-width: 1px;
outline-offset: 3px;
outline-color: hsl(var(--bc) / 0.2);
}
+15 -52
View File
@@ -18,64 +18,27 @@ export default targetable(class extends HTMLElement {
this.toggleState()
fetch(this.dataset.src).then((response) => response.json()).then((urls) => {
const isSafariIos = /iPhone|iPad|iPod/i.test(navigator.userAgent)
const fileRequests = urls.map((url) => {
return () => {
return fetch(url).then(async (resp) => {
const blobUrl = URL.createObjectURL(await resp.blob())
const link = document.createElement('a')
if (isSafariIos && urls.length > 1) {
this.downloadSafariIos(urls)
} else {
this.downloadUrls(urls)
}
})
}
link.href = blobUrl
link.setAttribute('download', decodeURI(url.split('/').pop()))
downloadUrls (urls) {
const fileRequests = urls.map((url) => {
return () => {
return fetch(url).then(async (resp) => {
const blobUrl = URL.createObjectURL(await resp.blob())
const link = document.createElement('a')
link.click()
link.href = blobUrl
link.setAttribute('download', decodeURI(url.split('/').pop()))
link.click()
URL.revokeObjectURL(blobUrl)
})
}
})
fileRequests.reduce(
(prevPromise, request) => prevPromise.then(() => request()),
Promise.resolve()
)
this.toggleState()
}
downloadSafariIos (urls) {
const fileRequests = urls.map((url) => {
return fetch(url).then(async (resp) => {
const blob = await resp.blob()
const blobUrl = URL.createObjectURL(blob.slice(0, blob.size, 'application/octet-stream'))
const link = document.createElement('a')
link.href = blobUrl
link.setAttribute('download', decodeURI(url.split('/').pop()))
return link
URL.revokeObjectURL(url)
})
}
})
})
Promise.all(fileRequests).then((links) => {
links.forEach((link, index) => {
setTimeout(() => {
link.click()
fileRequests.reduce(
(prevPromise, request) => prevPromise.then(() => request()),
Promise.resolve()
)
URL.revokeObjectURL(link.href)
}, index * 50)
})
}).finally(() => {
this.toggleState()
})
}
+3 -9
View File
@@ -38,11 +38,7 @@ export default actionable(targetable(class extends HTMLElement {
this.uploadFiles(this.input.files)
}
toggleLoading = (e) => {
if (e && e.target && !e.target.contains(this)) {
return
}
toggleLoading = () => {
this.loading.classList.toggle('hidden')
this.icon.classList.toggle('hidden')
this.classList.toggle('opacity-50')
@@ -91,13 +87,11 @@ export default actionable(targetable(class extends HTMLElement {
this.append(input)
})
if (this.dataset.submitOnUpload === 'true') {
if (this.dataset.submitOnUpload) {
this.closest('form').querySelector('button[type="submit"]').click()
}
}).finally(() => {
if (this.dataset.submitOnUpload !== 'true') {
this.toggleLoading()
}
this.toggleLoading()
})
} else {
if (this.dataset.submitOnUpload) {
@@ -1,45 +0,0 @@
import autocomplete from 'autocompleter'
export default class extends HTMLElement {
connectedCallback () {
autocomplete({
input: this.input,
preventSubmit: this.dataset.submitOnSelect === 'true' ? 0 : 1,
minLength: 0,
showOnFocus: true,
onSelect: this.onSelect,
render: this.render,
fetch: this.fetch
})
}
onSelect = (item) => {
this.input.value = item.name
}
fetch = (text, resolve) => {
const queryParams = new URLSearchParams({ q: text })
fetch('/api/template_folders_autocomplete?' + queryParams).then(async (resp) => {
const items = await resp.json()
resolve(items)
}).catch(() => {
resolve([])
})
}
render = (item) => {
const div = document.createElement('div')
div.setAttribute('dir', 'auto')
div.textContent = item.name
return div
}
get input () {
return this.querySelector('input')
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
export default class extends HTMLElement {
connectedCallback () {
this.querySelectorAll('a').forEach((link) => {
if (document.location.pathname.startsWith(link.pathname) && !link.getAttribute('href').startsWith('http')) {
if (document.location.pathname.startsWith(link.pathname)) {
link.classList.add('bg-base-300')
}
})
@@ -1,19 +0,0 @@
export default class extends HTMLElement {
connectedCallback () {
const input = document.createElement('input')
input.type = 'hidden'
input.name = 'password'
input.value = prompt('Enter PDF password')
this.form.append(input)
this.form.requestSubmit()
this.remove()
}
get form () {
return this.closest('form')
}
}
+1 -15
View File
@@ -1,21 +1,7 @@
export default class extends HTMLElement {
connectedCallback () {
if (this.dataset.inputId) {
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
if (this.dataset.params === 'true') {
const params = new URLSearchParams(this.input.value)
params.set('timezone', timezone)
this.input.value = params.toString()
} else {
this.input.value = timezone
}
document.getElementById(this.dataset.inputId).value = Intl.DateTimeFormat().resolvedOptions().timeZone
}
}
get input () {
return document.getElementById(this.dataset.inputId)
}
}
-46
View File
@@ -1,46 +0,0 @@
import { target, targetable } from '@github/catalyst/lib/targetable'
import { cropCanvasAndExportToPNG } from '../submission_form/crop_canvas'
export default targetable(class extends HTMLElement {
static [target.static] = ['canvas', 'input', 'clear', 'button']
async connectedCallback () {
this.canvas.width = this.canvas.parentNode.parentNode.clientWidth
this.canvas.height = this.canvas.parentNode.parentNode.clientWidth / 3
const { default: SignaturePad } = await import('signature_pad')
this.pad = new SignaturePad(this.canvas)
this.clear.addEventListener('click', (e) => {
e.preventDefault()
this.pad.clear()
})
this.button.addEventListener('click', (e) => {
e.preventDefault()
this.button.disabled = true
this.submit()
})
}
async submit () {
const blob = await cropCanvasAndExportToPNG(this.canvas)
const file = new File([blob], 'signature.png', { type: 'image/png' })
const dataTransfer = new DataTransfer()
dataTransfer.items.add(file)
this.input.files = dataTransfer.files
if (this.input.webkitEntries.length) {
this.input.dataset.file = `${dataTransfer.files[0].name}`
}
this.closest('form').requestSubmit()
}
})
-5
View File
@@ -1,5 +0,0 @@
export default class extends HTMLElement {
connectedCallback () {
this.querySelector('form').requestSubmit()
}
}
@@ -1,69 +0,0 @@
import autocomplete from 'autocompleter'
export default class extends HTMLElement {
connectedCallback () {
autocomplete({
input: this.input,
preventSubmit: 1,
minLength: 1,
showOnFocus: true,
onSelect: this.onSelect,
render: this.render,
fetch: this.fetch
})
}
onSelect = (item) => {
const fields = ['email', 'name', 'phone']
const submitterItemEl = this.closest('submitter-item')
fields.forEach((field) => {
const input = submitterItemEl.querySelector(`submitters-autocomplete[data-field="${field}"] input`)
const textarea = submitterItemEl.querySelector(`submitters-autocomplete[data-field="${field}"] textarea`)
if (input && item[field]) {
input.value = item[field]
}
if (textarea && item[field]) {
textarea.value = textarea.value.replace(/[^;,\s]+$/, item[field] + ' ')
}
})
}
fetch = (text, resolve) => {
const q = text.split(/[;,\s]+/).pop().trim()
if (q) {
const queryParams = new URLSearchParams({ q, field: this.dataset.field })
fetch('/api/submitters_autocomplete?' + queryParams).then(async (resp) => {
const items = await resp.json()
if (q.length < 3) {
resolve(items.filter((e) => e[this.dataset.field].startsWith(q)))
} else {
resolve(items)
}
}).catch(() => {
resolve([])
})
} else {
resolve([])
}
}
render = (item) => {
const div = document.createElement('div')
div.setAttribute('dir', 'auto')
div.textContent = item[this.dataset.field]
return div
}
get input () {
return this.querySelector('input') || this.querySelector('textarea')
}
}
+2 -6
View File
@@ -5,19 +5,15 @@ export default actionable(class extends HTMLElement {
document.body.classList.add('overflow-hidden')
document.addEventListener('keyup', this.onEscKey)
document.addEventListener('turbo:submit-end', this.onSubmit)
document.addEventListener('turbo:before-cache', this.close)
if (this.dataset.closeAfterSubmit !== 'false') {
document.addEventListener('turbo:submit-end', this.onSubmit)
}
}
disconnectedCallback () {
document.body.classList.remove('overflow-hidden')
document.removeEventListener('keyup', this.onEscKey)
document.removeEventListener('turbo:submit-end', this.onSubmit)
document.removeEventListener('turbo:submit-end', this.handleSubmit)
document.removeEventListener('turbo:before-cache', this.close)
}
+3 -8
View File
@@ -9,17 +9,12 @@ window.customElements.define('submission-form', class extends HTMLElement {
this.appElem = document.createElement('div')
this.app = createApp(Form, {
submitter: JSON.parse(this.dataset.submitter),
submitterSlug: this.dataset.submitterSlug,
submitterUuid: this.dataset.submitterUuid,
authenticityToken: this.dataset.authenticityToken,
canSendEmail: this.dataset.canSendEmail === 'true',
isDirectUpload: this.dataset.isDirectUpload === 'true',
goToLast: this.dataset.goToLast === 'true',
isDemo: this.dataset.isDemo === 'true',
attribution: this.dataset.attribution !== 'false',
withConfetti: this.dataset.withConfetti !== 'false',
withTypedSignature: this.dataset.withTypedSignature !== 'false',
values: reactive(JSON.parse(this.dataset.values)),
completedButton: JSON.parse(this.dataset.completedButton),
completedRedirectUrl: this.dataset.completedRedirectUrl,
attachments: reactive(JSON.parse(this.dataset.attachments)),
fields: JSON.parse(this.dataset.fields)
})
-4
View File
@@ -47,10 +47,6 @@ select:required:invalid {
@apply border-base-content/20;
}
.base-textarea {
@apply textarea textarea-bordered bg-white rounded-3xl;
}
.btn {
@apply no-animation;
}
@@ -1,52 +0,0 @@
<template>
<div
v-if="uniqueAreas.length > 1"
dir="auto"
class="mb-2"
>
<div class="flex space-x-1 text-sm ml-1 -mt-3">
<span>
{{ t('appears_on') }}:
</span>
<a
v-for="area in uniqueAreas"
:key="area.uuid"
href="#"
class="link pr-0.5"
@click.prevent="scrollIntoArea(area)"
>
{{ t('page') }} <template v-if="isMultipleDocs">{{ attachmentUuids.indexOf(area.attachment_uuid) + 1 }}-</template>{{ area.page + 1 }}
</a>
</div>
</div>
</template>
<script>
export default {
name: 'AppearsOn',
inject: ['t', 'scrollIntoArea'],
props: {
field: {
type: Object,
required: true
}
},
computed: {
isMultipleDocs () {
return this.attachmentUuids.length > 1
},
attachmentUuids () {
return [...new Set(this.uniqueAreas.map((e) => e.attachment_uuid))]
},
uniqueAreas () {
const areas = {}
this.field.areas?.forEach((area) => {
areas[area.attachment_uuid + area.page] ||= area
})
return Object.values(areas).slice(0, 6)
}
}
}
</script>
+29 -205
View File
@@ -1,12 +1,11 @@
<template>
<div
class="flex absolute lg:text-base -outline-offset-1"
dir="auto"
class="flex absolute text-[1.5vw] lg:text-base"
:style="computedStyle"
:class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'outline-red-500 outline-dashed outline-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
:class="{ 'cursor-default': !submittable, 'bg-red-100 border cursor-pointer ': submittable, 'border-red-100': !isActive && submittable, 'bg-opacity-70': !isActive && !isValueSet && submittable, 'border-red-500 border-dashed z-10': isActive && submittable, 'bg-opacity-30': (isActive || isValueSet) && submittable }"
>
<div
v-if="!isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
v-if="!isActive && !isValueSet && field.type !== 'checkbox' && submittable"
class="absolute top-0 bottom-0 right-0 left-0 items-center justify-center h-full w-full"
>
<span
@@ -22,20 +21,15 @@
</span>
</div>
<div
v-if="isActive && withLabel && (!area.option_uuid || !option.value)"
class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap pointer-events-none"
v-if="isActive"
class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap"
>
<template v-if="area.option_uuid && !option.value">
{{ optionValue(option) }}
{{ field.name || fieldNames[field.type] }}
<template v-if="field.type === 'checkbox'">
{{ fieldIndex + 1 }}
</template>
<template v-else>
{{ field.name || fieldNames[field.type] }}
<template v-if="field.type === 'checkbox' && !field.name">
{{ fieldIndex + 1 }}
</template>
<template v-else-if="!field.required && field.type !== 'checkbox'">
(optional)
</template>
<template v-else-if="!field.required">
(optional)
</template>
</div>
<div
@@ -48,23 +42,13 @@
class="object-contain mx-auto"
:src="image.url"
>
<img
v-else-if="field.type === 'stamp' && stamp"
class="object-contain mx-auto"
:src="stamp.url"
>
<img
v-else-if="field.type === 'signature' && signature"
class="object-contain mx-auto"
:src="signature.url"
>
<img
v-else-if="field.type === 'initials' && initials"
class="object-contain mx-auto"
:src="initials.url"
>
<div
v-else-if="field.type === 'file' || field.type === 'payment'"
v-else-if="field.type === 'file'"
class="px-0.5 flex flex-col justify-center"
>
<a
@@ -81,8 +65,7 @@
</div>
<div
v-else-if="field.type === 'checkbox'"
class="w-full p-[1px] flex items-center justify-center"
@click="$event.target.querySelector('input')?.click()"
class="w-full p-[0.2vw] flex items-center justify-center"
>
<input
v-if="submittable"
@@ -99,46 +82,6 @@
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
/>
</div>
<div
v-else-if="field.type === 'radio' && area.option_uuid"
class="w-full p-[1px] flex items-center justify-center"
@click="$event.target.querySelector('input')?.click()"
>
<input
v-if="submittable"
type="radio"
:value="false"
class="aspect-square checked:checkbox checked:checkbox-xs"
:class="{ 'base-radio': !modelValue || modelValue !== optionValue(option), '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
:checked="!!modelValue && modelValue === optionValue(option)"
@click="$emit('update:model-value', optionValue(option))"
>
<IconCheck
v-else-if="!!modelValue && modelValue === optionValue(option)"
class="aspect-square"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
/>
</div>
<div
v-else-if="field.type === 'multiple' && area.option_uuid"
class="w-full p-[1px] flex items-center justify-center"
@click="$event.target.querySelector('input')?.click()"
>
<input
v-if="submittable"
type="checkbox"
:value="false"
class="aspect-square base-checkbox"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
:checked="!!modelValue && modelValue.includes(optionValue(option))"
@change="updateMultipleSelectValue(optionValue(option))"
>
<IconCheck
v-else-if="!!modelValue && modelValue.includes(optionValue(option))"
class="aspect-square"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
/>
</div>
<div
v-else-if="field.type === 'cells'"
class="w-full flex items-center"
@@ -154,9 +97,7 @@
</div>
<div
v-else
ref="textContainer"
dir="auto"
class="flex items-center px-0.5 w-full"
class="flex items-center px-0.5"
>
<span v-if="Array.isArray(modelValue)">
{{ modelValue.join(', ') }}
@@ -164,16 +105,15 @@
<span v-else-if="field.type === 'date'">
{{ formattedDate }}
</span>
<span
v-else
class="whitespace-pre-wrap"
>{{ modelValue }}</span>
<span v-else>
{{ modelValue }}
</span>
</div>
</div>
</template>
<script>
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp } from '@tabler/icons-vue'
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3 } from '@tabler/icons-vue'
export default {
name: 'FieldArea',
@@ -181,7 +121,6 @@ export default {
IconPaperclip,
IconCheck
},
inject: ['t'],
props: {
field: {
type: Object,
@@ -207,11 +146,6 @@ export default {
required: false,
default: false
},
withLabel: {
type: Boolean,
required: false,
default: true
},
fieldIndex: {
type: Number,
required: false,
@@ -228,49 +162,33 @@ export default {
}
},
emits: ['update:model-value'],
data () {
return {
textOverflowChars: 0
}
},
computed: {
fieldNames () {
return {
text: this.t('text'),
signature: this.t('signature'),
initials: this.t('initials'),
date: this.t('date'),
image: this.t('image'),
file: this.t('file'),
select: this.t('select'),
checkbox: this.t('checkbox'),
multiple: this.t('multiple'),
radio: this.t('radio'),
cells: this.t('cells'),
stamp: this.t('stamp'),
payment: this.t('payment'),
phone: this.t('phone')
text: 'Text',
cells: 'Text',
signature: 'Signature',
date: 'Date',
image: 'Image',
file: 'File',
select: 'Select',
checkbox: 'Checkbox',
radio: 'Radio',
multiple: 'Multiple Select'
}
},
option () {
return this.field.options.find((o) => o.uuid === this.area.option_uuid)
},
fieldIcons () {
return {
text: IconTextSize,
signature: IconWritingSign,
date: IconCalendarEvent,
image: IconPhoto,
initials: IconLetterCaseUpper,
file: IconPaperclip,
select: IconSelect,
checkbox: IconCheckbox,
radio: IconCircleDot,
stamp: IconRubberStamp,
cells: IconColumns3,
multiple: IconChecks,
phone: IconPhoneCheck,
payment: IconCreditCard
multiple: IconChecks
}
},
image () {
@@ -280,13 +198,6 @@ export default {
return null
}
},
stamp () {
if (this.field.type === 'stamp') {
return this.attachmentsIndex[this.modelValue]
} else {
return null
}
},
signature () {
if (this.field.type === 'signature') {
return this.attachmentsIndex[this.modelValue]
@@ -294,22 +205,9 @@ export default {
return null
}
},
initials () {
if (this.field.type === 'initials') {
return this.attachmentsIndex[this.modelValue]
} else {
return null
}
},
locale () {
return Intl.DateTimeFormat().resolvedOptions()?.locale
},
formattedDate () {
if (this.field.type === 'date' && this.modelValue) {
return this.formatDate(
this.modelValue === '{{date}}' ? new Date() : new Date(this.modelValue),
this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
)
return new Intl.DateTimeFormat([], { year: 'numeric', month: 'long', day: 'numeric' }).format(new Date(this.modelValue))
} else {
return ''
}
@@ -317,8 +215,6 @@ export default {
attachments () {
if (this.field.type === 'file') {
return (this.modelValue || []).map((uuid) => this.attachmentsIndex[uuid])
} else if (this.field.type === 'payment') {
return [this.attachmentsIndex[this.modelValue]].filter(Boolean)
} else {
return []
}
@@ -333,78 +229,6 @@ export default {
height: h * 100 + '%'
}
}
},
watch: {
modelValue () {
if (this.field.type === 'text' && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > this.modelValue.length)) {
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? this.modelValue.length : 0
}
}
},
mounted () {
if (this.field.type === 'text' && this.$refs.textContainer) {
this.$nextTick(() => {
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? this.modelValue.length : 0
})
}
},
methods: {
optionValue (option) {
if (option) {
if (option.value) {
return option.value
} else {
const index = this.field.options.indexOf(option)
return `${this.t('option')} ${index + 1}`
}
}
},
formatDate (date, format) {
const monthFormats = {
M: 'numeric',
MM: '2-digit',
MMM: 'short',
MMMM: 'long'
}
const dayFormats = {
D: 'numeric',
DD: '2-digit'
}
const yearFormats = {
YYYY: 'numeric',
YY: '2-digit'
}
const parts = new Intl.DateTimeFormat([], {
day: dayFormats[format.match(/D+/)],
month: monthFormats[format.match(/M+/)],
year: yearFormats[format.match(/Y+/)],
timeZone: 'UTC'
}).formatToParts(date)
return format
.replace(/D+/, parts.find((p) => p.type === 'day').value)
.replace(/M+/, parts.find((p) => p.type === 'month').value)
.replace(/Y+/, parts.find((p) => p.type === 'year').value)
},
updateMultipleSelectValue (value) {
if (this.modelValue?.includes(value)) {
const newValue = [...this.modelValue]
newValue.splice(newValue.indexOf(value), 1)
this.$emit('update:model-value', newValue)
} else {
const newValue = this.modelValue ? [...this.modelValue] : []
newValue.push(value)
this.$emit('update:model-value', newValue)
}
}
}
}
</script>
+13 -37
View File
@@ -11,10 +11,7 @@
v-for="(area, areaIndex) in field.areas"
:key="areaIndex"
>
<Teleport
v-if="findPageElementForArea(area)"
:to="findPageElementForArea(area)"
>
<Teleport :to="`#page-${area.attachment_uuid}-${area.page}`">
<FieldArea
:ref="setAreaRef"
v-model="values[field.uuid]"
@@ -23,7 +20,6 @@
:submittable="true"
:field-index="fieldIndex"
:is-active="currentStep === step"
:with-label="withLabel"
:is-value-set="step.some((f) => f.uuid in values)"
:attachments-index="attachmentsIndex"
@click="$emit('focus-step', step)"
@@ -58,11 +54,6 @@ export default {
required: false,
default: () => ({})
},
withLabel: {
type: Boolean,
required: false,
default: true
},
currentStep: {
type: Array,
required: false,
@@ -79,44 +70,29 @@ export default {
this.areaRefs = []
},
methods: {
findPageElementForArea (area) {
return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${area.attachment_uuid}-${area.page}`)
},
scrollIntoField (field) {
if (field?.areas) {
this.scrollIntoArea(field.areas[0])
}
},
scrollIntoArea (area) {
const areaRef = this.areaRefs.find((a) => a.area === area)
this.areaRefs.find((area) => {
if (area.field === field) {
if (document.body.style.overflow === 'hidden') {
this.scrollInContainer(area.$el)
} else {
area.$refs.scrollToElem.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
if (areaRef) {
const root = this.$root.$el.parentNode.getRootNode()
const container = root.body || root.querySelector('div')
if (container.style.overflow === 'hidden') {
this.scrollInContainer(areaRef.$el)
return true
} else {
areaRef.$refs.scrollToElem.scrollIntoView({ behavior: 'smooth', block: 'start' })
return null
}
return true
}
})
},
scrollInContainer (target) {
const root = this.$root.$el.parentNode.getRootNode()
const scrollbox = root.getElementById('scrollbox')
const formContainer = root.getElementById('form_container')
const container = root.body || root.querySelector('div')
const padding = 64
const boxRect = scrollbox.children[0].getBoundingClientRect()
const boxRect = window.scrollbox.children[0].getBoundingClientRect()
const targetRect = target.getBoundingClientRect()
const targetTopRelativeToBox = targetRect.top - boxRect.top
scrollbox.scrollTop = targetTopRelativeToBox - container.offsetHeight + formContainer.offsetHeight + target.offsetHeight + padding
window.scrollbox.scrollTop = targetTopRelativeToBox - document.body.offsetHeight + window.form_container.offsetHeight + target.offsetHeight + padding
},
setAreaRef (el) {
if (el) {
@@ -1,8 +1,8 @@
<template>
<div>
<div v-if="value.length">
<div v-if="modelValue.length">
<div
v-for="(val, index) in value"
v-for="(val, index) in modelValue"
:key="index"
class="flex mb-2"
>
@@ -26,7 +26,10 @@
{{ attachmentsIndex[val].filename }}
</span>
</a>
<button @click.prevent="removeAttachment(val)">
<button
v-if="modelValue"
@click.prevent="removeAttachment(val)"
>
<IconTrashX
:width="18"
:heigh="19"
@@ -42,7 +45,7 @@
>
</template>
<FileDropzone
:message="`${t('upload')} ${field.name || t('files')}${field.required ? '' : ` (${t('optional')})`}`"
:message="`Upload ${field.name || 'Attachments'}${field.required ? '' : ' (optional)'}`"
:submitter-slug="submitterSlug"
:is-direct-upload="isDirectUpload"
:multiple="true"
@@ -62,7 +65,6 @@ export default {
IconPaperclip,
IconTrashX
},
inject: ['t'],
props: {
field: {
type: Object,
@@ -89,28 +91,18 @@ export default {
}
},
emits: ['attached', 'update:model-value'],
computed: {
value: {
set (value) {
this.$emit('update:model-value', this.modelValue || [])
},
get () {
return this.modelValue || []
}
}
},
methods: {
removeAttachment (uuid) {
this.value.splice(this.value.indexOf(uuid), 1)
this.modelValue.splice(this.modelValue.indexOf(uuid), 1)
this.$emit('update:model-value', this.value)
this.$emit('update:model-value', this.modelValue)
},
onUpload (attachments) {
attachments.forEach((attachment) => {
this.$emit('attached', attachment)
})
this.$emit('update:model-value', [...this.value, ...attachments.map(a => a.uuid)])
this.$emit('update:model-value', [...this.modelValue, ...attachments.map(a => a.uuid)])
}
}
}
+35 -147
View File
@@ -1,8 +1,5 @@
<template>
<div
class="mx-auto max-w-md flex flex-col"
dir="auto"
>
<div class="mx-auto max-w-md flex flex-col">
<p class="font-medium text-2xl flex items-center space-x-1.5 mx-auto">
<IconCircleCheck
class="inline text-green-600"
@@ -10,22 +7,13 @@
:height="30"
/>
<span>
{{ t('form_has_been_completed') }}
Form has been completed!
</span>
</p>
<div class="space-y-3 mt-5">
<a
v-if="completedButton.url"
:href="completedButton.url"
class="white-button flex items-center w-full"
>
<span>
{{ completedButton.title || 'Back to Website' }}
</span>
</a>
<button
v-if="canSendEmail && !isDemo && withSendCopyButton"
class="white-button !h-auto flex items-center space-x-1 w-full"
v-if="canSendEmail"
class="white-button flex items-center space-x-1 w-full"
:disabled="isSendingCopy"
@click.prevent="sendCopyToEmail"
>
@@ -35,11 +23,10 @@
/>
<IconMail v-else />
<span>
{{ t('send_copy_via_email') }}
Send copy via email
</span>
</button>
<button
v-if="!isWebView && withDownloadButton"
class="base-button flex items-center space-x-1 w-full"
:disabled="isDownloading"
@click.prevent="download"
@@ -50,98 +37,41 @@
/>
<IconDownload v-else />
<span>
{{ t('download') }}
Download
</span>
</button>
<a
v-if="isDemo"
target="_blank"
href="https://github.com/docusealco/docuseal"
class="white-button flex items-center space-x-1 w-full"
>
<IconBrandGithub />
<span>
Star on Github
</span>
</a>
<a
v-if="isDemo"
href="https://docuseal.co/sign_up"
class="white-button flex items-center space-x-1 w-full"
>
<IconLogin />
<span>
{{ t('create_a_free_account') }}
</span>
</a>
</div>
<div
v-if="attribution"
class="text-center mt-4"
>
{{ t('signed_with') }}
<div class="text-center mt-4">
Signed with
<a
href="https://www.docuseal.co/start"
href="https://www.docuseal.co"
target="_blank"
class="underline"
>DocuSeal</a> - {{ t('open_source_documents_software') }}
>DocuSeal</a> - open source documents software
</div>
</div>
</template>
<script>
import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop, IconLogin } from '@tabler/icons-vue'
import { IconCircleCheck, IconMail, IconDownload, IconInnerShadowTop } from '@tabler/icons-vue'
export default {
name: 'FormCompleted',
components: {
IconCircleCheck,
IconInnerShadowTop,
IconBrandGithub,
IconMail,
IconLogin,
IconDownload
},
inject: ['baseUrl', 't'],
props: {
submitterSlug: {
type: String,
required: true
},
isDemo: {
type: Boolean,
required: false,
default: false
},
attribution: {
type: Boolean,
required: false,
default: true
},
withDownloadButton: {
type: Boolean,
required: false,
default: true
},
withSendCopyButton: {
type: Boolean,
required: false,
default: true
},
withConfetti: {
type: Boolean,
required: false,
default: false
},
canSendEmail: {
type: Boolean,
required: false,
default: false
},
completedButton: {
type: Object,
required: false,
default: () => ({})
}
},
data () {
@@ -150,30 +80,23 @@ export default {
isDownloading: false
}
},
computed: {
isWebView () {
return /webview|wv|ip((?!.*Safari)|(?=.*like Safari))/i.test(window.navigator.userAgent)
}
},
async mounted () {
if (this.withConfetti) {
const { default: confetti } = await import('canvas-confetti')
const { default: confetti } = await import('canvas-confetti')
confetti({
particleCount: 50,
startVelocity: 30,
spread: 140
})
}
confetti({
particleCount: 50,
startVelocity: 30,
spread: 140
})
},
methods: {
sendCopyToEmail () {
this.isSendingCopy = true
fetch(this.baseUrl + `/send_submission_email.json?submitter_slug=${this.submitterSlug}`, {
fetch(`/send_submission_email.json?submitter_slug=${this.submitterSlug}`, {
method: 'POST'
}).then(() => {
alert(this.t('email_has_been_sent'))
alert('Email has been sent')
}).finally(() => {
this.isSendingCopy = false
})
@@ -181,63 +104,28 @@ export default {
download () {
this.isDownloading = true
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`).then((response) => response.json()).then((urls) => {
const isSafariIos = /iPhone|iPad|iPod/i.test(navigator.userAgent)
fetch(`/submitters/${this.submitterSlug}/download`).then((response) => response.json()).then((urls) => {
const fileRequests = urls.map((url) => {
return () => {
return fetch(url).then(async (resp) => {
const blobUrl = URL.createObjectURL(await resp.blob())
const link = document.createElement('a')
if (isSafariIos && urls.length > 1) {
this.downloadSafariIos(urls)
} else {
this.downloadUrls(urls)
}
})
},
downloadUrls (urls) {
const fileRequests = urls.map((url) => {
return () => {
return fetch(url).then(async (resp) => {
const blobUrl = URL.createObjectURL(await resp.blob())
const link = document.createElement('a')
link.href = blobUrl
link.setAttribute('download', decodeURI(url.split('/').pop()))
link.href = blobUrl
link.setAttribute('download', decodeURI(url.split('/').pop()))
link.click()
link.click()
URL.revokeObjectURL(blobUrl)
})
}
})
fileRequests.reduce(
(prevPromise, request) => prevPromise.then(() => request()),
Promise.resolve()
)
this.isDownloading = false
},
downloadSafariIos (urls) {
const fileRequests = urls.map((url) => {
return fetch(url).then(async (resp) => {
const blob = await resp.blob()
const blobUrl = URL.createObjectURL(blob.slice(0, blob.size, 'application/octet-stream'))
const link = document.createElement('a')
link.href = blobUrl
link.setAttribute('download', decodeURI(url.split('/').pop()))
return link
URL.revokeObjectURL(url)
})
}
})
})
Promise.all(fileRequests).then((links) => {
links.forEach((link, index) => {
setTimeout(() => {
link.click()
fileRequests.reduce(
(prevPromise, request) => prevPromise.then(() => request()),
Promise.resolve()
)
URL.revokeObjectURL(link.href)
}, index * 50)
})
}).finally(() => {
this.isDownloading = false
})
}
@@ -1,49 +0,0 @@
function cropCanvasAndExportToPNG (canvas) {
const ctx = canvas.getContext('2d')
const width = canvas.width
const height = canvas.height
let topmost = height
let bottommost = 0
let leftmost = width
let rightmost = 0
const imageData = ctx.getImageData(0, 0, width, height)
const pixels = imageData.data
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const pixelIndex = (y * width + x) * 4
const alpha = pixels[pixelIndex + 3]
if (alpha !== 0) {
topmost = Math.min(topmost, y)
bottommost = Math.max(bottommost, y)
leftmost = Math.min(leftmost, x)
rightmost = Math.max(rightmost, x)
}
}
}
const croppedWidth = rightmost - leftmost + 1
const croppedHeight = bottommost - topmost + 1
const croppedCanvas = document.createElement('canvas')
croppedCanvas.width = croppedWidth
croppedCanvas.height = croppedHeight
const croppedCtx = croppedCanvas.getContext('2d')
croppedCtx.drawImage(canvas, leftmost, topmost, croppedWidth, croppedHeight, 0, 0, croppedWidth, croppedHeight)
return new Promise((resolve, reject) => {
croppedCanvas.toBlob((blob) => {
if (blob) {
resolve(blob)
} else {
reject(new Error('Failed to create a PNG blob.'))
}
}, 'image/png')
})
}
export { cropCanvasAndExportToPNG }
@@ -1,81 +0,0 @@
<template>
<div dir="auto">
<div class="flex justify-between items-center w-full mb-2">
<label
:for="field.uuid"
class="label text-2xl"
>{{ field.name && showFieldNames ? field.name : t('date') }}
<template v-if="!field.required">({{ t('optional') }})</template>
</label>
<button
class="btn btn-outline btn-sm !normal-case font-normal"
@click.prevent="setCurrentDate"
>
<IconCalendarCheck :width="16" />
{{ t('set_today') }}
</button>
</div>
<AppearsOn :field="field" />
<div class="text-center">
<input
ref="input"
v-model="value"
class="base-input !text-2xl text-center w-full"
:required="field.required"
type="date"
:name="`values[${field.uuid}]`"
@focus="$emit('focus')"
>
</div>
</div>
</template>
<script>
import { IconCalendarCheck } from '@tabler/icons-vue'
import AppearsOn from './appears_on'
export default {
name: 'DateStep',
components: {
IconCalendarCheck,
AppearsOn
},
inject: ['t'],
props: {
field: {
type: Object,
required: true
},
showFieldNames: {
type: Boolean,
required: false,
default: true
},
modelValue: {
type: String,
required: false,
default: ''
}
},
emits: ['update:model-value', 'focus'],
computed: {
value: {
set (value) {
this.$emit('update:model-value', value)
},
get () {
return this.modelValue
}
}
},
methods: {
setCurrentDate () {
const inputEl = this.$refs.input
inputEl.valueAsDate = new Date(new Date().getTime() - new Date().getTimezoneOffset() * 60000)
inputEl.dispatchEvent(new Event('input', { bubbles: true }))
}
}
}
</script>
+3 -4
View File
@@ -29,7 +29,7 @@
{{ message }}
</div>
<div class="text-xs">
<span class="font-medium">{{ t('click_to_upload') }}</span> {{ t('or_drag_and_drop_files') }}
<span class="font-medium">Click to upload</span> or drag and drop files
</div>
</div>
</div>
@@ -55,7 +55,6 @@ export default {
IconCloudUpload,
IconInnerShadowTop
},
inject: ['baseUrl', 't'],
props: {
message: {
type: String,
@@ -142,7 +141,7 @@ export default {
return await Promise.all(
blobs.map((blob) => {
return fetch(this.baseUrl + '/api/attachments', {
return fetch('/api/attachments', {
method: 'POST',
body: JSON.stringify({
name: 'attachments',
@@ -167,7 +166,7 @@ export default {
formData.append('submitter_slug', this.submitterSlug)
formData.append('name', 'attachments')
return fetch(this.baseUrl + '/api/attachments', {
return fetch('/api/attachments', {
method: 'POST',
body: formData
}).then(resp => resp.json()).then((data) => {
File diff suppressed because it is too large. Load diff
-688
View File
@@ -1,688 +0,0 @@
const en = {
text: 'Text',
signature: 'Signature',
initials: 'Initials',
date: 'Date',
image: 'Image',
take_photo: 'Take photo',
file: 'File',
select: 'Select',
checkbox: 'Checkbox',
multiple: 'Multiple',
radio: 'Radio',
cells: 'Cells',
stamp: 'Stamp',
payment: 'Payment',
phone: 'Phone',
submit_form: 'Submit Form',
type_here_: 'Type here...',
optional: 'optional',
option: 'Option',
appears_on: 'Appears on',
page: 'Page',
select_your_option: 'Select your option',
complete_hightlighted_checkboxes_and_click: 'Complete hightlighted checkboxes and click',
submit: 'submit',
next: 'next',
click_to_upload: 'Click to upload',
or_drag_and_drop_files: 'or drag and drop files',
send_copy_via_email: 'Send copy via email',
download: 'Download',
clear: 'Clear',
redraw: 'Redraw',
draw_initials: 'Draw initials',
type_signature_here: 'Type signature here',
type_initial_here: 'Type initials here',
form_has_been_completed: 'Form has been completed!',
create_a_free_account: 'Create a Free Account',
signed_with: 'Signed with',
please_check_the_box_to_continue: 'Please check the box to continue',
open_source_documents_software: 'open source documents software',
verified_phone_number: 'Verify Phone Number',
use_international_format: 'Use internatioanl format: +1xxx',
six_digits_code: '6-digit code',
change_phone_number: 'Change phone number',
sending: 'Sending...',
resend_code: 'Re-send code',
verification_code_has_been_resent: 'Verification code has been re-sent via SMS',
please_fill_all_required_fields: 'Please fill all required fields',
set_today: 'Set Today',
toggle_multiline_text: 'Toggle Multiline Text',
draw_signature: 'Draw signature',
type_initial: 'Type initials',
draw: 'Draw',
type: 'Type',
type_text: 'Type text',
email_has_been_sent: 'Email has been sent',
processing: 'Processing',
pay_with_strip: 'Pay with Stripe',
reupload: 'Reupload',
upload: 'Upload',
files: 'Files'
}
const es = {
text: 'Texto',
signature: 'Firma',
initials: 'Iniciales',
date: 'Fecha',
image: 'Imagen',
file: 'Archivo',
select: 'Seleccionar',
checkbox: 'Casilla',
multiple: 'Múltiple',
radio: 'Radio',
cells: 'Celdas',
stamp: 'Sello',
payment: 'Pago',
phone: 'Teléfono',
take_photo: 'Tomar foto',
submit_form: 'Enviar Formulario',
type_here_: 'Escribe aquí...',
optional: 'opcional',
appears_on: 'Aparece en',
page: 'Página',
select_your_option: 'Selecciona tu opción',
complete_hightlighted_checkboxes_and_click: 'Completa las casillas resaltadas y haz clic',
submit: 'enviar',
next: 'siguiente',
click_to_upload: 'Haz clic para cargar',
or_drag_and_drop_files: 'o arrastra y suelta archivos',
send_copy_via_email: 'Enviar copia por correo electrónico',
download: 'Descargar',
clear: 'Borrar',
redraw: 'Redibujar',
draw_initials: 'Dibujar iniciales',
type_signature_here: 'Escribe la firma aquí',
type_initial_here: 'Escribe las iniciales aquí',
form_has_been_completed: '¡El formulario ha sido completado!',
create_a_free_account: 'Crear una Cuenta Gratuita',
signed_with: 'Firmado con',
please_check_the_box_to_continue: 'Por favor marque la casilla para continuar',
open_source_documents_software: 'software de documentos de código abierto',
verified_phone_number: 'Verificar número de teléfono',
use_international_format: 'Usar formato internacional: +1xxx',
six_digits_code: 'Código de 6 dígitos',
change_phone_number: 'Cambiar número de teléfono',
sending: 'Enviando...',
resend_code: 'Reenviar código',
verification_code_has_been_resent: 'El código de verificación ha sido reenviado por SMS',
please_fill_all_required_fields: 'Por favor, complete todos los campos obligatorios',
set_today: 'Establecer Hoy',
toggle_multiline_text: 'Alternar Texto Multilínea',
draw_signature: 'Dibujar firma',
type_initial: 'Escribir iniciales',
draw: 'Dibujar',
type: 'Escribir',
type_text: 'Escribir texto',
email_has_been_sent: 'El correo electrónico ha sido enviado',
processing: 'Procesando',
pay_with_strip: 'Pagar con Stripe',
reupload: 'Volver a subir',
upload: 'Subir',
files: 'Archivos'
}
const it = {
text: 'Testo',
signature: 'Firma',
initials: 'Iniziali',
date: 'Data',
image: 'Immagine',
file: 'File',
select: 'Seleziona',
checkbox: 'Casella di controllo',
multiple: 'Multiplo',
radio: 'Radio',
cells: 'Celle',
stamp: 'Timbro',
payment: 'Pagamento',
phone: 'Telefono',
submit_form: 'Invia Modulo',
type_here_: 'Digita qui...',
optional: 'opzionale',
appears_on: 'Compare su',
page: 'Pagina',
take_photo: 'Scattare una foto',
select_your_option: 'Seleziona la tua opzione',
complete_hightlighted_checkboxes_and_click: 'Completa le caselle evidenziate e fai clic',
submit: 'invia',
next: 'avanti',
click_to_upload: 'Clicca per caricare',
or_drag_and_drop_files: 'oppure trascina e rilascia i file',
send_copy_via_email: 'Invia copia via email',
download: 'Scarica',
clear: 'Cancella',
redraw: 'Ridisegna',
draw_initials: 'Disegna iniziali',
type_signature_here: 'Scrivi la firma qui',
type_initial_here: 'Scrivi le iniziali qui',
form_has_been_completed: 'Il modulo è stato completato!',
create_a_free_account: 'Crea un Account Gratuito',
signed_with: 'Firmato con',
please_check_the_box_to_continue: 'Si prega di spuntare la casella per continuare',
open_source_documents_software: 'software di documenti open source',
verified_phone_number: 'Verifica numero di telefono',
use_international_format: 'Usa formato internazionale: +1xxx',
six_digits_code: 'Codice a 6 cifre',
change_phone_number: 'Cambia numero di telefono',
sending: 'Invio in corso...',
resend_code: 'Rinvia codice',
verification_code_has_been_resent: 'Il codice di verifica è stato rinviato tramite SMS',
please_fill_all_required_fields: 'Si prega di compilare tutti i campi obbligatori',
set_today: 'Imposta Oggi',
draw_signature: 'Disegna firma',
type_initial: 'Inserisci iniziali',
draw: 'Disegna',
type: 'Inserisci',
type_text: 'Inserisci testo',
toggle_multiline_text: 'Attiva Testo Multilinea',
email_has_been_sent: "L'email è stata inviata",
processing: 'Elaborazione',
pay_with_strip: 'Paga con Stripe',
reupload: 'Ricarica',
upload: 'Carica',
files: 'File'
}
const de = {
text: 'Text',
signature: 'Unterschrift',
initials: 'Initialen',
date: 'Datum',
image: 'Bild',
file: 'Datei',
select: 'Auswählen',
checkbox: 'Checkbox',
multiple: 'Mehrere',
radio: 'Radio',
cells: 'Zellen',
stamp: 'Stempel',
payment: 'Zahlung',
phone: 'Telefon',
submit_form: 'Formular absenden',
type_here_: 'Hier eingeben...',
optional: 'optional',
appears_on: 'Erscheint auf',
page: 'Seite',
take_photo: 'Foto aufnehmen',
select_your_option: 'Wähle deine Option',
complete_hightlighted_checkboxes_and_click: 'Markierte Kontrollkästchen ausfüllen und klicken',
submit: 'absenden',
next: 'weiter',
click_to_upload: 'Klicken zum Hochladen',
or_drag_and_drop_files: 'oder Dateien hierher ziehen und ablegen',
send_copy_via_email: 'Kopie per E-Mail senden',
download: 'Herunterladen',
clear: 'Löschen',
redraw: 'Neu zeichnen',
draw_initials: 'Initialen zeichnen',
type_signature_here: 'Unterschrift hier eingeben',
type_initial_here: 'Initialen hier eingeben',
form_has_been_completed: 'Formular wurde ausgefüllt!',
create_a_free_account: 'Kostenloses Konto erstellen',
signed_with: 'Unterschrieben mit',
please_check_the_box_to_continue: 'Bitte setzen Sie das Häkchen, um fortzufahren',
open_source_documents_software: 'Open-Source-Dokumentensoftware',
verified_phone_number: 'Telefonnummer überprüfen',
use_international_format: 'Internationales Format verwenden: +1xxx',
six_digits_code: '6-stelliger Code',
change_phone_number: 'Telefonnummer ändern',
sending: 'Senden...',
resend_code: 'Code erneut senden',
verification_code_has_been_resent: 'Die Verifizierungscode wurde erneut per SMS gesendet',
please_fill_all_required_fields: 'Bitte füllen Sie alle erforderlichen Felder aus',
set_today: 'Heute einstellen',
draw_signature: 'Unterschrift zeichnen',
type_initial: 'Initialen eingeben',
draw: 'Zeichnen',
type: 'Eingeben',
type_text: 'Text eingeben',
toggle_multiline_text: 'Mehrzeiligen Text umschalten',
email_has_been_sent: 'Die E-Mail wurde gesendet',
processing: 'Verarbeitung',
pay_with_strip: 'Mit Stripe bezahlen',
reupload: 'Erneut hochladen',
upload: 'Hochladen',
files: 'Dateien'
}
const fr = {
text: 'Texte',
signature: 'Signature',
initials: 'Initiales',
date: 'Date',
image: 'Image',
file: 'Fichier',
select: 'Choisir',
checkbox: 'Coche',
multiple: 'Multiple',
radio: 'Radio',
cells: 'Cellules',
stamp: 'Tampon',
payment: 'Paiement',
phone: 'Téléphone',
submit_form: 'Envoyer le Formulaire',
type_here_: 'Tapez ici...',
optional: 'facultatif',
appears_on: 'Apparaît sur',
page: 'Page',
take_photo: 'Prendre une photo',
select_your_option: 'Sélectionnez votre option',
complete_hightlighted_checkboxes_and_click: 'Complétez les cases à cocher en surbrillance et cliquez',
submit: 'envoyer',
next: 'suivant',
click_to_upload: 'Cliquez pour télécharger',
or_drag_and_drop_files: 'ou faites glisser-déposer les fichiers',
send_copy_via_email: 'Envoyer une copie par e-mail',
download: 'Télécharger',
clear: 'Effacer',
redraw: 'Redessiner',
draw_initials: 'Dessiner les initiales',
type_signature_here: 'Tapez la signature ici',
type_initial_here: 'Tapez les initiales ici',
form_has_been_completed: 'Le formulaire a été complété !',
create_a_free_account: 'Créer un Compte Gratuit',
signed_with: 'Signé avec',
please_check_the_box_to_continue: 'Veuillez cocher la case pour continuer',
open_source_documents_software: 'logiciel de documents open source',
verified_phone_number: 'Vérifier le numéro de téléphone',
use_international_format: 'Utiliser le format international : +1xxx',
six_digits_code: 'Code à 6 chiffres',
change_phone_number: 'Changer le numéro de téléphone',
sending: 'Envoi en cours...',
resend_code: 'Renvoyer le code',
verification_code_has_been_resent: 'Le code de vérification a été renvoyé par SMS',
please_fill_all_required_fields: 'Veuillez remplir tous les champs obligatoires',
set_today: "Définir Aujourd'hui",
draw_signature: 'Dessiner une signature',
type_initial: 'Saisir les initiales',
draw: 'Dessiner',
type: 'Saisir',
type_text: 'Saisir du texte',
toggle_multiline_text: 'Basculer le Texte Multiligne',
email_has_been_sent: "L'email a été envoyé",
processing: 'Traitement',
pay_with_strip: 'Paiement avec Stripe',
reupload: 'Recharger',
upload: 'Télécharger',
files: 'Fichiers'
}
const pl = {
text: 'Tekst',
signature: 'Podpis',
initials: 'Inicjały',
date: 'Data',
image: 'Obraz',
file: 'Plik',
select: 'Wybierz',
checkbox: 'Pole wyboru',
multiple: 'Wielokrotne',
radio: 'Radio',
cells: 'Komórki',
stamp: 'Pieczątka',
payment: 'Płatność',
phone: 'Telefon',
submit_form: 'Wyślij Formularz',
type_here_: 'Wpisz tutaj...',
optional: 'opcjonalny',
appears_on: 'Pojawia się na',
page: 'Strona',
select_your_option: 'Wybierz swoją opcję',
complete_hightlighted_checkboxes_and_click: 'Wypełnij zaznaczone pola wyboru i kliknij',
submit: 'wyślij',
next: 'dalej',
take_photo: 'Zrób zdjęcie',
click_to_upload: 'Kliknij, aby przesłać',
or_drag_and_drop_files: 'lub przeciągnij i upuść pliki',
send_copy_via_email: 'Wyślij kopię drogą mailową',
download: 'Pobierz',
clear: 'Wyczyść',
redraw: 'Przerysuj',
draw_initials: 'Narysuj inicjały',
type_signature_here: 'Wpisz podpis tutaj',
type_initial_here: 'Wpisz inicjały tutaj',
form_has_been_completed: 'Formularz został wypełniony!',
create_a_free_account: 'Utwórz darmowe konto',
signed_with: 'Podpisane za pomocą',
please_check_the_box_to_continue: 'Proszę zaznaczyć pole, aby kontynuować',
open_source_documents_software: 'oprogramowanie do dokumentów open source',
verified_phone_number: 'Zweryfikuj numer telefonu',
use_international_format: 'Użyj międzynarodowego formatu: +1xxx',
six_digits_code: '6-cyfrowy kod',
change_phone_number: 'Zmień numer telefonu',
sending: 'Wysyłanie...',
resend_code: 'Ponownie wyślij kod',
verification_code_has_been_resent: 'Kod weryfikacyjny został ponownie wysłany',
please_fill_all_required_fields: 'Proszę wypełnić wszystkie wymagane pola',
set_today: 'Ustaw Dziś',
draw_signature: 'Rysuj podpis',
type_initial: 'Wprowadź inicjały',
draw: 'Rysuj',
type: 'Wprowadź',
type_text: 'Wprowadź tekst',
toggle_multiline_text: 'Przełącz Tekst Wielolinijkowy',
email_has_been_sent: 'E-mail został wysłany',
processing: 'Przetwarzanie',
pay_with_strip: 'Płatność za pomocą Stripe',
reupload: 'Ponowne przesłanie',
upload: 'Przesyłanie',
files: 'Pliki'
}
const uk = {
text: 'Текст',
signature: 'Підпис',
initials: 'Ініціали',
date: 'Дата',
image: 'Зображення',
file: 'Файл',
select: 'Вибрати',
checkbox: 'Прапорець',
multiple: 'Множинний',
radio: 'Радіо',
cells: 'Клітинки',
stamp: 'Штамп',
payment: 'Платіж',
phone: 'Телефон',
submit_form: 'Надіслати Форму',
type_here_: 'Введіть тут',
optional: 'необов’язково',
appears_on: "З'являється на",
page: 'Сторінка',
take_photo: 'Зробити фото',
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: 'Код підтвердження був повторно надісланий',
please_fill_all_required_fields: "Будь ласка, заповніть всі обов'язкові поля",
set_today: 'Задати Сьогодні',
draw_signature: 'Намалюйте підпис',
type_initial: 'Введіть ініціали',
draw: 'Підпис',
type: 'Текст',
type_text: 'Введіть текст',
toggle_multiline_text: 'Перемкнути Багаторядковий Текст',
email_has_been_sent: 'Електронний лист був відправлений',
processing: 'Обробка',
pay_with_strip: 'Сплатити за допомогою Stripe',
reupload: 'Перезавантажити',
upload: 'Завантажити',
files: 'Файли'
}
const cs = {
text: 'Text',
signature: 'Podpis',
initials: 'Iniciály',
date: 'Datum',
image: 'Obrázek',
file: 'Soubor',
select: 'Vybrat',
checkbox: 'Zaškrtávací políčko',
multiple: 'Více možností',
radio: 'Rádio',
cells: 'Buňky',
stamp: 'Razítko',
payment: 'Platba',
phone: 'Telefon',
submit_form: 'Odeslat formulář',
type_here_: 'Zadejte zde',
optional: 'volitelné',
appears_on: 'Zobrazuje se na',
page: 'Stránka',
select_your_option: 'Vyberte svou volbu',
take_photo: 'Fotit',
complete_hightlighted_checkboxes_and_click: 'Označte zvýrazněné zaškrtávací políčka a klikněte na',
submit: 'odeslat',
next: 'další',
click_to_upload: 'Klikněte pro nahrání',
or_drag_and_drop_files: 'nebo přetáhněte soubory sem',
send_copy_via_email: 'Odeslat kopii e-mailem',
download: 'Stáhnout',
clear: 'Smazat',
redraw: 'Překreslit',
draw_initials: 'Nakreslit iniciály',
type_signature_here: 'Sem zadejte podpis',
type_initial_here: 'Sem zadejte iniciály',
form_has_been_completed: 'Formulář byl dokončen!',
create_a_free_account: 'Vytvořit bezplatný účet',
signed_with: 'Podepsáno pomocí',
please_check_the_box_to_continue: 'Prosím, zaškrtněte políčko pro pokračování',
open_source_documents_software: 'open source software pro dokumenty',
verified_phone_number: 'Ověřte telefonní číslo',
use_international_format: 'Použijte mezinárodní formát: +1xxx',
six_digits_code: '6-místný kód',
change_phone_number: 'Změnit telefonní číslo',
sending: 'Odesílání...',
resend_code: 'Znovu odeslat kód',
verification_code_has_been_resent: 'Ověřovací kód byl znovu odeslán',
please_fill_all_required_fields: 'Prosím vyplňte všechny povinné položky',
set_today: 'Nastavit Dnes',
draw_signature: 'Nakreslit podpis',
type_initial: 'Zadat iniciály',
draw: 'Kreslit',
type: 'Zadat',
type_text: 'Zadat text',
toggle_multiline_text: 'Přepnout Víceřádkový Text',
email_has_been_sent: 'E-mail byl odeslán',
processing: 'Zpracování',
pay_with_strip: 'Zaplacení přes Stripe',
reupload: 'Znovu nahrát',
upload: 'Nahrát',
files: 'Soubory'
}
const pt = {
text: 'Texto',
signature: 'Assinatura',
initials: 'Iniciais',
date: 'Data',
image: 'Imagem',
file: 'Arquivo',
select: 'Selecionar',
checkbox: 'Caixa',
multiple: 'Múltiplo',
radio: 'Rádio',
cells: 'Células',
stamp: 'Carimbo',
payment: 'Pagamento',
phone: 'Telefone',
submit_form: 'Enviar Formulário',
type_here_: 'Digite aqui',
optional: 'opcional',
appears_on: 'Aparece em',
page: 'Página',
take_photo: 'Tirar foto',
select_your_option: 'Selecione sua opção',
complete_hightlighted_checkboxes_and_click: 'Complete as caixas de seleção destacadas e clique',
submit: 'enviar',
next: 'próximo',
click_to_upload: 'Clique para fazer o upload',
or_drag_and_drop_files: 'ou arraste e solte arquivos',
send_copy_via_email: 'Enviar cópia por e-mail',
download: 'Baixar',
clear: 'Limpar',
redraw: 'Redesenhar',
draw_initials: 'Desenhar iniciais',
type_signature_here: 'Digite a assinatura aqui',
type_initial_here: 'Digite as iniciais aqui',
form_has_been_completed: 'O formulário foi concluído!',
create_a_free_account: 'Criar uma Conta Gratuita',
signed_with: 'Assinado com',
please_check_the_box_to_continue: 'Por favor, marque a caixa para continuar',
open_source_documents_software: 'software de documentos de código aberto',
verified_phone_number: 'Verificar Número de Telefone',
use_international_format: 'Use formato internacional: +1xxx',
six_digits_code: 'Código de 6 dígitos',
change_phone_number: 'Alterar número de telefone',
sending: 'Enviando...',
resend_code: 'Reenviar código',
verification_code_has_been_resent: 'O código de verificação foi reenviado via SMS',
please_fill_all_required_fields: 'Por favor, preencha todos os campos obrigatórios',
set_today: 'Definir Hoje',
draw_signature: 'Desenhar assinatura',
type_initial: 'Inserir iniciais',
draw: 'Desenhar',
type: 'Inserir',
type_text: 'Inserir texto',
toggle_multiline_text: 'Alternar Texto Multilinha',
email_has_been_sent: 'Email enviado',
processing: 'Processamento',
pay_with_strip: 'Pagar com Stripe',
reupload: 'Reenviar',
upload: 'Carregar',
files: 'Arquivos'
}
const he = {
text: 'טקסט',
signature: 'חתימה',
initials: 'ראשי תיקיות',
date: 'תאריך',
image: 'תמונה',
file: 'קובץ',
select: 'בחר',
checkbox: 'תיק בחירה',
multiple: 'רב ערכים',
radio: 'רדיו',
cells: 'תאים',
stamp: 'חותמת',
payment: 'תשלום',
phone: 'טלפון',
submit_form: 'שלח טופס',
type_here_: 'הקלד כאן',
optional: 'אופציונלי',
option: 'אפשרות',
appears_on: 'מופיע ב',
page: 'עמוד',
take_photo: 'צלם תמונה',
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: 'קוד משתמש שש ספרות',
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 nl = {
text: 'Tekst',
signature: 'Handtekening',
initials: 'Initialen',
date: 'Datum',
image: 'Afbeelding',
take_photo: 'Maak een foto',
file: 'Bestand',
select: 'Selecteer',
checkbox: 'Aankruisvakje',
multiple: 'Meerdere',
radio: 'Radio',
cells: 'Cellen',
stamp: 'Stempel',
payment: 'Betaling',
phone: 'Telefoon',
submit_form: 'Formulier verzenden',
type_here_: 'Typ hier...',
optional: 'Optioneel',
option: 'Optie',
appears_on: 'Verschijnt op',
page: 'Pagina',
select_your_option: 'Selecteer uw optie',
complete_hightlighted_checkboxes_and_click: 'Voltooi de gemarkeerde selectievakjes en klik',
submit: 'Voltooien',
next: 'Volgende',
click_to_upload: 'Klik om te uploaden',
or_drag_and_drop_files: 'of sleep en plaats bestanden',
send_copy_via_email: 'Stuur kopie per e-mail',
download: 'Downloaden',
clear: 'Wissen',
redraw: 'Opnieuw tekenen',
draw_initials: 'Initialen tekenen',
type_signature_here: 'Typ hier uw handtekening',
type_initial_here: 'Typ hier uw initialen',
form_has_been_completed: 'Formulier is voltooid!',
create_a_free_account: 'Maak een gratis account aan',
signed_with: 'Ondertekend met',
please_check_the_box_to_continue: 'Vink het vakje aan om door te gaan',
open_source_documents_software: 'Open source documenten software',
verified_phone_number: 'Geverifieerd telefoonnummer',
use_international_format: 'Gebruik internationaal formaat: +1xxx',
six_digits_code: '6-cijferige code',
change_phone_number: 'Wijzig telefoonnummer',
sending: 'Voltooien...',
resend_code: 'Code opnieuw verzenden',
verification_code_has_been_resent: 'Verificatiecode is opnieuw verzonden via SMS',
please_fill_all_required_fields: 'Vul alle verplichte velden in',
set_today: 'Vandaag',
toggle_multiline_text: 'Schakel meerregelige tekst in/uit',
draw_signature: 'Handtekening tekenen',
type_initial: 'Typ initialen',
draw: 'Tekenen',
type: 'Typen',
type_text: 'Typ tekst',
email_has_been_sent: 'E-mail is verzonden',
processing: 'Verwerken',
pay_with_strip: 'Betalen met Stripe',
reupload: 'Opnieuw uploaden',
upload: 'Uploaden',
files: 'Bestanden'
}
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl }
export default i18n
@@ -3,13 +3,13 @@
<div class="flex justify-between items-center w-full mb-2">
<label
class="label text-2xl"
>{{ showFieldNames && field.name ? field.name : t('image') }}</label>
>{{ field.name || 'Image' }}</label>
<button
class="btn btn-outline btn-sm"
@click.prevent="remove"
>
<IconReload :width="16" />
{{ t('reupload') }}
Reupload
</button>
</div>
<div>
@@ -27,7 +27,7 @@
<div>
<FileDropzone
v-if="!modelValue"
:message="`${t('upload')} ${field.name || t('image')}${field.required ? '' : ` (${t('optional')})`}`"
:message="`Upload ${field.name || 'Image'}${field.required ? '' : ' (optional)'}`"
:submitter-slug="submitterSlug"
:accept="'image/*'"
:is-direct-upload="isDirectUpload"
@@ -46,7 +46,6 @@ export default {
FileDropzone,
IconReload
},
inject: ['t'],
props: {
field: {
type: Object,
@@ -57,11 +56,6 @@ export default {
required: true,
default: false
},
showFieldNames: {
type: Boolean,
required: false,
default: true
},
submitterSlug: {
type: String,
required: true
@@ -1,320 +0,0 @@
<template>
<div dir="auto">
<div class="flex justify-between items-center w-full mb-2">
<label
class="label text-2xl"
>{{ showFieldNames && field.name ? field.name : t('initials') }}</label>
<div class="space-x-2 flex">
<span
v-if="isDrawInitials"
class="tooltip"
:data-tip="t('type_initials')"
>
<a
id="type_text_button"
href="#"
class="btn btn-outline font-medium btn-sm"
@click.prevent="toggleTextInput"
>
<IconTextSize :width="16" />
<span class="hidden sm:inline">
{{ t('type') }}
</span>
</a>
</span>
<span
v-else
class="tooltip ml-2"
:data-tip="t('draw_initials')"
>
<a
id="type_text_button"
href="#"
class="btn btn-outline font-medium btn-sm"
@click.prevent="toggleTextInput"
>
<IconSignature :width="16" />
<span class="hidden sm:inline">
{{ t('draw') }}
</span>
</a>
</span>
<a
v-if="modelValue || computedPreviousValue"
href="#"
class="btn font-medium btn-outline btn-sm"
@click.prevent="remove"
>
<IconReload :width="16" />
{{ t('clear') }}
</a>
<a
v-else
href="#"
class="btn font-medium btn-outline btn-sm"
@click.prevent="clear"
>
<IconReload :width="16" />
{{ t('clear') }}
</a>
<a
title="Minimize"
href="#"
class="py-1.5 inline md:hidden"
@click.prevent="$emit('minimize')"
>
<IconArrowsDiagonalMinimize2
:width="20"
:height="20"
/>
</a>
</div>
</div>
<AppearsOn :field="field" />
<input
:value="modelValue || computedPreviousValue"
type="hidden"
:name="`values[${field.uuid}]`"
>
<img
v-if="modelValue || computedPreviousValue"
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
class="mx-auto bg-white border border-base-300 rounded max-h-72"
>
<canvas
v-show="!modelValue && !computedPreviousValue"
ref="canvas"
class="bg-white border border-base-300 rounded-2xl w-full"
/>
<input
v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
id="initials_text_input"
ref="textInput"
class="base-input !text-2xl w-full mt-6 text-center"
:required="field.required && !isInitialsStarted"
:placeholder="`${t('type_initial_here')}...`"
type="text"
@focus="$emit('focus')"
@input="updateWrittenInitials"
>
</div>
</template>
<script>
import { cropCanvasAndExportToPNG } from './crop_canvas'
import { IconReload, IconTextSize, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
import SignaturePad from 'signature_pad'
import AppearsOn from './appears_on'
const scale = 3
export default {
name: 'InitialsStep',
components: {
AppearsOn,
IconReload,
IconTextSize,
IconSignature,
IconArrowsDiagonalMinimize2
},
inject: ['baseUrl', 't'],
props: {
field: {
type: Object,
required: true
},
submitterSlug: {
type: String,
required: true
},
showFieldNames: {
type: Boolean,
required: false,
default: true
},
isDirectUpload: {
type: Boolean,
required: true,
default: false
},
attachmentsIndex: {
type: Object,
required: false,
default: () => ({})
},
previousValue: {
type: String,
required: false,
default: ''
},
modelValue: {
type: String,
required: false,
default: ''
}
},
emits: ['attached', 'update:model-value', 'start', 'minimize', 'focus'],
data () {
return {
isInitialsStarted: !!this.previousValue,
isUsePreviousValue: true,
isDrawInitials: false
}
},
computed: {
computedPreviousValue () {
if (this.isUsePreviousValue) {
return this.previousValue
} else {
return null
}
}
},
async mounted () {
this.$nextTick(() => {
if (this.$refs.canvas) {
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
this.$refs.canvas.height = (this.$refs.canvas.parentNode.clientWidth / 3) * scale
this.$refs.canvas.getContext('2d').scale(scale, scale)
}
this.$refs.textInput?.focus()
})
if (this.isDirectUpload) {
import('@rails/activestorage')
}
if (this.$refs.canvas) {
this.pad = new SignaturePad(this.$refs.canvas)
this.pad.addEventListener('beginStroke', () => {
this.isInitialsStarted = true
this.$emit('start')
})
this.intersectionObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
this.$refs.canvas.height = (this.$refs.canvas.parentNode.clientWidth / 3) * scale
this.$refs.canvas.getContext('2d').scale(scale, scale)
this.intersectionObserver?.disconnect()
}
})
})
this.intersectionObserver.observe(this.$refs.canvas)
}
},
beforeUnmount () {
this.intersectionObserver?.disconnect()
},
methods: {
remove () {
this.$emit('update:model-value', '')
this.isUsePreviousValue = false
this.isInitialsStarted = false
},
clear () {
this.pad.clear()
this.isInitialsStarted = false
if (this.$refs.textInput) {
this.$refs.textInput.value = ''
}
},
updateWrittenInitials (e) {
this.isInitialsStarted = true
const canvas = this.$refs.canvas
const context = canvas.getContext('2d')
const fontFamily = 'Arial'
const fontSize = '44px'
const fontStyle = 'italic'
const fontWeight = ''
context.font = fontStyle + ' ' + fontWeight + ' ' + fontSize + ' ' + fontFamily
context.textAlign = 'center'
context.clearRect(0, 0, canvas.width / scale, canvas.height / scale)
context.fillText(e.target.value, canvas.width / 2 / scale, canvas.height / 2 / scale + 11)
},
toggleTextInput () {
this.remove()
this.clear()
this.isDrawInitials = !this.isDrawInitials
if (!this.isDrawInitials) {
this.$nextTick(() => {
this.$refs.textInput.focus()
this.$emit('start')
})
}
},
async submit () {
if (this.modelValue || this.computedPreviousValue) {
if (this.computedPreviousValue) {
this.$emit('update:model-value', this.computedPreviousValue)
}
return Promise.resolve({})
}
return new Promise((resolve) => {
cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => {
const file = new File([blob], 'initials.png', { type: 'image/png' })
if (this.isDirectUpload) {
const { DirectUpload } = await import('@rails/activestorage')
new DirectUpload(
file,
'/direct_uploads'
).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)
})
})
} else {
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((attachment) => {
this.$emit('attached', attachment)
this.$emit('update:model-value', attachment.uuid)
return resolve(attachment)
})
}
})
})
}
}
}
</script>
@@ -1,50 +1,31 @@
<template>
<label
v-if="showFieldNames && field.name"
v-if="field.name"
:for="field.uuid"
dir="auto"
class="label text-2xl mb-2"
>{{ field.name }}</label>
<div class="flex w-full max-h-44 overflow-y-auto">
<input
v-if="modelValue.length === 0"
type="text"
:name="`values[${field.uuid}][]`"
:value="''"
class="hidden"
>
<div
v-if="!showOptions"
class="text-xl px-1"
>
<span @click="scrollIntoField(field)">
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ isLastStep ? t('submit') : t('next') }}</span>.
</span>
</div>
<div
class="space-y-3.5 mx-auto"
:class="{ hidden: !showOptions }"
>
<div class="flex w-full">
<div class="space-y-3.5 mx-auto">
<div
v-for="(option, index) in field.options"
:key="option.uuid"
:key="index"
>
<label
:for="option.uuid"
:for="field.uuid + option"
class="flex items-center space-x-3"
>
<input
:id="option.uuid"
:id="field.uuid + option"
:ref="setInputRef"
type="checkbox"
:name="`values[${field.uuid}][]`"
:value="optionValue(option, index)"
:value="option"
class="base-checkbox !h-7 !w-7"
:checked="(modelValue || []).includes(optionValue(option, index))"
:checked="modelValue.includes(option)"
@change="onChange"
>
<span class="text-xl">
{{ optionValue(option, index) }}
{{ option }}
</span>
</label>
</div>
@@ -55,22 +36,11 @@
<script>
export default {
name: 'MultiSelectStep',
inject: ['t', 'scrollIntoField'],
props: {
field: {
type: Object,
required: true
},
isLastStep: {
type: Boolean,
required: true,
default: false
},
showFieldNames: {
type: Boolean,
required: false,
default: true
},
modelValue: {
type: Array,
required: false,
@@ -83,11 +53,6 @@ export default {
inputRefs: []
}
},
computed: {
showOptions () {
return this.showFieldNames && (this.field.options.some((e) => e.value) || this.field.options.length < 5)
}
},
beforeUpdate () {
this.inputRefs = []
},
@@ -97,15 +62,8 @@ export default {
this.inputRefs.push(el)
}
},
optionValue (option, index) {
if (option.value) {
return option.value
} else {
return `${this.t('option')} ${index + 1}`
}
},
onChange () {
this.$emit('update:model-value', this.inputRefs.filter(e => e.checked).map((e, index) => this.optionValue(e, index)))
this.$emit('update:model-value', this.inputRefs.filter(e => e.checked).map(e => e.value))
}
}
}
Loaded 100 of 433 files, more files were not shown because too many files have changed in this diff. Show more