mirror of
https://github.com/docusealco/docuseal
synced 2026-11-05 06:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
No files matched your search
@@ -27,3 +27,4 @@
|
|||||||
/attachments
|
/attachments
|
||||||
/docuseal
|
/docuseal
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.env
|
||||||
@@ -127,6 +127,7 @@ jobs:
|
|||||||
bundle config path vendor/bundle
|
bundle config path vendor/bundle
|
||||||
bundle install --jobs 4 --retry 4
|
bundle install --jobs 4 --retry 4
|
||||||
yarn install
|
yarn install
|
||||||
|
sudo apt-get update
|
||||||
sudo apt-get install libvips
|
sudo apt-get install libvips
|
||||||
- name: Run
|
- name: Run
|
||||||
env:
|
env:
|
||||||
|
|||||||
+4
-1
@@ -27,7 +27,7 @@ Lint/MissingSuper:
|
|||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Metrics/MethodLength:
|
Metrics/MethodLength:
|
||||||
Max: 20
|
Max: 25
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'db/migrate/**'
|
- 'db/migrate/**'
|
||||||
|
|
||||||
@@ -61,5 +61,8 @@ RSpec/ExampleLength:
|
|||||||
Rails/I18nLocaleTexts:
|
Rails/I18nLocaleTexts:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/SkipsModelValidations:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
Rails/ApplicationController:
|
Rails/ApplicationController:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
@@ -13,6 +13,7 @@ gem 'faraday'
|
|||||||
gem 'google-cloud-storage', require: false
|
gem 'google-cloud-storage', require: false
|
||||||
gem 'hexapdf'
|
gem 'hexapdf'
|
||||||
gem 'image_processing'
|
gem 'image_processing'
|
||||||
|
gem 'jwt'
|
||||||
gem 'lograge'
|
gem 'lograge'
|
||||||
gem 'mysql2', require: false
|
gem 'mysql2', require: false
|
||||||
gem 'oj'
|
gem 'oj'
|
||||||
|
|||||||
@@ -540,6 +540,7 @@ DEPENDENCIES
|
|||||||
google-cloud-storage
|
google-cloud-storage
|
||||||
hexapdf
|
hexapdf
|
||||||
image_processing
|
image_processing
|
||||||
|
jwt
|
||||||
letter_opener_web
|
letter_opener_web
|
||||||
lograge
|
lograge
|
||||||
mysql2
|
mysql2
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
</p>
|
</p>
|
||||||
<h2 align="center">
|
<h2 align="center">
|
||||||
<a href="https://demo.docuseal.co">✨ Live Demo</a>
|
<a href="https://demo.docuseal.co">✨ Live Demo</a>
|
||||||
|
<span>|</span>
|
||||||
|
<a href="https://docuseal.co/sign_up">☁️ Try in Cloud</a>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
[](https://demo.docuseal.co)
|
[](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] PDF signature verification
|
||||||
- [x] Users management
|
- [x] Users management
|
||||||
- [x] Mobile-optimized
|
- [x] Mobile-optimized
|
||||||
|
- [x] API and Webhooks for integrations
|
||||||
- [x] Easy to deploy in minutes
|
- [x] Easy to deploy in minutes
|
||||||
|
|
||||||
## Deploy
|
## Deploy
|
||||||
|
|
||||||
| Heroku | Railway | DigitalOcean |
|
|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) | [<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 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
|
#### 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
|
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
|
## 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.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api
|
||||||
|
class SubmissionsController < ApiBaseController
|
||||||
|
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.create_from_submitters(template:,
|
||||||
|
user: current_user,
|
||||||
|
source: :api,
|
||||||
|
send_email: params[:send_email] != 'false',
|
||||||
|
submissions_attrs: submissions_params[:submission])
|
||||||
|
end
|
||||||
|
|
||||||
|
submitters = submissions.flat_map(&:submitters)
|
||||||
|
|
||||||
|
if params[:send_email] != 'false'
|
||||||
|
submitters.each do |submitter|
|
||||||
|
SubmitterMailer.invitation_email(submitter, message: params[:message]).deliver_later!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: submitters
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def submissions_params
|
||||||
|
params.permit(submission: [{ submitters: [%i[uuid name email]] }])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
module Api
|
module Api
|
||||||
class TemplatesController < ApiBaseController
|
class TemplatesController < ApiBaseController
|
||||||
|
def index
|
||||||
|
render json: current_account.templates
|
||||||
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@template = current_account.templates.find(params[:id])
|
@template = current_account.templates.find(params[:id])
|
||||||
|
|
||||||
|
|||||||
@@ -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?
|
redirect_to setup_index_path unless User.exists?
|
||||||
end
|
end
|
||||||
|
|
||||||
def button_title(title: 'Submit', disabled_with: 'Submitting', icon: nil)
|
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: })
|
render_to_string(partial: 'shared/button_title', locals: { title:, disabled_with:, icon:, icon_disabled: })
|
||||||
end
|
end
|
||||||
|
|
||||||
def svg_icon(icon_name, class: '')
|
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
|
||||||
@@ -25,7 +25,7 @@ class StartFormController < ApplicationController
|
|||||||
ua: request.user_agent
|
ua: request.user_agent
|
||||||
)
|
)
|
||||||
|
|
||||||
@submitter.submission ||= Submission.new(template: @template)
|
@submitter.submission ||= Submission.new(template: @template, source: :link)
|
||||||
|
|
||||||
if @submitter.save
|
if @submitter.save
|
||||||
redirect_to submit_form_path(@submitter.slug)
|
redirect_to submit_form_path(@submitter.slug)
|
||||||
|
|||||||
@@ -17,19 +17,29 @@ class SubmissionsController < ApplicationController
|
|||||||
def create
|
def create
|
||||||
submissions =
|
submissions =
|
||||||
if params[:emails].present?
|
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
|
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
|
end
|
||||||
|
|
||||||
|
submitters = submissions.flat_map(&:submitters)
|
||||||
|
|
||||||
if params[:send_email] == '1'
|
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!
|
SubmitterMailer.invitation_email(submitter, message: params[:message]).deliver_later!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
redirect_to template_path(@template),
|
redirect_to template_path(@template),
|
||||||
notice: "#{submissions.size} #{'recipient'.pluralize(submissions.size)} added"
|
notice: "#{submitters.size} #{'recipient'.pluralize(submitters.size)} added"
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@@ -43,30 +53,6 @@ class SubmissionsController < ApplicationController
|
|||||||
|
|
||||||
private
|
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
|
def submissions_params
|
||||||
params.permit(submission: { submitters: [%i[uuid email]] })
|
params.permit(submission: { submitters: [%i[uuid email]] })
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -17,15 +17,20 @@ class SubmitFormController < ApplicationController
|
|||||||
|
|
||||||
def update
|
def update
|
||||||
submitter = Submitter.find_by!(slug: params[:slug])
|
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!
|
update_submitter!(submitter)
|
||||||
|
|
||||||
|
Submissions.update_template_fields!(submitter.submission) if submitter.submission.template_fields.blank?
|
||||||
|
|
||||||
|
submitter.submission.save!
|
||||||
|
|
||||||
if submitter.completed_at?
|
if submitter.completed_at?
|
||||||
GenerateSubmitterResultAttachmentsJob.perform_later(submitter)
|
GenerateSubmitterResultAttachmentsJob.perform_later(submitter)
|
||||||
|
|
||||||
|
if submitter.account.encrypted_configs.exists?(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
||||||
|
SendWebhookRequestJob.perform_later(submitter)
|
||||||
|
end
|
||||||
|
|
||||||
submitter.submission.template.account.users.active.each do |user|
|
submitter.submission.template.account.users.active.each do |user|
|
||||||
SubmitterMailer.completed_email(submitter, user).deliver_later!
|
SubmitterMailer.completed_email(submitter, user).deliver_later!
|
||||||
end
|
end
|
||||||
@@ -40,6 +45,16 @@ class SubmitFormController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def update_submitter!(submitter)
|
||||||
|
submitter.values.merge!(normalized_values)
|
||||||
|
submitter.completed_at = Time.current if params[:completed] == 'true'
|
||||||
|
submitter.opened_at ||= Time.current
|
||||||
|
|
||||||
|
submitter.save!
|
||||||
|
|
||||||
|
submitter
|
||||||
|
end
|
||||||
|
|
||||||
def normalized_values
|
def normalized_values
|
||||||
params.fetch(:values, {}).to_unsafe_h.transform_values do |v|
|
params.fetch(:values, {}).to_unsafe_h.transform_values do |v|
|
||||||
if params[:cast_boolean] == 'true'
|
if params[:cast_boolean] == 'true'
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
export default class extends HTMLElement {
|
export default class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.querySelectorAll('a').forEach((link) => {
|
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')
|
link.classList.add('bg-base-300')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default actionable(class extends HTMLElement {
|
|||||||
document.body.classList.remove('overflow-hidden')
|
document.body.classList.remove('overflow-hidden')
|
||||||
|
|
||||||
document.removeEventListener('keyup', this.onEscKey)
|
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)
|
document.removeEventListener('turbo:before-cache', this.close)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,14 +21,14 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<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"
|
class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap"
|
||||||
>
|
>
|
||||||
{{ field.name || fieldNames[field.type] }}
|
{{ field.name || fieldNames[field.type] }}
|
||||||
<template v-if="field.type === 'checkbox'">
|
<template v-if="field.type === 'checkbox' && !field.name">
|
||||||
{{ fieldIndex + 1 }}
|
{{ fieldIndex + 1 }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="!field.required">
|
<template v-else-if="!field.required && field.type !== 'checkbox'">
|
||||||
(optional)
|
(optional)
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -146,6 +146,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
withLabel: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
fieldIndex: {
|
fieldIndex: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -207,7 +212,7 @@ export default {
|
|||||||
},
|
},
|
||||||
formattedDate () {
|
formattedDate () {
|
||||||
if (this.field.type === 'date' && this.modelValue) {
|
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 {
|
} else {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,9 @@
|
|||||||
v-for="(area, areaIndex) in field.areas"
|
v-for="(area, areaIndex) in field.areas"
|
||||||
:key="areaIndex"
|
:key="areaIndex"
|
||||||
>
|
>
|
||||||
<Teleport :to="`#page-${area.attachment_uuid}-${area.page}`">
|
<Teleport
|
||||||
|
:to="`#page-${area.attachment_uuid}-${area.page}`"
|
||||||
|
>
|
||||||
<FieldArea
|
<FieldArea
|
||||||
:ref="setAreaRef"
|
:ref="setAreaRef"
|
||||||
v-model="values[field.uuid]"
|
v-model="values[field.uuid]"
|
||||||
@@ -20,6 +22,7 @@
|
|||||||
:submittable="true"
|
:submittable="true"
|
||||||
:field-index="fieldIndex"
|
:field-index="fieldIndex"
|
||||||
:is-active="currentStep === step"
|
:is-active="currentStep === step"
|
||||||
|
:with-label="withLabel"
|
||||||
:is-value-set="step.some((f) => f.uuid in values)"
|
:is-value-set="step.some((f) => f.uuid in values)"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
@click="$emit('focus-step', step)"
|
@click="$emit('focus-step', step)"
|
||||||
@@ -54,6 +57,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
},
|
},
|
||||||
|
withLabel: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
currentStep: {
|
currentStep: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -51,6 +51,16 @@
|
|||||||
Star on Github
|
Star on Github
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</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>
|
||||||
<div class="text-center mt-4">
|
<div class="text-center mt-4">
|
||||||
Signed with
|
Signed with
|
||||||
@@ -64,7 +74,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop } from '@tabler/icons-vue'
|
import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop, IconLogin } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FormCompleted',
|
name: 'FormCompleted',
|
||||||
@@ -73,6 +83,7 @@ export default {
|
|||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
IconBrandGithub,
|
IconBrandGithub,
|
||||||
IconMail,
|
IconMail,
|
||||||
|
IconLogin,
|
||||||
IconDownload
|
IconDownload
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
:steps="stepFields"
|
:steps="stepFields"
|
||||||
:values="values"
|
:values="values"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
|
:with-label="!isAnonymousChecboxes"
|
||||||
:current-step="currentStepFields"
|
:current-step="currentStepFields"
|
||||||
@focus-step="[saveStep(), goToStep($event, false, true)]"
|
@focus-step="[saveStep(), goToStep($event, false, true), currentField.type !== 'checkbox' ? isFormVisible = true : '']"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="!isFormVisible"
|
v-if="!isFormVisible"
|
||||||
@@ -20,7 +21,7 @@
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-show="isFormVisible"
|
||||||
id="form_container"
|
id="form_container"
|
||||||
class="shadow-md bg-base-100 absolute bottom-0 md:bottom-4 w-full border-base-200 border p-4 rounded"
|
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
|
<div
|
||||||
v-else-if="currentField.type === 'checkbox'"
|
v-else-if="currentField.type === 'checkbox'"
|
||||||
class="flex w-full"
|
class="flex w-full max-h-44 overflow-y-auto"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
@@ -192,31 +193,36 @@
|
|||||||
<div
|
<div
|
||||||
class="space-y-3.5 mx-auto"
|
class="space-y-3.5 mx-auto"
|
||||||
>
|
>
|
||||||
<div
|
<template v-if="isAnonymousChecboxes">
|
||||||
v-for="(field, index) in currentStepFields"
|
Complete hightlighted checkboxes and click <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? 'submit' : 'next' }}</span>.
|
||||||
:key="field.uuid"
|
</template>
|
||||||
>
|
<template v-else>
|
||||||
<label
|
<div
|
||||||
:for="field.uuid"
|
v-for="(field, index) in currentStepFields"
|
||||||
class="flex items-center space-x-3"
|
:key="field.uuid"
|
||||||
>
|
>
|
||||||
<input
|
<label
|
||||||
type="hidden"
|
:for="field.uuid"
|
||||||
:name="`values[${field.uuid}]`"
|
class="flex items-center space-x-3"
|
||||||
:value="!!values[field.uuid]"
|
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
:id="field.uuid"
|
type="hidden"
|
||||||
type="checkbox"
|
:name="`values[${field.uuid}]`"
|
||||||
class="base-checkbox !h-7 !w-7"
|
:value="!!values[field.uuid]"
|
||||||
:checked="!!values[field.uuid]"
|
>
|
||||||
@click="[$refs.areas.scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
|
<input
|
||||||
>
|
:id="field.uuid"
|
||||||
<span class="text-xl">
|
type="checkbox"
|
||||||
{{ currentField.name || currentField.type + ' ' + (index + 1) }}
|
class="base-checkbox !h-7 !w-7"
|
||||||
</span>
|
:checked="!!values[field.uuid]"
|
||||||
</label>
|
@click="[$refs.areas.scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
|
||||||
</div>
|
>
|
||||||
|
<span class="text-xl">
|
||||||
|
{{ field.name || field.type + ' ' + (index + 1) }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ImageStep
|
<ImageStep
|
||||||
@@ -374,6 +380,9 @@ export default {
|
|||||||
currentStepFields () {
|
currentStepFields () {
|
||||||
return this.stepFields[this.currentStep]
|
return this.stepFields[this.currentStep]
|
||||||
},
|
},
|
||||||
|
isAnonymousChecboxes () {
|
||||||
|
return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name) && this.currentStepFields.length > 4
|
||||||
|
},
|
||||||
isButtonDisabled () {
|
isButtonDisabled () {
|
||||||
return this.isSubmitting ||
|
return this.isSubmitting ||
|
||||||
(this.currentField.required && ['image', 'file'].includes(this.currentField.type) && !this.values[this.currentField.uuid]?.length) ||
|
(this.currentField.required && ['image', 'file'].includes(this.currentField.type) && !this.values[this.currentField.uuid]?.length) ||
|
||||||
@@ -430,7 +439,7 @@ export default {
|
|||||||
|
|
||||||
this.$refs.form.querySelector('input[type="date"], input[type="text"], select')?.focus()
|
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()
|
this.$refs.form.querySelector('input[type="file"]')?.click()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
@blur="onNameBlur"
|
@blur="onNameBlur"
|
||||||
>{{ field.name || defaultName }}</span>
|
>{{ field.name || defaultName }}</span>
|
||||||
<div
|
<div
|
||||||
v-if="isNameFocus"
|
v-if="isNameFocus && field.type !== 'checkbox'"
|
||||||
class="flex items-center ml-1.5"
|
class="flex items-center ml-1.5"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -64,8 +64,11 @@
|
|||||||
:with-arrows="template.schema.length > 1"
|
:with-arrows="template.schema.length > 1"
|
||||||
:item="item"
|
:item="item"
|
||||||
:document="sortedDocuments[index]"
|
:document="sortedDocuments[index]"
|
||||||
|
:template="template"
|
||||||
|
:is-direct-upload="isDirectUpload"
|
||||||
@scroll-to="scrollIntoDocument(item)"
|
@scroll-to="scrollIntoDocument(item)"
|
||||||
@remove="onDocumentRemove"
|
@remove="onDocumentRemove"
|
||||||
|
@replace="onDocumentReplace"
|
||||||
@up="moveDocument(item, -1)"
|
@up="moveDocument(item, -1)"
|
||||||
@down="moveDocument(item, 1)"
|
@down="moveDocument(item, 1)"
|
||||||
@change="save"
|
@change="save"
|
||||||
@@ -133,7 +136,7 @@
|
|||||||
<p>
|
<p>
|
||||||
<button
|
<button
|
||||||
class="base-button"
|
class="base-button"
|
||||||
@click="drawField = false"
|
@click="drawField = null"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
@@ -314,7 +317,7 @@ export default {
|
|||||||
const field = {
|
const field = {
|
||||||
name: '',
|
name: '',
|
||||||
uuid: v4(),
|
uuid: v4(),
|
||||||
required: true,
|
required: type !== 'checkbox',
|
||||||
type,
|
type,
|
||||||
submitter_uuid: this.selectedSubmitter.uuid,
|
submitter_uuid: this.selectedSubmitter.uuid,
|
||||||
areas: [area]
|
areas: [area]
|
||||||
@@ -333,7 +336,7 @@ export default {
|
|||||||
type: this.dragFieldType,
|
type: this.dragFieldType,
|
||||||
uuid: v4(),
|
uuid: v4(),
|
||||||
submitter_uuid: this.selectedSubmitter.uuid,
|
submitter_uuid: this.selectedSubmitter.uuid,
|
||||||
required: true
|
required: this.dragFieldType !== 'checkbox'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['select', 'multiple', 'radio'].includes(this.dragFieldType)) {
|
if (['select', 'multiple', 'radio'].includes(this.dragFieldType)) {
|
||||||
@@ -353,7 +356,7 @@ export default {
|
|||||||
|
|
||||||
if (this.selectedField?.type === this.dragFieldType) {
|
if (this.selectedField?.type === this.dragFieldType) {
|
||||||
baseArea = this.selectedAreaRef.value
|
baseArea = this.selectedAreaRef.value
|
||||||
} else if (previousField?.areas) {
|
} else if (previousField?.areas?.length) {
|
||||||
baseArea = previousField.areas[previousField.areas.length - 1]
|
baseArea = previousField.areas[previousField.areas.length - 1]
|
||||||
} else {
|
} else {
|
||||||
if (['checkbox'].includes(this.dragFieldType)) {
|
if (['checkbox'].includes(this.dragFieldType)) {
|
||||||
@@ -417,6 +420,27 @@ export default {
|
|||||||
this.template.schema.splice(this.template.schema.indexOf(item), 1)
|
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()
|
this.save()
|
||||||
},
|
},
|
||||||
moveDocument (item, direction) {
|
moveDocument (item, direction) {
|
||||||
|
|||||||
@@ -93,12 +93,21 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mounted () {
|
||||||
|
if (this.isDirectUpload) {
|
||||||
|
import('@rails/activestorage')
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
upload: Upload.methods.upload,
|
upload: Upload.methods.upload,
|
||||||
onDropFiles (e) {
|
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"
|
v-if="isNameFocus"
|
||||||
class="flex items-center relative"
|
class="flex items-center relative"
|
||||||
>
|
>
|
||||||
<input
|
<template v-if="field.type !== 'checkbox'">
|
||||||
:id="`required-checkbox-${field.uuid}`"
|
<input
|
||||||
v-model="field.required"
|
:id="`required-checkbox-${field.uuid}`"
|
||||||
type="checkbox"
|
v-model="field.required"
|
||||||
class="checkbox checkbox-xs no-animation rounded"
|
type="checkbox"
|
||||||
@mousedown.prevent
|
class="checkbox checkbox-xs no-animation rounded"
|
||||||
>
|
@mousedown.prevent
|
||||||
<label
|
>
|
||||||
:for="`required-checkbox-${field.uuid}`"
|
<label
|
||||||
class="label text-xs"
|
:for="`required-checkbox-${field.uuid}`"
|
||||||
@click.prevent="field.required = !field.required"
|
class="label text-xs"
|
||||||
@mousedown.prevent
|
@click.prevent="field.required = !field.required"
|
||||||
>Required</label>
|
@mousedown.prevent
|
||||||
|
>Required</label>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
Draw a text field on the page with a mouse
|
Draw a text field on the page with a mouse
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
Single click of the page to add a checkbox
|
Single click on the page to add a checkbox
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
Drag & drop any other field type on the page
|
Drag & drop any other field type on the page
|
||||||
@@ -171,7 +171,8 @@ export default {
|
|||||||
const field = {
|
const field = {
|
||||||
name: '',
|
name: '',
|
||||||
uuid: v4(),
|
uuid: v4(),
|
||||||
required: true,
|
required: type !== 'checkbox',
|
||||||
|
areas: [],
|
||||||
submitter_uuid: this.selectedSubmitter.uuid,
|
submitter_uuid: this.selectedSubmitter.uuid,
|
||||||
type
|
type
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,39 +9,51 @@
|
|||||||
loading="lazy"
|
loading="lazy"
|
||||||
>
|
>
|
||||||
<div
|
<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)"
|
@click="$emit('scroll-to', item)"
|
||||||
>
|
>
|
||||||
<div
|
<div class="flex justify-between w-full">
|
||||||
class="flex flex-col justify-between opacity-0 group-hover:opacity-100"
|
<div style="width: 26px" />
|
||||||
>
|
<div class="">
|
||||||
<div class="rounded-bl rounded-tr bg-white border">
|
<ReplaceButton
|
||||||
<button
|
:is-direct-upload="isDirectUpload"
|
||||||
class="rounded-bl rounded-tr hover:text-base-100 hover:bg-base-content w-full transition-colors"
|
:template-id="template.id"
|
||||||
style="width: 24px"
|
class="opacity-0 group-hover:opacity-100"
|
||||||
@click.stop="$emit('remove', item)"
|
@click.stop
|
||||||
>
|
@success="$emit('replace', { replaceSchemaItem: item, ...$event })"
|
||||||
×
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="withArrows"
|
class="flex flex-col justify-between opacity-0 group-hover:opacity-100"
|
||||||
class="flex flex-col border rounded-br rounded-tl bg-white divide-y"
|
|
||||||
>
|
>
|
||||||
<button
|
<div>
|
||||||
class="rounded-tl hover:text-base-100 hover:bg-base-content w-full transition-colors"
|
<button
|
||||||
style="width: 24px"
|
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"
|
||||||
@click.stop="$emit('up', item)"
|
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="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"
|
||||||
<button
|
style="width: 24px; height: 24px"
|
||||||
class="rounded-br hover:text-base-100 hover:bg-base-content w-full transition-colors"
|
@click.stop="$emit('up', item)"
|
||||||
style="width: 24px"
|
>
|
||||||
@click.stop="$emit('down', item)"
|
↑
|
||||||
>
|
</button>
|
||||||
↓
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,34 +72,52 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Contenteditable from './contenteditable'
|
import Contenteditable from './contenteditable'
|
||||||
|
import Upload from './upload'
|
||||||
|
import ReplaceButton from './replace'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DocumentPreview',
|
name: 'DocumentPreview',
|
||||||
components: {
|
components: {
|
||||||
Contenteditable
|
Contenteditable,
|
||||||
|
ReplaceButton
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
item: {
|
item: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
template: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
document: {
|
document: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
isDirectUpload: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
withArrows: {
|
withArrows: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['scroll-to', 'change', 'remove', 'up', 'down'],
|
emits: ['scroll-to', 'change', 'remove', 'up', 'down', 'replace'],
|
||||||
computed: {
|
computed: {
|
||||||
previewImage () {
|
previewImage () {
|
||||||
return this.document.preview_images[0]
|
return this.document.preview_images[0]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mounted () {
|
||||||
|
if (this.isDirectUpload) {
|
||||||
|
import('@rails/activestorage')
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
upload: Upload.methods.upload,
|
||||||
onUpdateName (value) {
|
onUpdateName (value) {
|
||||||
this.item.name = 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
|
||||||
@@ -3,12 +3,13 @@
|
|||||||
class SubmitterMailer < ApplicationMailer
|
class SubmitterMailer < ApplicationMailer
|
||||||
DEFAULT_MESSAGE = %(You have been invited to submit the "%<name>s" form:)
|
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: '')
|
||||||
@submitter = submitter
|
@submitter = submitter
|
||||||
@message = message
|
@message = message.presence || format(DEFAULT_MESSAGE, name: submitter.submission.template.name)
|
||||||
|
|
||||||
mail(to: @submitter.email,
|
mail(to: @submitter.email,
|
||||||
subject: 'You have been invited to submit a form')
|
subject: 'You have been invited to submit a form',
|
||||||
|
reply_to: submitter.submission.created_by_user&.friendly_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
def completed_email(submitter, user)
|
def completed_email(submitter, user)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -15,6 +15,8 @@ class Account < ApplicationRecord
|
|||||||
has_many :users, dependent: :destroy
|
has_many :users, dependent: :destroy
|
||||||
has_many :encrypted_configs, dependent: :destroy
|
has_many :encrypted_configs, dependent: :destroy
|
||||||
has_many :templates, dependent: :destroy
|
has_many :templates, dependent: :destroy
|
||||||
|
has_many :submissions, through: :templates
|
||||||
|
has_many :submitters, through: :submissions
|
||||||
has_many :active_users, -> { active }, dependent: :destroy,
|
has_many :active_users, -> { active }, dependent: :destroy,
|
||||||
inverse_of: :account, class_name: 'User'
|
inverse_of: :account, class_name: 'User'
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class EncryptedConfig < ApplicationRecord
|
|||||||
EMAIL_SMTP_KEY = 'action_mailer_smtp'
|
EMAIL_SMTP_KEY = 'action_mailer_smtp'
|
||||||
ESIGN_CERTS_KEY = 'esign_certs'
|
ESIGN_CERTS_KEY = 'esign_certs'
|
||||||
APP_URL_KEY = 'app_url'
|
APP_URL_KEY = 'app_url'
|
||||||
|
WEBHOOK_URL_KEY = 'webhook_url'
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,16 @@
|
|||||||
#
|
#
|
||||||
# Table name: submissions
|
# Table name: submissions
|
||||||
#
|
#
|
||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
# deleted_at :datetime
|
# deleted_at :datetime
|
||||||
# created_at :datetime not null
|
# source :text not null
|
||||||
# updated_at :datetime not null
|
# template_fields :text
|
||||||
# created_by_user_id :bigint
|
# template_schema :text
|
||||||
# template_id :bigint not null
|
# template_submitters :text
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# created_by_user_id :bigint
|
||||||
|
# template_id :bigint not null
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
@@ -23,9 +27,23 @@
|
|||||||
#
|
#
|
||||||
class Submission < ApplicationRecord
|
class Submission < ApplicationRecord
|
||||||
belongs_to :template
|
belongs_to :template
|
||||||
|
has_one :account, through: :template
|
||||||
belongs_to :created_by_user, class_name: 'User', optional: true
|
belongs_to :created_by_user, class_name: 'User', optional: true
|
||||||
|
|
||||||
has_many :submitters, dependent: :destroy
|
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) }
|
scope :active, -> { where(deleted_at: nil) }
|
||||||
|
|
||||||
|
enum :source, {
|
||||||
|
invite: 'invite',
|
||||||
|
api: 'api',
|
||||||
|
embed: 'embed',
|
||||||
|
link: 'link'
|
||||||
|
}, scope: false, prefix: true
|
||||||
end
|
end
|
||||||
@@ -30,6 +30,8 @@
|
|||||||
#
|
#
|
||||||
class Submitter < ApplicationRecord
|
class Submitter < ApplicationRecord
|
||||||
belongs_to :submission
|
belongs_to :submission
|
||||||
|
has_one :template, through: :submission
|
||||||
|
has_one :account, through: :template
|
||||||
|
|
||||||
attribute :values, :string, default: -> { {} }
|
attribute :values, :string, default: -> { {} }
|
||||||
attribute :slug, :string, default: -> { SecureRandom.base58(14) }
|
attribute :slug, :string, default: -> { SecureRandom.base58(14) }
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ class Template < ApplicationRecord
|
|||||||
|
|
||||||
has_many_attached :documents
|
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
|
has_many :submissions, dependent: :destroy
|
||||||
|
|
||||||
scope :active, -> { where(deleted_at: nil) }
|
scope :active, -> { where(deleted_at: nil) }
|
||||||
|
|||||||
+13
-2
@@ -22,6 +22,7 @@
|
|||||||
# role :string not null
|
# role :string not null
|
||||||
# sign_in_count :integer default(0), not null
|
# sign_in_count :integer default(0), not null
|
||||||
# unlock_token :string
|
# unlock_token :string
|
||||||
|
# uuid :text not null
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
# account_id :bigint not null
|
# account_id :bigint not null
|
||||||
@@ -32,6 +33,7 @@
|
|||||||
# index_users_on_email (email) UNIQUE
|
# index_users_on_email (email) UNIQUE
|
||||||
# index_users_on_reset_password_token (reset_password_token) UNIQUE
|
# index_users_on_reset_password_token (reset_password_token) UNIQUE
|
||||||
# index_users_on_unlock_token (unlock_token) UNIQUE
|
# index_users_on_unlock_token (unlock_token) UNIQUE
|
||||||
|
# index_users_on_uuid (uuid) UNIQUE
|
||||||
#
|
#
|
||||||
# Foreign Keys
|
# Foreign Keys
|
||||||
#
|
#
|
||||||
@@ -40,22 +42,31 @@
|
|||||||
class User < ApplicationRecord
|
class User < ApplicationRecord
|
||||||
ROLES = %w[admin].freeze
|
ROLES = %w[admin].freeze
|
||||||
|
|
||||||
EMAIL_REGEXP =
|
EMAIL_REGEXP = /[^@,\s]+@[^@,\s]+/
|
||||||
/[a-z0-9][.']?(?:(?:[a-z0-9_-]++[.'])*[a-z0-9_-]++)*@(?:[a-z0-9]++[.-])*[a-z0-9]++\.[a-z]{2,}/i
|
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
|
has_one :access_token, dependent: :destroy
|
||||||
|
|
||||||
devise :database_authenticatable, :recoverable, :rememberable, :validatable, :trackable
|
devise :database_authenticatable, :recoverable, :rememberable, :validatable, :trackable
|
||||||
devise :registerable, :omniauthable, omniauth_providers: [:google_oauth2] if Docuseal.multitenant?
|
devise :registerable, :omniauthable, omniauth_providers: [:google_oauth2] if Docuseal.multitenant?
|
||||||
|
|
||||||
attribute :role, :string, default: 'admin'
|
attribute :role, :string, default: 'admin'
|
||||||
|
attribute :uuid, :string, default: -> { SecureRandom.uuid }
|
||||||
|
|
||||||
scope :active, -> { where(deleted_at: nil) }
|
scope :active, -> { where(deleted_at: nil) }
|
||||||
|
|
||||||
|
def access_token
|
||||||
|
super || build_access_token.tap(&:save!)
|
||||||
|
end
|
||||||
|
|
||||||
def active_for_authentication?
|
def active_for_authentication?
|
||||||
!deleted_at?
|
!deleted_at?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def remember_me
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
def initials
|
def initials
|
||||||
[first_name.first, last_name.first].join.upcase
|
[first_name.first, last_name.first].join.upcase
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<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
|
||||||
|
</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@') %>" },
|
||||||
|
{ "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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -10,14 +10,6 @@
|
|||||||
<%= f.label :password, class: 'label' %>
|
<%= f.label :password, class: 'label' %>
|
||||||
<%= f.password_field :password, autocomplete: 'current-password', class: 'base-input' %>
|
<%= f.password_field :password, autocomplete: 'current-password', class: 'base-input' %>
|
||||||
</div>
|
</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>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.button button_title(title: 'Log In', disabled_with: 'Logging In'), class: 'base-button' %>
|
<%= f.button button_title(title: 'Log In', disabled_with: 'Logging In'), class: 'base-button' %>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
---
|
---
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Sent using <a href="<%= Docuseal::PRODUCT_URL %>"><%= Docuseal::PRODUCT_NAME %></a> documents software.
|
Sent using <a href="<%= Docuseal::PRODUCT_URL %>"><%= Docuseal::PRODUCT_NAME %></a> free document signing.
|
||||||
</p>
|
</p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
DocuSeal
|
DocuSeal
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</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.
|
A self-hosted and open-source web platform that provides secure and efficient digital document signing and processing.
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<h3 class="mb-4 text-2xl font-semibold">Easy to Start</h3>
|
<h3 class="mb-4 text-2xl font-semibold">Easy to Start</h3>
|
||||||
<p class="text-base text-gray-500">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,3 +70,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<%= render 'shared/attribution' %>
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class="disabled">
|
<span class="disabled">
|
||||||
<span class="flex items-center justify-center space-x-2">
|
<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><%= disabled_with %>...</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -2,13 +2,13 @@
|
|||||||
<label class="<%= local_assigns[:class] %>">
|
<label class="<%= local_assigns[:class] %>">
|
||||||
<input type="radio" class="peer hidden">
|
<input type="radio" class="peer hidden">
|
||||||
<span class="peer-checked:hidden flex items-center space-x-2">
|
<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">
|
<span class="hidden md:inline">
|
||||||
<%= local_assigns[:copy_title] || 'Copy' %>
|
<%= local_assigns[:copy_title] || 'Copy' %>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="hidden peer-checked:flex items-center space-x-2">
|
<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">
|
<span class="hidden md:inline">
|
||||||
<%= local_assigns[:copied_title] || 'Copied' %>
|
<%= local_assigns[:copied_title] || 'Copied' %>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -23,6 +23,22 @@
|
|||||||
<li>
|
<li>
|
||||||
<%= link_to 'Team', settings_users_path, class: 'text-base hover:bg-base-300' %>
|
<%= link_to 'Team', settings_users_path, class: 'text-base hover:bg-base-300' %>
|
||||||
</li>
|
</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 %>
|
||||||
|
<% unless Docuseal.demo? %>
|
||||||
|
<li>
|
||||||
|
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, class: 'text-base hover:bg-base-300' do %>
|
||||||
|
Console
|
||||||
|
<span class="badge badge-warning">New</span>
|
||||||
|
<% end %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
</menu-active>
|
</menu-active>
|
||||||
<div class="mx-4 border-t border-base-300 hidden md:block">
|
<div class="mx-4 border-t border-base-300 hidden md:block">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<turbo-frame id="modal">
|
<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="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] %>
|
<% if local_assigns[:title] %>
|
||||||
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
|
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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="flex items-center">
|
||||||
<div class="mr-3">
|
<div class="mr-3">
|
||||||
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
|
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="space-y-6 mx-auto">
|
<div class="space-y-6 mx-auto">
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="text-center w-full space-y-6">
|
<div class="text-center w-full space-y-6">
|
||||||
<a href="/" class="flex justify-center">
|
<a href="/" class="flex justify-center items-center">
|
||||||
<span class="mr-3">
|
<span class="mr-3">
|
||||||
<%= render 'shared/logo', width: '50px', height: '50px' %>
|
<%= render 'shared/logo', width: '50px', height: '50px' %>
|
||||||
</span>
|
</span>
|
||||||
@@ -10,14 +10,14 @@
|
|||||||
</a>
|
</a>
|
||||||
<p class="text-xl font-semibold text-center">You have been invited to submit a form</p>
|
<p class="text-xl font-semibold text-center">You have been invited to submit a form</p>
|
||||||
</div>
|
</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="flex items-center">
|
||||||
<div class="mr-3">
|
<div class="mr-3">
|
||||||
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
|
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-lg font-bold mb-1"><%= @template.name %></p>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex md:max-h-[calc(100vh-60px)]">
|
<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">
|
<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 } %>
|
<% 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">
|
<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">
|
<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>
|
||||||
<div class="w-full overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5">
|
<div class="w-full overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5">
|
||||||
<div class="pr-3.5 pl-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) } %>
|
<% 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) %>
|
<% 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 = @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| %>
|
<% 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">
|
<div id="<%= "page-#{document.uuid}-#{index}" %>" class="relative">
|
||||||
@@ -57,15 +57,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="hidden md:block relative w-80 flex-none pt-0.5 pr-4 pl-0.5 overflow-auto space">
|
<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] %>
|
<% 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'] } %>
|
<% submitter_fields_index = (@submission.template_fields || @submission.template.fields).group_by { |f| f['submitter_uuid'] } %>
|
||||||
<% @submission.template.submitters.each_with_index do |item, index| %>
|
<% (@submission.template_submitters || @submission.template.submitters).each_with_index do |item, index| %>
|
||||||
<% submitter = @submission.submitters.find { |e| e.uuid == item['uuid'] } %>
|
<% 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="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="border border-base-300 rounded-md px-2 py-1 mb-1">
|
||||||
<div class="flex items-center space-x-1">
|
<div class="flex items-center space-x-1">
|
||||||
<span class="mx-1 w-3 h-3 rounded-full <%= colors[index] %>"></span>
|
<span class="mx-1 w-3 h-3 rounded-full <%= colors[index] %>"></span>
|
||||||
<span class="text-lg">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-1 mt-1">
|
<div class="flex items-center space-x-1 mt-1">
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
|
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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="flex items-center">
|
||||||
<div class="mr-3">
|
<div class="mr-3">
|
||||||
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
|
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<% if Accounts.can_send_emails?(@submitter.submission.template.account) %>
|
<% 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>
|
<div class="divider">OR</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<download-button data-src="<%= submitter_download_index_path(@submitter.slug) %>" class="base-button w-full">
|
<download-button data-src="<%= submitter_download_index_path(@submitter.slug) %>" class="base-button w-full">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<% 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) } %>
|
<% 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) %>
|
<% attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %>
|
||||||
<div style="max-height: -webkit-fill-available;">
|
<div style="max-height: -webkit-fill-available;">
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<span>DocuSeal</span>
|
<span>DocuSeal</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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 = @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| %>
|
<% document.preview_images.sort_by { |a| a.filename.base.to_i }.each_with_index do |page, index| %>
|
||||||
<div class="relative my-4 shadow-md">
|
<div class="relative my-4 shadow-md">
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
<div class="fixed bottom-0 w-full h-0 z-20">
|
<div class="fixed bottom-0 w-full h-0 z-20">
|
||||||
<div class="mx-auto" style="max-width: 1000px">
|
<div class="mx-auto" style="max-width: 1000px">
|
||||||
<div class="relative md:mx-32">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -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">
|
<div class="space-y-4">
|
||||||
<% @submissions.each do |submission| %>
|
<% @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">
|
<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?) %>
|
<% is_submission_complated = submitters.all?(&:completed_at?) %>
|
||||||
<% if submitters.size == 1 %>
|
<% if submitters.size == 1 %>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -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 'action_mailer/railtie'
|
||||||
require 'active_job/railtie'
|
require 'active_job/railtie'
|
||||||
|
|
||||||
|
require_relative '../lib/api_path_consider_json_middleware'
|
||||||
|
|
||||||
Bundler.require(*Rails.groups)
|
Bundler.require(*Rails.groups)
|
||||||
|
|
||||||
module DocuSeal
|
module DocuSeal
|
||||||
@@ -26,5 +28,6 @@ module DocuSeal
|
|||||||
config.action_view.frozen_string_literal = true
|
config.action_view.frozen_string_literal = true
|
||||||
|
|
||||||
config.middleware.insert_before ActionDispatch::Static, Rack::Deflater
|
config.middleware.insert_before ActionDispatch::Static, Rack::Deflater
|
||||||
|
config.middleware.insert_before ActionDispatch::Static, ApiPathConsiderJsonMiddleware
|
||||||
end
|
end
|
||||||
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.
|
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
|
||||||
config.action_mailer.raise_delivery_errors = false
|
config.action_mailer.raise_delivery_errors = false
|
||||||
|
|
||||||
if ENV['SMTP_USERNAME']
|
if ENV['SMTP_ADDRESS']
|
||||||
config.action_mailer.delivery_method = :smtp
|
config.action_mailer.delivery_method = :smtp
|
||||||
config.action_mailer.smtp_settings = {
|
config.action_mailer.smtp_settings = {
|
||||||
address: ENV.fetch('SMTP_ADDRESS', nil),
|
address: ENV.fetch('SMTP_ADDRESS', nil),
|
||||||
@@ -86,7 +86,7 @@ Rails.application.configure do
|
|||||||
password: ENV.fetch('SMTP_PASSWORD', nil),
|
password: ENV.fetch('SMTP_PASSWORD', nil),
|
||||||
authentication: ENV.fetch('SMTP_AUTHENTICATION', 'plain'),
|
authentication: ENV.fetch('SMTP_AUTHENTICATION', 'plain'),
|
||||||
enable_starttls_auto: ENV['SMTP_ENABLE_STARTTLS_AUTO'] == 'true'
|
enable_starttls_auto: ENV['SMTP_ENABLE_STARTTLS_AUTO'] == 'true'
|
||||||
}
|
}.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
|
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
# frozen_string_literal: true
|
# 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
|
# 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
|
# 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
|
# are not: uncommented lines are intended to protect your configuration from
|
||||||
@@ -159,7 +163,7 @@ Devise.setup do |config|
|
|||||||
|
|
||||||
# ==> Configuration for :rememberable
|
# ==> Configuration for :rememberable
|
||||||
# The time the user will be remembered without asking for credentials again.
|
# 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.
|
# Invalidates all the remember me tokens when the user signs out.
|
||||||
config.expire_all_remember_me_on_sign_out = true
|
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
|
# 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.
|
# change the failure app, you can configure them inside the config.warden block.
|
||||||
#
|
#
|
||||||
# config.warden do |manager|
|
config.warden do |manager|
|
||||||
# manager.intercept_401 = false
|
# manager.intercept_401 = false
|
||||||
# manager.default_strategies(scope: :user).unshift :some_external_strategy
|
manager.default_strategies(scope: :user).unshift(:auth_token)
|
||||||
# end
|
end
|
||||||
|
|
||||||
# ==> Mountable engine configurations
|
# ==> Mountable engine configurations
|
||||||
# When using Devise inside an engine, let's call it `MyEngine`, and this engine
|
# When using Devise inside an engine, let's call it `MyEngine`, and this engine
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
# frozen_string_literal: true
|
# 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
|
||||||
+9
-2
@@ -27,9 +27,11 @@ Rails.application.routes.draw do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
namespace :api do
|
namespace :api, defaults: { format: :json } do
|
||||||
resources :attachments, only: %i[create]
|
resources :attachments, only: %i[create]
|
||||||
resources :templates, only: %i[update] do
|
resources :submissions, only: %i[create]
|
||||||
|
resources :templates, only: %i[update index] do
|
||||||
|
resources :submissions, only: %i[create]
|
||||||
resources :documents, only: %i[create destroy], controller: 'templates_documents'
|
resources :documents, only: %i[create destroy], controller: 'templates_documents'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -39,6 +41,7 @@ Rails.application.routes.draw do
|
|||||||
resource :newsletter, only: %i[show update]
|
resource :newsletter, only: %i[show update]
|
||||||
resources :users, only: %i[new create edit update destroy]
|
resources :users, only: %i[new create edit update destroy]
|
||||||
resources :submissions, only: %i[show destroy]
|
resources :submissions, only: %i[show destroy]
|
||||||
|
resources :console_redirect, only: %i[index]
|
||||||
resources :templates, only: %i[new create edit show destroy] do
|
resources :templates, only: %i[new create edit show destroy] do
|
||||||
resources :submissions, only: %i[new create]
|
resources :submissions, only: %i[new create]
|
||||||
end
|
end
|
||||||
@@ -67,6 +70,10 @@ Rails.application.routes.draw do
|
|||||||
end
|
end
|
||||||
resources :esign, only: %i[index create], controller: 'esign_settings'
|
resources :esign, only: %i[index create], controller: 'esign_settings'
|
||||||
resources :users, only: %i[index]
|
resources :users, only: %i[index]
|
||||||
|
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]
|
resource :account, only: %i[show update]
|
||||||
resources :profile, only: %i[index] do
|
resources :profile, only: %i[index] do
|
||||||
collection do
|
collection do
|
||||||
|
|||||||
@@ -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
|
||||||
+18
-1
@@ -10,10 +10,20 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# 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_06_140534) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
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 "accounts", force: :cascade do |t|
|
create_table "accounts", force: :cascade do |t|
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
t.string "timezone", null: false
|
t.string "timezone", null: false
|
||||||
@@ -77,6 +87,10 @@ ActiveRecord::Schema[7.0].define(version: 2023_07_01_075115) do
|
|||||||
t.datetime "deleted_at"
|
t.datetime "deleted_at"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_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 ["created_by_user_id"], name: "index_submissions_on_created_by_user_id"
|
||||||
t.index ["template_id"], name: "index_submissions_on_template_id"
|
t.index ["template_id"], name: "index_submissions_on_template_id"
|
||||||
end
|
end
|
||||||
@@ -136,12 +150,15 @@ ActiveRecord::Schema[7.0].define(version: 2023_07_01_075115) do
|
|||||||
t.datetime "deleted_at"
|
t.datetime "deleted_at"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_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 ["account_id"], name: "index_users_on_account_id"
|
||||||
t.index ["email"], name: "index_users_on_email", unique: true
|
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 ["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 ["unlock_token"], name: "index_users_on_unlock_token", unique: true
|
||||||
|
t.index ["uuid"], name: "index_users_on_uuid", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
add_foreign_key "access_tokens", "users"
|
||||||
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
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 "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
|
||||||
add_foreign_key "document_generation_events", "submitters"
|
add_foreign_key "document_generation_events", "submitters"
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ module ActionMailerConfigsInterceptor
|
|||||||
port: email_configs.value['port'],
|
port: email_configs.value['port'],
|
||||||
tls: email_configs.value['port'].to_s == '465')
|
tls: email_configs.value['port'].to_s == '465')
|
||||||
|
|
||||||
message.from = email_configs.value['from_email']
|
message.from = "#{email_configs.account.name} <#{email_configs.value['from_email']}>"
|
||||||
else
|
else
|
||||||
message.delivery_method(:test)
|
message.delivery_method(:test)
|
||||||
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
|
||||||
@@ -10,6 +10,11 @@ module Docuseal
|
|||||||
TWITTER_URL = 'https://twitter.com/docusealco'
|
TWITTER_URL = 'https://twitter.com/docusealco'
|
||||||
TWITTER_HANDLE = '@docusealco'
|
TWITTER_HANDLE = '@docusealco'
|
||||||
SUPPORT_EMAIL = 'support@docuseal.co'
|
SUPPORT_EMAIL = 'support@docuseal.co'
|
||||||
|
CONSOLE_URL = if Rails.env.development?
|
||||||
|
'http://console.localhost.io:3001'
|
||||||
|
else
|
||||||
|
'https://console.docuseal.co'
|
||||||
|
end
|
||||||
|
|
||||||
CERTS = JSON.parse(ENV.fetch('CERTS', '{}'))
|
CERTS = JSON.parse(ENV.fetch('CERTS', '{}'))
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module JsonWebToken
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def encode(payload)
|
||||||
|
JWT.encode(payload, Rails.application.secrets.secret_key_base)
|
||||||
|
end
|
||||||
|
|
||||||
|
def decode(token)
|
||||||
|
JWT.decode(token, Rails.application.secrets.secret_key_base)[0]
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,4 +1,47 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Submissions
|
module Submissions
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def update_template_fields!(submission)
|
||||||
|
submission.template_fields = submission.template.fields
|
||||||
|
submission.template_schema = submission.template.schema
|
||||||
|
submission.template_submitters = submission.template.submitters
|
||||||
|
|
||||||
|
submission.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_from_emails(template:, user:, emails:, source:, send_email: false)
|
||||||
|
emails = emails.to_s.scan(User::EMAIL_REGEXP)
|
||||||
|
|
||||||
|
emails.map do |email|
|
||||||
|
submission = template.submissions.new(created_by_user: user, source:)
|
||||||
|
submission.submitters.new(email:,
|
||||||
|
uuid: template.submitters.first['uuid'],
|
||||||
|
sent_at: send_email ? Time.current : nil)
|
||||||
|
|
||||||
|
submission.tap(&:save!)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_from_submitters(template:, user:, submissions_attrs:, source:, send_email: false)
|
||||||
|
submissions_attrs.map do |attrs|
|
||||||
|
submission = template.submissions.new(created_by_user: user, source:)
|
||||||
|
|
||||||
|
attrs[:submitters].each_with_index do |submitter_attrs, index|
|
||||||
|
uuid =
|
||||||
|
submitter_attrs[:uuid].presence ||
|
||||||
|
template.submitters.find { |e| e['name'] == submitter_attrs[:name] }&.dig('uuid') ||
|
||||||
|
template.submitters[index]&.dig('uuid')
|
||||||
|
|
||||||
|
next if uuid.blank?
|
||||||
|
|
||||||
|
submission.submitters.new(email: submitter_attrs[:email],
|
||||||
|
sent_at: send_email ? Time.current : nil,
|
||||||
|
uuid:)
|
||||||
|
end
|
||||||
|
|
||||||
|
submission.tap(&:save!)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -27,7 +27,7 @@ module Submissions
|
|||||||
|
|
||||||
pdfs_index = build_pdfs_index(submitter)
|
pdfs_index = build_pdfs_index(submitter)
|
||||||
|
|
||||||
template.fields.each do |field|
|
submitter.submission.template_fields.each do |field|
|
||||||
next if field['submitter_uuid'] != submitter.uuid
|
next if field['submitter_uuid'] != submitter.uuid
|
||||||
|
|
||||||
field.fetch('areas', []).each do |area|
|
field.fetch('areas', []).each do |area|
|
||||||
@@ -159,7 +159,7 @@ module Submissions
|
|||||||
original_documents = template.documents.preload(:blob)
|
original_documents = template.documents.preload(:blob)
|
||||||
|
|
||||||
results =
|
results =
|
||||||
template.schema.map do |item|
|
submitter.submission.template_schema.map do |item|
|
||||||
pdf = pdfs_index[item['attachment_uuid']]
|
pdf = pdfs_index[item['attachment_uuid']]
|
||||||
|
|
||||||
attachment = save_signed_pdf(pdf:, submitter:, certs:, uuid: item['attachment_uuid'], name: item['name'])
|
attachment = save_signed_pdf(pdf:, submitter:, certs:, uuid: item['attachment_uuid'], name: item['name'])
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Submitters
|
||||||
|
module SerializeForWebhook
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def call(submitter)
|
||||||
|
values = build_values_array(submitter)
|
||||||
|
documents = build_documents_array(submitter)
|
||||||
|
|
||||||
|
submitter_name = submitter.submission.template_submitters.find { |e| e['uuid'] == submitter.uuid }['name']
|
||||||
|
|
||||||
|
submitter.as_json(include: [template: { only: %i[id name created_at updated_at] }])
|
||||||
|
.except('uuid', 'values', 'slug')
|
||||||
|
.merge('values' => values,
|
||||||
|
'documents' => documents,
|
||||||
|
'submitter_name' => submitter_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_values_array(submitter)
|
||||||
|
fields_index = submitter.submission.template_fields.index_by { |e| e['uuid'] }
|
||||||
|
attachments_index = submitter.attachments.preload(:blob).index_by(&:uuid)
|
||||||
|
submitter_field_counters = Hash.new { 0 }
|
||||||
|
|
||||||
|
submitter.values.map do |uuid, value|
|
||||||
|
field = fields_index[uuid]
|
||||||
|
submitter_field_counters[field['type']] += 1
|
||||||
|
|
||||||
|
field_name =
|
||||||
|
field['name'].presence || "#{field['type'].titleize} Field #{submitter_field_counters[field['type']]}"
|
||||||
|
|
||||||
|
value = fetch_field_value(field, value, attachments_index)
|
||||||
|
|
||||||
|
{ field: field_name, value: }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_documents_array(submitter)
|
||||||
|
submitter.documents.preload(:blob).map do |attachment|
|
||||||
|
{ name: attachment.filename.base, url: attachment.url }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_field_value(field, value, attachments_index)
|
||||||
|
if field['type'].in?(%w[image signature])
|
||||||
|
attachments_index[value]&.url
|
||||||
|
elsif field['type'] == 'file'
|
||||||
|
Array.wrap(value).compact_blank.filter_map { |e| attachments_index[e]&.url }
|
||||||
|
else
|
||||||
|
value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
+2
-2
@@ -3,10 +3,10 @@
|
|||||||
module Templates
|
module Templates
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def build_field_areas_index(template)
|
def build_field_areas_index(fields)
|
||||||
hash = {}
|
hash = {}
|
||||||
|
|
||||||
template.fields.each do |field|
|
fields.each do |field|
|
||||||
(field['areas'] || []).each do |area|
|
(field['areas'] || []).each do |area|
|
||||||
hash[area['attachment_uuid']] ||= {}
|
hash[area['attachment_uuid']] ||= {}
|
||||||
acc = (hash[area['attachment_uuid']][area['page']] ||= [])
|
acc = (hash[area['attachment_uuid']][area['page']] ||= [])
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@
|
|||||||
"compression-webpack-plugin": "10.0.0",
|
"compression-webpack-plugin": "10.0.0",
|
||||||
"css-loader": "^6.7.3",
|
"css-loader": "^6.7.3",
|
||||||
"css-minimizer-webpack-plugin": "^5.0.0",
|
"css-minimizer-webpack-plugin": "^5.0.0",
|
||||||
"daisyui": "^3.1.5",
|
"daisyui": "^3.2.1",
|
||||||
"mini-css-extract-plugin": "^2.7.5",
|
"mini-css-extract-plugin": "^2.7.5",
|
||||||
"postcss": "^8.4.23",
|
"postcss": "^8.4.23",
|
||||||
"postcss-import": "^15.1.0",
|
"postcss-import": "^15.1.0",
|
||||||
|
|||||||
@@ -2300,13 +2300,14 @@ csstype@^3.1.1:
|
|||||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
|
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
|
||||||
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
|
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
|
||||||
|
|
||||||
daisyui@^3.1.5:
|
daisyui@^3.2.1:
|
||||||
version "3.1.5"
|
version "3.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/daisyui/-/daisyui-3.1.5.tgz#49cb961d03504beebd238d58f0169dbe81103c95"
|
resolved "https://registry.yarnpkg.com/daisyui/-/daisyui-3.2.1.tgz#7fac542b03023adbde2afc869518840dbef509ee"
|
||||||
integrity sha512-3nk5L//IU31ISzsonKEFG8ris8hS/MRAc+PrOTOyRbssjv1fYmNKSdIrOhG6op2ED3RyJL4p6I81JUwd7RWqTw==
|
integrity sha512-gIqE6wiqoJt9G8+n3R/SwLeUnpNCE2eDhT73rP0yZYVaM7o6zVcakBH3aEW5RGpx3UkonPiLuvcgxRcb2lE8TA==
|
||||||
dependencies:
|
dependencies:
|
||||||
colord "^2.9"
|
colord "^2.9"
|
||||||
css-selector-tokenizer "^0.8"
|
css-selector-tokenizer "^0.8"
|
||||||
|
postcss "^8"
|
||||||
postcss-js "^4"
|
postcss-js "^4"
|
||||||
tailwindcss "^3"
|
tailwindcss "^3"
|
||||||
|
|
||||||
@@ -4448,6 +4449,15 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^
|
|||||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
|
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
|
||||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||||
|
|
||||||
|
postcss@^8:
|
||||||
|
version "8.4.26"
|
||||||
|
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.26.tgz#1bc62ab19f8e1e5463d98cf74af39702a00a9e94"
|
||||||
|
integrity sha512-jrXHFF8iTloAenySjM/ob3gSj7pCu0Ji49hnjqzsgSRa50hkWCKD0HQ+gMNJkW38jBI68MpAAg7ZWwHwX8NMMw==
|
||||||
|
dependencies:
|
||||||
|
nanoid "^3.3.6"
|
||||||
|
picocolors "^1.0.0"
|
||||||
|
source-map-js "^1.0.2"
|
||||||
|
|
||||||
postcss@^8.1.10, postcss@^8.4.21, postcss@^8.4.23:
|
postcss@^8.1.10, postcss@^8.4.21, postcss@^8.4.23:
|
||||||
version "8.4.23"
|
version "8.4.23"
|
||||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.23.tgz#df0aee9ac7c5e53e1075c24a3613496f9e6552ab"
|
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.23.tgz#df0aee9ac7c5e53e1075c24a3613496f9e6552ab"
|
||||||
|
|||||||
Reference in new issue
Block a user