initial commit

pull/105/head
Alex Turchyn 2 years ago
commit 97c462ead2

@ -0,0 +1,32 @@
---
EnableDefaultLinters: true
linters:
ErbSafety:
enabled: false
HardCodedString:
enabled: false
Rubocop:
enabled: true
rubocop_config:
inherit_from:
- .rubocop.yml
Layout/InitialIndentation:
Enabled: false
Layout/LineLength:
Enabled: false
Layout/TrailingEmptyLines:
Enabled: false
Layout/TrailingWhitespace:
Enabled: false
Naming/FileName:
Enabled: false
Style/FrozenStringLiteralComment:
Enabled: false
Lint/UselessAssignment:
Enabled: false
Rails/OutputSafety:
Enabled: false
Style/NestedTernaryOperator:
Enabled: false
Style/ZeroLengthPredicate:
Enabled: false

@ -0,0 +1,17 @@
{
"extends": [
"standard",
"plugin:vue/vue3-recommended"
],
"env": {
"browser": true
},
"rules": {
"vue/no-deprecated-html-element-is": 0,
"vue/no-mutating-props": 0
},
"parserOptions": {
"ecmaVersion": 2022,
"parser": "@babel/eslint-parser"
}
}

@ -0,0 +1,25 @@
name: Erblint
on: [push]
jobs:
erblint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.2.2
- name: Cache gems
uses: actions/cache@v1
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gem-
- name: Install gems
run: |
gem install bundler
bundle config path vendor/bundle
bundle install --jobs 4 --retry 4
- name: Run Erblint
run: bundle exec erblint ./app

@ -0,0 +1,28 @@
name: ESLint
on: [push]
jobs:
eslint:
name: Run ESLint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Node.js
uses: actions/setup-node@v1
with:
node-version: 16.13.1
- name: Cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v1
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies
run: |
yarn install
- name: Run eslint
run: |
./node_modules/eslint/bin/eslint.js "app/javascript/**/*.js"

@ -0,0 +1,65 @@
name: Rspec
on: push
jobs:
verify:
name: Build
runs-on: ubuntu-latest
services:
postgres:
image: postgres:14
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: docuseal_test
ports: ["5432:5432"]
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v2
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.2.1
- name: Set up Node
uses: actions/setup-node@v1
with:
node-version: 16.13.1
- name: Install Chrome
uses: browser-actions/setup-chrome@latest
- name: Cache node_modules
uses: actions/cache@v1
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
- name: Cache gems
uses: actions/cache@v1
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gem-
- name: Install dependencies
env:
RAILS_ENV: test
run: |
gem install bundler
bundle config path vendor/bundle
bundle install --jobs 4 --retry 4
yarn install
- name: Run
env:
RAILS_ENV: test
NODE_ENV: test
COVERAGE: true
DATABASE_URL: postgres://postgres:postgres@localhost:5432/docuseal_test
run: |
bundle exec rake db:create
bundle exec rake db:migrate
bundle exec rake assets:precompile
bundle exec rspec

@ -0,0 +1,25 @@
name: Rubocop
on: [push]
jobs:
rubocop:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.2.2
- name: Cache gems
uses: actions/cache@v1
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gem-
- name: Install gems
run: |
gem install bundler
bundle config path vendor/bundle
bundle install --jobs 4 --retry 4
- name: Run RuboCop
run: bundle exec rubocop

36
.gitignore vendored

@ -0,0 +1,36 @@
/.bundle
/db/*.sqlite3
/db/*.sqlite3-*
/log/*
/tmp/*
!/log/.keep
!/tmp/.keep
/tmp/pids/*
!/tmp/pids/
!/tmp/pids/.keep
/storage/*
!/storage/.keep
/tmp/storage/*
!/tmp/storage/
!/tmp/storage/.keep
/public/assets
/config/master.key
/public/packs
/public/packs-test
/node_modules
/yarn-error.log
yarn-debug.log*
.yarn-integrity
.env
.DS_Store
/coverage
/docuseal-attachments

@ -0,0 +1,55 @@
require:
- rubocop-performance
- rubocop-rails
- rubocop-rspec
AllCops:
NewCops: enable
Exclude:
- db/schema.rb
- node_modules/**/*
- bin/*
TargetRubyVersion: '3.2'
Metrics/BlockLength:
Exclude:
- Rakefile
- '**/*.rake'
- spec/**/*
- config/environments/**/*
- config/routes.rb
Style/Documentation:
Enabled: false
Lint/MissingSuper:
Enabled: false
Metrics/MethodLength:
Max: 20
Exclude:
- 'db/migrate/**'
Metrics/CyclomaticComplexity:
Max: 10
Metrics/PerceivedComplexity:
Max: 10
Metrics/AbcSize:
Max: 35
RSpec/NestedGroups:
Max: 6
RSpec/MultipleExpectations:
Max: 7
RSpec/ExampleLength:
Max: 15
Rails/I18nLocaleTexts:
Enabled: false
Rails/ApplicationController:
Enabled: false

@ -0,0 +1,58 @@
# frozen_string_literal: true
source 'https://rubygems.org'
ruby '3.2.2'
gem 'audited'
gem 'aws-sdk-s3'
gem 'azure-storage-blob'
gem 'bootsnap', require: false
gem 'combine_pdf'
gem 'devise'
gem 'faraday'
gem 'geoip'
gem 'google-cloud-storage'
gem 'image_processing'
gem 'lograge'
gem 'oj'
gem 'pagy'
gem 'pg'
gem 'premailer-rails'
gem 'puma'
gem 'rails'
gem 'ruby-vips'
gem 'shakapacker'
gem 'sqlite3'
gem 'strip_attributes'
gem 'turbo-rails'
gem 'tzinfo-data'
gem 'zip'
group :development, :test do
gem 'annotate'
gem 'better_html'
gem 'bullet'
gem 'debug'
gem 'erb_lint', require: false
gem 'factory_bot_rails'
gem 'faker'
gem 'pry-rails'
gem 'rspec-rails'
gem 'rubocop', require: false
gem 'rubocop-performance', require: false
gem 'rubocop-rails', require: false
gem 'rubocop-rspec', require: false
gem 'simplecov', require: false
end
group :development do
gem 'letter_opener_web'
gem 'web-console'
end
group :test do
gem 'capybara'
gem 'cuprite'
gem 'webmock'
end

@ -0,0 +1,526 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (7.0.4.3)
actionpack (= 7.0.4.3)
activesupport (= 7.0.4.3)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (7.0.4.3)
actionpack (= 7.0.4.3)
activejob (= 7.0.4.3)
activerecord (= 7.0.4.3)
activestorage (= 7.0.4.3)
activesupport (= 7.0.4.3)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
actionmailer (7.0.4.3)
actionpack (= 7.0.4.3)
actionview (= 7.0.4.3)
activejob (= 7.0.4.3)
activesupport (= 7.0.4.3)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.0)
actionpack (7.0.4.3)
actionview (= 7.0.4.3)
activesupport (= 7.0.4.3)
rack (~> 2.0, >= 2.2.0)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (7.0.4.3)
actionpack (= 7.0.4.3)
activerecord (= 7.0.4.3)
activestorage (= 7.0.4.3)
activesupport (= 7.0.4.3)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.0.4.3)
activesupport (= 7.0.4.3)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (7.0.4.3)
activesupport (= 7.0.4.3)
globalid (>= 0.3.6)
activemodel (7.0.4.3)
activesupport (= 7.0.4.3)
activerecord (7.0.4.3)
activemodel (= 7.0.4.3)
activesupport (= 7.0.4.3)
activestorage (7.0.4.3)
actionpack (= 7.0.4.3)
activejob (= 7.0.4.3)
activerecord (= 7.0.4.3)
activesupport (= 7.0.4.3)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
activesupport (7.0.4.3)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
addressable (2.8.4)
public_suffix (>= 2.0.2, < 6.0)
annotate (3.2.0)
activerecord (>= 3.2, < 8.0)
rake (>= 10.4, < 14.0)
ast (2.4.2)
audited (5.3.3)
activerecord (>= 5.0, < 7.1)
request_store (~> 1.2)
aws-eventstream (1.2.0)
aws-partitions (1.765.0)
aws-sdk-core (3.172.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.64.0)
aws-sdk-core (~> 3, >= 3.165.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.122.0)
aws-sdk-core (~> 3, >= 3.165.0)
aws-sdk-kms (~> 1)
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)
nokogiri (~> 1, >= 1.10.8)
azure-storage-common (2.0.4)
faraday (~> 1.0)
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
net-http-persistent (~> 4.0)
nokogiri (~> 1, >= 1.10.8)
bcrypt (3.1.18)
better_html (2.0.1)
actionview (>= 6.0)
activesupport (>= 6.0)
ast (~> 2.0)
erubi (~> 1.4)
parser (>= 2.4)
smart_properties
bindex (0.8.1)
bootsnap (1.16.0)
msgpack (~> 1.2)
builder (3.2.4)
bullet (7.0.7)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
capybara (3.39.1)
addressable
matrix
mini_mime (>= 0.1.3)
nokogiri (~> 1.8)
rack (>= 1.6.0)
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
coderay (1.1.3)
combine_pdf (1.0.23)
matrix
ruby-rc4 (>= 0.1.5)
concurrent-ruby (1.2.2)
connection_pool (2.4.0)
crack (0.4.5)
rexml
crass (1.0.6)
css_parser (1.14.0)
addressable
cuprite (0.14.3)
capybara (~> 3.0)
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.2)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
responders
warden (~> 1.2.3)
diff-lcs (1.5.0)
digest-crc (0.6.4)
rake (>= 12.0.0, < 14.0.0)
docile (1.4.0)
erb_lint (0.4.0)
activesupport
better_html (>= 2.0.1)
parser (>= 2.7.1.4)
rainbow
rubocop
smart_properties
erubi (1.12.0)
factory_bot (6.2.1)
activesupport (>= 5.0.0)
factory_bot_rails (6.2.0)
factory_bot (~> 6.2.0)
railties (>= 5.0.0)
faker (3.2.0)
i18n (>= 1.8.11, < 2)
faraday (1.10.3)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday (~> 1.0)
ferrum (0.13)
addressable (~> 2.5)
concurrent-ruby (~> 1.1)
webrick (~> 1.7)
websocket-driver (>= 0.6, < 0.8)
ffi (1.15.5)
geoip (1.6.4)
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)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
webrick
google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.11.0, < 2.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 (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.3.1)
google-cloud-storage (1.44.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.19.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.5.2)
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.0.1)
htmlentities (4.3.4)
httpclient (2.8.3)
i18n (1.13.0)
concurrent-ruby (~> 1.0)
image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
io-console (0.6.0)
irb (1.6.4)
reline (>= 0.3.0)
jmespath (1.6.2)
json (2.6.3)
jwt (2.7.0)
launchy (2.5.2)
addressable (~> 2.8)
letter_opener (1.8.1)
launchy (>= 2.2, < 3)
letter_opener_web (2.0.0)
actionmailer (>= 5.2)
letter_opener (~> 1.7)
railties (>= 5.2)
rexml
lograge (0.12.0)
actionpack (>= 4)
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
loofah (2.21.3)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
mini_mime (>= 0.1.1)
net-imap
net-pop
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.2)
minitest (5.18.0)
msgpack (1.7.0)
multi_json (1.15.0)
multipart-post (2.3.0)
net-http-persistent (4.0.2)
connection_pool (~> 2.2)
net-imap (0.3.4)
date
net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.2.1)
timeout
net-smtp (0.3.3)
net-protocol
nio4r (2.5.9)
nokogiri (1.15.0-arm64-darwin)
racc (~> 1.4)
oj (3.14.3)
orm_adapter (0.5.0)
os (1.1.4)
pagy (6.0.4)
parallel (1.23.0)
parser (3.2.2.1)
ast (~> 2.4.1)
pg (1.5.3)
premailer (1.21.0)
addressable
css_parser (>= 1.12.0)
htmlentities (>= 4.0.0)
premailer-rails (1.12.0)
actionmailer (>= 3)
net-smtp
premailer (~> 1.7, >= 1.7.9)
pry (0.14.2)
coderay (~> 1.1)
method_source (~> 1.0)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (5.0.1)
puma (6.2.2)
nio4r (~> 2.0)
racc (1.6.2)
rack (2.2.7)
rack-proxy (0.7.6)
rack
rack-test (2.1.0)
rack (>= 1.3)
rails (7.0.4.3)
actioncable (= 7.0.4.3)
actionmailbox (= 7.0.4.3)
actionmailer (= 7.0.4.3)
actionpack (= 7.0.4.3)
actiontext (= 7.0.4.3)
actionview (= 7.0.4.3)
activejob (= 7.0.4.3)
activemodel (= 7.0.4.3)
activerecord (= 7.0.4.3)
activestorage (= 7.0.4.3)
activesupport (= 7.0.4.3)
bundler (>= 1.15.0)
railties (= 7.0.4.3)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.5.0)
loofah (~> 2.19, >= 2.19.1)
railties (7.0.4.3)
actionpack (= 7.0.4.3)
activesupport (= 7.0.4.3)
method_source
rake (>= 12.2)
thor (~> 1.0)
zeitwerk (~> 2.5)
rainbow (3.1.1)
rake (13.0.6)
regexp_parser (2.8.0)
reline (0.3.3)
io-console (~> 0.5)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
request_store (1.5.1)
rack (>= 1.4)
responders (3.1.0)
actionpack (>= 5.2)
railties (>= 5.2)
retriable (3.1.2)
rexml (3.2.5)
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.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-rails (6.0.2)
actionpack (>= 6.1)
activesupport (>= 6.1)
railties (>= 6.1)
rspec-core (~> 3.12)
rspec-expectations (~> 3.12)
rspec-mocks (~> 3.12)
rspec-support (~> 3.12)
rspec-support (3.12.0)
rubocop (1.51.0)
json (~> 2.3)
parallel (~> 1.10)
parser (>= 3.2.0.0)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.28.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.28.1)
parser (>= 3.2.1.0)
rubocop-capybara (2.18.0)
rubocop (~> 1.41)
rubocop-factory_bot (2.23.1)
rubocop (~> 1.33)
rubocop-performance (1.17.1)
rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0)
rubocop-rails (2.19.1)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0)
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.1.4)
ffi (~> 1.12)
ruby2_keywords (0.0.5)
semantic_range (3.0.0)
shakapacker (6.6.0)
activesupport (>= 5.2)
rack-proxy (>= 0.6.1)
railties (>= 5.2)
semantic_range (>= 2.3.0)
signet (0.17.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1)
simplecov-html (0.12.3)
simplecov_json_formatter (0.1.4)
smart_properties (1.17.0)
sqlite3 (1.6.3-arm64-darwin)
strip_attributes (1.13.0)
activemodel (>= 3.0, < 8.0)
thor (1.2.2)
timeout (0.3.2)
trailblazer-option (0.1.2)
turbo-rails (1.4.0)
actionpack (>= 6.0.0)
activejob (>= 6.0.0)
railties (>= 6.0.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
tzinfo-data (1.2023.3)
tzinfo (>= 1.0.0)
uber (0.1.0)
unicode-display_width (2.4.2)
uniform_notifier (1.16.0)
warden (1.2.9)
rack (>= 2.0.9)
web-console (4.2.0)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
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.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.6.8)
zip (2.0.2)
PLATFORMS
arm64-darwin-22
DEPENDENCIES
annotate
audited
aws-sdk-s3
azure-storage-blob
better_html
bootsnap
bullet
capybara
combine_pdf
cuprite
debug
devise
erb_lint
factory_bot_rails
faker
faraday
geoip
google-cloud-storage
image_processing
letter_opener_web
lograge
oj
pagy
pg
premailer-rails
pry-rails
puma
rails
rspec-rails
rubocop
rubocop-performance
rubocop-rails
rubocop-rspec
ruby-vips
shakapacker
simplecov
sqlite3
strip_attributes
turbo-rails
tzinfo-data
web-console
webmock
zip
RUBY VERSION
ruby 3.2.2p53
BUNDLED WITH
2.4.10

@ -0,0 +1 @@
web: puma -p $PORT -C ./config/puma.rb

@ -0,0 +1,2 @@
web: PORT=3000 bundle exec rails s -p 3000
webpacker: bundle exec ./bin/webpacker-dev-server

@ -0,0 +1,24 @@
# README
This README would normally document whatever steps are necessary to get the
application up and running.
Things you may want to cover:
* Ruby version
* System dependencies
* Configuration
* Database creation
* Database initialization
* How to run the test suite
* Services (job queues, cache servers, search engines, etc.)
* Deployment instructions
* ...

@ -0,0 +1,5 @@
# frozen_string_literal: true
require_relative 'config/application'
Rails.application.load_tasks

@ -0,0 +1,15 @@
# frozen_string_literal: true
module Api
class ApiBaseController < ActionController::API
include ActiveStorage::SetCurrent
before_action :authenticate_user!
private
def current_account
current_user&.account
end
end
end

@ -0,0 +1,20 @@
# frozen_string_literal: true
module Api
class AttachmentsController < ApiBaseController
skip_before_action :authenticate_user!
def create
submission = Submission.find_by!(slug: params[:submission_slug])
blob = ActiveStorage::Blob.find_signed(params[:blob_signed_id])
attachment = ActiveStorage::Attachment.create!(
blob:,
name: params[:name],
record: submission
)
render json: attachment.as_json(only: %i[uuid], methods: %i[url filename content_type])
end
end
end

@ -0,0 +1,22 @@
# frozen_string_literal: true
module Api
class FlowsController < ApiBaseController
def update
@flow = current_account.flows.find(params[:id])
@flow.update!(flow_params)
render :ok
end
private
def flow_params
params.require(:flow).permit(:name,
schema: [%i[attachment_uuid name]],
fields: [[:uuid, :name, :type, :required,
{ options: [], areas: [%i[x y w h attachment_uuid page]] }]])
end
end
end

@ -0,0 +1,31 @@
# frozen_string_literal: true
module Api
class FlowsDocumentsController < ApiBaseController
def create
@flow = current_account.flows.find(params[:flow_id])
documents =
params[:blobs].map do |blob|
blob = ActiveStorage::Blob.find_signed(blob[:signed_id])
document = @flow.documents.create!(blob:)
Flows::ProcessDocument.call(document)
end
schema = documents.map do |doc|
{ attachment_uuid: doc.uuid, name: doc.filename.base }
end
render json: {
schema:,
documents: documents.as_json(
include: {
preview_images: { methods: %i[url metadata filename] }
}
)
}
end
end
end

@ -0,0 +1,25 @@
# frozen_string_literal: true
class ApplicationController < ActionController::Base
include ActiveStorage::SetCurrent
before_action :maybe_redirect_to_setup, unless: :signed_in?
before_action :authenticate_user!, unless: :devise_controller?
helper_method :button_title,
:current_account
private
def current_account
current_user&.account
end
def maybe_redirect_to_setup
redirect_to setup_index_path unless User.exists?
end
def button_title(title = 'Submit', disabled_with = 'Submitting...')
render_to_string(partial: 'shared/button_title', locals: { title:, disabled_with: })
end
end

@ -0,0 +1,7 @@
# frozen_string_literal: true
class DashboardController < ApplicationController
def index
@flows = current_account.flows.active
end
end

@ -0,0 +1,28 @@
# frozen_string_literal: true
class EmailSettingsController < ApplicationController
before_action :load_encrypted_config
def index; end
def create
if @encrypted_config.update(storage_configs)
redirect_to settings_email_index_path, notice: 'Changes have been saved'
else
render :index, status: :unprocessable_entity
end
end
private
def load_encrypted_config
@encrypted_config =
EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::EMAIL_SMTP_KEY)
end
def storage_configs
params.require(:encrypted_config).permit(value: {}).tap do |e|
e[:value].compact_blank!
end
end
end

@ -0,0 +1,36 @@
# frozen_string_literal: true
class FlowsController < ApplicationController
def show
@flow = current_account.flows.preload(documents_attachments: { preview_images_attachments: :blob })
.find(params[:id])
end
def new
@flow = current_account.flows.new
end
def create
@flow = current_account.flows.new(flow_params)
@flow.author = current_user
if @flow.save
redirect_to flow_path(@flow)
else
render turbo_stream: turbo_stream.replace(:modal, template: 'flows/new'), status: :unprocessable_entity
end
end
def destroy
@flow = current_account.flows.find(params[:id])
@flow.update!(deleted_at: Time.current)
redirect_to settings_users_path, notice: 'Flow has been archived.'
end
private
def flow_params
params.require(:flow).permit(:name, :schema)
end
end

@ -0,0 +1,4 @@
# frozen_string_literal: true
class InvitationsController < Devise::PasswordsController
end

@ -0,0 +1,23 @@
# frozen_string_literal: true
class RegistrationsController < Devise::RegistrationsController
private
def build_resource(_hash = {})
account = Account.new(account_params)
self.resource = account.users.new(user_params)
end
def user_params
return {} if params[:user].blank?
params.require(:user).permit(:first_name, :last_name, :email, :password)
end
def account_params
return {} if params[:account].blank?
params.require(:account).permit(:name)
end
end

@ -0,0 +1,21 @@
# frozen_string_literal: true
class SendSubmissionEmailController < ApplicationController
layout 'flow'
skip_before_action :authenticate_user!
def success; end
def create
@submission = if params[:flow_slug]
Submission.joins(:flow).find_by!(email: params[:email], flow: { slug: params[:flow_slug] })
else
Submission.find_by!(slug: params[:submission_slug])
end
SubmissionMailer.copy_to_submitter(@submission).deliver_later!
redirect_to success_send_submission_email_index_path
end
end

@ -0,0 +1,49 @@
# frozen_string_literal: true
class SetupController < ApplicationController
skip_before_action :maybe_redirect_to_setup
skip_before_action :authenticate_user!
before_action :redirect_to_root_if_signed, if: :signed_in?
before_action :ensure_first_user_not_created!
def index
@account = Account.new(account_params)
@user = @account.users.new(user_params)
end
def create
@account = Account.new(account_params)
@user = @account.users.new(user_params)
if @user.save
sign_in(@user)
redirect_to root_path
else
render :index, status: :unprocessable_entity
end
end
private
def user_params
return {} unless params[:user]
params.require(:user).permit(:first_name, :last_name, :email, :password)
end
def account_params
return {} unless params[:account]
params.require(:account).permit(:name)
end
def redirect_to_root_if_signed
redirect_to root_path, notice: 'You are already signed in'
end
def ensure_first_user_not_created!
redirect_to new_user_session_path, notice: 'Please sign in.' if User.exists?
end
end

@ -0,0 +1,51 @@
# frozen_string_literal: true
class StartFlowController < ApplicationController
layout 'flow'
skip_before_action :authenticate_user!
before_action :load_flow
def show
@submission = @flow.submissions.new
end
def update
@submission = @flow.submissions.find_or_initialize_by(
deleted_at: nil, **submission_params
)
if @submission.completed_at?
redirect_to start_flow_completed_path(@flow.slug, email: submission_params[:email])
else
@submission.assign_attributes(
opened_at: Time.current,
ip: request.remote_ip,
ua: request.user_agent
)
if @submission.save
redirect_to submit_flow_path(@submission.slug)
else
render :show
end
end
end
def completed
@submission = @flow.submissions.find_by(email: params[:email])
end
private
def submission_params
params.require(:submission).permit(:email)
end
def load_flow
slug = params[:slug] || params[:start_flow_slug]
@flow = Flow.find_by!(slug:)
end
end

@ -0,0 +1,32 @@
# frozen_string_literal: true
class StorageSettingsController < ApplicationController
before_action :load_encrypted_config
def index; end
def create
if @encrypted_config.update(storage_configs)
LoadActiveStorageConfigs.reload
redirect_to settings_storage_index_path, notice: 'Changes have been saved'
else
render :index, status: :unprocessable_entity
end
end
private
def load_encrypted_config
@encrypted_config =
EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::FILES_STORAGE_KEY)
end
def storage_configs
params.require(:encrypted_config).permit(value: {}).tap do |e|
e[:value].compact_blank!
e.dig(:value, :configs)&.compact_blank!
end
end
end

@ -0,0 +1,50 @@
# frozen_string_literal: true
class SubmissionsController < ApplicationController
before_action :load_flow, only: %i[index new create]
def index
@submissions = @flow.submissions.active
end
def show
@submission =
Submission.joins(:flow).where(flow: { account_id: current_account.id })
.preload(flow: { documents_attachments: { preview_images_attachments: :blob } })
.find(params[:id])
end
def new; end
def create
emails = params[:emails].to_s.scan(User::EMAIL_REGEXP)
submissions =
emails.map do |email|
submission = @flow.submissions.create!(email:, sent_at: params[:send_email] == '1' ? Time.current : nil)
if params[:send_email] == '1'
SubmissionMailer.invitation_email(submission, message: params[:message]).deliver_later!
end
submission
end
redirect_to flow_submissions_path(@flow), notice: "#{submissions.size} recepients added"
end
def destroy
submission = Submission.joins(:flow).where(flow: { account_id: current_account.id })
.find(params[:id])
submission.update!(deleted_at: Time.current)
redirect_to flow_submissions_path(submission.flow), notice: 'Submission has been archieved'
end
private
def load_flow
@flow = current_account.flows.find(params[:flow_id])
end
end

@ -0,0 +1,13 @@
# frozen_string_literal: true
class SubmissionsDownloadController < ApplicationController
skip_before_action :authenticate_user!
def index
submission = Submission.find_by(slug: params[:submission_slug])
Submissions::GenerateResultAttachments.call(submission)
redirect_to submission.archive.url, allow_other_host: true
end
end

@ -0,0 +1,28 @@
# frozen_string_literal: true
class SubmitFlowController < ApplicationController
layout 'flow'
skip_before_action :authenticate_user!
def show
@submission = Submission.preload(flow: { documents_attachments: { preview_images_attachments: :blob } })
.find_by!(slug: params[:slug])
return redirect_to submit_flow_completed_path(@submission.slug) if @submission.completed_at?
end
def update
submission = Submission.find_by!(slug: params[:slug])
submission.values.merge!(params[:values].to_unsafe_h)
submission.completed_at = Time.current if params[:completed] == 'true'
submission.save
head :ok
end
def completed
@submission = Submission.find_by!(slug: params[:submit_flow_slug])
end
end

@ -0,0 +1,56 @@
# frozen_string_literal: true
class UsersController < ApplicationController
before_action :load_user, only: %i[edit update destroy]
def index
@users = current_account.users.active.order(id: :desc)
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.'
else
render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity
end
end
def update
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
@user.update!(deleted_at: Time.current)
redirect_to settings_users_path, notice: 'User has been removed.'
end
private
def load_user
@user = current_account.users.find(params[:id])
end
def user_params
params.require(:user).permit(:email, :first_name, :last_name, :password)
end
end

@ -0,0 +1,36 @@
import '@hotwired/turbo-rails'
import { createApp } from 'vue'
import ToggleVisible from './elements/toggle_visible'
import DisableHidden from './elements/disable_hidden'
import TurboModal from './elements/turbo_modal'
import FlowArea from './elements/flow_area'
import FlowView from './elements/flow_view'
import Builder from './components/builder'
window.customElements.define('toggle-visible', ToggleVisible)
window.customElements.define('disable-hidden', DisableHidden)
window.customElements.define('turbo-modal', TurboModal)
window.customElements.define('flow-view', FlowView)
window.customElements.define('flow-area', FlowArea)
window.customElements.define('flow-builder', class extends HTMLElement {
connectedCallback () {
this.appElem = document.createElement('div')
this.app = createApp(Builder, {
dataFlow: this.dataset.flow
})
this.app.mount(this.appElem)
this.appendChild(this.appElem)
}
disconnectedCallback () {
this.app?.unmount()
this.appElem?.remove()
}
})

@ -0,0 +1,32 @@
@config "../../tailwind.application.config.js";
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
a[href],
input[type='checkbox'],
input[type='submit'],
input[type='image'],
input[type='radio'],
label[for],
select,
button {
cursor: pointer;
}
button .disabled {
display: none;
}
button[disabled] .disabled {
display: initial;
}
button .enabled {
display: initial;
}
button[disabled] .enabled {
display: none;
}

@ -0,0 +1,107 @@
<template>
<div
class="bg-red-100 absolute opacity-70"
:style="positionStyle"
@mousedown="startDrag"
>
<div
v-if="field"
class="flex items-center justify-center h-full w-full"
>
{{ field?.name || field.type }}
</div>
<span
class="h-2 w-2 right-0 bottom-0 bg-red-900 absolute cursor-nwse-resize"
@mousedown.stop="startResize"
/>
</div>
</template>
<script>
export default {
name: 'FieldArea',
props: {
scale: {
type: Number,
required: false,
default: 1
},
bounds: {
type: Object,
required: false,
default () {
return {
x: 0,
y: 0,
w: 0,
h: 0
}
}
},
field: {
type: Object,
required: false,
default: null
}
},
emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag'],
data () {
return {
isResize: false,
dragFrom: { x: 0, y: 0 }
}
},
computed: {
positionStyle () {
const { x, y, w, h } = this.bounds
return {
top: this.scale * y + 'px',
left: this.scale * x + 'px',
width: this.scale * w + 'px',
height: this.scale * h + 'px'
}
}
},
methods: {
resize (e) {
this.bounds.w = e.layerX / this.scale - this.bounds.x
this.bounds.h = e.layerY / this.scale - this.bounds.y
},
drag (e) {
if (e.toElement.id === 'mask') {
this.bounds.x = (e.layerX - this.dragFrom.x) / this.scale
this.bounds.y = (e.layerY - this.dragFrom.y) / this.scale
}
},
startDrag (e) {
const rect = e.target.getBoundingClientRect()
this.dragFrom = { x: e.clientX - rect.left, y: e.clientY - rect.top }
document.addEventListener('mousemove', this.drag)
document.addEventListener('mouseup', this.stopDrag)
this.$emit('start-drag')
},
stopDrag () {
document.removeEventListener('mousemove', this.drag)
document.removeEventListener('mouseup', this.stopDrag)
this.$emit('stop-drag')
},
startResize () {
document.addEventListener('mousemove', this.resize)
document.addEventListener('mouseup', this.stopResize)
this.$emit('start-resize')
},
stopResize () {
document.removeEventListener('mousemove', this.resize)
document.removeEventListener('mouseup', this.stopResize)
this.$emit('stop-resize')
}
}
}
</script>

@ -0,0 +1,154 @@
<template>
<div
class="flex"
style="max-height: calc(100vh - 24px)"
>
<div
class="overflow-auto w-full"
style="max-width: 280px"
>
Show documents preview (not pages but documents)
Allow to edit name
Allow to reorder
{{ flow.schema }}
<Upload
:flow-id="flow.id"
@success="updateFromUpload"
/>
<button
class="bg-green-300"
@click="save"
>
Save changes
</button>
<a
:href="`/flows/${flow.id}/submissions`"
>
Add Recepients
</a>
</div>
<div class="w-full overflow-auto">
<Document
v-for="document in sortedDocuments"
:key="document.uuid"
:areas-index="fieldAreasIndex[document.uuid]"
:document="document"
:is-draw="!!drawField"
@draw="onDraw"
/>
</div>
<div
class="w-full relative"
:class="drawField ? 'overflow-hidden' : 'overflow-auto'"
style="max-width: 280px"
>
<div
v-if="drawField"
class="sticky inset-0 bg-white h-full"
>
Draw {{ drawField.name }} field on the page
<button @click="drawField = false">
Cancel
</button>
</div>
<div>
FIelds
<Fields
v-model:fields="flow.fields"
@set-draw="drawField = $event"
/>
</div>
</div>
</div>
</template>
<script>
import Upload from './upload'
import Fields from './fields'
import Document from './document'
export default {
name: 'FlowBuilder',
components: {
Upload,
Document,
Fields
},
props: {
dataFlow: {
type: String,
default: '{}'
}
},
data () {
return {
drawField: null,
flow: {
name: '',
schema: [],
documents: [],
fields: []
}
}
},
computed: {
fieldAreasIndex () {
const areas = {}
this.flow.fields.forEach((f) => {
(f.areas || []).forEach((a) => {
areas[a.attachment_uuid] ||= {}
const acc = (areas[a.attachment_uuid][a.page] ||= [])
acc.push({ area: a, field: f })
})
})
return areas
},
sortedDocuments () {
return this.flow.schema.map((item) => {
return this.flow.documents.find(doc => doc.uuid === item.attachment_uuid)
})
}
},
mounted () {
this.flow = JSON.parse(this.dataFlow)
document.addEventListener('keyup', this.disableDrawOnEsc)
},
unmounted () {
document.removeEventListener('keyup', this.disableDrawOnEsc)
},
methods: {
disableDrawOnEsc (e) {
if (e.code === 'Escape') {
this.drawField = null
}
},
onDraw (area) {
this.drawField.areas ||= []
this.drawField.areas.push(area)
this.drawField = null
},
updateFromUpload ({ schema, documents }) {
this.flow.schema.push(...schema)
this.flow.documents.push(...documents)
this.save()
},
save () {
return fetch(`/api/flows/${this.flow.id}`, {
method: 'PUT',
body: JSON.stringify({ flow: this.flow }),
headers: { 'Content-Type': 'application/json' }
}).then((resp) => {
console.log(resp)
})
}
}
}
</script>

@ -0,0 +1,44 @@
<template>
<Page
v-for="(image, index) in sortedPreviewImages"
:key="image.id"
:number="index"
:areas="areasIndex[index]"
:is-draw="isDraw"
:class="{ 'cursor-crosshair': isDraw }"
:image="image"
@draw="$emit('draw', {...$event, attachment_uuid: document.uuid })"
/>
</template>
<script>
import Page from './page'
export default {
name: 'FlowDocument',
components: {
Page
},
props: {
document: {
type: Object,
required: true
},
areasIndex: {
type: Object,
required: false,
default: () => ({})
},
isDraw: {
type: Boolean,
required: false,
default: false
}
},
emits: ['draw'],
computed: {
sortedPreviewImages () {
return [...this.document.preview_images].sort((a, b) => parseInt(a.filename) - parseInt(b.filename))
}
}
}
</script>

@ -0,0 +1,81 @@
<template>
<div>
{{ field.type }}
<div v-if="field.type !== 'signature'">
<label>Name</label>
<input
v-model="field.name"
type="text"
required
>
</div>
<div>
<div
v-for="(option, index) in field.options"
:key="index"
class="flex"
>
<input
v-model="field.options[index]"
type="text"
required
>
<button @click="field.options.splice(index, 1)">
Remove
</button>
</div>
<button @click="field.options.push('')">
Add option
</button>
</div>
<div>
<div
v-for="(area, index) in areas"
:key="index"
>
{{ area }}
<button @click="removeArea(area)">
&times;
</button>
</div>
<button
class="block"
@click="$emit('set-draw', field)"
>
Draw area
</button>
</div>
<div>
<input
:id="`field_required_${field.uuid}`"
v-model="field.required"
type="checkbox"
required
>
<label :for="`field_required_${field.uuid}`">Required</label>
</div>
</div>
</template>
<script>
export default {
name: 'FlowField',
props: {
field: {
type: Object,
required: true
}
},
emits: ['set-draw'],
computed: {
areas () {
return this.field.areas || []
}
},
methods: {
removeArea (area) {
this.field.areas.splice(this.field.areas.indexOf(area), 1)
}
}
}
</script>

@ -0,0 +1,68 @@
<template>
<div class="space-y-2">
<Field
v-for="field in fields"
:key="field.uuid"
class="border"
:field="field"
@set-draw="$emit('set-draw', $event)"
/>
</div>
<button
v-for="item in fieldTypes"
:key="item.type"
class="block w-full"
@click="addField(item.value)"
>
Add {{ item.label }}
</button>
</template>
<script>
import Field from './field'
import { v4 } from 'uuid'
export default {
name: 'FlowFields',
components: {
Field
},
props: {
fields: {
type: Array,
required: true
}
},
emits: ['set-draw'],
computed: {
fieldTypes () {
return [
{ label: 'Text', value: 'text' },
{ label: 'Signature', value: 'signature' },
{ label: 'Date', value: 'date' },
{ label: 'Image', value: 'image' },
{ label: 'Attachment', value: 'attachment' },
{ label: 'Select', value: 'select' },
{ label: 'Checkbox', value: 'checkbox' },
{ label: 'Radio Group', value: 'radio' }
]
}
},
methods: {
addField (type) {
const field = {
name: type === 'signature' ? 'Signature' : '',
uuid: v4(),
required: true,
type
}
if (['select', 'checkbox', 'radio'].includes(type)) {
field.options = ['']
}
this.fields.push(field)
}
}
}
</script>

@ -0,0 +1,146 @@
<template>
<div class="relative">
<img
ref="image"
:src="image.url"
:width="width"
:height="height"
loading="lazy"
>
<div
class="top-0 bottom-0 left-0 right-0 absolute"
>
<FieldArea
v-for="(item, i) in areas"
:key="i"
:scale="scale"
:bounds="item.area"
:field="item.field"
@start-resize="showMask = true"
@stop-resize="showMask = false"
@start-drag="showMask = true"
@stop-drag="showMask = false"
/>
<FieldArea
v-if="newArea"
:scale="scale"
:bounds="newArea"
/>
</div>
<div
v-show="isDraw || showMask"
id="mask"
ref="mask"
class="top-0 bottom-0 left-0 right-0 absolute"
@pointerdown="onPointerdown"
@pointermove="onPointermove"
@pointerup="onPointerup"
/>
</div>
</template>
<script>
import FieldArea from './area'
export default {
name: 'FlowPage',
components: {
FieldArea
},
props: {
image: {
type: Object,
required: true
},
areas: {
type: Array,
required: false,
default: () => []
},
isDraw: {
type: Boolean,
required: false,
default: false
},
number: {
type: Number,
required: true
}
},
emits: ['draw'],
data () {
return {
scale: 1,
showMask: false,
newArea: null
}
},
computed: {
width () {
return this.image.metadata.width
},
height () {
return this.image.metadata.height
}
},
mounted () {
this.resizeObserver = new ResizeObserver(this.onResize)
this.resizeObserver.observe(this.$refs.image)
},
beforeUnmount () {
this.resizeObserver.unobserve(this.$refs.image)
},
methods: {
onResize () {
this.scale = this.$refs.image.clientWidth / this.image.metadata.width
},
onPointerdown (e) {
if (this.isDraw) {
this.newArea = {
initialX: e.layerX / this.scale,
initialY: e.layerY / this.scale,
x: e.layerX / this.scale,
y: e.layerY / this.scale,
w: 0,
h: 0
}
}
},
onPointermove (e) {
if (this.newArea) {
const dx = e.layerX / this.scale - this.newArea.initialX
const dy = e.layerY / this.scale - this.newArea.initialY
if (dx > 0) {
this.newArea.x = this.newArea.initialX
} else {
this.newArea.x = e.layerX / this.scale
}
if (dy > 0) {
this.newArea.y = this.newArea.initialY
} else {
this.newArea.y = e.layerY / this.scale
}
this.newArea.w = Math.abs(dx)
this.newArea.h = Math.abs(dy)
}
},
onPointerup (e) {
if (this.isDraw && this.newArea) {
this.$emit('draw', {
x: this.newArea.x,
y: this.newArea.y,
w: Math.max(this.newArea.w, 50),
h: Math.max(this.newArea.h, 40),
page: this.number
})
}
this.newArea = null
}
}
}
</script>

@ -0,0 +1,59 @@
<template>
<input
ref="input"
type="file"
multiple
@change="upload"
>
</template>
<script>
import { DirectUpload } from '@rails/activestorage'
export default {
name: 'DocumentsUpload',
props: {
flowId: {
type: [Number, String],
required: true
}
},
emits: ['success'],
methods: {
async upload () {
const blobs = await Promise.all(
Array.from(this.$refs.input.files).map(async (file) => {
const upload = new DirectUpload(
file,
'/direct_uploads',
this.$refs.input
)
return new Promise((resolve, reject) => {
upload.create((error, blob) => {
if (error) {
console.error(error)
return reject(error)
} else {
return resolve(blob)
}
})
}).catch((error) => {
console.error(error)
})
})
)
fetch(`/api/flows/${this.flowId}/documents`, {
method: 'POST',
body: JSON.stringify({ blobs }),
headers: { 'Content-Type': 'application/json' }
}).then(resp => resp.json()).then((data) => {
this.$emit('success', data)
this.$refs.input.value = ''
})
}
}
}
</script>

@ -0,0 +1,38 @@
export default class extends HTMLElement {
static observedAttributes = ['class']
connectedCallback () {
this.trigger()
}
attributeChangedCallback (attributeName, oldValue, newValue) {
if (attributeName === 'class' && oldValue !== newValue) {
this.trigger()
}
}
trigger () {
const hasHiddenClass = this.classList.contains('hidden')
const elements = this.querySelectorAll('input, textarea, select')
elements.forEach((element) => {
if (hasHiddenClass) {
element.disabled = true
if (!element.dataset.wasRequired) {
element.dataset.wasRequired = element.required
}
element.required = false
} else {
element.disabled = false
if (element.dataset.wasRequired) {
element.required = element.dataset.wasRequired === 'true'
delete element.dataset.wasRequired
}
}
})
}
}

@ -0,0 +1,76 @@
import { DirectUpload } from '@rails/activestorage'
import { actionable } from '@github/catalyst/lib/actionable'
import { target, targetable } from '@github/catalyst/lib/targetable'
export default actionable(targetable(class extends HTMLElement {
static [target.static] = [
'loading',
'input'
]
connectedCallback () {
this.addEventListener('drop', this.onDrop)
this.addEventListener('dragover', (e) => e.preventDefault())
}
onDrop (e) {
e.preventDefault()
this.uploadFiles(e.dataTransfer.files)
}
onSelectFiles (e) {
e.preventDefault()
this.uploadFiles(this.input.files).then(() => {
this.input.value = ''
})
}
async uploadFiles (files) {
const blobs = await Promise.all(
Array.from(files).map(async (file) => {
const upload = new DirectUpload(
file,
'/direct_uploads',
this.input
)
return new Promise((resolve, reject) => {
upload.create((error, blob) => {
if (error) {
console.error(error)
return reject(error)
} else {
return resolve(blob)
}
})
}).catch((error) => {
console.error(error)
})
})
)
await Promise.all(
blobs.map((blob) => {
return fetch('/api/attachments', {
method: 'POST',
body: JSON.stringify({
name: 'attachments',
blob_signed_id: blob.signed_id,
submission_slug: this.dataset.submissionSlug
}),
headers: { 'Content-Type': 'application/json' }
}).then(resp => resp.json()).then((data) => {
return data
})
})).then((result) => {
result.forEach((attachment) => {
this.dispatchEvent(new CustomEvent('upload', { detail: attachment }))
})
})
}
}))

@ -0,0 +1,16 @@
import { actionable } from '@github/catalyst/lib/actionable'
import { targets, targetable } from '@github/catalyst/lib/targetable'
export default actionable(targetable(class extends HTMLElement {
static [targets.static] = [
'items'
]
add (e) {
const elem = document.createElement('input')
elem.value = e.detail.uuid
elem.name = `values[${this.dataset.fieldUuid}][]`
this.prepend(elem)
}
}))

@ -0,0 +1,5 @@
export default class extends HTMLElement {
setValue (value) {
this.innerHTML = value
}
}

@ -0,0 +1,93 @@
import { targets, target, targetable } from '@github/catalyst/lib/targetable'
import { actionable } from '@github/catalyst/lib/actionable'
export default actionable(targetable(class extends HTMLElement {
static observedAttributes = ['data-scale']
static [target.static] = [
'form',
'completed',
'submitButton'
]
static [targets.static] = [
'areas',
'fields',
'steps'
]
passValueToArea (e) {
return (this.areas || []).forEach((area) => {
if (area.dataset.fieldUuid === e.target.id) {
area.setValue(e.target.value)
}
})
}
submitSignature () {
this.submitButton.click()
}
setVisibleStep (uuid) {
this.steps.forEach((step) => {
step.classList.toggle('hidden', step.dataset.fieldUuid !== uuid)
})
this.fields.find(f => f.id === uuid).focus()
}
submitForm (e) {
e.preventDefault()
e.submitter.setAttribute('disabled', true)
fetch(this.form.action, {
method: this.form.method,
body: new FormData(this.form)
}).then(response => {
console.log('Form submitted successfully!', response)
this.moveNextStep()
}).catch(error => {
console.error('Error submitting form:', error)
}).finally(() => {
e.submitter.removeAttribute('disabled')
})
}
moveStepBack (e) {
e.preventDefault()
const currentStepIndex = this.steps.findIndex((el) => !el.classList.contains('hidden'))
const previousStep = this.steps[currentStepIndex - 1]
if (previousStep) {
this.setVisibleStep(previousStep.dataset.fieldUuid)
}
}
moveNextStep () {
const currentStepIndex = this.steps.findIndex((el) => !el.classList.contains('hidden'))
const nextStep = this.steps[currentStepIndex + 1]
if (nextStep) {
this.setVisibleStep(nextStep.dataset.fieldUuid)
} else {
this.form.classList.add('hidden')
this.completed.classList.remove('hidden')
}
}
focusField ({ target }) {
this.setVisibleStep(target.dataset.fieldUuid)
}
focusArea ({ target }) {
const area = this.areas.find(a => target.id === a.dataset.fieldUuid)
if (area) {
area.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
}
}))

@ -0,0 +1,48 @@
import SignaturePad from 'signature_pad'
import { target, targetable } from '@github/catalyst/lib/targetable'
import { actionable } from '@github/catalyst/lib/actionable'
import { DirectUpload } from '@rails/activestorage'
export default actionable(targetable(class extends HTMLElement {
static [target.static] = [
'canvas',
'input'
]
connectedCallback () {
this.pad = new SignaturePad(this.canvas)
}
submit (e) {
e?.preventDefault()
this.canvas.toBlob((blob) => {
const file = new File([blob], 'signature.jpg', { type: 'image/jpg' })
new DirectUpload(
file,
'/direct_uploads'
).create((_error, data) => {
fetch('/api/attachments', {
method: 'POST',
body: JSON.stringify({
submission_slug: this.dataset.submissionSlug,
blob_signed_id: data.signed_id,
name: 'signatures'
}),
headers: { 'Content-Type': 'application/json' }
}).then((resp) => resp.json()).then((attachment) => {
this.input.value = attachment.uuid
this.dispatchEvent(new CustomEvent('upload', { details: attachment }))
})
})
}, 'image/jpeg', 0.95)
}
clear (e) {
e?.preventDefault()
this.pad.clear()
}
}))

@ -0,0 +1,11 @@
import { actionable } from '@github/catalyst/lib/actionable'
export default actionable(class extends HTMLElement {
trigger (event) {
const elementIds = JSON.parse(this.dataset.elementIds)
elementIds.forEach((elementId) => {
document.getElementById(elementId).classList.toggle('hidden', event.target.value !== elementId)
})
}
})

@ -0,0 +1,37 @@
import { actionable } from '@github/catalyst/lib/actionable'
export default actionable(class extends HTMLElement {
connectedCallback () {
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)
}
disconnectedCallback () {
document.body.classList.remove('overflow-hidden')
document.removeEventListener('keyup', this.onEscKey)
document.removeEventListener('turbo:submit-end', this.handleSubmit)
document.removeEventListener('turbo:before-cache', this.close)
}
onSubmit = (e) => {
if (e.detail.success) {
this.close()
}
}
onEscKey = (e) => {
if (e.code === 'Escape') {
this.close()
}
}
close = (e) => {
e?.preventDefault()
this.remove()
}
})

@ -0,0 +1,13 @@
import FlowArea from './elements/flow_area'
import FlowView from './elements/flow_view'
import DisableHidden from './elements/disable_hidden'
import FileDropzone from './elements/file_dropzone'
import SignaturePad from './elements/signature_pad'
import FilesList from './elements/files_list'
window.customElements.define('flow-view', FlowView)
window.customElements.define('flow-area', FlowArea)
window.customElements.define('disable-hidden', DisableHidden)
window.customElements.define('file-dropzone', FileDropzone)
window.customElements.define('signature-pad', SignaturePad)
window.customElements.define('files-list', FilesList)

@ -0,0 +1,32 @@
@config "../../tailwind.flow.config.js";
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
a[href],
input[type='checkbox'],
input[type='submit'],
input[type='image'],
input[type='radio'],
label[for],
select,
button {
cursor: pointer;
}
button .disabled {
display: none;
}
button[disabled] .disabled {
display: initial;
}
button .enabled {
display: initial;
}
button[disabled] .enabled {
display: none;
}

@ -0,0 +1,8 @@
# frozen_string_literal: true
class ApplicationMailer < ActionMailer::Base
default from: 'from@example.com'
layout 'mailer'
register_interceptor ActionMailerConfigsInterceptor
end

@ -0,0 +1,19 @@
# frozen_string_literal: true
class SubmissionMailer < ApplicationMailer
DEFAULT_MESSAGE = "You've been invited to submit documents."
def invitation_email(submission, message: DEFAULT_MESSAGE)
@submission = submission
@message = message
mail(to: @submission.email,
subject: 'You have been invited to submit forms')
end
def copy_to_submitter(submission)
@submission = submission
mail(to: submission.email, subject: 'Here is your copy')
end
end

@ -0,0 +1,11 @@
# frozen_string_literal: true
class UserMailer < ApplicationMailer
def invitation_email(user)
@user = user
@token = @user.send(:set_reset_password_token)
mail(to: @user.friendly_name,
subject: 'You have been invited to Docuseal')
end
end

@ -0,0 +1,18 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: accounts
#
# id :bigint not null, primary key
# name :string not null
# created_at :datetime not null
# updated_at :datetime not null
#
class Account < ApplicationRecord
has_many :users, dependent: :destroy
has_many :encrypted_configs, dependent: :destroy
has_many :flows, dependent: :destroy
has_many :active_users, -> { active }, dependent: :destroy,
inverse_of: :account, class_name: 'User'
end

@ -0,0 +1,7 @@
# frozen_string_literal: true
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
strip_attributes
end

@ -0,0 +1,32 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: encrypted_configs
#
# id :bigint not null, primary key
# key :string not null
# value :text not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
#
# Indexes
#
# index_encrypted_configs_on_account_id (account_id)
# index_encrypted_configs_on_account_id_and_key (account_id,key) UNIQUE
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id)
#
class EncryptedConfig < ApplicationRecord
FILES_STORAGE_KEY = 'active_storage'
EMAIL_SMTP_KEY = 'action_mailer_smtp'
belongs_to :account
encrypts :value
serialize :value, JSON
end

@ -0,0 +1,45 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: flows
#
# id :bigint not null, primary key
# deleted_at :datetime
# fields :string not null
# name :string not null
# schema :string not null
# slug :string not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# author_id :bigint not null
#
# Indexes
#
# index_flows_on_account_id (account_id)
# index_flows_on_author_id (author_id)
# index_flows_on_slug (slug) UNIQUE
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id)
# fk_rails_... (author_id => users.id)
#
class Flow < ApplicationRecord
belongs_to :author, class_name: 'User'
belongs_to :account
attribute :fields, :string, default: -> { [] }
attribute :schema, :string, default: -> { [] }
attribute :slug, :string, default: -> { SecureRandom.base58(8) }
serialize :fields, JSON
serialize :schema, JSON
has_many_attached :documents
has_many :submissions, dependent: :destroy
scope :active, -> { where(deleted_at: nil) }
end

@ -0,0 +1,47 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: submissions
#
# id :bigint not null, primary key
# completed_at :datetime
# deleted_at :datetime
# email :string not null
# ip :string
# opened_at :datetime
# sent_at :datetime
# slug :string not null
# ua :string
# values :string not null
# created_at :datetime not null
# updated_at :datetime not null
# flow_id :bigint not null
#
# Indexes
#
# index_submissions_on_email (email)
# index_submissions_on_flow_id (flow_id)
# index_submissions_on_slug (slug) UNIQUE
#
# Foreign Keys
#
# fk_rails_... (flow_id => flows.id)
#
class Submission < ApplicationRecord
belongs_to :flow
attribute :values, :string, default: -> { {} }
attribute :slug, :string, default: -> { SecureRandom.base58(8) }
serialize :values, JSON
has_one_attached :archive
has_many_attached :documents
has_many_attached :attachments
has_many_attached :images
has_many_attached :signatures
scope :active, -> { where(deleted_at: nil) }
end

@ -0,0 +1,66 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: users
#
# id :bigint not null, primary key
# current_sign_in_at :datetime
# current_sign_in_ip :string
# deleted_at :datetime
# email :string not null
# encrypted_password :string not null
# failed_attempts :integer default(0), not null
# first_name :string not null
# last_name :string not null
# last_sign_in_at :datetime
# last_sign_in_ip :string
# locked_at :datetime
# remember_created_at :datetime
# reset_password_sent_at :datetime
# reset_password_token :string
# role :string not null
# sign_in_count :integer default(0), not null
# unlock_token :string
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
#
# Indexes
#
# index_users_on_account_id (account_id)
# index_users_on_email (email) UNIQUE
# index_users_on_reset_password_token (reset_password_token) UNIQUE
# index_users_on_unlock_token (unlock_token) UNIQUE
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id)
#
class User < ApplicationRecord
ROLES = %w[admin].freeze
EMAIL_REGEXP =
/[a-z0-9][.']?(?:(?:[a-z0-9_-]++[.'])*[a-z0-9_-]++)*@(?:[a-z0-9]++[.-])*[a-z0-9]++\.[a-z]{2,}/i
belongs_to :account
devise :database_authenticatable, :recoverable, :rememberable, :validatable, :trackable
devise :registerable # if ENV['APP_MULTITENANT']
attribute :role, :string, default: 'admin'
scope :active, -> { where(deleted_at: nil) }
def active_for_authentication?
!deleted_at?
end
def full_name
[first_name, last_name].join(' ')
end
def friendly_name
"#{full_name} <#{email}>"
end
end

@ -0,0 +1,16 @@
Hellp
<div>
<%= link_to 'Create Flow', new_flow_path, data: { turbo_frame: :modal } %>
<%= link_to 'Storage settings', settings_storage_index_path %>
<%= link_to 'Email settings', settings_email_index_path %>
<%= link_to 'Users', settings_users_path %>
</div>
<div>
<% @flows.each do |flow| %>
<div>
<%= flow.name %> |
<a href="<%= flow_path(flow) %>">edit</a> |
<a href="<%= flow_submissions_path(flow) %>">submissions</a> |
</div>
<% end %>
</div>

@ -0,0 +1,5 @@
<p>Hello <%= @resource.email %>!</p>
<p>Someone has requested a link to change your password. You can do this through the link below.</p>
<p><%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %></p>
<p>If you didn't request this, please ignore this email.</p>
<p>Your password won't change until you access the link above and create a new one.</p>

@ -0,0 +1,20 @@
<h2>Change your password</h2>
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %>
<%= render 'devise/shared/error_messages', resource: %>
<%= f.hidden_field :reset_password_token %>
<div class="field">
<%= f.label :password, 'New password' %><br>
<% if @minimum_password_length %>
<em>(<%= @minimum_password_length %> characters minimum)</em><br>
<% end %>
<%= f.password_field :password, autofocus: true, autocomplete: 'new-password' %>
</div>
<div class="field">
<%= f.label :password_confirmation, 'Confirm new password' %><br>
<%= f.password_field :password_confirmation, autocomplete: 'new-password' %>
</div>
<div class="actions">
<%= f.submit 'Change my password' %>
</div>
<% end %>
<%= render 'devise/shared/links' %>

@ -0,0 +1,12 @@
<h2>Forgot your password?</h2>
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %>
<%= render 'devise/shared/error_messages', resource: %>
<div class="field">
<%= f.label :email %><br>
<%= f.email_field :email, autofocus: true, autocomplete: 'email' %>
</div>
<div class="actions">
<%= f.submit 'Send me reset password instructions' %>
</div>
<% end %>
<%= render 'devise/shared/links' %>

@ -0,0 +1,34 @@
<h2>Sign up</h2>
<%= form_for('', as: resource_name, url: registration_path) do |f| %>
<%= render 'devise/shared/error_messages', resource: %>
<%= f.fields_for resource do |ff| %>
<div>
<%= ff.label :first_name %>
<%= ff.text_field :first_name, required: true %>
</div>
<div>
<%= ff.label :last_name %>
<%= ff.text_field :last_name, required: true %>
</div>
<% end %>
<%= f.fields_for resource.account do |ff| %>
<div>
<%= ff.label :name, 'Company name' %>
<%= ff.text_field :name, required: true %>
</div>
<% end %>
<%= f.fields_for resource do |ff| %>
<div>
<%= ff.label :email %>
<%= ff.email_field :email, required: true %>
</div>
<div>
<%= ff.label :password %>
<%= ff.password_field :password, required: true %>
</div>
<% end %>
<div class="actions">
<%= f.submit 'Sign up' %>
</div>
<% end %>
<%= render 'devise/shared/links' %>

@ -0,0 +1,21 @@
<h2>Log in</h2>
<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
<div class="field">
<%= f.label :email %><br>
<%= f.email_field :email, autofocus: true, autocomplete: 'email' %>
</div>
<div class="field">
<%= f.label :password %><br>
<%= f.password_field :password, autocomplete: 'current-password' %>
</div>
<% if devise_mapping.rememberable? %>
<div class="field">
<%= f.check_box :remember_me, checked: true %>
<%= f.label :remember_me %>
</div>
<% end %>
<div class="actions">
<%= f.submit 'Log in' %>
</div>
<% end %>
<%= render 'devise/shared/links' %>

@ -0,0 +1,14 @@
<% if resource.errors.any? %>
<div id="error_explanation" data-turbo-cache="false">
<h2>
<%= I18n.t('errors.messages.not_saved',
count: resource.errors.count,
resource: resource.class.model_name.human.downcase) %>
</h2>
<ul>
<% resource.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>

@ -0,0 +1,25 @@
<%- if controller_name != 'sessions' %>
<%= link_to 'Log in', new_session_path(resource_name) %><br>
<% end %>
<%- if devise_mapping.registerable? && controller_name != 'registrations' %>
<%= link_to 'Sign up', new_registration_path %><br>
<% end %>
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
<%= link_to 'Forgot your password?', new_password_path(resource_name) %><br>
<% end %>
<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
<%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %><br>
<% end %>
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
<%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %><br>
<% end %>
<%- if devise_mapping.omniauthable? %>
<%- resource_class.omniauth_providers.each do |provider| %>
<%= button_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), data: { turbo: false } %><br>
<% end %>
<% end %>

@ -0,0 +1,27 @@
Email settings
<% value = @encrypted_config.value || {} %>
<%= form_for @encrypted_config, url: settings_email_index_path, method: :post, html: { autocomplete: 'off' } do |f| %>
<%= f.fields_for :value do |ff| %>
<div>
<%= ff.label :host %>
<%= ff.text_field :host, value: value['host'], required: true %>
</div>
<div>
<%= ff.label :port %>
<%= ff.text_field :port, value: value['port'], required: true %>
</div>
<div>
<%= ff.label :username %>
<%= ff.text_field :username, value: value['username'], required: true %>
</div>
<div>
<%= ff.label :password %>
<%= ff.password_field :password, value: value['password'], required: true %>
</div>
<div>
<%= ff.label :from_email, 'Send from' %>
<%= ff.email_field :from_email, value: value['from_email'], required: true %>
</div>
<% end %>
<%= f.button button_title %>
<% end %>

@ -0,0 +1,9 @@
<%= render 'shared/turbo_modal' do %>
<%= form_for @flow, data: { turbo_frame: :_top } do |f| %>
<div>
<%= f.label :name %>
<%= f.text_field :name, required: true %>
</div>
<%= f.button button_title %>
<% end %>
<% end %>

@ -0,0 +1 @@
<flow-builder data-flow="<%= @flow.to_json(include: { documents: { include: { preview_images: { methods: %i[url metadata filename] } } } }) %>"></flow-builder>

@ -0,0 +1,19 @@
<h2>Welcome to Docuseal</h2>
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %>
<%= render 'devise/shared/error_messages', resource: %>
<%= f.hidden_field :reset_password_token %>
<div class="field">
<%= f.label :password, 'Set password' %><br>
<% if @minimum_password_length %>
<em>(<%= @minimum_password_length %> characters minimum)</em><br>
<% end %>
<%= f.password_field :password, autofocus: true, autocomplete: 'new-password' %>
</div>
<div class="field">
<%= f.label :password_confirmation, 'Confirm new password' %><br>
<%= f.password_field :password_confirmation, autocomplete: 'new-password' %>
</div>
<div class="actions">
<%= f.submit 'Save password and Sign in' %>
</div>
<% end %>

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<title>
Docuseal
</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<%= javascript_pack_tag 'application', defer: true %>
<%= stylesheet_pack_tag 'application', media: 'all' %>
<%= yield :head %>
</head>
<body class="font-sans antialiased font-normal leading-normal bg-white text-gray-700">
<turbo-frame id="modal"></turbo-frame>
<%= render 'shared/navbar' %>
<div>
<%= flash[:notice] || flash[:alert] %>
</div>
<%= yield %>
</body>
</html>

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<title>
Docuseal
</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<%= javascript_pack_tag 'flow', defer: true %>
<%= stylesheet_pack_tag 'flow', media: 'all' %>
<%= yield :head %>
</head>
<body class="font-sans antialiased font-normal leading-normal bg-white text-gray-700">
<%= yield %>
</body>
</html>

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style>
</style>
</head>
<body>
<%= yield %>
</body>
</html>

@ -0,0 +1,30 @@
Setup
<%= form_for '', url: setup_index_path do |f| %>
<%= f.fields_for @user do |ff| %>
<div>
<%= ff.label :first_name %>
<%= ff.text_field :first_name, required: true %>
</div>
<div>
<%= ff.label :last_name %>
<%= ff.text_field :last_name, required: true %>
</div>
<% end %>
<%= f.fields_for @account do |ff| %>
<div>
<%= ff.label :name, 'Company name' %>
<%= ff.text_field :name, required: true %>
</div>
<% end %>
<%= f.fields_for @user do |ff| %>
<div>
<%= ff.label :email %>
<%= ff.email_field :email, required: true %>
</div>
<div>
<%= ff.label :password %>
<%= ff.password_field :password, required: true %>
</div>
<% end %>
<%= f.button button_title %>
<% end %>

@ -0,0 +1,6 @@
<span class="enabled">
<%= title %>
</span>
<span class="disabled">
<%= disabled_with %>
</span>

@ -0,0 +1,7 @@
<% if signed_in? %>
<div>
<%= link_to 'Home', root_path, class: 'bg-red-500' %>
<%= link_to 'Sign out', destroy_user_session_path, data: { turbo_method: :delete } %>
<%= current_user.email %>
</div>
<% end %>

@ -0,0 +1,11 @@
<turbo-frame id="modal">
<turbo-modal class="left-52 absolute top-0 z-50 bg-white h-[100vh] w-full">
<div>
Modal window Title
<a href="#" data-action="click:turbo-modal#close">&times;</a>
</div>
<div>
<%= yield %>
</div>
</turbo-modal>
</turbo-frame>

@ -0,0 +1,5 @@
<p>
Form has been submitted alredy by ypu - thanks!
</p>
<%= button_to button_title('Send copy to Email'), send_submission_email_index_path, params: { flow_slug: @flow.slug, email: params[:email] }, form: { onsubmit: 'event.submitter.disabled = true' } %>
<%# do not allow donwload for securetiy reaosn<a href="">Download documets</a> %>

@ -0,0 +1,9 @@
You have been invited to submit flow <%= @flow.name %>
<%= form_for @submission, url: start_flow_path(@flow.slug), data: { turbo_frame: :_top }, method: :put do |f| %>
Provide youe email to start
<div>
<%= f.label :email %>
<%= f.email_field :email, required: true %>
</div>
<%= f.button button_title %>
<% end %>

@ -0,0 +1,67 @@
Storage settings
<% value = @encrypted_config.value || { 'service' => 'disk' } %>
<% configs = value['configs'] || {} %>
<%= form_for @encrypted_config, url: settings_storage_index_path, method: :post, html: { autocomplete: 'off' } do |f| %>
<% options = [['Disk', 'disk'], ['AWS S3', 'aws_s3'], ['Google Cloud', 'google']] %>
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>">
<% [['Disk', 'disk'], ['AWS S3', 'aws_s3'], ['Google Cloud', 'google']].each do |(label, val)| %>
<%= f.radio_button :selected, val, checked: value['service'] == val, data: { action: 'change:toggle-visible#trigger' } %>
<%= f.label :selected, label, value: val %>
<% end %>
</toggle-visible>
<disable-hidden id="disk" class="<%= 'hidden' if value['service'] != 'disk' %>">
<%= f.fields_for :value do |ff| %>
<%= ff.hidden_field :service, value: 'disk' %>
<% end %>
<div>
Disk storage - no configs needed but make sure you have a persistant disk (heroku doesnt not have one)
</div>
</disable-hidden>
<disable-hidden id="aws_s3" class="<%= 'hidden' if value['service'] != 'aws_s3' %>">
<%= f.fields_for :value do |ff| %>
<%= ff.hidden_field :service, value: 'aws_s3' %>
<%= ff.fields_for :configs, configs do |fff| %>
<div>
<%= fff.label :access_key_id, 'Access key ID' %>
<%= fff.text_field :access_key_id, value: configs['access_key_id'], required: true %>
</div>
<div>
<%= fff.label :secret_access_key %>
<%= fff.password_field :secret_access_key, value: configs['secret_access_key'], required: true %>
</div>
<div>
<%= fff.label :region %>
<%= fff.text_field :region, value: configs['region'], required: true %>
</div>
<div>
<%= fff.label :bucket %>
<%= fff.text_field :bucket, value: value['service'] == 'aws_s3' ? configs['bucket'] : '', required: true %>
</div>
<div>
<%= fff.label :endpoint %>
<%= fff.text_field :endpoint, value: configs['endpoint'], type: :url %>
</div>
<% end %>
<% end %>
</disable-hidden>
<disable-hidden id="google" class="<%= 'hidden' if value['service'] != 'google' %>">
<%= f.fields_for :value do |ff| %>
<%= ff.hidden_field :service, value: 'google' %>
<%= ff.fields_for :configs, configs do |fff| %>
<div>
<%= fff.label :project, 'Project' %>
<%= fff.text_field :project, value: configs['project'], required: true %>
</div>
<div>
<%= fff.label :bucket %>
<%= fff.text_field :bucket, value: value['service'] == 'google' ? configs['bucket'] : '', required: true %>
</div>
<div>
<%= fff.label :credentials, 'Credentials (JSON key content)' %>
<%= fff.text_area :credentials, value: configs['credentials'], required: true %>
</div>
<% end %>
<% end %>
</disable-hidden>
<%= f.button button_title %>
<% end %>

@ -0,0 +1,3 @@
<p>Hi</a>
<%= @submission.values %>
<%= link_to 'Download', submission_download_index_url(@submission.slug) %>

@ -0,0 +1,4 @@
<p>Hi there</p>
<p>You have been invited to submit a form:</p>
<p><%= link_to 'Submit', submit_flow_index_url(slug: @submission.slug) %></p>
<p>If you didn't request this, please ignore this email.</p>

@ -0,0 +1,40 @@
Submissions
Flow <%= @flow.name %>
Copy share link:
<input autocomplete="off" type="text" class="w-full" value="<%= start_flow_url(slug: @flow.slug) %>" disabled>
<a href="<%= new_flow_submission_path(@flow) %>" class="bg-green-600" data-turbo-frame="modal">Add Recepients</a>
<table>
<tr>
<th>
Email
</th>
<th>
Status
</th>
<th>
</th>
</tr>
<% @submissions.each do |submission| %>
<tr>
<td>
<%= submission.email %>
</td>
<td>
<% if submission.completed_at? %>
Completed
<% elsif submission.opened_at? %>
Opened
<% elsif submission.sent_at? %>
Sent
<% else %>
Awaiting
<% end %>
</td>
<td>
copy link<br>
<%= link_to 'View', submission_path(@flow) %>
<%= button_to 'Remove', submission_path(submission), method: :delete, data: { turbo_confirm: 'Are you sure?' } %>
</td>
</tr>
<% end %>
</table>

@ -0,0 +1,19 @@
<%= render 'shared/turbo_modal' do %>
<%= form_for '', url: flow_submissions_path(@flow), data: { turbo_frame: :_top } do |f| %>
<div>
<%= f.label :emails %>
<%= f.text_area :emails, required: true %>
</div>
<div>
<%= f.check_box :send_email, { onchange: "message_field.classList.toggle('hidden', !event.currentTarget.checked)" } %>
<%= f.label :send_email %>
</div>
<div id="message_field" class="hidden">
Hi There,
<%= f.text_area :message, value: SubmissionMailer::DEFAULT_MESSAGE, required: true %>
Thanks,
<%= current_account.name %>
</div>
<%= f.button button_title %>
<% end %>
<% end %>

@ -0,0 +1,21 @@
Flow: <%= @submission.flow.name %>
Sub: <%= @submission.slug %>
Copy share link:
<input autocomplete="off" type="text" class="w-full" value="<%= submit_flow_url(slug: @submission.slug) %>" disabled>
<% @submission.flow.fields.each do |field| %>
<div>
<%= field['name'] %>:
<% if ['image', 'signature'].include?(field['type']) %>
<% Array.wrap(@submission.values[field['uuid']]).each do |uuid| %>
<img src="<%= ActiveStorage::Attachment.find_by(uuid:).url %>">
<% end %>
<% elsif ['attachment'].include?(field['type']) %>
<% Array.wrap(@submission.values[field['uuid']]).each do |uuid| %>
<% attachment = ActiveStorage::Attachment.find_by(uuid:) %>
<a href="<%= attachment.url %>"><%= attachment.filename %></a>
<% end %>
<% else %>
<%= @submission.values[field['uuid']] %>
<% end %>
</div>
<% end %>

@ -0,0 +1,3 @@
<flow-area data-field-uuid="<%= field['uuid'] %>" data-action="click:flow-view#focusField" data-targets="flow-view.areas" class=" cursor-pointer bg-red-100 absolute" style="width: <%= area['w'] * 100 / page.metadata['width'] %>%; height: <%= area['h'] * 100 / page.metadata['height'] %>%; left: <%= area['x'] * 100 / page.metadata['width'] %>%; top: <%= area['y'] * 100 / page.metadata['height'] %>%">
<%= submission.values[field['uuid']] %>
</flow-area>

@ -0,0 +1,5 @@
<p>
Form completed - thanks!
</p>
<%= button_to button_title('Send copy to Email'), send_submission_email_index_path, params: { submission_slug: @submission.slug }, form: { onsubmit: 'event.submitter.disabled = true' } %>
<%= button_to button_title('Download documents'), submission_download_index_path(@submission.slug), method: :get, form: { onsubmit: 'event.submitter.disabled = true' } %>

@ -0,0 +1,83 @@
<% fields_index = Flows.build_field_areas_index(@submission.flow) %>
<flow-view class="mx-auto block" style="max-width: 1000px">
<% @submission.flow.schema.each do |item| %>
<% document = @submission.flow.documents.find { |a| a.uuid == item['attachment_uuid'] } %>
<% document.preview_images.sort_by { |a| a.filename.base.to_i }.each_with_index do |page, index| %>
<div class="relative">
<img src="<%= page.url %>" width="<%= page.metadata['width'] %>" height="<%= page.metadata['height'] %>" loading="lazy">
<div class="top-0 bottom-0 left-0 right-0 absolute">
<% fields_index.dig(document.uuid, index)&.each do |values| %>
<%= render 'area', submission: @submission, page:, **values %>
<% end %>
</div>
</div>
<% end %>
<% end %>
<div class="sticky bottom-8 w-full">
<div class="bg-white mx-8 md:mx-32 border p-4 rounded">
<form data-target="flow-view.form" data-action="submit:flow-view#submitForm" action="<%= submit_flow_path(slug: @submission.slug) %>" method="post">
<input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>">
<input value="put" name="_method" type="hidden">
<% visible_step_index = nil %>
<% @submission.flow.fields.each_with_index do |field, index| %>
<% visible_step_index ||= index if @submission.values[field['uuid']].blank? %>
<disable-hidden data-field-uuid="<%= field['uuid'] %>" data-targets="flow-view.steps" class="block <%= 'hidden' if index != visible_step_index %>">
<% if index != 0 %>
<button data-action="click:flow-view#moveStepBack">
Back
</button>
<% end %>
<label for="<%= field['uuid'] %>"><%= field['name'].presence || 'FIeld' %></label>
<% if index == @submission.flow.fields.size - 1 %>
<input type="hidden" name="completed" value="true">
<% end %>
<% if field['type'].in?(['text', 'date']) %>
<input <%= html_attributes(required: 'true') if field['required'] %> id="<%= field['uuid'] %>" data-targets="flow-view.fields" data-action="input:flow-view#passValueToArea focus:flow-view#focusArea" value="<%= @submission.values[field['uuid']] %>" type="<%= field['type'] %>" name="values[<%= field['uuid'] %>]">
<% elsif field['type'] == 'select' %>
<select <%= html_attributes(required: 'true') if field['required'] %> id="<%= field['uuid'] %>" data-targets="flow-view.fields" data-action="input:flow-view#passValueToArea focus:flow-view#focusArea" name="values[<%= field['uuid'] %>]">
<option value="" disabled selected>Select your option</option>
<% field['options'].each do |option| %>
<option <%= html_attributes(selected: 'true') if @submission.values[field['uuid']] == option %> value="<%= option %>"><%= option %></option>
<% end %>
</select>
<% elsif field['type'] == 'image' || field['type'] == 'attachment' %>
<br>
<files-list data-field-uuid="<%= field['uuid'] %>">
<file-dropzone data-action="upload:files-list#add" data-submission-slug="<%= @submission.slug %>">
<% uuid = SecureRandom.uuid %>
<label for="<%= uuid %>">
Upload
<%= field['name'] || 'Attach' %>
</label>
<input multiple data-target="file-dropzone.input" data-action="change:file-dropzone#onSelectFiles" id="<%= uuid %>" type="file" class="hidden">
</file-dropzone>
</files-list>
<% elsif field['type'] == 'signature' %>
<signature-pad data-submission-slug="<%= @submission.slug %>" data-action="upload:flow-view#submitSignature">
<input data-target="signature-pad.input" type="hidden" name="values[<%= field['uuid'] %>]" value="<%= @submission.values[field['uuid']] %>">
<canvas data-target="signature-pad.canvas">
</canvas>
<button data-action="click:signature-pad#submit">
Ok
</button>
<button data-action="click:signature-pad#clear">
Clear
</button>
</signature-pad>
<% elsif field['type'] == 'radio' %>
<% elsif field['type'] == 'checkbox' %>
<% end %>
</disable-hidden>
<% end %>
<button data-target="flow-view.submitButton" type="submit"><%= button_title %></button>
</form>
<div data-target="flow-view.completed" class="hidden">
<p>
Form completed - thanks!
</p>
<%= button_to 'Send copy to Email', send_submission_email_index_path, params: { submission_slug: @submission.slug }, form: { onsubmit: 'event.submitter.disabled = true' } %>
<%= button_to button_title('Download documents'), submission_download_index_path(@submission.slug), method: :get, form: { onsubmit: 'event.submitter.disabled = true' } %>
</div>
</div>
</div>
</flow-view>

@ -0,0 +1,4 @@
<p>Hello <%= @user.first_name %>,</p>
<p>You have been invited to Docuseal. You can sign up this through the link below.</p>
<p><%= link_to 'Set my password', invitation_url(reset_password_token: @token) %></p>
<p>If you didn't request this, please ignore this email.</p>

@ -0,0 +1,19 @@
<%= form_for user, data: { turbo_frame: :_top } do |f| %>
<div>
<%= f.label :first_name %>
<%= f.text_field :first_name, required: true %>
</div>
<div>
<%= f.label :last_name %>
<%= f.text_field :last_name, required: true %>
</div>
<div>
<%= f.label :email %>
<%= f.email_field :email, required: true %>
</div>
<div>
<%= f.label :password %>
<%= f.password_field :password, required: user.new_record? %>
</div>
<%= f.button button_title %>
<% end %>

@ -0,0 +1,3 @@
<%= render 'shared/turbo_modal' do %>
<%= render 'form', user: @user %>
<% end %>

@ -0,0 +1,37 @@
<div>
Users
<a href="<%= new_user_path %>" data-turbo-frame="modal">New User</a>
</div>
<table>
<tr>
<th>
User
</th>
<th>
Role
</th>
<th>
Last session
</th>
<th>
</th>
</tr>
<% @users.each do |user| %>
<tr>
<td>
<%= user.full_name %><br>
<%= user.email %>
</td>
<td>
<%= user.role %>
</td>
<td>
<%= user.last_sign_in_at ? l(user.last_sign_in_at) : '-' %>
</td>
<td>
<%= link_to 'Edit', edit_user_path(user), data: { turbo_frame: 'modal' } %>
<%= button_to 'Remove', user_path(user), method: :delete, data: { turbo_confirm: 'Are you sure?' } %>
</td>
</tr>
<% end %>
</table>

@ -0,0 +1,3 @@
<%= render 'shared/turbo_modal' do %>
<%= render 'form', user: @user %>
<% end %>

@ -0,0 +1,109 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# This file was generated by Bundler.
#
# The application 'bundle' is installed as part of a gem, and
# this file is here to facilitate running it.
#
require "rubygems"
m = Module.new do
module_function
def invoked_as_script?
File.expand_path($0) == File.expand_path(__FILE__)
end
def env_var_version
ENV["BUNDLER_VERSION"]
end
def cli_arg_version
return unless invoked_as_script? # don't want to hijack other binstubs
return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update`
bundler_version = nil
update_index = nil
ARGV.each_with_index do |a, i|
if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN
bundler_version = a
end
next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/
bundler_version = $1
update_index = i
end
bundler_version
end
def gemfile
gemfile = ENV["BUNDLE_GEMFILE"]
return gemfile if gemfile && !gemfile.empty?
File.expand_path("../Gemfile", __dir__)
end
def lockfile
lockfile =
case File.basename(gemfile)
when "gems.rb" then gemfile.sub(/\.rb$/, ".locked")
else "#{gemfile}.lock"
end
File.expand_path(lockfile)
end
def lockfile_version
return unless File.file?(lockfile)
lockfile_contents = File.read(lockfile)
return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/
Regexp.last_match(1)
end
def bundler_requirement
@bundler_requirement ||=
env_var_version ||
cli_arg_version ||
bundler_requirement_for(lockfile_version)
end
def bundler_requirement_for(version)
return "#{Gem::Requirement.default}.a" unless version
bundler_gem_version = Gem::Version.new(version)
bundler_gem_version.approximate_recommendation
end
def load_bundler!
ENV["BUNDLE_GEMFILE"] ||= gemfile
activate_bundler
end
def activate_bundler
gem_error = activation_error_handling do
gem "bundler", bundler_requirement
end
return if gem_error.nil?
require_error = activation_error_handling do
require "bundler/version"
end
return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION))
warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`"
exit 42
end
def activation_error_handling
yield
nil
rescue StandardError, LoadError => e
e
end
end
m.load_bundler!
if m.invoked_as_script?
load Gem.bin_path("bundler", "bundle")
end

@ -0,0 +1,4 @@
#!/usr/bin/env ruby
APP_PATH = File.expand_path("../config/application", __dir__)
require_relative "../config/boot"
require "rails/commands"

@ -0,0 +1,4 @@
#!/usr/bin/env ruby
require_relative "../config/boot"
require "rake"
Rake.application.run

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

Loading…
Cancel
Save