mirror of
https://github.com/docusealco/docuseal.git
synced 2026-08-07 15:25:16 +00:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 077c55f7e8 | |||
| e5c5620b20 | |||
| 187b354605 | |||
| 545b18086a | |||
| 130c91178f | |||
| 47c57158f4 | |||
| 3b745a650d | |||
| 6871a3f840 | |||
| c57148eada | |||
| d875477ee6 | |||
| 1daee8214c | |||
| 9cc85595e3 | |||
| f6879d7f94 | |||
| f6247e68b5 | |||
| 454acb63db | |||
| 6d78d5430c | |||
| e75e7b18b4 | |||
| 644b142798 | |||
| e4bb5466f5 | |||
| 8929adbe83 | |||
| 896b149b25 | |||
| 7697b94f73 | |||
| 7912c9a1ee | |||
| 63e1246a11 | |||
| cbca34a7d7 | |||
| 9471a510c9 | |||
| e754c5d414 | |||
| 182f593d86 | |||
| 7f4548e5c7 | |||
| 5e4f207844 | |||
| 1f29e624e7 | |||
| 658e8a7bd6 | |||
| 08c3b04511 | |||
| 7779eb880d | |||
| 21b9ca8ea5 | |||
| db7e3794d9 | |||
| df1d78897a | |||
| e1eb2da6f9 | |||
| a92d1d82c1 | |||
| 2dcfc41365 | |||
| 15cbdfb20c | |||
| a2cf8fc5b5 | |||
| f94ef3afdc | |||
| 9e3a9f0eeb | |||
| 1280572e6f | |||
| cba2e84338 | |||
| 17ca8f013a | |||
| 96ef7ffaf7 | |||
| 1bbcdb8073 | |||
| da130c3f4f | |||
| c15f3ecd83 | |||
| df0e0e6a1b |
@@ -1,5 +1,6 @@
|
||||
/db/*.sqlite3
|
||||
|
||||
**/node_modules
|
||||
/node_modules
|
||||
/coverage
|
||||
/doc
|
||||
@@ -27,3 +28,4 @@
|
||||
/attachments
|
||||
/docuseal
|
||||
.DS_Store
|
||||
.env
|
||||
|
||||
@@ -127,6 +127,7 @@ jobs:
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 4
|
||||
yarn install
|
||||
sudo apt-get update
|
||||
sudo apt-get install libvips
|
||||
- name: Run
|
||||
env:
|
||||
|
||||
@@ -35,3 +35,4 @@ yarn-debug.log*
|
||||
/coverage
|
||||
/attachments
|
||||
/docuseal
|
||||
/ee
|
||||
|
||||
+4
-1
@@ -27,7 +27,7 @@ Lint/MissingSuper:
|
||||
Enabled: false
|
||||
|
||||
Metrics/MethodLength:
|
||||
Max: 20
|
||||
Max: 25
|
||||
Exclude:
|
||||
- 'db/migrate/**'
|
||||
|
||||
@@ -61,5 +61,8 @@ RSpec/ExampleLength:
|
||||
Rails/I18nLocaleTexts:
|
||||
Enabled: false
|
||||
|
||||
Rails/SkipsModelValidations:
|
||||
Enabled: false
|
||||
|
||||
Rails/ApplicationController:
|
||||
Enabled: false
|
||||
|
||||
+9
-1
@@ -37,7 +37,15 @@ COPY ./Gemfile ./Gemfile.lock ./
|
||||
|
||||
RUN bundle update --bundler && bundle install && rm -rf ~/.bundle
|
||||
|
||||
COPY . ./
|
||||
COPY ./bin ./bin
|
||||
COPY ./app ./app
|
||||
COPY ./config ./config
|
||||
COPY ./db ./db
|
||||
COPY ./log ./log
|
||||
COPY ./lib ./lib
|
||||
COPY ./public ./public
|
||||
COPY ./tmp ./tmp
|
||||
COPY LICENSE README.md Rakefile config.ru ./
|
||||
|
||||
COPY --from=webpack /app/public/packs ./public/packs
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ gem 'faraday'
|
||||
gem 'google-cloud-storage', require: false
|
||||
gem 'hexapdf'
|
||||
gem 'image_processing'
|
||||
gem 'jwt'
|
||||
gem 'lograge'
|
||||
gem 'mysql2', require: false
|
||||
gem 'oj'
|
||||
@@ -23,6 +24,7 @@ gem 'pg', require: false
|
||||
gem 'premailer-rails'
|
||||
gem 'puma'
|
||||
gem 'rails'
|
||||
gem 'rails_autolink'
|
||||
gem 'rails-i18n'
|
||||
gem 'rollbar', require: ENV.key?('ROLLBAR_ACCESS_TOKEN')
|
||||
gem 'ruby-vips'
|
||||
|
||||
@@ -379,6 +379,10 @@ GEM
|
||||
rails-i18n (7.0.7)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 6.0.0, < 8)
|
||||
rails_autolink (1.1.8)
|
||||
actionview (> 3.1)
|
||||
activesupport (> 3.1)
|
||||
railties (> 3.1)
|
||||
railties (7.0.5)
|
||||
actionpack (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
@@ -540,6 +544,7 @@ DEPENDENCIES
|
||||
google-cloud-storage
|
||||
hexapdf
|
||||
image_processing
|
||||
jwt
|
||||
letter_opener_web
|
||||
lograge
|
||||
mysql2
|
||||
@@ -553,6 +558,7 @@ DEPENDENCIES
|
||||
puma
|
||||
rails
|
||||
rails-i18n
|
||||
rails_autolink
|
||||
rollbar
|
||||
rspec-rails
|
||||
rubocop
|
||||
|
||||
@@ -26,6 +26,8 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
||||
</p>
|
||||
<h2 align="center">
|
||||
<a href="https://demo.docuseal.co">✨ Live Demo</a>
|
||||
<span>|</span>
|
||||
<a href="https://docuseal.co/sign_up">☁️ Try in Cloud</a>
|
||||
</h2>
|
||||
|
||||
[](https://demo.docuseal.co)
|
||||
@@ -40,13 +42,16 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
||||
- [x] PDF signature verification
|
||||
- [x] Users management
|
||||
- [x] Mobile-optimized
|
||||
- [x] API and Webhooks for integrations
|
||||
- [x] Easy to deploy in minutes
|
||||
|
||||
## Deploy
|
||||
|
||||
| Heroku | Railway | DigitalOcean |
|
||||
| ---------------------------------------------------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
|
||||
| [<img alt="Deploy on Heroku" src="https://www.herokucdn.com/deploy/button.svg" height="40">](https://heroku.com/deploy?template=https://github.com/docusealco/docuseal-heroku) | [<img alt="Deploy on Railway" src="https://railway.app/button.svg" height="40">](https://railway.app/template/IGoDnc?referralCode=ruU7JR) | [<img alt="Deploy on DigitalOcean" src="https://www.deploytodo.com/do-btn-blue.svg" height="40">](https://cloud.digitalocean.com/apps/new?repo=https://github.com/docusealco/docuseal-digitalocean/tree/master&refcode=421d50f53990) |
|
||||
|Heroku|Railway|
|
||||
|:--:|:---:|
|
||||
| [<img alt="Deploy on Heroku" src="https://www.herokucdn.com/deploy/button.svg" height="40">](https://heroku.com/deploy?template=https://github.com/docusealco/docuseal-heroku) | [<img alt="Deploy on Railway" src="https://railway.app/button.svg" height="40">](https://railway.app/template/IGoDnc?referralCode=ruU7JR)|
|
||||
|**DigitalOcean**|**Render**|
|
||||
| [<img alt="Deploy on DigitalOcean" src="https://www.deploytodo.com/do-btn-blue.svg" height="40">](https://cloud.digitalocean.com/apps/new?repo=https://github.com/docusealco/docuseal-digitalocean/tree/master&refcode=421d50f53990) | [<img alt="Deploy to Render" src="https://render.com/images/deploy-to-render-button.svg" height="40">](https://render.com/deploy?repo=https://github.com/docusealco/docuseal-render)
|
||||
|
||||
|
||||
#### Docker
|
||||
@@ -69,6 +74,14 @@ Run the app under a custom domain over https using docker compose (make sure you
|
||||
HOST=your-domain-name.com docker-compose up
|
||||
```
|
||||
|
||||
## For Companies
|
||||
### Integrate seamless document signing into your web or mobile apps with DocuSeal!
|
||||
|
||||
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
|
||||
|
||||
[](https://cal.com/docuseal)
|
||||
|
||||
## License
|
||||
|
||||
DocuSeal is released under the GNU Affero General Public License v3.0.
|
||||
Distributed under the AGPLv3 License. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) for more information.
|
||||
Unless otherwise noted, all files © 2023 Oleksandr Turchyn.
|
||||
|
||||
@@ -7,20 +7,7 @@ module Api
|
||||
def create
|
||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||
|
||||
blob =
|
||||
if (file = params[:file])
|
||||
ActiveStorage::Blob.create_and_upload!(io: file.open,
|
||||
filename: file.original_filename,
|
||||
content_type: file.content_type)
|
||||
else
|
||||
ActiveStorage::Blob.find_signed(params[:blob_signed_id])
|
||||
end
|
||||
|
||||
attachment = ActiveStorage::Attachment.create!(
|
||||
blob:,
|
||||
name: params[:name],
|
||||
record: submitter
|
||||
)
|
||||
attachment = Submitters.create_attachment!(submitter, params)
|
||||
|
||||
render json: attachment.as_json(only: %i[uuid], methods: %i[url filename content_type])
|
||||
end
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
class SubmissionsController < ApiBaseController
|
||||
UnknownFieldName = Class.new(StandardError)
|
||||
UnknownSubmitterName = Class.new(StandardError)
|
||||
|
||||
def create
|
||||
template = current_account.templates.find(params[:template_id])
|
||||
|
||||
submissions =
|
||||
if params[:emails].present?
|
||||
Submissions.create_from_emails(template:,
|
||||
user: current_user,
|
||||
source: :api,
|
||||
send_email: params[:send_email] != 'false',
|
||||
emails: params[:emails] || params[:email])
|
||||
else
|
||||
submissions_attrs = normalize_submissions_params!(submissions_params[:submission], template)
|
||||
|
||||
Submissions.create_from_submitters(template:,
|
||||
user: current_user,
|
||||
source: :api,
|
||||
send_email: params[:send_email] != 'false',
|
||||
submissions_attrs:)
|
||||
end
|
||||
|
||||
submitters = submissions.flat_map(&:submitters)
|
||||
|
||||
send_invitation_emails(submitters) if params[:send_email] != 'false'
|
||||
|
||||
render json: submitters
|
||||
rescue UnknownFieldName, UnknownSubmitterName => e
|
||||
render json: { error: e.message }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def send_invitation_emails(submitters)
|
||||
submitters.each do |submitter|
|
||||
SubmitterMailer.invitation_email(submitter, message: params[:message]).deliver_later!
|
||||
end
|
||||
end
|
||||
|
||||
def submissions_params
|
||||
params.permit(submission: [{ submitters: [[:uuid, :name, :email, { values: {} }]] }])
|
||||
end
|
||||
|
||||
def normalize_submissions_params!(submissions_params, template)
|
||||
submissions_params.each do |submission|
|
||||
submission[:submitters].each_with_index do |submitter, index|
|
||||
next if submitter[:values].blank?
|
||||
|
||||
submitter[:values] =
|
||||
normalize_submitter_values(template,
|
||||
submitter[:values], submitter[:name] || template.submitters[index]['name'])
|
||||
end
|
||||
end
|
||||
|
||||
submissions_params
|
||||
end
|
||||
|
||||
def normalize_submitter_values(template, values, submitter_name)
|
||||
submitter =
|
||||
template.submitters.find { |e| e['name'] == submitter_name } ||
|
||||
raise(UnknownSubmitterName, "Unknown submitter: #{submitter_name}")
|
||||
|
||||
fields = template.fields.select { |e| e['submitter_uuid'] == submitter['uuid'] }
|
||||
|
||||
fields_uuid_index = fields.index_by { |e| e['uuid'] }
|
||||
fields_name_index = fields.index_by { |e| e['name'] }
|
||||
|
||||
values.transform_keys do |key|
|
||||
next key if fields_uuid_index[key].present?
|
||||
|
||||
fields_name_index[key]&.dig('uuid') || raise(UnknownFieldName, "Unknown field: #{key}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2,9 +2,18 @@
|
||||
|
||||
module Api
|
||||
class TemplatesController < ApiBaseController
|
||||
def update
|
||||
@template = current_account.templates.find(params[:id])
|
||||
before_action :load_template, only: %i[show update]
|
||||
|
||||
def index
|
||||
render json: current_account.templates
|
||||
end
|
||||
|
||||
def show
|
||||
render json: @template.as_json(include: { author: { only: %i[id email first_name last_name] },
|
||||
documents: { only: %i[id uuid], methods: %i[url filename] } })
|
||||
end
|
||||
|
||||
def update
|
||||
@template.update!(template_params)
|
||||
|
||||
render :ok
|
||||
@@ -19,5 +28,9 @@ module Api
|
||||
fields: [[:uuid, :submitter_uuid, :name, :type, :required,
|
||||
{ options: [], areas: [%i[x y w h cell_w attachment_uuid page]] }]])
|
||||
end
|
||||
|
||||
def load_template
|
||||
@template = current_account.templates.find(params[:id])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ApiSettingsController < ApplicationController
|
||||
def index; end
|
||||
end
|
||||
@@ -34,8 +34,8 @@ class ApplicationController < ActionController::Base
|
||||
redirect_to setup_index_path unless User.exists?
|
||||
end
|
||||
|
||||
def button_title(title: 'Submit', disabled_with: 'Submitting', icon: nil)
|
||||
render_to_string(partial: 'shared/button_title', locals: { title:, disabled_with:, icon: })
|
||||
def button_title(title: 'Submit', disabled_with: 'Submitting', icon: nil, icon_disabled: nil)
|
||||
render_to_string(partial: 'shared/button_title', locals: { title:, disabled_with:, icon:, icon_disabled: })
|
||||
end
|
||||
|
||||
def svg_icon(icon_name, class: '')
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ConsoleRedirectController < ApplicationController
|
||||
def index
|
||||
auth = JsonWebToken.encode(uuid: current_user.uuid,
|
||||
scope: :console,
|
||||
exp: 1.minute.from_now.to_i)
|
||||
|
||||
redirect_to("#{Docuseal::CONSOLE_URL}?#{{ auth: }.to_query}", allow_other_host: true)
|
||||
end
|
||||
end
|
||||
@@ -7,10 +7,16 @@ class EmailSettingsController < ApplicationController
|
||||
|
||||
def create
|
||||
if @encrypted_config.update(storage_configs)
|
||||
SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email']).deliver_now!
|
||||
|
||||
redirect_to settings_email_index_path, notice: 'Changes have been saved'
|
||||
else
|
||||
render :index, status: :unprocessable_entity
|
||||
end
|
||||
rescue StandardError => e
|
||||
flash[:alert] = e.message
|
||||
|
||||
render :index, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class PersonalizationSettingsController < ApplicationController
|
||||
def show; end
|
||||
|
||||
def create
|
||||
account_config =
|
||||
current_account.account_configs.find_or_initialize_by(key: encrypted_config_params[:key])
|
||||
|
||||
account_config.update!(encrypted_config_params)
|
||||
|
||||
redirect_back(fallback_location: settings_personalization_path, notice: 'Settings have been saved.')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def encrypted_config_params
|
||||
params.require(:account_config).permit!
|
||||
end
|
||||
end
|
||||
@@ -25,7 +25,7 @@ class StartFormController < ApplicationController
|
||||
ua: request.user_agent
|
||||
)
|
||||
|
||||
@submitter.submission ||= Submission.new(template: @template)
|
||||
@submitter.submission ||= Submission.new(template: @template, source: :link)
|
||||
|
||||
if @submitter.save
|
||||
redirect_to submit_form_path(@submitter.slug)
|
||||
@@ -36,7 +36,9 @@ class StartFormController < ApplicationController
|
||||
end
|
||||
|
||||
def completed
|
||||
@submitter = Submitter.where(submission: @template.submissions).find_by!(email: params[:email])
|
||||
@submitter = Submitter.where(submission: @template.submissions)
|
||||
.where.not(completed_at: nil)
|
||||
.find_by!(email: params[:email])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -17,19 +17,29 @@ class SubmissionsController < ApplicationController
|
||||
def create
|
||||
submissions =
|
||||
if params[:emails].present?
|
||||
create_submissions_from_emails
|
||||
Submissions.create_from_emails(template: @template,
|
||||
user: current_user,
|
||||
source: :invite,
|
||||
send_email: params[:send_email] == '1',
|
||||
emails: params[:emails])
|
||||
else
|
||||
create_submissions_from_submitters
|
||||
Submissions.create_from_submitters(template: @template,
|
||||
user: current_user,
|
||||
source: :invite,
|
||||
send_email: params[:send_email] == '1',
|
||||
submissions_attrs: submissions_params[:submission].to_h.values)
|
||||
end
|
||||
|
||||
submitters = submissions.flat_map(&:submitters)
|
||||
|
||||
if params[:send_email] == '1'
|
||||
submissions.flat_map(&:submitters).each do |submitter|
|
||||
submitters.each do |submitter|
|
||||
SubmitterMailer.invitation_email(submitter, message: params[:message]).deliver_later!
|
||||
end
|
||||
end
|
||||
|
||||
redirect_to template_path(@template),
|
||||
notice: "#{submissions.size} #{'recipient'.pluralize(submissions.size)} added"
|
||||
notice: "#{submitters.size} #{'recipient'.pluralize(submitters.size)} added"
|
||||
end
|
||||
|
||||
def destroy
|
||||
@@ -43,30 +53,6 @@ class SubmissionsController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def create_submissions_from_emails
|
||||
emails = params[:emails].to_s.scan(User::EMAIL_REGEXP)
|
||||
|
||||
emails.map do |email|
|
||||
submission = @template.submissions.new(created_by_user: current_user)
|
||||
submission.submitters.new(email:, uuid: @template.submitters.first['uuid'],
|
||||
sent_at: params[:send_email] == '1' ? Time.current : nil)
|
||||
|
||||
submission.tap(&:save!)
|
||||
end
|
||||
end
|
||||
|
||||
def create_submissions_from_submitters
|
||||
submissions_params[:submission].to_h.map do |_, attrs|
|
||||
submission = @template.submissions.new(created_by_user: current_user)
|
||||
|
||||
attrs[:submitters].each do |submitter_attrs|
|
||||
submission.submitters.new(**submitter_attrs, sent_at: params[:send_email] == '1' ? Time.current : nil)
|
||||
end
|
||||
|
||||
submission.tap(&:save!)
|
||||
end
|
||||
end
|
||||
|
||||
def submissions_params
|
||||
params.permit(submission: { submitters: [%i[uuid email]] })
|
||||
end
|
||||
|
||||
@@ -17,19 +17,8 @@ class SubmitFormController < ApplicationController
|
||||
|
||||
def update
|
||||
submitter = Submitter.find_by!(slug: params[:slug])
|
||||
submitter.values.merge!(normalized_values)
|
||||
submitter.completed_at = Time.current if params[:completed] == 'true'
|
||||
submitter.opened_at ||= Time.current
|
||||
|
||||
submitter.save!
|
||||
|
||||
if submitter.completed_at?
|
||||
GenerateSubmitterResultAttachmentsJob.perform_later(submitter)
|
||||
|
||||
submitter.submission.template.account.users.active.each do |user|
|
||||
SubmitterMailer.completed_email(submitter, user).deliver_later!
|
||||
end
|
||||
end
|
||||
Submitters::SubmitValues.call(submitter, params, request)
|
||||
|
||||
head :ok
|
||||
end
|
||||
@@ -37,16 +26,4 @@ class SubmitFormController < ApplicationController
|
||||
def completed
|
||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def normalized_values
|
||||
params.fetch(:values, {}).to_unsafe_h.transform_values do |v|
|
||||
if params[:cast_boolean] == 'true'
|
||||
v == 'true'
|
||||
else
|
||||
v.is_a?(Array) ? v.compact_blank : v
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class WebhookSettingsController < ApplicationController
|
||||
def show
|
||||
@encrypted_config =
|
||||
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
||||
end
|
||||
|
||||
def create
|
||||
@encrypted_config =
|
||||
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
||||
|
||||
@encrypted_config.update!(encrypted_config_params)
|
||||
|
||||
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook URL has been saved.')
|
||||
end
|
||||
|
||||
def update
|
||||
submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last
|
||||
|
||||
SendWebhookRequestJob.perform_later(submitter)
|
||||
|
||||
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook request has been sent.')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def encrypted_config_params
|
||||
params.require(:encrypted_config).permit(:value)
|
||||
end
|
||||
end
|
||||
@@ -70,3 +70,7 @@ button[disabled] .enabled {
|
||||
.base-radio {
|
||||
@apply radio bg-white radio-sm no-animation;
|
||||
}
|
||||
|
||||
.base-select {
|
||||
@apply select base-input w-full font-normal;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.querySelectorAll('a').forEach((link) => {
|
||||
if (document.location.pathname.startsWith(link.pathname)) {
|
||||
if (document.location.pathname.startsWith(link.pathname) && !link.getAttribute('href').startsWith('http')) {
|
||||
link.classList.add('bg-base-300')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -13,7 +13,7 @@ export default actionable(class extends HTMLElement {
|
||||
document.body.classList.remove('overflow-hidden')
|
||||
|
||||
document.removeEventListener('keyup', this.onEscKey)
|
||||
document.removeEventListener('turbo:submit-end', this.handleSubmit)
|
||||
document.removeEventListener('turbo:submit-end', this.onSubmit)
|
||||
document.removeEventListener('turbo:before-cache', this.close)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ window.customElements.define('submission-form', class extends HTMLElement {
|
||||
canSendEmail: this.dataset.canSendEmail === 'true',
|
||||
isDirectUpload: this.dataset.isDirectUpload === 'true',
|
||||
isDemo: this.dataset.isDemo === 'true',
|
||||
withConfetti: true,
|
||||
values: reactive(JSON.parse(this.dataset.values)),
|
||||
attachments: reactive(JSON.parse(this.dataset.attachments)),
|
||||
fields: JSON.parse(this.dataset.fields)
|
||||
|
||||
@@ -21,14 +21,14 @@
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="isActive"
|
||||
v-if="isActive && withLabel"
|
||||
class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap"
|
||||
>
|
||||
{{ field.name || fieldNames[field.type] }}
|
||||
<template v-if="field.type === 'checkbox'">
|
||||
<template v-if="field.type === 'checkbox' && !field.name">
|
||||
{{ fieldIndex + 1 }}
|
||||
</template>
|
||||
<template v-else-if="!field.required">
|
||||
<template v-else-if="!field.required && field.type !== 'checkbox'">
|
||||
(optional)
|
||||
</template>
|
||||
</div>
|
||||
@@ -146,6 +146,11 @@ export default {
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
withLabel: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
fieldIndex: {
|
||||
type: Number,
|
||||
required: false,
|
||||
@@ -207,7 +212,7 @@ export default {
|
||||
},
|
||||
formattedDate () {
|
||||
if (this.field.type === 'date' && this.modelValue) {
|
||||
return new Intl.DateTimeFormat([], { year: 'numeric', month: 'long', day: 'numeric' }).format(new Date(this.modelValue))
|
||||
return new Intl.DateTimeFormat([], { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' }).format(new Date(this.modelValue))
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
v-for="(area, areaIndex) in field.areas"
|
||||
:key="areaIndex"
|
||||
>
|
||||
<Teleport :to="`#page-${area.attachment_uuid}-${area.page}`">
|
||||
<Teleport
|
||||
:to="findPageElementForArea(area)"
|
||||
>
|
||||
<FieldArea
|
||||
:ref="setAreaRef"
|
||||
v-model="values[field.uuid]"
|
||||
@@ -20,6 +22,7 @@
|
||||
:submittable="true"
|
||||
:field-index="fieldIndex"
|
||||
:is-active="currentStep === step"
|
||||
:with-label="withLabel"
|
||||
:is-value-set="step.some((f) => f.uuid in values)"
|
||||
:attachments-index="attachmentsIndex"
|
||||
@click="$emit('focus-step', step)"
|
||||
@@ -54,6 +57,11 @@ export default {
|
||||
required: false,
|
||||
default: () => ({})
|
||||
},
|
||||
withLabel: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
currentStep: {
|
||||
type: Array,
|
||||
required: false,
|
||||
@@ -70,10 +78,16 @@ export default {
|
||||
this.areaRefs = []
|
||||
},
|
||||
methods: {
|
||||
findPageElementForArea (area) {
|
||||
return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${area.attachment_uuid}-${area.page}`)
|
||||
},
|
||||
scrollIntoField (field) {
|
||||
this.areaRefs.find((area) => {
|
||||
if (area.field === field) {
|
||||
if (document.body.style.overflow === 'hidden') {
|
||||
const root = this.$root.$el.parentNode.getRootNode()
|
||||
const container = root.body || root.querySelector('div')
|
||||
|
||||
if (container.style.overflow === 'hidden') {
|
||||
this.scrollInContainer(area.$el)
|
||||
} else {
|
||||
area.$refs.scrollToElem.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
@@ -86,13 +100,19 @@ export default {
|
||||
})
|
||||
},
|
||||
scrollInContainer (target) {
|
||||
const root = this.$root.$el.parentNode.getRootNode()
|
||||
|
||||
const scrollbox = root.getElementById('scrollbox')
|
||||
const formContainer = root.getElementById('form_container')
|
||||
const container = root.body || root.querySelector('div')
|
||||
|
||||
const padding = 64
|
||||
const boxRect = window.scrollbox.children[0].getBoundingClientRect()
|
||||
const boxRect = scrollbox.children[0].getBoundingClientRect()
|
||||
const targetRect = target.getBoundingClientRect()
|
||||
|
||||
const targetTopRelativeToBox = targetRect.top - boxRect.top
|
||||
|
||||
window.scrollbox.scrollTop = targetTopRelativeToBox - document.body.offsetHeight + window.form_container.offsetHeight + target.offsetHeight + padding
|
||||
scrollbox.scrollTop = targetTopRelativeToBox - container.offsetHeight + formContainer.offsetHeight + target.offsetHeight + padding
|
||||
},
|
||||
setAreaRef (el) {
|
||||
if (el) {
|
||||
|
||||
@@ -51,6 +51,16 @@
|
||||
Star on Github
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
v-if="isDemo"
|
||||
href="https://docuseal.co/sign_up"
|
||||
class="white-button flex items-center space-x-1 w-full"
|
||||
>
|
||||
<IconLogin />
|
||||
<span>
|
||||
Create a Free Account
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-center mt-4">
|
||||
Signed with
|
||||
@@ -64,7 +74,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop } from '@tabler/icons-vue'
|
||||
import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop, IconLogin } from '@tabler/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'FormCompleted',
|
||||
@@ -73,8 +83,10 @@ export default {
|
||||
IconInnerShadowTop,
|
||||
IconBrandGithub,
|
||||
IconMail,
|
||||
IconLogin,
|
||||
IconDownload
|
||||
},
|
||||
inject: ['baseUrl'],
|
||||
props: {
|
||||
submitterSlug: {
|
||||
type: String,
|
||||
@@ -85,6 +97,11 @@ export default {
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
withConfetti: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
canSendEmail: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -98,19 +115,21 @@ export default {
|
||||
}
|
||||
},
|
||||
async mounted () {
|
||||
const { default: confetti } = await import('canvas-confetti')
|
||||
if (this.withConfetti) {
|
||||
const { default: confetti } = await import('canvas-confetti')
|
||||
|
||||
confetti({
|
||||
particleCount: 50,
|
||||
startVelocity: 30,
|
||||
spread: 140
|
||||
})
|
||||
confetti({
|
||||
particleCount: 50,
|
||||
startVelocity: 30,
|
||||
spread: 140
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
sendCopyToEmail () {
|
||||
this.isSendingCopy = true
|
||||
|
||||
fetch(`/send_submission_email.json?submitter_slug=${this.submitterSlug}`, {
|
||||
fetch(this.baseUrl + `/send_submission_email.json?submitter_slug=${this.submitterSlug}`, {
|
||||
method: 'POST'
|
||||
}).then(() => {
|
||||
alert('Email has been sent')
|
||||
@@ -121,7 +140,7 @@ export default {
|
||||
download () {
|
||||
this.isDownloading = true
|
||||
|
||||
fetch(`/submitters/${this.submitterSlug}/download`).then((response) => response.json()).then((urls) => {
|
||||
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`).then((response) => response.json()).then((urls) => {
|
||||
const fileRequests = urls.map((url) => {
|
||||
return () => {
|
||||
return fetch(url).then(async (resp) => {
|
||||
|
||||
@@ -55,6 +55,7 @@ export default {
|
||||
IconCloudUpload,
|
||||
IconInnerShadowTop
|
||||
},
|
||||
inject: ['baseUrl'],
|
||||
props: {
|
||||
message: {
|
||||
type: String,
|
||||
@@ -141,7 +142,7 @@ export default {
|
||||
|
||||
return await Promise.all(
|
||||
blobs.map((blob) => {
|
||||
return fetch('/api/attachments', {
|
||||
return fetch(this.baseUrl + '/api/attachments', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: 'attachments',
|
||||
@@ -166,7 +167,7 @@ export default {
|
||||
formData.append('submitter_slug', this.submitterSlug)
|
||||
formData.append('name', 'attachments')
|
||||
|
||||
return fetch('/api/attachments', {
|
||||
return fetch(this.baseUrl + '/api/attachments', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}).then(resp => resp.json()).then((data) => {
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
:steps="stepFields"
|
||||
:values="values"
|
||||
:attachments-index="attachmentsIndex"
|
||||
:with-label="!isAnonymousChecboxes"
|
||||
:current-step="currentStepFields"
|
||||
@focus-step="[saveStep(), goToStep($event, false, true)]"
|
||||
@focus-step="[saveStep(), goToStep($event, false, true), currentField.type !== 'checkbox' ? isFormVisible = true : '']"
|
||||
/>
|
||||
<button
|
||||
v-if="!isFormVisible"
|
||||
class="btn btn-neutral text-white absolute rounded-none border-x-0 md:border md:rounded-full bottom-0 w-full md:mb-4 text-base"
|
||||
class="btn btn-neutral flex text-white absolute rounded-none border-x-0 md:border md:rounded-full bottom-0 w-full md:mb-4 text-base"
|
||||
@click.prevent="isFormVisible = true"
|
||||
>
|
||||
Submit Form
|
||||
@@ -20,7 +21,7 @@
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
v-else
|
||||
v-show="isFormVisible"
|
||||
id="form_container"
|
||||
class="shadow-md bg-base-100 absolute bottom-0 md:bottom-4 w-full border-base-200 border p-4 rounded"
|
||||
>
|
||||
@@ -182,7 +183,7 @@
|
||||
/>
|
||||
<div
|
||||
v-else-if="currentField.type === 'checkbox'"
|
||||
class="flex w-full"
|
||||
class="flex w-full max-h-44 overflow-y-auto"
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
@@ -192,31 +193,45 @@
|
||||
<div
|
||||
class="space-y-3.5 mx-auto"
|
||||
>
|
||||
<div
|
||||
v-for="(field, index) in currentStepFields"
|
||||
:key="field.uuid"
|
||||
>
|
||||
<label
|
||||
:for="field.uuid"
|
||||
class="flex items-center space-x-3"
|
||||
<template v-if="isAnonymousChecboxes">
|
||||
<span class="text-xl">
|
||||
Complete hightlighted checkboxes and click <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? 'submit' : 'next' }}</span>.
|
||||
</span>
|
||||
<input
|
||||
v-for="field in currentStepFields"
|
||||
:key="field.uuid"
|
||||
type="hidden"
|
||||
:name="`values[${field.uuid}]`"
|
||||
:value="!!values[field.uuid]"
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
:name="`values[${field.uuid}]`"
|
||||
:value="!!values[field.uuid]"
|
||||
</template>
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="(field, index) in currentStepFields"
|
||||
:key="field.uuid"
|
||||
>
|
||||
<label
|
||||
:for="field.uuid"
|
||||
class="flex items-center space-x-3"
|
||||
>
|
||||
<input
|
||||
:id="field.uuid"
|
||||
type="checkbox"
|
||||
class="base-checkbox !h-7 !w-7"
|
||||
:checked="!!values[field.uuid]"
|
||||
@click="[$refs.areas.scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
|
||||
>
|
||||
<span class="text-xl">
|
||||
{{ currentField.name || currentField.type + ' ' + (index + 1) }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<input
|
||||
type="hidden"
|
||||
:name="`values[${field.uuid}]`"
|
||||
:value="!!values[field.uuid]"
|
||||
>
|
||||
<input
|
||||
:id="field.uuid"
|
||||
type="checkbox"
|
||||
class="base-checkbox !h-7 !w-7"
|
||||
:checked="!!values[field.uuid]"
|
||||
@click="[$refs.areas.scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
|
||||
>
|
||||
<span class="text-xl">
|
||||
{{ field.name || field.type + ' ' + (index + 1) }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<ImageStep
|
||||
@@ -277,6 +292,7 @@
|
||||
<FormCompleted
|
||||
v-else
|
||||
:is-demo="isDemo"
|
||||
:with-confetti="withConfetti"
|
||||
:can-send-email="canSendEmail"
|
||||
:submitter-slug="submitterSlug"
|
||||
/>
|
||||
@@ -318,6 +334,11 @@ export default {
|
||||
IconArrowsDiagonalMinimize2,
|
||||
FormCompleted
|
||||
},
|
||||
provide () {
|
||||
return {
|
||||
baseUrl: this.baseUrl
|
||||
}
|
||||
},
|
||||
props: {
|
||||
submitterSlug: {
|
||||
type: String,
|
||||
@@ -337,6 +358,16 @@ export default {
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
withConfetti: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
baseUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
fields: {
|
||||
type: Array,
|
||||
required: false,
|
||||
@@ -344,16 +375,17 @@ export default {
|
||||
},
|
||||
authenticityToken: {
|
||||
type: String,
|
||||
required: true
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
isDirectUpload: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
isDemo: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
values: {
|
||||
@@ -374,6 +406,9 @@ export default {
|
||||
currentStepFields () {
|
||||
return this.stepFields[this.currentStep]
|
||||
},
|
||||
isAnonymousChecboxes () {
|
||||
return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name) && this.currentStepFields.length > 4
|
||||
},
|
||||
isButtonDisabled () {
|
||||
return this.isSubmitting ||
|
||||
(this.currentField.required && ['image', 'file'].includes(this.currentField.type) && !this.values[this.currentField.uuid]?.length) ||
|
||||
@@ -413,10 +448,17 @@ export default {
|
||||
)
|
||||
|
||||
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
|
||||
document.body.style.overflow = 'hidden'
|
||||
this.$nextTick(() => {
|
||||
const root = this.$root.$el.parentNode.getRootNode()
|
||||
const scrollbox = root.getElementById('scrollbox')
|
||||
const parent = root.body || root.querySelector('div')
|
||||
|
||||
window.scrollbox.classList.add('h-full', 'overflow-y-auto')
|
||||
window.scrollbox.parentNode.classList.add('h-screen', 'overflow-y-auto')
|
||||
parent.style.overflow = 'hidden'
|
||||
|
||||
scrollbox.classList.add('h-full', 'overflow-y-auto')
|
||||
scrollbox.parentNode.classList.add('h-screen', 'overflow-y-auto')
|
||||
scrollbox.parentNode.style.maxHeight = '-webkit-fill-available'
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -430,7 +472,7 @@ export default {
|
||||
|
||||
this.$refs.form.querySelector('input[type="date"], input[type="text"], select')?.focus()
|
||||
|
||||
if (clickUpload && !this.values[this.currentField.uuid]) {
|
||||
if (clickUpload && !this.values[this.currentField.uuid] && ['file', 'image'].includes(this.currentField.type)) {
|
||||
this.$refs.form.querySelector('input[type="file"]')?.click()
|
||||
}
|
||||
})
|
||||
@@ -439,7 +481,7 @@ export default {
|
||||
if (this.isCompleted) {
|
||||
return Promise.resolve({})
|
||||
} else {
|
||||
return fetch(this.submitPath, {
|
||||
return fetch(this.baseUrl + this.submitPath, {
|
||||
method: 'POST',
|
||||
body: formData || new FormData(this.$refs.form)
|
||||
})
|
||||
|
||||
@@ -99,6 +99,7 @@ export default {
|
||||
IconTextSize,
|
||||
IconArrowsDiagonalMinimize2
|
||||
},
|
||||
inject: ['baseUrl'],
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
@@ -304,7 +305,7 @@ export default {
|
||||
file,
|
||||
'/direct_uploads'
|
||||
).create((_error, data) => {
|
||||
fetch('/api/attachments', {
|
||||
fetch(this.baseUrl + '/api/attachments', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
submitter_slug: this.submitterSlug,
|
||||
@@ -326,12 +327,12 @@ export default {
|
||||
formData.append('submitter_slug', this.submitterSlug)
|
||||
formData.append('name', 'attachments')
|
||||
|
||||
return fetch('/api/attachments', {
|
||||
return fetch(this.baseUrl + '/api/attachments', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}).then((resp) => resp.json()).then((attachment) => {
|
||||
this.$emit('update:model-value', attachment.uuid)
|
||||
this.$emit('attached', attachment)
|
||||
this.$emit('update:model-value', attachment.uuid)
|
||||
|
||||
return resolve(attachment)
|
||||
})
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
@blur="onNameBlur"
|
||||
>{{ field.name || defaultName }}</span>
|
||||
<div
|
||||
v-if="isNameFocus"
|
||||
v-if="isNameFocus && field.type !== 'checkbox'"
|
||||
class="flex items-center ml-1.5"
|
||||
>
|
||||
<input
|
||||
|
||||
@@ -64,8 +64,11 @@
|
||||
:with-arrows="template.schema.length > 1"
|
||||
:item="item"
|
||||
:document="sortedDocuments[index]"
|
||||
:template="template"
|
||||
:is-direct-upload="isDirectUpload"
|
||||
@scroll-to="scrollIntoDocument(item)"
|
||||
@remove="onDocumentRemove"
|
||||
@replace="onDocumentReplace"
|
||||
@up="moveDocument(item, -1)"
|
||||
@down="moveDocument(item, 1)"
|
||||
@change="save"
|
||||
@@ -133,7 +136,7 @@
|
||||
<p>
|
||||
<button
|
||||
class="base-button"
|
||||
@click="drawField = false"
|
||||
@click="drawField = null"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -314,7 +317,7 @@ export default {
|
||||
const field = {
|
||||
name: '',
|
||||
uuid: v4(),
|
||||
required: true,
|
||||
required: type !== 'checkbox',
|
||||
type,
|
||||
submitter_uuid: this.selectedSubmitter.uuid,
|
||||
areas: [area]
|
||||
@@ -333,7 +336,7 @@ export default {
|
||||
type: this.dragFieldType,
|
||||
uuid: v4(),
|
||||
submitter_uuid: this.selectedSubmitter.uuid,
|
||||
required: true
|
||||
required: this.dragFieldType !== 'checkbox'
|
||||
}
|
||||
|
||||
if (['select', 'multiple', 'radio'].includes(this.dragFieldType)) {
|
||||
@@ -353,7 +356,7 @@ export default {
|
||||
|
||||
if (this.selectedField?.type === this.dragFieldType) {
|
||||
baseArea = this.selectedAreaRef.value
|
||||
} else if (previousField?.areas) {
|
||||
} else if (previousField?.areas?.length) {
|
||||
baseArea = previousField.areas[previousField.areas.length - 1]
|
||||
} else {
|
||||
if (['checkbox'].includes(this.dragFieldType)) {
|
||||
@@ -417,6 +420,27 @@ export default {
|
||||
this.template.schema.splice(this.template.schema.indexOf(item), 1)
|
||||
}
|
||||
|
||||
this.template.fields.forEach((field) => {
|
||||
[...(field.areas || [])].forEach((area) => {
|
||||
if (area.attachment_uuid === item.attachment_uuid) {
|
||||
field.areas.splice(field.areas.indexOf(area), 1)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.save()
|
||||
},
|
||||
onDocumentReplace ({ replaceSchemaItem, schema, documents }) {
|
||||
this.template.schema.splice(this.template.schema.indexOf(replaceSchemaItem), 1, schema[0])
|
||||
this.template.documents.push(...documents)
|
||||
this.template.fields.forEach((field) => {
|
||||
(field.areas || []).forEach((area) => {
|
||||
if (area.attachment_uuid === replaceSchemaItem.attachment_uuid) {
|
||||
area.attachment_uuid = schema[0].attachment_uuid
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.save()
|
||||
},
|
||||
moveDocument (item, direction) {
|
||||
|
||||
@@ -93,12 +93,21 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.isDirectUpload) {
|
||||
import('@rails/activestorage')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
upload: Upload.methods.upload,
|
||||
onDropFiles (e) {
|
||||
this.$refs.input.files = e.dataTransfer.files
|
||||
if ([...e.dataTransfer.files].every((f) => f.type.match(/(?:image\/)|(?:application\/pdf)/))) {
|
||||
this.$refs.input.files = e.dataTransfer.files
|
||||
|
||||
this.upload()
|
||||
this.upload()
|
||||
} else {
|
||||
alert('Only PDF and images are supported.')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,19 +31,21 @@
|
||||
v-if="isNameFocus"
|
||||
class="flex items-center relative"
|
||||
>
|
||||
<input
|
||||
:id="`required-checkbox-${field.uuid}`"
|
||||
v-model="field.required"
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-xs no-animation rounded"
|
||||
@mousedown.prevent
|
||||
>
|
||||
<label
|
||||
:for="`required-checkbox-${field.uuid}`"
|
||||
class="label text-xs"
|
||||
@click.prevent="field.required = !field.required"
|
||||
@mousedown.prevent
|
||||
>Required</label>
|
||||
<template v-if="field.type !== 'checkbox'">
|
||||
<input
|
||||
:id="`required-checkbox-${field.uuid}`"
|
||||
v-model="field.required"
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-xs no-animation rounded"
|
||||
@mousedown.prevent
|
||||
>
|
||||
<label
|
||||
:for="`required-checkbox-${field.uuid}`"
|
||||
class="label text-xs"
|
||||
@click.prevent="field.required = !field.required"
|
||||
@mousedown.prevent
|
||||
>Required</label>
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
Draw a text field on the page with a mouse
|
||||
</li>
|
||||
<li>
|
||||
Single click of the page to add a checkbox
|
||||
Single click on the page to add a checkbox
|
||||
</li>
|
||||
<li>
|
||||
Drag & drop any other field type on the page
|
||||
@@ -171,7 +171,8 @@ export default {
|
||||
const field = {
|
||||
name: '',
|
||||
uuid: v4(),
|
||||
required: true,
|
||||
required: type !== 'checkbox',
|
||||
areas: [],
|
||||
submitter_uuid: this.selectedSubmitter.uuid,
|
||||
type
|
||||
}
|
||||
|
||||
@@ -9,39 +9,51 @@
|
||||
loading="lazy"
|
||||
>
|
||||
<div
|
||||
class="group flex justify-end cursor-pointer top-0 bottom-0 left-0 right-0 absolute"
|
||||
class="group flex justify-end cursor-pointer top-0 bottom-0 left-0 right-0 absolute p-1"
|
||||
@click="$emit('scroll-to', item)"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col justify-between opacity-0 group-hover:opacity-100"
|
||||
>
|
||||
<div class="rounded-bl rounded-tr bg-white border">
|
||||
<button
|
||||
class="rounded-bl rounded-tr hover:text-base-100 hover:bg-base-content w-full transition-colors"
|
||||
style="width: 24px"
|
||||
@click.stop="$emit('remove', item)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<div class="flex justify-between w-full">
|
||||
<div style="width: 26px" />
|
||||
<div class="">
|
||||
<ReplaceButton
|
||||
:is-direct-upload="isDirectUpload"
|
||||
:template-id="template.id"
|
||||
class="opacity-0 group-hover:opacity-100"
|
||||
@click.stop
|
||||
@success="$emit('replace', { replaceSchemaItem: item, ...$event })"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="withArrows"
|
||||
class="flex flex-col border rounded-br rounded-tl bg-white divide-y"
|
||||
class="flex flex-col justify-between opacity-0 group-hover:opacity-100"
|
||||
>
|
||||
<button
|
||||
class="rounded-tl hover:text-base-100 hover:bg-base-content w-full transition-colors"
|
||||
style="width: 24px"
|
||||
@click.stop="$emit('up', item)"
|
||||
<div>
|
||||
<button
|
||||
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
|
||||
style="width: 24px; height: 24px"
|
||||
@click.stop="$emit('remove', item)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="withArrows"
|
||||
class="flex flex-col space-y-1"
|
||||
>
|
||||
↑
|
||||
</button>
|
||||
<button
|
||||
class="rounded-br hover:text-base-100 hover:bg-base-content w-full transition-colors"
|
||||
style="width: 24px"
|
||||
@click.stop="$emit('down', item)"
|
||||
>
|
||||
↓
|
||||
</button>
|
||||
<button
|
||||
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
|
||||
style="width: 24px; height: 24px"
|
||||
@click.stop="$emit('up', item)"
|
||||
>
|
||||
↑
|
||||
</button>
|
||||
<button
|
||||
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
|
||||
style="width: 24px; height: 24px"
|
||||
@click.stop="$emit('down', item)"
|
||||
>
|
||||
↓
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -60,34 +72,52 @@
|
||||
|
||||
<script>
|
||||
import Contenteditable from './contenteditable'
|
||||
import Upload from './upload'
|
||||
import ReplaceButton from './replace'
|
||||
|
||||
export default {
|
||||
name: 'DocumentPreview',
|
||||
components: {
|
||||
Contenteditable
|
||||
Contenteditable,
|
||||
ReplaceButton
|
||||
},
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
template: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
document: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isDirectUpload: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false
|
||||
},
|
||||
withArrows: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
emits: ['scroll-to', 'change', 'remove', 'up', 'down'],
|
||||
emits: ['scroll-to', 'change', 'remove', 'up', 'down', 'replace'],
|
||||
computed: {
|
||||
previewImage () {
|
||||
return this.document.preview_images[0]
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.isDirectUpload) {
|
||||
import('@rails/activestorage')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
upload: Upload.methods.upload,
|
||||
onUpdateName (value) {
|
||||
this.item.name = value
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<label
|
||||
:for="inputId"
|
||||
class="btn btn-neutral btn-xs text-white transition-none"
|
||||
:class="{ 'opacity-100': isLoading || isProcessing }"
|
||||
>
|
||||
{{ message }}
|
||||
<form
|
||||
ref="form"
|
||||
class="hidden"
|
||||
>
|
||||
<input
|
||||
:id="inputId"
|
||||
ref="input"
|
||||
name="files[]"
|
||||
type="file"
|
||||
accept="image/*, application/pdf"
|
||||
@change="upload"
|
||||
>
|
||||
</form>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Upload from './upload'
|
||||
|
||||
export default {
|
||||
name: 'ReplaceDocument',
|
||||
props: {
|
||||
templateId: {
|
||||
type: [Number, String],
|
||||
required: true
|
||||
},
|
||||
isDirectUpload: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['success'],
|
||||
data () {
|
||||
return {
|
||||
isLoading: false,
|
||||
isProcessing: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
inputId () {
|
||||
return 'el' + Math.random().toString(32).split('.')[1]
|
||||
},
|
||||
message () {
|
||||
if (this.isLoading) {
|
||||
return 'Uploading...'
|
||||
} else if (this.isProcessing) {
|
||||
return 'Processing...'
|
||||
} else {
|
||||
return 'Replace'
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.isDirectUpload) {
|
||||
import('@rails/activestorage')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
upload: Upload.methods.upload
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SendWebhookRequestJob < ApplicationJob
|
||||
USER_AGENT = 'DocuSeal.co Webhook'
|
||||
|
||||
def perform(submitter)
|
||||
config = submitter.submission.account.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
||||
|
||||
return if config.blank? || config.value.blank?
|
||||
|
||||
Submissions::EnsureResultGenerated.call(submitter)
|
||||
|
||||
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
||||
|
||||
Faraday.post(config.value,
|
||||
{
|
||||
event_type: 'form.submitted',
|
||||
timestamp: Time.current.iso8601,
|
||||
data: Submitters::SerializeForWebhook.call(submitter)
|
||||
}.to_json,
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => USER_AGENT)
|
||||
end
|
||||
end
|
||||
@@ -6,6 +6,10 @@ class ApplicationMailer < ActionMailer::Base
|
||||
|
||||
register_interceptor ActionMailerConfigsInterceptor
|
||||
|
||||
before_action do
|
||||
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
||||
end
|
||||
|
||||
def default_url_options
|
||||
Docuseal.default_url_options
|
||||
end
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SettingsMailer < ApplicationMailer
|
||||
def smtp_successful_setup(email)
|
||||
mail(to: email, subject: 'SMTP has been configured')
|
||||
end
|
||||
end
|
||||
@@ -3,15 +3,27 @@
|
||||
class SubmitterMailer < ApplicationMailer
|
||||
DEFAULT_MESSAGE = %(You have been invited to submit the "%<name>s" form:)
|
||||
|
||||
def invitation_email(submitter, message: format(DEFAULT_MESSAGE, name: submitter.submission.template.name))
|
||||
def invitation_email(submitter, message: '')
|
||||
@current_account = submitter.submission.template.account
|
||||
@submitter = submitter
|
||||
@message = message
|
||||
@message = message.presence || format(DEFAULT_MESSAGE, name: submitter.submission.template.name)
|
||||
|
||||
@email_config = @current_account.account_configs.find_by(key: AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY)
|
||||
|
||||
subject =
|
||||
if @email_config
|
||||
ReplaceEmailVariables.call(@email_config.value['subject'], submitter:)
|
||||
else
|
||||
'You have been invited to submit a form'
|
||||
end
|
||||
|
||||
mail(to: @submitter.email,
|
||||
subject: 'You have been invited to submit a form')
|
||||
subject:,
|
||||
reply_to: submitter.submission.created_by_user&.friendly_name)
|
||||
end
|
||||
|
||||
def completed_email(submitter, user)
|
||||
@current_account = submitter.submission.template.account
|
||||
@submitter = submitter
|
||||
@user = user
|
||||
|
||||
@@ -20,6 +32,7 @@ class SubmitterMailer < ApplicationMailer
|
||||
end
|
||||
|
||||
def documents_copy_email(submitter)
|
||||
@current_account = submitter.submission.template.account
|
||||
@submitter = submitter
|
||||
|
||||
Submissions::EnsureResultGenerated.call(@submitter)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
class UserMailer < ApplicationMailer
|
||||
def invitation_email(user)
|
||||
@current_account = user.account
|
||||
@user = user
|
||||
@token = @user.send(:set_reset_password_token)
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: access_tokens
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# sha256 :text not null
|
||||
# token :text not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# user_id :bigint not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_access_tokens_on_sha256 (sha256) UNIQUE
|
||||
# index_access_tokens_on_user_id (user_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# fk_rails_... (user_id => users.id)
|
||||
#
|
||||
class AccessToken < ApplicationRecord
|
||||
TOKEN_LENGTH = 22
|
||||
|
||||
belongs_to :user
|
||||
|
||||
before_validation :set_sha256, on: :create
|
||||
|
||||
attribute :token, :string, default: -> { SecureRandom.base58(TOKEN_LENGTH) }
|
||||
|
||||
encrypts :token
|
||||
|
||||
private
|
||||
|
||||
def set_sha256
|
||||
self.sha256 = Digest::SHA256.hexdigest(token)
|
||||
end
|
||||
end
|
||||
@@ -14,7 +14,10 @@
|
||||
class Account < ApplicationRecord
|
||||
has_many :users, dependent: :destroy
|
||||
has_many :encrypted_configs, dependent: :destroy
|
||||
has_many :account_configs, dependent: :destroy
|
||||
has_many :templates, dependent: :destroy
|
||||
has_many :submissions, through: :templates
|
||||
has_many :submitters, through: :submissions
|
||||
has_many :active_users, -> { active }, dependent: :destroy,
|
||||
inverse_of: :account, class_name: 'User'
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: account_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_account_configs_on_account_id (account_id)
|
||||
# index_account_configs_on_account_id_and_key (account_id,key) UNIQUE
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# fk_rails_... (account_id => accounts.id)
|
||||
#
|
||||
class AccountConfig < ApplicationRecord
|
||||
SUBMITTER_INVITATION_EMAIL_KEY = 'submitter_invitation_email'
|
||||
|
||||
DEFAULT_VALUES = {
|
||||
SUBMITTER_INVITATION_EMAIL_KEY => {
|
||||
'subject' => 'You have been invited to submit a form',
|
||||
'body' => "Hi there,\n\n" \
|
||||
"You have been invited to submit the \"{{template.name}}\" form:\n\n" \
|
||||
"{{submitter.link}}\n\n" \
|
||||
"Please contact us by replying to this email if you didn't request this.\n\n" \
|
||||
"Thanks,\n" \
|
||||
'{{account.name}}'
|
||||
}
|
||||
}.freeze
|
||||
|
||||
belongs_to :account
|
||||
|
||||
serialize :value, JSON
|
||||
end
|
||||
@@ -25,6 +25,7 @@ class EncryptedConfig < ApplicationRecord
|
||||
EMAIL_SMTP_KEY = 'action_mailer_smtp'
|
||||
ESIGN_CERTS_KEY = 'esign_certs'
|
||||
APP_URL_KEY = 'app_url'
|
||||
WEBHOOK_URL_KEY = 'webhook_url'
|
||||
|
||||
belongs_to :account
|
||||
|
||||
|
||||
@@ -4,12 +4,16 @@
|
||||
#
|
||||
# Table name: submissions
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# deleted_at :datetime
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# created_by_user_id :bigint
|
||||
# template_id :bigint not null
|
||||
# id :bigint not null, primary key
|
||||
# deleted_at :datetime
|
||||
# source :text not null
|
||||
# template_fields :text
|
||||
# template_schema :text
|
||||
# template_submitters :text
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# created_by_user_id :bigint
|
||||
# template_id :bigint not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
@@ -23,9 +27,23 @@
|
||||
#
|
||||
class Submission < ApplicationRecord
|
||||
belongs_to :template
|
||||
has_one :account, through: :template
|
||||
belongs_to :created_by_user, class_name: 'User', optional: true
|
||||
|
||||
has_many :submitters, dependent: :destroy
|
||||
|
||||
serialize :template_fields, JSON
|
||||
serialize :template_schema, JSON
|
||||
serialize :template_submitters, JSON
|
||||
|
||||
attribute :source, :string, default: 'link'
|
||||
|
||||
scope :active, -> { where(deleted_at: nil) }
|
||||
|
||||
enum :source, {
|
||||
invite: 'invite',
|
||||
api: 'api',
|
||||
embed: 'embed',
|
||||
link: 'link'
|
||||
}, scope: false, prefix: true
|
||||
end
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
#
|
||||
class Submitter < ApplicationRecord
|
||||
belongs_to :submission
|
||||
has_one :template, through: :submission
|
||||
has_one :account, through: :template
|
||||
|
||||
attribute :values, :string, default: -> { {} }
|
||||
attribute :slug, :string, default: -> { SecureRandom.base58(14) }
|
||||
|
||||
@@ -44,6 +44,9 @@ class Template < ApplicationRecord
|
||||
|
||||
has_many_attached :documents
|
||||
|
||||
has_many :schema_documents, ->(e) { where(uuid: e.schema.pluck('attachment_uuid')) },
|
||||
class_name: 'ActiveStorage::Attachment', dependent: :destroy, as: :record, inverse_of: :record
|
||||
|
||||
has_many :submissions, dependent: :destroy
|
||||
|
||||
scope :active, -> { where(deleted_at: nil) }
|
||||
|
||||
+13
-2
@@ -22,6 +22,7 @@
|
||||
# role :string not null
|
||||
# sign_in_count :integer default(0), not null
|
||||
# unlock_token :string
|
||||
# uuid :text not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint not null
|
||||
@@ -32,6 +33,7 @@
|
||||
# index_users_on_email (email) UNIQUE
|
||||
# index_users_on_reset_password_token (reset_password_token) UNIQUE
|
||||
# index_users_on_unlock_token (unlock_token) UNIQUE
|
||||
# index_users_on_uuid (uuid) UNIQUE
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
@@ -40,22 +42,31 @@
|
||||
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
|
||||
EMAIL_REGEXP = /[^@,\s]+@[^@,\s]+/
|
||||
|
||||
belongs_to :account
|
||||
has_one :access_token, dependent: :destroy
|
||||
|
||||
devise :database_authenticatable, :recoverable, :rememberable, :validatable, :trackable
|
||||
devise :registerable, :omniauthable, omniauth_providers: [:google_oauth2] if Docuseal.multitenant?
|
||||
|
||||
attribute :role, :string, default: 'admin'
|
||||
attribute :uuid, :string, default: -> { SecureRandom.uuid }
|
||||
|
||||
scope :active, -> { where(deleted_at: nil) }
|
||||
|
||||
def access_token
|
||||
super || build_access_token.tap(&:save!)
|
||||
end
|
||||
|
||||
def active_for_authentication?
|
||||
!deleted_at?
|
||||
end
|
||||
|
||||
def remember_me
|
||||
true
|
||||
end
|
||||
|
||||
def initials
|
||||
[first_name.first, last_name.first].join.upcase
|
||||
end
|
||||
|
||||
@@ -11,13 +11,13 @@
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<%= ff.label :timezone, class: 'label' %>
|
||||
<%= ff.select :timezone, nil, {}, class: 'select base-input w-full font-normal' do %>
|
||||
<%= ff.select :timezone, nil, {}, class: 'base-select' do %>
|
||||
<%= time_zone_options_for_select(current_account.timezone) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= ff.label :locale, 'Time format', class: 'label' %>
|
||||
<%= ff.select :locale, options_for_select(controller.class::LOCALE_OPTIONS.invert, current_account.locale), {}, class: 'select base-input w-full font-normal' %>
|
||||
<%= ff.select :locale, options_for_select(controller.class::LOCALE_OPTIONS.invert, current_account.locale), {}, class: 'base-select' %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
|
||||
<%= render 'shared/settings_nav' %>
|
||||
<div class="flex-grow">
|
||||
<h1 class="text-4xl font-bold mb-4">API</h1>
|
||||
<div class="card bg-base-200">
|
||||
<div class="card-body p-6">
|
||||
<label for="api_key" class="text-sm font-semibold">X-Auth-Token</label>
|
||||
<div class="flex flex-row space-x-4">
|
||||
<input id="api_key" type="text" value="<%= current_user.access_token.token %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
|
||||
<%= render 'shared/clipboard_copy', icon: 'copy', text: current_user.access_token.token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-4 mt-4">
|
||||
<div class="collapse collapse-plus bg-base-200 px-1">
|
||||
<input type="checkbox">
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
<div>
|
||||
Request signature, single submitter
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<div class="badge badge-warning badge-lg">POST</div>
|
||||
<div class="badge badge-primary badge-lg"><%= api_submissions_path %></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse-content" style="display: inherit">
|
||||
<div class="mockup-code overflow-hidden">
|
||||
<% text = capture do %>curl --location '<%= api_submissions_url %>' \
|
||||
--header 'X-Auth-Token: <%= current_user.access_token.token %>' \
|
||||
--data-raw '{
|
||||
"template_id": <%= current_account.templates.last&.id || 1 %>,
|
||||
"emails": "<%= current_user.email.sub('@', '+test@') %>, <%= current_user.email.sub('@', '+test2@') %>"
|
||||
}'<% end.to_str %>
|
||||
<span class="top-0 right-0 absolute">
|
||||
<%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
|
||||
</span>
|
||||
<pre data-prefix="$"><code class="overflow-hidden w-full"><%= text %></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse collapse-plus bg-base-200 px-1">
|
||||
<input type="checkbox">
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
<div>
|
||||
Request signature, multiple submitters with default values
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<div class="badge badge-warning badge-lg">POST</div>
|
||||
<div class="badge badge-primary badge-lg"><%= api_submissions_path %></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse-content" style="display: inherit">
|
||||
<div class="mockup-code overflow-hidden">
|
||||
<% text = capture do %>curl --location '<%= api_submissions_url %>' \
|
||||
--header 'X-Auth-Token: <%= current_user.access_token.token %>' \
|
||||
--data-raw '{
|
||||
"template_id": <%= current_account.templates.last&.id || 1 %>,
|
||||
"submission": [
|
||||
{
|
||||
"submitters": [
|
||||
{
|
||||
"name": "<%= current_account.templates.last ? current_account.templates.last.submitters.first['name'] : 'First Submitter' %>",
|
||||
"email": "<%= current_user.email.sub('@', '+test@') %>",
|
||||
"values": {
|
||||
"Form Text Field Name": "Default Value"
|
||||
}
|
||||
},
|
||||
{ "name": "Second Submitter", "email": "<%= current_user.email.sub('@', '+test2@') %>" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}'<% end.to_str %>
|
||||
<span class="top-0 right-0 absolute">
|
||||
<%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
|
||||
</span>
|
||||
<pre data-prefix="$"><code class="overflow-hidden w-full"><%= text %></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse collapse-plus bg-base-200 px-1">
|
||||
<input type="checkbox">
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
<div>
|
||||
Template details
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<div class="badge badge-info badge-lg">GET</div>
|
||||
<div class="badge badge-primary badge-lg"><%= api_template_path(':id') %></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse-content" style="display: inherit">
|
||||
<div class="mockup-code overflow-hidden">
|
||||
<% text = capture do %>curl '<%= api_template_url(current_account.templates&.last || 1) %>' \
|
||||
--header 'X-Auth-Token: <%= current_user.access_token.token %>'<% end.to_str %>
|
||||
<span class="top-0 right-0 absolute">
|
||||
<%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
|
||||
</span>
|
||||
<pre data-prefix="$"><code class="overflow-hidden w-full"><%= text %></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -10,14 +10,6 @@
|
||||
<%= f.label :password, class: 'label' %>
|
||||
<%= f.password_field :password, autocomplete: 'current-password', class: 'base-input' %>
|
||||
</div>
|
||||
<% if devise_mapping.rememberable? %>
|
||||
<div class="form-control">
|
||||
<%= f.label :remember_me, class: 'flex items-center cursor-pointer' do %>
|
||||
<%= f.check_box :remember_me, checked: true, class: 'base-checkbox' %>
|
||||
<span class="label">Remember me</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= f.button button_title(title: 'Log In', disabled_with: 'Logging In'), class: 'base-button' %>
|
||||
|
||||
@@ -25,6 +25,27 @@
|
||||
<%= ff.password_field :password, value: value['password'], required: true, class: 'base-input' %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<%= ff.label :domain, 'Domain (optional)', class: 'label' %>
|
||||
<%= ff.text_field :domain, value: value['domain'], class: 'base-input' %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= ff.label :authentication, class: 'label' %>
|
||||
<%= ff.select :authentication, options_for_select([%w[Plain plain], %w[Login login], %w[CRAM-MD5 cram_md5]], value.fetch('authentication', 'plain')), { prompt: true }, required: true, class: 'base-select' %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= ff.label :security_label, 'SMTP Security', class: 'label' %>
|
||||
<div class="flex items-center space-x-6">
|
||||
<% [%w[None none], %w[SSL ssl], %w[TLS tls]].each do |(label, val)| %>
|
||||
<%= ff.label :security, value: val, for: "#{val}_radio", class: 'label' do %>
|
||||
<%= ff.radio_button :security, val, checked: (value['security'].blank? && val == 'none') || value['security'] == val, id: "#{val}_radio", class: 'base-radio mr-2' %>
|
||||
<%= label %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= ff.label :from_email, 'Send from Email', class: 'label' %>
|
||||
<%= ff.email_field :from_email, value: value['from_email'], required: true, class: 'base-input' %>
|
||||
|
||||
@@ -8,11 +8,6 @@
|
||||
</head>
|
||||
<body>
|
||||
<%= yield %>
|
||||
<p>
|
||||
---
|
||||
</p>
|
||||
<p>
|
||||
Sent using <a href="<%= Docuseal::PRODUCT_URL %>"><%= Docuseal::PRODUCT_NAME %></a> documents software.
|
||||
</p>
|
||||
<%= render partial: 'shared/mailer_attribution' %>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
DocuSeal
|
||||
</h1>
|
||||
</div>
|
||||
<h2 class="mb-16 text-lg text-center text-gray-500">
|
||||
<h2 class="mb-16 text-lg text-center text-gray-600">
|
||||
A self-hosted and open-source web platform that provides secure and efficient digital document signing and processing.
|
||||
</h2>
|
||||
</div>
|
||||
@@ -22,7 +22,7 @@
|
||||
</div>
|
||||
<h3 class="mb-4 text-2xl font-semibold">Easy to Start</h3>
|
||||
<p class="text-base text-gray-500">
|
||||
Run on your own host using <a href="https://hub.docker.com/r/docuseal/docuseal" class="link link-neutral font-bold" target="_blank">Docker</a> container, or deploy on your favorite managed PaaS with a single <a href="https:///www.docuseal.co/install" class="link link-neutral font-bold">click</a>.
|
||||
Run on your own host using <a href="https://hub.docker.com/r/docuseal/docuseal" class="link link-neutral font-bold" target="_blank">Docker</a> container, or deploy on your favorite managed PaaS with a single <a href="https://www.docuseal.co/install" class="link link-neutral font-bold">click</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -70,3 +70,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<%= render 'shared/attribution' %>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<%= render 'logo_placeholder' %>
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="alert my-4">
|
||||
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
|
||||
<div>
|
||||
<p class="font-bold">Unlock with DocuSeal Enterprise</p>
|
||||
<p>
|
||||
Display your company name and logo when signing documents.
|
||||
<br>
|
||||
<a class="link font-medium" target="_blank" href="<%= "#{Docuseal::PRODUCT_URL}/pricing" %>">Learn More</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,27 @@
|
||||
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
|
||||
<%= render 'shared/settings_nav' %>
|
||||
<div class="flex-grow max-w-xl mx-auto">
|
||||
<p class="text-4xl font-bold mb-4">Signature Request Email</p>
|
||||
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
|
||||
<%= f.hidden_field :key %>
|
||||
<%= f.fields_for :value, Struct.new(:subject, :body).new(*f.object.value.values_at('subject', 'body')) do |ff| %>
|
||||
<div class="form-control">
|
||||
<%= ff.label :subject, class: 'label' %>
|
||||
<%= ff.text_field :subject, required: true, class: 'base-input' %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= ff.label :body, class: 'label' %>
|
||||
<autoresize-textarea>
|
||||
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2' %>
|
||||
</autoresize-textarea>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="form-control pt-2">
|
||||
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<p class="text-4xl font-bold mb-4 mt-8">Company Logo</p>
|
||||
<%= render 'logo_form' %>
|
||||
</div>
|
||||
<div class="w-0 md:w-52"></div>
|
||||
</div>
|
||||
@@ -2,12 +2,7 @@
|
||||
<div class="space-y-6 mx-auto">
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-center">
|
||||
<a href="/" class="flex items-center">
|
||||
<div class="mr-3">
|
||||
<%= render 'shared/logo', width: '50px', height: '50px' %>
|
||||
</div>
|
||||
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
|
||||
</a>
|
||||
<%= render 'start_form/docuseal_logo' %>
|
||||
</div>
|
||||
<div class="text-center text-4xl font-bold">
|
||||
Email has been sent
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<p>SMTP has been successfully configured!</p>
|
||||
@@ -8,7 +8,7 @@
|
||||
</span>
|
||||
<span class="disabled">
|
||||
<span class="flex items-center justify-center space-x-2">
|
||||
<%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
|
||||
<%= local_assigns[:icon_disabled] || svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
|
||||
<span><%= disabled_with %>...</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
<label class="<%= local_assigns[:class] %>">
|
||||
<input type="radio" class="peer hidden">
|
||||
<span class="peer-checked:hidden flex items-center space-x-2">
|
||||
<%= svg_icon('link', class: local_assigns[:icon_class] || 'w-6 h-6 text-white') %>
|
||||
<%= svg_icon(local_assigns[:icon] || 'link', class: local_assigns[:icon_class] || 'w-6 h-6 text-white') %>
|
||||
<span class="hidden md:inline">
|
||||
<%= local_assigns[:copy_title] || 'Copy' %>
|
||||
</span>
|
||||
</span>
|
||||
<span class="hidden peer-checked:flex items-center space-x-2">
|
||||
<%= svg_icon('clipboard_copy', class: local_assigns[:icon_class] || 'w-6 h-6 text-white') %>
|
||||
<%= svg_icon(local_assigns[:copied_icon] || 'clipboard_copy', class: local_assigns[:icon_class] || 'w-6 h-6 text-white') %>
|
||||
<span class="hidden md:inline">
|
||||
<%= local_assigns[:copied_title] || 'Copied' %>
|
||||
</span>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<p>
|
||||
---
|
||||
</p>
|
||||
<p>
|
||||
Sent using <a href="<%= Docuseal::PRODUCT_URL %>"><%= Docuseal::PRODUCT_NAME %></a> free document signing.
|
||||
</p>
|
||||
@@ -0,0 +1 @@
|
||||
<%= render 'shared/email_attribution' %>
|
||||
@@ -23,6 +23,25 @@
|
||||
<li>
|
||||
<%= link_to 'Team', settings_users_path, class: 'text-base hover:bg-base-300' %>
|
||||
</li>
|
||||
<% if Docuseal.demo? || !Docuseal.multitenant? %>
|
||||
<li>
|
||||
<%= link_to 'API', settings_api_index_path, class: 'text-base hover:bg-base-300' %>
|
||||
</li>
|
||||
<li>
|
||||
<%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %>
|
||||
</li>
|
||||
<% end %>
|
||||
<li>
|
||||
<%= link_to 'Personalization', settings_personalization_path, class: 'text-base hover:bg-base-300' %>
|
||||
</li>
|
||||
<% unless Docuseal.demo? %>
|
||||
<li>
|
||||
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
||||
Console
|
||||
<span class="badge badge-warning">New</span>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</menu-active>
|
||||
<div class="mx-4 border-t border-base-300 hidden md:block">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<turbo-frame id="modal">
|
||||
<turbo-modal class="modal modal-open items-start !animate-none">
|
||||
<turbo-modal class="modal modal-open items-start !animate-none overflow-y-auto">
|
||||
<div class="absolute top-0 bottom-0 right-0 left-0" data-action="click:turbo-modal#close"></div>
|
||||
<div class="modal-box pt-4 pb-6 px-6 mt-20">
|
||||
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none">
|
||||
<% if local_assigns[:title] %>
|
||||
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||
<span>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<%= render 'docuseal_logo' %>
|
||||
@@ -0,0 +1,6 @@
|
||||
<a href="/" class="flex justify-center items-center">
|
||||
<span class="mr-3">
|
||||
<%= render 'shared/logo', width: '50px', height: '50px' %>
|
||||
</span>
|
||||
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
|
||||
</a>
|
||||
@@ -2,14 +2,9 @@
|
||||
<div class="space-y-6 mx-auto">
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-center">
|
||||
<a href="/" class="flex items-center">
|
||||
<div class="mr-3">
|
||||
<%= render 'shared/logo', width: '50px', height: '50px' %>
|
||||
</div>
|
||||
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
|
||||
</a>
|
||||
<%= render 'banner' %>
|
||||
</div>
|
||||
<div class="flex items-center bg-base-300 rounded-xl p-4 mb-4">
|
||||
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
|
||||
<div class="flex items-center">
|
||||
<div class="mr-3">
|
||||
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
|
||||
|
||||
@@ -2,22 +2,17 @@
|
||||
<div class="space-y-6 mx-auto">
|
||||
<div class="space-y-6">
|
||||
<div class="text-center w-full space-y-6">
|
||||
<a href="/" class="flex justify-center">
|
||||
<span class="mr-3">
|
||||
<%= render 'shared/logo', width: '50px', height: '50px' %>
|
||||
</span>
|
||||
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
|
||||
</a>
|
||||
<%= render 'banner' %>
|
||||
<p class="text-xl font-semibold text-center">You have been invited to submit a form</p>
|
||||
</div>
|
||||
<div class="flex items-center bg-base-300 rounded-xl p-4 mb-4">
|
||||
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
|
||||
<div class="flex items-center">
|
||||
<div class="mr-3">
|
||||
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-lg font-bold mb-1"><%= @template.name %></p>
|
||||
<p class="text-sm">Invited by <%= @template.account.name %></p>
|
||||
<p class="text-sm">Invited by <span class="font-semibold"><%= @template.account.name %></span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<div class="form-control">
|
||||
<% is_smtp_configured = Accounts.can_send_emails?(current_account) %>
|
||||
<%= f.label :send_email, class: 'flex items-center cursor-pointer' do %>
|
||||
<%= f.check_box :send_email, class: 'base-checkbox', disabled: !is_smtp_configured, onchange: "message_field.classList.toggle('hidden', !event.currentTarget.checked)" %>
|
||||
<%= f.check_box :send_email, class: 'base-checkbox', disabled: !is_smtp_configured, onchange: "window.message_field && message_field.classList.toggle('hidden', !event.currentTarget.checked)" %>
|
||||
<span class="label">Send Email</span>
|
||||
<% end %>
|
||||
<% unless is_smtp_configured %>
|
||||
@@ -57,21 +57,23 @@
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div id="message_field" class="card card-compact bg-base-200 hidden">
|
||||
<div class="card-body">
|
||||
<div class="form-control space-y-2">
|
||||
<span class="label-text">Hi there,</span>
|
||||
<autoresize-textarea>
|
||||
<%= f.text_area :message, value: format(SubmitterMailer::DEFAULT_MESSAGE, name: @template.name), required: true, class: 'base-textarea !rounded-lg w-full' %>
|
||||
</autoresize-textarea>
|
||||
<span class="label-text">
|
||||
Thanks,
|
||||
<br>
|
||||
<%= current_account.name %>
|
||||
</span>
|
||||
<% unless AccountConfig.exists?(key: AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) %>
|
||||
<div id="message_field" class="card card-compact bg-base-200 hidden">
|
||||
<div class="card-body">
|
||||
<div class="form-control space-y-2">
|
||||
<span class="label-text">Hi there,</span>
|
||||
<autoresize-textarea>
|
||||
<%= f.text_area :message, value: format(SubmitterMailer::DEFAULT_MESSAGE, name: @template.name), required: true, class: 'base-textarea !rounded-lg w-full' %>
|
||||
</autoresize-textarea>
|
||||
<span class="label-text">
|
||||
Thanks,
|
||||
<br>
|
||||
<%= current_account.name %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="form-control">
|
||||
<%= f.button button_title(title: 'Add Recipients'), class: 'base-button' %>
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</div>
|
||||
<div class="flex md:max-h-[calc(100vh-60px)]">
|
||||
<div class="overflow-y-auto overflow-x-hidden hidden lg:block w-52 flex-none pr-3 mt-0.5 pt-0.5">
|
||||
<% @submission.template.schema.each do |item| %>
|
||||
<% (@submission.template_schema || @submission.template.schema).each do |item| %>
|
||||
<% document = @submission.template.documents.find { |a| item['attachment_uuid'] == a.uuid } %>
|
||||
<a href="#<%= "page-#{document.uuid}-0" %>" onclick="[event.preventDefault(), window[event.target.closest('a').href.split('#')[1]].scrollIntoView({ behavior: 'smooth', block: 'start' })]" class="block cursor-pointer">
|
||||
<img src="<%= document.preview_images.first.url %>" width="<%= document.preview_images.first.metadata['width'] %>" height="<%= document.preview_images.first.metadata['height'] %>" class="rounded border" loading="lazy">
|
||||
@@ -35,10 +35,10 @@
|
||||
</div>
|
||||
<div class="w-full overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5">
|
||||
<div class="pr-3.5 pl-0.5">
|
||||
<% fields_index = Templates.build_field_areas_index(@submission.template) %>
|
||||
<% fields_index = Templates.build_field_areas_index(@submission.template_fields || @submission.template.fields) %>
|
||||
<% values = @submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %>
|
||||
<% attachments_index = ActiveStorage::Attachment.where(record: @submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %>
|
||||
<% @submission.template.schema.each do |item| %>
|
||||
<% (@submission.template_schema || @submission.template.schema).each do |item| %>
|
||||
<% document = @submission.template.documents.find { |e| e.uuid == item['attachment_uuid'] } %>
|
||||
<% document.preview_images.sort_by { |a| a.filename.base.to_i }.each_with_index do |page, index| %>
|
||||
<div id="<%= "page-#{document.uuid}-#{index}" %>" class="relative">
|
||||
@@ -57,15 +57,15 @@
|
||||
</div>
|
||||
<div class="hidden md:block relative w-80 flex-none pt-0.5 pr-4 pl-0.5 overflow-auto space">
|
||||
<% colors = %w[bg-red-500 bg-sky-500 bg-emerald-500 bg-yellow-300 bg-purple-600] %>
|
||||
<% submitter_fields_index = @submission.template.fields.group_by { |f| f['submitter_uuid'] } %>
|
||||
<% @submission.template.submitters.each_with_index do |item, index| %>
|
||||
<% submitter_fields_index = (@submission.template_fields || @submission.template.fields).group_by { |f| f['submitter_uuid'] } %>
|
||||
<% (@submission.template_submitters || @submission.template.submitters).each_with_index do |item, index| %>
|
||||
<% submitter = @submission.submitters.find { |e| e.uuid == item['uuid'] } %>
|
||||
<div class="sticky -top-1 bg-base-100 pt-1 -mt-1 z-10">
|
||||
<div class="border border-base-300 rounded-md px-2 py-1 mb-1">
|
||||
<div class="flex items-center space-x-1">
|
||||
<span class="mx-1 w-3 h-3 rounded-full <%= colors[index] %>"></span>
|
||||
<span class="text-lg">
|
||||
<%= @submission.template.submitters.find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %>
|
||||
<%= (@submission.template_submitters || @submission.template.submitters).find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-1 mt-1">
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<%= render 'docuseal_logo' %>
|
||||
@@ -0,0 +1,4 @@
|
||||
<a href="<%= root_path %>" class="mx-auto text-2xl md:text-3xl font-bold items-center flex space-x-3">
|
||||
<%= render 'shared/logo', class: 'w-9 h-9 md:w-12 md:h-12' %>
|
||||
<span><%= Docuseal::PRODUCT_NAME %></span>
|
||||
</a>
|
||||
@@ -2,14 +2,9 @@
|
||||
<div class="space-y-6 mx-auto">
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-center">
|
||||
<a href="/" class="flex items-center">
|
||||
<div class="mr-3">
|
||||
<%= render 'shared/logo', width: '50px', height: '50px' %>
|
||||
</div>
|
||||
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
|
||||
</a>
|
||||
<%= render 'start_form/banner' %>
|
||||
</div>
|
||||
<div class="flex items-center bg-base-300 rounded-xl p-4 mb-4">
|
||||
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
|
||||
<div class="flex items-center">
|
||||
<div class="mr-3">
|
||||
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
|
||||
@@ -23,7 +18,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<% if Accounts.can_send_emails?(@submitter.submission.template.account) %>
|
||||
<%= button_to button_title(title: 'Send copy to Email', disabled_with: 'Sending', icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'base-button w-full' %>
|
||||
<%= button_to button_title(title: 'Send copy to Email', disabled_with: 'Sending', icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
|
||||
<div class="divider">OR</div>
|
||||
<% end %>
|
||||
<download-button data-src="<%= submitter_download_index_path(@submitter.slug) %>" class="base-button w-full">
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
<% fields_index = Templates.build_field_areas_index(@submitter.submission.template) %>
|
||||
<% fields_index = Templates.build_field_areas_index(@submitter.submission.template_fields || @submitter.submission.template.fields) %>
|
||||
<% values = @submitter.submission.submitters.where.not(id: @submitter.id).reduce({}) { |acc, sub| acc.merge(sub.values) } %>
|
||||
<% attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %>
|
||||
<div style="max-height: -webkit-fill-available;">
|
||||
<div id="scrollbox">
|
||||
<div class="mx-auto block pb-72" style="max-width: 1000px">
|
||||
<div class="mt-4 flex">
|
||||
<a href="<%= root_path %>" class="mx-auto text-2xl md:text-3xl font-bold items-center flex space-x-3">
|
||||
<%= render 'shared/logo', class: 'w-9 h-9 md:w-12 md:h-12' %>
|
||||
<span>DocuSeal</span>
|
||||
</a>
|
||||
<%= render 'banner' %>
|
||||
</div>
|
||||
<% @submitter.submission.template.schema.each do |item| %>
|
||||
<% (@submitter.submission.template_schema || @submitter.submission.template.schema).each do |item| %>
|
||||
<% document = @submitter.submission.template.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 my-4 shadow-md">
|
||||
@@ -32,7 +29,7 @@
|
||||
<div class="fixed bottom-0 w-full h-0 z-20">
|
||||
<div class="mx-auto" style="max-width: 1000px">
|
||||
<div class="relative md:mx-32">
|
||||
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter-uuid="<%= @submitter.uuid %>" data-submitter-slug="<%= @submitter.slug %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" data-attachments="<%= attachments_index.values.select { |e| e.record_id == @submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>" data-fields="<%= @submitter.submission.template.fields.select { |f| f['submitter_uuid'] == @submitter.uuid }.to_json %>" data-values="<%= @submitter.values.to_json %>" data-authenticity-token="<%= form_authenticity_token %>"></submission-form>
|
||||
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter-uuid="<%= @submitter.uuid %>" data-submitter-slug="<%= @submitter.slug %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" data-attachments="<%= attachments_index.values.select { |e| e.record_id == @submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>" data-fields="<%= (@submitter.submission.template_fields || @submitter.submission.template.fields).select { |f| f['submitter_uuid'] == @submitter.uuid }.to_json %>" data-values="<%= @submitter.values.to_json %>" data-authenticity-token="<%= form_authenticity_token %>"></submission-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
</ul>
|
||||
<% end %>
|
||||
<p>
|
||||
Thanks,<br><%= @submitter.submission.template.account.name %>
|
||||
Thanks,<br><%= @current_account.name %>
|
||||
</p>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<p>Hi there,</p>
|
||||
<%= simple_format(@message) %>
|
||||
<p><%= link_to 'Submit Form', submit_form_url(slug: @submitter.slug) %></p>
|
||||
<p>Please contact us by replying to this email if you didn't request this.</p>
|
||||
<p>
|
||||
Thanks,<br><%= @submitter.submission.template.account.name %>
|
||||
</p>
|
||||
<% if @email_config %>
|
||||
<%= auto_link(simple_format(h(ReplaceEmailVariables.call(@email_config.value['body'], submitter: @submitter)))) %>
|
||||
<% else %>
|
||||
<p>Hi there,</p>
|
||||
<%= simple_format(@message) %>
|
||||
<p><%= link_to 'Submit Form', submit_form_url(slug: @submitter.slug) %></p>
|
||||
<p>Please contact us by replying to this email if you didn't request this.</p>
|
||||
<p>
|
||||
Thanks,<br><%= @current_account.name %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
@@ -1 +1 @@
|
||||
<template-builder data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-template="<%= @template.to_json(include: { documents: { include: { preview_images: { methods: %i[url metadata filename] } } } }) %>"></template-builder>
|
||||
<template-builder data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-template="<%= @template.as_json.merge(documents: @template.schema_documents.as_json(include: { preview_images: { methods: %i[url metadata filename] } })).to_json %>"></template-builder>
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<div class="space-y-4">
|
||||
<% @submissions.each do |submission| %>
|
||||
<a href="<%= submission_path(submission) %>" class="bg-base-200 w-full flex flex-col md:flex-row space-y-4 md:space-y-0 md:justify-between rounded-2xl px-6 py-5 md:items-center">
|
||||
<% submitters = submission.template.submitters.filter_map { |item| submission.submitters.find { |e| e.uuid == item['uuid'] } } %>
|
||||
<% submitters = (submission.template_submitters || submission.template.submitters).filter_map { |item| submission.submitters.find { |e| e.uuid == item['uuid'] } } %>
|
||||
<% is_submission_complated = submitters.all?(&:completed_at?) %>
|
||||
<% if submitters.size == 1 %>
|
||||
<div>
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
<p><%= link_to 'Sign up', invitation_url(reset_password_token: @token) %></p>
|
||||
<p>Please contact us by replying to this email if you didn't request this.</p>
|
||||
<p>
|
||||
Thanks,<br><%= @user.account.name %>
|
||||
Thanks,<br><%= @current_account.name %>
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
|
||||
<%= render 'shared/settings_nav' %>
|
||||
<div class="flex-grow">
|
||||
<h1 class="text-4xl font-bold mb-4">Webhooks</h1>
|
||||
<div class="card bg-base-200">
|
||||
<div class="card-body p-6">
|
||||
<%= form_for @encrypted_config, url: settings_webhooks_path, method: :post, html: { autocomplete: 'off' } do |f| %>
|
||||
<%= f.label :value, 'Webhook URL', class: 'text-sm font-semibold' %>
|
||||
<div class="flex flex-row space-x-4 mt-2">
|
||||
<%= f.text_field :value, required: true, class: 'input font-mono input-bordered w-full', placeholder: 'https://example.com/hook' %>
|
||||
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button w-32' %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last %>
|
||||
<% if submitter %>
|
||||
<div class="space-y-4 mt-4">
|
||||
<div class="collapse collapse-open bg-base-200 px-1">
|
||||
<div class="p-4 text-xl font-medium">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>
|
||||
Submission example payload
|
||||
</span>
|
||||
<% if @encrypted_config.value.present? %>
|
||||
<%= button_to button_title(title: 'Test Webhook', disabled_with: 'Sending', icon_disabled: svg_icon('loader', class: 'w-4 h-4 animate-spin')), settings_webhooks_path, class: 'btn btn-neutral btn-outline btn-sm', method: :put %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse-content" style="display: inherit">
|
||||
<div class="mockup-code overflow-hidden relative">
|
||||
<span class="top-0 right-0 absolute">
|
||||
<%= render 'shared/clipboard_copy', icon: 'copy', text: code = JSON.pretty_generate({ event_type: 'form.submitted', timestamp: Time.current.iso8601, data: Submitters::SerializeForWebhook.call(submitter) }).gsub(/^/, ' ').sub(/^\s+/, ''), class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
|
||||
</span>
|
||||
<pre><code class="overflow-hidden w-full"><%= code %></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -9,6 +9,8 @@ require 'action_view/railtie'
|
||||
require 'action_mailer/railtie'
|
||||
require 'active_job/railtie'
|
||||
|
||||
require_relative '../lib/api_path_consider_json_middleware'
|
||||
|
||||
Bundler.require(*Rails.groups)
|
||||
|
||||
module DocuSeal
|
||||
@@ -26,5 +28,8 @@ module DocuSeal
|
||||
config.action_view.frozen_string_literal = true
|
||||
|
||||
config.middleware.insert_before ActionDispatch::Static, Rack::Deflater
|
||||
config.middleware.insert_before ActionDispatch::Static, ApiPathConsiderJsonMiddleware
|
||||
|
||||
ActiveSupport.run_load_hooks(:application_config, self)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -76,7 +76,7 @@ Rails.application.configure do
|
||||
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
|
||||
config.action_mailer.raise_delivery_errors = false
|
||||
|
||||
if ENV['SMTP_USERNAME']
|
||||
if ENV['SMTP_ADDRESS']
|
||||
config.action_mailer.delivery_method = :smtp
|
||||
config.action_mailer.smtp_settings = {
|
||||
address: ENV.fetch('SMTP_ADDRESS', nil),
|
||||
@@ -86,7 +86,7 @@ Rails.application.configure do
|
||||
password: ENV.fetch('SMTP_PASSWORD', nil),
|
||||
authentication: ENV.fetch('SMTP_AUTHENTICATION', 'plain'),
|
||||
enable_starttls_auto: ENV['SMTP_ENABLE_STARTTLS_AUTO'] == 'true'
|
||||
}
|
||||
}.compact
|
||||
end
|
||||
|
||||
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
|
||||
|
||||
@@ -13,6 +13,20 @@ Rails.configuration.to_prepare do
|
||||
response.set_header('Cache-Control', 'public, max-age=31536000') if action_name == 'show'
|
||||
end
|
||||
|
||||
ActiveStorage::Blobs::ProxyController.before_action do
|
||||
response.set_header('Access-Control-Allow-Origin', '*')
|
||||
response.set_header('Access-Control-Allow-Methods', 'GET')
|
||||
response.set_header('Access-Control-Allow-Headers', '*')
|
||||
response.set_header('Access-Control-Max-Age', '1728000')
|
||||
end
|
||||
|
||||
ActiveStorage::Blobs::RedirectController.before_action do
|
||||
response.set_header('Access-Control-Allow-Origin', '*')
|
||||
response.set_header('Access-Control-Allow-Methods', 'GET')
|
||||
response.set_header('Access-Control-Allow-Headers', '*')
|
||||
response.set_header('Access-Control-Max-Age', '1728000')
|
||||
end
|
||||
|
||||
ActiveStorage::DirectUploadsController.before_action do
|
||||
next if current_user
|
||||
next if Submitter.find_signed(cookies[:submitter_sid])
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../../lib/auth_with_token_strategy'
|
||||
|
||||
Warden::Strategies.add(:auth_token, AuthWithTokenStrategy)
|
||||
|
||||
# Assuming you have not yet modified this file, each configuration option below
|
||||
# is set to its default value. Note that some are commented out while others
|
||||
# are not: uncommented lines are intended to protect your configuration from
|
||||
@@ -159,7 +163,7 @@ Devise.setup do |config|
|
||||
|
||||
# ==> Configuration for :rememberable
|
||||
# The time the user will be remembered without asking for credentials again.
|
||||
# config.remember_for = 2.weeks
|
||||
config.remember_for = 2.years
|
||||
|
||||
# Invalidates all the remember me tokens when the user signs out.
|
||||
config.expire_all_remember_me_on_sign_out = true
|
||||
@@ -272,10 +276,10 @@ Devise.setup do |config|
|
||||
# If you want to use other strategies, that are not supported by Devise, or
|
||||
# change the failure app, you can configure them inside the config.warden block.
|
||||
#
|
||||
# config.warden do |manager|
|
||||
# manager.intercept_401 = false
|
||||
# manager.default_strategies(scope: :user).unshift :some_external_strategy
|
||||
# end
|
||||
config.warden do |manager|
|
||||
# manager.intercept_401 = false
|
||||
manager.default_strategies(scope: :user).unshift(:auth_token)
|
||||
end
|
||||
|
||||
# ==> Mountable engine configurations
|
||||
# When using Devise inside an engine, let's call it `MyEngine`, and this engine
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
ActiveRecord::Tasks::DatabaseTasks.migrate if ENV['RAILS_ENV'] == 'production'
|
||||
Rails.configuration.to_prepare do
|
||||
ActiveRecord::Tasks::DatabaseTasks.migrate if ENV['RAILS_ENV'] == 'production'
|
||||
end
|
||||
|
||||
+12
-2
@@ -27,9 +27,11 @@ Rails.application.routes.draw do
|
||||
end
|
||||
end
|
||||
|
||||
namespace :api do
|
||||
namespace :api, defaults: { format: :json } do
|
||||
resources :attachments, only: %i[create]
|
||||
resources :templates, only: %i[update] do
|
||||
resources :submissions, only: %i[create]
|
||||
resources :templates, only: %i[update show index] do
|
||||
resources :submissions, only: %i[create]
|
||||
resources :documents, only: %i[create destroy], controller: 'templates_documents'
|
||||
end
|
||||
end
|
||||
@@ -39,6 +41,7 @@ Rails.application.routes.draw do
|
||||
resource :newsletter, only: %i[show update]
|
||||
resources :users, only: %i[new create edit update destroy]
|
||||
resources :submissions, only: %i[show destroy]
|
||||
resources :console_redirect, only: %i[index]
|
||||
resources :templates, only: %i[new create edit show destroy] do
|
||||
resources :submissions, only: %i[new create]
|
||||
end
|
||||
@@ -67,6 +70,11 @@ Rails.application.routes.draw do
|
||||
end
|
||||
resources :esign, only: %i[index create], controller: 'esign_settings'
|
||||
resources :users, only: %i[index]
|
||||
resource :personalization, only: %i[show create], controller: 'personalization_settings'
|
||||
if !Docuseal.multitenant? || Docuseal.demo?
|
||||
resources :api, only: %i[index], controller: 'api_settings'
|
||||
resource :webhooks, only: %i[show create update], controller: 'webhook_settings'
|
||||
end
|
||||
resource :account, only: %i[show update]
|
||||
resources :profile, only: %i[index] do
|
||||
collection do
|
||||
@@ -76,4 +84,6 @@ Rails.application.routes.draw do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
ActiveSupport.run_load_hooks(:routes, self)
|
||||
end
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddTemplateFieldsToSubmission < ActiveRecord::Migration[7.0]
|
||||
class MigrationTemplate < ApplicationRecord
|
||||
self.table_name = 'templates'
|
||||
end
|
||||
|
||||
class MigrationSubmission < ApplicationRecord
|
||||
self.table_name = 'submissions'
|
||||
end
|
||||
|
||||
def up
|
||||
add_column :submissions, :template_fields, :text
|
||||
add_column :submissions, :template_schema, :text
|
||||
add_column :submissions, :template_submitters, :text
|
||||
|
||||
MigrationTemplate.all.each do |template|
|
||||
MigrationSubmission.where(template_id: template.id).each do |submission|
|
||||
submission.update_columns(template_fields: template.fields,
|
||||
template_schema: template.schema,
|
||||
template_submitters: template.submitters)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column :submissions, :template_fields
|
||||
remove_column :submissions, :template_schema
|
||||
remove_column :submissions, :template_submitters
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddUuidToUsers < ActiveRecord::Migration[7.0]
|
||||
class MigrationUser < ApplicationRecord
|
||||
self.table_name = 'users'
|
||||
end
|
||||
|
||||
def up
|
||||
add_column :users, :uuid, :text
|
||||
add_index :users, :uuid, unique: true
|
||||
|
||||
MigrationUser.all.each do |user|
|
||||
user.update_columns(uuid: SecureRandom.uuid)
|
||||
end
|
||||
|
||||
change_column_null :users, :uuid, false
|
||||
end
|
||||
|
||||
def down
|
||||
drop_column :users, :uuid
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddSourceToSubmissions < ActiveRecord::Migration[7.0]
|
||||
class MigrationSubmission < ApplicationRecord
|
||||
self.table_name = 'submissions'
|
||||
end
|
||||
|
||||
def up
|
||||
add_column :submissions, :source, :text
|
||||
|
||||
MigrationSubmission.where(source: nil).update_all(source: :invite)
|
||||
|
||||
change_column_null :submissions, :source, false
|
||||
end
|
||||
|
||||
def down
|
||||
drop_column :submissions, :source
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CreateAccessTokens < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
create_table :access_tokens do |t|
|
||||
t.references :user, null: false, foreign_key: true, index: true
|
||||
t.text :token, null: false
|
||||
t.text :sha256, null: false, index: { unique: true }
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CreateAccountConfigs < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
create_table :account_configs do |t|
|
||||
t.references :account, null: false, foreign_key: true, index: true
|
||||
t.string :key, null: false
|
||||
t.text :value, null: false
|
||||
|
||||
t.index %i[account_id key], unique: true
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
||||
+29
-1
@@ -10,10 +10,30 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.0].define(version: 2023_07_01_075115) do
|
||||
ActiveRecord::Schema[7.0].define(version: 2023_08_15_190540) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
||||
create_table "access_tokens", force: :cascade do |t|
|
||||
t.bigint "user_id", null: false
|
||||
t.text "token", null: false
|
||||
t.text "sha256", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["sha256"], name: "index_access_tokens_on_sha256", unique: true
|
||||
t.index ["user_id"], name: "index_access_tokens_on_user_id"
|
||||
end
|
||||
|
||||
create_table "account_configs", force: :cascade do |t|
|
||||
t.bigint "account_id", null: false
|
||||
t.string "key", null: false
|
||||
t.text "value", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["account_id", "key"], name: "index_account_configs_on_account_id_and_key", unique: true
|
||||
t.index ["account_id"], name: "index_account_configs_on_account_id"
|
||||
end
|
||||
|
||||
create_table "accounts", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.string "timezone", null: false
|
||||
@@ -77,6 +97,10 @@ ActiveRecord::Schema[7.0].define(version: 2023_07_01_075115) do
|
||||
t.datetime "deleted_at"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.text "template_fields"
|
||||
t.text "template_schema"
|
||||
t.text "template_submitters"
|
||||
t.text "source", null: false
|
||||
t.index ["created_by_user_id"], name: "index_submissions_on_created_by_user_id"
|
||||
t.index ["template_id"], name: "index_submissions_on_template_id"
|
||||
end
|
||||
@@ -136,12 +160,16 @@ ActiveRecord::Schema[7.0].define(version: 2023_07_01_075115) do
|
||||
t.datetime "deleted_at"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.text "uuid", null: false
|
||||
t.index ["account_id"], name: "index_users_on_account_id"
|
||||
t.index ["email"], name: "index_users_on_email", unique: true
|
||||
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
||||
t.index ["unlock_token"], name: "index_users_on_unlock_token", unique: true
|
||||
t.index ["uuid"], name: "index_users_on_uuid", unique: true
|
||||
end
|
||||
|
||||
add_foreign_key "access_tokens", "users"
|
||||
add_foreign_key "account_configs", "accounts"
|
||||
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
||||
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
|
||||
add_foreign_key "document_generation_events", "submitters"
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module AccountConfigs
|
||||
module_function
|
||||
|
||||
def find_or_initialize_for_key(account, key)
|
||||
account.account_configs.find_by(key:) ||
|
||||
account.account_configs.new(key:, value: AccountConfig::DEFAULT_VALUES[key])
|
||||
end
|
||||
end
|
||||
@@ -62,6 +62,7 @@ module Accounts
|
||||
|
||||
def can_send_emails?(account)
|
||||
return true if Docuseal.multitenant?
|
||||
return true if ENV['SMTP_ADDRESS'].present?
|
||||
|
||||
EncryptedConfig.exists?(account_id: account.id, key: EncryptedConfig::EMAIL_SMTP_KEY)
|
||||
end
|
||||
|
||||
@@ -19,17 +19,29 @@ module ActionMailerConfigsInterceptor
|
||||
email_configs = EncryptedConfig.find_by(key: EncryptedConfig::EMAIL_SMTP_KEY)
|
||||
|
||||
if email_configs
|
||||
message.delivery_method(:smtp, user_name: email_configs.value['username'],
|
||||
password: email_configs.value['password'],
|
||||
address: email_configs.value['host'],
|
||||
port: email_configs.value['port'],
|
||||
tls: email_configs.value['port'].to_s == '465')
|
||||
message.delivery_method(:smtp, build_smtp_configs_hash(email_configs))
|
||||
|
||||
message.from = email_configs.value['from_email']
|
||||
message.from = "#{email_configs.account.name} <#{email_configs.value['from_email']}>"
|
||||
else
|
||||
message.delivery_method(:test)
|
||||
end
|
||||
|
||||
message
|
||||
end
|
||||
|
||||
def build_smtp_configs_hash(email_configs)
|
||||
value = email_configs.value
|
||||
|
||||
{
|
||||
user_name: value['username'],
|
||||
password: value['password'],
|
||||
address: value['host'],
|
||||
port: value['port'],
|
||||
domain: value['domain'],
|
||||
authentication: value.fetch('authentication', 'plain'),
|
||||
enable_starttls_auto: true,
|
||||
ssl: value['security'] == 'ssl',
|
||||
tls: value['security'] == 'tls' || (value['security'].blank? && value['port'].to_s == '465')
|
||||
}.compact_blank
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ApiPathConsiderJsonMiddleware
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
if env['PATH_INFO'].starts_with?('/api') &&
|
||||
!env['PATH_INFO'].ends_with?('/documents') &&
|
||||
!env['PATH_INFO'].ends_with?('/attachments')
|
||||
env['CONTENT_TYPE'] = 'application/json'
|
||||
end
|
||||
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AuthWithTokenStrategy < Devise::Strategies::Base
|
||||
def valid?
|
||||
request.headers['X-Auth-Token'].present?
|
||||
end
|
||||
|
||||
def authenticate!
|
||||
sha256 = Digest::SHA256.hexdigest(request.headers['X-Auth-Token'])
|
||||
|
||||
user = User.joins(:access_token).find_by(access_token: { sha256: })
|
||||
|
||||
if user
|
||||
success!(user)
|
||||
else
|
||||
fail!('Invalid token')
|
||||
end
|
||||
rescue JWT::VerificationError
|
||||
fail!('Invalid token')
|
||||
end
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user