mirror of https://github.com/docusealco/docuseal
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
|
||||
@ -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,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)">
|
||||
×
|
||||
</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 @@
|
||||
<%= yield %>
|
||||
@ -0,0 +1 @@
|
||||
Email has bee sent
|
||||
@ -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">×</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…
Reference in new issue